1use gpui_kit::accesskit::Action;
14use gpui_kit::component::{IconName, IconNamed as _};
15use gpui_kit::{
16 Context, InteractiveElement, MouseDownEvent, Pixels, Point, SharedString, Window, px,
17};
18
19use super::{
20 CELL_PADDING, ColumnOf, KeyOf, LiveGridDelegate, LiveGridState, SelectionOrigin,
21 a11y::RowContent,
22 events::{ExpandTarget, GroupOrigin},
23 frame::{LEAD_ICON_LANE, PaintedLine},
24 group_hash,
25 scroll::{AnchorLine, LineAnchor},
26 selection::{FocusedCell, LineTarget, Navigation, ShownRows},
27};
28use crate::{
29 actions::{CollapseAll, CollapseRow, ExpandAll, ExpandRow, ShowCaptionsOnly},
30 engine::{DisplayRow, ExpandDepth, GroupInfo, GroupPath, Projection},
31 model::GridRow,
32 policy::{ExpandArea, ExpandEdit, ExpandGesture, ExpandGestureContext},
33};
34
35pub(super) const INDENT: Pixels = px(16.);
37
38pub(super) fn chevron_size(row_height: Pixels) -> Pixels {
41 if row_height < px(32.) {
42 px(12.)
43 } else {
44 px(14.)
45 }
46}
47
48pub(super) struct Chevrons {
50 expanded: SharedString,
51 collapsed: SharedString,
52}
53
54impl Chevrons {
55 pub(super) fn new() -> Self {
56 Self {
57 expanded: IconName::ChevronDown.path(),
58 collapsed: IconName::ChevronRight.path(),
59 }
60 }
61
62 pub(super) fn path(&self, expanded: bool) -> SharedString {
63 if expanded {
64 self.expanded.clone()
65 } else {
66 self.collapsed.clone()
67 }
68 }
69}
70
71fn selection_origin(origin: GroupOrigin) -> SelectionOrigin {
73 match origin {
74 GroupOrigin::User => SelectionOrigin::User,
75 GroupOrigin::Programmatic => SelectionOrigin::Programmatic,
76 }
77}
78
79pub(super) fn row_group(projection: &Projection, order_ix: usize) -> Option<GroupInfo<'_>> {
84 projection
85 .groups()
86 .iter()
87 .filter(|group| {
88 let rows = group.rows();
89 rows.first()
90 .and_then(|first| projection.order_ix(*first))
91 .is_some_and(|start| (start..start + rows.len()).contains(&order_ix))
92 })
93 .last()
94}
95
96impl<D: LiveGridDelegate> LiveGridState<D> {
97 pub fn with_expand_gesture(mut self, gesture: impl ExpandGesture) -> Self {
102 self.expand_gesture = Box::new(gesture);
103 self
104 }
105
106 pub(super) fn caption_lane_x(&self, depth: u16) -> Pixels {
110 self.body_bounds.origin.x + CELL_PADDING + INDENT * f32::from(depth)
111 }
112
113 pub(super) fn disclosure_hit(
117 &self,
118 position: Point<Pixels>,
119 ) -> Option<(PaintedLine, ExpandArea)> {
120 let line = self.painted.get(self.line_at(position)?)?;
121 let lane = |lane_x: Pixels| (lane_x..lane_x + LEAD_ICON_LANE).contains(&position.x);
122 match line {
123 PaintedLine::Caption(path) => {
124 let area = if lane(self.caption_lane_x(path.depth())) {
125 ExpandArea::Chevron
126 } else {
127 ExpandArea::Caption
128 };
129 Some((line.clone(), area))
130 }
131 PaintedLine::Row(id) => {
132 let projection = &self.projection;
133 projection.branch(*id)?;
134 let column = self.tree_column()?;
135 if self.column_at_position(position) != Some(column) {
136 return None;
137 }
138 let depth = projection.line(projection.display_ix(*id)?)?.depth();
139 let geometry = self
140 .columns
141 .geometry(self.body_bounds.size.width, self.scroll_x);
142 let (zone, i) = self.columns.zone_position(column)?;
143 let cell_x = self.body_bounds.origin.x
144 + geometry
145 .zone(zone)
146 .column_x(self.columns.zone_columns(zone), i);
147 let area = if lane(cell_x + CELL_PADDING + INDENT * f32::from(depth)) {
148 ExpandArea::Chevron
149 } else {
150 ExpandArea::TreeCell
151 };
152 Some((line.clone(), area))
153 }
154 PaintedLine::Footer(_) => None,
156 PaintedLine::Ghost(_) => None,
158 }
159 }
160
161 fn line_expand_target(&self, line: &PaintedLine) -> Option<ExpandTarget<KeyOf<D>>> {
163 match line {
164 PaintedLine::Caption(path) => Some(ExpandTarget::Group(path.clone())),
165 PaintedLine::Row(id) => Some(ExpandTarget::Row(self.snapshot.row(*id)?.key().clone())),
166 PaintedLine::Footer(_) => None,
168 PaintedLine::Ghost(_) => None,
170 }
171 }
172
173 pub(super) fn press_disclosure(
177 &mut self,
178 line: &PaintedLine,
179 area: ExpandArea,
180 event: &MouseDownEvent,
181 cx: &mut Context<Self>,
182 ) -> bool {
183 let context =
184 ExpandGestureContext::new(area, event.button, event.modifiers, event.click_count);
185 let Some(edit) = self.expand_gesture.edit(&context) else {
186 return false;
187 };
188 let Some(target) = self.line_expand_target(line) else {
189 return false;
190 };
191 self.anchor_toggled(line);
192 self.toggle_line(target, edit, GroupOrigin::User, cx)
193 }
194
195 fn toggle_line(
197 &mut self,
198 target: ExpandTarget<KeyOf<D>>,
199 edit: ExpandEdit,
200 origin: GroupOrigin,
201 cx: &mut Context<Self>,
202 ) -> bool {
203 let expanded = !self.is_expanded(&target);
204 match edit {
205 ExpandEdit::ToggleDeep => self.apply_deep_expanded(target, expanded, origin, cx),
206 _ => self.apply_expanded(target, expanded, origin, cx),
207 }
208 }
209
210 pub(crate) fn apply_deep_expanded(
213 &mut self,
214 target: ExpandTarget<KeyOf<D>>,
215 expanded: bool,
216 origin: GroupOrigin,
217 cx: &mut Context<Self>,
218 ) -> bool {
219 let mut expansion = self.view.expansion().clone();
220 match &target {
221 ExpandTarget::Group(path) => {
222 if path.is_root() {
223 return false;
224 }
225 let projection = self.projection.clone();
226 let below: Vec<GroupPath> = projection
227 .groups()
228 .iter()
229 .filter(|group| group.path().starts_with(path) && group.path() != path)
230 .map(|group| group.path().clone())
231 .collect();
232 for path in std::iter::once(path.clone()).chain(below) {
233 let levels = self.group_levels(&path);
234 expansion = expansion.with_group(path, expanded, levels);
235 }
236 }
237 ExpandTarget::Row(key) => {
238 let (projection, snapshot) = (self.projection.clone(), self.snapshot.clone());
239 let Some(tree) = projection.tree_data() else {
240 return false;
241 };
242 let Some((id, order_ix)) = snapshot
243 .row_id(key)
244 .and_then(|id| Some((id, projection.order_ix(id)?)))
245 else {
246 return false;
247 };
248 let descendants = projection
249 .branch(id)
250 .map_or(0, |branch| branch.descendant_count());
251 for ix in order_ix..=order_ix + descendants {
252 let Some(&row_id) = projection.rows().get(ix) else {
253 break;
254 };
255 if projection.branch(row_id).is_none() {
256 continue;
257 }
258 if let Some(row) = snapshot.row(row_id) {
259 expansion = expansion.with_row(row.key().clone(), expanded, tree.depth(ix));
260 }
261 }
262 }
263 }
264 self.apply_expansion(expansion, Some(target), origin, cx)
265 }
266
267 fn anchor_toggled(&mut self, line: &PaintedLine) {
270 let Some(screen) = self.painted.iter().position(|painted| painted == line) else {
271 return;
272 };
273 let anchor = match line {
274 PaintedLine::Caption(path) => AnchorLine::Group(path.clone()),
275 PaintedLine::Row(id) => AnchorLine::Row(*id),
276 PaintedLine::Footer(_) => return,
278 PaintedLine::Ghost(_) => return,
280 };
281 self.expand_anchor = Some(LineAnchor::new(anchor, screen, true));
282 }
283
284 fn anchor_focused(&mut self) {
286 let focused = self.selection.focus().and_then(|focus| {
287 Some(match (focus.row(), focus.group()) {
288 (Some(key), _) => PaintedLine::Row(self.snapshot.row_id(key)?),
289 (None, Some(path)) => PaintedLine::Caption(path.clone()),
290 (None, None) => return None,
291 })
292 });
293 if let Some(line) = focused {
294 self.anchor_toggled(&line);
295 }
296 }
297
298 pub(super) fn caption_arrow(&mut self, path: &GroupPath, right: bool, cx: &mut Context<Self>) {
302 let target = ExpandTarget::Group(path.clone());
303 let expanded = self.is_expanded(&target);
304 if right != expanded {
305 self.anchor_focused();
306 self.apply_expanded(target, right, GroupOrigin::User, cx);
307 } else if right {
308 self.navigate(Navigation::Down, false, cx);
309 } else if let Some(parent) = path.parent().filter(|parent| !parent.is_root()) {
310 let focused = self.edit_selection_focus(&parent, SelectionOrigin::User, cx);
311 if focused {
312 self.reveal_focus();
313 cx.notify();
314 }
315 }
316 }
317
318 fn on_expand_row(&mut self, _: &ExpandRow, _: &mut Window, cx: &mut Context<Self>) {
324 if self.would_type_into_focus("+", cx) {
325 cx.propagate();
326 return;
327 }
328 let target = match self.focus_target() {
329 Some(LineTarget::Group(path)) => Some(ExpandTarget::Group(path)),
330 Some(LineTarget::Row(key)) if self.is_tree_branch(&key) => Some(ExpandTarget::Row(key)),
331 _ => None,
332 };
333 match target.filter(|target| !self.is_expanded(target)) {
334 Some(target) => {
335 self.anchor_focused();
336 self.apply_expanded(target, true, GroupOrigin::User, cx);
337 }
338 None => cx.propagate(),
339 }
340 }
341
342 fn on_collapse_row(&mut self, _: &CollapseRow, _: &mut Window, cx: &mut Context<Self>) {
353 if self.would_type_into_focus("-", cx) {
354 cx.propagate();
355 return;
356 }
357 let target = match self.focus_target() {
358 Some(LineTarget::Group(path)) => Some(ExpandTarget::Group(path)),
359 Some(LineTarget::Row(key)) if self.projection.is_tree() => {
360 let branch = ExpandTarget::Row(key.clone());
361 if self.is_tree_branch(&key) && self.is_expanded(&branch) {
362 Some(branch)
363 } else {
364 self.tree_parent(&key).map(ExpandTarget::Row)
365 }
366 }
367 Some(LineTarget::Row(key)) => self
368 .snapshot
369 .row_id(&key)
370 .and_then(|id| self.projection.order_ix(id))
371 .and_then(|order_ix| row_group(&self.projection, order_ix))
372 .map(|group| ExpandTarget::Group(group.path().clone())),
373 None => None,
374 };
375 match target.filter(|target| self.is_expanded(target)) {
376 Some(target) => {
377 let line = match &target {
378 ExpandTarget::Group(path) => Some(PaintedLine::Caption(path.clone())),
379 ExpandTarget::Row(key) => self.snapshot.row_id(key).map(PaintedLine::Row),
380 };
381 if let Some(line) = line {
382 self.anchor_toggled(&line);
383 }
384 self.apply_expanded(target, false, GroupOrigin::User, cx);
385 }
386 None => cx.propagate(),
387 }
388 }
389
390 fn set_depth_from_user(&mut self, depth: ExpandDepth, cx: &mut Context<Self>) {
393 self.apply_expand_depth(depth, GroupOrigin::User, cx);
394 }
395
396 pub(super) fn bind_disclosure_actions<E: InteractiveElement>(root: E, cx: &Context<Self>) -> E {
398 root.on_action(cx.listener(Self::on_expand_row))
399 .on_action(cx.listener(Self::on_collapse_row))
400 .on_action(cx.listener(|this, _: &ExpandAll, _, cx| {
401 this.set_depth_from_user(ExpandDepth::All, cx)
402 }))
403 .on_action(cx.listener(|this, _: &CollapseAll, _, cx| {
404 this.set_depth_from_user(ExpandDepth::Levels(0), cx)
405 }))
406 .on_action(cx.listener(|this, _: &ShowCaptionsOnly, _, cx| {
407 this.set_depth_from_user(ExpandDepth::Captions, cx)
408 }))
409 }
410
411 pub(crate) fn on_a11y_disclosure(
415 &mut self,
416 content: RowContent,
417 key_hash: u64,
418 action: Action,
419 cx: &mut Context<Self>,
420 ) {
421 let line = self.painted.iter().find(|line| match (line, content) {
422 (PaintedLine::Caption(path), RowContent::Caption) => group_hash(path) == key_hash,
423 (PaintedLine::Row(id), RowContent::Data) => self
424 .snapshot
425 .row(*id)
426 .is_some_and(|row| super::key_hash(row.key()) == key_hash),
427 _ => false,
428 });
429 let Some(line) = line.cloned() else {
430 return;
431 };
432 let Some(target) = self.line_expand_target(&line) else {
433 return;
434 };
435 let expanded = match action {
436 Action::Expand => true,
437 Action::Collapse => false,
438 _ => return,
439 };
440 if self.is_expanded(&target) != expanded {
441 self.anchor_toggled(&line);
442 self.apply_expanded(target, expanded, GroupOrigin::User, cx);
443 }
444 }
445
446 #[cfg(any(test, feature = "test-support"))]
452 pub fn send_a11y_action(
453 &mut self,
454 row_id: &gpui_kit::ElementId,
455 action: Action,
456 cx: &mut Context<Self>,
457 ) {
458 let gpui_kit::ElementId::NamedInteger(name, key_hash) = row_id else {
459 return;
460 };
461 let content = match name.as_ref() {
462 "live-grid-group-row" => RowContent::Caption,
463 "live-grid-row" => RowContent::Data,
464 _ => return,
465 };
466 self.on_a11y_disclosure(content, *key_hash, action, cx);
467 }
468
469 pub(super) fn focus_off_collapsed(&mut self, origin: GroupOrigin, cx: &mut Context<Self>) {
475 let Some(target) = self.collapsed_ancestor() else {
476 return;
477 };
478 let mark = self.selection.mark();
479 let column = self.selection.focus().map(|focus| focus.column());
480 if let Some(column) = column {
481 self.selection.focus_on(match target {
482 LineTarget::Group(path) => FocusedCell::caption(path, column),
483 LineTarget::Row(key) => FocusedCell::new(key, column),
484 });
485 }
486 self.finish_change(mark, selection_origin(origin), cx);
487 }
488
489 fn collapsed_ancestor(&self) -> Option<LineTarget<KeyOf<D>>> {
493 let focus = self.selection.focus()?;
494 let (projection, snapshot) = (&self.projection, &self.snapshot);
495 let expansion = self.view.expansion();
496 if let Some(path) = focus.group() {
497 let group = projection.groups().find(path)?;
498 return self
499 .outermost_collapsed(group.parent().and_then(|id| projection.groups().get(id)));
500 }
501 let id = snapshot.row_id(focus.row()?)?;
502 if projection.is_grouped() {
503 let group = row_group(projection, projection.order_ix(id)?)?;
504 return self.outermost_collapsed(Some(group));
505 }
506 if !projection.is_tree() {
507 return None;
508 }
509 let mut ancestors: Vec<KeyOf<D>> = Vec::new();
511 let mut row = snapshot.row(id)?;
512 while ancestors.len() < usize::from(u16::MAX) {
513 let Some(parent) = row
514 .parent_key()
515 .and_then(|parent| snapshot.row_id(parent))
516 .filter(|parent| projection.contains(*parent))
517 else {
518 break;
519 };
520 row = snapshot.row(parent)?;
521 ancestors.push(row.key().clone());
522 }
523 ancestors
524 .iter()
525 .rev()
526 .enumerate()
527 .find(|(depth, key)| !expansion.is_row_expanded(key, *depth as u16))
528 .map(|(_, key)| LineTarget::Row(key.clone()))
529 }
530
531 fn outermost_collapsed(&self, group: Option<GroupInfo<'_>>) -> Option<LineTarget<KeyOf<D>>> {
534 let projection = &self.projection;
535 let expansion = self.view.expansion();
536 let mut collapsed = None;
537 let mut group = group;
538 while let Some(info) = group {
539 let path = info.path();
540 if !expansion.is_group_expanded(path, self.group_levels(path)) {
541 collapsed = Some(path.clone());
542 }
543 group = info.parent().and_then(|id| projection.groups().get(id));
544 }
545 collapsed.map(LineTarget::Group)
546 }
547
548 pub(super) fn follow_shown_lines(&mut self, previous: &Projection) {
555 let Some(focus) = self.selection.focus() else {
556 return;
557 };
558 let column = focus.column();
559 if let Some(path) = focus.group() {
560 if let Some(group) = self.projection.groups().find(path) {
561 if self.projection.group_line(group.id()).is_some() {
562 return;
563 }
564 } else {
565 let place = previous
566 .groups()
567 .find(path)
568 .and_then(|group| previous.group_line(group.id()))
569 .unwrap_or(0);
570 if let Some(cell) = self.focusable_at_or_above(place, column) {
571 self.selection.focus_on(cell);
572 return;
573 }
574 }
575 } else {
576 let shown = focus
577 .row()
578 .and_then(|key| self.snapshot.row_id(key))
579 .map(|id| {
580 self.projection.display_ix(id).is_some() || !self.projection.contains(id)
581 });
582 if shown != Some(false) {
583 return;
584 }
585 }
586 if let Some(target) = self.collapsed_ancestor() {
587 self.selection.focus_on(match target {
588 LineTarget::Group(path) => FocusedCell::caption(path, column),
589 LineTarget::Row(key) => FocusedCell::new(key, column),
590 });
591 }
592 }
593
594 fn focusable_at_or_above(
597 &self,
598 place: usize,
599 column: ColumnOf<D>,
600 ) -> Option<FocusedCell<KeyOf<D>, ColumnOf<D>>> {
601 let (projection, snapshot) = (&self.projection, &self.snapshot);
602 let len = projection.len();
603 let place = place.min(len.checked_sub(1)?);
604 let cell_at = |ix: usize| -> Option<FocusedCell<KeyOf<D>, ColumnOf<D>>> {
605 match projection.line(ix)? {
606 DisplayRow::Data { row, .. } => {
607 Some(FocusedCell::new(snapshot.row(row)?.key().clone(), column))
608 }
609 DisplayRow::GroupCaption { group, .. } => Some(FocusedCell::caption(
610 projection.groups().get(group)?.path().clone(),
611 column,
612 )),
613 _ => None,
614 }
615 };
616 (0..=place)
617 .rev()
618 .find_map(cell_at)
619 .or_else(|| (place + 1..len).find_map(cell_at))
620 }
621
622 fn edit_selection_focus(
624 &mut self,
625 path: &GroupPath,
626 origin: SelectionOrigin,
627 cx: &mut Context<Self>,
628 ) -> bool {
629 let mark = self.selection.mark();
630 let (projection, snapshot) = (self.projection.clone(), self.snapshot.clone());
631 let rows = ShownRows::new(&projection, &snapshot);
632 let focused = self.selection.focus_caption(path, rows, &self.columns);
633 self.finish_change(mark, origin, cx);
634 focused
635 }
636}