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