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