pane.rs

  1use super::{ItemViewHandle, SplitDirection};
  2use crate::{settings::Settings, watch};
  3use gpui::{
  4    color::{ColorF, ColorU},
  5    elements::*,
  6    geometry::{rect::RectF, vector::vec2f},
  7    keymap::Binding,
  8    App, AppContext, Border, Entity, Quad, View, ViewContext,
  9};
 10use std::cmp;
 11
 12pub fn init(app: &mut App) {
 13    app.add_action(
 14        "pane:activate_item",
 15        |pane: &mut Pane, index: &usize, ctx| {
 16            pane.activate_item(*index, ctx);
 17        },
 18    );
 19    app.add_action("pane:activate_prev_item", |pane: &mut Pane, _: &(), ctx| {
 20        pane.activate_prev_item(ctx);
 21    });
 22    app.add_action("pane:activate_next_item", |pane: &mut Pane, _: &(), ctx| {
 23        pane.activate_next_item(ctx);
 24    });
 25    app.add_action("pane:close_active_item", |pane: &mut Pane, _: &(), ctx| {
 26        pane.close_active_item(ctx);
 27    });
 28    app.add_action("pane:split_up", |pane: &mut Pane, _: &(), ctx| {
 29        pane.split(SplitDirection::Up, ctx);
 30    });
 31    app.add_action("pane:split_down", |pane: &mut Pane, _: &(), ctx| {
 32        pane.split(SplitDirection::Down, ctx);
 33    });
 34    app.add_action("pane:split_left", |pane: &mut Pane, _: &(), ctx| {
 35        pane.split(SplitDirection::Left, ctx);
 36    });
 37    app.add_action("pane:split_right", |pane: &mut Pane, _: &(), ctx| {
 38        pane.split(SplitDirection::Right, ctx);
 39    });
 40
 41    app.add_bindings(vec![
 42        Binding::new("shift-cmd-{", "pane:activate_prev_item", Some("Pane")),
 43        Binding::new("shift-cmd-}", "pane:activate_next_item", Some("Pane")),
 44        Binding::new("cmd-w", "pane:close_active_item", Some("Pane")),
 45        Binding::new("cmd-k up", "pane:split_up", Some("Pane")),
 46        Binding::new("cmd-k down", "pane:split_down", Some("Pane")),
 47        Binding::new("cmd-k left", "pane:split_left", Some("Pane")),
 48        Binding::new("cmd-k right", "pane:split_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, ctx: &mut ViewContext<Self>) {
 85        ctx.emit(Event::Activate);
 86    }
 87
 88    pub fn add_item(
 89        &mut self,
 90        item: Box<dyn ItemViewHandle>,
 91        ctx: &mut ViewContext<Self>,
 92    ) -> usize {
 93        let item_idx = cmp::min(self.active_item + 1, self.items.len());
 94        self.items.insert(item_idx, item);
 95        ctx.notify();
 96        item_idx
 97    }
 98
 99    #[cfg(test)]
100    pub fn items(&self) -> &[Box<dyn ItemViewHandle>] {
101        &self.items
102    }
103
104    pub fn active_item(&self) -> Option<Box<dyn ItemViewHandle>> {
105        self.items.get(self.active_item).cloned()
106    }
107
108    pub fn activate_entry(
109        &mut self,
110        entry_id: (usize, usize),
111        ctx: &mut ViewContext<Self>,
112    ) -> bool {
113        if let Some(index) = self
114            .items
115            .iter()
116            .position(|item| item.entry_id(ctx.app()).map_or(false, |id| id == entry_id))
117        {
118            self.activate_item(index, ctx);
119            true
120        } else {
121            false
122        }
123    }
124
125    pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
126        self.items.iter().position(|i| i.id() == item.id())
127    }
128
129    pub fn activate_item(&mut self, index: usize, ctx: &mut ViewContext<Self>) {
130        if index < self.items.len() {
131            self.active_item = index;
132            self.focus_active_item(ctx);
133            ctx.notify();
134        }
135    }
136
137    pub fn activate_prev_item(&mut self, ctx: &mut ViewContext<Self>) {
138        if self.active_item > 0 {
139            self.active_item -= 1;
140        } else {
141            self.active_item = self.items.len() - 1;
142        }
143        self.focus_active_item(ctx);
144        ctx.notify();
145    }
146
147    pub fn activate_next_item(&mut self, ctx: &mut ViewContext<Self>) {
148        if self.active_item + 1 < self.items.len() {
149            self.active_item += 1;
150        } else {
151            self.active_item = 0;
152        }
153        self.focus_active_item(ctx);
154        ctx.notify();
155    }
156
157    pub fn close_active_item(&mut self, ctx: &mut ViewContext<Self>) {
158        if !self.items.is_empty() {
159            self.items.remove(self.active_item);
160            if self.active_item >= self.items.len() {
161                self.active_item = self.items.len().saturating_sub(1);
162            }
163            ctx.notify();
164        }
165        if self.items.is_empty() {
166            ctx.emit(Event::Remove);
167        }
168    }
169
170    fn focus_active_item(&mut self, ctx: &mut ViewContext<Self>) {
171        if let Some(active_item) = self.active_item() {
172            ctx.focus(active_item.to_any());
173        }
174    }
175
176    pub fn split(&mut self, direction: SplitDirection, ctx: &mut ViewContext<Self>) {
177        ctx.emit(Event::Split(direction));
178    }
179
180    fn render_tabs(&self, app: &AppContext) -> ElementBox {
181        let settings = smol::block_on(self.settings.read());
182        let border_color = ColorU::from_u32(0xdbdbdcff);
183
184        let mut row = Flex::row();
185        let last_item_ix = self.items.len() - 1;
186        for (ix, item) in self.items.iter().enumerate() {
187            let title = item.title(app);
188
189            let mut border = Border::new(1.0, border_color);
190            border.left = ix > 0;
191            border.right = ix == last_item_ix;
192            border.bottom = ix != self.active_item;
193
194            let padding = 6.;
195            let mut container = Container::new(
196                Align::new(
197                    Flex::row()
198                        .with_child(
199                            Label::new(title, settings.ui_font_family, settings.ui_font_size)
200                                .boxed(),
201                        )
202                        .with_child(
203                            Container::new(
204                                LineBox::new(
205                                    settings.ui_font_family,
206                                    settings.ui_font_size,
207                                    ConstrainedBox::new(Self::render_modified_icon(
208                                        item.is_dirty(app),
209                                    ))
210                                    .with_max_width(12.)
211                                    .boxed(),
212                                )
213                                .boxed(),
214                            )
215                            .with_margin_left(20.)
216                            .boxed(),
217                        )
218                        .boxed(),
219                )
220                .boxed(),
221            )
222            .with_uniform_padding(padding)
223            .with_border(border);
224
225            if ix == self.active_item {
226                container = container
227                    .with_background_color(ColorU::white())
228                    .with_padding_bottom(padding + border.width);
229            } else {
230                container = container.with_background_color(ColorU::from_u32(0xeaeaebff));
231            }
232
233            row.add_child(
234                Expanded::new(
235                    1.0,
236                    ConstrainedBox::new(
237                        EventHandler::new(container.boxed())
238                            .on_mouse_down(move |ctx| {
239                                ctx.dispatch_action("pane:activate_item", ix);
240                                true
241                            })
242                            .boxed(),
243                    )
244                    .with_max_width(264.0)
245                    .boxed(),
246                )
247                .boxed(),
248            );
249        }
250
251        row.add_child(
252            Expanded::new(
253                1.0,
254                Container::new(
255                    LineBox::new(
256                        settings.ui_font_family,
257                        settings.ui_font_size,
258                        Empty::new().boxed(),
259                    )
260                    .boxed(),
261                )
262                .with_uniform_padding(6.0)
263                .with_border(Border::bottom(1.0, border_color))
264                .boxed(),
265            )
266            .boxed(),
267        );
268
269        row.boxed()
270    }
271
272    fn render_modified_icon(is_modified: bool) -> ElementBox {
273        Canvas::new(move |bounds, ctx| {
274            if is_modified {
275                let padding = if bounds.height() < bounds.width() {
276                    vec2f(bounds.width() - bounds.height(), 0.0)
277                } else {
278                    vec2f(0.0, bounds.height() - bounds.width())
279                };
280                let square = RectF::new(bounds.origin() + padding / 2., bounds.size() - padding);
281                ctx.scene.push_quad(Quad {
282                    bounds: square,
283                    background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()),
284                    border: Default::default(),
285                    corner_radius: square.width() / 2.,
286                });
287            }
288        })
289        .boxed()
290    }
291}
292
293impl Entity for Pane {
294    type Event = Event;
295}
296
297impl View for Pane {
298    fn ui_name() -> &'static str {
299        "Pane"
300    }
301
302    fn render<'a>(&self, app: &AppContext) -> ElementBox {
303        if let Some(active_item) = self.active_item() {
304            Flex::column()
305                .with_child(self.render_tabs(app))
306                .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
307                .boxed()
308        } else {
309            Empty::new().boxed()
310        }
311    }
312
313    fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
314        self.focus_active_item(ctx);
315    }
316
317    // fn state(&self, app: &AppContext) -> Self::State {
318    //     State {
319    //         tabs: self
320    //             .items
321    //             .iter()
322    //             .enumerate()
323    //             .map(|(idx, item)| TabState {
324    //                 title: item.title(app),
325    //                 active: idx == self.active_item,
326    //             })
327    //             .collect(),
328    //     }
329    // }
330}