//! 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>, _available: taffy::Size, ) -> taffy::Size { 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 { 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 { 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 { 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 { 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 { 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(&self) -> Option<&T> { (self as &dyn Any).downcast_ref() } /// Downcast a widget to its concrete type, mutably. pub fn downcast_mut(&mut self) -> Option<&mut T> { (self as &mut dyn Any).downcast_mut() } } struct WidgetNode { widget: Box, parent: Option, children: Vec, taffy_id: taffy::NodeId, handlers: Vec, classes: Vec, /// `:tooltip "text"` — shown after the pointer rests here. Universal, so /// it lives on the node rather than in any widget. tooltip: Option, /// 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)>; /// 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; /// 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>>, } 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(&self, id: WidgetId, f: impl FnOnce(&mut T) + 'static) { self.mutate_dyn(id, move |widget| { if let Some(widget) = widget.downcast_mut::() { 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 { std::mem::take(&mut self.queue.borrow_mut()) } } /// The retained tree of widgets with layout and paint passes. pub struct WidgetTree { nodes: SlotMap, /// The retained scene: one fragment per widget, stable across frames. fragments: FragmentStore, taffy: TaffyTree, text: TextContext, root: Option, /// Overlay layers above the base root, bottom to top. pub(crate) overlays: Vec, /// 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, /// 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, pub(crate) dirt: Vec<(WidgetId, Dirt)>, /// The viewport of the last completed layout, if any. laid_out: Option, pub(crate) pointer: crate::event::PointerState, /// The application's theme, layered over the always-present default. app_theme: Option, /// 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, focus: Option, /// 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, /// 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, crate::event::Keystroke, Box)>, /// The sources this tree takes its structure from — one per live mount. pub(crate) live: Vec>, /// 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 { 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` is all it ever holds. pub fn insert_boxed( &mut self, widget: Box, mut style: taffy::Style, parent: Option, ) -> 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 { 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, 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 { self.root } /// The base root — `root()` under a name that stays honest beside /// overlay roots. pub(crate) fn base_root(&self) -> Option { 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 { 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(&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(&mut self, id: WidgetId) -> Option> { 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> { 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> { 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 = 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 { 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 = 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 { self.nodes .iter() .filter(|(id, _)| self.widget::(*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) { 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) { 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) { 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 { 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 { self.focus } /// Move focus, delivering blur/focus to the affected widgets. pub fn set_focus(&mut self, target: Option) { 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) { 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 { 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 { 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( &self, id: WidgetId, f: impl FnOnce(&dyn Widget) -> R, ) -> Option { 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( &mut self, id: WidgetId, f: impl FnOnce(&mut dyn Widget, &mut TextContext) -> R, ) -> Option { 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( &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::(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 + '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 { 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 = 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 { 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 = 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 = dirt .iter() .map(|(id, _)| *id) .filter(|id| self.nodes.contains_key(*id)) .chain(late) .chain(revealed) .collect(); if needs_layout { let ids: Vec = 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 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 Deref for WidgetMut<'_, T> { type Target = T; fn deref(&self) -> &T { self.widget } } impl DerefMut for WidgetMut<'_, T> { fn deref_mut(&mut self) -> &mut T { self.widget } } impl 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;