sequoia_elements/
dialog.rs1use std::rc::Rc;
2
3use sequoia_core::{Color, Edges};
4use sequoia_reactive::Signal;
5use sequoia_view::{
6 AnyView, Column, Dismiss, DismissableLayer, FocusScope, Row, Spacer, Text, Unit, View,
7};
8
9use crate::button::Button;
10use crate::modal::Modal;
11use crate::modal::ModalScrim;
12use sequoia_style::{DIALOG_DANGER, DialogStyle};
13
14type ActionFn = Rc<dyn Fn()>;
15
16#[derive(Clone)]
17struct DialogAction {
18 label: String,
19 danger: bool,
20 action: ActionFn,
21}
22
23#[derive(Clone)]
24pub struct Dialog {
25 open: Signal<bool>,
26 title: String,
27 content: AnyView,
28 actions: Vec<DialogAction>,
29 cancel_label: Option<String>,
30 width: f32,
31 on_close: Option<ActionFn>,
32 style: Option<DialogStyle>,
33}
34
35impl Dialog {
36 pub fn new(open: Signal<bool>, title: impl Into<String>) -> Self {
37 Self {
38 open,
39 title: title.into(),
40 content: AnyView::new(Unit),
41 actions: Vec::new(),
42 cancel_label: None,
43 width: 520.0,
44 on_close: None,
45 style: None,
46 }
47 }
48
49 pub fn style(mut self, style: DialogStyle) -> Self {
50 self.style = Some(style);
51 self
52 }
53
54 pub fn content(mut self, content: impl View + 'static) -> Self {
55 self.content = AnyView::new(content);
56 self
57 }
58
59 pub fn action(mut self, label: impl Into<String>, action: impl Fn() + 'static) -> Self {
60 self.actions.push(DialogAction {
61 label: label.into(),
62 danger: false,
63 action: Rc::new(action),
64 });
65 self
66 }
67
68 pub fn danger(mut self, label: impl Into<String>, action: impl Fn() + 'static) -> Self {
69 self.actions.push(DialogAction {
70 label: label.into(),
71 danger: true,
72 action: Rc::new(action),
73 });
74 self
75 }
76
77 pub fn cancel(mut self, label: impl Into<String>) -> Self {
78 self.cancel_label = Some(label.into());
79 self
80 }
81
82 pub fn width(mut self, width: f32) -> Self {
83 if width.is_finite() && width > 0.0 {
84 self.width = width;
85 }
86 self
87 }
88
89 pub fn on_close(mut self, f: impl Fn() + 'static) -> Self {
90 self.on_close = Some(Rc::new(f));
91 self
92 }
93
94 pub fn is_open(&self) -> bool {
95 self.open.get_untracked()
96 }
97
98 pub fn open_signal(&self) -> Signal<bool> {
99 self.open.clone()
100 }
101
102 pub fn close(&self) {
103 self.open.set(false);
104 if let Some(f) = &self.on_close {
105 f();
106 }
107 }
108
109 pub fn scrim(&self) -> ModalScrim {
110 Modal::new(self.open.clone()).scrim()
111 }
112
113 fn footer(&self) -> AnyView {
114 let mut row = Row::new().gap(8.0);
115 if let Some(label) = &self.cancel_label {
116 let this = self.clone();
117 row = row
118 .child(Button::new(label.clone()).on_click(move || this.close()))
119 .child(Spacer::flex(1.0));
120 } else {
121 row = row.child(Spacer::flex(1.0));
122 }
123 for a in &self.actions {
124 let action = a.action.clone();
125 let btn = Button::new(a.label.clone()).on_click(move || {
126 action();
127 });
128 let btn = if a.danger {
129 btn.color(self.danger_color())
130 } else {
131 btn
132 };
133 row = row.child(btn);
134 }
135 AnyView::new(row)
136 }
137
138 fn danger_color(&self) -> Color {
139 self.style.and_then(|s| s.danger).unwrap_or(DIALOG_DANGER)
140 }
141
142 fn title_size(&self) -> f32 {
143 self.style.and_then(|s| s.title_size).unwrap_or(16.0)
144 }
145
146 fn card(&self) -> AnyView {
147 let this = self.clone();
148 let content = self.content.clone();
149 let footer = self.footer();
150 let title = self.title.clone();
151 let card = Column::new()
152 .gap(12.0)
153 .child(Text::new(title).size(this.title_size()).bold())
154 .child(content)
155 .child(footer);
156 let open = self.open.clone();
157
158 let layer = DismissableLayer::wrap(FocusScope::new().trap(true).child(card))
159 .bind(open)
160 .on_escape(move || {
161 this.close();
162 });
163 AnyView::new(layer)
164 }
165}
166
167impl View for Dialog {
168 fn body(&self) -> impl View + 'static {
169 Unit
170 }
171
172 fn accessibility(&self) -> Option<sequoia_view::Accessible> {
173 Some(sequoia_view::Accessible::new(
174 sequoia_view::AccessibleRole::Dialog,
175 self.title.clone(),
176 ))
177 }
178
179 fn children_override(&self) -> Option<Vec<AnyView>> {
180 let modal = Modal::new(self.open.clone())
181 .width(self.width)
182 .padding(Edges::all(20.0))
183 .dismiss(Dismiss::OutsideClick)
184 .child(self.card());
185 Some(vec![AnyView::new(modal)])
186 }
187
188 fn layout_style(&self) -> sequoia_layout::LayoutStyle {
189 sequoia_layout::LayoutStyle::default()
190 }
191}