Skip to repository content618 lines · 21.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:40:44.978Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
indent_guides.rs
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(¤t_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