pane.rs

  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    Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle,
 11};
 12use postage::watch;
 13use project::ProjectEntry;
 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!(CloseItem, usize);
 23action!(GoBack);
 24action!(GoForward);
 25
 26const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 27
 28pub fn init(cx: &mut MutableAppContext) {
 29    cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
 30        pane.activate_item(action.0, cx);
 31    });
 32    cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
 33        pane.activate_prev_item(cx);
 34    });
 35    cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
 36        pane.activate_next_item(cx);
 37    });
 38    cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
 39        pane.close_active_item(cx);
 40    });
 41    cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
 42        pane.close_item(action.0, cx);
 43    });
 44    cx.add_action(|pane: &mut Pane, action: &Split, cx| {
 45        pane.split(action.0, cx);
 46    });
 47    cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
 48        Pane::go_back(workspace, cx).detach();
 49    });
 50    cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
 51        Pane::go_forward(workspace, cx).detach();
 52    });
 53
 54    cx.add_bindings(vec![
 55        Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
 56        Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
 57        Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
 58        Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
 59        Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
 60        Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
 61        Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
 62        Binding::new("ctrl--", GoBack, Some("Pane")),
 63        Binding::new("shift-ctrl-_", GoForward, Some("Pane")),
 64    ]);
 65}
 66
 67pub enum Event {
 68    Activate,
 69    Remove,
 70    Split(SplitDirection),
 71}
 72
 73const MAX_TAB_TITLE_LEN: usize = 24;
 74
 75pub struct Pane {
 76    item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
 77    active_item_index: usize,
 78    settings: watch::Receiver<Settings>,
 79    nav_history: Rc<NavHistory>,
 80}
 81
 82#[derive(Default)]
 83pub struct NavHistory(RefCell<NavHistoryState>);
 84
 85#[derive(Default)]
 86struct NavHistoryState {
 87    mode: NavigationMode,
 88    backward_stack: VecDeque<NavigationEntry>,
 89    forward_stack: VecDeque<NavigationEntry>,
 90    project_entries_by_item: HashMap<usize, ProjectEntry>,
 91}
 92
 93#[derive(Copy, Clone)]
 94enum NavigationMode {
 95    Normal,
 96    GoingBack,
 97    GoingForward,
 98}
 99
