Skip to content

chat: dockable-panel layout foundation (mouse resize/dock — RFC, coordinating with your interface work)#419

Open
hardlygospel wants to merge 4 commits into
mpiorowski:mainfrom
hardlygospel:feat/dockable-panels
Open

chat: dockable-panel layout foundation (mouse resize/dock — RFC, coordinating with your interface work)#419
hardlygospel wants to merge 4 commits into
mpiorowski:mainfrom
hardlygospel:feat/dockable-panels

Conversation

@hardlygospel

@hardlygospel hardlygospel commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Thanks @mpiorowski! Opened this early to coordinate — I saw you mention your next PR is interface-related, and I'm building mouse-driven dockable panels for the home screen (drag a divider to resize a column; later, drag a panel's header to move it to the other side / reorder — including sending the bonsai over to the left). It started as just the tested model; per your steer below it now also renders from that model and does live divider-resize (steps 1 & 2).

What's here

A pure, self-contained layout model — late-ssh/src/app/common/dock.rs — now wired into the live home screen:

  • DockLayout = the movable panels (room rail + visualizer / music / bonsai / lobby), each with a side, a height weight, and enabled — plus the two column widths. Its default() matches today's layout exactly.
  • frame(area) computes the left/centre/right column rects and stacks each side's panels by weight; columns clamp to a min/max and can never squeeze the centre chat away.
  • hit(point) classifies a mouse point as a column divider (→ resize), a panel header (→ drag to move), or a body; drop_zone(point) picks the side + the panel a drag would land above; resize() / dock() apply clamped edits.

Now wired (steps 1 & 2)

  1. The home renders from DockLayout. The two side columns take their widths from the model instead of the hardcoded 24; default is 24/24, so the render is byte-identical until a divider is dragged — zero behaviour change out of the box.
  2. Drag state machine on the MouseEventKind::{Down,Drag,Up} path. Grab a column border and drag to resize the room rail or the right sidebar live, clamped so the centre chat can never be squeezed away. Reuses the model's hit() / resize(); the column rects are recorded each frame like the existing pet-strip click targets.

Scope note: this is the column-resize half of the drag. Cross-side panel docking (dragging the bonsai header over to the left) is held back on purpose — the right sidebar's contents come from your configurable right_sidebar_components list, so making panels physically movable means reconciling that with the dock model, and I didn't want to change sidebar behaviour under your interface work. It's the natural next slice.

  1. Persisted per user. The layout lives in users.settings under one opaque home_dock_layout key (no migration); DockLayout::to_value()/from_value() use the stable panel/side keys and defensively fill any missing panel from the default. Seeded at session start, saved fire-and-forget when a resize settles. Render also clamps widths to the live area so a wide-terminal save can't squeeze the centre on a narrow one.

Still to come

  • Cross-side panel docking (drag the bonsai header over to the left) — the next slice, once the resize feel is confirmed and the right_sidebar_components reconciliation is agreed.

Tests

cargo test -p late-ssh -p late-core --lib   # green (10 dock)
cargo clippy --lib / cargo fmt --check      # clean

The dock model has 10 unit tests covering the geometry, resize clamps + the drag-resize math, cross-side docking, hit-testing, drop-zone selection, and the JSON persistence round-trip + malformed-blob fallback.

Rebased onto current main. Totally fine to hold or reshape this if it overlaps what you're already doing — I'd rather build it the way that fits your interface work than duplicate it.

@hardlygospel
hardlygospel requested a review from mpiorowski as a code owner July 13, 2026 08:10
@mpiorowski

Copy link
Copy Markdown
Owner

yeah, i would love to test this new idea, so if its not a problem, pls do the follow up of 1 and 2 for now:

  1. Render the home from DockLayout (replace the hardcoded column constraints — default = identical to today, zero behaviour change).
  2. Drag state machine on the existing MouseEventKind::{Down,Drag,Up} path (the Artboard already proves press-hold-drag works here).

Tony Hosaroygard added 2 commits July 18, 2026 07:07
The load-bearing core for a mouse-driven, dockable home layout: a pure,
fully-tested model with no UI wiring yet.

- DockPanel (room rail + visualizer/music/bonsai/lobby), DockSide, and a
  PanelSlot { side, weight, enabled }; DockLayout holds the ordered slots
  plus the two column widths, with a default matching today's layout.
- frame(area) computes the left/centre/right column rects and stacks each
  side's panels by weight; columns clamp to a min/max and never eat the
  centre.
- hit(point) classifies a mouse point as a column divider, a panel header
  (drag to move), or a panel body; drop_zone(point) picks the side + the
  panel a drag would land above; resize()/dock() apply clamped edits.

