1use iced::{Background, border::Radius, color};
4use iced_widget::{
5 Container, Row, Theme, column, container, mouse_area, opaque,
6 space::vertical,
7 stack, text,
8 text::{Fragment, IntoFragment},
9};
10
11use iced_widget::core::{self, Color, Element, Length, Padding, Pixels, alignment};
12
13use crate::{
14 style::shadow,
15 theme::{ColorScheme, Palette},
16};
17
18pub struct Dialog<'a, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
34where
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_inner: Padding,
53 padding_outer: Padding,
54 button_alignment: alignment::Vertical,
55 palette: &'a Palette,
56 class: <Theme as 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 fn new(
67 is_open: bool,
68 base: impl Into<Element<'a, Message, Theme, Renderer>>,
69 content: impl Into<Element<'a, Message, Theme, Renderer>>,
70 palette: &'a Palette,
71 ) -> Self {
72 Self::with_buttons(is_open, base, content, Vec::new(), palette)
73 }
74
75 pub fn with_buttons(
77 is_open: bool,
78 base: impl Into<Element<'a, Message, Theme, Renderer>>,
79 content: impl Into<Element<'a, Message, Theme, Renderer>>,
80 buttons: Vec<Element<'a, Message, Theme, Renderer>>,
81 palette: &'a Palette,
82 ) -> Self {
83 let content = content.into();
84
85 Self {
86 is_open,
87 base: base.into(),
88 title: None,
89 content,
90 buttons,
91 on_press: None,
92 font: None,
93 width: Length::Shrink,
94 height: Length::Shrink,
95 max_width: None,
96 max_height: None,
97 horizontal_alignment: alignment::Horizontal::Center,
98 vertical_alignment: alignment::Vertical::Center,
99 spacing: 8.0,
100 padding_inner: 24.into(),
101 padding_outer: Padding::ZERO,
102 button_alignment: alignment::Vertical::Top,
103 palette,
104 class: <Theme as Catalog>::default(),
105 }
106 }
107
108 pub fn title(mut self, title: impl IntoFragment<'a>) -> Self {
110 self.title = Some(title.into_fragment());
111 self
112 }
113
114 pub fn on_press(mut self, on_press: Message) -> Self
116 where
117 Message: Clone,
118 {
119 self.on_press = Some(Box::new(move || on_press.clone()));
120 self
121 }
122
123 pub fn on_press_with(mut self, on_press: impl Fn() -> Message + 'a) -> Self {
128 self.on_press = Some(Box::new(on_press));
129 self
130 }
131
132 pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self
134 where
135 Message: Clone,
136 {
137 self.on_press = on_press.map(|message| Box::new(move || message.clone()) as _);
138
139 self
140 }
141
142 pub fn width(mut self, width: impl Into<Length>) -> Self {
144 self.width = width.into();
145 self
146 }
147
148 pub fn height(mut self, height: impl Into<Length>) -> Self {
150 self.height = height.into();
151 self
152 }
153
154 pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
156 self.max_width = Some(max_width.into().0);
157 self
158 }
159
160 pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
162 self.max_height = Some(max_height.into().0);
163 self
164 }
165
166 pub fn align_left(self) -> Self {
168 self.align_x(alignment::Horizontal::Left)
169 }
170
171 pub fn align_right(self) -> Self {
173 self.align_x(alignment::Horizontal::Right)
174 }
175
176 pub fn align_top(self) -> Self {
178 self.align_y(alignment::Vertical::Top)
179 }
180
181 pub fn align_bottom(self) -> Self {
183 self.align_y(alignment::Vertical::Bottom)
184 }
185
186 pub fn align_x(mut self, alignment: impl Into<alignment::Horizontal>) -> Self {
190 self.horizontal_alignment = alignment.into();
191 self
192 }
193
194 pub fn align_y(mut self, alignment: impl Into<alignment::Vertical>) -> Self {
198 self.vertical_alignment = alignment.into();
199 self
200 }
201
202 pub fn padding_inner(mut self, padding: impl Into<Padding>) -> Self {
204 self.padding_inner = padding.into();
205 self
206 }
207
208 pub fn padding_outer(mut self, padding: impl Into<Padding>) -> Self {
210 self.padding_outer = padding.into();
211 self
212 }
213
214 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
216 self.spacing = spacing.into().0;
217 self
218 }
219
220 pub fn align_buttons(mut self, align: impl Into<alignment::Vertical>) -> Self {
222 self.button_alignment = align.into();
223 self
224 }
225
226 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
230 self.font = Some(font.into());
231 self
232 }
233
234 pub fn push_button(mut self, button: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
236 self.buttons.push(button.into());
237 self
238 }
239
240 pub fn push_button_maybe(
242 self,
243 button: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
244 ) -> Self {
245 if let Some(button) = button {
246 self.push_button(button)
247 } else {
248 self
249 }
250 }
251
252 pub fn extend_buttons(
254 self,
255 buttons: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
256 ) -> Self {
257 buttons.into_iter().fold(self, Self::push_button)
258 }
259
260 pub fn backdrop(self, color: impl Into<Color>) -> Self
262 where
263 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
264 {
265 let backdrop_color = color.into();
266
267 self.style(move |_theme| Style { backdrop_color })
268 }
269
270 #[must_use]
272 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
273 where
274 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
275 {
276 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
277 self
278 }
279
280 #[must_use]
282 pub fn class(mut self, class: impl Into<<Theme as Catalog>::Class<'a>>) -> Self {
283 self.class = class.into();
284 self
285 }
286
287 fn view(self) -> Element<'a, Message, Theme, Renderer>
288 where
289 <Theme as container::Catalog>::Class<'a>: From<container::StyleFn<'a, Theme>>,
290 {
291 let dialog = self.is_open.then(|| {
292 let has_title = self.title.is_some();
293 let has_buttons = !self.buttons.is_empty();
294
295 let contents = Container::new(column![
296 self.title.map(|title| {
297 let text = text(title)
298 .size(20)
299 .line_height(text::LineHeight::Absolute(Pixels(26.0)));
300
301 if let Some(font) = self.font {
302 text.font(font)
303 } else {
304 text
305 }
306 }),
307 has_title.then_some(vertical().height(12)),
308 self.content
309 ])
310 .padding(self.padding_inner);
311
312 let contents = if has_buttons {
313 contents.width(Length::Fill)
314 } else {
315 contents
316 };
317
318 let buttons = has_buttons.then_some(
319 Container::new(
320 Row::with_children(self.buttons)
321 .spacing(self.spacing)
322 .align_y(self.button_alignment),
323 )
324 .padding(self.padding_inner),
325 );
326
327 let max_width = self.max_width.unwrap_or(
328 if has_buttons && !matches!(self.width, Length::Fixed(_)) {
329 DEFAULT_MAX_WIDTH
330 } else {
331 f32::INFINITY
332 },
333 );
334
335 let max_height = self.max_height.unwrap_or(
336 if has_buttons && !matches!(self.height, Length::Fixed(_)) {
337 DEFAULT_MAX_HEIGHT
338 } else {
339 f32::INFINITY
340 },
341 );
342
343 let content = Container::new(column![
344 contents,
345 has_buttons.then_some(vertical()),
346 buttons,
347 ])
348 .width(self.width)
349 .max_width(max_width)
350 .height(self.height)
351 .max_height(max_height)
352 .style(|_| container::Style {
353 background: Some(Background::Color(self.palette.surface_container_high)),
354 border: core::Border {
355 radius: Radius::from(24),
356 ..Default::default()
357 },
358 shadow: shadow(self.palette.shadow(), 1.0),
359 ..Default::default()
360 })
361 .clip(true);
362
363 let backdrop = mouse_area(
364 container(opaque(content))
365 .style(move |theme| container::Style {
366 background: Some(Catalog::style(theme, &self.class).backdrop_color.into()),
367 ..Default::default()
368 })
369 .padding(self.padding_outer)
370 .width(Length::Fill)
371 .height(Length::Fill)
372 .align_x(self.horizontal_alignment)
373 .align_y(self.vertical_alignment),
374 );
375
376 if let Some(on_press) = self.on_press {
377 opaque(backdrop.on_press(on_press()))
378 } else {
379 opaque(backdrop)
380 }
381 });
382
383 stack![self.base, dialog].into()
384 }
385}
386
387pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
391
392pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
396
397impl<'a, Message, Theme, Renderer> From<Dialog<'a, Message, Theme, Renderer>>
398 for Element<'a, Message, Theme, Renderer>
399where
400 Renderer: 'a + core::Renderer + core::text::Renderer,
401 Theme: 'a + Catalog,
402 Message: 'a + Clone,
403 <Theme as container::Catalog>::Class<'a>: From<container::StyleFn<'a, Theme>>,
404{
405 fn from(dialog: Dialog<'a, Message, Theme, Renderer>) -> Self {
406 dialog.view()
407 }
408}
409
410#[derive(Debug, Clone, Copy, PartialEq)]
412pub struct Style {
413 pub backdrop_color: Color,
415}
416
417pub trait Catalog: text::Catalog + container::Catalog {
419 type Class<'a>;
421
422 fn default<'a>() -> <Self as Catalog>::Class<'a>;
424
425 fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
427 <Self as text::Catalog>::default()
428 }
429
430 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
432 <Self as container::Catalog>::default()
433 }
434
435 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
437}
438
439pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
441
442impl Catalog for Theme {
443 type Class<'a> = StyleFn<'a, Self>;
444
445 fn default<'a>() -> <Self as Catalog>::Class<'a> {
446 Box::new(default)
447 }
448
449 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
450 Box::new(|_| container::background(color!(0xff0000)))
451 }
452
453 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
454 class(self)
455 }
456}
457
458pub fn default<Theme>(_theme: &Theme) -> Style {
460 Style {
461 backdrop_color: core::color!(0x000000, 0.3),
462 }
463}