1use iced_widget::{
4 Container, Row, Theme, column, container, mouse_area, opaque, stack, text,
5 text::{Fragment, IntoFragment},
6 vertical_space,
7};
8
9use crate::core::{self, Color, Element, Length, Padding, Pixels, alignment};
10
11pub struct Dialog<
27 'a,
28 Message,
29 Theme = iced_widget::Theme,
30 Renderer = iced_widget::Renderer,
31> where
32 Renderer: 'a + core::text::Renderer,
33 Theme: 'a + Catalog,
34{
35 is_open: bool,
36 base: Element<'a, Message, Theme, Renderer>,
37 title: Option<Fragment<'a>>,
38 content: Element<'a, Message, Theme, Renderer>,
39 buttons: Vec<Element<'a, Message, Theme, Renderer>>,
40 on_press: Option<Box<dyn Fn() -> Message + 'a>>,
41 font: Option<Renderer::Font>,
42 width: Length,
43 height: Length,
44 max_width: Option<f32>,
45 max_height: Option<f32>,
46 horizontal_alignment: alignment::Horizontal,
47 vertical_alignment: alignment::Vertical,
48 spacing: f32,
49 padding_inner: Padding,
50 padding_outer: Padding,
51 button_alignment: alignment::Vertical,
52 class: <Theme as Catalog>::Class<'a>,
53 title_class: <Theme as text::Catalog>::Class<'a>,
54 container_class: <Theme as container::Catalog>::Class<'a>,
55}
56
57impl<'a, Message, Theme, Renderer> Dialog<'a, Message, Theme, Renderer>
58where
59 Renderer: 'a + core::Renderer + core::text::Renderer,
60 Theme: 'a + Catalog,
61 Message: 'a + Clone,
62{
63 pub fn new(
65 is_open: bool,
66 base: impl Into<Element<'a, Message, Theme, Renderer>>,
67 content: impl Into<Element<'a, Message, Theme, Renderer>>,
68 ) -> Self {
69 Self::with_buttons(is_open, base, content, Vec::new())
70 }
71
72 pub fn with_buttons(
74 is_open: bool,
75 base: impl Into<Element<'a, Message, Theme, Renderer>>,
76 content: impl Into<Element<'a, Message, Theme, Renderer>>,
77 buttons: Vec<Element<'a, Message, Theme, Renderer>>,
78 ) -> Self {
79 let content = content.into();
80 let size = content.as_widget().size_hint();
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: size.width.fluid(),
91 height: size.height.fluid(),
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_space().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_space()),
414 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 let backdrop = mouse_area(
424 container(opaque(content))
425 .style(move |theme| container::Style {
426 background: Some(
427 Catalog::style(theme, &self.class)
428 .backdrop_color
429 .into(),
430 ),
431 ..Default::default()
432 })
433 .padding(self.padding_outer)
434 .width(Length::Fill)
435 .height(Length::Fill)
436 .align_x(self.horizontal_alignment)
437 .align_y(self.vertical_alignment),
438 );
439
440 if let Some(on_press) = self.on_press {
441 opaque(backdrop.on_press(on_press()))
442 } else {
443 opaque(backdrop)
444 }
445 });
446
447 stack![self.base, dialog].into()
448 }
449}
450
451pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
455
456pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
460
461impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
462 for Element<'a, Message, Theme, Renderer>
463where
464 Renderer: 'a + core::Renderer + core::text::Renderer,
465 Theme: 'a + Catalog,
466 Message: 'a + Clone,
467 <Theme as container::Catalog>::Class<'a>:
468 From<container::StyleFn<'a, Theme>>,
469{
470 fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
471 dialog.view()
472 }
473}
474
475#[derive(Debug, Clone, Copy, PartialEq)]
477pub struct Style {
478 pub backdrop_color: Color,
480}
481
482pub trait Catalog: text::Catalog + container::Catalog {
484 type Class<'a>;
486
487 fn default<'a>() -> <Self as Catalog>::Class<'a>;
489
490 fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
492 <Self as text::Catalog>::default()
493 }
494
495 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
497 <Self as container::Catalog>::default()
498 }
499
500 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
502}
503
504pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
506
507impl Catalog for Theme {
508 type Class<'a> = StyleFn<'a, Self>;
509
510 fn default<'a>() -> <Self as Catalog>::Class<'a> {
511 Box::new(default)
512 }
513
514 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
515 Box::new(|theme| container::background(theme.palette().background))
516 }
517
518 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
519 class(self)
520 }
521}
522
523pub fn default<Theme>(_theme: &Theme) -> Style {
525 Style {
526 backdrop_color: core::color!(0x000000, 0.3),
527 }
528}