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