git.lucas.co / cce-designer
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 }