Skip to main content

fission_core/ui/widgets/
pressable.rs

1use crate::internal::InternalLower;
2use crate::lowering::{InternalIrBuilder, InternalLoweringCx};
3use crate::motion::{
4    color, fill as motion_fill, px, scalar, shadows as motion_shadows, Motion, MotionExpr,
5    MotionPredicate, MotionPropertyId, MotionStartValue, MotionTrack, MotionTransition,
6    MotionValue, RippleFx, RippleLayer,
7};
8use crate::ui::Widget;
9use crate::ActionEnvelope;
10use fission_ir::{
11    op::{BoxShadow, BoxStyle, Color, Fill, LayoutOp, Length, Op, PaintOp, Stroke},
12    semantics::ActionTrigger,
13    ActionEntry, CompositeScalar, CompositeStyle, FocusPolicy, Role, Semantics, WidgetId,
14};
15use serde::{Deserialize, Serialize};
16
17use super::split_box_margin;
18
19/// Accessibility behavior exposed by a [`Pressable`].
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
21pub enum PressableRole {
22    /// Standard button semantics.
23    #[default]
24    Button,
25    /// Link semantics for navigation-like activation.
26    Link,
27    /// Menu-item semantics for commands inside menus and popovers.
28    MenuItem,
29}
30
31impl PressableRole {
32    fn semantics_role(self) -> Role {
33        match self {
34            Self::Button => Role::Button,
35            Self::Link => Role::Link,
36            Self::MenuItem => Role::MenuItem,
37        }
38    }
39}
40
41/// Partial visual style used by a [`Pressable`] interaction state.
42///
43/// Every property is opt-in. An empty style adds no fill, padding, border,
44/// shadow, opacity, scale, or geometry.
45#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
46pub struct PressableStyle {
47    /// Background fill for the pressable's content box.
48    pub background: Option<Fill>,
49    /// Border stroke.
50    pub border: Option<Stroke>,
51    /// Corner radius in logical points.
52    pub corner_radius: Option<f32>,
53    /// Ordered outer or inset shadows.
54    pub shadows: Option<Vec<BoxShadow>>,
55    /// Inner spacing in `[left, right, top, bottom]` order.
56    pub padding: Option<[Length; 4]>,
57    /// Compositor opacity; `0.0` is transparent and `1.0` is opaque.
58    pub opacity: Option<f32>,
59    /// Uniform compositor scale; `1.0` is unchanged size.
60    pub scale: Option<f32>,
61}
62
63impl PressableStyle {
64    fn merged(&self, overlay: Option<&Self>) -> Self {
65        let Some(overlay) = overlay else {
66            return self.clone();
67        };
68        Self {
69            background: overlay
70                .background
71                .clone()
72                .or_else(|| self.background.clone()),
73            border: overlay.border.clone().or_else(|| self.border.clone()),
74            corner_radius: overlay.corner_radius.or(self.corner_radius),
75            shadows: overlay.shadows.clone().or_else(|| self.shadows.clone()),
76            padding: overlay.padding.clone().or_else(|| self.padding.clone()),
77            opacity: overlay.opacity.or(self.opacity),
78            scale: overlay.scale.or(self.scale),
79        }
80    }
81}
82
83/// A visually neutral accessible interaction surface.
84///
85/// `Pressable` contributes no visual chrome or geometry unless explicitly
86/// supplied through [`PressableStyle`] or [`BoxStyle`]. It still provides
87/// semantic activation, focus handling, keyboard activation, pointer state,
88/// optional transitions, and optional ripple feedback.
89///
90/// # Example
91///
92/// ```rust,ignore
93/// Pressable::new(Text::new("Open report"))
94///     .id(WidgetId::explicit("report.open"))
95///     .semantics_identifier("report.open")
96///     .label("Open report")
97///     .role(PressableRole::Button)
98///     .on_press(open_report)
99///     .layout(BoxStyle::default().padding_symmetric(
100///         Length::points(tokens.spacing.m),
101///         Length::points(tokens.spacing.s),
102///     ))
103///     .into()
104/// ```
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct Pressable {
107    /// Optional stable identity used for interaction state, focus, and motion.
108    pub id: Option<WidgetId>,
109    /// Child content rendered inside the interaction surface.
110    pub child: Widget,
111    /// Action dispatched when the pressable is activated.
112    pub on_press: Option<ActionEnvelope>,
113    /// Accessible label exposed to assistive technologies.
114    pub label: Option<String>,
115    /// Stable semantic identifier used by tests and accessibility tooling.
116    pub semantics_identifier: Option<String>,
117    /// Accessibility role exposed by the semantic node.
118    pub role: PressableRole,
119    /// How pointer activation should affect keyboard focus.
120    pub focus_policy: FocusPolicy,
121    /// Whether the pressable can receive focus or activation.
122    pub disabled: bool,
123    /// Typed layout box model for size, margin, padding, and placement.
124    pub layout: BoxStyle,
125    /// Legacy point-based flex grow factor.
126    pub flex_grow: f32,
127    /// Legacy point-based flex shrink factor.
128    pub flex_shrink: f32,
129    /// Base visual state.
130    pub style: PressableStyle,
131    /// Style overlay while hovered.
132    pub hover_style: Option<PressableStyle>,
133    /// Style overlay while actively pressed.
134    pub pressed_style: Option<PressableStyle>,
135    /// Style overlay while focused.
136    pub focused_style: Option<PressableStyle>,
137    /// Style overlay while disabled.
138    pub disabled_style: Option<PressableStyle>,
139    /// Optional transition for state-style changes.
140    pub transition: Option<MotionTransition>,
141    /// Optional ripple feedback.
142    pub ripple: Option<RippleFx>,
143}
144
145impl Pressable {
146    const MOTION_SALT: u32 = 0x5052_4553;
147
148    /// Creates a neutral pressable around `child`.
149    pub fn new(child: impl Into<Widget>) -> Self {
150        Self {
151            child: child.into(),
152            ..Default::default()
153        }
154    }
155
156    /// Uses an explicit stable identity for interaction, focus, and motion.
157    pub fn id(mut self, id: WidgetId) -> Self {
158        self.id = Some(id);
159        self
160    }
161
162    /// Dispatches `action` for pointer, keyboard, or accessibility activation.
163    pub fn on_press(mut self, action: ActionEnvelope) -> Self {
164        self.on_press = Some(action);
165        self
166    }
167
168    /// Enables or disables activation and focus participation.
169    pub fn disabled(mut self, disabled: bool) -> Self {
170        self.disabled = disabled;
171        self
172    }
173
174    /// Applies the shared typed box model without introducing visual defaults.
175    pub fn layout(mut self, style: BoxStyle) -> Self {
176        self.layout = style;
177        self
178    }
179
180    /// Selects how pointer activation affects keyboard focus.
181    pub fn focus_policy(mut self, policy: FocusPolicy) -> Self {
182        self.focus_policy = policy;
183        self
184    }
185
186    /// Sets the base visual style.
187    pub fn style(mut self, style: PressableStyle) -> Self {
188        self.style = style;
189        self
190    }
191
192    /// Sets the style overlay while the pointer is over the pressable.
193    pub fn hover(mut self, style: PressableStyle) -> Self {
194        self.hover_style = Some(style);
195        self
196    }
197
198    /// Sets the style overlay while the pressable is active.
199    pub fn pressed(mut self, style: PressableStyle) -> Self {
200        self.pressed_style = Some(style);
201        self
202    }
203
204    /// Sets the style overlay while the pressable owns keyboard focus.
205    pub fn focused(mut self, style: PressableStyle) -> Self {
206        self.focused_style = Some(style);
207        self
208    }
209
210    /// Sets the style overlay while activation is disabled.
211    pub fn disabled_style(mut self, style: PressableStyle) -> Self {
212        self.disabled_style = Some(style);
213        self
214    }
215
216    /// Animates supported properties between interaction states.
217    pub fn transition(mut self, transition: MotionTransition) -> Self {
218        self.transition = Some(transition);
219        self
220    }
221
222    /// Enables optional ripple feedback.
223    pub fn ripple(mut self, ripple: RippleFx) -> Self {
224        self.ripple = Some(ripple);
225        self
226    }
227
228    /// Sets a stable semantic identifier for tests and accessibility tools.
229    pub fn semantics_identifier(mut self, identifier: impl Into<String>) -> Self {
230        self.semantics_identifier = Some(identifier.into());
231        self
232    }
233
234    /// Sets the accessible label.
235    pub fn label(mut self, label: impl Into<String>) -> Self {
236        self.label = Some(label.into());
237        self
238    }
239
240    /// Sets button, link, or menu-item semantics.
241    pub fn role(mut self, role: PressableRole) -> Self {
242        self.role = role;
243        self
244    }
245
246    fn resolved_style(&self, cx: &InternalLoweringCx<'_>, id: WidgetId) -> PressableStyle {
247        if self.disabled {
248            return self.style.merged(self.disabled_style.as_ref());
249        }
250        if cx.runtime_state.interaction.is_pressed(id) {
251            self.style.merged(self.pressed_style.as_ref())
252        } else if cx.runtime_state.interaction.is_focused(id) {
253            self.style.merged(self.focused_style.as_ref())
254        } else if cx.runtime_state.interaction.is_hovered(id) {
255            self.style.merged(self.hover_style.as_ref())
256        } else {
257            self.style.clone()
258        }
259    }
260
261    fn state_scalar_expr(&self, property: fn(&PressableStyle) -> Option<f32>) -> MotionExpr {
262        let base = property(&self.style).unwrap_or(1.0);
263        if self.disabled {
264            return scalar(
265                property(&self.style.merged(self.disabled_style.as_ref())).unwrap_or(base),
266            );
267        }
268        let hover = property(&self.style.merged(self.hover_style.as_ref())).unwrap_or(base);
269        let focused = property(&self.style.merged(self.focused_style.as_ref())).unwrap_or(base);
270        let pressed = property(&self.style.merged(self.pressed_style.as_ref())).unwrap_or(base);
271        let id = self.id.expect("pressable motion requires a stable id");
272        MotionExpr::If {
273            predicate: MotionPredicate::Pressed(id),
274            then_expr: Box::new(scalar(pressed)),
275            else_expr: Box::new(MotionExpr::If {
276                predicate: MotionPredicate::Focused(id),
277                then_expr: Box::new(scalar(focused)),
278                else_expr: Box::new(MotionExpr::If {
279                    predicate: MotionPredicate::Hovered(id),
280                    then_expr: Box::new(scalar(hover)),
281                    else_expr: Box::new(scalar(base)),
282                }),
283            }),
284        }
285    }
286
287    fn state_px_expr(
288        &self,
289        property: impl Fn(&PressableStyle) -> Option<f32> + Copy,
290    ) -> MotionExpr {
291        self.state_value_expr(property, 0.0, px)
292    }
293
294    fn state_color_expr(&self, property: fn(&PressableStyle) -> Option<Color>) -> MotionExpr {
295        let transparent = Color::TRANSPARENT;
296        let base = property(&self.style).unwrap_or(transparent);
297        if self.disabled {
298            return color(
299                property(&self.style.merged(self.disabled_style.as_ref())).unwrap_or(base),
300            );
301        }
302        let hover = property(&self.style.merged(self.hover_style.as_ref())).unwrap_or(base);
303        let focused = property(&self.style.merged(self.focused_style.as_ref())).unwrap_or(base);
304        let pressed = property(&self.style.merged(self.pressed_style.as_ref())).unwrap_or(base);
305        let id = self.id.expect("pressable motion requires a stable id");
306        MotionExpr::If {
307            predicate: MotionPredicate::Pressed(id),
308            then_expr: Box::new(color(pressed)),
309            else_expr: Box::new(MotionExpr::If {
310                predicate: MotionPredicate::Focused(id),
311                then_expr: Box::new(color(focused)),
312                else_expr: Box::new(MotionExpr::If {
313                    predicate: MotionPredicate::Hovered(id),
314                    then_expr: Box::new(color(hover)),
315                    else_expr: Box::new(color(base)),
316                }),
317            }),
318        }
319    }
320
321    fn state_fill_expr(&self) -> MotionExpr {
322        let default = Fill::Solid(Color::TRANSPARENT);
323        let base = self.style.background.clone().unwrap_or(default);
324        if self.disabled {
325            return motion_fill(
326                self.style
327                    .merged(self.disabled_style.as_ref())
328                    .background
329                    .unwrap_or(base),
330            );
331        }
332        let hover = self
333            .style
334            .merged(self.hover_style.as_ref())
335            .background
336            .unwrap_or_else(|| base.clone());
337        let focused = self
338            .style
339            .merged(self.focused_style.as_ref())
340            .background
341            .unwrap_or_else(|| base.clone());
342        let pressed = self
343            .style
344            .merged(self.pressed_style.as_ref())
345            .background
346            .unwrap_or_else(|| base.clone());
347        let id = self.id.expect("pressable motion requires a stable id");
348        MotionExpr::If {
349            predicate: MotionPredicate::Pressed(id),
350            then_expr: Box::new(motion_fill(pressed)),
351            else_expr: Box::new(MotionExpr::If {
352                predicate: MotionPredicate::Focused(id),
353                then_expr: Box::new(motion_fill(focused)),
354                else_expr: Box::new(MotionExpr::If {
355                    predicate: MotionPredicate::Hovered(id),
356                    then_expr: Box::new(motion_fill(hover)),
357                    else_expr: Box::new(motion_fill(base)),
358                }),
359            }),
360        }
361    }
362
363    fn state_shadows_expr(&self) -> MotionExpr {
364        let base = self.style.shadows.clone().unwrap_or_default();
365        if self.disabled {
366            return motion_shadows(
367                self.style
368                    .merged(self.disabled_style.as_ref())
369                    .shadows
370                    .unwrap_or(base),
371            );
372        }
373        let hover = self
374            .style
375            .merged(self.hover_style.as_ref())
376            .shadows
377            .unwrap_or_else(|| base.clone());
378        let focused = self
379            .style
380            .merged(self.focused_style.as_ref())
381            .shadows
382            .unwrap_or_else(|| base.clone());
383        let pressed = self
384            .style
385            .merged(self.pressed_style.as_ref())
386            .shadows
387            .unwrap_or_else(|| base.clone());
388        let id = self.id.expect("pressable motion requires a stable id");
389        MotionExpr::If {
390            predicate: MotionPredicate::Pressed(id),
391            then_expr: Box::new(motion_shadows(pressed)),
392            else_expr: Box::new(MotionExpr::If {
393                predicate: MotionPredicate::Focused(id),
394                then_expr: Box::new(motion_shadows(focused)),
395                else_expr: Box::new(MotionExpr::If {
396                    predicate: MotionPredicate::Hovered(id),
397                    then_expr: Box::new(motion_shadows(hover)),
398                    else_expr: Box::new(motion_shadows(base)),
399                }),
400            }),
401        }
402    }
403
404    fn state_value_expr(
405        &self,
406        property: impl Fn(&PressableStyle) -> Option<f32> + Copy,
407        default: f32,
408        expression: fn(f32) -> MotionExpr,
409    ) -> MotionExpr {
410        let base = property(&self.style).unwrap_or(default);
411        if self.disabled {
412            return expression(
413                property(&self.style.merged(self.disabled_style.as_ref())).unwrap_or(base),
414            );
415        }
416        let hover = property(&self.style.merged(self.hover_style.as_ref())).unwrap_or(base);
417        let focused = property(&self.style.merged(self.focused_style.as_ref())).unwrap_or(base);
418        let pressed = property(&self.style.merged(self.pressed_style.as_ref())).unwrap_or(base);
419        let id = self.id.expect("pressable motion requires a stable id");
420        MotionExpr::If {
421            predicate: MotionPredicate::Pressed(id),
422            then_expr: Box::new(expression(pressed)),
423            else_expr: Box::new(MotionExpr::If {
424                predicate: MotionPredicate::Focused(id),
425                then_expr: Box::new(expression(focused)),
426                else_expr: Box::new(MotionExpr::If {
427                    predicate: MotionPredicate::Hovered(id),
428                    then_expr: Box::new(expression(hover)),
429                    else_expr: Box::new(expression(base)),
430                }),
431            }),
432        }
433    }
434
435    fn any_state(&self, predicate: impl Fn(&PressableStyle) -> bool) -> bool {
436        predicate(&self.style)
437            || self.hover_style.as_ref().is_some_and(&predicate)
438            || self.pressed_style.as_ref().is_some_and(&predicate)
439            || self.focused_style.as_ref().is_some_and(&predicate)
440            || self.disabled_style.as_ref().is_some_and(predicate)
441    }
442
443    fn motion_id(id: WidgetId) -> WidgetId {
444        WidgetId::derived(id.as_u128(), &[Self::MOTION_SALT])
445    }
446
447    fn animated_style(
448        &self,
449        cx: &InternalLoweringCx<'_>,
450        id: WidgetId,
451        mut style: PressableStyle,
452    ) -> PressableStyle {
453        if self.transition.is_none() {
454            return style;
455        }
456        let motion_id = Self::motion_id(id);
457        let value = |property| cx.runtime_state.motion.values.get(&(motion_id, property));
458
459        if let Some(MotionValue::Color(background)) = value(MotionPropertyId::BackgroundColor) {
460            style.background = Some(Fill::Solid(*background));
461        }
462        if let Some(MotionValue::Fill(background)) = value(MotionPropertyId::BackgroundFill) {
463            style.background = Some(background.clone());
464        }
465        if let Some(MotionValue::Shadows(shadows)) = value(MotionPropertyId::BoxShadows) {
466            style.shadows = Some(shadows.clone());
467        }
468
469        let animated_border_color = match value(MotionPropertyId::BorderColor) {
470            Some(MotionValue::Color(color)) => Some(*color),
471            _ => None,
472        };
473        let animated_border_width = match value(MotionPropertyId::BorderWidth) {
474            Some(MotionValue::Px(width)) => Some(*width),
475            _ => None,
476        };
477        if animated_border_color.is_some() || animated_border_width.is_some() {
478            let mut border = style
479                .border
480                .clone()
481                .or_else(|| self.first_border())
482                .unwrap_or_else(|| Stroke {
483                    fill: Fill::Solid(Color::TRANSPARENT),
484                    width: 0.0,
485                    dash_array: None,
486                    line_cap: fission_ir::op::LineCap::Butt,
487                    line_join: fission_ir::op::LineJoin::Miter,
488                });
489            if let Some(color) = animated_border_color {
490                border.fill = Fill::Solid(color);
491            }
492            if let Some(width) = animated_border_width {
493                border.width = width;
494            }
495            style.border = Some(border);
496        }
497        if let Some(MotionValue::Px(radius)) = value(MotionPropertyId::CornerRadius) {
498            style.corner_radius = Some(*radius);
499        }
500
501        let padding_properties = [
502            MotionPropertyId::PaddingLeft,
503            MotionPropertyId::PaddingRight,
504            MotionPropertyId::PaddingTop,
505            MotionPropertyId::PaddingBottom,
506        ];
507        let mut animated_padding = style.padding.clone().unwrap_or_else(|| {
508            std::array::from_fn(|index| Length::Points(self.base_padding(index)))
509        });
510        let mut has_animated_padding = false;
511        for (index, property) in padding_properties.into_iter().enumerate() {
512            if let Some(MotionValue::Px(padding)) = value(property) {
513                animated_padding[index] = Length::Points(*padding);
514                has_animated_padding = true;
515            }
516        }
517        if has_animated_padding {
518            style.padding = Some(animated_padding);
519        }
520        style
521    }
522
523    fn first_border(&self) -> Option<Stroke> {
524        [
525            Some(&self.style),
526            self.hover_style.as_ref(),
527            self.pressed_style.as_ref(),
528            self.focused_style.as_ref(),
529            self.disabled_style.as_ref(),
530        ]
531        .into_iter()
532        .flatten()
533        .find_map(|style| style.border.clone())
534    }
535
536    fn base_padding(&self, index: usize) -> f32 {
537        self.style
538            .padding
539            .as_ref()
540            .and_then(|padding| length_points(&padding[index]))
541            .unwrap_or(0.0)
542    }
543
544    fn motion_tracks(&self, transition: &MotionTransition) -> Vec<MotionTrack> {
545        let mut tracks = Vec::new();
546        if self.style.opacity.is_some()
547            || self
548                .hover_style
549                .as_ref()
550                .is_some_and(|style| style.opacity.is_some())
551            || self
552                .pressed_style
553                .as_ref()
554                .is_some_and(|style| style.opacity.is_some())
555            || self
556                .focused_style
557                .as_ref()
558                .is_some_and(|style| style.opacity.is_some())
559            || self
560                .disabled_style
561                .as_ref()
562                .is_some_and(|style| style.opacity.is_some())
563        {
564            tracks.push(
565                MotionTrack::composite(
566                    MotionPropertyId::Opacity,
567                    MotionStartValue::Explicit(scalar(self.style.opacity.unwrap_or(1.0))),
568                    self.state_scalar_expr(|style| style.opacity),
569                )
570                .transition(transition.clone()),
571            );
572        }
573        if self.style.scale.is_some()
574            || self
575                .hover_style
576                .as_ref()
577                .is_some_and(|style| style.scale.is_some())
578            || self
579                .pressed_style
580                .as_ref()
581                .is_some_and(|style| style.scale.is_some())
582            || self
583                .focused_style
584                .as_ref()
585                .is_some_and(|style| style.scale.is_some())
586            || self
587                .disabled_style
588                .as_ref()
589                .is_some_and(|style| style.scale.is_some())
590        {
591            tracks.push(
592                MotionTrack::composite(
593                    MotionPropertyId::Scale,
594                    MotionStartValue::Explicit(scalar(self.style.scale.unwrap_or(1.0))),
595                    self.state_scalar_expr(|style| style.scale),
596                )
597                .transition(transition.clone()),
598            );
599        }
600        let has_non_solid_background = self.any_state(|style| {
601            style
602                .background
603                .as_ref()
604                .is_some_and(|fill| !matches!(fill, Fill::Solid(_)))
605        });
606        if has_non_solid_background {
607            tracks.push(
608                MotionTrack::paint(
609                    MotionPropertyId::BackgroundFill,
610                    MotionStartValue::Explicit(motion_fill(
611                        self.style
612                            .background
613                            .clone()
614                            .unwrap_or(Fill::Solid(Color::TRANSPARENT)),
615                    )),
616                    self.state_fill_expr(),
617                )
618                .transition(MotionTransition::Instant),
619            );
620        } else if self.any_state(|style| solid_background(style).is_some()) {
621            tracks.push(
622                MotionTrack::paint(
623                    MotionPropertyId::BackgroundColor,
624                    MotionStartValue::Explicit(color(
625                        solid_background(&self.style).unwrap_or(Color::TRANSPARENT),
626                    )),
627                    self.state_color_expr(solid_background),
628                )
629                .transition(transition.clone()),
630            );
631        }
632        if self.any_state(|style| style.shadows.is_some()) {
633            tracks.push(
634                MotionTrack::paint(
635                    MotionPropertyId::BoxShadows,
636                    MotionStartValue::Explicit(motion_shadows(
637                        self.style.shadows.clone().unwrap_or_default(),
638                    )),
639                    self.state_shadows_expr(),
640                )
641                .transition(MotionTransition::Instant),
642            );
643        }
644        if self.any_state(|style| solid_border_color(style).is_some()) {
645            tracks.push(
646                MotionTrack::paint(
647                    MotionPropertyId::BorderColor,
648                    MotionStartValue::Explicit(color(
649                        solid_border_color(&self.style).unwrap_or(Color::TRANSPARENT),
650                    )),
651                    self.state_color_expr(solid_border_color),
652                )
653                .transition(transition.clone()),
654            );
655        }
656        if self.any_state(|style| style.border.is_some()) {
657            tracks.push(
658                MotionTrack::paint(
659                    MotionPropertyId::BorderWidth,
660                    MotionStartValue::Explicit(px(border_width(&self.style).unwrap_or(0.0))),
661                    self.state_px_expr(border_width),
662                )
663                .transition(transition.clone()),
664            );
665        }
666        if self.any_state(|style| style.corner_radius.is_some()) {
667            tracks.push(
668                MotionTrack::paint(
669                    MotionPropertyId::CornerRadius,
670                    MotionStartValue::Explicit(px(self.style.corner_radius.unwrap_or(0.0))),
671                    self.state_px_expr(|style| style.corner_radius),
672                )
673                .transition(transition.clone()),
674            );
675        }
676        for (index, property) in [
677            MotionPropertyId::PaddingLeft,
678            MotionPropertyId::PaddingRight,
679            MotionPropertyId::PaddingTop,
680            MotionPropertyId::PaddingBottom,
681        ]
682        .into_iter()
683        .enumerate()
684        {
685            if self.any_state(|style| padding_points(style, index).is_some()) {
686                tracks.push(
687                    MotionTrack::layout(
688                        property,
689                        MotionStartValue::Explicit(px(self.base_padding(index))),
690                        self.state_value_expr(move |style| padding_points(style, index), 0.0, px),
691                    )
692                    .transition(transition.clone()),
693                );
694            }
695        }
696        tracks
697    }
698}
699
700fn length_points(length: &Length) -> Option<f32> {
701    match length {
702        Length::Points(value) => Some(*value),
703        _ => None,
704    }
705}
706
707fn padding_points(style: &PressableStyle, index: usize) -> Option<f32> {
708    style
709        .padding
710        .as_ref()
711        .and_then(|padding| length_points(&padding[index]))
712}
713
714fn solid_background(style: &PressableStyle) -> Option<Color> {
715    match &style.background {
716        Some(Fill::Solid(color)) => Some(*color),
717        _ => None,
718    }
719}
720
721fn solid_border_color(style: &PressableStyle) -> Option<Color> {
722    match style.border.as_ref().map(|border| &border.fill) {
723        Some(Fill::Solid(color)) => Some(*color),
724        _ => None,
725    }
726}
727
728fn border_width(style: &PressableStyle) -> Option<f32> {
729    style.border.as_ref().map(|border| border.width)
730}
731
732impl Default for Pressable {
733    fn default() -> Self {
734        Self {
735            id: None,
736            child: crate::ui::Spacer::default().into(),
737            on_press: None,
738            label: None,
739            semantics_identifier: None,
740            role: PressableRole::Button,
741            focus_policy: FocusPolicy::FocusOnPointer,
742            disabled: false,
743            layout: BoxStyle::default(),
744            flex_grow: 0.0,
745            flex_shrink: 1.0,
746            style: PressableStyle::default(),
747            hover_style: None,
748            pressed_style: None,
749            focused_style: None,
750            disabled_style: None,
751            transition: None,
752            ripple: None,
753        }
754    }
755}
756
757impl InternalLower for Pressable {
758    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
759        let id = self.id.unwrap_or_else(|| cx.next_node_id());
760        let layout_id = cx.next_node_id();
761        let style = self.animated_style(cx, id, self.resolved_style(cx, id));
762        cx.push_scope(layout_id);
763
764        let mut layout_style = self.layout.clone();
765        layout_style.padding = style.padding.clone().or(layout_style.padding);
766        let margin_style = split_box_margin(&mut layout_style);
767        let position = layout_style.position.take();
768        let grid = layout_style.grid.take();
769        let flex_grow = layout_style
770            .flex_grow
771            .map(|value| value.0)
772            .unwrap_or(self.flex_grow);
773        let flex_shrink = layout_style
774            .flex_shrink
775            .map(|value| value.0)
776            .unwrap_or(self.flex_shrink);
777        let mut layout = InternalIrBuilder::new(
778            layout_id,
779            Op::Layout(LayoutOp::StyledBox {
780                style: layout_style,
781                flex_grow,
782                flex_shrink,
783            }),
784        )
785        .composite(CompositeStyle {
786            opacity: self
787                .transition
788                .is_none()
789                .then(|| style.opacity.map(CompositeScalar::new))
790                .flatten(),
791            scale: self
792                .transition
793                .is_none()
794                .then(|| style.scale.map(CompositeScalar::new))
795                .flatten(),
796            ..Default::default()
797        });
798
799        for shadow in style.shadows.as_deref().unwrap_or_default() {
800            layout.add_child(
801                InternalIrBuilder::new(
802                    cx.next_node_id(),
803                    Op::Paint(PaintOp::DrawRect {
804                        fill: None,
805                        stroke: None,
806                        corner_radius: style.corner_radius.unwrap_or(0.0),
807                        shadow: Some(*shadow),
808                    }),
809                )
810                .build(cx),
811            );
812        }
813        if style.background.is_some() || style.border.is_some() {
814            layout.add_child(
815                InternalIrBuilder::new(
816                    cx.next_node_id(),
817                    Op::Paint(PaintOp::DrawRect {
818                        fill: style.background,
819                        stroke: style.border,
820                        corner_radius: style.corner_radius.unwrap_or(0.0),
821                        shadow: None,
822                    }),
823                )
824                .build(cx),
825            );
826        }
827        layout.add_child(self.child.lower(cx));
828        let layout_id = layout.build(cx);
829        cx.pop_scope();
830
831        let mut semantics = Semantics {
832            role: self.role.semantics_role(),
833            label: self.label.clone(),
834            identifier: self.semantics_identifier.clone(),
835            focusable: !self.disabled,
836            focus_policy: self.focus_policy,
837            disabled: self.disabled,
838            ..Default::default()
839        };
840        if let Some(action) = &self.on_press {
841            if !self.disabled {
842                semantics.actions.entries.push(ActionEntry {
843                    trigger: ActionTrigger::Default,
844                    action_id: action.id.as_u128(),
845                    payload_data: Some(action.payload.clone()),
846                });
847            }
848        }
849        let mut semantics_node = InternalIrBuilder::new(id, Op::Semantics(semantics));
850        semantics_node.add_child(layout_id);
851        let mut content_id = semantics_node.build(cx);
852
853        if let Some(margin_style) = margin_style {
854            let mut outer = InternalIrBuilder::new(
855                cx.next_node_id(),
856                Op::Layout(LayoutOp::StyledBox {
857                    style: margin_style,
858                    flex_grow,
859                    flex_shrink,
860                }),
861            );
862            outer.add_child(content_id);
863            content_id = outer.build(cx);
864        }
865        if let Some(position) = position {
866            let mut outer = InternalIrBuilder::new(
867                cx.next_node_id(),
868                Op::Layout(LayoutOp::PositionedLengths {
869                    left: position.left,
870                    top: position.top,
871                    right: position.right,
872                    bottom: position.bottom,
873                    width: None,
874                    height: None,
875                }),
876            );
877            outer.add_child(content_id);
878            content_id = outer.build(cx);
879        }
880        if let Some(grid) = grid {
881            let mut outer = InternalIrBuilder::new(
882                cx.next_node_id(),
883                Op::Layout(LayoutOp::GridItem {
884                    row_start: grid.row_start,
885                    row_end: grid.row_end,
886                    col_start: grid.col_start,
887                    col_end: grid.col_end,
888                }),
889            );
890            outer.add_child(content_id);
891            content_id = outer.build(cx);
892        }
893
894        content_id
895    }
896}
897
898impl From<Pressable> for Widget {
899    fn from(mut pressable: Pressable) -> Self {
900        let id = pressable
901            .id
902            .or_else(crate::build::current_widget_id)
903            .or_else(|| crate::build::next_implicit_widget_id(Pressable::MOTION_SALT))
904            .unwrap_or_else(|| WidgetId::explicit("fission.core.pressable"));
905        pressable.id = Some(id);
906        let transition = pressable.transition.clone();
907        let ripple = (!pressable.disabled)
908            .then(|| pressable.ripple.clone())
909            .flatten();
910        pressable.ripple = None;
911        let state_transition = transition.unwrap_or(MotionTransition::Instant);
912        let tracks = pressable.motion_tracks(&state_transition);
913        let base = Widget::from_pressable_raw(pressable);
914        let animated = if tracks.is_empty() {
915            base
916        } else {
917            Motion {
918                id: Pressable::motion_id(id),
919                tracks,
920                child: base,
921                ..Default::default()
922            }
923            .into()
924        };
925        if let Some(effect) = ripple {
926            RippleLayer {
927                id: WidgetId::derived(id.as_u128(), &[0x5249_5050]),
928                effect,
929                child: animated,
930            }
931            .into()
932        } else {
933            animated
934        }
935    }
936}