git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit53eb8146c05bd9619273f5cd43a12ed125d14d05
parentf7c9c13020
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-06 21:31
feat(network): the plate is off by default, its menu a page of the viewport's

With the network's plate off, a right press on empty graph space is the
scene's and opens the viewport menu, so the network's own menu (Add Node,
Layout, Frame, the Plate page) had no way in by mouse. A Network row now
heads the viewport menu while the network has no plate and turns into the
network menu under a band back to the viewport's. The turn focuses the
network and puts the grid cursor on the cell the menu was opened over, so
Add Node places there; opening the viewport menu alone moves no cursor.

The plate is off by default. Tests written against the plated layout pin
it on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md        |  25 +++++++++++--
 src/app.rs       |  60 ++++++++++++++++++++++++++++++--
 src/main.rs      | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++----
 src/menu_page.rs |  13 ++++++-
 4 files changed, 190 insertions(+), 12 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 2290566..4689705 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2749,7 +2749,7 @@ relief is two shapes", "Frost is one block"); this is the designer's view of
 them, written because the question "what are the style parameters of the
 plates" took a session to answer from the code.
 
-### The network plate is optional
+### The network plate is optional, and off by default (since 2026-10-06)
 
 The network pane can drop its PLATE — the filled, frosted surface its graph
 sits on — so the nodes and wires overlay the 3D scene directly. The viewport is
@@ -2904,6 +2904,14 @@ are more immediate than a selection. There is also a `deselect` command, shipped
 UNBOUND so it is findable in the palette — deliberately not Ctrl+D, which the
 plugin uses for deselect-all but which this app already gives to Circular Pane.
 
+**Off is the default** (since 2026-10-06; `default_network_plate`, and
+what a state.kdl or a project display block without the key reads). A
+project saved with the plate on still opens with it on, its display block
+saying so. The suite's tests written against the plated layout — the dock
+geometry, the grid drags, a pointer at the viewport's middle — pin
+`state.network_plate = true` after `State::new`, which keeps what they
+check what it was.
+
 `ViewportSettings::network_plate` persists it, beside the viewport toggles
 rather than in the project's pane-state list: a pane's VISIBILITY belongs to
 the project, but whether its surface is drawn is how you like to work, and it
@@ -2945,6 +2953,7 @@ 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 **Network**, the network menu, while the network has no plate;
 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**
@@ -3015,7 +3024,19 @@ here would duplicate a key the loop claims.
 
 With the plate OFF the press never gets here — `in_network_pane` narrows to the
 nodes in overlay mode, so empty space is the scene's and opens the VIEWPORT
-menu. That is the overlay's whole rule, and it predates this menu.
+menu. That is the overlay's whole rule, and it predates this menu. So in
+overlay mode (the default since 2026-10-06) **this menu is a page of the
+viewport's**: a **Network** row heads the viewport menu
+(`ViewportMenuAction::NetworkPage`) and turns it into this one under a
+`‹ Viewport` band (`State::network_menu_from`). The viewport menu's press
+remembers the cell under it when it lands in the network's area
+(`State::network_menu_cell`), and the turn (`open_network_menu_from_viewport`)
+puts the grid cursor there and focuses the network — so Add Node places
+where the menu was opened, and Frame Cursor, gated on the network's focus,
+acts. Merely opening the viewport menu moves no cursor: the cursor is the
+selection, and a right-click to flip a display switch must not deselect.
+`the_network_menu_is_a_page_of_the_viewport_menu_without_the_plate` is the
+test.
 
 The press moves the grid cursor to the clicked cell BEFORE the menu goes up,
 because that cell is where Add Node will place what it adds — the cursor is the
