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::{
140 DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent,
141 };
142
143 use super::*;
144
145 impl UniformListDecoration for IndentGuides {
146 fn compute(
147 &self,
148 visible_range: Range<usize>,
149 bounds: Bounds<Pixels>,
150 _scroll_offset: Point<Pixels>,
151 item_height: Pixels,
152 item_count: usize,
153 window: &mut Window,
154 cx: &mut App,
155 ) -> AnyElement {
156 let mut visible_range = visible_range.clone();
157 let includes_trailing_indent = visible_range.end < item_count;
158 // Check if we have entries after the visible range,
159 // if so extend the visible range so we can fetch a trailing indent,
160 // which is needed to compute indent guides correctly.
161 if includes_trailing_indent {
162 visible_range.end += 1;
163 }
164 let visible_entries = &(self.compute_indents_fn)(visible_range.clone(), window, cx);
165 let indent_guides = compute_indent_guides(
166 &visible_entries,
167 visible_range.start,
168 includes_trailing_indent,
169 );
170 let mut indent_guides = if let Some(ref custom_render) = self.render_fn {
171 let params = RenderIndentGuideParams {
172 indent_guides,
173 indent_size: self.indent_size,
174 item_height,
175 };
176 custom_render(params, window, cx)
177 } else {
178 indent_guides
179 .into_iter()
180 .map(|layout| RenderedIndentGuide {
181 bounds: Bounds::new(
182 point(
183 layout.offset.x * self.indent_size,
184 layout.offset.y * item_height,
185 ),
186 size(px(1.), layout.length * item_height),
187 ),
188 layout,
189 is_active: false,
190 hitbox: None,
191 })
192 .collect()
193 };
194 for guide in &mut indent_guides {
195 guide.bounds.origin += bounds.origin;
196 if let Some(hitbox) = guide.hitbox.as_mut() {
197 hitbox.origin += bounds.origin;
198 }
199 }
200
201 let indent_guides = IndentGuidesElement {
202 indent_guides: Rc::new(indent_guides),
203 colors: self.colors.clone(),
204 on_hovered_indent_guide_click: self.on_click.clone(),
205 };
206 indent_guides.into_any_element()
207 }
208 }
209
210 struct IndentGuidesElement {
211 colors: IndentGuideColors,
212 indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>,
213 on_hovered_indent_guide_click:
214 Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
215 }
216
217 enum IndentGuidesElementPrepaintState {
218 Static,
219 Interactive {
220 hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
221 on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
222 },
223 }
224
225 impl Element for IndentGuidesElement {
226 type RequestLayoutState = ();
227 type PrepaintState = IndentGuidesElementPrepaintState;
228
229 fn id(&self) -> Option<ElementId> {
230 None
231 }
232
233 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
234 None
235 }
236
237 fn request_layout(
238 &mut self,
239 _id: Option<&gpui::GlobalElementId>,
240 _inspector_id: Option<&gpui::InspectorElementId>,
241 window: &mut Window,
242 cx: &mut App,
243 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
244 (window.request_layout(gpui::Style::default(), [], cx), ())
245 }
246
247 fn prepaint(
248 &mut self,
249 _id: Option<&gpui::GlobalElementId>,
250 _inspector_id: Option<&gpui::InspectorElementId>,
251 _bounds: Bounds<Pixels>,
252 _request_layout: &mut Self::RequestLayoutState,
253 window: &mut Window,
254 _cx: &mut App,
255 ) -> Self::PrepaintState {
256 if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone()
257 {
258 let hitboxes = self
259 .indent_guides
260 .as_ref()
261 .iter()
262 .map(|guide| {
263 window.insert_hitbox(
264 guide.hitbox.unwrap_or(guide.bounds),
265 HitboxBehavior::Normal,
266 )
267 })
268 .collect();
269 Self::PrepaintState::Interactive {
270 hitboxes: Rc::new(hitboxes),
271 on_hovered_indent_guide_click,
272 }
273 } else {
274 Self::PrepaintState::Static
275 }
276 }
277
278 fn paint(
279 &mut self,
280 _id: Option<&gpui::GlobalElementId>,
281 _inspector_id: Option<&gpui::InspectorElementId>,
282 _bounds: Bounds<Pixels>,
283 _request_layout: &mut Self::RequestLayoutState,
284 prepaint: &mut Self::PrepaintState,
285 window: &mut Window,
286 _cx: &mut App,
287 ) {
288 let current_view = window.current_view();
289
290 match prepaint {
291 IndentGuidesElementPrepaintState::Static => {
292 for indent_guide in self.indent_guides.as_ref() {
293 let fill_color = if indent_guide.is_active {
294 self.colors.active
295 } else {
296 self.colors.default
297 };
298
299 window.paint_quad(fill(indent_guide.bounds, fill_color));
300 }
301 }
302 IndentGuidesElementPrepaintState::Interactive {
303 hitboxes,
304 on_hovered_indent_guide_click,
305 } => {
306 window.on_mouse_event({
307 let hitboxes = hitboxes.clone();
308 let indent_guides = self.indent_guides.clone();
309 let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone();
310 move |event: &MouseDownEvent, phase, window, cx| {
311 if phase == DispatchPhase::Bubble && event.button == MouseButton::Left {
312 let mut active_hitbox_ix = None;
313 for (i, hitbox) in hitboxes.iter().enumerate() {
314 if hitbox.is_hovered(window) {
315 active_hitbox_ix = Some(i);
316 break;
317 }
318 }
319
320 let Some(active_hitbox_ix) = active_hitbox_ix else {
321 return;
322 };
323
324 let active_indent_guide = &indent_guides[active_hitbox_ix].layout;
325 on_hovered_indent_guide_click(active_indent_guide, window, cx);
326
327 cx.stop_propagation();
328 window.prevent_default();
329 }
330 }
331 });
332 let mut hovered_hitbox_id = None;
333 for (i, hitbox) in hitboxes.iter().enumerate() {
334 window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
335 let indent_guide = &self.indent_guides[i];
336 let fill_color = if hitbox.is_hovered(window) {
337 hovered_hitbox_id = Some(hitbox.id);
338 self.colors.hover
339 } else if indent_guide.is_active {
340 self.colors.active
341 } else {
342 self.colors.default
343 };
344
345 window.paint_quad(fill(indent_guide.bounds, fill_color));
346 }
347
348 window.on_mouse_event({
349 let prev_hovered_hitbox_id = hovered_hitbox_id;
350 let hitboxes = hitboxes.clone();
351 move |_: &MouseMoveEvent, phase, window, cx| {
352 let mut hovered_hitbox_id = None;
353 for hitbox in hitboxes.as_ref() {
354 if hitbox.is_hovered(window) {
355 hovered_hitbox_id = Some(hitbox.id);
356 break;
357 }
358 }
359 if phase == DispatchPhase::Capture {
360 // If the hovered hitbox has changed, we need to re-paint the indent guides.
361 match (prev_hovered_hitbox_id, hovered_hitbox_id) {
362 (Some(prev_id), Some(id)) => {
363 if prev_id != id {
364 cx.notify(current_view)
365 }
366 }
367 (None, Some(_)) => cx.notify(current_view),
368 (Some(_), None) => cx.notify(current_view),
369 (None, None) => {}
370 }
371 }
372 }
373 });
374 }
375 }
376 }
377 }
378
379 impl IntoElement for IndentGuidesElement {
380 type Element = Self;
381
382 fn into_element(self) -> Self::Element {
383 self
384 }
385 }
386}
387
388fn compute_indent_guides(
389 indents: &[usize],
390 offset: usize,
391 includes_trailing_indent: bool,
392) -> SmallVec<[IndentGuideLayout; 12]> {
393 let mut indent_guides = SmallVec::<[IndentGuideLayout; 12]>::new();
394 let mut indent_stack = SmallVec::<[IndentGuideLayout; 8]>::new();
395
396 let mut min_depth = usize::MAX;
397 for (row, &depth) in indents.iter().enumerate() {
398 if includes_trailing_indent && row == indents.len() - 1 {
399 continue;
400 }
401
402 let current_row = row + offset;
403 let current_depth = indent_stack.len();
404 if depth < min_depth {
405 min_depth = depth;
406 }
407
408 match depth.cmp(¤t_depth) {
409 Ordering::Less => {
410 for _ in 0..(current_depth - depth) {
411 if let Some(guide) = indent_stack.pop() {
412 indent_guides.push(guide);
413 }
414 }
415 }
416 Ordering::Greater => {
417 for new_depth in current_depth..depth {
418 indent_stack.push(IndentGuideLayout {
419 offset: Point::new(new_depth, current_row),
420 length: current_row,
421 continues_offscreen: false,
422 });
423 }
424 }
425 _ => {}
426 }
427
428 for indent in indent_stack.iter_mut() {
429 indent.length = current_row - indent.offset.y + 1;
430 }
431 }
432
433 indent_guides.extend(indent_stack);
434
435 for guide in indent_guides.iter_mut() {
436 if includes_trailing_indent
437 && guide.offset.y + guide.length == offset + indents.len().saturating_sub(1)
438 {
439 guide.continues_offscreen = indents
440 .last()
441 .map(|last_indent| guide.offset.x < *last_indent)
442 .unwrap_or(false);
443 }
444 }
445
446 indent_guides
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn test_compute_indent_guides() {
455 fn assert_compute_indent_guides(
456 input: &[usize],
457 offset: usize,
458 includes_trailing_indent: bool,
459 expected: Vec<IndentGuideLayout>,
460 ) {
461 use std::collections::HashSet;
462 assert_eq!(
463 compute_indent_guides(input, offset, includes_trailing_indent)
464 .into_vec()
465 .into_iter()
466 .collect::<HashSet<_>>(),
467 expected.into_iter().collect::<HashSet<_>>(),
468 );
469 }
470
471 assert_compute_indent_guides(
472 &[0, 1, 2, 2, 1, 0],
473 0,
474 false,
475 vec![
476 IndentGuideLayout {
477 offset: Point::new(0, 1),
478 length: 4,
479 continues_offscreen: false,
480 },
481 IndentGuideLayout {
482 offset: Point::new(1, 2),
483 length: 2,
484 continues_offscreen: false,
485 },
486 ],
487 );
488
489 assert_compute_indent_guides(
490 &[2, 2, 2, 1, 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, 0),
501 length: 3,
502 continues_offscreen: false,
503 },
504 ],
505 );
506
507 assert_compute_indent_guides(
508 &[1, 2, 3, 2, 1],
509 0,
510 false,
511 vec![
512 IndentGuideLayout {
513 offset: Point::new(0, 0),
514 length: 5,
515 continues_offscreen: false,
516 },
517 IndentGuideLayout {
518 offset: Point::new(1, 1),
519 length: 3,
520 continues_offscreen: false,
521 },
522 IndentGuideLayout {
523 offset: Point::new(2, 2),
524 length: 1,
525 continues_offscreen: false,
526 },
527 ],
528 );
529
530 assert_compute_indent_guides(
531 &[0, 1, 0],
532 0,
533 true,
534 vec![IndentGuideLayout {
535 offset: Point::new(0, 1),
536 length: 1,
537 continues_offscreen: false,
538 }],
539 );
540
541 assert_compute_indent_guides(
542 &[0, 1, 1],
543 0,
544 true,
545 vec![IndentGuideLayout {
546 offset: Point::new(0, 1),
547 length: 1,
548 continues_offscreen: true,
549 }],
550 );
551 assert_compute_indent_guides(
552 &[0, 1, 2],
553 0,
554 true,
555 vec![IndentGuideLayout {
556 offset: Point::new(0, 1),
557 length: 1,
558 continues_offscreen: true,
559 }],
560 );
561 }
562}