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            mut 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 includes_trailing_indent = visible_range.end < item_count;
207            // Check if we have entries after the visible range,
208            // if so extend the visible range so we can fetch a trailing indent,
209            // which is needed to compute indent guides correctly.
210            if includes_trailing_indent {
211                visible_range.end += 1;
212            }
213            let Some(ref compute_indents_fn) = self.compute_indents_fn else {
214                panic!("compute_indents_fn is required for UniformListDecoration");
215            };
216            let visible_entries = &compute_indents_fn(visible_range.clone(), window, cx);
217            let indent_guides = compute_indent_guides(
218                visible_entries,
219                visible_range.start,
220                includes_trailing_indent,
221            );
222            self.render_from_layout(indent_guides, bounds, item_height, window, cx)
223        }
224    }
225}
226
227/// Implements the necessary functionality for rendering indent guides inside a sticky items.
228mod sticky_items {
229    use crate::StickyItemsDecoration;
230
231    use super::*;
232
233    impl StickyItemsDecoration for IndentGuides {
234        fn compute(
235            &self,
236            indents: &SmallVec<[usize; 8]>,
237            bounds: Bounds<Pixels>,
238            _scroll_offset: Point<Pixels>,
239            item_height: Pixels,
240            window: &mut Window,
241            cx: &mut App,
242        ) -> AnyElement {
243            let indent_guides = compute_indent_guides(indents, 0, false);
244            self.render_from_layout(indent_guides, bounds, item_height, window, cx)
245        }
246    }
247}
248
249struct IndentGuidesElement {
250    colors: IndentGuideColors,
251    indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>,
252    on_hovered_indent_guide_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
253}
254
255enum IndentGuidesElementPrepaintState {
256    Static,
257    Interactive {
258        hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
259        on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
260    },
261}
262
263impl Element for IndentGuidesElement {
264    type RequestLayoutState = ();
265    type PrepaintState = IndentGuidesElementPrepaintState;
266
267    fn id(&self) -> Option<ElementId> {
268        None
269    }
270
271    fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
272        None
273    }
274
275    fn request_layout(
276        &mut self,
277        _id: Option<&gpui::GlobalElementId>,
278        _inspector_id: Option<&gpui::InspectorElementId>,
279        window: &mut Window,
280        cx: &mut App,
281    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
282        (window.request_layout(gpui::Style::default(), [], cx), ())
283    }
284
285    fn prepaint(
286        &mut self,
287        _id: Option<&gpui::GlobalElementId>,
288        _inspector_id: Option<&gpui::InspectorElementId>,
289        _bounds: Bounds<Pixels>,
290        _request_layout: &mut Self::RequestLayoutState,
291        window: &mut Window,
292        _cx: &mut App,
293    ) -> Self::PrepaintState {
294        if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone() {
295            let hitboxes = self
296                .indent_guides
297                .as_ref()
298                .iter()
299                .map(|guide| {
300                    window
301                        .insert_hitbox(guide.hitbox.unwrap_or(guide.bounds), HitboxBehavior::Normal)
302                })
303                .collect();
304            Self::PrepaintState::Interactive {
305                hitboxes: Rc::new(hitboxes),
306                on_hovered_indent_guide_click,
307            }
308        } else {
309            Self::PrepaintState::Static
310        }
311    }
312
313    fn paint(
314        &mut self,
315        _id: Option<&gpui::GlobalElementId>,
316        _inspector_id: Option<&gpui::InspectorElementId>,
317        _bounds: Bounds<Pixels>,
318        _request_layout: &mut Self::RequestLayoutState,
319        prepaint: &mut Self::PrepaintState,
320        window: &mut Window,
321        _cx: &mut App,
322    ) {
323        let current_view = window.current_view();
324
325        match prepaint {
326            IndentGuidesElementPrepaintState::Static => {
327                for indent_guide in self.indent_guides.as_ref() {
328                    let fill_color = if indent_guide.is_active {
329                        self.colors.active
330                    } else {
331                        self.colors.default
332                    };
333
334                    window.paint_quad(fill(indent_guide.bounds, fill_color));
335                }
336            }
337            IndentGuidesElementPrepaintState::Interactive {
338                hitboxes,
339                on_hovered_indent_guide_click,
340            } => {
341                window.on_mouse_event({
342                    let hitboxes = hitboxes.clone();
343                    let indent_guides = self.indent_guides.clone();
344                    let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone();
345                    move |event: &MouseDownEvent, phase, window, cx| {
346                        if phase == DispatchPhase::Bubble && event.button == MouseButton::Left {
347                            let mut active_hitbox_ix = None;
348                            for (i, hitbox) in hitboxes.iter().enumerate() {
349                                if hitbox.is_hovered(window) {
350                                    active_hitbox_ix = Some(i);
351                                    break;
352                                }
353                            }
354
355                            let Some(active_hitbox_ix) = active_hitbox_ix else {
356                                return;
357                            };
358
359                            let active_indent_guide = &indent_guides[active_hitbox_ix].layout;
360                            on_hovered_indent_guide_click(active_indent_guide, window, cx);
361
362                            cx.stop_propagation();
363                            window.prevent_default();
364                        }
365                    }
366                });
367                let mut hovered_hitbox_id = None;
368                for (i, hitbox) in hitboxes.iter().enumerate() {
369                    window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
370                    let indent_guide = &self.indent_guides[i];
371                    let fill_color = if hitbox.is_hovered(window) {
372                        hovered_hitbox_id = Some(hitbox.id);
373                        self.colors.hover
374                    } else if indent_guide.is_active {
375                        self.colors.active
376                    } else {
377                        self.colors.default
378                    };
379
380                    window.paint_quad(fill(indent_guide.bounds, fill_color));
381                }
382
383                window.on_mouse_event({
384                    let prev_hovered_hitbox_id = hovered_hitbox_id;
385                    let hitboxes = hitboxes.clone();
386                    move |_: &MouseMoveEvent, phase, window, cx| {
387                        let mut hovered_hitbox_id = None;
388                        for hitbox in hitboxes.as_ref() {
389                            if hitbox.is_hovered(window) {
390                                hovered_hitbox_id = Some(hitbox.id);
391                                break;
392                            }
393                        }
394                        if phase == DispatchPhase::Capture {
395                            // If the hovered hitbox has changed, we need to re-paint the indent guides.
396                            match (prev_hovered_hitbox_id, hovered_hitbox_id) {
397                                (Some(prev_id), Some(id)) => {
398                                    if prev_id != id {
399                                        cx.notify(current_view)
400                                    }
401                                }
402                                (None, Some(_)) => cx.notify(current_view),
403                                (Some(_), None) => cx.notify(current_view),
404                                (None, None) => {}
405                            }
406                        }
407                    }
408                });
409            }
410        }
411    }
412}
413
414impl IntoElement for IndentGuidesElement {
415    type Element = Self;
416
417    fn into_element(self) -> Self::Element {
418        self
419    }
420}
421
422fn compute_indent_guides(
423    indents: &[usize],
424    offset: usize,
425    includes_trailing_indent: bool,
426) -> SmallVec<[IndentGuideLayout; 12]> {
427    let mut indent_guides = SmallVec::<[IndentGuideLayout; 12]>::new();
428    let mut indent_stack = SmallVec::<[IndentGuideLayout; 8]>::new();
429
430    let mut min_depth = usize::MAX;
431    for (row, &depth) in indents.iter().enumerate() {
432        if includes_trailing_indent && row == indents.len() - 1 {
433            continue;
434        }
435
436        let current_row = row + offset;
437        let current_depth = indent_stack.len();
438        if depth < min_depth {
439            min_depth = depth;
440        }
441
442        match depth.cmp(¤t_depth) {
443            Ordering::Less => {
444                for _ in 0..(current_depth - depth) {
445                    if let Some(guide) = indent_stack.pop() {
446                        indent_guides.push(guide);
447                    }
448                }
449            }
450            Ordering::Greater => {
451                for new_depth in current_depth..depth {
452                    indent_stack.push(IndentGuideLayout {
453                        offset: Point::new(new_depth, current_row),
454                        length: current_row,
455                        continues_offscreen: false,
456                    });
457                }
458            }
459            _ => {}
460        }
461
462        for indent in indent_stack.iter_mut() {
463            indent.length = current_row - indent.offset.y + 1;
464        }
465    }
466
467    indent_guides.extend(indent_stack);
468
469    for guide in indent_guides.iter_mut() {
470        if includes_trailing_indent
471            && guide.offset.y + guide.length == offset + indents.len().saturating_sub(1)
472        {
473            guide.continues_offscreen = indents
474                .last()
475                .map(|last_indent| guide.offset.x < *last_indent)
476                .unwrap_or(false);
477        }
478    }
479
480    indent_guides
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn test_compute_indent_guides() {
489        fn assert_compute_indent_guides(
490            input: &[usize],
491            offset: usize,
492            includes_trailing_indent: bool,
493            expected: Vec<IndentGuideLayout>,
494        ) {
495            use std::collections::HashSet;
496            assert_eq!(
497                compute_indent_guides(input, offset, includes_trailing_indent)
498                    .into_vec()
499                    .into_iter()
500                    .collect::<HashSet<_>>(),
501                expected.into_iter().collect::<HashSet<_>>(),
502            );
503        }
504
505        assert_compute_indent_guides(
506            &[0, 1, 2, 2, 1, 0],
507            0,
508            false,
509            vec![
510                IndentGuideLayout {
511                    offset: Point::new(0, 1),
512                    length: 4,
513                    continues_offscreen: false,
514                },
515                IndentGuideLayout {
516                    offset: Point::new(1, 2),
517                    length: 2,
518                    continues_offscreen: false,
519                },
520            ],
521        );
522
523        assert_compute_indent_guides(
524            &[2, 2, 2, 1, 1],
525            0,
526            false,
527            vec![
528                IndentGuideLayout {
529                    offset: Point::new(0, 0),
530                    length: 5,
531                    continues_offscreen: false,
532                },
533                IndentGuideLayout {
534                    offset: Point::new(1, 0),
535                    length: 3,
536                    continues_offscreen: false,
537                },
538            ],
539        );
540
541        assert_compute_indent_guides(
542            &[1, 2, 3, 2, 1],
543            0,
544            false,
545            vec![
546                IndentGuideLayout {
547                    offset: Point::new(0, 0),
548                    length: 5,
549                    continues_offscreen: false,
550                },
551                IndentGuideLayout {
552                    offset: Point::new(1, 1),
553                    length: 3,
554                    continues_offscreen: false,
555                },
556                IndentGuideLayout {
557                    offset: Point::new(2, 2),
558                    length: 1,
559                    continues_offscreen: false,
560                },
561            ],
562        );
563
564        assert_compute_indent_guides(
565            &[0, 1, 0],
566            0,
567            true,
568            vec![IndentGuideLayout {
569                offset: Point::new(0, 1),
570                length: 1,
571                continues_offscreen: false,
572            }],
573        );
574
575        assert_compute_indent_guides(
576            &[0, 1, 1],
577            0,
578            true,
579            vec![IndentGuideLayout {
580                offset: Point::new(0, 1),
581                length: 1,
582                continues_offscreen: true,
583            }],
584        );
585        assert_compute_indent_guides(
586            &[0, 1, 2],
587            0,
588            true,
589            vec![IndentGuideLayout {
590                offset: Point::new(0, 1),
591                length: 1,
592                continues_offscreen: true,
593            }],
594        );
595    }
596}