diff --git a/crates/component/src/list/list.rs b/crates/component/src/list/list.rs index 732b8da91e..b9b72d4aaf 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,23 @@ 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); + // 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) + { + 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) + .into_any_element() + .layout_as_root(available_space, window, cx); + } if let Some(mut el) = self .delegate @@ -778,3 +793,97 @@ 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); + } + 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() + }, + ); + } +}