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