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