gpui: Recalculate list layout after the window has been resized (#51414)

Finn Eitreim created

Closes #51417

I noticed this bug in the settings menu where when I opened the settings
menu, I could not scroll down through all the available options, eg. on
the initial page I wasn't able to scroll down to privacy. When I saw
that no one else had reported this issue, I figured it may be due to my
setup, and it turns out that using Aerospace, the window manager I use,
was what made this bug visible to me. Because aerospace resizes the
window right after it launches, the originally computed heights for the
list are incorrect, meaning the scroll bar is the wrong size as well.

in the relevant code there was a comment that says "If the width of the
list has changed, invalidate all cached item heights" which wasn't
incorrect per-se, but it just invalidated them without triggering any
re-computation, causing incorrect scroll bars.

My intuition is that window resizes/events that change the width of the
list bounds are fairly rare, so there shouldn't be a large performance
hit from the change.

Also implemented a test that directly showcases the behavior, if you run
the test without the change it fails, as the max_offset_for_scrollbar
will be wrong.

Videos:

Before 


https://github.com/user-attachments/assets/2b680222-7071-4098-863f-519361f0756a

After:


https://github.com/user-attachments/assets/1222a299-23d7-4007-8e88-55d2daccce64


[x] Tests
[x] Video of behavior


Release Notes:

- gpui: fixed list height re-computation when the list width changes.

Change summary

crates/gpui/src/elements/list.rs | 36 ++++++++++++++++++++++++++++++++++
1 file changed, 36 insertions(+)

Detailed changes

crates/gpui/src/elements/list.rs 🔗

@@ -1103,6 +1103,7 @@ impl Element for List {
             );
 
             state.items = new_items;
+            state.measuring_behavior.reset();
         }
 
         let padding = style
@@ -1348,6 +1349,41 @@ mod test {
         assert_eq!(offset.offset_in_item, px(0.));
     }
 
+    #[gpui::test]
+    fn test_measure_all_after_width_change(cx: &mut TestAppContext) {
+        let cx = cx.add_empty_window();
+
+        let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
+
+        struct TestView(ListState);
+        impl Render for TestView {
+            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+                list(self.0.clone(), |_, _, _| {
+                    div().h(px(50.)).w_full().into_any()
+                })
+                .w_full()
+                .h_full()
+            }
+        }
+
+        let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
+
+        // First draw at width 100: all 10 items measured (total 500px).
+        // Viewport is 200px, so max scroll offset should be 300px.
+        cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
+            view.clone().into_any_element()
+        });
+        assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
+
+        // Second draw at a different width: items get invalidated.
+        // Without the fix, max_offset would drop because unmeasured items
+        // contribute 0 height.
+        cx.draw(point(px(0.), px(0.)), size(px(200.), px(200.)), |_, _| {
+            view.into_any_element()
+        });
+        assert_eq!(state.max_offset_for_scrollbar().y, px(300.));
+    }
+
     #[gpui::test]
     fn test_remeasure(cx: &mut TestAppContext) {
         let cx = cx.add_empty_window();