Seven unit tests cover the geometry, resize clamps, cross-side docking,
hit-testing, and drop-zone selection. Nothing renders from it yet, so it's
additive and safe. cargo test / clippy / fmt green.

Signed-off-by: Tony Hosaroygard <tasmaniamate@gmail.com>
Wire the tested dock layout (common/dock.rs) into the live home screen,
the follow-up asked for on mpiorowski#419:

1. The home's two side columns now take their widths from DockLayout
   instead of the hardcoded 24. The default is 24/24, so the render is
   byte-identical until a divider is moved.
2. A drag state machine on the existing MouseEventKind::{Down,Drag,Up}
   path: pressing a column border grabs its divider, dragging resizes the
   column live (clamped so the centre chat is never squeezed away), and
   release ends it. Reuses the model's hit()/resize(); the column rects
   are recorded each frame like the pet-strip click targets.

Session-only for now - per-user persistence (step 3) is still to come.

Signed-off-by: Tony Hosaroygard <tasmaniamate@gmail.com>
@hardlygospel
hardlygospel force-pushed the feat/dockable-panels branch from 329e58d to fbf03e6 Compare July 17, 2026 21:26
@hardlygospel

Copy link
Copy Markdown
Contributor Author

Thanks Mat — pushed the follow-up for 1 and 2 (rebased onto current main):

1. The home now renders from DockLayout. The two side columns take their widths from the layout model instead of the hardcoded 24. Default is 24/24, so the render is byte-identical until a divider is dragged — zero behaviour change out of the box.

2. A drag state machine on the existing MouseEventKind::{Down,Drag,Up} path. Grab a column border and drag to resize the room rail or the right sidebar live; it's clamped so the centre chat can never be squeezed away. Reuses the model's hit() / resize(), and records the column rects each frame the same way the pet-strip click targets already do.

Scope note (so nothing surprises you): this slice is the column-resize half of the drag. I deliberately held back cross-side panel docking (dragging the bonsai header over to the left) — the right sidebar's contents come from your configurable right_sidebar_components list, so making panels physically movable means reconciling that with the dock model, and I didn't want to quietly change sidebar behaviour under your interface work. Happy to do it as the next slice if the resize feels right, or reshape it however fits your plans.

Session-only for now; per-user persistence (3) is still to come whenever you want it.

  • cargo test -p late-ssh --lib → 1693 pass (8 dock)
  • cargo clippy -p late-ssh --lib / cargo fmt --check → clean

Step 3 of the dockable-panel work (mpiorowski#419): the resized layout now
survives reconnects.

- late-core stores the layout under a home_dock_layout settings key
  (extract_home_dock_layout / set_home_dock_layout); the blob is opaque
  to the core, keyed by the SSH client.
- DockLayout gains to_value()/from_value() using the stable panel/side
  keys, so the JSON survives enum reordering. from_value() is defensive:
  unknown panels are skipped, missing ones are filled from the default
  (so a new panel is never hidden by an old save), bad widths fall back.
- The layout is seeded from the user's settings at session start and
  persisted fire-and-forget when a resize drag settles.
- Render now clamps the column widths to the live area, so a layout
  saved on a wide terminal can never squeeze the centre chat on a narrow
  one (the default 24 is unchanged on normal widths).

Signed-off-by: Tony Hosaroygard <tasmaniamate@gmail.com>
@hardlygospel

Copy link
Copy Markdown
Contributor Author

And 3 — per-user persistence is in too, so the whole thing round-trips now:

  • The layout is stored in your users.settings JSON under one home_dock_layout key (extract_home_dock_layout / set_home_dock_layout in late-core). The blob is opaque to the core — it's keyed by the SSH client, so no migration and nothing else needs to know its shape.
  • DockLayout gained to_value() / from_value() keyed by the stable panel/side keys, so the JSON survives any later reordering of the Rust enums. from_value() is defensive: unknown panels are skipped, any panel missing from the blob is filled from the default (so a panel you add later can never be hidden by an old save), and bad widths fall back.
  • Seeded from settings at session start; persisted fire-and-forget the moment a resize drag settles.
  • One extra safety touch: the render now clamps the column widths to the live terminal area, so a layout saved on a wide terminal can never squeeze the centre chat on a narrow one. The default 24 is unchanged on normal widths.

cargo test -p late-ssh -p late-core --lib green (10 dock tests), clippy/fmt clean. Resize → reconnect → your columns come back. Whenever you get a chance to test it, keen to hear if the feel is right before I build cross-side panel docking on top.

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