Skip to main content

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