Skip to content

Add gui.add_panel() for floating side-by-side control panels#711

Open
ArthurAllshire wants to merge 4 commits into
viser-project:mainfrom
ArthurAllshire:add-panel-api
Open

Add gui.add_panel() for floating side-by-side control panels#711
ArthurAllshire wants to merge 4 commits into
viser-project:mainfrom
ArthurAllshire:add-panel-api

Conversation

@ArthurAllshire
Copy link
Copy Markdown
Contributor

@ArthurAllshire ArthurAllshire commented May 5, 2026

Summary

Screen.Recording.2026-05-04.at.8.10.40.PM_compressed.mp4

Adds viser.GuiApi.add_panel(), which opens a draggable, resizable floating window as a sibling of the main control panel. Useful when the default single-panel layout gets too tall or too narrow — panels can be placed side-by-side and sized independently.

with server.gui.add_panel(
    \"Cameras\",
    initial_position=(\"center\", 20),
    initial_width_px=540,
    layout=\"row\",
) as panel:
    for name in (\"left\", \"top\", \"right\"):
        server.gui.add_image(frame, label=f\"Camera {name}\")

GuiPanelHandle subclasses GuiFolderHandle, so it works as a context manager exactly like folders do — the only difference is that the panel renders as its own floating window instead of inline in the main panel.

Features

  • Drag to move: grab the title bar.
  • Drag to resize: grab the right edge (6px strip, tinted blue on hover).
  • Persistence: per-panel position + width saved to localStorage (keyed by the panel's UUID).
  • Positioning: initial_position=(x, y) accepts int | \"center\" for either axis. Negative integers anchor to the right/bottom edge (e.g. (-20, 20) is 20px from the top-right corner).
  • layout=\"row\": renders children side-by-side with equal flex share, non-wrapping — useful for rows of camera/video feeds.
  • Flicker gate: a resizingRef suppresses the ResizeObserver's reposition pass during an active drag so width updates don't fight it.

Backward-compatible: all changes are additive. SidebarPanel, BottomPanel, and existing FloatingPanel call sites are untouched.

Demo

examples/02_gui/11_panels.py — standalone demo with animated "video" feeds. Shows the main panel alongside three user panels (top-left, centered, bottom-right), with working Zoom slider and Reset button. Run with python examples/02_gui/11_panels.py and open http://localhost:8080.

Stats

13 files changed, 528 insertions(+), 6 deletions(-). Largest changes are in UserPanels.tsx (new, 121 lines — the renderer) and FloatingPanel.tsx (+101, adds the resize handle and initialPosition / onGeometryChange props).

Test plan

  • examples/02_gui/11_panels.py runs; all three panels visible and animate.
  • Drag a panel's header — it moves, doesn't overlap neighbors if you pick them apart.
  • Drag the right edge — panel resizes; no flicker.
  • Reload the page — panel positions and widths persist.
  • Resize the browser window — \"center\" x keeps the Cameras panel horizontally centered; negative-y keeps Status anchored to the bottom.
  • Row layout: at very narrow widths, images shrink together instead of wrapping or overflowing.
  • Existing add_folder, configure_theme, sidebar layout, and bottom (mobile) layout are unchanged — verified via 02_gui/00_basic_controls.py and 02_gui/05_theming.py.

Open questions / happy to bikeshed

API shape wasn't obvious — would love maintainer input on any of these before you merge:

  1. initial_position convention. I used int | \"center\" with negative = right/bottom anchor. Clean in the common case, but the negative-sign thing is a little clever. An alternative would be an explicit anchor: Literal[\"top-left\", \"top-right\", \"top-center\", \"bottom-left\", \"bottom-right\"] = \"top-left\" field with all-positive offsets. Same expressiveness, more verbose.
  2. layout=\"row\". Currently only exists on panels. If this feels generally useful, it could move to add_folder / a new add_row container. Kept it panel-only here to minimize surface area.
  3. Persistence. Lives entirely in UserPanels.tsx and persists by panel UUID. If uuids aren't stable across Python restarts (they aren't — uuid.uuid4()), persistence only survives browser reloads, not server restarts. Happy to key on title instead if that's preferable, or make persistence opt-in via a prop.
  4. Panel-placement collision handling. None — panels are positioned independently; on narrow viewports they can overlap (the demo works around this by staggering y-offsets). Could add a simple stacking fallback in UserPanels.tsx, or leave it to the user.

Also: I ran ruff (passes), npm run typecheck (passes), and smoke-tested existing GUI examples (00_basic_controls.py, 01_callbacks.py, 05_theming.py). I couldn't run pyright locally due to an unrelated environment issue — CI should catch any type problems.


🤖 Generated with Claude Code

Adds viser.GuiApi.add_panel(), which opens a draggable, resizable
floating window as a sibling of the main control panel. Useful when
the default single-panel layout gets too tall or too narrow — panels
can be placed side-by-side and sized independently.

Features:
- Drag title bar to move, right edge to resize.
- Position and width persist to localStorage per-panel uuid.
- initial_position=(x, y); negative values anchor to right/bottom edge.
- layout="row" renders children side-by-side with equal flex share.

Example: examples/02_gui/11_panels.py.
test_docs_coverage.py was failing because GuiPanelHandle wasn't listed
in docs/source/api/handles/gui_handles.rst. Fixed by re-running
`python docs/generate_handle_docs.py`.
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.

1 participant