Skip to content

Commit c197d24

Browse files
committed
Add data-driven group_by API for DataTable row grouping
TableState::group_by(&[col_ix]) scans delegate.cell_text() to automatically build a group tree from flat data, rendering group header rows with expand/collapse toggles on the right side. - Multi-level grouping via group_by(&[1, 0]) with nested children - Group tree is cached and only recomputed when row count or columns change, avoiding per-frame cell_text() scanning - Delegate can customize toggle icon (render_row_toggle) and group header row (render_group_tr) - TableEvent::ToggleGroup emitted on expand/collapse - groups() accessor returns all group info
1 parent bfe5780 commit c197d24

4 files changed

Lines changed: 399 additions & 30 deletions

File tree

crates/story/src/stories/data_table_story.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ pub struct DataTableStory {
742742
num_extra_cols_input: Entity<InputState>,
743743
stripe: bool,
744744
refresh_data: bool,
745+
group_by_enabled: bool,
745746
size: Size,
746747

747748
_subscriptions: Vec<Subscription>,
@@ -841,6 +842,7 @@ impl DataTableStory {
841842
num_extra_cols_input,
842843
stripe: false,
843844
refresh_data: false,
845+
group_by_enabled: false,
844846
size: Size::default(),
845847
_subscriptions,
846848
_load_task,
@@ -964,6 +966,22 @@ impl DataTableStory {
964966
cx.notify();
965967
}
966968

969+
fn toggle_group_by(&mut self, checked: &bool, _: &mut Window, cx: &mut Context<Self>) {
970+
self.group_by_enabled = *checked;
971+
self.table.update(cx, |table, cx| {
972+
if *checked {
973+
table
974+
.delegate_mut()
975+
.stocks
976+
.sort_by(|a, b| a.counter.market.cmp(&b.counter.market));
977+
table.set_group_by(&[1], cx);
978+
} else {
979+
table.set_group_by(&[], cx);
980+
}
981+
});
982+
cx.notify();
983+
}
984+
967985
fn on_change_size(&mut self, a: &ChangeSize, _: &mut Window, cx: &mut Context<Self>) {
968986
self.size = a.0;
969987
cx.notify();
@@ -1011,6 +1029,9 @@ impl DataTableStory {
10111029
TableEvent::ClearSelection => {
10121030
println!("Selection cleared");
10131031
}
1032+
TableEvent::ToggleGroup(group_ix, expanded) => {
1033+
println!("Toggle group: {}, expanded={}", group_ix, expanded);
1034+
}
10141035
}
10151036
}
10161037

@@ -1166,6 +1187,12 @@ impl Render for DataTableStory {
11661187
.label("Group Headers")
11671188
.checked(self.table.read(cx).delegate().show_group_headers)
11681189
.on_click(cx.listener(Self::toggle_group_headers)),
1190+
)
1191+
.child(
1192+
Checkbox::new("group-by-market")
1193+
.label("Group by Market")
1194+
.selected(self.group_by_enabled)
1195+
.on_click(cx.listener(Self::toggle_group_by)),
11691196
),
11701197
)
11711198
.child(

crates/ui/src/table/data_table.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ where
9898
{
9999
/// Create a new DataTable element with the given [`TableState`].
100100
pub fn new(state: &Entity<TableState<D>>) -> Self {
101-
Self { state: state.clone(), options: TableOptions::default() }
101+
Self {
102+
state: state.clone(),
103+
options: TableOptions::default(),
104+
}
102105
}
103106

104107
/// Set to use stripe style of the table, default to false.
@@ -115,8 +118,11 @@ where
115118

116119
/// Set scrollbar visibility.
117120
pub fn scrollbar_visible(mut self, vertical: bool, horizontal: bool) -> Self {
118-
self.options.scrollbar_visible =
119-
Edges { right: vertical, bottom: horizontal, ..Default::default() };
121+
self.options.scrollbar_visible = Edges {
122+
right: vertical,
123+
bottom: horizontal,
124+
..Default::default()
125+
};
120126
self
121127
}
122128
}
@@ -158,7 +164,9 @@ where
158164
.on_action(window.listener_for(&self.state, TableState::action_select_page_down))
159165
.bg(cx.theme().table)
160166
.when(bordered, |this| {
161-
this.rounded(cx.theme().radius).border_1().border_color(cx.theme().border)
167+
this.rounded(cx.theme().radius)
168+
.border_1()
169+
.border_color(cx.theme().border)
162170
})
163171
.child(self.state)
164172
}

crates/ui/src/table/delegate.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,41 @@ pub trait TableDelegate: Sized + 'static {
226226
fn cell_text(&self, row_ix: usize, col_ix: usize, cx: &App) -> String {
227227
String::new()
228228
}
229+
230+
// ── Row Grouping ────────────────────────────────────────────────
231+
232+
/// Render the toggle icon (chevron) for a group header row.
233+
///
234+
/// The default implementation draws a `ChevronRight` / `ChevronDown`
235+
/// icon. Override to customise the icon or spacing.
236+
fn render_row_toggle(
237+
&mut self,
238+
is_expanded: bool,
239+
_window: &mut Window,
240+
cx: &mut Context<TableState<Self>>,
241+
) -> impl IntoElement {
242+
let icon = if is_expanded {
243+
IconName::ChevronDown
244+
} else {
245+
IconName::ChevronRight
246+
};
247+
div()
248+
.flex()
249+
.items_center()
250+
.justify_center()
251+
.child(Icon::new(icon).size_3())
252+
}
253+
254+
/// Render a group header row. Called for each group row
255+
/// produced by [`TableState::group_by`].
256+
///
257+
/// The default renders a label showing the group value and count.
258+
fn render_group_tr(
259+
&mut self,
260+
group: &crate::table::GroupInfo,
261+
_window: &mut Window,
262+
_cx: &mut Context<TableState<Self>>,
263+
) -> Stateful<Div> {
264+
div().id(group.key.clone()).child(group.label.clone())
265+
}
229266
}

0 commit comments

Comments
 (0)