graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: menu rows that lead somewhere turn the menu, by a press or a side swipe
One kind of row for what was two: the viewport menu's Style and Markers
flew a submenu out on hover, while Add Node, Add Tab, Attribute
Visualizers and Rename swapped the menu for another plate on a click,
mostly with no way back. Every such row is now a page row (cce-ui's
set_row_page): it wears ›, and a press on it or a two-finger swipe to the
side over it turns the menu into what it names, with the new plate's
top-left where the menu's was. A swipe the other way turns back; a page of
rows also has a ‹ back band.
src/menu_page.rs holds the one dispatch (run_menu_turn), MenuOrigin and
reopen_menu. The dialog remembers the menu it was turned from
(dialog_from) and the modes it turned through (dialog_trail), so a swipe
back from one visualizer goes to the list, and from there to the viewport
menu. The palette's Group Markers / Attribute Visualizers and the
visualizer rows are page rows of the dialog too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 95 ++++++++++++++------
src/app.rs | 213 +++++++++++++++++++++++++-------------------
src/dialog.rs | 116 +++++++++++++++++++++++-
src/main.rs | 155 +++++++++++++++++++-------------
src/menu_page.rs | 260 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/plate_menu.rs | 43 ++++-----
6 files changed, 680 insertions(+), 202 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index eb7483b..ab5cd2c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -259,8 +259,10 @@ gone from cce-ui with the wgpu path).
playbar's (`PlaybarMenuAction::Plate`), and the whole menu where a pane has
none of its own — the params pane off a row, the spreadsheet, the second
network editor (`open_plate_menu`, at the pointer). A row from any of them runs
- through `run_plate_menu_action`, whose Add Tab / Back page swaps open where the
- menu stood. Collapse shrinks a plate to its title stub via
+ through `run_plate_menu_action`. Add Tab is a PAGE row (see "Page rows"
+ below): the menu turns into the list of panes where it stood, under a back
+ band to whichever menu it turned from — the network's, the playbar's, or the
+ plate menu alone (`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`
@@ -2618,15 +2620,63 @@ ON a node still opens that node's menu, which is the more specific thing under
the pointer. Until 2026-09-22 the empty-space press opened the **add-node
palette** outright, which left the network the one pane whose right-click was
not a context menu, and left every other graph-wide command reachable only by
-chord or through the palette. **Add Node is the first row** instead, and picking
-it opens the same palette — ON THE MENU'S CORNER (since 2026-10-01): the menu
-transforms into the list, as the palette transforms into Group Markers, where
-it used to vanish for a plate centred across the window. `Dialog::anchor` holds
+chord or through the palette. **Add Node is the first row** instead, a PAGE
+row (see "Page rows" below): a press, or a side swipe forward over it, turns
+the menu into the same palette ON THE MENU'S CORNER (since 2026-10-01), and a
+swipe back turns the palette back into the menu. `Dialog::anchor` holds
the corner and `dialog::layout_at` places the plate there, giving up height
(down to `ANCHORED_MIN_H`) before it moves up and pulling in from the right
edge; every other opening clears the anchor and centres, Tab's Add Node
included.
+### Page rows: a menu turns into what a row names (since 2026-10-02)
+
+`src/menu_page.rs`. A row of a context menu that leads to another plate is a
+PAGE row (cce-ui's `context_menu::set_row_page`; see its CLAUDE.md, "A row
+can lead to a page"): it wears `›`, and a press on it, or a two-finger swipe
+to the side with the pointer on it, TURNS the menu into what it names with
+the new plate's top-left where the menu's was. A swipe the other way, from
+anywhere on the new plate, turns back; a page of rows also has a back band
+(`‹ Viewport`) for a press. Under natural scrolling forward is the fingers
+going LEFT, as the content goes (cce-ui's `side_swipe`). Until this the
+viewport menu's Style and Markers flew a second menu out on hover while the
+other rows below swapped the plate on a click — some with a Back row, most
+with no way back — two gestures for one idea.
+
+The page rows: the viewport menu's **Style** and **Markers** (pages of rows)
+and **Attribute Visualizers** (the dialog); 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
+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 and Attribute Visualizers, a visualizer and Add Visualizer.
+
+- **`State::run_menu_turn` is the one dispatch**, reached by a left press
+ (`press_menu_turn`, ahead of every menu's own click handler) and by a swipe
+ (`take_menu_turn`, from the wheel arm, which now routes the wheel to ANY
+ open menu, not only the slider menus). `MenuOrigin` names the menu a turn
+ came from; `reopen_menu` shows it again at a corner (each menu's opener
+ takes an `at`, through `put_up_menu`). The wheel arm first asks cce-ui's
+ `side_swipe::swallow`: what is left of a swipe that turned is dropped,
+ so the end of a swipe back from the wide Add Node list does not orbit
+ the scene the narrower menu uncovers.
+- **A page of the viewport menu stays up while its rows run**: a switch
+ flips and is re-marked in place (`refill_viewport_menu`, cce-ui's
+ `refill`), a slider is worked; a row of the menu itself runs and closes
+ it, as before.
+- **The dialog remembers where it was turned from**: `State::dialog_from`,
+ the menu (shown again at the dialog's corner by a swipe back), and
+ `State::dialog_trail`, the modes it turned through while up — a mode
+ opened while the dialog is up keeps the plate where it stands and puts
+ the mode it leaves on the trail, and turning to the trail's last (by a
+ swipe back, Escape out of a visualizer or its Back row) takes it off.
+ The palette's Group Markers and Attribute Visualizers rows now run with
+ the palette still up, so it is on the trail. Opened afresh, the dialog
+ has neither. A swipe back with neither does nothing.
+
+`the_viewport_menu_turns_into_its_pages_and_back` drives the viewport
+menu's pages, the back band and both swipes, into the dialog and back.
+
Rows are `NETWORK_MENU_COMMANDS` — a list of COMMAND IDS, `None` for a
separator — resolved through `command::by_id`, so a label is the registry's
label and `NetworkMenuAction::Command(id)` dispatches through `run_command`.
@@ -2959,7 +3009,8 @@ as `step_buttons` in `get_state`'s playbar block.
### Display mode: the viewport menu, and smooth shading
-The viewport's right-click menu has two SUBMENUS (since 2026-09-29;
+The viewport's right-click menu has two PAGES (since 2026-09-29, as
+flyout submenus until 2026-10-02 — see "Page rows";
until then it was one list of some twenty rows). The menu itself holds
what is done — Frame All, View 1:1 — the guides (Show Grid, Show Origin,
and since 2026-09-29 Show Camera Pivot with a Camera Pivot Size slider
@@ -2971,7 +3022,7 @@ the reference CUBE guide was removed on 2026-09-25 — its command, mesh,
RT-scene copy, settings field and menubar item, with the Guides menubar
addressed through `GUIDES_MENU` / `GUIDE_*` so no item slid onto another's
action, while old files carrying `show_cube_enabled` still load), and a
-row for each submenu: **Style** (how the geometry is drawn: the
+row for each page: **Style** (how the geometry is drawn: the
wireframe's switch, thickness and opacity, then the surface's shading,
opacity and Show Occluded) and **Markers** (what is drawn on it: Group
Marker Size and Pull Arrow Scale; then the
@@ -2979,25 +3030,15 @@ overlays a class at a time — Show Point Markers and its size, Show Point
Numbers, Show Point Normals; Show Primitive Numbers, Show Primitive
Normals; Show Vertex Markers, Show Vertex Numbers, Show Vertex Normals).
-**The submenus are cce-ui's** (`context_menu::set_row_submenu`, see its
-CLAUDE.md, "A row can open a submenu"): a second menu in its own popup
-that flies out beside the row under the pointer, which the toolkit opens,
-closes and places. What this app does is fill and dispatch.
+**The pages are turned to in place** (see "Page rows"):
`viewport_menu_rows_of(page)` is the rows of the menu (`None`) or of a
-submenu; `fill_viewport_submenus` hands each `ViewportMenuAction::Submenu`
-row its rows and sliders, read from the live state, at the open and again
-after anything a submenu row did — the toolkit changes an open submenu
-where it stands, which is how a mark follows its switch — and
-`open_viewport_submenu_actions` is the open submenu's actions, by
-`submenu::parent_row`. **A row of a submenu keeps both menus up**: a
-submenu is a panel of settings, opened to set several, so they close on a
-press outside or Escape; a row of the menu itself closes it as before.
-The slider hooks are unchanged, the toolkit's pointer calls answering for
-both menus; `drain_viewport_menu_slider` drains the menu's sliders and
-then the submenu's. For one day the two were PAGES of the one popup,
-entered by a row and left by a Back row, because the toolkit had no
-submenu. `the_viewport_menu_flies_its_settings_out_beside_it` drives it
-by pointer.
+page, `show_viewport_menu_page` puts either up — at the pointer, or at the
+corner of the plate it replaces with a back band to the menu — and
+`State::viewport_menu_page` says which is up. `viewport_menu_actions` is
+always the shown rows' actions, so the slider hooks and
+`drain_viewport_menu_slider` need nothing per page. History: on
+2026-09-29 the two were pages of the one popup with a Back row, then the
+same day flyout submenus (cce-ui's, retired with this), then pages again.
**The primitive and vertex overlays** (`toggle_prim_numbers`,
`toggle_prim_normals`, `toggle_vertex_numbers`, the same day) are
@@ -3236,7 +3277,7 @@ the later over the earlier, as a chain of Visualize nodes composites.
display settings.
- **They are edited in the dialog**, two modes: `Mode::Visualizers` (the
`attribute_visualizers` command — the palette, and a row of the viewport
- menu under its submenus) lists them, a switch each, and Add Visualizer;
+ menu under its pages, a page row turning the menu into the list) lists them, a switch each, and Add Visualizer;
a press on a row's SWITCH turns it on or off and a press on the rest of
the row, or Enter, opens it (`Dialog::activated_on_control`,
`take_dialog_pick_at`). `Mode::VisualizerEdit` (`State::vis_editing`) is
diff --git a/src/app.rs b/src/app.rs
index 19d15e4..7af2480 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -442,8 +442,8 @@ pub const GUIDE_GRID: usize = 0;
pub const GUIDE_ORIGIN: usize = 1;
pub const GUIDE_CAMERA_PIVOT: usize = 2;
-/// A submenu of the viewport menu: a second menu that flies out beside its
-/// row (cce-ui's `context_menu::SubmenuSpec`).
+/// A page of the viewport menu: a row of the menu that the menu turns into
+/// (see `crate::menu_page`), with a way back to the menu at its top.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewportMenuPage {
/// How the geometry itself is drawn: the wireframe and the surface.
@@ -464,8 +464,8 @@ impl ViewportMenuPage {
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ViewportMenuAction {
- /// A row that opens a submenu beside it. The menu stays open.
- Submenu(ViewportMenuPage),
+ /// A row that turns the menu into one of its pages.
+ Page(ViewportMenuPage),
/// Move the active camera so the visible node geometry fills the view.
FrameAll,
/// Put the pivot plane at true size: one world unit (the Guides "World
@@ -2613,6 +2613,18 @@ pub struct State {
/// machinery as the node menu; this flag says the open menu is OURS).
pub viewport_menu_active: bool,
pub viewport_menu_actions: Vec<ViewportMenuAction>,
+ /// Which page of the viewport menu is up: `None` for the menu itself.
+ pub viewport_menu_page: Option<ViewportMenuPage>,
+ /// The menu the dialog was turned to from by a page row (Add Node,
+ /// Rename, Attribute Visualizers), which a swipe back from it shows
+ /// again — see `crate::menu_page`.
+ pub dialog_from: Option<crate::menu_page::MenuOrigin>,
+ /// The dialog's own modes it was turned through on the way to the one
+ /// 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.
+ pub plate_page_from: Option<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.
@@ -5399,12 +5411,18 @@ impl State {
}
fn open_node_context_menu(&mut self, slot: usize) {
+ self.open_node_context_menu_at(slot, None);
+ }
+
+ /// A node's menu at the pointer, or with its top-left at `at`.
+ pub(crate) fn open_node_context_menu_at(&mut self, slot: usize, at: Option<(f32, f32)>) {
let (options, actions) = self.node_menu_rows(slot);
if options.is_empty() {
return;
}
let target = self.slots.get_dyn(CONTENT_IDX).base().id();
- cce_ui::widget::context_menu::show(self.cursor_x, self.cursor_y, options, 0, target);
+ self.put_up_menu(at, None, options, 0, target);
+ crate::menu_page::mark_page_rows(&actions, |a| a == NodeMenuAction::Rename);
self.node_menu_slot = Some(slot);
self.node_menu_actions = actions;
}
@@ -5413,7 +5431,7 @@ impl State {
cce_ui::widget::context_menu::is_visible() && self.node_menu_slot.is_some()
}
- fn close_node_menu(&mut self) {
+ pub(crate) fn close_node_menu(&mut self) {
cce_ui::widget::context_menu::hide();
self.node_menu_slot = None;
self.node_menu_actions.clear();
@@ -5929,9 +5947,15 @@ impl State {
}
pub(crate) fn open_playbar_context_menu(&mut self) {
+ self.open_playbar_context_menu_at(None);
+ }
+
+ /// The playbar's menu at the pointer, or with its top-left at `at`.
+ pub(crate) fn open_playbar_context_menu_at(&mut self, at: Option<(f32, f32)>) {
let (options, actions) = self.playbar_menu_rows();
let target = self.slots.get_dyn(PLAYBAR_IDX).base().id();
- cce_ui::widget::context_menu::show(self.cursor_x, self.cursor_y, options, 0, target);
+ self.put_up_menu(at, None, options, 0, target);
+ crate::menu_page::mark_page_rows(&actions, |a| a == PlaybarMenuAction::Plate(crate::plate_menu::PlateMenuAction::AddTabMenu));
for (i, a) in actions.iter().enumerate() {
if let Some(slider) = self.playbar_menu_slider(*a) {
cce_ui::widget::context_menu::set_row_slider(i, slider);
@@ -5945,7 +5969,7 @@ impl State {
cce_ui::widget::context_menu::is_visible() && self.playbar_menu_active
}
- fn close_playbar_menu(&mut self) {
+ pub(crate) fn close_playbar_menu(&mut self) {
cce_ui::widget::context_menu::hide();
self.playbar_menu_active = false;
self.playbar_menu_actions.clear();
@@ -6146,39 +6170,39 @@ impl State {
/// Open the viewport right-click context menu at the cursor.
pub(crate) fn open_viewport_context_menu(&mut self) {
- let (options, actions) = self.viewport_menu_rows();
+ self.show_viewport_menu_page(None, None);
+ }
+
+ /// Put up the viewport menu (`None`) or one of its pages: at the
+ /// pointer, or with its top-left at `at` in place of the plate that
+ /// stood there — a page under a back band to the menu.
+ pub(crate) fn show_viewport_menu_page(&mut self, page: Option<ViewportMenuPage>, at: Option<(f32, f32)>) {
+ let (options, actions) = self.viewport_menu_rows_of(page);
let target = self.slots.viewport.id();
- cce_ui::widget::context_menu::show(self.cursor_x, self.cursor_y, options, 0, target);
+ let back = page.map(|_| crate::menu_page::MenuOrigin::Viewport);
+ self.put_up_menu(at, back, options, 0, target);
for (i, a) in actions.iter().enumerate() {
if let Some(slider) = self.viewport_menu_slider(*a) {
cce_ui::widget::context_menu::set_row_slider(i, slider);
}
}
+ crate::menu_page::mark_page_rows(&actions, |a| a.leads_to_page());
self.viewport_menu_actions = actions;
- self.fill_viewport_submenus();
+ self.viewport_menu_page = page;
self.viewport_menu_active = true;
}
- /// Give each submenu row of the open viewport menu its submenu, read
- /// from the live state: at the open, and again after anything a row of
- /// one did, which is how its marks and readouts follow. The menu changes
- /// an open submenu where it stands.
- pub(crate) fn fill_viewport_submenus(&self) {
- use cce_ui::widget::context_menu::{self, SubmenuSpec};
- for (i, a) in self.viewport_menu_actions.iter().enumerate() {
- let ViewportMenuAction::Submenu(page) = *a else { continue };
- let (options, actions) = self.viewport_menu_rows_of(Some(page));
- let sliders = actions.iter().map(|a| self.viewport_menu_slider(*a)).collect();
- context_menu::set_row_submenu(i, SubmenuSpec { options, header_count: 0, sliders });
- }
- }
-
- /// The rows' actions of the submenu that is open, if one is.
- pub(crate) fn open_viewport_submenu_actions(&self) -> Option<Vec<ViewportMenuAction>> {
- let row = cce_ui::widget::context_menu::submenu::parent_row()?;
- match self.viewport_menu_actions.get(row)? {
- ViewportMenuAction::Submenu(page) => Some(self.viewport_menu_rows_of(Some(*page)).1),
- _ => None,
+ /// Re-read the shown viewport menu page's labels and slider values in
+ /// place, after a row of it ran and it stays up: how a switch's mark
+ /// follows the switch.
+ pub(crate) fn refill_viewport_menu(&mut self) {
+ let (options, actions) = self.viewport_menu_rows_of(self.viewport_menu_page);
+ let sliders: Vec<_> = actions.iter().map(|a| self.viewport_menu_slider(*a)).collect();
+ if cce_ui::widget::context_menu::refill(options, &sliders) {
+ self.viewport_menu_actions = actions;
+ } else {
+ let at = (cce_ui::widget::context_menu::x(), cce_ui::widget::context_menu::y());
+ self.show_viewport_menu_page(self.viewport_menu_page, Some(at));
}
}
@@ -6348,13 +6372,8 @@ impl State {
/// drag does once, on the release.
pub(crate) fn drain_viewport_menu_slider(&mut self, persist: bool) -> bool {
use cce_ui::widget::context_menu;
- // The menu's own sliders, then those of its open submenu.
let changed = context_menu::take_slider_change()
- .and_then(|(idx, v)| Some((self.viewport_menu_actions.get(idx).copied()?, v)))
- .or_else(|| {
- let (idx, v) = context_menu::submenu::take_slider_change()?;
- Some((self.open_viewport_submenu_actions()?.get(idx).copied()?, v))
- });
+ .and_then(|(idx, v)| Some((self.viewport_menu_actions.get(idx).copied()?, v)));
let Some((action, v)) = changed else {
if persist {
self.save_settings();
@@ -6364,8 +6383,6 @@ impl State {
{
if self.viewport_menu_slider(action).is_some() {
self.land_viewport_menu_slider(action, v);
- // What a submenu is opened with next time is what is so now.
- self.fill_viewport_submenus();
if persist {
self.save_settings();
}
@@ -6390,13 +6407,14 @@ impl State {
self.viewport_menu_rows_of(None)
}
- /// Open the submenu of the open viewport menu that holds `action`, as a
- /// press on its row does, and hand back that submenu's actions.
+ /// Turn the open viewport menu to the page that holds `action`, as a
+ /// press on its row does, and hand back that page's actions.
#[cfg(test)]
- pub(crate) fn open_viewport_submenu_with(&mut self, action: ViewportMenuAction) -> Vec<ViewportMenuAction> {
- let page = self.viewport_menu_page_of(action).expect("a submenu holds the row");
- self.run_viewport_menu_action(ViewportMenuAction::Submenu(page));
- self.open_viewport_submenu_actions().expect("the submenu opened")
+ pub(crate) fn open_viewport_page_with(&mut self, action: ViewportMenuAction) -> Vec<ViewportMenuAction> {
+ let page = self.viewport_menu_page_of(action).expect("a page holds the row");
+ self.run_viewport_menu_action(ViewportMenuAction::Page(page));
+ assert_eq!(self.viewport_menu_page, Some(page), "the menu turned");
+ self.viewport_menu_actions.clone()
}
/// The page of the viewport menu that holds `action`, `None` for the
@@ -6409,10 +6427,10 @@ impl State {
.find(|page| self.viewport_menu_rows_of(Some(*page)).1.contains(&action))
}
- /// The rows of the viewport menu (`None`) or of one of its submenus.
+ /// The rows of the viewport menu (`None`) or of one of its pages.
/// The menu holds what is DONE (framing), the guides, and a row for each
- /// submenu; the STYLE submenu holds how the geometry is drawn
- /// (wireframe, then surface) and the MARKERS submenu what is drawn on
+ /// page; the STYLE page holds how the geometry is drawn
+ /// (wireframe, then surface) and the MARKERS page what is drawn on
/// it (the points, then the overlays of each element class).
pub(crate) fn viewport_menu_rows_of(&self, page: Option<ViewportMenuPage>) -> (Vec<String>, Vec<ViewportMenuAction>) {
let mut options: Vec<String> = Vec::new();
@@ -6486,13 +6504,13 @@ impl State {
toggle(&mut options, &mut actions, "toggle_camera_pivot");
row(&mut options, &mut actions, "Camera Pivot Size".into(), ViewportMenuAction::CameraPivotSizeSlider);
- // The display settings, a submenu each; the menu marks the rows.
+ // The display settings, a page each, and the visualizers' editor,
+ // which is the dialog: three rows the menu turns into what they
+ // name.
row(&mut options, &mut actions, "-".into(), sep);
for page in [ViewportMenuPage::Style, ViewportMenuPage::Markers] {
- row(&mut options, &mut actions, page.label().into(), ViewportMenuAction::Submenu(page));
+ row(&mut options, &mut actions, page.label().into(), ViewportMenuAction::Page(page));
}
- // The visualizers' editor, which is the dialog — a list is not a
- // submenu's shape.
row(
&mut options,
&mut actions,
@@ -6532,12 +6550,10 @@ impl State {
ViewportMenuAction::OneToOne => {
self.view_one_to_one();
}
- // The menu opens a submenu under the pointer by itself; a press
- // on the row opens it for a pointer that has not moved.
- ViewportMenuAction::Submenu(_) => {
- if let Some(i) = self.viewport_menu_actions.iter().position(|a| *a == action) {
- cce_ui::widget::context_menu::open_submenu(i);
- }
+ // The menu turns into the page where it stands.
+ ViewportMenuAction::Page(page) => {
+ let at = (cce_ui::widget::context_menu::x(), cce_ui::widget::context_menu::y());
+ self.show_viewport_menu_page(Some(page), Some(at));
}
ViewportMenuAction::PinFollow => {
self.viewport_pin = None;
@@ -6571,10 +6587,11 @@ impl State {
cce_ui::widget::context_menu::is_visible() && self.viewport_menu_active
}
- fn close_viewport_menu(&mut self) {
+ pub(crate) fn close_viewport_menu(&mut self) {
cce_ui::widget::context_menu::hide();
self.viewport_menu_active = false;
self.viewport_menu_actions.clear();
+ self.viewport_menu_page = None;
}
/// Route a left press while the viewport menu is open — same contract as
@@ -6590,30 +6607,25 @@ impl State {
return true;
}
use cce_ui::widget::context_menu;
- // A row of the open submenu runs and the menu stays up: a submenu
- // is a panel of settings, opened to set several, and a switch that
- // closed the menu would cost a right-click and a row per setting.
- // It is filled again for its marks.
- if context_menu::submenu::hit_test(self.cursor_x, self.cursor_y) {
- let picked = context_menu::submenu::row_at(self.cursor_x, self.cursor_y)
- .and_then(|i| self.open_viewport_submenu_actions()?.get(i).copied());
- if let Some(action) = picked {
- self.run_viewport_menu_action(action);
- self.fill_viewport_submenus();
- }
- return true;
- }
if context_menu::hit_test(self.cursor_x, self.cursor_y) {
let idx = context_menu::row_at(self.cursor_x, self.cursor_y);
let picked = idx.and_then(|i| self.viewport_menu_actions.get(i).copied());
- match picked {
- Some(a @ ViewportMenuAction::Submenu(_)) => self.run_viewport_menu_action(a),
- picked => {
- self.close_viewport_menu();
- if let Some(action) = picked {
- self.run_viewport_menu_action(action);
+ // A row of a PAGE runs and the page stays up, re-marked: a page
+ // is a panel of settings, opened to set several, and a switch
+ // that closed it would cost a right-click and a turn per
+ // setting. A row of the menu itself runs and closes it.
+ if self.viewport_menu_page.is_some() {
+ if let Some(action) = picked {
+ self.run_viewport_menu_action(action);
+ if self.viewport_menu_open() {
+ self.refill_viewport_menu();
}
}
+ return true;
+ }
+ self.close_viewport_menu();
+ if let Some(action) = picked {
+ self.run_viewport_menu_action(action);
}
return true;
}
@@ -6636,6 +6648,12 @@ impl State {
/// the viewport menu's radio rows use, read through
/// `command_toggle_state` — the one table the dialog's switches read too.
fn open_network_context_menu(&mut self) {
+ self.open_network_context_menu_at(None);
+ }
+
+ /// The network menu at the pointer, or with its top-left at `at` in
+ /// place of a page it is turned back to from.
+ pub(crate) fn open_network_context_menu_at(&mut self, at: Option<(f32, f32)>) {
let mut options: Vec<String> = Vec::new();
let mut actions: Vec<NetworkMenuAction> = Vec::new();
for entry in NETWORK_MENU_COMMANDS {
@@ -6671,7 +6689,8 @@ impl State {
}
let target = self.slots.get_dyn(CONTENT_IDX).base().id();
- cce_ui::widget::context_menu::show(self.cursor_x, self.cursor_y, options, 0, target);
+ self.put_up_menu(at, None, options, 0, target);
+ crate::menu_page::mark_page_rows(&actions, |a| a.leads_to_page());
self.network_menu_active = true;
self.network_menu_actions = actions;
}
@@ -6680,7 +6699,7 @@ impl State {
cce_ui::widget::context_menu::is_visible() && self.network_menu_active
}
- fn close_network_menu(&mut self) {
+ pub(crate) fn close_network_menu(&mut self) {
cce_ui::widget::context_menu::hide();
self.network_menu_active = false;
self.network_menu_actions.clear();
@@ -6697,13 +6716,12 @@ impl State {
if cce_ui::widget::context_menu::hit_test(self.cursor_x, self.cursor_y) {
let idx = cce_ui::widget::context_menu::row_at(self.cursor_x, self.cursor_y);
let picked = idx.and_then(|i| self.network_menu_actions.get(i).copied());
- // Where the menu stands, read before it is hidden: Add Node
- // opens its list on the menu's corner, so the menu turns into
- // the list rather than vanishing for one across the window.
+ // Where the menu stands, read before it is hidden. The rows
+ // that turn the menu (Add Node, Add Tab) were taken by the
+ // press's page turn ahead of this.
let corner = (cce_ui::widget::context_menu::x(), cce_ui::widget::context_menu::y());
self.close_network_menu();
match picked {
- Some(NetworkMenuAction::Command("add_node")) => self.open_node_palette_at(corner.0, corner.1),
Some(NetworkMenuAction::Command(id)) => {
self.run_command(id);
}
@@ -7820,6 +7838,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
node_menu_actions: Vec::new(),
viewport_menu_active: false,
viewport_menu_actions: Vec::new(),
+ viewport_menu_page: None,
+ dialog_from: None,
+ dialog_trail: Vec::new(),
+ plate_page_from: None,
param_menu_active: false,
param_menu_actions: Vec::new(),
playbar_menu_active: false,
@@ -10087,6 +10109,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
}
match event {
WindowEvent::MouseWheel { delta } => {
+ // What is left of a swipe that turned a menu or the dialog is
+ // the turn's, not the scene's or a list's under the new plate.
+ if cce_ui::widget::side_swipe::swallow(delta) {
+ return true;
+ }
// The dialog is modal: a wheel over it scrolls it, and a wheel
// anywhere else does nothing rather than scrolling — and
// focusing — the pane it is covering.
@@ -10098,13 +10125,16 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
if self.dialog_visible() {
return self.dialog_mouse_wheel(*delta);
}
- // Over an open viewport menu the wheel is the menu's: a
- // slider row steps, and nothing scrolls or orbits beneath.
- if self.slider_menu_open()
+ // Over an open menu the wheel is the menu's: a side swipe
+ // turns it (`crate::menu_page`), a slider row steps, and
+ // nothing scrolls or orbits beneath.
+ if self.open_menu_origin().is_some()
&& cce_ui::widget::context_menu::hit_test(self.cursor_x, self.cursor_y)
{
if cce_ui::widget::context_menu::mouse_wheel(delta, self.cursor_x, self.cursor_y) {
- self.drain_menu_slider(true);
+ if !self.take_menu_turn() && self.slider_menu_open() {
+ self.drain_menu_slider(true);
+ }
}
return true;
}
@@ -10307,7 +10337,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
// Track hover on the node/viewport/network context menus so
// the highlight follows.
- if (self.node_menu_open() || self.viewport_menu_open() || self.network_menu_open() || self.playbar_menu_open())
+ if self.open_menu_origin().is_some()
&& cce_ui::widget::context_menu::cursor_moved(self.cursor_x, self.cursor_y)
{
changed = true;
@@ -10636,6 +10666,11 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
match btn_state {
ElementState::Pressed => {
+ // A left press on a page row or a back band of any
+ // open menu turns it, ahead of the menus' own rows.
+ if *button == MouseButton::Left && self.press_menu_turn() {
+ return true;
+ }
// The node context menu takes the first shot at a
// press: a left click on it runs the item; any other
// press (or a left click outside) dismisses it and
diff --git a/src/dialog.rs b/src/dialog.rs
index 707db07..7eb40d4 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -524,6 +524,11 @@ impl Dialog {
Some(Rect { x: list.x, y: list.y + offset, width: list.width, height: ROW_H })
}
+ /// The row under `(x, y)` in a dialog laid out on `rect`.
+ pub fn row_index_at(&self, rect: Rect, x: f32, y: f32) -> Option<usize> {
+ self.row_at(rect, x, y)
+ }
+
fn row_at(&self, rect: Rect, x: f32, y: f32) -> Option<usize> {
let list = list_rect(rect);
if x < list.x || x >= list.x + list.width || y < list.y || y >= list.y + list.height {
@@ -1538,6 +1543,21 @@ pub const SETTINGS: &[Setting] = &[
Setting::field("Origin Size", "origin_size", Ctl::Spin { min: 1.0, max: 50.0, unit: 10.0 }),
];
+/// The page mark, as a menu's page row wears it.
+use cce_ui::widget::context_menu::PAGE_MARK;
+
+/// Whether a row of the dialog in `mode` turns it into another list: the
+/// palette's Group Markers and Attribute Visualizers, a visualizer of the
+/// list and Add Visualizer. A press runs it, as before; a side swipe forward
+/// with the pointer on it does too.
+pub fn dialog_row_leads(mode: Mode, id: &str) -> bool {
+ match mode {
+ Mode::Commands => matches!(id, "group_markers" | "attribute_visualizers"),
+ Mode::Visualizers => id.starts_with(VIS_ROW_PREFIX),
+ _ => false,
+ }
+}
+
impl State {
pub fn dialog_visible(&self) -> bool {
self.slots.dialog.visible()
@@ -1580,7 +1600,29 @@ impl State {
/// is what makes it read as the same plate grown, not a second one
/// arriving across the window.
pub fn open_node_palette_at(&mut self, x: f32, y: f32) {
- self.open_dialog_anchored(Mode::AddNode, Some((x, y)));
+ self.open_dialog_from(Mode::AddNode, crate::menu_page::MenuOrigin::Network, (x, y));
+ }
+
+ /// The dialog in `mode`, turned to from the menu `origin` by one of its
+ /// page rows: its top-left where the menu's was, and a swipe back shows
+ /// the menu again (see `crate::menu_page`).
+ pub fn open_dialog_from(&mut self, mode: Mode, origin: crate::menu_page::MenuOrigin, at: (f32, f32)) {
+ self.close_dialog();
+ self.open_dialog_anchored(mode, Some(at));
+ self.dialog_from = Some(origin);
+ }
+
+ /// Turn the open dialog to `mode`, as the row or key that leads there
+ /// does.
+ pub(crate) fn open_dialog_mode(&mut self, mode: Mode) {
+ match mode {
+ Mode::Visualizers => self.open_visualizers_dialog(),
+ Mode::VisualizerEdit => match self.vis_editing {
+ Some(i) => self.open_visualizer_editor(i),
+ None => self.open_visualizers_dialog(),
+ },
+ other => self.open_dialog_in(other),
+ }
}
/// Wire the node just added (the level's last) into the wire from the
@@ -1603,12 +1645,21 @@ impl State {
/// The query line is the name: it opens holding the one the node has,
/// so a rename that changes a letter is a letter typed.
pub fn open_rename_dialog(&mut self, slot: usize) {
+ self.open_rename_dialog_from(slot, None);
+ }
+
+ /// [`Self::open_rename_dialog`], turned to from a menu's Rename row
+ /// standing at a corner, when `from` says so.
+ pub fn open_rename_dialog_from(&mut self, slot: usize, from: Option<(crate::menu_page::MenuOrigin, (f32, f32))>) {
let Some((id, name)) = self.current_dir().children.get(slot).map(|n| (n.id.clone(), n.name.clone())) else {
self.update_status_text("Select a node to rename.");
return;
};
self.rename_target = Some(id);
- self.open_dialog_in(Mode::Rename);
+ match from {
+ Some((origin, at)) => self.open_dialog_from(Mode::Rename, origin, at),
+ None => self.open_dialog_in(Mode::Rename),
+ }
self.slots.dialog.query = name;
self.refresh_dialog_rows();
}
@@ -1663,11 +1714,30 @@ impl State {
}
fn open_dialog_anchored(&mut self, mode: Mode, anchor: Option<(f32, f32)>) {
+ // A dialog that is up and turns to another mode stays where it
+ // stands, turned to from where it was: the mode it leaves goes on
+ // the trail a swipe back follows — or, when the mode it turns to IS
+ // the trail's last, it is going back, and comes off. Opened afresh
+ // it has neither, and whoever opened it from a menu says so after.
+ let was = self.dialog_visible().then_some(self.slots.dialog.mode);
+ match was {
+ Some(prev) if anchor.is_none() => {
+ if self.dialog_trail.last() == Some(&mode) {
+ self.dialog_trail.pop();
+ } else if prev != mode {
+ self.dialog_trail.push(prev);
+ }
+ }
+ _ => {
+ self.slots.dialog.anchor = anchor;
+ self.dialog_trail.clear();
+ self.dialog_from = None;
+ }
+ }
// Always with an empty query: a dialog that reopens holding the last
// search has to be cleared before it can be used, which is a step
// every single time to save one occasionally.
self.slots.dialog.mode = mode;
- self.slots.dialog.anchor = anchor;
self.slots.dialog.query.clear();
self.slots.dialog.set_visible(true);
self.refresh_dialog_rows();
@@ -1696,6 +1766,8 @@ impl State {
self.slots.dialog.dropdown_row = None;
self.slots.dialog.dropdown_armed = false;
self.slots.dialog.set_visible(false);
+ self.dialog_from = None;
+ self.dialog_trail.clear();
if self.focused_widget == Some(DIALOG_IDX) {
self.focused_widget = None;
}
@@ -1889,6 +1961,18 @@ impl State {
.collect()
}
};
+ // A row that turns the dialog into another list says so where a
+ // menu's page row does, at its right end.
+ let mode = self.slots.dialog.mode;
+ let rows = rows
+ .into_iter()
+ .map(|mut r| {
+ if dialog_row_leads(mode, &r.id) {
+ r.chord = if r.chord.is_empty() { PAGE_MARK.to_string() } else { format!("{} {PAGE_MARK}", r.chord) };
+ }
+ r
+ })
+ .collect();
self.slots.dialog.set_rows(rows);
}
@@ -2784,6 +2868,12 @@ impl State {
return;
}
}
+ // A row that turns the palette into another list does so in place,
+ // the palette left on the trail for a swipe back.
+ if mode == Mode::Commands && dialog_row_leads(mode, &id) {
+ self.run_command(&id);
+ return;
+ }
if mode == Mode::Commands && self.command_toggle_state(&id).is_some() {
self.run_command(&id);
self.refresh_dialog_controls();
@@ -2944,6 +3034,26 @@ impl State {
let (x, y) = (self.cursor_x, self.cursor_y);
let ev = cce_ui::widget::Event::MouseWheel { delta, x, y, local_x: x, local_y: y };
if self.in_dialog_slot(DIALOG_IDX, x, y) {
+ // A side swipe turns the dialog: into what the row under the
+ // pointer leads to, or back to where it was turned from.
+ match cce_ui::widget::side_swipe::feed(&delta) {
+ Some(cce_ui::widget::SwipeDir::Forward) => {
+ let (rx, ry, rw, rh) = self.positions[DIALOG_IDX];
+ let rect = Rect { x: rx, y: ry, width: rw, height: rh };
+ let mode = self.slots.dialog.mode;
+ let row = self.slots.dialog.row_index_at(rect, x, y).and_then(|i| self.slots.dialog.rows.get(i)).map(|r| r.id.clone());
+ if let Some(id) = row.filter(|id| dialog_row_leads(mode, id)) {
+ self.take_dialog_pick(id);
+ return true;
+ }
+ }
+ Some(cce_ui::widget::SwipeDir::Back) => {
+ if self.dialog_back() {
+ return true;
+ }
+ }
+ None => {}
+ }
let taken = self.dispatch_uncovered(DIALOG_IDX, &ev);
// A wheel over a slider row moved it: land the value.
self.drain_dialog_clicks();
diff --git a/src/main.rs b/src/main.rs
index 3db5b9f..dbdf75d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -23,6 +23,7 @@ pub mod collide;
#[allow(unused_imports)]
use app::{CustomEvent, McpAction, ModifiersState};
pub mod plate_menu;
+pub mod menu_page;
pub mod visualizer;
pub mod playbar;
pub mod viewport_3d;
@@ -2220,15 +2221,15 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
- let sub_actions = state.open_viewport_submenu_with(A::OpacitySlider);
+ let sub_actions = state.open_viewport_page_with(A::OpacitySlider);
assert!(state.viewport_menu_open());
let i = sub_actions.iter().position(|a| *a == A::OpacitySlider).expect("an Opacity row");
- let s = context_menu::submenu::slider(i).expect("the row is a slider");
+ let s = context_menu::slider(i).expect("the row is a slider");
assert_eq!((s.value, s.min, s.max, s.step), (50.0, 0.0, 100.0, 5.0));
// Wheel over the row: one notch up is 5% more, saved, menu still up.
- state.cursor_y = context_menu::submenu::row_y(i) + context_menu::ROW_H * 0.5;
- state.cursor_x = context_menu::submenu::x() + 20.0;
+ state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
+ state.cursor_x = context_menu::x() + 20.0;
let wheel = |state: &mut State, notches: f32| {
state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, notches) })
};
@@ -2241,7 +2242,7 @@ mod tests {
assert!((state.geo_opacity - 0.40).abs() < 1e-6);
// Press on the band's right end: 100%, live; drag back; release.
- let band = context_menu::SUBMENU.with(|m| m.borrow().slider_band(i));
+ let band = context_menu::CONTEXT_MENU.with(|m| m.borrow().slider_band(i));
state.cursor_x = band.x + band.width - 1.0;
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
assert!((state.geo_opacity - 1.0).abs() < 1e-6, "{}", state.geo_opacity);
@@ -2311,20 +2312,20 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
- let sub_actions = state.open_viewport_submenu_with(A::WireOpacitySlider);
+ let sub_actions = state.open_viewport_page_with(A::WireOpacitySlider);
let acts = sub_actions.clone();
let i = acts.iter().position(|a| *a == A::WireOpacitySlider).expect("a Wire Opacity row");
- let sl = context_menu::submenu::slider(i).expect("the row is a slider");
+ let sl = context_menu::slider(i).expect("the row is a slider");
assert_eq!((sl.value, sl.min, sl.max, sl.step), (50.0, 0.0, 100.0, 5.0));
- state.cursor_x = context_menu::submenu::x() + 20.0;
- state.cursor_y = context_menu::submenu::row_y(i) + context_menu::ROW_H * 0.5;
+ state.cursor_x = context_menu::x() + 20.0;
+ state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, -2.0) });
assert!((state.wire_opacity - 0.40).abs() < 1e-6, "{}", state.wire_opacity);
assert!((state.geo_opacity - 0.5).abs() < 1e-6, "the polygon opacity moved with the wires'");
let j = acts.iter().position(|a| *a == A::OpacitySlider).expect("an Opacity row");
- state.cursor_y = context_menu::submenu::row_y(j) + context_menu::ROW_H * 0.5;
+ state.cursor_y = context_menu::row_y(j) + context_menu::ROW_H * 0.5;
state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 2.0) });
assert!((state.geo_opacity - 0.60).abs() < 1e-6, "{}", state.geo_opacity);
assert!((state.wire_opacity - 0.40).abs() < 1e-6, "the wires' opacity moved with the polygons'");
@@ -2374,7 +2375,7 @@ mod tests {
}
/// The viewport menu holds framing, the guides and a row for each
- /// submenu; the display rows are in the submenus, in groups a separator
+ /// page; the display rows are in the pages, in groups a separator
/// apart — Style the wireframe then the surface, Markers the points then
/// each element class's overlays — every one in exactly one.
#[test]
@@ -2392,7 +2393,7 @@ mod tests {
vec![
vec![A::FrameAll, A::OneToOne],
vec![A::Command("toggle_grid"), A::Command("toggle_origin"), A::Command("toggle_camera_pivot"), A::CameraPivotSizeSlider],
- vec![A::Submenu(P::Style), A::Submenu(P::Markers), A::Command("attribute_visualizers")],
+ vec![A::Page(P::Style), A::Page(P::Markers), A::Command("attribute_visualizers")],
]
);
assert_eq!(
@@ -2422,21 +2423,24 @@ mod tests {
);
}
- /// The display settings fly out beside their row: the pointer on the
- /// row opens its submenu, beside the menu and level with the row; a
- /// switch in it flips, is re-marked where it stands and leaves both
- /// menus up; the pointer on the other row swaps the submenu; a row of
- /// the menu itself closes everything, as it always did.
+ /// The display settings are PAGES of the viewport menu: pointing at
+ /// their row turns nothing; a press on it, or a side swipe forward over
+ /// it, turns the menu into the page where it stands, under a back band;
+ /// a switch on the page flips, is re-marked in place and leaves the page
+ /// up; a press on the band or a swipe back turns back. The visualizers'
+ /// row turns the menu into the dialog, which a swipe back turns back
+ /// into the menu at its corner. A row of the menu itself still closes it.
#[test]
- fn the_viewport_menu_flies_its_settings_out_beside_it() {
+ fn the_viewport_menu_turns_into_its_pages_and_back() {
use crate::app::{ViewportMenuAction as A, ViewportMenuPage as P};
use crate::window::{LocalPosition, WindowEvent};
- use cce_ui::widget::{context_menu, context_menu::submenu, ElementState, MouseButton};
+ use cce_ui::widget::{context_menu, ElementState, MouseButton, MouseScrollDelta, Position};
let mut state = State::new(false);
state.show_prim_numbers = false;
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
+ let corner = (context_menu::x(), context_menu::y());
let row_of = |state: &State, a: A| state.viewport_menu_actions.iter().position(|x| *x == a).unwrap_or_else(|| panic!("no {a:?} row"));
let move_to = |state: &mut State, x: f32, y: f32| {
state.cursor_x = x;
@@ -2447,37 +2451,64 @@ mod tests {
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left });
};
- let (style, markers) = (row_of(&state, A::Submenu(P::Style)), row_of(&state, A::Submenu(P::Markers)));
- assert!(context_menu::has_submenu(style) && context_menu::has_submenu(markers));
- assert!(!submenu::is_visible(), "nothing flies out until a row is pointed at");
-
- move_to(&mut state, context_menu::x() + 20.0, context_menu::row_y(markers) + 12.0);
- assert_eq!(submenu::parent_row(), Some(markers));
- assert_eq!(submenu::x(), context_menu::x() + context_menu::w(), "beside the menu");
- assert_eq!(submenu::row_y(0), context_menu::row_y(markers), "level with its row");
- let actions = state.open_viewport_submenu_actions().unwrap();
- let prims = actions.iter().position(|a| *a == A::Command("toggle_prim_numbers")).unwrap();
- assert!(submenu::options()[prims].starts_with('○'));
-
- // Into the submenu: its row hovers, and the menu's row stays lit.
- move_to(&mut state, submenu::x() + 20.0, submenu::row_y(prims) + 12.0);
- assert_eq!(submenu::hovered_item(), Some(prims));
- assert_eq!(context_menu::hovered_item(), Some(markers));
- assert_eq!(context_menu::row_at(state.cursor_x, state.cursor_y), None, "a row of the submenu is not a row of the menu");
+ // Two fingers to the side, a few events long: negative x shows what
+ // is to the right, which is forward.
+ let swipe = |state: &mut State, dx: f64| {
+ cce_ui::widget::side_swipe::end_gesture();
+ for _ in 0..4 {
+ state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::PixelDelta(Position { x: dx / 4.0, y: 0.0 }) });
+ }
+ };
+ let (style, markers) = (row_of(&state, A::Page(P::Style)), row_of(&state, A::Page(P::Markers)));
+ let vis = row_of(&state, A::Command("attribute_visualizers"));
+ assert!([style, markers, vis].iter().all(|&i| context_menu::leads_to_page(i)), "the three rows are page rows");
+ assert!(!context_menu::leads_to_page(row_of(&state, A::FrameAll)));
+
+ move_to(&mut state, corner.0 + 20.0, context_menu::row_y(markers) + 12.0);
+ assert_eq!(state.viewport_menu_page, None, "pointing turns nothing");
+ press(&mut state);
+ assert_eq!(state.viewport_menu_page, Some(P::Markers), "a press turns the menu");
+ assert_eq!((context_menu::x(), context_menu::y()), corner, "where the menu stood");
+ assert_eq!(context_menu::back_title().as_deref(), Some("Viewport"));
+ let prims = row_of(&state, A::Command("toggle_prim_numbers"));
+ assert!(context_menu::options()[prims].starts_with('○'));
+
+ move_to(&mut state, corner.0 + 20.0, context_menu::row_y(prims) + 12.0);
press(&mut state);
assert!(state.show_prim_numbers, "the switch flipped");
- assert!(state.viewport_menu_open() && submenu::is_visible(), "and both are still up");
- assert!(submenu::options()[prims].starts_with('●'), "re-marked: {}", submenu::options()[prims]);
+ assert!(state.viewport_menu_open() && state.viewport_menu_page == Some(P::Markers), "and the page is still up");
+ assert!(context_menu::options()[prims].starts_with('●'), "re-marked: {}", context_menu::options()[prims]);
- // Straight back onto the other row: its submenu takes the place.
- move_to(&mut state, context_menu::x() + 20.0, context_menu::row_y(style) + 12.0);
- assert_eq!(submenu::parent_row(), Some(style));
- assert_eq!(state.open_viewport_submenu_actions().unwrap()[0], A::Command("toggle_wireframe"));
+ swipe(&mut state, 80.0);
+ assert_eq!(state.viewport_menu_page, None, "a swipe back turned back to the menu");
+ assert!(state.viewport_menu_open());
+ assert_eq!((context_menu::x(), context_menu::y()), corner);
+ assert_eq!(context_menu::back_title(), None, "the menu goes back nowhere");
+
+ move_to(&mut state, corner.0 + 20.0, context_menu::row_y(style) + 12.0);
+ swipe(&mut state, -80.0);
+ assert_eq!(state.viewport_menu_page, Some(P::Style), "a swipe forward over the row turns into its page");
+ assert_eq!(state.viewport_menu_actions[0], A::Command("toggle_wireframe"));
+ move_to(&mut state, corner.0 + 20.0, corner.1 + context_menu::PAD + context_menu::ROW_H * 0.5);
+ press(&mut state);
+ assert_eq!(state.viewport_menu_page, None, "a press on the back band turns back");
- // A row with no submenu closes it; picking that row closes the menu.
+ // Into the dialog, and back.
+ move_to(&mut state, corner.0 + 20.0, context_menu::row_y(vis) + 12.0);
+ press(&mut state);
+ assert!(!state.viewport_menu_open());
+ assert!(state.dialog_visible() && state.slots.dialog.mode == crate::dialog::Mode::Visualizers);
+ assert_eq!(state.slots.dialog.anchor, Some(corner), "the dialog took the menu's corner");
+ let (dx, dy, _, _) = state.positions[crate::slots::DIALOG_IDX];
+ move_to(&mut state, dx + 30.0, dy + 30.0);
+ swipe(&mut state, 80.0);
+ assert!(!state.dialog_visible(), "a swipe back closes the dialog");
+ assert!(state.viewport_menu_open() && state.viewport_menu_page.is_none(), "and turns it back into the menu");
+ assert_eq!((context_menu::x(), context_menu::y()), (dx, dy), "at the dialog's corner");
+
+ // A row of the menu itself runs and closes it.
let grid = row_of(&state, A::Command("toggle_grid"));
move_to(&mut state, context_menu::x() + 20.0, context_menu::row_y(grid) + 12.0);
- assert!(!submenu::is_visible());
press(&mut state);
assert!(!state.viewport_menu_open(), "a row of the menu closes it");
}
@@ -2932,26 +2963,26 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
- let sub_actions = state.open_viewport_submenu_with(A::WireThicknessSlider);
+ let sub_actions = state.open_viewport_page_with(A::WireThicknessSlider);
let acts = sub_actions.clone();
let i = acts.iter().position(|a| *a == A::WireThicknessSlider).expect("a Wire Thickness row");
assert_eq!(acts[i - 1], A::Command("toggle_wireframe"), "it sits under Show Wireframe");
- let sl = context_menu::submenu::slider(i).expect("the row is a slider");
+ let sl = context_menu::slider(i).expect("the row is a slider");
assert_eq!((sl.value, sl.min, sl.max, sl.step), (2.0, 1.0, 8.0, 0.5));
// Every other slider the menu carries answers the same table.
for (k, a) in acts.iter().enumerate() {
- assert_eq!(context_menu::submenu::slider(k).is_some(), state.viewport_menu_slider(*a).is_some(), "row {k} {a:?}");
+ assert_eq!(context_menu::slider(k).is_some(), state.viewport_menu_slider(*a).is_some(), "row {k} {a:?}");
}
- state.cursor_x = context_menu::submenu::x() + 20.0;
- state.cursor_y = context_menu::submenu::row_y(i) + context_menu::ROW_H * 0.5;
+ state.cursor_x = context_menu::x() + 20.0;
+ state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 2.0) });
assert!((state.wire_width - 3.0).abs() < 1e-6, "{}", state.wire_width);
let kdl = fs::read_to_string(crate::app::DesignSettings::file_path()).expect("saved");
assert!(kdl.contains("wire_width (f64)3") || kdl.contains("wire_width 3"), "persisted: {kdl}");
assert!(state.viewport_menu_open());
- let band = context_menu::SUBMENU.with(|m| m.borrow().slider_band(i));
+ let band = context_menu::CONTEXT_MENU.with(|m| m.borrow().slider_band(i));
state.cursor_x = band.x + 1.0;
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left });
@@ -3020,15 +3051,15 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
- let sub_actions = state.open_viewport_submenu_with(A::PointMarkerSizeSlider);
+ let sub_actions = state.open_viewport_page_with(A::PointMarkerSizeSlider);
let i = sub_actions.iter().position(|a| *a == A::PointMarkerSizeSlider).expect("a Point Marker Size row");
assert_eq!(sub_actions[i - 1], A::Command("toggle_point_markers"), "it sits under its switch");
- let sl = context_menu::submenu::slider(i).expect("a slider");
+ let sl = context_menu::slider(i).expect("a slider");
assert_eq!((sl.min, sl.max, sl.step), (0.005, 0.1, 0.005));
assert!((sl.value - 0.02).abs() < 1e-6);
- state.cursor_x = context_menu::submenu::x() + 20.0;
- state.cursor_y = context_menu::submenu::row_y(i) + context_menu::ROW_H * 0.5;
+ state.cursor_x = context_menu::x() + 20.0;
+ state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 4.0) });
assert!((state.point_marker_size - 0.04).abs() < 1e-5, "{}", state.point_marker_size);
assert_eq!(state.rt_geometry_version, version, "no rebuild ran");
@@ -3070,14 +3101,14 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
- let sub_actions = state.open_viewport_submenu_with(A::PullArrowScaleSlider);
+ let sub_actions = state.open_viewport_page_with(A::PullArrowScaleSlider);
let i = sub_actions.iter().position(|a| *a == A::PullArrowScaleSlider).expect("a Pull Arrow Scale row");
- let sl = context_menu::submenu::slider(i).expect("a slider");
+ let sl = context_menu::slider(i).expect("a slider");
assert_eq!((sl.min, sl.max, sl.step, sl.suffix), (0.25, 10.0, 0.25, "x"));
assert_eq!(sl.readout(), "1.00x");
- state.cursor_x = context_menu::submenu::x() + 20.0;
- state.cursor_y = context_menu::submenu::row_y(i) + context_menu::ROW_H * 0.5;
+ state.cursor_x = context_menu::x() + 20.0;
+ state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 15.0) });
let k = state.pull_arrow_scale;
assert!(k > 1.0, "the wheel raised the scale: {k}");
@@ -3108,14 +3139,14 @@ mod tests {
state.cursor_x = 300.0;
state.cursor_y = 200.0;
state.open_viewport_context_menu();
- let sub_actions = state.open_viewport_submenu_with(A::GroupMarkerSizeSlider);
+ let sub_actions = state.open_viewport_page_with(A::GroupMarkerSizeSlider);
let i = sub_actions.iter().position(|a| *a == A::GroupMarkerSizeSlider).expect("a Group Marker Size row");
- let sl = context_menu::submenu::slider(i).expect("a slider");
+ let sl = context_menu::slider(i).expect("a slider");
assert_eq!((sl.min, sl.max, sl.step, sl.decimals), (0.0, 0.2, 0.005, 3));
assert!((sl.value - 0.025).abs() < 1e-6);
- state.cursor_x = context_menu::submenu::x() + 20.0;
- state.cursor_y = context_menu::submenu::row_y(i) + context_menu::ROW_H * 0.5;
+ state.cursor_x = context_menu::x() + 20.0;
+ state.cursor_y = context_menu::row_y(i) + context_menu::ROW_H * 0.5;
state.handle_event(&WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(0.0, 3.0) });
assert!((state.group_marker_size - 0.04).abs() < 1e-5, "{}", state.group_marker_size);
assert!((radius(&state) - 0.04).abs() < 1e-5, "the markers re-sized: {}", radius(&state));
diff --git a/src/menu_page.rs b/src/menu_page.rs
new file mode 100644
index 0000000..2cc42f5
--- /dev/null
+++ b/src/menu_page.rs
@@ -0,0 +1,260 @@
+//! Page rows: a row of a context menu that TURNS the menu into another
+//! plate where it stands — a page of more rows, or the dialog — with a way
+//! back.
+//!
+//! Until 2026-10-02 there were two kinds of such row. The viewport menu's
+//! Style and Markers flew a second menu out beside themselves on hover
+//! (cce-ui's submenu), while the network menu's Add Node, the plate menu's
+//! Add Tab, the viewport menu's Attribute Visualizers and the node menu's
+//! Rename replaced the menu with another plate on a click — one with a Back
+//! row, the others with no way back at all. One idea, two gestures, and the
+//! user could not tell from a row which it was.
+//!
+//! Now every such row is a PAGE row (cce-ui's `context_menu::set_row_page`):
+//! it wears `›`, and a press on it, or a two-finger swipe to the side with
+//! the pointer on it, turns the menu into what it names, the new plate's
+//! top-left where the menu's was. A swipe the other way goes back, from
+//! anywhere on the page; a page that is a menu also has a back band across
+//! its top (`‹ Viewport`) for a press. Which way is forward follows the
+//! 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 network or playbar menu or a stub's plate menu.
+//! - 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
+//! visualizer from the list) goes back to it first (`State::dialog_trail`).
+//!
+//! What runs a turn is [`State::run_menu_turn`], which a press
+//! ([`State::press_menu_turn`]) and a swipe ([`State::take_menu_turn`]) both
+//! reach, so the two cannot disagree about where a row leads.
+
+use crate::app::{NetworkMenuAction, NodeMenuAction, PlaybarMenuAction, State, ViewportMenuAction};
+use crate::dialog::Mode;
+use crate::plate_menu::{plate_title, PlateMenuAction};
+use crate::slots::{DIALOG_IDX, NETWORK_PANEL_IDX, PLAYBAR_IDX};
+use cce_ui::widget::context_menu::{self, PageTurn};
+use cce_ui::widget::WidgetId;
+
+/// A context menu a page or the dialog was turned to from: where a turn back
+/// goes, and what the back band names.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum MenuOrigin {
+ Viewport,
+ Network,
+ Playbar,
+ /// A pane's plate menu shown alone (a stub's, the params pane's…).
+ Plate(usize),
+ /// A node's menu, by slot on the current level.
+ Node(usize),
+}
+
+impl MenuOrigin {
+ /// The menu a plate's rows are part of: the network and playbar menus
+ /// carry their plates' rows, every other plate's are a menu alone.
+ pub fn of_plate(idx: usize) -> MenuOrigin {
+ match idx {
+ NETWORK_PANEL_IDX => MenuOrigin::Network,
+ PLAYBAR_IDX => MenuOrigin::Playbar,
+ other => MenuOrigin::Plate(other),
+ }
+ }
+}
+
+impl ViewportMenuAction {
+ /// Whether the row turns the menu: its pages, and the visualizers'
+ /// editor, which is the dialog.
+ pub fn leads_to_page(self) -> bool {
+ matches!(self, ViewportMenuAction::Page(_) | ViewportMenuAction::Command("attribute_visualizers"))
+ }
+}
+
+impl NetworkMenuAction {
+ /// Add Node turns the menu into the add-node list; Add Tab into its page.
+ pub fn leads_to_page(self) -> bool {
+ matches!(self, NetworkMenuAction::Command("add_node") | NetworkMenuAction::Plate(PlateMenuAction::AddTabMenu))
+ }
+}
+
+/// Mark the rows of the menu just shown that lead to a page.
+pub fn mark_page_rows<A: Copy>(actions: &[A], leads: impl Fn(A) -> bool) {
+ for (i, a) in actions.iter().enumerate() {
+ if leads(*a) {
+ context_menu::set_row_page(i);
+ }
+ }
+}
+
+impl State {
+ /// Show a menu: at the pointer (`at` None), as a menu opens, or with its
+ /// top-left at `at` in place of the plate that stood there — under a
+ /// back band to `back` when there is somewhere to go back to.
+ pub(crate) fn put_up_menu(&self, at: Option<(f32, f32)>, back: Option<MenuOrigin>, options: Vec<String>, header_count: usize, target: WidgetId) {
+ match at {
+ None => context_menu::show(self.cursor_x, self.cursor_y, options, header_count, target),
+ Some((x, y)) => {
+ let title = back.map(|o| self.menu_title(o));
+ context_menu::show_page(x, y, title.as_deref(), options, header_count, target);
+ }
+ }
+ }
+
+ /// What a back band to `origin` reads.
+ pub fn menu_title(&self, origin: MenuOrigin) -> String {
+ match origin {
+ MenuOrigin::Viewport => "Viewport".to_string(),
+ MenuOrigin::Network => "Network".to_string(),
+ MenuOrigin::Playbar => "Playbar".to_string(),
+ MenuOrigin::Plate(idx) => plate_title(idx).to_string(),
+ MenuOrigin::Node(slot) => self.current_dir().children.get(slot).map(|n| n.name.clone()).unwrap_or_else(|| "Node".to_string()),
+ }
+ }
+
+ /// Which of the app's menus is up, if one is.
+ pub fn open_menu_origin(&self) -> Option<MenuOrigin> {
+ if !context_menu::is_visible() {
+ return None;
+ }
+ if self.viewport_menu_active {
+ Some(MenuOrigin::Viewport)
+ } else if self.network_menu_active {
+ Some(MenuOrigin::Network)
+ } else if self.playbar_menu_active {
+ Some(MenuOrigin::Playbar)
+ } else if let Some(idx) = self.plate_menu_slot {
+ Some(MenuOrigin::Plate(idx))
+ } else {
+ self.node_menu_slot.map(MenuOrigin::Node)
+ }
+ }
+
+ /// Close whichever of the app's menus is up.
+ fn close_open_menu(&mut self) {
+ match self.open_menu_origin() {
+ Some(MenuOrigin::Viewport) => self.close_viewport_menu(),
+ Some(MenuOrigin::Network) => self.close_network_menu(),
+ Some(MenuOrigin::Playbar) => self.close_playbar_menu(),
+ Some(MenuOrigin::Plate(_)) => self.close_plate_menu(),
+ Some(MenuOrigin::Node(_)) => self.close_node_menu(),
+ None => context_menu::hide(),
+ }
+ }
+
+ /// Show `origin`'s menu again with its top-left at `(x, y)`: a turn back
+ /// to it from a page or the dialog.
+ pub fn reopen_menu(&mut self, origin: MenuOrigin, x: f32, y: f32) {
+ let at = Some((x, y));
+ match origin {
+ 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),
+ MenuOrigin::Plate(idx) => self.open_plate_menu_at(idx, at),
+ MenuOrigin::Node(slot) => self.open_node_context_menu_at(slot, at),
+ }
+ }
+
+ /// A left press on a page row or a back band of the open menu turns it.
+ /// `true` when it did, and the press is spent.
+ pub fn press_menu_turn(&mut self) -> bool {
+ if self.open_menu_origin().is_none() {
+ return false;
+ }
+ match context_menu::turn_at(self.cursor_x, self.cursor_y) {
+ Some(turn) => self.run_menu_turn(turn),
+ None => false,
+ }
+ }
+
+ /// The turn a side swipe over the open menu asked for, run.
+ pub fn take_menu_turn(&mut self) -> bool {
+ match context_menu::take_turn() {
+ Some(turn) => self.run_menu_turn(turn),
+ None => false,
+ }
+ }
+
+ /// Turn the open menu: into what row `n` leads to, or back to the menu
+ /// this page was turned from. `false` when the menu has no such turn.
+ pub fn run_menu_turn(&mut self, turn: PageTurn) -> bool {
+ let Some(origin) = self.open_menu_origin() else { return false };
+ let at = (context_menu::x(), context_menu::y());
+ match turn {
+ PageTurn::Back => match origin {
+ MenuOrigin::Viewport if self.viewport_menu_page.is_some() => {
+ self.show_viewport_menu_page(None, Some(at));
+ }
+ MenuOrigin::Plate(_) if self.plate_page_from.is_some() => {
+ let from = self.plate_page_from.take().unwrap();
+ self.close_plate_menu();
+ self.reopen_menu(from, at.0, at.1);
+ }
+ _ => return false,
+ },
+ PageTurn::Into(n) => match origin {
+ MenuOrigin::Viewport => match self.viewport_menu_actions.get(n).copied() {
+ Some(ViewportMenuAction::Page(page)) => self.show_viewport_menu_page(Some(page), Some(at)),
+ Some(ViewportMenuAction::Command("attribute_visualizers")) => {
+ self.close_viewport_menu();
+ self.vis_editing = None;
+ self.open_dialog_from(Mode::Visualizers, origin, at);
+ }
+ _ => return false,
+ },
+ MenuOrigin::Network => match self.network_menu_actions.get(n).copied() {
+ Some(NetworkMenuAction::Command("add_node")) => {
+ self.close_network_menu();
+ self.open_dialog_from(Mode::AddNode, origin, at);
+ }
+ Some(NetworkMenuAction::Plate(PlateMenuAction::AddTabMenu)) => {
+ self.close_network_menu();
+ self.open_plate_add_tab_menu(NETWORK_PANEL_IDX, at, origin);
+ }
+ _ => return false,
+ },
+ MenuOrigin::Playbar => match self.playbar_menu_actions.get(n).copied() {
+ Some(PlaybarMenuAction::Plate(PlateMenuAction::AddTabMenu)) => {
+ self.close_playbar_menu();
+ self.open_plate_add_tab_menu(PLAYBAR_IDX, at, origin);
+ }
+ _ => 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,
+ },
+ MenuOrigin::Node(slot) => match self.node_menu_actions.get(n).copied() {
+ Some(NodeMenuAction::Rename) => {
+ self.close_node_menu();
+ self.open_rename_dialog_from(slot, Some((origin, at)));
+ }
+ _ => return false,
+ },
+ },
+ }
+ true
+ }
+
+ /// Turn the dialog back: to the mode it was turned to from, else to the
+ /// menu it was turned to from, shown at the dialog's corner. `false`
+ /// when it came from neither.
+ pub fn dialog_back(&mut self) -> bool {
+ if !self.dialog_visible() {
+ return false;
+ }
+ if let Some(&prev) = self.dialog_trail.last() {
+ self.open_dialog_mode(prev);
+ return true;
+ }
+ let Some(origin) = self.dialog_from else { return false };
+ let (x, y, _, _) = self.positions[DIALOG_IDX];
+ self.close_dialog();
+ self.close_open_menu();
+ self.reopen_menu(origin, x, y);
+ true
+ }
+}
diff --git a/src/plate_menu.rs b/src/plate_menu.rs
index a7606c9..fe99f0b 100644
--- a/src/plate_menu.rs
+++ b/src/plate_menu.rs
@@ -51,11 +51,10 @@ pub enum PlateMenuAction {
ShowTab(usize),
/// Pull the named pane out of its dock and tab it into this one, active.
AddTab(usize),
- /// Swap the menu for the Add Tab page — the list of panes that can be
- /// pulled in ([`State::open_plate_add_tab_menu`]).
+ /// 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,
- /// The Add Tab page's Back row: swap the plate's rows back in.
- BackToMain,
/// 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
@@ -240,19 +239,22 @@ impl State {
/// Show `idx`'s plate menu alone, at the pointer — a pane with no
/// context menu of its own, or a stub.
pub fn open_plate_menu(&mut self, idx: usize) {
- self.open_plate_menu_at(idx, self.cursor_x, self.cursor_y);
+ self.open_plate_menu_at(idx, None);
}
- /// Show `idx`'s plate menu alone, its top-left at (x, y).
- pub fn open_plate_menu_at(&mut self, idx: usize, x: f32, y: f32) {
+ /// Show `idx`'s plate menu alone: at the pointer, or with its top-left
+ /// at `at` in place of a page it is turned back to from.
+ pub fn open_plate_menu_at(&mut self, idx: usize, at: Option<(f32, f32)>) {
let (options, actions) = self.plate_menu_rows(idx);
if options.is_empty() {
return;
}
let target = self.slots.get_dyn(idx).base().id();
- cce_ui::widget::context_menu::show(x, y, options, 0, target);
+ 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)
@@ -268,10 +270,10 @@ impl State {
.collect()
}
- /// The Add Tab page: swaps the menu in place for the list of addable
- /// panes, under a dimmed header row, at (x, y) — where the menu that
- /// asked for it stood. A second PAGE of the menu, not a second menu.
- pub fn open_plate_add_tab_menu(&mut self, idx: usize, x: f32, y: f32) {
+ /// 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() {
@@ -283,14 +285,11 @@ impl State {
options.push(plate_title(other).to_string());
actions.push(PlateMenuAction::AddTab(other));
}
- options.push("-".to_string());
- actions.push(PlateMenuAction::Separator);
- options.push("‹ Back".to_string());
- actions.push(PlateMenuAction::BackToMain);
let target = self.slots.get_dyn(idx).base().id();
- cce_ui::widget::context_menu::show(x, y, options, 1, target);
+ 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);
}
pub fn plate_menu_open(&self) -> bool {
@@ -301,6 +300,7 @@ impl State {
cce_ui::widget::context_menu::hide();
self.plate_menu_slot = None;
self.plate_menu_actions.clear();
+ self.plate_page_from = None;
}
/// Route a left press while the plate menu is open — same contract as
@@ -324,8 +324,8 @@ impl State {
}
/// Run a plate row for `idx`, picked from a menu whose top-left was `at`
- /// — where a page swap (Add Tab, Back) puts the next page, whichever
- /// menu the row was part of.
+ /// — 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)) {
match action {
PlateMenuAction::Collapse => self.set_pane_collapsed(idx, true),
@@ -344,8 +344,9 @@ impl State {
self.add_dock_tab(d, o);
}
}
- PlateMenuAction::AddTabMenu => self.open_plate_add_tab_menu(idx, at.0, at.1),
- PlateMenuAction::BackToMain => self.open_plate_menu_at(idx, at.0, at.1),
+ 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::CloseTab => self.close_dock_tab(idx),