Skip to main content

tessera_components/
modifier.rs

1//! Modifier extensions for basic components.
2//!
3//! ## Usage
4//!
5//! Attach layout and drawing behavior like padding and opacity to any subtree.
6
7#[cfg(test)]
8mod insets_tests;
9mod interaction;
10mod shadow;
11mod visual;
12
13use tessera_foundation::modifier::ModifierExt as FoundationModifierExt;
14use tessera_ui::{
15    AxisConstraint, Callback, CallbackWith, Color, Dp, Modifier, State,
16    modifier::ModifierCapabilityExt as _,
17};
18
19pub use tessera_foundation::modifier::{
20    ClickableArgs, DragDelta, DraggableArgs, InteractionState, InteractionVisualState,
21    MinimumInteractiveComponentEnforcement, Padding, PointerEventContext, SelectableArgs,
22    SemanticsArgs, ToggleableArgs,
23};
24
25pub(crate) use tessera_foundation::modifier::{AlignmentParentData, WeightParentData};
26
27use crate::{
28    alignment::Alignment, scrollable::ScrollableController, shape_def::Shape,
29    theme::MaterialColorScheme,
30};
31
32use interaction::{
33    apply_block_touch_propagation_modifier, apply_clickable_modifier, apply_selectable_modifier,
34    apply_toggleable_modifier, apply_window_drag_region_modifier,
35};
36use visual::{AlphaModifierNode, BackgroundModifierNode, BorderModifierNode, ClipModifierNode};
37
38pub use shadow::ShadowArgs;
39
40pub(crate) use interaction::{
41    DisabledToggleSemantics, apply_combined_clickable_modifier, with_keyboard_input,
42    with_pointer_input,
43};
44
45/// Extensions for composing reusable wrapper behavior around component
46/// subtrees.
47pub trait ModifierExt {
48    /// Pads and consumes the space not already occupied by enclosing inset modifiers.
49    fn window_insets_padding(self, insets: tessera_ui::WindowInsets) -> Modifier;
50
51    /// Marks inset space as occupied for nested padding and size modifiers.
52    fn consume_window_insets(self, insets: tessera_ui::WindowInsets) -> Modifier;
53
54    /// Sets width to the remaining left window inset.
55    fn window_insets_left_width(self, insets: tessera_ui::WindowInsets) -> Modifier;
56
57    /// Sets height to the remaining top window inset.
58    fn window_insets_top_height(self, insets: tessera_ui::WindowInsets) -> Modifier;
59
60    /// Sets width to the remaining right window inset.
61    fn window_insets_right_width(self, insets: tessera_ui::WindowInsets) -> Modifier;
62
63    /// Sets height to the remaining bottom window inset.
64    fn window_insets_bottom_height(self, insets: tessera_ui::WindowInsets) -> Modifier;
65
66    /// Adds padding around the content.
67    fn padding(self, padding: Padding) -> Modifier;
68
69    /// Adds symmetric padding on all edges.
70    fn padding_all(self, padding: Dp) -> Modifier;
71
72    /// Adds symmetric padding for horizontal and vertical edges.
73    fn padding_symmetric(self, horizontal: Dp, vertical: Dp) -> Modifier;
74
75    /// Offsets the content without affecting layout size.
76    fn offset(self, x: Dp, y: Dp) -> Modifier;
77
78    /// Multiplies the opacity of the subtree by `alpha`.
79    fn alpha(self, alpha: f32) -> Modifier;
80
81    /// Clips descendants to this modifier's bounds.
82    fn clip_to_bounds(self) -> Modifier;
83
84    /// Clips this node and its descendants to a shape without resizing content.
85    fn clip_shape(self, shape: Shape) -> Modifier;
86
87    /// Makes this node a vertical scroll viewport driven by `controller`.
88    fn vertical_scroll(self, controller: State<ScrollableController>) -> Modifier;
89
90    /// Makes this node a horizontal scroll viewport driven by `controller`.
91    fn horizontal_scroll(self, controller: State<ScrollableController>) -> Modifier;
92
93    /// Makes this node a scroll viewport on the selected axes.
94    fn scroll(
95        self,
96        controller: State<ScrollableController>,
97        vertical: bool,
98        horizontal: bool,
99    ) -> Modifier;
100
101    /// Draws a background behind the subtree.
102    fn background(self, color: Color) -> Modifier;
103
104    /// Draws a background behind the subtree using a custom shape.
105    fn background_with_shape(self, color: Color, shape: Shape) -> Modifier;
106
107    /// Draws a background scaled around its center while preserving layout and
108    /// input bounds.
109    ///
110    /// Both scale axes must be finite and in `0..=1`. A zero axis draws
111    /// nothing.
112    fn background_with_scaled_shape(self, color: Color, shape: Shape, scale: [f32; 2]) -> Modifier;
113
114    /// Draws a border stroke above the subtree.
115    fn border(self, width: Dp, color: Color) -> Modifier;
116
117    /// Draws a border stroke above the subtree using a custom shape.
118    fn border_with_shape(self, width: Dp, color: Color, shape: Shape) -> Modifier;
119
120    /// Adds a shadow with advanced configuration options.
121    fn shadow(self, scheme: &MaterialColorScheme, args: &ShadowArgs) -> Modifier;
122
123    /// Constrains the content to an exact size when possible.
124    fn size(self, width: Dp, height: Dp) -> Modifier;
125
126    /// Pins the content to an exact size regardless of incoming constraints.
127    fn required_size(self, width: Dp, height: Dp) -> Modifier;
128
129    /// Pins the content width regardless of incoming constraints.
130    fn required_width(self, width: Dp) -> Modifier;
131
132    /// Pins the content height regardless of incoming constraints.
133    fn required_height(self, height: Dp) -> Modifier;
134
135    /// Applies a default minimum size only when the incoming axis leaves the
136    /// minimum open, so an explicit outer size always wins.
137    fn default_min_size(self, min_width: Dp, min_height: Dp) -> Modifier;
138
139    /// Constrains the content to an exact width when possible.
140    fn width(self, width: Dp) -> Modifier;
141
142    /// Constrains the content to an exact height when possible.
143    fn height(self, height: Dp) -> Modifier;
144
145    /// Constrains the content size within optional min/max bounds.
146    fn size_in(
147        self,
148        min_width: Option<Dp>,
149        max_width: Option<Dp>,
150        min_height: Option<Dp>,
151        max_height: Option<Dp>,
152    ) -> Modifier;
153
154    /// Applies explicit width/height interval constraints.
155    fn constrain(self, width: Option<AxisConstraint>, height: Option<AxisConstraint>) -> Modifier;
156
157    /// Fills the available width within parent bounds.
158    fn fill_max_width(self) -> Modifier;
159
160    /// Fills the available height within parent bounds.
161    fn fill_max_height(self) -> Modifier;
162
163    /// Fills the available size within parent bounds.
164    fn fill_max_size(self) -> Modifier;
165
166    /// Enforces a minimum interactive size by expanding and centering content.
167    fn minimum_interactive_component_size(self) -> Modifier;
168
169    /// Provides weighted parent data for row and column layouts.
170    fn weight(self, weight: f32) -> Modifier;
171
172    /// Provides alignment parent data for layered boxed layouts.
173    fn align(self, alignment: Alignment) -> Modifier;
174
175    /// Emits drag deltas for free-form dragging.
176    fn draggable<C>(self, on_drag_delta: C) -> Modifier
177    where
178        C: Into<CallbackWith<DragDelta, ()>>;
179
180    /// Emits drag deltas with custom drag configuration.
181    fn draggable_with(self, args: DraggableArgs) -> Modifier;
182
183    /// Prevents cursor events from propagating to components behind this
184    /// subtree.
185    fn block_touch_propagation(self) -> Modifier;
186
187    /// Attaches accessibility semantics metadata to this subtree.
188    fn semantics(self, args: SemanticsArgs) -> Modifier;
189
190    /// Clears descendant semantics and applies the provided metadata.
191    fn clear_and_set_semantics(self, args: SemanticsArgs) -> Modifier;
192
193    /// Makes the subtree clickable with optional ripple feedback and an
194    /// accessibility click action.
195    fn clickable<C>(self, on_click: C) -> Modifier
196    where
197        C: Into<Callback>;
198
199    /// Makes the subtree clickable with advanced configuration options.
200    fn clickable_with(self, args: ClickableArgs) -> Modifier;
201
202    /// Makes the subtree toggleable with optional ripple/state-layer feedback.
203    fn toggleable<C>(self, value: bool, on_value_change: C) -> Modifier
204    where
205        C: Into<CallbackWith<bool, ()>>;
206
207    /// Makes the subtree toggleable with advanced configuration options.
208    fn toggleable_with(self, args: ToggleableArgs) -> Modifier;
209
210    /// Makes the subtree selectable with optional ripple/state-layer feedback.
211    fn selectable<C>(self, selected: bool, on_click: C) -> Modifier
212    where
213        C: Into<Callback>;
214
215    /// Makes the subtree selectable with advanced configuration options.
216    fn selectable_with(self, args: SelectableArgs) -> Modifier;
217
218    /// Marks this subtree as a draggable window region. On macOS, double-clicking
219    /// the region follows the system title bar preference.
220    fn window_drag_region(self) -> Modifier;
221}
222
223impl ModifierExt for Modifier {
224    fn window_insets_padding(self, insets: tessera_ui::WindowInsets) -> Modifier {
225        FoundationModifierExt::window_insets_padding(self, insets)
226    }
227    fn consume_window_insets(self, insets: tessera_ui::WindowInsets) -> Modifier {
228        FoundationModifierExt::consume_window_insets(self, insets)
229    }
230    fn window_insets_left_width(self, insets: tessera_ui::WindowInsets) -> Modifier {
231        FoundationModifierExt::window_insets_left_width(self, insets)
232    }
233    fn window_insets_top_height(self, insets: tessera_ui::WindowInsets) -> Modifier {
234        FoundationModifierExt::window_insets_top_height(self, insets)
235    }
236    fn window_insets_right_width(self, insets: tessera_ui::WindowInsets) -> Modifier {
237        FoundationModifierExt::window_insets_right_width(self, insets)
238    }
239    fn window_insets_bottom_height(self, insets: tessera_ui::WindowInsets) -> Modifier {
240        FoundationModifierExt::window_insets_bottom_height(self, insets)
241    }
242
243    fn padding(self, padding: Padding) -> Modifier {
244        FoundationModifierExt::padding(self, padding)
245    }
246
247    fn padding_all(self, padding: Dp) -> Modifier {
248        FoundationModifierExt::padding(self, Padding::all(padding))
249    }
250
251    fn padding_symmetric(self, horizontal: Dp, vertical: Dp) -> Modifier {
252        FoundationModifierExt::padding(self, Padding::symmetric(horizontal, vertical))
253    }
254
255    fn offset(self, x: Dp, y: Dp) -> Modifier {
256        FoundationModifierExt::offset(self, x, y)
257    }
258
259    fn alpha(self, alpha: f32) -> Modifier {
260        let alpha = alpha.clamp(0.0, 1.0);
261        if (alpha - 1.0).abs() <= f32::EPSILON {
262            return self;
263        }
264
265        self.push_draw(AlphaModifierNode { alpha })
266    }
267
268    fn clip_to_bounds(self) -> Modifier {
269        self.push_draw(ClipModifierNode)
270    }
271
272    fn clip_shape(self, shape: Shape) -> Modifier {
273        self.push_draw(visual::ShapeClipModifierNode { shape })
274    }
275
276    fn vertical_scroll(self, controller: State<ScrollableController>) -> Modifier {
277        crate::scrollable::apply_scroll_modifier(self, controller, true, false)
278    }
279
280    fn horizontal_scroll(self, controller: State<ScrollableController>) -> Modifier {
281        crate::scrollable::apply_scroll_modifier(self, controller, false, true)
282    }
283
284    fn scroll(
285        self,
286        controller: State<ScrollableController>,
287        vertical: bool,
288        horizontal: bool,
289    ) -> Modifier {
290        crate::scrollable::apply_scroll_modifier(self, controller, vertical, horizontal)
291    }
292
293    fn background(self, color: Color) -> Modifier {
294        self.background_with_shape(color, Shape::RECTANGLE)
295    }
296
297    fn background_with_shape(self, color: Color, shape: Shape) -> Modifier {
298        if color.a <= 0.0 {
299            return self;
300        }
301
302        self.push_draw(BackgroundModifierNode { color, shape })
303    }
304
305    fn background_with_scaled_shape(self, color: Color, shape: Shape, scale: [f32; 2]) -> Modifier {
306        assert!(
307            scale
308                .iter()
309                .all(|value| value.is_finite() && (0.0..=1.0).contains(value)),
310            "background scales must be finite and in 0..=1",
311        );
312        self.push_draw(visual::ScaledBackgroundModifierNode {
313            color,
314            shape,
315            scale,
316        })
317    }
318
319    fn border(self, width: Dp, color: Color) -> Modifier {
320        self.border_with_shape(width, color, Shape::RECTANGLE)
321    }
322
323    fn border_with_shape(self, width: Dp, color: Color, shape: Shape) -> Modifier {
324        if width.0 <= 0.0 || color.a <= 0.0 {
325            return self;
326        }
327
328        self.push_draw(BorderModifierNode {
329            width,
330            color,
331            shape,
332        })
333    }
334
335    fn shadow(self, scheme: &MaterialColorScheme, args: &ShadowArgs) -> Modifier {
336        shadow::apply_shadow_modifier(self, scheme, args.clone())
337    }
338
339    fn default_min_size(self, min_width: Dp, min_height: Dp) -> Modifier {
340        FoundationModifierExt::default_min_size(self, min_width, min_height)
341    }
342
343    fn size(self, width: Dp, height: Dp) -> Modifier {
344        FoundationModifierExt::size(self, width, height)
345    }
346
347    fn required_size(self, width: Dp, height: Dp) -> Modifier {
348        FoundationModifierExt::required_size(self, width, height)
349    }
350
351    fn required_width(self, width: Dp) -> Modifier {
352        FoundationModifierExt::required_width(self, width)
353    }
354
355    fn required_height(self, height: Dp) -> Modifier {
356        FoundationModifierExt::required_height(self, height)
357    }
358    fn width(self, width: Dp) -> Modifier {
359        FoundationModifierExt::width(self, width)
360    }
361
362    fn height(self, height: Dp) -> Modifier {
363        FoundationModifierExt::height(self, height)
364    }
365
366    fn size_in(
367        self,
368        min_width: Option<Dp>,
369        max_width: Option<Dp>,
370        min_height: Option<Dp>,
371        max_height: Option<Dp>,
372    ) -> Modifier {
373        FoundationModifierExt::size_in(self, min_width, max_width, min_height, max_height)
374    }
375
376    fn constrain(self, width: Option<AxisConstraint>, height: Option<AxisConstraint>) -> Modifier {
377        FoundationModifierExt::constrain(self, width, height)
378    }
379
380    fn fill_max_width(self) -> Modifier {
381        FoundationModifierExt::fill_max_width(self)
382    }
383
384    fn fill_max_height(self) -> Modifier {
385        FoundationModifierExt::fill_max_height(self)
386    }
387
388    fn fill_max_size(self) -> Modifier {
389        FoundationModifierExt::fill_max_size(self)
390    }
391
392    fn minimum_interactive_component_size(self) -> Modifier {
393        if !MinimumInteractiveComponentEnforcement::default().enabled {
394            return self;
395        }
396
397        FoundationModifierExt::minimum_interactive_component_size(self)
398    }
399
400    fn weight(self, weight: f32) -> Modifier {
401        FoundationModifierExt::weight(self, weight)
402    }
403
404    fn align(self, alignment: Alignment) -> Modifier {
405        FoundationModifierExt::align(self, alignment)
406    }
407
408    fn draggable<C>(self, on_drag_delta: C) -> Modifier
409    where
410        C: Into<CallbackWith<DragDelta, ()>>,
411    {
412        FoundationModifierExt::draggable(self, on_drag_delta)
413    }
414
415    fn draggable_with(self, args: DraggableArgs) -> Modifier {
416        FoundationModifierExt::draggable_with(self, args)
417    }
418
419    fn block_touch_propagation(self) -> Modifier {
420        apply_block_touch_propagation_modifier(self)
421    }
422
423    fn semantics(self, args: SemanticsArgs) -> Modifier {
424        FoundationModifierExt::semantics(self, args)
425    }
426
427    fn clear_and_set_semantics(self, mut args: SemanticsArgs) -> Modifier {
428        args.merge_descendants = false;
429        FoundationModifierExt::semantics(self, args)
430    }
431
432    fn clickable<C>(self, on_click: C) -> Modifier
433    where
434        C: Into<Callback>,
435    {
436        self.clickable_with(ClickableArgs {
437            on_click: on_click.into(),
438            ..Default::default()
439        })
440    }
441
442    fn clickable_with(self, args: ClickableArgs) -> Modifier {
443        apply_clickable_modifier(self, args)
444    }
445
446    fn toggleable<C>(self, value: bool, on_value_change: C) -> Modifier
447    where
448        C: Into<CallbackWith<bool, ()>>,
449    {
450        self.toggleable_with(ToggleableArgs {
451            value,
452            on_value_change: on_value_change.into(),
453            ..Default::default()
454        })
455    }
456
457    fn toggleable_with(self, args: ToggleableArgs) -> Modifier {
458        apply_toggleable_modifier(self, args)
459    }
460
461    fn selectable<C>(self, selected: bool, on_click: C) -> Modifier
462    where
463        C: Into<Callback>,
464    {
465        self.selectable_with(SelectableArgs {
466            selected,
467            on_click: on_click.into(),
468            ..Default::default()
469        })
470    }
471
472    fn selectable_with(self, args: SelectableArgs) -> Modifier {
473        apply_selectable_modifier(self, args)
474    }
475
476    fn window_drag_region(self) -> Modifier {
477        apply_window_drag_region_modifier(self)
478    }
479}