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