iced_dialog/
dialog.rs

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