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