From 85921a6120d3e5a564d820f9e0b2d49147dff4dc Mon Sep 17 00:00:00 2001 From: "Sergei G." Date: Sat, 5 Sep 2026 10:45:44 +0400 Subject: [PATCH 1/3] list: measure an existing row when the configured item is absent An absent measurement row produces a zero-height wrapper even when other sections contain items. Validate the configured index and fall back to a real row without overwriting the caller's preference. --- crates/component/src/list/list.rs | 95 +++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/crates/component/src/list/list.rs b/crates/component/src/list/list.rs index 732b8da91e..17abab7128 100644 --- a/crates/component/src/list/list.rs +++ b/crates/component/src/list/list.rs @@ -223,6 +223,8 @@ where } /// Set a specific list item for measurement. + /// + /// If the item is absent, measure the first item in the first non-empty section. pub fn set_item_to_measure_index( &mut self, ix: IndexPath, @@ -430,10 +432,21 @@ where // Measure the item_height and section header/footer height. let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - measured_size.item_size = self - .render_list_item(self.item_to_measure_index, window, cx) - .into_any_element() - .layout_as_root(available_space, window, cx); + let requested = self.item_to_measure_index; + let item_to_measure = (requested.section < sections_count + && requested.row < self.delegate.items_count(requested.section, cx)) + .then_some(requested) + .or_else(|| { + (0..sections_count) + .find(|section| self.delegate.items_count(*section, cx) > 0) + .map(|section| IndexPath::default().section(section)) + }); + if let Some(index) = item_to_measure { + measured_size.item_size = self + .render_list_item(index, window, cx) + .into_any_element() + .layout_as_root(available_space, window, cx); + } if let Some(mut el) = self .delegate @@ -778,3 +791,77 @@ where .child(self.state.clone()) } } + +#[cfg(test)] +mod measurement_tests { + use super::*; + use crate::list::ListItem; + use gpui::TestAppContext; + + struct Delegate { + counts: Vec, + } + + impl ListDelegate for Delegate { + type Item = ListItem; + fn sections_count(&self, _: &App) -> usize { + self.counts.len() + } + fn items_count(&self, section: usize, _: &App) -> usize { + self.counts[section] + } + fn set_selected_index( + &mut self, + _: Option, + _: &mut Window, + _: &mut Context>, + ) { + } + fn render_item( + &mut self, + index: IndexPath, + _: &mut Window, + _: &mut Context>, + ) -> Option { + (index.row < *self.counts.get(index.section)?) + .then(|| ListItem::new(index.row).h(px(if index.row == 0 { 36. } else { 48. }))) + } + } + + #[gpui::test] + fn measures_an_existing_row_when_the_requested_item_is_absent(cx: &mut TestAppContext) { + cx.update(crate::init); + let window = cx.add_empty_window(); + window.draw( + gpui::point(px(0.), px(0.)), + size(px(300.), px(300.)), + |window, cx| { + let list = cx.new(|cx| ListState::new(Delegate { counts: vec![0, 2] }, window, cx)); + list.update(cx, |list, cx| { + for (requested, expected_height) in [ + (IndexPath::default(), 36.), + (IndexPath::new(1).section(1), 48.), + (IndexPath::new(99).section(1), 36.), + (IndexPath::new(0).section(99), 36.), + ] { + list.set_item_to_measure_index(requested, window, cx); + list.prepare_items_if_needed(window, cx); + let position = list + .rows_cache + .position_of(&IndexPath::new(0).section(1)) + .unwrap(); + assert_eq!( + list.rows_cache.entries_sizes[position].height, + px(expected_height) + ); + assert_eq!(list.item_to_measure_index, requested); + } + list.delegate.counts = vec![0, 0]; + list.prepare_items_if_needed(window, cx); + assert_eq!(list.rows_cache.items_count(), 0); + }); + div() + }, + ); + } +} From b18d0e805bdfa24c18ba8354c8363d65551bfa2b Mon Sep 17 00:00:00 2001 From: "Sergei G." Date: Sat, 5 Sep 2026 22:53:42 +0400 Subject: [PATCH 2/3] list: clarify measurement fallback and test row restoration --- crates/component/src/list/list.rs | 38 ++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/crates/component/src/list/list.rs b/crates/component/src/list/list.rs index 17abab7128..4d0eb997f2 100644 --- a/crates/component/src/list/list.rs +++ b/crates/component/src/list/list.rs @@ -432,15 +432,17 @@ where // Measure the item_height and section header/footer height. let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + // Keep the configured index so it can be used again when filtered rows return. let requested = self.item_to_measure_index; - let item_to_measure = (requested.section < sections_count - && requested.row < self.delegate.items_count(requested.section, cx)) - .then_some(requested) - .or_else(|| { + let item_to_measure = if requested.section < sections_count + && requested.row < self.delegate.items_count(requested.section, cx) + { + Some(requested) + } else { (0..sections_count) .find(|section| self.delegate.items_count(*section, cx) > 0) .map(|section| IndexPath::default().section(section)) - }); + }; if let Some(index) = item_to_measure { measured_size.item_size = self .render_list_item(index, window, cx) @@ -856,9 +858,29 @@ mod measurement_tests { ); assert_eq!(list.item_to_measure_index, requested); } - list.delegate.counts = vec![0, 0]; - list.prepare_items_if_needed(window, cx); - assert_eq!(list.rows_cache.items_count(), 0); + let requested = IndexPath::new(1).section(1); + list.set_item_to_measure_index(requested, window, cx); + // Filtering removes the requested row, then all rows, before restoring it. + for (counts, expected_height) in [ + (vec![0, 2], Some(48.)), + (vec![0, 1], Some(36.)), + (vec![0, 0], None), + (vec![0, 2], Some(48.)), + ] { + list.delegate.counts = counts; + list.prepare_items_if_needed(window, cx); + if let Some(height) = expected_height { + let position = list + .rows_cache + .position_of(&IndexPath::new(0).section(1)) + .unwrap(); + assert_eq!(list.rows_cache.entries_sizes[position].height, px(height)); + } else { + assert_eq!(list.rows_cache.items_count(), 0); + assert!(list.rows_cache.entries_sizes.is_empty()); + } + assert_eq!(list.item_to_measure_index, requested); + } }); div() }, From 9a70c547cfea6cdf44e85bf29bf1799562a8f483 Mon Sep 17 00:00:00 2001 From: "Sergei G." Date: Sat, 5 Sep 2026 23:10:05 +0400 Subject: [PATCH 3/3] list: clarify temporary measurement fallback comment --- crates/component/src/list/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/component/src/list/list.rs b/crates/component/src/list/list.rs index 4d0eb997f2..b9b72d4aaf 100644 --- a/crates/component/src/list/list.rs +++ b/crates/component/src/list/list.rs @@ -432,7 +432,7 @@ where // Measure the item_height and section header/footer height. let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - // Keep the configured index so it can be used again when filtered rows return. + // Use the fallback for this measurement without overwriting the caller's configured index. let requested = self.item_to_measure_index; let item_to_measure = if requested.section < sections_count && requested.row < self.delegate.items_count(requested.section, cx)