pane.rs

  1use super::{ItemViewHandle, SplitDirection};
  2use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace};
  3use collections::HashMap;
  4use gpui::{
  5    action,
  6    elements::*,
  7    geometry::{rect::RectF, vector::vec2f},
  8    keymap::Binding,
  9    platform::CursorStyle,
 10    Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
 11};
 12use postage::watch;
 13use project::ProjectPath;
 14use std::{any::Any, cell::RefCell, cmp, rc::Rc};
 15
 16action!(Split, SplitDirection);
 17action!(ActivateItem, usize);
 18action!(ActivatePrevItem);
 19action!(ActivateNextItem);
 20action!(CloseActiveItem);
 21action!(CloseItem, usize);
 22action!(GoBack);
 23action!(GoForward);
 24
 25pub fn init(cx: &mut MutableAppContext) {
 26    cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
 27        pane.activate_item(action.0, cx);
 28    });
 29    cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
 30        pane.activate_prev_item(cx);
 31    });
 32    cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
 33        pane.activate_next_item(cx);
 34    });
 35    cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
 36        pane.close_active_item(cx);
 37    });
 38    cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
 39        pane.close_item(action.0, cx);
 40    });
 41    cx.add_action(|pane: &mut Pane, action: &Split, cx| {
 42        pane.split(action.0, cx);
 43    });
 44    cx.add_action(Pane::go_back);
 45    cx.add_action(Pane::go_forward);
 46
 47    cx.add_bindings(vec![
 48        Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
 49        Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
 50        Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
 51        Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
 52        Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
 53        Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
 54        Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
 55        Binding::new("ctrl-", GoBack, Some("Pane")),
 56        Binding::new("ctrl-shift-_", GoForward, Some("Pane")),
 57    ]);
 58}
 59
 60pub enum Event {
 61    Activate,
 62    Remove,
 63    Split(SplitDirection),
 64}
 65
 66const MAX_TAB_TITLE_LEN: usize = 24;
 67
 68pub struct Pane {
 69    item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
 70    active_item: usize,
 71    settings: watch::Receiver<Settings>,
 72    navigation: Rc<Navigation>,
 73}
 74
 75#[derive(Default)]
 76pub struct Navigation(RefCell<NavigationHistory>);
 77
 78#[derive(Default)]
 79struct NavigationHistory {
 80    backward_stack: Vec<NavigationEntry>,
 81    forward_stack: Vec<NavigationEntry>,
 82    paths_by_item: HashMap<usize, ProjectPath>,
 83}
 84
 85struct NavigationEntry {
 86    item_view: Box<dyn WeakItemViewHandle>,
 87    data: Option<Box<dyn Any>>,
 88}
 89
 90impl Pane {
 91    pub fn new(settings: watch::Receiver<Settings>) -> Self {
 92        Self {
 93            item_views: Vec::new(),
 94            active_item: 0,
 95            settings,
 96            navigation: Default::default(),
 97        }
 98    }
 99
100    pub fn activate(&self, cx: &mut ViewContext<Self>) {
101        cx.emit(Event::Activate);
102    }
103
104    pub fn go_back(&mut self, _: &GoBack, cx: &mut ViewContext<Self>) {
105        let mut navigation = self.navigation.0.borrow_mut();
106        if let Some(entry) = navigation.go_back() {}
107    }
108
109    pub fn go_forward(&mut self, _: &GoForward, cx: &mut ViewContext<Self>) {
110        let mut navigation = self.navigation.0.borrow_mut();
111        if let Some(entry) = navigation.go_forward() {}
112    }
113
114    pub fn open_item<T>(
115        &mut self,
116        item_handle: T,
117        workspace: &Workspace,
118        cx: &mut ViewContext<Self>,
119    ) -> Box<dyn ItemViewHandle>
120    where
121        T: 'static + ItemHandle,
122    {
123        for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() {
124            if *item_id == item_handle.id() {
125                let item_view = item_view.boxed_clone();
126                self.activate_item(ix, cx);
127                return item_view;
128            }
129        }
130
131        let item_view =
132            item_handle.add_view(cx.window_id(), workspace, self.navigation.clone(), cx);
133        self.add_item_view(item_view.boxed_clone(), cx);
134        item_view
135    }
136
137    pub fn add_item_view(
138        &mut self,
139        mut item_view: Box<dyn ItemViewHandle>,
140        cx: &mut ViewContext<Self>,
141    ) {
142        item_view.added_to_pane(cx);
143        let item_idx = cmp::min(self.active_item + 1, self.item_views.len());
144        self.item_views
145            .insert(item_idx, (item_view.item_handle(cx).id(), item_view));
146        self.activate_item(item_idx, cx);
147        cx.notify();
148    }
149
150    pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
151        let item_id = item.id();
152        self.item_views
153            .iter()
154            .any(|(existing_item_id, _)| *existing_item_id == item_id)
155    }
156
157    pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
158        self.item_views.iter().map(|(_, view)| view)
159    }
160
161    pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
162        self.item_views
163            .get(self.active_item)
164            .map(|(_, view)| view.clone())
165    }
166
167    pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option<usize> {
168        self.item_views
169            .iter()
170            .position(|(_, i)| i.id() == item_view.id())
171    }
172
173    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
174        self.item_views.iter().position(|(id, _)| *id == item.id())
175    }
176
177    pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
178        if index < self.item_views.len() {
179            self.active_item = index;
180            self.focus_active_item(cx);
181            self.item_views[index].1.activated(cx);
182            cx.notify();
183        }
184    }
185
186    pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
187        if self.active_item > 0 {
188            self.active_item -= 1;
189        } else if self.item_views.len() > 0 {
190            self.active_item = self.item_views.len() - 1;
191        }
192        self.focus_active_item(cx);
193        cx.notify();
194    }
195
196    pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
197        if self.active_item + 1 < self.item_views.len() {
198            self.active_item += 1;
199        } else {
200            self.active_item = 0;
201        }
202        self.focus_active_item(cx);
203        cx.notify();
204    }
205
206    pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
207        if !self.item_views.is_empty() {
208            self.close_item(self.item_views[self.active_item].1.id(), cx)
209        }
210    }
211
212    pub fn close_item(&mut self, item_view_id: usize, cx: &mut ViewContext<Self>) {
213        self.item_views.retain(|(item_id, item)| {
214            if item.id() == item_view_id {
215                let mut navigation = self.navigation.0.borrow_mut();
216                if let Some(path) = item.project_path(cx) {
217                    navigation.paths_by_item.insert(*item_id, path);
218                } else {
219                    navigation.paths_by_item.remove(item_id);
220                }
221
222                false
223            } else {
224                true
225            }
226        });
227        self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1));
228        if self.item_views.is_empty() {
229            cx.emit(Event::Remove);
230        }
231        cx.notify();
232    }
233
234    fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
235        if let Some(active_item) = self.active_item() {
236            cx.focus(active_item.to_any());
237        }
238    }
239
240    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
241        cx.emit(Event::Split(direction));
242    }
243
244    fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
245        let settings = self.settings.borrow();
246        let theme = &settings.theme;
247
248        enum Tabs {}
249        let tabs = MouseEventHandler::new::<Tabs, _, _, _>(cx.view_id(), cx, |mouse_state, cx| {
250            let mut row = Flex::row();
251            for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
252                let is_active = ix == self.active_item;
253
254                row.add_child({
255                    let mut title = item_view.title(cx);
256                    if title.len() > MAX_TAB_TITLE_LEN {
257                        let mut truncated_len = MAX_TAB_TITLE_LEN;
258                        while !title.is_char_boundary(truncated_len) {
259                            truncated_len -= 1;
260                        }
261                        title.truncate(truncated_len);
262                        title.push('…');
263                    }
264
265                    let mut style = if is_active {
266                        theme.workspace.active_tab.clone()
267                    } else {
268                        theme.workspace.tab.clone()
269                    };
270                    if ix == 0 {
271                        style.container.border.left = false;
272                    }
273
274                    EventHandler::new(
275                        Container::new(
276                            Flex::row()
277                                .with_child(
278                                    Align::new({
279                                        let diameter = 7.0;
280                                        let icon_color = if item_view.has_conflict(cx) {
281                                            Some(style.icon_conflict)
282                                        } else if item_view.is_dirty(cx) {
283                                            Some(style.icon_dirty)
284                                        } else {
285                                            None
286                                        };
287
288                                        ConstrainedBox::new(
289                                            Canvas::new(move |bounds, _, cx| {
290                                                if let Some(color) = icon_color {
291                                                    let square = RectF::new(
292                                                        bounds.origin(),
293                                                        vec2f(diameter, diameter),
294                                                    );
295                                                    cx.scene.push_quad(Quad {
296                                                        bounds: square,
297                                                        background: Some(color),
298                                                        border: Default::default(),
299                                                        corner_radius: diameter / 2.,
300                                                    });
301                                                }
302                                            })
303                                            .boxed(),
304                                        )
305                                        .with_width(diameter)
306                                        .with_height(diameter)
307                                        .boxed()
308                                    })
309                                    .boxed(),
310                                )
311                                .with_child(
312                                    Container::new(
313                                        Align::new(
314                                            Label::new(
315                                                title,
316                                                if is_active {
317                                                    theme.workspace.active_tab.label.clone()
318                                                } else {
319                                                    theme.workspace.tab.label.clone()
320                                                },
321                                            )
322                                            .boxed(),
323                                        )
324                                        .boxed(),
325                                    )
326                                    .with_style(ContainerStyle {
327                                        margin: Margin {
328                                            left: style.spacing,
329                                            right: style.spacing,
330                                            ..Default::default()
331                                        },
332                                        ..Default::default()
333                                    })
334                                    .boxed(),
335                                )
336                                .with_child(
337                                    Align::new(
338                                        ConstrainedBox::new(if mouse_state.hovered {
339                                            let item_id = item_view.id();
340                                            enum TabCloseButton {}
341                                            let icon = Svg::new("icons/x.svg");
342                                            MouseEventHandler::new::<TabCloseButton, _, _, _>(
343                                                item_id,
344                                                cx,
345                                                |mouse_state, _| {
346                                                    if mouse_state.hovered {
347                                                        icon.with_color(style.icon_close_active)
348                                                            .boxed()
349                                                    } else {
350                                                        icon.with_color(style.icon_close).boxed()
351                                                    }
352                                                },
353                                            )
354                                            .with_padding(Padding::uniform(4.))
355                                            .with_cursor_style(CursorStyle::PointingHand)
356                                            .on_click(move |cx| {
357                                                cx.dispatch_action(CloseItem(item_id))
358                                            })
359                                            .named("close-tab-icon")
360                                        } else {
361                                            Empty::new().boxed()
362                                        })
363                                        .with_width(style.icon_width)
364                                        .boxed(),
365                                    )
366                                    .boxed(),
367                                )
368                                .boxed(),
369                        )
370                        .with_style(style.container)
371                        .boxed(),
372                    )
373                    .on_mouse_down(move |cx| {
374                        cx.dispatch_action(ActivateItem(ix));
375                        true
376                    })
377                    .boxed()
378                })
379            }
380
381            row.add_child(
382                Empty::new()
383                    .contained()
384                    .with_border(theme.workspace.tab.container.border)
385                    .flexible(0., true)
386                    .named("filler"),
387            );
388
389            row.boxed()
390        });
391
392        ConstrainedBox::new(tabs.boxed())
393            .with_height(theme.workspace.tab.height)
394            .named("tabs")
395    }
396}
397
398impl Entity for Pane {
399    type Event = Event;
400}
401
402impl View for Pane {
403    fn ui_name() -> &'static str {
404        "Pane"
405    }
406
407    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
408        if let Some(active_item) = self.active_item() {
409            Flex::column()
410                .with_child(self.render_tabs(cx))
411                .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
412                .named("pane")
413        } else {
414            Empty::new().named("pane")
415        }
416    }
417
418    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
419        self.focus_active_item(cx);
420    }
421}
422
423impl Navigation {
424    pub fn push<D: 'static + Any, T: ItemView>(&self, data: Option<D>, cx: &mut ViewContext<T>) {
425        let mut state = self.0.borrow_mut();
426        state.backward_stack.push(NavigationEntry {
427            item_view: Box::new(cx.weak_handle()),
428            data: data.map(|data| Box::new(data) as Box<dyn Any>),
429        });
430    }
431}
432
433impl NavigationHistory {
434    fn go_back(&mut self) -> Option<&NavigationEntry> {
435        if let Some(backward) = self.backward_stack.pop() {
436            self.forward_stack.push(backward);
437            self.forward_stack.last()
438        } else {
439            None
440        }
441    }
442
443    fn go_forward(&mut self) -> Option<&NavigationEntry> {
444        if let Some(forward) = self.forward_stack.pop() {
445            self.backward_stack.push(forward);
446            self.backward_stack.last()
447        } else {
448            None
449        }
450    }
451}