graphic design tool
git clone https://git.lucas.co/cce-designer.git
src/menu_page.rs (10.9K)
1 //! Page rows: a row of a context menu that TURNS the menu into another
2 //! plate where it stands — a page of more rows, or the dialog — with a way
3 //! back.
4 //!
5 //! Until 2026-10-02 there were two kinds of such row. The viewport menu's
6 //! Style and Markers flew a second menu out beside themselves on hover
7 //! (cce-ui's submenu), while the network menu's Add Node, the plate menu's
8 //! Add Tab, the viewport menu's Attribute Visualizers and the node menu's
9 //! Rename replaced the menu with another plate on a click — one with a Back
10 //! row, the others with no way back at all. One idea, two gestures, and the
11 //! user could not tell from a row which it was.
12 //!
13 //! Now every such row is a PAGE row (cce-ui's `context_menu::set_row_page`):
14 //! it wears `›`, and a press on it, or a two-finger swipe to the side with
15 //! the pointer on it, turns the menu into what it names, the new plate's
16 //! top-left where the menu's was. A swipe the other way goes back, from
17 //! anywhere on the page; a page that is a menu also has a back band across
18 //! its top (`‹ Viewport`) for a press. Which way is forward follows the
19 //! content, as a horizontal list does: see `cce_ui::widget::side_swipe`.
20 //!
21 //! - A page of rows is shown by the menu that owns it (`put_up_menu` with
22 //! an `at`): the viewport menu's Style, Markers and Network, the
23 //! playbar menu's Plate. (The plate rows' Add Tab was one until docks
24 //! lost their tabs, 2026-10-07.)
25 //! - The dialog turned to from a row (`State::dialog_from`) goes back to
26 //! that menu, shown again at the dialog's corner; inside the dialog, a
27 //! mode turned to from another (Group Markers from the palette, one
28 //! visualizer from the list) goes back to it first (`State::dialog_trail`).
29 //!
30 //! What runs a turn is [`State::run_menu_turn`], which a press
31 //! ([`State::press_menu_turn`]) and a swipe ([`State::take_menu_turn`]) both
32 //! reach, so the two cannot disagree about where a row leads.
33
34 use crate::app::{NetworkMenuAction, NodeMenuAction, PlaybarMenuAction, State, ViewportMenuAction};
35 use crate::dialog::Mode;
36 use crate::plate_menu::plate_title;
37 use crate::slots::{DIALOG_IDX, PLAYBAR_IDX};
38 use cce_ui::widget::context_menu::{self, PageTurn};
39 use cce_ui::widget::WidgetId;
40
41 /// A context menu a page or the dialog was turned to from: where a turn back
42 /// goes, and what the back band names.
43 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
44 pub enum MenuOrigin {
45 Viewport,
46 Network,
47 Playbar,
48 /// A pane's plate menu shown alone (a stub's, the params pane's…).
49 Plate(usize),
50 /// A node's menu, by slot on the current level.
51 Node(usize),
52 }
53
54
55 impl ViewportMenuAction {
56 /// Whether the row turns the menu: its pages, and the visualizers'
57 /// editor, which is the dialog.
58 pub fn leads_to_page(self) -> bool {
59 matches!(self, ViewportMenuAction::Page(_) | ViewportMenuAction::NetworkPage)
60 }
61 }
62
63 impl NetworkMenuAction {
64 /// Add Node turns the menu into the add-node list.
65 pub fn leads_to_page(self) -> bool {
66 matches!(self, NetworkMenuAction::Command("add_node"))
67 }
68 }
69
70 /// Mark the rows of the menu just shown that lead to a page.
71 pub fn mark_page_rows<A: Copy>(actions: &[A], leads: impl Fn(A) -> bool) {
72 for (i, a) in actions.iter().enumerate() {
73 if leads(*a) {
74 context_menu::set_row_page(i);
75 }
76 }
77 }
78
79 impl State {
80 /// Show a menu: at the pointer (`at` None), as a menu opens, or with its
81 /// top-left at `at` in place of the plate that stood there — under a
82 /// back band to `back` when there is somewhere to go back to.
83 pub(crate) fn put_up_menu(&self, at: Option<(f32, f32)>, back: Option<MenuOrigin>, options: Vec<String>, header_count: usize, target: WidgetId) {
84 match at {
85 None => context_menu::show(self.cursor_x, self.cursor_y, options, header_count, target),
86 Some((x, y)) => {
87 let title = back.map(|o| self.menu_title(o));
88 context_menu::show_page(x, y, title.as_deref(), options, header_count, target);
89 }
90 }
91 }
92
93 /// What a back band to `origin` reads.
94 pub fn menu_title(&self, origin: MenuOrigin) -> String {
95 match origin {
96 MenuOrigin::Viewport => "Viewport".to_string(),
97 MenuOrigin::Network => "Network".to_string(),
98 MenuOrigin::Playbar => "Playbar".to_string(),
99 MenuOrigin::Plate(idx) => plate_title(idx).to_string(),
100 MenuOrigin::Node(slot) => self.current_dir().children.get(slot).map(|n| n.name.clone()).unwrap_or_else(|| "Node".to_string()),
101 }
102 }
103
104 /// Which of the app's menus is up, if one is.
105 pub fn open_menu_origin(&self) -> Option<MenuOrigin> {
106 if !context_menu::is_visible() {
107 return None;
108 }
109 if self.viewport_menu_active {
110 Some(MenuOrigin::Viewport)
111 } else if self.network_menu_active {
112 Some(MenuOrigin::Network)
113 } else if self.playbar_menu_active {
114 Some(MenuOrigin::Playbar)
115 } else if let Some(idx) = self.plate_menu_slot {
116 Some(MenuOrigin::Plate(idx))
117 } else {
118 self.node_menu_slot.map(MenuOrigin::Node)
119 }
120 }
121
122 /// Close whichever of the app's menus is up.
123 fn close_open_menu(&mut self) {
124 match self.open_menu_origin() {
125 Some(MenuOrigin::Viewport) => self.close_viewport_menu(),
126 Some(MenuOrigin::Network) => self.close_network_menu(),
127 Some(MenuOrigin::Playbar) => self.close_playbar_menu(),
128 Some(MenuOrigin::Plate(_)) => self.close_plate_menu(),
129 Some(MenuOrigin::Node(_)) => self.close_node_menu(),
130 None => context_menu::hide(),
131 }
132 }
133
134 /// Show `origin`'s menu again with its top-left at `(x, y)`: a turn back
135 /// to it from a page or the dialog.
136 pub fn reopen_menu(&mut self, origin: MenuOrigin, x: f32, y: f32) {
137 let at = Some((x, y));
138 match origin {
139 MenuOrigin::Viewport => self.show_viewport_menu_page(None, at),
140 MenuOrigin::Network => self.open_network_context_menu_at(at),
141 MenuOrigin::Playbar => self.open_playbar_context_menu_at(at),
142 MenuOrigin::Plate(idx) => self.open_plate_menu_at(idx, at),
143 MenuOrigin::Node(slot) => self.open_node_context_menu_at(slot, at),
144 }
145 }
146
147 /// A left press on a page row or a back band of the open menu turns it.
148 /// `true` when it did, and the press is spent.
149 pub fn press_menu_turn(&mut self) -> bool {
150 if self.open_menu_origin().is_none() {
151 return false;
152 }
153 match context_menu::turn_at(self.cursor_x, self.cursor_y) {
154 Some(turn) => self.run_menu_turn(turn),
155 None => false,
156 }
157 }
158
159 /// The turn a side swipe over the open menu asked for, run.
160 pub fn take_menu_turn(&mut self) -> bool {
161 match context_menu::take_turn() {
162 Some(turn) => self.run_menu_turn(turn),
163 None => false,
164 }
165 }
166
167 /// Turn the open menu: into what row `n` leads to, or back to the menu
168 /// this page was turned from. `false` when the menu has no such turn.
169 pub fn run_menu_turn(&mut self, turn: PageTurn) -> bool {
170 let Some(origin) = self.open_menu_origin() else { return false };
171 let at = (context_menu::x(), context_menu::y());
172 match turn {
173 PageTurn::Back => match origin {
174 MenuOrigin::Viewport if self.viewport_menu_page.is_some() => {
175 self.show_viewport_menu_page(None, Some(at));
176 }
177 // The network menu turned to from the viewport's (the plate
178 // off) goes back to it.
179 MenuOrigin::Network if self.network_menu_from.is_some() => {
180 let from = self.network_menu_from.take().unwrap();
181 self.close_network_menu();
182 self.reopen_menu(from, at.0, at.1);
183 }
184 MenuOrigin::Plate(_) if self.plate_page_from.is_some() => {
185 let from = self.plate_page_from.take().unwrap();
186 self.close_plate_menu();
187 self.reopen_menu(from, at.0, at.1);
188 }
189 _ => return false,
190 },
191 PageTurn::Into(n) => match origin {
192 MenuOrigin::Viewport => match self.viewport_menu_actions.get(n).copied() {
193 Some(ViewportMenuAction::Page(page)) => self.show_viewport_menu_page(Some(page), Some(at)),
194 Some(ViewportMenuAction::NetworkPage) => {
195 self.close_viewport_menu();
196 self.open_network_menu_from_viewport(at);
197 }
198 _ => return false,
199 },
200 MenuOrigin::Network => match self.network_menu_actions.get(n).copied() {
201 Some(NetworkMenuAction::Command("add_node")) => {
202 self.close_network_menu();
203 self.open_dialog_from(Mode::AddNode, origin, at);
204 }
205 _ => return false,
206 },
207 MenuOrigin::Playbar => match self.playbar_menu_actions.get(n).copied() {
208 Some(PlaybarMenuAction::PlatePage) => {
209 self.close_playbar_menu();
210 self.open_plate_page(PLAYBAR_IDX, at, origin);
211 }
212 _ => return false,
213 },
214 // A plate menu has no page rows (Add Tab was one, until
215 // docks lost their tabs).
216 MenuOrigin::Plate(_) => return false,
217 MenuOrigin::Node(slot) => match self.node_menu_actions.get(n).copied() {
218 Some(NodeMenuAction::Rename) => {
219 self.close_node_menu();
220 self.open_rename_dialog_from(slot, Some((origin, at)));
221 }
222 _ => return false,
223 },
224 },
225 }
226 true
227 }
228
229 /// Turn the dialog back: to the mode it was turned to from, else to the
230 /// menu it was turned to from, shown at the dialog's corner. `false`
231 /// when it came from neither.
232 pub fn dialog_back(&mut self) -> bool {
233 if !self.dialog_visible() {
234 return false;
235 }
236 if let Some(&prev) = self.dialog_trail.last() {
237 self.open_dialog_mode(prev);
238 return true;
239 }
240 let Some(origin) = self.dialog_from else { return false };
241 // The menu turns back out of the plate as it is drawn: it shrinks
242 // from the dialog's size into its own.
243 let (x, y, w, h) = self.positions[DIALOG_IDX];
244 let drawn = self.ui_context[self.slots.dialog].drawn_rect(cce_ui::scene::layout::Rect { x, y, width: w, height: h });
245 self.close_dialog();
246 self.close_open_menu();
247 self.reopen_menu(origin, drawn.x, drawn.y);
248 context_menu::turn_from_size(drawn.width, drawn.height, false);
249 true
250 }
251 }