indent_guides.rs

  1use std::{cmp::Ordering, ops::Range, rc::Rc};
  2
  3use gpui::{
  4    AnyElement, App, Bounds, Entity, Hsla, Point, UniformListDecoration, fill, point, size,
  5};
  6use smallvec::SmallVec;
  7
  8use crate::prelude::*;
  9
 10/// Represents the colors used for different states of indent guides.
 11#[derive(Debug, Clone)]
 12pub struct IndentGuideColors {
 13    /// The color of the indent guide when it's neither active nor hovered.
 14    pub default: Hsla,
 15    /// The color of the indent guide when it's hovered.
 16    pub hover: Hsla,
 17    /// The color of the indent guide when it's active.
 18    pub active: Hsla,
 19}
 20
 21impl IndentGuideColors {
 22    /// Returns the indent guide colors that should be used for panels.
 23    pub fn panel(cx: &App) -> Self {
 24        Self {
 25            default: cx.theme().colors().panel_indent_guide,
 26            hover: cx.theme().colors().panel_indent_guide_hover,
 27            active: cx.theme().colors().panel_indent_guide_active,
 28        }
 29    }
 30}
 31
 32pub struct IndentGuides {
 33    colors: IndentGuideColors,
 34    indent_size: Pixels,
 35    compute_indents_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[usize; 64]>>,
 36    render_fn: Option<
 37        Box<
 38            dyn Fn(
 39                RenderIndentGuideParams,
 40                &mut Window,
 41                &mut App,
 42            ) -> SmallVec<[RenderedIndentGuide; 12]>,
 43        >,
 44    >,
 45    on_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
 46}
 47
 48pub fn indent_guides<V: Render>(
 49    entity: Entity<V>,
 50    indent_size: Pixels,
 51    colors: IndentGuideColors,
 52    compute_indents_fn: impl Fn(
 53        &mut V,
 54        Range<usize>,
 55        &mut Window,
 56        &mut Context<V>,
 57    ) -> SmallVec<[usize; 64]>
 58    + 'static,
 59) -> IndentGuides {
 60    let compute_indents_fn = Box::new(move |range, window: &mut Window, cx: &mut App| {
 61        entity.update(cx, |this, cx| compute_indents_fn(this, range, window, cx))
 62    });
 63    IndentGuides {
 64        colors,
 65        indent_size,
 66        compute_indents_fn,
 67        render_fn: None,
 68        on_click: None,
 69    }
 70}
 71
 72impl IndentGuides {
 73    /// Sets the callback that will be called when the user clicks on an indent guide.
 74    pub fn on_click(
 75        mut self,
 76        on_click: impl Fn(&IndentGuideLayout, &mut Window, &mut App) + 'static,
 77    ) -> Self {
 78        self.on_click = Some(Rc::new(on_click));
 79        self
 80    }
 81
 82    /// Sets a custom callback that will be called when the indent guides need to be rendered.
 83    pub fn with_render_fn<V: Render>(
 84        mut self,
 85        entity: Entity<V>,
 86        render_fn: impl Fn(
 87            &mut V,
 88            RenderIndentGuideParams,
 89            &mut Window,
 90            &mut App,
 91        ) -> SmallVec<[RenderedIndentGuide; 12]>
 92        + 'static,
 93    ) -> Self {
 94        let render_fn = move |params, window: &mut Window, cx: &mut App| {
 95            entity.update(cx, |this, cx| render_fn(this, params, window, cx))
 96        };
 97        self.render_fn = Some(Box::new(render_fn));
 98        self
 99    }
100}
101
102/// Parameters for rendering indent guides.
103pub struct RenderIndentGuideParams {
104    /// The calculated layouts for the indent guides to be rendered.
105    pub indent_guides: SmallVec<[IndentGuideLayout; 12]>,
106    /// The size of each indentation level in pixels.
107    pub indent_size: Pixels,
108    /// The height of each item in pixels.
109    pub item_height: Pixels,
110}
111
112/// Represents a rendered indent guide with its visual properties and interaction areas.
113pub struct RenderedIndentGuide {
114    /// The bounds of the rendered indent guide in pixels.
115    pub bounds: Bounds<Pixels>,
116    /// The layout information for the indent guide.
117    pub layout: IndentGuideLayout,
118    /// Indicates whether the indent guide is currently active.
119    pub is_active: bool,
120    /// Can be used to customize the hitbox of the indent guide,
121    /// if this is set to `None`, the bounds of the indent guide will be used.
122    pub hitbox: Option<Bounds<Pixels>>,
123}
124
125/// Represents the layout information for an indent guide.
126#[derive(Debug, PartialEq, Eq, Hash)]
127pub struct IndentGuideLayout {
128    /// The starting position of the indent guide, where x is the indentation level
129    /// and y is the starting row.
130    pub offset: Point<usize>,
131    /// The length of the indent guide in rows.
132    pub length: usize,
133    /// Indicates whether the indent guide continues beyond the visible bounds.
134    pub continues_offscreen: bool,
135}
136
137/// Implements the necessary functionality for rendering indent guides inside a uniform list.
138mod uniform_list {
139    use gpui::{
140        DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent,
141    };
142
143    use super::*;
144
145    impl UniformListDecoration for IndentGuides {
146        fn compute(
147            &self,
148            visible_range: Range<usize>,
149            bounds: Bounds<Pixels>,
150            item_height: Pixels,
151            item_count: usize,
152            window: &mut Window,
153            cx: &mut App,
154        ) -> AnyElement {
155            let mut visible_range = visible_range.clone();
156            let includes_trailing_indent = visible_range.end < item_count;
157            // Check if we have entries after the visible range,
158            // if so extend the visible range so we can fetch a trailing indent,
159            // which is needed to compute indent guides correctly.
160            if includes_trailing_indent {
161                visible_range.end += 1;
162            }
163            let visible_entries = &(self.compute_indents_fn)(visible_range.clone(), window, cx);
164            let indent_guides = compute_indent_guides(
165                &visible_entries,
166                visible_range.start,
167                includes_trailing_indent,
168            );
169            let mut indent_guides = if let Some(ref custom_render) = self.render_fn {
170                let params = RenderIndentGuideParams {
171                    indent_guides,
172                    indent_size: self.indent_size,
173                    item_height,
174                };
175                custom_render(params, window, cx)
176            } else {
177                indent_guides
178                    .into_iter()
179                    .map(|layout| RenderedIndentGuide {
180                        bounds: Bounds::new(
181                            point(
182                                layout.offset.x * self.indent_size,
183                                layout.offset.y * item_height,
184                            ),
185                            size(px(1.), layout.length * item_height),
186                        ),
187                        layout,
188                        is_active: false,
189                        hitbox: None,
190                    })
191                    .collect()
192            };
193            for guide in &mut indent_guides {
194                guide.bounds.origin += bounds.origin;
195                if let Some(hitbox) = guide.hitbox.as_mut() {
196                    hitbox.origin += bounds.origin;
197                }
198            }
199
200            let indent_guides = IndentGuidesElement {
201                indent_guides: Rc::new(indent_guides),
202                colors: self.colors.clone(),
203                on_hovered_indent_guide_click: self.on_click.clone(),
204            };
205            indent_guides.into_any_element()
206        }
207    }
208
209    struct IndentGuidesElement {
210        colors: IndentGuideColors,
211        indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>,
212        on_hovered_indent_guide_click:
213            Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
214    }
215
216    enum IndentGuidesElementPrepaintState {
217        Static,
218        Interactive {
219            hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
220            on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
221        },
222    }
223
224    impl Element for IndentGuidesElement {
225        type RequestLayoutState = ();
226        type PrepaintState = IndentGuidesElementPrepaintState;
227
228        fn id(&self) -> Option<ElementId> {
229            None
230        }
231
232        fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
233            None
234        }
235
236        fn request_layout(
237            &mut self,
238            _id: Option<&gpui::GlobalElementId>,
239            _inspector_id: Option<&gpui::InspectorElementId>,
240            window: &mut Window,
241            cx: &mut App,
242        ) -> (gpui::LayoutId, Self::RequestLayoutState) {
243            (window.request_layout(gpui::Style::default(), [], cx), ())
244        }
245
246        fn prepaint(
247            &mut self,
248            _id: Option<&gpui::GlobalElementId>,
249            _inspector_id: Option<&gpui::InspectorElementId>,
250            _bounds: Bounds<Pixels>,
251            _request_layout: &mut Self::RequestLayoutState,
252            window: &mut Window,
253            _cx: &mut App,
254        ) -> Self::PrepaintState {
255            if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone()
256            {
257                let hitboxes = self
258                    .indent_guides
259                    .as_ref()
260                    .iter()
261                    .map(|guide| {
262                        window.insert_hitbox(
263                            guide.hitbox.unwrap_or(guide.bounds),
264                            HitboxBehavior::Normal,
265                        )
266                    })
267                    .collect();
268                Self::PrepaintState::Interactive {
269                    hitboxes: Rc::new(hitboxes),
270                    on_hovered_indent_guide_click,
271                }
272            } else {
273                Self::PrepaintState::Static
274            }
275        }
276
277        fn paint(
278            &mut self,
279            _id: Option<&gpui::GlobalElementId>,
280            _inspector_id: Option<&gpui::InspectorElementId>,
281            _bounds: Bounds<Pixels>,
282            _request_layout: &mut Self::RequestLayoutState,
283            prepaint: &mut Self::PrepaintState,
284            window: &mut Window,
285            _cx: &mut App,
286        ) {
287            let current_view = window.current_view();
288
289            match prepaint {
290                IndentGuidesElementPrepaintState::Static => {
291                    for indent_guide in self.indent_guides.as_ref() {
292                        let fill_color = if indent_guide.is_active {
293                            self.colors.active
294                        } else {
295                            self.colors.default
296                        };
297
298                        window.paint_quad(fill(indent_guide.bounds, fill_color));
299                    }
300                }
301                IndentGuidesElementPrepaintState::Interactive {
302                    hitboxes,
303                    on_hovered_indent_guide_click,
304                } => {
305                    window.on_mouse_event({
306                        let hitboxes = hitboxes.clone();
307                        let indent_guides = self.indent_guides.clone();
308                        let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone();
309                        move |event: &MouseDownEvent, phase, window, cx| {
310                            if phase == DispatchPhase::Bubble && event.button == MouseButton::Left {
311                                let mut active_hitbox_ix = None;
312                                for (i, hitbox) in hitboxes.iter().enumerate() {
313                                    if hitbox.is_hovered(window) {
314                                        active_hitbox_ix = Some(i);
315                                        break;
316                                    }
317                                }
318
319                                let Some(active_hitbox_ix) = active_hitbox_ix else {
320                                    return;
321                                };
322
323                                let active_indent_guide = &indent_guides[active_hitbox_ix].layout;
324                                on_hovered_indent_guide_click(active_indent_guide, window, cx);
325
326                                cx.stop_propagation();
327                                window.prevent_default();
328                            }
329                        }
330                    });
331                    let mut hovered_hitbox_id = None;
332                    for (i, hitbox) in hitboxes.iter().enumerate() {
333                        window.set_cursor_style(gpui::CursorStyle::PointingHand, Some(hitbox));
334                        let indent_guide = &self.indent_guides[i];
335                        let fill_color = if hitbox.is_hovered(window) {
336                            hovered_hitbox_id = Some(hitbox.id);
337                            self.colors.hover
338                        } else if indent_guide.is_active {
339                            self.colors.active
340                        } else {
341                            self.colors.default
342                        };
343
344                        window.paint_quad(fill(indent_guide.bounds, fill_color));
345                    }
346
347                    window.on_mouse_event({
348                        let prev_hovered_hitbox_id = hovered_hitbox_id;
349                        let hitboxes = hitboxes.clone();
350                        move |_: &MouseMoveEvent, phase, window, cx| {
351                            let mut hovered_hitbox_id = None;
352                            for hitbox in hitboxes.as_ref() {
353                                if hitbox.is_hovered(window) {
354                                    hovered_hitbox_id = Some(hitbox.id);
355                                    break;
356                                }
357                            }
358                            if phase == DispatchPhase::Capture {
359                                // If the hovered hitbox has changed, we need to re-paint the indent guides.
360                                match (prev_hovered_hitbox_id, hovered_hitbox_id) {
361                                    (Some(prev_id), Some(id)) => {
362                                        if prev_id != id {
363                                            cx.notify(current_view)
364                                        }
365                                    }
366                                    (None, Some(_)) => cx.notify(current_view),
367                                    (Some(_), None) => cx.notify(current_view),
368                                    (None, None) => {}
369                                }
370                            }
371                        }
372                    });
373                }
374            }
375        }
376    }
377
378    impl IntoElement for IndentGuidesElement {
379        type Element = Self;
380
381        fn into_element(self) -> Self::Element {
382            self
383        }
384    }
385}
386
387fn compute_indent_guides(
388    indents: &[usize],
389    offset: usize,
390    includes_trailing_indent: bool,
391) -> SmallVec<[IndentGuideLayout; 12]> {
392    let mut indent_guides = SmallVec::<[IndentGuideLayout; 12]>::new();
393    let mut indent_stack = SmallVec::<[IndentGuideLayout; 8]>::new();
394
395    let mut min_depth = usize::MAX;
396    for (row, &depth) in indents.iter().enumerate() {
397        if includes_trailing_indent && row == indents.len() - 1 {
398            continue;
399        }
400
401        let current_row = row + offset;
402        let current_depth = indent_stack.len();
403        if depth < min_depth {
404            min_depth = depth;
405        }
406
407        match depth.cmp(&current_depth) {
408            Ordering::Less => {
409                for _ in 0..(current_depth - depth) {
410                    if let Some(guide) = indent_stack.pop() {
411                        indent_guides.push(guide);
412                    }
413                }
414            }
415            Ordering::Greater => {
416                for new_depth in current_depth..depth {
417                    indent_stack.push(IndentGuideLayout {
418                        offset: Point::new(new_depth, current_row),
419                        length: current_row,
420                        continues_offscreen: false,
421                    });
422                }
423            }
424            _ => {}
425        }
426
427        for indent in indent_stack.iter_mut() {
428            indent.length = current_row - indent.offset.y + 1;
429        }
430    }
431
432    indent_guides.extend(indent_stack);
433
434    for guide in indent_guides.iter_mut() {
435        if includes_trailing_indent
436            && guide.offset.y + guide.length == offset + indents.len().saturating_sub(1)
437        {
438            guide.continues_offscreen = indents
439                .last()
440                .map(|last_indent| guide.offset.x < *last_indent)
441                .unwrap_or(false);
442        }
443    }
444
445    indent_guides
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn test_compute_indent_guides() {
454        fn assert_compute_indent_guides(
455            input: &[usize],
456            offset: usize,
457            includes_trailing_indent: bool,
458            expected: Vec<IndentGuideLayout>,
459        ) {
460            use std::collections::HashSet;
461            assert_eq!(
462                compute_indent_guides(input, offset, includes_trailing_indent)
463                    .into_vec()
464                    .into_iter()
465                    .collect::<HashSet<_>>(),
466                expected.into_iter().collect::<HashSet<_>>(),
467            );
468        }
469
470        assert_compute_indent_guides(
471            &[0, 1, 2, 2, 1, 0],
472            0,
473            false,
474            vec![
475                IndentGuideLayout {
476                    offset: Point::new(0, 1),
477                    length: 4,
478                    continues_offscreen: false,
479                },
480                IndentGuideLayout {
481                    offset: Point::new(1, 2),
482                    length: 2,
483                    continues_offscreen: false,
484                },
485            ],
486        );
487
488        assert_compute_indent_guides(
489            &[2, 2, 2, 1, 1],
490            0,
491            false,
492            vec![
493                IndentGuideLayout {
494                    offset: Point::new(0, 0),
495                    length: 5,
496                    continues_offscreen: false,
497                },
498                IndentGuideLayout {
499                    offset: Point::new(1, 0),
500                    length: 3,
501                    continues_offscreen: false,
502                },
503            ],
504        );
505
506        assert_compute_indent_guides(
507            &[1, 2, 3, 2, 1],
508            0,
509            false,
510            vec![
511                IndentGuideLayout {
512                    offset: Point::new(0, 0),
513                    length: 5,
514                    continues_offscreen: false,
515                },
516                IndentGuideLayout {
517                    offset: Point::new(1, 1),
518                    length: 3,
519                    continues_offscreen: false,
520                },
521                IndentGuideLayout {
522                    offset: Point::new(2, 2),
523                    length: 1,
524                    continues_offscreen: false,
525                },
526            ],
527        );
528
529        assert_compute_indent_guides(
530            &[0, 1, 0],
531            0,
532            true,
533            vec![IndentGuideLayout {
534                offset: Point::new(0, 1),
535                length: 1,
536                continues_offscreen: false,
537            }],
538        );
539
540        assert_compute_indent_guides(
541            &[0, 1, 1],
542            0,
543            true,
544            vec![IndentGuideLayout {
545                offset: Point::new(0, 1),
546                length: 1,
547                continues_offscreen: true,
548            }],
549        );
550        assert_compute_indent_guides(
551            &[0, 1, 2],
552            0,
553            true,
554            vec![IndentGuideLayout {
555                offset: Point::new(0, 1),
556                length: 1,
557                continues_offscreen: true,
558            }],
559        );
560    }
561}