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