git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commit78935a381ef23b449e4e031f4641b2b830f100d2
parent5842fcfb37
authorLucas Galante <[email protected]>
date2026-08-30 22:12
feat: well focus — the clicked well's ring replaces its relief lighting

The recessed wells (list, treemap, the preview pane's two sections) gain
pointer focus: the well the last left click landed in renders as the
tinted carve, so the accent ring REPLACES the relief lighting on its rim
-- the DE's one focus language (the shader drops the diffuse/curvature
terms for tinted carves, matching the focused-plate glint). Unfocused
wells keep the plain recess; the content well starts focused.

Tracking is keyed on mouse RELEASE, not press: the routed-widget path
consumes left presses before the Application hook (the plate-dock
control's rule), which a press-keyed first cut confirmed the hard way --
the hook simply never saw them. The hit-test runs after every overlay
(dock menu, context menu, dialog, dropdown, divider grab) has returned,
so only clicks reaching page content move the ring; clicks outside any
well change nothing. PreviewPane::well_at mirrors the paint-time well
geometry and reports nothing while no file is shown (no wells drawn).

Plumbing: RELIEF_RECESSED_FOCUS relief kind -> WidgetFx::RecessFocus ->
PaintCtx::recess_tinted with the highlight accent.

Verified in a shadow session: the ring moves list -> details -> preview
-> list on clicks, one well ringed at a time.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/main.rs         | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/pages/mod.rs    | 13 ++++++++++
 src/pages/space.rs  | 10 +++++++-
 src/preview_pane.rs | 48 ++++++++++++++++++++++++++++++++++--
 src/row_list.rs     | 13 ++++++++--
 5 files changed, 150 insertions(+), 5 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index b125367..5d7e5cd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -182,6 +182,11 @@ enum WidgetFx {
     Plate(f32),
     /// Edges-only carve into whatever is painted below (recessed wells).
     Recess(f32),
+    /// [`WidgetFx::Recess`] with pointer focus: the tinted carve — the wrapped
+    /// accent glint REPLACING the relief lighting (the DE's one focus
+    /// language; the shader drops the diffuse/curvature terms for tinted
+    /// carves, so the ring is all that shows).
+    RecessFocus(f32),
     /// A flush inset control (buttons): groove ring carved down around the
     /// rect, beveled lip back up inside, face level with the surface.
     Inset(f32),
@@ -370,12 +375,25 @@ impl BrowseKeys {
     }
 }
 
+/// Which recessed well holds pointer focus — the one wearing the accent ring
+/// (its relief lighting swapped for the tinted carve's wrapped glint, the
+/// same focus language as plates). Follows the last left press that reaches
+/// page content; the content well (list / treemap) starts focused.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum FocusedWell {
+    Content,
+    PreviewTop,
+    PreviewBottom,
+}
+
 struct FilesystemApp {
     current_page: Page,
     browse: pages::browse::BrowseState,
     network: pages::network::NetworkState,
     space: pages::space::SpaceState,
     preview: cce_files::preview_pane::PreviewPane,
+    /// See [`FocusedWell`]; drives the per-well `focused` flags each rebuild.
+    focused_well: FocusedWell,
 
     // Command-line chooser options
     select_mode: bool,
@@ -530,6 +548,15 @@ impl FilesystemApp {
 
     fn rebuild_layout(&mut self) {
 
+        // Well focus → the emitters (each picks the tinted carve when set).
+        self.browse.list.focused = self.focused_well == FocusedWell::Content;
+        self.space.focused = self.focused_well == FocusedWell::Content;
+        self.preview.focused_well = match self.focused_well {
+            FocusedWell::PreviewTop => Some(cce_files::preview_pane::PreviewWell::Top),
+            FocusedWell::PreviewBottom => Some(cce_files::preview_pane::PreviewWell::Bottom),
+            FocusedWell::Content => None,
+        };
+
         self.ui_context.clear_hierarchy();
         self.browse.save_name_box.prepare_text(&mut self.font_system);
         self.browse.search_box.prepare_text(&mut self.font_system);
@@ -936,6 +963,7 @@ impl FilesystemApp {
                     fx: match *kind {
                         pages::RELIEF_RAISED => WidgetFx::Boss(*rd),
                         pages::RELIEF_INSET => WidgetFx::Inset(*rd),
+                        pages::RELIEF_RECESSED_FOCUS => WidgetFx::RecessFocus(*rd),
                         _ => WidgetFx::Recess(*rd),
                     },
                 });
@@ -1212,6 +1240,7 @@ impl Application for FilesystemApp {
             network: pages::network::NetworkState::default(),
             space: pages::space::SpaceState::default(),
             preview: Default::default(),
+            focused_well: FocusedWell::Content,
             preview_dock: Default::default(),
             preview_prior_fracs: None,
             plate_menu_actions: Vec::new(),
@@ -1568,6 +1597,10 @@ impl Application for FilesystemApp {
                 WidgetFx::Plate(depth) => pc.plate(rect, radii, w.color, depth),
                 WidgetFx::Boss(depth) => pc.boss(rect, radii, depth),
                 WidgetFx::Recess(depth) => pc.recess(rect, radii, depth),
+                WidgetFx::RecessFocus(depth) => {
+                    let hc = cce_ui::color::highlight_primary_color();
+                    pc.recess_tinted(rect, radii, depth, [hc[0], hc[1], hc[2]]);
+                }
                 WidgetFx::Inset(depth) => pc.inset_plate(rect, radii, w.color, depth),
                 WidgetFx::Image { id, alpha } => pc.image(id, rect, alpha),
                 WidgetFx::Groove { ax, ay, bx, by, width, depth } => {
@@ -2125,6 +2158,44 @@ impl Application for FilesystemApp {
             }
         }
 
+        // Well focus follows the left click, keyed on RELEASE like the
+        // 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
+        // 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.
+        if button == MouseButton::Left && state == ElementState::Released {
+            let new_focus = if !self.preview_dock.collapsed
+                && let Some(well) = self.preview.well_at(pos.x, pos.y)
+            {
+                Some(match well {
+                    cce_files::preview_pane::PreviewWell::Top => FocusedWell::PreviewTop,
+                    cce_files::preview_pane::PreviewWell::Bottom => FocusedWell::PreviewBottom,
+                })
+            } else {
+                let split = match self.current_page {
+                    Page::Browse => &self.browse_split,
+                    Page::Network => &self.network_split,
+                    Page::Space => &self.space_split,
+                };
+                let (lx, ly, lw, lh) = split.left_rect();
+                if pos.x >= lx && pos.x <= lx + lw && pos.y >= ly && pos.y <= ly + lh {
+                    Some(FocusedWell::Content)
+                } else {
+                    None
+                }
+            };
+            if let Some(f) = new_focus
+                && f != self.focused_well
+            {
+                self.focused_well = f;
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+        }
+
         if self.current_page == Page::Browse {
             if self.select_mode {
                 if { let root = self.browse.save_name_box.id(); self.ui_context.propagate_event(&ev, root) } {
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 26f8a6a..a6894d8 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -145,6 +145,10 @@ pub fn dropdown_relief(pc: &mut PageContent, rect: cce_ui::scene::layout::Rect)
 pub const RELIEF_RECESSED: u8 = 0;
 pub const RELIEF_RAISED: u8 = 1;
 pub const RELIEF_INSET: u8 = 2;
+/// A recessed well with pointer focus: renders as the tinted carve — the
+/// wrapped accent glint REPLACING the relief lighting (the DE's one focus
+/// language, same treatment as a focused plate's ring).
+pub const RELIEF_RECESSED_FOCUS: u8 = 3;
 
 pub struct PageContent {
     pub rects: Vec<([f32; 4], f32, f32, f32, f32, f32, (bool, bool, bool, bool))>,
@@ -270,6 +274,15 @@ impl PageContent {
         }
     }
 
+    /// [`Self::relief_recessed`] for the well holding pointer focus: the ring
+    /// replaces the lighting (see [`RELIEF_RECESSED_FOCUS`]).
+    pub fn relief_recessed_focused(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
+        if cce_ui::layout::control_relief() {
+            let depth = Self::carve_depth(h, radius);
+            self.reliefs.push((x, y, w, h, radius, depth, RELIEF_RECESSED_FOCUS));
+        }
+    }
+
     /// A raised plateau over the control at (x, y, w, h) — no-op when the DE's
     /// control_relief styling is off.
     pub fn relief_raised(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32) {
diff --git a/src/pages/space.rs b/src/pages/space.rs
index 67efad7..c5fa8fb 100644
--- a/src/pages/space.rs
+++ b/src/pages/space.rs
@@ -313,6 +313,9 @@ pub struct SpaceState {
     /// Raised to abandon the in-flight scan when a newer one supersedes it.
     pub cancel: Arc<AtomicBool>,
     pub error: Option<String>,
+    /// Pointer focus (the app's well-focus tracking): the map well renders as
+    /// the tinted carve — accent ring replacing the relief lighting.
+    pub focused: bool,
 }
 
 impl Default for SpaceState {
@@ -333,6 +336,7 @@ impl Default for SpaceState {
             scan_bytes: 0,
             cancel: Arc::new(AtomicBool::new(false)),
             error: None,
+            focused: false,
         }
     }
 }
@@ -505,7 +509,11 @@ pub fn view(
         use cce_ui::layout::RenderTarget;
         pc.rect_with_radius_corners(bg, map.0, map.1, map.2, map.3, radius, (true, true, true, true));
     }
-    pc.relief_recessed(map.0, map.1, map.2, map.3, radius);
+    if state.focused {
+        pc.relief_recessed_focused(map.0, map.1, map.2, map.3, radius);
+    } else {
+        pc.relief_recessed(map.0, map.1, map.2, map.3, radius);
+    }
 
     let text_dim = cce_ui::color::TEXT_DIM;
     let text_fg = cce_ui::color::TEXT_FG;
diff --git a/src/preview_pane.rs b/src/preview_pane.rs
index ebd0e36..e61fd65 100644
--- a/src/preview_pane.rs
+++ b/src/preview_pane.rs
@@ -66,6 +66,18 @@ pub struct PreviewPane {
     /// site, so a stale id can never leak against the renderer's image budget.
     image_tex: Option<(u32, u32, u32)>,
     pub scroll_line: usize,
+    /// Which of the pane's two wells holds pointer focus (the app's well-focus
+    /// tracking): that well renders as the tinted carve — accent ring
+    /// replacing the relief lighting.
+    pub focused_well: Option<PreviewWell>,
+}
+
+/// The preview pane's two recessed wells: the file-preview section (top half)
+/// and the details section below it.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PreviewWell {
+    Top,
+    Bottom,
 }
 
 impl Default for PreviewPane {
@@ -84,6 +96,7 @@ impl Default for PreviewPane {
             content_preview: None,
             image_tex: None,
             scroll_line: 0,
+            focused_well: None,
         }
     }
 }
@@ -106,6 +119,29 @@ impl PreviewPane {
         self.rect = (x, y, w, h);
     }
 
+    /// Which well the point lands in, mirroring the paint-time geometry below
+    /// (top well = upper half of the pane rect; details well from half + the
+    /// plate gap down). `None` when no file is shown — the wells aren't drawn,
+    /// so there is nothing to focus.
+    pub fn well_at(&self, px: f32, py: f32) -> Option<PreviewWell> {
+        if self.path.is_none() {
+            return None;
+        }
+        let (cx, cy, cw, ch) = self.rect;
+        if cw <= 0.0 || ch <= 0.0 || px < cx || px > cx + cw {
+            return None;
+        }
+        let half_h = ch * 0.5;
+        let details_top = cy + half_h + cce_ui::layout::root_plate_gap();
+        if py >= cy && py <= cy + half_h {
+            Some(PreviewWell::Top)
+        } else if py >= details_top && py <= cy + ch {
+            Some(PreviewWell::Bottom)
+        } else {
+            None
+        }
+    }
+
     pub fn rect(&self) -> (f32, f32, f32, f32) {
         self.rect
     }
@@ -228,7 +264,11 @@ impl PreviewPane {
             }
         }
         if relief {
-            pc.relief_recessed(cx, cy, cw, half_h, radius);
+            if self.focused_well == Some(PreviewWell::Top) {
+                pc.relief_recessed_focused(cx, cy, cw, half_h, radius);
+            } else {
+                pc.relief_recessed(cx, cy, cw, half_h, radius);
+            }
         }
         let rect_y = cy + 12.0;
         let rect_h = half_h - pad - 24.0;
@@ -298,7 +338,11 @@ impl PreviewPane {
             }
         }
         if relief {
-            pc.relief_recessed(cx, details_top, cw, (cy + ch) - details_top, radius);
+            if self.focused_well == Some(PreviewWell::Bottom) {
+                pc.relief_recessed_focused(cx, details_top, cw, (cy + ch) - details_top, radius);
+            } else {
+                pc.relief_recessed(cx, details_top, cw, (cy + ch) - details_top, radius);
+            }
         }
 
         let header_y = details_content_start_y + 6.0;
diff --git a/src/row_list.rs b/src/row_list.rs
index cc93068..cbf9b9b 100644
--- a/src/row_list.rs
+++ b/src/row_list.rs
@@ -66,6 +66,9 @@ pub struct RowList {
     last_click_time: Option<std::time::Instant>,
     pub dragging: bool,
     drag_offset_y: f32,
+    /// Pointer focus (the app's well-focus tracking): the well renders as the
+    /// tinted carve — accent ring replacing the relief lighting.
+    pub focused: bool,
 }
 
 impl RowList {
@@ -90,6 +93,7 @@ impl RowList {
             last_click_time: None,
             dragging: false,
             drag_offset_y: 0.0,
+            focused: false,
         }
     }
 
@@ -354,8 +358,13 @@ impl RowList {
             (true, true, true, true),
         );
         // Recessed well like a text box: the list floor sits below the pane
-        // surface, its wall carved over the bg and row overlays.
-        pc.relief_recessed(x, y, w, h, cce_ui::layout::list_corner_radius());
+        // surface, its wall carved over the bg and row overlays. Focused, the
+        // well swaps that lighting for the accent ring (the tinted carve).
+        if self.focused {
+            pc.relief_recessed_focused(x, y, w, h, cce_ui::layout::list_corner_radius());
+        } else {
+            pc.relief_recessed(x, y, w, h, cce_ui::layout::list_corner_radius());
+        }
 
         if self.content_h > self.viewport_h {
             let (sb_x, track_y, sb_w, track_h, thumb_y, thumb_h) = self.scrollbar_geom();