Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 101 additions & 2 deletions crates/base/src/resizable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>) -> 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<ResizableState>,
}

impl Render for CallerStateHarness {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> 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<ResizableState>,
resizes: Rc<Cell<usize>>,
Expand Down
14 changes: 13 additions & 1 deletion crates/base/src/resizable/panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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());
});
}
})
}
Expand Down
Loading