Skip to content

resizable: Let an application build its own resize handle - #3302

Merged
madcodelife merged 2 commits into
mainfrom
ihavecoke/public-resize-handle
Oct 7, 2026
Merged

madcodelife merged 2 commits into
mainfrom
ihavecoke/public-resize-handle

Conversation

@ihavecoke

@ihavecoke ihavecoke commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

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>:

- .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

  • 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.

@ihavecoke
ihavecoke marked this pull request as draft September 28, 2026 14:52
@ihavecoke
ihavecoke force-pushed the ihavecoke/public-resize-handle branch 3 times, most recently from 128d93d to 51d7083 Compare October 7, 2026 11:26
`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
ihavecoke force-pushed the ihavecoke/public-resize-handle branch from b09295c to 119231e Compare October 7, 2026 13:02
@ihavecoke
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
madcodelife self-requested a review October 7, 2026 14:15
@madcodelife
madcodelife enabled auto-merge (squash) October 7, 2026 14:16
@madcodelife
madcodelife merged commit bd8ed21 into main Oct 7, 2026
16 checks passed
@madcodelife
madcodelife deleted the ihavecoke/public-resize-handle branch October 7, 2026 14:24
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>
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