GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor(widget)!: focus stores keyed by WidgetId (Phase 6bc slice 1)
Both focus stores — UiContext.focused_widget and the core.rs
thread-local FOCUSED_WIDGET — now hold Option<WidgetId> instead of
*mut dyn Element. Dispatch to the previous holder (unfocus/FocusOut)
and KeyInput delivery resolve through the generational tree, so a
stale id is skipped instead of dereferencing freed memory (the 6ao/6w
UAF class). set_focused/set_focused_ptr refresh the registry with the
pointer they are handed, so focusing a not-yet-registered widget
keeps working. is_focused_addr is replaced by is_focused_id (base-id
comparison); thread-local fns that dispatch gained an Option<&mut
UiContext>/ctx param (set_focused, clear_focus, navigate_focus).
Verified: 168 tests; workspace builds; A/B identical click sequences
on text-editor (static/editor-focus/menu AE<=6, empty 8% masks) and
data-editor (search-box focus click AE=0); settings notifications
spinbox click-to-focus live.
src/context.rs | 92 ++++++++++++++++++-------------
src/widget/container/scroll_box.rs | 22 ++++----
src/widget/core.rs | 107 +++++++++++++++++++++----------------
src/widget/mod.rs | 4 +-
src/widget/model.rs | 4 +-
5 files changed, 132 insertions(+), 97 deletions(-)
diff --git a/src/context.rs b/src/context.rs
index 615cec1..415ea84 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -54,7 +54,9 @@ pub struct UiContext {
/// core rebuild). Replaces the former `layout_tree` + `widget_registry` maps; see
/// `scene/tree.rs`.
pub tree: crate::scene::WidgetTree,
- pub focused_widget: Option<*mut (dyn Element + 'static)>,
+ /// The focused widget's id (Phase 6bc: stored ids, not pointers — a stale id resolves to
+ /// `None` through the generational tree instead of dereferencing freed memory).
+ pub focused_widget: Option<WidgetId>,
pub active_popovers: Vec<*const (dyn Element + 'static)>,
pub hover_state: HoverState,
pub cursor_pos: (f32, f32),
@@ -139,7 +141,7 @@ impl UiContext {
};
if is_scroll_key {
let mut handled = false;
- if let Some(focused) = self.focused_widget {
+ if let Some(focused) = self.focused_widget.and_then(|id| self.tree.get_ptr(id)) {
unsafe {
if (*focused).handle_event(event, self) {
(*focused).mark_dirty(self);
@@ -245,7 +247,7 @@ impl UiContext {
// For KeyInput, send directly to focused widget if it exists
if let Event::KeyInput(_) = event {
- if let Some(focused) = self.focused_widget {
+ if let Some(focused) = self.focused_widget.and_then(|id| self.tree.get_ptr(id)) {
if (*focused).handle_event(event, self) {
(*focused).mark_dirty(self);
return true;
@@ -413,64 +415,82 @@ impl UiContext {
changed
}
- // --- Focus management ---
+ // --- Focus management (id-keyed; Phase 6bc) ---
pub fn set_focused(&mut self, w: &mut dyn Element) {
+ let Some(id) = w.base().map(|b| b.id()) else { return };
+ // Refresh the registry with the pointer we were just handed, so focus on a
+ // not-yet-registered widget keeps working (the legacy code stored this pointer
+ // directly; the id must resolve for FocusOut/KeyInput dispatch to reach it).
let new_ptr = unsafe {
std::mem::transmute::<*mut dyn Element, *mut (dyn Element + 'static)>(w as *mut dyn Element)
};
- self.set_focused_ptr(new_ptr);
+ self.tree.register(id, new_ptr);
+ self.set_focused_id(id);
}
+ /// Transitional pointer form (TreeList focuses its adapter via `EventCtx::host_ptr`). The
+ /// pointer must be live at the call — it is only used to derive the id and refresh the
+ /// registry, never stored.
pub fn set_focused_ptr(&mut self, new_ptr: *mut (dyn Element + 'static)) {
- if let Some(old_ptr) = self.focused_widget {
- let old_data = old_ptr as *mut () as usize;
- let new_data = new_ptr as *mut () as usize;
- if old_data != new_data {
- unsafe {
- (*old_ptr).unfocus();
- (*old_ptr).handle_event(&Event::FocusOut, self);
+ if new_ptr.is_null() {
+ return;
+ }
+ let Some(id) = (unsafe { (*new_ptr).base().map(|b| b.id()) }) else { return };
+ self.tree.register(id, new_ptr);
+ self.set_focused_id(id);
+ }
+
+ pub fn set_focused_id(&mut self, id: WidgetId) {
+ if let Some(old_id) = self.focused_widget {
+ if old_id != id {
+ if let Some(old_ptr) = self.tree.get_ptr(old_id) {
+ unsafe {
+ (*old_ptr).unfocus();
+ (*old_ptr).handle_event(&Event::FocusOut, self);
+ }
}
- self.focused_widget = Some(new_ptr);
- unsafe {
- (*new_ptr).handle_event(&Event::FocusIn, self);
+ self.focused_widget = Some(id);
+ if let Some(new_ptr) = self.tree.get_ptr(id) {
+ unsafe {
+ (*new_ptr).handle_event(&Event::FocusIn, self);
+ }
}
}
} else {
- self.focused_widget = Some(new_ptr);
- unsafe {
- (*new_ptr).handle_event(&Event::FocusIn, self);
+ self.focused_widget = Some(id);
+ if let Some(new_ptr) = self.tree.get_ptr(id) {
+ unsafe {
+ (*new_ptr).handle_event(&Event::FocusIn, self);
+ }
}
}
}
pub fn is_focused(&self, w: &dyn Element) -> bool {
- let addr = w as *const dyn Element as *const () as usize;
- self.is_focused_addr(addr)
+ match w.base() {
+ Some(b) => self.is_focused_id(b.id()),
+ None => false,
+ }
}
- pub fn is_focused_addr(&self, addr: usize) -> bool {
- if let Some(ptr) = self.focused_widget {
- let current_data = ptr as *const () as usize;
- current_data == addr
- } else {
- false
- }
+ pub fn is_focused_id(&self, id: WidgetId) -> bool {
+ self.focused_widget == Some(id)
}
pub fn clear_focus(&mut self) {
- if let Some(ptr) = self.focused_widget.take() {
- unsafe {
- (*ptr).unfocus();
- (*ptr).handle_event(&Event::FocusOut, self);
+ if let Some(id) = self.focused_widget.take() {
+ if let Some(ptr) = self.tree.get_ptr(id) {
+ unsafe {
+ (*ptr).unfocus();
+ (*ptr).handle_event(&Event::FocusOut, self);
+ }
}
}
}
pub fn clear_if_matches(&mut self, w: &dyn Element) {
- let query_data = w as *const dyn Element as *const () as usize;
- if let Some(ptr) = self.focused_widget {
- let current_data = ptr as *const () as usize;
- if current_data == query_data {
+ if let Some(b) = w.base() {
+ if self.focused_widget == Some(b.id()) {
self.focused_widget = None;
}
}
@@ -481,7 +501,7 @@ impl UiContext {
}
pub fn navigate_focus(&mut self, key: &Key, ctrl: bool) -> bool {
- let ptr = match self.focused_widget {
+ let ptr = match self.focused_widget.and_then(|id| self.tree.get_ptr(id)) {
Some(p) => p,
None => return false,
};
diff --git a/src/widget/container/scroll_box.rs b/src/widget/container/scroll_box.rs
index 9379d95..6784e3e 100644
--- a/src/widget/container/scroll_box.rs
+++ b/src/widget/container/scroll_box.rs
@@ -101,15 +101,15 @@ impl ScrollBox {
/// unfocusing the previously focused widget (nothing ever queried focus ON the scroll
/// box through the thread-local, and its own `unfocus` was a no-op) — so just release
/// the current holder instead of storing a pointer to a non-Element.
- fn claim_focus(&self) {
- focus::clear_focus();
+ fn claim_focus(&self, ctx: &mut UiContext) {
+ focus::clear_focus(Some(ctx));
}
pub fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
if button == MouseButton::Left {
if state == ElementState::Pressed {
if self.hit_test_scrollbar(px, py) {
- self.claim_focus();
+ self.claim_focus(ctx);
self.scrollbar_dragging = true;
let sb_track_h = self.viewport_h - 8.0;
@@ -143,7 +143,7 @@ impl ScrollBox {
self.scrollbar_dragging = false;
}
if self.hit_test(px, py, ctx) {
- self.claim_focus();
+ self.claim_focus(ctx);
}
} else if state == ElementState::Released {
self.scrollbar_dragging = false;
@@ -303,17 +303,19 @@ impl ScrollBox {
}
pub fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
- let self_addr = self as *const Self as *const () as usize;
- let has_focus = ctx.is_focused_addr(self_addr) || {
+ // Focus never lands on the box itself (post-6av it is not an `Element`), and its id is
+ // never a tree ancestor of the focused widget — like the legacy address walk, this
+ // gate only ever passes via the hover check below.
+ let self_id = self.base.id();
+ let has_focus = ctx.is_focused_id(self_id) || {
let mut current = ctx.focused_widget;
let mut found = false;
- while let Some(ptr) = current {
- let ptr_addr = ptr as *const () as usize;
- if ptr_addr == self_addr {
+ while let Some(id) = current {
+ if id == self_id {
found = true;
break;
}
- current = unsafe { (*ptr).parent(ctx) };
+ current = ctx.tree.parent_id(id);
}
found
};
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 9444f0a..fd33eb8 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -1,63 +1,72 @@
use crate::widget::{Element, Key};
pub mod focus {
- use super:: Element;
+ use super::Element;
+ use crate::widget::WidgetId;
use std::cell::Cell;
+ // Phase 6bc: the thread-local focus store keys by id, not pointer. Dispatching to the
+ // previous holder (`unfocus`) resolves through the caller's generational tree, so a
+ // stale id is skipped instead of dereferencing freed memory (the 6w settings UAF class).
thread_local! {
- static FOCUSED_WIDGET: Cell<Option<*mut (dyn Element + 'static)>> = Cell::new(None);
+ static FOCUSED_WIDGET: Cell<Option<WidgetId>> = Cell::new(None);
}
- pub fn set_focused(w: &mut dyn Element) {
- FOCUSED_WIDGET.with(|cell| {
- let new_ptr = unsafe {
- std::mem::transmute::<*mut dyn Element, *mut (dyn Element + 'static)>(w as *mut dyn Element)
- };
- if let Some(old_ptr) = cell.get() {
- let old_data = old_ptr as *mut () as usize;
- let new_data = new_ptr as *mut () as usize;
- if old_data != new_data {
- unsafe {
- (*old_ptr).unfocus();
- }
- cell.set(Some(new_ptr));
+ /// Resolve `id` in `ctx`'s tree (when a ctx is in reach) and call `unfocus()` on it.
+ fn unfocus_via(ctx: Option<&mut crate::context::UiContext>, id: WidgetId) {
+ if let Some(ctx) = ctx {
+ if let Some(ptr) = ctx.tree.get_ptr(id) {
+ unsafe {
+ (*ptr).unfocus();
}
- } else {
- cell.set(Some(new_ptr));
}
- });
+ }
}
- pub fn is_focused(w: &dyn Element) -> bool {
- FOCUSED_WIDGET.with(|cell| {
- if let Some(ptr) = cell.get() {
- let current_data = ptr as *const () as usize;
- let query_data = w as *const dyn Element as *const () as usize;
- current_data == query_data
- } else {
- false
- }
- })
+ pub fn set_focused(w: &mut dyn Element, ctx: Option<&mut crate::context::UiContext>) {
+ let Some(id) = w.base().map(|b| b.id()) else { return };
+ set_focused_id(id, ctx);
}
- pub fn clear_focus() {
- FOCUSED_WIDGET.with(|cell| {
- if let Some(ptr) = cell.take() {
- unsafe {
- (*ptr).unfocus();
- }
+ pub fn set_focused_id(id: WidgetId, ctx: Option<&mut crate::context::UiContext>) {
+ let old = FOCUSED_WIDGET.with(|cell| cell.get());
+ if let Some(old_id) = old {
+ if old_id != id {
+ unfocus_via(ctx, old_id);
+ FOCUSED_WIDGET.with(|cell| cell.set(Some(id)));
}
- });
+ } else {
+ FOCUSED_WIDGET.with(|cell| cell.set(Some(id)));
+ }
+ }
+
+ pub fn is_focused(w: &dyn Element) -> bool {
+ match w.base() {
+ Some(b) => is_focused_id(b.id()),
+ None => false,
+ }
+ }
+
+ pub fn is_focused_id(id: WidgetId) -> bool {
+ FOCUSED_WIDGET.with(|cell| cell.get() == Some(id))
+ }
+
+ pub fn clear_focus(ctx: Option<&mut crate::context::UiContext>) {
+ if let Some(id) = FOCUSED_WIDGET.with(|cell| cell.take()) {
+ unfocus_via(ctx, id);
+ }
}
pub fn clear_if_matches(w: &dyn Element) {
+ if let Some(b) = w.base() {
+ clear_if_matches_id(b.id());
+ }
+ }
+
+ pub fn clear_if_matches_id(id: WidgetId) {
FOCUSED_WIDGET.with(|cell| {
- if let Some(ptr) = cell.get() {
- let current_data = ptr as *const () as usize;
- let query_data = w as *const dyn Element as *const () as usize;
- if current_data == query_data {
- cell.set(None);
- }
+ if cell.get() == Some(id) {
+ cell.set(None);
}
});
}
@@ -81,9 +90,13 @@ pub mod focus {
child.set_parent(Some(parent_ptr), ctx);
}
- pub fn navigate_focus(key: &super::Key, ctrl: bool) -> bool {
+ /// Keyboard tree navigation from the focused widget. `ctx` resolves the focused id to a
+ /// live widget; the parent/children walk itself deliberately keeps the legacy dummy-ctx
+ /// semantics (only `container_children`-style overrides that ignore the ctx ever yielded
+ /// anything here).
+ pub fn navigate_focus(key: &super::Key, ctrl: bool, ctx: &mut crate::context::UiContext) -> bool {
FOCUSED_WIDGET.with(|cell| {
- let ptr = match cell.get() {
+ let ptr = match cell.get().and_then(|id| ctx.tree.get_ptr(id)) {
Some(p) => p,
None => return false,
};
@@ -94,7 +107,7 @@ pub mod focus {
let dummy = crate::context::UiContext::new();
if let Some(parent_ptr) = (*ptr).parent(&dummy) {
let parent_ref = &mut *parent_ptr;
- set_focused(parent_ref);
+ set_focused(parent_ref, Some(&mut *ctx));
parent_ref.focus();
return true;
}
@@ -104,7 +117,7 @@ pub mod focus {
let mut children = (*ptr).children(&dummy);
if !children.is_empty() {
let child_ref = &mut *children[0];
- set_focused(child_ref);
+ set_focused(child_ref, Some(&mut *ctx));
child_ref.focus();
return true;
}
@@ -121,7 +134,7 @@ pub mod focus {
if let Some(idx) = current_idx {
let next_idx = (idx + 1) % siblings.len();
let sibling_ref = &mut *siblings[next_idx];
- set_focused(sibling_ref);
+ set_focused(sibling_ref, Some(&mut *ctx));
sibling_ref.focus();
return true;
}
@@ -139,7 +152,7 @@ pub mod focus {
if let Some(idx) = current_idx {
let prev_idx = if idx == 0 { siblings.len() - 1 } else { idx - 1 };
let sibling_ref = &mut *siblings[prev_idx];
- set_focused(sibling_ref);
+ set_focused(sibling_ref, Some(&mut *ctx));
sibling_ref.focus();
return true;
}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 37ace6d..8ffde22 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -406,7 +406,7 @@ pub trait Element {
}
fn highlight_color(&self, ctx: &UiContext) -> Option<[f32; 4]> {
- let is_focused = ctx.is_focused_addr(self as *const Self as *const () as usize);
+ let is_focused = self.base().map(|b| ctx.is_focused_id(b.id())).unwrap_or(false);
if is_focused {
Some(colors::highlight_primary_color())
} else if self.hovered() {
@@ -587,7 +587,7 @@ pub trait Element {
}
}
fn focused(&self, ctx: &UiContext) -> bool {
- ctx.is_focused_addr(self as *const Self as *const () as usize)
+ self.base().map(|b| ctx.is_focused_id(b.id())).unwrap_or(false)
}
fn prepare_text(&mut self, _fs: &mut glyphon::FontSystem) {}
fn set_selected(&mut self, _selected: bool) {}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index e3971a8..474fe76 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -383,7 +383,7 @@ impl EventCtx<'_> {
/// Make this widget the global focus target (legacy `focus::set_focused(self)`).
pub fn request_focus(&mut self) {
if let Some(ptr) = self.self_ptr {
- unsafe { crate::widget::focus::set_focused(&mut *ptr) };
+ unsafe { crate::widget::focus::set_focused(&mut *ptr, self.ui.as_deref_mut()) };
}
}
@@ -1117,7 +1117,7 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
if !Paint::legacy_focus_highlight(&self.inner) {
return None;
}
- let is_focused = ctx.is_focused_addr(self as *const Self as *const () as usize);
+ let is_focused = ctx.is_focused_id(self.base.id());
let hc = if is_focused {
crate::colors::highlight_primary_color()
} else if self.base.hovered {