graphic design tool
git clone https://git.lucas.co/cce-designer.git
refactor(docks): remove the dock tab system
With the network, the params HUD and the second network editor out of the
docks, the spreadsheet is the one pane a dock can hold, so tabs had
nothing to share a dock with. Gone: State::dock_tabs, add_dock_tab,
show_dock_tab, split_dock_tab, first_empty_dock, tab_dock_of_pane,
TAB_CANDIDATES, the tab radio rows, Add Tab and its page, Move To Own
Plate, plate_page_root and MenuOrigin::of_plate. dock_panes is the
arrangement, still changed by Move To.
The save keeps its dock_tabs field and list shape, one name to a dock,
and an older save's tab group loads as its first dockable name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 26 +++++-----
src/app.rs | 113 +++++++------------------------------------
src/main.rs | 12 ++---
src/menu_page.rs | 38 ++++-----------
src/plate_menu.rs | 140 ++++++------------------------------------------------
src/project.rs | 35 +++++---------
6 files changed, 70 insertions(+), 294 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index f9de9eb..03f6085 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -247,8 +247,12 @@ gone from cce-ui with the wgpu path).
- `src/plate_menu.rs` — the plate menu: what can be done to a pane's PLATE
(`PLATE_SLOTS` — network, params, spreadsheet, playbar; NOT the viewport,
whose plate is the window-spanning lip) — Collapse/Expand, Detach/Reattach,
- Full Width (only with a plate in a side dock to tuck under), the dock's tabs,
- Add Tab, Move To Own Plate and **Move To Left/Right/Bottom**. The network and
+ Full Width (only with a plate in a side dock to tuck under) and **Move To
+ Left/Right/Bottom**. (Docks held TABS until 2026-10-07 — several panes to a
+ dock, with tab rows, Add Tab and Move To Own Plate here — retired once the
+ spreadsheet was the one pane a dock could hold; a save keeps the
+ `dock_tabs` field and its list shape, one name to a dock, and an older
+ save's tab group loads as its first dockable name.) The network and
the params HUD are in no dock, so theirs are Detach alone, and the
network's are in no menu: its Plate page went on 2026-10-07, Detach being the
`detach_circular_window` command in the palette. `plate_menu_rows(idx)` is the
@@ -263,11 +267,8 @@ gone from cce-ui with the wgpu path).
inline there), and the whole menu where a pane has none of its own — the
params pane off a row, the spreadsheet (`open_plate_menu`, at the
pointer). A row from any of them runs through
- `run_plate_menu_action`. Add Tab is a page row too: the menu turns into
- the list of panes where it stood, under a back band to whichever menu it
- turned from (`State::plate_page_from`) — from a Plate page, back to that
- page, whose band goes back to the playbar's menu
- (`State::plate_page_root`). Collapse shrinks a plate to its title stub via
+ `run_plate_menu_action`; a Plate page's band goes back to the menu it was
+ turned from (`State::plate_page_from`). Collapse shrinks a plate to its title stub via
`apply_collapsed_panes`, a post-pass over `positions[..]` (one place, all three
branches); a LEFT press on a collapsed stub expands it, and a right press on any
stub (collapsed or detached) opens its plate menu. `a_plates_rows_are_in_its_right_click_menu`
@@ -2779,8 +2780,7 @@ An older save's `"network2"` in its dock lists names no pane and is dropped
wherever it stood, its `current_path2` and pins ignored
(`an_older_saves_second_network_editor_is_dropped`). With the network and
the params HUD out of the docks too, the spreadsheet is the one dockable
-pane: Move To still moves it between the three docks, the tab rows have
-nothing to offer, and its Full Width row is shown only while a side dock
+pane: Move To still moves it between the three docks, and its Full Width row is shown only while a side dock
holds a plate to tuck under — which, with nothing else dockable, is never.
### The network has no plate (since 2026-10-06)
@@ -2809,7 +2809,7 @@ navigation.
none. The left dock starts EMPTY (`dock_panes` `[NO_PANE, NO_PANE,
spreadsheet]`), so the spreadsheet runs flush to the window's left, where
it stopped short of the network's invisible dock until then;
-`TAB_CANDIDATES` is the spreadsheet alone; the network's
+the network's
plate menu is Detach alone (no tab, Move To or Collapse rows) — and is in
no menu: the network menu's Plate page went the next day, Detach being the
palette's `detach_circular_window` — and
@@ -2943,7 +2943,7 @@ bottom raised by a spreadsheet tucked under it, tabbable and movable.
`AppDrag::RightDockResize`, asked first, its plate being on top.
- **Out of the docks.** The right dock starts EMPTY (`dock_panes`
`[NO_PANE, NO_PANE, spreadsheet]` — the network left the left dock the
- next day); `TAB_CANDIDATES` no longer lists params, its plate menu has no tab, Move To or Collapse rows (Detach
+ next day); the tab candidates (tabs went 2026-10-07) no longer listed params, its plate menu has no tab, Move To or Collapse rows (Detach
stays), `set_pane_collapsed` refuses it, and the pane-state load
takes it out of an older save's tab lists — the dock it fronted fronts
its next tab or empties. A plate moved into the right dock is drawn over
@@ -3022,8 +3022,8 @@ and **Network**, the network menu, while the network overlays the scene
its **Attribute Visualizers** row was one too, into the dialog, until
2026-10-06 — it is a plain row now, opening them in the params HUD); the
network menu's **Add Node**
-(the dialog); the plate rows' **Add Tab** (a page, wherever the plate rows
-are); the node menu's **Rename** (the dialog). Inside the dialog the rows
+(the dialog); the playbar menu's **Plate** (its plate rows); the node
+menu's **Rename** (the dialog). Inside the dialog the rows
that turn it into another list are marked `›` in the chord column and take
the forward swipe too (`dialog::dialog_row_leads`): the palette's Group
Markers. The mark rides the row's chord TEXT (cce-ui's `PAGE_MARK`), and a
diff --git a/src/app.rs b/src/app.rs
index 0a9a7ec..d1ccee1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -284,11 +284,14 @@ pub struct ProjectViewState {
/// window size. None in older saves keeps the live positions.
#[serde(default)]
pub splitters: Option<(f32, f32)>,
- /// The docks' tab groups, Left/Right/Bottom order, pane names with the
- /// ACTIVE tab first. Empty (older saves) keeps the default one-pane-per-
- /// dock arrangement; a list that does not name each core docked pane
- /// exactly once is ignored the same way. ("network2", the second network
- /// editor, removed on 2026-10-07, is dropped from an older save's.)
+ /// What each dock holds, Left/Right/Bottom order: a list of pane names,
+ /// at most one since docks held no tabs (2026-10-07) — the name and the
+ /// list shape kept, so an older save, whose lists were tab groups with
+ /// the active tab first, still loads (its first dockable name is the
+ /// dock's). Empty (older saves) keeps the default arrangement; a set
+ /// that does not name each dockable pane exactly once is ignored the
+ /// same way. ("network2", the second network editor, removed the same
+ /// day, is dropped from an older save's.)
#[serde(default)]
pub dock_tabs: Vec<Vec<String>>,
// `current_path2` and the viewport / params / spreadsheet pins rode
@@ -2833,8 +2836,7 @@ pub enum Dock {
Bottom,
}
-/// `dock_panes` entry for a dock whose tabs were all pulled elsewhere: no
-/// slot index, so `pane_shown` reads it as hidden and the dock lays out
+/// `dock_panes` entry for a dock that holds nothing: no slot index, so `pane_shown` reads it as hidden and the dock lays out
/// nothing. Never a valid `positions[..]` index.
pub const NO_PANE: usize = usize::MAX;
@@ -3015,12 +3017,9 @@ pub struct State {
/// up, nearest last: Commands under Group Markers, the visualizer list
/// under one visualizer.
pub dialog_trail: Vec<crate::dialog::Mode>,
- /// The menu the plate menu's Add Tab page was turned to from.
+ /// The menu a plate PAGE was turned to from (the playbar's Plate row),
+ /// which its back band returns to (`open_plate_page`).
pub plate_page_from: Option<crate::menu_page::MenuOrigin>,
- /// The plate PAGE shown from another menu's Plate row: the plate's slot
- /// and that menu. Add Tab turned to from the page goes back to the page,
- /// and the page back to the menu (`open_plate_page`).
- pub plate_page_root: Option<(usize, crate::menu_page::MenuOrigin)>,
/// A parameter row's right-click menu — the same thread-local; the
/// target is (node id, parameter name) rather than a slot and a row, so
/// it holds across a re-layout of the pane.
@@ -3130,17 +3129,10 @@ pub struct State {
/// Panes shrunk to their title stub, indexed by slot. Only the
/// `plate_menu::PLATE_SLOTS` entries are ever set.
pub collapsed_panes: [bool; WIDGET_COUNT],
- /// Dock occupancy, indexed Left/Right/Bottom. Swapped by the plate
- /// menu's Move To rows.
- /// With tabs this names each dock's ACTIVE pane — always a member of the
- /// dock's `dock_tabs` list — or [`NO_PANE`] for a dock whose tabs were
- /// all pulled elsewhere.
+ /// Dock occupancy, indexed Left/Right/Bottom: the pane each dock holds,
+ /// or [`NO_PANE`]. Swapped by the plate menu's Move To rows. One pane
+ /// to a dock: the tabs that let several share one went on 2026-10-07.
pub dock_panes: [usize; 3],
- /// The panes tabbed into each dock, indexed Left/Right/Bottom. One dock
- /// rect, several panes: only the active one (`dock_panes`) is laid out;
- /// the rest wait as tabs, switched and moved through the plate
- /// menus. Every docked pane lives in exactly ONE dock's list.
- pub dock_tabs: [Vec<usize>; 3],
pub drag_widget: Option<usize>,
/// Where the pointer pressed when `drag_widget` armed — the drag
@@ -4728,19 +4720,16 @@ impl State {
/// Move a plate to a dock, swapping with the pane that held it. The
/// dock-owned dimensions stay put, so the geometry survives the swap.
- /// With tabs, the whole GROUPS trade places — a dot drag moves the
- /// plate and every tab riding it, exactly what the drag shows moving.
pub fn move_pane_to_dock(&mut self, slot: usize, dock: Dock) {
let Some(from) = self.dock_of_pane(slot) else { return };
if from == dock {
return;
}
self.dock_panes.swap(from as usize, dock as usize);
- self.dock_tabs.swap(from as usize, dock as usize);
self.after_dock_change();
}
- /// The full re-sync a dock/tab change needs: layout, panel offsets, the
+ /// The full re-sync a dock change needs: layout, panel offsets, the
/// graphs' grid origins (they follow their pane rects), and the node
/// lists — a fronted pane must not wait for the next unrelated event to
/// fill in.
@@ -4752,65 +4741,6 @@ impl State {
self.sync_nodes();
}
- /// The dock whose TAB LIST holds `slot` — its home whether or not it is
- /// the active tab there ([`Self::dock_of_pane`] finds only actives).
- pub fn tab_dock_of_pane(&self, slot: usize) -> Option<Dock> {
- [Dock::Left, Dock::Right, Dock::Bottom]
- .into_iter()
- .find(|&d| self.dock_tabs[d as usize].contains(&slot))
- }
-
- /// Bring one of a dock's tabs to the front (the corner menu's tab switch).
- pub fn show_dock_tab(&mut self, dock: Dock, slot: usize) {
- if !self.dock_tabs[dock as usize].contains(&slot) || self.dock_panes[dock as usize] == slot {
- return;
- }
- self.dock_panes[dock as usize] = slot;
- self.after_dock_change();
- }
-
- /// Pull `slot` out of its current dock (if it has one — an unplaced pane
- /// simply joins) and tab it into
- /// `dock`, active. The dock it leaves fronts its next remaining tab, or
- /// empties ([`NO_PANE`]) — its rect stays reserved by the dock-owned
- /// dimensions either way, ready for a tab to move back.
- pub fn add_dock_tab(&mut self, dock: Dock, slot: usize) {
- if let Some(from) = self.tab_dock_of_pane(slot) {
- if from == dock {
- self.show_dock_tab(dock, slot);
- return;
- }
- let f = from as usize;
- self.dock_tabs[f].retain(|&s| s != slot);
- if self.dock_panes[f] == slot {
- self.dock_panes[f] = self.dock_tabs[f].first().copied().unwrap_or(NO_PANE);
- }
- }
- self.dock_tabs[dock as usize].push(slot);
- self.dock_panes[dock as usize] = slot;
- self.after_dock_change();
- }
-
- /// The dock a split would move a tab to: the first EMPTY one, Left,
- /// Right, Bottom. There are four tab candidates for three docks, so two
- /// docks can each hold two with none left free — the corner menu reads
- /// this to leave Move To Own Plate out rather than offer a dead row.
- pub fn first_empty_dock(&self) -> Option<Dock> {
- [Dock::Left, Dock::Right, Dock::Bottom]
- .into_iter()
- .find(|&d| self.dock_tabs[d as usize].is_empty())
- }
-
- /// Move `slot` out of its shared dock to the first EMPTY dock — the
- /// corner menu's inverse of Add Tab. No empty dock, no move.
- pub fn split_dock_tab(&mut self, slot: usize) {
- if self.tab_dock_of_pane(slot).is_none() {
- return;
- }
- let Some(empty) = self.first_empty_dock() else { return };
- self.add_dock_tab(empty, slot);
- }
-
pub fn floating_spreadsheet_rect(&self) -> (f32, f32, f32, f32) {
let gap = 18.0_f32;
let fx = gap;
@@ -8549,7 +8479,6 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
dialog_from: None,
dialog_trail: Vec::new(),
plate_page_from: None,
- plate_page_root: None,
param_menu_active: false,
param_menu_actions: Vec::new(),
playbar_menu_active: false,
@@ -8585,11 +8514,6 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
// the params HUD is a dock pane (both live on the scene), and a
// plate moved into either is drawn over them.
dock_panes: [NO_PANE, NO_PANE, SPREADSHEET_IDX],
- dock_tabs: [
- Vec::new(),
- Vec::new(),
- vec![SPREADSHEET_IDX],
- ],
drag_widget: None,
drag_press_cursor: None,
focused_widget: None,
@@ -9626,11 +9550,6 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
};
self.slots.playbar.inner_mut().frame = cce_ui::layout::bevel_width();
- // Tab-aware visibility: a pane WAITING in a dock's tab list
- // is hidden regardless of its View flag — a zero rect alone
- // does not stop the text pass, so a waiting editor's labels
- // would paint over whichever pane fronted.
- let ss_active = self.dock_of_pane(SPREADSHEET_IDX).is_some();
self.slots.header.set_visible(false);
self.slots.status.set_visible(false);
self.slots.playbar.set_visible(self.show_playbar);
@@ -9644,7 +9563,7 @@ pub(crate) fn geometry_to_spreadsheet_columns(geom: &Detail) -> (Vec<String>, Ve
self.slots.right_menubar.set_visible(false);
self.slots.param.set_visible(right_visible);
self.slots.param_menubar.set_visible(false);
- self.slots.spreadsheet.set_visible(spreadsheet_visible && ss_active);
+ self.slots.spreadsheet.set_visible(spreadsheet_visible);
self.slots.spreadsheet_menubar.set_visible(false);
}
}
diff --git a/src/main.rs b/src/main.rs
index b94a6c7..e27f8c6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -486,7 +486,7 @@ mod tests {
assert!(state.plate_menu_actions.contains(&PlateMenuAction::MoveTo(Dock::Right)));
assert!(!state.plate_menu_actions.contains(&PlateMenuAction::MoveTo(Dock::Bottom)), "not to its own dock");
state.close_plate_menu();
- state.run_plate_menu_action(SPREADSHEET_IDX, PlateMenuAction::MoveTo(Dock::Left), (0.0, 0.0));
+ state.run_plate_menu_action(SPREADSHEET_IDX, PlateMenuAction::MoveTo(Dock::Left));
assert_eq!(state.dock_of_pane(SPREADSHEET_IDX), Some(Dock::Left));
assert_eq!(state.pane_in_dock(Dock::Bottom), NO_PANE, "it swapped with the empty dock");
let (x, _, w, _) = state.positions[SPREADSHEET_IDX];
@@ -1514,7 +1514,7 @@ mod tests {
let mut state = State::new(false);
state.resize(1600.0, 900.0, 1.0);
state.execute_menu_action("Show Spreadsheet Pane");
- assert_eq!(state.tab_dock_of_pane(NETWORK_PANEL_IDX), None);
+ assert_eq!(state.dock_of_pane(NETWORK_PANEL_IDX), None);
assert_eq!(state.pane_in_dock(Dock::Left), NO_PANE);
assert_eq!(state.positions[NETWORK_PANEL_IDX], (0.0, HEADER_H, 1600.0, state.body_h()));
assert!(state.slots.get_dyn(crate::slots::CONTENT_IDX).visible(), "shown though it fronts no dock");
@@ -1523,7 +1523,7 @@ mod tests {
state.set_pane_collapsed(NETWORK_PANEL_IDX, true);
assert!(!state.pane_is_collapsed(NETWORK_PANEL_IDX));
state.move_pane_to_dock(NETWORK_PANEL_IDX, Dock::Bottom);
- assert_eq!(state.tab_dock_of_pane(NETWORK_PANEL_IDX), None);
+ assert_eq!(state.dock_of_pane(NETWORK_PANEL_IDX), None);
state.open_plate_menu(NETWORK_PANEL_IDX);
assert_eq!(state.plate_menu_actions, vec![PlateMenuAction::Detach]);
state.close_plate_menu();
@@ -1643,7 +1643,7 @@ mod tests {
b.splitter_layout.splitter1_x);
// The dock arrangement rides the file; the network is in none.
assert_eq!(b.pane_in_dock(crate::app::Dock::Left), crate::slots::SPREADSHEET_IDX, "the spreadsheet loads where it was moved");
- assert_eq!(b.tab_dock_of_pane(crate::slots::NETWORK_PANEL_IDX), None, "the network loads in no dock");
+ assert_eq!(b.dock_of_pane(crate::slots::NETWORK_PANEL_IDX), None, "the network loads in no dock");
// A detached pane window must ignore the same file's pane state.
let mut d = State::new(true);
@@ -2224,8 +2224,8 @@ mod tests {
assert_eq!(b.pane_in_dock(Dock::Left), NO_PANE, "the dock the params fronted is empty");
assert_eq!(b.pane_in_dock(Dock::Right), NO_PANE, "the dock the network fronted is empty");
assert_eq!(b.pane_in_dock(Dock::Bottom), SPREADSHEET_IDX, "the rest of the arrangement loads");
- assert_eq!(b.tab_dock_of_pane(PARAM_IDX), None);
- assert_eq!(b.tab_dock_of_pane(crate::slots::NETWORK_PANEL_IDX), None);
+ assert_eq!(b.dock_of_pane(PARAM_IDX), None);
+ assert_eq!(b.dock_of_pane(crate::slots::NETWORK_PANEL_IDX), None);
let _ = fs::remove_dir_all(&dir);
}
diff --git a/src/menu_page.rs b/src/menu_page.rs
index e2b6399..6faa7c4 100644
--- a/src/menu_page.rs
+++ b/src/menu_page.rs
@@ -19,9 +19,9 @@
//! content, as a horizontal list does: see `cce_ui::widget::side_swipe`.
//!
//! - A page of rows is shown by the menu that owns it (`put_up_menu` with
-//! an `at`): the viewport menu's Style and Markers, the plate rows' Add
-//! Tab — whose back band returns to whichever menu it was turned from,
-//! the playbar menu or a stub's plate menu.
+//! an `at`): the viewport menu's Style, Markers and Network, the
+//! playbar menu's Plate. (The plate rows' Add Tab was one until docks
+//! lost their tabs, 2026-10-07.)
//! - The dialog turned to from a row (`State::dialog_from`) goes back to
//! that menu, shown again at the dialog's corner; inside the dialog, a
//! mode turned to from another (Group Markers from the palette, one
@@ -33,7 +33,7 @@
use crate::app::{NetworkMenuAction, NodeMenuAction, PlaybarMenuAction, State, ViewportMenuAction};
use crate::dialog::Mode;
-use crate::plate_menu::{plate_title, PlateMenuAction};
+use crate::plate_menu::plate_title;
use crate::slots::{DIALOG_IDX, PLAYBAR_IDX};
use cce_ui::widget::context_menu::{self, PageTurn};
use cce_ui::widget::WidgetId;
@@ -51,16 +51,6 @@ pub enum MenuOrigin {
Node(usize),
}
-impl MenuOrigin {
- /// The menu a plate's rows are part of: the playbar menu carries its
- /// plate's rows, every other plate's are a menu alone.
- pub fn of_plate(idx: usize) -> MenuOrigin {
- match idx {
- PLAYBAR_IDX => MenuOrigin::Playbar,
- other => MenuOrigin::Plate(other),
- }
- }
-}
impl ViewportMenuAction {
/// Whether the row turns the menu: its pages, and the visualizers'
@@ -149,11 +139,7 @@ impl State {
MenuOrigin::Viewport => self.show_viewport_menu_page(None, at),
MenuOrigin::Network => self.open_network_context_menu_at(at),
MenuOrigin::Playbar => self.open_playbar_context_menu_at(at),
- // A plate PAGE of another menu goes back to being that page.
- MenuOrigin::Plate(idx) => match self.plate_page_root.filter(|(i, _)| *i == idx) {
- Some((_, root)) => self.open_plate_page(idx, (x, y), root),
- None => self.open_plate_menu_at(idx, at),
- },
+ MenuOrigin::Plate(idx) => self.open_plate_menu_at(idx, at),
MenuOrigin::Node(slot) => self.open_node_context_menu_at(slot, at),
}
}
@@ -197,10 +183,6 @@ impl State {
}
MenuOrigin::Plate(_) if self.plate_page_from.is_some() => {
let from = self.plate_page_from.take().unwrap();
- // Back out of a plate page to its menu: the page is done.
- if !matches!(from, MenuOrigin::Plate(_)) {
- self.plate_page_root = None;
- }
self.close_plate_menu();
self.reopen_menu(from, at.0, at.1);
}
@@ -229,13 +211,9 @@ impl State {
}
_ => return false,
},
- MenuOrigin::Plate(idx) => match self.plate_menu_actions.get(n).copied() {
- Some(PlateMenuAction::AddTabMenu) => {
- self.close_plate_menu();
- self.open_plate_add_tab_menu(idx, at, origin);
- }
- _ => return false,
- },
+ // A plate menu has no page rows (Add Tab was one, until
+ // docks lost their tabs).
+ MenuOrigin::Plate(_) => return false,
MenuOrigin::Node(slot) => match self.node_menu_actions.get(n).copied() {
Some(NodeMenuAction::Rename) => {
self.close_node_menu();
diff --git a/src/plate_menu.rs b/src/plate_menu.rs
index 8ef947c..2beb45c 100644
--- a/src/plate_menu.rs
+++ b/src/plate_menu.rs
@@ -1,6 +1,5 @@
//! The plate menu: what can be done to a pane's PLATE — collapse, detach,
-//! its dock's tabs, where it is docked — as rows of that pane's right-click
-//! menu.
+//! where it is docked — as rows of that pane's right-click menu.
//!
//! Until 2026-10-01 these rows were a menu of their own, opened by a small
//! circular trigger on the top-right of every plate (and that trigger, dragged,
@@ -26,14 +25,6 @@ use cce_ui::widget::plate_dock::{self, PlateDockAction, PlateDockState};
/// its "plate" is the window-spanning lip, not a pane.
pub const PLATE_SLOTS: [usize; 4] = [NETWORK_PANEL_IDX, PARAM_IDX, SPREADSHEET_IDX, PLAYBAR_IDX];
-/// The panes the tab rows offer — the dockable set. The playbar's strip is
-/// not a dock. The params pane is not one (since 2026-10-06): it is a HUD
-/// on the scene, under the plates, laid out from the viewport alone
-/// (`State::params_hud_rect`); nor is the network (since 2026-10-07), which
-/// spans the window. The second network editor, the one closable pane, was
-/// removed the same day.
-pub const TAB_CANDIDATES: [usize; 1] = [SPREADSHEET_IDX];
-
/// What the plate menu can do to its plate.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlateMenuAction {
@@ -49,18 +40,8 @@ pub enum PlateMenuAction {
FullWidth,
/// Spreadsheet: back to the strip between the network and params panes.
BetweenPanes,
- /// Bring this dock's named tab to the front.
- ShowTab(usize),
- /// Pull the named pane out of its dock and tab it into this one, active.
- AddTab(usize),
- /// Turn the menu into the Add Tab page — the list of panes that can be
- /// pulled in ([`State::open_plate_add_tab_menu`]). A page row: its back
- /// band, or a swipe back, returns to the menu it was turned from.
- AddTabMenu,
- /// Move this pane out of its shared dock into the first empty one.
- SplitTab,
- /// Move this pane, and the tabs riding it, to another dock, swapping
- /// with what is there — what dragging the plate's corner used to do.
+ /// Move this pane to another dock, swapping with what is there — what
+ /// dragging the plate's corner used to do.
MoveTo(Dock),
/// A "-" row: engraved, inert — keeps the action list aligned with the
/// option rows so a click on the line dispatches nothing.
@@ -146,8 +127,7 @@ impl State {
}
// Group boundaries are engraved separators ("-" rows — the toolkit
- // convention): window actions | layout spans | tab switching | tab
- // management. Pushed lazily so a group that contributes nothing
+ // convention): window actions | layout spans | Move To. Pushed lazily so a group that contributes nothing
// leaves no orphaned line.
let separate = |options: &mut Vec<String>, actions: &mut Vec<PlateMenuAction>| {
if !options.is_empty() && options.last().map(String::as_str) != Some("-") {
@@ -177,47 +157,16 @@ impl State {
}
}
- // Tabs — only on docked plates (the playbar's strip is not a dock).
- // The dock's other tabs switch to the front; panes docked elsewhere
- // can be pulled in as tabs; a pane sharing its dock can move back
- // out to the empty dock its arrival left behind.
+ // Move To — only on docked plates (the playbar's strip is not a
+ // dock): every other dock, swapping with what is there, the drag the
+ // corner trigger used to start, as rows. (Docks held TABS until
+ // 2026-10-07 — several panes to a dock, switched, added and split
+ // from here — retired when the spreadsheet became the one pane a
+ // dock could hold.)
if let Some(d) = self.dock_of_pane(idx) {
- // The dock's tabs as a RADIO group: every tab listed, the front
- // one marked. Clicking the marked row is a no-op (show_dock_tab
- // declines the already-active slot), so the list reads as state,
- // not just as actions.
separate(&mut options, &mut actions);
- for &t in &self.dock_tabs[d as usize] {
- let mark = if t == idx { cce_ui::widget::context_menu::MARK_ON } else { cce_ui::widget::context_menu::MARK_OFF };
- options.push(format!("{mark}{}", plate_title(t)));
- actions.push(PlateMenuAction::ShowTab(t));
- }
- let mut managed = false;
- let mut manage_row = |options: &mut Vec<String>, actions: &mut Vec<PlateMenuAction>| {
- if !managed {
- separate(options, actions);
- managed = true;
- }
- };
- // ONE "Add Tab" row: clicking it swaps the menu for the page of
- // addable panes, instead of one row per candidate here.
- if !self.plate_add_tab_candidates(idx, d).is_empty() {
- manage_row(&mut options, &mut actions);
- options.push("Add Tab".to_string());
- actions.push(PlateMenuAction::AddTabMenu);
- }
- // Only while a dock is free to take it: with none, the split
- // has nowhere to go and the row would do nothing.
- if self.dock_tabs[d as usize].len() > 1 && self.first_empty_dock().is_some() {
- manage_row(&mut options, &mut actions);
- options.push("Move To Own Plate".to_string());
- actions.push(PlateMenuAction::SplitTab);
- }
- // Every other dock, swapping with what is there — the drag the
- // corner trigger used to start, as rows.
for other in [Dock::Left, Dock::Right, Dock::Bottom] {
if other != d {
- manage_row(&mut options, &mut actions);
options.push(format!("Move To {}", dock_title(other)));
actions.push(PlateMenuAction::MoveTo(other));
}
@@ -239,55 +188,15 @@ impl State {
if options.is_empty() {
return;
}
- // A menu of its own, not another menu's page.
- self.plate_page_root = None;
let target = self.slots.get_dyn(idx).base().id();
self.put_up_menu(at, None, options, 0, target);
- crate::menu_page::mark_page_rows(&actions, |a| a == PlateMenuAction::AddTabMenu);
self.plate_menu_slot = Some(idx);
self.plate_menu_actions = actions;
self.plate_page_from = None;
}
- /// The panes a plate's Add Tab page can offer: docked (or dockable)
- /// elsewhere, not already in this dock's list, not detached.
- fn plate_add_tab_candidates(&self, idx: usize, d: crate::app::Dock) -> Vec<usize> {
- TAB_CANDIDATES
- .into_iter()
- .filter(|&other| {
- other != idx
- && !self.dock_tabs[d as usize].contains(&other)
- && !self.pane_is_detached(other)
- })
- .collect()
- }
-
- /// The Add Tab page: the menu `from` turned in place into the list of
- /// addable panes, under a dimmed header row, its top-left at `at` —
- /// where that menu stood — and a back band to it across the top.
- pub fn open_plate_add_tab_menu(&mut self, idx: usize, at: (f32, f32), from: crate::menu_page::MenuOrigin) {
- let Some(d) = self.dock_of_pane(idx) else { return };
- let candidates = self.plate_add_tab_candidates(idx, d);
- if candidates.is_empty() {
- return;
- }
- let mut options = vec!["Add Tab".to_string()];
- let mut actions = vec![PlateMenuAction::Separator];
- for other in candidates {
- options.push(plate_title(other).to_string());
- actions.push(PlateMenuAction::AddTab(other));
- }
- let target = self.slots.get_dyn(idx).base().id();
- self.put_up_menu(Some(at), Some(from), options, 1, target);
- self.plate_menu_slot = Some(idx);
- self.plate_menu_actions = actions;
- self.plate_page_from = Some(from);
- }
-
- /// `idx`'s plate rows as a PAGE of another menu — the network's or the
- /// playbar's Plate row turned into them — at `at`, under a back band to
- /// `from`. Add Tab turned to from here comes back here, and the band
- /// here goes back to `from` (`State::plate_page_root`).
+ /// `idx`'s plate rows as a PAGE of another menu — the playbar's Plate
+ /// row turned into them — at `at`, under a back band to `from`.
pub fn open_plate_page(&mut self, idx: usize, at: (f32, f32), from: crate::menu_page::MenuOrigin) {
let (options, actions) = self.plate_menu_rows(idx);
if options.is_empty() {
@@ -295,11 +204,9 @@ impl State {
}
let target = self.slots.get_dyn(idx).base().id();
self.put_up_menu(Some(at), Some(from), options, 0, target);
- crate::menu_page::mark_page_rows(&actions, |a| a == PlateMenuAction::AddTabMenu);
self.plate_menu_slot = Some(idx);
self.plate_menu_actions = actions;
self.plate_page_from = Some(from);
- self.plate_page_root = Some((idx, from));
}
pub fn plate_menu_open(&self) -> bool {
@@ -322,10 +229,9 @@ impl State {
if cce_ui::widget::context_menu::hit_test(self.cursor_x, self.cursor_y) {
let row = cce_ui::widget::context_menu::row_at(self.cursor_x, self.cursor_y);
let picked = self.plate_menu_slot.zip(row.and_then(|r| self.plate_menu_actions.get(r).copied()));
- let at = (cce_ui::widget::context_menu::x(), cce_ui::widget::context_menu::y());
self.close_plate_menu();
if let Some((idx, action)) = picked {
- self.run_plate_menu_action(idx, action, at);
+ self.run_plate_menu_action(idx, action);
}
return true;
}
@@ -333,10 +239,8 @@ impl State {
false
}
- /// Run a plate row for `idx`, picked from a menu whose top-left was `at`
- /// — where the Add Tab page goes, turned from the menu the plate's rows
- /// are part of.
- pub fn run_plate_menu_action(&mut self, idx: usize, action: PlateMenuAction, at: (f32, f32)) {
+ /// Run a plate row for `idx`.
+ pub fn run_plate_menu_action(&mut self, idx: usize, action: PlateMenuAction) {
match action {
PlateMenuAction::Collapse => self.set_pane_collapsed(idx, true),
PlateMenuAction::Expand => self.set_pane_collapsed(idx, false),
@@ -344,20 +248,6 @@ impl State {
PlateMenuAction::Reattach => self.reattach_plate(idx),
PlateMenuAction::FullWidth => self.set_spreadsheet_full_width(true),
PlateMenuAction::BetweenPanes => self.set_spreadsheet_full_width(false),
- PlateMenuAction::ShowTab(t) => {
- if let Some(d) = self.dock_of_pane(idx) {
- self.show_dock_tab(d, t);
- }
- }
- PlateMenuAction::AddTab(o) => {
- if let Some(d) = self.dock_of_pane(idx) {
- self.add_dock_tab(d, o);
- }
- }
- PlateMenuAction::AddTabMenu => {
- self.open_plate_add_tab_menu(idx, at, crate::menu_page::MenuOrigin::of_plate(idx))
- }
- PlateMenuAction::SplitTab => self.split_dock_tab(idx),
PlateMenuAction::MoveTo(d) => self.move_pane_to_dock(idx, d),
PlateMenuAction::Separator => {}
}
diff --git a/src/project.rs b/src/project.rs
index 086baf7..f929438 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -114,23 +114,13 @@ impl State {
} else {
None
};
- // Each dock's tabs by name, active first — the order the loader
- // reads back (first = front).
+ // What each dock holds, by name: a list of at most one, the shape
+ // the save has kept since docks held tabs.
let dock_tabs = (0..3)
.map(|d| {
- let active = self.dock_panes[d];
- let mut names: Vec<String> = Vec::new();
- if let Some(n) = crate::plate_menu::pane_name_from_slot(active) {
- names.push(n.to_string());
- }
- for &t in &self.dock_tabs[d] {
- if t != active {
- if let Some(n) = crate::plate_menu::pane_name_from_slot(t) {
- names.push(n.to_string());
- }
- }
- }
- names
+ crate::plate_menu::pane_name_from_slot(self.dock_panes[d])
+ .map(|n| vec![n.to_string()])
+ .unwrap_or_default()
})
.collect();
// A plate's stored size is what was asked for and may exceed a window
@@ -328,10 +318,11 @@ impl State {
.map_or(false, |n| vs.collapsed_panes.iter().any(|c| c == n));
self.set_pane_collapsed(idx, desired);
}
- // Dock tab groups: accepted only whole — three lists whose names
- // resolve and cover each CORE docked pane exactly once. Anything
- // else (older saves' empty list included) keeps the current layout
- // rather than loading half of one.
+ // What the docks hold: accepted only whole — three lists whose names
+ // resolve and cover each dockable pane exactly once. Anything else
+ // (older saves' empty list included) keeps the current layout rather
+ // than loading half of one. A list of an older save is a tab group,
+ // active first; its first dockable name is what the dock holds.
if vs.dock_tabs.len() == 3 {
let resolved: Vec<Vec<usize>> = vs
.dock_tabs
@@ -349,7 +340,7 @@ impl State {
// Neither the params pane nor the network is docked since
// 2026-10-06 (the one a HUD on the scene, the other an overlay
// spanning it): an older save lists them in docks, and they are
- // taken out — a dock one fronted fronts its next tab, or is
+ // taken out — a dock one fronted holds its next tab, or is
// empty.
let undocked = |s: usize| s == crate::slots::PARAM_IDX || s == crate::slots::NETWORK_PANEL_IDX;
let resolved: Vec<Vec<usize>> = resolved
@@ -360,9 +351,7 @@ impl State {
let expected = vec![crate::slots::SPREADSHEET_IDX];
if all == expected {
for d in 0..3 {
- self.dock_tabs[d] = resolved[d].clone();
- self.dock_panes[d] =
- resolved[d].first().copied().unwrap_or(crate::app::NO_PANE);
+ self.dock_panes[d] = resolved[d].first().copied().unwrap_or(crate::app::NO_PANE);
}
self.rebuild_positions();
self.apply_layout();