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