gpui_kit_grid/state/grouping.rs
1//! Grouping and expansion in the grid state: the grouping levels and
2//! aggregates of the view, the expansion the grid owns, the one path each
3//! change takes and the events it emits.
4//!
5//! The grid owns the expansion as it owns the quick filter: it lives in the
6//! view ([`ViewSpec::expansion`]) and survives every new view. Toggles are
7//! kept by [`GroupPath`] and by row key, so a change of grouping silences
8//! them and going back brings them back; only a new default depth clears
9//! them.
10
11use std::{mem, sync::Arc};
12
13use gpui_kit::Context;
14
15use super::{
16 ColumnOf, KeyOf, LiveGridDelegate, LiveGridEvent, LiveGridState,
17 events::{ExpandTarget, GroupOrigin},
18 projection::PendingWork,
19};
20use crate::{
21 engine::{
22 Aggregate, DisplayRow, ExpandDepth, Expansion, GroupBy, GroupPath, Structure, TreeSpec,
23 ViewSpec,
24 },
25 model::GridRow,
26};
27
28/// Counts the edits of the view's expansion. A job and a held order
29/// remember the revision their lines are laid out for, and lines of an
30/// older one are laid out again when shown.
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub(super) struct ExpansionRevision(u64);
33
34impl ExpansionRevision {
35 fn next(self) -> Self {
36 Self(self.0 + 1)
37 }
38}
39
40impl<D: LiveGridDelegate> LiveGridState<D> {
41 /// The grouping levels, the top level first; empty when not grouped.
42 pub fn group_by(&self) -> &[GroupBy<D::Row>] {
43 self.view.group_by()
44 }
45
46 /// Replaces the grouping and rebuilds at once (`GroupChange` with
47 /// `GroupOrigin::Programmatic`). The new order is shown from the top,
48 /// unless the focused line (a row or a group caption) is on screen and
49 /// still shown: it keeps its screen line. Repeated levels are dropped,
50 /// the first one stays; the same grouping again (the same level ids and
51 /// directions) does nothing. Works for columns the group panel does not
52 /// accept. A grouping replaces a tree; no levels turn grouping off and
53 /// leave a tree alone.
54 ///
55 /// The expansion stays: toggles are kept by group path, so those of
56 /// another grouping come back with it.
57 pub fn set_group_by(
58 &mut self,
59 levels: impl IntoIterator<Item = GroupBy<D::Row>>,
60 cx: &mut Context<Self>,
61 ) {
62 self.apply_group_by(levels.into_iter().collect(), GroupOrigin::Programmatic, cx);
63 }
64
65 /// The one path of every grouping change: the group panel and its menu,
66 /// grouping Actions, a layout, `set_group_by`. Drops repeated levels
67 /// (the first one stays) and returns false, doing nothing, if the
68 /// grouping is unchanged. Otherwise releases a held order
69 /// (`OrderHeldChange { held: false }`), emits `GroupChange { origin }`
70 /// and rebuilds at once, as a user action. The new order is shown from
71 /// the top, unless the focused line is on screen and still shown: it
72 /// keeps its screen line (`restore_line_anchor`).
73 pub(crate) fn apply_group_by(
74 &mut self,
75 levels: Vec<GroupBy<D::Row>>,
76 origin: GroupOrigin,
77 cx: &mut Context<Self>,
78 ) -> bool {
79 let mut normalized: Vec<GroupBy<D::Row>> = Vec::with_capacity(levels.len());
80 for level in levels {
81 if !normalized.iter().any(|kept| kept.id() == level.id()) {
82 normalized.push(level);
83 }
84 }
85 if same_levels(&normalized, self.view.group_by()) {
86 return false;
87 }
88 self.view = mem::take(&mut self.view).with_group_by(normalized);
89 self.view_changed(None, Some(origin), cx);
90 true
91 }
92
93 /// The tree of the view, if it builds one; `None` when flat or grouped.
94 pub fn tree(&self) -> Option<&TreeSpec<D::Row>> {
95 self.view.tree()
96 }
97
98 /// The aggregates of group captions, group footers and the totals, by
99 /// column.
100 pub fn aggregates(&self) -> &[(ColumnOf<D>, Aggregate<D::Row>)] {
101 self.view.aggregates()
102 }
103
104 /// Replaces the aggregates of group captions, group footers and the
105 /// totals; a repeated column keeps its last aggregate. The same
106 /// aggregates again (custom ones by identity, in any order) do nothing;
107 /// otherwise the view rebuilds at once, as a user action, keeping the
108 /// scroll position (an aggregate order may move groups), and a held
109 /// order is dropped (`OrderHeldChange { held: false }`). No event of its
110 /// own: the application sets the aggregates, and a layout does not keep
111 /// them.
112 pub fn set_aggregates(
113 &mut self,
114 aggregates: impl IntoIterator<Item = (ColumnOf<D>, Aggregate<D::Row>)>,
115 cx: &mut Context<Self>,
116 ) {
117 let view = self.view.clone().with_aggregates(aggregates);
118 if same_aggregates(view.aggregates(), self.view.aggregates()) {
119 return;
120 }
121 self.view = view;
122 self.restart_projection(cx);
123 cx.notify();
124 }
125
126 /// Sets how deep groups and tree rows are expanded by default
127 /// ([`ExpandDepth::All`] unless set), with nothing toggled. For a grid
128 /// being built; [`Self::set_expand_depth`] changes it later. The
129 /// expansion of a layout given to [`Self::with_layout`], before or after,
130 /// replaces it.
131 pub fn with_expand_depth(mut self, depth: ExpandDepth) -> Self {
132 self.view = mem::take(&mut self.view).with_expansion(Expansion::new(depth));
133 self.expansion_revision = self.expansion_revision.next();
134 // The default layout keeps this depth.
135 self.layout.set_view_depth(depth);
136 self.apply_initial_layout()
137 }
138
139 /// How deep groups and tree rows are expanded by default.
140 pub fn expand_depth(&self) -> ExpandDepth {
141 self.view.expansion().depth()
142 }
143
144 /// Sets the default depth and clears every toggle: expand all
145 /// (`ExpandDepth::All`), collapse all (`Levels(0)`), captions only
146 /// (`Captions`). Emits `ExpansionChange { target: None }` with
147 /// `GroupOrigin::Programmatic`, unless the depth is the same and nothing
148 /// is toggled.
149 pub fn set_expand_depth(&mut self, depth: ExpandDepth, cx: &mut Context<Self>) {
150 self.apply_expand_depth(depth, GroupOrigin::Programmatic, cx);
151 }
152
153 /// [`Self::set_expand_depth`] for `origin`: the Actions and menu items
154 /// of expand all, collapse all and captions only are `User`. Returns
155 /// false if nothing changed.
156 pub(crate) fn apply_expand_depth(
157 &mut self,
158 depth: ExpandDepth,
159 origin: GroupOrigin,
160 cx: &mut Context<Self>,
161 ) -> bool {
162 self.apply_expansion(Expansion::new(depth), None, origin, cx)
163 }
164
165 /// Whether a group or a tree row is expanded now: the view's expansion,
166 /// not the projection shown, which may lag one job behind. The root
167 /// path is expanded. A tree row's depth comes from the shown projection:
168 /// a row it does not hold, or one of a projection that is not a tree,
169 /// is not expanded.
170 pub fn is_expanded(&self, target: &ExpandTarget<KeyOf<D>>) -> bool {
171 let expansion = self.view.expansion();
172 match target {
173 ExpandTarget::Group(path) => {
174 path.is_root() || expansion.is_group_expanded(path, self.group_levels(path))
175 }
176 ExpandTarget::Row(key) => self
177 .tree_row_depth(key)
178 .is_some_and(|depth| expansion.is_row_expanded(key, depth)),
179 }
180 }
181
182 /// Expands or collapses one group or tree row. Returns false, doing
183 /// nothing, if it was so already, for the root path, and for a tree row
184 /// the shown projection does not hold as a tree. Otherwise emits
185 /// `ExpansionChange` with `GroupOrigin::Programmatic` at once; the lines
186 /// are laid out again in the background, without the throttle and the
187 /// reorder hold, and a `ProjectionChange` follows.
188 ///
189 /// A group of another grouping may be toggled too: the toggle shows when
190 /// that grouping comes back. A new row never expands its group: it
191 /// arrives in the group's state.
192 pub fn set_expanded(
193 &mut self,
194 target: ExpandTarget<KeyOf<D>>,
195 expanded: bool,
196 cx: &mut Context<Self>,
197 ) -> bool {
198 self.apply_expanded(target, expanded, GroupOrigin::Programmatic, cx)
199 }
200
201 /// [`Self::set_expanded`] for `origin`: chevrons, keys and menu items
202 /// are `User`.
203 pub(crate) fn apply_expanded(
204 &mut self,
205 target: ExpandTarget<KeyOf<D>>,
206 expanded: bool,
207 origin: GroupOrigin,
208 cx: &mut Context<Self>,
209 ) -> bool {
210 let expansion = self.view.expansion().clone();
211 let expansion = match &target {
212 ExpandTarget::Group(path) => {
213 if path.is_root() {
214 return false;
215 }
216 expansion.with_group(path.clone(), expanded, self.group_levels(path))
217 }
218 ExpandTarget::Row(key) => {
219 let Some(depth) = self.tree_row_depth(key) else {
220 return false;
221 };
222 expansion.with_row(key.clone(), expanded, depth)
223 }
224 };
225 self.apply_expansion(expansion, Some(target), origin, cx)
226 }
227
228 /// The one path of every expansion change: puts `expansion` into the
229 /// view and emits `ExpansionChange { target, origin }` at once, before
230 /// the lines are laid out; returns false, doing nothing, if it equals
231 /// the view's. A grouped or tree view lays its lines out again as a user
232 /// action (`expand`): no throttle, no reorder hold, ahead of a
233 /// data-driven rebuild that waits; a held order stays held and is laid
234 /// out when shown. A flat view has nothing to lay out.
235 ///
236 /// A deep toggle (a line and every descendant) builds the whole
237 /// expansion and passes the line as `target`: one event.
238 ///
239 /// A focused line the new expansion hides moves at once to the caption
240 /// or tree row of the outermost collapsed line above it: `FocusChange`
241 /// after `ExpansionChange`, with the same origin.
242 pub(crate) fn apply_expansion(
243 &mut self,
244 expansion: Expansion<KeyOf<D>>,
245 target: Option<ExpandTarget<KeyOf<D>>>,
246 origin: GroupOrigin,
247 cx: &mut Context<Self>,
248 ) -> bool {
249 if expansion == *self.view.expansion() {
250 return false;
251 }
252 self.view = mem::take(&mut self.view).with_expansion(expansion);
253 self.expansion_revision = self.expansion_revision.next();
254 if self.has_structure() {
255 self.pending = self.pending.with(PendingWork::EXPAND);
256 }
257 cx.emit(LiveGridEvent::ExpansionChange { target, origin });
258 self.focus_off_collapsed(origin, cx);
259 // The chevrons follow the view's expansion in the next frame, which
260 // also starts the layout.
261 cx.notify();
262 true
263 }
264
265 /// The levels a group's default expansion is decided by: the view's
266 /// grouping, or the path's own depth for a group of a deeper one.
267 pub(super) fn group_levels(&self, path: &GroupPath) -> usize {
268 self.view
269 .group_by()
270 .len()
271 .max(usize::from(path.depth()) + 1)
272 }
273
274 /// The depth of a tree row in the shown projection; `None` if the
275 /// projection is not a tree or does not hold the row. A row under a
276 /// collapsed ancestor is not on screen: its depth counts the ancestors
277 /// the projection holds.
278 fn tree_row_depth(&self, key: &KeyOf<D>) -> Option<u16> {
279 let (projection, snapshot) = (&self.projection, &self.snapshot);
280 if !projection.is_tree() {
281 return None;
282 }
283 let id = snapshot.row_id(key)?;
284 if let Some(display_ix) = projection.display_ix(id) {
285 return projection.line(display_ix).map(DisplayRow::depth);
286 }
287 if !projection.contains(id) {
288 return None;
289 }
290 let mut row = snapshot.row(id)?;
291 let mut depth = 0u16;
292 // A depth fits `u16`: the bound also ends a cycle of parents.
293 while depth < u16::MAX {
294 let Some(parent) = row
295 .parent_key()
296 .and_then(|parent| snapshot.row_id(parent))
297 .filter(|parent| projection.contains(*parent))
298 else {
299 break;
300 };
301 depth += 1;
302 row = snapshot.row(parent)?;
303 }
304 Some(depth)
305 }
306}
307
308/// Returns true if two lists of levels group alike: the same level ids and
309/// directions, in order. A group key is known by its id.
310fn same_levels<R: GridRow>(a: &[GroupBy<R>], b: &[GroupBy<R>]) -> bool {
311 a.len() == b.len()
312 && a.iter()
313 .zip(b)
314 .all(|(a, b)| a.id() == b.id() && a.direction() == b.direction())
315}
316
317/// Returns true if two views arrange rows alike: both flat, grouped by the
318/// same levels, or trees of the same column. What a `GroupChange` reports.
319pub(super) fn same_structure<R: GridRow>(a: &ViewSpec<R>, b: &ViewSpec<R>) -> bool {
320 match (a.structure(), b.structure()) {
321 (Structure::Flat, Structure::Flat) => true,
322 (Structure::Groups(_), Structure::Groups(_)) => same_levels(a.group_by(), b.group_by()),
323 (Structure::Tree(a), Structure::Tree(b)) => a.column() == b.column(),
324 _ => false,
325 }
326}
327
328/// Returns true if two sets of aggregates are the same, in any order: built-in
329/// ones by kind, custom ones by identity. Columns are unique in each.
330fn same_aggregates<R: GridRow>(
331 a: &[(R::Column, Aggregate<R>)],
332 b: &[(R::Column, Aggregate<R>)],
333) -> bool {
334 a.len() == b.len()
335 && a.iter().all(|(column, aggregate)| {
336 b.iter()
337 .any(|(other, kept)| other == column && same_aggregate(aggregate, kept))
338 })
339}
340
341fn same_aggregate<R: GridRow>(a: &Aggregate<R>, b: &Aggregate<R>) -> bool {
342 match (a, b) {
343 (Aggregate::Custom(a), Aggregate::Custom(b)) => Arc::ptr_eq(a, b),
344 (Aggregate::Custom(_), _) | (_, Aggregate::Custom(_)) => false,
345 _ => mem::discriminant(a) == mem::discriminant(b),
346 }
347}