1use super::DialogAppearance;
2use super::{Button, Clickable, Form};
3use crate::app;
4use crate::bindings::ui;
5use crate::ffi::{
6 AlignItems, BorderStyle, FlexDirection, JustifyContent, PositionType, SemanticRole, Unit,
7};
8use crate::node::{
9 flex_box, portal, Border, ChildContainerSurface, FlexBox, Node, NodeRef, TextNode,
10 ThemeBindable, WeakFlexBox,
11};
12use crate::theme::{current_theme, subscribe, Theme};
13use std::cell::RefCell;
14use std::rc::Rc;
15
16const DIALOG_CARD_WIDTH: f32 = 420.0;
17const DIALOG_BACKGROUND_BLUR: f32 = 16.0;
18const DIALOG_SHADOW_OFFSET_X: f32 = 0.0;
19const DIALOG_SHADOW_OFFSET_Y: f32 = 8.0;
20const DIALOG_SHADOW_BLUR: f32 = 10.0;
21const DIALOG_SHADOW_SPREAD: f32 = 0.0;
22
23thread_local! {
24 static ACTIVE_DIALOG: RefCell<Option<DialogEventTarget>> = const { RefCell::new(None) };
25}
26
27type VoidCallback = Rc<dyn Fn()>;
28type ShownCallback = Rc<dyn Fn(DialogShownEventArgs)>;
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31pub struct DialogShownEventArgs;
32
33#[derive(Clone)]
34pub struct Dialog {
35 root: FlexBox,
36 title_node: TextNode,
37 body_node: TextNode,
38 content_host: FlexBox,
39 accept_button: Button,
40 cancel_button: Button,
41 _buttons_row: FlexBox,
42 form: Form,
43 card: FlexBox,
44 overlay: FlexBox,
45 state: Rc<RefCell<DialogState>>,
46}
47
48#[derive(Clone)]
49struct DialogEventTarget {
50 root: WeakFlexBox,
51 overlay: WeakFlexBox,
52 card: WeakFlexBox,
53 form: Form,
54 state: Rc<RefCell<DialogState>>,
55}
56
57struct DialogState {
58 visible: bool,
59 semantic_scope_token: u32,
60 accept_callback: Option<VoidCallback>,
61 cancel_callback: Option<VoidCallback>,
62 shown_callback: Option<ShownCallback>,
63 backdrop_color_value: u32,
64 dialog_background_blur_sigma_value: f32,
65 shadow_color_value: u32,
66 shadow_offset_x_value: f32,
67 shadow_offset_y_value: f32,
68 shadow_blur_sigma_value: f32,
69 shadow_spread_value: f32,
70 card_background_color_value: u32,
71 card_border_width_value: f32,
72 card_border_color_value: u32,
73 card_border_style_value: BorderStyle,
74 card_border_dash_on_value: f32,
75 card_border_dash_off_value: f32,
76 card_corner_top_left_value: f32,
77 card_corner_top_right_value: f32,
78 card_corner_bottom_right_value: f32,
79 card_corner_bottom_left_value: f32,
80 backdrop_color_overridden: bool,
81 shadow_overridden: bool,
82 card_background_overridden: bool,
83 card_border_overridden: bool,
84 card_corner_radius_overridden: bool,
85}
86
87impl DialogState {
88 fn from_theme(theme: Theme) -> Self {
89 Self {
90 visible: false,
91 semantic_scope_token: 0,
92 accept_callback: None,
93 cancel_callback: None,
94 shown_callback: None,
95 backdrop_color_value: theme.colors.dialog_backdrop,
96 dialog_background_blur_sigma_value: DIALOG_BACKGROUND_BLUR,
97 shadow_color_value: theme.colors.dialog_shadow,
98 shadow_offset_x_value: DIALOG_SHADOW_OFFSET_X,
99 shadow_offset_y_value: DIALOG_SHADOW_OFFSET_Y,
100 shadow_blur_sigma_value: DIALOG_SHADOW_BLUR,
101 shadow_spread_value: DIALOG_SHADOW_SPREAD,
102 card_background_color_value: theme.colors.surface,
103 card_border_width_value: 1.0,
104 card_border_color_value: theme.colors.border,
105 card_border_style_value: BorderStyle::Solid,
106 card_border_dash_on_value: 0.0,
107 card_border_dash_off_value: 0.0,
108 card_corner_top_left_value: theme.spacing.md,
109 card_corner_top_right_value: theme.spacing.md,
110 card_corner_bottom_right_value: theme.spacing.md,
111 card_corner_bottom_left_value: theme.spacing.md,
112 backdrop_color_overridden: false,
113 shadow_overridden: false,
114 card_background_overridden: false,
115 card_border_overridden: false,
116 card_corner_radius_overridden: false,
117 }
118 }
119}
120
121impl Default for Dialog {
122 fn default() -> Self {
123 Self::new("", "")
124 }
125}
126
127impl Dialog {
128 pub fn new(title: impl Into<String>, body: impl Into<String>) -> Self {
129 let theme = current_theme();
130 let root = portal();
131 let title_node = TextNode::new("");
132 title_node
133 .selectable(false)
134 .semantic_role(SemanticRole::Heading);
135 let body_node = TextNode::new("");
136 body_node
137 .selectable(false)
138 .semantic_role(SemanticRole::StaticText);
139 let content_host = flex_box();
140 content_host
141 .width(100.0, Unit::Percent)
142 .flex_direction(FlexDirection::Column);
143 let cancel_button = Button::new("Cancel");
144 let accept_button = Button::new("OK");
145 let buttons_row = flex_box();
146 buttons_row
147 .flex_direction(FlexDirection::Row)
148 .justify_content(JustifyContent::End)
149 .align_items(AlignItems::Center)
150 .width(100.0, Unit::Percent)
151 .child(&accept_button)
152 .child(
153 &flex_box()
154 .width(12.0, Unit::Pixel)
155 .height(1.0, Unit::Pixel)
156 .clone(),
157 )
158 .child(&cancel_button);
159 let form = Form::new();
160 form.default_btn(&accept_button)
161 .cancel_btn(&cancel_button)
162 .child(&buttons_row);
163 let card = flex_box();
164 card.width(DIALOG_CARD_WIDTH, Unit::Pixel)
165 .flex_direction(FlexDirection::Column)
166 .interactive(true)
167 .child(&title_node)
168 .child(
169 &flex_box()
170 .width(100.0, Unit::Percent)
171 .height(12.0, Unit::Pixel)
172 .clone(),
173 )
174 .child(&body_node)
175 .child(
176 &flex_box()
177 .width(100.0, Unit::Percent)
178 .height(16.0, Unit::Pixel)
179 .clone(),
180 )
181 .child(&content_host)
182 .child(
183 &flex_box()
184 .width(100.0, Unit::Percent)
185 .height(24.0, Unit::Pixel)
186 .clone(),
187 )
188 .child(&form)
189 .semantic_role(SemanticRole::Dialog);
190 let overlay = flex_box();
191 overlay
192 .width(100.0, Unit::Percent)
193 .height(100.0, Unit::Percent)
194 .interactive(true)
195 .justify_content(JustifyContent::Center)
196 .align_items(AlignItems::Center)
197 .child(&card);
198 let state = Rc::new(RefCell::new(DialogState::from_theme(theme)));
199
200 root.position_type(PositionType::Absolute)
201 .position(0.0, 0.0)
202 .width(100.0, Unit::Percent)
203 .height(100.0, Unit::Percent);
204
205 let dialog = Self {
206 root,
207 title_node,
208 body_node,
209 content_host,
210 accept_button,
211 cancel_button,
212 _buttons_row: buttons_row,
213 form,
214 card,
215 overlay,
216 state,
217 };
218 dialog.content(title, body);
219 dialog.bind_events();
220 dialog.install_theme_subscription();
221 dialog.apply_theme();
222 dialog
223 }
224
225 pub fn accept_active_dialog() {
226 let dialog = ACTIVE_DIALOG.with(|slot| slot.borrow().as_ref().cloned());
227 if let Some(dialog) = dialog {
228 dialog.accept();
229 }
230 }
231
232 pub fn cancel_active_dialog() {
233 let dialog = ACTIVE_DIALOG.with(|slot| slot.borrow().as_ref().cloned());
234 if let Some(dialog) = dialog {
235 dialog.cancel();
236 }
237 }
238
239 pub fn is_open(&self) -> bool {
240 self.state.borrow().visible
241 }
242
243 pub fn on_accept(&self, handler: impl Fn() + 'static) -> &Self {
244 self.state.borrow_mut().accept_callback = Some(Rc::new(handler));
245 self
246 }
247
248 pub fn on_cancel(&self, handler: impl Fn() + 'static) -> &Self {
249 self.state.borrow_mut().cancel_callback = Some(Rc::new(handler));
250 self
251 }
252
253 pub fn on_shown(&self, handler: impl Fn(DialogShownEventArgs) + 'static) -> &Self {
254 self.state.borrow_mut().shown_callback = Some(Rc::new(handler));
255 self
256 }
257
258 pub fn title_text(&self) -> TextNode {
259 self.title_node.clone()
260 }
261
262 pub fn body_text(&self) -> TextNode {
263 self.body_node.clone()
264 }
265
266 pub fn content_host(&self) -> FlexBox {
267 self.content_host.clone()
268 }
269
270 pub fn accept_action_button(&self) -> Button {
271 self.accept_button.clone()
272 }
273
274 pub fn cancel_action_button(&self) -> Button {
275 self.cancel_button.clone()
276 }
277
278 pub fn appearance(&self, appearance: DialogAppearance) -> &Self {
279 let mut state = self.state.borrow_mut();
280 let backdrop = appearance.backdrop.unwrap_or_default();
281 state.backdrop_color_overridden = backdrop.color.is_some();
282 if let Some(color) = backdrop.color {
283 state.backdrop_color_value = color;
284 }
285 state.dialog_background_blur_sigma_value = backdrop.blur.unwrap_or(DIALOG_BACKGROUND_BLUR);
286
287 let card = appearance.card.unwrap_or_default();
288 state.card_background_overridden = card.background.is_some();
289 if let Some(color) = card.background {
290 state.card_background_color_value = color;
291 }
292 state.card_border_overridden = card.border.is_some();
293 if let Some(border) = card.border {
294 state.card_border_width_value = border.width;
295 state.card_border_color_value = border.color;
296 state.card_border_style_value = border.style;
297 state.card_border_dash_on_value = border.dash_on;
298 state.card_border_dash_off_value = border.dash_off;
299 }
300 state.card_corner_radius_overridden = card.corners.is_some();
301 if let Some(corners) = card.corners {
302 state.card_corner_top_left_value = corners.top_left;
303 state.card_corner_top_right_value = corners.top_right;
304 state.card_corner_bottom_right_value = corners.bottom_right;
305 state.card_corner_bottom_left_value = corners.bottom_left;
306 }
307 state.shadow_overridden = card.shadow.is_some();
308 if let Some(shadow) = card.shadow {
309 state.shadow_color_value = shadow.color;
310 state.shadow_offset_x_value = shadow.offset_x;
311 state.shadow_offset_y_value = shadow.offset_y;
312 state.shadow_blur_sigma_value = shadow.blur_sigma;
313 state.shadow_spread_value = shadow.spread;
314 }
315 drop(state);
316 self.event_target().handle_theme_changed(current_theme());
317 self.apply_theme();
318 self
319 }
320
321 pub fn clear_appearance(&self) -> &Self {
322 self.appearance(DialogAppearance::new())
323 }
324
325 pub fn content(&self, title: impl Into<String>, body: impl Into<String>) -> &Self {
326 let title = title.into();
327 let body = body.into();
328 self.title_node
329 .text(title.clone())
330 .semantic_label(title.clone());
331 self.body_node.text(body.clone()).semantic_label(body);
332 self.card.semantic_label(title);
333 self
334 }
335
336 pub fn show(&self) {
337 self.apply_theme();
338 if self.overlay.parent_handle().is_none() {
339 self.root.child(&self.overlay);
340 }
341 {
342 let mut state = self.state.borrow_mut();
343 if state.semantic_scope_token == 0 && self.overlay.handle().raw() != 0 {
344 state.semantic_scope_token = ui::push_semantic_scope(self.overlay.handle().raw());
345 }
346 state.visible = true;
347 }
348 self.form.activate();
349 if self.accept_button.has_built_handle() {
350 ui::request_focus(self.accept_button.handle().raw());
351 }
352 let event_target = self.event_target();
353 ACTIVE_DIALOG.with(|slot| slot.borrow_mut().replace(event_target.clone()));
354 app::after_next_commit(move || {
355 event_target.fire_shown_after_commit();
356 });
357 }
358
359 pub fn hide(&self) {
360 self.event_target().hide();
361 }
362
363 fn bind_events(&self) {
364 self.card.on_pointer_down(|event| {
365 event.handled = true;
366 });
367
368 let event_target = self.event_target();
369 self.overlay.on_pointer_click(move |event| {
370 event_target.handle_backdrop_click(event);
371 });
372
373 self.accept_button.on_click(|_event| {
374 Dialog::accept_active_dialog();
375 });
376
377 self.cancel_button.on_click(|_event| {
378 Dialog::cancel_active_dialog();
379 });
380 }
381
382 fn install_theme_subscription(&self) {
383 let event_target = self.event_target();
384 let title = self.title_node.clone();
385 let body = self.body_node.clone();
386 let guard = subscribe(move |theme| {
387 event_target.handle_theme_changed(theme);
388 event_target.apply_theme(&title, &body);
389 });
390 self.root
391 .retained_node_ref()
392 .retain_attachment(Rc::new(guard));
393 }
394
395 fn apply_theme(&self) {
396 self.event_target()
397 .apply_theme(&self.title_node, &self.body_node);
398 }
399
400 fn event_target(&self) -> DialogEventTarget {
401 DialogEventTarget {
402 root: self.root.downgrade(),
403 overlay: self.overlay.downgrade(),
404 card: self.card.downgrade(),
405 form: self.form.clone(),
406 state: self.state.clone(),
407 }
408 }
409}
410
411impl Node for Dialog {
412 fn retained_node_ref(&self) -> NodeRef {
413 self.root.retained_node_ref()
414 }
415
416 fn retained_owner_attachment(&self) -> Option<Rc<dyn std::any::Any>> {
417 Some(Rc::new(self.clone()))
418 }
419
420 fn build_self(&self) {
421 self.apply_theme();
422 self.root.build_self();
423 }
424
425 fn dispose(&self) {
426 self.hide();
427 if self.overlay.handle().raw() != 0 {
428 self.overlay.dispose();
429 }
430 self.root.dispose();
431 }
432}
433
434impl crate::node::HasFlexBoxRoot for Dialog {
435 fn flex_box_root(&self) -> &FlexBox {
436 &self.root
437 }
438}
439
440impl ThemeBindable for Dialog {
441 fn theme_binding_node(&self) -> NodeRef {
442 self.root.retained_node_ref()
443 }
444
445 fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
446 let root = self.root.downgrade();
447 let title_node = self.title_node.clone();
448 let body_node = self.body_node.clone();
449 let content_host = self.content_host.clone();
450 let accept_button = self.accept_button.clone();
451 let cancel_button = self.cancel_button.clone();
452 let buttons_row = self._buttons_row.clone();
453 let form = self.form.clone();
454 let card = self.card.clone();
455 let overlay = self.overlay.clone();
456 let state = self.state.clone();
457 Box::new(move || {
458 Some(Self {
459 root: root.upgrade()?,
460 title_node: title_node.clone(),
461 body_node: body_node.clone(),
462 content_host: content_host.clone(),
463 accept_button: accept_button.clone(),
464 cancel_button: cancel_button.clone(),
465 _buttons_row: buttons_row.clone(),
466 form: form.clone(),
467 card: card.clone(),
468 overlay: overlay.clone(),
469 state: state.clone(),
470 })
471 })
472 }
473}
474
475impl DialogEventTarget {
476 fn hide(&self) {
477 let Some(root) = self.root.upgrade() else {
478 return;
479 };
480 let Some(overlay) = self.overlay.upgrade() else {
481 return;
482 };
483 let mut state = self.state.borrow_mut();
484 if !state.visible && overlay.parent_handle().is_none() {
485 return;
486 }
487 if state.semantic_scope_token != 0 {
488 ui::remove_semantic_scope(state.semantic_scope_token);
489 state.semantic_scope_token = 0;
490 }
491 state.visible = false;
492 drop(state);
493 self.form.deactivate();
494 root.remove_child(&overlay);
495 ACTIVE_DIALOG.with(|slot| {
496 let should_clear = {
497 let borrowed = slot.borrow();
498 borrowed
499 .as_ref()
500 .is_some_and(|active| Rc::ptr_eq(&active.state, &self.state))
501 };
502 if should_clear {
503 slot.borrow_mut().take();
504 }
505 });
506 }
507
508 fn accept(&self) {
509 let callback = self.state.borrow().accept_callback.clone();
510 self.hide();
511 if let Some(callback) = callback {
512 callback();
513 }
514 }
515
516 fn cancel(&self) {
517 let callback = self.state.borrow().cancel_callback.clone();
518 self.hide();
519 if let Some(callback) = callback {
520 callback();
521 }
522 }
523
524 fn fire_shown_after_commit(&self) {
525 let state = self.state.borrow();
526 if !state.visible {
527 return;
528 }
529 let Some(overlay) = self.overlay.upgrade() else {
530 return;
531 };
532 if overlay.parent_handle().is_none() {
533 return;
534 }
535 let callback = state.shown_callback.clone();
536 drop(state);
537 if let Some(callback) = callback {
538 callback(DialogShownEventArgs);
539 }
540 }
541
542 fn handle_theme_changed(&self, theme: Theme) {
543 let mut state = self.state.borrow_mut();
544 if !state.backdrop_color_overridden {
545 state.backdrop_color_value = theme.colors.dialog_backdrop;
546 }
547 if !state.shadow_overridden {
548 state.shadow_color_value = theme.colors.dialog_shadow;
549 }
550 if !state.card_background_overridden {
551 state.card_background_color_value = theme.colors.surface;
552 }
553 if !state.card_border_overridden {
554 state.card_border_width_value = 1.0;
555 state.card_border_color_value = theme.colors.border;
556 state.card_border_style_value = BorderStyle::Solid;
557 state.card_border_dash_on_value = 0.0;
558 state.card_border_dash_off_value = 0.0;
559 }
560 if !state.card_corner_radius_overridden {
561 state.card_corner_top_left_value = theme.spacing.md;
562 state.card_corner_top_right_value = theme.spacing.md;
563 state.card_corner_bottom_right_value = theme.spacing.md;
564 state.card_corner_bottom_left_value = theme.spacing.md;
565 }
566 }
567
568 fn apply_theme(&self, title: &TextNode, body: &TextNode) {
569 let theme = current_theme();
570 let state = self.state.borrow();
571 if let Some(overlay) = self.overlay.upgrade() {
572 overlay
573 .bg_color(state.backdrop_color_value)
574 .background_blur(state.dialog_background_blur_sigma_value);
575 }
576 if let Some(card) = self.card.upgrade() {
577 card.bg_color(state.card_background_color_value)
578 .corners(
579 state.card_corner_top_left_value,
580 state.card_corner_top_right_value,
581 state.card_corner_bottom_right_value,
582 state.card_corner_bottom_left_value,
583 )
584 .border_config(Border {
585 width: state.card_border_width_value,
586 color: state.card_border_color_value,
587 style: state.card_border_style_value,
588 dash_on: state.card_border_dash_on_value,
589 dash_off: state.card_border_dash_off_value,
590 })
591 .drop_shadow(
592 state.shadow_color_value,
593 state.shadow_offset_x_value,
594 state.shadow_offset_y_value,
595 state.shadow_blur_sigma_value,
596 state.shadow_spread_value,
597 )
598 .padding(24.0, 24.0, 24.0, 24.0);
599 }
600 drop(state);
601 title
602 .font_family(theme.fonts.heading_family.clone())
603 .font_size(theme.fonts.size_heading)
604 .text_color(theme.colors.text_primary);
605 body.font_family(theme.fonts.body_family.clone())
606 .font_size(theme.fonts.size_body)
607 .text_color(theme.colors.text_muted);
608 }
609
610 fn handle_backdrop_click(&self, event: &mut crate::PointerEventArgs) {
611 let Some(card) = self.card.upgrade() else {
612 return;
613 };
614 if let Some(bounds) = ui::get_bounds(card.handle().raw()) {
615 let left = bounds[0];
616 let top = bounds[1];
617 let right = left + bounds[2];
618 let bottom = top + bounds[3];
619 if event.scene_x >= left
620 && event.scene_x <= right
621 && event.scene_y >= top
622 && event.scene_y <= bottom
623 {
624 return;
625 }
626 }
627 event.handled = true;
628 self.cancel();
629 }
630}