Skip to content

resizable: Fix panel resizing flickering - #2907

Merged
huacnlee merged 3 commits into
longbridge:mainfrom
hlcfan:resizable-settle-layout
Sep 4, 2026
Merged

resizable: Fix panel resizing flickering#2907
huacnlee merged 3 commits into
longbridge:mainfrom
hlcfan:resizable-settle-layout

Conversation

@hlcfan

@hlcfan hlcfan commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Description

It seems after the gpui-base separation, resizing a panel then repaint can cause a flickering in the UI. The fix schedules a repaint after container size adjusted after current draw cycle.

Be

Screenshot

Before

Screen.Recording.2026-09-02.at.10.19.53.AM.mov

After

Screen.Recording.2026-09-02.at.10.30.32.AM.mov

Break Changes

NA

How to Test

  • mixed_sizing_is_stable_between_resize_and_followup_frame

Checklist

  • I have read the CONTRIBUTING document and followed the guidelines.
  • Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate.
  • Passed cargo run for story tests related to the changes.
  • Tested macOS, Windows and Linux platforms performance (if the change is platform-specific)

Comment thread crates/base/src/resizable/panel.rs Outdated
// until the current effect cycle has completed. Otherwise
// the settling frame can remain pending until pointer input
// triggers a repaint, making the divider jump on hover.
window.defer(cx, |window, _| window.refresh());

@huacnlee huacnlee Sep 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This obviously is a monkey patch, not a really to solve the root problem.

huacnlee and others added 2 commits September 4, 2026 21:27
…ow refresh

`window.refresh()` sets `Window::refreshing`, which disables the view
element cache for the whole window, so every `.cached(...)` view
re-renders and re-lays-out on the settling frame. The dock renders its
tab panels that way and builds its splits from resizable groups, so the
cost lands exactly where resizables are used most.

A deferred `cx.notify()` on the state entity schedules the same settling
frame without touching that cache: `invalidate_view` marks the window
dirty once the draw has finished, and a cached view still re-renders
when its own bounds change. Measured on a `.cached(...)` sibling view, a
container resize goes from one forced re-render to none.

The comment is corrected too. A refresh raised mid-draw is not coalesced
into that draw, it is dropped by the `not_drawing()` guard; and a notify
raised mid-draw records the view as dirty without scheduling a frame,
which is why the deferral is what makes either one work.

Adds a regression test for the `with_state` path the dock takes, which
has no `use_keyed_state` observer behind it to turn a state change into
a redraw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UezrY94M8EFhB5r1w1Rn3S
@huacnlee
huacnlee enabled auto-merge (squash) September 4, 2026 13:30
@huacnlee

huacnlee commented Sep 4, 2026

Copy link
Copy Markdown
Member

I pushed a follow-up commit to this branch (f6198f2e) rather than asking for another round. The diagnosis in this PR is right and the deferral is necessary — I only narrowed what gets deferred.

window.refresh() sets Window::refreshing, and the view element cache in gpui's view.rs reuses a prepaint only when !window.refreshing. So the settling frame re-renders and re-lays-out every .cached(...) view in the window. This repo has exactly one such call site — crates/ui/src/dock/tab_panel.rs:730 — and the dock builds its splits from resizable groups (dock_area.rs:1398), so the cost lands precisely where resizable groups are used most and panels are heaviest.

A deferred cx.notify() on the state entity schedules the same frame without touching the cache: Invalidator::invalidate_view marks the window dirty once draw_phase is back to None, and a cached view still re-renders when its own bounds actually change. Measured with a .cached(...) sibling view, one container resize goes from one forced re-render to zero, and your mixed_sizing_is_stable_between_resize_and_followup_frame still passes unchanged.

I also corrected the comment. A refresh raised mid-draw is not coalesced into that draw — Window::refresh is guarded by if self.invalidator.not_drawing(), so it is dropped outright. And a notify raised mid-draw records the view as dirty without setting dirty, so no frame is scheduled. Those are two different failure modes, and the deferral is what fixes both.

One addition: the group's state can come from use_keyed_state (which installs an observer turning a state change into a view notify) or from with_state, which the dock uses at dock_area.rs:1443 and which has no such observer. I added caller_owned_state_settles_on_the_same_frame to cover that second path, since your test only exercises the first. Without the fix it settles at 440px and then drops to 360px on the next frame; with it, both frames read 360px.

Two things I did not change, for you to decide:

  1. adjust_to_container_size rescales every panel proportionally, including one the caller pinned with .size(px(240.)). On an 800 → 1200 container that sidebar becomes 360px. ResizablePanel's own doc comment presents .flex_none() as the way to hold a fixed width, but the adjustment does not consult it. This PR does not introduce that, it just makes it happen immediately and on every resize instead of on some later repaint.
  2. assert_ne!(settled_frame, before) in the new test encodes that behavior as the contract, so whoever fixes (1) will have to delete the assertion. Worth a comment saying it is descriptive rather than intended.

🤖 Review assisted by Claude Code

@huacnlee
huacnlee merged commit 4a654f4 into longbridge:main Sep 4, 2026
9 checks passed
@hlcfan
hlcfan deleted the resizable-settle-layout branch September 4, 2026 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants