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, stack, text,
5    text::{Fragment, IntoFragment},
6    vertical_space,
7};
8
9use crate::core::{self, Color, Element, Length, Padding, Pixels, alignment};
10
11/// A message dialog.
12///
13/// Only the content is required, [`buttons`] and the [`title`] are optional.
14///
15/// The sizing strategy used depends on whether you add any buttons: if you don't, the dialog will
16/// be sized to fit the content similarly to a container. If you *do* add buttons, [`max_width`]
17/// & [`max_height`] (or [`width`] & [`height`] when set to a fixed pixel value) are used. If
18/// these aren't set, [`DEFAULT_MAX_WIDTH`] and/or [`DEFAULT_MAX_HEIGHT`] are used.
19///
20/// [`buttons`]: Dialog::with_buttons
21/// [`title`]: Dialog::title
22/// [`max_width`]: Dialog::max_width
23/// [`max_height`]: Dialog::max_height
24/// [`width`]: Dialog::width
25/// [`height`]: Dialog::height
26pub struct Dialog<
27    'a,
28    Message,
29    Theme = iced_widget::Theme,
30    Renderer = iced_widget::Renderer,
31> where
32    Renderer: 'a + core::text::Renderer,
33    Theme: 'a + Catalog,
34{
35    is_open: bool,
36    base: Element<'a, Message, Theme, Renderer>,
37    title: Option<Fragment<'a>>,
38    content: Element<'a, Message, Theme, Renderer>,
39    buttons: Vec<Element<'a, Message, Theme, Renderer>>,
40    on_press: Option<Box<dyn Fn() -> Message + 'a>>,
41    font: Option<Renderer::Font>,
42    width: Length,
43    height: Length,
44    max_width: Option<f32>,
45    max_height: Option<f32>,
46    horizontal_alignment: alignment::Horizontal,
47    vertical_alignment: alignment::Vertical,
48    spacing: f32,
49    padding_inner: Padding,
50    padding_outer: Padding,
51    button_alignment: alignment::Vertical,
52    class: <Theme as Catalog>::Class<'a>,
53    title_class: <Theme as text::Catalog>::Class<'a>,
54    container_class: <Theme as container::Catalog>::Class<'a>,
55}
56
57impl<'a, Message, Theme, Renderer> Dialog<'a, Message, Theme, Renderer>
58where
59    Renderer: 'a + core::Renderer + core::text::Renderer,
60    Theme: 'a + Catalog,
61    Message: 'a + Clone,
62{
63    /// Creates a new [`Dialog`] with the given base and dialog content.
64    pub fn new(
65        is_open: bool,
66        base: impl Into<Element<'a, Message, Theme, Renderer>>,
67        content: impl Into<Element<'a, Message, Theme, Renderer>>,
68    ) -> Self {
69        Self::with_buttons(is_open, base, content, Vec::new())
70    }
71
72    /// Creates a new [`Dialog`] with the given base, dialog content and buttons.
73    pub fn with_buttons(
74        is_open: bool,
75        base: impl Into<Element<'a, Message, Theme, Renderer>>,
76        content: impl Into<Element<'a, Message, Theme, Renderer>>,
77        buttons: Vec<Element<'a, Message, Theme, Renderer>>,
78    ) -> Self {
79        let content = content.into();
80        let size = content.as_widget().size_hint();
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: size.width.fluid(),
91            height: size.height.fluid(),
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_space().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_space()),
414                buttons,
415            ])
416            .width(self.width)
417            .height(self.height)
418            .max_width(max_width)
419            .max_height(max_height)
420            .class(self.container_class)
421            .clip(true);
422
423            let backdrop = mouse_area(
424                container(opaque(content))
425                    .style(move |theme| container::Style {
426                        background: Some(
427                            Catalog::style(theme, &self.class)
428                                .backdrop_color
429                                .into(),
430                        ),
431                        ..Default::default()
432                    })
433                    .padding(self.padding_outer)
434                    .width(Length::Fill)
435                    .height(Length::Fill)
436                    .align_x(self.horizontal_alignment)
437                    .align_y(self.vertical_alignment),
438            );
439
440            if let Some(on_press) = self.on_press {
441                opaque(backdrop.on_press(on_press()))
442            } else {
443                opaque(backdrop)
444            }
445        });
446
447        stack![self.base, dialog].into()
448    }
449}
450
451/// The default maximum width of a [`Dialog`].
452///
453/// Check the main documentation of [`Dialog`] to see when this is used.
454pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
455
456/// The default maximum height of a [`Dialog`].
457///
458/// Check the main documentation of [`Dialog`] to see when this is used.
459pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
460
461impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
462    for Element<'a, Message, Theme, Renderer>
463where
464    Renderer: 'a + core::Renderer + core::text::Renderer,
465    Theme: 'a + Catalog,
466    Message: 'a + Clone,
467    <Theme as container::Catalog>::Class<'a>:
468        From<container::StyleFn<'a, Theme>>,
469{
470    fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
471        dialog.view()
472    }
473}
474
475/// The style of a [`Dialog`].
476#[derive(Debug, Clone, Copy, PartialEq)]
477pub struct Style {
478    /// The [`Dialog`]'s backdrop.
479    pub backdrop_color: Color,
480}
481
482/// The theme catalog of a [`Dialog`].
483pub trait Catalog: text::Catalog + container::Catalog {
484    /// The item class of the [`Catalog`].
485    type Class<'a>;
486
487    /// The default class produced by the [`Catalog`].
488    fn default<'a>() -> <Self as Catalog>::Class<'a>;
489
490    /// The default class for the [`Dialog`]'s title.
491    fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
492        <Self as text::Catalog>::default()
493    }
494
495    /// The default class for the [`Dialog`]'s container.
496    fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
497        <Self as container::Catalog>::default()
498    }
499
500    /// The [`Style`] of a class.
501    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
502}
503
504/// A styling function for a [`Dialog`].
505pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
506
507impl Catalog for Theme {
508    type Class<'a> = StyleFn<'a, Self>;
509
510    fn default<'a>() -> <Self as Catalog>::Class<'a> {
511        Box::new(default)
512    }
513
514    fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
515        Box::new(|theme| container::background(theme.palette().background))
516    }
517
518    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
519        class(self)
520    }
521}
522
523/// The default style of a [`Dialog`].
524pub fn default<Theme>(_theme: &Theme) -> Style {
525    Style {
526        backdrop_color: core::color!(0x000000, 0.3),
527    }
528}