Skip to main content

repose_core/
modifier.rs

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