From dee6cb94bc797487f13d7920f04a8472448d7beb Mon Sep 17 00:00:00 2001 From: Andy Hsu Date: Thu, 8 Oct 2026 22:53:20 +0800 Subject: [PATCH 1/2] table: Allow a header row height separate from the body A taller header was forcing every body row to grow, because both bands used Size::table_row_height. header_height sets the header band only. Page Up and Page Down subtract that band before dividing by the body row height. Co-authored-by: Cursor --- crates/component/src/table/data_table.rs | 14 ++++++++++++- crates/component/src/table/state.rs | 26 ++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/crates/component/src/table/data_table.rs b/crates/component/src/table/data_table.rs index 3017a957f4..f33522bcbd 100644 --- a/crates/component/src/table/data_table.rs +++ b/crates/component/src/table/data_table.rs @@ -8,7 +8,7 @@ use crate::{ }; use gpui::{ App, Edges, Entity, Focusable, InteractiveElement, IntoElement, KeyBinding, ParentElement, - RenderOnce, Styled, Window, div, prelude::FluentBuilder, + Pixels, RenderOnce, Styled, Window, div, prelude::FluentBuilder, }; use gpui_base::TestSupportExt as _; @@ -37,6 +37,8 @@ pub(super) struct TableOptions { pub(super) bordered: bool, /// The cell size of the table. pub(super) size: Size, + /// Height of each header row. `None` uses [`Size::table_row_height`]. + pub(super) header_height: Option, } impl Default for TableOptions { @@ -46,6 +48,7 @@ impl Default for TableOptions { stripe: false, bordered: true, size: Size::default(), + header_height: None, } } } @@ -117,6 +120,15 @@ where self } + /// Set the height of each header row. + /// + /// Body rows keep the height from [`Size::table_row_height`]. When this is + /// unset, header rows use that same height. + pub fn header_height(mut self, height: impl Into) -> Self { + self.options.header_height = Some(height.into()); + self + } + /// Set scrollbar visibility. pub fn scrollbar_visible(mut self, vertical: bool, horizontal: bool) -> Self { self.options.scrollbar_visible = Edges { diff --git a/crates/component/src/table/state.rs b/crates/component/src/table/state.rs index 9b23a4e357..ade12935bc 100644 --- a/crates/component/src/table/state.rs +++ b/crates/component/src/table/state.rs @@ -771,11 +771,24 @@ where .count() } + /// One header row. An unset header height matches the body rows. + fn header_row_height(&self) -> Pixels { + self.options + .header_height + .unwrap_or(self.options.size.table_row_height()) + } + + /// Every header row, including column-group rows. An empty layout still + /// reserves one row, matching the vertical scrollbar's top inset. + fn header_band_height(&self) -> Pixels { + self.header_row_height() * self.header_layout.len().max(1) as f32 + } + fn page_item_count(&self) -> usize { let row_height = self.options.size.table_row_height(); - let height = self.bounds.size.height; - let count = (height / row_height).floor() as usize; - count.saturating_sub(1).max(1) + let body_height = (self.bounds.size.height - self.header_band_height()).max(px(0.)); + let count = (body_height / row_height).floor() as usize; + count.max(1) } fn on_row_right_click( @@ -1821,6 +1834,7 @@ where self.calculate_visible_leaf_col_range(left_columns_count); let layout_len = self.header_layout.len(); + let header_row_height = self.header_row_height(); // Reset fixed head columns bounds, if no fixed columns are present if left_columns_count == 0 { @@ -1881,7 +1895,7 @@ where layout.iter().enumerate().map(|(_row_ix, row_cells)| { h_flex() .min_w_full() - .h(self.options.size.table_row_height()) + .h(header_row_height) .border_b_1() .border_color(cx.theme().border) .children(row_cells.iter().filter_map(|cell| { @@ -1942,7 +1956,7 @@ where let is_leaf_row = row_ix + 1 == layout_len; h_flex() .min_w_full() - .h(self.options.size.table_row_height()) + .h(header_row_height) .border_b_1() .border_color(cx.theme().border) .map(|this| { @@ -2395,7 +2409,7 @@ where Some( div() .absolute() - .top(self.options.size.table_row_height() * self.header_layout.len().max(1) as f32) + .top(self.header_band_height()) .right_0() .bottom_0() .w(Scrollbar::width()) From 8bf0e127b8856ec14419903756e59a255ccf1cf9 Mon Sep 17 00:00:00 2001 From: Floyd Wang Date: Fri, 9 Oct 2026 11:21:36 +0800 Subject: [PATCH 2/2] table: Rename `header_height` to `header_row_height`, add docs and story Co-Authored-By: Claude Opus 5.5 --- crates/component/src/table/data_table.rs | 14 +++--- crates/component/src/table/state.rs | 2 +- crates/story/src/stories/data_table_story.rs | 45 ++++++++++++++++++++ website/component/data-table.md | 5 ++- website/zh-CN/component/data-table.md | 5 ++- 5 files changed, 60 insertions(+), 11 deletions(-) diff --git a/crates/component/src/table/data_table.rs b/crates/component/src/table/data_table.rs index f33522bcbd..f263a603cf 100644 --- a/crates/component/src/table/data_table.rs +++ b/crates/component/src/table/data_table.rs @@ -38,7 +38,7 @@ pub(super) struct TableOptions { /// The cell size of the table. pub(super) size: Size, /// Height of each header row. `None` uses [`Size::table_row_height`]. - pub(super) header_height: Option, + pub(super) header_row_height: Option, } impl Default for TableOptions { @@ -48,7 +48,7 @@ impl Default for TableOptions { stripe: false, bordered: true, size: Size::default(), - header_height: None, + header_row_height: None, } } } @@ -122,10 +122,12 @@ where /// Set the height of each header row. /// - /// Body rows keep the height from [`Size::table_row_height`]. When this is - /// unset, header rows use that same height. - pub fn header_height(mut self, height: impl Into) -> Self { - self.options.header_height = Some(height.into()); + /// Column-group rows use the same height, so the whole header is this + /// height times the number of header rows. Body rows keep the height from + /// [`Size::table_row_height`]. When this is unset, header rows use that + /// same height. + pub fn header_row_height(mut self, height: impl Into) -> Self { + self.options.header_row_height = Some(height.into()); self } diff --git a/crates/component/src/table/state.rs b/crates/component/src/table/state.rs index ade12935bc..a1fb26a1cb 100644 --- a/crates/component/src/table/state.rs +++ b/crates/component/src/table/state.rs @@ -774,7 +774,7 @@ where /// One header row. An unset header height matches the body rows. fn header_row_height(&self) -> Pixels { self.options - .header_height + .header_row_height .unwrap_or(self.options.size.table_row_height()) } diff --git a/crates/story/src/stories/data_table_story.rs b/crates/story/src/stories/data_table_story.rs index 9ce641958e..8b9f73a200 100644 --- a/crates/story/src/stories/data_table_story.rs +++ b/crates/story/src/stories/data_table_story.rs @@ -28,6 +28,10 @@ use crate::story_toolbar_group; #[action(namespace = data_table_story, no_json)] struct ChangeSize(Size); +#[derive(Action, Clone, PartialEq, Eq, Deserialize)] +#[action(namespace = data_table_story, no_json)] +struct ChangeHeaderRowHeight(Option); + #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = data_table_story, no_json)] struct ChangeRows(usize); @@ -898,6 +902,7 @@ pub struct DataTableStory { stripe: bool, refresh_data: bool, size: Size, + header_row_height: Option, _subscriptions: Vec, _load_task: Task<()>, @@ -980,6 +985,7 @@ impl DataTableStory { stripe: false, refresh_data: false, size: Size::default(), + header_row_height: None, _subscriptions, _load_task, _load_rows_task: Task::ready(()), @@ -1059,6 +1065,16 @@ impl DataTableStory { cx.notify(); } + fn on_change_header_row_height( + &mut self, + a: &ChangeHeaderRowHeight, + _: &mut Window, + cx: &mut Context, + ) { + self.header_row_height = a.0; + cx.notify(); + } + fn toggle_refresh_data(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.refresh_data = *checked; cx.notify(); @@ -1173,6 +1189,7 @@ impl Render for DataTableStory { let rows_count = delegate.rows_count(cx); let columns_count = delegate.columns_count(cx); let size = self.size; + let header_row_height = self.header_row_height; let loop_selection = table.loop_selection; let col_resizable = table.col_resizable; let col_movable = table.col_movable; @@ -1190,6 +1207,7 @@ impl Render for DataTableStory { v_flex() .on_action(cx.listener(Self::on_change_size)) + .on_action(cx.listener(Self::on_change_header_row_height)) .on_action(cx.listener(|this, action: &ChangeRows, _, cx| { if action.0 == this.table.read(cx).delegate().stocks.len() { return; @@ -1335,6 +1353,30 @@ impl Render for DataTableStory { ) }, ) + .dropdown_child( + Button::new("header-row-height").label(format!( + "Header Row Height: {}", + header_row_height + .map_or("Default".to_string(), |height| height.to_string()) + )), + move |menu, _, _| { + menu.menu_with_check( + "Default", + header_row_height.is_none(), + Box::new(ChangeHeaderRowHeight(None)), + ) + .menu_with_check( + "40px", + header_row_height == Some(px(40.)), + Box::new(ChangeHeaderRowHeight(Some(px(40.)))), + ) + .menu_with_check( + "48px", + header_row_height == Some(px(48.)), + Box::new(ChangeHeaderRowHeight(Some(px(48.)))), + ) + }, + ) .dropdown_child( Button::new("rows").label(format!("Rows: {rows_count}")), move |menu, _, _| { @@ -1492,6 +1534,9 @@ impl Render for DataTableStory { .child( DataTable::new(&self.table) .with_size(self.size) + .when_some(self.header_row_height, |this, height| { + this.header_row_height(height) + }) .stripe(self.stripe), ) .child( diff --git a/website/component/data-table.md b/website/component/data-table.md index bc6c762fab..614c70df3c 100644 --- a/website/component/data-table.md +++ b/website/component/data-table.md @@ -438,7 +438,7 @@ impl TableDelegate for MyTableDelegate { ### Table Styling -Customize table appearance. `DataTable` implements `Sizable`: use preset sizes such as `.small()` and `.large()` for standard density, or pass a custom pixel size to set a uniform header and body row height. +Customize table appearance. `DataTable` implements `Sizable`: use preset sizes such as `.small()` and `.large()` for standard density, or pass a custom pixel size to set the row height. Header rows use the same height unless you set `header_row_height`, which applies to each header row, column-group rows included. ```rust use gpui_kit::px; @@ -450,7 +450,8 @@ let state = cx.new(|cx| { // In render DataTable::new(&state) - .with_size(px(48.)) // Custom uniform row height + .with_size(px(32.)) // Custom body row height + .header_row_height(px(48.)) // Taller header rows .stripe(true) // Alternating row colors .bordered(true) // Border around table .scrollbar_visible(true, true) // Vertical, horizontal scrollbars diff --git a/website/zh-CN/component/data-table.md b/website/zh-CN/component/data-table.md index bdab756e5f..5b183a2ee2 100644 --- a/website/zh-CN/component/data-table.md +++ b/website/zh-CN/component/data-table.md @@ -350,14 +350,15 @@ impl TableDelegate for MyTableDelegate { ## 表格样式 -`DataTable` 实现了 `Sizable`:可以用 `.small()`、`.large()` 等预设尺寸调整表格密度,也可以传入自定义像素值来设置统一的表头和表体行高。 +`DataTable` 实现了 `Sizable`:可以用 `.small()`、`.large()` 等预设尺寸调整表格密度,也可以传入自定义像素值来设置行高。表头默认沿用这个行高;用 `header_row_height` 可以单独设置每一行表头的高度,分组表头的每一行也使用这个高度。 ```rust use gpui_kit::px; use gpui_kit::component::Sizable as _; DataTable::new(&state) - .with_size(px(48.)) + .with_size(px(32.)) + .header_row_height(px(48.)) .stripe(true) .bordered(true) .scrollbar_visible(true, true)