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::{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}