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::{Backdrop, ButtonStyle, 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())
111 .scrim()
112 .backdrop(Backdrop::blur(10.0))
113 }
114
115 fn footer(&self) -> AnyView {
116 let mut row = Row::new().gap(8.0);
117 if let Some(label) = &self.cancel_label {
118 let this = self.clone();
119 row = row
120 .child(
121 Button::new(label.clone())
122 .style(ButtonStyle::OUTLINE)
123 .on_click(move || this.close()),
124 )
125 .child(Spacer::flex(1.0));
126 } else {
127 row = row.child(Spacer::flex(1.0));
128 }
129 for a in &self.actions {
130 let action = a.action.clone();
131 let style = if a.danger {
132 ButtonStyle::OUTLINE_ROSE
133 .text(self.danger_color())
134 .border_color(self.danger_color())
135 } else {
136 ButtonStyle::PRIMARY
137 };
138 let btn = Button::new(a.label.clone()).style(style).on_click(move || {
139 action();
140 });
141 row = row.child(btn);
142 }
143 AnyView::new(row)
144 }
145
146 fn danger_color(&self) -> Color {
147 self.style.and_then(|s| s.danger).unwrap_or(DIALOG_DANGER)
148 }
149
150 fn title_size(&self) -> f32 {
151 self.style.and_then(|s| s.title_size).unwrap_or(16.0)
152 }
153
154 fn card(&self) -> AnyView {
155 let this = self.clone();
156 let content = self.content.clone();
157 let footer = self.footer();
158 let title = self.title.clone();
159 let card = Column::new()
160 .gap(12.0)
161 .child(Text::new(title).size(this.title_size()).bold())
162 .child(content)
163 .child(footer);
164 let open = self.open.clone();
165
166 let layer = DismissableLayer::wrap(FocusScope::new().trap(true).child(card))
167 .bind(open)
168 .on_escape(move || {
169 this.close();
170 });
171 AnyView::new(layer)
172 }
173}
174
175impl View for Dialog {
176 fn body(&self) -> impl View + 'static {
177 Unit
178 }
179
180 fn accessibility(&self) -> Option<sequoia_view::Accessible> {
181 Some(sequoia_view::Accessible::new(
182 sequoia_view::AccessibleRole::Dialog,
183 self.title.clone(),
184 ))
185 }
186
187 fn children_override(&self) -> Option<Vec<AnyView>> {
188 let modal = Modal::new(self.open.clone())
189 .width(self.width)
190 .padding(Edges::all(20.0))
191 .dismiss(Dismiss::OutsideClick)
192 .child(self.card());
193 Some(vec![AnyView::new(modal)])
194 }
195
196 fn layout_style(&self) -> sequoia_layout::LayoutStyle {
197 sequoia_layout::LayoutStyle::default()
198 }
199
200 fn hit_testable(&self) -> bool {
201 false
202 }
203}