diff --git a/crates/base/src/resizable/mod.rs b/crates/base/src/resizable/mod.rs index f4e4c52721..1f3f9ed8a7 100644 --- a/crates/base/src/resizable/mod.rs +++ b/crates/base/src/resizable/mod.rs @@ -400,12 +400,111 @@ mod tests { use gpui::{ AppContext as _, Context, InteractiveElement as _, IntoElement, Modifiers, MouseButton, - ParentElement as _, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, - point, px, size, + ParentElement as _, Pixels, Render, Styled as _, TestAppContext, VisualTestContext, Window, + div, point, px, size, }; use super::{ResizableState, h_resizable, resizable_panel}; + struct MixedSizingHarness { + width: Pixels, + } + + impl Render for MixedSizingHarness { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div().w(self.width).h(px(100.)).child( + h_resizable("mixed-sizing") + .child( + resizable_panel() + .size(px(240.)) + .child(div().size_full().debug_selector(|| "fixed-sidebar".into())), + ) + .child( + resizable_panel().child( + div() + .size_full() + .debug_selector(|| "flexible-content".into()), + ), + ), + ) + } + } + + #[gpui::test] + fn mixed_sizing_is_stable_between_resize_and_followup_frame(cx: &mut TestAppContext) { + let (view, cx) = cx.add_window_view(|_, _| MixedSizingHarness { width: px(800.) }); + cx.update(|window, cx| { + window.draw(cx).clear(cx); + window.draw(cx).clear(cx); + }); + let before = cx.debug_bounds("fixed-sidebar").unwrap().size.width; + + view.update(cx, |view, cx| { + view.width = px(1200.); + cx.notify(); + }); + cx.run_until_parked(); + let settled_frame = cx.debug_bounds("fixed-sidebar").unwrap().size.width; + cx.update(|window, cx| window.draw(cx).clear(cx)); + let followup_frame = cx.debug_bounds("fixed-sidebar").unwrap().size.width; + + // Resizable panels preserve their proportional sizing across a + // container resize; the important invariant is that applying the + // state on the follow-up frame does not move the divider again. + assert_ne!(settled_frame, before); + assert_eq!(followup_frame, settled_frame); + } + + struct CallerStateHarness { + width: Pixels, + state: gpui::Entity, + } + + impl Render for CallerStateHarness { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div().w(self.width).h(px(100.)).child( + h_resizable("caller-state") + .with_state(&self.state) + .child( + resizable_panel() + .size(px(240.)) + .child(div().size_full().debug_selector(|| "cs-sidebar".into())), + ) + .child(resizable_panel().child(div().size_full())), + ) + } + } + + /// A group whose state the caller owns (`with_state`, as the dock does) + /// has no `use_keyed_state` observer behind it, so the settling frame has + /// to be scheduled by the deferred notify rather than by that observer. + #[gpui::test] + fn caller_owned_state_settles_on_the_same_frame(cx: &mut TestAppContext) { + let state = cx.update(|cx| cx.new(|_| ResizableState::default())); + let (view, cx) = cx.add_window_view({ + let state = state.clone(); + move |_, _| CallerStateHarness { + width: px(800.), + state, + } + }); + cx.update(|window, cx| { + window.draw(cx).clear(cx); + window.draw(cx).clear(cx); + }); + + view.update(cx, |view, cx| { + view.width = px(1200.); + cx.notify(); + }); + cx.run_until_parked(); + let settled = cx.debug_bounds("cs-sidebar").unwrap().size.width; + cx.update(|window, cx| window.draw(cx).clear(cx)); + let followup = cx.debug_bounds("cs-sidebar").unwrap().size.width; + + assert_eq!(followup, settled, "settling frame must not be pending"); + } + struct ResizableHarness { state: gpui::Entity, resizes: Rc>, diff --git a/crates/base/src/resizable/panel.rs b/crates/base/src/resizable/panel.rs index 8cacd7dc0f..d4032077f7 100644 --- a/crates/base/src/resizable/panel.rs +++ b/crates/base/src/resizable/panel.rs @@ -174,7 +174,7 @@ impl RenderOnce for ResizablePanelGroup { ) .on_prepaint({ let state = state.clone(); - move |bounds, _, cx| { + move |bounds, window, cx| { state.update(cx, |state, cx| { let size_changed = state.bounds.size.along(self.axis) != bounds.size.along(self.axis); @@ -183,6 +183,18 @@ impl RenderOnce for ResizablePanelGroup { if size_changed { state.adjust_to_container_size(cx); + // The adjustment lands after this frame's layout has + // already been computed, and a notify raised during a + // draw only records the view as dirty without scheduling + // a frame for it. Defer the notify so it runs once the + // draw has finished and can schedule the settling frame. + // Otherwise that frame stays pending until some later + // input repaints the window, and the divider appears to + // jump on hover. + let state = cx.entity(); + window.defer(cx, move |_, cx| { + state.update(cx, |_, cx| cx.notify()); + }); } }) }