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