GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(focus): focus_stepped tells a caching app to rebuild; CCE_FOCUS_DEBUG
An app that caches its geometry until its own rebuild flag (relief carves
collected in a view pass, widget lists built on layout) never drew the ring
a Tab step moved: the runner only redrew. `Application::focus_stepped`
(default nothing) fires after a step so such an app raises its flag.
`CCE_FOCUS_DEBUG=1` prints the walk's stops with their roles and rects.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 7 ++++++-
src/backend/window_runner.rs | 18 ++++++++++++------
src/context.rs | 18 ++++++++++++++++++
3 files changed, 36 insertions(+), 7 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 774d060..b77745b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -141,7 +141,12 @@ What this buys, and where the code is heading:
(not a stop). `UiContext::focus_step` walks the stops in reading order (row,
then x), wrapping; the runner calls it for Tab / Shift+Tab when the app opts
in with `Application::plate_navigation` (default off, so an app that routes
- Tab itself — a terminal, a web view, its own field order — is undisturbed).
+ Tab itself — a terminal, a web view, its own field order — is undisturbed)
+ and tells the app through `Application::focus_stepped` — an app that caches
+ its geometry until its own rebuild flag raises it there. The walk needs the
+ app's context exposed (`ui_context_mut`); the ring reaches flat-path hosts
+ through `RenderTarget::inset_plate_tinted` and `CarveKind::Boss { tint }`.
+ `CCE_FOCUS_DEBUG=1` prints the stops in walk order.
The focus ring is the plate's own silhouette: `ControlPlate::with_tint`
lights the rim (a tinted `Trough`, `Boss` or `Bevel`), the same treatment a
well's `recess_tinted` gives its rim while editing — never extra geometry.
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 335d893..146336f 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -3302,6 +3302,13 @@ pub trait Application: Sized + 'static {
fn plate_navigation(&self) -> bool {
false
}
+
+ /// Keyboard focus just moved by the toolkit's Tab traversal. An app that
+ /// caches its geometry until its own rebuild flag (relief carves collected
+ /// in a view pass, widget lists built on layout) raises that flag here, so
+ /// the new ring is drawn; an app that paints fresh every frame needs
+ /// nothing. Default: nothing.
+ fn focus_stepped(&mut self) {}
/// Keyboard focus entered/left the window (the compositor keyboard-focuses
/// the focused window, so this is the "am I the focused window" signal —
/// e.g. for focus-dependent chrome). Default: ignore.
@@ -4855,13 +4862,12 @@ impl<A: Application> EngineState<A> {
if !app.plate_navigation() {
return false;
}
- if let Some(ctx) = app.ui_context_mut() {
- if ctx.focus_step(reverse) {
- *rebuild = true;
- return true;
- }
+ let moved = app.ui_context_mut().is_some_and(|ctx| ctx.focus_step(reverse));
+ if moved {
+ app.focus_stepped();
+ *rebuild = true;
}
- false
+ moved
}
fn route_history_chord(&mut self, event: &KeyEvent, rebuild: &mut bool) -> bool {
diff --git a/src/context.rs b/src/context.rs
index 57ffefe..b48e4a2 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -594,6 +594,9 @@ impl UiContext {
found.push((y, y + height, x, id));
}
if found.is_empty() {
+ if std::env::var_os("CCE_FOCUS_DEBUG").is_some() {
+ eprintln!("[focus] no stops: no registered, visible widget with a focus role and a rect");
+ }
return false;
}
// Reading order: rows first, x within a row. A stop joins the current
@@ -613,6 +616,21 @@ impl UiContext {
row.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
stops.extend(row.into_iter().map(|s| s.3));
}
+ // CCE_FOCUS_DEBUG=1: the stops in walk order, with what each is.
+ if std::env::var_os("CCE_FOCUS_DEBUG").is_some() {
+ for (i, id) in stops.iter().enumerate() {
+ if let Some(ptr) = self.tree.get_ptr(*id) {
+ let w = unsafe { &*ptr };
+ let (x, y, width, height) = w.rect();
+ eprintln!(
+ "[focus] stop {i}: {} {:?} at ({x:.0},{y:.0} {width:.0}x{height:.0}){}",
+ w.type_name(),
+ w.focus_role(),
+ if self.focused_widget == Some(*id) { " <- focused" } else { "" }
+ );
+ }
+ }
+ }
let n = stops.len();
let current = self.focused_widget.and_then(|f| stops.iter().position(|s| *s == f));
let next = match (current, reverse) {