Skip to main content

sequoia_elements/
dialog.rs

1use 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}