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 let size = content.as_widget().size_hint();
82
83 Self {
84 is_open,
85 base: base.into(),
86 title: None,
87 content,
88 buttons,
89 on_press: None,
90 font: None,
91 width: size.width.fluid(),
92 height: size.height.fluid(),
93 max_width: None,
94 max_height: None,
95 horizontal_alignment: alignment::Horizontal::Center,
96 vertical_alignment: alignment::Vertical::Center,
97 spacing: 8.0,
98 padding_inner: 24.into(),
99 padding_outer: Padding::ZERO,
100 button_alignment: alignment::Vertical::Top,
101 class: <Theme as Catalog>::default(),
102 title_class: <Theme as Catalog>::default_title(),
103 container_class: <Theme as Catalog>::default_container(),
104 }
105 }
106
107 pub fn title(mut self, title: impl IntoFragment<'a>) -> Self {
109 self.title = Some(title.into_fragment());
110 self
111 }
112
113 pub fn on_press(mut self, on_press: Message) -> Self
115 where
116 Message: Clone,
117 {
118 self.on_press = Some(Box::new(move || on_press.clone()));
119 self
120 }
121
122 pub fn on_press_with(
127 mut self,
128 on_press: impl Fn() -> Message + 'a,
129 ) -> Self {
130 self.on_press = Some(Box::new(on_press));
131 self
132 }
133
134 pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self
136 where
137 Message: Clone,
138 {
139 self.on_press =
140 on_press.map(|message| Box::new(move || message.clone()) as _);
141
142 self
143 }
144
145 pub fn width(mut self, width: impl Into<Length>) -> Self {
147 self.width = width.into();
148 self
149 }
150
151 pub fn height(mut self, height: impl Into<Length>) -> Self {
153 self.height = height.into();
154 self
155 }
156
157 pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
159 self.max_width = Some(max_width.into().0);
160 self
161 }
162
163 pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
165 self.max_height = Some(max_height.into().0);
166 self
167 }
168
169 pub fn align_left(self) -> Self {
171 self.align_x(alignment::Horizontal::Left)
172 }
173
174 pub fn align_right(self) -> Self {
176 self.align_x(alignment::Horizontal::Right)
177 }
178
179 pub fn align_top(self) -> Self {
181 self.align_y(alignment::Vertical::Top)
182 }
183
184 pub fn align_bottom(self) -> Self {
186 self.align_y(alignment::Vertical::Bottom)
187 }
188
189 pub fn align_x(
193 mut self,
194 alignment: impl Into<alignment::Horizontal>,
195 ) -> Self {
196 self.horizontal_alignment = alignment.into();
197 self
198 }
199
200 pub fn align_y(
204 mut self,
205 alignment: impl Into<alignment::Vertical>,
206 ) -> Self {
207 self.vertical_alignment = alignment.into();
208 self
209 }
210
211 pub fn padding_inner(mut self, padding: impl Into<Padding>) -> Self {
213 self.padding_inner = padding.into();
214 self
215 }
216
217 pub fn padding_outer(mut self, padding: impl Into<Padding>) -> Self {
219 self.padding_outer = padding.into();
220 self
221 }
222
223 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
225 self.spacing = spacing.into().0;
226 self
227 }
228
229 pub fn align_buttons(
231 mut self,
232 align: impl Into<alignment::Vertical>,
233 ) -> Self {
234 self.button_alignment = align.into();
235 self
236 }
237
238 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
242 self.font = Some(font.into());
243 self
244 }
245
246 pub fn push_button(
248 mut self,
249 button: impl Into<Element<'a, Message, Theme, Renderer>>,
250 ) -> Self {
251 self.buttons.push(button.into());
252 self
253 }
254
255 pub fn push_button_maybe(
257 self,
258 button: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
259 ) -> Self {
260 if let Some(button) = button {
261 self.push_button(button)
262 } else {
263 self
264 }
265 }
266
267 pub fn extend_buttons(
269 self,
270 buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
271 ) -> Self {
272 buttons.into_iter().fold(self, Self::push_button)
273 }
274
275 pub fn backdrop(self, color: impl Into<Color>) -> Self
277 where
278 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
279 {
280 let backdrop_color = color.into();
281
282 self.style(move |_theme| Style { backdrop_color })
283 }
284
285 #[must_use]
287 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
288 where
289 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
290 {
291 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
292 self
293 }
294
295 #[must_use]
297 pub fn title_style(
298 mut self,
299 style: impl Fn(&Theme) -> text::Style + 'a,
300 ) -> Self
301 where
302 <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
303 {
304 self.title_class = (Box::new(style) as text::StyleFn<'a, Theme>).into();
305 self
306 }
307
308 #[must_use]
310 pub fn container_style(
311 mut self,
312 style: impl Fn(&Theme) -> container::Style + 'a,
313 ) -> Self
314 where
315 <Theme as container::Catalog>::Class<'a>:
316 From<container::StyleFn<'a, Theme>>,
317 {
318 self.container_class =
319 (Box::new(style) as container::StyleFn<'a, Theme>).into();
320 self
321 }
322
323 #[must_use]
325 pub fn class(
326 mut self,
327 class: impl Into<<Theme as Catalog>::Class<'a>>,
328 ) -> Self {
329 self.class = class.into();
330 self
331 }
332
333 #[must_use]
335 pub fn title_class(
336 mut self,
337 class: impl Into<<Theme as text::Catalog>::Class<'a>>,
338 ) -> Self {
339 self.title_class = class.into();
340 self
341 }
342
343 #[must_use]
345 pub fn container_class(
346 mut self,
347 class: impl Into<<Theme as container::Catalog>::Class<'a>>,
348 ) -> Self {
349 self.container_class = class.into();
350 self
351 }
352
353 fn view(self) -> Element<'a, Message, Theme, Renderer>
354 where
355 <Theme as container::Catalog>::Class<'a>:
356 From<container::StyleFn<'a, Theme>>,
357 {
358 let dialog = self.is_open.then(|| {
359 let has_title = self.title.is_some();
360 let has_buttons = !self.buttons.is_empty();
361
362 let contents = Container::new(column![
363 self.title.map(|title| {
364 let text = text(title)
365 .size(20)
366 .line_height(text::LineHeight::Absolute(Pixels(26.0)))
367 .class(self.title_class);
368
369 if let Some(font) = self.font {
370 text.font(font)
371 } else {
372 text
373 }
374 }),
375 has_title.then_some(vertical().height(12)),
376 self.content
377 ])
378 .padding(self.padding_inner);
379
380 let contents = if has_buttons {
381 contents.width(Length::Fill)
382 } else {
383 contents
384 };
385
386 let buttons = has_buttons.then_some(
387 Container::new(
388 Row::with_children(self.buttons)
389 .spacing(self.spacing)
390 .align_y(self.button_alignment),
391 )
392 .height(80)
393 .padding(self.padding_inner),
394 );
395
396 let max_width = self.max_width.unwrap_or(
397 if has_buttons && !matches!(self.width, Length::Fixed(_)) {
398 DEFAULT_MAX_WIDTH
399 } else {
400 f32::INFINITY
401 },
402 );
403
404 let max_height = self.max_height.unwrap_or(
405 if has_buttons && !matches!(self.height, Length::Fixed(_)) {
406 DEFAULT_MAX_HEIGHT
407 } else {
408 f32::INFINITY
409 },
410 );
411
412 let content = Container::new(column![
413 contents,
414 has_buttons.then_some(vertical()),
415 buttons,
416 ])
417 .width(if self.width == Length::Shrink && has_buttons {
418 Length::Fill
419 } else {
420 self.width
421 })
422 .height(if self.height == Length::Shrink && has_buttons {
423 Length::Fill
424 } else {
425 self.height
426 })
427 .max_width(max_width)
428 .max_height(max_height)
429 .class(self.container_class)
430 .clip(true);
431
432 let backdrop = mouse_area(
433 container(opaque(content))
434 .style(move |theme| container::Style {
435 background: Some(
436 Catalog::style(theme, &self.class)
437 .backdrop_color
438 .into(),
439 ),
440 ..Default::default()
441 })
442 .padding(self.padding_outer)
443 .width(Length::Fill)
444 .height(Length::Fill)
445 .align_x(self.horizontal_alignment)
446 .align_y(self.vertical_alignment),
447 );
448
449 if let Some(on_press) = self.on_press {
450 opaque(backdrop.on_press(on_press()))
451 } else {
452 opaque(backdrop)
453 }
454 });
455
456 stack![self.base, dialog].into()
457 }
458}
459
460pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
464
465pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
469
470impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
471 for Element<'a, Message, Theme, Renderer>
472where
473 Renderer: 'a + core::Renderer + core::text::Renderer,
474 Theme: 'a + Catalog,
475 Message: 'a + Clone,
476 <Theme as container::Catalog>::Class<'a>:
477 From<container::StyleFn<'a, Theme>>,
478{
479 fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
480 dialog.view()
481 }
482}
483
484#[derive(Debug, Clone, Copy, PartialEq)]
486pub struct Style {
487 pub backdrop_color: Color,
489}
490
491pub trait Catalog: text::Catalog + container::Catalog {
493 type Class<'a>;
495
496 fn default<'a>() -> <Self as Catalog>::Class<'a>;
498
499 fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
501 <Self as text::Catalog>::default()
502 }
503
504 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
506 <Self as container::Catalog>::default()
507 }
508
509 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
511}
512
513pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
515
516impl Catalog for Theme {
517 type Class<'a> = StyleFn<'a, Self>;
518
519 fn default<'a>() -> <Self as Catalog>::Class<'a> {
520 Box::new(default)
521 }
522
523 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
524 Box::new(|theme| container::background(theme.palette().background))
525 }
526
527 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
528 class(self)
529 }
530}
531
532pub fn default<Theme>(_theme: &Theme) -> Style {
534 Style {
535 backdrop_color: core::color!(0x000000, 0.3),
536 }
537}