GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor(widget)!: base() is guaranteed — the flip part 1 (6bd)
`Element::base`/`base_mut` return `&Widget`/`&mut Widget` directly: the
Option escape hatch is gone, and with it the WidgetId(0) no-base sentinel
class (hit_test/cursor_moved/focus/coverage queries all carry real ids now).
`as_any`/`as_any_mut`/`as_ptr`/`as_ptr_mut` became required — their defaults
manufactured DummyAny/null-DummyElement stand-ins nothing could use;
`impl_widget_base!` provides all six for test shims (layout/arena/tree
mocks grew a base field).
Every Option-handling call site rewrote 1:1 (map/map_or/and_then/unwrap/
if-let over a base that always exists). Adapted was already the only
production implementor.
Verified: 163 tests; workspace builds; settings audio render stream
byte-identical; files + data-editor A/B AE=0; live settings page-dropdown
popover open -> Fonts page switch.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk
src/context.rs | 41 ++++------
src/layout.rs | 39 +++++----
src/main.rs | 2 +-
src/scene/arena.rs | 10 ++-
src/scene/painter.rs | 3 +-
src/scene/tree.rs | 9 ++-
src/widget/container/treelist.rs | 14 ++--
src/widget/core.rs | 32 +++-----
src/widget/display/serialize.rs | 6 +-
src/widget/input/ramp.rs | 16 ++--
src/widget/layout_helper.rs | 4 +-
src/widget/mod.rs | 170 +++++++++++++--------------------------
src/widget/model.rs | 53 +++++-------
13 files changed, 160 insertions(+), 239 deletions(-)
diff --git a/src/context.rs b/src/context.rs
index 9346eda..f05994f 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -308,9 +308,7 @@ impl UiContext {
}
if self.propagate_event_impl(&local_adjusted, child) {
if check_drag_target {
- if let Some(b) = (*child).base() {
- self.drag_target = Some(b.id());
- }
+ self.drag_target = Some((*child).base().id());
}
return true;
}
@@ -318,9 +316,7 @@ impl UiContext {
if (*root).handle_event(event, self) {
(*root).mark_dirty(self);
if check_drag_target {
- if let Some(b) = (*root).base() {
- self.drag_target = Some(b.id());
- }
+ self.drag_target = Some((*root).base().id());
}
return true;
}
@@ -340,9 +336,7 @@ impl UiContext {
self.tree.iter_registered().map(|(_, ptr)| ptr).collect();
for ptr in ptrs {
unsafe {
- if let Some(b) = (*ptr).base_mut() {
- b.dirty = false;
- }
+ (*ptr).base_mut().dirty = false;
}
}
self.rebuild_spatial_grid();
@@ -418,7 +412,7 @@ impl UiContext {
// --- 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 };
+ let id = w.base().id();
// 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).
@@ -436,7 +430,7 @@ impl UiContext {
if new_ptr.is_null() {
return;
}
- let Some(id) = (unsafe { (*new_ptr).base().map(|b| b.id()) }) else { return };
+ let id = unsafe { (*new_ptr).base().id() };
self.tree.register(id, new_ptr);
self.set_focused_id(id);
}
@@ -468,10 +462,7 @@ impl UiContext {
}
pub fn is_focused(&self, w: &dyn Element) -> bool {
- match w.base() {
- Some(b) => self.is_focused_id(b.id()),
- None => false,
- }
+ self.is_focused_id(w.base().id())
}
pub fn is_focused_id(&self, id: WidgetId) -> bool {
@@ -490,10 +481,8 @@ impl UiContext {
}
pub fn clear_if_matches(&mut self, w: &dyn Element) {
- if let Some(b) = w.base() {
- if self.focused_widget == Some(b.id()) {
- self.focused_widget = None;
- }
+ if self.focused_widget == Some(w.base().id()) {
+ self.focused_widget = None;
}
}
@@ -600,7 +589,7 @@ impl UiContext {
/// Register an open popover. Takes `&mut` so the registry can be refreshed with the
/// pointer we are handed (the occlusion walks resolve the stored id through the tree).
pub fn register_popover(&mut self, w: &mut (dyn Element + 'static)) {
- let Some(id) = w.base().map(|b| b.id()) else { return };
+ let id = w.base().id();
self.tree.register(id, w as *mut (dyn Element + 'static));
if !self.active_popovers.contains(&id) {
self.active_popovers.push(id);
@@ -611,7 +600,7 @@ impl UiContext {
if ptr.is_null() {
return;
}
- let Some(id) = (unsafe { (*ptr).base().map(|b| b.id()) }) else { return };
+ let id = unsafe { (*ptr).base().id() };
self.tree.register(id, ptr);
if !self.active_popovers.contains(&id) {
self.active_popovers.push(id);
@@ -619,9 +608,8 @@ impl UiContext {
}
/// Whether `(px, py)` is covered by an open popover or a popover-carrying widget other
- /// than `query_id` (the querying widget excludes itself). Pass `WidgetId(0)` for a widget
- /// with no base — ids start at 1, so it matches nothing, like the legacy address of a
- /// widget that could never be registered.
+ /// than `query_id` (the querying widget excludes itself). Every widget has a base id
+ /// now (the flip) — the old `WidgetId(0)` no-base sentinel is gone.
pub fn is_coordinate_covered(&self, query_id: WidgetId, px: f32, py: f32) -> bool {
for &pop_id in self.active_popovers.iter() {
if pop_id == query_id {
@@ -708,7 +696,7 @@ impl UiContext {
if target.is_null() {
return;
}
- let Some(id) = (unsafe { (*target).base().map(|b| b.id()) }) else { return };
+ let id = unsafe { (*target).base().id() };
self.tree.register(id, target);
crate::widget::context_menu::show(x, y, options, header_count, id);
}
@@ -737,7 +725,8 @@ impl UiContext {
};
let mut config_info = None;
- if let Some(b) = unsafe { (*target).base() } {
+ {
+ let b = unsafe { (*target).base() };
if let (Some(ref file), Some(ref key)) = (&b.config_file, &b.config_key) {
config_info = Some((file.clone(), key.clone()));
}
diff --git a/src/layout.rs b/src/layout.rs
index 30be521..7a8d500 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -3142,7 +3142,7 @@ impl RenderTarget for PopoverCollector {
pub fn render_widget<T: Element + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
- let id = w.base().map(|b| b.id());
+ let id = Some(w.base().id());
if let Some(w_id) = id {
ctx.register_widget(w_id, w.as_ptr_mut());
}
@@ -5096,6 +5096,7 @@ mod tests {
}
struct MockWidget {
+ base: crate::widget::Widget,
x: f32,
y: f32,
w: f32,
@@ -5103,6 +5104,7 @@ mod tests {
}
impl Element for MockWidget {
+ crate::impl_widget_base!(MockWidget);
fn rect(&self) -> (f32, f32, f32, f32) {
(self.x, self.y, self.w, self.h)
}
@@ -5122,12 +5124,7 @@ mod tests {
}
impl Element for MockWidgetWithLabel {
- fn base(&self) -> Option<&crate::widget::Widget> {
- Some(&self.base)
- }
- fn base_mut(&mut self) -> Option<&mut crate::widget::Widget> {
- Some(&mut self.base)
- }
+ crate::impl_widget_base!(MockWidgetWithLabel);
fn rect(&self) -> (f32, f32, f32, f32) {
let offset = crate::widget::label_offset(self);
(self.base.x, self.base.y - offset, self.base.w, self.base.h + offset)
@@ -5161,14 +5158,14 @@ mod tests {
let mut stack = sec.vstack(&mut mock_pc, 10.0);
let mut dummy = crate::context::UiContext::new();
- let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+ let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
stack.add_widget(&mut w1, 50.0, 30.0, &mut dummy);
// Standard margin should be applied
assert_eq!(w1.x, 38.0);
assert_eq!(w1.y, start_y);
- let mut w2 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+ let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
stack.add_widget(&mut w2, 60.0, 40.0, &mut dummy);
// Second widget should start after first widget height + vstack spacing
@@ -5227,7 +5224,7 @@ mod tests {
assert!(subsec.is_child);
let mut dummy = crate::context::UiContext::new();
- let mut w = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+ let mut w = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
subsec.widget(&mut pc, &mut w, 12.0, 100.0, 40.0, &mut dummy);
let bottom = subsec.finish(&mut pc);
@@ -5312,10 +5309,10 @@ mod tests {
assert_eq!(ctx.cw, 500.0);
let mut ui_ctx = crate::context::UiContext::new();
- let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+ let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
ctx.widget(&mut w1, 12.0, 100.0, 40.0, &mut ui_ctx);
- let mut w2 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+ let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
ctx.widget(&mut w2, 12.0, 100.0, 30.0, &mut ui_ctx);
// Since cw=500, we should have multiple columns!
@@ -5333,10 +5330,10 @@ mod tests {
assert_eq!(ctx.grid.col_heights.len(), 1);
let mut ui_ctx = crate::context::UiContext::new();
- let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+ let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
ctx.widget(&mut w1, 12.0, 100.0, 40.0, &mut ui_ctx);
- let mut w2 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+ let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
ctx.widget(&mut w2, 12.0, 100.0, 30.0, &mut ui_ctx);
// Since it's a child section, we should have a single column only, so w1.x == w2.x.
@@ -5371,7 +5368,7 @@ mod tests {
assert_eq!(ctx.grid.col_heights.len(), 2);
let mut ui_ctx = crate::context::UiContext::new();
- let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
+ let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
ctx.widget(&mut w1, 12.0, 100.0, 40.0, &mut ui_ctx); // placed in col 0
let height_col_0_before = ctx.grid.col_heights[0];
@@ -5460,9 +5457,9 @@ mod tests {
};
let mut dummy = crate::context::UiContext::new();
- let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 100.0, h: 50.0 };
- let mut w2 = MockWidget { x: 0.0, y: 0.0, w: 100.0, h: 80.0 };
- let mut w3 = MockWidget { x: 0.0, y: 0.0, w: 80.0, h: 40.0 };
+ let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 100.0, h: 50.0 };
+ let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 100.0, h: 80.0 };
+ let mut w3 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 80.0, h: 40.0 };
let children = vec![
&mut w1 as *mut MockWidget as *mut (dyn Element + 'static),
@@ -5492,9 +5489,9 @@ mod tests {
};
let mut dummy = crate::context::UiContext::new();
- let mut w1 = MockWidget { x: 0.0, y: 0.0, w: 100.0, h: 50.0 };
- let mut w2 = MockWidget { x: 0.0, y: 0.0, w: 100.0, h: 80.0 };
- let mut w3 = MockWidget { x: 0.0, y: 0.0, w: 80.0, h: 40.0 };
+ let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 100.0, h: 50.0 };
+ let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 100.0, h: 80.0 };
+ let mut w3 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 80.0, h: 40.0 };
let children = vec![
&mut w1 as *mut MockWidget as *mut (dyn Element + 'static),
diff --git a/src/main.rs b/src/main.rs
index 97487c3..84bd5e4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -175,7 +175,7 @@ impl Application for DemoApp {
let self_ptr = self as *mut Self;
unsafe {
for w in (*self_ptr).roots() {
- let id = (*w).base().unwrap().id();
+ let id = (*w).base().id();
self.ui_context.register_widget(id, w);
}
}
diff --git a/src/scene/arena.rs b/src/scene/arena.rs
index 917da71..c44fa40 100644
--- a/src/scene/arena.rs
+++ b/src/scene/arena.rs
@@ -571,10 +571,12 @@ mod tests {
// else is defaulted, so this exercises the actual trait object without dragging in a
// heavyweight widget constructor.
struct Marker {
+ base: crate::widget::Widget,
tint: [f32; 4],
painted: std::cell::Cell<bool>,
}
impl Element for Marker {
+ crate::impl_widget_base!(Marker);
fn color(&self) -> [f32; 4] {
self.painted.set(true);
self.tint
@@ -582,8 +584,8 @@ mod tests {
}
let mut arena: Arena<Box<dyn Element>> = Arena::new();
- let root = arena.insert(Box::new(Marker { tint: [1.0, 0.0, 0.0, 1.0], painted: false.into() }));
- let child = arena.insert(Box::new(Marker { tint: [0.0, 1.0, 0.0, 1.0], painted: false.into() }));
+ let root = arena.insert(Box::new(Marker { base: crate::widget::Widget::new(), tint: [1.0, 0.0, 0.0, 1.0], painted: false.into() }));
+ let child = arena.insert(Box::new(Marker { base: crate::widget::Widget::new(), tint: [0.0, 1.0, 0.0, 1.0], painted: false.into() }));
arena.append_child(root, child);
// Walk the subtree the way a paint pass will, calling a real trait method on each node.
@@ -593,8 +595,8 @@ mod tests {
.collect();
assert_eq!(tints, vec![[1.0, 0.0, 0.0, 1.0], [0.0, 1.0, 0.0, 1.0]]);
- // The trait object is genuinely stored (its interior mutation is observable).
- assert!(arena.value(root).unwrap().base().is_none()); // default impl still reachable
+ // The trait object is genuinely stored (its base is reachable through the box).
+ let _ = arena.value(root).unwrap().base();
// Removing the root frees the child too, proving ownership lives in the arena.
arena.remove_subtree(root);
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index be4e5ad..34f3415 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -106,7 +106,8 @@ pub fn scroll_ancestor_text_bounds(_w: &dyn Element, _ui: &UiContext) -> Option<
/// stored on the widget base, positioned by the configured control-label layout. For
/// legacy widgets whose only text was that label (List's columns=None frame).
pub fn base_control_label(w: &dyn Element) -> Vec<TextLabel> {
- if let Some(b) = w.base() {
+ {
+ let b = w.base();
if let Some(ref label) = b.label {
let (_, font_size) = crate::layout::control_label_font_detached_parsed();
let color = crate::colors::control_label_color_detached_for_state(b.hovered, b.focused);
diff --git a/src/scene/tree.rs b/src/scene/tree.rs
index c0098ad..070b142 100644
--- a/src/scene/tree.rs
+++ b/src/scene/tree.rs
@@ -223,8 +223,13 @@ mod tests {
// A minimal real `Element` so tests exercise genuine `*mut dyn Element` payloads. The boxes
// are kept alive in a local `Vec` for the duration of each test; we hand the tree raw
// pointers into them, mirroring how widgets (owned by the app) are referenced by the tree.
- struct Marker(#[allow(dead_code)] u32);
+ struct Marker {
+ base: crate::widget::Widget,
+ #[allow(dead_code)]
+ tag: u32,
+ }
impl Element for Marker {
+ crate::impl_widget_base!(Marker);
fn color(&self) -> [f32; 4] {
[0.0, 0.0, 0.0, 0.0]
}
@@ -240,7 +245,7 @@ mod tests {
}
/// Create a widget, returning `(WidgetId, *mut dyn Element)`.
fn make(&mut self, tag: u32) -> (WidgetId, *mut (dyn Element + 'static)) {
- let mut b = Box::new(Marker(tag));
+ let mut b = Box::new(Marker { base: crate::widget::Widget::new(), tag: tag });
let ptr: *mut (dyn Element + 'static) = &mut *b;
self.boxes.push(b);
(WidgetId(tag as usize), ptr)
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index e12add1..c30125c 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -465,7 +465,7 @@ impl TreeList {
self.edit_box.select_anchor = Some(0);
let eb_ptr = self.edit_box.as_ptr_mut();
- let eb_id = self.edit_box.base().unwrap().id();
+ let eb_id = self.edit_box.base().id();
ui.register_widget(eb_id, eb_ptr);
ui.link_ids(host_id, eb_id);
@@ -652,23 +652,23 @@ impl Layout for TreeList {
/// `set_parent` side effect; also heals the inline rename editor's registry entry).
fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
let sb_ptr = self.search_box.as_ptr_mut();
- let sb_id = self.search_box.base().unwrap().id();
+ let sb_id = self.search_box.base().id();
ctx.register_widget(sb_id, sb_ptr);
ctx.link_ids(host_id, sb_id);
let btn_ptr = self.add_key_btn.as_ptr_mut();
- let btn_id = self.add_key_btn.base().unwrap().id();
+ let btn_id = self.add_key_btn.base().id();
ctx.register_widget(btn_id, btn_ptr);
ctx.link_ids(host_id, btn_id);
let pop_ptr = self.add_key_popover_box.as_ptr_mut();
- let pop_id = self.add_key_popover_box.base().unwrap().id();
+ let pop_id = self.add_key_popover_box.base().id();
ctx.register_widget(pop_id, pop_ptr);
ctx.link_ids(host_id, pop_id);
if self.editing_key_idx.is_some() {
let eb_ptr = self.edit_box.as_ptr_mut();
- let eb_id = self.edit_box.base().unwrap().id();
+ let eb_id = self.edit_box.base().id();
ctx.register_widget(eb_id, eb_ptr);
ctx.link_ids(host_id, eb_id);
}
@@ -1289,7 +1289,7 @@ mod tests {
let mut tree_list = TreeList::new();
tree_list.set_rect(10.0, 52.0, 380.0, 500.0);
- ctx.register_widget(tree_list.base().unwrap().id(), tree_list.as_ptr_mut());
+ ctx.register_widget(tree_list.base().id(), tree_list.as_ptr_mut());
ctx.tick(0.016);
ctx.clear_dirty();
@@ -1303,7 +1303,7 @@ mod tests {
let mut ctx = UiContext::new();
let mut tree_list = TreeList::new();
- ctx.register_widget(tree_list.base().unwrap().id(), tree_list.as_ptr_mut());
+ ctx.register_widget(tree_list.base().id(), tree_list.as_ptr_mut());
ctx.rebuild_spatial_grid();
let list_top = 52.0;
diff --git a/src/widget/core.rs b/src/widget/core.rs
index 0a65d0e..4db482b 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -24,8 +24,7 @@ pub mod focus {
}
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);
+ set_focused_id(w.base().id(), ctx);
}
pub fn set_focused_id(id: WidgetId, ctx: Option<&mut crate::context::UiContext>) {
@@ -41,10 +40,7 @@ pub mod focus {
}
pub fn is_focused(w: &dyn Element) -> bool {
- match w.base() {
- Some(b) => is_focused_id(b.id()),
- None => false,
- }
+ is_focused_id(w.base().id())
}
pub fn is_focused_id(id: WidgetId) -> bool {
@@ -58,9 +54,7 @@ pub mod focus {
}
pub fn clear_if_matches(w: &dyn Element) {
- if let Some(b) = w.base() {
- clear_if_matches_id(b.id());
- }
+ clear_if_matches_id(w.base().id());
}
pub fn clear_if_matches_id(id: WidgetId) {
@@ -82,14 +76,12 @@ pub mod focus {
let child_ptr = unsafe {
std::mem::transmute::<*mut dyn Element, *mut (dyn Element + 'static)>(child as *mut dyn Element)
};
- if let (Some(p_base), Some(c_base)) = (parent.base(), child.base()) {
- let (p_id, c_id) = (p_base.id(), c_base.id());
- ctx.register_widget(p_id, parent_ptr);
- ctx.register_widget(c_id, child_ptr);
- // The old add_child + set_parent pair, as the tree ops they always were.
- ctx.tree.link(p_id, c_id);
- ctx.tree.set_parent(c_id, Some(p_id));
- }
+ let (p_id, c_id) = (parent.base().id(), child.base().id());
+ ctx.register_widget(p_id, parent_ptr);
+ ctx.register_widget(c_id, child_ptr);
+ // The old add_child + set_parent pair, as the tree ops they always were.
+ ctx.tree.link(p_id, c_id);
+ ctx.tree.set_parent(c_id, Some(p_id));
}
/// Keyboard tree navigation from the focused widget. `ctx` resolves the focused id to a
@@ -587,7 +579,7 @@ pub mod context_menu {
}
pub fn clear_if_matches(w: &dyn Element) {
- let Some(id) = w.base().map(|b| b.id()) else { return };
+ let id = w.base().id();
CONTEXT_MENU.with(|m| {
let mut menu = m.borrow_mut();
if menu.target == Some(id) {
@@ -713,8 +705,8 @@ pub fn clear_widget_references(w: &dyn Element) {
#[macro_export]
macro_rules! impl_widget_base {
($name:ident) => {
- fn base(&self) -> Option<&$crate::widget::Widget> { Some(&self.base) }
- fn base_mut(&mut self) -> Option<&mut $crate::widget::Widget> { Some(&mut self.base) }
+ fn base(&self) -> &$crate::widget::Widget { &self.base }
+ fn base_mut(&mut self) -> &mut $crate::widget::Widget { &mut self.base }
fn as_any(&self) -> &dyn std::any::Any { self }
fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
fn as_ptr(&self) -> *mut (dyn $crate::widget::Element + 'static) {
diff --git a/src/widget/display/serialize.rs b/src/widget/display/serialize.rs
index a12ee5c..fc0f9d7 100644
--- a/src/widget/display/serialize.rs
+++ b/src/widget/display/serialize.rs
@@ -2,9 +2,9 @@ use crate::widget::*;
fn serialize_single_widget(w: &dyn Element, json: &mut String) {
let (x, y, width, height) = w.rect();
- let label = w.label().or_else(|| w.base().and_then(|b| b.label.clone())).unwrap_or_default();
- let focused = w.base().map_or(false, |b| b.focused);
- let hovered = w.base().map_or(false, |b| b.hovered);
+ let label = w.label().or_else(|| w.base().label.clone()).unwrap_or_default();
+ let focused = w.base().focused;
+ let hovered = w.base().hovered;
let value = w.value();
let type_name = w.type_name();
diff --git a/src/widget/input/ramp.rs b/src/widget/input/ramp.rs
index f9ca94f..ce7b076 100644
--- a/src/widget/input/ramp.rs
+++ b/src/widget/input/ramp.rs
@@ -375,19 +375,19 @@ impl Layout for ColorRamp {
fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
let p = self.r_slider.as_ptr_mut();
- let id = self.r_slider.base().unwrap().id();
+ let id = self.r_slider.base().id();
ctx.register_widget(id, p);
ctx.link_ids(host_id, id);
let p = self.g_slider.as_ptr_mut();
- let id = self.g_slider.base().unwrap().id();
+ let id = self.g_slider.base().id();
ctx.register_widget(id, p);
ctx.link_ids(host_id, id);
let p = self.b_slider.as_ptr_mut();
- let id = self.b_slider.base().unwrap().id();
+ let id = self.b_slider.base().id();
ctx.register_widget(id, p);
ctx.link_ids(host_id, id);
let p = self.del_button.as_ptr_mut();
- let id = self.del_button.base().unwrap().id();
+ let id = self.del_button.base().id();
ctx.register_widget(id, p);
ctx.link_ids(host_id, id);
}
@@ -758,19 +758,19 @@ impl Layout for Ramp {
fn register_embedded_children(&mut self, host_id: WidgetId, ctx: &mut UiContext) {
let p = self.preset_dropdown.as_ptr_mut();
- let id = self.preset_dropdown.base().unwrap().id();
+ let id = self.preset_dropdown.base().id();
ctx.register_widget(id, p);
ctx.link_ids(host_id, id);
let p = self.line_type_dropdown.as_ptr_mut();
- let id = self.line_type_dropdown.base().unwrap().id();
+ let id = self.line_type_dropdown.base().id();
ctx.register_widget(id, p);
ctx.link_ids(host_id, id);
let p = self.val_slider.as_ptr_mut();
- let id = self.val_slider.base().unwrap().id();
+ let id = self.val_slider.base().id();
ctx.register_widget(id, p);
ctx.link_ids(host_id, id);
let p = self.del_button.as_ptr_mut();
- let id = self.del_button.base().unwrap().id();
+ let id = self.del_button.base().id();
ctx.register_widget(id, p);
ctx.link_ids(host_id, id);
}
diff --git a/src/widget/layout_helper.rs b/src/widget/layout_helper.rs
index 9945eab..21b5629 100644
--- a/src/widget/layout_helper.rs
+++ b/src/widget/layout_helper.rs
@@ -22,7 +22,7 @@ impl ColumnLayout {
}
pub fn add_widget(&mut self, widget: &mut dyn Element, height: f32) {
- let label_off = widget.base().map_or(0.0, |b| b.label_offset());
+ let label_off = widget.base().label_offset();
let total_h = height + label_off;
widget.set_rect(self.x + self.margin, self.current_y, self.width - 2.0 * self.margin, height);
self.current_y += total_h + self.gap;
@@ -36,7 +36,7 @@ impl ColumnLayout {
let mut max_label_off = 0.0;
for &widget_ptr in widgets {
unsafe {
- let off = (*widget_ptr).base().map_or(0.0, |b| b.label_offset());
+ let off = (*widget_ptr).base().label_offset();
if off > max_label_off {
max_label_off = off;
}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 3e17518..b13c842 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -188,20 +188,20 @@ pub struct Size {
}
pub trait Element {
- fn base(&self) -> Option<&Widget> { None }
- fn base_mut(&mut self) -> Option<&mut Widget> { None }
+ /// The widget's shared base state — GUARANTEED (the flip): the `Option` escape hatch
+ /// and its `WidgetId(0)` sentinel class are gone. `Adapted` (the one production
+ /// implementor) always owns a base; test shims carry one via `impl_widget_base!`.
+ fn base(&self) -> &Widget;
+ fn base_mut(&mut self) -> &mut Widget;
fn preferred_height(&self) -> Option<f32> { None }
fn mark_dirty(&mut self, ctx: &mut UiContext) {
- let mut parent_id = None;
- if let Some(b) = self.base_mut() {
- if b.dirty {
- return;
- }
- b.dirty = true;
- parent_id = b.id.get();
+ let b = self.base_mut();
+ if b.dirty {
+ return;
}
- if let Some(id) = parent_id {
+ b.dirty = true;
+ if let Some(id) = b.id.get() {
if let Some(parent_ptr) = ctx.tree.parent_ptr(id) {
unsafe {
(*parent_ptr).mark_dirty(ctx);
@@ -210,32 +210,12 @@ pub trait Element {
}
}
- fn as_any(&self) -> &dyn std::any::Any {
- struct DummyAny;
- static DUMMY: DummyAny = DummyAny;
- &DUMMY
- }
- fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
- struct DummyAny;
- thread_local! {
- static DUMMY_MUT: std::cell::UnsafeCell<DummyAny> = std::cell::UnsafeCell::new(DummyAny);
- }
- DUMMY_MUT.with(|d| unsafe { &mut *d.get() })
- }
- fn as_ptr(&self) -> *mut (dyn Element + 'static) {
- struct DummyElement;
- impl Element for DummyElement {
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
- }
- std::ptr::null_mut::<DummyElement>() as *mut (dyn Element + 'static)
- }
- fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
- struct DummyElement;
- impl Element for DummyElement {
- fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
- }
- std::ptr::null_mut::<DummyElement>() as *mut (dyn Element + 'static)
- }
+ // Required (the flip): the old defaults manufactured DummyAny/null-DummyElement
+ // stand-ins nothing could legitimately use. `impl_widget_base!` provides all four.
+ fn as_any(&self) -> &dyn std::any::Any;
+ fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
+ fn as_ptr(&self) -> *mut (dyn Element + 'static);
+ fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static);
fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
match event {
@@ -274,15 +254,12 @@ pub trait Element {
}
fn rect(&self) -> (f32, f32, f32, f32) {
- if let Some(b) = self.base() {
- (b.x, b.y, b.w, b.h)
- } else {
- (0.0, 0.0, 0.0, 0.0)
- }
+ let b = self.base();
+ (b.x, b.y, b.w, b.h)
}
fn label(&self) -> Option<String> {
- self.base().and_then(|b| b.label.clone())
+ self.base().label.clone()
}
fn get_value_string(&self) -> Option<String> { None }
@@ -297,38 +274,29 @@ pub trait Element {
}
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- if let Some(b) = self.base_mut() {
- b.x = x;
- b.y = y;
- b.w = w;
- b.h = h;
- }
+ let b = self.base_mut();
+ b.x = x;
+ b.y = y;
+ b.w = w;
+ b.h = h;
}
fn set_row_rect(&mut self, x: f32, w: f32) {
- if let Some(b) = self.base_mut() {
- b.row_x = x;
- b.row_w = w;
- }
+ let b = self.base_mut();
+ b.row_x = x;
+ b.row_w = w;
}
fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
- if ctx.is_coordinate_covered(self.base().map(|b| b.id()).unwrap_or(WidgetId(0)), px, py) {
+ if ctx.is_coordinate_covered(self.base().id(), px, py) {
return false;
}
let (x, y, w, h) = self.rect();
if w <= 0.0 || h <= 0.0 {
return false;
}
- let (mut hx, mut hw) = if let Some(b) = self.base() {
- if b.row_w > 0.0 {
- (b.row_x, b.row_w)
- } else {
- (x, w)
- }
- } else {
- (x, w)
- };
+ let b = self.base();
+ let (mut hx, mut hw) = if b.row_w > 0.0 { (b.row_x, b.row_w) } else { (x, w) };
let label_x = self.label_x_offset();
hx += label_x;
hw -= label_x;
@@ -337,12 +305,10 @@ pub trait Element {
fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
ctx.set_cursor_pos(px, py);
- if ctx.is_coordinate_covered(self.base().map(|b| b.id()).unwrap_or(WidgetId(0)), px, py) {
- let was = self.base().map_or(false, |b| b.hovered);
+ if ctx.is_coordinate_covered(self.base().id(), px, py) {
+ let was = self.base().hovered;
if was {
- if let Some(b) = self.base_mut() {
- b.hovered = false;
- }
+ self.base_mut().hovered = false;
self.handle_event(&Event::MouseLeave, ctx);
}
return was;
@@ -351,22 +317,16 @@ pub trait Element {
}
fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if self.base().is_some() {
- let was = self.base().map_or(false, |b| b.hovered);
- let is_hit = self.hit_test(px, py, ctx);
- if let Some(b) = self.base_mut() {
- b.hovered = is_hit;
- }
- if was != is_hit {
- if is_hit {
- self.handle_event(&Event::MouseEnter, ctx);
- } else {
- self.handle_event(&Event::MouseLeave, ctx);
- }
- was != is_hit
+ let was = self.base().hovered;
+ let is_hit = self.hit_test(px, py, ctx);
+ self.base_mut().hovered = is_hit;
+ if was != is_hit {
+ if is_hit {
+ self.handle_event(&Event::MouseEnter, ctx);
} else {
- false
+ self.handle_event(&Event::MouseLeave, ctx);
}
+ true
} else {
false
}
@@ -381,23 +341,19 @@ pub trait Element {
fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
// Focus/hover highlight color, folded from the zero-override `highlight_color` (6bd).
- let is_focused = self.base().map(|b| ctx.is_focused_id(b.id())).unwrap_or(false);
+ let is_focused = ctx.is_focused_id(self.base().id());
let hc = if is_focused {
colors::highlight_primary_color()
- } else if self.base().map_or(false, |b| b.hovered) {
+ } else if self.base().hovered {
colors::HIGHLIGHT_SECONDARY
} else {
return None;
};
let label_x = self.label_x_offset();
- if let Some(b) = self.base() {
- let hx = if b.row_w > 0.0 { b.row_x } else { b.x } + label_x;
- let hw = if b.row_w > 0.0 { b.row_w } else { b.w } - label_x;
- Some((hx, b.y, hw, b.h, hc))
- } else {
- let (x, y, w, h) = self.rect();
- Some((x + label_x, y, w - label_x, h, hc))
- }
+ let b = self.base();
+ let hx = if b.row_w > 0.0 { b.row_x } else { b.x } + label_x;
+ let hw = if b.row_w > 0.0 { b.row_w } else { b.w } - label_x;
+ Some((hx, b.y, hw, b.h, hc))
}
fn color(&self) -> [f32; 4];
@@ -416,7 +372,7 @@ pub trait Element {
if name == "Label" || name == "Button" || name == "Checkbox" || name == "Toggle" || name == "Ramp" {
return 0.0;
}
- if crate::layout::control_label_layout() == "side" && self.base().map_or(false, |b| b.label.is_some()) {
+ if crate::layout::control_label_layout() == "side" && self.base().label.is_some() {
90.0
} else {
0.0
@@ -539,23 +495,17 @@ pub trait Element {
fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> { None }
fn render_popover(&self, _pc: &mut dyn crate::layout::RenderTarget) {}
fn set_text(&mut self, text: &str) {
- if let Some(b) = self.base_mut() {
- b.label = Some(text.to_string());
- }
+ self.base_mut().label = Some(text.to_string());
}
fn focus(&mut self) {
- if let Some(b) = self.base_mut() {
- b.focused = true;
- }
+ self.base_mut().focused = true;
}
fn unfocus(&mut self) {
- if let Some(b) = self.base_mut() {
- b.focused = false;
- }
+ self.base_mut().focused = false;
}
fn focused(&self, ctx: &UiContext) -> bool {
- self.base().map(|b| ctx.is_focused_id(b.id())).unwrap_or(false)
+ ctx.is_focused_id(self.base().id())
}
fn prepare_text(&mut self, _fs: &mut glyphon::FontSystem) {}
fn set_selected(&mut self, _selected: bool) {}
@@ -569,8 +519,7 @@ pub trait Element {
fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
- let base = self.base()?;
- ctx.tree.parent_ptr(base.id())
+ ctx.tree.parent_ptr(self.base().id())
}
@@ -579,10 +528,7 @@ pub trait Element {
// through `focus::link_parent_child` or `ctx.tree` directly.
fn children(&self, ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
- match self.base() {
- Some(base) => ctx.tree.children_ptrs(base.id()),
- None => vec![],
- }
+ ctx.tree.children_ptrs(self.base().id())
}
fn z_index(&self) -> i32 { 0 }
@@ -610,13 +556,11 @@ pub trait Element {
pub trait Control: Element {
fn set_label(&mut self, label: &str) {
- if let Some(b) = self.base_mut() {
- b.label = Some(label.to_string());
- }
+ self.base_mut().label = Some(label.to_string());
}
fn control_label(&self) -> Option<TextLabel> {
- let b = self.base()?;
+ let b = self.base();
let label = b.label.as_ref()?;
let name = self.type_name();
@@ -751,7 +695,7 @@ pub fn label_offset(w: &dyn Element) -> f32 {
if name == "Label" || name == "Button" || name == "Checkbox" || name == "Toggle" {
return 0.0;
}
- w.base().map_or(0.0, |b| b.label_offset())
+ w.base().label_offset()
}
#[derive(Debug, Clone, Copy, PartialEq)]
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 55d9f03..7e5708b 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -693,24 +693,19 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
/// went to `focus::link_parent_child`/tree ops).
pub fn add_child(&mut self, child: *mut (dyn Element + 'static), ctx: &mut UiContext) {
// The old Element default's tree link…
- if let Some(c_base) = unsafe { (*child).base() } {
- let c_id = c_base.id();
- let p_id = self.base.id();
- let self_ptr = self.as_ptr();
- ctx.register_widget(p_id, self_ptr);
- ctx.register_widget(c_id, child);
- ctx.tree.link(p_id, c_id);
- }
+ let c_id = unsafe { (*child).base().id() };
+ let p_id = self.base.id();
+ let self_ptr = self.as_ptr();
+ ctx.register_widget(p_id, self_ptr);
+ ctx.register_widget(c_id, child);
+ ctx.tree.link(p_id, c_id);
// …plus, for containers, the legacy container extra: parent the child back (Layer,
// Switcher) — the symmetric tree link the child's own set_parent used to make.
if Layout::has_container_children(&self.inner) {
let self_ptr = self.as_ptr_mut();
- if let Some(c_base) = unsafe { (*child).base() } {
- let c_id = c_base.id();
- ctx.register_widget(self.base.id(), self_ptr);
- ctx.register_widget(c_id, child);
- ctx.tree.set_parent(c_id, Some(self.base.id()));
- }
+ ctx.register_widget(self.base.id(), self_ptr);
+ ctx.register_widget(c_id, child);
+ ctx.tree.set_parent(c_id, Some(self.base.id()));
}
}
@@ -719,13 +714,11 @@ impl<W: Layout + Paint + Input + 'static> Adapted<W> {
// Replica of the old Element default: symmetric tree link.
let id = self.base.id();
if let Some(p_ptr) = parent {
- if let Some(p_base) = unsafe { (*p_ptr).base() } {
- let p_id = p_base.id();
- ctx.register_widget(p_id, p_ptr);
- let self_ptr = self.as_ptr();
- ctx.register_widget(id, self_ptr);
- ctx.tree.set_parent(id, Some(p_id));
- }
+ let p_id = unsafe { (*p_ptr).base().id() };
+ ctx.register_widget(p_id, p_ptr);
+ let self_ptr = self.as_ptr();
+ ctx.register_widget(id, self_ptr);
+ ctx.tree.set_parent(id, Some(p_id));
} else {
ctx.tree.set_parent(id, None);
}
@@ -875,11 +868,11 @@ impl<W: Layout + Paint + Input + 'static> std::ops::DerefMut for Adapted<W> {
}
impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
- fn base(&self) -> Option<&Widget> {
- Some(&self.base)
+ fn base(&self) -> &Widget {
+ &self.base
}
- fn base_mut(&mut self) -> Option<&mut Widget> {
- Some(&mut self.base)
+ fn base_mut(&mut self) -> &mut Widget {
+ &mut self.base
}
// `as_any` exposes the *inner* widget: legacy code downcasts by concrete widget type
// (`json_layout`'s `downcast_mut::<Checkbox>()`), and the adapter must be transparent to it.
@@ -923,10 +916,8 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
return true;
}
for child in Layout::container_children(&self.inner) {
- if let Some(b) = unsafe { (*child).base() } {
- if b.id() == child_id {
- return Layout::child_visible(&self.inner, child);
- }
+ if unsafe { (*child).base().id() } == child_id {
+ return Layout::child_visible(&self.inner, child);
}
}
false
@@ -1699,10 +1690,10 @@ mod tests {
// preserves) and sets the base hover flag; moving away synthesizes MouseLeave.
ctx.propagate_event(&Event::PointerMove { x: 20.0, y: 15.0, local_x: 20.0, local_y: 15.0 }, ptr);
assert_eq!(w.inner().entered, 1, "MouseEnter reached on_event");
- assert!(unsafe { (*ptr).base().map_or(false, |b| b.hovered) }, "base hover flag set through the adapter");
+ assert!(unsafe { (*ptr).base().hovered }, "base hover flag set through the adapter");
ctx.propagate_event(&Event::PointerMove { x: 200.0, y: 200.0, local_x: 200.0, local_y: 200.0 }, ptr);
assert_eq!(w.inner().left, 1, "MouseLeave reached on_event");
- assert!(!unsafe { (*ptr).base().map_or(false, |b| b.hovered) }, "base hover flag cleared");
+ assert!(!unsafe { (*ptr).base().hovered }, "base hover flag cleared");
}
/// A narrow widget that is also a controller: the controller trait is reached through the