file manager
git clone https://git.lucas.co/cce-files.git
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();