diff --git a/src/app.rs b/src/app.rs
index 5dd5bc5..585c475 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -490,6 +490,10 @@ impl ViewportMenuPage {
 pub enum ViewportMenuAction {
     /// A row that turns the menu into one of its pages.
     Page(ViewportMenuPage),
+    /// The Network row, while the network has no plate: a page turning the
+    /// menu into the network's own menu, which a right press on empty graph
+    /// space can no longer reach — that space is the scene's.
+    NetworkPage,
     /// 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
@@ -2175,6 +2179,7 @@ pub struct ViewportSettings {
     /// settings rather than in a pane-state list because it is an appearance
     /// choice that outlives any one project — a pane's VISIBILITY belongs to
     /// the project, but whether its surface is drawn is how you like to work.
+    /// Off by default (since 2026-10-06): the nodes stand on the scene.
     #[serde(default = "default_network_plate")]
     pub network_plate: bool,
     /// Whether the params HUD draws its plate — one FITTED to its rows,
@@ -2399,7 +2404,7 @@ impl Default for RenderSettings {
 }
 
 fn default_network_plate() -> bool {
-    true
+    false
 }
 
 fn default_params_plate() -> bool {
@@ -2415,7 +2420,7 @@ impl Default for ViewportSettings {
             camera_pivot_size: 1.0,
             show_grid_enabled: true,
             show_origin_enabled: true,
-            network_plate: true,
+            network_plate: false,
             params_plate: true,
             node_wire_style: String::new(),
             show_point_markers: false,
@@ -3048,6 +3053,15 @@ pub struct State {
     /// with the flag saying the open menu is this one.
     pub network_menu_active: bool,
     pub network_menu_actions: Vec<NetworkMenuAction>,
+    /// The menu the network menu was turned to from — the viewport's, with
+    /// the plate off — which its back band returns to. None when a right
+    /// press on the graph opened it.
+    pub network_menu_from: Option<crate::menu_page::MenuOrigin>,
+    /// The grid cell under the viewport menu's right press, while the
+    /// network has no plate and the press was over its area: where the
+    /// grid cursor goes if the menu is turned to the network's, so Add Node
+    /// places at the cell pointed at, as the plated network's press does.
+    pub network_menu_cell: Option<(i32, i32)>,
     /// The plate corner menu — same `context_menu` thread-local again; the slot
     /// says which plate's control opened it (and doubles as the pressed state
     /// the corner control paints with).
@@ -7288,6 +7302,13 @@ impl State {
             None => {}
         }
 
+        // With the network's plate off, empty graph space is the scene's,
+        // so the network's menu is a page of this one, at its head.
+        if self.network_overlay() {
+            row(&mut options, &mut actions, "Network".into(), ViewportMenuAction::NetworkPage);
+            row(&mut options, &mut actions, "-".into(), sep);
+        }
+
         row(&mut options, &mut actions, "Frame All".into(), ViewportMenuAction::FrameAll);
         row(&mut options, &mut actions, "View 1:1".into(), ViewportMenuAction::OneToOne);
 
@@ -7355,6 +7376,11 @@ impl State {
                 let at = (cce_ui::widget::context_menu::x(), cce_ui::widget::context_menu::y());
                 self.show_viewport_menu_page(Some(page), Some(at));
             }
+            ViewportMenuAction::NetworkPage => {
+                let at = (cce_ui::widget::context_menu::x(), cce_ui::widget::context_menu::y());
+                self.close_viewport_menu();
+                self.open_network_menu_from_viewport(at);
+            }
             ViewportMenuAction::PinFollow => {
                 self.viewport_pin = None;
                 self.rebuild_scene_geometry();
@@ -7448,9 +7474,31 @@ 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.network_menu_from = None;
         self.open_network_context_menu_at(None);
     }
 
+    /// Turn the viewport menu, standing at `at`, into the network's: what
+    /// its Network row does while the network has no plate. The network
+    /// takes focus — its menu's commands are the network's, and the
+    /// cursor ones act only on a focused network — and the grid cursor goes
+    /// to the cell the menu was opened over, when it was opened over one.
+    pub(crate) fn open_network_menu_from_viewport(&mut self, at: (f32, f32)) {
+        if let Some((col, row)) = self.network_menu_cell.take() {
+            self.grid_cursor_col = col;
+            self.grid_cursor_row = row;
+        }
+        if self.focused_pane != LEFT_MENUBAR_IDX {
+            self.focused_pane = LEFT_MENUBAR_IDX;
+            if let Some(old) = self.focused_widget.take() {
+                self.slots.get_dyn_mut(old).unfocus();
+            }
+            self.sync_pane_focus();
+        }
+        self.network_menu_from = Some(crate::menu_page::MenuOrigin::Viewport);
+        self.open_network_context_menu_at(Some(at));
+    }
+
     /// 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)>) {
@@ -7489,7 +7537,8 @@ impl State {
         }
 
         let target = self.slots.get_dyn(CONTENT_IDX).base().id();
-        self.put_up_menu(at, None, options, 0, target);
+        let back = self.network_menu_from.filter(|_| at.is_some());
+        self.put_up_menu(at, back, 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;
@@ -8655,6 +8704,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             structure_base: None,
             network_menu_active: false,
             network_menu_actions: Vec::new(),
+            network_menu_from: None,
+            network_menu_cell: None,
             sim_cache: crate::geometry::SimCache::default(),
             playbar_cache_key: None,
             page_image: None,
@@ -11857,6 +11908,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                             self.close_network_menu();
                             self.close_param_menu();
                             if self.cursor_in_viewport() && !in_circle_network_pane {
+                                self.network_menu_cell = (self.network_overlay()
+                                    && self.in_network_area(self.cursor_x, self.cursor_y))
+                                .then(|| self.cell_at(self.cursor_x, self.cursor_y));
                                 self.open_viewport_context_menu();
                                 return true;
                             }
diff --git a/src/main.rs b/src/main.rs
index bb212e1..9642eda 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -407,6 +407,7 @@ mod tests {
         use crate::slots::{NETWORK_PANEL_IDX, PARAM_IDX, PLAYBAR_IDX, SPREADSHEET_IDX};
         use cce_ui::widget::{context_menu, MouseButton};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.show_spreadsheet = true;
         state.show_playbar = true;
@@ -636,6 +637,79 @@ mod tests {
         assert!(state.viewport_menu_open(), "a right-click beside the plate opens the viewport menu");
     }
 
+    /// With the network's plate off (the default) a right press on empty
+    /// graph space is the scene's and opens the VIEWPORT menu, so the
+    /// network's own menu is a page of it: a Network row at its head that
+    /// turns into the network menu under a band back to the viewport's.
+    /// Turning there focuses the network and puts the grid cursor on the
+    /// cell the menu was opened over, where Add Node will place; a menu
+    /// opened over no network area moves no cursor. With the plate on there
+    /// is no such row: the network's empty space opens its menu itself.
+    #[test]
+    fn the_network_menu_is_a_page_of_the_viewport_menu_without_the_plate() {
+        use crate::app::{NetworkMenuAction, ViewportMenuAction as A};
+        use crate::menu_page::MenuOrigin;
+        use crate::slots::LEFT_MENUBAR_IDX;
+        use crate::window::{LocalPosition, WindowEvent};
+        use cce_ui::widget::context_menu::{self, PageTurn};
+        use cce_ui::widget::{ElementState, MouseButton};
+        let mut state = State::new(false);
+        state.resize(1600.0, 900.0, 1.0);
+        state.rebuild_positions();
+        state.apply_layout();
+        assert!(state.network_overlay(), "the plate is off by default");
+        let press = |state: &mut State, x: f32, y: f32, b: MouseButton| {
+            state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } });
+            state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: b });
+            state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: b });
+        };
+
+        // An empty cell of the graph, away from the cursor and the HUD.
+        let (cx, cy, cw, ch) = state.positions[crate::slots::CONTENT_IDX];
+        let (px, py) = (cx + cw * 0.3, cy + ch * 0.6);
+        assert!(state.graph().node_at(px, py).is_none() && !state.over_floating_pane_at(px, py));
+        let cell = state.cell_at(px, py);
+        assert_ne!((state.grid_cursor_col, state.grid_cursor_row), cell, "pick a cell the cursor is not on");
+        press(&mut state, px, py, MouseButton::Right);
+        assert!(state.viewport_menu_open(), "empty graph space is the scene's");
+        assert_eq!(state.viewport_menu_actions.first(), Some(&A::NetworkPage));
+        assert!(context_menu::leads_to_page(0), "a page row");
+        assert_eq!(context_menu::options().first().map(String::as_str), Some("Network"));
+        assert_ne!((state.grid_cursor_col, state.grid_cursor_row), cell, "opening the menu moves no cursor");
+
+        // The turn: the network menu, under a band back to the viewport's.
+        assert!(state.run_menu_turn(PageTurn::Into(0)));
+        assert_eq!(state.open_menu_origin(), Some(MenuOrigin::Network));
+        assert_eq!(state.network_menu_actions.first(), Some(&NetworkMenuAction::Command("add_node")));
+        assert_eq!(state.network_menu_from, Some(MenuOrigin::Viewport));
+        assert_eq!((state.grid_cursor_col, state.grid_cursor_row), cell, "the cursor is on the cell pressed");
+        assert_eq!(state.focused_pane, LEFT_MENUBAR_IDX, "the network has focus");
+
+        // Back to the viewport menu, then forward again by its row.
+        assert!(state.run_menu_turn(PageTurn::Back));
+        assert_eq!(state.open_menu_origin(), Some(MenuOrigin::Viewport));
+        assert_eq!(state.network_menu_from, None);
+        state.run_viewport_menu_action(A::NetworkPage);
+        assert_eq!(state.open_menu_origin(), Some(MenuOrigin::Network));
+
+        // Add Node from there, and back out of the dialog to the network
+        // menu, which still goes back to the viewport's.
+        let add = state.network_menu_actions.iter().position(|a| *a == NetworkMenuAction::Command("add_node")).unwrap();
+        assert!(state.run_menu_turn(PageTurn::Into(add)));
+        assert!(state.dialog_visible());
+        assert!(state.dialog_back());
+        assert_eq!(state.open_menu_origin(), Some(MenuOrigin::Network));
+        assert!(state.run_menu_turn(PageTurn::Back));
+        assert_eq!(state.open_menu_origin(), Some(MenuOrigin::Viewport));
+        state.close_viewport_menu();
+
+        // With the plate on, the viewport menu has no Network row.
+        state.network_plate = true;
+        state.rebuild_positions();
+        state.apply_layout();
+        assert!(!state.viewport_menu_rows_of(None).1.contains(&A::NetworkPage));
+    }
+
     #[test]
     fn spreadsheet_header_press_reaches_the_widget_not_the_camera() {
         use crate::slots::{SPREADSHEET_IDX, SPREADSHEET_MENUBAR_IDX, VIEWPORT_IDX};
@@ -962,6 +1036,7 @@ mod tests {
         use crate::app::Dock;
         use crate::slots::{NETWORK_PANEL_IDX, SPREADSHEET_IDX};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.show_spreadsheet = true;
         state.rebuild_positions();
@@ -2114,6 +2189,7 @@ mod tests {
         use crate::app::{playbar_shelf_h, PLAYBAR_H, STATUS_H};
         use crate::slots::{NETWORK_PANEL_IDX, PLAYBAR_IDX, SPREADSHEET_IDX};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.execute_menu_action("Show Playbar Pane");
         state.execute_menu_action("Show Spreadsheet Pane");
@@ -2153,6 +2229,7 @@ mod tests {
         use crate::app::{Dock, NO_PANE};
         use crate::slots::{NETWORK_PANEL2_IDX, PARAM_IDX, PLAYBAR_IDX, SPREADSHEET_IDX};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -2276,6 +2353,7 @@ mod tests {
     #[test]
     fn the_params_plate_fits_its_rows() {
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         assert!(state.params_plate, "the plate is on by default");
         assert_eq!(state.command_toggle_state("toggle_params_plate"), Some(true));
         // A HUD with no rows collapses its plate to the small circle, and
@@ -2730,7 +2808,8 @@ mod tests {
     #[test]
     fn the_viewport_menu_groups_its_display_rows() {
         use crate::app::{ViewportMenuAction as A, ViewportMenuPage as P};
-        let state = State::new(false);
+        let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         let groups = |page: Option<P>| -> Vec<Vec<A>> {
             let (options, actions) = state.viewport_menu_rows_of(page);
             assert_eq!(options.len(), actions.len());
@@ -3344,6 +3423,7 @@ mod tests {
         use crate::window::{LocalPosition, WindowEvent};
         use cce_ui::widget::{context_menu, ElementState, MouseButton, MouseScrollDelta};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.cursor_x = 300.0;
         state.cursor_y = 200.0;
         state.open_viewport_context_menu();
@@ -3785,6 +3865,7 @@ mod tests {
         let _ = fs::remove_dir_all(&dir);
 
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -3795,6 +3876,7 @@ mod tests {
         state.set_active_camera("Default Camera");
         state.save_to_file(&dir).expect("save");
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -3817,11 +3899,13 @@ mod tests {
 
         // New: back to the Default Camera from a node, on both copies.
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.new_project();
         assert_eq!(state.viewport().active_camera, "Default Camera");
 
         // The viewport menu's choice: a node, then the default again.
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.set_active_camera("Default Camera");
         state.set_active_camera("camera1");
         assert_eq!(state.viewport().active_camera, "camera1");
@@ -3882,6 +3966,7 @@ mod tests {
         use crate::window::{LocalPosition, WindowEvent};
         use cce_ui::widget::{ElementState, MouseButton, MouseScrollDelta, Position};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -9590,7 +9675,7 @@ mod tests {
         use crate::command::{by_id, Run};
 
         let state = State::new(false);
-        assert!(state.network_plate, "the plate is on unless the user turned it off");
+        assert!(!state.network_plate, "the plate is off unless the user turned it on");
 
         // The command exists, is rebindable, and runs the same action the menu
         // row does — one implementation behind both.
@@ -9604,10 +9689,10 @@ mod tests {
         // than mirroring the flag onto a node that could fall out of step
         // with it.
         let mut state = State::new(false);
-        assert_eq!(state.command_toggle_state("toggle_network_plate"), Some(true));
-        assert!(state.run_command("toggle_network_plate"));
-        assert!(!state.network_plate);
         assert_eq!(state.command_toggle_state("toggle_network_plate"), Some(false));
+        assert!(state.run_command("toggle_network_plate"));
+        assert!(state.network_plate);
+        assert_eq!(state.command_toggle_state("toggle_network_plate"), Some(true));
 
         state.run_command("command_palette");
         let row = state
@@ -9617,7 +9702,7 @@ mod tests {
             .iter()
             .find(|r| r.id == "toggle_network_plate")
             .expect("a Network Plate row");
-        assert_eq!(row.toggle(), Some(false), "the row's switch reads the live flag");
+        assert_eq!(row.toggle(), Some(true), "the row's switch reads the live flag");
     }
 
     /// The viewport guide toggles survive the next parameter edit.
@@ -16208,6 +16293,7 @@ mod tests {
         use crate::window::{LocalPosition, WindowEvent};
         use cce_ui::widget::{ElementState, MouseButton};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -16463,6 +16549,7 @@ mod tests {
         use crate::window::{LocalPosition, WindowEvent};
         use cce_ui::widget::{ElementState, MouseButton};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -16542,6 +16629,7 @@ mod tests {
         use crate::window::{LocalPosition, WindowEvent};
         use cce_ui::widget::{ElementState, MouseButton};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -16961,6 +17049,7 @@ mod tests {
         use crate::window::{LocalPosition, WindowEvent};
         use cce_ui::widget::{ElementState, MouseButton};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -17964,6 +18053,7 @@ mod tests {
         use crate::window::{LocalPosition, WindowEvent};
         use cce_ui::widget::{scroll_motion::set_scroll_phase, MouseScrollDelta, Position, ScrollPhase};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -18329,6 +18419,7 @@ mod tests {
         use crate::window::{LocalPosition, WindowEvent};
         use cce_ui::widget::{scroll_motion::set_scroll_phase, MouseScrollDelta, ParametersBg, Position, ScrollPhase};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
@@ -18463,6 +18554,7 @@ mod tests {
         use crate::window::{LocalPosition, WindowEvent};
         use cce_ui::widget::{scroll_motion::set_scroll_phase, MouseScrollDelta, ParametersBg, Position, ScrollPhase};
         let mut state = State::new(false);
+        state.network_plate = true; // written against the plated network
         state.resize(1600.0, 900.0, 1.0);
         state.rebuild_positions();
         state.apply_layout();
diff --git a/src/menu_page.rs b/src/menu_page.rs
index d80a6c6..08cd78e 100644
--- a/src/menu_page.rs
+++ b/src/menu_page.rs
@@ -67,7 +67,7 @@ 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(_))
+        matches!(self, ViewportMenuAction::Page(_) | ViewportMenuAction::NetworkPage)
     }
 }
 
@@ -189,6 +189,13 @@ impl State {
                 MenuOrigin::Viewport if self.viewport_menu_page.is_some() => {
                     self.show_viewport_menu_page(None, Some(at));
                 }
+                // The network menu turned to from the viewport's (the plate
+                // off) goes back to it.
+                MenuOrigin::Network if self.network_menu_from.is_some() => {
+                    let from = self.network_menu_from.take().unwrap();
+                    self.close_network_menu();
+                    self.reopen_menu(from, at.0, at.1);
+                }
                 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.
@@ -203,6 +210,10 @@ impl State {
             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::NetworkPage) => {
+                        self.close_viewport_menu();
+                        self.open_network_menu_from_viewport(at);
+                    }
                     _ => return false,
                 },
                 MenuOrigin::Network => match self.network_menu_actions.get(n).copied() {