GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(registry): the widget registry's aliasing is sound, and checked by Miri
- Owned held its widget in a Box, a unique pointer: every app reborrow
(self.button.take_click()) invalidated the raw pointer the registry had
taken, so its next dispatch was UB every frame in every app (Miri:
"trying to retag ... that tag does not exist in the borrow stack"). The
allocation is now a raw NonNull root the app's Deref and the registry both
derive from.
- WidgetTree::register keeps a live Owned root against a pointer to the same
address taken from inside the widget (its context menu, its focus,
set_parent) -- a reborrow the next app access invalidates. Compared by
address, never read through.
- UiContext::claim_focus: a widget focusing itself mid-event is recorded
without FocusIn re-entering it through the registry (the tree list did);
EventCtx::request_focus uses it.
- The tree list's and paginator's embedded children are Owned fields.
CI gains a miri job: the widget::owned tests under Stacked and Tree Borrows.
On a throwaway branch the Box-based Owned fails it; this passes it, with every
other job.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.github/workflows/ci.yml | 30 +++++
CLAUDE.md | 41 ++++++-
src/context.rs | 16 +++
src/scene/tree.rs | 29 ++++-
src/widget/container/paginator.rs | 8 +-
src/widget/container/treelist.rs | 43 +++++---
src/widget/model.rs | 9 +-
src/widget/owned.rs | 226 ++++++++++++++++++++++++++------------
8 files changed, 294 insertions(+), 108 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 398e765..820b0b1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -92,6 +92,36 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --all-targets --all-features -- -D warnings
+ # The registry's pointers into app-owned widgets, under Miri: the aliasing rules a
+ # compiler does not check. Narrow on purpose (Miri interprets, slowly): the `Owned`
+ # tests, which take turns between the app's access and the registry's, under both of
+ # Miri's aliasing models. Until 2026-10-08 `Owned` held its widget in a `Box`, and every
+ # app access invalidated the registry's pointer.
+ miri:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 45
+ env:
+ # A nightly may warn where stable does not; this job is about aliasing, not lints.
+ RUSTFLAGS: ""
+ MIRIFLAGS: -Zmiri-disable-isolation
+ steps:
+ - uses: actions/checkout@v5
+ - name: Install system packages
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y --no-install-recommends pkg-config libwayland-dev libxkbcommon-dev
+ - uses: dtolnay/rust-toolchain@nightly
+ with:
+ components: miri, rust-src
+ - uses: Swatinem/rust-cache@v2
+ - run: cargo miri setup
+ - name: Stacked Borrows
+ run: cargo miri test --lib widget::owned
+ - name: Tree Borrows
+ run: cargo miri test --lib widget::owned
+ env:
+ MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-tree-borrows
+
# The browser half (src/web, the wasm examples) is cfg'd out of the Linux
# build above, so nothing else compiles it. scripts/check-wasm type-checks
# the library, its features and the four wasm examples.
diff --git a/CLAUDE.md b/CLAUDE.md
index 3837db1..3b10d4e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -484,7 +484,7 @@ following the caret through every step, each `done`'s serial equal to the commit
Sway routes text-input focus only while an input method is bound, so with none (the
24-step harness) nothing changes: 0 px.
-CI (`.github/workflows/ci.yml`, every push and PR) has four jobs, warnings as errors in each:
+CI (`.github/workflows/ci.yml`, every push and PR) has five jobs, warnings as errors in the first four:
- **`test`** (Ubuntu 24.04) builds and tests with default and with all features. It installs
`libwayland-dev` and `libxkbcommon-dev` (the two native libraries the build links, through
@@ -501,6 +501,9 @@ CI (`.github/workflows/ci.yml`, every push and PR) has four jobs, warnings as er
loops, `new` without `Default`); anything else clippy reports is fixed, not allowed —
locally, `cargo clippy -p cce-ui --all-features --all-targets -- -D warnings` is the check.
Linux only: `src/web` and `src/mac` are compiled out there.
+- **`miri`** runs the `widget::owned` tests under Miri, Stacked and Tree Borrows (nightly):
+ the app's access to a widget and the registry's taking turns (see "The registry holds
+ pointers, and knows when they die").
- **`wasm`** runs `scripts/check-wasm`: the library, its features and the four wasm examples,
type-checked for the browser. Its `RUSTFLAGS` carries `--cfg=web_sys_unstable_apis` itself,
since an environment `RUSTFLAGS` replaces `.cargo/config.toml`'s. Its first run (2026-10-08)
@@ -1721,13 +1724,39 @@ The pointer-taking entry points say so:
whose trait objects are not `'static`, and tests that exercise raw pointers.
- The demo's `register_roots` is the pattern to copy.
-What is still open is the ALIASING model. The registry derefs its pointers while the app holds
-the `Owned`, so a `&mut` reached through the registry and one reached through the field can
-coexist. The end state for that is a registry that owns its widgets and lends them out.
+**The aliasing is sound since 2026-10-08, and checked by Miri.** Three things were not:
+
+- **`Owned` held its widget in a `Box`.** A `Box` is a unique pointer to the language, so every
+ reborrow of the widget through it — each `self.button.take_click()` — invalidated the raw
+ pointer the registry had taken, and the registry's next dispatch was undefined behaviour,
+ every frame in every app. Miri reported it ("trying to retag … but that tag does not exist in
+ the borrow stack", at the registry's write). `Owned` now holds the allocation as a raw
+ `NonNull` ROOT, which the app's `Deref` and the registry both derive their references from.
+- **A registration from inside a widget replaced that root.** Opening a context menu
+ (`show_context_menu`), focusing by pointer (`set_focused_ptr`), `set_parent` and the like
+ re-registered the widget with a pointer taken from its own `&mut self` — a reborrow the next
+ app access invalidates. `WidgetTree::register` now KEEPS a live `Owned` root against a
+ pointer to the same address (compared by address, never read through); a widget swapped out
+ of its box is elsewhere and registers as usual.
+- **A widget focused itself through the registry mid-event.** The tree list's click handler
+ called `set_focused_ptr` on itself, and the context delivered FocusIn back through the
+ registry — a second `&mut` while its own was live. `UiContext::claim_focus` records the
+ focus and tells the old holder without re-entering the claimant (what
+ `EventCtx::request_focus` does too); the widget sets its own focused state.
+
+The toolkit's embedded children (the tree list's search box, add-key button and popover,
+inline editor; the paginator's menu) are `Owned` fields now, so they have roots of their own.
+The rule left is the ordinary one: a `&mut` reached through the registry must not overlap one
+taken through the `Owned` — an app does not hold `&mut self.x` across a `UiContext` call that
+reaches `x`, and a `UiContext` call handed the widget uses what it was handed. The end state
+that makes even that the compiler's job is a registry that owns its widgets and lends them out
+by handle; it would touch every widget access in every app.
`a_dropped_widget_is_never_handed_out`, `a_clone_has_a_liveness_of_its_own`,
-`an_owned_widget_survives_its_vec_reallocating` and
-`swapping_the_widget_out_of_its_box_never_leaves_a_dangling_entry` are the tests.
+`an_owned_widget_survives_its_vec_reallocating`,
+`swapping_the_widget_out_of_its_box_never_leaves_a_dangling_entry`,
+`an_app_and_the_registry_take_turns_soundly` and `an_inner_registration_keeps_the_root` are
+the tests; CI's `miri` job runs the `widget::owned` ones under Stacked and Tree Borrows.
**Runtime verification matters here.** Several scene changes are "compiles + tests pass; runtime
verification pending" per the RFC — the headless tests can't catch paint/event regressions. When
diff --git a/src/context.rs b/src/context.rs
index 00e42dd..5453696 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -651,6 +651,22 @@ impl UiContext {
false
}
+ /// Make `id` the window's focus from INSIDE that widget's own event handling: recorded,
+ /// and the holder before told (`unfocus`), but no FocusIn delivered back to `id` —
+ /// it is running, holding `&mut self`, and a FocusIn through the registry would be a
+ /// second `&mut` to it while the first is live. What a widget that focuses itself does
+ /// (`EventCtx::request_focus`; a tree list on a click into its rows), setting whatever
+ /// its FocusIn would have set itself.
+ pub fn claim_focus(&mut self, id: WidgetId) {
+ if let Some(old) = self.focused_widget.filter(|old| *old != id) {
+ if let Some(ptr) = self.tree.get_ptr(old) {
+ // SAFETY: a registry-resolved live widget other than the claimant.
+ unsafe { (*ptr).unfocus() };
+ }
+ }
+ self.focused_widget = Some(id);
+ }
+
/// Focus `w` as a direct `w.focus()` did — the widget is told (`focus`), the holder before
/// it is told it lost focus (`unfocus`) — and record it as the window's focus, which a
/// direct call never did: the Tab walk and the accessibility tree read the record, and
diff --git a/src/scene/tree.rs b/src/scene/tree.rs
index 9f150a9..859224a 100644
--- a/src/scene/tree.rs
+++ b/src/scene/tree.rs
@@ -51,6 +51,9 @@ struct Entry {
id: WidgetId,
ptr: Option<*mut (dyn WidgetHost + 'static)>,
alive: Option<Weak<()>>,
+ /// The pointer is an `Owned` box's raw root (`WidgetHost::stable_target`), which every
+ /// later access to the widget — the app's and the registry's — derives from.
+ stable: bool,
}
/// Resolve an entry's pointer to a usable one: non-null (skipping link-only and null-data
@@ -99,7 +102,7 @@ impl WidgetTree {
return node;
}
}
- let node = self.arena.insert(Entry { id, ptr: None, alive: None });
+ let node = self.arena.insert(Entry { id, ptr: None, alive: None, stable: false });
self.by_id.insert(id, node);
node
}
@@ -114,13 +117,28 @@ impl WidgetTree {
/// a watch on the widget's liveness token. After the call the tree resolves the pointer only
/// while that widget has not been dropped.
pub unsafe fn register(&mut self, id: WidgetId, ptr: *mut (dyn WidgetHost + 'static)) {
+ // A live `Owned` root already registered for this widget is KEPT against a pointer to
+ // the same address taken some other way — a widget's own `&mut self` mid-event (its
+ // context menu, its focus), the inner widget handed for the `Owned`: that pointer is a
+ // reborrow the next access to the widget invalidates, and the root is what stays valid
+ // (`widget::Owned`). Compared by address, without reading through either pointer,
+ // which would disturb the borrow of a widget that is handling an event. A widget moved
+ // out of its box is at another address and registers as usual.
+ if let Some(node) = self.by_id.get(&id).copied() {
+ if let Some(e) = self.arena.value(node) {
+ let same = e.ptr.is_some_and(|p| std::ptr::addr_eq(p, ptr));
+ if e.stable && same && e.alive.as_ref().is_some_and(|w| w.strong_count() > 0) {
+ return;
+ }
+ }
+ }
// SAFETY: the caller's contract — null, or a live widget. A widget in an `Owned` box
// names the boxed widget and the allocation's token instead of itself.
- let (ptr, alive) = match unsafe { ptr.as_mut() } {
- None => (ptr, None),
+ let (ptr, alive, stable) = match unsafe { ptr.as_mut() } {
+ None => (ptr, None, false),
Some(w) => match w.stable_target() {
- Some((inner, alive)) => (inner, Some(alive)),
- None => (ptr, Some(w.base().live.watch())),
+ Some((inner, alive)) => (inner, Some(alive), true),
+ None => (ptr, Some(w.base().live.watch()), false),
},
};
let node = self.ensure_node(id);
@@ -128,6 +146,7 @@ impl WidgetTree {
let entry = self.arena.value_mut(node).unwrap();
entry.ptr = Some(ptr);
entry.alive = alive;
+ entry.stable = stable;
}
/// Make `child` a child of `parent` (deduped, reparenting from any previous parent). Mirrors
diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index de6de82..8a1fba3 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -25,7 +25,9 @@ use crate::widget::{
};
pub struct Paginator {
- pub sidebar_menu: Adapted<ButtonStrip>,
+ /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
+ /// stable root rather than into this widget (see `widget::Owned`).
+ pub sidebar_menu: crate::widget::Owned<Adapted<ButtonStrip>>,
pub selected_page: usize,
pub sidebar_w: f32,
pub page_labels: Vec<String>,
@@ -38,7 +40,7 @@ impl Paginator {
let num_pages = pages.len();
let temp_paginator = Paginator {
- sidebar_menu: Adapted::new(ButtonStrip::new(0.0, 0.0, 0.0, 0.0)),
+ sidebar_menu: Adapted::new(ButtonStrip::new(0.0, 0.0, 0.0, 0.0)).into(),
selected_page: 0,
sidebar_w: 0.0,
page_labels: pages.clone(),
@@ -57,7 +59,7 @@ impl Paginator {
}
Adapted::new(Paginator {
- sidebar_menu,
+ sidebar_menu: sidebar_menu.into(),
selected_page: 0,
sidebar_w,
page_labels: pages,
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index ecdfdaa..0f71937 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -164,10 +164,16 @@ fn build_tree(
pub struct TreeList {
pub base: Widget,
pub scroll_box: ScrollBox,
- pub search_box: crate::widget::Adapted<TextBox>,
- pub add_key_btn: crate::widget::Adapted<Button>,
+ /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
+ /// stable root rather than into this widget (see `widget::Owned`).
+ pub search_box: crate::widget::Owned<crate::widget::Adapted<TextBox>>,
+ /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
+ /// stable root rather than into this widget (see `widget::Owned`).
+ pub add_key_btn: crate::widget::Owned<crate::widget::Adapted<Button>>,
pub add_key_popover_open: bool,
- pub add_key_popover_box: crate::widget::Adapted<TextBox>,
+ /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
+ /// stable root rather than into this widget (see `widget::Owned`).
+ pub add_key_popover_box: crate::widget::Owned<crate::widget::Adapted<TextBox>>,
pub new_key_path_request: Option<String>,
pub flat_keys: Vec<(String, serde_json::Value)>,
pub annotations: Vec<Option<String>>,
@@ -186,7 +192,9 @@ pub struct TreeList {
/// tree while still being, visually, part of the tree pane.
pub focused: bool,
pub deleted_key_path: Option<String>,
- pub edit_box: crate::widget::Adapted<TextBox>,
+ /// In an [`Owned`](crate::widget::Owned) box of its own, so the registry points at a
+ /// stable root rather than into this widget (see `widget::Owned`).
+ pub edit_box: crate::widget::Owned<crate::widget::Adapted<TextBox>>,
pub editing_key_idx: Option<usize>,
pub double_click_timer: Option<(web_time::Instant, usize)>,
pub rename_request: Option<(String, String)>,
@@ -202,10 +210,10 @@ impl TreeList {
Adapted::new(TreeList {
base: Widget::new(),
scroll_box,
- search_box: TextBox::new(String::new()).with_search().with_update_on_type(true),
- add_key_btn: Button::new(0.0, 0.0, 80.0, 26.0).with_label("+ Add Key"),
+ search_box: TextBox::new(String::new()).with_search().with_update_on_type(true).into(),
+ add_key_btn: Button::new(0.0, 0.0, 80.0, 26.0).with_label("+ Add Key").into(),
add_key_popover_open: false,
- add_key_popover_box: TextBox::new(String::new()).with_placeholder("new.key.path").with_multiline(false),
+ add_key_popover_box: TextBox::new(String::new()).with_placeholder("new.key.path").with_multiline(false).into(),
new_key_path_request: None,
flat_keys: Vec::new(),
annotations: Vec::new(),
@@ -219,7 +227,7 @@ impl TreeList {
last_scroll_y: 0.0,
focused: false,
deleted_key_path: None,
- edit_box: TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true),
+ edit_box: TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true).into(),
editing_key_idx: None,
double_click_timer: None,
rename_request: None,
@@ -454,8 +462,10 @@ impl TreeList {
if button == MouseButton::Left && state == ElementState::Pressed && !covered {
let on_scrollbar = self.scroll_box.hit_test_scrollbar(px, py) || self.scroll_box.scrollbar_dragging;
if !on_scrollbar && px >= list_left && px <= list_left + list_width && py >= list_top && py <= list_bottom {
- // SAFETY: `host` is this widget's own adapter, live while its event is routed.
- if let Some(h) = host { unsafe { ui.set_focused_ptr(h) }; }
+ // Focus claimed from inside this event (`UiContext::claim_focus`): a FocusIn
+ // through the registry would re-enter this widget while it holds `&mut self`.
+ ui.claim_focus(host_id);
+ self.focused = true;
let relative_y = py - list_top + self.scroll_box.scroll_y;
let row_idx = (relative_y / self.item_height) as usize;
if row_idx < self.items.len() {
@@ -484,7 +494,7 @@ impl TreeList {
TreeElement::Leaf { path, name, .. } => (path.clone(), name.clone()),
};
self.editing_key_idx = Some(row_idx);
- self.edit_box = TextBox::new(relative_name).with_multiline(false).with_draw_bg_border(true);
+ self.edit_box = TextBox::new(relative_name).with_multiline(false).with_draw_bg_border(true).into();
self.edit_box.editing = true;
self.edit_box.cursor_idx = self.edit_box.text.chars().count();
self.edit_box.select_anchor = Some(0);
@@ -531,8 +541,10 @@ impl TreeList {
if button == MouseButton::Right && state == ElementState::Pressed && !covered
&& px >= list_left && px <= list_left + list_width && py >= list_top && py <= list_bottom {
- // SAFETY: `host` is this widget's own adapter, live while its event is routed.
- if let Some(h) = host { unsafe { ui.set_focused_ptr(h) }; }
+ // Focus claimed from inside this event (`UiContext::claim_focus`): a FocusIn
+ // through the registry would re-enter this widget while it holds `&mut self`.
+ ui.claim_focus(host_id);
+ self.focused = true;
let relative_y = py - list_top + self.scroll_box.scroll_y;
let row_idx = (relative_y / self.item_height) as usize;
if row_idx < self.items.len() {
@@ -1094,7 +1106,6 @@ impl Input for TreeList {
/// search box, positions/commits the inline rename editor (re-targeting focus to the
/// adapter on commit), and runs the scrollbar activity fade.
fn tick_ctx(&mut self, dt: f32, ectx: &mut EventCtx) -> bool {
- let host = ectx.host_ptr();
let host_id = ectx.id;
let Some(ui) = ectx.ui.as_deref_mut() else {
return false;
@@ -1168,8 +1179,8 @@ impl Input for TreeList {
let eb_id = self.edit_box.base().id();
ui.unlink_child(host_id, eb_id);
ui.unregister_widget(eb_id);
- // SAFETY: `host` is this widget's own adapter, live while its event is routed.
- if let Some(h) = host { unsafe { ui.set_focused_ptr(h) }; }
+ ui.claim_focus(host_id);
+ self.focused = true;
changed = true;
}
}
diff --git a/src/widget/model.rs b/src/widget/model.rs
index eb30922..bd1f03f 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -354,14 +354,9 @@ impl EventCtx<'_> {
/// `focus()` / `unfocus()`, whose caller is the context) there is nothing to record.
pub fn request_focus(&mut self) {
let id = self.id;
- let Some(ui) = self.ui.as_deref_mut() else { return };
- if let Some(old) = ui.focused_widget.filter(|old| *old != id) {
- if let Some(ptr) = ui.tree.get_ptr(old) {
- // SAFETY: a registry-resolved live widget other than this one.
- unsafe { (*ptr).unfocus() };
- }
+ if let Some(ui) = self.ui.as_deref_mut() {
+ ui.claim_focus(id);
}
- ui.focused_widget = Some(id);
}
/// Drop this widget's hold on the window's focus, if it has it (MenuBar releases focus
diff --git a/src/widget/owned.rs b/src/widget/owned.rs
index f55f524..551b889 100644
--- a/src/widget/owned.rs
+++ b/src/widget/owned.rs
@@ -30,7 +30,9 @@
//! app.search.set_text("x"); // Deref: the widget's own methods
//! ```
+use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};
+use std::ptr::NonNull;
use super::core::Liveness;
use super::{ContextAction, CornerRadii, Event, FocusRole, LayoutConstraints, Point, Size, Widget, WidgetHost, WidgetId};
@@ -38,36 +40,72 @@ use crate::context::UiContext;
/// A widget in a heap allocation of its own, which the registry can point at however the
/// `Owned` is moved. See the module docs.
+///
+/// **The allocation is held as a raw pointer, not a `Box`** (since 2026-10-08). A `Box` is a
+/// unique pointer to the language: every reborrow of the widget through it — each
+/// `self.button.take_click()` an app makes — is a write through that unique pointer, which
+/// under Rust's aliasing model invalidates every raw pointer taken from an earlier borrow.
+/// The registry's pointer was one, so the next dispatch through it was undefined behaviour,
+/// every frame in every app (a compiler does not exploit it today; Miri reports it). Now the
+/// app's `Deref` and the registry both derive their references from the same raw root, and
+/// neither invalidates the other; what remains is the rule every `&mut` already obeys — one
+/// reached through the registry must not overlap one taken through the `Owned`, which a
+/// `UiContext` call that is handed the widget honours by re-deriving its pointer from what
+/// it is handed (`UiContext::set_focused`). `an_app_and_the_registry_take_turns_soundly` is
+/// the check, run under Miri in CI.
pub struct Owned<W: WidgetHost + 'static> {
- // Declared first so it is dropped first: the registry stops resolving the widget before
- // the widget's own `Drop` runs and the allocation is freed.
- live: Liveness,
- widget: Box<W>,
+ // Dropped in `Drop` before the allocation is freed: the registry stops resolving the
+ // widget before the widget's own `Drop` runs.
+ live: ManuallyDrop<Liveness>,
+ widget: NonNull<W>,
+ _owns: std::marker::PhantomData<W>,
}
+// What a `Box<W>` is: the `Owned` owns its `W` outright.
+unsafe impl<W: WidgetHost + Send + 'static> Send for Owned<W> {}
+unsafe impl<W: WidgetHost + Sync + 'static> Sync for Owned<W> {}
+
impl<W: WidgetHost + 'static> Owned<W> {
pub fn new(widget: W) -> Self {
- Owned { live: Liveness::new(), widget: Box::new(widget) }
+ let widget = Box::into_non_null(Box::new(widget));
+ Owned { live: ManuallyDrop::new(Liveness::new()), widget, _owns: std::marker::PhantomData }
}
/// The widget, by value; the allocation and its token go with the `Owned`.
pub fn into_inner(self) -> W {
- let Owned { live, widget } = self;
- drop(live);
- *widget
+ let mut this = ManuallyDrop::new(self);
+ // SAFETY: `this` is never used or dropped again; the token is dropped (and the
+ // registry stops resolving the widget) before the allocation is taken back.
+ unsafe {
+ ManuallyDrop::drop(&mut this.live);
+ *Box::from_raw(this.widget.as_ptr())
+ }
+ }
+}
+
+impl<W: WidgetHost + 'static> Drop for Owned<W> {
+ fn drop(&mut self) {
+ // SAFETY: dropped exactly once, here; the allocation was `Box`-made in `new` and is
+ // freed only here, after the token that lets the registry resolve it is gone.
+ unsafe {
+ ManuallyDrop::drop(&mut self.live);
+ drop(Box::from_raw(self.widget.as_ptr()));
+ }
}
}
impl<W: WidgetHost + 'static> Deref for Owned<W> {
type Target = W;
fn deref(&self) -> &W {
- &self.widget
+ // SAFETY: the allocation is live while `self` is, and holds a valid `W`.
+ unsafe { self.widget.as_ref() }
}
}
impl<W: WidgetHost + 'static> DerefMut for Owned<W> {
fn deref_mut(&mut self) -> &mut W {
- &mut self.widget
+ // SAFETY: as in `deref`; `&mut self` is the app's exclusive access.
+ unsafe { self.widget.as_mut() }
}
}
@@ -80,7 +118,7 @@ impl<W: WidgetHost + 'static> From<W> for Owned<W> {
/// A clone is a different widget in a different allocation, with a token of its own.
impl<W: WidgetHost + Clone + 'static> Clone for Owned<W> {
fn clone(&self) -> Self {
- Owned::new((*self.widget).clone())
+ Owned::new((**self).clone())
}
}
@@ -92,7 +130,7 @@ impl<W: WidgetHost + Default + 'static> Default for Owned<W> {
impl<W: WidgetHost + std::fmt::Debug + 'static> std::fmt::Debug for Owned<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_tuple("Owned").field(&*self.widget).finish()
+ f.debug_tuple("Owned").field(&**self).finish()
}
}
@@ -100,68 +138,69 @@ impl<W: WidgetHost + std::fmt::Debug + 'static> std::fmt::Debug for Owned<W> {
/// the one addition is [`stable_target`](WidgetHost::stable_target).
impl<W: WidgetHost + 'static> WidgetHost for Owned<W> {
fn stable_target(&mut self) -> Option<(*mut (dyn WidgetHost + 'static), std::sync::Weak<()>)> {
- let ptr: *mut (dyn WidgetHost + 'static) = &mut *self.widget as *mut W;
+ // The raw root itself, not a pointer taken from a reborrow (see the type's docs).
+ let ptr: *mut (dyn WidgetHost + 'static) = self.widget.as_ptr();
Some((ptr, self.live.watch()))
}
- fn base(&self) -> &Widget { self.widget.base() }
- fn base_mut(&mut self) -> &mut Widget { self.widget.base_mut() }
- fn preferred_height(&self) -> Option<f32> { self.widget.preferred_height() }
- fn label_strip(&self) -> f32 { self.widget.label_strip() }
- fn detached_label_rect(&self) -> Option<crate::scene::layout::Rect> { self.widget.detached_label_rect() }
- fn mark_dirty(&mut self, ctx: &mut UiContext) { self.widget.mark_dirty(ctx) }
- fn as_any(&self) -> &dyn std::any::Any { self.widget.as_any() }
- fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self.widget.as_any_mut() }
- fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool { self.widget.handle_event(event, ctx) }
- fn measure(&self, constraints: LayoutConstraints, ctx: &UiContext) -> Size { self.widget.measure(constraints, ctx) }
- fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) { self.widget.layout(origin, constraints, ctx) }
- fn rect(&self) -> (f32, f32, f32, f32) { self.widget.rect() }
- fn label(&self) -> Option<String> { self.widget.label() }
- fn context_action(&mut self, action: ContextAction) -> bool { self.widget.context_action(action) }
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.widget.set_rect(x, y, w, h) }
- fn set_row_rect(&mut self, x: f32, w: f32) { self.widget.set_row_rect(x, w) }
- fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool { self.widget.hit_test(px, py, ctx) }
- fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> { self.widget.highlight_quad(ctx) }
- fn color(&self) -> [f32; 4] { self.widget.color() }
- fn solid_border(&self) -> Option<([f32; 4], f32)> { self.widget.solid_border() }
- fn plate_bevel(&self) -> Option<f32> { self.widget.plate_bevel() }
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { self.widget.extra_quads() }
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> { self.widget.extra_arcs() }
- fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> { self.widget.extra_circles() }
- fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> { self.widget.all_quads(ctx) }
- fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) { self.widget.paint_self(ui, ctx) }
- fn clips_children(&self) -> bool { self.widget.clips_children() }
- fn renders_own_subtree(&self) -> bool { self.widget.renders_own_subtree() }
+ fn base(&self) -> &Widget { (**self).base() }
+ fn base_mut(&mut self) -> &mut Widget { (**self).base_mut() }
+ fn preferred_height(&self) -> Option<f32> { (**self).preferred_height() }
+ fn label_strip(&self) -> f32 { (**self).label_strip() }
+ fn detached_label_rect(&self) -> Option<crate::scene::layout::Rect> { (**self).detached_label_rect() }
+ fn mark_dirty(&mut self, ctx: &mut UiContext) { (**self).mark_dirty(ctx) }
+ fn as_any(&self) -> &dyn std::any::Any { (**self).as_any() }
+ fn as_any_mut(&mut self) -> &mut dyn std::any::Any { (**self).as_any_mut() }
+ fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool { (**self).handle_event(event, ctx) }
+ fn measure(&self, constraints: LayoutConstraints, ctx: &UiContext) -> Size { (**self).measure(constraints, ctx) }
+ fn layout(&mut self, origin: Point, constraints: LayoutConstraints, ctx: &mut UiContext) { (**self).layout(origin, constraints, ctx) }
+ fn rect(&self) -> (f32, f32, f32, f32) { (**self).rect() }
+ fn label(&self) -> Option<String> { (**self).label() }
+ fn context_action(&mut self, action: ContextAction) -> bool { (**self).context_action(action) }
+ fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { (**self).set_rect(x, y, w, h) }
+ fn set_row_rect(&mut self, x: f32, w: f32) { (**self).set_row_rect(x, w) }
+ fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool { (**self).hit_test(px, py, ctx) }
+ fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> { (**self).highlight_quad(ctx) }
+ fn color(&self) -> [f32; 4] { (**self).color() }
+ fn solid_border(&self) -> Option<([f32; 4], f32)> { (**self).solid_border() }
+ fn plate_bevel(&self) -> Option<f32> { (**self).plate_bevel() }
+ fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> { (**self).extra_quads() }
+ fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> { (**self).extra_arcs() }
+ fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> { (**self).extra_circles() }
+ fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> { (**self).all_quads(ctx) }
+ fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) { (**self).paint_self(ui, ctx) }
+ fn clips_children(&self) -> bool { (**self).clips_children() }
+ fn renders_own_subtree(&self) -> bool { (**self).renders_own_subtree() }
fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
- self.widget.all_rounded_quads(ctx)
- }
- fn widget_font(&self) -> Option<String> { self.widget.widget_font() }
- fn type_name(&self) -> &'static str { self.widget.type_name() }
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> { self.widget.popover_rect() }
- fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) { self.widget.render_popover(pc) }
- fn focus(&mut self) { self.widget.focus() }
- fn unfocus(&mut self) { self.widget.unfocus() }
- fn focused(&self, ctx: &UiContext) -> bool { self.widget.focused(ctx) }
- fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem) { self.widget.prepare_text(fs) }
- fn set_visible(&mut self, visible: bool) { self.widget.set_visible(visible) }
- fn visible(&self) -> bool { self.widget.visible() }
- fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool { self.widget.tick(dt, ctx) }
- fn wants_tick(&self) -> bool { self.widget.wants_tick() }
- fn is_child_visible(&self, child_id: WidgetId) -> bool { self.widget.is_child_visible(child_id) }
- fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) { self.widget.set_modifiers(ctrl, shift, alt) }
- fn z_index(&self) -> i32 { self.widget.z_index() }
- fn is_scrollable(&self) -> bool { self.widget.is_scrollable() }
- fn blocks_root_plate_drag(&self) -> bool { self.widget.blocks_root_plate_drag() }
- fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) { self.widget.corner_style() }
- fn focus_role(&self) -> FocusRole { self.widget.focus_role() }
- fn keeps_tab(&self) -> bool { self.widget.keeps_tab() }
- fn a11y_role(&self) -> Option<accesskit::Role> { self.widget.a11y_role() }
- fn a11y_value(&self) -> Option<String> { self.widget.a11y_value() }
- fn a11y_range(&self) -> Option<(f64, f64, f64)> { self.widget.a11y_range() }
- fn a11y_set_value(&mut self, value: f64) -> bool { self.widget.a11y_set_value(value) }
- fn a11y_items(&self) -> Vec<crate::a11y::A11yItem> { self.widget.a11y_items() }
- fn a11y_select_item(&mut self, idx: usize) -> bool { self.widget.a11y_select_item(idx) }
- fn corner_radii(&self) -> CornerRadii { self.widget.corner_radii() }
+ (**self).all_rounded_quads(ctx)
+ }
+ fn widget_font(&self) -> Option<String> { (**self).widget_font() }
+ fn type_name(&self) -> &'static str { (**self).type_name() }
+ fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> { (**self).popover_rect() }
+ fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) { (**self).render_popover(pc) }
+ fn focus(&mut self) { (**self).focus() }
+ fn unfocus(&mut self) { (**self).unfocus() }
+ fn focused(&self, ctx: &UiContext) -> bool { (**self).focused(ctx) }
+ fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem) { (**self).prepare_text(fs) }
+ fn set_visible(&mut self, visible: bool) { (**self).set_visible(visible) }
+ fn visible(&self) -> bool { (**self).visible() }
+ fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool { (**self).tick(dt, ctx) }
+ fn wants_tick(&self) -> bool { (**self).wants_tick() }
+ fn is_child_visible(&self, child_id: WidgetId) -> bool { (**self).is_child_visible(child_id) }
+ fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) { (**self).set_modifiers(ctrl, shift, alt) }
+ fn z_index(&self) -> i32 { (**self).z_index() }
+ fn is_scrollable(&self) -> bool { (**self).is_scrollable() }
+ fn blocks_root_plate_drag(&self) -> bool { (**self).blocks_root_plate_drag() }
+ fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) { (**self).corner_style() }
+ fn focus_role(&self) -> FocusRole { (**self).focus_role() }
+ fn keeps_tab(&self) -> bool { (**self).keeps_tab() }
+ fn a11y_role(&self) -> Option<accesskit::Role> { (**self).a11y_role() }
+ fn a11y_value(&self) -> Option<String> { (**self).a11y_value() }
+ fn a11y_range(&self) -> Option<(f64, f64, f64)> { (**self).a11y_range() }
+ fn a11y_set_value(&mut self, value: f64) -> bool { (**self).a11y_set_value(value) }
+ fn a11y_items(&self) -> Vec<crate::a11y::A11yItem> { (**self).a11y_items() }
+ fn a11y_select_item(&mut self, idx: usize) -> bool { (**self).a11y_select_item(idx) }
+ fn corner_radii(&self) -> CornerRadii { (**self).corner_radii() }
}
#[cfg(test)]
@@ -190,6 +229,51 @@ mod tests {
Some(unsafe { (*p).as_any().downcast_ref::<Tag>().unwrap().n })
}
+ /// The app and the registry take turns on one widget, as every frame of every app does:
+ /// the app changes it through the `Owned`, the registry reads and writes it through its
+ /// pointer, and back. With the widget in a `Box`, each app access invalidated the
+ /// registry's pointer under Rust's aliasing rules; run under Miri (`miri` in CI), this is
+ /// the check that neither way in invalidates the other.
+ #[test]
+ fn an_app_and_the_registry_take_turns_soundly() {
+ let mut w = tag(0);
+ let id = w.base().id();
+ let mut tree = WidgetTree::new();
+ unsafe { tree.register(id, &mut w as &mut (dyn WidgetHost + 'static)) };
+ for i in 1..=4u32 {
+ w.n += 1; // the app, through `DerefMut`
+ let p = tree.get_ptr(id).unwrap();
+ // SAFETY: the registry resolved it; no app reference is live across this.
+ unsafe { (*p).as_any_mut().downcast_mut::<Tag>().unwrap().n += 10 };
+ assert_eq!(w.n, i * 11, "the app sees what the registry wrote");
+ assert_eq!(read(&tree, id), Some(w.n), "and the registry what the app wrote");
+ }
+ let moved = w; // moving the `Owned` moves no widget
+ assert_eq!(read(&tree, id), Some(44));
+ drop(moved);
+ assert_eq!(read(&tree, id), None, "dropped: the registry no longer resolves it");
+ }
+
+ /// A registration through a pointer to the boxed widget itself — what a widget mid-event
+ /// hands over to open its context menu or take focus — keeps the box's root: that
+ /// pointer is a reborrow the next app access invalidates. The app and the registry then
+ /// still take turns soundly (under Miri, the check).
+ #[test]
+ fn an_inner_registration_keeps_the_root() {
+ let mut w = tag(1);
+ let id = w.base().id();
+ let mut tree = WidgetTree::new();
+ unsafe { tree.register(id, &mut w as &mut (dyn WidgetHost + 'static)) };
+ let root = tree.get_ptr(id).unwrap();
+ let inner: &mut Tag = &mut w;
+ unsafe { tree.register(id, inner as &mut (dyn WidgetHost + 'static)) };
+ assert!(std::ptr::addr_eq(tree.get_ptr(id).unwrap(), root), "the root is kept");
+ for i in 2..=4u32 {
+ w.n = i; // the app
+ assert_eq!(read(&tree, id), Some(i), "the registry, through the kept root");
+ }
+ }
+
#[test]
fn an_owned_widget_survives_its_vec_reallocating() {
// The hole `Owned` closes: rows registered, then the Vec grows and moves them.
@@ -232,7 +316,7 @@ mod tests {
owned.set_rect(1.0, 2.0, 3.0, 4.0);
assert_eq!(WidgetHost::rect(&*owned), (1.0, 2.0, 3.0, 4.0));
assert_eq!(WidgetHost::rect(&owned), (1.0, 2.0, 3.0, 4.0));
- assert_eq!(owned.base().id(), owned.widget.base().id());
+ assert_eq!(owned.base().id(), unsafe { owned.widget.as_ref() }.base().id());
assert!(owned.as_any().downcast_ref::<Tag>().is_some(), "as_any reaches the widget");
let copy = owned.clone();
let (a, _) = owned.stable_target().unwrap();