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