Runtime Panel Layout Updates¶
Problem¶
LayoutSpec.panels is fixed at scene creation. Sessions cannot change which controls are visible, swap panel arrangements, or support model-variant switching without emitting a full SceneReady — which reinitializes fields, views, operators, controls, actions, and geometry. That is wasteful: if only the panel arrangement changes, the rest of the scene state should remain cold.
The concrete trigger: scientific apps with multiple model variants each have their own control set. When the user picks a variant, the controls panel should show only that variant's parameters. No mechanism for this exists today.
Design¶
Two new SessionUpdate types.
PanelPatch¶
Surgical update to one panel's contents. Does not affect any other panel or any scene data.
@dataclass(frozen=True, slots=True)
class PanelPatch(SessionUpdate):
panel_id: str
control_ids: tuple[str, ...] | None = None # None = no change
action_ids: tuple[str, ...] | None = None # None = no change
view_ids: tuple[str, ...] | None = None # None = no change
title: str | None = None # None = no change
Semantics:
- None = leave existing value unchanged.
- () = explicitly clear (e.g. remove all controls from a panel).
- Tuple of ids = replace with this set.
Frontend behavior: look up panel by panel_id in scene.layout, apply non-None changes via dataclasses.replace, then trigger RefreshTarget.CONTROLS to re-render the controls widget list. The Qt widget for the panel is kept; only its contents change.
Primary use case: controls panel content swap when model variant changes.
LayoutReplace¶
Replaces the full panel arrangement (panels tuple + grid). Does not touch fields, geometries, views, operators, controls, or actions.
@dataclass(frozen=True, slots=True)
class LayoutReplace(SessionUpdate):
panels: tuple[PanelSpec, ...]
panel_grid: tuple[tuple[str, ...], ...] = ()
Frontend behavior: replace scene.layout.panels and scene.layout.panel_grid, call _rebuild_panels() + _update_panel_visibility(), then trigger a full content refresh so new panels receive their current data.
This is a widget-tree rebuild, not a scene rebuild. Fields and render state are preserved.
Use cases: - Switching between completely different panel arrangements (e.g. compact vs. expanded layout). - Adding or removing panels at runtime (e.g. revealing a second trace panel on demand). - Multiple controls panels, each for a different logical group.
Frontend Reconciliation Rule¶
The frontend maintains a {panel_id → widget} dict. The id is the stability key:
| Update | Panel id stable | Panel id new | Panel id gone |
|---|---|---|---|
PanelPatch |
update in place | ignored | ignored |
LayoutReplace |
update widget contents | create widget | destroy widget |
The RefreshPlanner is not rebuilt on either update — it depends on views, not panels.
Scene Helper¶
LayoutSpec.patch_panel(panel_id, **changes) -> bool applies dataclasses.replace to one panel in the panels tuple. Returns True if the panel was found. Used by the frontend when processing PanelPatch.
Protocol Granularity Rule (Extended)¶
Following the existing rule ("use the narrowest update that correctly describes the change"):
- Use
PanelPatchwhen one panel's contents change (controls list, action list, title). - Use
LayoutReplacewhen panels are added, removed, or rearranged. - Use
SceneReadyonly when fields, views, operators, controls, or geometry change structurally.
Do not emit SceneReady just to swap a controls panel's content. PanelPatch is the right update.
What This Enables¶
- Model variant switching: session emits
ScenePatchto update control definitions for the new variant, thenPanelPatchto updatecontrol_idson the controls panel to show only relevant controls. - Multiple controls panels:
LayoutSpec.panelsalready supports multiplekind="controls"panels.LayoutReplacemakes it possible to add or remove them at runtime. - Composable panel authoring: panels are independent units identified by id. Sessions compose layouts by choosing which panels exist and what they contain.
What This Does Not Enable¶
PanelPatchdoes not support changing a panel'skindorcamera_distance. For structural panel changes, useLayoutReplace.LayoutReplacedoes not change fields, views, or operators. For those, useScenePatchorFieldReplacein the sameread_updates()response.- Neither update type adds new controls or actions to the scene. Controls/actions are defined in
Scene.controls/Scene.actionsat scene creation or viaScenePatch.control_updates.PanelPatchonly changes which existing controls are shown in a panel.
Phase¶
Phase 2 — part of the transition to feature-composable authoring and session-driven layout.