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