Skip to main content

repose_core/
modifier.rs

1use std::cell::{Cell, RefCell};
2use std::collections::HashSet;
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use taffy::{AlignContent, AlignItems, AlignSelf, FlexDirection, FlexWrap, JustifyContent};
7
8use crate::animation::AnimationSpec;
9use crate::indication::IndicationNodeFactory;
10use crate::{Brush, Color, PointerEvent, Size, Transform, Vec2};
11
12/// State-driven colors. Priority: disabled > dragged > pressed > focused > hovered > default.
13///
14/// Priority (paint): disabled > dragged > pressed > focused > hovered > default.
15#[derive(Clone, Copy, Debug)]
16pub struct StateColors {
17    pub default: Color,
18    pub hovered: Color,
19    /// Color while focused.
20    pub focused: Color,
21    pub pressed: Color,
22    pub disabled: Color,
23    /// While dragged (overrides hover/press/focus).
24    pub dragged: Color,
25}
26
27/// State-driven elevation. Priority: disabled > dragged > pressed > focused > hovered > default.
28#[derive(Clone, Copy, Debug)]
29pub struct StateElevation {
30    pub default: f32,
31    pub hovered: f32,
32    /// Applied between pressed and hovered in the paint priority order.
33    pub focused: f32,
34    pub pressed: f32,
35    pub disabled: f32,
36    /// Elevation while the component is being dragged (preferred over hovered/pressed/focused).
37    pub dragged: f32,
38}
39
40impl StateColors {
41    pub const fn transparent() -> Self {
42        Self {
43            default: Color::TRANSPARENT,
44            hovered: Color::TRANSPARENT,
45            focused: Color::TRANSPARENT,
46            pressed: Color::TRANSPARENT,
47            disabled: Color::TRANSPARENT,
48            dragged: Color::TRANSPARENT,
49        }
50    }
51}
52
53impl StateElevation {
54    pub const fn zero() -> Self {
55        Self {
56            default: 0.0,
57            hovered: 0.0,
58            focused: 0.0,
59            pressed: 0.0,
60            disabled: 0.0,
61            dragged: 0.0,
62        }
63    }
64}
65
66macro_rules! merge_opts {
67    ($dst:ident, $src:ident; $($f:ident),+ $(,)?) => {
68        $( $dst.$f = $src.$f.or($dst.$f); )+
69    };
70}
71macro_rules! merge_flags {
72    ($dst:ident, $src:ident; $($f:ident),+ $(,)?) => {
73        $( $dst.$f |= $src.$f; )+
74    };
75}
76
77macro_rules! impl_option_fields {
78    ($ty:ty, $fn:ident) => {
79        impl $ty {
80            $fn!(replace);
81        }
82    };
83    ($ty:ident) => {
84        impl $ty {
85            /// Chain another modifier's settings onto this one.
86            /// Useful for creating reusable modifier templates.
87            pub fn then(mut self, other: Self) -> Self {
88                merge_opts!(self, other;
89                    key, size, width, height, required_size,
90                    padding, padding_values,
91                    min_width, min_height, max_width, max_height,
92                    required_min_width, required_max_width,
93                    required_min_height, required_max_height,
94                    default_min_width, default_min_height,
95                    fill_max, fill_max_w, fill_max_h,
96                    background, state_colors, state_elevation, border,
97                    flex_grow, flex_shrink, flex_basis, flex_wrap, flex_dir,
98                    gap, row_gap, column_gap,
99                    align_self, justify_content, align_items_container, align_content,
100                    clip_rounded, clip_rect, overflow, render_z_index,
101                    on_scroll,
102                    nested_scroll_connection,
103                    scroll,
104                    on_pointer_down, on_pointer_move, on_pointer_up,
105                    on_pointer_enter, on_pointer_leave,
106                    on_click, on_double_click, on_long_click,
107                    semantics, alpha, transform,
108                    grid, grid_col_span, grid_row_span,
109                    position_type,
110                    offset_left, offset_right, offset_top, offset_bottom,
111                    margin_left, margin_right, margin_top, margin_bottom,
112                    aspect_ratio, intrinsic_width, intrinsic_height,
113                    painter,
114                    paint_callback,
115                    on_drag_start, on_drag_end, on_drag_enter, on_drag_over, on_drag_leave, on_drop,
116                    drag_preview,
117                    on_action, cursor, animate_content_size, focus_requester, on_focus_changed,
118                    interaction_source, text_input,
119                );
120                        merge_flags!(self, other;
121                    hit_passthrough, input_blocker, repaint_boundary, click, disabled,
122                    propagate_min, focus_group,
123                );
124
125                if let Some(f) = other.focusable {
126                    self.focusable = Some(f);
127                }
128                if other.indication.is_some() {
129                    self.indication = other.indication;
130                }
131                if other.z_index != 0.0 {
132                    self.z_index = other.z_index;
133                }
134                self
135            }
136        }
137    };
138}
139
140#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
141pub enum ClipOp {
142    /// Keep content inside the clip rect (default).
143    #[default]
144    Intersect,
145    /// Remove content inside the clip rect (cutout).
146    Difference,
147}
148
149/// Controls whether child content is clipped to the parent bounds.
150///
151/// Analogous to CSS `overflow`:
152/// - `Clip` (default): content extending beyond the parent is hidden.
153/// - `Visible`: content is allowed to overflow the parent bounds.
154#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
155pub enum Overflow {
156    #[default]
157    Clip,
158    Visible,
159}
160
161/// Rectangular clip with a clipping operation.
162/// The rect is relative to the element bounds, in dp.
163#[derive(Clone, Copy, Debug)]
164pub struct ClipRect {
165    pub left: f32,
166    pub top: f32,
167    pub right: f32,
168    pub bottom: f32,
169    pub op: ClipOp,
170}
171
172#[derive(Clone, Debug)]
173pub struct Border {
174    pub width: f32,
175    pub color: Color,
176    pub radius: [f32; 4],
177}
178
179#[derive(Clone, Copy, Debug, Default)]
180pub struct PaddingValues {
181    pub left: f32,
182    pub right: f32,
183    pub top: f32,
184    pub bottom: f32,
185}
186
187#[derive(Clone, Debug)]
188pub struct GridConfig {
189    pub columns: usize,
190    pub row_gap: f32,
191    pub column_gap: f32,
192}
193
194/// Edge treatment for `Modifier::blur` -> controls how pixels at the edges
195/// of the blurred region are handled.
196#[derive(Clone, Copy, Debug, PartialEq)]
197pub enum BlurredEdgeTreatment {
198    /// Clip the blur to the element's bounds and clamp edge pixels
199    /// (extend the outermost pixels). This is the Compose default.
200    Rectangle,
201    /// Allow the blur to extend beyond the element's bounds.
202    /// Edge pixels are treated as transparent (decal).
203    Unbounded,
204}
205
206/// Gaussian blur parameters for `Modifier::blur`.
207#[derive(Clone, Copy, Debug)]
208pub struct BlurStyle {
209    /// Horizontal blur radius in dp.
210    pub radius_x: f32,
211    /// Vertical blur radius in dp.
212    pub radius_y: f32,
213    /// Controls edge pixel behavior.
214    pub edge_treatment: BlurredEdgeTreatment,
215}
216
217/// Constraints passed to the `Modifier::layout` callback.
218/// Mirrors Compose's `Constraints` -> the element's size must fall within
219/// `[min_width, max_width]` × `[min_height, max_height]`.
220/// A dimension with `INFINITY` max means unbounded in that direction.
221#[derive(Clone, Copy, Debug)]
222pub struct LayoutConstraints {
223    pub min_width: f32,
224    pub max_width: f32,
225    pub min_height: f32,
226    pub max_height: f32,
227}
228
229/// Drop-shadow parameters applied to a graphics layer.
230///
231/// `blur_radius` is the Gaussian blur radius in dp (1.0 = subtle, 8.0 = soft,
232/// 16.0 = very diffuse). `offset_y` is the vertical offset of the shadow in dp
233/// (positive = below the layer). `color` is the shadow color (premultiplied
234/// alpha controls shadow darkness).
235#[derive(Clone, Copy, Debug)]
236pub struct ShadowSpec {
237    pub blur_radius: f32,
238    pub offset_y: f32,
239    pub color: Color,
240}
241
242#[derive(Clone, Copy, Debug)]
243#[non_exhaustive]
244pub enum PositionType {
245    Relative,
246    Absolute,
247}
248
249/// Configuration for a text input field.
250#[derive(Clone)]
251pub struct TextInputConfig {
252    pub hint: String,
253    pub multiline: bool,
254    pub on_change: Option<Rc<dyn Fn(String)>>,
255    pub on_submit: Option<Rc<dyn Fn(String)>>,
256    pub focus_tracker: Option<Rc<Cell<bool>>>,
257    pub value: String,
258    pub visual_transformation: Option<Rc<dyn crate::text::VisualTransformation>>,
259    pub keyboard_type: crate::text::KeyboardType,
260    pub capitalization: crate::text::KeyboardCapitalization,
261    pub ime_action: crate::text::ImeAction,
262    /// Platform keyboard auto-correct hint. `None` = follow platform default
263    /// (except password keyboards, which never auto-correct).
264    pub auto_correct_enabled: Option<bool>,
265    /// When false, the text field is not editable, not focusable, and input is not selectable.
266    pub enabled: bool,
267    /// When true, the text field can be focused and text can be selected/copied, but not modified.
268    pub read_only: bool,
269    /// Maximum visible lines. Only effective when `multiline` is true.
270    pub max_lines: Option<usize>,
271    /// Minimum visible lines. Only effective when `multiline` is true.
272    pub min_lines: usize,
273    /// Override the cursor color. When None, uses the theme's `on_surface`.
274    pub cursor_color: Option<Color>,
275    /// Callback invoked after each text layout computation, providing layout details
276    /// such as line count and content size.
277    pub on_text_layout: Option<Rc<dyn Fn(&crate::text::TextLayoutResult)>>,
278    /// Style for the text content (font size, color, weight, etc.).
279    /// None = use defaults (16dp, theme color, NORMAL weight).
280    pub text_style: Option<crate::text::TextStyle>,
281    /// Per-action IME callbacks (onDone, onGo, onNext, etc.).
282    /// None = use `on_submit` for all actions.
283    pub keyboard_actions: Option<crate::text::KeyboardActions>,
284    /// Interaction source for tracking focus/press/hover state.
285    pub interaction_source: Option<InteractionSource>,
286    /// Line limits (SingleLine or MultiLine). Overrides `multiline`/`max_lines`/`min_lines`.
287    pub line_limits: Option<crate::text::TextFieldLineLimits>,
288}
289
290impl Default for TextInputConfig {
291    fn default() -> Self {
292        Self {
293            hint: String::new(),
294            multiline: false,
295            on_change: None,
296            on_submit: None,
297            focus_tracker: None,
298            value: String::new(),
299            visual_transformation: None,
300            keyboard_type: crate::text::KeyboardType::default(),
301            capitalization: crate::text::KeyboardCapitalization::default(),
302            ime_action: crate::text::ImeAction::default(),
303            auto_correct_enabled: None,
304            enabled: true,
305            read_only: false,
306            max_lines: None,
307            min_lines: 1,
308            cursor_color: None,
309            on_text_layout: None,
310            text_style: None,
311            keyboard_actions: None,
312            interaction_source: None,
313            line_limits: None,
314        }
315    }
316}
317
318impl std::fmt::Debug for TextInputConfig {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        let mut s = f.debug_struct("TextInputConfig");
321        s.field("hint", &self.hint);
322        s.field("multiline", &self.multiline);
323        if self.on_change.is_some() {
324            s.field("on_change", &"…");
325        }
326        if self.on_submit.is_some() {
327            s.field("on_submit", &"…");
328        }
329        if self.focus_tracker.is_some() {
330            s.field("focus_tracker", &"…");
331        }
332        s.field("value", &self.value);
333        if self.visual_transformation.is_some() {
334            s.field("visual_transformation", &"…");
335        }
336        s.field("keyboard_type", &self.keyboard_type);
337        s.field("capitalization", &self.capitalization);
338        s.field("ime_action", &self.ime_action);
339        s.field("auto_correct_enabled", &self.auto_correct_enabled);
340        s.field("enabled", &self.enabled);
341        s.field("read_only", &self.read_only);
342        s.field("max_lines", &self.max_lines);
343        s.field("min_lines", &self.min_lines);
344        s.field("cursor_color", &self.cursor_color);
345        if self.on_text_layout.is_some() {
346            s.field("on_text_layout", &"…");
347        }
348        s.finish()
349    }
350}
351
352/// Intrinsic sizing mode for [`Modifier::intrinsic_width`] and [`Modifier::intrinsic_height`].
353/// When set, the node sizes itself to the intrinsic content size in that dimension.
354#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
355pub enum IntrinsicSize {
356    Min,
357    Max,
358}
359
360static PRESS_COUNTER: AtomicU64 = AtomicU64::new(1);
361
362/// A press identifier for linking Press -> Release/Cancel pairs.
363pub type PressId = u64;
364
365/// An interaction event that can be emitted by a [`MutableInteractionSource`].
366///
367/// Compose-like per-interaction-type hierarchy:
368/// - `PressInteraction.Press(position)` / `Release(press)` / `Cancel(press)`
369/// - `HoverInteraction.Enter` / `Exit`
370/// - `FocusInteraction.Focus` / `Unfocus`
371/// - `DragInteraction.Start` / `Stop` / `Cancel`
372#[derive(Clone, Copy, Debug, PartialEq)]
373pub enum Interaction {
374    /// A press started at the given position (in local coords).
375    /// Carries a unique `PressId` so Release/Cancel can identify which press to end.
376    Press(PressId, Vec2),
377    /// The press with the given `PressId` was released.
378    Release(PressId),
379    /// The press with the given `PressId` was cancelled
380    /// (e.g. gesture disambiguation, pointer leave during press).
381    Cancel(PressId),
382    HoverEnter,
383    HoverLeave,
384    Focus,
385    Unfocus,
386    DragStart,
387    DragStop,
388    DragCancel,
389}
390
391impl Interaction {
392    /// Create a new `Press` with a fresh unique ID and the given position.
393    #[inline]
394    pub fn new_press(position: Vec2) -> Self {
395        Interaction::Press(PRESS_COUNTER.fetch_add(1, Ordering::Relaxed), position)
396    }
397}
398
399/// Read-only handle to a shared interaction state.
400///
401/// Use [`MutableInteractionSource::source`] to obtain a read handle, or
402/// [`MutableInteractionSource::new`] + `.source()` to create a new source pair.
403///
404/// Multiple clones share the same underlying state.
405#[derive(Clone)]
406pub struct InteractionSource {
407    pub(crate) state: Rc<RefCell<InteractionState>>,
408}
409
410impl InteractionSource {
411    pub fn collect_is_pressed(&self) -> bool {
412        !self.state.borrow().active_presses.is_empty()
413    }
414    pub fn collect_is_hovered(&self) -> bool {
415        self.state.borrow().hovered
416    }
417    pub fn collect_is_focused(&self) -> bool {
418        self.state.borrow().focused
419    }
420
421    /// Focused and keyboard/D-pad input mode (Compose `:focus-visible`).
422    pub fn collect_is_focus_visible(&self) -> bool {
423        crate::input::is_focus_visible(self.collect_is_focused())
424    }
425
426    pub fn collect_is_dragged(&self) -> bool {
427        self.state.borrow().dragged > 0
428    }
429    pub fn collect_last_press_position(&self) -> Option<Vec2> {
430        self.state.borrow().last_press_position
431    }
432    pub fn collect_last_press_id(&self) -> Option<PressId> {
433        self.state.borrow().last_press_id
434    }
435    /// Stable identity: the pointer of the shared state Rc.
436    pub fn stable_id(&self) -> *const () {
437        Rc::as_ptr(&self.state) as *const ()
438    }
439    /// Get a mutable handle to the same underlying state.
440    /// Both handles share the same `Rc<RefCell<..>>`, so mutations via
441    /// the returned `MutableInteractionSource` are reflected here.
442    pub fn to_mutable(&self) -> MutableInteractionSource {
443        MutableInteractionSource {
444            state: self.state.clone(),
445        }
446    }
447
448    /// Convenience: hard-reset via read handle (same Rc).
449    pub fn reset(&self) {
450        self.to_mutable().reset();
451    }
452
453    /// Convenience: clear hover only via read handle (same Rc).
454    pub fn reset_hover(&self) {
455        self.to_mutable().reset_hover();
456    }
457}
458
459/// Mutable handle to a shared interaction state.
460///
461/// Create one via [`MutableInteractionSource::new`], then pass the read-only
462/// [`InteractionSource`] to modifiers via `.interaction_source(&source)`.
463///
464/// ```ignore
465/// let src = remember(MutableInteractionSource::new);
466/// m = m.clickable().interaction_source(&src).state_colors(...);
467/// ```
468#[derive(Clone)]
469pub struct MutableInteractionSource {
470    pub(crate) state: Rc<RefCell<InteractionState>>,
471}
472
473impl std::fmt::Debug for MutableInteractionSource {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        f.debug_struct("MutableInteractionSource")
476            .finish_non_exhaustive()
477    }
478}
479
480impl MutableInteractionSource {
481    pub fn new() -> Self {
482        Self {
483            state: Rc::new(RefCell::new(InteractionState::default())),
484        }
485    }
486
487    /// Emit an interaction event, updating the shared state.
488    pub fn emit(&self, interaction: Interaction) {
489        let changed = {
490            let mut s = self.state.borrow_mut();
491            match interaction {
492                Interaction::Press(id, pos) => {
493                    let inserted = s.active_presses.insert(id);
494                    s.last_press_id = Some(id);
495                    s.last_press_position = Some(pos);
496                    inserted
497                }
498                Interaction::Release(id) | Interaction::Cancel(id) => {
499                    if s.active_presses.remove(&id) {
500                        true
501                    } else if id == 0 {
502                        if let Some(any) = s.active_presses.iter().next().copied() {
503                            s.active_presses.remove(&any);
504                            true
505                        } else {
506                            false
507                        }
508                    } else {
509                        false
510                    }
511                }
512                Interaction::HoverEnter => {
513                    let changed = !s.hovered;
514                    s.hovered = true;
515                    changed
516                }
517                Interaction::HoverLeave => {
518                    let changed = s.hovered;
519                    s.hovered = false;
520                    // Leaving while pressed cancels all presses (Compose-like).
521                    if !s.active_presses.is_empty() {
522                        s.active_presses.clear();
523                        true
524                    } else {
525                        changed
526                    }
527                }
528                Interaction::Focus => {
529                    let changed = !s.focused;
530                    s.focused = true;
531                    changed
532                }
533                Interaction::Unfocus => {
534                    let changed = s.focused;
535                    s.focused = false;
536                    changed
537                }
538                Interaction::DragStart => {
539                    let changed = s.dragged == 0;
540                    s.dragged = s.dragged.saturating_add(1);
541                    changed
542                }
543                Interaction::DragStop | Interaction::DragCancel => {
544                    let was = s.dragged;
545                    s.dragged = s.dragged.saturating_sub(1);
546                    was != s.dragged
547                }
548            }
549        };
550        if changed {
551            // So source-driven paint (ripple, state layers) cannot stick stale.
552            crate::frame_clock::request_frame();
553        }
554    }
555
556    /// Get a read-only handle to the shared state.
557    pub fn source(&self) -> InteractionSource {
558        InteractionSource {
559            state: self.state.clone(),
560        }
561    }
562
563    /// Hard-reset all interaction flags.
564    pub fn reset(&self) {
565        let mut s = self.state.borrow_mut();
566        *s = InteractionState::default();
567        crate::frame_clock::request_frame();
568    }
569
570    /// Clear hover only (keep press/focus/drag).
571    pub fn reset_hover(&self) {
572        let mut s = self.state.borrow_mut();
573        if s.hovered {
574            s.hovered = false;
575            crate::frame_clock::request_frame();
576        }
577    }
578}
579
580impl Default for MutableInteractionSource {
581    fn default() -> Self {
582        Self::new()
583    }
584}
585
586#[derive(Clone, Default)]
587pub(crate) struct InteractionState {
588    /// Active press IDs (Press → Release/Cancel pairing).
589    active_presses: HashSet<PressId>,
590    hovered: bool,
591    focused: bool,
592    dragged: u32,
593    /// Most recent press position (used by ripple for origin).
594    pub(crate) last_press_position: Option<Vec2>,
595    /// Most recent press ID.
596    pub(crate) last_press_id: Option<PressId>,
597}
598
599#[derive(Clone, Default)]
600pub struct Modifier {
601    /// Optional stable identity key for this view node.
602    ///
603    /// If set, `layout_and_paint` will prefer this over child index when assigning stable ViewIds.
604    /// This is the “escape hatch” for dynamic lists / conditional UI where index-based identity
605    /// would otherwise shift.
606    pub key: Option<u64>,
607
608    pub size: Option<Size>,
609    pub width: Option<f32>,
610    pub height: Option<f32>,
611    pub required_size: Option<Size>,
612    pub fill_max: Option<f32>,
613    pub fill_max_w: Option<f32>,
614    pub fill_max_h: Option<f32>,
615    pub padding: Option<f32>,
616    pub padding_values: Option<PaddingValues>,
617    pub min_width: Option<f32>,
618    pub min_height: Option<f32>,
619    pub max_width: Option<f32>,
620    pub max_height: Option<f32>,
621    /// Like [`required_size`] but only for min width. Overrides parent min constraints.
622    pub required_min_width: Option<f32>,
623    /// Like [`required_size`] but only for max width. Overrides parent max constraints.
624    pub required_max_width: Option<f32>,
625    /// Like [`required_size`] but only for min height. Overrides parent min constraints.
626    pub required_min_height: Option<f32>,
627    /// Like [`required_size`] but only for max height. Overrides parent max constraints.
628    pub required_max_height: Option<f32>,
629    /// Minimum size that only applies when the incoming constraint is 0 (unconstrained).
630    /// Use [`min_width`] for an unconditional minimum.
631    pub default_min_width: Option<f32>,
632    pub default_min_height: Option<f32>,
633    pub background: Option<Brush>,
634    pub state_colors: Option<StateColors>,
635    pub state_elevation: Option<StateElevation>,
636
637    pub border: Option<Border>,
638    pub flex_grow: Option<f32>,
639    pub flex_shrink: Option<f32>,
640    pub flex_basis: Option<f32>,
641    pub flex_wrap: Option<FlexWrap>,
642    pub flex_dir: Option<FlexDirection>,
643    pub gap: Option<f32>,
644    pub row_gap: Option<f32>,
645    pub column_gap: Option<f32>,
646    pub align_self: Option<AlignSelf>,
647    pub justify_content: Option<JustifyContent>,
648    pub align_items_container: Option<AlignItems>,
649    pub align_content: Option<AlignContent>,
650    pub clip_rounded: Option<[f32; 4]>,
651    /// Rectangular clip with a clipping operation (Intersect or Difference).
652    /// The rect is relative to the element bounds, in dp.
653    pub clip_rect: Option<ClipRect>,
654    /// Controls whether child content is clipped to the parent bounds.
655    ///
656    /// Defaults to `Clip`. When set to `Visible`, children can overflow
657    /// the parent's rounded rect or clip rect boundary.
658    pub overflow: Option<Overflow>,
659    /// Z-index for hit-testing order (higher = receives events first).
660    pub z_index: f32,
661    /// Z-index for render order (higher = painted on top). If None, uses tree order.
662    pub render_z_index: Option<f32>,
663    /// If true, this view does not create hit regions.
664    pub hit_passthrough: bool,
665    /// If true, this view blocks pointer/touch input for hits below it.
666    pub input_blocker: bool,
667    pub repaint_boundary: bool,
668    pub click: bool,
669    /// When true, the component ignores pointer events and appears disabled.
670    pub disabled: bool,
671    /// When Some(true), the component can receive keyboard focus regardless of interactivity.
672    /// When Some(false), the component cannot receive focus even if interactive.
673    /// When None, focusability is determined implicitly by interactivity (click/pointer/dnd handlers).
674    pub focusable: Option<bool>,
675    /// When true, the Box passes its min constraints to children instead of removing them.
676    pub propagate_min: bool,
677    /// When true, this node and its children form a focus group: focus cycles within
678    /// the group before moving outside it.
679    pub focus_group: bool,
680    pub on_scroll: Option<Rc<dyn Fn(Vec2) -> Vec2>>,
681    /// Scroll modifier binding. When set, the layout engine treats this view as
682    /// a scroll container, applying clipping and offset to children.
683    ///
684    /// Use `Modifier::vertical_scroll()`, `Modifier::horizontal_scroll()`, or
685    /// `Modifier::scrollable()` to set this.
686    pub scroll: Option<crate::scroll::ScrollBinding>,
687    /// Nested scroll connection for coordinated scrolling between this element
688    /// and its scrollable descendants.
689    ///
690    /// When set on an ancestor of a scroll container (e.g. `ScrollArea`,
691    /// `LazyColumn`), the scroll container automatically discovers this
692    /// connection and dispatches pre/post scroll events to it during layout.
693    ///
694    /// Mirrors Compose's `Modifier.nestedScroll(NestedScrollConnection)`.
695    pub nested_scroll_connection: Option<crate::nested_scroll::NestedScrollConnection>,
696    pub on_pointer_down: Option<Rc<dyn Fn(PointerEvent)>>,
697    pub on_pointer_move: Option<Rc<dyn Fn(PointerEvent)>>,
698    pub on_pointer_up: Option<Rc<dyn Fn(PointerEvent)>>,
699    pub on_pointer_cancel: Option<Rc<dyn Fn(PointerEvent)>>,
700    pub on_pointer_enter: Option<Rc<dyn Fn(PointerEvent)>>,
701    pub on_pointer_leave: Option<Rc<dyn Fn(PointerEvent)>>,
702    /// Called when the element is clicked (pointer down then up within bounds).
703    pub on_click: Option<Rc<dyn Fn()>>,
704    /// Called when the element is double-clicked/tapped.
705    pub on_double_click: Option<Rc<dyn Fn()>>,
706    /// Called when the element is long-pressed.
707    pub on_long_click: Option<Rc<dyn Fn()>>,
708    /// Called when the element's global position changes after layout.
709    /// Provides the element's rect in window coordinates.
710    pub on_globally_positioned: Option<Rc<dyn Fn(crate::Rect)>>,
711    /// Called when the element's size changes after layout.
712    /// Provides the new size.
713    pub on_size_changed: Option<Rc<dyn Fn(crate::Vec2)>>,
714    /// Called when a key event is received while this element is focused.
715    /// Return `true` to consume the event. This is the normal handler.
716    pub on_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
717    /// Called before `on_key_event` -> if the preview handler returns `true`,
718    /// the event is consumed and `on_key_event` is NOT called.
719    pub on_preview_key_event: Option<Rc<dyn Fn(crate::input::KeyEvent) -> bool>>,
720    /// Apply a gaussian blur to this element's rendered content.
721    /// When set, `graphics_layer` is auto-enabled if not already set.
722    /// Use `Modifier::blur(radius)` for uniform blur, or
723    /// `Modifier::blur_with_edge(rx, ry, edge)` for per-axis control.
724    pub blur: Option<BlurStyle>,
725    /// Custom layout callback. When set, the element's measurement is delegated
726    /// to this function instead of the default Taffy-based layout.
727    /// The callback receives `LayoutConstraints` (min/max width/height in dp).
728    /// Returns `(width, height)` for this element.
729    pub layout: Option<Rc<dyn Fn(LayoutConstraints) -> (f32, f32)>>,
730    pub semantics: Option<crate::Semantics>,
731    pub alpha: Option<f32>,
732    pub graphics_layer: Option<f32>,
733    pub shadow: Option<ShadowSpec>,
734    pub transform: Option<Transform>,
735    pub grid: Option<GridConfig>,
736    pub grid_col_span: Option<u16>,
737    pub grid_row_span: Option<u16>,
738    pub position_type: Option<PositionType>,
739    pub offset_left: Option<f32>,
740    pub offset_right: Option<f32>,
741    pub offset_top: Option<f32>,
742    pub offset_bottom: Option<f32>,
743
744    pub margin_left: Option<f32>,
745    pub margin_right: Option<f32>,
746    pub margin_top: Option<f32>,
747    pub margin_bottom: Option<f32>,
748    pub aspect_ratio: Option<f32>,
749    /// Size this node's width to its min or max intrinsic content size.
750    pub intrinsic_width: Option<IntrinsicSize>,
751    /// Size this node's height to its min or max intrinsic content size.
752    pub intrinsic_height: Option<IntrinsicSize>,
753    pub painter: Option<Rc<dyn Fn(&mut crate::Scene, crate::Rect, f32)>>,
754    pub paint_callback: Option<crate::PaintCallbackPayload>,
755
756    // Drag-drop (internal)
757    pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
758    pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
759    pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
760    pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
761    pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
762    pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
763    /// Compose-like `drawDragDecoration`: paints the floating preview while dragging.
764    pub drag_preview: Option<crate::dnd::DragPreview>,
765
766    pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
767
768    /// Cursor icon hint for desktop/web runners.
769    pub cursor: Option<crate::CursorIcon>,
770
771    /// If set, the size of this node will smoothly animate to its target size
772    /// whenever content size changes. Uses the provided animation spec.
773    pub animate_content_size: Option<AnimationSpec>,
774
775    /// A `FocusRequester` handle that will be associated with this view.
776    /// When the requester's `request_focus()` is called, keyboard focus will
777    /// move to this view.
778    pub focus_requester: Option<crate::runtime::FocusRequester>,
779
780    /// Called when this view gains or loses focus. The boolean parameter is
781    /// `true` when focused, `false` when unfocused.
782    pub on_focus_changed: Option<Rc<dyn Fn(bool)>>,
783
784    /// If set, this view reads its interaction state (hover/press) from this source
785    /// in addition to the implicit view-ID-based matching. The source state is OR'd
786    /// with the implicit state, enabling programmatic override of hover/press visuals.
787    ///
788    /// When set, the layout engine also auto-wires the source to emit PointerDown/Up
789    /// and HoverEnter/Leave events into the source via the hit region's callbacks.
790    pub interaction_source: Option<InteractionSource>,
791
792    /// Text input configuration. When set, this box acts as a text input field.
793    pub text_input: Option<TextInputConfig>,
794
795    /// Indication (ripple/overlay) factory for visual feedback on interaction.
796    pub indication: Option<Rc<dyn IndicationNodeFactory>>,
797}
798
799impl std::fmt::Debug for Modifier {
800    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
801        let mut s = f.debug_struct("Modifier");
802
803        macro_rules! opt_val {
804            ($($name:ident),+ $(,)?) => {
805                $( if self.$name.is_some() { s.field(stringify!($name), &self.$name); } )+
806            };
807        }
808        if self.indication.is_some() {
809            s.field("indication", &"…");
810        }
811
812        opt_val!(
813            key,
814            size,
815            width,
816            height,
817            required_size,
818            padding,
819            padding_values,
820            min_width,
821            min_height,
822            max_width,
823            max_height,
824            required_min_width,
825            required_max_width,
826            required_min_height,
827            required_max_height,
828            default_min_width,
829            default_min_height,
830            fill_max,
831            fill_max_w,
832            fill_max_h,
833            background,
834            state_colors,
835            state_elevation,
836            border,
837            flex_grow,
838            flex_shrink,
839            flex_basis,
840            flex_wrap,
841            flex_dir,
842            gap,
843            row_gap,
844            column_gap,
845            align_self,
846            justify_content,
847            align_items_container,
848            align_content,
849            clip_rounded,
850            clip_rect,
851            render_z_index,
852            semantics,
853            alpha,
854            transform,
855            grid,
856            grid_col_span,
857            grid_row_span,
858            position_type,
859            offset_left,
860            offset_right,
861            offset_top,
862            offset_bottom,
863            margin_left,
864            margin_right,
865            margin_top,
866            margin_bottom,
867            aspect_ratio,
868            intrinsic_width,
869            intrinsic_height,
870            cursor,
871            animate_content_size,
872            blur,
873        );
874
875        macro_rules! opt_cb {
876            ($($name:ident),+ $(,)?) => {
877                $( if self.$name.is_some() { s.field(stringify!($name), &"…"); } )+
878            };
879        }
880        opt_cb!(
881            on_scroll,
882            scroll,
883            nested_scroll_connection,
884            on_pointer_down,
885            on_pointer_move,
886            on_pointer_up,
887            on_pointer_cancel,
888            on_pointer_enter,
889            on_pointer_leave,
890            on_click,
891            on_double_click,
892            on_long_click,
893            on_globally_positioned,
894            on_size_changed,
895            on_key_event,
896            on_preview_key_event,
897            painter,
898            paint_callback,
899            on_drag_start,
900            on_drag_end,
901            on_drag_enter,
902            on_drag_over,
903            on_drag_leave,
904            on_drop,
905            drag_preview,
906            on_action,
907            on_focus_changed,
908            interaction_source,
909            text_input,
910            layout,
911        );
912
913        macro_rules! flag {
914            ($($name:ident),+ $(,)?) => {
915                $( if self.$name { s.field(stringify!($name), &true); } )+
916            };
917        }
918        flag!(
919            hit_passthrough,
920            input_blocker,
921            repaint_boundary,
922            click,
923            disabled,
924            propagate_min,
925            focus_group,
926        );
927
928        if let Some(f) = self.focusable {
929            s.field("focusable", &f);
930        }
931        if self.z_index != 0.0 {
932            s.field("z_index", &self.z_index);
933        }
934
935        s.finish()
936    }
937}
938
939impl_option_fields!(Modifier);
940
941/// Content alignment for a container, applied as the flexbox cross-axis
942/// (`align_items`) and main-axis (`justify_content`) pair. Mirrors Compose's
943/// `Alignment` for `Box(contentAlignment = ...)`.
944#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
945pub enum Alignment {
946    TopStart,
947    TopCenter,
948    TopEnd,
949    CenterStart,
950    #[default]
951    Center,
952    CenterEnd,
953    BottomStart,
954    BottomCenter,
955    BottomEnd,
956}
957
958impl Alignment {
959    /// The corresponding (`align_items`, `justify_content`) pair for flexbox layout.
960    pub fn to_flex(self) -> (AlignItems, JustifyContent) {
961        use AlignItems as AI;
962        use JustifyContent as JC;
963        match self {
964            Self::TopStart => (AI::START, JC::START),
965            Self::TopCenter => (AI::START, JC::CENTER),
966            Self::TopEnd => (AI::START, JC::END),
967            Self::CenterStart => (AI::CENTER, JC::START),
968            Self::Center => (AI::CENTER, JC::CENTER),
969            Self::CenterEnd => (AI::CENTER, JC::END),
970            Self::BottomStart => (AI::END, JC::START),
971            Self::BottomCenter => (AI::END, JC::CENTER),
972            Self::BottomEnd => (AI::END, JC::END),
973        }
974    }
975}
976
977impl Modifier {
978    pub fn new() -> Self {
979        Self::default()
980    }
981
982    /// Attaches a stable identity key to this view node.
983    /// Use for dynamic lists / conditional UI where index-based identity can shift.
984    pub fn key(mut self, key: u64) -> Self {
985        self.key = Some(key);
986        self
987    }
988
989    pub fn size(mut self, w: f32, h: f32) -> Self {
990        self.size = Some(Size {
991            width: w,
992            height: h,
993        });
994        self
995    }
996    pub fn width(mut self, w: f32) -> Self {
997        self.width = Some(w);
998        self
999    }
1000    pub fn height(mut self, h: f32) -> Self {
1001        self.height = Some(h);
1002        self
1003    }
1004    /// Set a fixed size that overrides parent constraints.
1005    /// Unlike `size()` which is bounded by the parent's max constraints,
1006    /// `required_size()` forces the node to this size regardless of the parent,
1007    /// acting as both min and max.
1008    pub fn required_size(mut self, w: f32, h: f32) -> Self {
1009        self.required_size = Some(Size {
1010            width: w,
1011            height: h,
1012        });
1013        self
1014    }
1015    pub fn required_width_in(mut self, min: f32, max: f32) -> Self {
1016        self.required_min_width = Some(min.max(0.0));
1017        self.required_max_width = Some(max.max(0.0));
1018        self
1019    }
1020    pub fn required_height_in(mut self, min: f32, max: f32) -> Self {
1021        self.required_min_height = Some(min.max(0.0));
1022        self.required_max_height = Some(max.max(0.0));
1023        self
1024    }
1025    pub fn required_min_width(mut self, w: f32) -> Self {
1026        self.required_min_width = Some(w.max(0.0));
1027        self
1028    }
1029    pub fn required_max_width(mut self, w: f32) -> Self {
1030        self.required_max_width = Some(w.max(0.0));
1031        self
1032    }
1033    pub fn required_min_height(mut self, h: f32) -> Self {
1034        self.required_min_height = Some(h.max(0.0));
1035        self
1036    }
1037    pub fn required_max_height(mut self, h: f32) -> Self {
1038        self.required_max_height = Some(h.max(0.0));
1039        self
1040    }
1041    /// Minimum size that only takes effect when the incoming constraint is 0 (unconstrained).
1042    pub fn default_min_size(mut self, w: f32, h: f32) -> Self {
1043        self.default_min_width = Some(w.max(0.0));
1044        self.default_min_height = Some(h.max(0.0));
1045        self
1046    }
1047    /// Fill the available space in both dimensions.
1048    /// By default fills 100% (fraction = 1.0). Pass a fraction to fill partially.
1049    pub fn fill_max_size(mut self) -> Self {
1050        self.fill_max = Some(1.0);
1051        self
1052    }
1053    pub fn fill_max_size_frac(mut self, fraction: f32) -> Self {
1054        self.fill_max = Some(fraction.clamp(0.0, 1.0));
1055        self
1056    }
1057    /// Fill the available width. By default fills 100%.
1058    pub fn fill_max_width(mut self) -> Self {
1059        self.fill_max_w = Some(1.0);
1060        self
1061    }
1062    pub fn fill_max_width_frac(mut self, fraction: f32) -> Self {
1063        self.fill_max_w = Some(fraction.clamp(0.0, 1.0));
1064        self
1065    }
1066    /// Fill the available height. By default fills 100%.
1067    pub fn fill_max_height(mut self) -> Self {
1068        self.fill_max_h = Some(1.0);
1069        self
1070    }
1071    pub fn fill_max_height_frac(mut self, fraction: f32) -> Self {
1072        self.fill_max_h = Some(fraction.clamp(0.0, 1.0));
1073        self
1074    }
1075    pub fn padding(mut self, v: f32) -> Self {
1076        self.padding = Some(v);
1077        self
1078    }
1079    pub fn padding_values(mut self, padding: PaddingValues) -> Self {
1080        self.padding_values = Some(padding);
1081        self
1082    }
1083    /// Add padding equal to the current IME (soft keyboard) bottom inset.
1084    /// Combine with `system_bars_padding()` to handle both system bars and keyboard.
1085    pub fn ime_padding(mut self) -> Self {
1086        let insets = crate::locals::window_insets();
1087        let scale = crate::locals::effective_density_scale();
1088        let mut p = self.padding_values.unwrap_or_default();
1089        p.bottom += insets.ime_bottom / scale;
1090        self.padding_values = Some(p);
1091        self
1092    }
1093    /// Add padding equal to the current system bar insets (status bar top, nav bar bottom).
1094    pub fn system_bars_padding(mut self) -> Self {
1095        let insets = crate::locals::window_insets();
1096        let scale = crate::locals::effective_density_scale();
1097        let mut p = self.padding_values.unwrap_or_default();
1098        p.top += insets.top / scale;
1099        p.bottom += insets.bottom / scale;
1100        self.padding_values = Some(p);
1101        self
1102    }
1103    /// Add status bar inset as top padding.
1104    pub fn status_bars_padding(mut self) -> Self {
1105        let insets = crate::locals::window_insets();
1106        let scale = crate::locals::effective_density_scale();
1107        let mut p = self.padding_values.unwrap_or_default();
1108        p.top += insets.top / scale;
1109        self.padding_values = Some(p);
1110        self
1111    }
1112    /// Add navigation bar inset as bottom padding.
1113    pub fn navigation_bars_padding(mut self) -> Self {
1114        let insets = crate::locals::window_insets();
1115        let scale = crate::locals::effective_density_scale();
1116        let mut p = self.padding_values.unwrap_or_default();
1117        p.bottom += insets.bottom / scale;
1118        self.padding_values = Some(p);
1119        self
1120    }
1121    pub fn min_size(mut self, w: f32, h: f32) -> Self {
1122        self.min_width = Some(w);
1123        self.min_height = Some(h);
1124        self
1125    }
1126    pub fn max_size(mut self, w: f32, h: f32) -> Self {
1127        self.max_width = Some(w);
1128        self.max_height = Some(h);
1129        self
1130    }
1131    pub fn min_width(mut self, w: f32) -> Self {
1132        self.min_width = Some(w);
1133        self
1134    }
1135    pub fn min_height(mut self, h: f32) -> Self {
1136        self.min_height = Some(h);
1137        self
1138    }
1139    pub fn max_width(mut self, w: f32) -> Self {
1140        self.max_width = Some(w);
1141        self
1142    }
1143    pub fn max_height(mut self, h: f32) -> Self {
1144        self.max_height = Some(h);
1145        self
1146    }
1147    /// Set a solid color background.
1148    pub fn background(mut self, color: Color) -> Self {
1149        self.background = Some(Brush::Solid(color));
1150        self
1151    }
1152    /// Set a brush (solid, gradient, etc.) background.
1153    pub fn background_brush(mut self, brush: Brush) -> Self {
1154        self.background = Some(brush);
1155        self
1156    }
1157    pub fn border(mut self, width: f32, color: Color, radius: f32) -> Self {
1158        self.border = Some(Border {
1159            width,
1160            color,
1161            radius: [radius; 4],
1162        });
1163        self
1164    }
1165    pub fn border_radii(mut self, width: f32, color: Color, radii: [f32; 4]) -> Self {
1166        self.border = Some(Border {
1167            width,
1168            color,
1169            radius: radii,
1170        });
1171        self
1172    }
1173    pub fn flex_grow(mut self, v: f32) -> Self {
1174        self.flex_grow = Some(v);
1175        self
1176    }
1177    pub fn flex_shrink(mut self, v: f32) -> Self {
1178        self.flex_shrink = Some(v);
1179        self
1180    }
1181    pub fn flex_basis(mut self, v: f32) -> Self {
1182        self.flex_basis = Some(v);
1183        self
1184    }
1185    pub fn flex_wrap(mut self, w: FlexWrap) -> Self {
1186        self.flex_wrap = Some(w);
1187        self
1188    }
1189    pub fn flex_dir(mut self, d: FlexDirection) -> Self {
1190        self.flex_dir = Some(d);
1191        self
1192    }
1193    pub fn gap(mut self, v: f32) -> Self {
1194        let v = v.max(0.0);
1195        self.gap = Some(v);
1196        self.row_gap = Some(v);
1197        self.column_gap = Some(v);
1198        self
1199    }
1200    pub fn row_gap(mut self, v: f32) -> Self {
1201        self.row_gap = Some(v.max(0.0));
1202        self
1203    }
1204    pub fn column_gap(mut self, v: f32) -> Self {
1205        self.column_gap = Some(v.max(0.0));
1206        self
1207    }
1208    pub fn align_self(mut self, a: AlignSelf) -> Self {
1209        self.align_self = Some(a);
1210        self
1211    }
1212    pub fn align_self_center(mut self) -> Self {
1213        self.align_self = Some(AlignSelf::CENTER);
1214        self
1215    }
1216    pub fn justify_content(mut self, j: JustifyContent) -> Self {
1217        self.justify_content = Some(j);
1218        self
1219    }
1220    pub fn align_items(mut self, a: AlignItems) -> Self {
1221        self.align_items_container = Some(a);
1222        self
1223    }
1224    /// Compose-like content alignment (sets both `align_items` and
1225    /// `justify_content` in one call).
1226    pub fn content_alignment(self, alignment: Alignment) -> Self {
1227        let (ai, jc) = alignment.to_flex();
1228        self.align_items(ai).justify_content(jc)
1229    }
1230    pub fn align_content(mut self, a: AlignContent) -> Self {
1231        self.align_content = Some(a);
1232        self
1233    }
1234    pub fn clip_rounded(mut self, radius: f32) -> Self {
1235        self.clip_rounded = Some([radius; 4]);
1236        self
1237    }
1238    pub fn clip_rounded_radii(mut self, radii: [f32; 4]) -> Self {
1239        self.clip_rounded = Some(radii);
1240        self
1241    }
1242    /// Clip a rectangular region from this element using the given operation.
1243    /// `left`, `top`, `right`, `bottom` are relative to the element bounds, in dp.
1244    pub fn clip_rect(mut self, left: f32, top: f32, right: f32, bottom: f32, op: ClipOp) -> Self {
1245        self.clip_rect = Some(ClipRect {
1246            left,
1247            top,
1248            right,
1249            bottom,
1250            op,
1251        });
1252        self
1253    }
1254    pub fn overflow(mut self, overflow: Overflow) -> Self {
1255        self.overflow = Some(overflow);
1256        self
1257    }
1258    pub fn z_index(mut self, z: f32) -> Self {
1259        self.z_index = z;
1260        self
1261    }
1262
1263    /// Sets the render z-index for this view. Higher values are painted on top.
1264    /// Unlike `z_index` (which only affects hit-testing), this affects visual layering.
1265    pub fn render_z_index(mut self, z: f32) -> Self {
1266        self.render_z_index = Some(z);
1267        self
1268    }
1269
1270    /// Prevent pointer/touch from reaching lower layers.
1271    pub fn input_blocker(mut self) -> Self {
1272        self.input_blocker = true;
1273        self
1274    }
1275
1276    pub fn hit_passthrough(mut self) -> Self {
1277        self.hit_passthrough = true;
1278        self
1279    }
1280    pub fn clickable(mut self) -> Self {
1281        self.click = true;
1282        if self.indication.is_none() {
1283            self.indication = crate::locals::local_indication();
1284        }
1285        self
1286    }
1287    /// Make this element clickable and attach an [`InteractionSource`] for state tracking.
1288    /// Combines `.clickable()` and `.interaction_source(&source)` in one call.
1289    pub fn clickable_with_source(mut self, source: &MutableInteractionSource) -> Self {
1290        self.click = true;
1291        self.interaction_source = Some(source.source());
1292        if self.indication.is_none() {
1293            self.indication = crate::locals::local_indication();
1294        }
1295        self
1296    }
1297    /// Set state-driven background colors for hover, press, disabled states.
1298    /// The layout engine automatically selects and animates between these based on interaction.
1299    pub fn state_colors(mut self, colors: StateColors) -> Self {
1300        self.state_colors = Some(colors);
1301        self
1302    }
1303    /// Set state-driven elevation values for hover, press, disabled states.
1304    pub fn state_elevation(mut self, elev: StateElevation) -> Self {
1305        self.state_elevation = Some(elev);
1306        self
1307    }
1308    /// Mark this component as disabled - it won't respond to pointer events.
1309    pub fn disabled(mut self) -> Self {
1310        self.disabled = true;
1311        self
1312    }
1313    /// Mark this component as enabled or disabled.
1314    pub fn enabled(mut self, enabled: bool) -> Self {
1315        self.disabled = !enabled;
1316        self
1317    }
1318    /// Set explicit focusability for this component.
1319    /// When `true`, the component can receive keyboard focus even without
1320    /// explicit click/pointer/dnd handlers. When `false`, focus is suppressed
1321    /// even for interactive components.
1322    pub fn focusable(mut self, focusable: bool) -> Self {
1323        self.focusable = Some(focusable);
1324        self
1325    }
1326    /// Mark this node as a focus group: focus cycles within this group before
1327    /// moving to siblings outside it.
1328    pub fn focus_group(mut self) -> Self {
1329        self.focus_group = true;
1330        self
1331    }
1332    /// Attach an [`InteractionSource`] to this view. The source provides shared
1333    /// interaction state (hover/press/focus/drag) that supplements the implicit
1334    /// view-ID-based state. The layout engine auto-wires pointer events
1335    /// (press/hover), keyboard activation (Space/Enter press parity), focus
1336    /// transitions (Focus/Unfocus) and DnD drag start/end into the source so it
1337    /// stays in sync with user interaction.
1338    ///
1339    /// Use this when you need programmatic control of interaction state (e.g.,
1340    /// showing pressed state during an async operation) or to share interaction
1341    /// state between components.
1342    pub fn interaction_source(mut self, source: &MutableInteractionSource) -> Self {
1343        self.interaction_source = Some(source.source());
1344        self
1345    }
1346    /// Convenience: register hover enter/leave callbacks.
1347    /// Shorthand for setting `on_pointer_enter` and `on_pointer_leave`.
1348    pub fn hoverable(
1349        mut self,
1350        on_enter: impl Fn() + 'static,
1351        on_leave: impl Fn() + 'static,
1352    ) -> Self {
1353        self.on_pointer_enter = Some(Rc::new(move |_| on_enter()));
1354        self.on_pointer_leave = Some(Rc::new(move |_| on_leave()));
1355        self
1356    }
1357    /// Attach an [`InteractionSource`] to track hover state without explicit callbacks.
1358    /// The source automatically receives HoverEnter/Leave events from the layout engine's
1359    /// auto-wiring, so you can use it with `collect_is_hovered()` for custom visuals.
1360    pub fn hoverable_with_source(mut self, source: &MutableInteractionSource) -> Self {
1361        self.interaction_source = Some(source.source());
1362        self
1363    }
1364    /// When true, Box passes min-width/min-height constraints to its children
1365    /// instead of allowing them to shrink below the parent's min constraints.
1366    pub fn propagate_min_constraints(mut self, propagate: bool) -> Self {
1367        self.propagate_min = propagate;
1368        self
1369    }
1370    pub fn on_scroll(mut self, f: impl Fn(Vec2) -> Vec2 + 'static) -> Self {
1371        self.on_scroll = Some(Rc::new(f));
1372        self
1373    }
1374    /// Attach a vertical scroll binding to this modifier.
1375    /// The binding provides callbacks for scroll handling, viewport tracking, etc.
1376    /// Use `ScrollState::to_binding()` to create one from a scroll state.
1377    pub fn vertical_scroll(mut self, binding: crate::scroll::ScrollAxisBinding) -> Self {
1378        self.scroll = Some(crate::scroll::ScrollBinding::Vertical(binding));
1379        self
1380    }
1381    /// Attach a horizontal scroll binding to this modifier.
1382    pub fn horizontal_scroll(mut self, binding: crate::scroll::ScrollAxisBinding) -> Self {
1383        self.scroll = Some(crate::scroll::ScrollBinding::Horizontal(binding));
1384        self
1385    }
1386    /// Attach a 2D scroll binding to this modifier.
1387    pub fn scrollable(mut self, binding: crate::scroll::ScrollBothBinding) -> Self {
1388        self.scroll = Some(crate::scroll::ScrollBinding::Both(binding));
1389        self
1390    }
1391    /// Attach a nested scroll connection that descendant scrollable containers
1392    /// will discover during layout. Mirrors Compose's `Modifier.nestedScroll`.
1393    ///
1394    /// The connection receives pre/post scroll and pre/post fling callbacks
1395    /// when a scrollable child dispatches events, enabling coordinated scrolling
1396    /// patterns like collapsing toolbars and pull-to-refresh.
1397    pub fn nested_scroll(mut self, conn: crate::nested_scroll::NestedScrollConnection) -> Self {
1398        self.nested_scroll_connection = Some(conn);
1399        self
1400    }
1401    pub fn on_pointer_down(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
1402        self.on_pointer_down = Some(Rc::new(f));
1403        self
1404    }
1405    pub fn on_pointer_move(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
1406        self.on_pointer_move = Some(Rc::new(f));
1407        self
1408    }
1409    pub fn on_pointer_up(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
1410        self.on_pointer_up = Some(Rc::new(f));
1411        self
1412    }
1413    pub fn on_pointer_cancel(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
1414        self.on_pointer_cancel = Some(Rc::new(f));
1415        self
1416    }
1417
1418    pub fn draggable(self, on_drag: impl Fn(Vec2) + 'static) -> Self {
1419        let drag_pos = crate::state::remember_mutable(|| Vec2::default());
1420        let is_dragging = crate::state::remember_mutable(|| false);
1421        let on_drag = Rc::new(on_drag);
1422        self.on_pointer_down({
1423            let drag_pos = drag_pos.clone();
1424            let is_dragging = is_dragging.clone();
1425            move |ev| {
1426                is_dragging.set(true);
1427                drag_pos.set(ev.position);
1428            }
1429        })
1430        .on_pointer_up({
1431            let is_dragging = is_dragging.clone();
1432            move |_| is_dragging.set(false)
1433        })
1434        .on_pointer_cancel({
1435            let is_dragging = is_dragging.clone();
1436            move |_| is_dragging.set(false)
1437        })
1438        .on_pointer_move({
1439            let drag_pos = drag_pos.clone();
1440            let is_dragging = is_dragging.clone();
1441            let on_drag = on_drag.clone();
1442            move |ev| {
1443                if !*is_dragging.get() {
1444                    return;
1445                }
1446                let prev = *drag_pos.get();
1447                let cur = ev.position;
1448                let delta = Vec2 {
1449                    x: cur.x - prev.x,
1450                    y: cur.y - prev.y,
1451                };
1452                drag_pos.set(cur);
1453                on_drag(delta);
1454                crate::frame_clock::request_frame();
1455            }
1456        })
1457    }
1458    pub fn on_pointer_enter(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
1459        self.on_pointer_enter = Some(Rc::new(f));
1460        self
1461    }
1462    pub fn on_pointer_leave(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
1463        self.on_pointer_leave = Some(Rc::new(f));
1464        self
1465    }
1466    pub fn on_click(mut self, f: impl Fn() + 'static) -> Self {
1467        self.on_click = Some(Rc::new(f));
1468        self.click = true;
1469        if self.semantics.is_none() {
1470            self.semantics = Some(crate::Semantics::new(crate::Role::Button));
1471        }
1472        self
1473    }
1474    pub fn on_double_click(mut self, f: impl Fn() + 'static) -> Self {
1475        self.on_double_click = Some(Rc::new(f));
1476        self.click = true;
1477        self
1478    }
1479    pub fn on_long_click(mut self, f: impl Fn() + 'static) -> Self {
1480        self.on_long_click = Some(Rc::new(f));
1481        self.click = true;
1482        self
1483    }
1484    pub fn clickable_ext(
1485        mut self,
1486        enabled: bool,
1487        on_click_label: Option<String>,
1488        role: Option<crate::semantics::Role>,
1489        on_click: impl Fn() + 'static,
1490    ) -> Self {
1491        if !enabled {
1492            let mut s = self.semantics.clone().unwrap_or_else(|| {
1493                crate::semantics::Semantics::new(role.unwrap_or(crate::semantics::Role::Button))
1494            });
1495            s.enabled = false;
1496            if let Some(r) = role {
1497                s.role = r;
1498            }
1499            if let Some(l) = on_click_label {
1500                s.label = Some(l);
1501            }
1502            return self
1503                .clickable()
1504                .enabled(false)
1505                .default_min_size(48.0, 48.0)
1506                .semantics(s);
1507        }
1508        self = self.clickable().on_click(on_click);
1509        if role.is_some() || on_click_label.is_some() {
1510            let mut s = self.semantics.clone().unwrap_or_else(|| {
1511                crate::semantics::Semantics::new(role.unwrap_or(crate::semantics::Role::Button))
1512            });
1513            s.enabled = true;
1514            if let Some(r) = role {
1515                s.role = r;
1516            }
1517            if let Some(l) = on_click_label {
1518                s.label = Some(l);
1519            }
1520            self = self.semantics(s);
1521        }
1522        self.default_min_size(48.0, 48.0)
1523    }
1524    pub fn combined_clickable(
1525        mut self,
1526        enabled: bool,
1527        on_click_label: Option<String>,
1528        role: Option<crate::semantics::Role>,
1529        on_long_click_label: Option<String>,
1530        on_click: impl Fn() + 'static,
1531        on_long_click: Option<impl Fn() + 'static>,
1532        on_double_click: Option<impl Fn() + 'static>,
1533    ) -> Self {
1534        let _ = on_long_click_label;
1535        if !enabled {
1536            return self.clickable_ext(false, on_click_label, role, || {});
1537        }
1538        self = self.clickable_ext(true, on_click_label, role, on_click);
1539        if let Some(f) = on_long_click {
1540            self = self.on_long_click(f);
1541        }
1542        if let Some(f) = on_double_click {
1543            self = self.on_double_click(f);
1544        }
1545        self.default_min_size(48.0, 48.0)
1546    }
1547    pub fn semantics(mut self, s: crate::Semantics) -> Self {
1548        self.semantics = Some(s);
1549        self
1550    }
1551    pub fn alpha(mut self, a: f32) -> Self {
1552        self.alpha = Some(a);
1553        self
1554    }
1555    /// Render this subtree into an offscreen texture, then composite it
1556    /// back into the parent with the given group `alpha` (0.0..=1.0).
1557    /// Allows correct blending when children overlap inside the layer, and
1558    /// sets up the architecture for future layer effects (shadow, blur, clip).
1559    pub fn graphics_layer(mut self, alpha: f32) -> Self {
1560        self.graphics_layer = Some(alpha.clamp(0.0, 1.0));
1561        self
1562    }
1563    /// Drop shadow with the given `blur_radius` (dp) and vertical `offset_y` (dp).
1564    /// The shadow color defaults to black with alpha 64 (~25%). Combines with
1565    /// [`Modifier::graphics_layer`] to draw a shadow underneath the layer.
1566    pub fn shadow(mut self, blur_radius: f32, offset_y: f32) -> Self {
1567        self.shadow = Some(ShadowSpec {
1568            blur_radius: blur_radius.max(0.0),
1569            offset_y,
1570            color: Color(0, 0, 0, 64),
1571        });
1572        self
1573    }
1574    /// Drop shadow with a custom color. Alpha 0..=255.
1575    pub fn shadow_with_color(mut self, blur_radius: f32, offset_y: f32, color: Color) -> Self {
1576        self.shadow = Some(ShadowSpec {
1577            blur_radius: blur_radius.max(0.0),
1578            offset_y,
1579            color,
1580        });
1581        self
1582    }
1583    /// Material-style elevation. Auto-scales blur and offset by `level` (dp)
1584    /// and uses a default shadow color. Level 0 = no shadow. 4 = subtle;
1585    /// 16 = strong. Requires [`Modifier::graphics_layer`] to take effect.
1586    pub fn elevation(mut self, level: f32) -> Self {
1587        if level <= 0.0 {
1588            self.shadow = None;
1589            return self;
1590        }
1591        self.shadow = Some(ShadowSpec {
1592            blur_radius: level * 2.0,
1593            offset_y: level * 0.5,
1594            color: Color(0, 0, 0, (level * 8.0).clamp(8.0, 80.0) as u8),
1595        });
1596        self
1597    }
1598    pub fn transform(mut self, t: Transform) -> Self {
1599        self.transform = Some(t);
1600        self
1601    }
1602    pub fn grid(mut self, columns: usize, row_gap: f32, column_gap: f32) -> Self {
1603        self.grid = Some(GridConfig {
1604            columns,
1605            row_gap,
1606            column_gap,
1607        });
1608        self
1609    }
1610    pub fn grid_span(mut self, col_span: u16, row_span: u16) -> Self {
1611        self.grid_col_span = Some(col_span);
1612        self.grid_row_span = Some(row_span);
1613        self
1614    }
1615    pub fn absolute(mut self) -> Self {
1616        self.position_type = Some(PositionType::Absolute);
1617        self
1618    }
1619    pub fn offset(
1620        mut self,
1621        left: Option<f32>,
1622        top: Option<f32>,
1623        right: Option<f32>,
1624        bottom: Option<f32>,
1625    ) -> Self {
1626        self.offset_left = left;
1627        self.offset_top = top;
1628        self.offset_right = right;
1629        self.offset_bottom = bottom;
1630        self
1631    }
1632    pub fn offset_left(mut self, v: f32) -> Self {
1633        self.offset_left = Some(v);
1634        self
1635    }
1636    pub fn offset_right(mut self, v: f32) -> Self {
1637        self.offset_right = Some(v);
1638        self
1639    }
1640    pub fn offset_top(mut self, v: f32) -> Self {
1641        self.offset_top = Some(v);
1642        self
1643    }
1644    pub fn offset_bottom(mut self, v: f32) -> Self {
1645        self.offset_bottom = Some(v);
1646        self
1647    }
1648    pub fn margin(mut self, v: f32) -> Self {
1649        self.margin_left = Some(v);
1650        self.margin_right = Some(v);
1651        self.margin_top = Some(v);
1652        self.margin_bottom = Some(v);
1653        self
1654    }
1655
1656    pub fn margin_horizontal(mut self, v: f32) -> Self {
1657        self.margin_left = Some(v);
1658        self.margin_right = Some(v);
1659        self
1660    }
1661
1662    pub fn margin_vertical(mut self, v: f32) -> Self {
1663        self.margin_top = Some(v);
1664        self.margin_bottom = Some(v);
1665        self
1666    }
1667    pub fn aspect_ratio(mut self, ratio: f32) -> Self {
1668        self.aspect_ratio = Some(ratio);
1669        self
1670    }
1671    /// Size this node's width to its min or max intrinsic content size.
1672    pub fn intrinsic_width(mut self, mode: IntrinsicSize) -> Self {
1673        self.intrinsic_width = Some(mode);
1674        self
1675    }
1676    /// Size this node's height to its min or max intrinsic content size.
1677    pub fn intrinsic_height(mut self, mode: IntrinsicSize) -> Self {
1678        self.intrinsic_height = Some(mode);
1679        self
1680    }
1681    pub fn painter(mut self, f: impl Fn(&mut crate::Scene, crate::Rect, f32) + 'static) -> Self {
1682        self.painter = Some(Rc::new(f));
1683        self
1684    }
1685    /// Attach a custom GPU callback (like `egui::PaintCallback`). The payload
1686    /// is type-erased (`Arc<dyn Any>`) so the core crate stays backend-agnostic.
1687    /// Prefer `repose_render_wgpu::Callback::new` + `repose_ui::Embedded` or
1688    /// `repose_render_wgpu::Callback::embedded_view` over calling this directly.
1689    /// `rect` for the callback comes from layout (`SceneNode::Callback.rect`).
1690    pub fn paint_callback(mut self, payload: crate::PaintCallbackPayload) -> Self {
1691        self.paint_callback = Some(payload);
1692        self
1693    }
1694    pub fn scale(self, s: f32) -> Self {
1695        self.scale2(s, s)
1696    }
1697    pub fn scale2(mut self, sx: f32, sy: f32) -> Self {
1698        let mut t = self.transform.unwrap_or_else(Transform::identity);
1699        t.scale_x *= sx;
1700        t.scale_y *= sy;
1701        self.transform = Some(t);
1702        self
1703    }
1704    pub fn translate(mut self, x: f32, y: f32) -> Self {
1705        let t = self.transform.unwrap_or_else(Transform::identity);
1706        self.transform = Some(t.combine(&Transform::translate(x, y)));
1707        self
1708    }
1709    pub fn translate_vec2(self, v: Vec2) -> Self {
1710        self.translate(v.x, v.y)
1711    }
1712    pub fn rotate(mut self, radians: f32) -> Self {
1713        let mut t = self.transform.unwrap_or_else(Transform::identity);
1714        t.rotate += radians;
1715        self.transform = Some(t);
1716        self
1717    }
1718    pub fn transform_origin(mut self, x: f32, y: f32) -> Self {
1719        let mut t = self.transform.unwrap_or_else(Transform::identity);
1720        t.origin_x = x;
1721        t.origin_y = y;
1722        self.transform = Some(t);
1723        self
1724    }
1725    pub fn weight(mut self, w: f32) -> Self {
1726        let w = w.max(0.0);
1727        self.flex_grow = Some(w);
1728        self.flex_shrink = Some(1.0);
1729        // dp units; 0 is fine.
1730        self.flex_basis = Some(0.0);
1731        self
1732    }
1733    /// Marks this view as a repaint boundary candidate.
1734    ///
1735    /// The engine may cache its painted output.
1736    pub fn repaint_boundary(mut self) -> Self {
1737        self.repaint_boundary = true;
1738        self
1739    }
1740    pub fn on_action(mut self, f: impl Fn(crate::shortcuts::Action) -> bool + 'static) -> Self {
1741        self.on_action = Some(Rc::new(f));
1742        self
1743    }
1744
1745    /// Mark this node as a drag source. Return `Some(payload)` to start dragging.
1746    pub fn on_drag_start(
1747        mut self,
1748        f: impl Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload> + 'static,
1749    ) -> Self {
1750        self.on_drag_start = Some(Rc::new(f));
1751        self
1752    }
1753
1754    /// Called when a drag ends (drop accepted or canceled/ignored).
1755    pub fn on_drag_end(mut self, f: impl Fn(crate::dnd::DragEnd) + 'static) -> Self {
1756        self.on_drag_end = Some(Rc::new(f));
1757        self
1758    }
1759
1760    /// Called when a drag first enters this target.
1761    pub fn on_drag_enter(mut self, f: impl Fn(crate::dnd::DragOver) + 'static) -> Self {
1762        self.on_drag_enter = Some(Rc::new(f));
1763        self
1764    }
1765
1766    /// Called on every pointer move while a drag is over this target.
1767    pub fn on_drag_over(mut self, f: impl Fn(crate::dnd::DragOver) + 'static) -> Self {
1768        self.on_drag_over = Some(Rc::new(f));
1769        self
1770    }
1771
1772    /// Called when a drag leaves this target.
1773    pub fn on_drag_leave(mut self, f: impl Fn(crate::dnd::DragOver) + 'static) -> Self {
1774        self.on_drag_leave = Some(Rc::new(f));
1775        self
1776    }
1777
1778    /// Called on pointer release while a drag is over this target.
1779    /// Return `true` to accept the drop.
1780    pub fn on_drop(mut self, f: impl Fn(crate::dnd::DropEvent) -> bool + 'static) -> Self {
1781        self.on_drop = Some(Rc::new(f));
1782        self
1783    }
1784
1785    /// Custom drag preview decoration (Compose `drawDragDecoration`).
1786    ///
1787    /// Called every frame while this node is the active drag source.
1788    /// Coordinates are screen px; see [`crate::dnd::DragPreviewCtx`].
1789    pub fn draw_drag_decoration(
1790        mut self,
1791        f: impl Fn(&mut crate::Scene, &crate::dnd::DragPreviewCtx) + 'static,
1792    ) -> Self {
1793        self.drag_preview = Some(Rc::new(f));
1794        self
1795    }
1796
1797    /// Same as [`Self::draw_drag_decoration`] but takes an existing [`crate::dnd::DragPreview`] Rc.
1798    pub fn draw_drag_decoration_rc(mut self, preview: crate::dnd::DragPreview) -> Self {
1799        self.drag_preview = Some(preview);
1800        self
1801    }
1802
1803    /// Convenience: floating label chip as the drag preview.
1804    pub fn drag_preview_label(self, label: impl Into<String>, accent: crate::Color) -> Self {
1805        self.draw_drag_decoration_rc(crate::dnd::drag_preview_label(label, accent))
1806    }
1807
1808    /// Convenience: elevated chip preview.
1809    pub fn drag_preview_chip(self, label: impl Into<String>, accent: crate::Color) -> Self {
1810        self.draw_drag_decoration_rc(crate::dnd::drag_preview_chip(label, accent))
1811    }
1812
1813    /// Set the cursor icon hint for desktop/web runners.
1814    pub fn cursor(mut self, c: crate::CursorIcon) -> Self {
1815        self.cursor = Some(c);
1816        self
1817    }
1818
1819    /// Animate size changes smoothly when the content's natural size changes.
1820    /// Uses the provided `AnimationSpec` for the transition.
1821    /// The content will be clipped to the animated size during transitions.
1822    pub fn animate_content_size(mut self, spec: AnimationSpec) -> Self {
1823        self.animate_content_size = Some(spec);
1824        self
1825    }
1826
1827    /// Attach a `FocusRequester` to this view. The requester will be associated
1828    /// with the view's focusable element, allowing programmatic focus requests.
1829    pub fn focus_requester(mut self, fr: crate::runtime::FocusRequester) -> Self {
1830        self.focus_requester = Some(fr);
1831        self
1832    }
1833
1834    /// Make this composable a focus target (`.focusable(true)`).
1835    /// Corresponds to Compose's `Modifier.focusTarget()`.
1836    pub fn focus_target(mut self) -> Self {
1837        self.focusable = Some(true);
1838        self
1839    }
1840
1841    /// Register a callback that fires when this view gains or loses keyboard focus.
1842    /// The argument is `true` when the view receives focus, `false` when it loses it.
1843    pub fn on_focus_changed(mut self, f: impl Fn(bool) + 'static) -> Self {
1844        self.on_focus_changed = Some(Rc::new(f));
1845        self
1846    }
1847
1848    /// Called after layout when this element's position changes.
1849    /// The callback receives the element's rect in dp (device-independent pixels).
1850    /// Fires whenever the rect changes, including on the initial layout.
1851    pub fn on_globally_positioned(mut self, f: impl Fn(crate::Rect) + 'static) -> Self {
1852        self.on_globally_positioned = Some(Rc::new(f));
1853        self
1854    }
1855
1856    /// Called after layout when this element's size changes.
1857    /// Provides the new (width, height) in dp.
1858    pub fn on_size_changed(mut self, f: impl Fn(crate::Vec2) + 'static) -> Self {
1859        self.on_size_changed = Some(Rc::new(f));
1860        self
1861    }
1862
1863    /// Called when a key event is received while this element is focused.
1864    /// Return `true` to indicate the event was consumed and should not
1865    /// propagate further (e.g. to text input handling or shortcut dispatch).
1866    pub fn on_key_event(mut self, f: impl Fn(crate::input::KeyEvent) -> bool + 'static) -> Self {
1867        self.on_key_event = Some(Rc::new(f));
1868        self
1869    }
1870
1871    /// Preview variant of `on_key_event`. Called before `on_key_event`;
1872    /// if the preview handler returns `true`, the event is consumed
1873    /// and `on_key_event` is NOT called.
1874    pub fn on_preview_key_event(
1875        mut self,
1876        f: impl Fn(crate::input::KeyEvent) -> bool + 'static,
1877    ) -> Self {
1878        self.on_preview_key_event = Some(Rc::new(f));
1879        self
1880    }
1881
1882    /// Apply a gaussian blur to this element's rendered content.
1883    /// `radius_dp` is the uniform blur radius in device-independent pixels.
1884    /// Larger values produce a stronger blur.
1885    /// Uses `Rectangle` edge treatment (clip to bounds).
1886    ///
1887    /// Requires `graphics_layer` to be enabled (set automatically if not).
1888    pub fn blur(mut self, radius_dp: f32) -> Self {
1889        self.blur = Some(BlurStyle {
1890            radius_x: radius_dp.max(0.0),
1891            radius_y: radius_dp.max(0.0),
1892            edge_treatment: BlurredEdgeTreatment::Rectangle,
1893        });
1894        self
1895    }
1896
1897    /// Apply a gaussian blur with separate horizontal/vertical radii.
1898    /// `edge_treatment` controls how edge pixels are handled.
1899    ///
1900    /// Requires `graphics_layer` to be enabled (set automatically if not).
1901    pub fn blur_with_edge(
1902        mut self,
1903        radius_x: f32,
1904        radius_y: f32,
1905        edge_treatment: BlurredEdgeTreatment,
1906    ) -> Self {
1907        self.blur = Some(BlurStyle {
1908            radius_x: radius_x.max(0.0),
1909            radius_y: radius_y.max(0.0),
1910            edge_treatment,
1911        });
1912        self
1913    }
1914
1915    /// Override this element's measured size with a custom callback.
1916    /// The callback receives `LayoutConstraints` (min/max width/height in dp),
1917    /// where `max_width`/`max_height` may be `f32::INFINITY` if unbounded.
1918    /// Returns `(width, height)` for this element.
1919    ///
1920    /// Child placement is handled by the parent layout (same as Compose's
1921    /// `Modifier.size` family).
1922    pub fn layout(mut self, f: impl Fn(LayoutConstraints) -> (f32, f32) + 'static) -> Self {
1923        self.layout = Some(Rc::new(f));
1924        self
1925    }
1926
1927    /// Mark this Box as a text input field with the given configuration.
1928    pub fn text_input(mut self, config: TextInputConfig) -> Self {
1929        self.text_input = Some(config);
1930        self
1931    }
1932
1933    /// Attach an indication (ripple/highlight) factory for visual feedback.
1934    /// The factory is paired with an `InteractionSource` (via `.interaction_source(...)`)
1935    /// to draw press/hover/focus visual feedback.
1936    pub fn indication(mut self, factory: Rc<dyn IndicationNodeFactory>) -> Self {
1937        self.indication = Some(factory);
1938        self
1939    }
1940}