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