Skip to main content

fret_ui/
element.rs

1use crate::UiHost;
2use crate::elements::{ElementContext, GlobalElementId};
3use crate::overlay_placement::{Align, AnchoredPanelLayout, AnchoredPanelOptions, Side};
4use fret_core::scene::{BlendMode, CustomEffectPyramidRequestV1, Mask, Paint};
5use fret_core::{
6    AttributedText, CaretAffinity, Color, Corners, Edges, EffectChain, EffectMode, EffectQuality,
7    ImageId, KeyCode, NodeId, Px, Rect, RenderTargetId, SemanticsLive, SemanticsOrientation,
8    SemanticsRole, Size, SvgFit, TextAlign, TextOverflow, TextStyle, TextStyleRefinement, TextWrap,
9    UvRect, ViewportFit,
10};
11use fret_runtime::{CommandId, Model};
12use std::sync::Arc;
13
14use crate::{ResizablePanelGroupStyle, SvgSource, TextAreaStyle, TextInputStyle};
15
16/// Declarative element tree node (ephemeral per frame), keyed by a stable `GlobalElementId`.
17///
18/// This is the authoring-layer representation described by ADR 0028 / ADR 0039.
19///
20/// Note: `AnyElement` is intentionally move-only. Reusing the same `AnyElement` value in multiple
21/// places (e.g. via cloning) can create duplicate `GlobalElementId`s within a single frame, which
22/// violates the element-tree contract and can lead to downstream traversal issues.
23#[derive(Debug)]
24pub struct AnyElement {
25    pub id: GlobalElementId,
26    pub kind: ElementKind,
27    pub children: Vec<AnyElement>,
28    /// Layout-transparent inherited foreground installed on this subtree root.
29    ///
30    /// This is the non-wrapper equivalent of `ForegroundScope`: descendants that opt into
31    /// `currentColor`-style paint inheritance can resolve this value during paint without adding a
32    /// new layout node.
33    pub inherited_foreground: Option<Color>,
34    /// Layout-transparent inherited passive-text typography installed on this subtree root.
35    ///
36    /// This is consumed by passive text leaves (`Text`, `StyledText`, `SelectableText`) via the
37    /// runtime's inherited text-style cascade (ADR 0314) without introducing a layout wrapper.
38    pub inherited_text_style: Option<TextStyleRefinement>,
39    /// Layout-transparent semantics overrides applied when producing semantics snapshots.
40    pub semantics_decoration: Option<SemanticsDecoration>,
41    /// Layout-transparent key context identifier used by shortcut/keymap `when` expressions.
42    pub key_context: Option<Arc<str>>,
43}
44
45impl AnyElement {
46    pub fn new(id: GlobalElementId, kind: ElementKind, children: Vec<AnyElement>) -> Self {
47        Self {
48            id,
49            kind,
50            children,
51            inherited_foreground: None,
52            inherited_text_style: None,
53            semantics_decoration: None,
54            key_context: None,
55        }
56    }
57
58    /// Attach a subtree-local inherited foreground without introducing a layout wrapper.
59    ///
60    /// Descendants that support `currentColor` / `IconTheme`-style paint inheritance resolve this
61    /// value at paint time.
62    pub fn inherit_foreground(mut self, foreground: Color) -> Self {
63        self.inherited_foreground = Some(foreground);
64        self
65    }
66
67    /// Attach a subtree-local inherited passive-text refinement without introducing a layout wrapper.
68    ///
69    /// Descendants that render passive text (`Text`, `StyledText`, `SelectableText`) resolve this
70    /// refinement through the runtime's inherited text-style cascade.
71    pub fn inherit_text_style(mut self, refinement: TextStyleRefinement) -> Self {
72        match self.inherited_text_style.as_mut() {
73            Some(existing) => existing.merge(&refinement),
74            None => self.inherited_text_style = Some(refinement),
75        }
76        self
77    }
78
79    /// Attach layout-transparent semantics metadata to this element (ADR 0222).
80    ///
81    /// Prefer this over wrapping a subtree in `Semantics` when you only need to stamp
82    /// `test_id` / `label` / `role` / `value` for diagnostics or UI automation, since `Semantics`
83    /// introduces a real layout node.
84    ///
85    /// ```ignore
86    /// use fret_core::SemanticsRole;
87    /// use fret_ui::element::SemanticsDecoration;
88    ///
89    /// // `some_element` is any `AnyElement` produced by your view constructors:
90    /// let el = some_element.attach_semantics(
91    ///     SemanticsDecoration::default()
92    ///         .role(SemanticsRole::Button)
93    ///         .label("Save")
94    ///         .test_id("toolbar.save"),
95    /// );
96    /// ```
97    pub fn attach_semantics(mut self, decoration: SemanticsDecoration) -> Self {
98        self.semantics_decoration = Some(match self.semantics_decoration.take() {
99            Some(existing) => existing.merge(decoration),
100            None => decoration,
101        });
102        self
103    }
104
105    /// Shorthand for attaching a [`SemanticsDecoration`] without introducing a layout node.
106    ///
107    /// This is a convenience wrapper over [`AnyElement::attach_semantics`].
108    pub fn a11y(self, decoration: SemanticsDecoration) -> Self {
109        self.attach_semantics(decoration)
110    }
111
112    /// Attach a semantics role override (ARIA `role`-like outcome).
113    pub fn a11y_role(self, role: SemanticsRole) -> Self {
114        self.a11y(SemanticsDecoration::default().role(role))
115    }
116
117    /// Attach a semantics label override (ARIA `aria-label`-like outcome).
118    pub fn a11y_label(self, label: impl Into<Arc<str>>) -> Self {
119        self.a11y(SemanticsDecoration::default().label(label))
120    }
121
122    /// Attach a debug/test-only identifier for diagnostics and deterministic UI automation.
123    ///
124    /// This is shorthand for attaching a [`SemanticsDecoration`] with `test_id` set.
125    ///
126    /// ```ignore
127    /// let el = some_element.test_id("settings.theme.toggle");
128    /// ```
129    pub fn test_id(self, test_id: impl Into<Arc<str>>) -> Self {
130        self.a11y(SemanticsDecoration::default().test_id(test_id))
131    }
132
133    /// Attach a semantics value override (ARIA `aria-valuetext`-like outcome).
134    pub fn a11y_value(self, value: impl Into<Arc<str>>) -> Self {
135        self.a11y(SemanticsDecoration::default().value(value))
136    }
137
138    /// Attach a disabled override (ARIA `aria-disabled`-like outcome).
139    pub fn a11y_disabled(self, disabled: bool) -> Self {
140        self.a11y(SemanticsDecoration::default().disabled(disabled))
141    }
142
143    /// Attach a selected override (ARIA `aria-selected`-like outcome).
144    pub fn a11y_selected(self, selected: bool) -> Self {
145        self.a11y(SemanticsDecoration::default().selected(selected))
146    }
147
148    /// Attach an expanded override (ARIA `aria-expanded`-like outcome).
149    pub fn a11y_expanded(self, expanded: bool) -> Self {
150        self.a11y(SemanticsDecoration::default().expanded(expanded))
151    }
152
153    /// Attach a tri-state checked override (ARIA `aria-checked`-like outcome).
154    pub fn a11y_checked(self, checked: Option<bool>) -> Self {
155        self.a11y(SemanticsDecoration::default().checked(checked))
156    }
157
158    /// Attach a key context identifier to this element for shortcut routing.
159    ///
160    /// This is a layout-transparent annotation used by `when` expressions via `keyctx.*`.
161    pub fn key_context(mut self, key_context: impl Into<Arc<str>>) -> Self {
162        self.key_context = Some(key_context.into());
163        self
164    }
165}
166
167#[allow(clippy::large_enum_variant)]
168#[derive(Debug, Clone)]
169pub enum ElementKind {
170    Container(ContainerProps),
171    Semantics(SemanticsProps),
172    /// A flex container that also contributes a semantics node with a fixed role.
173    ///
174    /// This is used by higher-level libraries (e.g. Radix/shadcn ports) to model structural
175    /// grouping (`role="group"`) without introducing an extra semantics wrapper layer that would
176    /// otherwise be separated from layout.
177    SemanticFlex(SemanticFlexProps),
178    FocusScope(FocusScopeProps),
179    /// A layout wrapper used for frame-lagged container queries (ADR 0231).
180    ///
181    /// This is paint- and input-transparent. It exists to provide a stable, queryable bounds
182    /// snapshot for component-layer "responsive" policies that must adapt to **panel width**
183    /// rather than viewport width.
184    LayoutQueryRegion(LayoutQueryRegionProps),
185    /// A transparent wrapper that gates subtree presence and interactivity.
186    ///
187    /// This is a mechanism-oriented primitive intended to support Radix-style authoring outcomes
188    /// like `forceMount` while still being able to make a subtree non-interactive (click/keyboard)
189    /// or fully absent from layout/paint, without deleting the subtree (so per-element state can be
190    /// preserved).
191    InteractivityGate(InteractivityGateProps),
192    /// A transparent wrapper that gates pointer hit-testing for a subtree without affecting focus
193    /// traversal or semantics.
194    ///
195    /// When `hit_test == false`, the subtree remains present for layout/paint and can still
196    /// participate in keyboard focus traversal, but pointer hit-testing will ignore the subtree
197    /// (click-through behavior).
198    ///
199    /// This is intended to support editor-grade "peek through" surfaces (ImGui-style
200    /// `NoMouseInputs`) without making the subtree inert for keyboard navigation.
201    HitTestGate(HitTestGateProps),
202    /// A transparent wrapper that gates focus traversal for a subtree without affecting pointer
203    /// hit-testing or semantics.
204    ///
205    /// When `traverse == false`, the subtree remains present for layout/paint and pointer
206    /// hit-testing, but focus traversal will not recurse into the subtree.
207    ///
208    /// This is intended to support editor-grade "disabled but hoverable" surfaces (e.g. tooltips
209    /// over disabled items) without requiring authors to restructure hit-testing.
210    FocusTraversalGate(FocusTraversalGateProps),
211    /// A paint- and input-transparent wrapper that installs a subtree-local foreground color.
212    ///
213    /// This is the declarative equivalent of CSS `currentColor` inheritance or Flutter's
214    /// `IconTheme`/`DefaultTextStyle` *foreground* behavior: descendants that opt into inheriting
215    /// foreground color can resolve it from the nearest `ForegroundScope`.
216    ///
217    /// Note: v2 only covers foreground color. A richer text-style stack (font/size/weight/etc.)
218    /// can be layered on later without changing the element tree contract.
219    ForegroundScope(ForegroundScopeProps),
220    Opacity(OpacityProps),
221    /// A scoped post-processing effect group wrapper (ADR 0117).
222    EffectLayer(EffectLayerProps),
223    /// A scoped backdrop source group wrapper (ADR 0305).
224    BackdropSourceGroup(BackdropSourceGroupProps),
225    /// A scoped alpha mask layer wrapper (ADR 0239).
226    ///
227    /// This emits a `SceneOp::PushMask/PopMask` pair around the subtree during painting. The
228    /// mask's computation bounds are the wrapper's final layout bounds.
229    MaskLayer(MaskLayerProps),
230    /// A scoped isolated compositing group wrapper (ADR 0247).
231    ///
232    /// This emits a `SceneOp::PushCompositeGroup/PopCompositeGroup` pair around the subtree during
233    /// painting. The compositing group's computation bounds are the wrapper's final layout bounds.
234    CompositeGroup(CompositeGroupProps),
235    /// Experimental view-level cache boundary wrapper.
236    ///
237    /// When enabled by the runtime, this marks a subtree as a cache root for range-replay and
238    /// invalidation containment experiments (see `docs/workstreams/gpui-parity-refactor/gpui-parity-refactor.md`).
239    ViewCache(ViewCacheProps),
240    VisualTransform(VisualTransformProps),
241    RenderTransform(RenderTransformProps),
242    FractionalRenderTransform(FractionalRenderTransformProps),
243    Anchored(AnchoredProps),
244    Pressable(PressableProps),
245    PointerRegion(PointerRegionProps),
246    /// A focusable, text-input-capable event region primitive.
247    ///
248    /// Unlike `TextInput` / `TextArea`, this does not own an internal text model. It exists as a
249    /// mechanism-only building block for ecosystem text surfaces (e.g. code editors) that need
250    /// to receive `Event::TextInput` / `Event::Ime` / clipboard events while owning their own
251    /// buffer and rendering pipeline.
252    TextInputRegion(TextInputRegionProps),
253    /// An internal drag event listener region primitive.
254    ///
255    /// This is a mechanism-only building block: it does not own policy for any particular drag
256    /// kind, and is intended to be used by higher-level layers (workspace, docking, etc.).
257    InternalDragRegion(InternalDragRegionProps),
258    /// An external OS drag-and-drop event listener region primitive.
259    ///
260    /// This receives `Event::ExternalDrag` (Enter/Over/Drop/Leave) events and is intended to be
261    /// used by higher-level layers to implement portable file-drop workflows (ADR 0053).
262    ExternalDragRegion(ExternalDragRegionProps),
263    RovingFlex(RovingFlexProps),
264    Stack(StackProps),
265    Column(ColumnProps),
266    Row(RowProps),
267    Spacer(SpacerProps),
268    Text(TextProps),
269    StyledText(StyledTextProps),
270    SelectableText(SelectableTextProps),
271    TextInput(TextInputProps),
272    TextArea(TextAreaProps),
273    ResizablePanelGroup(ResizablePanelGroupProps),
274    VirtualList(VirtualListProps),
275    Flex(FlexProps),
276    Grid(GridProps),
277    Image(ImageProps),
278    /// A declarative, leaf canvas element for custom scene emission (ADR 0141).
279    Canvas(CanvasProps),
280    /// Unstable bridge element for hosting a retained subtree under declarative mount.
281    #[cfg(feature = "unstable-retained-bridge")]
282    RetainedSubtree(crate::retained_bridge::RetainedSubtreeProps),
283    /// Composites an app-owned render target (Tier A; ADR 0007 / ADR 0038 / ADR 0123).
284    ViewportSurface(ViewportSurfaceProps),
285    SvgIcon(SvgIconProps),
286    Spinner(SpinnerProps),
287    HoverRegion(HoverRegionProps),
288    /// An event-only wheel listener that updates an imperative scroll handle.
289    ///
290    /// Unlike `Scroll`, this element does not translate its children; it only mutates the provided
291    /// `ScrollHandle` and invalidates an optional target.
292    WheelRegion(WheelRegionProps),
293    Scroll(ScrollProps),
294    Scrollbar(ScrollbarProps),
295}
296
297#[derive(Debug, Clone, Copy)]
298pub struct SemanticFlexProps {
299    pub role: SemanticsRole,
300    pub flex: FlexProps,
301}
302
303/// Per-element pointer state for `PointerRegion`.
304#[derive(Debug, Default, Clone)]
305pub struct PointerRegionState {
306    pub last_down: Option<crate::action::PointerDownCx>,
307}
308
309/// A pointer event listener region primitive.
310///
311/// This is a mechanism-only building block: it does not imply click/activation semantics.
312#[derive(Debug, Clone, Copy)]
313pub struct PointerRegionProps {
314    pub layout: LayoutStyle,
315    pub enabled: bool,
316    /// When set, `PointerEvent::Move` is dispatched to this region during the Capture phase
317    /// (root → target) rather than Bubble.
318    ///
319    /// This is a mechanism-only knob intended for "gesture arena" style arbitration where a
320    /// parent wrapper must observe pointer moves even when a descendant would otherwise stop
321    /// bubbling (e.g. pressables capturing/stopping on pointer down).
322    ///
323    /// When enabled, Bubble-phase handling for `PointerEvent::Move` is skipped to avoid
324    /// double-dispatch.
325    pub capture_phase_pointer_moves: bool,
326}
327
328/// A focusable event region that participates in text input / IME routing.
329#[derive(Debug, Clone)]
330pub struct TextInputRegionProps {
331    pub layout: LayoutStyle,
332    pub enabled: bool,
333    pub text_boundary_mode_override: Option<fret_runtime::TextBoundaryMode>,
334    /// Optional IME cursor area in window visual space.
335    ///
336    /// When set, this is forwarded to `WindowTextInputSnapshot.ime_cursor_area` while the region
337    /// is focused. This is a data-only escape hatch for editor ecosystems that own the geometry
338    /// mapping (buffer ↔ rows ↔ caret rect) outside the mechanism layer.
339    pub ime_cursor_area: Option<fret_core::Rect>,
340    /// Optional accessibility label for this text input region.
341    pub a11y_label: Option<Arc<str>>,
342    /// Optional accessibility value text for this text input region.
343    ///
344    /// When present, selection and composition ranges are interpreted as UTF-8 byte offsets within
345    /// this value (ADR 0071).
346    pub a11y_value: Option<Arc<str>>,
347    pub a11y_required: bool,
348    pub a11y_invalid: Option<fret_core::SemanticsInvalid>,
349    /// Optional selection range (anchor, focus) in UTF-8 byte offsets within `a11y_value`.
350    pub a11y_text_selection: Option<(u32, u32)>,
351    /// Optional IME composition range (start, end) in UTF-8 byte offsets within `a11y_value`.
352    pub a11y_text_composition: Option<(u32, u32)>,
353    /// Best-effort surrounding text excerpt for IME backends that support it.
354    ///
355    /// This SHOULD exclude any active preedit/composing text and SHOULD be limited to
356    /// `WindowImeSurroundingText::MAX_TEXT_BYTES`.
357    pub ime_surrounding_text: Option<fret_runtime::WindowImeSurroundingText>,
358}
359
360/// An internal drag event listener region primitive.
361///
362/// This is a mechanism-only building block for cross-window and internal drag flows.
363#[derive(Debug, Clone, Copy)]
364pub struct InternalDragRegionProps {
365    pub layout: LayoutStyle,
366    pub enabled: bool,
367}
368
369/// An external drag event listener region primitive.
370///
371/// This is a mechanism-only building block for external file drop workflows.
372#[derive(Debug, Clone, Copy)]
373pub struct ExternalDragRegionProps {
374    pub layout: LayoutStyle,
375    pub enabled: bool,
376}
377
378impl Default for InternalDragRegionProps {
379    fn default() -> Self {
380        Self {
381            layout: LayoutStyle::default(),
382            enabled: true,
383        }
384    }
385}
386
387impl Default for ExternalDragRegionProps {
388    fn default() -> Self {
389        Self {
390            layout: LayoutStyle::default(),
391            enabled: true,
392        }
393    }
394}
395
396impl Default for PointerRegionProps {
397    fn default() -> Self {
398        Self {
399            layout: LayoutStyle::default(),
400            enabled: true,
401            capture_phase_pointer_moves: false,
402        }
403    }
404}
405
406impl Default for TextInputRegionProps {
407    fn default() -> Self {
408        Self {
409            layout: LayoutStyle::default(),
410            enabled: true,
411            text_boundary_mode_override: None,
412            ime_cursor_area: None,
413            a11y_label: None,
414            a11y_value: None,
415            a11y_required: false,
416            a11y_invalid: None,
417            a11y_text_selection: None,
418            a11y_text_composition: None,
419            ime_surrounding_text: None,
420        }
421    }
422}
423
424#[derive(Debug, Clone, Copy, Default, PartialEq)]
425pub struct LayoutStyle {
426    pub size: SizeStyle,
427    pub flex: FlexItemStyle,
428    pub overflow: Overflow,
429    pub margin: MarginEdges,
430    pub position: PositionStyle,
431    pub inset: InsetStyle,
432    pub aspect_ratio: Option<f32>,
433    pub grid: GridItemStyle,
434}
435
436#[derive(Debug, Clone, Copy, PartialEq)]
437pub enum MarginEdge {
438    Px(Px),
439    Fill,
440    Fraction(f32),
441    Auto,
442}
443
444impl Default for MarginEdge {
445    fn default() -> Self {
446        Self::Px(Px(0.0))
447    }
448}
449
450impl From<Px> for MarginEdge {
451    fn from(px: Px) -> Self {
452        Self::Px(px)
453    }
454}
455
456impl From<Option<Px>> for MarginEdge {
457    fn from(px: Option<Px>) -> Self {
458        match px {
459            Some(px) => Self::Px(px),
460            None => Self::Auto,
461        }
462    }
463}
464
465#[derive(Debug, Clone, Copy, Default, PartialEq)]
466pub struct MarginEdges {
467    pub top: MarginEdge,
468    pub right: MarginEdge,
469    pub bottom: MarginEdge,
470    pub left: MarginEdge,
471}
472
473impl MarginEdges {
474    pub fn all(edge: MarginEdge) -> Self {
475        Self {
476            top: edge,
477            right: edge,
478            bottom: edge,
479            left: edge,
480        }
481    }
482}
483
484#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
485pub enum Overflow {
486    #[default]
487    Visible,
488    Clip,
489}
490
491#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
492pub enum PositionStyle {
493    /// Default flow position; inset offsets are ignored.
494    #[default]
495    Static,
496    /// Inset offsets tweak the final position without affecting siblings.
497    Relative,
498    /// Removed from flow and positioned via inset offsets.
499    Absolute,
500}
501
502#[derive(Debug, Clone, Copy, Default, PartialEq)]
503pub struct InsetStyle {
504    pub top: InsetEdge,
505    pub right: InsetEdge,
506    pub bottom: InsetEdge,
507    pub left: InsetEdge,
508}
509
510#[derive(Debug, Clone, Copy, Default, PartialEq)]
511pub enum InsetEdge {
512    Px(Px),
513    Fill,
514    Fraction(f32),
515    #[default]
516    Auto,
517}
518
519impl From<Px> for InsetEdge {
520    fn from(px: Px) -> Self {
521        Self::Px(px)
522    }
523}
524
525impl From<Option<Px>> for InsetEdge {
526    fn from(px: Option<Px>) -> Self {
527        match px {
528            Some(px) => Self::Px(px),
529            None => Self::Auto,
530        }
531    }
532}
533
534#[derive(Debug, Clone, Copy, Default, PartialEq)]
535pub struct GridItemStyle {
536    pub column: GridLine,
537    pub row: GridLine,
538    pub align_self: Option<CrossAlign>,
539    pub justify_self: Option<CrossAlign>,
540}
541
542#[derive(Debug, Clone, Copy, Default, PartialEq)]
543pub struct GridLine {
544    pub start: Option<i16>,
545    pub span: Option<u16>,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq)]
549pub struct SizeStyle {
550    pub width: Length,
551    pub height: Length,
552    /// Minimum width constraint.
553    ///
554    /// Percent sizing semantics follow `Length` rules: `Fill`/`Fraction` only resolve under a
555    /// definite containing block; otherwise they should behave like `auto` in measurement paths.
556    pub min_width: Option<Length>,
557    /// Minimum height constraint.
558    pub min_height: Option<Length>,
559    /// Maximum width constraint.
560    pub max_width: Option<Length>,
561    /// Maximum height constraint.
562    pub max_height: Option<Length>,
563}
564
565impl Default for SizeStyle {
566    fn default() -> Self {
567        Self {
568            width: Length::Auto,
569            height: Length::Auto,
570            min_width: None,
571            min_height: None,
572            max_width: None,
573            max_height: None,
574        }
575    }
576}
577
578#[derive(Debug, Clone, Copy, PartialEq)]
579pub struct FlexItemStyle {
580    /// Visual order within a flex container.
581    ///
582    /// This matches the web flexbox `order` property: it affects layout order only and does not
583    /// change tree order (e.g. focus navigation that follows element order).
584    pub order: i32,
585    pub grow: f32,
586    pub shrink: f32,
587    pub basis: Length,
588    pub align_self: Option<CrossAlign>,
589}
590
591impl Default for FlexItemStyle {
592    fn default() -> Self {
593        Self {
594            order: 0,
595            grow: 0.0,
596            // Tailwind/DOM default is `flex-shrink: 1`. Recipes should opt out via
597            // `LayoutRefinement::flex_shrink_0()` when needed.
598            shrink: 1.0,
599            basis: Length::Auto,
600            align_self: None,
601        }
602    }
603}
604
605#[derive(Debug, Clone, Copy, Default, PartialEq)]
606pub enum Length {
607    #[default]
608    Auto,
609    Px(Px),
610    /// Fraction of the containing block size (percent sizing).
611    ///
612    /// This is expressed as a ratio (e.g. `0.5` for 50%). When the containing block size is not
613    /// definite, this should behave like `Auto` (CSS-like percent sizing semantics).
614    Fraction(f32),
615    Fill,
616}
617
618/// A length type for spacing (padding/gap) that can express percent sizing but has no `auto`.
619///
620/// Taffy resolves percent padding/border against the containing block *width* (inline size),
621/// including vertical edges (CSS-like). We mirror that behavior in the declarative bridge by
622/// resolving percent spacing only when the containing block width is definite; otherwise it
623/// resolves to `0` (definite-only).
624#[derive(Debug, Clone, Copy, PartialEq)]
625pub enum SpacingLength {
626    Px(Px),
627    /// Fraction of the containing block size (percent spacing).
628    ///
629    /// Expressed as a ratio (e.g. `0.5` for 50%).
630    Fraction(f32),
631    /// Shorthand for 100% (equivalent intent to `Fraction(1.0)`).
632    Fill,
633}
634
635impl Default for SpacingLength {
636    fn default() -> Self {
637        Self::Px(Px(0.0))
638    }
639}
640
641impl SpacingLength {
642    pub const fn px(px: Px) -> Self {
643        Self::Px(px)
644    }
645
646    pub const fn fraction(fraction: f32) -> Self {
647        Self::Fraction(fraction)
648    }
649
650    pub const fn fill() -> Self {
651        Self::Fill
652    }
653}
654
655impl From<Px> for SpacingLength {
656    fn from(value: Px) -> Self {
657        Self::Px(value)
658    }
659}
660
661#[derive(Debug, Clone, Copy, PartialEq)]
662pub struct SpacingEdges {
663    pub top: SpacingLength,
664    pub right: SpacingLength,
665    pub bottom: SpacingLength,
666    pub left: SpacingLength,
667}
668
669impl Default for SpacingEdges {
670    fn default() -> Self {
671        Self::all(SpacingLength::Px(Px(0.0)))
672    }
673}
674
675impl SpacingEdges {
676    pub const fn all(value: SpacingLength) -> Self {
677        Self {
678            top: value,
679            right: value,
680            bottom: value,
681            left: value,
682        }
683    }
684
685    pub const fn symmetric(horizontal: SpacingLength, vertical: SpacingLength) -> Self {
686        Self {
687            top: vertical,
688            right: horizontal,
689            bottom: vertical,
690            left: horizontal,
691        }
692    }
693}
694
695impl From<Edges> for SpacingEdges {
696    fn from(value: Edges) -> Self {
697        Self {
698            top: SpacingLength::Px(value.top),
699            right: SpacingLength::Px(value.right),
700            bottom: SpacingLength::Px(value.bottom),
701            left: SpacingLength::Px(value.left),
702        }
703    }
704}
705
706/// A low-opinionated container primitive for declarative authoring.
707///
708/// This is intentionally small and composable: it provides padding and an optional quad background
709/// (including border and corner radii) so component-layer recipes can build shadcn-like widgets
710/// via composition.
711#[derive(Debug, Clone, Copy)]
712pub struct ContainerProps {
713    pub layout: LayoutStyle,
714    pub padding: SpacingEdges,
715    pub background: Option<Color>,
716    /// Optional paint override for the container background (ADR 0233).
717    ///
718    /// When set, this takes precedence over `background` and enables gradients/materials for
719    /// declarative container chrome.
720    pub background_paint: Option<Paint>,
721    pub shadow: Option<ShadowStyle>,
722    pub border: Edges,
723    pub border_color: Option<Color>,
724    /// Optional paint override for the container border (ADR 0233).
725    ///
726    /// When set, this takes precedence over `border_color`.
727    pub border_paint: Option<Paint>,
728    /// Optional dashed border pattern (v1).
729    pub border_dash: Option<fret_core::scene::DashPatternV1>,
730    /// Optional focus-visible ring decoration.
731    pub focus_ring: Option<RingStyle>,
732    /// When true, paint the focus ring even when the element is not focused.
733    ///
734    /// This is intended for component-layer animation parity with CSS `transition` outcomes
735    /// (e.g. `transition-[color,box-shadow]`), where the focus ring can animate out after focus
736    /// moves away.
737    ///
738    /// When `false` (default), the focus ring is painted only while focus-visible is active.
739    pub focus_ring_always_paint: bool,
740    /// Optional border-color override applied when focus-visible is active.
741    ///
742    /// This is primarily used for shadcn-style `focus-visible:border-ring` outcomes without
743    /// requiring a dedicated "border state" API at the layout layer.
744    pub focus_border_color: Option<Color>,
745    /// When true, focus state is derived from any focused descendant (focus-within).
746    pub focus_within: bool,
747    pub corner_radii: Corners,
748    /// When true, snap paint bounds to device pixels (policy-only).
749    pub snap_to_device_pixels: bool,
750}
751
752impl Default for ContainerProps {
753    fn default() -> Self {
754        Self {
755            layout: LayoutStyle::default(),
756            padding: SpacingEdges::all(SpacingLength::Px(Px(0.0))),
757            background: None,
758            background_paint: None,
759            shadow: None,
760            border: Edges::all(Px(0.0)),
761            border_color: None,
762            border_paint: None,
763            border_dash: None,
764            focus_ring: None,
765            focus_ring_always_paint: false,
766            focus_border_color: None,
767            focus_within: false,
768            corner_radii: Corners::all(Px(0.0)),
769            snap_to_device_pixels: false,
770        }
771    }
772}
773
774/// Layout-transparent semantics overrides attached to an existing element (ADR 0222).
775///
776/// This is primarily intended for diagnostics and UI automation (`test_id`) and for restricted
777/// a11y stamping on typed elements without introducing a layout wrapper.
778///
779/// ```ignore
780/// use fret_core::SemanticsRole;
781/// use fret_ui::element::SemanticsDecoration;
782///
783/// let decoration = SemanticsDecoration::default()
784///     .role(SemanticsRole::Checkbox)
785///     .label("Enable autosave")
786///     .checked(Some(true))
787///     .test_id("settings.autosave");
788///
789/// let el = some_element.attach_semantics(decoration);
790/// ```
791#[derive(Debug, Default, Clone)]
792pub struct SemanticsDecoration {
793    pub role: Option<SemanticsRole>,
794    pub label: Option<Arc<str>>,
795    /// Optional role description override (ARIA `aria-roledescription`-like outcome).
796    pub role_description: Option<Arc<str>>,
797    /// Debug/test-only identifier for deterministic automation.
798    ///
799    /// This MUST NOT be mapped into platform accessibility name/label fields by default.
800    pub test_id: Option<Arc<str>>,
801    pub value: Option<Arc<str>>,
802    pub disabled: Option<bool>,
803    pub read_only: Option<bool>,
804    pub required: Option<bool>,
805    pub invalid: Option<fret_core::SemanticsInvalid>,
806    pub hidden: Option<bool>,
807    pub visited: Option<bool>,
808    pub multiselectable: Option<bool>,
809    pub busy: Option<bool>,
810    /// Live region setting (ARIA `aria-live`), applied as a semantics flags override.
811    ///
812    /// `Some(None)` clears any live region semantics from the underlying element.
813    pub live: Option<Option<SemanticsLive>>,
814    pub live_atomic: Option<bool>,
815    pub selected: Option<bool>,
816    pub expanded: Option<bool>,
817    /// Tri-state checked override (Some(None) clears; Some(Some(v)) sets to v).
818    pub checked: Option<Option<bool>>,
819    pub placeholder: Option<Arc<str>>,
820    pub url: Option<Arc<str>>,
821    /// Optional hierarchy level for outline/tree semantics (1-based).
822    pub level: Option<u32>,
823    pub orientation: Option<SemanticsOrientation>,
824    pub numeric_value: Option<f64>,
825    pub min_numeric_value: Option<f64>,
826    pub max_numeric_value: Option<f64>,
827    pub numeric_value_step: Option<f64>,
828    pub numeric_value_jump: Option<f64>,
829    pub scroll_x: Option<f64>,
830    pub scroll_x_min: Option<f64>,
831    pub scroll_x_max: Option<f64>,
832    pub scroll_y: Option<f64>,
833    pub scroll_y_min: Option<f64>,
834    pub scroll_y_max: Option<f64>,
835    /// Declarative-only: element ID of the active descendant for composite widgets.
836    pub active_descendant_element: Option<u64>,
837    /// Declarative-only: element ID of a node which labels this node (`aria-labelledby`).
838    pub labelled_by_element: Option<u64>,
839    /// Declarative-only: element ID of a node which describes this node (`aria-describedby`).
840    pub described_by_element: Option<u64>,
841    /// Declarative-only: element ID of a node which this node controls (`aria-controls`).
842    pub controls_element: Option<u64>,
843    /// Overrides whether this node supports the platform "invoke"/click action.
844    ///
845    /// This is useful for modeling ARIA patterns like Radix Accordion's `aria-disabled` trigger
846    /// state, where the element remains focusable but should not expose an "activate" action.
847    pub invokable: Option<bool>,
848}
849
850impl SemanticsDecoration {
851    /// Merges two decorations, with `other` taking precedence.
852    pub fn merge(self, other: Self) -> Self {
853        Self {
854            role: other.role.or(self.role),
855            label: other.label.or(self.label),
856            role_description: other.role_description.or(self.role_description),
857            test_id: other.test_id.or(self.test_id),
858            value: other.value.or(self.value),
859            disabled: other.disabled.or(self.disabled),
860            read_only: other.read_only.or(self.read_only),
861            required: other.required.or(self.required),
862            invalid: other.invalid.or(self.invalid),
863            hidden: other.hidden.or(self.hidden),
864            visited: other.visited.or(self.visited),
865            multiselectable: other.multiselectable.or(self.multiselectable),
866            busy: other.busy.or(self.busy),
867            live: other.live.or(self.live),
868            live_atomic: other.live_atomic.or(self.live_atomic),
869            selected: other.selected.or(self.selected),
870            expanded: other.expanded.or(self.expanded),
871            checked: other.checked.or(self.checked),
872            placeholder: other.placeholder.or(self.placeholder),
873            url: other.url.or(self.url),
874            level: other.level.or(self.level),
875            orientation: other.orientation.or(self.orientation),
876            numeric_value: other.numeric_value.or(self.numeric_value),
877            min_numeric_value: other.min_numeric_value.or(self.min_numeric_value),
878            max_numeric_value: other.max_numeric_value.or(self.max_numeric_value),
879            numeric_value_step: other.numeric_value_step.or(self.numeric_value_step),
880            numeric_value_jump: other.numeric_value_jump.or(self.numeric_value_jump),
881            scroll_x: other.scroll_x.or(self.scroll_x),
882            scroll_x_min: other.scroll_x_min.or(self.scroll_x_min),
883            scroll_x_max: other.scroll_x_max.or(self.scroll_x_max),
884            scroll_y: other.scroll_y.or(self.scroll_y),
885            scroll_y_min: other.scroll_y_min.or(self.scroll_y_min),
886            scroll_y_max: other.scroll_y_max.or(self.scroll_y_max),
887            active_descendant_element: other
888                .active_descendant_element
889                .or(self.active_descendant_element),
890            labelled_by_element: other.labelled_by_element.or(self.labelled_by_element),
891            described_by_element: other.described_by_element.or(self.described_by_element),
892            controls_element: other.controls_element.or(self.controls_element),
893            invokable: other.invokable.or(self.invokable),
894        }
895    }
896
897    pub fn role(mut self, role: SemanticsRole) -> Self {
898        self.role = Some(role);
899        self
900    }
901
902    pub fn label(mut self, label: impl Into<Arc<str>>) -> Self {
903        self.label = Some(label.into());
904        self
905    }
906
907    pub fn role_description(mut self, role_description: impl Into<Arc<str>>) -> Self {
908        self.role_description = Some(role_description.into());
909        self
910    }
911
912    pub fn test_id(mut self, test_id: impl Into<Arc<str>>) -> Self {
913        self.test_id = Some(test_id.into());
914        self
915    }
916
917    pub fn value(mut self, value: impl Into<Arc<str>>) -> Self {
918        self.value = Some(value.into());
919        self
920    }
921
922    pub fn disabled(mut self, disabled: bool) -> Self {
923        self.disabled = Some(disabled);
924        self
925    }
926
927    pub fn read_only(mut self, read_only: bool) -> Self {
928        self.read_only = Some(read_only);
929        self
930    }
931
932    pub fn required(mut self, required: bool) -> Self {
933        self.required = Some(required);
934        self
935    }
936
937    pub fn invalid(mut self, invalid: fret_core::SemanticsInvalid) -> Self {
938        self.invalid = Some(invalid);
939        self
940    }
941
942    pub fn hidden(mut self, hidden: bool) -> Self {
943        self.hidden = Some(hidden);
944        self
945    }
946
947    pub fn visited(mut self, visited: bool) -> Self {
948        self.visited = Some(visited);
949        self
950    }
951
952    pub fn multiselectable(mut self, multiselectable: bool) -> Self {
953        self.multiselectable = Some(multiselectable);
954        self
955    }
956
957    pub fn busy(mut self, busy: bool) -> Self {
958        self.busy = Some(busy);
959        self
960    }
961
962    pub fn live(mut self, live: Option<SemanticsLive>) -> Self {
963        self.live = Some(live);
964        self
965    }
966
967    pub fn live_atomic(mut self, live_atomic: bool) -> Self {
968        self.live_atomic = Some(live_atomic);
969        self
970    }
971
972    pub fn selected(mut self, selected: bool) -> Self {
973        self.selected = Some(selected);
974        self
975    }
976
977    pub fn expanded(mut self, expanded: bool) -> Self {
978        self.expanded = Some(expanded);
979        self
980    }
981
982    pub fn checked(mut self, checked: Option<bool>) -> Self {
983        self.checked = Some(checked);
984        self
985    }
986
987    pub fn placeholder(mut self, placeholder: impl Into<Arc<str>>) -> Self {
988        self.placeholder = Some(placeholder.into());
989        self
990    }
991
992    pub fn url(mut self, url: impl Into<Arc<str>>) -> Self {
993        self.url = Some(url.into());
994        self
995    }
996
997    pub fn level(mut self, level: u32) -> Self {
998        self.level = Some(level);
999        self
1000    }
1001
1002    pub fn orientation(mut self, orientation: SemanticsOrientation) -> Self {
1003        self.orientation = Some(orientation);
1004        self
1005    }
1006
1007    pub fn numeric_value(mut self, value: f64) -> Self {
1008        self.numeric_value = Some(value);
1009        self
1010    }
1011
1012    pub fn numeric_range(mut self, min: f64, max: f64) -> Self {
1013        self.min_numeric_value = Some(min);
1014        self.max_numeric_value = Some(max);
1015        self
1016    }
1017
1018    pub fn numeric_step(mut self, step: f64) -> Self {
1019        self.numeric_value_step = Some(step);
1020        self
1021    }
1022
1023    pub fn numeric_jump(mut self, jump: f64) -> Self {
1024        self.numeric_value_jump = Some(jump);
1025        self
1026    }
1027
1028    pub fn scroll_x(mut self, x: f64, min: f64, max: f64) -> Self {
1029        self.scroll_x = Some(x);
1030        self.scroll_x_min = Some(min);
1031        self.scroll_x_max = Some(max);
1032        self
1033    }
1034
1035    pub fn scroll_y(mut self, y: f64, min: f64, max: f64) -> Self {
1036        self.scroll_y = Some(y);
1037        self.scroll_y_min = Some(min);
1038        self.scroll_y_max = Some(max);
1039        self
1040    }
1041
1042    pub fn active_descendant_element(mut self, element: u64) -> Self {
1043        self.active_descendant_element = Some(element);
1044        self
1045    }
1046
1047    pub fn labelled_by_element(mut self, element: u64) -> Self {
1048        self.labelled_by_element = Some(element);
1049        self
1050    }
1051
1052    pub fn described_by_element(mut self, element: u64) -> Self {
1053        self.described_by_element = Some(element);
1054        self
1055    }
1056
1057    pub fn controls_element(mut self, element: u64) -> Self {
1058        self.controls_element = Some(element);
1059        self
1060    }
1061
1062    pub fn invokable(mut self, invokable: bool) -> Self {
1063        self.invokable = Some(invokable);
1064        self
1065    }
1066}
1067
1068/// A transparent semantics wrapper for structuring the accessibility tree.
1069///
1070/// This is intentionally input-transparent (hit-test passes through) and paint-transparent: it
1071/// only contributes layout and semantics.
1072///
1073/// Note: `Semantics` is a real layout wrapper. Do not use it only to stamp `test_id` / labels for
1074/// UI automation; prefer `AnyElement::attach_semantics` (`SemanticsDecoration`) to avoid subtle
1075/// layout regressions.
1076#[derive(Debug, Clone)]
1077pub struct SemanticsProps {
1078    pub layout: LayoutStyle,
1079    pub role: SemanticsRole,
1080    pub label: Option<Arc<str>>,
1081    /// Debug/test-only identifier for deterministic automation.
1082    ///
1083    /// This MUST NOT be mapped into platform accessibility name/label fields by default.
1084    pub test_id: Option<Arc<str>>,
1085    pub value: Option<Arc<str>>,
1086    pub placeholder: Option<Arc<str>>,
1087    pub url: Option<Arc<str>>,
1088    /// Optional hierarchy level for outline/tree semantics (1-based).
1089    pub level: Option<u32>,
1090    pub orientation: Option<SemanticsOrientation>,
1091    pub numeric_value: Option<f64>,
1092    pub min_numeric_value: Option<f64>,
1093    pub max_numeric_value: Option<f64>,
1094    pub numeric_value_step: Option<f64>,
1095    pub numeric_value_jump: Option<f64>,
1096    pub scroll_x: Option<f64>,
1097    pub scroll_x_min: Option<f64>,
1098    pub scroll_x_max: Option<f64>,
1099    pub scroll_y: Option<f64>,
1100    pub scroll_y_min: Option<f64>,
1101    pub scroll_y_max: Option<f64>,
1102    /// Whether this semantics wrapper participates in focus traversal.
1103    ///
1104    /// Note: this is intentionally separate from pointer hit-testing. `Semantics` remains
1105    /// input-transparent; use `Pressable` when you need pointer-driven focus.
1106    pub focusable: bool,
1107    /// Overrides whether this node supports `SetValue` actions (text or numeric).
1108    ///
1109    /// For `TextField` roles, this surfaces as the platform's "set value" action surface.
1110    ///
1111    /// For `Slider` roles, this is interpreted as stepper semantics and maps to
1112    /// Increment/Decrement actions. `SetValue` for sliders is derived conservatively by the
1113    /// runtime when sufficient numeric metadata is present.
1114    pub value_editable: Option<bool>,
1115    pub disabled: bool,
1116    pub read_only: bool,
1117    pub required: bool,
1118    pub invalid: Option<fret_core::SemanticsInvalid>,
1119    pub hidden: bool,
1120    pub visited: bool,
1121    pub multiselectable: bool,
1122    pub busy: bool,
1123    pub live: Option<SemanticsLive>,
1124    pub live_atomic: bool,
1125    pub selected: bool,
1126    pub expanded: Option<bool>,
1127    pub checked: Option<bool>,
1128    pub active_descendant: Option<NodeId>,
1129    /// Declarative-only: element ID of a node which labels this node.
1130    ///
1131    /// This is an authoring convenience for relationships like `aria-labelledby` where the target
1132    /// is another declarative element. The runtime resolves this into a `NodeId` during semantics
1133    /// snapshot production.
1134    pub labelled_by_element: Option<u64>,
1135    /// Declarative-only: element ID of a node which describes this node.
1136    ///
1137    /// This is an authoring convenience for relationships like `aria-describedby` where the target
1138    /// is another declarative element. The runtime resolves this into a `NodeId` during semantics
1139    /// snapshot production.
1140    pub described_by_element: Option<u64>,
1141    /// Declarative-only: element ID of a node which this node controls.
1142    ///
1143    /// This is an authoring convenience for relationships like `aria-controls` where the target
1144    /// is another declarative element. The runtime resolves this into a `NodeId` during semantics
1145    /// snapshot production.
1146    pub controls_element: Option<u64>,
1147}
1148
1149impl Default for SemanticsProps {
1150    fn default() -> Self {
1151        Self {
1152            layout: LayoutStyle::default(),
1153            role: SemanticsRole::Generic,
1154            label: None,
1155            test_id: None,
1156            value: None,
1157            placeholder: None,
1158            url: None,
1159            level: None,
1160            orientation: None,
1161            numeric_value: None,
1162            min_numeric_value: None,
1163            max_numeric_value: None,
1164            numeric_value_step: None,
1165            numeric_value_jump: None,
1166            scroll_x: None,
1167            scroll_x_min: None,
1168            scroll_x_max: None,
1169            scroll_y: None,
1170            scroll_y_min: None,
1171            scroll_y_max: None,
1172            focusable: false,
1173            value_editable: None,
1174            disabled: false,
1175            read_only: false,
1176            required: false,
1177            invalid: None,
1178            hidden: false,
1179            visited: false,
1180            multiselectable: false,
1181            busy: false,
1182            live: None,
1183            live_atomic: false,
1184            selected: false,
1185            expanded: None,
1186            checked: None,
1187            active_descendant: None,
1188            labelled_by_element: None,
1189            described_by_element: None,
1190            controls_element: None,
1191        }
1192    }
1193}
1194
1195/// A paint- and input-transparent layout wrapper that records a queryable bounds snapshot.
1196///
1197/// This is a mechanism-only primitive: breakpoint tables and hysteresis policies live in the
1198/// component ecosystem (ADR 0066 / ADR 0231).
1199#[derive(Debug, Default, Clone)]
1200pub struct LayoutQueryRegionProps {
1201    pub layout: LayoutStyle,
1202    /// Optional name used for diagnostics and audit readability.
1203    ///
1204    /// This is not a stable identifier and must not be used for equality.
1205    pub name: Option<Arc<str>>,
1206}
1207
1208/// A transparent focus-scope wrapper that can trap focus traversal within its subtree.
1209///
1210/// This is a small, mechanism-oriented primitive intended to support component-owned focus scopes
1211/// (ADR 0068). It does not imply modal barriers or pointer blocking; it only affects `focus.next`
1212/// / `focus.previous` command routing when focus is inside the subtree.
1213#[derive(Debug, Default, Clone, Copy)]
1214pub struct FocusScopeProps {
1215    pub layout: LayoutStyle,
1216    pub trap_focus: bool,
1217}
1218
1219/// Gate subtree presence (layout/paint) and interactivity (hit-testing + focus traversal).
1220///
1221/// When `present == false`, the subtree remains mounted but is treated like `display: none`:
1222/// it does not participate in layout, paint, hit-testing, or focus traversal.
1223///
1224/// When `present == true` and `interactive == false`, the subtree is still laid out/painted but is
1225/// inert for pointer and focus traversal (useful for close animations).
1226#[derive(Debug, Clone, Copy)]
1227pub struct InteractivityGateProps {
1228    pub layout: LayoutStyle,
1229    pub present: bool,
1230    pub interactive: bool,
1231}
1232
1233impl Default for InteractivityGateProps {
1234    fn default() -> Self {
1235        Self {
1236            layout: LayoutStyle::default(),
1237            present: true,
1238            interactive: true,
1239        }
1240    }
1241}
1242
1243/// Gate pointer hit-testing for a subtree without affecting focus traversal.
1244///
1245/// This is intentionally narrower than `InteractivityGateProps`: it does not change whether the
1246/// subtree participates in focus traversal or semantics snapshots.
1247#[derive(Debug, Clone, Copy)]
1248pub struct HitTestGateProps {
1249    pub layout: LayoutStyle,
1250    pub hit_test: bool,
1251}
1252
1253impl Default for HitTestGateProps {
1254    fn default() -> Self {
1255        Self {
1256            layout: LayoutStyle::default(),
1257            hit_test: true,
1258        }
1259    }
1260}
1261
1262/// Gate focus traversal for a subtree without affecting pointer hit-testing.
1263///
1264/// This is intentionally narrower than `InteractivityGateProps`: it does not change whether the
1265/// subtree participates in pointer hit-testing or semantics snapshots.
1266#[derive(Debug, Clone, Copy)]
1267pub struct FocusTraversalGateProps {
1268    pub layout: LayoutStyle,
1269    pub traverse: bool,
1270}
1271
1272impl Default for FocusTraversalGateProps {
1273    fn default() -> Self {
1274        Self {
1275            layout: LayoutStyle::default(),
1276            traverse: true,
1277        }
1278    }
1279}
1280
1281/// A paint-only opacity group wrapper (ADR 0019).
1282///
1283/// This is intentionally layout-only + paint-only: it does not imply semantics beyond its
1284/// children, and it is input-transparent (hit-test passes through).
1285#[derive(Debug, Clone, Copy)]
1286pub struct OpacityProps {
1287    pub layout: LayoutStyle,
1288    pub opacity: f32,
1289}
1290
1291impl Default for OpacityProps {
1292    fn default() -> Self {
1293        Self {
1294            layout: LayoutStyle::default(),
1295            opacity: 1.0,
1296        }
1297    }
1298}
1299
1300/// A paint-only foreground scope wrapper (v2).
1301///
1302/// This is intentionally layout-only + paint-only: it does not imply semantics beyond its
1303/// children, and it is input-transparent (hit-test passes through).
1304#[derive(Debug, Clone, Copy, Default)]
1305pub struct ForegroundScopeProps {
1306    pub layout: LayoutStyle,
1307    pub foreground: Option<Color>,
1308}
1309
1310/// Scoped post-processing effect wrapper for declarative element subtrees (ADR 0117).
1311///
1312/// This emits a `SceneOp::PushEffect/PopEffect` pair around the subtree during painting. The
1313/// effect's computation bounds are the wrapper's final layout bounds.
1314#[derive(Debug, Clone, Copy)]
1315pub struct EffectLayerProps {
1316    pub layout: LayoutStyle,
1317    pub mode: EffectMode,
1318    pub chain: EffectChain,
1319    pub quality: EffectQuality,
1320}
1321
1322impl Default for EffectLayerProps {
1323    fn default() -> Self {
1324        Self {
1325            layout: LayoutStyle::default(),
1326            mode: EffectMode::FilterContent,
1327            chain: EffectChain::EMPTY,
1328            quality: EffectQuality::Auto,
1329        }
1330    }
1331}
1332
1333/// Scoped backdrop source group wrapper for declarative element subtrees (ADR 0305).
1334///
1335/// This emits a `SceneOp::PushBackdropSourceGroupV1/PopBackdropSourceGroup` pair around the
1336/// subtree during painting. The group's computation bounds are the wrapper's final layout bounds.
1337#[derive(Debug, Clone, Copy)]
1338pub struct BackdropSourceGroupProps {
1339    pub layout: LayoutStyle,
1340    pub pyramid: Option<CustomEffectPyramidRequestV1>,
1341    pub quality: EffectQuality,
1342}
1343
1344impl Default for BackdropSourceGroupProps {
1345    fn default() -> Self {
1346        Self {
1347            layout: LayoutStyle::default(),
1348            pyramid: None,
1349            quality: EffectQuality::Auto,
1350        }
1351    }
1352}
1353
1354/// Scoped alpha mask wrapper for declarative element subtrees (ADR 0239).
1355///
1356/// This emits a `SceneOp::PushMask/PopMask` pair around the subtree during painting. The mask's
1357/// computation bounds are the wrapper's final layout bounds.
1358#[derive(Debug, Clone, Copy, PartialEq)]
1359pub struct MaskLayerProps {
1360    pub layout: LayoutStyle,
1361    pub mask: Mask,
1362}
1363
1364/// Scoped isolated compositing group wrapper for declarative element subtrees (ADR 0247).
1365///
1366/// This emits a `SceneOp::PushCompositeGroup/PopCompositeGroup` pair around the subtree during
1367/// painting. The group's computation bounds are the wrapper's final layout bounds.
1368#[derive(Debug, Clone, Copy, PartialEq)]
1369pub struct CompositeGroupProps {
1370    pub layout: LayoutStyle,
1371    pub mode: BlendMode,
1372    pub quality: EffectQuality,
1373}
1374
1375impl Default for CompositeGroupProps {
1376    fn default() -> Self {
1377        Self {
1378            layout: LayoutStyle::default(),
1379            mode: BlendMode::Over,
1380            quality: EffectQuality::Auto,
1381        }
1382    }
1383}
1384
1385/// Experimental cache boundary wrapper for declarative element subtrees.
1386///
1387/// This is a mechanism-only primitive intended to support GPUI-style view caching experiments
1388/// without committing to a stable authoring API.
1389#[derive(Debug, Clone, Copy, Default)]
1390pub struct ViewCacheProps {
1391    pub layout: LayoutStyle,
1392    /// Whether the subtree should be treated as layout-contained by the runtime when view caching is enabled.
1393    pub contained_layout: bool,
1394    /// Explicit cache key for view-cache reuse (experimental).
1395    ///
1396    /// The runtime will reuse cached output for this view-cache root only when the computed key is
1397    /// unchanged. This mirrors GPUI's `ViewCacheKey` gating behavior.
1398    pub cache_key: u64,
1399}
1400
1401/// Paint-only transform wrapper for declarative element subtrees.
1402///
1403/// This applies a `SceneOp::PushTransform` / `PopTransform` around the subtree during painting,
1404/// without affecting layout, hit-testing, or pointer event coordinates.
1405///
1406/// This is intentionally similar to GPUI's `with_transformation(...)` semantics for elements like
1407/// `Svg`: it is useful for spinners and decorative animations, and is cheap to optimize because it
1408/// does not require inverse mapping during hit-testing.
1409#[derive(Debug, Clone, Copy, Default)]
1410pub struct VisualTransformProps {
1411    pub layout: LayoutStyle,
1412    /// A transform expressed in the element's local coordinate space.
1413    ///
1414    /// The runtime composes this around the element's bounds origin so that local transforms can be
1415    /// expressed in px relative to the element (e.g. rotate around `Point(Px(w/2), Px(h/2))`).
1416    pub transform: fret_core::Transform2D,
1417}
1418
1419/// Render transform wrapper for declarative element subtrees.
1420///
1421/// This applies `Widget::render_transform(...)` for the subtree rooted at this element:
1422/// - Paint and hit-testing are both transformed.
1423/// - Pointer event coordinates are mapped through the inverse transform (when invertible).
1424/// - Layout bounds remain authoritative (this is not a layout transform).
1425///
1426/// This is useful for interactive translations (e.g. drag-to-dismiss surfaces) that must keep input
1427/// aligned with the rendered output.
1428#[derive(Debug, Clone, Copy, Default)]
1429pub struct RenderTransformProps {
1430    pub layout: LayoutStyle,
1431    pub transform: fret_core::Transform2D,
1432}
1433
1434/// Render transform wrapper for declarative element subtrees.
1435///
1436/// This is a convenience wrapper for cases where the desired translation is best expressed as a
1437/// fraction of the element's own laid-out bounds, similar to CSS percentage translate operations.
1438///
1439/// This is computed during layout so the first painted frame can use the correct pixel offset.
1440#[derive(Debug, Clone, Copy, Default)]
1441pub struct FractionalRenderTransformProps {
1442    pub layout: LayoutStyle,
1443    /// Translation in units of the element's own width (e.g. `-1.0` shifts left by one full width).
1444    pub translate_x_fraction: f32,
1445    /// Translation in units of the element's own height.
1446    pub translate_y_fraction: f32,
1447}
1448
1449/// Layout-driven anchored placement wrapper for declarative element subtrees (ADR 0103).
1450///
1451/// This wrapper computes a placement transform during layout (based on the child's intrinsic
1452/// size) and applies it via the retained runtime's `Widget::render_transform` hook.
1453///
1454/// Unlike `VisualTransformProps`, this affects hit-testing and pointer coordinate mapping.
1455#[derive(Debug, Clone)]
1456pub struct AnchoredProps {
1457    pub layout: LayoutStyle,
1458    /// Insets applied to the wrapper bounds before placement.
1459    pub outer_margin: Edges,
1460    /// Anchor rect in the same coordinate space as the wrapper bounds.
1461    pub anchor: fret_core::Rect,
1462    /// Optional anchor element ID to resolve during layout (ADR 0103).
1463    ///
1464    /// When set, the layout pass attempts to resolve the element's current-frame bounds and uses
1465    /// that rect as the anchor. This avoids cross-frame geometry jitter from
1466    /// `bounds_for_element(...)` / `last_bounds_for_element(...)` queries and better matches GPUI's
1467    /// layout-driven placement model.
1468    ///
1469    /// If the element cannot be resolved (e.g. not mounted yet), `anchor` is used as a fallback.
1470    pub anchor_element: Option<u64>,
1471    pub side: Side,
1472    pub align: Align,
1473    /// Gap between the anchor and the placed subtree.
1474    pub side_offset: Px,
1475    pub options: AnchoredPanelOptions,
1476    /// Optional output model updated with the computed layout during layout.
1477    pub layout_out: Option<Model<AnchoredPanelLayout>>,
1478}
1479
1480impl Default for AnchoredProps {
1481    fn default() -> Self {
1482        let mut layout = LayoutStyle::default();
1483        layout.size.width = Length::Fill;
1484        layout.size.height = Length::Fill;
1485
1486        Self {
1487            layout,
1488            outer_margin: Edges::all(Px(0.0)),
1489            anchor: fret_core::Rect::default(),
1490            anchor_element: None,
1491            side: Side::Bottom,
1492            align: Align::Start,
1493            side_offset: Px(0.0),
1494            options: AnchoredPanelOptions::default(),
1495            layout_out: None,
1496        }
1497    }
1498}
1499
1500/// One `box-shadow` layer (CSS-style) for component-level elevation recipes.
1501///
1502/// This is renderer-friendly: runtimes can approximate blur by drawing multiple expanded quads with
1503/// alpha falloff (ADR 0060) until we have a true blur pipeline.
1504#[derive(Debug, Clone, Copy, PartialEq)]
1505pub struct ShadowLayerStyle {
1506    pub color: Color,
1507    pub offset_x: Px,
1508    pub offset_y: Px,
1509    /// Blur radius in pixels.
1510    pub blur: Px,
1511    /// Spread radius in pixels (can be negative).
1512    pub spread: Px,
1513}
1514
1515/// A low-level drop shadow primitive for component-level elevation recipes.
1516///
1517/// Many Tailwind/shadcn recipes are multi-layer shadows (e.g. `shadow-md`), so we support up to two
1518/// layers without forcing heap allocation (keeps `ContainerProps` `Copy`).
1519#[derive(Debug, Clone, Copy, PartialEq)]
1520pub struct ShadowStyle {
1521    pub primary: ShadowLayerStyle,
1522    pub secondary: Option<ShadowLayerStyle>,
1523    pub corner_radii: Corners,
1524}
1525
1526#[derive(Clone)]
1527pub struct PressableProps {
1528    pub layout: LayoutStyle,
1529    pub enabled: bool,
1530    /// Whether this pressable is a focus traversal stop (Tab order).
1531    ///
1532    /// When `false`, the node can still be focused programmatically (e.g. roving focus),
1533    /// but it is skipped by the default focus traversal.
1534    pub focusable: bool,
1535    pub focus_ring: Option<RingStyle>,
1536    /// When true, paint the focus ring even when the pressable is not focused.
1537    ///
1538    /// This is intended for component-layer animation parity with CSS `transition` outcomes where
1539    /// focus ring (box-shadow-like) decorations can animate out after focus changes.
1540    ///
1541    /// When `false` (default), the focus ring is painted only while focus-visible is active on the
1542    /// pressable.
1543    pub focus_ring_always_paint: bool,
1544    /// Optional override for the bounds used when painting the focus ring.
1545    ///
1546    /// Coordinates are **local** to the pressable's origin (i.e. `0,0` is the pressable's top-left),
1547    /// and are translated into absolute coordinates at paint time.
1548    ///
1549    /// This is useful when the pressable is wider than the visual control chrome (e.g. a "row"
1550    /// pressable that should paint focus ring only around an icon-sized control).
1551    pub focus_ring_bounds: Option<Rect>,
1552    pub key_activation: PressableKeyActivation,
1553    pub a11y: PressableA11y,
1554}
1555
1556impl std::fmt::Debug for PressableProps {
1557    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1558        let mut out = f.debug_struct("PressableProps");
1559        out.field("layout", &self.layout)
1560            .field("enabled", &self.enabled)
1561            .field("focusable", &self.focusable);
1562
1563        out.field("focus_ring", &self.focus_ring)
1564            .field("focus_ring_always_paint", &self.focus_ring_always_paint)
1565            .field("focus_ring_bounds", &self.focus_ring_bounds)
1566            .field("key_activation", &self.key_activation)
1567            .field("a11y", &self.a11y)
1568            .finish()
1569    }
1570}
1571
1572impl Default for PressableProps {
1573    fn default() -> Self {
1574        Self {
1575            layout: LayoutStyle::default(),
1576            enabled: true,
1577            focusable: true,
1578            focus_ring: None,
1579            focus_ring_always_paint: false,
1580            focus_ring_bounds: None,
1581            key_activation: PressableKeyActivation::default(),
1582            a11y: PressableA11y::default(),
1583        }
1584    }
1585}
1586
1587#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
1588pub enum PressableKeyActivation {
1589    /// Activate on Enter/NumpadEnter and Space (button-like default).
1590    #[default]
1591    EnterAndSpace,
1592    /// Activate on Enter/NumpadEnter only (link-like).
1593    EnterOnly,
1594}
1595
1596impl PressableKeyActivation {
1597    pub fn allows(self, key: KeyCode) -> bool {
1598        match self {
1599            Self::EnterAndSpace => {
1600                matches!(key, KeyCode::Enter | KeyCode::NumpadEnter | KeyCode::Space)
1601            }
1602            Self::EnterOnly => matches!(key, KeyCode::Enter | KeyCode::NumpadEnter),
1603        }
1604    }
1605}
1606
1607#[derive(Clone, Default)]
1608pub struct RovingFlexProps {
1609    pub flex: FlexProps,
1610    pub roving: RovingFocusProps,
1611}
1612
1613impl std::fmt::Debug for RovingFlexProps {
1614    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1615        f.debug_struct("RovingFlexProps")
1616            .field("flex", &self.flex)
1617            .field("roving", &self.roving)
1618            .finish()
1619    }
1620}
1621
1622#[derive(Debug, Clone)]
1623pub struct RovingFocusProps {
1624    pub enabled: bool,
1625    pub wrap: bool,
1626    pub disabled: Arc<[bool]>,
1627}
1628
1629impl Default for RovingFocusProps {
1630    fn default() -> Self {
1631        Self {
1632            enabled: true,
1633            wrap: true,
1634            disabled: Arc::from([]),
1635        }
1636    }
1637}
1638
1639#[derive(Debug, Default, Clone)]
1640pub struct PressableA11y {
1641    pub role: Option<SemanticsRole>,
1642    pub label: Option<Arc<str>>,
1643    /// Optional hierarchy level for outline/tree semantics (1-based).
1644    pub level: Option<u32>,
1645    /// Debug/test-only identifier for deterministic automation.
1646    ///
1647    /// This MUST NOT be mapped into platform accessibility name/label fields by default.
1648    pub test_id: Option<Arc<str>>,
1649    /// When true, suppress exposing this pressable to assistive technologies (aria-hidden).
1650    ///
1651    /// This is useful for purely visual affordances (e.g. decorative scroll buttons in Radix
1652    /// Select) that should remain interactive for pointer users but should not appear in the
1653    /// accessibility tree.
1654    pub hidden: bool,
1655    /// Indicates that the pressable represents a visited link.
1656    ///
1657    /// This is a portable approximation of the "visited link" concept in HTML.
1658    pub visited: bool,
1659    /// Indicates that this collection supports selecting multiple items.
1660    ///
1661    /// This is a portable approximation of ARIA `aria-multiselectable`.
1662    pub multiselectable: bool,
1663    pub required: bool,
1664    pub invalid: Option<fret_core::SemanticsInvalid>,
1665    pub selected: bool,
1666    pub expanded: Option<bool>,
1667    pub checked: Option<bool>,
1668    pub checked_state: Option<fret_core::SemanticsCheckedState>,
1669    pub pressed_state: Option<fret_core::SemanticsPressedState>,
1670    pub active_descendant: Option<NodeId>,
1671    /// Declarative-only: element ID of a node which labels this node.
1672    ///
1673    /// This is an authoring convenience for relationships like `aria-labelledby` where the target
1674    /// is another declarative element. The runtime resolves this into a `NodeId` during semantics
1675    /// snapshot production.
1676    pub labelled_by_element: Option<u64>,
1677    /// Declarative-only: element ID of a node which describes this node.
1678    ///
1679    /// This is an authoring convenience for relationships like `aria-describedby` where the target
1680    /// is another declarative element. The runtime resolves this into a `NodeId` during semantics
1681    /// snapshot production.
1682    pub described_by_element: Option<u64>,
1683    /// Declarative-only: element ID of a node which this node controls.
1684    ///
1685    /// This is an authoring convenience for relationships like `aria-controls` where the target
1686    /// is another declarative element. The runtime resolves this into a `NodeId` during semantics
1687    /// snapshot production.
1688    pub controls_element: Option<u64>,
1689    pub pos_in_set: Option<u32>,
1690    pub set_size: Option<u32>,
1691}
1692
1693#[derive(Debug, Clone, Copy, Default)]
1694pub struct PressableState {
1695    pub hovered: bool,
1696    pub hovered_raw: bool,
1697    /// Pointer-hover signal that ignores modal/popup barrier gating.
1698    ///
1699    /// When a modal barrier is active (e.g. a popup that blocks underlay input), the UI runtime
1700    /// suppresses underlay hit-testing and hover for blocked layers. This flag is populated from a
1701    /// best-effort underlay hit-test performed only when the pointer is not currently over any
1702    /// active (non-blocked) layer.
1703    pub hovered_raw_below_barrier: bool,
1704    pub pressed: bool,
1705    pub focused: bool,
1706}
1707
1708#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1709pub enum RingPlacement {
1710    /// Draw the ring inside the element bounds.
1711    Inset,
1712    /// Draw the ring outside the element bounds (best effort; may be clipped by parent clips).
1713    #[default]
1714    Outset,
1715}
1716
1717/// A simple focus ring decoration, intended for component-layer recipes (e.g. shadcn-style
1718/// focus-visible ring).
1719///
1720/// This is intentionally small and renderer-friendly: it maps to one or two `SceneOp::Quad`
1721/// operations.
1722#[derive(Debug, Clone, Copy, PartialEq)]
1723pub struct RingStyle {
1724    pub placement: RingPlacement,
1725    pub width: Px,
1726    pub offset: Px,
1727    pub color: Color,
1728    pub offset_color: Option<Color>,
1729    pub corner_radii: Corners,
1730}
1731
1732#[derive(Debug, Default, Clone, Copy)]
1733pub struct StackProps {
1734    pub layout: LayoutStyle,
1735}
1736
1737#[derive(Debug, Clone, Copy)]
1738pub struct ColumnProps {
1739    pub layout: LayoutStyle,
1740    pub gap: SpacingLength,
1741    pub padding: SpacingEdges,
1742    pub justify: MainAlign,
1743    pub align: CrossAlign,
1744}
1745
1746impl Default for ColumnProps {
1747    fn default() -> Self {
1748        Self {
1749            layout: LayoutStyle::default(),
1750            gap: SpacingLength::Px(Px(0.0)),
1751            padding: SpacingEdges::all(SpacingLength::Px(Px(0.0))),
1752            justify: MainAlign::Start,
1753            align: CrossAlign::Stretch,
1754        }
1755    }
1756}
1757
1758#[derive(Debug, Clone, Copy)]
1759pub struct RowProps {
1760    pub layout: LayoutStyle,
1761    pub gap: SpacingLength,
1762    pub padding: SpacingEdges,
1763    pub justify: MainAlign,
1764    pub align: CrossAlign,
1765}
1766
1767impl Default for RowProps {
1768    fn default() -> Self {
1769        Self {
1770            layout: LayoutStyle::default(),
1771            gap: SpacingLength::Px(Px(0.0)),
1772            padding: SpacingEdges::all(SpacingLength::Px(Px(0.0))),
1773            justify: MainAlign::Start,
1774            align: CrossAlign::Stretch,
1775        }
1776    }
1777}
1778
1779#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1780pub enum MainAlign {
1781    #[default]
1782    Start,
1783    Center,
1784    End,
1785    SpaceBetween,
1786    SpaceAround,
1787    SpaceEvenly,
1788}
1789
1790#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1791pub enum CrossAlign {
1792    Start,
1793    #[default]
1794    Center,
1795    End,
1796    Stretch,
1797}
1798
1799#[derive(Debug, Clone, Copy)]
1800pub struct SpacerProps {
1801    pub layout: LayoutStyle,
1802    pub min: Px,
1803}
1804
1805impl Default for SpacerProps {
1806    fn default() -> Self {
1807        let mut layout = LayoutStyle::default();
1808        layout.flex.grow = 1.0;
1809        layout.flex.shrink = 1.0;
1810        layout.flex.basis = Length::Px(Px(0.0));
1811        Self {
1812            layout,
1813            min: Px(0.0),
1814        }
1815    }
1816}
1817
1818#[derive(Debug, Clone)]
1819pub struct TextProps {
1820    pub layout: LayoutStyle,
1821    pub text: std::sync::Arc<str>,
1822    pub style: Option<TextStyle>,
1823    pub color: Option<Color>,
1824    pub wrap: TextWrap,
1825    pub overflow: TextOverflow,
1826    pub align: TextAlign,
1827    /// Policy for handling ink overflow when the line box is fixed (e.g. emoji/CJK fallback).
1828    ///
1829    /// This is a mechanism-level escape hatch to avoid visual clipping when parents clip to
1830    /// rounded corners or other shapes. Callers that want typography-driven layout should prefer
1831    /// `TextLineHeightPolicy::ExpandToFit` instead.
1832    pub ink_overflow: TextInkOverflow,
1833}
1834
1835#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1836pub enum TextInkOverflow {
1837    /// Do not apply any extra padding; tall glyph ink may be clipped if an ancestor clips.
1838    #[default]
1839    None,
1840    /// Adds top/bottom padding to accommodate first/last-line ink extents when available.
1841    ///
1842    /// Notes:
1843    /// - This is best-effort: if ink metrics are unavailable, no padding is applied.
1844    /// - If the widget is height-constrained, padding may be partially applied (or ignored).
1845    AutoPad,
1846}
1847
1848#[derive(Debug, Clone)]
1849pub struct StyledTextProps {
1850    pub layout: LayoutStyle,
1851    pub rich: AttributedText,
1852    pub style: Option<TextStyle>,
1853    /// Base color for glyphs without a per-run override.
1854    pub color: Option<Color>,
1855    pub wrap: TextWrap,
1856    pub overflow: TextOverflow,
1857    pub align: TextAlign,
1858    pub ink_overflow: TextInkOverflow,
1859}
1860
1861#[derive(Debug, Clone)]
1862pub struct SelectableTextProps {
1863    pub layout: LayoutStyle,
1864    pub rich: AttributedText,
1865    pub style: Option<TextStyle>,
1866    /// Base color for glyphs without a per-run override.
1867    pub color: Option<Color>,
1868    pub wrap: TextWrap,
1869    pub overflow: TextOverflow,
1870    pub align: TextAlign,
1871    pub ink_overflow: TextInkOverflow,
1872    /// Optional interactive span ranges (e.g. link spans).
1873    ///
1874    /// The runtime owns hit-testing and event routing; component/ecosystem code decides what
1875    /// activation does via `ElementContext::selectable_text_on_activate_span*` hooks.
1876    pub interactive_spans: std::sync::Arc<[SelectableTextInteractiveSpan]>,
1877}
1878
1879#[derive(Debug, Clone, PartialEq, Eq)]
1880pub struct SelectableTextInteractiveSpan {
1881    pub range: std::ops::Range<usize>,
1882    /// A stable, component-defined tag for the span (e.g. a URL for markdown links).
1883    pub tag: std::sync::Arc<str>,
1884}
1885
1886#[derive(Debug, Clone, PartialEq)]
1887pub struct SelectableTextInteractiveSpanBounds {
1888    pub range: std::ops::Range<usize>,
1889    pub tag: std::sync::Arc<str>,
1890    /// Span bounds in local widget coordinates (relative to the widget's bounds origin).
1891    pub bounds_local: fret_core::Rect,
1892}
1893
1894#[derive(Debug, Clone)]
1895pub struct SelectableTextState {
1896    pub selection_anchor: usize,
1897    pub caret: usize,
1898    pub affinity: CaretAffinity,
1899    pub preferred_x: Option<Px>,
1900    pub dragging: bool,
1901    pub last_pointer_pos: Option<fret_core::Point>,
1902    pub pointer_down_pos: Option<fret_core::Point>,
1903    pub pending_span_activation: Option<crate::action::SelectableTextSpanActivation>,
1904    pub pending_span_click_count: u8,
1905    pub interactive_span_bounds: Vec<SelectableTextInteractiveSpanBounds>,
1906}
1907
1908impl Default for SelectableTextState {
1909    fn default() -> Self {
1910        Self {
1911            selection_anchor: 0,
1912            caret: 0,
1913            affinity: CaretAffinity::Downstream,
1914            preferred_x: None,
1915            dragging: false,
1916            last_pointer_pos: None,
1917            pointer_down_pos: None,
1918            pending_span_activation: None,
1919            pending_span_click_count: 0,
1920            interactive_span_bounds: Vec::new(),
1921        }
1922    }
1923}
1924
1925#[derive(Clone)]
1926pub struct TextInputProps {
1927    pub layout: LayoutStyle,
1928    pub enabled: bool,
1929    pub focusable: bool,
1930    pub model: Model<String>,
1931    pub a11y_label: Option<std::sync::Arc<str>>,
1932    pub a11y_role: Option<SemanticsRole>,
1933    pub test_id: Option<std::sync::Arc<str>>,
1934    pub placeholder: Option<std::sync::Arc<str>>,
1935    /// When true, visually obscures the rendered text (e.g. password fields) while keeping the
1936    /// underlying model value unchanged.
1937    pub obscure_text: bool,
1938    pub a11y_required: bool,
1939    pub a11y_invalid: Option<fret_core::SemanticsInvalid>,
1940    pub active_descendant: Option<NodeId>,
1941    /// Declarative-only: element ID of the active descendant for composite widgets.
1942    ///
1943    /// This is an authoring convenience for `aria-activedescendant`-style relationships where the
1944    /// target is another declarative element. The runtime resolves this into a `NodeId` during
1945    /// semantics snapshot production.
1946    pub active_descendant_element: Option<u64>,
1947    /// Declarative-only: element ID of a node which this text input controls.
1948    ///
1949    /// This is an authoring convenience for relationships like `aria-controls` where the target
1950    /// is another declarative element. The runtime resolves this into a `NodeId` during semantics
1951    /// snapshot production.
1952    pub controls_element: Option<u64>,
1953    pub expanded: Option<bool>,
1954    pub chrome: TextInputStyle,
1955    /// When true, paints the focus ring even if focus-visible is currently false.
1956    ///
1957    /// This exists to support CSS-like `transition-[..., box-shadow]` semantics where the ring
1958    /// animates out after blur. Policy code is expected to drive ring alpha to zero and set this
1959    /// flag only while the transition is animating.
1960    pub focus_ring_always_paint: bool,
1961    pub text_style: TextStyle,
1962    pub submit_command: Option<CommandId>,
1963    pub cancel_command: Option<CommandId>,
1964}
1965
1966impl TextInputProps {
1967    pub fn new(model: Model<String>) -> Self {
1968        Self {
1969            layout: LayoutStyle::default(),
1970            enabled: true,
1971            focusable: true,
1972            model,
1973            a11y_label: None,
1974            a11y_role: None,
1975            test_id: None,
1976            placeholder: None,
1977            obscure_text: false,
1978            a11y_required: false,
1979            a11y_invalid: None,
1980            active_descendant: None,
1981            active_descendant_element: None,
1982            controls_element: None,
1983            expanded: None,
1984            chrome: TextInputStyle::default(),
1985            focus_ring_always_paint: false,
1986            text_style: TextStyle::default(),
1987            submit_command: None,
1988            cancel_command: None,
1989        }
1990    }
1991}
1992
1993impl std::fmt::Debug for TextInputProps {
1994    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1995        f.debug_struct("TextInputProps")
1996            .field("layout", &self.layout)
1997            .field("enabled", &self.enabled)
1998            .field("focusable", &self.focusable)
1999            .field("model", &"<model>")
2000            .field("a11y_label", &self.a11y_label.as_ref().map(|s| s.as_ref()))
2001            .field("a11y_role", &self.a11y_role)
2002            .field("test_id", &self.test_id.as_ref().map(|s| s.as_ref()))
2003            .field(
2004                "placeholder",
2005                &self.placeholder.as_ref().map(|s| s.as_ref()),
2006            )
2007            .field("obscure_text", &self.obscure_text)
2008            .field("active_descendant_element", &self.active_descendant_element)
2009            .field("controls_element", &self.controls_element)
2010            .field("expanded", &self.expanded)
2011            .field("chrome", &self.chrome)
2012            .field("focus_ring_always_paint", &self.focus_ring_always_paint)
2013            .field("text_style", &self.text_style)
2014            .field("submit_command", &self.submit_command)
2015            .field("cancel_command", &self.cancel_command)
2016            .finish()
2017    }
2018}
2019
2020#[derive(Clone)]
2021pub struct TextAreaProps {
2022    pub layout: LayoutStyle,
2023    pub enabled: bool,
2024    pub focusable: bool,
2025    pub model: Model<String>,
2026    pub placeholder: Option<std::sync::Arc<str>>,
2027    pub a11y_required: bool,
2028    pub a11y_invalid: Option<fret_core::SemanticsInvalid>,
2029    pub a11y_label: Option<std::sync::Arc<str>>,
2030    pub test_id: Option<std::sync::Arc<str>>,
2031    pub chrome: TextAreaStyle,
2032    /// When true, paints the focus ring even if focus-visible is currently false.
2033    ///
2034    /// This exists to support CSS-like `transition-[..., box-shadow]` semantics where the ring
2035    /// animates out after blur. Policy code is expected to drive ring alpha to zero and set this
2036    /// flag only while the transition is animating.
2037    pub focus_ring_always_paint: bool,
2038    pub text_style: TextStyle,
2039    pub min_height: Px,
2040}
2041
2042impl TextAreaProps {
2043    pub fn new(model: Model<String>) -> Self {
2044        Self {
2045            layout: LayoutStyle::default(),
2046            enabled: true,
2047            focusable: true,
2048            model,
2049            placeholder: None,
2050            a11y_required: false,
2051            a11y_invalid: None,
2052            a11y_label: None,
2053            test_id: None,
2054            chrome: TextAreaStyle::default(),
2055            focus_ring_always_paint: false,
2056            text_style: TextStyle::default(),
2057            min_height: Px(80.0),
2058        }
2059    }
2060}
2061
2062impl std::fmt::Debug for TextAreaProps {
2063    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2064        f.debug_struct("TextAreaProps")
2065            .field("layout", &self.layout)
2066            .field("enabled", &self.enabled)
2067            .field("focusable", &self.focusable)
2068            .field("model", &"<model>")
2069            .field(
2070                "placeholder",
2071                &self.placeholder.as_ref().map(|s| s.as_ref()),
2072            )
2073            .field("a11y_label", &self.a11y_label.as_ref().map(|s| s.as_ref()))
2074            .field("test_id", &self.test_id.as_ref().map(|s| s.as_ref()))
2075            .field("chrome", &self.chrome)
2076            .field("focus_ring_always_paint", &self.focus_ring_always_paint)
2077            .field("text_style", &self.text_style)
2078            .field("min_height", &self.min_height)
2079            .finish()
2080    }
2081}
2082
2083#[derive(Clone)]
2084pub struct ResizablePanelGroupProps {
2085    pub layout: LayoutStyle,
2086    pub axis: fret_core::Axis,
2087    pub model: Model<Vec<f32>>,
2088    pub min_px: Vec<Px>,
2089    pub enabled: bool,
2090    pub chrome: ResizablePanelGroupStyle,
2091}
2092
2093impl ResizablePanelGroupProps {
2094    pub fn new(axis: fret_core::Axis, model: Model<Vec<f32>>) -> Self {
2095        let mut layout = LayoutStyle::default();
2096        layout.size.width = Length::Fill;
2097        layout.size.height = Length::Fill;
2098
2099        Self {
2100            layout,
2101            axis,
2102            model,
2103            min_px: Vec::new(),
2104            enabled: true,
2105            chrome: ResizablePanelGroupStyle::default(),
2106        }
2107    }
2108}
2109
2110impl std::fmt::Debug for ResizablePanelGroupProps {
2111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2112        f.debug_struct("ResizablePanelGroupProps")
2113            .field("layout", &self.layout)
2114            .field("axis", &self.axis)
2115            .field("model", &"<model>")
2116            .field("min_px_len", &self.min_px.len())
2117            .field("enabled", &self.enabled)
2118            .field("chrome", &self.chrome)
2119            .finish()
2120    }
2121}
2122
2123#[derive(Debug, Clone, Copy)]
2124pub struct ImageProps {
2125    pub layout: LayoutStyle,
2126    pub image: ImageId,
2127    pub fit: ViewportFit,
2128    pub sampling: fret_core::scene::ImageSamplingHint,
2129    pub opacity: f32,
2130    pub uv: Option<UvRect>,
2131}
2132
2133impl ImageProps {
2134    pub fn new(image: ImageId) -> Self {
2135        Self {
2136            layout: LayoutStyle::default(),
2137            image,
2138            fit: ViewportFit::Stretch,
2139            sampling: fret_core::scene::ImageSamplingHint::Default,
2140            opacity: 1.0,
2141            uv: None,
2142        }
2143    }
2144
2145    pub fn sampling(mut self, sampling: fret_core::scene::ImageSamplingHint) -> Self {
2146        self.sampling = sampling;
2147        self
2148    }
2149}
2150
2151#[derive(Debug, Clone, Copy)]
2152pub struct ViewportSurfaceProps {
2153    pub layout: LayoutStyle,
2154    pub target: RenderTargetId,
2155    pub target_px_size: (u32, u32),
2156    pub fit: ViewportFit,
2157    pub opacity: f32,
2158}
2159
2160impl ViewportSurfaceProps {
2161    pub fn new(target: RenderTargetId) -> Self {
2162        Self {
2163            layout: LayoutStyle::default(),
2164            target,
2165            target_px_size: (1, 1),
2166            fit: ViewportFit::Stretch,
2167            opacity: 1.0,
2168        }
2169    }
2170}
2171
2172/// A declarative leaf canvas element.
2173///
2174/// Paint handlers are registered via element-local state (not props) so the element tree can
2175/// remain `Clone + Debug` (see ADR 0141).
2176#[derive(Debug, Clone, Copy)]
2177pub struct CanvasProps {
2178    pub layout: LayoutStyle,
2179    pub cache_policy: CanvasCachePolicy,
2180}
2181
2182/// Cache tuning for a single hosted resource kind (text/path/svg) within a declarative `Canvas`.
2183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2184pub struct CanvasCacheTuning {
2185    /// How long an unused entry may remain cached (in UI frames).
2186    pub keep_frames: u64,
2187    /// Hard cap on cached entries for this resource kind.
2188    pub max_entries: usize,
2189}
2190
2191impl CanvasCacheTuning {
2192    pub const fn transient() -> Self {
2193        Self {
2194            keep_frames: 0,
2195            max_entries: 0,
2196        }
2197    }
2198}
2199
2200/// Hosted cache policy for declarative `Canvas` resources.
2201///
2202/// This is intentionally numeric-only configuration: it does not encode interaction policy or
2203/// domain semantics (ADR 0141 / ADR 0128).
2204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2205pub struct CanvasCachePolicy {
2206    pub text: CanvasCacheTuning,
2207    pub shared_text: CanvasCacheTuning,
2208    pub path: CanvasCacheTuning,
2209    pub svg: CanvasCacheTuning,
2210}
2211
2212impl CanvasCachePolicy {
2213    pub const fn smooth_default() -> Self {
2214        Self {
2215            // ~1s at 60fps; reduces prepare/release thrash during scroll/pan.
2216            text: CanvasCacheTuning {
2217                keep_frames: 60,
2218                max_entries: 4096,
2219            },
2220            // Shared cache (keyed by content/style/constraints) is useful for repeated labels,
2221            // but should remain bounded and configurable for large, scroll-driven surfaces.
2222            //
2223            // Default preserves the previous hard-coded behavior in `CanvasCache`.
2224            shared_text: CanvasCacheTuning {
2225                keep_frames: 120,
2226                max_entries: 4096,
2227            },
2228            path: CanvasCacheTuning {
2229                keep_frames: 60,
2230                max_entries: 2048,
2231            },
2232            svg: CanvasCacheTuning {
2233                keep_frames: 60,
2234                max_entries: 256,
2235            },
2236        }
2237    }
2238}
2239
2240impl Default for CanvasCachePolicy {
2241    fn default() -> Self {
2242        Self::smooth_default()
2243    }
2244}
2245
2246impl Default for CanvasProps {
2247    fn default() -> Self {
2248        let mut layout = LayoutStyle::default();
2249        layout.size.width = Length::Fill;
2250        layout.size.height = Length::Fill;
2251        Self {
2252            layout,
2253            cache_policy: CanvasCachePolicy::default(),
2254        }
2255    }
2256}
2257
2258#[derive(Debug, Clone)]
2259pub struct SvgIconProps {
2260    pub layout: LayoutStyle,
2261    pub svg: SvgSource,
2262    pub fit: SvgFit,
2263    pub color: Color,
2264    /// When true, the icon will use the nearest inherited foreground (if present) during paint.
2265    ///
2266    /// When no foreground is inherited, `color` is used as the fallback.
2267    pub inherit_color: bool,
2268    pub opacity: f32,
2269}
2270
2271impl SvgIconProps {
2272    pub fn new(svg: SvgSource) -> Self {
2273        Self {
2274            layout: LayoutStyle::default(),
2275            svg,
2276            fit: SvgFit::Contain,
2277            color: Color {
2278                r: 1.0,
2279                g: 1.0,
2280                b: 1.0,
2281                a: 1.0,
2282            },
2283            inherit_color: false,
2284            opacity: 1.0,
2285        }
2286    }
2287}
2288
2289/// A simple loading spinner primitive.
2290///
2291/// This is intentionally low-opinionated and renderer-friendly: it paints a ring of small rounded
2292/// quads with frame-driven alpha modulation (`Effect::RequestAnimationFrame`).
2293#[derive(Debug, Clone, Copy)]
2294pub struct SpinnerProps {
2295    pub layout: LayoutStyle,
2296    pub color: Option<Color>,
2297    pub dot_count: u8,
2298    /// Phase increment per frame, in dot steps. (`0.0` disables animation.)
2299    pub speed: f32,
2300}
2301
2302impl Default for SpinnerProps {
2303    fn default() -> Self {
2304        let mut layout = LayoutStyle::default();
2305        layout.size.width = Length::Px(Px(16.0));
2306        layout.size.height = Length::Px(Px(16.0));
2307
2308        Self {
2309            layout,
2310            color: None,
2311            dot_count: 12,
2312            speed: 0.2,
2313        }
2314    }
2315}
2316
2317/// A hover tracking region primitive.
2318///
2319/// This is a small substrate building block: it provides a `hovered: bool` signal to component
2320/// code (via `ElementCx::hover_region(...)`) without imposing click/focus semantics.
2321#[derive(Debug, Clone, Copy, Default)]
2322pub struct HoverRegionProps {
2323    pub layout: LayoutStyle,
2324}
2325
2326/// A wheel listener region that mutates a scroll handle without affecting layout.
2327#[derive(Debug, Clone)]
2328pub struct WheelRegionProps {
2329    pub layout: LayoutStyle,
2330    pub axis: ScrollAxis,
2331    /// Declarative element id to invalidate when the scroll offset changes.
2332    pub scroll_target: Option<GlobalElementId>,
2333    pub scroll_handle: crate::scroll::ScrollHandle,
2334}
2335
2336impl Default for WheelRegionProps {
2337    fn default() -> Self {
2338        Self {
2339            layout: LayoutStyle::default(),
2340            axis: ScrollAxis::Y,
2341            scroll_target: None,
2342            scroll_handle: crate::scroll::ScrollHandle::default(),
2343        }
2344    }
2345}
2346
2347impl TextProps {
2348    pub fn new(text: impl Into<std::sync::Arc<str>>) -> Self {
2349        Self {
2350            layout: LayoutStyle::default(),
2351            text: text.into(),
2352            style: None,
2353            color: None,
2354            wrap: TextWrap::Word,
2355            overflow: TextOverflow::Clip,
2356            align: TextAlign::Start,
2357            ink_overflow: TextInkOverflow::None,
2358        }
2359    }
2360
2361    pub(crate) fn resolved_text_style_with_inherited(
2362        &self,
2363        theme: crate::ThemeSnapshot,
2364        inherited: Option<&fret_core::TextStyleRefinement>,
2365    ) -> TextStyle {
2366        crate::text_props::resolve_text_style(theme, self.style.clone(), inherited)
2367    }
2368
2369    pub(crate) fn build_text_input_with_style(&self, style: TextStyle) -> fret_core::TextInput {
2370        crate::text_props::build_text_input_plain(self.text.clone(), style)
2371    }
2372}
2373
2374impl StyledTextProps {
2375    pub fn new(rich: AttributedText) -> Self {
2376        Self {
2377            layout: LayoutStyle::default(),
2378            rich,
2379            style: None,
2380            color: None,
2381            wrap: TextWrap::Word,
2382            overflow: TextOverflow::Clip,
2383            align: TextAlign::Start,
2384            ink_overflow: TextInkOverflow::None,
2385        }
2386    }
2387
2388    pub(crate) fn resolved_text_style_with_inherited(
2389        &self,
2390        theme: crate::ThemeSnapshot,
2391        inherited: Option<&fret_core::TextStyleRefinement>,
2392    ) -> TextStyle {
2393        crate::text_props::resolve_text_style(theme, self.style.clone(), inherited)
2394    }
2395
2396    pub(crate) fn build_text_input_with_style(&self, style: TextStyle) -> fret_core::TextInput {
2397        crate::text_props::build_text_input_attributed(&self.rich, style)
2398    }
2399}
2400
2401impl SelectableTextProps {
2402    pub fn new(rich: AttributedText) -> Self {
2403        Self {
2404            layout: LayoutStyle::default(),
2405            rich,
2406            style: None,
2407            color: None,
2408            wrap: TextWrap::Word,
2409            overflow: TextOverflow::Clip,
2410            align: TextAlign::Start,
2411            ink_overflow: TextInkOverflow::None,
2412            interactive_spans: std::sync::Arc::from([]),
2413        }
2414    }
2415
2416    pub(crate) fn resolved_text_style_with_inherited(
2417        &self,
2418        theme: crate::ThemeSnapshot,
2419        inherited: Option<&fret_core::TextStyleRefinement>,
2420    ) -> TextStyle {
2421        crate::text_props::resolve_text_style(theme, self.style.clone(), inherited)
2422    }
2423
2424    pub(crate) fn build_text_input_with_style(&self, style: TextStyle) -> fret_core::TextInput {
2425        crate::text_props::build_text_input_attributed(&self.rich, style)
2426    }
2427}
2428
2429#[derive(Debug, Clone, Copy)]
2430pub struct FlexProps {
2431    pub layout: LayoutStyle,
2432    pub direction: fret_core::Axis,
2433    pub gap: SpacingLength,
2434    pub padding: SpacingEdges,
2435    pub justify: MainAlign,
2436    pub align: CrossAlign,
2437    pub wrap: bool,
2438}
2439
2440impl Default for FlexProps {
2441    fn default() -> Self {
2442        Self {
2443            layout: LayoutStyle::default(),
2444            direction: fret_core::Axis::Horizontal,
2445            gap: SpacingLength::Px(Px(0.0)),
2446            padding: SpacingEdges::all(SpacingLength::Px(Px(0.0))),
2447            justify: MainAlign::Start,
2448            align: CrossAlign::Stretch,
2449            wrap: false,
2450        }
2451    }
2452}
2453
2454#[derive(Debug, Clone, Copy, PartialEq)]
2455pub enum GridTrackSizing {
2456    Auto,
2457    MinContent,
2458    MaxContent,
2459    Px(Px),
2460    Fr(f32),
2461    /// `minmax(0, Nfr)` style track sizing for shrinkable content columns.
2462    Flex(f32),
2463}
2464
2465#[derive(Debug, Clone, PartialEq)]
2466pub struct GridProps {
2467    pub layout: LayoutStyle,
2468    pub cols: u16,
2469    pub rows: Option<u16>,
2470    /// Explicit per-track column sizing.
2471    ///
2472    /// When present and non-empty, this takes precedence over `cols`.
2473    pub template_columns: Option<Vec<GridTrackSizing>>,
2474    /// Explicit per-track row sizing.
2475    ///
2476    /// When present and non-empty, this takes precedence over `rows`.
2477    pub template_rows: Option<Vec<GridTrackSizing>>,
2478    pub gap: SpacingLength,
2479    /// Grid inline-axis gap.
2480    ///
2481    /// When `None`, the runtime falls back to the shared `gap` shorthand.
2482    pub column_gap: Option<SpacingLength>,
2483    /// Grid block-axis gap.
2484    ///
2485    /// When `None`, the runtime falls back to the shared `gap` shorthand.
2486    pub row_gap: Option<SpacingLength>,
2487    pub padding: SpacingEdges,
2488    pub justify: MainAlign,
2489    pub align: CrossAlign,
2490    /// Grid-only inline-axis item alignment (`justify-items`).
2491    ///
2492    /// When `None`, the runtime preserves the underlying grid default (`stretch`).
2493    pub justify_items: Option<CrossAlign>,
2494}
2495
2496impl Default for GridProps {
2497    fn default() -> Self {
2498        Self {
2499            layout: LayoutStyle::default(),
2500            cols: 1,
2501            rows: None,
2502            template_columns: None,
2503            template_rows: None,
2504            gap: SpacingLength::Px(Px(0.0)),
2505            column_gap: None,
2506            row_gap: None,
2507            padding: SpacingEdges::all(SpacingLength::Px(Px(0.0))),
2508            justify: MainAlign::Start,
2509            align: CrossAlign::Stretch,
2510            justify_items: None,
2511        }
2512    }
2513}
2514
2515impl GridProps {
2516    pub fn resolved_column_gap(&self) -> SpacingLength {
2517        self.column_gap.unwrap_or(self.gap)
2518    }
2519
2520    pub fn resolved_row_gap(&self) -> SpacingLength {
2521        self.row_gap.unwrap_or(self.gap)
2522    }
2523}
2524
2525#[derive(Debug, Clone)]
2526pub struct VirtualListProps {
2527    pub layout: LayoutStyle,
2528    pub axis: fret_core::Axis,
2529    pub len: usize,
2530    pub items_revision: u64,
2531    pub estimate_row_height: Px,
2532    pub measure_mode: VirtualListMeasureMode,
2533    pub key_cache: VirtualListKeyCacheMode,
2534    pub overscan: usize,
2535    /// Number of off-window items that a retained virtual-list host may keep alive for reuse.
2536    ///
2537    /// This is primarily consumed by retained/windowed host implementations (ADR 0177) so window
2538    /// shifts can reuse previously-mounted item subtrees without forcing the parent cache root to
2539    /// rerender.
2540    pub keep_alive: usize,
2541    pub scroll_margin: Px,
2542    pub gap: Px,
2543    pub scroll_handle: crate::scroll::VirtualListScrollHandle,
2544    pub visible_items: Vec<crate::virtual_list::VirtualItem>,
2545}
2546
2547#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2548pub enum VirtualListMeasureMode {
2549    /// Performs a measurement pass for all visible items and updates the virtualizer with the
2550    /// measured sizes. Correct for variable-height items.
2551    Measured,
2552    /// Skips the measurement pass and assumes all items have the estimated size.
2553    /// Intended for fixed-height lists/tables.
2554    Fixed,
2555    /// Skips the measurement pass and uses caller-provided per-index row heights.
2556    ///
2557    /// This mode is intended for “known-height” virtualization (e.g. fixed-height rows with
2558    /// occasional deterministic height changes like group headers), where measuring each visible
2559    /// row would be wasted work.
2560    ///
2561    /// Correctness requires that the provided height function matches the rendered row layout.
2562    Known,
2563}
2564
2565#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
2566pub enum VirtualListKeyCacheMode {
2567    /// Cache the full `index -> key` mapping so we can:
2568    /// - restore scroll anchor across reorder
2569    /// - provide stable keys to measured virtualization
2570    #[default]
2571    AllKeys,
2572    /// Do not cache `index -> key`. Keys are computed on-demand for visible items only.
2573    ///
2574    /// This is intended for very large fixed-height lists (e.g. tables) where caching the full
2575    /// key map can dominate startup time and memory.
2576    VisibleOnly,
2577}
2578
2579#[derive(Clone)]
2580pub struct VirtualListOptions {
2581    pub axis: fret_core::Axis,
2582    pub items_revision: u64,
2583    pub estimate_row_height: Px,
2584    pub measure_mode: VirtualListMeasureMode,
2585    pub key_cache: VirtualListKeyCacheMode,
2586    pub overscan: usize,
2587    pub keep_alive: usize,
2588    pub scroll_margin: Px,
2589    pub gap: Px,
2590    pub known_row_height_at: Option<Arc<dyn Fn(usize) -> Px + Send + Sync>>,
2591}
2592
2593impl VirtualListOptions {
2594    pub fn new(estimate_row_height: Px, overscan: usize) -> Self {
2595        Self {
2596            axis: fret_core::Axis::Vertical,
2597            items_revision: 0,
2598            estimate_row_height,
2599            measure_mode: VirtualListMeasureMode::Measured,
2600            key_cache: VirtualListKeyCacheMode::AllKeys,
2601            overscan,
2602            keep_alive: 0,
2603            scroll_margin: Px(0.0),
2604            gap: Px(0.0),
2605            known_row_height_at: None,
2606        }
2607    }
2608
2609    pub fn keep_alive(mut self, keep_alive: usize) -> Self {
2610        self.keep_alive = keep_alive;
2611        self
2612    }
2613
2614    pub fn fixed(estimate_row_height: Px, overscan: usize) -> Self {
2615        Self {
2616            measure_mode: VirtualListMeasureMode::Fixed,
2617            ..Self::new(estimate_row_height, overscan)
2618        }
2619    }
2620
2621    pub fn known(
2622        estimate_row_height: Px,
2623        overscan: usize,
2624        height_at: impl Fn(usize) -> Px + Send + Sync + 'static,
2625    ) -> Self {
2626        let mut options = Self::new(estimate_row_height, overscan);
2627        options.measure_mode = VirtualListMeasureMode::Known;
2628        options.known_row_height_at = Some(Arc::new(height_at));
2629        options
2630    }
2631}
2632
2633impl std::fmt::Debug for VirtualListOptions {
2634    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2635        f.debug_struct("VirtualListOptions")
2636            .field("axis", &self.axis)
2637            .field("items_revision", &self.items_revision)
2638            .field("estimate_row_height", &self.estimate_row_height)
2639            .field("measure_mode", &self.measure_mode)
2640            .field("key_cache", &self.key_cache)
2641            .field("overscan", &self.overscan)
2642            .field("keep_alive", &self.keep_alive)
2643            .field("scroll_margin", &self.scroll_margin)
2644            .field("gap", &self.gap)
2645            .field("known_row_height_at", &self.known_row_height_at.is_some())
2646            .finish()
2647    }
2648}
2649
2650/// Cross-frame element-local state for a virtual list (stored in the element state store).
2651#[derive(Debug, Default, Clone)]
2652pub struct VirtualListState {
2653    pub offset_x: Px,
2654    pub offset_y: Px,
2655    pub viewport_w: Px,
2656    pub viewport_h: Px,
2657    pub(crate) window_range: Option<crate::virtual_list::VirtualRange>,
2658    pub(crate) render_window_range: Option<crate::virtual_list::VirtualRange>,
2659    pub(crate) last_scroll_direction_forward: Option<bool>,
2660    pub(crate) has_final_viewport: bool,
2661    pub(crate) deferred_scroll_offset_hint: Option<Px>,
2662    pub(crate) metrics: crate::virtual_list::VirtualListMetrics,
2663    pub(crate) items_revision: u64,
2664    pub(crate) items_len: usize,
2665    pub(crate) key_cache: VirtualListKeyCacheMode,
2666    pub(crate) keys: Vec<crate::ItemKey>,
2667    pub(crate) layout_scratch: VirtualListLayoutScratch,
2668}
2669
2670#[derive(Debug, Default, Clone)]
2671pub(crate) struct VirtualListLayoutScratch {
2672    pub(crate) measured_updates: Vec<(NodeId, usize, Px)>,
2673    pub(crate) barrier_roots: Vec<(NodeId, Rect)>,
2674}
2675
2676#[derive(Debug, Clone)]
2677pub struct ScrollProps {
2678    pub layout: LayoutStyle,
2679    pub axis: ScrollAxis,
2680    pub scroll_handle: Option<crate::scroll::ScrollHandle>,
2681    pub intrinsic_measure_mode: ScrollIntrinsicMeasureMode,
2682    /// When true, the scroll subtree's paint output depends on the scroll offset in a
2683    /// windowed/virtualized way (e.g. a single `Canvas` that only paints the visible range).
2684    ///
2685    /// In this mode, scroll-handle updates must be allowed to invalidate view-cache reuse so the
2686    /// subtree can re-render and re-run paint handlers for the new visible window.
2687    ///
2688    /// This is a mechanism-only switch; policy lives in ecosystem layers.
2689    pub windowed_paint: bool,
2690    /// When true (default), scroll containers probe their content with a very large available size
2691    /// along the scroll axis to measure the full scrollable extent.
2692    ///
2693    /// When false, probing uses the viewport constraints, which allows word-wrapping content while
2694    /// still permitting scrolling for long unbreakable tokens.
2695    pub probe_unbounded: bool,
2696}
2697
2698impl Default for ScrollProps {
2699    fn default() -> Self {
2700        let layout = LayoutStyle {
2701            overflow: Overflow::Clip,
2702            ..Default::default()
2703        };
2704        Self {
2705            layout,
2706            axis: ScrollAxis::Y,
2707            scroll_handle: None,
2708            intrinsic_measure_mode: ScrollIntrinsicMeasureMode::Content,
2709            windowed_paint: false,
2710            probe_unbounded: true,
2711        }
2712    }
2713}
2714
2715#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2716pub enum ScrollIntrinsicMeasureMode {
2717    /// Default behavior: scroll measurement probes children (potentially using MaxContent on the
2718    /// scroll axis when `probe_unbounded` is true).
2719    Content,
2720    /// Treat the scroll container as a viewport-sized barrier in intrinsic measurement contexts.
2721    ///
2722    /// This avoids recursively measuring large scrollable subtrees (virtualized surfaces, large
2723    /// tables, code views) during Min/MaxContent measurement passes.
2724    ///
2725    /// Note: this affects only `measure()` / intrinsic sizing; final layout under definite
2726    /// available space is unchanged.
2727    Viewport,
2728}
2729
2730#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2731pub enum ScrollAxis {
2732    X,
2733    Y,
2734    Both,
2735}
2736
2737impl ScrollAxis {
2738    pub fn scroll_x(self) -> bool {
2739        matches!(self, Self::X | Self::Both)
2740    }
2741
2742    pub fn scroll_y(self) -> bool {
2743        matches!(self, Self::Y | Self::Both)
2744    }
2745}
2746
2747/// Cross-frame element-local state for scroll containers.
2748#[derive(Debug, Default, Clone)]
2749pub struct ScrollState {
2750    pub scroll_handle: crate::scroll::ScrollHandle,
2751    pub(crate) intrinsic_measure_cache: Option<ScrollIntrinsicMeasureCache>,
2752    pub(crate) pending_extent_probe: bool,
2753}
2754
2755#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2756pub(crate) struct ScrollIntrinsicMeasureCacheKey {
2757    pub avail_w: u64,
2758    pub avail_h: u64,
2759    pub axis: u8,
2760    pub probe_unbounded: bool,
2761    pub scale_bits: u32,
2762}
2763
2764#[derive(Debug, Clone, Copy)]
2765pub(crate) struct ScrollIntrinsicMeasureCache {
2766    pub key: ScrollIntrinsicMeasureCacheKey,
2767    pub max_child: Size,
2768}
2769
2770#[derive(Debug, Clone, Copy)]
2771pub struct ScrollbarStyle {
2772    pub thumb: Color,
2773    pub thumb_hover: Color,
2774    pub thumb_idle_alpha: f32,
2775    /// Padding (main axis) reserved at both ends of the scrollbar track.
2776    ///
2777    /// This is part of Radix ScrollArea's thumb sizing/offset math. Component libraries should set
2778    /// this to match the visual padding they apply to the scrollbar container (e.g. shadcn/ui v4
2779    /// uses `p-px`, so `Px(1.0)`).
2780    pub track_padding: Px,
2781}
2782
2783impl Default for ScrollbarStyle {
2784    fn default() -> Self {
2785        Self {
2786            thumb: Color {
2787                r: 0.35,
2788                g: 0.38,
2789                b: 0.45,
2790                a: 1.0,
2791            },
2792            thumb_hover: Color {
2793                r: 0.45,
2794                g: 0.50,
2795                b: 0.60,
2796                a: 1.0,
2797            },
2798            thumb_idle_alpha: 0.65,
2799            track_padding: Px(1.0),
2800        }
2801    }
2802}
2803
2804#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2805pub enum ScrollbarAxis {
2806    #[default]
2807    Vertical,
2808    Horizontal,
2809}
2810
2811/// A mechanism-only scrollbar primitive.
2812///
2813/// Component libraries decide when to show/hide scrollbars and resolve theme tokens into this
2814/// style. The runtime owns hit-testing, thumb/track interactions, and paints using the resolved
2815/// style.
2816#[derive(Debug, Clone, Default)]
2817pub struct ScrollbarProps {
2818    pub layout: LayoutStyle,
2819    pub axis: ScrollbarAxis,
2820    /// Declarative element id for the associated scroll container, if any.
2821    ///
2822    /// When provided, the scrollbar will invalidate the target node's layout/paint when the
2823    /// scroll handle offset changes (e.g. thumb drag or track paging).
2824    pub scroll_target: Option<GlobalElementId>,
2825    pub scroll_handle: crate::scroll::ScrollHandle,
2826    pub style: ScrollbarStyle,
2827}
2828
2829/// Cross-frame element-local state for scrollbars.
2830#[derive(Debug, Default, Clone)]
2831pub struct ScrollbarState {
2832    pub dragging_thumb: bool,
2833    pub drag_start_pointer: Px,
2834    pub drag_start_offset: Px,
2835    pub drag_baseline_viewport: Option<Px>,
2836    pub drag_baseline_content: Option<Px>,
2837    pub hovered: bool,
2838}
2839
2840/// Authoring conversion boundary (ADR 0039).
2841///
2842/// Most application code does not implement this directly; component crates typically expose
2843/// ergonomic constructors that return `AnyElement` (or helpers that build `Elements`).
2844pub trait IntoElement {
2845    fn into_element(self, id: GlobalElementId) -> AnyElement;
2846}
2847
2848/// A small owned collection wrapper for element lists.
2849///
2850/// This is intended for authoring-facing APIs that want an "iterator-friendly" return type without
2851/// forcing callers into `Vec<AnyElement>` as the only option.
2852///
2853/// This type is commonly used as a view return value (e.g. `ViewElements` in `fret-bootstrap`).
2854#[derive(Debug, Default)]
2855pub struct Elements(pub Vec<AnyElement>);
2856
2857impl Elements {
2858    pub fn new(children: impl IntoIterator<Item = AnyElement>) -> Self {
2859        Self(children.into_iter().collect())
2860    }
2861
2862    pub fn into_vec(self) -> Vec<AnyElement> {
2863        self.0
2864    }
2865}
2866
2867impl From<Vec<AnyElement>> for Elements {
2868    fn from(value: Vec<AnyElement>) -> Self {
2869        Self(value)
2870    }
2871}
2872
2873impl From<AnyElement> for Elements {
2874    fn from(value: AnyElement) -> Self {
2875        Self::new([value])
2876    }
2877}
2878
2879impl<const N: usize> From<[AnyElement; N]> for Elements {
2880    fn from(value: [AnyElement; N]) -> Self {
2881        Self::new(value)
2882    }
2883}
2884
2885impl std::iter::FromIterator<AnyElement> for Elements {
2886    fn from_iter<T: IntoIterator<Item = AnyElement>>(iter: T) -> Self {
2887        Self::new(iter)
2888    }
2889}
2890
2891impl std::ops::Deref for Elements {
2892    type Target = Vec<AnyElement>;
2893
2894    fn deref(&self) -> &Self::Target {
2895        &self.0
2896    }
2897}
2898
2899impl std::ops::DerefMut for Elements {
2900    fn deref_mut(&mut self) -> &mut Self::Target {
2901        &mut self.0
2902    }
2903}
2904
2905impl IntoIterator for Elements {
2906    type Item = AnyElement;
2907    type IntoIter = std::vec::IntoIter<AnyElement>;
2908
2909    fn into_iter(self) -> Self::IntoIter {
2910        self.0.into_iter()
2911    }
2912}
2913
2914impl<'a> IntoIterator for &'a Elements {
2915    type Item = &'a AnyElement;
2916    type IntoIter = std::slice::Iter<'a, AnyElement>;
2917
2918    fn into_iter(self) -> Self::IntoIter {
2919        self.0.iter()
2920    }
2921}
2922
2923impl<'a> IntoIterator for &'a mut Elements {
2924    type Item = &'a mut AnyElement;
2925    type IntoIter = std::slice::IterMut<'a, AnyElement>;
2926
2927    fn into_iter(self) -> Self::IntoIter {
2928        self.0.iter_mut()
2929    }
2930}
2931
2932/// Authoring helper for collecting iterator-produced child elements.
2933///
2934/// This exists to reduce boilerplate after switching common `children: Vec<AnyElement>` APIs to
2935/// accept `IntoIterator<Item = AnyElement>` (e.g. `ElementContext::{row,column}`), where the target
2936/// collection type is no longer implied by the callee.
2937///
2938/// Example:
2939/// `let children = (0..10).map(|i| cx.text(format!("row-{i}"))).elements();`
2940pub trait AnyElementIterExt: Iterator<Item = AnyElement> + Sized {
2941    fn elements(self) -> Vec<AnyElement> {
2942        self.collect()
2943    }
2944
2945    fn elements_owned(self) -> Elements {
2946        self.collect::<Elements>()
2947    }
2948}
2949
2950impl<T> AnyElementIterExt for T where T: Iterator<Item = AnyElement> + Sized {}
2951
2952impl IntoElement for AnyElement {
2953    fn into_element(self, _id: GlobalElementId) -> AnyElement {
2954        self
2955    }
2956}
2957
2958impl IntoElement for TextProps {
2959    fn into_element(self, id: GlobalElementId) -> AnyElement {
2960        AnyElement::new(id, ElementKind::Text(self), Vec::new())
2961    }
2962}
2963
2964impl IntoElement for StyledTextProps {
2965    fn into_element(self, id: GlobalElementId) -> AnyElement {
2966        AnyElement::new(id, ElementKind::StyledText(self), Vec::new())
2967    }
2968}
2969
2970impl IntoElement for SelectableTextProps {
2971    fn into_element(self, id: GlobalElementId) -> AnyElement {
2972        AnyElement::new(id, ElementKind::SelectableText(self), Vec::new())
2973    }
2974}
2975
2976impl IntoElement for ImageProps {
2977    fn into_element(self, id: GlobalElementId) -> AnyElement {
2978        AnyElement::new(id, ElementKind::Image(self), Vec::new())
2979    }
2980}
2981
2982impl IntoElement for ViewportSurfaceProps {
2983    fn into_element(self, id: GlobalElementId) -> AnyElement {
2984        AnyElement::new(id, ElementKind::ViewportSurface(self), Vec::new())
2985    }
2986}
2987
2988impl IntoElement for SvgIconProps {
2989    fn into_element(self, id: GlobalElementId) -> AnyElement {
2990        AnyElement::new(id, ElementKind::SvgIcon(self), Vec::new())
2991    }
2992}
2993
2994impl IntoElement for ScrollProps {
2995    fn into_element(self, id: GlobalElementId) -> AnyElement {
2996        AnyElement::new(id, ElementKind::Scroll(self), Vec::new())
2997    }
2998}
2999
3000impl IntoElement for std::sync::Arc<str> {
3001    fn into_element(self, id: GlobalElementId) -> AnyElement {
3002        TextProps::new(self).into_element(id)
3003    }
3004}
3005
3006impl IntoElement for &'static str {
3007    fn into_element(self, id: GlobalElementId) -> AnyElement {
3008        TextProps::new(self).into_element(id)
3009    }
3010}
3011
3012/// Stateful view authoring layer (ADR 0039).
3013pub trait Render {
3014    fn render<H: UiHost>(&mut self, cx: &mut ElementContext<'_, H>) -> AnyElement;
3015}
3016
3017/// Stateless component authoring layer (ADR 0039).
3018pub trait RenderOnce {
3019    fn render_once<H: UiHost>(self, cx: &mut ElementContext<'_, H>) -> AnyElement;
3020}
3021
3022#[cfg(test)]
3023mod default_semantics_tests {
3024    use super::*;
3025
3026    #[test]
3027    fn flex_props_default_width_is_auto() {
3028        assert_eq!(FlexProps::default().layout.size.width, Length::Auto);
3029    }
3030
3031    #[test]
3032    fn length_default_is_auto() {
3033        assert_eq!(Length::default(), Length::Auto);
3034    }
3035
3036    #[test]
3037    fn scroll_props_default_probe_unbounded_is_true() {
3038        assert!(ScrollProps::default().probe_unbounded);
3039    }
3040}