Skip to main content

repose_core/
modifier.rs

1use std::rc::Rc;
2
3use taffy::{AlignContent, AlignItems, AlignSelf, FlexDirection, FlexWrap, JustifyContent};
4
5use crate::animation::AnimationSpec;
6use crate::{Brush, Color, PointerEvent, Size, Transform, Vec2};
7
8/// State-driven colors for interactive components.
9/// The layout engine selects the appropriate color based on hover/press/disabled state
10/// and animates transitions between them.
11#[derive(Clone, Copy, Debug)]
12pub struct StateColors {
13    pub default: Color,
14    pub hovered: Color,
15    pub pressed: Color,
16    pub disabled: Color,
17}
18
19/// State-driven elevation for interactive components.
20#[derive(Clone, Copy, Debug)]
21pub struct StateElevation {
22    pub default: f32,
23    pub hovered: f32,
24    pub pressed: f32,
25    pub disabled: f32,
26}
27
28macro_rules! merge_opts {
29    ($dst:ident, $src:ident; $($f:ident),+ $(,)?) => {
30        $( $dst.$f = $src.$f.or($dst.$f); )+
31    };
32}
33macro_rules! merge_flags {
34    ($dst:ident, $src:ident; $($f:ident),+ $(,)?) => {
35        $( $dst.$f |= $src.$f; )+
36    };
37}
38
39macro_rules! impl_option_fields {
40    ($ty:ty, $fn:ident) => {
41        impl $ty {
42            $fn!(replace);
43        }
44    };
45    ($ty:ident) => {
46        impl $ty {
47            /// Chain another modifier's settings onto this one.
48            /// Useful for creating reusable modifier templates.
49            pub fn then(mut self, other: Self) -> Self {
50                merge_opts!(self, other;
51                    key, size, width, height,
52                    padding, padding_values,
53                    min_width, min_height, max_width, max_height,
54                    background, state_colors, state_elevation, border,
55                    flex_grow, flex_shrink, flex_basis, flex_wrap, flex_dir,
56                    gap, row_gap, column_gap,
57                    align_self, justify_content, align_items_container, align_content,
58                    clip_rounded, render_z_index,
59                    on_scroll,
60                    on_pointer_down, on_pointer_move, on_pointer_up,
61                    on_pointer_enter, on_pointer_leave,
62                    semantics, alpha, transform,
63                    grid, grid_col_span, grid_row_span,
64                    position_type,
65                    offset_left, offset_right, offset_top, offset_bottom,
66                    margin_left, margin_right, margin_top, margin_bottom,
67                    aspect_ratio, painter,
68                    on_drag_start, on_drag_end, on_drag_enter, on_drag_over, on_drag_leave, on_drop,
69                    on_action, cursor, animate_content_size, focus_requester, on_focus_changed,
70                );
71                merge_flags!(self, other;
72                    fill_max, fill_max_w, fill_max_h,
73                    hit_passthrough, input_blocker, repaint_boundary, click, disabled,
74                );
75                if other.z_index != 0.0 {
76                    self.z_index = other.z_index;
77                }
78                self
79            }
80        }
81    };
82}
83
84#[derive(Clone, Debug)]
85pub struct Border {
86    pub width: f32,
87    pub color: Color,
88    pub radius: f32,
89}
90
91#[derive(Clone, Copy, Debug, Default)]
92pub struct PaddingValues {
93    pub left: f32,
94    pub right: f32,
95    pub top: f32,
96    pub bottom: f32,
97}
98
99#[derive(Clone, Debug)]
100pub struct GridConfig {
101    pub columns: usize,
102    pub row_gap: f32,
103    pub column_gap: f32,
104}
105
106/// Drop-shadow parameters applied to a graphics layer.
107///
108/// `blur_radius` is the Gaussian blur radius in dp (1.0 = subtle, 8.0 = soft,
109/// 16.0 = very diffuse). `offset_y` is the vertical offset of the shadow in dp
110/// (positive = below the layer). `color` is the shadow color (premultiplied
111/// alpha controls shadow darkness).
112#[derive(Clone, Copy, Debug)]
113pub struct ShadowSpec {
114    pub blur_radius: f32,
115    pub offset_y: f32,
116    pub color: Color,
117}
118
119#[derive(Clone, Copy, Debug)]
120#[non_exhaustive]
121pub enum PositionType {
122    Relative,
123    Absolute,
124}
125
126#[derive(Clone, Default)]
127pub struct Modifier {
128    /// Optional stable identity key for this view node.
129    ///
130    /// If set, `layout_and_paint` will prefer this over child index when assigning stable ViewIds.
131    /// This is the “escape hatch” for dynamic lists / conditional UI where index-based identity
132    /// would otherwise shift.
133    pub key: Option<u64>,
134
135    pub size: Option<Size>,
136    pub width: Option<f32>,
137    pub height: Option<f32>,
138    pub fill_max: bool,
139    pub fill_max_w: bool,
140    pub fill_max_h: bool,
141    pub padding: Option<f32>,
142    pub padding_values: Option<PaddingValues>,
143    pub min_width: Option<f32>,
144    pub min_height: Option<f32>,
145    pub max_width: Option<f32>,
146    pub max_height: Option<f32>,
147    pub background: Option<Brush>,
148    pub state_colors: Option<StateColors>,
149    pub state_elevation: Option<StateElevation>,
150
151    pub border: Option<Border>,
152    pub flex_grow: Option<f32>,
153    pub flex_shrink: Option<f32>,
154    pub flex_basis: Option<f32>,
155    pub flex_wrap: Option<FlexWrap>,
156    pub flex_dir: Option<FlexDirection>,
157    pub gap: Option<f32>,
158    pub row_gap: Option<f32>,
159    pub column_gap: Option<f32>,
160    pub align_self: Option<AlignSelf>,
161    pub justify_content: Option<JustifyContent>,
162    pub align_items_container: Option<AlignItems>,
163    pub align_content: Option<AlignContent>,
164    pub clip_rounded: Option<f32>,
165    /// Z-index for hit-testing order (higher = receives events first).
166    pub z_index: f32,
167    /// Z-index for render order (higher = painted on top). If None, uses tree order.
168    pub render_z_index: Option<f32>,
169    /// If true, this view does not create hit regions.
170    pub hit_passthrough: bool,
171    /// If true, this view blocks pointer/touch input for hits below it.
172    pub input_blocker: bool,
173    pub repaint_boundary: bool,
174    pub click: bool,
175    /// When true, the component ignores pointer events and appears disabled.
176    pub disabled: bool,
177    pub on_scroll: Option<Rc<dyn Fn(Vec2) -> Vec2>>,
178    pub on_pointer_down: Option<Rc<dyn Fn(PointerEvent)>>,
179    pub on_pointer_move: Option<Rc<dyn Fn(PointerEvent)>>,
180    pub on_pointer_up: Option<Rc<dyn Fn(PointerEvent)>>,
181    pub on_pointer_enter: Option<Rc<dyn Fn(PointerEvent)>>,
182    pub on_pointer_leave: Option<Rc<dyn Fn(PointerEvent)>>,
183    pub semantics: Option<crate::Semantics>,
184    pub alpha: Option<f32>,
185    pub graphics_layer: Option<f32>,
186    pub shadow: Option<ShadowSpec>,
187    pub transform: Option<Transform>,
188    pub grid: Option<GridConfig>,
189    pub grid_col_span: Option<u16>,
190    pub grid_row_span: Option<u16>,
191    pub position_type: Option<PositionType>,
192    pub offset_left: Option<f32>,
193    pub offset_right: Option<f32>,
194    pub offset_top: Option<f32>,
195    pub offset_bottom: Option<f32>,
196    pub margin_left: Option<f32>,
197    pub margin_right: Option<f32>,
198    pub margin_top: Option<f32>,
199    pub margin_bottom: Option<f32>,
200    pub aspect_ratio: Option<f32>,
201    pub painter: Option<Rc<dyn Fn(&mut crate::Scene, crate::Rect)>>,
202
203    // Drag-drop (internal)
204    pub on_drag_start: Option<Rc<dyn Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload>>>,
205    pub on_drag_end: Option<Rc<dyn Fn(crate::dnd::DragEnd)>>,
206    pub on_drag_enter: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
207    pub on_drag_over: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
208    pub on_drag_leave: Option<Rc<dyn Fn(crate::dnd::DragOver)>>,
209    pub on_drop: Option<Rc<dyn Fn(crate::dnd::DropEvent) -> bool>>,
210
211    pub on_action: Option<Rc<dyn Fn(crate::shortcuts::Action) -> bool>>,
212
213    /// Cursor icon hint for desktop/web runners.
214    pub cursor: Option<crate::CursorIcon>,
215
216    /// If set, the size of this node will smoothly animate to its target size
217    /// whenever content size changes. Uses the provided animation spec.
218    pub animate_content_size: Option<AnimationSpec>,
219
220    /// A `FocusRequester` handle that will be associated with this view.
221    /// When the requester's `request_focus()` is called, keyboard focus will
222    /// move to this view.
223    pub focus_requester: Option<crate::runtime::FocusRequester>,
224
225    /// Called when this view gains or loses focus. The boolean parameter is
226    /// `true` when focused, `false` when unfocused.
227    pub on_focus_changed: Option<Rc<dyn Fn(bool)>>,
228}
229
230impl std::fmt::Debug for Modifier {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        let mut s = f.debug_struct("Modifier");
233
234        macro_rules! opt_val {
235            ($($name:ident),+ $(,)?) => {
236                $( if self.$name.is_some() { s.field(stringify!($name), &self.$name); } )+
237            };
238        }
239        opt_val!(
240            key,
241            size,
242            width,
243            height,
244            padding,
245            padding_values,
246            min_width,
247            min_height,
248            max_width,
249            max_height,
250            background,
251            state_colors,
252            state_elevation,
253            border,
254            flex_grow,
255            flex_shrink,
256            flex_basis,
257            flex_wrap,
258            flex_dir,
259            gap,
260            row_gap,
261            column_gap,
262            align_self,
263            justify_content,
264            align_items_container,
265            align_content,
266            clip_rounded,
267            render_z_index,
268            semantics,
269            alpha,
270            transform,
271            grid,
272            grid_col_span,
273            grid_row_span,
274            position_type,
275            offset_left,
276            offset_right,
277            offset_top,
278            offset_bottom,
279            margin_left,
280            margin_right,
281            margin_top,
282            margin_bottom,
283            aspect_ratio,
284            cursor,
285            animate_content_size,
286        );
287
288        macro_rules! opt_cb {
289            ($($name:ident),+ $(,)?) => {
290                $( if self.$name.is_some() { s.field(stringify!($name), &"…"); } )+
291            };
292        }
293        opt_cb!(
294            on_scroll,
295            on_pointer_down,
296            on_pointer_move,
297            on_pointer_up,
298            on_pointer_enter,
299            on_pointer_leave,
300            painter,
301            on_drag_start,
302            on_drag_end,
303            on_drag_enter,
304            on_drag_over,
305            on_drag_leave,
306            on_drop,
307            on_action,
308            on_focus_changed,
309        );
310
311        macro_rules! flag {
312            ($($name:ident),+ $(,)?) => {
313                $( if self.$name { s.field(stringify!($name), &true); } )+
314            };
315        }
316        flag!(
317            fill_max,
318            fill_max_w,
319            fill_max_h,
320            hit_passthrough,
321            input_blocker,
322            repaint_boundary,
323            click,
324            disabled,
325        );
326
327        if self.z_index != 0.0 {
328            s.field("z_index", &self.z_index);
329        }
330
331        s.finish()
332    }
333}
334
335impl_option_fields!(Modifier);
336
337impl Modifier {
338    pub fn new() -> Self {
339        Self::default()
340    }
341
342    /// Attaches a stable identity key to this view node.
343    /// Use for dynamic lists / conditional UI where index-based identity can shift.
344    pub fn key(mut self, key: u64) -> Self {
345        self.key = Some(key);
346        self
347    }
348
349    pub fn size(mut self, w: f32, h: f32) -> Self {
350        self.size = Some(Size {
351            width: w,
352            height: h,
353        });
354        self
355    }
356    pub fn width(mut self, w: f32) -> Self {
357        self.width = Some(w);
358        self
359    }
360    pub fn height(mut self, h: f32) -> Self {
361        self.height = Some(h);
362        self
363    }
364    pub fn fill_max_size(mut self) -> Self {
365        self.fill_max = true;
366        self
367    }
368    pub fn fill_max_width(mut self) -> Self {
369        self.fill_max_w = true;
370        self
371    }
372    pub fn fill_max_height(mut self) -> Self {
373        self.fill_max_h = true;
374        self
375    }
376    pub fn padding(mut self, v: f32) -> Self {
377        self.padding = Some(v);
378        self
379    }
380    pub fn padding_values(mut self, padding: PaddingValues) -> Self {
381        self.padding_values = Some(padding);
382        self
383    }
384    /// Add padding equal to the current IME (soft keyboard) bottom inset.
385    /// Combine with `system_bars_padding()` to handle both system bars and keyboard.
386    pub fn ime_padding(mut self) -> Self {
387        let insets = crate::locals::window_insets();
388        let scale = crate::locals::density().scale * crate::locals::ui_scale().0;
389        let mut p = self.padding_values.unwrap_or_default();
390        p.bottom += insets.ime_bottom / scale;
391        self.padding_values = Some(p);
392        self
393    }
394    /// Add padding equal to the current system bar insets (status bar top, nav bar bottom).
395    pub fn system_bars_padding(mut self) -> Self {
396        let insets = crate::locals::window_insets();
397        let scale = crate::locals::density().scale * crate::locals::ui_scale().0;
398        let mut p = self.padding_values.unwrap_or_default();
399        p.top += insets.top / scale;
400        p.bottom += insets.bottom / scale;
401        self.padding_values = Some(p);
402        self
403    }
404    /// Add status bar inset as top padding.
405    pub fn status_bars_padding(mut self) -> Self {
406        let insets = crate::locals::window_insets();
407        let scale = crate::locals::density().scale * crate::locals::ui_scale().0;
408        let mut p = self.padding_values.unwrap_or_default();
409        p.top += insets.top / scale;
410        self.padding_values = Some(p);
411        self
412    }
413    /// Add navigation bar inset as bottom padding.
414    pub fn navigation_bars_padding(mut self) -> Self {
415        let insets = crate::locals::window_insets();
416        let scale = crate::locals::density().scale * crate::locals::ui_scale().0;
417        let mut p = self.padding_values.unwrap_or_default();
418        p.bottom += insets.bottom / scale;
419        self.padding_values = Some(p);
420        self
421    }
422    pub fn min_size(mut self, w: f32, h: f32) -> Self {
423        self.min_width = Some(w);
424        self.min_height = Some(h);
425        self
426    }
427    pub fn max_size(mut self, w: f32, h: f32) -> Self {
428        self.max_width = Some(w);
429        self.max_height = Some(h);
430        self
431    }
432    pub fn min_width(mut self, w: f32) -> Self {
433        self.min_width = Some(w);
434        self
435    }
436    pub fn min_height(mut self, h: f32) -> Self {
437        self.min_height = Some(h);
438        self
439    }
440    pub fn max_width(mut self, w: f32) -> Self {
441        self.max_width = Some(w);
442        self
443    }
444    pub fn max_height(mut self, h: f32) -> Self {
445        self.max_height = Some(h);
446        self
447    }
448    /// Set a solid color background.
449    pub fn background(mut self, color: Color) -> Self {
450        self.background = Some(Brush::Solid(color));
451        self
452    }
453    /// Set a brush (solid, gradient, etc.) background.
454    pub fn background_brush(mut self, brush: Brush) -> Self {
455        self.background = Some(brush);
456        self
457    }
458    pub fn border(mut self, width: f32, color: Color, radius: f32) -> Self {
459        self.border = Some(Border {
460            width,
461            color,
462            radius,
463        });
464        self
465    }
466    pub fn flex_grow(mut self, v: f32) -> Self {
467        self.flex_grow = Some(v);
468        self
469    }
470    pub fn flex_shrink(mut self, v: f32) -> Self {
471        self.flex_shrink = Some(v);
472        self
473    }
474    pub fn flex_basis(mut self, v: f32) -> Self {
475        self.flex_basis = Some(v);
476        self
477    }
478    pub fn flex_wrap(mut self, w: FlexWrap) -> Self {
479        self.flex_wrap = Some(w);
480        self
481    }
482    pub fn flex_dir(mut self, d: FlexDirection) -> Self {
483        self.flex_dir = Some(d);
484        self
485    }
486    pub fn gap(mut self, v: f32) -> Self {
487        let v = v.max(0.0);
488        self.gap = Some(v);
489        self.row_gap = Some(v);
490        self.column_gap = Some(v);
491        self
492    }
493    pub fn row_gap(mut self, v: f32) -> Self {
494        self.row_gap = Some(v.max(0.0));
495        self
496    }
497    pub fn column_gap(mut self, v: f32) -> Self {
498        self.column_gap = Some(v.max(0.0));
499        self
500    }
501    pub fn align_self(mut self, a: AlignSelf) -> Self {
502        self.align_self = Some(a);
503        self
504    }
505    pub fn align_self_center(mut self) -> Self {
506        self.align_self = Some(AlignSelf::Center);
507        self
508    }
509    pub fn justify_content(mut self, j: JustifyContent) -> Self {
510        self.justify_content = Some(j);
511        self
512    }
513    pub fn align_items(mut self, a: AlignItems) -> Self {
514        self.align_items_container = Some(a);
515        self
516    }
517    pub fn align_content(mut self, a: AlignContent) -> Self {
518        self.align_content = Some(a);
519        self
520    }
521    pub fn clip_rounded(mut self, radius: f32) -> Self {
522        self.clip_rounded = Some(radius);
523        self
524    }
525    pub fn z_index(mut self, z: f32) -> Self {
526        self.z_index = z;
527        self
528    }
529
530    /// Sets the render z-index for this view. Higher values are painted on top.
531    /// Unlike `z_index` (which only affects hit-testing), this affects visual layering.
532    pub fn render_z_index(mut self, z: f32) -> Self {
533        self.render_z_index = Some(z);
534        self
535    }
536
537    /// Prevent pointer/touch from reaching lower layers.
538    pub fn input_blocker(mut self) -> Self {
539        self.input_blocker = true;
540        self
541    }
542
543    pub fn hit_passthrough(mut self) -> Self {
544        self.hit_passthrough = true;
545        self
546    }
547    pub fn clickable(mut self) -> Self {
548        self.click = true;
549        self
550    }
551    /// Set state-driven background colors for hover, press, disabled states.
552    /// The layout engine automatically selects and animates between these based on interaction.
553    pub fn state_colors(mut self, colors: StateColors) -> Self {
554        self.state_colors = Some(colors);
555        self
556    }
557    /// Set state-driven elevation values for hover, press, disabled states.
558    pub fn state_elevation(mut self, elev: StateElevation) -> Self {
559        self.state_elevation = Some(elev);
560        self
561    }
562    /// Mark this component as disabled - it won't respond to pointer events.
563    pub fn disabled(mut self) -> Self {
564        self.disabled = true;
565        self
566    }
567    /// Mark this component as enabled or disabled.
568    pub fn enabled(mut self, enabled: bool) -> Self {
569        self.disabled = !enabled;
570        self
571    }
572    pub fn on_scroll(mut self, f: impl Fn(Vec2) -> Vec2 + 'static) -> Self {
573        self.on_scroll = Some(Rc::new(f));
574        self
575    }
576    pub fn on_pointer_down(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
577        self.on_pointer_down = Some(Rc::new(f));
578        self
579    }
580    pub fn on_pointer_move(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
581        self.on_pointer_move = Some(Rc::new(f));
582        self
583    }
584    pub fn on_pointer_up(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
585        self.on_pointer_up = Some(Rc::new(f));
586        self
587    }
588    pub fn on_pointer_enter(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
589        self.on_pointer_enter = Some(Rc::new(f));
590        self
591    }
592    pub fn on_pointer_leave(mut self, f: impl Fn(PointerEvent) + 'static) -> Self {
593        self.on_pointer_leave = Some(Rc::new(f));
594        self
595    }
596    pub fn semantics(mut self, s: crate::Semantics) -> Self {
597        self.semantics = Some(s);
598        self
599    }
600    pub fn alpha(mut self, a: f32) -> Self {
601        self.alpha = Some(a);
602        self
603    }
604    /// Render this subtree into an offscreen texture, then composite it
605    /// back into the parent with the given group `alpha` (0.0..=1.0).
606    /// Allows correct blending when children overlap inside the layer, and
607    /// sets up the architecture for future layer effects (shadow, blur, clip).
608    pub fn graphics_layer(mut self, alpha: f32) -> Self {
609        self.graphics_layer = Some(alpha.clamp(0.0, 1.0));
610        self
611    }
612    /// Drop shadow with the given `blur_radius` (dp) and vertical `offset_y` (dp).
613    /// The shadow color defaults to black with alpha 64 (~25%). Combines with
614    /// [`Modifier::graphics_layer`] to draw a shadow underneath the layer.
615    pub fn shadow(mut self, blur_radius: f32, offset_y: f32) -> Self {
616        self.shadow = Some(ShadowSpec {
617            blur_radius: blur_radius.max(0.0),
618            offset_y,
619            color: Color(0, 0, 0, 64),
620        });
621        self
622    }
623    /// Drop shadow with a custom color. Alpha 0..=255.
624    pub fn shadow_with_color(mut self, blur_radius: f32, offset_y: f32, color: Color) -> Self {
625        self.shadow = Some(ShadowSpec {
626            blur_radius: blur_radius.max(0.0),
627            offset_y,
628            color,
629        });
630        self
631    }
632    /// Material-style elevation. Auto-scales blur and offset by `level` (dp)
633    /// and uses a default shadow color. Level 0 = no shadow; 4 = subtle;
634    /// 16 = strong. Requires [`Modifier::graphics_layer`] to take effect.
635    pub fn elevation(mut self, level: f32) -> Self {
636        if level <= 0.0 {
637            self.shadow = None;
638            return self;
639        }
640        self.shadow = Some(ShadowSpec {
641            blur_radius: level * 2.0,
642            offset_y: level * 0.5,
643            color: Color(0, 0, 0, (level * 8.0).clamp(8.0, 80.0) as u8),
644        });
645        self
646    }
647    pub fn transform(mut self, t: Transform) -> Self {
648        self.transform = Some(t);
649        self
650    }
651    pub fn grid(mut self, columns: usize, row_gap: f32, column_gap: f32) -> Self {
652        self.grid = Some(GridConfig {
653            columns,
654            row_gap,
655            column_gap,
656        });
657        self
658    }
659    pub fn grid_span(mut self, col_span: u16, row_span: u16) -> Self {
660        self.grid_col_span = Some(col_span);
661        self.grid_row_span = Some(row_span);
662        self
663    }
664    pub fn absolute(mut self) -> Self {
665        self.position_type = Some(PositionType::Absolute);
666        self
667    }
668    pub fn offset(
669        mut self,
670        left: Option<f32>,
671        top: Option<f32>,
672        right: Option<f32>,
673        bottom: Option<f32>,
674    ) -> Self {
675        self.offset_left = left;
676        self.offset_top = top;
677        self.offset_right = right;
678        self.offset_bottom = bottom;
679        self
680    }
681    pub fn offset_left(mut self, v: f32) -> Self {
682        self.offset_left = Some(v);
683        self
684    }
685    pub fn offset_right(mut self, v: f32) -> Self {
686        self.offset_right = Some(v);
687        self
688    }
689    pub fn offset_top(mut self, v: f32) -> Self {
690        self.offset_top = Some(v);
691        self
692    }
693    pub fn offset_bottom(mut self, v: f32) -> Self {
694        self.offset_bottom = Some(v);
695        self
696    }
697
698    pub fn margin(mut self, v: f32) -> Self {
699        self.margin_left = Some(v);
700        self.margin_right = Some(v);
701        self.margin_top = Some(v);
702        self.margin_bottom = Some(v);
703        self
704    }
705
706    pub fn margin_horizontal(mut self, v: f32) -> Self {
707        self.margin_left = Some(v);
708        self.margin_right = Some(v);
709        self
710    }
711
712    pub fn margin_vertical(mut self, v: f32) -> Self {
713        self.margin_top = Some(v);
714        self.margin_bottom = Some(v);
715        self
716    }
717    pub fn aspect_ratio(mut self, ratio: f32) -> Self {
718        self.aspect_ratio = Some(ratio);
719        self
720    }
721    pub fn painter(mut self, f: impl Fn(&mut crate::Scene, crate::Rect) + 'static) -> Self {
722        self.painter = Some(Rc::new(f));
723        self
724    }
725    pub fn scale(self, s: f32) -> Self {
726        self.scale2(s, s)
727    }
728    pub fn scale2(mut self, sx: f32, sy: f32) -> Self {
729        let mut t = self.transform.unwrap_or_else(Transform::identity);
730        t.scale_x *= sx;
731        t.scale_y *= sy;
732        self.transform = Some(t);
733        self
734    }
735    pub fn translate(mut self, x: f32, y: f32) -> Self {
736        let t = self.transform.unwrap_or_else(Transform::identity);
737        self.transform = Some(t.combine(&Transform::translate(x, y)));
738        self
739    }
740    pub fn translate_vec2(self, v: Vec2) -> Self {
741        self.translate(v.x, v.y)
742    }
743    pub fn rotate(mut self, radians: f32) -> Self {
744        let mut t = self.transform.unwrap_or_else(Transform::identity);
745        t.rotate += radians;
746        self.transform = Some(t);
747        self
748    }
749    pub fn weight(mut self, w: f32) -> Self {
750        let w = w.max(0.0);
751        self.flex_grow = Some(w);
752        self.flex_shrink = Some(1.0);
753        // dp units; 0 is fine.
754        self.flex_basis = Some(0.0);
755        self
756    }
757    /// Marks this view as a repaint boundary candidate.
758    ///
759    /// The engine may cache its painted output.
760    pub fn repaint_boundary(mut self) -> Self {
761        self.repaint_boundary = true;
762        self
763    }
764    pub fn on_action(mut self, f: impl Fn(crate::shortcuts::Action) -> bool + 'static) -> Self {
765        self.on_action = Some(Rc::new(f));
766        self
767    }
768
769    /// Mark this node as a drag source. Return `Some(payload)` to start dragging.
770    pub fn on_drag_start(
771        mut self,
772        f: impl Fn(crate::dnd::DragStart) -> Option<crate::dnd::DragPayload> + 'static,
773    ) -> Self {
774        self.on_drag_start = Some(Rc::new(f));
775        self
776    }
777
778    /// Called when a drag ends (drop accepted or canceled/ignored).
779    pub fn on_drag_end(mut self, f: impl Fn(crate::dnd::DragEnd) + 'static) -> Self {
780        self.on_drag_end = Some(Rc::new(f));
781        self
782    }
783
784    /// Called when a drag first enters this target.
785    pub fn on_drag_enter(mut self, f: impl Fn(crate::dnd::DragOver) + 'static) -> Self {
786        self.on_drag_enter = Some(Rc::new(f));
787        self
788    }
789
790    /// Called on every pointer move while a drag is over this target.
791    pub fn on_drag_over(mut self, f: impl Fn(crate::dnd::DragOver) + 'static) -> Self {
792        self.on_drag_over = Some(Rc::new(f));
793        self
794    }
795
796    /// Called when a drag leaves this target.
797    pub fn on_drag_leave(mut self, f: impl Fn(crate::dnd::DragOver) + 'static) -> Self {
798        self.on_drag_leave = Some(Rc::new(f));
799        self
800    }
801
802    /// Called on pointer release while a drag is over this target.
803    /// Return `true` to accept the drop.
804    pub fn on_drop(mut self, f: impl Fn(crate::dnd::DropEvent) -> bool + 'static) -> Self {
805        self.on_drop = Some(Rc::new(f));
806        self
807    }
808
809    /// Set the cursor icon hint for desktop/web runners.
810    pub fn cursor(mut self, c: crate::CursorIcon) -> Self {
811        self.cursor = Some(c);
812        self
813    }
814
815    /// Animate size changes smoothly when the content's natural size changes.
816    /// Uses the provided `AnimationSpec` for the transition.
817    /// The content will be clipped to the animated size during transitions.
818    pub fn animate_content_size(mut self, spec: AnimationSpec) -> Self {
819        self.animate_content_size = Some(spec);
820        self
821    }
822
823    /// Attach a `FocusRequester` to this view. The requester will be associated
824    /// with the view's focusable element, allowing programmatic focus requests.
825    pub fn focus_requester(mut self, fr: crate::runtime::FocusRequester) -> Self {
826        self.focus_requester = Some(fr);
827        self
828    }
829
830    /// Register a callback that fires when this view gains or loses keyboard focus.
831    /// The argument is `true` when the view receives focus, `false` when it loses it.
832    pub fn on_focus_changed(mut self, f: impl Fn(bool) + 'static) -> Self {
833        self.on_focus_changed = Some(Rc::new(f));
834        self
835    }
836}