indent_guides.rs

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