pane.rs

  1use super::{ItemViewHandle, SplitDirection};
  2use crate::{ItemHandle, Settings, Workspace};
  3use gpui::{
  4    action,
  5    elements::*,
  6    geometry::{rect::RectF, vector::vec2f},
  7    keymap::Binding,
  8    platform::CursorStyle,
  9    Entity, MutableAppContext, Quad, RenderContext, View, ViewContext,
 10};
 11use postage::watch;
 12use std::cmp;
 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
 58const MAX_TAB_TITLE_LEN: usize = 24;
 59
 60#[derive(Debug, Eq, PartialEq)]
 61pub struct State {
 62    pub tabs: Vec<TabState>,
 63}
 64
 65#[derive(Debug, Eq, PartialEq)]
 66pub struct TabState {
 67    pub title: String,
 68    pub active: bool,
 69}
 70
 71pub struct Pane {
 72    item_views: Vec<Box<dyn ItemViewHandle>>,
 73    active_item: usize,
 74    settings: watch::Receiver<Settings>,
 75}
 76
 77impl Pane {
 78    pub fn new(settings: watch::Receiver<Settings>) -> Self {
 79        Self {
 80            item_views: Vec::new(),
 81            active_item: 0,
 82            settings,
 83        }
 84    }
 85
 86    pub fn activate(&self, cx: &mut ViewContext<Self>) {
 87        cx.emit(Event::Activate);
 88    }
 89
 90    pub fn open_item<T>(
 91        &mut self,
 92        item_handle: T,
 93        workspace: &Workspace,
 94        cx: &mut ViewContext<Self>,
 95    ) -> Box<dyn ItemViewHandle>
 96    where
 97        T: 'static + ItemHandle,
 98    {
 99        for (ix, item_view) in self.item_views.iter().enumerate() {
100            if item_view.item_handle(cx).id() == item_handle.id() {
101                let item_view = item_view.boxed_clone();
102                self.activate_item(ix, cx);
103                return item_view;
104            }
105        }
106
107        let item_view = item_handle.add_view(cx.window_id(), workspace, cx);
108        self.add_item_view(item_view.boxed_clone(), cx);
109        item_view
110    }
111
112    pub fn add_item_view(
113        &mut self,
114        item_view: Box<dyn ItemViewHandle>,
115        cx: &mut ViewContext<Self>,
116    ) {
117        item_view.added_to_pane(cx);
118        let item_idx = cmp::min(self.active_item + 1, self.item_views.len());
119        self.item_views.insert(item_idx, item_view);
120        self.activate_item(item_idx, cx);
121        cx.notify();
122    }
123
124    pub fn item_views(&self) -> &[Box<dyn ItemViewHandle>] {
125        &self.item_views
126    }
127
128    pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
129        self.item_views.get(self.active_item).cloned()
130    }
131
132    pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
133        self.item_views.iter().position(|i| i.id() == item.id())
134    }
135
136    pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
137        if index < self.item_views.len() {
138            self.active_item = index;
139            self.focus_active_item(cx);
140            cx.notify();
141        }
142    }
143
144    pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
145        if self.active_item > 0 {
146            self.active_item -= 1;
147        } else if self.item_views.len() > 0 {
148            self.active_item = self.item_views.len() - 1;
149        }
150        self.focus_active_item(cx);
151        cx.notify();
152    }
153
154    pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
155        if self.active_item + 1 < self.item_views.len() {
156            self.active_item += 1;
157        } else {
158            self.active_item = 0;
159        }
160        self.focus_active_item(cx);
161        cx.notify();
162    }
163
164    pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
165        if !self.item_views.is_empty() {
166            self.close_item(self.item_views[self.active_item].id(), cx)
167        }
168    }
169
170    pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
171        self.item_views.retain(|item| item.id() != item_id);
172        self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1));
173        if self.item_views.is_empty() {
174            cx.emit(Event::Remove);
175        }
176        cx.notify();
177    }
178
179    fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
180        if let Some(active_item) = self.active_item() {
181            cx.focus(active_item.to_any());
182        }
183    }
184
185    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
186        cx.emit(Event::Split(direction));
187    }
188
189    fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
190        let settings = self.settings.borrow();
191        let theme = &settings.theme;
192
193        enum Tabs {}
194        let tabs = MouseEventHandler::new::<Tabs, _, _, _>(cx.view_id(), cx, |mouse_state, cx| {
195            let mut row = Flex::row();
196            for (ix, item_view) in self.item_views.iter().enumerate() {
197                let is_active = ix == self.active_item;
198
199                row.add_child({
200                    let mut title = item_view.title(cx);
201                    if title.len() > MAX_TAB_TITLE_LEN {
202                        let mut truncated_len = MAX_TAB_TITLE_LEN;
203                        while !title.is_char_boundary(truncated_len) {
204                            truncated_len -= 1;
205                        }
206                        title.truncate(truncated_len);
207                        title.push('…');
208                    }
209
210                    let mut style = if is_active {
211                        theme.workspace.active_tab.clone()
212                    } else {
213                        theme.workspace.tab.clone()
214                    };
215                    if ix == 0 {
216                        style.container.border.left = false;
217                    }
218
219                    EventHandler::new(
220                        Container::new(
221                            Flex::row()
222                                .with_child(
223                                    Align::new({
224                                        let diameter = 7.0;
225                                        let icon_color = if item_view.has_conflict(cx) {
226                                            Some(style.icon_conflict)
227                                        } else if item_view.is_dirty(cx) {
228                                            Some(style.icon_dirty)
229                                        } else {
230                                            None
231                                        };
232
233                                        ConstrainedBox::new(
234                                            Canvas::new(move |bounds, _, cx| {
235                                                if let Some(color) = icon_color {
236                                                    let square = RectF::new(
237                                                        bounds.origin(),
238                                                        vec2f(diameter, diameter),
239                                                    );
240                                                    cx.scene.push_quad(Quad {
241                                                        bounds: square,
242                                                        background: Some(color),
243                                                        border: Default::default(),
244                                                        corner_radius: diameter / 2.,
245                                                    });
246                                                }
247                                            })
248                                            .boxed(),
249                                        )
250                                        .with_width(diameter)
251                                        .with_height(diameter)
252                                        .boxed()
253                                    })
254                                    .boxed(),
255                                )
256                                .with_child(
257                                    Container::new(
258                                        Align::new(
259                                            Label::new(
260                                                title,
261                                                if is_active {
262                                                    theme.workspace.active_tab.label.clone()
263                                                } else {
264                                                    theme.workspace.tab.label.clone()
265                                                },
266                                            )
267                                            .boxed(),
268                                        )
269                                        .boxed(),
270                                    )
271                                    .with_style(ContainerStyle {
272                                        margin: Margin {
273                                            left: style.spacing,
274                                            right: style.spacing,
275                                            ..Default::default()
276                                        },
277                                        ..Default::default()
278                                    })
279                                    .boxed(),
280                                )
281                                .with_child(
282                                    Align::new(
283                                        ConstrainedBox::new(if mouse_state.hovered {
284                                            let item_id = item_view.id();
285                                            enum TabCloseButton {}
286                                            let icon = Svg::new("icons/x.svg");
287                                            MouseEventHandler::new::<TabCloseButton, _, _, _>(
288                                                item_id,
289                                                cx,
290                                                |mouse_state, _| {
291                                                    if mouse_state.hovered {
292                                                        icon.with_color(style.icon_close_active)
293                                                            .boxed()
294                                                    } else {
295                                                        icon.with_color(style.icon_close).boxed()
296                                                    }
297                                                },
298                                            )
299                                            .with_padding(Padding::uniform(4.))
300                                            .with_cursor_style(CursorStyle::PointingHand)
301                                            .on_click(move |cx| {
302                                                cx.dispatch_action(CloseItem(item_id))
303                                            })
304                                            .named("close-tab-icon")
305                                        } else {
306                                            Empty::new().boxed()
307                                        })
308                                        .with_width(style.icon_width)
309                                        .boxed(),
310                                    )
311                                    .boxed(),
312                                )
313                                .boxed(),
314                        )
315                        .with_style(style.container)
316                        .boxed(),
317                    )
318                    .on_mouse_down(move |cx| {
319                        cx.dispatch_action(ActivateItem(ix));
320                        true
321                    })
322                    .boxed()
323                })
324            }
325
326            row.add_child(
327                Empty::new()
328                    .contained()
329                    .with_border(theme.workspace.tab.container.border)
330                    .flexible(0., true)
331                    .named("filler"),
332            );
333
334            row.boxed()
335        });
336
337        ConstrainedBox::new(tabs.boxed())
338            .with_height(theme.workspace.tab.height)
339            .named("tabs")
340    }
341}
342
343impl Entity for Pane {
344    type Event = Event;
345}
346
347impl View for Pane {
348    fn ui_name() -> &'static str {
349        "Pane"
350    }
351
352    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
353        if let Some(active_item) = self.active_item() {
354            Flex::column()
355                .with_child(self.render_tabs(cx))
356                .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
357                .named("pane")
358        } else {
359            Empty::new().named("pane")
360        }
361    }
362
363    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
364        self.focus_active_item(cx);
365    }
366}