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