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::{
11 self, Color, Element, Font, Length, Padding, Pixels, Widget, alignment,
12};
13
14pub struct Dialog<
26 'a,
27 Message,
28 Base,
29 Content,
30 Theme = iced_widget::Theme,
31 Renderer = iced_widget::Renderer,
32> where
33 Theme: 'a + Catalog,
34{
35 is_open: bool,
36 base: Base,
37 title: Option<Fragment<'a>>,
38 content: Content,
39 controls: Vec<Element<'a, Message, Theme, Renderer>>,
40 on_press: Option<Box<dyn Fn() -> Message + 'a>>,
41 font: Option<Font>,
42 width: Length,
43 height: Length,
44 horizontal_alignment: alignment::Horizontal,
45 vertical_alignment: alignment::Vertical,
46 spacing: f32,
47 box_padding: Padding,
48 backdrop_padding: Padding,
49 control_alignment: alignment::Vertical,
50 class: <Theme as Catalog>::Class<'a>,
51 title_class: <Theme as text::Catalog>::Class<'a>,
52 container_class: <Theme as container::Catalog>::Class<'a>,
53}
54
55impl<'a, Message, Base, Content, Theme, Renderer>
56 Dialog<'a, Message, Base, Content, Theme, Renderer>
57where
58 Message: 'a + Clone,
59 Theme: 'a + Catalog,
60 Renderer: core::text::Renderer,
61{
62 pub fn new(is_open: bool, base: Base, content: Content) -> Self {
64 Self::with_controls(is_open, base, content, Vec::new())
65 }
66
67 pub fn with_controls(
69 is_open: bool,
70 base: Base,
71 content: Content,
72 controls: Vec<Element<'a, Message, Theme, Renderer>>,
73 ) -> Self {
74 Self {
75 is_open,
76 base,
77 title: None,
78 content,
79 controls,
80 on_press: None,
81 font: None,
82 width: Length::Fit.max(DEFAULT_MAX_WIDTH),
83 height: Length::Fit.max(DEFAULT_MAX_HEIGHT),
84 horizontal_alignment: alignment::Horizontal::Center,
85 vertical_alignment: alignment::Vertical::Center,
86 spacing: 8.0,
87 box_padding: 24.into(),
88 backdrop_padding: Padding::ZERO,
89 control_alignment: alignment::Vertical::Top,
90 class: <Theme as Catalog>::default(),
91 title_class: <Theme as Catalog>::default_title(),
92 container_class: <Theme as Catalog>::default_container(),
93 }
94 }
95
96 pub fn title(mut self, title: impl IntoFragment<'a>) -> Self {
98 self.title = Some(title.into_fragment());
99 self
100 }
101
102 pub fn on_press(mut self, on_press: Message) -> Self {
104 self.on_press = Some(Box::new(move || on_press.clone()));
105 self
106 }
107
108 pub fn on_press_with(
113 mut self,
114 on_press: impl Fn() -> Message + 'a,
115 ) -> Self {
116 self.on_press = Some(Box::new(on_press));
117 self
118 }
119
120 pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self {
122 self.on_press =
123 on_press.map(|message| Box::new(move || message.clone()) as _);
124
125 self
126 }
127
128 pub fn width(mut self, width: impl Into<Length>) -> Self {
130 self.width = width.into();
131 self
132 }
133
134 pub fn height(mut self, height: impl Into<Length>) -> Self {
136 self.height = height.into();
137 self
138 }
139
140 pub fn align_left(self) -> Self {
142 self.align_x(alignment::Horizontal::Left)
143 }
144
145 pub fn align_right(self) -> Self {
147 self.align_x(alignment::Horizontal::Right)
148 }
149
150 pub fn align_top(self) -> Self {
152 self.align_y(alignment::Vertical::Top)
153 }
154
155 pub fn align_bottom(self) -> Self {
157 self.align_y(alignment::Vertical::Bottom)
158 }
159
160 pub fn align_x(
164 mut self,
165 alignment: impl Into<alignment::Horizontal>,
166 ) -> Self {
167 self.horizontal_alignment = alignment.into();
168 self
169 }
170
171 pub fn align_y(
175 mut self,
176 alignment: impl Into<alignment::Vertical>,
177 ) -> Self {
178 self.vertical_alignment = alignment.into();
179 self
180 }
181
182 pub fn box_padding(mut self, padding: impl Into<Padding>) -> Self {
184 self.box_padding = padding.into();
185 self
186 }
187
188 pub fn backdrop_padding(mut self, padding: impl Into<Padding>) -> Self {
190 self.backdrop_padding = padding.into();
191 self
192 }
193
194 pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self {
196 self.spacing = spacing.into().0;
197 self
198 }
199
200 pub fn align_controls(
202 mut self,
203 align: impl Into<alignment::Vertical>,
204 ) -> Self {
205 self.control_alignment = align.into();
206 self
207 }
208
209 pub fn font(mut self, font: Font) -> Self {
211 self.font = Some(font);
212 self
213 }
214
215 pub fn push_control(
217 mut self,
218 control: Element<'a, Message, Theme, Renderer>,
219 ) -> Self {
220 self.controls.push(control);
221 self
222 }
223
224 pub fn push_control_maybe(
226 self,
227 control: Option<Element<'a, Message, Theme, Renderer>>,
228 ) -> Self {
229 if let Some(control) = control {
230 self.push_control(control)
231 } else {
232 self
233 }
234 }
235
236 pub fn extend_controls(
238 self,
239 controls: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
240 ) -> Self {
241 controls.into_iter().fold(self, Self::push_control)
242 }
243
244 pub fn backdrop(self, color: impl Into<Color>) -> Self
246 where
247 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
248 {
249 let backdrop_color = color.into();
250
251 self.style(move |_theme| Style { backdrop_color })
252 }
253
254 #[must_use]
256 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
257 where
258 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
259 {
260 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
261 self
262 }
263
264 #[must_use]
266 pub fn title_style(
267 mut self,
268 style: impl Fn(&Theme) -> text::Style + 'a,
269 ) -> Self
270 where
271 <Theme as text::Catalog>::Class<'a>: From<text::StyleFn<'a, Theme>>,
272 {
273 self.title_class = (Box::new(style) as text::StyleFn<'a, Theme>).into();
274 self
275 }
276
277 #[must_use]
279 pub fn container_style(
280 mut self,
281 style: impl Fn(&Theme) -> container::Style + 'a,
282 ) -> Self
283 where
284 <Theme as container::Catalog>::Class<'a>:
285 From<container::StyleFn<'a, Theme>>,
286 {
287 self.container_class =
288 (Box::new(style) as container::StyleFn<'a, Theme>).into();
289 self
290 }
291
292 #[must_use]
294 pub fn class(
295 mut self,
296 class: impl Into<<Theme as Catalog>::Class<'a>>,
297 ) -> Self {
298 self.class = class.into();
299 self
300 }
301
302 #[must_use]
304 pub fn title_class(
305 mut self,
306 class: impl Into<<Theme as text::Catalog>::Class<'a>>,
307 ) -> Self {
308 self.title_class = class.into();
309 self
310 }
311
312 #[must_use]
314 pub fn container_class(
315 mut self,
316 class: impl Into<<Theme as container::Catalog>::Class<'a>>,
317 ) -> Self {
318 self.container_class = class.into();
319 self
320 }
321
322 #[must_use]
324 pub fn to_widget(self) -> impl Widget<Message, Theme, Renderer>
325 where
326 Base: Widget<Message, Theme, Renderer> + 'a,
327 Content: Widget<Message, Theme, Renderer> + 'a,
328 <Theme as container::Catalog>::Class<'a>:
329 From<container::StyleFn<'a, Theme>>,
330 {
331 let dialog = self.is_open.then(|| {
332 let has_title = self.title.is_some();
333 let has_controls = !self.controls.is_empty();
334
335 let contents = Container::new(column![
336 self.title.map(|title| {
337 let text = text(title)
338 .size(20)
339 .line_height(text::LineHeight::Absolute(Pixels(26.0)))
340 .class(self.title_class);
341
342 if let Some(font) = self.font {
343 text.font(font)
344 } else {
345 text
346 }
347 }),
348 has_title.then_some(vertical().height(12)),
349 self.content
350 ])
351 .padding(self.box_padding);
352
353 let contents = if has_controls {
354 contents.width(Length::Fill)
355 } else {
356 contents
357 };
358
359 let controls = has_controls.then_some(
360 Container::new(
361 Row::with_children(self.controls)
362 .spacing(self.spacing)
363 .align_y(self.control_alignment),
364 )
365 .height(80)
366 .padding(self.box_padding),
367 );
368
369 let content = Container::new(column![
370 contents,
371 has_controls.then_some(vertical()),
372 controls,
373 ])
374 .width(self.width)
375 .height(self.height)
376 .class(self.container_class)
377 .clip(true);
378
379 let backdrop = mouse_area(
380 container(opaque(content))
381 .style(move |theme| container::Style {
382 background: Some(
383 Catalog::style(theme, &self.class)
384 .backdrop_color
385 .into(),
386 ),
387 ..Default::default()
388 })
389 .padding(self.backdrop_padding)
390 .width(Length::Fill)
391 .height(Length::Fill)
392 .align_x(self.horizontal_alignment)
393 .align_y(self.vertical_alignment),
394 );
395
396 if let Some(on_press) = self.on_press {
397 opaque(backdrop.on_press(on_press()))
398 } else {
399 opaque(backdrop)
400 }
401 });
402
403 stack![self.base, dialog]
404 }
405}
406
407pub const DEFAULT_MAX_WIDTH: f32 = 400.0;
409
410pub const DEFAULT_MAX_HEIGHT: f32 = 260.0;
412
413#[derive(Debug, Clone, Copy, PartialEq)]
415pub struct Style {
416 pub backdrop_color: Color,
418}
419
420pub trait Catalog: text::Catalog + container::Catalog {
422 type Class<'a>;
424
425 fn default<'a>() -> <Self as Catalog>::Class<'a>;
427
428 fn default_title<'a>() -> <Self as text::Catalog>::Class<'a> {
430 <Self as text::Catalog>::default()
431 }
432
433 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
435 <Self as container::Catalog>::default()
436 }
437
438 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style;
440}
441
442pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
444
445impl Catalog for Theme {
446 type Class<'a> = StyleFn<'a, Self>;
447
448 fn default<'a>() -> <Self as Catalog>::Class<'a> {
449 Box::new(default)
450 }
451
452 fn default_container<'a>() -> <Self as container::Catalog>::Class<'a> {
453 Box::new(|theme| container::background(theme.seed().background))
454 }
455
456 fn style(&self, class: &<Self as Catalog>::Class<'_>) -> Style {
457 class(self)
458 }
459}
460
461pub fn default<Theme>(_theme: &Theme) -> Style {
463 Style {
464 backdrop_color: core::color!(0x000000, 0.3),
465 }
466}