widget.rs
raw
//! The retained widget tree: the Widget trait, the arena that owns widgets,
//! and its integration with taffy layout and fragment painting.
//!
//! The tree keeps three parallel structures in sync: the widget arena
//! (behavior and visual state), a taffy tree (layout styles and computed
//! geometry), and per-frame fragments (paint output). Widgets never hold
//! references to each other; all relationships go through [`WidgetId`].
pub mod button;
pub mod checkbox;
pub mod combo_box;
mod compare;
pub mod container;
pub mod dialog;
pub mod dynamic;
pub mod expander;
pub mod grouping;
pub mod image;
pub mod indicator;
pub mod menu;
pub mod overlay;
pub mod scroll_area;
pub mod slider;
pub mod stepper;
pub mod text;
pub mod text_input;
pub mod tooltip;
pub use button::Button;
pub use checkbox::{Checkbox, ListItem, Radio, Switch, Tab};
pub use combo_box::ComboBox;
pub use container::Container;
pub use dialog::{Dialog, DialogScrim, DialogSheet};
pub use expander::Expander;
pub use grouping::{ListBox, TabList};
pub use image::Image;
pub use indicator::{Progress, Separator};
pub use menu::{ContextMenu, Dropdown, Menu, MenuBar, MenuItem, MenuNav, MenuPanel, MenuSeparator};
pub use scroll_area::{ScrollArea, ScrollAxes};
pub use slider::Slider;
pub use stepper::Stepper;
pub use text::{RichText, Text, TextSpan};
pub use text_input::TextInput;
pub use tooltip::TooltipPanel;
use std::any::Any;
use std::cell::RefCell;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use guiduck_scene::geom::{Affine, Point, Rect, Size};
use guiduck_scene::{Fragment, FragmentId, FragmentStore};
use slotmap::{SlotMap, new_key_type};
use taffy::{AvailableSpace, TaffyTree};
use crate::content::ContentBuilder;
use crate::dirty::Dirt;
use crate::text::TextContext;
new_key_type! {
/// Identifier of a widget within a [`WidgetTree`].
pub struct WidgetId;
}
/// Store a freshly made selection as the primary selection, the Wayland/X11
/// convention that middle-click then pastes. One definition for every widget
/// that has a selection — a text input and a static label alike — so the
/// convention cannot come to mean two things. A collapsed (empty) selection
/// leaves the primary alone.
pub(crate) fn sync_primary_selection(
clipboard: &mut dyn crate::clipboard::Clipboard,
selected: Option<&str>,
) {
if let Some(selected) = selected.filter(|s| !s.is_empty()) {
clipboard.set_text(crate::clipboard::Selection::Primary, selected);
}
}
/// Pointer cursor shapes, in the framework's own vocabulary (the platform
/// shell maps them to winit's icons).
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum CursorShape {
#[default]
Default,
/// The hand — interactive controls.
Pointer,
/// The I-beam — editable text.
Text,
}
/// A node in the retained tree.
///
/// Layout style lives in taffy (set via [`WidgetTree::insert`] /
/// [`WidgetTree::set_style`]); widgets carry only their content and visual
/// properties.
pub trait Widget: Any {
/// Content-based size for leaf widgets (text, images). taffy calls this
/// only for widgets without children; the default reports no intrinsic
/// content.
fn measure(
&mut self,
_text: &mut TextContext,
_known: taffy::Size<Option<f32>>,
_available: taffy::Size<AvailableSpace>,
) -> taffy::Size<f32> {
taffy::Size::ZERO
}
/// Called after layout with the widget's final size and the extent of
/// its (possibly overflowing) content, before painting. Widgets whose
/// content depends on their size (text re-wrapping to the assigned
/// width) finish that work here; scroll containers learn their
/// scrollable range from `content_size`.
fn finalize_layout(&mut self, _text: &mut TextContext, _size: Size, _content_size: Size) {}
/// Translation applied to this widget's children beyond their layout
/// positions — the scroll seam. The paint walk, hit testing, and
/// absolute-origin computation all apply this one value, so pixels and
/// pointer targets can never disagree.
fn content_offset(&self) -> guiduck_scene::geom::Vec2 {
guiduck_scene::geom::Vec2::ZERO
}
/// Whether children paint clipped to this widget's bounds (scroll
/// containers).
fn clips_content(&self) -> bool {
false
}
/// Handle scroll input (wheel/touchpad) hitting this widget or a
/// descendant. Return true if the widget consumed it (moved); an
/// unconsumed scroll bubbles to ancestor scrollables.
fn on_scroll(&mut self, _delta: guiduck_scene::geom::Vec2) -> bool {
false
}
/// The sub-rectangle of this widget (in its own local coordinates) that
/// should be kept on screen when the widget is focused or edited — a text
/// input's caret. Widgets with no such point return `None`, and the tree
/// keeps their whole box in view instead. Read after layout, so the
/// geometry is current.
fn reveal_rect(&self) -> Option<Rect> {
None
}
/// If this widget scrolls, bring `target` (a rectangle in window
/// coordinates) into view by adjusting the scroll offset, and return the
/// offset change applied. `viewport_top_left` is the widget's own window
/// origin, supplied by the tree. A non-scrolling widget returns
/// [`Vec2::ZERO`](guiduck_scene::geom::Vec2) and the reveal passes through
/// it. The tree re-encodes a widget that reports a nonzero change, so this
/// need not mark dirt.
fn scroll_reveal(
&mut self,
_viewport_top_left: Point,
_target: Rect,
) -> guiduck_scene::geom::Vec2 {
guiduck_scene::geom::Vec2::ZERO
}
/// Adjust the taffy style at insert time with requirements the widget
/// itself owns (e.g. `overflow: scroll` for scroll containers), so
/// callers cannot forget them.
fn adjust_style(&self, _style: &mut taffy::Style) {}
/// Emit this widget's own display items. Children are composited by the
/// tree afterwards, on top.
fn paint(&mut self, _fragment: &mut Fragment, _size: Size) {}
/// Emit display items painted *over* the children (scrollbars). Most
/// widgets have none.
fn paint_overlay(&mut self, _fragment: &mut Fragment, _size: Size) {}
/// The widget's accessibility role.
fn role(&self) -> accesskit::Role {
accesskit::Role::GenericContainer
}
/// Populate accessibility properties beyond role and bounds (labels,
/// values, states).
fn accessibility(&self, _node: &mut accesskit::Node) {}
/// Full accessibility integration for widgets that contribute child
/// nodes (text runs with selection state). The default delegates to
/// [`accessibility`](Widget::accessibility); `next_id` allocates ids
/// from a range reserved for generated nodes, and implementations may
/// push child nodes into `update`.
fn accessibility_extended(
&mut self,
node: &mut accesskit::Node,
_update: &mut accesskit::TreeUpdate,
_next_id: &mut dyn FnMut() -> accesskit::NodeId,
_origin: Point,
_text: &mut TextContext,
) {
self.accessibility(node);
}
/// An assistive technology acted on a node this widget *generated* through
/// [`accessibility_extended`](Self::accessibility_extended) rather than on
/// the widget's own node.
///
/// The tree routes it here because a generated node has no widget id to
/// look up — and without this, a link a screen reader can see and announce
/// would be a link it cannot click, which is not accessibility. Widgets
/// that generate no nodes never see this; ones that do report the result
/// through [`take_emitted`](Self::take_emitted), like any other input.
fn accessibility_action(&mut self, _node: accesskit::NodeId, _action: accesskit::Action) {}
/// Take and reset the dirt accumulated by this widget's setters since
/// the last call. Widgets with setters keep a [`Dirt`] field their
/// setters mark; the classification (layout vs. paint) lives with each
/// setter, next to the property it belongs to.
fn take_dirt(&mut self) -> Dirt {
Dirt::CLEAN
}
/// Take the semantic events this widget emitted during the last
/// internal hook (e.g. [`EventData::Changed`] after a user edit). The
/// tree collects these on the same borrow that collects dirt and
/// dispatches them to external handlers. Programmatic setters must not
/// emit — only user input does, or bindings would loop.
fn take_emitted(&mut self) -> Vec<crate::event::EventData> {
Vec::new()
}
/// Take the widget's pending wake request: "call [`on_timer`] in this
/// long". One-shot, collected on the same borrow as dirt; a widget that
/// wants a steady beat (a blinking caret) re-requests from its
/// `on_timer`. The widget never sees a clock — the tree anchors the
/// duration when the platform supplies the current time, which is what
/// keeps timed behavior fully deterministic in tests.
///
/// [`on_timer`]: Self::on_timer
fn take_wake(&mut self) -> Option<std::time::Duration> {
None
}
/// A wake requested via [`take_wake`](Self::take_wake) has come due.
fn on_timer(&mut self) {}
/// Whether this widget distinguishes single from multi-clicks, so a press
/// on it (or a descendant) should accumulate into a burst and resolve as a
/// [`CountedClick`] rather than committing on the first release. A
/// paragraph with links returns true — a link must not be followed on the
/// first press of a double; a plain label returns false and its clicks are
/// never held. External `:on-counted-click` handlers opt a path in the same
/// way, so nothing pays the resolution latency unless it asked to.
///
/// [`CountedClick`]: crate::event::EventKind::CountedClick
fn handles_counted_click(&self) -> bool {
false
}
/// Whether a click on this widget is *its own* — a button or a checkbox —
/// so it does not also bubble to an enclosing container's `:on-click`. The
/// widget's own handlers still run; the click just stops there. Default
/// false: a plain container is transparent, so a click on a label inside a
/// clickable card still reaches the card.
fn consumes_click(&self) -> bool {
false
}
/// How this widget's popup opens, if it hosts one.
///
/// A menu, a dropdown, and a combo box all open the same panel; they
/// differ only in where it goes and how wide it is. Asking the widget is
/// what keeps the tree from carrying a list of which types can do this —
/// and is what lets an application's own widget host a popup, which was
/// the deferred half of making content-deferral universal.
fn popup(&self) -> Option<menu::Popup> {
None
}
/// Whether this widget builds its declared children on demand rather than
/// at mount — a menu, dropdown, dialog, or context menu, whose content
/// lives in a popup that exists only while it is open. The tree consults
/// this when mounting a node's children (through
/// [`realize_content`](WidgetTree::realize_content)): false, the default,
/// mounts them inline; true hands them to [`set_content`](Self::set_content)
/// to build when the widget opens.
///
/// It is a property of the widget, not of its place in a manifest, so an
/// application widget that hosts a popup opts in here exactly as a builtin
/// does — deferral is a universal capability, invisible to the `.gdw`.
fn defers_content(&self) -> bool {
false
}
/// Receive the builder for this widget's deferred children. The tree calls
/// this in place of mounting them inline, but only when
/// [`defers_content`](Self::defers_content) is true; the widget stores the
/// builder and runs it when it opens. The default drops it, which is why a
/// widget that does not defer need not implement it.
fn set_content(&mut self, _content: ContentBuilder) {}
/// The pointer cursor to show while this widget is hovered, with `local`
/// the pointer's position in the widget's own coordinates.
///
/// The position is a parameter rather than something the widget remembers
/// from `on_pointer`, because for a widget whose cursor varies *within* it
/// — a paragraph with links in it — remembering would mean tracking hover
/// a second time, in a second place, off a move the widget is not even
/// delivered unless it captured the pointer. A cursor is a pure function of
/// where the pointer is; saying so here is what keeps it from disagreeing
/// with the pixels.
fn cursor(&self, _local: Point) -> CursorShape {
CursorShape::Default
}
/// The widget's type name for theme rule matching (`container`, `text`).
fn type_name(&self) -> &'static str {
"widget"
}
/// Whether the widget participates in focus (click-to-focus and Tab
/// traversal).
fn focusable(&self) -> bool {
false
}
/// Whether Tab traversal stops here, and whether the focus invariant may
/// land here.
///
/// Defaults to [`focusable`](Self::focusable), because for a control the
/// two questions have one answer. They come apart for a container that
/// takes focus so it can be *operated* — a scroll area, which the
/// keyboard should scroll once you click it, but which has no business
/// interrupting a Tab walk between the controls inside it. The web makes
/// the same split (a scroll container is click-focusable, not tabbable).
fn tab_stop(&self) -> bool {
self.focusable()
}
/// Focus arrived or left.
fn on_focus_changed(&mut self, _focused: bool) {}
/// Handle a key while focused. Return true to consume it (unconsumed
/// Tab performs focus traversal; other unconsumed keys bubble to the
/// shell).
fn on_key(
&mut self,
_key: &crate::event::KeyInput,
_text: &mut TextContext,
_clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
false
}
/// Handle an input-method event while focused.
fn on_ime(&mut self, _ime: &crate::event::ImeInput, _text: &mut TextContext) {}
/// Handle pasted text while focused: the content of a clipboard read
/// that answered `Pending`, arriving after the gesture that asked for
/// it. Widgets that paste do the same thing here as on the `Ready`
/// path; everything else ignores it.
fn on_paste(&mut self, _pasted: &str, _text: &mut TextContext) {}
/// Internal pointer behavior (cursor placement, drag selection, primary
/// selection), with coordinates local to the widget. Return true to
/// consume the event and — for presses — capture subsequent moves until
/// release.
fn on_pointer(
&mut self,
_kind: crate::event::EventKind,
_event: &crate::event::PointerEvent,
_text: &mut TextContext,
_clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
false
}
/// Where the input method should place candidate windows, in widget-local
/// coordinates. Only meaningful for focused text-editing widgets.
fn ime_cursor_area(&self) -> Option<Rect> {
None
}
/// Apply a computed style by calling the widget's own setters for the
/// properties present. Setters compare-and-set, so re-applying an
/// unchanged style marks no dirt. Properties absent from the style are
/// left untouched (theme validation's base-coverage rule guarantees
/// state exits always have a base value to return to).
fn apply_style(&mut self, _style: &crate::style::ComputedStyle) {}
}
impl dyn Widget {
/// Downcast a widget to its concrete type.
pub fn downcast_ref<T: Widget>(&self) -> Option<&T> {
(self as &dyn Any).downcast_ref()
}
/// Downcast a widget to its concrete type, mutably.
pub fn downcast_mut<T: Widget>(&mut self) -> Option<&mut T> {
(self as &mut dyn Any).downcast_mut()
}
}
struct WidgetNode {
widget: Box<dyn Widget>,
parent: Option<WidgetId>,
children: Vec<WidgetId>,
taffy_id: taffy::NodeId,
handlers: Vec<crate::event::HandlerEntry>,
classes: Vec<String>,
/// `:tooltip "text"` — shown after the pointer rests here. Universal, so
/// it lives on the node rather than in any widget.
tooltip: Option<String>,
/// The widget's own enabled flag; a widget is *effectively* disabled
/// when it or any ancestor is disabled.
enabled: bool,
/// The widget's retained fragment: allocated at insert, re-encoded in
/// place when the widget is paint-dirty, freed at removal.
fragment: FragmentId,
/// The rectangle of the last completed layout, for detecting which
/// widgets a relayout actually moved or resized.
last_layout: Rect,
}
/// An erased widget setter: applies one value to one widget, recovering both
/// concrete types inside the closure so that neither appears in the
/// signature. This is what lets a binding be installed by code that cannot
/// name the widget it drives — [`WidgetTree::bind_dyn`] takes one, and
/// generated code bakes tables of them (a thunk per bindable property), the
/// way a component spec bakes its child factories.
///
/// A thunk whose widget or value does not have the type it expects applies
/// nothing, which is the erased spelling of the type mismatch that makes
/// [`Commands::mutate`] a no-op.
pub type SetterThunk = Rc<dyn Fn(&mut dyn Widget, Box<dyn Any>)>;
/// A deferred mutation of the tree, queued by effects and event handlers
/// (which cannot borrow the tree) and applied at the start of the next frame.
pub(crate) type Command = Box<dyn FnOnce(&mut WidgetTree)>;
/// Handle for queueing deferred tree mutations. Clones share one queue;
/// effects and event handlers capture a clone.
#[derive(Clone, Default)]
pub struct Commands {
queue: Rc<RefCell<Vec<Command>>>,
}
impl Commands {
pub fn push(&self, f: impl FnOnce(&mut WidgetTree) + 'static) {
self.queue.borrow_mut().push(Box::new(f));
}
/// Queue a typed mutation of one widget. A vanished widget or a type
/// mismatch makes the command a no-op.
pub fn mutate<T: Widget>(&self, id: WidgetId, f: impl FnOnce(&mut T) + 'static) {
self.mutate_dyn(id, move |widget| {
if let Some(widget) = widget.downcast_mut::<T>() {
f(widget);
}
});
}
/// Queue a mutation of one widget whose concrete type the caller cannot
/// name — the erased form of [`mutate`](Self::mutate), which is a thin
/// wrapper that moves the downcast inside `f`. A vanished widget makes
/// the command a no-op; the mutation collects dirt through the same
/// borrow guard as every other path.
pub fn mutate_dyn(&self, id: WidgetId, f: impl FnOnce(&mut dyn Widget) + 'static) {
self.push(move |tree| {
if let Some(mut widget) = tree.widget_dyn_mut(id) {
f(&mut *widget);
}
});
}
pub fn is_empty(&self) -> bool {
self.queue.borrow().is_empty()
}
fn take(&self) -> Vec<Command> {
std::mem::take(&mut self.queue.borrow_mut())
}
}
/// The retained tree of widgets with layout and paint passes.
pub struct WidgetTree {
nodes: SlotMap<WidgetId, WidgetNode>,
/// The retained scene: one fragment per widget, stable across frames.
fragments: FragmentStore,
taffy: TaffyTree<WidgetId>,
text: TextContext,
root: Option<WidgetId>,
/// Overlay layers above the base root, bottom to top.
pub(crate) overlays: Vec<overlay::Overlay>,
/// The composed frame: the base root plus each overlay at its position.
/// This is what `render_frame` returns; rebuilt only when layers change.
frame_fragment: FragmentId,
pub(crate) frame_dirty: bool,
commands: Commands,
tooltip: tooltip::TooltipState,
/// The dialogs currently on screen, reconciled each frame against their
/// `:open` bindings.
dialogs: Vec<dialog::OpenDialog>,
/// The open menu levels, innermost last. Focus-like state: which popups
/// are up, and which row of each the keyboard is on. It lives here rather
/// than in the widgets because every question it answers — is this a
/// sibling or a submenu, what are this panel's rows — is structural.
menu_stack: Vec<menu::OpenMenu>,
pub(crate) dirt: Vec<(WidgetId, Dirt)>,
/// The viewport of the last completed layout, if any.
laid_out: Option<Size>,
pub(crate) pointer: crate::event::PointerState,
/// The application's theme, layered over the always-present default.
app_theme: Option<crate::style::Theme>,
/// The resolved theme every widget styles against: the app theme over the
/// default (or just the default). Recomputed whenever the app theme
/// changes, so the style pass never rebuilds it per widget.
effective_theme: crate::style::Theme,
pub(crate) style_dirty: Vec<WidgetId>,
focus: Option<WidgetId>,
/// A widget to bring into view on the next frame, once layout has settled:
/// a newly focused control, or a text input whose caret just moved. The
/// frame scrolls its ancestors so its [`reveal_rect`](Widget::reveal_rect)
/// (or whole box) is visible.
pending_reveal: Option<WidgetId>,
/// Keyboard shortcuts, consulted only after the focused widget declines
/// a key. An owner means the shortcut belongs to that widget and goes
/// when it does — a menu's accelerator; `None` means it belongs to the
/// application and lasts as long as the tree.
pub(crate) shortcuts: Vec<(Option<WidgetId>, crate::event::Keystroke, Box<dyn FnMut()>)>,
/// The sources this tree takes its structure from — one per live mount.
pub(crate) live: Vec<Box<dyn crate::live::LiveReload>>,
/// Widget wake requests not yet anchored to a clock (the tree has no
/// clock of its own — the platform supplies `now`).
wake_requests: Vec<(WidgetId, std::time::Duration)>,
/// Anchored wake deadlines, one per widget (the latest request wins).
deadlines: Vec<(WidgetId, std::time::Instant)>,
/// Next id for generated accessibility nodes (text runs); the range
/// below 2^32 is disjoint from widget ids.
a11y_counter: u64,
/// Which widget generated each non-widget accessibility node, rebuilt with
/// the tree it describes. It is what lets an action on a generated node —
/// clicking a link inside a paragraph — reach the widget that owns it,
/// since a generated node has no widget id to decode.
a11y_owners: Vec<(accesskit::NodeId, WidgetId)>,
}
impl WidgetTree {
/// A tree using the platform's fonts.
pub fn new() -> Self {
Self::with_text_context(TextContext::new())
}
/// A tree with an explicit text context (e.g. [`TextContext::hermetic`]
/// for reproducible tests).
pub fn with_text_context(text: TextContext) -> Self {
let mut fragments = FragmentStore::new();
let frame_fragment = fragments.create();
Self {
nodes: SlotMap::with_key(),
fragments,
taffy: TaffyTree::new(),
text,
root: None,
overlays: Vec::new(),
frame_fragment,
frame_dirty: false,
commands: Commands::default(),
tooltip: tooltip::TooltipState::default(),
dialogs: Vec::new(),
menu_stack: Vec::new(),
dirt: Vec::new(),
laid_out: None,
pointer: crate::event::PointerState::default(),
app_theme: None,
effective_theme: crate::style::default_theme().clone(),
style_dirty: Vec::new(),
focus: None,
pending_reveal: None,
shortcuts: Vec::new(),
live: Vec::new(),
wake_requests: Vec::new(),
deadlines: Vec::new(),
a11y_counter: 1,
a11y_owners: Vec::new(),
}
}
/// Insert a widget. With `parent: None` the widget becomes the root,
/// which must not already exist; otherwise it is appended to the parent's
/// children.
pub fn insert(
&mut self,
widget: impl Widget,
style: taffy::Style,
parent: Option<WidgetId>,
) -> WidgetId {
self.insert_boxed(Box::new(widget), style, parent)
}
/// Insert a widget whose concrete type the caller cannot name — the
/// erased form of [`insert`](Self::insert), which is a thin wrapper that
/// boxes first. The interpreter builds app-registered widgets through a
/// factory thunk, so a `Box<dyn Widget>` is all it ever holds.
pub fn insert_boxed(
&mut self,
widget: Box<dyn Widget>,
mut style: taffy::Style,
parent: Option<WidgetId>,
) -> WidgetId {
if parent.is_none() {
// The root *is* the window, so it fills the viewport whatever it
// asked for. Any other size would be answering a question nothing
// posed — there is no surrounding box for the root to take a share
// of — and a shrink-to-content root is actively wrong twice over:
// it draws the application into a corner, and every `100%`
// beneath it resolves against that corner instead of the window.
style.size = taffy::Size {
width: taffy::prelude::percent(1.0_f32),
height: taffy::prelude::percent(1.0_f32),
};
}
let id = self.create_node_boxed(widget, style);
match parent {
Some(parent_id) => {
let parent_taffy = self.nodes[parent_id].taffy_id;
let taffy_id = self.nodes[id].taffy_id;
self.nodes[id].parent = Some(parent_id);
self.nodes[parent_id].children.push(id);
self.taffy
.add_child(parent_taffy, taffy_id)
.expect("both nodes exist");
}
None => {
assert!(self.root.is_none(), "tree already has a root");
self.root = Some(id);
self.frame_dirty = true;
}
}
// A structural change always needs relayout (hot reload inserts into
// an already-laid-out tree), and the parent's fragment must pick up
// the new child reference even if nothing else moves.
self.dirt.push((id, Dirt::LAYOUT));
if let Some(parent_id) = parent {
self.dirt.push((parent_id, Dirt::PAINT));
}
self.style_dirty.push(id);
id
}
/// Insert a widget at a specific position among the parent's children
/// (an [`insert`](Self::insert) appends). The seam dynamic regions
/// (`if`/`for` in `.gdc`) mount rows and branches through.
pub fn insert_at(
&mut self,
widget: impl Widget,
style: taffy::Style,
parent: WidgetId,
index: usize,
) -> WidgetId {
let id = self.create_node(widget, style);
let parent_taffy = self.nodes[parent].taffy_id;
let taffy_id = self.nodes[id].taffy_id;
self.nodes[id].parent = Some(parent);
self.nodes[parent].children.insert(index, id);
self.taffy
.insert_child_at_index(parent_taffy, index, taffy_id)
.expect("both nodes exist and the index is in range");
self.dirt.push((id, Dirt::LAYOUT));
self.dirt.push((parent, Dirt::PAINT));
self.style_dirty.push(id);
id
}
/// Move a child to a new position among its siblings (keyed-list
/// reorder). The subtree stays mounted; only the order changes.
pub fn move_child(&mut self, parent: WidgetId, child: WidgetId, index: usize) {
let current = self.nodes[parent]
.children
.iter()
.position(|c| *c == child)
.expect("child belongs to parent");
if current == index {
return;
}
self.nodes[parent].children.remove(current);
self.nodes[parent].children.insert(index, child);
let parent_taffy = self.nodes[parent].taffy_id;
let child_taffy = self.nodes[child].taffy_id;
self.taffy
.remove_child(parent_taffy, child_taffy)
.expect("both nodes exist");
self.taffy
.insert_child_at_index(parent_taffy, index, child_taffy)
.expect("both nodes exist and the index is in range");
self.dirt.push((parent, Dirt::LAYOUT));
}
/// The position of `child` among `parent`'s children.
pub fn child_index(&self, parent: WidgetId, child: WidgetId) -> Option<usize> {
self.nodes
.get(parent)?
.children
.iter()
.position(|c| *c == child)
}
/// Create a widget node with no attachment — the shared setup for
/// [`insert`](Self::insert) and overlay roots.
pub(crate) fn create_node(&mut self, widget: impl Widget, style: taffy::Style) -> WidgetId {
self.create_node_boxed(Box::new(widget), style)
}
/// [`create_node`](Self::create_node) with the widget already boxed.
pub(crate) fn create_node_boxed(
&mut self,
mut widget: Box<dyn Widget>,
mut style: taffy::Style,
) -> WidgetId {
widget.adjust_style(&mut style);
// A widget is built by construction plus setter calls, and setters mark
// dirt — but dirt marked before the widget was in the tree names
// nothing the tree can act on, and every caller here marks the new node
// `LAYOUT`-dirty, which subsumes it. Dropping it is what lets "build it,
// then set its properties" leave exactly the state a do-everything
// constructor left.
let _ = widget.take_dirt();
let taffy_id = self
.taffy
.new_leaf(style)
.expect("taffy node creation is infallible");
let fragment = self.fragments.create();
let id = self.nodes.insert(WidgetNode {
widget,
parent: None,
children: Vec::new(),
taffy_id,
handlers: Vec::new(),
classes: Vec::new(),
tooltip: None,
enabled: true,
fragment,
last_layout: Rect::ZERO,
});
// taffy consults node context only for leaves; every widget gets its
// id attached so content measurement works wherever it applies.
self.taffy
.set_node_context(taffy_id, Some(id))
.expect("node was just created");
id
}
/// Remove a widget and its whole subtree. The root may be removed and a
/// new one inserted afterwards (this is how hot reload swaps a mounted
/// component). Removing an overlay's root closes the overlay.
pub fn remove(&mut self, id: WidgetId) {
if self.is_overlay(id) {
self.close_overlay(id);
return;
}
self.remove_subtree(id);
}
/// The removal body, without the overlay-root indirection (which would
/// recurse — `close_overlay` calls this).
pub(crate) fn remove_subtree(&mut self, id: WidgetId) {
let Some(node) = self.nodes.get(id) else {
return;
};
let parent = node.parent;
self.remove_recursive(id);
match parent {
Some(parent_id) => {
if let Some(parent_node) = self.nodes.get_mut(parent_id) {
parent_node.children.retain(|c| *c != id);
self.dirt.push((parent_id, Dirt::LAYOUT));
}
}
None => {
if self.root == Some(id) {
self.root = None;
self.frame_dirty = true;
}
}
}
}
fn remove_recursive(&mut self, id: WidgetId) {
let Some(node) = self.nodes.remove(id) else {
return;
};
for child in node.children {
self.remove_recursive(child);
}
// Removing the taffy node also detaches it from its taffy parent.
let _ = self.taffy.remove(node.taffy_id);
self.fragments.remove(node.fragment);
// A shortcut this widget owns goes with it. Without this a dev
// reload would leave the previous subtree's accelerators registered,
// and since the first match wins they would shadow the new ones —
// the key you deleted from the file still firing the handler the file
// no longer names.
self.shortcuts.retain(|(owner, ..)| *owner != Some(id));
}
pub fn root(&self) -> Option<WidgetId> {
self.root
}
/// The base root — `root()` under a name that stays honest beside
/// overlay roots.
pub(crate) fn base_root(&self) -> Option<WidgetId> {
self.root
}
pub(crate) fn node_exists(&self, id: WidgetId) -> bool {
self.nodes.contains_key(id)
}
/// The widget's parent, or `None` for the root.
pub fn parent(&self, id: WidgetId) -> Option<WidgetId> {
self.nodes.get(id)?.parent
}
/// The widget's children, in layout order.
pub fn children(&self, id: WidgetId) -> &[WidgetId] {
self.nodes
.get(id)
.map(|n| n.children.as_slice())
.unwrap_or(&[])
}
/// Replace a widget's layout style.
pub fn set_style(&mut self, id: WidgetId, style: taffy::Style) {
let taffy_id = self.nodes[id].taffy_id;
self.taffy.set_style(taffy_id, style).expect("node exists");
}
/// Modify a widget's layout style in place — the seam for *patching*
/// (e.g. a parent overriding an instance root's flex-item properties)
/// without clobbering what `adjust_style` or earlier callers set.
pub fn update_style(&mut self, id: WidgetId, f: impl FnOnce(&mut taffy::Style)) {
let taffy_id = self.nodes[id].taffy_id;
let mut style = self.taffy.style(taffy_id).expect("node exists").clone();
f(&mut style);
self.taffy.set_style(taffy_id, style).expect("node exists");
self.dirt.push((id, Dirt::LAYOUT));
}
/// Access a widget by id and concrete type.
pub fn widget<T: Widget>(&self, id: WidgetId) -> Option<&T> {
self.nodes.get(id)?.widget.downcast_ref()
}
/// Borrow a widget mutably by id and concrete type. When the borrow
/// ends, any dirt the widget's setters marked is collected for the next
/// frame.
pub fn widget_mut<T: Widget>(&mut self, id: WidgetId) -> Option<WidgetMut<'_, T>> {
let dirt = &mut self.dirt;
let widget = self.nodes.get_mut(id)?.widget.downcast_mut()?;
Some(WidgetMut { id, widget, dirt })
}
/// Borrow a widget mutably without naming its concrete type: the path
/// for code that must drive a widget it has no compile-time knowledge of
/// (the hot-reload interpreter, and the setter thunks generated for
/// app-registered widgets). Dirt is collected when the borrow ends
/// exactly as it is for [`widget_mut`](Self::widget_mut) — the guard is
/// the same one.
pub fn widget_dyn_mut(&mut self, id: WidgetId) -> Option<WidgetMut<'_, dyn Widget>> {
let dirt = &mut self.dirt;
let widget: &mut dyn Widget = self.nodes.get_mut(id)?.widget.as_mut();
Some(WidgetMut { id, widget, dirt })
}
/// Whether the widget at `id` defers its content (builds its children on
/// open rather than at mount). A missing id defers nothing.
pub fn defers_content(&self, id: WidgetId) -> bool {
self.nodes
.get(id)
.is_some_and(|node| node.widget.defers_content())
}
/// Mount a node's declared children. Every widget with children is handed
/// its content builder here, and the widget decides — through
/// [`Widget::defers_content`] — when it runs: a widget that mounts inline
/// builds it now, into itself; one that defers (a menu, dropdown, dialog)
/// stores it and builds it when it opens. One rule for both, so a builtin
/// popup and an application one that hosts content are mounted by the same
/// code.
pub fn realize_content(&mut self, id: WidgetId, content: ContentBuilder) {
if self.defers_content(id) {
if let Some(mut widget) = self.widget_dyn_mut(id) {
widget.set_content(content);
}
} else {
content.build(self, id);
}
}
/// The shared text resources.
pub fn text_context_mut(&mut self) -> &mut TextContext {
&mut self.text
}
pub(crate) fn handlers_mut(
&mut self,
id: WidgetId,
) -> Option<&mut Vec<crate::event::HandlerEntry>> {
self.nodes.get_mut(id).map(|n| &mut n.handlers)
}
/// Compute layout for the whole tree within a viewport of the given
/// logical size, then let widgets finalize their content at their
/// assigned sizes.
pub fn compute_layout(&mut self, viewport: Size) {
let roots: Vec<WidgetId> = self
.root
.into_iter()
.chain(self.overlays.iter().map(|o| o.root))
.collect();
for root in roots {
let root_taffy = self.nodes[root].taffy_id;
let nodes = &mut self.nodes;
let text = &mut self.text;
self.taffy
.compute_layout_with_measure(
root_taffy,
taffy::Size {
width: AvailableSpace::Definite(viewport.width as f32),
height: AvailableSpace::Definite(viewport.height as f32),
},
|known, available, _taffy_id, ctx, _style| match ctx {
Some(id) => match nodes.get_mut(*id) {
Some(node) => node.widget.measure(text, known, available),
None => taffy::Size::ZERO,
},
None => taffy::Size::ZERO,
},
)
.expect("root node exists");
self.finalize_recursive(root);
}
}
fn finalize_recursive(&mut self, id: WidgetId) {
let size = self.layout(id).size();
let content_size = self.content_size(id);
let node = &mut self.nodes[id];
node.widget
.finalize_layout(&mut self.text, size, content_size);
// Finalizing can change what a widget draws — a scroll area clamps its
// offset to content that just shrank out from under it. That dirt is
// raised after the frame's set was taken, so it is collected here and
// folded into this frame's re-encode set; left in the widget it would
// wait for a frame nobody is going to schedule.
let dirt = node.widget.take_dirt();
if !dirt.is_clean() {
self.dirt.push((id, dirt));
}
let node = &mut self.nodes[id];
let children = node.children.clone();
for child in children {
self.finalize_recursive(child);
}
}
/// The extent of a widget's laid-out content, which can exceed its own
/// size when children overflow (the scrollable range's source).
pub fn content_size(&self, id: WidgetId) -> Size {
let layout = self
.taffy
.layout(self.nodes[id].taffy_id)
.expect("node exists");
Size::new(
layout.content_size.width as f64,
layout.content_size.height as f64,
)
}
/// The widget's scroll shift for its children, [`Vec2::ZERO`] for
/// non-scrolling widgets.
pub(crate) fn content_offset(&self, id: WidgetId) -> guiduck_scene::geom::Vec2 {
self.nodes
.get(id)
.map(|n| n.widget.content_offset())
.unwrap_or_default()
}
/// The widget's laid-out rectangle in its parent's coordinate space.
/// Valid after [`compute_layout`](Self::compute_layout).
pub fn layout(&self, id: WidgetId) -> Rect {
let layout = self
.taffy
.layout(self.nodes[id].taffy_id)
.expect("node exists");
Rect::new(
layout.location.x as f64,
layout.location.y as f64,
(layout.location.x + layout.size.width) as f64,
(layout.location.y + layout.size.height) as f64,
)
}
/// Paint the whole tree from scratch into an external `store`, ignoring
/// the retained fragments. This is the correctness oracle: the retained
/// scene must always be structurally identical to what this produces
/// (a differential test holds the two together).
pub fn paint_fresh(&mut self, store: &mut FragmentStore) -> Option<FragmentId> {
if self.root.is_none() && self.overlays.is_empty() {
return None;
}
let mut frame = Fragment::new();
if let Some(root) = self.root {
let fragment = self.paint_fresh_recursive(root, store);
frame.child(Affine::IDENTITY, fragment);
}
for index in 0..self.overlays.len() {
let (root, position) = {
let overlay = &self.overlays[index];
(overlay.root, overlay.position)
};
let fragment = self.paint_fresh_recursive(root, store);
frame.child(Affine::translate(position.to_vec2()), fragment);
}
Some(store.insert(frame))
}
fn paint_fresh_recursive(&mut self, id: WidgetId, store: &mut FragmentStore) -> FragmentId {
let mut fragment = Fragment::new();
self.encode_into(id, &mut fragment, &mut |tree: &mut Self, child| {
tree.paint_fresh_recursive(child, store)
});
store.insert(fragment)
}
/// Re-encode one widget's retained fragment in place: its own painting,
/// child references by stable id, overlay. Children's fragments are not
/// touched — that is the point.
fn encode_node(&mut self, id: WidgetId) {
let mut fragment = Fragment::new();
self.encode_into(id, &mut fragment, &mut |tree: &mut Self, child| {
tree.nodes[child].fragment
});
let slot = self.nodes[id].fragment;
if let Some(retained) = self.fragments.get_mut(slot) {
*retained = fragment;
}
}
/// One widget's display items, with children resolved by `child_ref` —
/// the single encoding of a widget's paint order, shared by the
/// retained path and the from-scratch oracle. Scroll containers shift
/// children by their content offset and clip them to their bounds; the
/// Child items sit inside the clip because children are inline in paint
/// order.
fn encode_into(
&mut self,
id: WidgetId,
fragment: &mut Fragment,
child_ref: &mut dyn FnMut(&mut Self, WidgetId) -> FragmentId,
) {
let size = self.layout(id).size();
self.nodes[id].widget.paint(fragment, size);
let children = self.nodes[id].children.clone();
let content_offset = self.nodes[id].widget.content_offset();
let clips = self.nodes[id].widget.clips_content();
if clips {
fragment.push_clip(Rect::from_origin_size((0.0, 0.0), size));
}
for child in children {
let origin = self.layout(child).origin() + content_offset;
let child_fragment = child_ref(self, child);
fragment.child(Affine::translate(origin.to_vec2()), child_fragment);
}
if clips {
fragment.pop_clip();
}
self.nodes[id].widget.paint_overlay(fragment, size);
}
/// The retained scene. Valid after [`render_frame`](Self::render_frame);
/// backends read it, tests compare it.
pub fn fragments(&self) -> &FragmentStore {
&self.fragments
}
/// Build the accessibility tree: every widget contributes a node with
/// its role and absolute bounds, wrapped in a window root. Text-editing
/// widgets add text-run child nodes (with selection state) through
/// [`Widget::accessibility_extended`].
pub fn accessibility_tree(&mut self) -> accesskit::TreeUpdate {
const WINDOW: accesskit::NodeId = accesskit::NodeId(u64::MAX);
let mut update = accesskit::TreeUpdate {
nodes: Vec::with_capacity(self.nodes.len() + 1),
tree: Some(accesskit::Tree::new(WINDOW)),
tree_id: accesskit::TreeId::ROOT,
focus: self.focus.map(access_id).unwrap_or(WINDOW),
};
let mut window = accesskit::Node::new(accesskit::Role::Window);
// Generated node ids (text runs) live below the slotmap key range
// (whose ffi encoding is always >= 2^32); parley keeps run ids
// stable across updates, so the counter persists on the tree.
let mut counter = self.a11y_counter;
// The ownership of generated nodes is rebuilt with the tree that
// states them, so it can never outlive what it describes.
self.a11y_owners.clear();
let mut children = Vec::new();
if let Some(root) = self.root {
children.push(access_id(root));
self.access_recursive(root, Point::ORIGIN, &mut update, &mut counter);
}
for index in 0..self.overlays.len() {
let (root, position, modal) = {
let overlay = &self.overlays[index];
(overlay.root, overlay.position, overlay.modal)
};
children.push(access_id(root));
self.access_recursive(root, position, &mut update, &mut counter);
if modal
&& let Some((_, node)) = update
.nodes
.iter_mut()
.find(|(id, _)| *id == access_id(root))
{
node.set_role(accesskit::Role::Dialog);
node.set_modal();
}
}
window.set_children(children);
self.a11y_counter = counter;
update.nodes.push((WINDOW, window));
update
}
/// Perform an accessibility action requested by an assistive technology.
pub fn accessibility_action(&mut self, request: &accesskit::ActionRequest) {
use slotmap::KeyData;
// A node a widget generated is not a widget: it belongs to one, and
// only that widget knows what acting on it means. This is checked
// first because the generated range and the widget-id range are
// disjoint, so a hit here settles the question.
if let Some(owner) = self
.a11y_owners
.iter()
.find(|(node_id, _)| *node_id == request.target_node)
.map(|(_, owner)| *owner)
{
let (node, action) = (request.target_node, request.action);
self.with_widget_and_text(owner, |widget, _| widget.accessibility_action(node, action));
return;
}
let id = WidgetId::from(KeyData::from_ffi(request.target_node.0));
if !self.nodes.contains_key(id) {
return;
}
match request.action {
accesskit::Action::Click => {
// Synthesize a click at the widget's center, through the
// ordinary dispatch path (capture, bubble, handlers).
let origin = self.absolute_origin(id);
let size = self.layout(id).size();
let center = Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0);
self.dispatch_click_to(id, center);
}
accesskit::Action::Focus => self.set_focus(Some(id)),
_ => {}
}
}
fn access_recursive(
&mut self,
id: WidgetId,
parent_origin: Point,
update: &mut accesskit::TreeUpdate,
counter: &mut u64,
) {
let Some(node) = self.nodes.get(id) else {
return;
};
let local = self.layout(id);
let origin = parent_origin + local.origin().to_vec2();
let clickable = node
.handlers
.iter()
.any(|h| h.kind == crate::event::EventKind::Click);
// A generic container wired for clicks is, semantically, a button.
let role = match node.widget.role() {
accesskit::Role::GenericContainer if clickable => accesskit::Role::Button,
role => role,
};
let children = node.children.clone();
let mut access = accesskit::Node::new(role);
access.set_bounds(accesskit::Rect::new(
origin.x,
origin.y,
origin.x + local.width(),
origin.y + local.height(),
));
if clickable {
access.add_action(accesskit::Action::Click);
}
if self.effectively_disabled(id) {
access.set_disabled();
}
// Whatever the extended hook pushes into the update is this widget's
// doing, so claiming exactly the window it wrote records the ownership
// without the widget having to declare it — and without depending on
// `next_id` being called, which a hook reusing an id it minted on an
// earlier pass (as parley's does) never is.
let generated_from = update.nodes.len();
{
let node = self.nodes.get_mut(id).expect("checked above");
if node.widget.focusable() {
access.add_action(accesskit::Action::Focus);
}
let mut next_id = || {
let id = accesskit::NodeId(*counter);
*counter += 1;
id
};
node.widget.accessibility_extended(
&mut access,
update,
&mut next_id,
origin,
&mut self.text,
);
}
let generated: Vec<accesskit::NodeId> = update.nodes[generated_from..]
.iter()
.map(|(node_id, _)| *node_id)
.collect();
self.a11y_owners
.extend(generated.into_iter().map(|node_id| (node_id, id)));
// Extended hooks may have attached generated children (text runs);
// widget children follow them.
let mut merged = access.children().to_vec();
merged.extend(children.iter().map(|c| access_id(*c)));
access.set_children(merged);
update.nodes.push((access_id(id), access));
for child in children {
self.access_recursive(child, origin, update, counter);
}
}
}
impl WidgetTree {
/// The shared command queue, for effects and handlers that outlive a
/// borrow of the tree.
/// Every `dialog` in the tree.
pub(crate) fn dialog_ids(&self) -> Vec<WidgetId> {
self.nodes
.iter()
.filter(|(id, _)| self.widget::<dialog::Dialog>(*id).is_some())
.map(|(id, _)| id)
.collect()
}
pub fn commands(&self) -> Commands {
self.commands.clone()
}
/// Set (or clear) the application theme. It is layered over the
/// always-present default theme (the app's rules win); the resolved
/// theme is recomputed here so the style pass never rebuilds it. All
/// widgets restyle on the next frame.
pub fn set_theme(&mut self, theme: Option<crate::style::Theme>) {
self.effective_theme = match &theme {
Some(app) => app.clone().over(crate::style::default_theme()),
None => crate::style::default_theme().clone(),
};
self.app_theme = theme;
self.style_dirty.extend(self.nodes.keys());
}
/// The application theme, if one was set (the default theme underneath is
/// always present but not returned here).
pub fn theme(&self) -> Option<&crate::style::Theme> {
self.app_theme.as_ref()
}
/// Replace a widget's style classes (space-separated in `.gdc`,
/// individual strings here).
pub fn set_classes(&mut self, id: WidgetId, classes: impl IntoIterator<Item = String>) {
if let Some(node) = self.nodes.get_mut(id) {
node.classes = classes.into_iter().collect();
self.style_dirty.push(id);
}
}
/// Append classes to a widget's list (a parent adding to an instance
/// root's own classes, say) rather than replacing it.
pub fn add_classes(&mut self, id: WidgetId, classes: impl IntoIterator<Item = String>) {
if let Some(node) = self.nodes.get_mut(id) {
node.classes.extend(classes);
self.style_dirty.push(id);
}
}
pub fn classes(&self, id: WidgetId) -> &[String] {
self.nodes
.get(id)
.map(|n| n.classes.as_slice())
.unwrap_or(&[])
}
/// Set a widget's enabled flag. Disabling makes the whole subtree
/// *effectively* disabled: inert to pointer events (while still
/// occluding what is beneath), unfocusable, and matching `:disabled`
/// theme rules.
pub fn set_enabled(&mut self, id: WidgetId, enabled: bool) {
let Some(node) = self.nodes.get_mut(id) else {
return;
};
if node.enabled == enabled {
return;
}
node.enabled = enabled;
// The effective state changed for the whole subtree; restyle it
// (visual-state delivery runs with or without a theme).
let mut stack = vec![id];
while let Some(current) = stack.pop() {
self.style_dirty.push(current);
stack.extend(self.children(current));
}
// If the focused widget just became disabled, the focus invariant
// moves focus on the next frame; events check live state meanwhile.
}
/// The widget's own enabled flag (ancestors not considered).
pub fn is_enabled(&self, id: WidgetId) -> bool {
self.nodes.get(id).is_none_or(|n| n.enabled)
}
/// Whether the widget or any of its ancestors is disabled.
pub fn effectively_disabled(&self, id: WidgetId) -> bool {
let mut current = Some(id);
while let Some(c) = current {
if !self.is_enabled(c) {
return true;
}
current = self.parent(c);
}
false
}
/// The nearest enabled widget at or above `id` — where pointer events
/// for a disabled target actually go (a disabled control occludes, but
/// its enabled container still hovers and clicks, exactly as if the
/// pointer were on the container's padding).
pub(crate) fn enabled_target(&self, id: WidgetId) -> Option<WidgetId> {
let mut current = Some(id);
while let Some(c) = current {
if !self.effectively_disabled(c) {
return Some(c);
}
current = self.parent(c);
}
None
}
/// The widget's current interaction state, derived from pointer
/// tracking: hovered if it is on the hover chain, active if it is the
/// pressed widget or one of its ancestors, disabled if it or an
/// ancestor is disabled.
pub fn interaction_state(&self, id: WidgetId) -> crate::style::InteractionState {
let hover = self.pointer.hover_chain.contains(&id);
let mut active = false;
let mut current = self.pointer.pressed;
while let Some(c) = current {
if c == id {
active = true;
break;
}
current = self.parent(c);
}
crate::style::InteractionState {
hover,
active,
focus: self.focus == Some(id),
disabled: self.effectively_disabled(id),
}
}
/// The cursor for the current hover target (the disabled-retargeting
/// in hover tracking already makes disabled controls answer Default).
///
/// The widget is asked in its own coordinates, so one that varies its
/// cursor internally — a paragraph with links in it — answers about the
/// place the pointer actually is.
pub fn cursor_shape(&self) -> CursorShape {
let Some(id) = self.pointer.hover_chain.last().copied() else {
return CursorShape::default();
};
let Some(position) = self.pointer.position else {
return CursorShape::default();
};
let local = position - self.absolute_origin(id).to_vec2();
self.with_widget(id, |w| w.cursor(local))
.unwrap_or_default()
}
/// The focused widget, if any.
pub fn focus(&self) -> Option<WidgetId> {
self.focus
}
/// Move focus, delivering blur/focus to the affected widgets.
pub fn set_focus(&mut self, target: Option<WidgetId>) {
let target = target.filter(|id| {
self.nodes
.get(*id)
.is_some_and(|node| node.widget.focusable())
&& !self.effectively_disabled(*id)
});
if self.focus == target {
return;
}
let lost = self.focus.take();
if let Some(old) = lost {
self.with_widget_and_text(old, |widget, _| widget.on_focus_changed(false));
self.style_dirty.push(old);
}
if let Some(new) = target {
self.with_widget_and_text(new, |widget, _| widget.on_focus_changed(true));
self.style_dirty.push(new);
// Bring the newly focused control into view — a Tab into a control
// scrolled out of a long form has the same need a moved caret does.
self.pending_reveal = Some(new);
}
self.focus = target;
// Report *after* `self.focus` is settled, so a handler that asks sees
// the new truth rather than the transition it is being told about.
if let Some(old) = lost {
self.dispatch_direct(
old,
crate::event::EventKind::FocusChange,
crate::event::EventData::FocusChanged(false),
);
}
if let Some(new) = target {
self.dispatch_direct(
new,
crate::event::EventKind::FocusChange,
crate::event::EventData::FocusChanged(true),
);
}
}
/// Drive one widget's focus from an application binding — the endpoint of
/// `:focused`.
///
/// Focus is exclusive and the tree owns it, so this is deliberately not
/// symmetric: `true` takes focus, and `false` gives it up *only if this
/// widget is the one holding it*. That asymmetry is what makes the
/// property order-independent. When focus moves between two widgets driven
/// by one piece of state, both bindings fire — one falling, one rising —
/// and nothing says which effect runs first; because a `false` on a widget
/// that is already unfocused does nothing, either order lands in the same
/// place. (The same shape as `:checked` across a radio group, for the same
/// reason: exclusivity is a fact about the app's data.)
pub fn set_focused(&mut self, id: WidgetId, focused: bool) {
if focused {
self.set_focus(Some(id));
} else if self.focus == Some(id) {
self.set_focus(None);
}
}
/// Move focus to the next (or previous) focusable widget in tree order,
/// cycling.
pub fn focus_step(&mut self, backwards: bool) {
let mut order = Vec::new();
if let Some(scope) = self.focus_scope_root() {
self.collect_focusable(scope, &mut order);
}
if order.is_empty() {
self.set_focus(None);
return;
}
let next = match self
.focus
.and_then(|f| order.iter().position(|id| *id == f))
{
Some(index) => {
let len = order.len();
if backwards {
order[(index + len - 1) % len]
} else {
order[(index + 1) % len]
}
}
None if backwards => *order.last().expect("nonempty"),
None => order[0],
};
self.set_focus(Some(next));
}
fn collect_focusable(&self, id: WidgetId, out: &mut Vec<WidgetId>) {
if let Some(node) = self.nodes.get(id) {
// A disabled widget takes its whole subtree out of traversal.
if !node.enabled {
return;
}
// Tab order, and the invariant's landing places: a widget that is
// only click-focusable is neither.
if node.widget.tab_stop() {
out.push(id);
}
for child in &node.children {
self.collect_focusable(*child, out);
}
}
}
/// Enforce the focus invariant: whenever the tree contains a focusable
/// widget, one of them has focus. Runs once per frame after commands are
/// drained, so startup, widget removal, and hot reload all converge on
/// a valid focus without per-mutation special cases. Without it,
/// keyboard and IME input silently goes nowhere — which real-desktop
/// testing showed is genuinely confusing (the IME's own UI keeps
/// working while the field stays blank).
fn ensure_focus(&mut self) {
if self.focus.is_some_and(|f| {
self.nodes.contains_key(f) && self.focus_admissible(f) && !self.effectively_disabled(f)
}) {
return;
}
// Focus on a removed widget clears silently (no blur to deliver);
// focus beneath a just-opened modal blurs normally through
// set_focus below.
if self.focus.is_some_and(|f| !self.nodes.contains_key(f)) {
self.focus = None;
}
let mut order = Vec::new();
if let Some(scope) = self.focus_scope_root() {
self.collect_focusable(scope, &mut order);
}
match order.first() {
Some(first) => self.set_focus(Some(*first)),
// A modal with no focusables (a confirmation box of plain
// click targets) must not leak keys to widgets beneath it.
None => self.set_focus(None),
}
}
/// Scroll ancestors so a pending target is visible, and report which
/// scroll containers moved so the frame can re-encode them. Runs after
/// layout, when both the target's geometry and every ancestor's are
/// settled.
///
/// The walk is in window coordinates: [`absolute_origin`](Self::absolute_origin)
/// already folds in every ancestor scroll offset, so translating the
/// target rectangle by the applied offset change after each container
/// keeps it correct through nested scrolling.
fn resolve_reveal(&mut self) -> Vec<WidgetId> {
let Some(target) = self.pending_reveal.take() else {
return Vec::new();
};
if !self.nodes.contains_key(target) {
return Vec::new();
}
let local = self.nodes[target]
.widget
.reveal_rect()
.unwrap_or_else(|| Rect::from_origin_size(Point::ORIGIN, self.layout(target).size()));
let origin = self.absolute_origin(target);
let mut rect = Rect::from_origin_size(local.origin() + origin.to_vec2(), local.size());
let mut moved = Vec::new();
let mut current = target;
while let Some(parent) = self.parent(current) {
let viewport = self.absolute_origin(parent);
let applied = self.nodes[parent].widget.scroll_reveal(viewport, rect);
if applied != guiduck_scene::geom::Vec2::ZERO {
// The container scrolled: its content — the target with it —
// shifted by the opposite of the offset change. Track that so
// an outer container reveals the target's new position.
rect = Rect::from_origin_size(rect.origin() - applied, rect.size());
moved.push(parent);
}
current = parent;
}
moved
}
/// Register an application-level keyboard shortcut. The routing order
/// is fixed: the focused widget sees every key first (so a text input's
/// Ctrl+C beats an app accelerator, and a bare-letter shortcut cannot
/// steal typing), then overlay dismissal (Escape), then shortcuts, then
/// Tab traversal.
pub fn on_shortcut(
&mut self,
keystroke: crate::event::Keystroke,
handler: impl FnMut() + 'static,
) {
self.shortcuts.push((None, keystroke, Box::new(handler)));
}
/// Dispatch a key: focused widget first, then its `:on-key` wires, then
/// overlay dismissal, then shortcuts, then Tab traversal. Returns true if
/// the tree used the key.
pub fn dispatch_key(
&mut self,
key: &crate::event::KeyInput,
clipboard: &mut dyn crate::clipboard::Clipboard,
) -> bool {
if !key.pressed {
return false;
}
if let Some(id) = self.focus {
let consumed = self
.with_widget_and_text(id, |widget, text| widget.on_key(key, text, clipboard))
.unwrap_or(false);
if consumed {
// A consumed key may have moved the caret; keep it in view.
self.pending_reveal = Some(id);
return true;
}
}
// The focused widget declined, so the key is offered to the
// application: that widget's own `:on-key` wire first, then its
// ancestors'. This sits *after* the widget's own handling — a text
// input's Ctrl+C is still the text input's — and *before* the overlay
// and shortcut layers, because a wire written on the focused widget is
// more specific than either. A handler consumes the key by consuming
// the event (`cx.consume()`).
//
// With nothing focused — a tree with no focusable widget at all, since
// the focus invariant covers every other case — the offer starts at
// the root, so a key wire on a container is never silently dead.
//
// A key with no canonical spelling is not offered: there would be
// nothing for a handler to compare against.
if let Some(target) = self.focus.or_else(|| self.root())
&& let Some(spelling) = key.spelling()
&& self.dispatch_bubbling(
target,
crate::event::EventKind::Key,
crate::event::EventData::Key(spelling),
)
{
return true;
}
// Escape on a dialog *reports*; it does not dismiss. Closing the
// overlay here would leave `:open` saying true while the sheet was
// gone, and the app's state is the truth. This runs before the
// overlay walk so a dialog's Escape never reaches it.
if key.key == crate::event::Key::Escape && self.request_close_topmost_dialog() {
return true;
}
if key.key == crate::event::Key::Escape && self.dismiss_for_escape() {
return true;
}
for (_, keystroke, handler) in &mut self.shortcuts {
if keystroke.matches(key) {
handler();
return true;
}
}
if key.key == crate::event::Key::Tab {
self.focus_step(key.modifiers.shift);
return true;
}
false
}
/// Forward pasted text to the focused widget: the delivery half of a
/// [`Clipboard::request_text`](crate::clipboard::Clipboard::request_text)
/// that answered `Pending`. Like IME input, the text goes to whatever
/// is focused when it arrives — the platform fetch takes long enough
/// that "the widget that asked" and "the widget being edited" can only
/// be told apart by a user who moved focus meanwhile, and input
/// follows focus.
pub fn dispatch_paste(&mut self, pasted: &str) {
if let Some(id) = self.focus {
self.with_widget_and_text(id, |widget, text| widget.on_paste(pasted, text));
// The paste moved the caret; keep it in view.
self.pending_reveal = Some(id);
}
}
/// Forward an input-method event to the focused widget.
pub fn dispatch_ime(&mut self, ime: &crate::event::ImeInput) {
if let Some(id) = self.focus {
self.with_widget_and_text(id, |widget, text| widget.on_ime(ime, text));
// A commit or preedit moves the caret; keep it in view.
self.pending_reveal = Some(id);
}
}
/// Where the input method should place its candidate window, in logical
/// window coordinates.
pub fn ime_cursor_area(&self) -> Option<Rect> {
let id = self.focus?;
let local = self.nodes.get(id)?.widget.ime_cursor_area()?;
let origin = self.absolute_origin(id);
Some(Rect::new(
local.x0 + origin.x,
local.y0 + origin.y,
local.x1 + origin.x,
local.y1 + origin.y,
))
}
/// Read-only access to one widget as a trait object.
pub(crate) fn with_widget<R>(
&self,
id: WidgetId,
f: impl FnOnce(&dyn Widget) -> R,
) -> Option<R> {
self.nodes.get(id).map(|node| f(node.widget.as_ref()))
}
/// Run `f` with mutable access to one widget and the shared text
/// context, collecting any dirt the widget marked. The single access
/// path for internal widget hooks (keys, IME, pointer, focus).
pub(crate) fn with_widget_and_text<R>(
&mut self,
id: WidgetId,
f: impl FnOnce(&mut dyn Widget, &mut TextContext) -> R,
) -> Option<R> {
let node = self.nodes.get_mut(id)?;
let result = f(node.widget.as_mut(), &mut self.text);
let dirt = node.widget.take_dirt();
let emitted = node.widget.take_emitted();
if let Some(wake) = node.widget.take_wake() {
self.wake_requests.push((id, wake));
}
if !dirt.is_clean() {
self.dirt.push((id, dirt));
}
// The widget borrow has ended; emitted semantic events dispatch to
// external handlers through the ordinary capture/bubble path.
for data in emitted {
let kind = match &data {
crate::event::EventData::Changed(_) => crate::event::EventKind::Changed,
crate::event::EventData::Toggled(_) => crate::event::EventKind::Toggled,
crate::event::EventData::ValueChanged(_) => crate::event::EventKind::ValueChanged,
crate::event::EventData::Scrolled { .. } => crate::event::EventKind::Scrolled,
// A link span reports its target and the tree dispatches it,
// like any other value a widget reports. Nothing here resolves
// the target: what it names is the application's business, and
// the wire is the whole of the framework's part.
crate::event::EventData::Link(_) => crate::event::EventKind::Link,
// Keyboard activation converts into a synthesized click at
// the widget's center — the path a11y Click already takes —
// so keyboard and pointer share the `:on-click` wire.
crate::event::EventData::Activated => {
let origin = self.absolute_origin(id);
let size = self.layout(id).size();
let center =
Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0);
self.dispatch_click_to(id, center);
continue;
}
// The menu family reports; the tree acts. Opening a popup
// needs the overlay stack, moving a highlight needs the
// panel's rows, and closing a stack needs the stack — none
// of which a widget can reach from inside its own borrow.
crate::event::EventData::MenuOpen => {
self.open_menu(id);
continue;
}
crate::event::EventData::MenuNav(nav) => {
self.menu_nav(id, *nav);
continue;
}
// A chosen row is one wire however it was chosen — pointer,
// Enter, or accelerator — as `Activated` is for a button.
crate::event::EventData::Selected => {
self.select_menu_row(id);
continue;
}
// A dialog's close request reaches handlers through the
// ordinary path; the tree does not act on it, because acting
// is the app's job — that is what controlled means.
crate::event::EventData::CloseRequested => crate::event::EventKind::Close,
// A manifest-declared event reaches handlers like any other
// semantic one; which event it is rides in the payload,
// because the kind enum is the closed vocabulary's.
crate::event::EventData::User { .. } => crate::event::EventKind::User,
// Widgets emit semantic events only; pointer and file-drop
// events always originate from the shell.
//
// Focus and unconsumed keys are the tree's to report, not a
// widget's: a widget cannot know it lost focus to someone else,
// and by the time a key is unconsumed the widget has already
// had it. Both dispatch from where that is known — `set_focus`
// and `dispatch_key` — so neither can arrive here.
crate::event::EventData::Pointer(_)
| crate::event::EventData::FileDrop(_)
| crate::event::EventData::FocusChanged(_)
| crate::event::EventData::Key(_) => {
continue;
}
};
self.dispatch_semantic(id, kind, data);
}
Some(result)
}
/// Re-resolve and apply computed styles for widgets whose style inputs
/// changed. Style application goes through ordinary setters, so the
/// resulting dirt is collected exactly like any other mutation.
fn style_pass(&mut self) {
let mut dirty = std::mem::take(&mut self.style_dirty);
dirty.sort_unstable();
dirty.dedup();
for id in dirty {
if !self.nodes.contains_key(id) {
continue;
}
let state = self.interaction_state(id);
// Every widget resolves against the always-present effective
// theme (the app theme over the default). Controls read their
// whole appearance — including state-driven chrome — from the
// computed style the theme's state selectors produce.
let node = &self.nodes[id];
let computed =
self.effective_theme
.resolve(node.widget.type_name(), &node.classes, state);
if computed.is_empty() {
continue;
}
let node = self.nodes.get_mut(id).expect("checked above");
node.widget.apply_style(&computed);
let dirt = node.widget.take_dirt();
if !dirt.is_clean() {
self.dirt.push((id, dirt));
}
}
}
/// Bind a widget property to reactive state: `compute` runs in an
/// effect (auto-tracking the signals it reads) and its result is applied
/// to the widget through `apply` — typically a setter, which classifies
/// the resulting dirt. The initial application happens on the next
/// frame.
///
/// A caller that cannot name `T` — a hot-reload interpreter, or code
/// generated for an app-registered widget — wants
/// [`bind_dyn`](Self::bind_dyn), which erases both types instead.
pub fn bind<T: Widget, V: 'static>(
&mut self,
id: WidgetId,
mut compute: impl FnMut() -> V + 'static,
apply: impl Fn(&mut T, V) + 'static,
) {
let commands = self.commands.clone();
let apply = Rc::new(apply);
// The Effect handle is intentionally dropped: the effect stays alive,
// owned by the current reactive scope.
guiduck_signals::Effect::new(move || {
let value = compute();
let apply = apply.clone();
commands.mutate::<T>(id, move |widget| apply(widget, value));
});
}
/// Bind a widget property to reactive state without naming the widget's
/// type: the erased form of [`bind`](Self::bind), for code that is
/// compiled before the widget it drives exists (the hot-reload
/// interpreter) or that is generated for an app-registered widget.
///
/// `compute` runs in an effect exactly as `bind`'s does, but yields its
/// value erased; `apply` is a [`SetterThunk`], which recovers both the
/// widget's type and the value's inside the closure. The resulting
/// mutation travels the same command queue and the same borrow guard, so
/// the dirt a setter marks is classified and collected identically.
pub fn bind_dyn(
&mut self,
id: WidgetId,
mut compute: impl FnMut() -> Box<dyn Any> + 'static,
apply: SetterThunk,
) {
let commands = self.commands.clone();
// As in `bind`, the Effect handle is intentionally dropped: the
// effect stays alive, owned by the current reactive scope.
guiduck_signals::Effect::new(move || {
let value = compute();
let apply = apply.clone();
commands.mutate_dyn(id, move |widget| apply(widget, value));
});
}
fn drain_commands(&mut self) {
loop {
let batch = self.commands.take();
if batch.is_empty() {
break;
}
for command in batch {
command(self);
}
}
}
/// Anchor pending wake requests against `now` and return the earliest
/// deadline, if any. The platform shell calls this after each frame to
/// decide between waiting indefinitely (no deadline — the zero-idle
/// guarantee) and waiting until the deadline; tests fabricate `now`.
pub fn next_wake(&mut self, now: std::time::Instant) -> Option<std::time::Instant> {
for (id, duration) in self.wake_requests.drain(..) {
let deadline = now + duration;
// The latest request from a widget replaces its earlier one
// (a reset blink phase supersedes the pending toggle).
self.deadlines.retain(|(existing, _)| *existing != id);
self.deadlines.push((id, deadline));
}
self.deadlines
.retain(|(id, _)| self.nodes.contains_key(*id));
// The tooltip's deadline is the tree's own, not a widget's: hovering
// is not something a widget is told about (enter and leave reach only
// external handlers), and a tooltip is the tree's policy anyway. It
// is anchored here, where the platform supplies `now` — the widget
// rule applied to the tree: say "in this long", never read a clock.
if self.tooltip.armed.is_some() && self.tooltip.overlay.is_none() {
self.tooltip.due.get_or_insert(now + tooltip::TOOLTIP_DELAY);
} else {
self.tooltip.due = None;
}
self.deadlines
.iter()
.map(|(_, at)| *at)
.chain(self.tooltip.due)
.chain(self.counted_click_deadline())
.min()
}
/// Deliver [`Widget::on_timer`] to every widget whose deadline has
/// passed. Dirt (and follow-up wake requests) are collected exactly as
/// for any other widget hook; the caller renders a frame afterwards if
/// [`needs_frame`](Self::needs_frame) says so.
pub fn tick(&mut self, now: std::time::Instant) {
let due: Vec<WidgetId> = self
.deadlines
.iter()
.filter(|(_, at)| *at <= now)
.map(|(id, _)| *id)
.collect();
self.deadlines.retain(|(_, at)| *at > now);
for id in due {
self.with_widget_and_text(id, |widget, _| widget.on_timer());
}
if self.tooltip.due.is_some_and(|at| at <= now) {
self.tooltip.due = None;
self.open_tooltip();
}
// A click burst whose window has closed settles now, with no clipboard
// (only Down-driven widgets — the text input — read it, and none of
// those handle CountedClick).
if self.counted_click_due(now) {
self.resolve_counted_click(&mut crate::clipboard::NoClipboard);
}
}
/// Whether anything requires rendering a new frame: pending effects,
/// queued commands, collected dirt, pending restyles, or no completed
/// layout yet.
pub fn needs_frame(&self) -> bool {
guiduck_signals::has_pending_effects()
|| !self.commands.is_empty()
|| !self.dirt.is_empty()
|| !self.style_dirty.is_empty()
|| self.laid_out.is_none()
|| self.frame_dirty
}
/// Produce a frame: flush reactive effects, apply queued mutations,
/// re-lay-out if anything layout-affecting changed (or the viewport
/// did), and paint.
///
/// This is the single frame pipeline; the platform shell calls it per
/// redraw, and headless tests call it directly.
pub fn render_frame(&mut self, viewport: Size) -> Option<FragmentId> {
guiduck_signals::flush_effects();
self.drain_commands();
// A dialog's `:open` is a binding, and a binding's effect lands in
// the command queue — so the flag is only true once those have
// settled. Reconciling here, after the drain, is what lets `:open`
// be the single truth rather than a setter that must remember to act.
self.sync_dialogs();
self.ensure_focus();
// Styles resolve after commands (which may have changed the theme or
// classes) and before layout, since style can affect text metrics.
self.style_pass();
let dirt = std::mem::take(&mut self.dirt);
let needs_layout = self.laid_out != Some(viewport) || dirt.iter().any(|(_, d)| d.layout);
if needs_layout {
for (id, d) in &dirt {
if d.layout
&& let Some(node) = self.nodes.get(*id)
{
self.taffy
.mark_dirty(node.taffy_id)
.expect("widget's taffy node exists");
}
}
self.compute_layout(viewport);
self.laid_out = Some(viewport);
} else {
// Paint-only changes keep their geometry, but the widget still
// gets to refresh size-dependent content (e.g. a text layout
// rebuilt with a new brush) before painting.
for (id, _) in &dirt {
if self.nodes.contains_key(*id) {
let size = self.layout(*id).size();
let content_size = self.content_size(*id);
if let Some(node) = self.nodes.get_mut(*id) {
node.widget
.finalize_layout(&mut self.text, size, content_size);
}
}
}
}
// Dirt raised *by* finalizing (a scroll area clamping its offset to
// content that shrank). It arrived after the frame's set was taken, so
// it is folded in here rather than left for a later frame: the change
// is already in the geometry this frame paints from, and no frame is
// scheduled to carry it. Consuming it is what keeps an idle
// application at zero frames.
let late: Vec<WidgetId> = std::mem::take(&mut self.dirt)
.into_iter()
.map(|(id, _)| id)
.filter(|id| self.nodes.contains_key(*id))
.collect();
// Bring a just-focused widget, or a moved caret, into view by
// scrolling its ancestors. Resolved here, after layout, because it
// reads settled geometry; the scroll containers it moves are
// re-encoded this frame alongside the dirty set.
let revealed = self.resolve_reveal();
// The re-encode set: paint-dirty widgets, plus — on layout frames —
// whatever the relayout actually moved or resized. A resized widget
// repaints itself; a moved widget's *parent* re-encodes, because
// child transforms live in the parent's fragment. Everything else
// keeps last frame's fragment untouched.
let mut encode: Vec<WidgetId> = dirt
.iter()
.map(|(id, _)| *id)
.filter(|id| self.nodes.contains_key(*id))
.chain(late)
.chain(revealed)
.collect();
if needs_layout {
let ids: Vec<WidgetId> = self.nodes.keys().collect();
for id in ids {
let rect = self.layout(id);
let node = &mut self.nodes[id];
let previous = node.last_layout;
node.last_layout = rect;
if rect == previous {
continue;
}
if rect.size() != previous.size() {
encode.push(id);
}
if rect.origin() != previous.origin()
&& let Some(parent) = self.nodes[id].parent
{
encode.push(parent);
}
}
}
encode.sort_unstable();
encode.dedup();
for id in encode {
if self.nodes.contains_key(id) {
self.encode_node(id);
}
}
// Overlay positions resolve against the settled layout (anchor
// rectangles are only now known), then the frame composes the base
// root and each overlay at its position. The frame fragment is
// rewritten only when layers appear, vanish, or move.
self.position_overlays(viewport);
if self.root.is_none() && self.overlays.is_empty() {
return None;
}
if self.frame_dirty {
let mut frame = Fragment::new();
if let Some(root) = self.root {
frame.child(Affine::IDENTITY, self.nodes[root].fragment);
}
for overlay in &self.overlays {
frame.child(
Affine::translate(overlay.position.to_vec2()),
self.nodes[overlay.root].fragment,
);
}
if let Some(retained) = self.fragments.get_mut(self.frame_fragment) {
*retained = frame;
}
self.frame_dirty = false;
}
Some(self.frame_fragment)
}
}
/// The dirt hand-off a [`WidgetMut`] performs when its borrow ends, spelled
/// as its own trait so that one guard — and so one copy of the rule that
/// governs dirt collection — serves both a statically typed borrow and an
/// erased one. The guard cannot simply ask for [`Widget`], because `dyn
/// Widget` does not implement it.
pub trait WidgetDirt {
/// Take the dirt the borrowed widget's setters marked. Both sides
/// forward to [`Widget::take_dirt`].
fn take_widget_dirt(&mut self) -> Dirt;
}
impl<T: Widget> WidgetDirt for T {
fn take_widget_dirt(&mut self) -> Dirt {
self.take_dirt()
}
}
impl WidgetDirt for dyn Widget {
fn take_widget_dirt(&mut self) -> Dirt {
self.take_dirt()
}
}
/// A mutable borrow of a widget that reports the dirt its setters marked
/// back to the tree when dropped. `T` is the concrete widget type for a
/// borrow through [`WidgetTree::widget_mut`], or `dyn Widget` for one
/// through [`WidgetTree::widget_dyn_mut`].
pub struct WidgetMut<'a, T: ?Sized + WidgetDirt> {
id: WidgetId,
widget: &'a mut T,
dirt: &'a mut Vec<(WidgetId, Dirt)>,
}
impl<T: ?Sized + WidgetDirt> Deref for WidgetMut<'_, T> {
type Target = T;
fn deref(&self) -> &T {
self.widget
}
}
impl<T: ?Sized + WidgetDirt> DerefMut for WidgetMut<'_, T> {
fn deref_mut(&mut self) -> &mut T {
self.widget
}
}
impl<T: ?Sized + WidgetDirt> Drop for WidgetMut<'_, T> {
fn drop(&mut self) {
let dirt = self.widget.take_widget_dirt();
if !dirt.is_clean() {
self.dirt.push((self.id, dirt));
}
}
}
impl Default for WidgetTree {
fn default() -> Self {
Self::new()
}
}
/// A std control's appearance tokens as the built-in fallback theme defines
/// them, in the resting interaction state.
///
/// Controls initialize their tokens from this rather than each carrying a
/// palette of literals in code, which keeps one definition per token: the
/// fallback theme states it, the default theme overrides it, an app theme
/// overrides that. The values here are never painted in practice — the style
/// pass runs before a widget's first paint and the default theme covers every
/// token — but they mean a control is fully dressed from the moment it is
/// constructed, with no ordering to get right.
pub(crate) fn fallback_style(widget_type: &str) -> crate::style::ComputedStyle {
crate::style::fallback_theme().resolve(
widget_type,
&[],
crate::style::InteractionState::default(),
)
}
/// Whether a brush is a fully transparent solid color — the signal a theme
/// uses to hide a token (a focus ring whose color the base rule leaves
/// transparent and the `:focus` rule makes opaque). Non-solid brushes are
/// treated as visible.
pub(crate) fn is_transparent(brush: &guiduck_scene::paint::Brush) -> bool {
matches!(brush, guiduck_scene::paint::Brush::Solid(c) if c.components[3] <= 0.0)
}
/// The accesskit id for a widget: slotmap keys are unique u64s, and the
/// window root uses `u64::MAX`, which no slotmap key takes.
fn access_id(id: WidgetId) -> accesskit::NodeId {
use slotmap::Key;
accesskit::NodeId(id.data().as_ffi())
}
#[cfg(test)]
mod tests;