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