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#[derive(Debug, Clone)]
11pub struct IndentGuideColors {
12 pub default: Hsla,
14 pub hover: Hsla,
16 pub active: Hsla,
18}
19
20impl IndentGuideColors {
21 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 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 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 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
154pub struct RenderIndentGuideParams {
156 pub indent_guides: SmallVec<[IndentGuideLayout; 12]>,
158 pub indent_size: Pixels,
160 pub item_height: Pixels,
162}
163
164pub struct RenderedIndentGuide {
166 pub bounds: Bounds<Pixels>,
168 pub layout: IndentGuideLayout,
170 pub is_active: bool,
172 pub hitbox: Option<Bounds<Pixels>>,
175}
176
177#[derive(Debug, PartialEq, Eq, Hash)]
179pub struct IndentGuideLayout {
180 pub offset: Point<usize>,
183 pub length: usize,
185 pub continues_offscreen: bool,
187}
188
189mod uniform_list {
191 use gpui::UniformListDecoration;
192
193 use super::*;
194
195 impl UniformListDecoration for IndentGuides {
196 fn compute(
197 &self,
198 mut 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 includes_trailing_indent = visible_range.end < item_count;
207 if includes_trailing_indent {
211 visible_range.end += 1;
212 }
213 let Some(ref compute_indents_fn) = self.compute_indents_fn else {
214 panic!("compute_indents_fn is required for UniformListDecoration");
215 };
216 let visible_entries = &compute_indents_fn(visible_range.clone(), window, cx);
217 let indent_guides = compute_indent_guides(
218 visible_entries,
219 visible_range.start,
220 includes_trailing_indent,
221 );
222 self.render_from_layout(indent_guides, bounds, item_height, window, cx)
223 }
224 }
225}
226
227mod sticky_items {
229 use crate::StickyItemsDecoration;
230
231 use super::*;
232
233 impl StickyItemsDecoration for IndentGuides {
234 fn compute(
235 &self,
236 indents: &SmallVec<[usize; 8]>,
237 bounds: Bounds<Pixels>,
238 _scroll_offset: Point<Pixels>,
239 item_height: Pixels,
240 window: &mut Window,
241 cx: &mut App,
242 ) -> AnyElement {
243 let indent_guides = compute_indent_guides(indents, 0, false);
244 self.render_from_layout(indent_guides, bounds, item_height, window, cx)
245 }
246 }
247}
248
249struct IndentGuidesElement {
250 colors: IndentGuideColors,
251 indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>,
252 on_hovered_indent_guide_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
253}
254
255enum IndentGuidesElementPrepaintState {
256 Static,
257 Interactive {
258 hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
259 on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
260 },
261}
262
263impl Element for IndentGuidesElement {
264 type RequestLayoutState = ();
265 type PrepaintState = IndentGuidesElementPrepaintState;
266
267 fn id(&self) -> Option<ElementId> {
268 None
269 }
270
271 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
272 None
273 }
274
275 fn request_layout(
276 &mut self,
277 _id: Option<&gpui::GlobalElementId>,
278 _inspector_id: Option<&gpui::InspectorElementId>,
279 window: &mut Window,
280 cx: &mut App,
281 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
282 (window.request_layout(gpui::Style::default(), [], cx), ())
283 }
284
285 fn prepaint(
286 &mut self,
287 _id: Option<&gpui::GlobalElementId>,
288 _inspector_id: Option<&gpui::InspectorElementId>,
289 _bounds: Bounds<Pixels>,
290 _request_layout: &mut Self::RequestLayoutState,
291 window: &mut Window,
292 _cx: &mut App,
293 ) -> Self::PrepaintState {
294 if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone() {
295 let hitboxes = self
296 .indent_guides
297 .as_ref()
298 .iter()
299 .map(|guide| {
300 window
301 .insert_hitbox(guide.hitbox.unwrap_or(guide.bounds), HitboxBehavior::Normal)
302 })
303 .collect();
304 Self::PrepaintState::Interactive {
305 hitboxes: Rc::new(hitboxes),
306 on_hovered_indent_guide_click,
307 }
308 } else {
309 Self::PrepaintState::Static
310 }
311 }
312
313 fn paint(
314 &mut self,
315 _id: Option<&gpui::GlobalElementId>,
316 _inspector_id: Option<&gpui::InspectorElementId>,
317 _bounds: Bounds<Pixels>,
318 _request_layout: &mut Self::RequestLayoutState,
319 prepaint: &mut Self::PrepaintState,
320 window: &mut Window,
321 _cx: &mut App,
322 ) {
323 let current_view = window.current_view();
324
325 match prepaint {
326 IndentGuidesElementPrepaintState::Static => {
327 for indent_guide in self.indent_guides.as_ref() {
328 let fill_color = if indent_guide.is_active {
329 self.colors.active
330 } else {
331 self.colors.default
332 };
333
334 window.paint_quad(fill(
335 window.pixel_snap_bounds(indent_guide.bounds),
336 fill_color,
337 ));
338 }
339 }
340 IndentGuidesElementPrepaintState::Interactive {
341 hitboxes,
342 on_hovered_indent_guide_click,
343 } => {
344 window.on_mouse_event({
345 let hitboxes = hitboxes.clone();
346 let indent_guides = self.indent_guides.clone();
347 let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone();
348 move |event: &MouseDownEvent, phase, window, cx| {
349 if phase == DispatchPhase::Bubble && event.button == MouseButton::Left {
350 let mut active_hitbox_ix = None;
351 for (i, hitbox) in hitboxes.iter().enumerate() {
352 if hitbox.is_hovered(window) {
353 active_hitbox_ix = Some(i);
354 break;
355 }
356 }
357
358 let Some(active_hitbox_ix) = active_hitbox_ix else {
359 return;
360 };
361
362 let active_indent_guide = &indent_guides[active_hitbox_ix].layout;
363 on_hovered_indent_guide_click(active_indent_guide, window, cx);
364
365 cx.stop_propagation();
366 window.prevent_default();
367 }
368 }
369 });
370 let mut hovered_hitbox_id = None;
371 for (i, hitbox) in hitboxes.iter().enumerate() {
372 window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
373 let indent_guide = &self.indent_guides[i];
374 let fill_color = if hitbox.is_hovered(window) {
375 hovered_hitbox_id = Some(hitbox.id);
376 self.colors.hover
377 } else if indent_guide.is_active {
378 self.colors.active
379 } else {
380 self.colors.default
381 };
382
383 window.paint_quad(fill(
384 window.pixel_snap_bounds(indent_guide.bounds),
385 fill_color,
386 ));
387 }
388
389 window.on_mouse_event({
390 let prev_hovered_hitbox_id = hovered_hitbox_id;
391 let hitboxes = hitboxes.clone();
392 move |_: &MouseMoveEvent, phase, window, cx| {
393 let mut hovered_hitbox_id = None;
394 for hitbox in hitboxes.as_ref() {
395 if hitbox.is_hovered(window) {
396 hovered_hitbox_id = Some(hitbox.id);
397 break;
398 }
399 }
400 if phase == DispatchPhase::Capture {
401 match (prev_hovered_hitbox_id, hovered_hitbox_id) {
403 (Some(prev_id), Some(id)) => {
404 if prev_id != id {
405 cx.notify(current_view)
406 }
407 }
408 (None, Some(_)) => cx.notify(current_view),
409 (Some(_), None) => cx.notify(current_view),
410 (None, None) => {}
411 }
412 }
413 }
414 });
415 }
416 }
417 }
418}
419
420impl IntoElement for IndentGuidesElement {
421 type Element = Self;
422
423 fn into_element(self) -> Self::Element {
424 self
425 }
426}
427
428fn compute_indent_guides(
429 indents: &[usize],
430 offset: usize,
431 includes_trailing_indent: bool,
432) -> SmallVec<[IndentGuideLayout; 12]> {
433 let mut indent_guides = SmallVec::<[IndentGuideLayout; 12]>::new();
434 let mut indent_stack = SmallVec::<[IndentGuideLayout; 8]>::new();
435
436 let mut min_depth = usize::MAX;
437 for (row, &depth) in indents.iter().enumerate() {
438 if includes_trailing_indent && row == indents.len() - 1 {
439 continue;
440 }
441
442 let current_row = row + offset;
443 let current_depth = indent_stack.len();
444 if depth < min_depth {
445 min_depth = depth;
446 }
447
448 match depth.cmp(¤t_depth) {
449 Ordering::Less => {
450 for _ in 0..(current_depth - depth) {
451 if let Some(guide) = indent_stack.pop() {
452 indent_guides.push(guide);
453 }
454 }
455 }
456 Ordering::Greater => {
457 for new_depth in current_depth..depth {
458 indent_stack.push(IndentGuideLayout {
459 offset: Point::new(new_depth, current_row),
460 length: current_row,
461 continues_offscreen: false,
462 });
463 }
464 }
465 _ => {}
466 }
467
468 for indent in indent_stack.iter_mut() {
469 indent.length = current_row - indent.offset.y + 1;
470 }
471 }
472
473 indent_guides.extend(indent_stack);
474
475 for guide in indent_guides.iter_mut() {
476 if includes_trailing_indent
477 && guide.offset.y + guide.length == offset + indents.len().saturating_sub(1)
478 {
479 guide.continues_offscreen = indents
480 .last()
481 .map(|last_indent| guide.offset.x < *last_indent)
482 .unwrap_or(false);
483 }
484 }
485
486 indent_guides
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 #[test]
494 fn test_compute_indent_guides() {
495 fn assert_compute_indent_guides(
496 input: &[usize],
497 offset: usize,
498 includes_trailing_indent: bool,
499 expected: Vec<IndentGuideLayout>,
500 ) {
501 use std::collections::HashSet;
502 assert_eq!(
503 compute_indent_guides(input, offset, includes_trailing_indent)
504 .into_vec()
505 .into_iter()
506 .collect::<HashSet<_>>(),
507 expected.into_iter().collect::<HashSet<_>>(),
508 );
509 }
510
511 assert_compute_indent_guides(
512 &[0, 1, 2, 2, 1, 0],
513 0,
514 false,
515 vec![
516 IndentGuideLayout {
517 offset: Point::new(0, 1),
518 length: 4,
519 continues_offscreen: false,
520 },
521 IndentGuideLayout {
522 offset: Point::new(1, 2),
523 length: 2,
524 continues_offscreen: false,
525 },
526 ],
527 );
528
529 assert_compute_indent_guides(
530 &[2, 2, 2, 1, 1],
531 0,
532 false,
533 vec![
534 IndentGuideLayout {
535 offset: Point::new(0, 0),
536 length: 5,
537 continues_offscreen: false,
538 },
539 IndentGuideLayout {
540 offset: Point::new(1, 0),
541 length: 3,
542 continues_offscreen: false,
543 },
544 ],
545 );
546
547 assert_compute_indent_guides(
548 &[1, 2, 3, 2, 1],
549 0,
550 false,
551 vec![
552 IndentGuideLayout {
553 offset: Point::new(0, 0),
554 length: 5,
555 continues_offscreen: false,
556 },
557 IndentGuideLayout {
558 offset: Point::new(1, 1),
559 length: 3,
560 continues_offscreen: false,
561 },
562 IndentGuideLayout {
563 offset: Point::new(2, 2),
564 length: 1,
565 continues_offscreen: false,
566 },
567 ],
568 );
569
570 assert_compute_indent_guides(
571 &[0, 1, 0],
572 0,
573 true,
574 vec![IndentGuideLayout {
575 offset: Point::new(0, 1),
576 length: 1,
577 continues_offscreen: false,
578 }],
579 );
580
581 assert_compute_indent_guides(
582 &[0, 1, 1],
583 0,
584 true,
585 vec![IndentGuideLayout {
586 offset: Point::new(0, 1),
587 length: 1,
588 continues_offscreen: true,
589 }],
590 );
591 assert_compute_indent_guides(
592 &[0, 1, 2],
593 0,
594 true,
595 vec![IndentGuideLayout {
596 offset: Point::new(0, 1),
597 length: 1,
598 continues_offscreen: true,
599 }],
600 );
601 }
602}