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