indent_guides.rs

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