Skip to main content

fret_ui/
widget.rs

1use crate::{Theme, UiHost};
2use fret_core::{
3    AppWindowId, Corners, Event, NodeId, Point, Rect, Scene, SemanticsCheckedState, SemanticsFlags,
4    SemanticsInvalid, SemanticsLive, SemanticsOrientation, SemanticsPressedState, SemanticsRole,
5    Size, Transform2D, UiServices,
6};
7use fret_runtime::{
8    CommandId, DefaultAction, DefaultActionSet, Effect, InputContext, Model, ModelId,
9};
10use std::any::{Any, TypeId};
11use std::collections::HashMap;
12
13use crate::layout_constraints::LayoutConstraints;
14use crate::layout_pass::LayoutPassKind;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Invalidation {
18    Layout,
19    Paint,
20    HitTest,
21    /// Recompute hit-testing and repaint, without forcing a layout pass.
22    ///
23    /// This is intended for state changes that affect coordinate mapping (e.g. scrolling) but do
24    /// not change layout geometry.
25    HitTestOnly,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct UiSourceLocation {
30    pub file: &'static str,
31    pub line: u32,
32    pub column: u32,
33}
34
35pub struct EventCx<'a, H: UiHost> {
36    pub app: &'a mut H,
37    pub services: &'a mut dyn UiServices,
38    pub node: NodeId,
39    pub layer_root: Option<NodeId>,
40    pub window: Option<AppWindowId>,
41    pub pointer_id: Option<fret_core::PointerId>,
42    /// Window scale factor recorded by the UI tree on the most recent layout pass.
43    ///
44    /// This is best-effort: events may arrive before the first layout, in which case the value
45    /// defaults to `1.0`.
46    pub scale_factor: f32,
47    /// The incoming pointer position in window-local logical pixels (before any transform-aware
48    /// mapping performed by the UI runtime).
49    ///
50    /// When an event does not carry a pointer position, this is `None`.
51    pub event_window_position: Option<Point>,
52    /// The incoming wheel delta in window-local logical pixels (before any transform-aware
53    /// mapping performed by the UI runtime).
54    ///
55    /// When the current event is not a wheel event, this is `None`.
56    pub event_window_wheel_delta: Option<Point>,
57    pub input_ctx: InputContext,
58    /// `true` when the pointer hit-test target is (or is inside) a text input element subtree
59    /// (`TextInput`, `TextArea`, or `TextInputRegion`).
60    ///
61    /// This is computed by the UI runtime during dispatch and is available to mechanism widgets
62    /// (e.g. `PointerRegion`) so action payloads can carry enough information for policy-level
63    /// gesture arbitration without exposing widget internals.
64    pub pointer_hit_is_text_input: bool,
65    /// `true` when the pointer hit-test target is (or is inside) a pressable element subtree
66    /// (`Pressable`).
67    ///
68    /// This is computed by the UI runtime during dispatch and is available to mechanism widgets
69    /// (e.g. `PointerRegion`) so action payloads can carry enough information for policy-level
70    /// gesture arbitration without exposing widget internals.
71    pub pointer_hit_is_pressable: bool,
72    /// The deepest pressable element in the pointer-down hit-test chain (if any).
73    ///
74    /// This is computed by the UI runtime during dispatch and is available to mechanism widgets
75    /// (e.g. `PointerRegion`, `Pressable`) so policy-level hooks can distinguish nested pressable
76    /// targets (e.g. "row click" vs "button inside row").
77    pub pointer_hit_pressable_target: Option<crate::GlobalElementId>,
78    /// `true` when `pointer_hit_pressable_target` is a strict descendant of the current event
79    /// target in the hit-test chain.
80    ///
81    /// This excludes ambient ancestor pressables and the current target itself, so policy hooks
82    /// can suppress forwarding only for truly nested interactive descendants.
83    pub pointer_hit_pressable_target_in_descendant_subtree: bool,
84    pub prevented_default_actions: &'a mut DefaultActionSet,
85    pub children: &'a [NodeId],
86    pub focus: Option<NodeId>,
87    pub captured: Option<NodeId>,
88    pub bounds: Rect,
89    pub invalidations: Vec<(NodeId, Invalidation)>,
90    pub(crate) scroll_handle_invalidations: Vec<ScrollHandleInvalidationRequest>,
91    pub(crate) scroll_target_invalidations: Vec<crate::GlobalElementId>,
92    pub requested_focus: Option<NodeId>,
93    pub requested_focus_target: Option<crate::GlobalElementId>,
94    pub requested_capture: Option<Option<NodeId>>,
95    pub requested_cursor: Option<fret_core::CursorIcon>,
96    pub notify_requested: bool,
97    pub notify_requested_location: Option<UiSourceLocation>,
98    pub stop_propagation: bool,
99}
100
101impl<'a, H: UiHost> EventCx<'a, H> {
102    #[allow(clippy::too_many_arguments)]
103    pub fn new(
104        app: &'a mut H,
105        services: &'a mut dyn UiServices,
106        node: NodeId,
107        layer_root: Option<NodeId>,
108        window: Option<AppWindowId>,
109        input_ctx: InputContext,
110        pointer_id: Option<fret_core::PointerId>,
111        scale_factor: f32,
112        event_window_position: Option<Point>,
113        event_window_wheel_delta: Option<Point>,
114        pointer_hit_is_text_input: bool,
115        pointer_hit_is_pressable: bool,
116        pointer_hit_pressable_target: Option<crate::GlobalElementId>,
117        pointer_hit_pressable_target_in_descendant_subtree: bool,
118        prevented_default_actions: &'a mut DefaultActionSet,
119        children: &'a [NodeId],
120        focus: Option<NodeId>,
121        captured: Option<NodeId>,
122        bounds: Rect,
123    ) -> Self {
124        Self {
125            app,
126            services,
127            node,
128            layer_root,
129            window,
130            pointer_id,
131            scale_factor,
132            event_window_position,
133            event_window_wheel_delta,
134            input_ctx,
135            pointer_hit_is_text_input,
136            pointer_hit_is_pressable,
137            pointer_hit_pressable_target,
138            pointer_hit_pressable_target_in_descendant_subtree,
139            prevented_default_actions,
140            children,
141            focus,
142            captured,
143            bounds,
144            invalidations: Vec::new(),
145            scroll_handle_invalidations: Vec::new(),
146            scroll_target_invalidations: Vec::new(),
147            requested_focus: None,
148            requested_focus_target: None,
149            requested_capture: None,
150            requested_cursor: None,
151            notify_requested: false,
152            notify_requested_location: None,
153            stop_propagation: false,
154        }
155    }
156
157    pub fn theme(&self) -> &Theme {
158        Theme::global(&*self.app)
159    }
160
161    /// Returns the pointer position in the current widget's local coordinate space (origin at
162    /// `(0, 0)`), derived from the mapped event position.
163    ///
164    /// Notes:
165    /// - The UI runtime maps pointer event positions into each widget's untransformed layout
166    ///   space (ADR 0238), so `event.position` is in the same space as `self.bounds`.
167    /// - This helper is purely derived and does not introduce state.
168    pub fn pointer_position_local(&self, event: &Event) -> Option<Point> {
169        let pos = Self::pointer_position_mapped(event)?;
170        Some(Point::new(
171            fret_core::Px(pos.x.0 - self.bounds.origin.x.0),
172            fret_core::Px(pos.y.0 - self.bounds.origin.y.0),
173        ))
174    }
175
176    /// Returns the pointer position in window-local logical pixels (pre-mapping).
177    pub fn pointer_position_window(&self, event: &Event) -> Option<Point> {
178        Self::pointer_position_mapped(event).and(self.event_window_position)
179    }
180
181    /// Returns the wheel delta in the current widget's local coordinate space (origin at
182    /// `(0, 0)`), derived from the mapped event delta.
183    pub fn pointer_delta_local(&self, event: &Event) -> Option<Point> {
184        match event {
185            Event::Pointer(fret_core::PointerEvent::Wheel { delta, .. }) => Some(*delta),
186            _ => None,
187        }
188    }
189
190    /// Returns the wheel delta in window-local logical pixels (pre-mapping).
191    pub fn pointer_delta_window(&self, event: &Event) -> Option<Point> {
192        self.pointer_delta_local(event)
193            .and(self.event_window_wheel_delta)
194    }
195
196    fn pointer_position_mapped(event: &Event) -> Option<Point> {
197        match event {
198            Event::Pointer(e) => match e {
199                fret_core::PointerEvent::Move { position, .. }
200                | fret_core::PointerEvent::Down { position, .. }
201                | fret_core::PointerEvent::Up { position, .. }
202                | fret_core::PointerEvent::Wheel { position, .. }
203                | fret_core::PointerEvent::PinchGesture { position, .. } => Some(*position),
204            },
205            Event::PointerCancel(e) => e.position,
206            Event::ExternalDrag(e) => Some(e.position),
207            Event::InternalDrag(e) => Some(e.position),
208            _ => None,
209        }
210    }
211
212    /// Best-effort frame clock snapshot for the current window (ADR 0240).
213    ///
214    /// This is intentionally a plain read (non-reactive): it does not participate in view-cache
215    /// dependency tracking.
216    pub fn frame_clock(&self) -> Option<fret_core::WindowFrameClockSnapshot> {
217        let window = self.window?;
218        self.app
219            .global::<fret_core::WindowFrameClockService>()
220            .and_then(|svc| svc.snapshot(window))
221    }
222
223    /// Best-effort reduced-motion preference for the current window (ADR 0232 / ADR 0240).
224    pub fn prefers_reduced_motion(&self) -> Option<bool> {
225        let window = self.window?;
226        self.app
227            .global::<fret_core::WindowMetricsService>()
228            .and_then(|svc| {
229                svc.prefers_reduced_motion_is_known(window)
230                    .then(|| svc.prefers_reduced_motion(window))
231                    .flatten()
232            })
233    }
234
235    /// Latest pointer position snapshot in window-local logical pixels (ADR 0243).
236    pub fn pointer_position_window_snapshot(
237        &self,
238        pointer_id: fret_core::PointerId,
239    ) -> Option<Point> {
240        let window = self.window?;
241        self.app
242            .global::<crate::pointer_motion::WindowPointerMotionService>()
243            .and_then(|svc| svc.position_window(window, pointer_id))
244    }
245
246    /// Latest pointer velocity snapshot in window-local logical pixels per second (ADR 0243).
247    pub fn pointer_velocity_window_snapshot(
248        &self,
249        pointer_id: fret_core::PointerId,
250    ) -> Option<Point> {
251        let window = self.window?;
252        self.app
253            .global::<crate::pointer_motion::WindowPointerMotionService>()
254            .and_then(|svc| svc.velocity_window(window, pointer_id))
255    }
256
257    pub fn invalidate(&mut self, node: NodeId, kind: Invalidation) {
258        self.invalidations.push((node, kind));
259    }
260
261    /// Request invalidation for all live nodes currently bound to a scroll handle.
262    ///
263    /// Resolution is deferred until the dispatch runtime regains access to `UiTree`, so widgets
264    /// do not need to reason about stale registry bindings or attachment state.
265    pub fn invalidate_scroll_handle_bindings(&mut self, handle_key: usize, kind: Invalidation) {
266        self.scroll_handle_invalidations
267            .push(ScrollHandleInvalidationRequest { handle_key, kind });
268    }
269
270    /// Request invalidation for the live attached node currently associated with an element-backed
271    /// scroll target.
272    ///
273    /// Resolution is deferred until the dispatch runtime regains access to `UiTree`, so widgets
274    /// do not need to interpret same-frame retained bookkeeping directly.
275    pub(crate) fn invalidate_scroll_target(&mut self, element: crate::GlobalElementId) {
276        self.scroll_target_invalidations.push(element);
277    }
278
279    pub fn invalidate_self(&mut self, kind: Invalidation) {
280        self.invalidate(self.node, kind);
281    }
282
283    pub fn dispatch_command(&mut self, command: CommandId) {
284        self.app.push_effect(Effect::Command {
285            window: self.window,
286            command,
287        });
288    }
289
290    pub fn request_focus(&mut self, node: NodeId) {
291        self.requested_focus = Some(node);
292    }
293
294    pub fn capture_pointer(&mut self, node: NodeId) {
295        if self.pointer_id.is_none() {
296            return;
297        }
298        self.requested_capture = Some(Some(node));
299    }
300
301    pub fn release_pointer_capture(&mut self) {
302        if self.pointer_id.is_none() {
303            return;
304        }
305        self.requested_capture = Some(None);
306    }
307
308    pub fn stop_propagation(&mut self) {
309        self.stop_propagation = true;
310    }
311
312    pub fn prevent_default(&mut self, action: DefaultAction) {
313        self.prevented_default_actions.insert(action);
314    }
315
316    pub fn default_prevented(&self, action: DefaultAction) -> bool {
317        self.prevented_default_actions.contains(action)
318    }
319
320    /// Request a window redraw (one-shot).
321    ///
322    /// Use this for one-shot updates after state changes (e.g. responding to input).
323    ///
324    /// Notes:
325    /// - A redraw does not necessarily imply a fresh widget `paint()` pass if the UI tree can
326    ///   replay a valid paint cache entry. If you need frame-driven updates, prefer
327    ///   `request_animation_frame()` (from `LayoutCx`/`PaintCx`/`MeasureCx`) which also ensures
328    ///   `Invalidation::Paint` is set.
329    /// - `request_redraw()` is not a timer. If you need continuous progression without input
330    ///   (animations, progressive rendering), you must request the next frame via
331    ///   `request_animation_frame()` (or a higher-level continuous-frames helper).
332    /// - A redraw request may be coalesced and does not necessarily wake a sleeping event loop on
333    ///   all platforms. Prefer `request_animation_frame()` for frame-driven progression.
334    pub fn request_redraw(&mut self) {
335        let Some(window) = self.window else {
336            return;
337        };
338        self.app.request_redraw(window);
339    }
340
341    /// Mark the current view as dirty and schedule a redraw.
342    ///
343    /// In view-cache mode, this forces the nearest cache root to rerender (skip view-cache reuse)
344    /// and prevents paint replay of stale recorded ranges.
345    #[track_caller]
346    pub fn notify(&mut self) {
347        self.notify_requested = true;
348        if self.notify_requested_location.is_none() {
349            let caller = std::panic::Location::caller();
350            self.notify_requested_location = Some(UiSourceLocation {
351                file: caller.file(),
352                line: caller.line(),
353                column: caller.column(),
354            });
355        }
356    }
357
358    pub fn set_cursor_icon(&mut self, icon: fret_core::CursorIcon) {
359        if !self.input_ctx.caps.ui.cursor_icons {
360            return;
361        }
362        self.requested_cursor = Some(icon);
363    }
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub(crate) struct ScrollHandleInvalidationRequest {
368    pub(crate) handle_key: usize,
369    pub(crate) kind: Invalidation,
370}
371
372/// Observer-only event context for the `InputDispatchPhase::Preview` pass.
373///
374/// This pass exists to support "click-through outside-press" policies (ADR 0069) without allowing
375/// widgets to mutate input routing state (focus / capture / propagation / default actions).
376pub struct ObserverCx<'a, H: UiHost> {
377    pub app: &'a mut H,
378    pub services: &'a mut dyn UiServices,
379    pub node: NodeId,
380    pub window: Option<AppWindowId>,
381    pub pointer_id: Option<fret_core::PointerId>,
382    pub input_ctx: InputContext,
383    pub children: &'a [NodeId],
384    pub focus: Option<NodeId>,
385    pub captured: Option<NodeId>,
386    pub bounds: Rect,
387    pub invalidations: Vec<(NodeId, Invalidation)>,
388    pub notify_requested: bool,
389    pub notify_requested_location: Option<UiSourceLocation>,
390}
391
392impl<'a, H: UiHost> ObserverCx<'a, H> {
393    pub fn theme(&self) -> &Theme {
394        Theme::global(&*self.app)
395    }
396
397    pub fn invalidate(&mut self, node: NodeId, kind: Invalidation) {
398        self.invalidations.push((node, kind));
399    }
400
401    pub fn invalidate_self(&mut self, kind: Invalidation) {
402        self.invalidate(self.node, kind);
403    }
404
405    pub fn dispatch_command(&mut self, command: CommandId) {
406        self.app.push_effect(Effect::Command {
407            window: self.window,
408            command,
409        });
410    }
411
412    /// Request a window redraw (one-shot).
413    pub fn request_redraw(&mut self) {
414        let Some(window) = self.window else {
415            return;
416        };
417        self.app.request_redraw(window);
418    }
419
420    /// Mark the current view as dirty and schedule a redraw.
421    ///
422    /// In view-cache mode, this forces the nearest cache root to rerender (skip view-cache reuse)
423    /// and prevents paint replay of stale recorded ranges.
424    #[track_caller]
425    pub fn notify(&mut self) {
426        self.notify_requested = true;
427        if self.notify_requested_location.is_none() {
428            let caller = std::panic::Location::caller();
429            self.notify_requested_location = Some(UiSourceLocation {
430                file: caller.file(),
431                line: caller.line(),
432                column: caller.column(),
433            });
434        }
435    }
436}
437
438pub struct CommandCx<'a, H: UiHost> {
439    pub app: &'a mut H,
440    pub services: &'a mut dyn UiServices,
441    pub tree: &'a mut crate::tree::UiTree<H>,
442    pub node: NodeId,
443    pub window: Option<AppWindowId>,
444    pub input_ctx: InputContext,
445    pub focus: Option<NodeId>,
446    pub invalidations: Vec<(NodeId, Invalidation)>,
447    pub requested_focus: Option<NodeId>,
448    pub notify_requested: bool,
449    pub notify_requested_location: Option<UiSourceLocation>,
450    pub stop_propagation: bool,
451}
452
453impl<'a, H: UiHost> CommandCx<'a, H> {
454    pub fn theme(&self) -> &Theme {
455        Theme::global(&*self.app)
456    }
457
458    pub fn invalidate(&mut self, node: NodeId, kind: Invalidation) {
459        self.invalidations.push((node, kind));
460    }
461
462    pub fn invalidate_self(&mut self, kind: Invalidation) {
463        self.invalidate(self.node, kind);
464    }
465
466    pub fn request_focus(&mut self, node: NodeId) {
467        self.requested_focus = Some(node);
468    }
469
470    pub fn stop_propagation(&mut self) {
471        self.stop_propagation = true;
472    }
473
474    /// Request a window redraw.
475    ///
476    /// Use this for one-shot updates after state changes. For frame-driven updates (animations,
477    /// progressive rendering), prefer `request_animation_frame()` when available.
478    pub fn request_redraw(&mut self) {
479        let Some(window) = self.window else {
480            return;
481        };
482        self.app.request_redraw(window);
483    }
484
485    /// Mark the current view as dirty and schedule a redraw.
486    ///
487    /// In view-cache mode, this forces the nearest cache root to rerender (skip view-cache reuse)
488    /// and prevents paint replay of stale recorded ranges.
489    #[track_caller]
490    pub fn notify(&mut self) {
491        self.notify_requested = true;
492        if self.notify_requested_location.is_none() {
493            let caller = std::panic::Location::caller();
494            self.notify_requested_location = Some(UiSourceLocation {
495                file: caller.file(),
496                line: caller.line(),
497                column: caller.column(),
498            });
499        }
500    }
501}
502
503/// Command availability query result used by `UiTree::is_command_available` (ADR 0218).
504///
505/// This is a pure query signal (no side effects). Consumers typically interpret:
506/// - `Available`: command should be treated as enabled for the current dispatch path.
507/// - `Blocked`: command must not bubble further to ancestors for availability purposes.
508/// - `NotHandled`: this node does not participate in availability for this command.
509#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
510pub enum CommandAvailability {
511    #[default]
512    NotHandled,
513    Available,
514    Blocked,
515}
516
517/// Context passed to `Widget::command_availability`.
518///
519/// This is intentionally read-only (no `UiServices`, no invalidations) to keep availability a pure
520/// query.
521pub struct CommandAvailabilityCx<'a, H: UiHost> {
522    pub app: &'a mut H,
523    pub tree: &'a crate::tree::UiTree<H>,
524    pub node: NodeId,
525    pub window: Option<AppWindowId>,
526    pub input_ctx: InputContext,
527    pub focus: Option<NodeId>,
528}
529
530pub struct LayoutCx<'a, H: UiHost> {
531    pub app: &'a mut H,
532    pub tree: &'a mut crate::tree::UiTree<H>,
533    pub node: NodeId,
534    pub window: Option<AppWindowId>,
535    pub focus: Option<NodeId>,
536    pub children: &'a [NodeId],
537    pub bounds: Rect,
538    pub available: Size,
539    pub pass_kind: LayoutPassKind,
540    pub overflow_ctx: crate::layout::overflow::LayoutOverflowContext,
541    pub scale_factor: f32,
542    pub services: &'a mut dyn UiServices,
543    pub observe_model: &'a mut dyn FnMut(ModelId, Invalidation),
544    pub observe_global: &'a mut dyn FnMut(TypeId, Invalidation),
545}
546
547impl<'a, H: UiHost> LayoutCx<'a, H> {
548    pub fn probe_constraints_for_size(&self, size: Size) -> LayoutConstraints {
549        self.overflow_ctx.probe_constraints_for_size(size)
550    }
551
552    pub fn with_overflow_context<R>(
553        &mut self,
554        overflow_ctx: crate::layout::overflow::LayoutOverflowContext,
555        f: impl FnOnce(&mut Self) -> R,
556    ) -> R {
557        let prev = self.overflow_ctx;
558        self.overflow_ctx = overflow_ctx;
559        let out = f(self);
560        self.overflow_ctx = prev;
561        out
562    }
563
564    pub fn theme(&mut self) -> &Theme {
565        self.observe_global::<Theme>(Invalidation::Layout);
566        Theme::global(&*self.app)
567    }
568
569    /// Request a window redraw (one-shot).
570    ///
571    /// This schedules a paint of the current UI state. If you need continuous frame progression
572    /// (e.g. animations or progressive rendering without input), use `request_animation_frame()`.
573    pub fn request_redraw(&mut self) {
574        let Some(window) = self.window else {
575            return;
576        };
577        self.app.request_redraw(window);
578    }
579
580    /// Request the next animation frame for this window.
581    ///
582    /// Use this for frame-driven behaviors (animations, progress indicators, progressive
583    /// rendering) where the UI must keep repainting even if there are no incoming events.
584    ///
585    /// This is a one-shot request. Code that animates should re-issue
586    /// `request_animation_frame()` each frame while it remains active.
587    ///
588    /// This method also ensures `Invalidation::Paint` is set for the calling node so paint caching
589    /// cannot short-circuit the widget `paint()` pass on the next frame.
590    pub fn request_animation_frame(&mut self) {
591        // Ensure animation-frame requests trigger a paint pass even when paint caching is enabled.
592        self.tree.invalidate_with_source_and_detail(
593            self.node,
594            Invalidation::Paint,
595            crate::tree::UiDebugInvalidationSource::Notify,
596            crate::tree::UiDebugInvalidationDetail::AnimationFrameRequest,
597        );
598        let Some(window) = self.window else {
599            return;
600        };
601        self.app.push_effect(Effect::RequestAnimationFrame(window));
602    }
603
604    /// Best-effort frame clock snapshot for the current window (ADR 0240).
605    ///
606    /// This is intentionally a plain read (non-reactive): it does not participate in view-cache
607    /// dependency tracking.
608    pub fn frame_clock(&self) -> Option<fret_core::WindowFrameClockSnapshot> {
609        let window = self.window?;
610        self.app
611            .global::<fret_core::WindowFrameClockService>()
612            .and_then(|svc| svc.snapshot(window))
613    }
614
615    /// Latest pointer position snapshot in window-local logical pixels (ADR 0243).
616    pub fn pointer_position_window_snapshot(
617        &self,
618        pointer_id: fret_core::PointerId,
619    ) -> Option<Point> {
620        let window = self.window?;
621        self.app
622            .global::<crate::pointer_motion::WindowPointerMotionService>()
623            .and_then(|svc| svc.position_window(window, pointer_id))
624    }
625
626    /// Latest pointer velocity snapshot in window-local logical pixels per second (ADR 0243).
627    pub fn pointer_velocity_window_snapshot(
628        &self,
629        pointer_id: fret_core::PointerId,
630    ) -> Option<Point> {
631        let window = self.window?;
632        self.app
633            .global::<crate::pointer_motion::WindowPointerMotionService>()
634            .and_then(|svc| svc.velocity_window(window, pointer_id))
635    }
636
637    /// Latest pointer position snapshot mapped into this node's local coordinate space
638    /// (origin at `(0, 0)`), transform-aware (ADR 0238 / ADR 0243).
639    pub fn pointer_position_local_snapshot(
640        &self,
641        pointer_id: fret_core::PointerId,
642    ) -> Option<Point> {
643        let window_pos = self.pointer_position_window_snapshot(pointer_id)?;
644        let mapped = self
645            .tree
646            .map_window_point_to_node_layout_space(self.node, window_pos)?;
647        Some(Point::new(
648            fret_core::Px(mapped.x.0 - self.bounds.origin.x.0),
649            fret_core::Px(mapped.y.0 - self.bounds.origin.y.0),
650        ))
651    }
652
653    /// Latest pointer velocity snapshot mapped into this node's local coordinate space (ADR 0238 / ADR 0243).
654    pub fn pointer_velocity_local_snapshot(
655        &self,
656        pointer_id: fret_core::PointerId,
657    ) -> Option<Point> {
658        let window_vec = self.pointer_velocity_window_snapshot(pointer_id)?;
659        self.tree
660            .map_window_vector_to_node_layout_space(self.node, window_vec)
661    }
662
663    pub fn observe_model<T>(&mut self, model: &Model<T>, invalidation: Invalidation) {
664        (self.observe_model)(model.id(), invalidation);
665    }
666
667    pub fn observe_global<T: Any>(&mut self, invalidation: Invalidation) {
668        (self.observe_global)(TypeId::of::<T>(), invalidation);
669    }
670
671    pub fn layout(&mut self, child: NodeId, available: Size) -> Size {
672        let rect = Rect::new(self.bounds.origin, available);
673        self.layout_in(child, rect)
674    }
675
676    pub fn layout_in(&mut self, child: NodeId, bounds: Rect) -> Size {
677        self.tree.layout_in_with_pass_kind(
678            self.app,
679            self.services,
680            child,
681            bounds,
682            self.scale_factor,
683            self.pass_kind,
684            self.overflow_ctx,
685        )
686    }
687
688    pub fn layout_in_probe(&mut self, child: NodeId, bounds: Rect) -> Size {
689        self.tree.layout_in_with_pass_kind(
690            self.app,
691            self.services,
692            child,
693            bounds,
694            self.scale_factor,
695            LayoutPassKind::Probe,
696            self.overflow_ctx,
697        )
698    }
699
700    pub fn layout_engine_child_bounds(&mut self, child: NodeId) -> Option<Rect> {
701        let local = self
702            .tree
703            .layout_engine_child_local_rect_profiled(self.node, child)?;
704        Some(Rect::new(
705            Point::new(
706                fret_core::Px(self.bounds.origin.x.0 + local.origin.x.0),
707                fret_core::Px(self.bounds.origin.y.0 + local.origin.y.0),
708            ),
709            local.size,
710        ))
711    }
712
713    pub fn layout_viewport_root(&mut self, child: NodeId, bounds: Rect) -> Size {
714        if self.pass_kind == LayoutPassKind::Probe {
715            return bounds.size;
716        }
717        self.tree.register_viewport_root(child, bounds);
718        bounds.size
719    }
720
721    pub fn solve_barrier_child_root(&mut self, child: NodeId, bounds: Rect) {
722        if self.pass_kind != LayoutPassKind::Final {
723            return;
724        }
725        self.tree.solve_barrier_flow_root(
726            self.app,
727            self.services,
728            child,
729            bounds,
730            self.scale_factor,
731        );
732    }
733
734    pub fn solve_barrier_child_root_if_needed(&mut self, child: NodeId, bounds: Rect) {
735        if self.pass_kind != LayoutPassKind::Final {
736            return;
737        }
738        self.tree.solve_barrier_flow_root_if_needed(
739            self.app,
740            self.services,
741            child,
742            bounds,
743            self.scale_factor,
744        );
745    }
746
747    pub fn solve_barrier_child_roots_if_needed(&mut self, roots: &[(NodeId, Rect)]) {
748        if self.pass_kind != LayoutPassKind::Final {
749            return;
750        }
751        self.tree.solve_barrier_flow_roots_if_needed(
752            self.app,
753            self.services,
754            roots,
755            self.scale_factor,
756        );
757    }
758    pub fn measure_in(&mut self, child: NodeId, constraints: LayoutConstraints) -> Size {
759        self.tree.measure_in(
760            self.app,
761            self.services,
762            child,
763            constraints,
764            self.scale_factor,
765        )
766    }
767}
768
769pub struct MeasureCx<'a, H: UiHost> {
770    pub app: &'a mut H,
771    pub tree: &'a mut crate::tree::UiTree<H>,
772    pub node: NodeId,
773    pub window: Option<AppWindowId>,
774    pub focus: Option<NodeId>,
775    pub children: &'a [NodeId],
776    pub constraints: LayoutConstraints,
777    pub scale_factor: f32,
778    pub services: &'a mut dyn UiServices,
779    pub observe_model: &'a mut dyn FnMut(ModelId, Invalidation),
780    pub observe_global: &'a mut dyn FnMut(TypeId, Invalidation),
781}
782
783impl<'a, H: UiHost> MeasureCx<'a, H> {
784    pub fn theme(&mut self) -> &Theme {
785        self.observe_global::<Theme>(Invalidation::Layout);
786        Theme::global(&*self.app)
787    }
788
789    /// Request a window redraw (one-shot).
790    ///
791    /// This is typically used after mutating model/state in response to user input. For
792    /// frame-driven updates, use `request_animation_frame()`.
793    pub fn request_redraw(&mut self) {
794        let Some(window) = self.window else {
795            return;
796        };
797        self.app.request_redraw(window);
798    }
799
800    /// Request the next animation frame for this window.
801    ///
802    /// Use this for animations/progressive rendering that must advance without input events.
803    ///
804    /// This is a one-shot request. Callers should re-issue `request_animation_frame()` each frame
805    /// while it remains active.
806    /// This also sets `Invalidation::Paint` for the current node so paint caching cannot skip
807    /// widget `paint()` on the next frame.
808    pub fn request_animation_frame(&mut self) {
809        // Ensure animation-frame requests trigger a paint pass even when paint caching is enabled.
810        self.tree.invalidate_with_source_and_detail(
811            self.node,
812            Invalidation::Paint,
813            crate::tree::UiDebugInvalidationSource::Notify,
814            crate::tree::UiDebugInvalidationDetail::AnimationFrameRequest,
815        );
816        let Some(window) = self.window else {
817            return;
818        };
819        self.app.push_effect(Effect::RequestAnimationFrame(window));
820    }
821
822    /// Best-effort frame clock snapshot for the current window (ADR 0240).
823    ///
824    /// This is intentionally a plain read (non-reactive): it does not participate in view-cache
825    /// dependency tracking.
826    pub fn frame_clock(&self) -> Option<fret_core::WindowFrameClockSnapshot> {
827        let window = self.window?;
828        self.app
829            .global::<fret_core::WindowFrameClockService>()
830            .and_then(|svc| svc.snapshot(window))
831    }
832
833    /// Latest pointer position snapshot in window-local logical pixels (ADR 0243).
834    pub fn pointer_position_window_snapshot(
835        &self,
836        pointer_id: fret_core::PointerId,
837    ) -> Option<Point> {
838        let window = self.window?;
839        self.app
840            .global::<crate::pointer_motion::WindowPointerMotionService>()
841            .and_then(|svc| svc.position_window(window, pointer_id))
842    }
843
844    /// Latest pointer velocity snapshot in window-local logical pixels per second (ADR 0243).
845    pub fn pointer_velocity_window_snapshot(
846        &self,
847        pointer_id: fret_core::PointerId,
848    ) -> Option<Point> {
849        let window = self.window?;
850        self.app
851            .global::<crate::pointer_motion::WindowPointerMotionService>()
852            .and_then(|svc| svc.velocity_window(window, pointer_id))
853    }
854
855    /// Latest pointer position snapshot mapped into this node's local coordinate space
856    /// (origin at `(0, 0)`), transform-aware (ADR 0238 / ADR 0243).
857    pub fn pointer_position_local_snapshot(
858        &self,
859        pointer_id: fret_core::PointerId,
860    ) -> Option<Point> {
861        let window_pos = self.pointer_position_window_snapshot(pointer_id)?;
862        let bounds = self.tree.node_bounds(self.node)?;
863        let mapped = self
864            .tree
865            .map_window_point_to_node_layout_space(self.node, window_pos)?;
866        Some(Point::new(
867            fret_core::Px(mapped.x.0 - bounds.origin.x.0),
868            fret_core::Px(mapped.y.0 - bounds.origin.y.0),
869        ))
870    }
871
872    /// Latest pointer velocity snapshot mapped into this node's local coordinate space (ADR 0238 / ADR 0243).
873    pub fn pointer_velocity_local_snapshot(
874        &self,
875        pointer_id: fret_core::PointerId,
876    ) -> Option<Point> {
877        let window_vec = self.pointer_velocity_window_snapshot(pointer_id)?;
878        self.tree
879            .map_window_vector_to_node_layout_space(self.node, window_vec)
880    }
881
882    pub fn observe_model<T>(&mut self, model: &Model<T>, invalidation: Invalidation) {
883        (self.observe_model)(model.id(), invalidation);
884    }
885
886    pub fn observe_global<T: Any>(&mut self, invalidation: Invalidation) {
887        (self.observe_global)(TypeId::of::<T>(), invalidation);
888    }
889
890    pub fn measure_in(&mut self, child: NodeId, constraints: LayoutConstraints) -> Size {
891        if !self.tree.debug_enabled() {
892            return self.tree.measure_in(
893                self.app,
894                self.services,
895                child,
896                constraints,
897                self.scale_factor,
898            );
899        }
900
901        let started = fret_core::time::Instant::now();
902        let size = self.tree.measure_in(
903            self.app,
904            self.services,
905            child,
906            constraints,
907            self.scale_factor,
908        );
909        let elapsed = started.elapsed();
910        self.tree
911            .debug_record_measure_child(self.node, child, elapsed);
912        size
913    }
914}
915
916/// Prepaint context invoked after layout, before paint.
917///
918/// This is intentionally narrow: it exists to support GPUI-aligned "ephemeral prepaint items"
919/// workflows (ADR 0167 / ADR 0175) without forcing a full rerender/relayout of a cache root.
920///
921/// Notes:
922/// - Prepaint runs after layout bounds are known.
923/// - Prepaint may request redraw/animation frames, but should avoid structural tree mutations.
924pub struct PrepaintCx<'a, H: UiHost> {
925    pub app: &'a mut H,
926    pub tree: &'a mut crate::tree::UiTree<H>,
927    pub node: NodeId,
928    pub window: Option<AppWindowId>,
929    pub bounds: Rect,
930    pub scale_factor: f32,
931}
932
933impl<'a, H: UiHost> PrepaintCx<'a, H> {
934    pub fn set_output<T: std::any::Any>(&mut self, value: T) {
935        self.tree.set_prepaint_output(self.node, value);
936    }
937
938    pub fn output<T: std::any::Any>(&mut self) -> Option<&T> {
939        self.tree.prepaint_output(self.node)
940    }
941
942    pub fn output_mut<T: std::any::Any>(&mut self) -> Option<&mut T> {
943        self.tree.prepaint_output_mut(self.node)
944    }
945
946    /// Mark an invalidation on `node` for the next frame.
947    ///
948    /// Prefer `Invalidation::Paint` / `Invalidation::HitTest` here. Invalidating `Layout` from
949    /// prepaint is allowed but can easily introduce avoidable churn.
950    pub fn invalidate(&mut self, node: NodeId, kind: Invalidation) {
951        self.tree
952            .debug_record_prepaint_action(crate::tree::UiDebugPrepaintAction {
953                node: self.node,
954                target: Some(node),
955                kind: crate::tree::UiDebugPrepaintActionKind::Invalidate,
956                invalidation: Some(kind),
957                element: None,
958                virtual_list_window_shift_kind: None,
959                virtual_list_window_shift_reason: None,
960                chart_sampling_window_key: None,
961                node_graph_cull_window_key: None,
962                frame_id: self.app.frame_id(),
963            });
964        self.tree.invalidate_with_detail(
965            node,
966            kind,
967            crate::tree::UiDebugInvalidationDetail::Unknown,
968        );
969    }
970
971    /// Mark an invalidation on the current node for the next frame.
972    pub fn invalidate_self(&mut self, kind: Invalidation) {
973        self.invalidate(self.node, kind);
974    }
975
976    /// Request a window redraw (one-shot).
977    ///
978    /// Use this for one-shot updates after prepaint-driven state changes.
979    pub fn request_redraw(&mut self) {
980        self.tree
981            .debug_record_prepaint_action(crate::tree::UiDebugPrepaintAction {
982                node: self.node,
983                target: None,
984                kind: crate::tree::UiDebugPrepaintActionKind::RequestRedraw,
985                invalidation: None,
986                element: None,
987                virtual_list_window_shift_kind: None,
988                virtual_list_window_shift_reason: None,
989                chart_sampling_window_key: None,
990                node_graph_cull_window_key: None,
991                frame_id: self.app.frame_id(),
992            });
993        let Some(window) = self.window else {
994            return;
995        };
996        self.app.request_redraw(window);
997    }
998
999    /// Request the next animation frame for this window.
1000    ///
1001    /// Prefer this over `request_redraw()` when you need frame-driven progression (animations,
1002    /// progressive rendering). This also sets `Invalidation::Paint` for the current node so paint
1003    /// caching cannot skip widget `paint()` on the next frame.
1004    pub fn request_animation_frame(&mut self) {
1005        self.tree
1006            .debug_record_prepaint_action(crate::tree::UiDebugPrepaintAction {
1007                node: self.node,
1008                target: Some(self.node),
1009                kind: crate::tree::UiDebugPrepaintActionKind::RequestAnimationFrame,
1010                invalidation: Some(Invalidation::Paint),
1011                element: None,
1012                virtual_list_window_shift_kind: None,
1013                virtual_list_window_shift_reason: None,
1014                chart_sampling_window_key: None,
1015                node_graph_cull_window_key: None,
1016                frame_id: self.app.frame_id(),
1017            });
1018        // Ensure animation-frame requests trigger a paint pass even when paint caching is enabled.
1019        self.tree.invalidate_with_source_and_detail(
1020            self.node,
1021            Invalidation::Paint,
1022            crate::tree::UiDebugInvalidationSource::Notify,
1023            crate::tree::UiDebugInvalidationDetail::AnimationFrameRequest,
1024        );
1025        let Some(window) = self.window else {
1026            return;
1027        };
1028        self.app.push_effect(Effect::RequestAnimationFrame(window));
1029    }
1030
1031    /// Records a debug-only "sampling window shift" prepaint action.
1032    ///
1033    /// This is intended for ecosystem canvases (charts/plots) that maintain an explicit sampling
1034    /// window contract and want to expose a stable output key in diagnostics bundles.
1035    pub fn debug_record_chart_sampling_window_shift(&mut self, sampling_window_key: u64) {
1036        self.tree
1037            .debug_record_prepaint_action(crate::tree::UiDebugPrepaintAction {
1038                node: self.node,
1039                target: None,
1040                kind: crate::tree::UiDebugPrepaintActionKind::ChartSamplingWindowShift,
1041                invalidation: None,
1042                element: None,
1043                virtual_list_window_shift_kind: None,
1044                virtual_list_window_shift_reason: None,
1045                chart_sampling_window_key: Some(sampling_window_key),
1046                node_graph_cull_window_key: None,
1047                frame_id: self.app.frame_id(),
1048            });
1049    }
1050
1051    /// Records a debug-only "cull window shift" prepaint action.
1052    ///
1053    /// This is intended for ecosystem canvases (e.g. node graphs) that maintain a windowed
1054    /// viewport culling contract and want to expose a stable output key in diagnostics bundles.
1055    pub fn debug_record_node_graph_cull_window_shift(&mut self, cull_window_key: u64) {
1056        self.tree
1057            .debug_record_prepaint_action(crate::tree::UiDebugPrepaintAction {
1058                node: self.node,
1059                target: None,
1060                kind: crate::tree::UiDebugPrepaintActionKind::NodeGraphCullWindowShift,
1061                invalidation: None,
1062                element: None,
1063                virtual_list_window_shift_kind: None,
1064                virtual_list_window_shift_reason: None,
1065                chart_sampling_window_key: None,
1066                node_graph_cull_window_key: Some(cull_window_key),
1067                frame_id: self.app.frame_id(),
1068            });
1069    }
1070}
1071
1072pub struct PaintCx<'a, H: UiHost> {
1073    pub app: &'a mut H,
1074    pub tree: &'a mut crate::tree::UiTree<H>,
1075    pub node: NodeId,
1076    pub window: Option<AppWindowId>,
1077    pub focus: Option<NodeId>,
1078    pub children: &'a [NodeId],
1079    pub bounds: Rect,
1080    pub scale_factor: f32,
1081    pub(crate) paint_style: crate::tree::paint_style::PaintStyleState,
1082    pub accumulated_transform: Transform2D,
1083    pub children_render_transform: Option<Transform2D>,
1084    pub services: &'a mut dyn UiServices,
1085    pub observe_model: &'a mut dyn FnMut(ModelId, Invalidation),
1086    pub observe_global: &'a mut dyn FnMut(TypeId, Invalidation),
1087    pub scene: &'a mut Scene,
1088}
1089
1090impl<'a, H: UiHost> PaintCx<'a, H> {
1091    #[allow(clippy::too_many_arguments)]
1092    pub fn new(
1093        app: &'a mut H,
1094        tree: &'a mut crate::tree::UiTree<H>,
1095        node: NodeId,
1096        window: Option<AppWindowId>,
1097        focus: Option<NodeId>,
1098        children: &'a [NodeId],
1099        bounds: Rect,
1100        scale_factor: f32,
1101        accumulated_transform: Transform2D,
1102        children_render_transform: Option<Transform2D>,
1103        services: &'a mut dyn UiServices,
1104        observe_model: &'a mut dyn FnMut(ModelId, Invalidation),
1105        observe_global: &'a mut dyn FnMut(TypeId, Invalidation),
1106        scene: &'a mut Scene,
1107    ) -> Self {
1108        Self {
1109            app,
1110            tree,
1111            node,
1112            window,
1113            focus,
1114            children,
1115            bounds,
1116            scale_factor,
1117            paint_style: Default::default(),
1118            accumulated_transform,
1119            children_render_transform,
1120            services,
1121            observe_model,
1122            observe_global,
1123            scene,
1124        }
1125    }
1126
1127    /// Returns the nearest inherited foreground color for the current paint traversal (v2).
1128    pub fn inherited_foreground(&self) -> Option<fret_core::Color> {
1129        self.paint_style.foreground
1130    }
1131
1132    pub fn prepaint_output<T: std::any::Any>(&mut self) -> Option<&T> {
1133        self.tree.prepaint_output(self.node)
1134    }
1135
1136    pub fn prepaint_output_mut<T: std::any::Any>(&mut self) -> Option<&mut T> {
1137        self.tree.prepaint_output_mut(self.node)
1138    }
1139
1140    pub fn theme(&mut self) -> &Theme {
1141        self.observe_global::<Theme>(Invalidation::Paint);
1142        Theme::global(&*self.app)
1143    }
1144
1145    /// Best-effort frame clock snapshot for the current window (ADR 0240).
1146    ///
1147    /// This is intentionally a plain read (non-reactive): it does not participate in view-cache
1148    /// dependency tracking.
1149    pub fn frame_clock(&self) -> Option<fret_core::WindowFrameClockSnapshot> {
1150        let window = self.window?;
1151        self.app
1152            .global::<fret_core::WindowFrameClockService>()
1153            .and_then(|svc| svc.snapshot(window))
1154    }
1155
1156    /// Latest pointer position snapshot in window-local logical pixels (ADR 0243).
1157    pub fn pointer_position_window_snapshot(
1158        &self,
1159        pointer_id: fret_core::PointerId,
1160    ) -> Option<Point> {
1161        let window = self.window?;
1162        self.app
1163            .global::<crate::pointer_motion::WindowPointerMotionService>()
1164            .and_then(|svc| svc.position_window(window, pointer_id))
1165    }
1166
1167    /// Latest pointer velocity snapshot in window-local logical pixels per second (ADR 0243).
1168    pub fn pointer_velocity_window_snapshot(
1169        &self,
1170        pointer_id: fret_core::PointerId,
1171    ) -> Option<Point> {
1172        let window = self.window?;
1173        self.app
1174            .global::<crate::pointer_motion::WindowPointerMotionService>()
1175            .and_then(|svc| svc.velocity_window(window, pointer_id))
1176    }
1177
1178    /// Latest pointer position snapshot mapped into this node's local coordinate space
1179    /// (origin at `(0, 0)`), transform-aware (ADR 0238 / ADR 0243).
1180    pub fn pointer_position_local_snapshot(
1181        &self,
1182        pointer_id: fret_core::PointerId,
1183    ) -> Option<Point> {
1184        let window_pos = self.pointer_position_window_snapshot(pointer_id)?;
1185        let mapped = self
1186            .tree
1187            .map_window_point_to_node_layout_space(self.node, window_pos)?;
1188        Some(Point::new(
1189            fret_core::Px(mapped.x.0 - self.bounds.origin.x.0),
1190            fret_core::Px(mapped.y.0 - self.bounds.origin.y.0),
1191        ))
1192    }
1193
1194    /// Latest pointer velocity snapshot mapped into this node's local coordinate space (ADR 0238 / ADR 0243).
1195    pub fn pointer_velocity_local_snapshot(
1196        &self,
1197        pointer_id: fret_core::PointerId,
1198    ) -> Option<Point> {
1199        let window_vec = self.pointer_velocity_window_snapshot(pointer_id)?;
1200        self.tree
1201            .map_window_vector_to_node_layout_space(self.node, window_vec)
1202    }
1203
1204    /// Convert a layout-space rect into the visual rect (AABB) after accumulated render transforms.
1205    ///
1206    /// This is useful for platform integrations (e.g. IME candidate positioning), where the OS
1207    /// expects coordinates in the same space the user sees on screen.
1208    pub fn visual_rect_aabb(&self, rect: Rect) -> Rect {
1209        let t = self.accumulated_transform;
1210        if t == Transform2D::IDENTITY {
1211            return rect;
1212        }
1213
1214        let x0 = rect.origin.x.0;
1215        let y0 = rect.origin.y.0;
1216        let x1 = x0 + rect.size.width.0;
1217        let y1 = y0 + rect.size.height.0;
1218
1219        let p00 = t.apply_point(Point::new(fret_core::Px(x0), fret_core::Px(y0)));
1220        let p10 = t.apply_point(Point::new(fret_core::Px(x1), fret_core::Px(y0)));
1221        let p01 = t.apply_point(Point::new(fret_core::Px(x0), fret_core::Px(y1)));
1222        let p11 = t.apply_point(Point::new(fret_core::Px(x1), fret_core::Px(y1)));
1223
1224        let min_x = p00.x.0.min(p10.x.0).min(p01.x.0).min(p11.x.0);
1225        let max_x = p00.x.0.max(p10.x.0).max(p01.x.0).max(p11.x.0);
1226        let min_y = p00.y.0.min(p10.y.0).min(p01.y.0).min(p11.y.0);
1227        let max_y = p00.y.0.max(p10.y.0).max(p01.y.0).max(p11.y.0);
1228
1229        if !min_x.is_finite() || !max_x.is_finite() || !min_y.is_finite() || !max_y.is_finite() {
1230            return rect;
1231        }
1232
1233        Rect::new(
1234            Point::new(fret_core::Px(min_x), fret_core::Px(min_y)),
1235            Size::new(
1236                fret_core::Px((max_x - min_x).max(0.0)),
1237                fret_core::Px((max_y - min_y).max(0.0)),
1238            ),
1239        )
1240    }
1241
1242    /// Request a window redraw (one-shot).
1243    ///
1244    /// Use this for one-shot updates. For frame-driven updates that must repaint continuously,
1245    /// use `request_animation_frame()`.
1246    pub fn request_redraw(&mut self) {
1247        let Some(window) = self.window else {
1248            return;
1249        };
1250        self.app.request_redraw(window);
1251    }
1252
1253    /// Request the next animation frame for this window.
1254    ///
1255    /// Prefer this over `request_redraw()` when you need frame-driven progression (animations,
1256    /// progressive rendering). This also sets `Invalidation::Paint` for the current node so paint
1257    /// caching cannot skip widget `paint()` on the next frame.
1258    ///
1259    /// This is a one-shot request. Callers should re-issue `request_animation_frame()` each frame
1260    /// while it remains active.
1261    pub fn request_animation_frame(&mut self) {
1262        // Ensure animation-frame requests trigger a paint pass even when paint caching is enabled.
1263        self.tree.invalidate_with_source_and_detail(
1264            self.node,
1265            Invalidation::Paint,
1266            crate::tree::UiDebugInvalidationSource::Notify,
1267            crate::tree::UiDebugInvalidationDetail::AnimationFrameRequest,
1268        );
1269        let Some(window) = self.window else {
1270            return;
1271        };
1272        self.app.push_effect(Effect::RequestAnimationFrame(window));
1273    }
1274
1275    /// Request the next animation frame for this window without marking the nearest cache root as
1276    /// dirty.
1277    ///
1278    /// This is intended for paint-only chrome (hover fades, drag indicators, caret blink) that
1279    /// must repaint every frame but should remain structurally reusable under view caching.
1280    pub fn request_animation_frame_paint_only(&mut self) {
1281        self.tree.invalidate_with_source_and_detail(
1282            self.node,
1283            Invalidation::Paint,
1284            crate::tree::UiDebugInvalidationSource::Other,
1285            crate::tree::UiDebugInvalidationDetail::AnimationFrameRequest,
1286        );
1287        let Some(window) = self.window else {
1288            return;
1289        };
1290        self.app.push_effect(Effect::RequestAnimationFrame(window));
1291    }
1292
1293    pub fn observe_model<T>(&mut self, model: &Model<T>, invalidation: Invalidation) {
1294        (self.observe_model)(model.id(), invalidation);
1295    }
1296
1297    pub fn observe_global<T: Any>(&mut self, invalidation: Invalidation) {
1298        (self.observe_global)(TypeId::of::<T>(), invalidation);
1299    }
1300
1301    pub fn paint(&mut self, child: NodeId, bounds: Rect) {
1302        let was_widget_timer_running = self.tree.debug_paint_widget_exclusive_pause();
1303        let child_transform = self.children_render_transform;
1304        if let Some(transform) = child_transform {
1305            self.scene
1306                .push(fret_core::SceneOp::PushTransform { transform });
1307        }
1308
1309        let accumulated = child_transform
1310            .map(|t| self.accumulated_transform.compose(t))
1311            .unwrap_or(self.accumulated_transform);
1312
1313        self.tree.paint_node(
1314            self.app,
1315            self.services,
1316            child,
1317            bounds,
1318            self.scene,
1319            self.scale_factor,
1320            self.paint_style,
1321            accumulated,
1322        );
1323
1324        if child_transform.is_some() {
1325            self.scene.push(fret_core::SceneOp::PopTransform);
1326        }
1327        if was_widget_timer_running {
1328            self.tree.debug_paint_widget_exclusive_resume();
1329        }
1330    }
1331
1332    /// Paint all child nodes using their last computed layout bounds.
1333    ///
1334    /// This is the default behavior of `Widget::paint()`.
1335    pub fn paint_children(&mut self) {
1336        for &child in self.children {
1337            if let Some(bounds) = self.child_bounds(child) {
1338                self.paint(child, bounds);
1339            } else {
1340                self.paint(child, self.bounds);
1341            }
1342        }
1343    }
1344
1345    pub fn child_bounds(&self, child: NodeId) -> Option<Rect> {
1346        self.tree.node_bounds(child)
1347    }
1348}
1349
1350pub struct SemanticsCx<'a, H: UiHost> {
1351    pub app: &'a mut H,
1352    pub node: NodeId,
1353    pub window: Option<AppWindowId>,
1354    pub element_id_map: Option<&'a HashMap<u64, NodeId>>,
1355    pub bounds: Rect,
1356    pub children: &'a [NodeId],
1357    pub focus: Option<NodeId>,
1358    pub captured: Option<NodeId>,
1359    pub(crate) role: &'a mut SemanticsRole,
1360    pub(crate) flags: &'a mut SemanticsFlags,
1361    pub(crate) label: &'a mut Option<String>,
1362    pub(crate) value: &'a mut Option<String>,
1363    pub(crate) test_id: &'a mut Option<String>,
1364    pub(crate) extra: &'a mut fret_core::SemanticsNodeExtra,
1365    pub(crate) text_selection: &'a mut Option<(u32, u32)>,
1366    pub(crate) text_composition: &'a mut Option<(u32, u32)>,
1367    pub(crate) actions: &'a mut fret_core::SemanticsActions,
1368    pub(crate) active_descendant: &'a mut Option<NodeId>,
1369    pub(crate) pos_in_set: &'a mut Option<u32>,
1370    pub(crate) set_size: &'a mut Option<u32>,
1371    pub(crate) labelled_by: &'a mut Vec<NodeId>,
1372    pub(crate) described_by: &'a mut Vec<NodeId>,
1373    pub(crate) controls: &'a mut Vec<NodeId>,
1374    pub(crate) inline_spans: &'a mut Vec<fret_core::SemanticsInlineSpan>,
1375}
1376
1377impl<'a, H: UiHost> SemanticsCx<'a, H> {
1378    pub fn resolve_declarative_element(&mut self, element: u64) -> Option<NodeId> {
1379        if let Some(node) = self.element_id_map.and_then(|m| m.get(&element).copied()) {
1380            return Some(node);
1381        }
1382
1383        let window = self.window?;
1384        crate::elements::live_node_for_element(
1385            self.app,
1386            window,
1387            crate::elements::GlobalElementId(element),
1388        )
1389    }
1390
1391    pub fn set_role(&mut self, role: SemanticsRole) {
1392        *self.role = role;
1393    }
1394
1395    pub fn set_label(&mut self, label: impl Into<String>) {
1396        *self.label = Some(label.into());
1397    }
1398
1399    pub fn set_test_id(&mut self, id: impl Into<String>) {
1400        *self.test_id = Some(id.into());
1401    }
1402
1403    pub fn clear_test_id(&mut self) {
1404        *self.test_id = None;
1405    }
1406
1407    pub fn set_value(&mut self, value: impl Into<String>) {
1408        *self.value = Some(value.into());
1409    }
1410
1411    pub fn set_placeholder<T: Into<String>>(&mut self, placeholder: Option<T>) {
1412        self.extra.placeholder = placeholder.map(Into::into);
1413    }
1414
1415    pub fn set_url<T: Into<String>>(&mut self, url: Option<T>) {
1416        self.extra.url = url.map(Into::into);
1417    }
1418
1419    pub fn set_role_description<T: Into<String>>(&mut self, role_description: Option<T>) {
1420        self.extra.role_description = role_description.map(Into::into);
1421    }
1422
1423    pub fn clear_role_description(&mut self) {
1424        self.extra.role_description = None;
1425    }
1426
1427    pub fn set_level(&mut self, level: Option<u32>) {
1428        self.extra.level = level;
1429    }
1430
1431    pub fn set_orientation(&mut self, orientation: Option<SemanticsOrientation>) {
1432        self.extra.orientation = orientation;
1433    }
1434
1435    pub fn clear_orientation(&mut self) {
1436        self.extra.orientation = None;
1437    }
1438
1439    pub fn set_numeric_value(&mut self, value: Option<f64>) {
1440        self.extra.numeric.value = value;
1441    }
1442
1443    pub fn set_numeric_range(&mut self, min: Option<f64>, max: Option<f64>) {
1444        self.extra.numeric.min = min;
1445        self.extra.numeric.max = max;
1446    }
1447
1448    pub fn set_numeric_step(&mut self, step: Option<f64>) {
1449        self.extra.numeric.step = step;
1450    }
1451
1452    pub fn set_numeric_jump(&mut self, jump: Option<f64>) {
1453        self.extra.numeric.jump = jump;
1454    }
1455
1456    pub fn set_scroll_x(&mut self, x: Option<f64>, min: Option<f64>, max: Option<f64>) {
1457        self.extra.scroll.x = x;
1458        self.extra.scroll.x_min = min;
1459        self.extra.scroll.x_max = max;
1460    }
1461
1462    pub fn set_scroll_y(&mut self, y: Option<f64>, min: Option<f64>, max: Option<f64>) {
1463        self.extra.scroll.y = y;
1464        self.extra.scroll.y_min = min;
1465        self.extra.scroll.y_max = max;
1466    }
1467
1468    pub fn set_text_selection(&mut self, anchor: u32, focus: u32) {
1469        *self.text_selection = Some((anchor, focus));
1470    }
1471
1472    pub fn clear_text_selection(&mut self) {
1473        *self.text_selection = None;
1474    }
1475
1476    pub fn set_text_composition(&mut self, start: u32, end: u32) {
1477        *self.text_composition = Some((start, end));
1478    }
1479
1480    pub fn clear_text_composition(&mut self) {
1481        *self.text_composition = None;
1482    }
1483
1484    pub fn set_focusable(&mut self, focusable: bool) {
1485        self.actions.focus = focusable;
1486    }
1487
1488    pub fn set_invokable(&mut self, invokable: bool) {
1489        self.actions.invoke = invokable;
1490    }
1491
1492    pub fn set_value_editable(&mut self, editable: bool) {
1493        match *self.role {
1494            // Text controls use AccessKit's `SetValue` / `ReplaceSelectedText` action surfaces.
1495            SemanticsRole::TextField => {
1496                self.actions.set_value = editable;
1497            }
1498            // Range controls generally surface as stepper semantics on platforms. Prefer
1499            // Increment/Decrement for sliders, spin buttons, and splitters.
1500            SemanticsRole::Slider | SemanticsRole::SpinButton | SemanticsRole::Splitter => {
1501                self.actions.increment = editable;
1502                self.actions.decrement = editable;
1503            }
1504            _ => {
1505                self.actions.set_value = editable;
1506            }
1507        }
1508    }
1509
1510    pub fn set_increment_supported(&mut self, supported: bool) {
1511        self.actions.increment = supported;
1512    }
1513
1514    pub fn set_decrement_supported(&mut self, supported: bool) {
1515        self.actions.decrement = supported;
1516    }
1517
1518    pub fn set_scroll_by_supported(&mut self, supported: bool) {
1519        self.actions.scroll_by = supported;
1520    }
1521
1522    pub fn set_text_selection_supported(&mut self, supported: bool) {
1523        self.actions.set_text_selection = supported;
1524    }
1525
1526    pub fn set_disabled(&mut self, disabled: bool) {
1527        self.flags.disabled = disabled;
1528    }
1529
1530    pub fn set_read_only(&mut self, read_only: bool) {
1531        self.flags.read_only = read_only;
1532    }
1533
1534    pub fn set_hidden(&mut self, hidden: bool) {
1535        self.flags.hidden = hidden;
1536    }
1537
1538    pub fn set_visited(&mut self, visited: bool) {
1539        self.flags.visited = visited;
1540    }
1541
1542    pub fn set_multiselectable(&mut self, multiselectable: bool) {
1543        self.flags.multiselectable = multiselectable;
1544    }
1545
1546    pub fn set_selected(&mut self, selected: bool) {
1547        self.flags.selected = selected;
1548    }
1549
1550    pub fn set_expanded(&mut self, expanded: bool) {
1551        self.flags.expanded = expanded;
1552    }
1553
1554    pub fn set_checked(&mut self, checked: Option<bool>) {
1555        self.flags.checked = checked;
1556    }
1557
1558    pub fn set_checked_state(&mut self, checked: Option<SemanticsCheckedState>) {
1559        self.flags.checked_state = checked;
1560        match checked {
1561            Some(SemanticsCheckedState::True) => self.flags.checked = Some(true),
1562            Some(SemanticsCheckedState::False) => self.flags.checked = Some(false),
1563            Some(SemanticsCheckedState::Mixed) => self.flags.checked = None,
1564            None => {}
1565            _ => {}
1566        }
1567    }
1568
1569    pub fn clear_checked_state(&mut self) {
1570        self.flags.checked_state = None;
1571    }
1572
1573    pub fn set_pressed_state(&mut self, pressed: Option<SemanticsPressedState>) {
1574        self.flags.pressed_state = pressed;
1575    }
1576
1577    pub fn clear_pressed_state(&mut self) {
1578        self.flags.pressed_state = None;
1579    }
1580
1581    pub fn set_required(&mut self, required: bool) {
1582        self.flags.required = required;
1583    }
1584
1585    pub fn set_invalid(&mut self, invalid: Option<SemanticsInvalid>) {
1586        self.flags.invalid = invalid;
1587    }
1588
1589    pub fn clear_invalid(&mut self) {
1590        self.flags.invalid = None;
1591    }
1592
1593    pub fn set_busy(&mut self, busy: bool) {
1594        self.flags.busy = busy;
1595    }
1596
1597    pub fn set_live(&mut self, live: Option<SemanticsLive>) {
1598        self.flags.live = live;
1599    }
1600
1601    pub fn clear_live(&mut self) {
1602        self.flags.live = None;
1603    }
1604
1605    pub fn set_live_atomic(&mut self, live_atomic: bool) {
1606        self.flags.live_atomic = live_atomic;
1607    }
1608
1609    pub fn set_active_descendant(&mut self, node: Option<NodeId>) {
1610        *self.active_descendant = node;
1611    }
1612
1613    pub fn set_pos_in_set(&mut self, pos_in_set: Option<u32>) {
1614        *self.pos_in_set = pos_in_set;
1615    }
1616
1617    pub fn set_set_size(&mut self, set_size: Option<u32>) {
1618        *self.set_size = set_size;
1619    }
1620
1621    pub fn set_collection_position(&mut self, pos_in_set: Option<u32>, set_size: Option<u32>) {
1622        *self.pos_in_set = pos_in_set;
1623        *self.set_size = set_size;
1624    }
1625
1626    pub fn push_labelled_by(&mut self, node: NodeId) {
1627        if self.labelled_by.contains(&node) {
1628            return;
1629        }
1630        self.labelled_by.push(node);
1631    }
1632
1633    pub fn clear_labelled_by(&mut self) {
1634        self.labelled_by.clear();
1635    }
1636
1637    pub fn push_described_by(&mut self, node: NodeId) {
1638        if self.described_by.contains(&node) {
1639            return;
1640        }
1641        self.described_by.push(node);
1642    }
1643
1644    pub fn clear_described_by(&mut self) {
1645        self.described_by.clear();
1646    }
1647
1648    pub fn push_controlled(&mut self, node: NodeId) {
1649        if self.controls.contains(&node) {
1650            return;
1651        }
1652        self.controls.push(node);
1653    }
1654
1655    pub fn push_inline_span(&mut self, span: fret_core::SemanticsInlineSpan) {
1656        self.inline_spans.push(span);
1657    }
1658
1659    pub fn push_inline_link_span(&mut self, start_utf8: u32, end_utf8: u32, tag: Option<String>) {
1660        self.push_inline_span(fret_core::SemanticsInlineSpan {
1661            range_utf8: (start_utf8, end_utf8),
1662            role: SemanticsRole::Link,
1663            tag,
1664        });
1665    }
1666
1667    pub fn clear_controls(&mut self) {
1668        self.controls.clear();
1669    }
1670}
1671
1672pub trait Widget<H: UiHost> {
1673    /// Capture-phase event dispatch (root → target).
1674    ///
1675    /// Default is no-op so existing widgets keep their current bubble-only behavior.
1676    fn event_capture(&mut self, _cx: &mut EventCx<'_, H>, _event: &Event) {}
1677
1678    /// Observer-phase event dispatch (`InputDispatchPhase::Preview`).
1679    ///
1680    /// This pass must not mutate input routing state (focus / capture / propagation / default
1681    /// actions). It exists to support outside-press dismissal and click-through overlay policies
1682    /// (ADR 0069).
1683    fn event_observer(&mut self, _cx: &mut ObserverCx<'_, H>, _event: &Event) {}
1684
1685    fn debug_type_name(&self) -> &'static str {
1686        std::any::type_name::<Self>()
1687    }
1688
1689    fn event(&mut self, _cx: &mut EventCx<'_, H>, _event: &Event) {}
1690    fn command(&mut self, _cx: &mut CommandCx<'_, H>, _command: &CommandId) -> bool {
1691        false
1692    }
1693
1694    /// Pure query: does this node participate in availability for `command`?
1695    fn command_availability(
1696        &self,
1697        _cx: &mut CommandAvailabilityCx<'_, H>,
1698        _command: &CommandId,
1699    ) -> CommandAvailability {
1700        CommandAvailability::NotHandled
1701    }
1702    fn cleanup_resources(&mut self, _services: &mut dyn UiServices) {}
1703    /// Optional affine transform applied to both paint and input for the subtree rooted at this node.
1704    ///
1705    /// This is a "render transform" (not a layout transform):
1706    /// - Layout bounds remain authoritative for measurement and positioning.
1707    /// - The transform is expressed in the same coordinate space as `bounds` (logical px, window-local).
1708    /// - Hit-testing and pointer event positions are mapped through the inverse transform so input stays
1709    ///   consistent with the rendered output.
1710    ///
1711    /// Notes:
1712    /// - If the transform is not invertible, hit-testing and pointer event mapping fall back to the
1713    ///   untransformed behavior.
1714    /// - Paint caching may be disabled for nodes that return a transform, depending on runtime policy.
1715    fn render_transform(&self, _bounds: Rect) -> Option<Transform2D> {
1716        None
1717    }
1718    /// Optional affine transform applied to children only (not to this node's own bounds).
1719    ///
1720    /// This is intended for behaviors like scrolling where the viewport bounds are fixed, but the
1721    /// content subtree is translated.
1722    ///
1723    /// The transform is expressed in the same coordinate space as `bounds` (logical px,
1724    /// window-local).
1725    fn children_render_transform(&self, _bounds: Rect) -> Option<Transform2D> {
1726        None
1727    }
1728    /// Optional cursor icon request for a pointer position.
1729    ///
1730    /// This is a pure query used to build an interaction stream that can be reused on cache-hit
1731    /// frames (ADR 0167). Prefer this over setting cursor icons via pointer-move event handlers
1732    /// when the cursor choice is a function of the current input state only.
1733    ///
1734    /// The provided `position` is already mapped into this node's coordinate space (including
1735    /// ancestor `render_transform` and `children_render_transform`), matching what the widget sees
1736    /// during pointer event dispatch.
1737    fn cursor_icon_at(
1738        &self,
1739        _bounds: Rect,
1740        _position: Point,
1741        _input_ctx: &fret_runtime::InputContext,
1742    ) -> Option<fret_core::CursorIcon> {
1743        None
1744    }
1745    /// Whether hit-testing should be clipped to `bounds`.
1746    ///
1747    /// When `false`, children can receive pointer input even if they are positioned outside the
1748    /// parent's bounds (useful for `overflow: visible` + absolute-positioned badges/icons).
1749    ///
1750    /// Default: `true`.
1751    fn clips_hit_test(&self, _bounds: Rect) -> bool {
1752        true
1753    }
1754    /// Optional rounded-rectangle clip shape for hit-testing.
1755    ///
1756    /// When provided and `clips_hit_test(...)` is `true`, the runtime additionally clips pointer
1757    /// targeting to the rounded-rectangle defined by `bounds` + these corner radii. This keeps
1758    /// hit-testing consistent with `overflow: clip` + rounded corners.
1759    ///
1760    /// Default: `None` (rectangular clipping only).
1761    fn clip_hit_test_corner_radii(&self, _bounds: Rect) -> Option<Corners> {
1762        None
1763    }
1764    /// Hit-test predicate for pointer input targeting.
1765    ///
1766    /// Returning `false` makes the node "transparent" to hit-testing (events fall through to
1767    /// underlay layers / widgets).
1768    ///
1769    /// Default: `true` (bounds-based hit testing).
1770    fn hit_test(&self, _bounds: Rect, _position: Point) -> bool {
1771        true
1772    }
1773    /// Whether the node's children participate in hit-testing.
1774    ///
1775    /// When `false`, the entire subtree behaves like CSS `pointer-events: none` (useful for
1776    /// disabled controls that must not intercept events).
1777    ///
1778    /// Default: `true`.
1779    fn hit_test_children(&self, _bounds: Rect, _position: Point) -> bool {
1780        true
1781    }
1782    /// Whether this node should be included in the semantics snapshot.
1783    ///
1784    /// This is a mechanism-only gate used to model `present=false` (display-none) subtrees that
1785    /// should not be exposed to assistive tech, while still keeping element state alive (e.g.
1786    /// Radix-style `forceMount`).
1787    ///
1788    /// Default: `true`.
1789    fn semantics_present(&self) -> bool {
1790        true
1791    }
1792    /// Whether semantics snapshot traversal should recurse into this node's children.
1793    ///
1794    /// Default: `true`.
1795    fn semantics_children(&self) -> bool {
1796        true
1797    }
1798    /// Optional synchronization hook for declarative `InteractivityGate` nodes.
1799    ///
1800    /// Declarative `InteractivityGate` is allowed to short-circuit layout when `present == false`
1801    /// (display-none behavior). In those frames the layout engine may skip calling `layout()` for
1802    /// the gate node, leaving cached widget gates stale. Declarative host widgets can override
1803    /// this hook so the mount pipeline can keep semantics/hit-test traversal consistent even when
1804    /// layout is skipped.
1805    fn sync_interactivity_gate(&mut self, _present: bool, _interactive: bool) {}
1806
1807    /// Optional synchronization hook for declarative `HitTestGate` nodes.
1808    ///
1809    /// Declarative `HitTestGate` toggles whether pointer hit-testing should recurse into the
1810    /// subtree. Host widgets can override this hook so the mount pipeline can update cached
1811    /// hit-test traversal flags without requiring a full layout pass.
1812    fn sync_hit_test_gate(&mut self, _hit_test: bool) {}
1813
1814    /// Optional synchronization hook for declarative `FocusTraversalGate` nodes.
1815    ///
1816    /// Declarative `FocusTraversalGate` toggles whether focus traversal should recurse into the
1817    /// subtree. Host widgets can override this hook so the mount pipeline can update cached
1818    /// traversal flags without requiring a full layout pass.
1819    fn sync_focus_traversal_gate(&mut self, _traverse: bool) {}
1820    /// Whether focus traversal should recurse into this node's children.
1821    ///
1822    /// This is a mechanism-only gate used by `UiTree` to model "inert" subtrees during
1823    /// transitions (e.g. `present=true` but `interactive=false`), without requiring every focusable
1824    /// widget to thread an "interactive" flag into its own `is_focusable()` logic.
1825    ///
1826    /// Default: `true`.
1827    fn focus_traversal_children(&self) -> bool {
1828        true
1829    }
1830    fn is_focusable(&self) -> bool {
1831        false
1832    }
1833    fn is_text_input(&self) -> bool {
1834        false
1835    }
1836
1837    /// Optional platform-facing text input snapshot for the focused widget.
1838    ///
1839    /// This exists to support editor-grade IME and accessibility bridges that need UTF-16 ranges
1840    /// and an IME cursor anchor, without depending on widget internals.
1841    ///
1842    /// Coordinate model: UTF-16 code units over the widget's "composed view" (base text with the
1843    /// active preedit spliced at the caret).
1844    fn platform_text_input_snapshot(&self) -> Option<fret_runtime::WindowTextInputSnapshot> {
1845        None
1846    }
1847
1848    /// Returns the focused selection range (UTF-16 code units over the composed view).
1849    fn platform_text_input_selected_range_utf16(&self) -> Option<fret_runtime::Utf16Range> {
1850        None
1851    }
1852
1853    /// Returns the marked (preedit) range (UTF-16 code units over the composed view).
1854    fn platform_text_input_marked_range_utf16(&self) -> Option<fret_runtime::Utf16Range> {
1855        None
1856    }
1857
1858    fn platform_text_input_text_for_range_utf16(
1859        &self,
1860        _range: fret_runtime::Utf16Range,
1861    ) -> Option<String> {
1862        None
1863    }
1864
1865    fn platform_text_input_bounds_for_range_utf16(
1866        &mut self,
1867        _cx: &mut PlatformTextInputCx<'_, H>,
1868        _range: fret_runtime::Utf16Range,
1869    ) -> Option<Rect> {
1870        None
1871    }
1872
1873    fn platform_text_input_character_index_for_point_utf16(
1874        &mut self,
1875        _cx: &mut PlatformTextInputCx<'_, H>,
1876        _point: Point,
1877    ) -> Option<u32> {
1878        None
1879    }
1880
1881    fn platform_text_input_replace_text_in_range_utf16(
1882        &mut self,
1883        _cx: &mut PlatformTextInputCx<'_, H>,
1884        _range: fret_runtime::Utf16Range,
1885        _text: &str,
1886    ) -> bool {
1887        false
1888    }
1889
1890    fn platform_text_input_replace_and_mark_text_in_range_utf16(
1891        &mut self,
1892        _cx: &mut PlatformTextInputCx<'_, H>,
1893        _range: fret_runtime::Utf16Range,
1894        _text: &str,
1895        _marked: Option<fret_runtime::Utf16Range>,
1896        _selected: Option<fret_runtime::Utf16Range>,
1897    ) -> bool {
1898        false
1899    }
1900    /// Whether this node supports direct "scroll-by" requests (typically for accessibility).
1901    fn can_scroll_by(&self) -> bool {
1902        false
1903    }
1904    fn scroll_by(&mut self, _cx: &mut ScrollByCx<'_, H>, _delta: Point) -> ScrollByResult {
1905        ScrollByResult::NotHandled
1906    }
1907    /// Whether this node can scroll a focused descendant into view.
1908    ///
1909    /// This is a mechanism-only capability used by `UiTree` to implement a minimal
1910    /// "scroll-into-view" contract for focus traversal (ADR 0068) without coupling focus traversal
1911    /// policy into component crates.
1912    fn can_scroll_descendant_into_view(&self) -> bool {
1913        false
1914    }
1915    fn scroll_descendant_into_view(
1916        &mut self,
1917        _cx: &mut ScrollIntoViewCx<'_, H>,
1918        _descendant_bounds: Rect,
1919    ) -> ScrollIntoViewResult {
1920        ScrollIntoViewResult::NotHandled
1921    }
1922    fn measure(&mut self, _cx: &mut MeasureCx<'_, H>) -> Size {
1923        Size::default()
1924    }
1925    fn layout(&mut self, _cx: &mut LayoutCx<'_, H>) -> Size {
1926        Size::default()
1927    }
1928    /// Prepaint hook invoked after layout, before paint.
1929    ///
1930    /// Default is no-op so existing widgets keep their current behavior.
1931    fn prepaint(&mut self, _cx: &mut PrepaintCx<'_, H>) {}
1932    fn paint(&mut self, cx: &mut PaintCx<'_, H>) {
1933        cx.paint_children();
1934    }
1935    fn semantics(&mut self, _cx: &mut SemanticsCx<'_, H>) {}
1936}
1937
1938#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
1939pub enum ScrollByResult {
1940    #[default]
1941    NotHandled,
1942    Handled {
1943        did_scroll: bool,
1944    },
1945}
1946
1947pub struct ScrollByCx<'a, H: UiHost> {
1948    pub app: &'a mut H,
1949    pub node: NodeId,
1950    pub window: Option<AppWindowId>,
1951    pub bounds: Rect,
1952}
1953
1954#[derive(Debug, Default, Clone, Copy)]
1955pub enum ScrollIntoViewResult {
1956    #[default]
1957    NotHandled,
1958    Handled {
1959        did_scroll: bool,
1960        // Bounds outer scroll ancestors should treat as the descendant after this widget handles
1961        // the request. Scroll surfaces use this to propagate their effective viewport rect instead
1962        // of the original deep descendant bounds.
1963        propagated_bounds: Option<Rect>,
1964    },
1965}
1966
1967pub struct ScrollIntoViewCx<'a, H: UiHost> {
1968    pub app: &'a mut H,
1969    pub node: NodeId,
1970    pub window: Option<AppWindowId>,
1971    pub bounds: Rect,
1972}
1973
1974pub struct PlatformTextInputCx<'a, H: UiHost> {
1975    pub app: &'a mut H,
1976    pub services: &'a mut dyn UiServices,
1977    pub window: Option<AppWindowId>,
1978    pub node: NodeId,
1979    pub bounds: Rect,
1980    pub scale_factor: f32,
1981}
1982
1983impl<'a, H: UiHost> PlatformTextInputCx<'a, H> {
1984    pub fn theme(&self) -> &Theme {
1985        Theme::global(&*self.app)
1986    }
1987}