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    AppContext, Border, Entity, MutableAppContext, Quad, View, ViewContext,
  9};
 10use std::cmp;
 11
 12pub fn init(app: &mut MutableAppContext) {
 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(&mut self, entry_id: (usize, u64), ctx: &mut ViewContext<Self>) -> bool {
109        if let Some(index) = self
110            .items
111            .iter()
112            .position(|item| item.entry_id(ctx.as_ref()).map_or(false, |id| id == entry_id))
113        {
114            self.activate_item(index, ctx);
115            true
116        } else {
117            false
118        }
119    }
120
121    pub fn item_index(&self, item: &dyn ItemViewHandle) -> Option<usize> {
122        self.items.iter().position(|i| i.id() == item.id())
123    }
124
125    pub fn activate_item(&mut self, index: usize, ctx: &mut ViewContext<Self>) {
126        if index < self.items.len() {
127            self.active_item = index;
128            self.focus_active_item(ctx);
129            ctx.notify();
130        }
131    }
132
133    pub fn activate_prev_item(&mut self, ctx: &mut ViewContext<Self>) {
134        if self.active_item > 0 {
135            self.active_item -= 1;
136        } else {
137            self.active_item = self.items.len() - 1;
138        }
139        self.focus_active_item(ctx);
140        ctx.notify();
141    }
142
143    pub fn activate_next_item(&mut self, ctx: &mut ViewContext<Self>) {
144        if self.active_item + 1 < self.items.len() {
145            self.active_item += 1;
146        } else {
147            self.active_item = 0;
148        }
149        self.focus_active_item(ctx);
150        ctx.notify();
151    }
152
153    pub fn close_active_item(&mut self, ctx: &mut ViewContext<Self>) {
154        if !self.items.is_empty() {
155            self.items.remove(self.active_item);
156            if self.active_item >= self.items.len() {
157                self.active_item = self.items.len().saturating_sub(1);
158            }
159            ctx.notify();
160        }
161        if self.items.is_empty() {
162            ctx.emit(Event::Remove);
163        }
164    }
165
166    fn focus_active_item(&mut self, ctx: &mut ViewContext<Self>) {
167        if let Some(active_item) = self.active_item() {
168            ctx.focus(active_item.to_any());
169        }
170    }
171
172    pub fn split(&mut self, direction: SplitDirection, ctx: &mut ViewContext<Self>) {
173        ctx.emit(Event::Split(direction));
174    }
175
176    fn render_tabs(&self, app: &AppContext) -> ElementBox {
177        let settings = smol::block_on(self.settings.read());
178        let border_color = ColorU::from_u32(0xdbdbdcff);
179
180        let mut row = Flex::row();
181        let last_item_ix = self.items.len() - 1;
182        for (ix, item) in self.items.iter().enumerate() {
183            let title = item.title(app);
184
185            let mut border = Border::new(1.0, border_color);
186            border.left = ix > 0;
187            border.right = ix == last_item_ix;
188            border.bottom = ix != self.active_item;
189
190            let padding = 6.;
191            let mut container = Container::new(
192                Stack::new()
193                    .with_child(
194                        Align::new(
195                            Label::new(title, settings.ui_font_family, settings.ui_font_size)
196                                .boxed(),
197                        )
198                        .boxed(),
199                    )
200                    .with_child(
201                        LineBox::new(
202                            settings.ui_font_family,
203                            settings.ui_font_size,
204                            Align::new(Self::render_modified_icon(item.is_dirty(app)))
205                                .right()
206                                .boxed(),
207                        )
208                        .boxed(),
209                    )
210                    .boxed(),
211            )
212            .with_vertical_padding(padding)
213            .with_horizontal_padding(10.)
214            .with_border(border);
215
216            if ix == self.active_item {
217                container = container
218                    .with_background_color(ColorU::white())
219                    .with_padding_bottom(padding + border.width);
220            } else {
221                container = container.with_background_color(ColorU::from_u32(0xeaeaebff));
222            }
223
224            row.add_child(
225                Expanded::new(
226                    1.0,
227                    ConstrainedBox::new(
228                        EventHandler::new(container.boxed())
229                            .on_mouse_down(move |ctx| {
230                                ctx.dispatch_action("pane:activate_item", ix);
231                                true
232                            })
233                            .boxed(),
234                    )
235                    .with_min_width(80.0)
236                    .with_max_width(264.0)
237                    .boxed(),
238                )
239                .named("tab"),
240            );
241        }
242
243        // Ensure there's always a minimum amount of space after the last tab,
244        // so that the tab's border doesn't abut the window's border.
245        row.add_child(
246            ConstrainedBox::new(
247                Container::new(
248                    LineBox::new(
249                        settings.ui_font_family,
250                        settings.ui_font_size,
251                        Empty::new().boxed(),
252                    )
253                    .boxed(),
254                )
255                .with_uniform_padding(6.0)
256                .with_border(Border::bottom(1.0, border_color))
257                .boxed(),
258            )
259            .with_min_width(20.)
260            .named("fixed-filler"),
261        );
262
263        row.add_child(
264            Expanded::new(
265                0.0,
266                Container::new(
267                    LineBox::new(
268                        settings.ui_font_family,
269                        settings.ui_font_size,
270                        Empty::new().boxed(),
271                    )
272                    .boxed(),
273                )
274                .with_uniform_padding(6.0)
275                .with_border(Border::bottom(1.0, border_color))
276                .boxed(),
277            )
278            .named("filler"),
279        );
280
281        row.named("tabs")
282    }
283
284    fn render_modified_icon(is_modified: bool) -> ElementBox {
285        let diameter = 8.;
286        ConstrainedBox::new(
287            Canvas::new(move |bounds, ctx| {
288                if is_modified {
289                    let square = RectF::new(bounds.origin(), vec2f(diameter, diameter));
290                    ctx.scene.push_quad(Quad {
291                        bounds: square,
292                        background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()),
293                        border: Default::default(),
294                        corner_radius: diameter / 2.,
295                    });
296                }
297            })
298            .boxed(),
299        )
300        .with_width(diameter)
301        .with_height(diameter)
302        .named("tab-right-icon")
303    }
304}
305
306impl Entity for Pane {
307    type Event = Event;
308}
309
310impl View for Pane {
311    fn ui_name() -> &'static str {
312        "Pane"
313    }
314
315    fn render<'a>(&self, app: &AppContext) -> ElementBox {
316        if let Some(active_item) = self.active_item() {
317            Flex::column()
318                .with_child(self.render_tabs(app))
319                .with_child(Expanded::new(1.0, ChildView::new(active_item.id()).boxed()).boxed())
320                .named("pane")
321        } else {
322            Empty::new().named("pane")
323        }
324    }
325
326    fn on_focus(&mut self, ctx: &mut ViewContext<Self>) {
327        self.focus_active_item(ctx);
328    }
329
330    // fn state(&self, app: &AppContext) -> Self::State {
331    //     State {
332    //         tabs: self
333    //             .items
334    //             .iter()
335    //             .enumerate()
336    //             .map(|(idx, item)| TabState {
337    //                 title: item.title(app),
338    //                 active: idx == self.active_item,
339    //             })
340    //             .collect(),
341    //     }
342    // }
343}