indent_guides.rs

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