Skip to main content

repose_core/
modifier.rs

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