Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:39:51.823Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

indent_guides.rs

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