iced_dialog/
dialog.rs

1//! Dialogs can be used to provide users with
2//! important information and make them act on it.
3use iced_core::{
4    self as core, Color, Element, Length, Padding, Pixels, alignment, color,
5};
6use iced_widget::{
7    Column, Container, Row, Theme, container, mouse_area, opaque, stack, text,
8    text::{Fragment, IntoFragment},
9    vertical_space,
10};
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
27/// [`DEFAULT_MAX_WIDTH`]: Dialog::DEFAULT_MAX_WIDTH
28/// [`DEFAULT_MAX_HEIGHT`]: Dialog::DEFAULT_MAX_HEIGHT
29pub struct Dialog<
30    'a,
31    Message,
32    Theme = iced_widget::Theme,
33    Renderer = iced_widget::Renderer,
34> where
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: Padding,
53    button_alignment: alignment::Vertical,
54    class: <Theme as Catalog>::Class<'a>,
55    title_class: <Theme as text::Catalog>::Class<'a>,
56    container_class: <Theme as container::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    /// The default maximum width of a [`Dialog`].
66    ///
67    /// Check the main documentation of [`Dialog`] to see when this is used.
68    pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
69
70    /// The default maximum height of a [`Dialog`].
71    ///
72    /// Check the main documentation of [`Dialog`] to see when this is used.
73    pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
74
75    /// Creates a new [`Dialog`] with the given base and dialog content.
76    pub fn new(
77        is_open: bool,
78        base: impl Into<Element<'a, Message, Theme, Renderer>>,
79        content: impl Into<Element<'a, Message, Theme, Renderer>>,
80    ) -> Self {
81        Self::with_buttons(is_open, base, content, Vec::new())
82    }
83
84    /// Creates a new [`Dialog`] with the given base, dialog content and buttons.
85    pub fn with_buttons(
86        is_open: bool,
87        base: impl Into<Element<'a, Message, Theme, Renderer>>,
88        content: impl Into<Element<'a, Message, Theme, Renderer>>,
89        buttons: Vec<Element<'a, Message, Theme, Renderer>>,
90    ) -> Self {
91        let content = content.into();
92        let size = content.as_widget().size_hint();
93
94        Self {
95            is_open,
96            base: base.into(),
97            title: None,
98            content,
99            buttons,
100            on_press: None,
101            font: None,
102            width: size.width.fluid(),
103            height: size.height.fluid(),
104            max_width: None,
105            max_height: None,
106            horizontal_alignment: alignment::Horizontal::Center,
107            vertical_alignment: alignment::Vertical::Center,
108            spacing: 8.0,
109            padding: 24.into(),
110            button_alignment: alignment::Vertical::Top,
111            class: <Theme as Catalog>::default(),
112            title_class: <Theme as Catalog>::default_title(),
113            container_class: <Theme as Catalog>::default_container(),
114        }
115    }
116
117    /// Sets the [`Dialog`]'s title.
118    pub fn title(mut self, title: impl IntoFragment<'a>) -> Self {
119        self.title = Some(title.into_fragment());
120        self
121    }
122
123    /// Sets the message that will be produced when the [`Dialog`]'s backdrop is pressed.
124    pub fn on_press(mut self, on_press: Message) -> Self
125    where
126        Message: Clone,
127    {
128        self.on_press = Some(Box::new(move || on_press.clone()));
129        self
130    }
131
132    /// Sets the message that will be produced when the [`Dialog`]'s backdrop is pressed.
133    ///
134    /// This is analogous to [`Dialog::on_press`], but using a closure to produce
135    /// the message.
136    pub fn on_press_with(
137        mut self,
138        on_press: impl Fn() -> Message + 'a,
139    ) -> Self {
140        self.on_press = Some(Box::new(on_press));
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 padding.
211    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
212        self.padding = padding.into();
213        self
214    }
215
216    /// Sets the [`Dialog`]'s spacing.
217    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
218        self.spacing = spacing.into().0;
219        self
220    }
221
222    /// Sets the vertical alignment of the [`Dialog`]'s buttons.
223    pub fn align_buttons(
224        mut self,
225        align: impl Into<alignment::Vertical>,
226    ) -> Self {
227        self.button_alignment = align.into();
228        self
229    }
230
231    /// Sets the [`Font`] of the [`Dialog`]'s title.
232    ///
233    /// [`Font`]: https://docs.iced.rs/iced_core/text/trait.Renderer.html#associatedtype.Font
234    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
235        self.font = Some(font.into());
236        self
237    }
238
239    /// Adds a button to the [`Dialog`].
240    pub fn push_button(
241        mut self,
242        button: impl Into<Element<'a, Message, Theme, Renderer>>,
243    ) -> Self {
244        self.buttons.push(button.into());
245        self
246    }
247
248    /// Adds a button to the [`Dialog`], if `Some`.
249    pub fn push_button_maybe(
250        self,
251        button: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
252    ) -> Self {
253        if let Some(button) = button {
254            self.push_button(button)
255        } else {
256            self
257        }
258    }
259
260    /// Extends the [`Dialog`] with the given buttons.
261    pub fn extend_buttons(
262        self,
263        buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
264    ) -> Self {
265        buttons.into_iter().fold(self, Self::push_button)
266    }
267
268    /// Sets the backdrop color of the [`Dialog`].
269    pub fn backdrop(self, color: impl Into<Color>) -> Self
270    where
271        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
272    {
273        let backdrop_color = color.into();
274
275        self.style(move |_theme| Style { backdrop_color })
276    }
277
278    /// Sets the style of the [`Dialog`].
279    #[must_use]
280    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
281    where
282        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
283    {
284        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
285        self
286    }
287
288    /// Sets the style of the [`Dialog`]'s title.
289    #[must_use]
290    pub fn title_style(
291        mut self,
292        style: impl Fn(&Theme) -> text::Style + 'a,
293    ) -> Self
294    where
295        <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
296    {
297        self.title_class = (Box::new(style) as text::StyleFn<'a, Theme>).into();
298        self
299    }
300
301    /// Sets the style of the [`Dialog`]'s container.
302    #[must_use]
303    pub fn container_style(
304        mut self,
305        style: impl Fn(&Theme) -> container::Style + 'a,
306    ) -> Self
307    where
308        <Theme as container::Catalog>::Class<'a>:
309            From<container::StyleFn<'a, Theme>>,
310    {
311        self.container_class =
312            (Box::new(style) as container::StyleFn<'a, Theme>).into();
313        self
314    }
315
316    /// Sets the style class of the [`Dialog`].
317    #[must_use]
318    pub fn class(
319        mut self,
320        class: impl Into<<Theme as Catalog>::Class<'a>>,
321    ) -> Self {
322        self.class = class.into();
323        self
324    }
325
326    /// Sets the style class of the [`Dialog`]'s title.
327    #[must_use]
328    pub fn title_class(
329        mut self,
330        class: impl Into<<Theme as text::Catalog>::Class<'a>>,
331    ) -> Self {
332        self.title_class = class.into();
333        self
334    }
335
336    /// Sets the style class of the [`Dialog`]'s container.
337    #[must_use]
338    pub fn container_class(
339        mut self,
340        class: impl Into<<Theme as container::Catalog>::Class<'a>>,
341    ) -> Self {
342        self.container_class = class.into();
343        self
344    }
345
346    fn view(self) -> Element<'a, Message, Theme, Renderer>
347    where
348        <Theme as container::Catalog>::Class<'a>:
349            From<container::StyleFn<'a, Theme>>,
350    {
351        let dialog = self.is_open.then(|| {
352            let has_title = self.title.is_some();
353            let has_buttons = !self.buttons.is_empty();
354
355            let contents = Container::new(
356                Column::new()
357                    .push_maybe(self.title.map(|title| {
358                        let text = text(title)
359                            .size(20)
360                            .line_height(text::LineHeight::Absolute(Pixels(
361                                26.0,
362                            )))
363                            .class(self.title_class);
364
365                        if let Some(font) = self.font {
366                            text.font(font)
367                        } else {
368                            text
369                        }
370                    }))
371                    .push_maybe(
372                        has_title.then_some(vertical_space().height(12)),
373                    )
374                    .push(self.content),
375            )
376            .padding(self.padding);
377
378            let contents = if has_buttons {
379                contents.width(Length::Fill)
380            } else {
381                contents
382            };
383
384            let buttons = has_buttons.then_some(
385                Container::new(
386                    Row::with_children(self.buttons)
387                        .spacing(self.spacing)
388                        .align_y(self.button_alignment),
389                )
390                .height(80)
391                .padding(self.padding),
392            );
393
394            let max_width = self.max_width.unwrap_or(
395                if has_buttons && !matches!(self.width, Length::Fixed(_)) {
396                    Self::DEFAULT_MAX_WIDTH
397                } else {
398                    f32::INFINITY
399                },
400            );
401
402            let max_height = self.max_height.unwrap_or(
403                if has_buttons && !matches!(self.height, Length::Fixed(_)) {
404                    Self::DEFAULT_MAX_HEIGHT
405                } else {
406                    f32::INFINITY
407                },
408            );
409
410            Container::new(
411                Column::new()
412                    .push(contents)
413                    .push_maybe(has_buttons.then_some(vertical_space()))
414                    .push_maybe(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
424        modal(
425            self.base,
426            dialog,
427            self.on_press,
428            self.horizontal_alignment,
429            self.vertical_alignment,
430            self.class,
431        )
432    }
433}
434
435impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
436    for Element<'a, Message, Theme, Renderer>
437where
438    Renderer: 'a + core::Renderer + core::text::Renderer,
439    Theme: 'a + Catalog,
440    Message: 'a + Clone,
441    <Theme as container::Catalog>::Class<'a>:
442        From<container::StyleFn<'a, Theme>>,
443{
444    fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
445        dialog.view()
446    }
447}
448
449fn modal<'a, Message, Theme, Renderer>(
450    base: impl Into<Element<'a, Message, Theme, Renderer>>,
451    content: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
452    on_press: Option<impl Fn() -> Message + 'a>,
453    horizontal_alignment: alignment::Horizontal,
454    vertical_alignment: alignment::Vertical,
455    class: <Theme as Catalog>::Class<'a>,
456) -> Element<'a, Message, Theme, Renderer>
457where
458    Message: 'a + Clone,
459    Renderer: 'a + core::Renderer,
460    Theme: 'a + container::Catalog + Catalog,
461    <Theme as container::Catalog>::Class<'a>:
462        From<container::StyleFn<'a, Theme>>,
463{
464    let area = content.map(|content| {
465        let backdrop = mouse_area(
466            container(opaque(content))
467                .style(move |theme| container::Style {
468                    background: Some(
469                        Catalog::style(theme, &class).backdrop_color.into(),
470                    ),
471                    ..Default::default()
472                })
473                .width(Length::Fill)
474                .height(Length::Fill)
475                .align_x(horizontal_alignment)
476                .align_y(vertical_alignment),
477        );
478
479        if let Some(on_press) = on_press {
480            opaque(backdrop.on_press(on_press()))
481        } else {
482            opaque(backdrop)
483        }
484    });
485
486    stack![base.into()].push_maybe(area).into()
487}
488
489/// The style of a [`Dialog`].
490#[derive(Debug, Clone, Copy, PartialEq)]
491pub struct Style {
492    /// The [`Dialog`]'s backdrop.
493    pub backdrop_color: Color,
494}
495
496/// The theme catalog of a [`Dialog`].
497pub trait Catalog: text::Catalog + container::Catalog {
498    /// The item class of the [`Catalog`].
499    type Class<'a>;
500
501    /// The default class produced by the [`Catalog`].
502    fn default<'a>() -> <Self as Catalog>::Class<'a>;
503
504    /// The default class for the [`Dialog`]'s title.
505    fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
506        <Self as text::Catalog>::default()
507    }
508
509    /// The default class for the [`Dialog`]'s container.
510    fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
511        <Self as container::Catalog>::default()
512    }
513
514    /// The [`Style`] of a class.
515    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
516}
517
518/// A styling function for a [`Dialog`].
519pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
520
521impl Catalog for Theme {
522    type Class<'a> = StyleFn<'a, Self>;
523
524    fn default<'a>() -> <Self as Catalog>::Class<'a> {
525        Box::new(default)
526    }
527
528    fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
529        Box::new(|theme| {
530            container::background(
531                theme.extended_palette().background.base.color,
532            )
533        })
534    }
535
536    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
537        class(self)
538    }
539}
540
541/// The default style of a [`Dialog`].
542pub fn default(_theme: &Theme) -> Style {
543    Style {
544        backdrop_color: color!(0x000000, 0.3),
545    }
546}