100impl Default for NavigationMode {
101    fn default() -> Self {
102        Self::Normal
103    }
104}
105
106pub struct NavigationEntry {
107    pub item_view: Box<dyn WeakItemViewHandle>,
108    pub data: Option<Box<dyn Any>>,
109}
110
111impl Pane {
112    pub fn new(settings: watch::Receiver<Settings>) -> Self {
113        Self {
114            item_views: Vec::new(),
115            active_item_index: 0,
116            settings,
117            nav_history: Default::default(),
118        }
119    }
120
121    pub fn activate(&self, cx: &mut ViewContext<Self>) {
122        cx.emit(Event::Activate);
123    }
124
125    pub fn go_back(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> {
126        Self::navigate_history(
127            workspace,
128            workspace.active_pane().clone(),
129            NavigationMode::GoingBack,
130            cx,
131        )
132    }
133
134    pub fn go_forward(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Task<()> {
135        Self::navigate_history(
136            workspace,
137            workspace.active_pane().clone(),
138            NavigationMode::GoingForward,
139            cx,
140        )
141    }
142
143    fn navigate_history(
144        workspace: &mut Workspace,
145        pane: ViewHandle<Pane>,
146        mode: NavigationMode,
147        cx: &mut ViewContext<Workspace>,
148    ) -> Task<()> {
149        let to_load = pane.update(cx, |pane, cx| {
150            // Retrieve the weak item handle from the history.
151            let nav_entry = pane.nav_history.pop(mode)?;
152
153            // If the item is still present in this pane, then activate it.
154            if let Some(index) = nav_entry
155                .item_view
156                .upgrade(cx)
157                .and_then(|v| pane.index_for_item_view(v.as_ref()))
158            {
159                if let Some(item_view) = pane.active_item() {
160                    pane.nav_history.set_mode(mode);
161                    item_view.deactivated(cx);
162                    pane.nav_history.set_mode(NavigationMode::Normal);
163                }
164
165                pane.active_item_index = index;
166                pane.focus_active_item(cx);
167                if let Some(data) = nav_entry.data {
168                    pane.active_item()?.navigate(data, cx);
169                }
170                cx.notify();
171                None
172            }
173            // If the item is no longer present in this pane, then retrieve its
174            // project path in order to reopen it.
175            else {
176                pane.nav_history
177                    .0
178                    .borrow_mut()
179                    .project_entries_by_item
180                    .get(&nav_entry.item_view.id())
181                    .cloned()
182                    .map(|project_entry| (project_entry, nav_entry))
183            }
184        });
185
186        if let Some((project_entry, nav_entry)) = to_load {
187            // If the item was no longer present, then load it again from its previous path.
188            let pane = pane.downgrade();
189            let task = workspace.load_entry(project_entry, cx);
190            cx.spawn(|workspace, mut cx| async move {
191                let item = task.await;
192                if let Some(pane) = cx.read(|cx| pane.upgrade(cx)) {
193                    if let Some(item) = item.log_err() {
194                        workspace.update(&mut cx, |workspace, cx| {
195                            pane.update(cx, |p, _| p.nav_history.set_mode(mode));
196                            let item_view = workspace.open_item_in_pane(item, &pane, cx);
197                            pane.update(cx, |p, _| p.nav_history.set_mode(NavigationMode::Normal));
198
199                            if let Some(data) = nav_entry.data {
200                                item_view.navigate(data, cx);
201                            }
202                        });
203                    } else {
204                        workspace
205                            .update(&mut cx, |workspace, cx| {
206                                Self::navigate_history(workspace, pane, mode, cx)
207                            })
208                            .await;
209                    }
210                }
211            })
212        } else {
213            Task::ready(())
214        }
215    }
216
217    pub fn open_item<T>(
218        &mut self,
219        item_handle: T,
220        workspace: &Workspace,
221        cx: &mut ViewContext<Self>,
222    ) -> Box<dyn ItemViewHandle>
223    where
224        T: 'static + ItemHandle,
225    {
226        for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() {
227            if *item_id == item_handle.id() {
228                let item_view = item_view.boxed_clone();
229                self.activate_item(ix, cx);
230                return item_view;
231            }
232        }
233
234        let item_view =
235            item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx);
236        self.add_item_view(item_view.boxed_clone(), cx);
237        item_view
238    }
239
240    pub fn add_item_view(
241        &mut self,
242        mut item_view: Box<dyn ItemViewHandle>,
243        cx: &mut ViewContext<Self>,
244    ) {
245        item_view.added_to_pane(cx);
246        let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len());
247        self.item_views
248            .insert(item_idx, (item_view.item_handle(cx).id(), item_view));
249        self.activate_item(item_idx, cx);
250        cx.notify();
251    }
252
253    pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
254        let item_id = item.id();
255        self.item_views
256            .iter()
257            .any(|(existing_item_id, _)| *existing_item_id == item_id)
258    }
259
260    pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
261        self.item_views.iter().map(|(_, view)| view)
262    }
263
264    pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
265        self.item_views
266            .get(self.active_item_index)
267            .map(|(_, view)| view.clone())
268    }
269
270    pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option<usize> {
271        self.item_views
272            .iter()
273            .position(|(_, i)| i.id() == item_view.id())
274    }
275
276    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
277        self.item_views.iter().position(|(id, _)| *id == item.id())
278    }
279
280    pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
281        if index < self.item_views.len() {
282            let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
283            if prev_active_item_ix != self.active_item_index {
284                self.item_views[prev_active_item_ix].1.deactivated(cx);
285            }
286            self.focus_active_item(cx);
287            cx.notify();
288        }
289    }
290
291    pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
292        let mut index = self.active_item_index;
293        if index > 0 {
294            index -= 1;
295        } else if self.item_views.len() > 0 {
296            index = self.item_views.len() - 1;
297        }
298        self.activate_item(index, cx);
299    }
300
301    pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
302        let mut index = self.active_item_index;
303        if index + 1 < self.item_views.len() {
304            index += 1;
305        } else {
306            index = 0;
307        }
308        self.activate_item(index, cx);
309    }
310
311    pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
312        if !self.item_views.is_empty() {
313            self.close_item(self.item_views[self.active_item_index].1.id(), cx)
314        }
315    }
316
317    pub fn close_item(&mut self, item_view_id: usize, cx: &mut ViewContext<Self>) {
318        let mut item_ix = 0;
319        self.item_views.retain(|(_, item_view)| {
320            if item_view.id() == item_view_id {
321                if item_ix == self.active_item_index {
322                    item_view.deactivated(cx);
323                }
324
325                let mut nav_history = self.nav_history.0.borrow_mut();
326                if let Some(entry) = item_view.project_entry(cx) {
327                    nav_history
328                        .project_entries_by_item
329                        .insert(item_view.id(), entry);
330                } else {
331                    nav_history.project_entries_by_item.remove(&item_view.id());
332                }
333
334                item_ix += 1;
335                false
336            } else {
337                item_ix += 1;
338                true
339            }
340        });
341        self.active_item_index = cmp::min(
342            self.active_item_index,
343            self.item_views.len().saturating_sub(1),
344        );
345
346        if self.item_views.is_empty() {
347            cx.emit(Event::Remove);
348        }
349        cx.notify();
350    }
351
352    fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
353        if let Some(active_item) = self.active_item() {
354            cx.focus(active_item.to_any());
355        }
356    }
357
358    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
359        cx.emit(Event::Split(direction));
360    }
361
362    fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
363        let settings = self.settings.borrow();
364        let theme = &settings.theme;
365
366        enum Tabs {}
367        let tabs = MouseEventHandler::new::<Tabs, _, _, _>(cx.view_id(), cx, |mouse_state, cx| {
368            let mut row = Flex::row();
369            for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
370                let is_active = ix == self.active_item_index;
371
372                row.add_child({
373                    let mut title = item_view.title(cx);
374                    if title.len() > MAX_TAB_TITLE_LEN {
375                        let mut truncated_len = MAX_TAB_TITLE_LEN;
376                        while !title.is_char_boundary(truncated_len) {
377                            truncated_len -= 1;
378                        }
379                        title.truncate(truncated_len);
380                        title.push('…');
381                    }
382
383                    let mut style = if is_active {
384                        theme.workspace.active_tab.clone()
385                    } else {
386                        theme.workspace.tab.clone()
387                    };
388                    if ix == 0 {
389                        style.container.border.left = false;
390                    }
391
392                    EventHandler::new(
393                        Container::new(
394                            Flex::row()
395                                .with_child(
396                                    Align::new({
397                                        let diameter = 7.0;
398                                        let icon_color = if item_view.has_conflict(cx) {
399                                            Some(style.icon_conflict)
400                                        } else if item_view.is_dirty(cx) {
401                                            Some(style.icon_dirty)
402                                        } else {
403                                            None
404                                        };
405
406                                        ConstrainedBox::new(
407                                            Canvas::new(move |bounds, _, cx| {
408                                                if let Some(color) = icon_color {
409                                                    let square = RectF::new(
410                                                        bounds.origin(),
411                                                        vec2f(diameter, diameter),
412                                                    );
413                                                    cx.scene.push_quad(Quad {
414                                                        bounds: square,
415                                                        background: Some(color),
416                                                        border: Default::default(),
417                                                        corner_radius: diameter / 2.,
418                                                    });
419                                                }
420                                            })
421                                            .boxed(),
422                                        )
423                                        .with_width(diameter)
424                                        .with_height(diameter)
425                                        .boxed()
426                                    })
427                                    .boxed(),
428                                )
429                                .with_child(
430                                    Container::new(
431                                        Align::new(
432                                            Label::new(
433                                                title,
434                                                if is_active {
435                                                    theme.workspace.active_tab.label.clone()
436                                                } else {
437                                                    theme.workspace.tab.label.clone()
438                                                },
439                                            )
440                                            .boxed(),
441                                        )
442                                        .boxed(),
443                                    )
444                                    .with_style(ContainerStyle {
445                                        margin: Margin {
446                                            left: style.spacing,
447                                            right: style.spacing,
448                                            ..Default::default()
449                                        },
450                                        ..Default::default()
451                                    })
452                                    .boxed(),
453                                )
454                                .with_child(
455                                    Align::new(
456                                        ConstrainedBox::new(if mouse_state.hovered {
457                                            let item_id = item_view.id();
458                                            enum TabCloseButton {}
459                                            let icon = Svg::new("icons/x.svg");
460                                            MouseEventHandler::new::<TabCloseButton, _, _, _>(
461                                                item_id,
462                                                cx,
463                                                |mouse_state, _| {
464                                                    if mouse_state.hovered {
465                                                        icon.with_color(style.icon_close_active)
466                                                            .boxed()
467                                                    } else {
468                                                        icon.with_color(style.icon_close).boxed()
469                                                    }
470                                                },
471                                            )
472                                            .with_padding(Padding::uniform(4.))
473                                            .with_cursor_style(CursorStyle::PointingHand)
474                                            .on_click(move |cx| {
475                                                cx.dispatch_action(CloseItem(item_id))
476                                            })
477                                            .named("close-tab-icon")
478                                        } else {
479                                            Empty::new().boxed()
480                                        })
481                                        .with_width(style.icon_width)
482                                        .boxed(),
483                                    )
484                                    .boxed(),
485                                )
486                                .boxed(),
487                        )
488                        .with_style(style.container)
489                        .boxed(),
490                    )
491                    .on_mouse_down(move |cx| {
492                        cx.dispatch_action(ActivateItem(ix));
493                        true
494                    })
495                    .boxed()
496                })
497            }
498
499            row.add_child(
500                Empty::new()
501                    .contained()
502                    .with_border(theme.workspace.tab.container.border)
503                    .flexible(0., true)
504                    .named("filler"),
505            );
506
507            row.boxed()
508        });
509
510        ConstrainedBox::new(tabs.boxed())
511            .with_height(theme.workspace.tab.height)
512            .named("tabs")
513    }
514}
515
516impl Entity for Pane {
517    type Event = Event;
518}
519
520impl View for Pane {
521    fn ui_name() -> &'static str {
522        "Pane"
523    }
524
525    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
526        if let Some(active_item) = self.active_item() {
527            Flex::column()
528                .with_child(self.render_tabs(cx))
529                .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
530                .named("pane")
531        } else {
532            Empty::new().named("pane")
533        }
534    }
535
536    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
537        self.focus_active_item(cx);
538    }
539}
540
541impl NavHistory {
542    pub fn pop_backward(&self) -> Option<NavigationEntry> {
543        self.0.borrow_mut().backward_stack.pop_back()
544    }
545
546    pub fn pop_forward(&self) -> Option<NavigationEntry> {
547        self.0.borrow_mut().forward_stack.pop_back()
548    }
549
550    fn pop(&self, mode: NavigationMode) -> Option<NavigationEntry> {
551        match mode {
552            NavigationMode::Normal => None,
553            NavigationMode::GoingBack => self.pop_backward(),
554            NavigationMode::GoingForward => self.pop_forward(),
555        }
556    }
557
558    fn set_mode(&self, mode: NavigationMode) {
559        self.0.borrow_mut().mode = mode;
560    }
561
562    pub fn push<D: 'static + Any, T: ItemView>(&self, data: Option<D>, cx: &mut ViewContext<T>) {
563        let mut state = self.0.borrow_mut();
564        match state.mode {
565            NavigationMode::Normal => {
566                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
567                    state.backward_stack.pop_front();
568                }
569                state.backward_stack.push_back(NavigationEntry {
570                    item_view: Box::new(cx.weak_handle()),
571                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
572                });
573                state.forward_stack.clear();
574            }
575            NavigationMode::GoingBack => {
576                if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
577                    state.forward_stack.pop_front();
578                }
579                state.forward_stack.push_back(NavigationEntry {
580                    item_view: Box::new(cx.weak_handle()),
581                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
582                });
583            }
584            NavigationMode::GoingForward => {
585                if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN {
586                    state.backward_stack.pop_front();
587                }
588                state.backward_stack.push_back(NavigationEntry {
589                    item_view: Box::new(cx.weak_handle()),
590                    data: data.map(|data| Box::new(data) as Box<dyn Any>),
591                });
592            }
593        }
594    }
595}