Skip to main content

material_ui_rs/widget/component/
dialog.rs

1//! Material 3 dialog surface constructors.
2
3use iced_widget::button::{Status, Style};
4use iced_widget::core::text as core_text;
5use iced_widget::core::time::Instant;
6use iced_widget::core::widget::{self, Tree, tree};
7use iced_widget::core::{
8    Background, Clipboard, Color, Element, Event, Layout, Length, Padding, Rectangle, Shell, Size,
9    Transformation, Vector, Widget, alignment, border, layout, mouse, overlay, renderer, touch,
10};
11use iced_widget::graphics::geometry;
12use iced_widget::renderer::wgpu::primitive;
13use iced_widget::text;
14use iced_widget::{Column, Container, Row, Space, Stack, Text, opaque};
15
16use super::absolute_line_height;
17use super::support::{alpha_color, duration_ms, lerp};
18use crate::utils::shadow_from_level;
19use crate::{Theme, fonts, tokens};
20
21/// Android dialog visibility animation state.
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct Transition {
24    phase: TransitionPhase,
25    started_at: Option<Instant>,
26}
27
28/// Android dialog transition phase.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum TransitionPhase {
31    Hidden,
32    Showing,
33    Shown,
34    Dismissing,
35}
36
37impl Default for Transition {
38    fn default() -> Self {
39        Self {
40            phase: TransitionPhase::Hidden,
41            started_at: None,
42        }
43    }
44}
45
46impl Transition {
47    /// Starts showing the dialog using Android's platform dialog window animation.
48    pub fn show(&mut self, now: Instant) {
49        match self.phase {
50            TransitionPhase::Showing | TransitionPhase::Shown => {}
51            TransitionPhase::Hidden | TransitionPhase::Dismissing => {
52                self.phase = TransitionPhase::Showing;
53                self.started_at = Some(now);
54            }
55        }
56    }
57
58    /// Starts dismissing the dialog.
59    pub fn dismiss(&mut self, now: Instant) {
60        if self.is_active() && self.phase != TransitionPhase::Dismissing {
61            self.phase = TransitionPhase::Dismissing;
62            self.started_at = Some(now);
63        }
64    }
65
66    /// Advances timers and hides the dialog once Android's exit animation ends.
67    pub fn advance(&mut self, now: Instant) -> bool {
68        match self.phase {
69            TransitionPhase::Hidden => {}
70            TransitionPhase::Showing => {
71                if self.scale_progress(now) >= 1.0 {
72                    self.phase = TransitionPhase::Shown;
73                    self.started_at = None;
74                }
75            }
76            TransitionPhase::Shown => {}
77            TransitionPhase::Dismissing => {
78                if self.scale_progress(now) >= 1.0 {
79                    *self = Self::default();
80                }
81            }
82        }
83
84        self.is_animating()
85    }
86
87    /// Returns whether the dialog should remain in the view tree.
88    pub fn is_active(&self) -> bool {
89        self.phase != TransitionPhase::Hidden
90    }
91
92    /// Returns whether the dialog is currently running an enter or exit animation.
93    pub fn is_animating(&self) -> bool {
94        matches!(
95            self.phase,
96            TransitionPhase::Showing | TransitionPhase::Dismissing
97        )
98    }
99
100    /// Returns the current transition phase.
101    pub fn phase(&self) -> TransitionPhase {
102        self.phase
103    }
104
105    /// Computes the Android dialog surface scale.
106    pub fn scale(&self, now: Instant) -> f32 {
107        let eased = android_decelerate(
108            self.scale_progress(now),
109            tokens::component::dialog::DECELERATE_QUINT_FACTOR,
110        );
111
112        match self.phase {
113            TransitionPhase::Hidden => tokens::component::dialog::ENTER_SCALE_FROM,
114            TransitionPhase::Showing => {
115                lerp(tokens::component::dialog::ENTER_SCALE_FROM, 1.0, eased)
116            }
117            TransitionPhase::Shown => 1.0,
118            TransitionPhase::Dismissing => {
119                lerp(1.0, tokens::component::dialog::EXIT_SCALE_TO, eased)
120            }
121        }
122    }
123
124    /// Computes the Android dialog window alpha.
125    pub fn alpha(&self, now: Instant) -> f32 {
126        let eased = android_decelerate(
127            self.alpha_progress(now),
128            tokens::component::dialog::DECELERATE_CUBIC_FACTOR,
129        );
130
131        match self.phase {
132            TransitionPhase::Hidden => 0.0,
133            TransitionPhase::Showing => eased,
134            TransitionPhase::Shown => 1.0,
135            TransitionPhase::Dismissing => 1.0 - eased,
136        }
137    }
138
139    /// Computes the modal scrim fade progress.
140    pub fn scrim_alpha(&self, now: Instant) -> f32 {
141        let eased = android_decelerate(
142            self.scrim_progress(now),
143            tokens::component::dialog::DECELERATE_CUBIC_FACTOR,
144        );
145
146        match self.phase {
147            TransitionPhase::Hidden => 0.0,
148            TransitionPhase::Showing => eased,
149            TransitionPhase::Shown => 1.0,
150            TransitionPhase::Dismissing => 1.0 - eased,
151        }
152    }
153
154    fn scale_progress(&self, now: Instant) -> f32 {
155        self.progress(now, tokens::component::dialog::SCALE_ANIMATION_DURATION_MS)
156    }
157
158    fn alpha_progress(&self, now: Instant) -> f32 {
159        self.progress(now, tokens::component::dialog::ALPHA_ANIMATION_DURATION_MS)
160    }
161
162    fn scrim_progress(&self, now: Instant) -> f32 {
163        self.progress(now, tokens::component::dialog::SCRIM_ANIMATION_DURATION_MS)
164    }
165
166    fn progress(&self, now: Instant, duration_ms_value: u16) -> f32 {
167        let Some(started_at) = self.started_at else {
168            return match self.phase {
169                TransitionPhase::Showing | TransitionPhase::Dismissing => 1.0,
170                TransitionPhase::Hidden | TransitionPhase::Shown => 0.0,
171            };
172        };
173
174        let duration = duration_ms(duration_ms_value);
175
176        if duration.is_zero() {
177            return 1.0;
178        }
179
180        (now.saturating_duration_since(started_at).as_secs_f32() / duration.as_secs_f32())
181            .clamp(0.0, 1.0)
182    }
183}
184
185/// Visual options for dialog pieces that only need an alpha override.
186#[derive(Debug, Clone, Copy, PartialEq)]
187pub struct AlphaOptions {
188    pub alpha: f32,
189}
190
191impl Default for AlphaOptions {
192    fn default() -> Self {
193        Self { alpha: 1.0 }
194    }
195}
196
197impl AlphaOptions {
198    /// Sets the Android window alpha applied to the dialog piece.
199    pub fn alpha(mut self, alpha: f32) -> Self {
200        self.alpha = alpha;
201        self
202    }
203}
204
205/// Visual options for alert dialog content.
206#[derive(Debug, Clone, PartialEq)]
207pub struct AlertOptions<'a> {
208    pub icon: Option<text::Fragment<'a>>,
209    pub alpha: f32,
210}
211
212impl Default for AlertOptions<'_> {
213    fn default() -> Self {
214        Self {
215            icon: None,
216            alpha: 1.0,
217        }
218    }
219}
220
221impl<'a> AlertOptions<'a> {
222    /// Sets the optional hero icon slot.
223    pub fn icon(mut self, icon: impl text::IntoFragment<'a>) -> Self {
224        self.icon = Some(icon.into_fragment());
225        self
226    }
227
228    /// Sets the Android window alpha applied to the alert content.
229    pub fn alpha(mut self, alpha: f32) -> Self {
230        self.alpha = alpha;
231        self
232    }
233}
234
235/// Creates a Material 3 basic dialog surface around custom content.
236pub fn basic<'a, Message, Renderer>(
237    content: impl Into<Element<'a, Message, Theme, Renderer>>,
238) -> Container<'a, Message, Theme, Renderer>
239where
240    Message: 'a,
241    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
242{
243    basic_with(content, AlphaOptions::default())
244}
245
246/// Creates a Material 3 basic dialog surface with custom visual options.
247pub fn basic_with<'a, Message, Renderer>(
248    content: impl Into<Element<'a, Message, Theme, Renderer>>,
249    options: AlphaOptions,
250) -> Container<'a, Message, Theme, Renderer>
251where
252    Message: 'a,
253    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
254{
255    Container::new(content)
256        .width(Length::Fill)
257        .max_width(tokens::component::dialog::CONTAINER_MAX_WIDTH)
258        .padding(tokens::component::dialog::CONTAINER_PADDING)
259        .style(move |theme| container_style_alpha(theme, options.alpha))
260}
261
262/// Creates a Material 3 dialog with a title, arbitrary interactive content,
263/// and actions.
264pub fn content<'a, Message, Renderer>(
265    title: impl text::IntoFragment<'a>,
266    body: impl Into<Element<'a, Message, Theme, Renderer>>,
267    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
268) -> Container<'a, Message, Theme, Renderer>
269where
270    Message: 'a,
271    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
272{
273    content_with(title, body, actions, AlphaOptions::default())
274}
275
276/// Creates a Material 3 dialog with arbitrary interactive content and custom
277/// visual options.
278///
279/// The alpha applies to the dialog-owned surface and title. Custom body and
280/// action widgets should use the same alpha when participating in a dialog
281/// transition.
282pub fn content_with<'a, Message, Renderer>(
283    title: impl text::IntoFragment<'a>,
284    body: impl Into<Element<'a, Message, Theme, Renderer>>,
285    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
286    options: AlphaOptions,
287) -> Container<'a, Message, Theme, Renderer>
288where
289    Message: 'a,
290    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
291{
292    dialog_content(
293        None,
294        title_text(title, alignment::Horizontal::Left, options.alpha),
295        body.into(),
296        actions,
297        options.alpha,
298    )
299}
300
301/// Creates a Material 3 alert dialog with title, supporting text, and actions.
302pub fn alert<'a, Message, Renderer>(
303    title: impl text::IntoFragment<'a>,
304    supporting_text: impl text::IntoFragment<'a>,
305    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
306) -> Container<'a, Message, Theme, Renderer>
307where
308    Message: 'a,
309    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
310    iced_widget::core::Font: Into<Renderer::Font>,
311{
312    alert_with(title, supporting_text, actions, AlertOptions::default())
313}
314
315/// Creates a Material 3 alert dialog with custom visual options.
316pub fn alert_with<'a, Message, Renderer>(
317    title: impl text::IntoFragment<'a>,
318    supporting_text: impl text::IntoFragment<'a>,
319    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
320    options: AlertOptions<'a>,
321) -> Container<'a, Message, Theme, Renderer>
322where
323    Message: 'a,
324    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
325    iced_widget::core::Font: Into<Renderer::Font>,
326{
327    let alpha = options.alpha;
328    let has_icon = options.icon.is_some();
329    let icon = options.icon.map(|icon| icon_text(icon, alpha));
330    let body = supporting_text_view(supporting_text, alpha).into();
331
332    dialog_content(
333        icon,
334        title_text(title, title_alignment(has_icon), alpha),
335        body,
336        actions,
337        alpha,
338    )
339}
340
341/// Creates a right-aligned Material 3 dialog actions row.
342pub fn actions<'a, Message, Renderer>(
343    buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
344) -> Container<'a, Message, Theme, Renderer>
345where
346    Message: 'a,
347    Renderer: iced_widget::core::Renderer + 'a,
348{
349    Container::new(
350        Row::with_children(buttons)
351            .spacing(tokens::component::dialog::ACTIONS_HORIZONTAL_SPACING)
352            .align_y(alignment::Vertical::Center),
353    )
354    .width(Length::Fill)
355    .align_x(alignment::Horizontal::Right)
356}
357
358/// Creates a Material 3 dialog text action.
359pub fn action<'a, Message, Renderer>(
360    label: impl text::IntoFragment<'a>,
361) -> super::button::Button<'a, Message, Renderer>
362where
363    Message: Clone + 'a,
364    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
365{
366    action_with(label, AlphaOptions::default())
367}
368
369/// Creates a Material 3 dialog text action with an on-press message.
370pub fn action_button<'a, Message, Renderer>(
371    label: impl text::IntoFragment<'a>,
372    on_press: Message,
373) -> Element<'a, Message, Theme, Renderer>
374where
375    Message: Clone + 'a,
376    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
377{
378    action_button_with(label, on_press, AlphaOptions::default())
379}
380
381/// Creates a Material 3 dialog text action with custom visual options.
382pub fn action_with<'a, Message, Renderer>(
383    label: impl text::IntoFragment<'a>,
384    options: AlphaOptions,
385) -> super::button::Button<'a, Message, Renderer>
386where
387    Message: Clone + 'a,
388    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
389{
390    super::button::button(label, super::button::ButtonVariant::Text)
391        .style(move |theme, status| action_style_alpha(theme, status, options.alpha))
392}
393
394/// Creates a Material 3 dialog text action button with custom visual options.
395pub fn action_button_with<'a, Message, Renderer>(
396    label: impl text::IntoFragment<'a>,
397    on_press: Message,
398    options: AlphaOptions,
399) -> Element<'a, Message, Theme, Renderer>
400where
401    Message: Clone + 'a,
402    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
403{
404    action_with(label, options).on_press(on_press).into()
405}
406
407/// Creates a Material 3 modal dialog scrim behind overlay content.
408pub fn scrim<'a, Message, Renderer>(
409    content: impl Into<Element<'a, Message, Theme, Renderer>>,
410) -> Container<'a, Message, Theme, Renderer>
411where
412    Message: 'a,
413    Renderer: iced_widget::core::Renderer + 'a,
414{
415    scrim_with(content, AlphaOptions::default())
416}
417
418/// Creates a Material 3 modal dialog scrim with custom visual options.
419pub fn scrim_with<'a, Message, Renderer>(
420    content: impl Into<Element<'a, Message, Theme, Renderer>>,
421    options: AlphaOptions,
422) -> Container<'a, Message, Theme, Renderer>
423where
424    Message: 'a,
425    Renderer: iced_widget::core::Renderer + 'a,
426{
427    Container::new(content)
428        .width(Length::Fill)
429        .height(Length::Fill)
430        .style(move |theme| scrim_style_alpha(theme, options.alpha))
431}
432
433/// Centers a Material 3 dialog surface over a modal scrim.
434pub fn modal_overlay<'a, Message, Renderer>(
435    content: impl Into<Element<'a, Message, Theme, Renderer>>,
436) -> Container<'a, Message, Theme, Renderer>
437where
438    Message: 'a,
439    Renderer: iced_widget::core::Renderer + 'a,
440{
441    scrim(
442        Container::new(content)
443            .width(Length::Fill)
444            .height(Length::Fill)
445            .align_x(alignment::Horizontal::Center)
446            .align_y(alignment::Vertical::Center),
447    )
448}
449
450/// Creates an event-blocking Material 3 modal dialog layer.
451///
452/// The scrim and dialog surface both absorb mouse presses, so clicks outside
453/// the dialog do not pass through to content underneath and do not require a
454/// no-op application message.
455pub fn modal_layer<'a, Message, Renderer>(
456    content: impl Into<Element<'a, Message, Theme, Renderer>>,
457) -> Element<'a, Message, Theme, Renderer>
458where
459    Message: 'a,
460    Renderer: iced_widget::core::Renderer + 'a,
461{
462    let scrim = opaque(scrim(Space::new().width(Length::Fill).height(Length::Fill)));
463    let dialog = opaque(Container::new(content).center(Length::Fill));
464
465    Stack::with_children([scrim, dialog])
466        .width(Length::Fill)
467        .height(Length::Fill)
468        .into()
469}
470
471/// Creates an event-blocking Material 3 modal dialog layer animated like Android.
472pub fn modal_layer_animated<'a, Message, Renderer>(
473    content: impl Into<Element<'a, Message, Theme, Renderer>>,
474    transition: &Transition,
475    now: Instant,
476) -> Element<'a, Message, Theme, Renderer>
477where
478    Message: 'a,
479    Renderer: iced_widget::core::Renderer + 'a,
480{
481    let scrim = opaque(scrim_with(
482        Space::new().width(Length::Fill).height(Length::Fill),
483        AlphaOptions::default().alpha(transition.scrim_alpha(now)),
484    ));
485    let dialog = opaque(
486        Container::new(scaled(
487            content,
488            transition.scale(now),
489            transition.phase() != TransitionPhase::Dismissing,
490        ))
491        .center(Length::Fill),
492    );
493
494    Stack::with_children([scrim, dialog])
495        .width(Length::Fill)
496        .height(Length::Fill)
497        .into()
498}
499
500/// Places a Material 3 modal dialog layer over existing content.
501pub fn modal<'a, Message, Renderer>(
502    content: impl Into<Element<'a, Message, Theme, Renderer>>,
503    dialog: impl Into<Element<'a, Message, Theme, Renderer>>,
504) -> Element<'a, Message, Theme, Renderer>
505where
506    Message: 'a,
507    Renderer: iced_widget::core::Renderer + 'a,
508{
509    Stack::with_children([content.into(), modal_layer(dialog)])
510        .width(Length::Fill)
511        .height(Length::Fill)
512        .into()
513}
514
515/// Places an Android-animated Material 3 modal dialog layer over existing content.
516pub fn modal_animated<'a, Message, Renderer>(
517    content: impl Into<Element<'a, Message, Theme, Renderer>>,
518    transition: &Transition,
519    now: Instant,
520    dialog: impl Into<Element<'a, Message, Theme, Renderer>>,
521) -> Element<'a, Message, Theme, Renderer>
522where
523    Message: 'a,
524    Renderer: iced_widget::core::Renderer + 'a,
525{
526    let content = content.into();
527
528    if !transition.is_active() {
529        return content;
530    }
531
532    Stack::with_children([content, modal_layer_animated(dialog, transition, now)])
533        .width(Length::Fill)
534        .height(Length::Fill)
535        .into()
536}
537
538fn dialog_content<'a, Message, Renderer>(
539    icon: Option<Text<'a, Theme, Renderer>>,
540    title: Text<'a, Theme, Renderer>,
541    body: Element<'a, Message, Theme, Renderer>,
542    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
543    alpha: f32,
544) -> Container<'a, Message, Theme, Renderer>
545where
546    Message: 'a,
547    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
548{
549    let mut content = Column::new().width(Length::Fill);
550
551    if let Some(icon) = icon {
552        content = content.push(
553            Container::new(icon)
554                .width(Length::Fill)
555                .padding(Padding {
556                    top: 0.0,
557                    right: 0.0,
558                    bottom: tokens::component::dialog::ICON_BOTTOM_PADDING,
559                    left: 0.0,
560                })
561                .align_x(alignment::Horizontal::Center),
562        );
563    }
564
565    content = content.push(Container::new(title).width(Length::Fill).padding(Padding {
566        top: 0.0,
567        right: 0.0,
568        bottom: tokens::component::dialog::TITLE_BOTTOM_PADDING,
569        left: 0.0,
570    }));
571
572    content = content.push(Container::new(body).width(Length::Fill).padding(Padding {
573        top: 0.0,
574        right: 0.0,
575        bottom: tokens::component::dialog::SUPPORTING_TEXT_BOTTOM_PADDING,
576        left: 0.0,
577    }));
578
579    content = content.push(actions.into());
580
581    basic_with(content, AlphaOptions::default().alpha(alpha))
582}
583
584fn icon_text<'a, Renderer>(icon: text::Fragment<'a>, alpha: f32) -> Text<'a, Theme, Renderer>
585where
586    Renderer: core_text::Renderer + 'a,
587    iced_widget::core::Font: Into<Renderer::Font>,
588{
589    fonts::filled_icon(icon, tokens::component::dialog::ICON_SIZE)
590        .width(Length::Fixed(tokens::component::dialog::ICON_SIZE))
591        .height(Length::Fixed(tokens::component::dialog::ICON_SIZE))
592        .center()
593        .style(move |theme| icon_style_alpha(theme, alpha))
594}
595
596fn title_text<'a, Renderer>(
597    title: impl text::IntoFragment<'a>,
598    alignment: alignment::Horizontal,
599    alpha: f32,
600) -> Text<'a, Theme, Renderer>
601where
602    Renderer: core_text::Renderer + 'a,
603{
604    let scale = tokens::component::dialog::HEADLINE_TEXT;
605
606    Text::new(title)
607        .size(scale.size)
608        .line_height(absolute_line_height(scale.line_height))
609        .width(Length::Fill)
610        .align_x(alignment)
611        .color_maybe(None::<iced_widget::core::Color>)
612        .style(move |theme| title_style_alpha(theme, alpha))
613}
614
615fn title_alignment(has_icon: bool) -> alignment::Horizontal {
616    if has_icon {
617        alignment::Horizontal::Center
618    } else {
619        alignment::Horizontal::Left
620    }
621}
622
623fn supporting_text_view<'a, Renderer>(
624    supporting_text: impl text::IntoFragment<'a>,
625    alpha: f32,
626) -> Text<'a, Theme, Renderer>
627where
628    Renderer: core_text::Renderer + 'a,
629{
630    let scale = tokens::component::dialog::SUPPORTING_TEXT;
631
632    Text::new(supporting_text)
633        .size(scale.size)
634        .line_height(absolute_line_height(scale.line_height))
635        .width(Length::Fill)
636        .color_maybe(None::<iced_widget::core::Color>)
637        .style(move |theme| supporting_text_style_alpha(theme, alpha))
638}
639
640fn container_style(theme: &Theme) -> iced_widget::container::Style {
641    let colors = theme.colors();
642
643    iced_widget::container::Style {
644        background: Some(Background::Color(colors.surface.container.high)),
645        text_color: Some(colors.surface.text_variant),
646        border: border::rounded(tokens::component::dialog::CONTAINER_SHAPE),
647        shadow: shadow_from_level(
648            tokens::component::dialog::CONTAINER_ELEVATION_LEVEL,
649            colors.shadow,
650        ),
651        snap: cfg!(feature = "crisp"),
652    }
653}
654
655fn container_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
656    let mut style = container_style(theme);
657
658    style.background = style
659        .background
660        .map(|background| background.scale_alpha(alpha));
661    style.text_color = style.text_color.map(|color| alpha_color(color, alpha));
662    style.border.color = alpha_color(style.border.color, alpha);
663    style.shadow.color = alpha_color(style.shadow.color, alpha);
664
665    style
666}
667
668fn icon_style(theme: &Theme) -> iced_widget::text::Style {
669    iced_widget::text::Style {
670        color: Some(theme.colors().secondary.color),
671    }
672}
673
674fn icon_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::text::Style {
675    iced_widget::text::Style {
676        color: icon_style(theme)
677            .color
678            .map(|color| alpha_color(color, alpha)),
679    }
680}
681
682fn title_style(theme: &Theme) -> iced_widget::text::Style {
683    iced_widget::text::Style {
684        color: Some(theme.colors().surface.text),
685    }
686}
687
688fn title_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::text::Style {
689    iced_widget::text::Style {
690        color: title_style(theme)
691            .color
692            .map(|color| alpha_color(color, alpha)),
693    }
694}
695
696fn supporting_text_style(theme: &Theme) -> iced_widget::text::Style {
697    iced_widget::text::Style {
698        color: Some(theme.colors().surface.text_variant),
699    }
700}
701
702fn supporting_text_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::text::Style {
703    iced_widget::text::Style {
704        color: supporting_text_style(theme)
705            .color
706            .map(|color| alpha_color(color, alpha)),
707    }
708}
709
710fn action_style_alpha(theme: &Theme, status: Status, alpha: f32) -> Style {
711    let mut style = crate::style::button::text(theme, status);
712
713    style.background = style
714        .background
715        .map(|background| background.scale_alpha(alpha));
716    style.text_color = alpha_color(style.text_color, alpha);
717    style.border.color = alpha_color(style.border.color, alpha);
718    style.shadow.color = alpha_color(style.shadow.color, alpha);
719
720    style
721}
722
723fn scrim_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
724    iced_widget::container::Style {
725        background: Some(Background::Color(alpha_color(
726            Color {
727                a: 1.0,
728                ..theme.colors().scrim
729            },
730            tokens::component::dialog::SCRIM_OPACITY * alpha,
731        ))),
732        text_color: Some(theme.colors().surface.text),
733        ..iced_widget::container::Style::default()
734    }
735}
736
737fn scaled<'a, Message, Renderer>(
738    content: impl Into<Element<'a, Message, Theme, Renderer>>,
739    scale: f32,
740    interactive: bool,
741) -> Element<'a, Message, Theme, Renderer>
742where
743    Message: 'a,
744    Renderer: iced_widget::core::Renderer + 'a,
745{
746    Element::new(Scaled {
747        content: content.into(),
748        scale,
749        interactive,
750    })
751}
752
753struct Scaled<'a, Message, Renderer> {
754    content: Element<'a, Message, Theme, Renderer>,
755    scale: f32,
756    interactive: bool,
757}
758
759struct ScaledTag;
760
761impl<Message, Renderer> std::fmt::Debug for Scaled<'_, Message, Renderer> {
762    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
763        f.debug_struct("Scaled")
764            .field("scale", &self.scale)
765            .field("interactive", &self.interactive)
766            .finish_non_exhaustive()
767    }
768}
769
770impl<Message, Renderer> Scaled<'_, Message, Renderer> {
771    fn transformation(&self, bounds: Rectangle) -> Transformation {
772        Transformation::translate(bounds.center_x(), bounds.center_y())
773            * Transformation::scale(self.scale)
774            * Transformation::translate(-bounds.center_x(), -bounds.center_y())
775    }
776}
777
778impl<Message, Renderer> Widget<Message, Theme, Renderer> for Scaled<'_, Message, Renderer>
779where
780    Renderer: iced_widget::core::Renderer,
781{
782    fn tag(&self) -> tree::Tag {
783        tree::Tag::of::<ScaledTag>()
784    }
785
786    fn state(&self) -> tree::State {
787        tree::State::None
788    }
789
790    fn children(&self) -> Vec<Tree> {
791        vec![Tree::new(&self.content)]
792    }
793
794    fn diff(&self, tree: &mut Tree) {
795        tree.diff_children(std::slice::from_ref(&self.content));
796    }
797
798    fn size(&self) -> Size<Length> {
799        self.content.as_widget().size()
800    }
801
802    fn size_hint(&self) -> Size<Length> {
803        self.content.as_widget().size_hint()
804    }
805
806    fn layout(
807        &mut self,
808        tree: &mut Tree,
809        renderer: &Renderer,
810        limits: &layout::Limits,
811    ) -> layout::Node {
812        self.content
813            .as_widget_mut()
814            .layout(&mut tree.children[0], renderer, limits)
815    }
816
817    fn operate(
818        &mut self,
819        tree: &mut Tree,
820        layout: Layout<'_>,
821        renderer: &Renderer,
822        operation: &mut dyn widget::Operation,
823    ) {
824        self.content
825            .as_widget_mut()
826            .operate(&mut tree.children[0], layout, renderer, operation);
827    }
828
829    fn update(
830        &mut self,
831        tree: &mut Tree,
832        event: &Event,
833        layout: Layout<'_>,
834        cursor: mouse::Cursor,
835        renderer: &Renderer,
836        clipboard: &mut dyn Clipboard,
837        shell: &mut Shell<'_, Message>,
838        viewport: &Rectangle,
839    ) {
840        if !self.interactive
841            && !matches!(
842                event,
843                Event::Window(iced_widget::core::window::Event::RedrawRequested(_))
844            )
845        {
846            return;
847        }
848
849        let inverse = self.transformation(layout.bounds()).inverse();
850        let cursor = if self.interactive {
851            cursor * inverse
852        } else {
853            mouse::Cursor::Unavailable
854        };
855        let transformed_event = transform_pointer_event(event, inverse);
856        let event = transformed_event.as_ref().unwrap_or(event);
857
858        self.content.as_widget_mut().update(
859            &mut tree.children[0],
860            event,
861            layout,
862            cursor,
863            renderer,
864            clipboard,
865            shell,
866            &(*viewport * inverse),
867        );
868    }
869
870    fn mouse_interaction(
871        &self,
872        tree: &Tree,
873        layout: Layout<'_>,
874        cursor: mouse::Cursor,
875        viewport: &Rectangle,
876        renderer: &Renderer,
877    ) -> mouse::Interaction {
878        if !self.interactive {
879            return mouse::Interaction::None;
880        }
881
882        let inverse = self.transformation(layout.bounds()).inverse();
883
884        self.content.as_widget().mouse_interaction(
885            &tree.children[0],
886            layout,
887            cursor * inverse,
888            &(*viewport * inverse),
889            renderer,
890        )
891    }
892
893    fn draw(
894        &self,
895        tree: &Tree,
896        renderer: &mut Renderer,
897        theme: &Theme,
898        style: &renderer::Style,
899        layout: Layout<'_>,
900        cursor: mouse::Cursor,
901        viewport: &Rectangle,
902    ) {
903        let Some(layer_bounds) = scaled_layer_bounds(layout.bounds(), viewport) else {
904            return;
905        };
906        let transformation = self.transformation(layout.bounds());
907        let inverse = transformation.inverse();
908        let cursor = if self.interactive {
909            cursor * inverse
910        } else {
911            mouse::Cursor::Unavailable
912        };
913
914        renderer.with_layer(layer_bounds, |renderer| {
915            renderer.with_transformation(transformation, |renderer| {
916                self.content.as_widget().draw(
917                    &tree.children[0],
918                    renderer,
919                    theme,
920                    style,
921                    layout,
922                    cursor,
923                    &(*viewport * inverse),
924                );
925            });
926        });
927    }
928
929    fn overlay<'b>(
930        &'b mut self,
931        tree: &'b mut Tree,
932        layout: Layout<'b>,
933        renderer: &Renderer,
934        viewport: &Rectangle,
935        translation: Vector,
936    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
937        if !self.interactive {
938            return None;
939        }
940
941        let transformation = self.transformation(layout.bounds());
942        let inverse = transformation.inverse();
943
944        self.content.as_widget_mut().overlay(
945            &mut tree.children[0],
946            layout,
947            renderer,
948            &(*viewport * inverse),
949            translation + transformation.translation(),
950        )
951    }
952}
953
954fn transform_pointer_event(event: &Event, transformation: Transformation) -> Option<Event> {
955    match event {
956        Event::Mouse(mouse::Event::CursorMoved { position }) => {
957            Some(Event::Mouse(mouse::Event::CursorMoved {
958                position: *position * transformation,
959            }))
960        }
961        Event::Touch(touch::Event::FingerPressed { id, position }) => {
962            Some(Event::Touch(touch::Event::FingerPressed {
963                id: *id,
964                position: *position * transformation,
965            }))
966        }
967        Event::Touch(touch::Event::FingerMoved { id, position }) => {
968            Some(Event::Touch(touch::Event::FingerMoved {
969                id: *id,
970                position: *position * transformation,
971            }))
972        }
973        Event::Touch(touch::Event::FingerLifted { id, position }) => {
974            Some(Event::Touch(touch::Event::FingerLifted {
975                id: *id,
976                position: *position * transformation,
977            }))
978        }
979        Event::Touch(touch::Event::FingerLost { id, position }) => {
980            Some(Event::Touch(touch::Event::FingerLost {
981                id: *id,
982                position: *position * transformation,
983            }))
984        }
985        _ => None,
986    }
987}
988
989fn scaled_layer_bounds(bounds: Rectangle, viewport: &Rectangle) -> Option<Rectangle> {
990    bounds.intersection(viewport).map(|_| *viewport)
991}
992
993fn android_decelerate(progress: f32, factor: f32) -> f32 {
994    let progress = progress.clamp(0.0, 1.0);
995
996    // Mirrors Android's DecelerateInterpolator: 1 - (1 - t) ^ (2 * factor).
997    if (factor - 1.0).abs() <= f32::EPSILON {
998        1.0 - (1.0 - progress) * (1.0 - progress)
999    } else {
1000        1.0 - (1.0 - progress).powf(2.0 * factor)
1001    }
1002}
1003
1004#[cfg(test)]
1005#[path = "../../../tests/widget/component/dialog.rs"]
1006mod tests;