file manager
git clone https://git.lucas.co/cce-files.git
feat(view): the view switch moves into the breadcrumb's context menu
The List/Graph/Space dropdown beside the breadcrumb is gone; the
breadcrumb runs the pane's full width. Right-clicking it opens the
toolkit's shared menu as before (segment header + Copy Path), which the
app re-shows in place with a separator and a row per other view. The
rows are app-run (BreadcrumbRow), routed ahead of the toolkit's
dispatch on press or release, and only while the menu's show
generation still matches. The current view is left out rather than
marked: the menu face has no radio or check glyph.
Page::view_label() names the rows, so nothing depends on a label list
tracking Page::ALL's order. pages::dropdown_relief had no other caller
and goes with the dropdown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 4 +-
src/main.rs | 191 +++++++++++++++++++++++++++------------------------
src/pages/browse.rs | 10 ++-
src/pages/mod.rs | 75 ++++++--------------
src/pages/network.rs | 11 ++-
src/pages/space.rs | 12 ++--
6 files changed, 138 insertions(+), 165 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 095d15c..2f1bfbf 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -39,7 +39,7 @@ Shared, page-independent formatting helpers (`format_size`, `format_permissions`
- **Overlay occlusion**: text/buttons under a popover, context menu, or dialog are either discarded or bound-clipped so they don't bleed through overlays. This is the logic behind commits like "Fix text rendering through popovers/overlays."
### Widgets register parentless, once per rebuild
-There are no composite container widgets. Every widget is owned outright by the app or by a page's state struct — `self.paginator`, `self.view_dropdown`, `self.browse.breadcrumb`, `self.browse.save_name_box`, `self.network.graph`, `self.space.breadcrumb`, … — and each `rebuild_layout` re-establishes the whole hierarchy from scratch: `ui_context.clear_hierarchy()`, then a teardown block calling `clear_children` + `set_parent(None)` on every widget, then registration via `ctx.register_widget(w.base().id(), w.as_ptr_mut())` with `set_parent(None)` again. Raw pointers are still involved (`as_ptr_mut`, plus a `self_ptr` alias so the registration loop can hold the app twice), but they belong to the cce-ui widget model rather than to any container of this crate's.
+There are no composite container widgets. Every widget is owned outright by the app or by a page's state struct — `self.paginator`, `self.browse.breadcrumb`, `self.browse.save_name_box`, `self.network.graph`, `self.space.breadcrumb`, … — and each `rebuild_layout` re-establishes the whole hierarchy from scratch: `ui_context.clear_hierarchy()`, then a teardown block calling `clear_children` + `set_parent(None)` on every widget, then registration via `ctx.register_widget(w.base().id(), w.as_ptr_mut())` with `set_parent(None)` again. Raw pointers are still involved (`as_ptr_mut`, plus a `self_ptr` alias so the registration loop can hold the app twice), but they belong to the cce-ui widget model rather than to any container of this crate's.
**When adding a widget, add it to both halves of that pass** — the teardown block and the registration block. Skipping the teardown leaves hierarchy links alive across frames.
@@ -49,7 +49,7 @@ There are no composite container widgets. Every widget is owned outright by the
- **Browse** — the `List` widget (columnar, integrated search box) plus a `Breadcrumb`. The right pane is a `Preview` widget, split from the list by `SplitPane` — app-owned (`main.rs`), carrying the `SplitBox` two-child horizontal math verbatim plus the divider quad, its hover tint, and the proportion drag.
- **Network** — a `Graph` view of the same directory (nodes = entries), also split against the preview. It uses the configured lattice and node body as the designer's network does (a node is CENTRED on a crossing, its name hangs off its right side), laid out to fit its pane by `NetworkState::lay_out`: the pitch across holds a body, the name's gap and the widest name (cut with an ellipsis past `NAME_MAX`), as many columns as fit, column 0 a margin inside the pane, re-laid out when the pane changes size. The graph is cut to its pane (`PageContent::clipped_to`) for when it is panned. Until 2026-10-06 it set the retired cell model (140 x 70 cells, origin 60, 60, seven columns), which the lattice turned into nodes left of the pane, past its right edge, and names lying across their neighbours. `the_graph_fits_its_pane_and_its_names_fit_their_columns` is the test. Its wires and ports draw since 2026-10-06: `render_widget` hands a widget's strokes, arcs and discs to `RenderTarget::line` / `arc` / `circle` (cce-ui c9b073a), which `PageContent` keeps as `strokes` and `rebuild_layout` paints as `WidgetFx::Stroke`, cut by a clip as a glyph is. The nodes wear no geometry toggle (`Graph::set_show_toggles(false)`). Nodes stand from column and row 1, the lattice's (0, 0) a pitch outside the pane, so its heavy axes do not lie over column 0's wires. A stroke's page-view clip limits it top and bottom only: clipped to its own one-pixel box across, a vertical wire on a half pixel rounded away and only the horizontal runs drew.
- **Space** — a GrandPerspective-style treemap of the whole subtree, also split against the preview.
-The active page is picked by `view_dropdown` next to the breadcrumb (there is no sidebar — it was removed; `has_sidebar` is hardcoded `false`). The dropdown's labels name the *visualization* ("List"/"Graph"/"Space") and are a separate list from `Page::label()` ("Browse"/"Network"/"Space") — but **its order must track `Page::ALL`**, because the selected index is indexed straight into it.
+The active page is picked from the breadcrumb's context menu (there is no sidebar — it was removed; `has_sidebar` is hardcoded `false` — and since 2026-10-06 no view dropdown beside the breadcrumb either, which now runs the pane's full width). Right-clicking the breadcrumb opens the toolkit's shared menu (segment header + Copy Path); the app then re-shows it in place with a separator and a row per *other* view, named by `Page::view_label()` ("List"/"Graph"/"Space" — the visualization, not `Page::label()`). The rows are app-run (`BreadcrumbRow`, `breadcrumb_menu_rows`), routed in `handle_mouse_input` ahead of the toolkit's dispatch like the plate-dock rows, on press or release, and only while `context_menu::generation()` still matches the show that made them. The current view is left out rather than marked: the menu face has no radio or check glyph.
### The list's scrollbar rides its centre line, behind the well
Browse's row list (`row_list.rs`, `RowList`) follows the DE's one scrollbar rule (cce-ui's CLAUDE.md, "Every scrollbar rides a centre line, behind the plate"; since 2026-10-06 — it was a flat bar at the right edge, always on top). The bar is a pair of pills down the middle of the list's width, `centred_scrollbar_width()` thick, over the rows: no column reserves a lane for it. A `ScrollbarActivity` decides its depth. Sunk, it is drawn before the well's translucent `list_bg_color` fill, which dims it, and it takes no press: a press on its lane goes to the row under it. A wheel, a glide or coast in motion, `scroll_into_view` (how keyboard navigation scrolls the list) and a thumb drag's release raise it, and a pointer over a raised bar holds it up. `RowList::tick`, which the app's `tick` already runs for the glide, ticks the activity too and returns true while the hold or the fade is running, so frames keep coming until the bar has sunk. `push_prims` draws the idle copy at full alpha every frame and the fore copy after the row overlays at `fade()`. That order holds because the list's prims go into the PAGE's `PageContent`, whose rects are replayed in call order (only `plain_pc` is partitioned by radius — see the note at the top of `row_list.rs`). A part's icons and reliefs are replayed after all its rects, so the row glyphs and the well's wall still draw over the fore copy. `the_scrollbar_rides_the_centre_and_sinks_until_scrolled` is the test.
diff --git a/src/main.rs b/src/main.rs
index 79c360b..f1aa8ae 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -299,6 +299,36 @@ struct ContextMenu {
hovered: Option<usize>,
}
+/// A row of the breadcrumb's context menu that the app runs itself.
+#[derive(Clone, Debug, PartialEq)]
+enum BreadcrumbRow {
+ CopyPath(String),
+ View(Page),
+}
+
+/// The breadcrumb's context menu while it is open: which show of the shared
+/// menu it is (`context_menu::generation`), so a stale one never runs, and
+/// what each row does (`None` for the header and the separator).
+struct BreadcrumbMenu {
+ generation: u64,
+ rows: Vec<Option<BreadcrumbRow>>,
+}
+
+/// The breadcrumb menu below its header: Copy Path, then the view switch —
+/// a dropdown beside the breadcrumb until 2026-10-06 — offering the views
+/// other than the one showing. Not all three with the current one marked:
+/// the menu face has no radio or check glyph (a "●" drew as a blank), and
+/// the page itself says which view it is.
+fn breadcrumb_menu_rows(path: String, current: Page) -> (Vec<String>, Vec<Option<BreadcrumbRow>>) {
+ let mut labels = vec!["Copy Path".to_string(), "-".to_string()];
+ let mut rows = vec![Some(BreadcrumbRow::CopyPath(path)), None];
+ for page in Page::ALL.into_iter().filter(|&p| p != current) {
+ labels.push(page.view_label().to_string());
+ rows.push(Some(BreadcrumbRow::View(page)));
+ }
+ (labels, rows)
+}
+
/// App-owned two-pane horizontal split, replacing the dissolved `SplitBox` +
/// `BrowseContainer`/`NetworkContainer` shims (Phase 6y). Those existed to (a) position
/// pane content — but the pages already lay out and render everything from the pane rect,
@@ -498,7 +528,6 @@ struct FilesystemApp {
cursor_x: f32,
cursor_y: f32,
paginator: cce_ui::widget::Adapted<Paginator>,
- view_dropdown: cce_ui::widget::Adapted<cce_ui::widget::Dropdown>,
just_initialized: bool,
ui_context: cce_ui::context::UiContext,
watcher: Option<notify::RecommendedWatcher>,
@@ -521,6 +550,9 @@ struct FilesystemApp {
/// Rows of the OPEN plate-dock corner menu (empty = not ours); routed
/// before the generic context-menu dispatch.
plate_menu_actions: Vec<cce_ui::widget::plate_dock::PlateDockAction>,
+ /// The breadcrumb's context menu when it is the one open; routed, like
+ /// the plate-dock rows, before the generic context-menu dispatch.
+ breadcrumb_menu: Option<BreadcrumbMenu>,
// Space's double-click is tracked by path, not row index: its tiles are
// renumbered by every relayout, so an index would not survive a resize.
last_space_click_time: std::time::Instant,
@@ -674,7 +706,6 @@ impl FilesystemApp {
// Clear all widgets' hierarchy links
self.paginator.clear_children(&mut self.ui_context); self.paginator.set_parent(None, &mut self.ui_context);
- self.view_dropdown.clear_children(&mut self.ui_context); self.view_dropdown.set_parent(None, &mut self.ui_context);
self.browse.save_name_box.clear_children(&mut self.ui_context); self.browse.save_name_box.set_parent(None, &mut self.ui_context);
@@ -711,8 +742,6 @@ impl FilesystemApp {
self.ui_context.register_widget((*self_ptr).paginator.base().id(), (*self_ptr).paginator.as_ptr_mut());
(*self_ptr).paginator.set_parent(None, &mut self.ui_context);
}
- self.ui_context.register_widget((*self_ptr).view_dropdown.base().id(), (*self_ptr).view_dropdown.as_ptr_mut());
- (*self_ptr).view_dropdown.set_parent(None, &mut self.ui_context);
}
}
@@ -757,10 +786,6 @@ impl FilesystemApp {
let self_ptr = self as *mut Self;
unsafe {
let mut plain_pc = pages::PageContent::new();
- // NOT the view dropdown: every page's `view()` already renders
- // it, so a copy here was a second draw of the same widget — at
- // the previous frame's rect, and compositing its label's
- // antialiased edges twice into a faux-bold.
// The dissolved splitter's paint: its divider quad, then the preview
// pane (the only pane content the pages don't render themselves). The
// left pane's container copy is gone — the legacy aggregate painted it
@@ -800,10 +825,6 @@ impl FilesystemApp {
window_pc.rects.extend(plain);
window_pc.rects.extend(rounded);
window_pc.absorb(plain_pc);
-
- // The view dropdown's flush inset plate is carved below, once
- // the pages have laid the dropdown out — carving it here would
- // read the previous frame's rect (`pages::dropdown_relief`).
}
}
@@ -812,36 +833,24 @@ impl FilesystemApp {
match self.current_page {
Page::Browse => {
let (bx, by, bw, bh) = self.browse_split.left_rect();
- let browse_pc = pages::browse::view(&mut self.browse, &mut self.view_dropdown, bx, by, bw, bh, self.select_mode, &mut self.ui_context);
+ let browse_pc = pages::browse::view(&mut self.browse, bx, by, bw, bh, self.select_mode, &mut self.ui_context);
pc.absorb(browse_pc);
}
Page::Network => {
let (nx, ny, nw, nh) = self.network_split.left_rect();
- let network_pc = pages::network::view(&mut self.network, &self.browse, &mut self.view_dropdown, nx, ny, nw, nh, &mut self.ui_context);
+ let network_pc = pages::network::view(&mut self.network, &self.browse, nx, ny, nw, nh, &mut self.ui_context);
pc.absorb(network_pc);
}
Page::Space => {
let (sx, sy, sw, sh) = self.space_split.left_rect();
- let space_pc = pages::space::view(&mut self.space, &self.browse, &mut self.view_dropdown, sx, sy, sw, sh, &mut self.ui_context);
+ let space_pc = pages::space::view(&mut self.space, &self.browse, sx, sy, sw, sh, &mut self.ui_context);
pc.absorb(space_pc);
}
}
- // The view dropdown's flush inset plate (control_relief) lives in its
- // modern paint(); the flat view loses it, so carve it here — from the
- // rect the page above just laid the dropdown out at, NOT the one it
- // held when this method started.
- {
- let (dx, dy, dw, dh) = self.view_dropdown.rect();
- pages::dropdown_relief(
- &mut window_pc,
- cce_ui::scene::layout::Rect { x: dx, y: dy, width: dw, height: dh },
- );
- }
-
// Draw bottom selection bar if select_mode is enabled. It lives below the
// content region, so it goes into window_pc: page content (pc) is clipped
// to the viewport and would swallow the bar entirely.
@@ -1330,7 +1339,7 @@ impl Application for FilesystemApp {
}
// The engine ticks the exposed context each loop — this is what drives the
- // dropdown expand/contract animation frames.
+ // registered widgets' animation frames.
fn ui_context_mut(&mut self) -> Option<&mut cce_ui::context::UiContext> {
Some(&mut self.ui_context)
}
@@ -1418,14 +1427,6 @@ impl Application for FilesystemApp {
let pages_names = Page::ALL.iter().map(|p| p.label().to_string()).collect::<Vec<_>>();
let paginator = cce_ui::widget::Paginator::new(pages_names);
- // These name the visualization rather than the page, so they are not
- // Page::label(). Order MUST track Page::ALL — the selected index is
- // indexed straight into it when the dropdown changes.
- let view_dropdown = cce_ui::widget::Dropdown::new(
- vec!["List".to_string(), "Graph".to_string(), "Space".to_string()],
- 0,
- ).with_font_family(&cce_ui::layout::list_font_parsed().0);
-
let fs_service = services::fs::FsService::new(sender.clone());
let initial_w = if select_mode { 900 } else { 1200 };
let initial_h = if select_mode { 500 } else { 720 };
@@ -1439,6 +1440,7 @@ impl Application for FilesystemApp {
preview_dock: Default::default(),
preview_prior_fracs: None,
plate_menu_actions: Vec::new(),
+ breadcrumb_menu: None,
select_mode,
select_directory,
save_mode,
@@ -1455,7 +1457,6 @@ impl Application for FilesystemApp {
cursor_x: 0.0,
cursor_y: 0.0,
paginator,
- view_dropdown,
just_initialized: true,
ui_context: cce_ui::context::UiContext::new(),
watcher: None,
@@ -1535,7 +1536,6 @@ impl Application for FilesystemApp {
self.current_page = page;
let page_idx = Page::ALL.iter().position(|&p| p == page).unwrap_or(0);
self.paginator.set_selected_page(page_idx);
- self.view_dropdown.selected = page_idx;
// Switching to Space is what triggers the first scan — it is
// far too expensive to run for a page nobody is looking at.
self.ensure_space_scan();
@@ -1737,9 +1737,8 @@ impl Application for FilesystemApp {
fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
// Pump the widget tick walk (the cce-data-editor pattern): animating
- // widgets — the view dropdown's expand/contract menu — register as
- // tick receivers and report changed until their transition lands;
- // without this the close animation freezes at fully open.
+ // widgets register as tick receivers and report changed until their
+ // transition lands; without this an animation freezes mid-way.
if self.ui_context.tick(dt) {
*needs_rebuild = true;
self.needs_rebuild = true;
@@ -2047,13 +2046,6 @@ impl Application for FilesystemApp {
}
}
- {
- let root = self.view_dropdown.id();
- if self.ui_context.propagate_event(&mv, root) {
- changed = true;
- }
- }
-
if self.current_page == Page::Browse {
if self.select_mode {
let root = self.browse.save_name_box.id();
@@ -2159,6 +2151,37 @@ impl Application for FilesystemApp {
return None;
}
+ // The breadcrumb's menu rows run here too, ahead of the toolkit's
+ // dispatch — it would run Copy Path but has no notion of the view
+ // rows. On the press as well as the release: the toolkit runs a row
+ // on the PRESS when one reaches it, so whichever arrives first wins,
+ // and the hide it does leaves nothing for the other.
+ if button == MouseButton::Left {
+ use cce_ui::widget::context_menu as cm;
+ let live = self.breadcrumb_menu.as_ref().is_some_and(|m| cm::is_visible() && m.generation == cm::generation());
+ if !live {
+ self.breadcrumb_menu = None;
+ } else if cm::hit_test(pos.x as f32, pos.y as f32) {
+ let picked = cm::row_at(pos.x as f32, pos.y as f32)
+ .and_then(|row| self.breadcrumb_menu.as_ref()?.rows.get(row).cloned().flatten());
+ // A press on the header or the separator leaves the menu up.
+ if let Some(row) = picked {
+ cm::hide();
+ self.breadcrumb_menu = None;
+ *needs_rebuild = true;
+ self.needs_rebuild = true;
+ return match row {
+ BreadcrumbRow::CopyPath(path) => {
+ cce_ui::widget::clipboard::copy_to_clipboard(&path);
+ None
+ }
+ BreadcrumbRow::View(page) => Some(Message::SwitchPage(page)),
+ };
+ }
+ return None;
+ }
+ }
+
// A left RELEASE on the preview pane's corner control opens its menu
// (the designer also opens on release; presses do not reliably reach
// this hook — see above). No press arming: the arming protocol
@@ -2291,9 +2314,20 @@ impl Application for FilesystemApp {
let ev = cce_ui::widget::Event::MouseButton { button, state, x: pos.x, y: pos.y, local_x: pos.x, local_y: pos.y };
let root = breadcrumb.id();
// The breadcrumb opens the toolkit's shared context menu itself
- // (open_context_menu → segment header + Copy Path); the app just
- // routes the event and redraws — no app-side menu duplicate.
+ // (open_context_menu → segment header + Copy Path), recording
+ // the segment pressed. The app then shows that menu again,
+ // where it stands and under the same header, with the view
+ // switch added: the toolkit builds a breadcrumb's rows by type
+ // name and takes none from the app.
if self.ui_context.propagate_event(&ev, root) {
+ use cce_ui::widget::context_menu as cm;
+ let seg = breadcrumb.right_clicked_seg.unwrap_or(breadcrumb.path.len());
+ let (labels, actions) = breadcrumb_menu_rows(breadcrumb.path_to_seg(seg), self.current_page);
+ let header = cm::options().into_iter().next().unwrap_or_default();
+ let options: Vec<String> = std::iter::once(header).chain(labels).collect();
+ let rows = std::iter::once(None).chain(actions).collect();
+ cm::show(cm::x(), cm::y(), options, 1, root);
+ self.breadcrumb_menu = Some(BreadcrumbMenu { generation: cm::generation(), rows });
*needs_rebuild = true;
self.needs_rebuild = true;
return None;
@@ -2400,22 +2434,6 @@ impl Application for FilesystemApp {
return None;
}
- if { let root = self.view_dropdown.id(); self.ui_context.propagate_event(&ev, root) } {
- *needs_rebuild = true;
- self.needs_rebuild = true;
- if self.view_dropdown.take_change() {
- // Indexed off Page::ALL rather than hand-mapped: the old
- // `== 0 { Browse } else { Network }` silently sent every
- // entry past the first to Network.
- let new_page = Page::ALL
- .get(self.view_dropdown.selected)
- .copied()
- .unwrap_or(Page::Browse);
- return Some(Message::SwitchPage(new_page));
- }
- return None;
- }
-
// The dissolved splitter's divider: a left press grabs it (stealing keyboard
// focus like the legacy ctx.set_focused_ptr / release's clear_focus pair did),
// a release ends the drag. A collapsed preview owns its column width;
@@ -2444,7 +2462,7 @@ impl Application for FilesystemApp {
// plate-dock control above: the routed-widget path consumes left
// PRESSES before this hook, so only releases reliably arrive here.
// Every overlay above the page (plate-dock menu, context menu, dialog,
- // dropdown, divider grab) has already returned, so a release reaching
+ // divider grab) has already returned, so a release reaching
// here is on page content: move the accent ring to the well it landed
// in, then let the release do its normal work. Releases outside any
// well (toolbar, plate) change nothing.
@@ -2785,28 +2803,6 @@ impl Application for FilesystemApp {
}
}
- // An open view dropdown takes the keyboard — Escape closes it, arrows
- // move the hover, Enter selects — routed to the widget exactly like
- // its mouse events are (see handle_mouse_input). Gated on `open`:
- // this app routes keys per feature rather than to a whole tree, and
- // the dropdown was simply never on the list, which left its own
- // Escape handling unreachable.
- if self.view_dropdown.open {
- let kev = cce_ui::widget::Event::KeyInput(event.clone());
- if { let root = self.view_dropdown.id(); self.ui_context.propagate_event(&kev, root) } {
- *needs_rebuild = true;
- self.needs_rebuild = true;
- if self.view_dropdown.take_change() {
- let new_page = Page::ALL
- .get(self.view_dropdown.selected)
- .copied()
- .unwrap_or(Page::Browse);
- return Some(Message::SwitchPage(new_page));
- }
- return None;
- }
- }
-
// The dissolved List's search keys, app-side: the open shortcut shows the strip
// and focuses the box; the close shortcut hides it and clears the filter (the
// legacy List set just_changed after clearing, which surfaced as an empty
@@ -3019,4 +3015,21 @@ mod tests {
assert_eq!(b[0], 80.0);
assert_eq!(b[2], WIN[2]);
}
+
+ #[test]
+ fn the_breadcrumb_menu_offers_the_other_views() {
+ let (labels, rows) = breadcrumb_menu_rows("/home/me".to_string(), Page::Network);
+ assert_eq!(labels, ["Copy Path", "-", "List", "Space"]);
+ assert_eq!(rows, [
+ Some(BreadcrumbRow::CopyPath("/home/me".to_string())),
+ None,
+ Some(BreadcrumbRow::View(Page::Browse)),
+ Some(BreadcrumbRow::View(Page::Space)),
+ ]);
+ for page in Page::ALL {
+ let (labels, rows) = breadcrumb_menu_rows(String::new(), page);
+ assert_eq!(labels.len(), rows.len(), "a row for every label");
+ assert!(!rows.contains(&Some(BreadcrumbRow::View(page))), "{page:?} offers itself");
+ }
+ }
}
diff --git a/src/pages/browse.rs b/src/pages/browse.rs
index 5be0815..6b8fa8e 100644
--- a/src/pages/browse.rs
+++ b/src/pages/browse.rs
@@ -212,7 +212,7 @@ pub fn next_selection_index(state: &BrowseState, direction: BrowseNavigation) ->
// ── View ────────────────────────────────────────────────────────────
-pub fn view(state: &mut BrowseState, view_dropdown: &mut cce_ui::widget::Adapted<cce_ui::widget::Dropdown>, cx: f32, cy: f32, cw: f32, ch: f32, select_mode: bool, ctx: &mut cce_ui::context::UiContext) -> PageContent {
+pub fn view(state: &mut BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, select_mode: bool, ctx: &mut cce_ui::context::UiContext) -> PageContent {
let mut pc = PageContent::new();
let text_dim = cce_ui::color::TEXT_DIM;
@@ -232,10 +232,9 @@ pub fn view(state: &mut BrowseState, view_dropdown: &mut cce_ui::widget::Adapted
let breadcrumb_h = 24.0;
let textbox_h = cce_ui::layout::textbox_height();
- // 1. Allocate and render Breadcrumb and Dropdown next to it
- let dropdown_w = 120.0;
- let breadcrumb_w = client_w - dropdown_w - gap;
- let (bx, by, bw, bh) = layout.allocate(breadcrumb_w, breadcrumb_h);
+ // 1. Allocate and render the Breadcrumb, full width: the view switch that
+ // used to sit beside it is in its context menu now.
+ let (bx, by, bw, bh) = layout.allocate(client_w, breadcrumb_h);
cce_ui::layout::render_widget(&mut pc, &mut state.breadcrumb, bx, by, bw, bh, ctx);
// The breadcrumb's well + segment plate live in its modern paint(); the flat
// view this host renders through loses them, so carve here: the full-width
@@ -245,7 +244,6 @@ pub fn view(state: &mut BrowseState, view_dropdown: &mut cce_ui::widget::Adapted
let rect = cce_ui::scene::layout::Rect { x: bx, y: by, width: bw, height: bh };
crate::pages::breadcrumb_relief(&mut pc, &state.breadcrumb, rect);
}
- cce_ui::layout::render_widget(&mut pc, view_dropdown, bx + bw + gap, by, dropdown_w, breadcrumb_h, ctx);
// 2. Allocate and render List (ScrollBox)
// The scrolling list height occupies the remaining vertical space:
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 6e1c842..4500b33 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -26,6 +26,16 @@ impl Page {
Page::Space => "Space",
}
}
+
+ /// The page's row in the breadcrumb's view menu. These name the
+ /// visualization rather than the page, so they are not [`Self::label`].
+ pub fn view_label(self) -> &'static str {
+ match self {
+ Page::Browse => "List",
+ Page::Network => "Graph",
+ Page::Space => "Space",
+ }
+ }
}
/// Mirror the breadcrumb's relief into a flat-path [`PageContent`]: the
@@ -37,8 +47,10 @@ impl Page {
/// in one place, since all three pages want it identically.
///
/// **Audited against `Breadcrumb::paint` and deliberately left page-side** —
-/// it does NOT need [`dropdown_relief`]'s treatment, for two reasons that are
-/// easy to assume away:
+/// it does NOT need the treatment the view dropdown's ring needed (carved into
+/// `window_pc` after the pages had laid it out; the dropdown is gone since
+/// 2026-10-06, its List/Graph/Space rows moved into this breadcrumb's context
+/// menu), for two reasons that are easy to assume away:
///
/// - *The depths already agree.* Each `relief_*`/`groove` helper derives depth
/// from the height it is handed, and all three here are handed what the
@@ -50,8 +62,8 @@ impl Page {
/// laying the breadcrumb out, rather than reading it back off the widget, so
/// there is no previous-frame rect to pick up.
///
-/// Nor does the `window_pc`-vs-`pc` split matter here, though `dropdown_relief`
-/// warns loudly about it. `display_list` emits ALL of a `PageContent`'s rects
+/// Nor does the `window_pc`-vs-`pc` split matter here, though it did for the
+/// dropdown's ring. `display_list` emits ALL of a `PageContent`'s rects
/// before ALL of its reliefs, so the call order within `pc` is irrelevant; only
/// a different PageContent could reorder these. The only quad the breadcrumb
/// puts under the carve is the hover tint, and that is inset to the seam's
@@ -73,9 +85,9 @@ pub fn breadcrumb_relief(
// the dropdown's configured fill: `render_widget` offers that fill for a
// Dropdown through a per-type hook (layout.rs) but has no Breadcrumb arm,
// so a run carved here with no face would keep showing the window plate
- // while the dropdown beside it went opaque. Both controls read
- // `dropdown_background_color` now, so they match under any config —
- // transparent leaves the plate as the face for both.
+ // while the DE's dropdowns went opaque. It reads
+ // `dropdown_background_color` too, so they match under any config —
+ // transparent leaves the plate as the face.
let radius = r.min(rh * 0.5);
let depth = cce_ui::layout::bevel_width().min(rh * 0.2);
let raw = cce_ui::color::dropdown_background_color();
@@ -92,48 +104,6 @@ pub fn breadcrumb_relief(
}
}
-/// Mirror the view dropdown's flush inset plate into a flat-path
-/// [`PageContent`]: the groove ring sunk around the control, and the control's
-/// own edge rolling back up out of it — face level with the window plate, so
-/// the seam is the only thing saying it is a separate part.
-///
-/// `Dropdown::paint` emits this as one `ctx.inset_plate`; `render_widget` keeps
-/// only quads and text, so the carve is app-side — the same story as
-/// [`breadcrumb_relief`], which is the well-and-plate this pairs with.
-///
-/// It reaches the SAME `inset_plate` call the widget makes, via
-/// [`PageContent::relief_inset`] → `WidgetFx::Inset`. It used to hand-roll the
-/// pair `inset_plate` expands to (`relief_recessed` over an expanded rect, then
-/// `relief_raised`) — which got the ring's depth wrong, because
-/// `relief_recessed` derives depth from the height it is HANDED, and that was
-/// the already-expanded one: a 5.76px wall against a 4.8px lip, so the
-/// descending wall over-ran the lip instead of meeting it in the tight V-groove
-/// with no flat floor that `inset_plate` documents.
-///
-/// **Carve it into `window_pc`, AFTER the page's `view()` has run.** Two
-/// constraints pin it there, and they pull in opposite directions:
-///
-/// - *After the pages* — because the pages are what lay the dropdown out. Read
-/// `view_dropdown.rect()` before they run and you get the rect they assigned
-/// on the PREVIOUS frame, so the ring trails the control by a frame through a
-/// resize (and on the very first frame it carves a 0×0 rect).
-/// - *Into `window_pc`, not the page's own `pc`* — because these are overlay
-/// carves, shaded against whatever is already beneath them. Emitting them
-/// page-side puts them after the dropdown's own background quad instead of
-/// before it, which visibly thins the lit top rim. Same rect, different
-/// material. (Verified by pixel-diffing the two orders; `CCE_PLATE_DEBUG=1`
-/// shows both as overlay fallback, so this is compositing order, not
-/// plate grouping.)
-pub fn dropdown_relief(pc: &mut PageContent, rect: cce_ui::scene::layout::Rect) {
- pc.relief_inset(
- rect.x,
- rect.y,
- rect.width,
- rect.height,
- cce_ui::layout::dropdown_corner_radius(),
- );
-}
-
pub const RELIEF_RECESSED: u8 = 0;
pub const RELIEF_RAISED: u8 = 1;
pub const RELIEF_INSET: u8 = 2;
@@ -511,8 +481,8 @@ impl RenderTarget for PageContent {
/// The flat-path bridge for a widget's flush inset plate — the Dropdown's
/// OPEN popover, which is its trigger surface grown over the unified box.
/// Without this override the default degrades it to a plain rounded fill,
- /// so the menu lost the groove ring and lip the closed trigger has (the
- /// `dropdown_relief` carve) the moment it expanded.
+ /// so the menu lost the groove ring and lip the closed trigger has the
+ /// moment it expanded.
///
/// The face and the walls go to different vecs on purpose — `reliefs` are
/// edges-only, drawn over the faces `rects` own — and `display_list` emits
@@ -523,8 +493,7 @@ impl RenderTarget for PageContent {
/// widget computes it from the TRIGGER's height; the box handed here is the
/// trigger plus the revealed menu, several times taller. `relief_inset`
/// would recompute `bevel_width().min(h * 0.2)` off that expanded height
- /// and thicken the ring as the menu grows — the same mistake
- /// [`dropdown_relief`] documents. A ring that swells during the open
+ /// and thicken the ring as the menu grows. A ring that swells during the open
/// animation is exactly the artifact this override exists to avoid.
fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
self.rects.push((color, x, y, w, h, radius, (true, true, true, true)));
diff --git a/src/pages/network.rs b/src/pages/network.rs
index c0a62df..ffc0033 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -201,21 +201,18 @@ impl NetworkState {
}
}
-pub fn view(state: &mut NetworkState, browse: &BrowseState, view_dropdown: &mut cce_ui::widget::Adapted<cce_ui::widget::Dropdown>, cx: f32, cy: f32, cw: f32, ch: f32, ctx: &mut cce_ui::context::UiContext) -> PageContent {
+pub fn view(state: &mut NetworkState, browse: &BrowseState, cx: f32, cy: f32, cw: f32, ch: f32, ctx: &mut cce_ui::context::UiContext) -> PageContent {
let mut pc = PageContent::new();
- // Render the Breadcrumb and Dropdown next to it
- let dropdown_w = 120.0;
+ // Render the Breadcrumb, full width (the view switch is in its context menu).
// TODO(style): the 4/6/16 offsets are a leftover inset from the pane rect
- // that browse.rs has already dropped; the gap to the dropdown is the rung.
- let gap = cce_ui::layout::root_plate_gap();
- let breadcrumb_w = cw - 16.0 - dropdown_w - gap;
+ // that browse.rs has already dropped.
+ let breadcrumb_w = cw - 16.0;
cce_ui::layout::render_widget(&mut pc, &mut state.breadcrumb, cx + 4.0, cy + 6.0, breadcrumb_w, 24.0, ctx);
{
let rect = cce_ui::scene::layout::Rect { x: cx + 4.0, y: cy + 6.0, width: breadcrumb_w, height: 24.0 };
crate::pages::breadcrumb_relief(&mut pc, &state.breadcrumb, rect);
}
- cce_ui::layout::render_widget(&mut pc, view_dropdown, cx + 4.0 + breadcrumb_w + gap, cy + 6.0, dropdown_w, 24.0, ctx);
// The pane the graph is laid out in, under the breadcrumb.
let pane = (cx, cy + 28.0, cw, (ch - 28.0).max(0.0));
diff --git a/src/pages/space.rs b/src/pages/space.rs
index 8978237..1cc8d16 100644
--- a/src/pages/space.rs
+++ b/src/pages/space.rs
@@ -459,7 +459,6 @@ fn place(node: &TreeNode, path: &Path, rect: (f32, f32, f32, f32), depth: u32, o
pub fn view(
state: &mut SpaceState,
browse: &BrowseState,
- view_dropdown: &mut Adapted<cce_ui::widget::Dropdown>,
cx: f32,
cy: f32,
cw: f32,
@@ -468,19 +467,16 @@ pub fn view(
) -> PageContent {
let mut pc = PageContent::new();
- // Breadcrumb + view dropdown, mirroring the Network page's header so the
- // two views line up when you switch between them.
- let dropdown_w = 120.0;
+ // The breadcrumb, mirroring the Network page's header so the two views
+ // line up when you switch between them.
// TODO(style): the 4/6/16 offsets are a leftover inset from the pane rect
- // that browse.rs has already dropped; the gap to the dropdown is the rung.
- let gap = cce_ui::layout::root_plate_gap();
- let breadcrumb_w = cw - 16.0 - dropdown_w - gap;
+ // that browse.rs has already dropped.
+ let breadcrumb_w = cw - 16.0;
cce_ui::layout::render_widget(&mut pc, &mut state.breadcrumb, cx + 4.0, cy + 6.0, breadcrumb_w, 24.0, ctx);
{
let rect = cce_ui::scene::layout::Rect { x: cx + 4.0, y: cy + 6.0, width: breadcrumb_w, height: 24.0 };
crate::pages::breadcrumb_relief(&mut pc, &state.breadcrumb, rect);
}
- cce_ui::layout::render_widget(&mut pc, view_dropdown, cx + 4.0 + breadcrumb_w + gap, cy + 6.0, dropdown_w, 24.0, ctx);
let mut segments = Vec::new();
for component in browse.current_dir.components() {