pane.rs

  1use super::{ItemViewHandle, SplitDirection};
  2use crate::{settings::Settings, theme};
  3use gpui::{
  4    action,
  5    color::Color,
  6    elements::*,
  7    geometry::{rect::RectF, vector::vec2f},
  8    keymap::Binding,
  9    Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
 10};
 11use postage::watch;
 12use std::{cmp, path::Path, sync::Arc};
 13
 14action!(Split, SplitDirection);
 15action!(ActivateItem, usize);
 16action!(ActivatePrevItem);
 17action!(ActivateNextItem);
 18action!(CloseActiveItem);
 19action!(CloseItem, usize);
 20
 21pub fn init(cx: &mut MutableAppContext) {
 22    cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
 23        pane.activate_item(action.0, cx);
 24    });
 25    cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
 26        pane.activate_prev_item(cx);
 27    });
 28    cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
 29        pane.activate_next_item(cx);
 30    });
 31    cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
 32        pane.close_active_item(cx);
 33    });
 34    cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
 35        pane.close_item(action.0, cx);
 36    });
 37    cx.add_action(|pane: &mut Pane, action: &Split, cx| {
 38        pane.split(action.0, cx);
 39    });
 40
 41    cx.add_bindings(vec![
 42        Binding::new("shift-cmd-{", ActivatePrevItem, Some("Pane")),
 43        Binding::new("shift-cmd-}", ActivateNextItem, Some("Pane")),
 44        Binding::new("cmd-w", CloseActiveItem, Some("Pane")),
 45        Binding::new("cmd-k up", Split(SplitDirection::Up), Some("Pane")),
 46        Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")),
 47        Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")),
 48        Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")),
 49    ]);
 50}
 51
 52pub enum Event {
 53    Activate,
 54    Remove,
 55    Split(SplitDirection),
 56}
 57
 58#[derive(Debug, Eq, PartialEq)]
 59pub struct State {
 60    pub tabs: Vec<TabState>,
 61}
 62
 63#[derive(Debug, Eq, PartialEq)]
 64pub struct TabState {
 65    pub title: String,
 66    pub active: bool,
 67}
 68
 69pub struct Pane {
 70    items: Vec<Box<dyn ItemViewHandle>>,
 71    active_item: usize,
 72    settings: watch::Receiver<Settings>,
 73}
 74
 75impl Pane {
 76    pub fn new(settings: watch::Receiver<Settings>) -> Self {
 77        Self {
 78            items: Vec::new(),
 79            active_item: 0,
 80            settings,
 81        }
 82    }
 83
 84    pub fn activate(&self, cx: &mut ViewContext<Self>) {
 85        cx.emit(Event::Activate);
 86    }
 87
 88    pub fn add_item(&mut self, item: Box<dyn ItemViewHandle>, cx: &mut ViewContext<Self>) -> usize {
 89        let item_idx = cmp::min(self.active_item + 1, self.items.len());
 90        self.items.insert(item_idx, item);
 91        cx.notify();
 92        item_idx
 93    }
 94
 95    #[cfg(test)]
 96    pub fn items(&self) -> &[Box<dyn ItemViewHandle>] {
 97        &self.items
 98    }
 99
100    pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
101        self.items.get(self.active_item).cloned()
102    }
103
104    pub fn activate_entry(
105        &mut self,
106        entry_id: (usize, Arc<Path>),
107        cx: &mut ViewContext<Self>,
108    ) -> bool {
109        if let Some(index) = self.items.iter().position(|item| {
110            item.entry_id(cx.as_ref())
111                .map_or(false, |id| id == entry_id)
112        }) {
113            self.activate_item(index, cx);
114            true
115        } else {
116            false
117        }
118    }
119
120    pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
121        self.items.iter().position(|i| i.id() == item.id())
122    }
123
124    pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
125        if index < self.items.len() {
126            self.active_item = index;
127            self.focus_active_item(cx);
128            cx.notify();
129        }
130    }
131
132    pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
133        if self.active_item > 0 {
134            self.active_item -= 1;
135        } else if self.items.len() > 0 {
136            self.active_item = self.items.len() - 1;
137        }
138        self.focus_active_item(cx);
139        cx.notify();
140    }
141
142    pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
143        if self.active_item + 1 < self.items.len() {
144            self.active_item += 1;
145        } else {
146            self.active_item = 0;
147        }
148        self.focus_active_item(cx);
149        cx.notify();
150    }
151
152    pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
153        if !self.items.is_empty() {
154            self.close_item(self.items[self.active_item].id(), cx)
155        }
156    }
157
158    pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
159        self.items.retain(|item| item.id() != item_id);
160        self.active_item = cmp::min(self.active_item, self.items.len().saturating_sub(1));
161        if self.items.is_empty() {
162            cx.emit(Event::Remove);
163        }
164        cx.notify();
165    }
166
167    fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
168        if let Some(active_item) = self.active_item() {
169            cx.focus(active_item.to_any());
170        }
171    }
172
173    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
174        cx.emit(Event::Split(direction));
175    }
176
177    fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
178        let settings = self.settings.borrow();
179        let theme = &settings.theme;
180        let line_height = cx.font_cache().line_height(
181            theme.workspace.tab.label.text.font_id,
182            theme.workspace.tab.label.text.font_size,
183        );
184
185        let mut row = Flex::row();
186        let last_item_ix = self.items.len() - 1;
187        for (ix, item) in self.items.iter().enumerate() {
188            let is_active = ix == self.active_item;
189
190            enum Tab {}
191            let border = &theme.workspace.tab.container.border;
192
193            row.add_child(
194                Expanded::new(
195                    1.0,
196                    MouseEventHandler::new::<Tab, _, _>(item.id(), cx, |mouse_state, cx| {
197                        let title = item.title(cx);
198
199                        let mut border = border.clone();
200                        border.left = ix > 0;
201                        border.right = ix == last_item_ix;
202                        border.bottom = !is_active;
203
204                        let mut container = Container::new(
205                            Stack::new()
206                                .with_child(
207                                    Align::new(
208                                        Label::new(
209                                            title,
210                                            if is_active {
211                                                theme.workspace.active_tab.label.clone()
212                                            } else {
213                                                theme.workspace.tab.label.clone()
214                                            },
215                                        )
216                                        .boxed(),
217                                    )
218                                    .boxed(),
219                                )
220                                .with_child(
221                                    Align::new(Self::render_tab_icon(
222                                        item.id(),
223                                        line_height - 2.,
224                                        mouse_state.hovered,
225                                        item.is_dirty(cx),
226                                        item.has_conflict(cx),
227                                        theme,
228                                        cx,
229                                    ))
230                                    .right()
231                                    .boxed(),
232                                )
233                                .boxed(),
234                        )
235                        .with_style(if is_active {
236                            &theme.workspace.active_tab.container
237                        } else {
238                            &theme.workspace.tab.container
239                        })
240                        .with_border(border);
241
242                        if is_active {
243                            container = container.with_padding_bottom(border.width);
244                        }
245
246                        ConstrainedBox::new(
247                            EventHandler::new(container.boxed())
248                                .on_mouse_down(move |cx| {
249                                    cx.dispatch_action(ActivateItem(ix));
250                                    true
251                                })
252                                .boxed(),
253                        )
254                        .with_min_width(80.0)
255                        .with_max_width(264.0)
256                        .boxed()
257                    })
258                    .boxed(),
259                )
260                .named("tab"),
261            );
262        }
263
264        // Ensure there's always a minimum amount of space after the last tab,
265        // so that the tab's border doesn't abut the window's border.
266        let mut border = Border::bottom(1.0, Color::default());
267        border.color = theme.workspace.tab.container.border.color;
268
269        row.add_child(
270            ConstrainedBox::new(
271                Container::new(Empty::new().boxed())
272                    .with_border(border)
273                    .boxed(),
274            )
275            .with_min_width(20.)
276            .named("fixed-filler"),
277        );
278
279        row.add_child(
280            Expanded::new(
281                0.0,
282                Container::new(Empty::new().boxed())
283                    .with_border(border)
284                    .boxed(),
285            )
286            .named("filler"),
287        );
288
289        ConstrainedBox::new(row.boxed())
290            .with_height(line_height + 16.)
291            .named("tabs")
292    }
293
294    fn render_tab_icon(
295        item_id: usize,
296        close_icon_size: f32,
297        tab_hovered: bool,
298        is_dirty: bool,
299        has_conflict: bool,
300        theme: &theme::Theme,
301        cx: &mut RenderContext<Self>,
302    ) -> ElementBox {
303        enum TabCloseButton {}
304
305        let mut clicked_color = theme.workspace.tab.icon_dirty;
306        clicked_color.a = 180;
307
308        let current_color = if has_conflict {
309            Some(theme.workspace.tab.icon_conflict)
310        } else if is_dirty {
311            Some(theme.workspace.tab.icon_dirty)
312        } else {
313            None
314        };
315
316        let icon = if tab_hovered {
317            let close_color = current_color.unwrap_or(theme.workspace.tab.icon_close);
318            let icon = Svg::new("icons/x.svg").with_color(close_color);
319
320            MouseEventHandler::new::<TabCloseButton, _, _>(item_id, cx, |mouse_state, _| {
321                if mouse_state.hovered {
322                    Container::new(icon.with_color(Color::white()).boxed())
323                        .with_background_color(if mouse_state.clicked {
324                            clicked_color
325                        } else {
326                            theme.workspace.tab.icon_dirty
327                        })
328                        .with_corner_radius(close_icon_size / 2.)
329                        .boxed()
330                } else {
331                    icon.boxed()
332                }
333            })
334            .on_click(move |cx| cx.dispatch_action(CloseItem(item_id)))
335            .named("close-tab-icon")
336        } else {
337            let diameter = 8.;
338            ConstrainedBox::new(
339                Canvas::new(move |bounds, cx| {
340                    if let Some(current_color) = current_color {
341                        let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
342                        cx.scene.push_quad(Quad {
343                            bounds: square,
344                            background: Some(current_color),
345                            border: Default::default(),
346                            corner_radius: diameter / 2.,
347                        });
348                    }
349                })
350                .boxed(),
351            )
352            .with_width(diameter)
353            .with_height(diameter)
354            .named("unsaved-tab-icon")
355        };
356
357        ConstrainedBox::new(Align::new(icon).boxed())
358            .with_width(close_icon_size)
359            .named("tab-icon")
360    }
361}
362
363impl Entity for Pane {
364    type Event = Event;
365}
366
367impl View for Pane {
368    fn ui_name() -> &'static str {
369        "Pane"
370    }
371
372    fn render(&self, cx: &mut RenderContext<Self>) -> ElementBox {
373        if let Some(active_item) = self.active_item() {
374            Flex::column()
375                .with_child(self.render_tabs(cx))
376                .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
377                .named("pane")
378        } else {
379            Empty::new().named("pane")
380        }
381    }
382
383    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
384        self.focus_active_item(cx);
385    }
386}
387
388pub trait PaneHandle {
389    fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext);
390}
391
392impl PaneHandle for ViewHandle<Pane> {
393    fn add_item_view(&self, item: Box<dyn ItemViewHandle>, cx: &mut MutableAppContext) {
394        item.set_parent_pane(self, cx);
395        self.update(cx, |pane, cx| {
396            let item_idx = pane.add_item(item, cx);
397            pane.activate_item(item_idx, cx);
398        });
399    }
400}