rosace_widgets/tree/mod.rs
1//! Widget tree — composable, layout-aware, paint-capable widgets.
2//!
3//! # Architecture
4//! Every widget implements [`Widget`]:
5//! - `layout(constraints) → Size` — measure pass (bottom-up)
6//! - `paint(ctx)` — paint pass (top-down, rect already allocated)
7//!
8//! Children are stored as `Vec<Box<dyn Widget>>`. [`Column`] / [`Row`] handle
9//! [`Expanded`] children by doing a two-pass measure internally.
10
11pub mod app;
12pub mod drawer;
13pub mod dropdown;
14pub mod accordion;
15mod hero;
16pub mod hero_tag;
17pub mod segmented;
18pub mod tabs;
19pub mod radio;
20pub mod skeleton;
21pub mod circular_progress;
22pub mod wrap;
23pub mod positioned;
24pub mod grid;
25pub mod aspect_ratio;
26pub mod app_bar;
27pub mod avatar;
28pub mod badge;
29pub mod button;
30pub mod card;
31pub mod checkbox;
32pub mod chip;
33pub mod column;
34pub mod container;
35pub mod custom_paint;
36pub mod dialog;
37pub mod divider;
38pub mod focus_api;
39pub mod icon;
40pub mod image;
41pub mod list_tile;
42pub mod list_view;
43pub mod menu;
44pub mod nav_rail;
45pub mod overlay;
46pub mod overlay_api;
47pub mod padding;
48pub mod autocomplete;
49pub mod dismissible;
50pub mod pointer;
51pub mod pressable;
52pub mod progress_bar;
53pub mod pull_to_refresh;
54pub mod rect_reader;
55pub mod bottom_nav;
56pub mod search_bar;
57pub mod snackbar;
58pub mod fab;
59pub mod table;
60pub mod carousel;
61pub mod stepper;
62pub mod rating_bar;
63pub mod interactive_viewer;
64pub mod material;
65pub mod selection;
66pub mod shader_paint;
67pub mod date_picker;
68pub mod time_picker;
69pub mod data_table;
70pub mod render_tree;
71pub mod repaint_boundary;
72pub mod row;
73pub mod scaffold;
74pub mod screen_transition_view;
75pub mod scroll_view;
76pub mod sheet;
77pub mod slider;
78pub mod spacer;
79pub mod stack;
80pub mod switch;
81pub mod tab;
82pub mod text;
83pub mod text_area;
84pub mod text_edit;
85pub mod text_input;
86pub mod toast;
87pub mod tooltip;
88pub mod transform_layer;
89
90pub use app::WidgetApp;
91pub use app_bar::AppBar;
92pub use avatar::Avatar;
93pub use badge::Badge;
94pub use button::{Button, ButtonVariant};
95pub use card::Card;
96pub use checkbox::Checkbox;
97pub use chip::Chip;
98pub use column::Column;
99pub use container::{BoxShape, Container};
100pub use aspect_ratio::AspectRatio;
101pub use grid::Grid;
102pub use circular_progress::CircularProgress;
103pub use skeleton::Skeleton;
104pub use radio::Radio;
105pub use segmented::SegmentedControl;
106pub use tabs::{TabView, Tabs};
107pub use accordion::Accordion;
108pub use hero_tag::{Hero, HeroApi};
109pub use dropdown::Dropdown;
110pub use drawer::Drawer;
111pub use positioned::Positioned;
112pub use wrap::Wrap;
113pub use custom_paint::CustomPaint;
114pub use dialog::{Dialog, DialogPresentation};
115pub use menu::Menu;
116pub use sheet::Sheet;
117pub use toast::{Toast, ToastKind};
118pub use divider::Divider;
119pub use focus_api::{FocusApi, WithFocus};
120pub use icon::{register_icon, resolve_icon, Icon, IconKind};
121pub use image::Image;
122pub use list_tile::ListTile;
123pub use list_view::ListView;
124pub use nav_rail::{NavItem, NavRail};
125pub use overlay::{
126 LayerId, LayerPosition, InputBehavior, FocusBehavior, ScrimConfig,
127 OverlayEntry, push_overlay, drain_overlays, clear_overlays,
128};
129pub use overlay_api::{OverlayApi, OverlayKind, WithOverlay};
130pub use padding::EdgeInsets;
131pub use pointer::{AbsorbPointer, IgnorePointer};
132pub use autocomplete::Autocomplete;
133pub use dismissible::{Dismissible, DismissDirection};
134pub use pressable::{LongPressable, PressApi, Pressable};
135pub use progress_bar::ProgressBar;
136pub use pull_to_refresh::PullToRefresh;
137pub use rect_reader::RectReader;
138pub use bottom_nav::{BottomNavItem, BottomNavigationBar};
139pub use search_bar::SearchBar;
140pub use snackbar::Snackbar;
141pub use fab::FloatingActionButton;
142pub use table::{Table, TableColumn};
143pub use carousel::{Carousel, PageView};
144pub use stepper::Stepper;
145pub use rating_bar::RatingBar;
146pub use interactive_viewer::InteractiveViewer;
147pub use material::{
148 MaterialKey, resolve_material, ContainerMaterial, CardMaterial,
149 DialogMaterial, SheetMaterial, DrawerMaterial, AppBarMaterial, BottomNavMaterial,
150};
151pub use selection::{GlassLens, SelectionKind, SelectionStyle};
152pub use shader_paint::ShaderPaint;
153pub use date_picker::{DatePicker, SimpleDate, SelectionMode, PageAxis};
154pub use time_picker::{TimePicker, SimpleTime, TimeUnit};
155pub use data_table::{DataTable, DataTableColumn, SortDirection};
156pub use render_tree::{HitHandler, InspectNode, NodeId, RenderTree, ScrollAxes, ScrollHandler, TreeNode};
157pub use repaint_boundary::RepaintBoundary;
158pub use row::Row;
159pub use scaffold::Scaffold;
160pub use screen_transition_view::ScreenTransitionView;
161pub use scroll_view::{ScrollView, ScrollAxis, MAX_TL_DIM};
162pub use slider::Slider;
163pub use spacer::{Expanded, Spacer};
164pub use stack::Stack;
165pub use switch::Switch;
166pub use tab::{Tab, TabBar};
167pub use text::{Text, TextAlign, FontWeight};
168pub use text_area::TextArea;
169pub use text_edit::{
170 CursorShape, CursorStyle, EditController, EditableDecl, InputFilter, Span, SpanFn,
171 TextEditState, TextLayoutSnapshot,
172};
173pub use text_input::TextInput;
174pub use tooltip::{Tooltip, TooltipStyle, WidgetExt};
175pub use transform_layer::TransformLayer;
176
177use std::rc::Rc;
178use std::cell::RefCell;
179use std::sync::Arc;
180
181use rosace_core::types::{Point, Rect, Size};
182use rosace_core::{Element, NativeElement, WidgetPayload};
183use rosace_layout::{AxisBound, Constraints};
184
185/// Shrink a bounded axis by `by` logical pixels (padding); unbounded and
186/// shrink-to-fit axes pass through unchanged — never collapse Unbounded
187/// into `Bounded(f32::INFINITY)`.
188pub(crate) fn shrink_axis(b: AxisBound, by: f32) -> AxisBound {
189 match b {
190 AxisBound::Bounded(v) => AxisBound::Bounded((v - by).max(0.0)),
191 other => other,
192 }
193}
194use rosace_render::{Color, DrawCommand, FontCache, Picture, PictureRecorder};
195use rosace_theme::ThemeData;
196
197// ── Continuous animation request (spinners, shimmer) ─────────────────────────
198
199use std::cell::Cell;
200thread_local! {
201 static ANIM_REQUEST: Cell<bool> = const { Cell::new(false) };
202}
203
204/// Ask the frame loop to schedule another frame — self-animating widgets
205/// (CircularProgress spinner, Skeleton shimmer) call this each paint.
206pub fn request_animation() { ANIM_REQUEST.with(|a| a.set(true)); }
207
208thread_local! {
209 static CURRENT_POINTER: Cell<(f32, f32)> = const { Cell::new((0.0, 0.0)) };
210}
211/// The latest pointer position (window-space logical px) — set by the engine on
212/// every pointer event. Read via [`PaintCtx::pointer`]: for widgets that must
213/// follow the finger CONTINUOUSLY during a drag (a clock hand sweeping to any
214/// angle), not just snap between discrete values.
215pub fn set_pointer(x: f32, y: f32) { CURRENT_POINTER.with(|p| p.set((x, y))); }
216/// The last pointer position the engine recorded.
217pub fn current_pointer() -> (f32, f32) { CURRENT_POINTER.with(|p| p.get()) }
218
219thread_local! {
220 static BOTTOM_OVERLAY_INSET: Cell<f32> = const { Cell::new(0.0) };
221}
222
223/// Height reserved at the window's bottom edge by chrome (the Scaffold's
224/// bottom bar) — bottom-anchored overlays (Snackbar/Toast) float ABOVE
225/// it, per platform convention. Declared fresh each frame by Scaffold's
226/// paint; the engine reads it during the overlay pass and resets it.
227pub fn set_bottom_overlay_inset(px: f32) { BOTTOM_OVERLAY_INSET.with(|v| v.set(px)); }
228
229/// Engine-side read+reset (once per frame, in the overlay pass).
230pub fn take_bottom_overlay_inset() -> f32 { BOTTOM_OVERLAY_INSET.with(|v| v.replace(0.0)) }
231
232/// Frame loop: did any widget request continuous animation this frame?
233pub fn take_animation_request() -> bool { ANIM_REQUEST.with(|a| a.replace(false)) }
234
235/// Seconds since process start — a shared clock for time-driven widgets.
236pub fn anim_clock() -> f32 {
237 use std::sync::OnceLock;
238 use web_time::Instant;
239 static START: OnceLock<Instant> = OnceLock::new();
240 START.get_or_init(Instant::now).elapsed().as_secs_f32()
241}
242
243/// Linear blend between two colors (t in 0..1) — animation interpolation.
244pub(crate) fn lerp_color(a: Color, b: Color, t: f32) -> Color {
245 let l = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t).round() as u8;
246 Color::rgba(l(a.r, b.r), l(a.g, b.g), l(a.b, b.b), l(a.a, b.a))
247}
248
249// ── Alignment ────────────────────────────────────────────────────────────────
250
251/// Where a single child sits inside its parent's rect (D095).
252/// Setting an alignment on [`Container`] makes it fill the available space
253/// (Flutter semantics) — otherwise there is nothing to align within.
254#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
255pub enum Alignment {
256 TopLeft, TopCenter, TopRight,
257 CenterLeft, #[default] Center, CenterRight,
258 BottomLeft, BottomCenter, BottomRight,
259}
260
261impl Alignment {
262 /// Child offset within a container for this alignment.
263 pub fn offset(&self, container: Size, child: Size) -> Point {
264 let fx = match self {
265 Alignment::TopLeft | Alignment::CenterLeft | Alignment::BottomLeft => 0.0,
266 Alignment::TopCenter | Alignment::Center | Alignment::BottomCenter => 0.5,
267 Alignment::TopRight | Alignment::CenterRight | Alignment::BottomRight => 1.0,
268 };
269 let fy = match self {
270 Alignment::TopLeft | Alignment::TopCenter | Alignment::TopRight => 0.0,
271 Alignment::CenterLeft | Alignment::Center | Alignment::CenterRight => 0.5,
272 Alignment::BottomLeft | Alignment::BottomCenter | Alignment::BottomRight => 1.0,
273 };
274 Point {
275 x: ((container.width - child.width) * fx).max(0.0),
276 y: ((container.height - child.height) * fy).max(0.0),
277 }
278 }
279}
280
281// ── Semantics ────────────────────────────────────────────────────────────────
282
283/// A declarative semantics entry (D099). Widgets push these during paint via
284/// [`PaintCtx::semantics`]; the frame derives the accessibility tree from the
285/// render tree. Roles come from `rosace_core::Role`.
286///
287/// `heading_level`/`href` (D107/Phase 25) mirror `rosace_core::SemanticNode`'s
288/// fields of the same name — carried through unchanged by `collect_semantics`.
289#[derive(Clone, Debug)]
290pub struct Semantics {
291 pub role: rosace_core::Role,
292 pub label: Option<String>,
293 pub value: Option<String>,
294 pub heading_level: Option<u8>,
295 pub href: Option<String>,
296}
297
298impl Semantics {
299 pub fn new(role: rosace_core::Role) -> Self {
300 Self { role, label: None, value: None, heading_level: None, href: None }
301 }
302 pub fn label(mut self, l: impl Into<String>) -> Self { self.label = Some(l.into()); self }
303 pub fn value(mut self, v: impl Into<String>) -> Self { self.value = Some(v.into()); self }
304 pub fn heading_level(mut self, level: u8) -> Self { self.heading_level = Some(level); self }
305 pub fn href(mut self, href: impl Into<String>) -> Self { self.href = Some(href.into()); self }
306}
307
308// ── HitTarget ────────────────────────────────────────────────────────────────
309
310/// A clickable region registered during painting.
311pub struct HitTarget {
312 pub rect: Rect,
313 pub callback: Arc<dyn Fn() + Send + Sync>,
314}
315
316// ── ScrollTarget ──────────────────────────────────────────────────────────────
317
318/// A scrollable viewport region registered during painting.
319///
320/// `ScrollView::paint` registers one per live scroll region. The event router
321/// dispatches `InputEvent::Scroll` to the target whose rect contains the cursor.
322/// The callback receives `(delta_x, delta_y)` in logical pixels
323/// (positive = content scrolls right / down).
324pub struct ScrollTarget {
325 pub rect: Rect,
326 pub callback: Arc<dyn Fn(f32, f32) + Send + Sync>,
327}
328
329// ── TransformLayerEntry ──────────────────────────────────────────────────────
330
331/// A captured TransformLayer — child content recorded into a separate Picture
332/// (D087) that the platform replays into its own SkiaCanvas and presents as an
333/// additional GPU compositor layer (D088).
334#[derive(Clone)]
335pub struct TransformLayerEntry {
336 /// Recorded child draw commands — replay-able independently of the main pass.
337 pub picture: Picture,
338 /// Natural (unconstrained) size of the child content in logical pixels.
339 pub child_size: Size,
340 /// Viewport rect in screen-space logical pixels.
341 pub viewport_rect: Rect,
342 /// Content magnification factor — `1.0` for ordinary scrolling (all
343 /// existing consumers). `InteractiveViewer` (Phase 32) is the only
344 /// consumer that varies this: the offscreen content texture is
345 /// rasterized at `dpi_scale * zoom` (engine.rs), so the compositor's
346 /// existing UV-window math (`uv_span = dest / tex_size`) naturally
347 /// samples a smaller fraction of a bigger texture — real GPU-crisp
348 /// zoom with no compositor changes. Screen<->content coordinate remap
349 /// (`child_coords`/`content_to_screen`) must divide/multiply by this.
350 pub zoom: f32,
351 /// Current horizontal scroll in logical pixels.
352 pub scroll_x: f32,
353 /// Current vertical scroll in logical pixels.
354 pub scroll_y: f32,
355}
356
357// ── PaintCtx ─────────────────────────────────────────────────────────────────
358
359/// Context passed to every widget's [`Widget::paint`] call.
360///
361/// Widgets push [`DrawCommand`]s via the helper methods here. Nothing writes
362/// pixels during paint — the commands are replayed by the compositor after
363/// the full tree has been walked.
364pub struct PaintCtx<'a> {
365 pub recorder: &'a mut PictureRecorder,
366 pub rect: Rect,
367 pub font: &'a FontCache,
368 pub theme: ThemeData,
369 /// The persistent render tree (D091) — sole owner of retained per-node
370 /// state. Widgets *declare* hit/scroll regions, focus nodes, overlays,
371 /// and transform layers onto `node`; the frame pipeline derives dispatch
372 /// order and the overlay stack from the tree.
373 pub tree: Rc<RefCell<RenderTree>>,
374 /// The tree node this widget declares onto.
375 pub node: NodeId,
376 /// The component that owns this paint subtree — node-created state
377 /// (default scroll controllers, D101) subscribes it so writes repaint.
378 pub owner: rosace_core::types::ComponentId,
379 /// Current clip viewport in world-space logical pixels. `None` means no clip.
380 /// Set by `ScrollView` so that `register_hit` ignores targets outside the
381 /// visible area, preventing phantom clicks in other panels below the fold.
382 pub clip_rect: Option<Rect>,
383}
384
385impl<'a> PaintCtx<'a> {
386 /// Root context for a standalone paint pass (golden tests, overlay pass).
387 /// Starts a frame on `tree` and paints into the root node. Windowed frame
388 /// loops that interleave cached subtrees manage the tree explicitly instead.
389 pub fn root(
390 recorder: &'a mut PictureRecorder,
391 rect: Rect,
392 font: &'a FontCache,
393 theme: ThemeData,
394 tree: Rc<RefCell<RenderTree>>,
395 ) -> PaintCtx<'a> {
396 tree.borrow_mut().start_frame();
397 PaintCtx {
398 recorder,
399 rect,
400 font,
401 theme,
402 tree,
403 node: RenderTree::ROOT,
404 owner: rosace_core::types::ComponentId(0),
405 clip_rect: None,
406 }
407 }
408
409 /// Derive a child context with a different rect (reborrowing the recorder).
410 /// Consumes the next child slot of this node — the child's previously
411 /// declared regions are cleared for re-declaration. `clip_rect` propagates.
412 pub fn child(&mut self, rect: Rect) -> PaintCtx<'_> {
413 let node = self.tree.borrow_mut().slot(self.node, true);
414 // Record every painted widget node's world-space rect (D123/O2):
415 // the DevTools element picker (`RenderTree::pick`/`inspect`) reads
416 // `cached_rect`, and without this only element/cache-boundary nodes
417 // — not the widgets inside a component's tree — were selectable.
418 // Cheap (one field write per child paint); walk_element sets its
419 // OWN element nodes' `cached_rect` separately, so this can't
420 // interfere with the picture-cache replay check.
421 self.tree.borrow_mut().node_mut(node).cached_rect = Some(rect);
422 PaintCtx {
423 recorder: self.recorder,
424 rect,
425 font: self.font,
426 theme: self.theme.clone(),
427 tree: Rc::clone(&self.tree),
428 node,
429 owner: self.owner,
430 clip_rect: self.clip_rect,
431 }
432 }
433
434 /// Like [`Self::child`], but the child's node is found by an explicit
435 /// stable `key` (`RenderTree::keyed_slot`) instead of positional call
436 /// order — used ONLY by `ScreenTransitionView` so a screen keeps its
437 /// own scroll position/animation state across navigation instead of
438 /// aliasing onto whatever screen last occupied that tree position. See
439 /// `render_tree.rs`'s module doc ("Identity" section) for the full story.
440 pub fn child_keyed(&mut self, rect: Rect, key: u64) -> PaintCtx<'_> {
441 let node = self.tree.borrow_mut().keyed_slot(self.node, key);
442 self.tree.borrow_mut().node_mut(node).cached_rect = Some(rect);
443 PaintCtx {
444 recorder: self.recorder,
445 rect,
446 font: self.font,
447 theme: self.theme.clone(),
448 tree: Rc::clone(&self.tree),
449 node,
450 owner: self.owner,
451 clip_rect: self.clip_rect,
452 }
453 }
454
455 /// Register a scroll viewport so the event router can dispatch wheel events
456 /// to the correct `ScrollView`. Called from `ScrollView::paint`. The
457 /// callback receives `(delta_x, delta_y)` in logical pixels.
458 pub fn register_scroll_target(
459 &self,
460 rect: Rect,
461 axes: render_tree::ScrollAxes,
462 callback: Arc<dyn Fn(f32, f32) + Send + Sync>,
463 ) {
464 self.tree.borrow_mut().node_mut(self.node).scrolls.push((rect, axes, callback));
465 }
466
467 /// Register a trackpad pinch-to-zoom region (`InteractiveViewer`, Phase
468 /// 32) — the callback receives the gesture's raw `delta` (see
469 /// [`render_tree::ZoomRegion`]'s doc: an increment, not a multiplier).
470 pub fn register_zoom_target(&self, rect: Rect, callback: Arc<dyn Fn(f32) + Send + Sync>) {
471 self.tree.borrow_mut().node_mut(self.node).zooms.push((rect, callback));
472 }
473
474 /// Register a focus node for Tab-cycle inclusion (called from `WithFocus<W>::paint`).
475 pub fn register_focus(&self, node: rosace_a11y::FocusNode) {
476 self.tree.borrow_mut().node_mut(self.node).focus.push(node);
477 }
478
479 /// Register a click callback for `self.rect`.
480 ///
481 /// If a `clip_rect` is active (set by `ScrollView`), the hit target is
482 /// intersected with it. Targets fully outside the clip are silently dropped
483 /// so they cannot intercept clicks in other panels below the fold.
484 pub fn register_hit(&self, callback: Arc<dyn Fn() + Send + Sync>) {
485 let hit_rect = if let Some(clip) = self.clip_rect {
486 match intersect_rect(self.rect, clip) {
487 Some(r) => r,
488 None => return, // widget is outside the visible scroll viewport
489 }
490 } else {
491 self.rect
492 };
493 self.tree.borrow_mut().node_mut(self.node).hits.push((hit_rect, callback));
494 }
495
496 /// Declare that this widget's rect responds to left-click (D099).
497 /// Sugar over [`PaintCtx::register_hit`] — clip-aware, z-order and
498 /// persistence handled by the render tree.
499 pub fn on_press(&self, f: impl Fn() + Send + Sync + 'static) {
500 self.register_hit(Arc::new(f));
501 }
502
503 /// The implicit scroll controller for this widget's tree node (D101):
504 /// created on first use, persists across rebuilds, subscribed to the
505 /// owning component so scroll writes repaint. This is why
506 /// `ScrollView::new(child)` scrolls with zero wiring.
507 pub fn scroll_controller(&self) -> rosace_scroll::ScrollController {
508 let mut tree = self.tree.borrow_mut();
509 let node = tree.node_mut(self.node);
510 if let Some(c) = &node.scroll_ctrl {
511 return c.clone();
512 }
513 let c = rosace_scroll::ScrollController::new();
514 c.offset.subscribe(self.owner);
515 c.content_size.subscribe(self.owner);
516 c.viewport_size.subscribe(self.owner);
517 node.scroll_ctrl = Some(c.clone());
518 c
519 }
520
521 /// True while the cursor is over this widget's interactive region —
522 /// paint hover feedback with it. Hover changes repaint automatically.
523 pub fn hovered(&self) -> bool {
524 self.tree.borrow().node(self.node).hovered
525 }
526
527 /// True from MouseDown until MouseUp while this widget is the pressed
528 /// target — pair with [`Self::animate_to`] for press/tap feedback
529 /// (D108/Phase 26 Step 1).
530 pub fn pressed(&self) -> bool {
531 self.tree.borrow().node(self.node).pressed
532 }
533
534 /// Declare a hover-only region (tooltips): participates in hover
535 /// tracking without swallowing clicks.
536 pub fn hoverable(&self) {
537 let r = self.rect;
538 self.tree.borrow_mut().node_mut(self.node).hover_regions.push(r);
539 }
540
541 /// Declare a long-press callback for this widget's rect (fires after
542 /// ~500 ms of press without movement).
543 pub fn on_long_press(&self, f: impl Fn() + Send + Sync + 'static) {
544 let r = self.rect;
545 self.tree.borrow_mut().node_mut(self.node).long_hits.push((r, Arc::new(f)));
546 }
547
548 /// Pointer interception for this subtree: `IgnorePointer` /
549 /// `AbsorbPointer` widgets call this — 1 = transparent, 2 = absorb.
550 pub fn set_pointer_mode(&self, mode: u8) {
551 self.tree.borrow_mut().node_mut(self.node).pointer_mode = mode;
552 }
553
554 /// Declare a POSITIONAL press region for this widget's rect — the
555 /// callback receives the click point in window-space logical pixels
556 /// (sliders, pickers, canvases). Clip-aware like register_hit.
557 pub fn on_press_at(&self, f: impl Fn(f32, f32) + Send + Sync + 'static) {
558 let hit_rect = if let Some(clip) = self.clip_rect {
559 match intersect_rect(self.rect, clip) {
560 Some(r) => r,
561 None => return,
562 }
563 } else {
564 self.rect
565 };
566 self.tree.borrow_mut().node_mut(self.node).hits_at.push((hit_rect, Arc::new(f)));
567 }
568
569 /// Declares a nested-scroll chain link over this widget's rect (see
570 /// `render_tree::ScrollHandler`'s doc for the full contract) — what
571 /// `ScrollView`'s own drag-to-pan registers instead of
572 /// [`Self::on_press_at`], so a gesture that starts on a plain-hit
573 /// child inside it (or on an inner nested `ScrollView`) can still
574 /// reach it, and it in turn can hand off to whatever encloses IT once
575 /// exhausted.
576 pub fn register_nested_scroll(&mut self, f: impl Fn(f32, f32) -> bool + Send + Sync + 'static) {
577 let hit_rect = if let Some(clip) = self.clip_rect {
578 match intersect_rect(self.rect, clip) {
579 Some(r) => r,
580 None => return,
581 }
582 } else {
583 self.rect
584 };
585 self.tree.borrow_mut().node_mut(self.node).nested_scrolls.push((hit_rect, Arc::new(f)));
586 }
587
588 /// Declare that this widget's rect responds to scroll wheel/trackpad.
589 /// The callback receives `(delta_x, delta_y)` in logical pixels.
590 pub fn on_scroll(&self, f: impl Fn(f32, f32) + Send + Sync + 'static) {
591 self.register_scroll_target(self.rect, render_tree::ScrollAxes::BOTH, Arc::new(f));
592 }
593
594 /// Declare semantics for this widget (D099): role, label, value.
595 /// Written to the render-tree node — persists on clean frames, cleared
596 /// on repaint, like every other declaration. The a11y tree is derived
597 /// from the render tree each frame.
598 pub fn semantics(&self, s: Semantics) {
599 self.tree.borrow_mut().node_mut(self.node).semantics.push(s);
600 }
601
602 /// The [`rosace_a11y::FocusNode`] for this widget's tree position —
603 /// created lazily on first paint and persists across rebuilds, the
604 /// same "zero wiring by default" precedent as [`Self::scroll_controller`]
605 /// (D101: "this is why `ScrollView::new(child)` scrolls with zero
606 /// wiring"). Powers `TextInput`'s built-in click-to-focus/Tab-cycling
607 /// (D112/Phase 28) without requiring every app to construct and wire
608 /// an explicit `FocusNode` for the common single-field case — apps
609 /// that DO want explicit neighbor wiring can still layer
610 /// `FocusApi::focus_node` on top; the two are independent focus-graph
611 /// nodes if both are used on the same widget.
612 pub fn focus_node(&self) -> rosace_a11y::FocusNode {
613 self.focus_node_seeded(false)
614 }
615
616 /// Same as [`Self::focus_node`], but if this is the FIRST paint of
617 /// this render-tree node (no focus node existed yet) and `seed` is
618 /// true, requests focus immediately. Backs `TextInput::focused()`'s
619 /// "start focused" behavior: a one-shot seed, not a per-frame
620 /// re-request — a later paint with `seed == true` on an
621 /// already-focus-noded position does NOT steal focus back after the
622 /// user has tabbed away.
623 pub fn focus_node_seeded(&self, seed: bool) -> rosace_a11y::FocusNode {
624 let mut tree = self.tree.borrow_mut();
625 let node = tree.node_mut(self.node);
626 if let Some(f) = &node.focus_node {
627 return f.clone();
628 }
629 let f = rosace_a11y::FocusNode::new();
630 if seed {
631 f.request();
632 }
633 node.focus_node = Some(f.clone());
634 f
635 }
636
637 /// Declare this widget's rect as editable text content (D112/Phase 28
638 /// Step 1). The engine's key/click dispatch (`rosace/src/engine.rs`)
639 /// finds it via the render tree, not a captured closure — see
640 /// [`text_edit::EditableDecl`]'s doc comment for why a plain
641 /// `Arc<dyn Fn + Send + Sync>` hit callback can't do this job.
642 pub fn register_editable(&self, decl: text_edit::EditableDecl) {
643 self.tree.borrow_mut().node_mut(self.node).editable = Some(decl);
644 }
645
646 /// This widget's persistent cursor/selection state (D091) — read
647 /// during paint to draw the caret/selection highlight. Mutated by the
648 /// engine's key/click dispatch, never by the widget itself (`paint`
649 /// takes `&self`) — with one deliberate exception: the VIEW-state
650 /// field `scrolled_cursor`, written through [`Self::set_scrolled_cursor`].
651 pub fn text_edit(&self) -> text_edit::TextEditState {
652 self.tree.borrow().node(self.node).text_edit.clone()
653 }
654
655 /// Record the caret position scroll-into-view has chased (see
656 /// `TextEditState::scrolled_cursor`). View state, so paint-written —
657 /// the one sanctioned widget-side write into `text_edit`.
658 pub fn set_scrolled_cursor(&self, cursor: Option<usize>) {
659 self.tree.borrow_mut().node_mut(self.node).text_edit.scrolled_cursor = cursor;
660 }
661
662 /// Record the horizontal scroll-into-view offset (see
663 /// [`TextEditState::scroll_x`]) — the single-line counterpart to
664 /// `set_scrolled_cursor`. View state, so paint-written: `TextInput`
665 /// computes how far the content must shift left to keep the caret
666 /// visible when the value overflows the field, and stores it here so
667 /// it persists across repaints instead of resetting to 0.
668 pub fn set_scroll_x(&self, scroll_x: f32) {
669 self.tree.borrow_mut().node_mut(self.node).text_edit.scroll_x = scroll_x;
670 }
671
672 /// Record `paint` into a standalone [`Picture`] at `rect`, returning it —
673 /// used by RepaintBoundary to cache an expensive subtree. Runs on a fresh
674 /// child slot so interactive regions declared inside still register.
675 pub fn capture(&mut self, rect: Rect, paint: impl FnOnce(&mut PaintCtx)) -> rosace_render::Picture {
676 let node = self.tree.borrow_mut().slot(self.node, true);
677 self.capture_into(node, rect, paint)
678 }
679
680 /// Consume the next child slot WITHOUT resetting it — preserves the
681 /// subtree's declared interactive regions across a cache-replay frame.
682 pub fn keep_child_slot(&mut self) {
683 self.tree.borrow_mut().slot(self.node, false);
684 }
685
686 fn capture_into(&mut self, node: NodeId, rect: Rect, paint: impl FnOnce(&mut PaintCtx)) -> rosace_render::Picture {
687 let mut rec = rosace_render::PictureRecorder::new();
688 {
689 let mut cctx = PaintCtx {
690 recorder: &mut rec,
691 rect,
692 font: self.font,
693 theme: self.theme.clone(),
694 tree: Rc::clone(&self.tree),
695 node,
696 owner: self.owner,
697 clip_rect: self.clip_rect,
698 };
699 paint(&mut cctx);
700 }
701 rec.finish()
702 }
703
704 /// Replay an already-recorded [`Picture`] into this context, translating
705 /// every command by `(dx, dy)`.
706 pub fn replay_offset(&mut self, picture: &rosace_render::Picture, dx: f32, dy: f32) {
707 for cmd in &picture.commands {
708 self.recorder.push(cmd.offset(dx, dy));
709 }
710 }
711
712 /// Replay a [`Picture`] captured at `src` instead at `dst` — translates
713 /// AND scales every command's geometry, unlike [`Self::replay_offset`]'s
714 /// translate-only. Backs Hero/shared-element transitions (D108/Phase 26
715 /// Step 5): a widget's captured appearance on one screen re-painted at a
716 /// different-sized rect on the other screen's tagged match.
717 pub fn replay_morphed(&mut self, picture: &rosace_render::Picture, src: Rect, dst: Rect) {
718 let sx = if src.size.width.abs() > f32::EPSILON { dst.size.width / src.size.width } else { 1.0 };
719 let sy = if src.size.height.abs() > f32::EPSILON { dst.size.height / src.size.height } else { 1.0 };
720 for cmd in &picture.commands {
721 self.recorder.push(cmd.morph(src.origin, dst.origin, sx, sy));
722 }
723 }
724
725 /// Attach an overlay entry to this node (called from `WithOverlay::paint`).
726 /// The entry persists on the node across cache-hit frames and is cleared
727 /// when the node repaints — open overlays cannot vanish on clean frames.
728 pub fn attach_overlay(&self, entry: OverlayEntry) {
729 self.tree.borrow_mut().node_mut(self.node).overlays.push(entry);
730 }
731
732 /// Attach a transform-layer entry to this node (called from
733 /// `TransformLayer::paint`). Persists like overlays (D087/D091).
734 pub fn attach_transform(&self, entry: TransformLayerEntry) {
735 self.tree.borrow_mut().node_mut(self.node).transforms.push(entry);
736 }
737
738 /// Convert a theme color (f32 0.0–1.0) to a render color (u8 0–255).
739 pub fn tc(&self, c: rosace_theme::Color) -> Color {
740 Color::rgba(
741 (c.r * 255.0) as u8,
742 (c.g * 255.0) as u8,
743 (c.b * 255.0) as u8,
744 (c.a * 255.0) as u8,
745 )
746 }
747
748 // ── Draw helpers — all push DrawCommands, no pixel writes ────────────────
749
750 /// Fill `self.rect` with a solid color.
751 pub fn fill(&mut self, color: Color) {
752 let rect = self.rect;
753 self.recorder.push(DrawCommand::FillRect { rect, color });
754 }
755
756 /// Stroke the outline of `self.rect`.
757 pub fn stroke(&mut self, color: Color, width: f32) {
758 let rect = self.rect;
759 self.recorder.push(DrawCommand::StrokeRect { rect, color, width });
760 }
761
762 /// Fill an arbitrary rectangle.
763 pub fn fill_rect(&mut self, rect: Rect, color: Color) {
764 self.recorder.push(DrawCommand::FillRect { rect, color });
765 }
766
767 /// Stroke an arbitrary rectangle.
768 pub fn stroke_rect(&mut self, rect: Rect, color: Color, width: f32) {
769 self.recorder.push(DrawCommand::StrokeRect { rect, color, width });
770 }
771
772 /// Fill a rounded rectangle with corner radius `radius`.
773 pub fn fill_rrect(&mut self, rect: Rect, radius: f32, color: Color) {
774 self.recorder.push(DrawCommand::FillRRect { rect, radius, color });
775 }
776
777 /// Fill a circle.
778 pub fn fill_circle(&mut self, center: Point, radius: f32, color: Color) {
779 self.recorder.push(DrawCommand::FillCircle { center, radius, color });
780 }
781
782 /// Frosted-glass panel (D-DEF-012): blurs and tints everything already
783 /// painted beneath `rect` behind a rounded panel — real backdrop
784 /// glassmorphism on GPU-composited targets (CPU fallback: translucent
785 /// tint, no blur). `blur` is the Gaussian strength in logical px;
786 /// `tint.a` controls how strongly the tint mixes over the blur.
787 pub fn backdrop_blur(&mut self, rect: Rect, radius: f32, blur: f32, tint: Color) {
788 self.recorder.push(DrawCommand::BackdropBlur { rect, radius, blur, tint });
789 }
790
791 /// Fill `rect` with a registered GPU shader pipeline (D109/Phase 27).
792 ///
793 /// `uniforms` come from a `#[derive(ShaderUniforms)]` struct's
794 /// `to_bytes()`. The pipeline must have been registered via
795 /// `rosace_shader::register_shader` (compiled eagerly at the next frame
796 /// boundary). Executes on the GPU at present time — this records a
797 /// command, like every other helper here, and never touches pixels.
798 /// Renders on GPU-composited targets only (desktop/mobile); web and the
799 /// softbuffer fallback drop it (Phase 27's documented scope).
800 pub fn shader_fill(&mut self, rect: Rect, pipeline: rosace_shader::PipelineId, uniforms: Vec<u8>) {
801 self.recorder.push(DrawCommand::ShaderFill {
802 pipeline_id: pipeline.raw(),
803 rect,
804 uniforms,
805 animate_time: false,
806 });
807 }
808
809 /// [`Self::shader_fill`] with the D109-maturity animation flag: the
810 /// PLATFORM patches the first 4 uniform bytes (the `time`-first
811 /// convention) with a live clock at every present, so continuous
812 /// animation costs a GPU buffer write per frame — record once, never
813 /// repaint, no `request_animation` loop.
814 pub fn shader_fill_animated(&mut self, rect: Rect, pipeline: rosace_shader::PipelineId, uniforms: Vec<u8>) {
815 self.recorder.push(DrawCommand::ShaderFill {
816 pipeline_id: pipeline.raw(),
817 rect,
818 uniforms,
819 animate_time: true,
820 });
821 }
822
823 /// Promotes `weight` one step toward bold when the OS accessibility
824 /// "bold text" setting is on (`mq.bold_text`) — leaves anything already
825 /// SemiBold/Bold alone, since it's already at or past that intent.
826 fn bold_text_weight(mq: rosace_core::MediaQuery, weight: rosace_render::FontWeight) -> rosace_render::FontWeight {
827 use rosace_render::FontWeight;
828 if mq.bold_text && matches!(weight, FontWeight::Light | FontWeight::Regular | FontWeight::Medium) {
829 FontWeight::SemiBold
830 } else {
831 weight
832 }
833 }
834
835 /// Draw text at an absolute position (not relative to `self.rect`).
836 pub fn draw_text_at(&mut self, text: &str, origin: Point, color: Color, px: f32) {
837 let mq = rosace_core::media_query::use_media_query();
838 let px = px * mq.text_scale;
839 self.recorder.push(DrawCommand::DrawText {
840 text: text.to_string(),
841 origin,
842 color,
843 px,
844 weight: Self::bold_text_weight(mq, rosace_render::FontWeight::Regular),
845 });
846 }
847
848 /// Draw text at `(self.rect.origin + (dx, dy))`.
849 pub fn text(&mut self, s: &str, dx: f32, dy: f32, color: Color, px: f32) {
850 let mq = rosace_core::media_query::use_media_query();
851 let px = px * mq.text_scale;
852 let origin = Point { x: self.rect.origin.x + dx, y: self.rect.origin.y + dy };
853 self.recorder.push(DrawCommand::DrawText {
854 text: s.to_string(), origin, color, px,
855 weight: Self::bold_text_weight(mq, rosace_render::FontWeight::Regular),
856 });
857 }
858
859 /// Draw text at `(self.rect.origin + (dx, dy))` with an explicit weight —
860 /// SemiBold/Bold route to the real bold face.
861 pub fn text_styled(&mut self, s: &str, dx: f32, dy: f32, color: Color, px: f32, weight: rosace_render::FontWeight) {
862 let mq = rosace_core::media_query::use_media_query();
863 let px = px * mq.text_scale;
864 let weight = Self::bold_text_weight(mq, weight);
865 let origin = Point { x: self.rect.origin.x + dx, y: self.rect.origin.y + dy };
866 self.recorder.push(DrawCommand::DrawText { text: s.to_string(), origin, color, px, weight });
867 }
868
869 /// Emit a blurred drop shadow behind a square-cornered `rect`.
870 pub fn fill_shadow(&mut self, rect: Rect, color: Color, blur: f32) {
871 self.recorder.push(DrawCommand::DrawShadow { rect, radius: 0.0, color, blur });
872 }
873
874 /// Emit a blurred drop shadow behind a rounded rect. `radius` must match
875 /// the widget's corner radius so the shadow hugs the rounded shape.
876 pub fn fill_shadow_rrect(&mut self, rect: Rect, radius: f32, color: Color, blur: f32) {
877 self.recorder.push(DrawCommand::DrawShadow { rect, radius, color, blur });
878 }
879
880 /// Stroke a rounded-rect outline matching [`PaintCtx::fill_rrect`] geometry.
881 pub fn stroke_rrect(&mut self, rect: Rect, radius: f32, color: Color, width: f32) {
882 self.recorder.push(DrawCommand::StrokeRRect { rect, radius, color, width });
883 }
884
885 /// Fill a (rounded) rect with a two-stop linear gradient.
886 pub fn fill_gradient(&mut self, rect: Rect, radius: f32, from: Color, to: Color, vertical: bool) {
887 self.recorder.push(DrawCommand::FillGradient { rect, radius, from, to, vertical });
888 }
889
890 /// Draw a ring segment (progress arc / spinner).
891 pub fn fill_arc(&mut self, center: Point, radius: f32, thickness: f32, start_deg: f32, sweep_deg: f32, color: Color) {
892 self.recorder.push(DrawCommand::FillArc { center, radius, thickness, start_deg, sweep_deg, color });
893 }
894
895 /// Request another frame — self-animating widgets (spinner, shimmer) call
896 /// this each paint so the frame loop keeps repainting them.
897 pub fn request_animation(&self) { crate::tree::request_animation(); }
898
899 /// The theme's [`AnimationConfig`], with `enabled` forced `false` when
900 /// the OS accessibility "reduce motion" setting is on
901 /// (`rosace_core::media_query().reduce_motion`) — same choke-point
902 /// pattern as `text_scale`: override at the point the raw theme value is
903 /// about to be used, no signature changes, no per-widget edits.
904 fn reduce_motion_animation_cfg(&self) -> rosace_theme::AnimationConfig {
905 let cfg = self.theme.animation;
906 if rosace_core::media_query::use_media_query().reduce_motion {
907 rosace_theme::AnimationConfig { enabled: false, ..cfg }
908 } else {
909 cfg
910 }
911 }
912
913 /// Ease this node's persistent scalar toward `target` and return the
914 /// current value. Honors the theme's global [`AnimationConfig`]: when
915 /// disabled it snaps; otherwise it exponentially eases over the theme's
916 /// duration (or `duration_ms` if > 0) and keeps requesting frames until
917 /// settled. This is how Switch/Checkbox/Radio animate WITHOUT any per-
918 /// widget state — the animation policy is global (theme), the value is
919 /// per-node. First observation snaps (no appear-animation).
920 pub fn animate_to(&self, target: f32, duration_ms: f32) -> f32 {
921 let cfg = self.reduce_motion_animation_cfg();
922 if !cfg.enabled {
923 self.tree.borrow_mut().node_mut(self.node).anim = Some(target);
924 return target;
925 }
926 let dur = (if duration_ms > 0.0 { duration_ms } else { cfg.duration_ms }).max(1.0);
927 let (val, settled) = {
928 let mut tree = self.tree.borrow_mut();
929 let node = tree.node_mut(self.node);
930 match node.anim {
931 None => { node.anim = Some(target); (target, true) }
932 Some(cur) => {
933 let dt = rosace_animate::frame_dt();
934 let alpha = 1.0 - (-dt * (1000.0 / dur)).exp();
935 let next = cur + (target - cur) * alpha;
936 let settled = (next - target).abs() < 0.001;
937 let v = if settled { target } else { next };
938 node.anim = Some(v);
939 (v, settled)
940 }
941 }
942 };
943 if !settled { crate::tree::request_animation(); }
944 val
945 }
946
947 /// Seeds this node's persistent animated scalar to `value` — but ONLY
948 /// if it has never been observed before (`None`). An already-set value
949 /// is left untouched. Pairs with `animate_to` to opt OUT of its "first
950 /// observation snaps straight to target" behavior for a genuine
951 /// appear-animation: call this with the START value (e.g. `0.0`)
952 /// before the first `animate_to` call on a node that should visibly
953 /// ease in rather than pop in fully-formed — e.g. an image fading in
954 /// from 0 opacity the first frame it has real decoded content
955 /// (D108/Phase 26 Step 4), not fully-formed from frame one.
956 pub fn seed_anim_if_unset(&self, value: f32) {
957 let mut tree = self.tree.borrow_mut();
958 let node = tree.node_mut(self.node);
959 if node.anim.is_none() {
960 node.anim = Some(value);
961 }
962 }
963
964 /// Unconditionally sets this node's persistent animated scalar (the same
965 /// one `animate_to` eases) to `value` — unlike `seed_anim_if_unset`, this
966 /// always overwrites. For a widget that renders its OWN live value
967 /// alongside `animate_to`'s eased one (a drag gesture's raw finger
968 /// offset, summed with the eased snap-to-page position — see
969 /// `Carousel`): call this right before switching `animate_to`'s target
970 /// so the eased value starts from wherever the combined visual position
971 /// actually was, instead of jumping from the stale pre-drag value and
972 /// losing the live offset in the same frame (found live: released a
973 /// carousel drag past the swipe threshold and the page visibly popped
974 /// before easing, instead of continuing smoothly from the finger).
975 pub fn set_anim(&self, value: f32) {
976 self.tree.borrow_mut().node_mut(self.node).anim = Some(value);
977 }
978
979 /// Ease the `channel`-th independent animated scalar of this node toward
980 /// `target` and return the current value. This is the multi-value sibling
981 /// of [`Self::animate_to`]: a widget that must animate more than one thing
982 /// at once (a Switch's thumb *position* AND its hover/press *state-layer*,
983 /// a Slider's fill AND its thumb halo) gives each its own `channel`.
984 ///
985 /// Channels are independent persistent scalars keyed by the explicit
986 /// `channel` index — no call-order coupling, so branches that skip a
987 /// channel some frames don't shift the others. Identical easing policy to
988 /// `animate_to`: honors the theme's global `AnimationConfig` (snaps when
989 /// disabled), exponentially eases over the theme duration (or `duration_ms`
990 /// if > 0), first observation snaps (no appear-pop), and keeps requesting
991 /// frames until settled.
992 pub fn animate_channel(&self, channel: usize, target: f32, duration_ms: f32) -> f32 {
993 let cfg = self.reduce_motion_animation_cfg();
994 let mut tree = self.tree.borrow_mut();
995 let node = tree.node_mut(self.node);
996 if node.anim_channels.len() <= channel {
997 node.anim_channels.resize(channel + 1, None);
998 }
999 if !cfg.enabled {
1000 node.anim_channels[channel] = Some(target);
1001 return target;
1002 }
1003 let dur = (if duration_ms > 0.0 { duration_ms } else { cfg.duration_ms }).max(1.0);
1004 let (val, settled) = match node.anim_channels[channel] {
1005 None => (target, true),
1006 Some(cur) => {
1007 let dt = rosace_animate::frame_dt();
1008 let alpha = 1.0 - (-dt * (1000.0 / dur)).exp();
1009 let next = cur + (target - cur) * alpha;
1010 let settled = (next - target).abs() < 0.001;
1011 (if settled { target } else { next }, settled)
1012 }
1013 };
1014 node.anim_channels[channel] = Some(val);
1015 drop(tree);
1016 if !settled { crate::tree::request_animation(); }
1017 val
1018 }
1019
1020 /// Seed an [`animate_channel`](Self::animate_channel) channel to `value`
1021 /// ONLY if it has never been observed — the multi-channel sibling of
1022 /// [`seed_anim_if_unset`](Self::seed_anim_if_unset). Use it to opt a
1023 /// channel OUT of animate_channel's "first observation snaps" behaviour so
1024 /// it visibly eases FROM `value` on the first frame (e.g. a clock hand
1025 /// starting at 12:00 and sweeping to the current time).
1026 pub fn seed_channel_if_unset(&self, channel: usize, value: f32) {
1027 let mut tree = self.tree.borrow_mut();
1028 let node = tree.node_mut(self.node);
1029 if node.anim_channels.len() <= channel {
1030 node.anim_channels.resize(channel + 1, None);
1031 }
1032 if node.anim_channels[channel].is_none() {
1033 node.anim_channels[channel] = Some(value);
1034 }
1035 }
1036
1037 /// Read an animation channel's current value (see [`animate_channel`](Self::animate_channel)),
1038 /// without advancing it — for widgets that need the settled position to
1039 /// compute a shortest-path target (e.g. a clock hand crossing 12).
1040 pub fn anim_channel(&self, channel: usize) -> Option<f32> {
1041 self.tree.borrow().node(self.node).anim_channels.get(channel).copied().flatten()
1042 }
1043
1044 /// The latest pointer position (window-space logical px) — for a widget
1045 /// that follows the finger during a drag (e.g. a clock hand). Combine with
1046 /// [`Self::pressed`]: while pressed, draw at the raw pointer angle (smooth);
1047 /// on release, snap to the nearest value.
1048 pub fn pointer(&self) -> Point {
1049 let (x, y) = crate::tree::current_pointer();
1050 Point { x, y }
1051 }
1052
1053 /// Snap an animation channel to `value` immediately (no easing) — for
1054 /// obeying a live drag: the hand tracks the finger exactly instead of
1055 /// lagging behind an ease.
1056 pub fn set_anim_channel(&self, channel: usize, value: f32) {
1057 let mut tree = self.tree.borrow_mut();
1058 let node = tree.node_mut(self.node);
1059 if node.anim_channels.len() <= channel { node.anim_channels.resize(channel + 1, None); }
1060 node.anim_channels[channel] = Some(value);
1061 }
1062
1063 /// Push a raw [`DrawCommand`] for advanced use.
1064 pub fn record(&mut self, cmd: DrawCommand) {
1065 self.recorder.push(cmd);
1066 }
1067
1068 /// Create a [`LayoutCtx`] from this paint context.
1069 ///
1070 /// Needed when a widget measures children inside `paint()` (e.g. to position
1071 /// them). Uses the available rect as tight constraints.
1072 pub fn layout_ctx(&self, constraints: Constraints) -> LayoutCtx<'_> {
1073 LayoutCtx::new(constraints, self.font, &self.theme)
1074 }
1075}
1076
1077// ── LayoutCtx ────────────────────────────────────────────────────────────────
1078
1079/// Context passed to every widget's [`Widget::layout`] call.
1080///
1081/// Carries the available constraints plus font and theme access so that widgets
1082/// can measure text accurately without relying on character-count heuristics.
1083pub struct LayoutCtx<'a> {
1084 pub constraints: Constraints,
1085 pub font: &'a FontCache,
1086 pub theme: &'a ThemeData,
1087}
1088
1089impl<'a> LayoutCtx<'a> {
1090 pub fn new(constraints: Constraints, font: &'a FontCache, theme: &'a ThemeData) -> Self {
1091 Self { constraints, font, theme }
1092 }
1093
1094 /// Derive a child context with tighter constraints (font/theme are shared).
1095 pub fn with_constraints(&self, constraints: Constraints) -> LayoutCtx<'_> {
1096 LayoutCtx { constraints, font: self.font, theme: self.theme }
1097 }
1098}
1099
1100// ── Widget trait ─────────────────────────────────────────────────────────────
1101
1102/// The render layer trait. Every built-in widget implements this.
1103///
1104/// `Widget` is the render/paint concern — layout + draw. It is NOT what users
1105/// implement to compose UI; that's [`rosace_core::Component`].
1106/// Custom widgets can implement `Widget` for low-level control.
1107/// How a widget exposes its structure to the framework (D098).
1108///
1109/// This is the taxonomy: a leaf keeps the default (`None`), a single-child
1110/// wrapper returns `One`, a container returns `Many`. Every [`Widget`]
1111/// default below keys off this, so a wrapper only implements the one
1112/// method it actually changes.
1113pub enum Children<'a> {
1114 /// Leaf — draws content, has no children.
1115 None,
1116 /// Single-child wrapper — decorates or constrains one child.
1117 One(&'a dyn Widget),
1118 /// Multi-child container — arranges several children.
1119 Many(&'a [BoxedWidget]),
1120}
1121
1122pub trait Widget: Send + Sync {
1123 /// Declare this widget's children. Drives every default below.
1124 fn children(&self) -> Children<'_> { Children::None }
1125
1126 /// Measure under `ctx.constraints` and return a size within them.
1127 ///
1128 /// Defaults: leaf → smallest allowed size; `One` → the child's size;
1129 /// `Many` → stack-like (max of children) — real containers override.
1130 fn layout(&self, ctx: &LayoutCtx) -> Size {
1131 match self.children() {
1132 Children::None => ctx.constraints.constrain(Size { width: 0.0, height: 0.0 }),
1133 Children::One(c) => c.layout(ctx),
1134 Children::Many(cs) => {
1135 let mut s = Size { width: 0.0, height: 0.0 };
1136 for c in cs {
1137 let cz = c.layout(ctx);
1138 s.width = s.width.max(cz.width);
1139 s.height = s.height.max(cz.height);
1140 }
1141 ctx.constraints.constrain(s)
1142 }
1143 }
1144 }
1145
1146 /// Record draw commands for `ctx.rect`.
1147 ///
1148 /// Defaults: leaf → nothing; `One` → paint the child in this rect;
1149 /// `Many` → stack-like (all children in this rect) — containers that
1150 /// position children override.
1151 fn paint(&self, ctx: &mut PaintCtx) {
1152 match self.children() {
1153 Children::None => {}
1154 Children::One(c) => {
1155 let r = ctx.rect;
1156 c.paint(&mut ctx.child(r));
1157 }
1158 Children::Many(cs) => {
1159 let r = ctx.rect;
1160 for c in cs {
1161 c.paint(&mut ctx.child(r));
1162 }
1163 }
1164 }
1165 }
1166
1167 /// Flex weight inside Row/Column. Wrappers are transparent by default.
1168 fn flex_factor(&self) -> f32 {
1169 match self.children() {
1170 Children::One(c) => c.flex_factor(),
1171 _ => 0.0,
1172 }
1173 }
1174
1175 /// Wrap this widget in an [`Element`] so it can be returned from
1176 /// `Component::build()`.
1177 fn into_element(self) -> Element
1178 where
1179 Self: Sized + 'static,
1180 {
1181 Element::Native(NativeElement {
1182 tag: std::any::type_name::<Self>(),
1183 payload: Some(Arc::new(WidgetBox(Box::new(self)))),
1184 children: vec![],
1185 key: None,
1186 })
1187 }
1188}
1189
1190/// Heap-allocated, type-erased widget.
1191pub type BoxedWidget = Box<dyn Widget>;
1192
1193/// `Box<dyn Widget>` is itself a Widget (D093) — builders accepting
1194/// `impl Widget` take boxed children without adapter structs. Fully
1195/// transparent delegation: no extra tree node, no behavior change.
1196impl Widget for Box<dyn Widget> {
1197 fn children(&self) -> Children<'_> { (**self).children() }
1198 fn layout(&self, ctx: &LayoutCtx) -> Size { (**self).layout(ctx) }
1199 fn paint(&self, ctx: &mut PaintCtx) { (**self).paint(ctx) }
1200 fn flex_factor(&self) -> f32 { (**self).flex_factor() }
1201}
1202
1203// ── WidgetBox — bridges Widget into the Element tree ─────────────────────────
1204
1205/// Concrete wrapper that stores a `Box<dyn Widget>` inside a `NativeElement`.
1206///
1207/// The element walker in the umbrella crate downcasts `NativeElement.payload`
1208/// to this type to retrieve the widget for layout + paint.
1209pub struct WidgetBox(pub Box<dyn Widget>);
1210
1211impl WidgetPayload for WidgetBox {
1212 fn as_any(&self) -> &dyn std::any::Any { self }
1213}
1214
1215// ── Helpers ───────────────────────────────────────────────────────────────────
1216
1217/// Extract the max available width from constraints (f32::INFINITY if unbounded).
1218pub(crate) fn avail_w(c: Constraints) -> f32 { c.max_width_f32() }
1219
1220/// Extract the max available height from constraints.
1221pub(crate) fn avail_h(c: Constraints) -> f32 { c.max_height_f32() }
1222
1223/// `PaintCtx::draw_text_at`'s `origin.y` is the TOP of the text line, not its
1224/// baseline or vertical center (`layout_glyphs` adds the font's ascender
1225/// internally) — an eyeballed fraction of a box's height overflows the box
1226/// whenever the box-height/font-size ratio changes (bit `DatePicker`'s
1227/// header/day-cells and `TimePicker`'s value pills/arrows: text spilled past
1228/// the box bottom). Center properly using the font's own line height.
1229pub(crate) fn vcenter_text_y(box_top: f32, box_h: f32, font: &rosace_render::FontCache, px: f32) -> f32 {
1230 box_top + (box_h - font.line_height(px)) / 2.0
1231}
1232
1233/// Build a Rect from origin point + size.
1234pub(crate) fn rect_at(origin: Point, size: Size) -> Rect {
1235 Rect { origin, size }
1236}
1237
1238/// Offset a point relative to a parent rect's origin.
1239pub(crate) fn offset(base: Point, dx: f32, dy: f32) -> Point {
1240 Point { x: base.x + dx, y: base.y + dy }
1241}
1242
1243/// Intersect two world-space rects. Returns `None` if they do not overlap.
1244pub(crate) fn intersect_rect(a: Rect, b: Rect) -> Option<Rect> {
1245 let x0 = a.origin.x.max(b.origin.x);
1246 let y0 = a.origin.y.max(b.origin.y);
1247 let x1 = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
1248 let y1 = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
1249 if x1 > x0 && y1 > y0 {
1250 Some(Rect { origin: Point { x: x0, y: y0 }, size: Size { width: x1 - x0, height: y1 - y0 } })
1251 } else {
1252 None
1253 }
1254}