Repository navigation
resizable: Let an application build its own resize handle - #3302
Merged
Merged
Conversation
ihavecoke
marked this pull request as draft
September 28, 2026 14:52
ihavecoke
force-pushed
the
ihavecoke/public-resize-handle
branch
3 times, most recently
from
October 7, 2026 11:26
128d93d to
51d7083
Compare
`resize_handle_appearance()` is documented as being for a hand-rolled handle in an application, but the handle it draws in was hidden and took no listener but its own `on_drag`. `resize_handle` is now public, and `ResizeHandle` implements `InteractiveElement`, `StatefulInteractiveElement` and `InteractiveElementExt`, so listeners such as `on_drag_move`, `on_hover` and `on_double_click` attach to the band itself, the way they do on a `SliderThumb`. `gpui-component` adds a `resize_handle` that installs the divider appearance. The builder keeps its signature, so Resizable and the Dock are untouched. Its `on_drag` now asks for `T: Clone` and hands GPUI the payload itself rather than an `Rc<T>`: GPUI tells drags apart by the payload's type, so a caller following the drag with `on_drag_move::<T>` never heard one that carried an `Rc<T>`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ihavecoke
force-pushed
the
ihavecoke/public-resize-handle
branch
from
October 7, 2026 13:02
b09295c to
119231e
Compare
ihavecoke
marked this pull request as ready for review
October 7, 2026 13:46
…e` bound Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
madcodelife
self-requested a review
October 7, 2026 14:15
madcodelife
approved these changes
Oct 7, 2026
madcodelife
enabled auto-merge (squash)
October 7, 2026 14:16
linruohan
pushed a commit
to linruohan/gpui-component
that referenced
this pull request
Oct 9, 2026
…#3302) ## Description `resize_handle_appearance()` says it is there for "a hand-rolled handle in an application", but an application could not build one. `resize_handle` was `#[doc(hidden)]` and took no listener beyond its own `on_drag`; `ResizeHandleContext` has no public constructor, so the appearance could not be put on anything else either. This makes the handle a public, standalone part, the way `SliderThumb` is. Resizable and the Dock keep their behavior; only their `on_drag` constructors change, to take a `&T` (see below). - `resize_handle(id, axis)` is public in `gpui-base`. `gpui_component::resize_handle` installs the divider appearance, as `h_resizable` does. - `ResizeHandle` implements `InteractiveElement`, `StatefulInteractiveElement` and `InteractiveElementExt`. The drag still goes through the handle's own `on_drag`; `on_drag_move`, `on_hover`, `on_double_click` and the rest attach to the band itself. Base still owns the band, the cursor, `inside(HandleEdge)` and the Idle → Hovered → Pressed → Dragging progression the appearance reads. - The band carries `test_support()`, so `window.find(id)` finds a handle. Why: an application sidebar has a hand-written edge handle with the same three-level indicator — the same 20 / 28 / 44 px at 0.35 / 0.6 / 0.9. It needs a double click that restores the default width, and a hover its hover-to-peek drawer counts as its own. `h_resizable` itself does not fit a collapsible fixed-width sidebar (it keeps panels proportional as the container resizes, and hides a panel without a transition), so the handle is the part it can share. ## Public API ### `gpui-base` - `pub fn resize_handle<T: 'static, E: 'static + Render>(id: impl Into<ElementId>, axis: Axis) -> ResizeHandle<T, E>` — no longer hidden, signature unchanged; a band that resizes whatever it edges. - `pub struct ResizeHandle<T, E>` — now exported from the crate root. - `impl InteractiveElement`, `impl StatefulInteractiveElement` and `impl InteractiveElementExt` for `ResizeHandle<T, E>` — listeners attach to the band. The handle's own `on_drag` shadows `StatefulInteractiveElement::on_drag`. - `pub fn on_drag(self, value: T, f: impl Fn(&T, &Point<Pixels>, &mut Window, &mut App) -> Entity<E> + 'static) -> Self` — the drag carries the `T` itself instead of an `Rc<T>`, and the constructor is handed a `&T` the way GPUI's own `on_drag` is. GPUI tells drags apart by payload type, so a caller's `on_drag_move::<T>` never heard a drag carrying `Rc<T>`. ### `gpui-component` - `pub fn resize_handle<T: 'static, E: 'static + Render>(id: impl Into<ElementId>, axis: Axis) -> ResizeHandle<T, E>` — the Base handle with `resize_handle_appearance()` installed. - `resizable::{HandleEdge, ResizeHandle}` — re-exported. ## Breaking Changes `ResizeHandle::on_drag` was `#[doc(hidden)]`; its constructor now takes `&T` instead of `Rc<T>`: ```diff - .on_drag(DragPanel, |drag, _, _, cx| cx.new(|_| drag.deref().clone())) + .on_drag(DragPanel, |drag, _, _, cx| cx.new(|_| drag.clone())) ``` ## How to Test 1. `cargo test -p gpui-base --lib resizable` — `a_callers_listeners_reach_the_band` hovers, double clicks and drags a standalone handle, and checks the container stops reading as hovered (the band occludes), the double click fires once, a drag reports moves without clicking, and the appearance still sees Idle → Hovered → Pressed → Dragging → Idle. 2. `cargo test -p gpui-component --lib -- theme:: resizable dock` — the Dock and the divider appearance, unchanged. 3. `cargo run -- resizable` / `cargo run -- dock`: dividers look and behave as before. ## Checklist - [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and followed the guidelines. - [x] 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. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Floyd Wang <gassnake999@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
resize_handle_appearance()says it is there for "a hand-rolled handle in an application", but an application could not build one.resize_handlewas#[doc(hidden)]and took no listener beyond its ownon_drag;ResizeHandleContexthas no public constructor, so the appearance could not be put on anything else either.This makes the handle a public, standalone part, the way
SliderThumbis. Resizable and the Dock keep their behavior; only theiron_dragconstructors change, to take a&T(see below).resize_handle(id, axis)is public ingpui-base.gpui_component::resize_handleinstalls the divider appearance, ash_resizabledoes.ResizeHandleimplementsInteractiveElement,StatefulInteractiveElementandInteractiveElementExt. The drag still goes through the handle's ownon_drag;on_drag_move,on_hover,on_double_clickand the rest attach to the band itself. Base still owns the band, the cursor,inside(HandleEdge)and the Idle → Hovered → Pressed → Dragging progression the appearance reads.test_support(), sowindow.find(id)finds a handle.Why: an application sidebar has a hand-written edge handle with the same three-level indicator — the same 20 / 28 / 44 px at 0.35 / 0.6 / 0.9. It needs a double click that restores the default width, and a hover its hover-to-peek drawer counts as its own.
h_resizableitself does not fit a collapsible fixed-width sidebar (it keeps panels proportional as the container resizes, and hides a panel without a transition), so the handle is the part it can share.Public API
gpui-basepub fn resize_handle<T: 'static, E: 'static + Render>(id: impl Into<ElementId>, axis: Axis) -> ResizeHandle<T, E>— no longer hidden, signature unchanged; a band that resizes whatever it edges.pub struct ResizeHandle<T, E>— now exported from the crate root.impl InteractiveElement,impl StatefulInteractiveElementandimpl InteractiveElementExtforResizeHandle<T, E>— listeners attach to the band. The handle's ownon_dragshadowsStatefulInteractiveElement::on_drag.pub fn on_drag(self, value: T, f: impl Fn(&T, &Point<Pixels>, &mut Window, &mut App) -> Entity<E> + 'static) -> Self— the drag carries theTitself instead of anRc<T>, and the constructor is handed a&Tthe way GPUI's ownon_dragis. GPUI tells drags apart by payload type, so a caller'son_drag_move::<T>never heard a drag carryingRc<T>.gpui-componentpub fn resize_handle<T: 'static, E: 'static + Render>(id: impl Into<ElementId>, axis: Axis) -> ResizeHandle<T, E>— the Base handle withresize_handle_appearance()installed.resizable::{HandleEdge, ResizeHandle}— re-exported.Breaking Changes
ResizeHandle::on_dragwas#[doc(hidden)]; its constructor now takes&Tinstead ofRc<T>:How to Test
cargo test -p gpui-base --lib resizable—a_callers_listeners_reach_the_bandhovers, double clicks and drags a standalone handle, and checks the container stops reading as hovered (the band occludes), the double click fires once, a drag reports moves without clicking, and the appearance still sees Idle → Hovered → Pressed → Dragging → Idle.cargo test -p gpui-component --lib -- theme:: resizable dock— the Dock and the divider appearance, unchanged.cargo run -- resizable/cargo run -- dock: dividers look and behave as before.Checklist
cargo runfor story tests related to the changes.