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<(usize, 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_id, item_view)) in self.item_views.iter().enumerate() {
100            if *item_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
120            .insert(item_idx, (item_view.item_handle(cx).id(), item_view));
121        self.activate_item(item_idx, cx);
122        cx.notify();
123    }
124
125    pub fn contains_item(&self, item: &dyn ItemHandle) -> bool {
126        let item_id = item.id();
127        self.item_views
128            .iter()
129            .any(|(existing_item_id, _)| *existing_item_id == item_id)
130    }
131
132    pub fn item_views(&self) -> impl Iterator<Item = &Box<dyn ItemViewHandle>> {
133        self.item_views.iter().map(|(_, view)| view)
134    }
135
136    pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
137        self.item_views
138            .get(self.active_item)
139            .map(|(_, view)| view.clone())
140    }
141
142    pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option<usize> {
143        self.item_views
144            .iter()
145            .position(|(_, i)| i.id() == item_view.id())
146    }
147
148    pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
149        self.item_views.iter().position(|(id, _)| *id == item.id())
150    }
151
152    pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext<Self>) {
153        if index < self.item_views.len() {
154            self.active_item = index;
155            self.focus_active_item(cx);
156            cx.notify();
157        }
158    }
159
160    pub fn activate_prev_item(&mut self, cx: &mut ViewContext<Self>) {
161        if self.active_item > 0 {
162            self.active_item -= 1;
163        } else if self.item_views.len() > 0 {
164            self.active_item = self.item_views.len() - 1;
165        }
166        self.focus_active_item(cx);
167        cx.notify();
168    }
169
170    pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
171        if self.active_item + 1 < self.item_views.len() {
172            self.active_item += 1;
173        } else {
174            self.active_item = 0;
175        }
176        self.focus_active_item(cx);
177        cx.notify();
178    }
179
180    pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
181        if !self.item_views.is_empty() {
182            self.close_item(self.item_views[self.active_item].1.id(), cx)
183        }
184    }
185
186    pub fn close_item(&mut self, item_id: usize, cx: &mut ViewContext<Self>) {
187        self.item_views.retain(|(_, item)| item.id() != item_id);
188        self.active_item = cmp::min(self.active_item, self.item_views.len().saturating_sub(1));
189        if self.item_views.is_empty() {
190            cx.emit(Event::Remove);
191        }
192        cx.notify();
193    }
194
195    fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
196        if let Some(active_item) = self.active_item() {
197            cx.focus(active_item.to_any());
198        }
199    }
200
201    pub fn split(&mut self, direction: SplitDirection, cx: &mut ViewContext<Self>) {
202        cx.emit(Event::Split(direction));
203    }
204
205    fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
206        let settings = self.settings.borrow();
207        let theme = &settings.theme;
208
209        enum Tabs {}
210        let tabs = MouseEventHandler::new::<Tabs, _, _, _>(cx.view_id(), cx, |mouse_state, cx| {
211            let mut row = Flex::row();
212            for (ix, (_, item_view)) in self.item_views.iter().enumerate() {
213                let is_active = ix == self.active_item;
214
215                row.add_child({
216                    let mut title = item_view.title(cx);
217                    if title.len() > MAX_TAB_TITLE_LEN {
218                        let mut truncated_len = MAX_TAB_TITLE_LEN;
219                        while !title.is_char_boundary(truncated_len) {
220                            truncated_len -= 1;
221                        }
222                        title.truncate(truncated_len);
223                        title.push('…');
224                    }
225
226                    let mut style = if is_active {
227                        theme.workspace.active_tab.clone()
228                    } else {
229                        theme.workspace.tab.clone()
230                    };
231                    if ix == 0 {
232                        style.container.border.left = false;
233                    }
234
235                    EventHandler::new(
236                        Container::new(
237                            Flex::row()
238                                .with_child(
239                                    Align::new({
240                                        let diameter = 7.0;
241                                        let icon_color = if item_view.has_conflict(cx) {
242                                            Some(style.icon_conflict)
243                                        } else if item_view.is_dirty(cx) {
244                                            Some(style.icon_dirty)
245                                        } else {
246                                            None
247                                        };
248
249                                        ConstrainedBox::new(
250                                            Canvas::new(move |bounds, _, cx| {
251                                                if let Some(color) = icon_color {
252                                                    let square = RectF::new(
253                                                        bounds.origin(),
254                                                        vec2f(diameter, diameter),
255                                                    );
256                                                    cx.scene.push_quad(Quad {
257                                                        bounds: square,
258                                                        background: Some(color),
259                                                        border: Default::default(),
260                                                        corner_radius: diameter / 2.,
261                                                    });
262                                                }
263                                            })
264                                            .boxed(),
265                                        )
266                                        .with_width(diameter)
267                                        .with_height(diameter)
268                                        .boxed()
269                                    })
270                                    .boxed(),
271                                )
272                                .with_child(
273                                    Container::new(
274                                        Align::new(
275                                            Label::new(
276                                                title,
277                                                if is_active {
278                                                    theme.workspace.active_tab.label.clone()
279                                                } else {
280                                                    theme.workspace.tab.label.clone()
281                                                },
282                                            )
283                                            .boxed(),
284                                        )
285                                        .boxed(),
286                                    )
287                                    .with_style(ContainerStyle {
288                                        margin: Margin {
289                                            left: style.spacing,
290                                            right: style.spacing,
291                                            ..Default::default()
292                                        },
293                                        ..Default::default()
294                                    })
295                                    .boxed(),
296                                )
297                                .with_child(
298                                    Align::new(
299                                        ConstrainedBox::new(if mouse_state.hovered {
300                                            let item_id = item_view.id();
301                                            enum TabCloseButton {}
302                                            let icon = Svg::new("icons/x.svg");
303                                            MouseEventHandler::new::<TabCloseButton, _, _, _>(
304                                                item_id,
305                                                cx,
306                                                |mouse_state, _| {
307                                                    if mouse_state.hovered {
308                                                        icon.with_color(style.icon_close_active)
309                                                            .boxed()
310                                                    } else {
311                                                        icon.with_color(style.icon_close).boxed()
312                                                    }
313                                                },
314                                            )
315                                            .with_padding(Padding::uniform(4.))
316                                            .with_cursor_style(CursorStyle::PointingHand)
317                                            .on_click(move |cx| {
318                                                cx.dispatch_action(CloseItem(item_id))
319                                            })
320                                            .named("close-tab-icon")
321                                        } else {
322                                            Empty::new().boxed()
323                                        })
324                                        .with_width(style.icon_width)
325                                        .boxed(),
326                                    )
327                                    .boxed(),
328                                )
329                                .boxed(),
330                        )
331                        .with_style(style.container)
332                        .boxed(),
333                    )
334                    .on_mouse_down(move |cx| {
335                        cx.dispatch_action(ActivateItem(ix));
336                        true
337                    })
338                    .boxed()
339                })
340            }
341
342            row.add_child(
343                Empty::new()
344                    .contained()
345                    .with_border(theme.workspace.tab.container.border)
346                    .flexible(0., true)
347                    .named("filler"),
348            );
349
350            row.boxed()
351        });
352
353        ConstrainedBox::new(tabs.boxed())
354            .with_height(theme.workspace.tab.height)
355            .named("tabs")
356    }
357}
358
359impl Entity for Pane {
360    type Event = Event;
361}
362
363impl View for Pane {
364    fn ui_name() -> &'static str {
365        "Pane"
366    }
367
368    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
369        if let Some(active_item) = self.active_item() {
370            Flex::column()
371                .with_child(self.render_tabs(cx))
372                .with_child(ChildView::new(active_item.id()).flexible(1., true).boxed())
373                .named("pane")
374        } else {
375            Empty::new().named("pane")
376        }
377    }
378
379    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
380        self.focus_active_item(cx);
381    }
382}