1use 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#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct Transition {
24 phase: TransitionPhase,
25 started_at: Option<Instant>,
26}
27
28#[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 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 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 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 pub fn is_active(&self) -> bool {
89 self.phase != TransitionPhase::Hidden
90 }
91
92 pub fn is_animating(&self) -> bool {
94 matches!(
95 self.phase,
96 TransitionPhase::Showing | TransitionPhase::Dismissing
97 )
98 }
99
100 pub fn phase(&self) -> TransitionPhase {
102 self.phase
103 }
104
105 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 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 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#[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 pub fn alpha(mut self, alpha: f32) -> Self {
200 self.alpha = alpha;
201 self
202 }
203}
204
205#[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 pub fn icon(mut self, icon: impl text::IntoFragment<'a>) -> Self {
224 self.icon = Some(icon.into_fragment());
225 self
226 }
227
228 pub fn alpha(mut self, alpha: f32) -> Self {
230 self.alpha = alpha;
231 self
232 }
233}
234
235pub 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
246pub 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
262pub 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
276pub 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
301pub 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
315pub 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
341pub 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
358pub 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
369pub 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
381pub 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
394pub 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
407pub 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
418pub 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
433pub 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
450pub 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
471pub 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
500pub 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
515pub 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 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;