1use 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
12pub 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 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 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
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: Length::Fit,
91 height: Length::Fit,
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 pub fn title(mut self, title: impl IntoFragment<'a>) -> Self {
108 self.title = Some(title.into_fragment());
109 self
110 }
111
112 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 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 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 pub fn width(mut self, width: impl Into<Length>) -> Self {
146 self.width = width.into();
147 self
148 }
149
150 pub fn height(mut self, height: impl Into<Length>) -> Self {
152 self.height = height.into();
153 self
154 }
155
156 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 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 pub fn align_left(self) -> Self {
170 self.align_x(alignment::Horizontal::Left)
171 }
172
173 pub fn align_right(self) -> Self {
175 self.align_x(alignment::Horizontal::Right)
176 }
177
178 pub fn align_top(self) -> Self {
180 self.align_y(alignment::Vertical::Top)
181 }
182
183 pub fn align_bottom(self) -> Self {
185 self.align_y(alignment::Vertical::Bottom)
186 }
187
188 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 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 pub fn padding_inner(mut self, padding: impl Into<Padding>) -> Self {
212 self.padding_inner = padding.into();
213 self
214 }
215
216 pub fn padding_outer(mut self, padding: impl Into<Padding>) -> Self {
218 self.padding_outer = padding.into();
219 self
220 }
221
222 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
224 self.spacing = spacing.into().0;
225 self
226 }
227
228 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 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
241 self.font = Some(font.into());
242 self
243 }
244
245 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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().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()),
414 buttons,
415 ])
416 .width(self.width.max(max_width))
417 .height(self.height.max(max_height))
418 .class(self.container_class)
419 .clip(true);
420
421 let backdrop = mouse_area(
422 container(opaque(content))
423 .style(move |theme| container::Style {
424 background: Some(
425 Catalog::style(theme, &self.class)
426 .backdrop_color
427 .into(),
428 ),
429 ..Default::default()
430 })
431 .padding(self.padding_outer)
432 .width(Length::Fill)
433 .height(Length::Fill)
434 .align_x(self.horizontal_alignment)
435 .align_y(self.vertical_alignment),
436 );
437
438 if let Some(on_press) = self.on_press {
439 opaque(backdrop.on_press(on_press()))
440 } else {
441 opaque(backdrop)
442 }
443 });
444
445 stack![self.base, dialog].into()
446 }
447}
448
449pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
453
454pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
458
459impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
460 for Element<'a, Message, Theme, Renderer>
461where
462 Renderer: 'a + core::Renderer + core::text::Renderer,
463 Theme: 'a + Catalog,
464 Message: 'a + Clone,
465 <Theme as container::Catalog>::Class<'a>:
466 From<container::StyleFn<'a, Theme>>,
467{
468 fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
469 dialog.view()
470 }
471}
472
473#[derive(Debug, Clone, Copy, PartialEq)]
475pub struct Style {
476 pub backdrop_color: Color,
478}
479
480pub trait Catalog: text::Catalog + container::Catalog {
482 type Class<'a>;
484
485 fn default<'a>() -> <Self as Catalog>::Class<'a>;
487
488 fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
490 <Self as text::Catalog>::default()
491 }
492
493 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
495 <Self as container::Catalog>::default()
496 }
497
498 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
500}
501
502pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
504
505impl Catalog for Theme {
506 type Class<'a> = StyleFn<'a, Self>;
507
508 fn default<'a>() -> <Self as Catalog>::Class<'a> {
509 Box::new(default)
510 }
511
512 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
513 Box::new(|theme| container::background(theme.seed().background))
514 }
515
516 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
517 class(self)
518 }
519}
520
521pub fn default<Theme>(_theme: &Theme) -> Style {
523 Style {
524 backdrop_color: core::color!(0x000000, 0.3),
525 }
526}