Skip to main content

iced_m3/widget/
dialog.rs

1//! Dialogs can be used to provide users with
2//! important information and make them act on it.
3use iced::{Background, border::Radius, color};
4use iced_widget::{
5    Container, Row, Theme, column, container, mouse_area, opaque,
6    space::vertical,
7    stack, text,
8    text::{Fragment, IntoFragment},
9};
10
11use iced_widget::core::{self, Color, Element, Length, Padding, Pixels, alignment};
12
13use crate::{
14    style::shadow,
15    theme::{ColorScheme, Palette},
16};
17
18/// A message dialog.
19///
20/// Only the content is required, [`buttons`] and the [`title`] are optional.
21///
22/// The sizing strategy used depends on whether you add any buttons: if you don't, the dialog will
23/// be sized to fit the content similarly to a container. If you *do* add buttons, [`max_width`]
24/// & [`max_height`] (or [`width`] & [`height`] when set to a fixed pixel value) are used. If
25/// these aren't set, [`DEFAULT_MAX_WIDTH`] and/or [`DEFAULT_MAX_HEIGHT`] are used.
26///
27/// [`buttons`]: Dialog::with_buttons
28/// [`title`]: Dialog::title
29/// [`max_width`]: Dialog::max_width
30/// [`max_height`]: Dialog::max_height
31/// [`width`]: Dialog::width
32/// [`height`]: Dialog::height
33pub struct Dialog<'a, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
34where
35    Renderer: 'a + core::text::Renderer,
36    Theme: 'a + Catalog,
37{
38    is_open: bool,
39    base: Element<'a, Message, Theme, Renderer>,
40    title: Option<Fragment<'a>>,
41    content: Element<'a, Message, Theme, Renderer>,
42    buttons: Vec<Element<'a, Message, Theme, Renderer>>,
43    on_press: Option<Box<dyn Fn() -> Message + 'a>>,
44    font: Option<Renderer::Font>,
45    width: Length,
46    height: Length,
47    max_width: Option<f32>,
48    max_height: Option<f32>,
49    horizontal_alignment: alignment::Horizontal,
50    vertical_alignment: alignment::Vertical,
51    spacing: f32,
52    padding_inner: Padding,
53    padding_outer: Padding,
54    button_alignment: alignment::Vertical,
55    palette: &'a Palette,
56    class: <Theme as Catalog>::Class<'a>,
57}
58
59impl<'a, Message, Theme, Renderer> Dialog<'a, Message, Theme, Renderer>
60where
61    Renderer: 'a + core::Renderer + core::text::Renderer,
62    Theme: 'a + Catalog,
63    Message: 'a + Clone,
64{
65    /// Creates a new [`Dialog`] with the given base and dialog content.
66    pub fn new(
67        is_open: bool,
68        base: impl Into<Element<'a, Message, Theme, Renderer>>,
69        content: impl Into<Element<'a, Message, Theme, Renderer>>,
70        palette: &'a Palette,
71    ) -> Self {
72        Self::with_buttons(is_open, base, content, Vec::new(), palette)
73    }
74
75    /// Creates a new [`Dialog`] with the given base, dialog content and buttons.
76    pub fn with_buttons(
77        is_open: bool,
78        base: impl Into<Element<'a, Message, Theme, Renderer>>,
79        content: impl Into<Element<'a, Message, Theme, Renderer>>,
80        buttons: Vec<Element<'a, Message, Theme, Renderer>>,
81        palette: &'a Palette,
82    ) -> Self {
83        let content = content.into();
84
85        Self {
86            is_open,
87            base: base.into(),
88            title: None,
89            content,
90            buttons,
91            on_press: None,
92            font: None,
93            width: Length::Shrink,
94            height: Length::Shrink,
95            max_width: None,
96            max_height: None,
97            horizontal_alignment: alignment::Horizontal::Center,
98            vertical_alignment: alignment::Vertical::Center,
99            spacing: 8.0,
100            padding_inner: 24.into(),
101            padding_outer: Padding::ZERO,
102            button_alignment: alignment::Vertical::Top,
103            palette,
104            class: <Theme as Catalog>::default(),
105        }
106    }
107
108    /// Sets the [`Dialog`]'s title.
109    pub fn title(mut self, title: impl IntoFragment<'a>) -> Self {
110        self.title = Some(title.into_fragment());
111        self
112    }
113
114    /// Sets the message that will be produced when the [`Dialog`]'s backdrop is pressed.
115    pub fn on_press(mut self, on_press: Message) -> Self
116    where
117        Message: Clone,
118    {
119        self.on_press = Some(Box::new(move || on_press.clone()));
120        self
121    }
122
123    /// Sets the message that will be produced when the [`Dialog`]'s backdrop is pressed.
124    ///
125    /// This is analogous to [`Dialog::on_press`], but using a closure to produce
126    /// the message.
127    pub fn on_press_with(mut self, on_press: impl Fn() -> Message + 'a) -> Self {
128        self.on_press = Some(Box::new(on_press));
129        self
130    }
131
132    /// Sets the message that will be produced when the [`Dialog`]'s backdrop is pressed, if `Some`.
133    pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self
134    where
135        Message: Clone,
136    {
137        self.on_press = on_press.map(|message| Box::new(move || message.clone()) as _);
138
139        self
140    }
141
142    /// Sets the [`Dialog`]'s width.
143    pub fn width(mut self, width: impl Into<Length>) -> Self {
144        self.width = width.into();
145        self
146    }
147
148    /// Sets the [`Dialog`]'s height.
149    pub fn height(mut self, height: impl Into<Length>) -> Self {
150        self.height = height.into();
151        self
152    }
153
154    /// Sets the [`Dialog`]'s maximum width.
155    pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
156        self.max_width = Some(max_width.into().0);
157        self
158    }
159
160    /// Sets the [`Dialog`]'s maximum height.
161    pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
162        self.max_height = Some(max_height.into().0);
163        self
164    }
165
166    /// Aligns the [`Dialog`] to the left.
167    pub fn align_left(self) -> Self {
168        self.align_x(alignment::Horizontal::Left)
169    }
170
171    /// Aligns the [`Dialog`] to the right.
172    pub fn align_right(self) -> Self {
173        self.align_x(alignment::Horizontal::Right)
174    }
175
176    /// Aligns the [`Dialog`] to the top.
177    pub fn align_top(self) -> Self {
178        self.align_y(alignment::Vertical::Top)
179    }
180
181    /// Aligns the [`Dialog`] to the bottom.
182    pub fn align_bottom(self) -> Self {
183        self.align_y(alignment::Vertical::Bottom)
184    }
185
186    /// Sets the [`Dialog`]'s alignment for the horizontal axis.
187    ///
188    /// [`Dialog`]s are horizontally centered by default.
189    pub fn align_x(mut self, alignment: impl Into<alignment::Horizontal>) -> Self {
190        self.horizontal_alignment = alignment.into();
191        self
192    }
193
194    /// Sets the [`Dialog`]'s alignment for the vertical axis.
195    ///
196    /// [`Dialog`]s are vertically centered by default.
197    pub fn align_y(mut self, alignment: impl Into<alignment::Vertical>) -> Self {
198        self.vertical_alignment = alignment.into();
199        self
200    }
201
202    /// Sets the [`Dialog`]'s inner padding.
203    pub fn padding_inner(mut self, padding: impl Into<Padding>) -> Self {
204        self.padding_inner = padding.into();
205        self
206    }
207
208    /// Sets the [`Dialog`]'s outer padding (sometimes called "margin").
209    pub fn padding_outer(mut self, padding: impl Into<Padding>) -> Self {
210        self.padding_outer = padding.into();
211        self
212    }
213
214    /// Sets the [`Dialog`]'s spacing.
215    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
216        self.spacing = spacing.into().0;
217        self
218    }
219
220    /// Sets the vertical alignment of the [`Dialog`]'s buttons.
221    pub fn align_buttons(mut self, align: impl Into<alignment::Vertical>) -> Self {
222        self.button_alignment = align.into();
223        self
224    }
225
226    /// Sets the [`Font`] of the [`Dialog`]'s title.
227    ///
228    /// [`Font`]: https://docs.iced.rs/iced_core/text/trait.Renderer.html#associatedtype.Font
229    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
230        self.font = Some(font.into());
231        self
232    }
233
234    /// Adds a button to the [`Dialog`].
235    pub fn push_button(mut self, button: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
236        self.buttons.push(button.into());
237        self
238    }
239
240    /// Adds a button to the [`Dialog`], if `Some`.
241    pub fn push_button_maybe(
242        self,
243        button: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
244    ) -> Self {
245        if let Some(button) = button {
246            self.push_button(button)
247        } else {
248            self
249        }
250    }
251
252    /// Extends the [`Dialog`] with the given buttons.
253    pub fn extend_buttons(
254        self,
255        buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
256    ) -> Self {
257        buttons.into_iter().fold(self, Self::push_button)
258    }
259
260    /// Sets the backdrop color of the [`Dialog`].
261    pub fn backdrop(self, color: impl Into<Color>) -> Self
262    where
263        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
264    {
265        let backdrop_color = color.into();
266
267        self.style(move |_theme| Style { backdrop_color })
268    }
269
270    /// Sets the style of the [`Dialog`].
271    #[must_use]
272    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
273    where
274        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
275    {
276        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
277        self
278    }
279
280    /// Sets the style class of the [`Dialog`].
281    #[must_use]
282    pub fn class(mut self, class: impl Into<<Theme as Catalog>::Class<'a>>) -> Self {
283        self.class = class.into();
284        self
285    }
286
287    fn view(self) -> Element<'a, Message, Theme, Renderer>
288    where
289        <Theme as container::Catalog>::Class<'a>: From<container::StyleFn<'a, Theme>>,
290    {
291        let dialog = self.is_open.then(|| {
292            let has_title = self.title.is_some();
293            let has_buttons = !self.buttons.is_empty();
294
295            let contents = Container::new(column![
296                self.title.map(|title| {
297                    let text = text(title)
298                        .size(20)
299                        .line_height(text::LineHeight::Absolute(Pixels(26.0)));
300
301                    if let Some(font) = self.font {
302                        text.font(font)
303                    } else {
304                        text
305                    }
306                }),
307                has_title.then_some(vertical().height(12)),
308                self.content
309            ])
310            .padding(self.padding_inner);
311
312            let contents = if has_buttons {
313                contents.width(Length::Fill)
314            } else {
315                contents
316            };
317
318            let buttons = has_buttons.then_some(
319                Container::new(
320                    Row::with_children(self.buttons)
321                        .spacing(self.spacing)
322                        .align_y(self.button_alignment),
323                )
324                .padding(self.padding_inner),
325            );
326
327            let max_width = self.max_width.unwrap_or(
328                if has_buttons && !matches!(self.width, Length::Fixed(_)) {
329                    DEFAULT_MAX_WIDTH
330                } else {
331                    f32::INFINITY
332                },
333            );
334
335            let max_height = self.max_height.unwrap_or(
336                if has_buttons && !matches!(self.height, Length::Fixed(_)) {
337                    DEFAULT_MAX_HEIGHT
338                } else {
339                    f32::INFINITY
340                },
341            );
342
343            let content = Container::new(column![
344                contents,
345                has_buttons.then_some(vertical()),
346                buttons,
347            ])
348            .width(self.width)
349            .max_width(max_width)
350            .height(self.height)
351            .max_height(max_height)
352            .style(|_| container::Style {
353                background: Some(Background::Color(self.palette.surface_container_high)),
354                border: core::Border {
355                    radius: Radius::from(24),
356                    ..Default::default()
357                },
358                shadow: shadow(self.palette.shadow(), 1.0),
359                ..Default::default()
360            })
361            .clip(true);
362
363            let backdrop = mouse_area(
364                container(opaque(content))
365                    .style(move |theme| container::Style {
366                        background: Some(Catalog::style(theme, &self.class).backdrop_color.into()),
367                        ..Default::default()
368                    })
369                    .padding(self.padding_outer)
370                    .width(Length::Fill)
371                    .height(Length::Fill)
372                    .align_x(self.horizontal_alignment)
373                    .align_y(self.vertical_alignment),
374            );
375
376            if let Some(on_press) = self.on_press {
377                opaque(backdrop.on_press(on_press()))
378            } else {
379                opaque(backdrop)
380            }
381        });
382
383        stack![self.base, dialog].into()
384    }
385}
386
387/// The default maximum width of a [`Dialog`].
388///
389/// Check the main documentation of [`Dialog`] to see when this is used.
390pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
391
392/// The default maximum height of a [`Dialog`].
393///
394/// Check the main documentation of [`Dialog`] to see when this is used.
395pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
396
397impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
398    for Element<'a, Message, Theme, Renderer>
399where
400    Renderer: 'a + core::Renderer + core::text::Renderer,
401    Theme: 'a + Catalog,
402    Message: 'a + Clone,
403    <Theme as container::Catalog>::Class<'a>: From<container::StyleFn<'a, Theme>>,
404{
405    fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
406        dialog.view()
407    }
408}
409
410/// The style of a [`Dialog`].
411#[derive(Debug, Clone, Copy, PartialEq)]
412pub struct Style {
413    /// The [`Dialog`]'s backdrop.
414    pub backdrop_color: Color,
415}
416
417/// The theme catalog of a [`Dialog`].
418pub trait Catalog: text::Catalog + container::Catalog {
419    /// The item class of the [`Catalog`].
420    type Class<'a>;
421
422    /// The default class produced by the [`Catalog`].
423    fn default<'a>() -> <Self as Catalog>::Class<'a>;
424
425    /// The default class for the [`Dialog`]'s title.
426    fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
427        <Self as text::Catalog>::default()
428    }
429
430    /// The default class for the [`Dialog`]'s container.
431    fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
432        <Self as container::Catalog>::default()
433    }
434
435    /// The [`Style`] of a class.
436    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
437}
438
439/// A styling function for a [`Dialog`].
440pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
441
442impl Catalog for Theme {
443    type Class<'a> = StyleFn<'a, Self>;
444
445    fn default<'a>() -> <Self as Catalog>::Class<'a> {
446        Box::new(default)
447    }
448
449    fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
450        Box::new(|_| container::background(color!(0xff0000)))
451    }
452
453    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
454        class(self)
455    }
456}
457
458/// The default style of a [`Dialog`].
459pub fn default<Theme>(_theme: &Theme) -> Style {
460    Style {
461        backdrop_color: core::color!(0x000000, 0.3),
462    }
463}