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, Alignment, Color, Element, Length, Padding, Pixels,
5    alignment::Vertical, color,
6};
7use iced_widget::{
8    Column, Container, Row, Theme, center, container, mouse_area, opaque,
9    stack, text, vertical_space,
10};
11
12/// A message dialog.
13///
14/// Only the content is required, [`buttons`] and the [`title`] are optional.
15///
16/// [`buttons`]: Dialog::with_buttons
17/// [`title`]: Dialog::title
18pub struct Dialog<
19    'a,
20    Message,
21    Theme = iced_widget::Theme,
22    Renderer = iced_widget::Renderer,
23> where
24    Renderer: 'a + core::text::Renderer,
25    Theme: 'a + Catalog,
26{
27    is_open: bool,
28    base: Element<'a, Message, Theme, Renderer>,
29    title: Option<String>,
30    content: Element<'a, Message, Theme, Renderer>,
31    buttons: Vec<Element<'a, Message, Theme, Renderer>>,
32    font: Option<Renderer::Font>,
33    width: Length,
34    height: Length,
35    spacing: f32,
36    padding: Padding,
37    button_alignment: Alignment,
38    class: <Theme as Catalog>::Class<'a>,
39    title_class: <Theme as text::Catalog>::Class<'a>,
40    container_class: <Theme as container::Catalog>::Class<'a>,
41}
42
43impl<'a, Message, Theme, Renderer> Dialog<'a, Message, Theme, Renderer>
44where
45    Renderer: 'a + core::Renderer + core::text::Renderer,
46    Theme: 'a + Catalog,
47    Message: 'a + Clone,
48{
49    /// Creates a new [`Dialog`] with the given base and dialog content.
50    pub fn new(
51        is_open: bool,
52        base: impl Into<Element<'a, Message, Theme, Renderer>>,
53        content: impl Into<Element<'a, Message, Theme, Renderer>>,
54    ) -> Self {
55        Self::with_buttons(is_open, base, content, Vec::new())
56    }
57
58    /// Creates a new [`Dialog`] with the given base, dialog content and buttons.
59    pub fn with_buttons(
60        is_open: bool,
61        base: impl Into<Element<'a, Message, Theme, Renderer>>,
62        content: impl Into<Element<'a, Message, Theme, Renderer>>,
63        buttons: Vec<Element<'a, Message, Theme, Renderer>>,
64    ) -> Self {
65        Self {
66            is_open,
67            base: base.into(),
68            title: None,
69            content: content.into(),
70            buttons,
71            font: None,
72            width: 400.into(),
73            height: 260.into(),
74            spacing: 8.0,
75            padding: 24.into(),
76            button_alignment: Alignment::Start,
77            class: <Theme as Catalog>::default(),
78            title_class: <Theme as Catalog>::default_title(),
79            container_class: <Theme as Catalog>::default_container(),
80        }
81    }
82
83    /// Sets the [`Dialog`]'s title.
84    pub fn title(mut self, title: impl Into<String>) -> Self {
85        self.title = Some(title.into());
86        self
87    }
88
89    /// Sets the [`Dialog`]'s width.
90    pub fn width(mut self, width: impl Into<Length>) -> Self {
91        self.width = width.into();
92        self
93    }
94
95    /// Sets the [`Dialog`]'s height.
96    pub fn height(mut self, height: impl Into<Length>) -> Self {
97        self.height = height.into();
98        self
99    }
100
101    /// Sets the [`Dialog`]'s padding.
102    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
103        self.padding = padding.into();
104        self
105    }
106
107    /// Sets the [`Dialog`]'s spacing.
108    pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
109        self.spacing = spacing.into().0;
110        self
111    }
112
113    /// Sets the vertical alignment of the [`Dialog`]'s buttons.
114    pub fn align_buttons(mut self, align: impl Into<Vertical>) -> Self {
115        self.button_alignment = Alignment::from(align.into());
116        self
117    }
118
119    /// Sets the [`Font`] of the [`Dialog`]'s title.
120    ///
121    /// [`Font`]: https://docs.iced.rs/iced_core/text/trait.Renderer.html#associatedtype.Font
122    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
123        self.font = Some(font.into());
124        self
125    }
126
127    /// Adds a button to the [`Dialog`].
128    pub fn push_button(
129        mut self,
130        button: impl Into<Element<'a, Message, Theme, Renderer>>,
131    ) -> Self {
132        self.buttons.push(button.into());
133        self
134    }
135
136    /// Adds a button to the [`Dialog`], if `Some`.
137    pub fn push_button_maybe(
138        self,
139        button: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
140    ) -> Self {
141        if let Some(button) = button {
142            self.push_button(button)
143        } else {
144            self
145        }
146    }
147
148    /// Extends the [`Dialog`] with the given buttons.
149    pub fn extend_buttons(
150        self,
151        buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
152    ) -> Self {
153        buttons.into_iter().fold(self, Self::push_button)
154    }
155
156    /// Sets the backdrop color of the [`Dialog`].
157    pub fn backdrop(self, color: impl Into<Color>) -> Self
158    where
159        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
160    {
161        let backdrop_color = color.into();
162
163        self.style(move |_theme| Style { backdrop_color })
164    }
165
166    /// Sets the style of the [`Dialog`].
167    #[must_use]
168    pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
169    where
170        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
171    {
172        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
173        self
174    }
175
176    /// Sets the style of the [`Dialog`]'s title.
177    #[must_use]
178    pub fn title_style(
179        mut self,
180        style: impl Fn(&Theme) -> text::Style + 'a,
181    ) -> Self
182    where
183        <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
184    {
185        self.title_class = (Box::new(style) as text::StyleFn<'a, Theme>).into();
186        self
187    }
188
189    /// Sets the style of the [`Dialog`]'s container.
190    #[must_use]
191    pub fn container_style(
192        mut self,
193        style: impl Fn(&Theme) -> container::Style + 'a,
194    ) -> Self
195    where
196        <Theme as container::Catalog>::Class<'a>:
197            From<container::StyleFn<'a, Theme>>,
198    {
199        self.container_class =
200            (Box::new(style) as container::StyleFn<'a, Theme>).into();
201        self
202    }
203
204    /// Sets the style class of the [`Dialog`].
205    #[must_use]
206    pub fn class(
207        mut self,
208        class: impl Into<<Theme as Catalog>::Class<'a>>,
209    ) -> Self {
210        self.class = class.into();
211        self
212    }
213
214    /// Sets the style class of the [`Dialog`]'s title.
215    #[must_use]
216    pub fn title_class(
217        mut self,
218        class: impl Into<<Theme as text::Catalog>::Class<'a>>,
219    ) -> Self {
220        self.title_class = class.into();
221        self
222    }
223
224    /// Sets the style class of the [`Dialog`]'s container.
225    #[must_use]
226    pub fn container_class(
227        mut self,
228        class: impl Into<<Theme as container::Catalog>::Class<'a>>,
229    ) -> Self {
230        self.container_class = class.into();
231        self
232    }
233
234    fn view(self) -> Element<'a, Message, Theme, Renderer>
235    where
236        <Theme as container::Catalog>::Class<'a>:
237            From<container::StyleFn<'a, Theme>>,
238    {
239        if self.is_open {
240            let contents = Container::new(
241                Column::new()
242                    .push_maybe(self.title.map(|title| {
243                        let text = text(title)
244                            .size(20)
245                            .line_height(text::LineHeight::Absolute(Pixels(
246                                26.0,
247                            )))
248                            .class(self.title_class);
249
250                        if let Some(font) = self.font {
251                            text.font(font)
252                        } else {
253                            text
254                        }
255                    }))
256                    .push(vertical_space().height(12))
257                    .push(self.content),
258            )
259            .width(Length::Fill)
260            .padding(self.padding);
261
262            let buttons = Container::new(
263                Row::with_children(self.buttons).spacing(self.spacing),
264            )
265            .height(80)
266            .padding(self.padding);
267
268            let dialog = Container::new(
269                Column::new()
270                    .push(contents)
271                    .push(vertical_space())
272                    .push(buttons),
273            )
274            .width(self.width)
275            .height(self.height)
276            .class(self.container_class)
277            .clip(true);
278
279            modal(self.base, dialog, self.class)
280        } else {
281            self.base
282        }
283    }
284}
285
286impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
287    for Element<'a, Message, Theme, Renderer>
288where
289    Renderer: 'a + core::Renderer + core::text::Renderer,
290    Theme: 'a + Catalog,
291    Message: 'a + Clone,
292    <Theme as container::Catalog>::Class<'a>:
293        From<container::StyleFn<'a, Theme>>,
294{
295    fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
296        dialog.view()
297    }
298}
299
300fn modal<'a, Message, Theme, Renderer>(
301    base: impl Into<Element<'a, Message, Theme, Renderer>>,
302    content: impl Into<Element<'a, Message, Theme, Renderer>>,
303    class: <Theme as Catalog>::Class<'a>,
304) -> Element<'a, Message, Theme, Renderer>
305where
306    Message: 'a + Clone,
307    Renderer: 'a + core::Renderer,
308    Theme: 'a + container::Catalog + Catalog,
309    <Theme as container::Catalog>::Class<'a>:
310        From<container::StyleFn<'a, Theme>>,
311{
312    let area = mouse_area(center(opaque(content)).style(move |theme| {
313        container::Style {
314            background: Some(
315                Catalog::style(theme, &class).backdrop_color.into(),
316            ),
317            ..Default::default()
318        }
319    }));
320
321    stack![base.into(), opaque(area)].into()
322}
323
324/// The style of a [`Dialog`].
325#[derive(Debug, Clone, Copy, PartialEq)]
326pub struct Style {
327    /// The [`Dialog`]'s backdrop.
328    pub backdrop_color: Color,
329}
330
331/// The theme catalog of a [`Dialog`].
332pub trait Catalog: text::Catalog + container::Catalog {
333    /// The item class of the [`Catalog`].
334    type Class<'a>;
335
336    /// The default class produced by the [`Catalog`].
337    fn default<'a>() -> <Self as Catalog>::Class<'a>;
338
339    /// The default class for the [`Dialog`]'s title.
340    fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
341        <Self as text::Catalog>::default()
342    }
343
344    /// The default class for the [`Dialog`]'s container.
345    fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
346        <Self as container::Catalog>::default()
347    }
348
349    /// The [`Style`] of a class.
350    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
351}
352
353/// A styling function for a [`Dialog`].
354pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
355
356impl Catalog for Theme {
357    type Class<'a> = StyleFn<'a, Self>;
358
359    fn default<'a>() -> <Self as Catalog>::Class<'a> {
360        Box::new(default)
361    }
362
363    fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
364        Box::new(|theme| {
365            container::background(
366                theme.extended_palette().background.base.color,
367            )
368        })
369    }
370
371    fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
372        class(self)
373    }
374}
375
376/// The default style of a [`Dialog`].
377pub fn default(_theme: &Theme) -> Style {
378    Style {
379        backdrop_color: color!(0x000000, 0.3),
380    }
381}