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,
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/// Creates a Material 3 basic dialog surface around custom content.
186pub fn basic<'a, Message, Renderer>(
187    content: impl Into<Element<'a, Message, Theme, Renderer>>,
188) -> Container<'a, Message, Theme, Renderer>
189where
190    Message: 'a,
191    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
192{
193    basic_alpha(content, 1.0)
194}
195
196/// Creates a Material 3 basic dialog surface with Android window alpha applied.
197pub fn basic_alpha<'a, Message, Renderer>(
198    content: impl Into<Element<'a, Message, Theme, Renderer>>,
199    alpha: f32,
200) -> Container<'a, Message, Theme, Renderer>
201where
202    Message: 'a,
203    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
204{
205    Container::new(content)
206        .width(Length::Fill)
207        .max_width(tokens::component::dialog::CONTAINER_MAX_WIDTH)
208        .padding(tokens::component::dialog::CONTAINER_PADDING)
209        .style(move |theme| container_style_alpha(theme, alpha))
210}
211
212/// Creates a Material 3 alert dialog with title, supporting text, and actions.
213pub fn alert<'a, Message, Renderer>(
214    title: impl text::IntoFragment<'a>,
215    supporting_text: impl text::IntoFragment<'a>,
216    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
217) -> Container<'a, Message, Theme, Renderer>
218where
219    Message: 'a,
220    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
221    iced_widget::core::Font: Into<Renderer::Font>,
222{
223    alert_alpha(title, supporting_text, actions, 1.0)
224}
225
226/// Creates a Material 3 alert dialog with Android window alpha applied.
227pub fn alert_alpha<'a, Message, Renderer>(
228    title: impl text::IntoFragment<'a>,
229    supporting_text: impl text::IntoFragment<'a>,
230    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
231    alpha: f32,
232) -> Container<'a, Message, Theme, Renderer>
233where
234    Message: 'a,
235    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
236    iced_widget::core::Font: Into<Renderer::Font>,
237{
238    alert_content(None, title, supporting_text, actions, alpha)
239}
240
241/// Creates a Material 3 alert dialog with the optional hero icon slot populated.
242pub fn alert_with_icon<'a, Message, Renderer>(
243    icon: impl text::IntoFragment<'a>,
244    title: impl text::IntoFragment<'a>,
245    supporting_text: impl text::IntoFragment<'a>,
246    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
247) -> Container<'a, Message, Theme, Renderer>
248where
249    Message: 'a,
250    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
251    iced_widget::core::Font: Into<Renderer::Font>,
252{
253    alert_with_icon_alpha(icon, title, supporting_text, actions, 1.0)
254}
255
256/// Creates a Material 3 alert dialog with an icon and Android window alpha applied.
257pub fn alert_with_icon_alpha<'a, Message, Renderer>(
258    icon: impl text::IntoFragment<'a>,
259    title: impl text::IntoFragment<'a>,
260    supporting_text: impl text::IntoFragment<'a>,
261    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
262    alpha: f32,
263) -> Container<'a, Message, Theme, Renderer>
264where
265    Message: 'a,
266    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
267    iced_widget::core::Font: Into<Renderer::Font>,
268{
269    alert_content(
270        Some(icon.into_fragment()),
271        title,
272        supporting_text,
273        actions,
274        alpha,
275    )
276}
277
278/// Creates a right-aligned Material 3 dialog actions row.
279pub fn actions<'a, Message, Renderer>(
280    buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
281) -> Container<'a, Message, Theme, Renderer>
282where
283    Message: 'a,
284    Renderer: iced_widget::core::Renderer + 'a,
285{
286    Container::new(
287        Row::with_children(buttons.into_iter())
288            .spacing(tokens::component::dialog::ACTIONS_HORIZONTAL_SPACING)
289            .align_y(alignment::Vertical::Center),
290    )
291    .width(Length::Fill)
292    .align_x(alignment::Horizontal::Right)
293}
294
295/// Creates a Material 3 dialog text action.
296pub fn action<'a, Message, Renderer>(
297    label: impl text::IntoFragment<'a>,
298) -> super::button::Button<'a, Message, Renderer>
299where
300    Message: Clone + 'a,
301    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
302{
303    super::button::text(label)
304}
305
306/// Creates a Material 3 dialog text action with an on-press message.
307pub fn action_button<'a, Message, Renderer>(
308    label: impl text::IntoFragment<'a>,
309    on_press: Message,
310) -> Element<'a, Message, Theme, Renderer>
311where
312    Message: Clone + 'a,
313    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
314{
315    action(label).on_press(on_press).into()
316}
317
318/// Creates a Material 3 dialog text action with Android window alpha applied.
319pub fn action_alpha<'a, Message, Renderer>(
320    label: impl text::IntoFragment<'a>,
321    alpha: f32,
322) -> super::button::Button<'a, Message, Renderer>
323where
324    Message: Clone + 'a,
325    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
326{
327    action(label).style(move |theme, status| action_style_alpha(theme, status, alpha))
328}
329
330/// Creates a Material 3 dialog text action button with Android window alpha applied.
331pub fn action_button_alpha<'a, Message, Renderer>(
332    label: impl text::IntoFragment<'a>,
333    on_press: Message,
334    alpha: f32,
335) -> Element<'a, Message, Theme, Renderer>
336where
337    Message: Clone + 'a,
338    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
339{
340    action_alpha(label, alpha).on_press(on_press).into()
341}
342
343/// Creates a Material 3 modal dialog scrim behind overlay content.
344pub fn scrim<'a, Message, Renderer>(
345    content: impl Into<Element<'a, Message, Theme, Renderer>>,
346) -> Container<'a, Message, Theme, Renderer>
347where
348    Message: 'a,
349    Renderer: iced_widget::core::Renderer + 'a,
350{
351    scrim_alpha(content, 1.0)
352}
353
354/// Creates a Material 3 modal dialog scrim with Android dim fade alpha applied.
355pub fn scrim_alpha<'a, Message, Renderer>(
356    content: impl Into<Element<'a, Message, Theme, Renderer>>,
357    alpha: f32,
358) -> Container<'a, Message, Theme, Renderer>
359where
360    Message: 'a,
361    Renderer: iced_widget::core::Renderer + 'a,
362{
363    Container::new(content)
364        .width(Length::Fill)
365        .height(Length::Fill)
366        .style(move |theme| scrim_style_alpha(theme, alpha))
367}
368
369/// Centers a Material 3 dialog surface over a modal scrim.
370pub fn modal_overlay<'a, Message, Renderer>(
371    content: impl Into<Element<'a, Message, Theme, Renderer>>,
372) -> Container<'a, Message, Theme, Renderer>
373where
374    Message: 'a,
375    Renderer: iced_widget::core::Renderer + 'a,
376{
377    scrim(
378        Container::new(content)
379            .width(Length::Fill)
380            .height(Length::Fill)
381            .align_x(alignment::Horizontal::Center)
382            .align_y(alignment::Vertical::Center),
383    )
384}
385
386/// Creates an event-blocking Material 3 modal dialog layer.
387///
388/// The scrim and dialog surface both absorb mouse presses, so clicks outside
389/// the dialog do not pass through to content underneath and do not require a
390/// no-op application message.
391pub fn modal_layer<'a, Message, Renderer>(
392    content: impl Into<Element<'a, Message, Theme, Renderer>>,
393) -> Element<'a, Message, Theme, Renderer>
394where
395    Message: 'a,
396    Renderer: iced_widget::core::Renderer + 'a,
397{
398    let scrim = opaque(scrim(Space::new().width(Length::Fill).height(Length::Fill)));
399    let dialog = opaque(Container::new(content).center(Length::Fill));
400
401    Stack::with_children([scrim, dialog])
402        .width(Length::Fill)
403        .height(Length::Fill)
404        .into()
405}
406
407/// Creates an event-blocking Material 3 modal dialog layer animated like Android.
408pub fn modal_layer_animated<'a, Message, Renderer>(
409    content: impl Into<Element<'a, Message, Theme, Renderer>>,
410    transition: &Transition,
411    now: Instant,
412) -> Element<'a, Message, Theme, Renderer>
413where
414    Message: 'a,
415    Renderer: iced_widget::core::Renderer + 'a,
416{
417    let scrim = opaque(scrim_alpha(
418        Space::new().width(Length::Fill).height(Length::Fill),
419        transition.scrim_alpha(now),
420    ));
421    let dialog = opaque(
422        Container::new(scaled(
423            content,
424            transition.scale(now),
425            transition.phase() != TransitionPhase::Dismissing,
426        ))
427        .center(Length::Fill),
428    );
429
430    Stack::with_children([scrim, dialog])
431        .width(Length::Fill)
432        .height(Length::Fill)
433        .into()
434}
435
436/// Places a Material 3 modal dialog layer over existing content.
437pub fn modal<'a, Message, Renderer>(
438    content: impl Into<Element<'a, Message, Theme, Renderer>>,
439    dialog: impl Into<Element<'a, Message, Theme, Renderer>>,
440) -> Element<'a, Message, Theme, Renderer>
441where
442    Message: 'a,
443    Renderer: iced_widget::core::Renderer + 'a,
444{
445    Stack::with_children([content.into(), modal_layer(dialog)])
446        .width(Length::Fill)
447        .height(Length::Fill)
448        .into()
449}
450
451/// Places an Android-animated Material 3 modal dialog layer over existing content.
452pub fn modal_animated<'a, Message, Renderer>(
453    content: impl Into<Element<'a, Message, Theme, Renderer>>,
454    transition: &Transition,
455    now: Instant,
456    dialog: 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 content = content.into();
463
464    if !transition.is_active() {
465        return content;
466    }
467
468    Stack::with_children([content, modal_layer_animated(dialog, transition, now)])
469        .width(Length::Fill)
470        .height(Length::Fill)
471        .into()
472}
473
474fn alert_content<'a, Message, Renderer>(
475    icon: Option<text::Fragment<'a>>,
476    title: impl text::IntoFragment<'a>,
477    supporting_text: impl text::IntoFragment<'a>,
478    actions: impl Into<Element<'a, Message, Theme, Renderer>>,
479    alpha: f32,
480) -> Container<'a, Message, Theme, Renderer>
481where
482    Message: 'a,
483    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
484    iced_widget::core::Font: Into<Renderer::Font>,
485{
486    let title_alignment = title_alignment(icon.is_some());
487    let mut content = Column::new().width(Length::Fill);
488
489    if let Some(icon) = icon {
490        content = content.push(
491            Container::new(icon_text(icon, alpha))
492                .width(Length::Fill)
493                .padding(Padding {
494                    top: 0.0,
495                    right: 0.0,
496                    bottom: tokens::component::dialog::ICON_BOTTOM_PADDING,
497                    left: 0.0,
498                })
499                .align_x(alignment::Horizontal::Center),
500        );
501    }
502
503    content = content.push(
504        Container::new(title_text(title, title_alignment, alpha))
505            .width(Length::Fill)
506            .padding(Padding {
507                top: 0.0,
508                right: 0.0,
509                bottom: tokens::component::dialog::TITLE_BOTTOM_PADDING,
510                left: 0.0,
511            }),
512    );
513
514    content = content.push(
515        Container::new(supporting_text_view(supporting_text, alpha))
516            .width(Length::Fill)
517            .padding(Padding {
518                top: 0.0,
519                right: 0.0,
520                bottom: tokens::component::dialog::SUPPORTING_TEXT_BOTTOM_PADDING,
521                left: 0.0,
522            }),
523    );
524
525    content = content.push(actions.into());
526
527    basic_alpha(content, alpha)
528}
529
530fn icon_text<'a, Renderer>(icon: text::Fragment<'a>, alpha: f32) -> Text<'a, Theme, Renderer>
531where
532    Renderer: core_text::Renderer + 'a,
533    iced_widget::core::Font: Into<Renderer::Font>,
534{
535    fonts::filled_icon(icon, tokens::component::dialog::ICON_SIZE)
536        .width(Length::Fixed(tokens::component::dialog::ICON_SIZE))
537        .height(Length::Fixed(tokens::component::dialog::ICON_SIZE))
538        .center()
539        .style(move |theme| icon_style_alpha(theme, alpha))
540}
541
542fn title_text<'a, Renderer>(
543    title: impl text::IntoFragment<'a>,
544    alignment: alignment::Horizontal,
545    alpha: f32,
546) -> Text<'a, Theme, Renderer>
547where
548    Renderer: core_text::Renderer + 'a,
549{
550    let scale = tokens::component::dialog::HEADLINE_TEXT;
551
552    Text::new(title)
553        .size(scale.size)
554        .line_height(absolute_line_height(scale.line_height))
555        .width(Length::Fill)
556        .align_x(alignment)
557        .color_maybe(None::<iced_widget::core::Color>)
558        .style(move |theme| title_style_alpha(theme, alpha))
559}
560
561fn title_alignment(has_icon: bool) -> alignment::Horizontal {
562    if has_icon {
563        alignment::Horizontal::Center
564    } else {
565        alignment::Horizontal::Left
566    }
567}
568
569fn supporting_text_view<'a, Renderer>(
570    supporting_text: impl text::IntoFragment<'a>,
571    alpha: f32,
572) -> Text<'a, Theme, Renderer>
573where
574    Renderer: core_text::Renderer + 'a,
575{
576    let scale = tokens::component::dialog::SUPPORTING_TEXT;
577
578    Text::new(supporting_text)
579        .size(scale.size)
580        .line_height(absolute_line_height(scale.line_height))
581        .width(Length::Fill)
582        .color_maybe(None::<iced_widget::core::Color>)
583        .style(move |theme| supporting_text_style_alpha(theme, alpha))
584}
585
586fn container_style(theme: &Theme) -> iced_widget::container::Style {
587    let colors = theme.colors();
588
589    iced_widget::container::Style {
590        background: Some(Background::Color(colors.surface.container.high)),
591        text_color: Some(colors.surface.text_variant),
592        border: border::rounded(tokens::component::dialog::CONTAINER_SHAPE),
593        shadow: shadow_from_level(
594            tokens::component::dialog::CONTAINER_ELEVATION_LEVEL,
595            colors.shadow,
596        ),
597        snap: cfg!(feature = "crisp"),
598    }
599}
600
601fn container_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
602    let mut style = container_style(theme);
603
604    style.background = style
605        .background
606        .map(|background| background.scale_alpha(alpha));
607    style.text_color = style.text_color.map(|color| alpha_color(color, alpha));
608    style.border.color = alpha_color(style.border.color, alpha);
609    style.shadow.color = alpha_color(style.shadow.color, alpha);
610
611    style
612}
613
614fn icon_style(theme: &Theme) -> iced_widget::text::Style {
615    iced_widget::text::Style {
616        color: Some(theme.colors().secondary.color),
617    }
618}
619
620fn icon_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::text::Style {
621    iced_widget::text::Style {
622        color: icon_style(theme)
623            .color
624            .map(|color| alpha_color(color, alpha)),
625    }
626}
627
628fn title_style(theme: &Theme) -> iced_widget::text::Style {
629    iced_widget::text::Style {
630        color: Some(theme.colors().surface.text),
631    }
632}
633
634fn title_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::text::Style {
635    iced_widget::text::Style {
636        color: title_style(theme)
637            .color
638            .map(|color| alpha_color(color, alpha)),
639    }
640}
641
642fn supporting_text_style(theme: &Theme) -> iced_widget::text::Style {
643    iced_widget::text::Style {
644        color: Some(theme.colors().surface.text_variant),
645    }
646}
647
648fn supporting_text_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::text::Style {
649    iced_widget::text::Style {
650        color: supporting_text_style(theme)
651            .color
652            .map(|color| alpha_color(color, alpha)),
653    }
654}
655
656fn action_style_alpha(theme: &Theme, status: Status, alpha: f32) -> Style {
657    let mut style = crate::style::button::text(theme, status);
658
659    style.background = style
660        .background
661        .map(|background| background.scale_alpha(alpha));
662    style.text_color = alpha_color(style.text_color, alpha);
663    style.border.color = alpha_color(style.border.color, alpha);
664    style.shadow.color = alpha_color(style.shadow.color, alpha);
665
666    style
667}
668
669fn scrim_style_alpha(theme: &Theme, alpha: f32) -> iced_widget::container::Style {
670    iced_widget::container::Style {
671        background: Some(Background::Color(alpha_color(
672            Color {
673                a: 1.0,
674                ..theme.colors().scrim
675            },
676            tokens::component::dialog::SCRIM_OPACITY * alpha,
677        ))),
678        text_color: Some(theme.colors().surface.text),
679        ..iced_widget::container::Style::default()
680    }
681}
682
683fn scaled<'a, Message, Renderer>(
684    content: impl Into<Element<'a, Message, Theme, Renderer>>,
685    scale: f32,
686    interactive: bool,
687) -> Element<'a, Message, Theme, Renderer>
688where
689    Message: 'a,
690    Renderer: iced_widget::core::Renderer + 'a,
691{
692    Element::new(Scaled {
693        content: content.into(),
694        scale,
695        interactive,
696    })
697}
698
699struct Scaled<'a, Message, Renderer> {
700    content: Element<'a, Message, Theme, Renderer>,
701    scale: f32,
702    interactive: bool,
703}
704
705struct ScaledTag;
706
707impl<Message, Renderer> std::fmt::Debug for Scaled<'_, Message, Renderer> {
708    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
709        f.debug_struct("Scaled")
710            .field("scale", &self.scale)
711            .field("interactive", &self.interactive)
712            .finish_non_exhaustive()
713    }
714}
715
716impl<Message, Renderer> Scaled<'_, Message, Renderer> {
717    fn transformation(&self, bounds: Rectangle) -> Transformation {
718        Transformation::translate(bounds.center_x(), bounds.center_y())
719            * Transformation::scale(self.scale)
720            * Transformation::translate(-bounds.center_x(), -bounds.center_y())
721    }
722}
723
724impl<Message, Renderer> Widget<Message, Theme, Renderer> for Scaled<'_, Message, Renderer>
725where
726    Renderer: iced_widget::core::Renderer,
727{
728    fn tag(&self) -> tree::Tag {
729        tree::Tag::of::<ScaledTag>()
730    }
731
732    fn state(&self) -> tree::State {
733        tree::State::None
734    }
735
736    fn children(&self) -> Vec<Tree> {
737        vec![Tree::new(&self.content)]
738    }
739
740    fn diff(&self, tree: &mut Tree) {
741        tree.diff_children(std::slice::from_ref(&self.content));
742    }
743
744    fn size(&self) -> Size<Length> {
745        self.content.as_widget().size()
746    }
747
748    fn size_hint(&self) -> Size<Length> {
749        self.content.as_widget().size_hint()
750    }
751
752    fn layout(
753        &mut self,
754        tree: &mut Tree,
755        renderer: &Renderer,
756        limits: &layout::Limits,
757    ) -> layout::Node {
758        self.content
759            .as_widget_mut()
760            .layout(&mut tree.children[0], renderer, limits)
761    }
762
763    fn operate(
764        &mut self,
765        tree: &mut Tree,
766        layout: Layout<'_>,
767        renderer: &Renderer,
768        operation: &mut dyn widget::Operation,
769    ) {
770        self.content
771            .as_widget_mut()
772            .operate(&mut tree.children[0], layout, renderer, operation);
773    }
774
775    fn update(
776        &mut self,
777        tree: &mut Tree,
778        event: &Event,
779        layout: Layout<'_>,
780        cursor: mouse::Cursor,
781        renderer: &Renderer,
782        clipboard: &mut dyn Clipboard,
783        shell: &mut Shell<'_, Message>,
784        viewport: &Rectangle,
785    ) {
786        if !self.interactive {
787            if !matches!(
788                event,
789                Event::Window(iced_widget::core::window::Event::RedrawRequested(_))
790            ) {
791                return;
792            }
793        }
794
795        let inverse = self.transformation(layout.bounds()).inverse();
796        let cursor = if self.interactive {
797            cursor * inverse
798        } else {
799            mouse::Cursor::Unavailable
800        };
801
802        self.content.as_widget_mut().update(
803            &mut tree.children[0],
804            event,
805            layout,
806            cursor,
807            renderer,
808            clipboard,
809            shell,
810            &(*viewport * inverse),
811        );
812    }
813
814    fn mouse_interaction(
815        &self,
816        tree: &Tree,
817        layout: Layout<'_>,
818        cursor: mouse::Cursor,
819        viewport: &Rectangle,
820        renderer: &Renderer,
821    ) -> mouse::Interaction {
822        if !self.interactive {
823            return mouse::Interaction::None;
824        }
825
826        let inverse = self.transformation(layout.bounds()).inverse();
827
828        self.content.as_widget().mouse_interaction(
829            &tree.children[0],
830            layout,
831            cursor * inverse,
832            &(*viewport * inverse),
833            renderer,
834        )
835    }
836
837    fn draw(
838        &self,
839        tree: &Tree,
840        renderer: &mut Renderer,
841        theme: &Theme,
842        style: &renderer::Style,
843        layout: Layout<'_>,
844        cursor: mouse::Cursor,
845        viewport: &Rectangle,
846    ) {
847        let Some(layer_bounds) = scaled_layer_bounds(layout.bounds(), viewport) else {
848            return;
849        };
850        let transformation = self.transformation(layout.bounds());
851        let inverse = transformation.inverse();
852        let cursor = if self.interactive {
853            cursor * inverse
854        } else {
855            mouse::Cursor::Unavailable
856        };
857
858        renderer.with_layer(layer_bounds, |renderer| {
859            renderer.with_transformation(transformation, |renderer| {
860                self.content.as_widget().draw(
861                    &tree.children[0],
862                    renderer,
863                    theme,
864                    style,
865                    layout,
866                    cursor,
867                    &(*viewport * inverse),
868                );
869            });
870        });
871    }
872
873    fn overlay<'b>(
874        &'b mut self,
875        tree: &'b mut Tree,
876        layout: Layout<'b>,
877        renderer: &Renderer,
878        viewport: &Rectangle,
879        translation: Vector,
880    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
881        if !self.interactive {
882            return None;
883        }
884
885        let transformation = self.transformation(layout.bounds());
886        let inverse = transformation.inverse();
887
888        self.content.as_widget_mut().overlay(
889            &mut tree.children[0],
890            layout,
891            renderer,
892            &(*viewport * inverse),
893            translation + transformation.translation(),
894        )
895    }
896}
897
898fn scaled_layer_bounds(bounds: Rectangle, viewport: &Rectangle) -> Option<Rectangle> {
899    bounds.intersection(viewport).map(|_| *viewport)
900}
901
902fn android_decelerate(progress: f32, factor: f32) -> f32 {
903    let progress = progress.clamp(0.0, 1.0);
904
905    // Mirrors Android's DecelerateInterpolator: 1 - (1 - t) ^ (2 * factor).
906    if (factor - 1.0).abs() <= f32::EPSILON {
907        1.0 - (1.0 - progress) * (1.0 - progress)
908    } else {
909        1.0 - (1.0 - progress).powf(2.0 * factor)
910    }
911}
912
913#[cfg(test)]
914#[path = "../../../tests/widget/component/dialog.rs"]
915mod tests;