1use super::{ItemHandle, SplitDirection};
2use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
3use collections::{HashMap, VecDeque};
4use gpui::{
5 action,
6 elements::*,
7 geometry::{rect::RectF, vector::vec2f},
8 keymap::Binding,
9 platform::{CursorStyle, NavigationDirection},
10 AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
11 ViewHandle, WeakViewHandle,
12};
13use project::{ProjectEntryId, ProjectPath};
14use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
15use util::ResultExt;
16
17action!(Split, SplitDirection);
18action!(ActivateItem, usize);
19action!(ActivatePrevItem);
20action!(ActivateNextItem);
21action!(CloseActiveItem);
22action!(CloseInactiveItems);
23action!(CloseItem, usize);
24action!(GoBack, Option<WeakViewHandle<Pane>>);
25action!(GoForward, Option<WeakViewHandle<Pane>>);
26
27const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
28
29pub fn init(cx: &mut MutableAppContext) {
30 cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
31 pane.activate_item(action.0, true, cx);
32 });
33 cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
34 pane.activate_prev_item(cx);
35 });
36 cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
37 pane.activate_next_item(cx);
38 });
39 cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
40 pane.close_active_item(cx);
41 });
42 cx.add_action(|pane: &mut Pane, _: &CloseInactiveItems, cx| {
43 pane.close_inactive_items(cx);
44 });
45 cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
46 pane.close_item(action.0, cx);
47 });
48 cx.add_action(|pane: &mut Pane, action: &Split, cx| {
49 pane.split(action.0, cx);
50 });
51 cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| {
52 Pane::go_back(
53 workspace,
54 action
55 .0
56 .as_ref()
57 .and_then(|weak_handle| weak_handle.upgrade(cx)),
58 cx,
59 )
60 .detach();
61 });
62 cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| {
63 Pane::go_forward(
64 workspace,
65 action
66 .0
67 .as_ref()
68 .and_then(|weak_handle| weak_handle.upgrade(cx)),
69 cx,
70 )
71 .detach();
72 });
73
74 cx.add_bindings(vec![
75 Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
76 Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
77 Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
78 Binding::new("alt-cmd-w", CloseInactiveItems, Some("Pane")),
79 Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
80 Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
81 Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
82 Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
83 Binding::new("ctrl--", GoBack(None), Some("Pane")),
84 Binding::new("shift-ctrl-_", GoForward(None), Some("Pane")),
85 ]);
86}
87
88pub enum Event {
89 Activate,
90 ActivateItem { local: bool },
91 Remove,
92 Split(SplitDirection),
93}
94
95pub struct Pane {
96 items: Vec<Box<dyn ItemHandle>>,
97 active_item_index: usize,
98 nav_history: Rc<RefCell<NavHistory>>,
99 toolbar: ViewHandle<Toolbar>,
100}
101
102pub struct ItemNavHistory {
103 history: Rc<RefCell<NavHistory>>,
104 item: Rc<dyn WeakItemHandle>,
105}
106
107#[derive(Default)]
108pub struct NavHistory {
109 mode: NavigationMode,
110 backward_stack: VecDeque<NavigationEntry>,
111 forward_stack: VecDeque<NavigationEntry>,
112 paths_by_item: HashMap<usize, ProjectPath>,
113}
114
115#[derive(Copy, Clone)]
116enum NavigationMode {
117 Normal,
118 GoingBack,
119 GoingForward,
120 Disabled,
121}
122
123impl Default for NavigationMode {
124 fn default() -> Self {
125 Self::Normal
126 }
127}
128
129pub struct NavigationEntry {
130 pub item: Rc<dyn WeakItemHandle>,
131 pub data: Option<Box<dyn Any>>,
132}
133
134impl Pane {
135 pub fn new(cx: &mut ViewContext<Self>) -> Self {
136 Self {
137 items: Vec::new(),
138 active_item_index: 0,
139 nav_history: Default::default(),
140 toolbar: cx.add_view(|_| Toolbar::new()),
141 }
142 }
143
144 pub fn nav_history(&self) -> &Rc<RefCell<NavHistory>> {
145 &self.nav_history
146 }
147
148 pub fn activate(&self, cx: &mut ViewContext<Self>) {
149 cx.emit(Event::Activate);
150 }
151
152 pub fn go_back(
153 workspace: &mut Workspace,
154 pane: Option<ViewHandle<Pane>>,
155 cx: &mut ViewContext<Workspace>,
156 ) -> Task<()> {
157 Self::navigate_history(
158 workspace,
159 pane.unwrap_or_else(|| workspace.active_pane().clone()),
160 NavigationMode::GoingBack,
161 cx,
162 )
163 }
164
165 pub fn go_forward(
166 workspace: &mut Workspace,
167 pane: Option<ViewHandle<Pane>>,
168 cx: &mut ViewContext<Workspace>,
169 ) -> Task<()> {
170 Self::navigate_history(
171 workspace,
172 pane.unwrap_or_else(|| workspace.active_pane().clone()),
173 NavigationMode::GoingForward,
174 cx,
175 )
176 }
177
178 fn navigate_history(
179 workspace: &mut Workspace,
180 pane: ViewHandle<Pane>,
181 mode: NavigationMode,
182 cx: &mut ViewContext<Workspace>,
183 ) -> Task<()> {
184 workspace.activate_pane(pane.clone(), cx);
185
186 let to_load = pane.update(cx, |pane, cx| {
187 loop {
188 // Retrieve the weak item handle from the history.
189 let entry = pane.nav_history.borrow_mut().pop(mode)?;
190
191 // If the item is still present in this pane, then activate it.
192 if let Some(index) = entry
193 .item
194 .upgrade(cx)
195 .and_then(|v| pane.index_for_item(v.as_ref()))
196 {
197 if let Some(item) = pane.active_item() {
198 pane.nav_history.borrow_mut().set_mode(mode);
199 item.deactivated(cx);
200 pane.nav_history
201 .borrow_mut()
202 .set_mode(NavigationMode::Normal);
203 }
204
205 let prev_active_index = mem::replace(&mut pane.active_item_index, index);
206 pane.focus_active_item(cx);
207 let mut navigated = prev_active_index != pane.active_item_index;
208 if let Some(data) = entry.data {
209 navigated |= pane.active_item()?.navigate(data, cx);
210 }
211
212 if navigated {
213 cx.notify();
214 break None;
215 }
216 }
217 // If the item is no longer present in this pane, then retrieve its
218 // project path in order to reopen it.
219 else {
220 break pane
221 .nav_history
222 .borrow_mut()
223 .paths_by_item
224 .get(&entry.item.id())
225 .cloned()
226 .map(|project_path| (project_path, entry));
227 }
228 }
229 });
230
231 if let Some((project_path, entry)) = to_load {
232 // If the item was no longer present, then load it again from its previous path.
233 let pane = pane.downgrade();
234 let task = workspace.load_path(project_path, cx);
235 cx.spawn(|workspace, mut cx| async move {
236 let task = task.await;
237 if let Some(pane) = pane.upgrade(&cx) {
238 if let Some((project_entry_id, build_item)) = task.log_err() {
239 pane.update(&mut cx, |pane, _| {
240 pane.nav_history.borrow_mut().set_mode(mode);
241 });
242 let item = workspace.update(&mut cx, |workspace, cx| {
243 Self::open_item(
244 workspace,
245 pane.clone(),
246 project_entry_id,
247 cx,
248 build_item,
249 )
250 });
251 pane.update(&mut cx, |pane, cx| {
252 pane.nav_history
253 .borrow_mut()
254 .set_mode(NavigationMode::Normal);
255 if let Some(data) = entry.data {
256 item.navigate(data, cx);
257 }
258 });
259 } else {
260 workspace
261 .update(&mut cx, |workspace, cx| {
262 Self::navigate_history(workspace, pane, mode, cx)
263 })
264 .await;
265 }
266 }
267 })
268 } else {
269 Task::ready(())
270 }
271 }
272
273 pub(crate) fn open_item(
274 workspace: &mut Workspace,
275 pane: ViewHandle<Pane>,
276 project_entry_id: ProjectEntryId,
277 cx: &mut ViewContext<Workspace>,
278 build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
279 ) -> Box<dyn ItemHandle> {
280 let existing_item = pane.update(cx, |pane, cx| {
281 for (ix, item) in pane.items.iter().enumerate() {
282 if item.project_entry_id(cx) == Some(project_entry_id) {
283 let item = item.boxed_clone();
284 pane.activate_item(ix, true, cx);
285 return Some(item);
286 }
287 }
288 None
289 });
290 if let Some(existing_item) = existing_item {
291 existing_item
292 } else {
293 let item = build_item(cx);
294 Self::add_item(workspace, pane, item.boxed_clone(), true, cx);
295 item
296 }
297 }
298
299 pub(crate) fn add_item(
300 workspace: &mut Workspace,
301 pane: ViewHandle<Pane>,
302 item: Box<dyn ItemHandle>,
303 local: bool,
304 cx: &mut ViewContext<Workspace>,
305 ) {
306 // Prevent adding the same item to the pane more than once.
307 if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
308 pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx));
309 return;
310 }
311
312 item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
313 item.added_to_pane(workspace, pane.clone(), cx);
314 pane.update(cx, |pane, cx| {
315 let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
316 pane.items.insert(item_idx, item);
317 pane.activate_item(item_idx, local, cx);
318 cx.notify();
319 });
320 }
321
322 pub fn items(&self) -> impl Iterator<Item = &Box<dyn ItemHandle>> {
323 self.items.iter()
324 }
325
326 pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator<Item = ViewHandle<T>> {
327 self.items
328 .iter()
329 .filter_map(|item| item.to_any().downcast())
330 }
331
332 pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
333 self.items.get(self.active_item_index).cloned()
334 }
335
336 pub fn project_entry_id_for_item(
337 &self,
338 item: &dyn ItemHandle,
339 cx: &AppContext,
340 ) -> Option<ProjectEntryId> {
341 self.items.iter().find_map(|existing| {
342 if existing.id() == item.id() {
343 existing.project_entry_id(cx)
344 } else {
345 None
346 }
347 })
348 }
349
350 pub fn item_for_entry(
351 &self,
352 entry_id: ProjectEntryId,
353 cx: &AppContext,
354 ) -> Option<Box<dyn ItemHandle>> {
355 self.items.iter().find_map(|item| {
356 if item.project_entry_id(cx) == Some(entry_id) {
357 Some(item.boxed_clone())
358 } else {
359 None
360 }
361 })
362 }
363
364 pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
365 self.items.iter().position(|i| i.id() == item.id())
366 }
367
368 pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
369 if index < self.items.len() {
370 let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
371 if prev_active_item_ix != self.active_item_index
372 && prev_active_item_ix < self.items.len()
373 {
374 self.items[prev_active_item_ix].deactivated(cx);
375 cx.emit(Event::ActivateItem { local });
376 }
377 self.update_toolbar(cx);
378 if local {
379 self.focus_active_item(cx);
380 self.activate(cx);
381 }
382 cx.notify();
383 }
384 }
385
386 pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
387 let mut index = self.active_item_index;
388 if index > 0 {
389 index -= 1;
390 } else if self.items.len() > 0 {
391 index = self.items.len() - 1;
392 }
393 self.activate_item(index, true, cx);
394 }
395
396 pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
397 let mut index = self.active_item_index;
398 if index + 1 < self.items.len() {
399 index += 1;
400 } else {
401 index = 0;
402 }
403 self.activate_item(index, true, cx);
404 }
405
406 pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
407 if !self.items.is_empty() {
408 self.close_item(self.items[self.active_item_index].id(), cx)
409 }
410 }
411
412 pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
413 if !self.items.is_empty() {
414 let active_item_id = self.items[self.active_item_index].id();
415 self.close_items(cx, |id| id != active_item_id);
416 }
417 }
418
419 pub fn close_item(&mut self, view_id_to_close: usize, cx: &mut ViewContext<Self>) {
420 self.close_items(cx, |view_id| view_id == view_id_to_close);
421 }
422
423 pub fn close_items(
424 &mut self,
425 cx: &mut ViewContext<Self>,
426 should_close: impl Fn(usize) -> bool,
427 ) {
428 let mut item_ix = 0;
429 let mut new_active_item_index = self.active_item_index;
430 self.items.retain(|item| {
431 if should_close(item.id()) {
432 if item_ix == self.active_item_index {
433 item.deactivated(cx);
434 }
435
436 if item_ix < self.active_item_index {
437 new_active_item_index -= 1;
438 }
439
440 let mut nav_history = self.nav_history.borrow_mut();
441 if let Some(path) = item.project_path(cx) {
442 nav_history.paths_by_item.insert(item.id(), path);
443 } else {
444 nav_history.paths_by_item.remove(&item.id());
445 }
446
447 item_ix += 1;
448 false
449 } else {
450 item_ix += 1;
451 true
452 }
453 });
454
455 if self.items.is_empty() {
456 cx.emit(Event::Remove);
457 } else {
458 self.active_item_index = cmp::min(new_active_item_index, self.items.len() - 1);
459 self.focus_active_item(cx);
460 self.activate(cx);
461 }
462 self.update_toolbar(cx);
463
464 cx.notify();
465 }
466
467 pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
468 if let Some(active_item) = self.active_item() {
469 cx.focus(active_item);
470 }
471 }
472
473 pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
474 cx.emit(Event::Split(direction));
475 }
476
477 pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
478 &self.toolbar
479 }
480
481 fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
482 let active_item = self
483 .items
484 .get(self.active_item_index)
485 .map(|item| item.as_ref());
486 self.toolbar.update(cx, |toolbar, cx| {
487 toolbar.set_active_pane_item(active_item, cx);
488 });
489 }
490
491 fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
492 let theme = cx.global::<Settings>().theme.clone();
493
494 enum Tabs {}
495 let tabs = MouseEventHandler::new::<Tabs, _, _>(0, cx, |mouse_state, cx| {
496 let mut row = Flex::row();
497 for (ix, item) in self.items.iter().enumerate() {
498 let is_active = ix == self.active_item_index;
499
500 row.add_child({
501 let tab_style = if is_active {
502 theme.workspace.active_tab.clone()
503 } else {
504 theme.workspace.tab.clone()
505 };
506 let title = item.tab_content(&tab_style, cx);
507
508 let mut style = if is_active {
509 theme.workspace.active_tab.clone()
510 } else {
511 theme.workspace.tab.clone()
512 };
513 if ix == 0 {
514 style.container.border.left = false;
515 }
516
517 EventHandler::new(
518 Container::new(
519 Flex::row()
520 .with_child(
521 Align::new({
522 let diameter = 7.0;
523 let icon_color = if item.has_conflict(cx) {
524 Some(style.icon_conflict)
525 } else if item.is_dirty(cx) {
526 Some(style.icon_dirty)
527 } else {
528 None
529 };
530
531 ConstrainedBox::new(
532 Canvas::new(move |bounds, _, cx| {
533 if let Some(color) = icon_color {
534 let square = RectF::new(
535 bounds.origin(),
536 vec2f(diameter, diameter),
537 );
538 cx.scene.push_quad(Quad {
539 bounds: square,
540 background: Some(color),
541 border: Default::default(),
542 corner_radius: diameter / 2.,
543 });
544 }
545 })
546 .boxed(),
547 )
548 .with_width(diameter)
549 .with_height(diameter)
550 .boxed()
551 })
552 .boxed(),
553 )
554 .with_child(
555 Container::new(Align::new(title).boxed())
556 .with_style(ContainerStyle {
557 margin: Margin {
558 left: style.spacing,
559 right: style.spacing,
560 ..Default::default()
561 },
562 ..Default::default()
563 })
564 .boxed(),
565 )
566 .with_child(
567 Align::new(
568 ConstrainedBox::new(if mouse_state.hovered {
569 let item_id = item.id();
570 enum TabCloseButton {}
571 let icon = Svg::new("icons/x.svg");
572 MouseEventHandler::new::<TabCloseButton, _, _>(
573 item_id,
574 cx,
575 |mouse_state, _| {
576 if mouse_state.hovered {
577 icon.with_color(style.icon_close_active)
578 .boxed()
579 } else {
580 icon.with_color(style.icon_close).boxed()
581 }
582 },
583 )
584 .with_padding(Padding::uniform(4.))
585 .with_cursor_style(CursorStyle::PointingHand)
586 .on_click(move |cx| {
587 cx.dispatch_action(CloseItem(item_id))
588 })
589 .named("close-tab-icon")
590 } else {
591 Empty::new().boxed()
592 })
593 .with_width(style.icon_width)
594 .boxed(),
595 )
596 .boxed(),
597 )
598 .boxed(),
599 )
600 .with_style(style.container)
601 .boxed(),
602 )
603 .on_mouse_down(move |cx| {
604 cx.dispatch_action(ActivateItem(ix));
605 true
606 })
607 .boxed()
608 })
609 }
610
611 row.add_child(
612 Empty::new()
613 .contained()
614 .with_border(theme.workspace.tab.container.border)
615 .flex(0., true)
616 .named("filler"),
617 );
618
619 row.boxed()
620 });
621
622 ConstrainedBox::new(tabs.boxed())
623 .with_height(theme.workspace.tab.height)
624 .named("tabs")
625 }
626}
627
628impl Entity for Pane {
629 type Event = Event;
630}
631
632impl View for Pane {
633 fn ui_name() -> &'static str {
634 "Pane"
635 }
636
637 fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
638 let this = cx.handle();
639
640 EventHandler::new(if let Some(active_item) = self.active_item() {
641 Flex::column()
642 .with_child(self.render_tabs(cx))
643 .with_child(ChildView::new(&self.toolbar).boxed())
644 .with_child(ChildView::new(active_item).flex(1., true).boxed())
645 .boxed()
646 } else {
647 Empty::new().boxed()
648 })
649 .on_navigate_mouse_down(move |direction, cx| {
650 let this = this.clone();
651 match direction {
652 NavigationDirection::Back => cx.dispatch_action(GoBack(Some(this))),
653 NavigationDirection::Forward => cx.dispatch_action(GoForward(Some(this))),
654 }
655
656 true
657 })
658 .named("pane")
659 }
660
661 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
662 self.focus_active_item(cx);
663 }
664}
665
666impl ItemNavHistory {
667 pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
668 Self {
669 history,
670 item: Rc::new(item.downgrade()),
671 }
672 }
673
674 pub fn history(&self) -> Rc<RefCell<NavHistory>> {
675 self.history.clone()
676 }
677
678 pub fn push<D: 'static + Any>(&self, data: Option<D>) {
679 self.history.borrow_mut().push(data, self.item.clone());
680 }
681}
682
683impl NavHistory {
684 pub fn disable(&mut self) {
685 self.mode = NavigationMode::Disabled;
686 }
687
688 pub fn enable(&mut self) {
689 self.mode = NavigationMode::Normal;
690 }
691
692 pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
693 self.backward_stack.pop_back()
694 }
695
696 pub fn pop_forward(&mut self) -> Option<NavigationEntry> {
697 self.forward_stack.pop_back()
698 }
699
700 fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
701 match mode {
702 NavigationMode::Normal | NavigationMode::Disabled => None,
703 NavigationMode::GoingBack => self.pop_backward(),
704 NavigationMode::GoingForward => self.pop_forward(),
705 }
706 }
707
708 fn set_mode(&mut self, mode: NavigationMode) {
709 self.mode = mode;
710 }
711
712 pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
713 match self.mode {
714 NavigationMode::Disabled => {}
715 NavigationMode::Normal => {
716 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
717 self.backward_stack.pop_front();
718 }
719 self.backward_stack.push_back(NavigationEntry {
720 item,
721 data: data.map(|data| Box::new(data) as Box<dyn Any>),
722 });
723 self.forward_stack.clear();
724 }
725 NavigationMode::GoingBack => {
726 if self.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
727 self.forward_stack.pop_front();
728 }
729 self.forward_stack.push_back(NavigationEntry {
730 item,
731 data: data.map(|data| Box::new(data) as Box<dyn Any>),
732 });
733 }
734 NavigationMode::GoingForward => {
735 if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
736 self.backward_stack.pop_front();
737 }
738 self.backward_stack.push_back(NavigationEntry {
739 item,
740 data: data.map(|data| Box::new(data) as Box<dyn Any>),
741 });
742 }
743 }
744 }
745}