1use iced_core::{
4 self as core, Color, Element, Length, Padding, Pixels, alignment, color,
5};
6use iced_widget::{
7 Column, Container, Row, Theme, container, mouse_area, opaque, stack, text,
8 text::{Fragment, IntoFragment},
9 vertical_space,
10};
11
12pub struct Dialog<
30 'a,
31 Message,
32 Theme = iced_widget::Theme,
33 Renderer = iced_widget::Renderer,
34> where
35 Renderer: 'a + core::text::Renderer,
36 Theme: 'a + Catalog,
37{
38 is_open: bool,
39 base: Element<'a, Message, Theme, Renderer>,
40 title: Option<Fragment<'a>>,
41 content: Element<'a, Message, Theme, Renderer>,
42 buttons: Vec<Element<'a, Message, Theme, Renderer>>,
43 on_press: Option<Box<dyn Fn() -> Message + 'a>>,
44 font: Option<Renderer::Font>,
45 width: Length,
46 height: Length,
47 max_width: Option<f32>,
48 max_height: Option<f32>,
49 horizontal_alignment: alignment::Horizontal,
50 vertical_alignment: alignment::Vertical,
51 spacing: f32,
52 padding: Padding,
53 button_alignment: alignment::Vertical,
54 class: <Theme as Catalog>::Class<'a>,
55 title_class: <Theme as text::Catalog>::Class<'a>,
56 container_class: <Theme as container::Catalog>::Class<'a>,
57}
58
59impl<'a, Message, Theme, Renderer> Dialog<'a, Message, Theme, Renderer>
60where
61 Renderer: 'a + core::Renderer + core::text::Renderer,
62 Theme: 'a + Catalog,
63 Message: 'a + Clone,
64{
65 pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
69
70 pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
74
75 pub fn new(
77 is_open: bool,
78 base: impl Into<Element<'a, Message, Theme, Renderer>>,
79 content: impl Into<Element<'a, Message, Theme, Renderer>>,
80 ) -> Self {
81 Self::with_buttons(is_open, base, content, Vec::new())
82 }
83
84 pub fn with_buttons(
86 is_open: bool,
87 base: impl Into<Element<'a, Message, Theme, Renderer>>,
88 content: impl Into<Element<'a, Message, Theme, Renderer>>,
89 buttons: Vec<Element<'a, Message, Theme, Renderer>>,
90 ) -> Self {
91 let content = content.into();
92 let size = content.as_widget().size_hint();
93
94 Self {
95 is_open,
96 base: base.into(),
97 title: None,
98 content,
99 buttons,
100 on_press: None,
101 font: None,
102 width: size.width.fluid(),
103 height: size.height.fluid(),
104 max_width: None,
105 max_height: None,
106 horizontal_alignment: alignment::Horizontal::Center,
107 vertical_alignment: alignment::Vertical::Center,
108 spacing: 8.0,
109 padding: 24.into(),
110 button_alignment: alignment::Vertical::Top,
111 class: <Theme as Catalog>::default(),
112 title_class: <Theme as Catalog>::default_title(),
113 container_class: <Theme as Catalog>::default_container(),
114 }
115 }
116
117 pub fn title(mut self, title: impl IntoFragment<'a>) -> Self {
119 self.title = Some(title.into_fragment());
120 self
121 }
122
123 pub fn on_press(mut self, on_press: Message) -> Self
125 where
126 Message: Clone,
127 {
128 self.on_press = Some(Box::new(move || on_press.clone()));
129 self
130 }
131
132 pub fn on_press_with(
137 mut self,
138 on_press: impl Fn() -> Message + 'a,
139 ) -> Self {
140 self.on_press = Some(Box::new(on_press));
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(mut self, padding: impl Into<Padding>) -> Self {
212 self.padding = padding.into();
213 self
214 }
215
216 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
218 self.spacing = spacing.into().0;
219 self
220 }
221
222 pub fn align_buttons(
224 mut self,
225 align: impl Into<alignment::Vertical>,
226 ) -> Self {
227 self.button_alignment = align.into();
228 self
229 }
230
231 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
235 self.font = Some(font.into());
236 self
237 }
238
239 pub fn push_button(
241 mut self,
242 button: impl Into<Element<'a, Message, Theme, Renderer>>,
243 ) -> Self {
244 self.buttons.push(button.into());
245 self
246 }
247
248 pub fn push_button_maybe(
250 self,
251 button: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
252 ) -> Self {
253 if let Some(button) = button {
254 self.push_button(button)
255 } else {
256 self
257 }
258 }
259
260 pub fn extend_buttons(
262 self,
263 buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
264 ) -> Self {
265 buttons.into_iter().fold(self, Self::push_button)
266 }
267
268 pub fn backdrop(self, color: impl Into<Color>) -> Self
270 where
271 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
272 {
273 let backdrop_color = color.into();
274
275 self.style(move |_theme| Style { backdrop_color })
276 }
277
278 #[must_use]
280 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
281 where
282 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
283 {
284 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
285 self
286 }
287
288 #[must_use]
290 pub fn title_style(
291 mut self,
292 style: impl Fn(&Theme) -> text::Style + 'a,
293 ) -> Self
294 where
295 <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
296 {
297 self.title_class = (Box::new(style) as text::StyleFn<'a, Theme>).into();
298 self
299 }
300
301 #[must_use]
303 pub fn container_style(
304 mut self,
305 style: impl Fn(&Theme) -> container::Style + 'a,
306 ) -> Self
307 where
308 <Theme as container::Catalog>::Class<'a>:
309 From<container::StyleFn<'a, Theme>>,
310 {
311 self.container_class =
312 (Box::new(style) as container::StyleFn<'a, Theme>).into();
313 self
314 }
315
316 #[must_use]
318 pub fn class(
319 mut self,
320 class: impl Into<<Theme as Catalog>::Class<'a>>,
321 ) -> Self {
322 self.class = class.into();
323 self
324 }
325
326 #[must_use]
328 pub fn title_class(
329 mut self,
330 class: impl Into<<Theme as text::Catalog>::Class<'a>>,
331 ) -> Self {
332 self.title_class = class.into();
333 self
334 }
335
336 #[must_use]
338 pub fn container_class(
339 mut self,
340 class: impl Into<<Theme as container::Catalog>::Class<'a>>,
341 ) -> Self {
342 self.container_class = class.into();
343 self
344 }
345
346 fn view(self) -> Element<'a, Message, Theme, Renderer>
347 where
348 <Theme as container::Catalog>::Class<'a>:
349 From<container::StyleFn<'a, Theme>>,
350 {
351 let dialog = self.is_open.then(|| {
352 let has_title = self.title.is_some();
353 let has_buttons = !self.buttons.is_empty();
354
355 let contents = Container::new(
356 Column::new()
357 .push_maybe(self.title.map(|title| {
358 let text = text(title)
359 .size(20)
360 .line_height(text::LineHeight::Absolute(Pixels(
361 26.0,
362 )))
363 .class(self.title_class);
364
365 if let Some(font) = self.font {
366 text.font(font)
367 } else {
368 text
369 }
370 }))
371 .push_maybe(
372 has_title.then_some(vertical_space().height(12)),
373 )
374 .push(self.content),
375 )
376 .padding(self.padding);
377
378 let contents = if has_buttons {
379 contents.width(Length::Fill)
380 } else {
381 contents
382 };
383
384 let buttons = has_buttons.then_some(
385 Container::new(
386 Row::with_children(self.buttons)
387 .spacing(self.spacing)
388 .align_y(self.button_alignment),
389 )
390 .height(80)
391 .padding(self.padding),
392 );
393
394 let max_width = self.max_width.unwrap_or(
395 if has_buttons && !matches!(self.width, Length::Fixed(_)) {
396 Self::DEFAULT_MAX_WIDTH
397 } else {
398 f32::INFINITY
399 },
400 );
401
402 let max_height = self.max_height.unwrap_or(
403 if has_buttons && !matches!(self.height, Length::Fixed(_)) {
404 Self::DEFAULT_MAX_HEIGHT
405 } else {
406 f32::INFINITY
407 },
408 );
409
410 Container::new(
411 Column::new()
412 .push(contents)
413 .push_maybe(has_buttons.then_some(vertical_space()))
414 .push_maybe(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
424 modal(
425 self.base,
426 dialog,
427 self.on_press,
428 self.horizontal_alignment,
429 self.vertical_alignment,
430 self.class,
431 )
432 }
433}
434
435impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
436 for Element<'a, Message, Theme, Renderer>
437where
438 Renderer: 'a + core::Renderer + core::text::Renderer,
439 Theme: 'a + Catalog,
440 Message: 'a + Clone,
441 <Theme as container::Catalog>::Class<'a>:
442 From<container::StyleFn<'a, Theme>>,
443{
444 fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
445 dialog.view()
446 }
447}
448
449fn modal<'a, Message, Theme, Renderer>(
450 base: impl Into<Element<'a, Message, Theme, Renderer>>,
451 content: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
452 on_press: Option<impl Fn() -> Message + 'a>,
453 horizontal_alignment: alignment::Horizontal,
454 vertical_alignment: alignment::Vertical,
455 class: <Theme as Catalog>::Class<'a>,
456) -> Element<'a, Message, Theme, Renderer>
457where
458 Message: 'a + Clone,
459 Renderer: 'a + core::Renderer,
460 Theme: 'a + container::Catalog + Catalog,
461 <Theme as container::Catalog>::Class<'a>:
462 From<container::StyleFn<'a, Theme>>,
463{
464 let area = content.map(|content| {
465 let backdrop = mouse_area(
466 container(opaque(content))
467 .style(move |theme| container::Style {
468 background: Some(
469 Catalog::style(theme, &class).backdrop_color.into(),
470 ),
471 ..Default::default()
472 })
473 .width(Length::Fill)
474 .height(Length::Fill)
475 .align_x(horizontal_alignment)
476 .align_y(vertical_alignment),
477 );
478
479 if let Some(on_press) = on_press {
480 opaque(backdrop.on_press(on_press()))
481 } else {
482 opaque(backdrop)
483 }
484 });
485
486 stack![base.into()].push_maybe(area).into()
487}
488
489#[derive(Debug, Clone, Copy, PartialEq)]
491pub struct Style {
492 pub backdrop_color: Color,
494}
495
496pub trait Catalog: text::Catalog + container::Catalog {
498 type Class<'a>;
500
501 fn default<'a>() -> <Self as Catalog>::Class<'a>;
503
504 fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
506 <Self as text::Catalog>::default()
507 }
508
509 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
511 <Self as container::Catalog>::default()
512 }
513
514 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
516}
517
518pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
520
521impl Catalog for Theme {
522 type Class<'a> = StyleFn<'a, Self>;
523
524 fn default<'a>() -> <Self as Catalog>::Class<'a> {
525 Box::new(default)
526 }
527
528 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
529 Box::new(|theme| {
530 container::background(
531 theme.extended_palette().background.base.color,
532 )
533 })
534 }
535
536 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
537 class(self)
538 }
539}
540
541pub fn default(_theme: &Theme) -> Style {
543 Style {
544 backdrop_color: color!(0x000000, 0.3),
545 }
546}