1#![forbid(unsafe_code)]
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5
6use sequoia_core::{Color, Edges, Length};
7use sequoia_layout::LayoutStyle;
8use sequoia_reactive::Signal;
9use sequoia_style::{Backdrop, DrawerStyle, Theme};
10use sequoia_view::{
11 Anchor, AnyView, Dismiss, Easing, InputMode, OverlayEntry, OverlayId, OverlayLayer,
12 OverlayOverflow, PaintCtx, Placement, Presence, Unit, View,
13};
14
15use crate::modal::{DEFAULT_SCRIM, ModalScrim, OverlayPanel};
16
17pub(crate) const DRAWER_ENTER_SECS: f32 = 0.3;
18const DRAWER_ENTER_EASE: Easing = Easing::CubicBezier(0.4, 0.0, 0.2, 1.0);
19
20const DRAWER_ENTER_X: f32 = 360.0;
21const DRAWER_ENTER_Y: f32 = 480.0;
22
23thread_local! {
24 static ENTRIES: RefCell<HashMap<usize, OverlayId>> = RefCell::new(HashMap::new());
25}
26
27fn get_entry(key: usize) -> Option<OverlayId> {
28 ENTRIES.with(|m| m.borrow().get(&key).copied())
29}
30
31fn insert_entry(key: usize, id: OverlayId) {
32 ENTRIES.with(|m| {
33 m.borrow_mut().insert(key, id);
34 });
35}
36
37fn take_entry(key: usize) -> Option<OverlayId> {
38 ENTRIES.with(|m| m.borrow_mut().remove(&key))
39}
40
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum DrawerSide {
44 Top,
45 Bottom,
46 Left,
47 #[default]
48 Right,
49}
50
51impl DrawerSide {
52 fn placement(self) -> Placement {
53 match self {
54 DrawerSide::Top => Placement::Top,
55 DrawerSide::Bottom => Placement::Bottom,
56 DrawerSide::Left => Placement::Left,
57 DrawerSide::Right => Placement::Right,
58 }
59 }
60}
61
62pub struct Drawer {
63 open: Signal<bool>,
64 child: Option<AnyView>,
65 side: DrawerSide,
66 width: Length,
67 height: Length,
68 padding: Edges,
69 style: Option<DrawerStyle>,
70 background: Option<Color>,
71 border: Option<Color>,
72 border_width: Option<f32>,
73 radius: Option<f32>,
74 backdrop: Option<Backdrop>,
75 scrim: Option<Color>,
76 scrim_backdrop: Option<Backdrop>,
77 dismiss: Dismiss,
78 margin: Edges,
79}
80
81impl Drawer {
82 pub fn new(open: Signal<bool>) -> Self {
83 Self {
84 open,
85 child: None,
86 side: DrawerSide::Right,
87 width: 330.0.into(),
88 height: Length::Fit,
89 padding: Edges::ZERO,
90 style: None,
91 background: None,
92 border: None,
93 border_width: None,
94 radius: None,
95 backdrop: None,
96 scrim: None,
97 scrim_backdrop: None,
98 dismiss: Dismiss::OutsideClick,
99 margin: Edges::ZERO,
100 }
101 }
102
103 pub fn child<V: View + 'static>(mut self, child: V) -> Self {
104 self.child = Some(AnyView::new(child));
105 self
106 }
107
108 pub fn child_if<V: View + 'static>(mut self, cond: bool, make: impl FnOnce() -> V) -> Self {
109 if cond {
110 self.child = Some(AnyView::new(make()));
111 }
112 self
113 }
114
115 pub fn child_opt<V: View + 'static>(mut self, child: Option<V>) -> Self {
116 if let Some(v) = child {
117 self.child = Some(AnyView::new(v));
118 }
119 self
120 }
121
122 pub fn side(mut self, side: DrawerSide) -> Self {
123 self.side = side;
124 self
125 }
126
127 pub fn right(self) -> Self {
128 self.side(DrawerSide::Right)
129 }
130
131 pub fn left(self) -> Self {
132 self.side(DrawerSide::Left)
133 }
134
135 pub fn top(self) -> Self {
136 self.side(DrawerSide::Top)
137 }
138
139 pub fn bottom(self) -> Self {
140 self.side(DrawerSide::Bottom)
141 }
142
143 pub fn width(mut self, width: impl Into<Length>) -> Self {
144 self.width = width.into();
145 self
146 }
147
148 pub fn height(mut self, height: impl Into<Length>) -> Self {
149 self.height = height.into();
150 self
151 }
152
153 pub fn size(mut self, width: impl Into<Length>, height: impl Into<Length>) -> Self {
154 self.width = width.into();
155 self.height = height.into();
156 self
157 }
158
159 pub fn padding(mut self, padding: impl Into<Edges>) -> Self {
160 self.padding = padding.into();
161 self
162 }
163
164 pub fn pad(self, padding: impl Into<Edges>) -> Self {
165 self.padding(padding)
166 }
167
168 pub fn padding_xy(mut self, x: f32, y: f32) -> Self {
169 self.padding = Edges::xy(x, y);
170 self
171 }
172
173 pub fn style(mut self, style: DrawerStyle) -> Self {
174 self.style = Some(style);
175 self
176 }
177
178 pub fn background(mut self, color: Color) -> Self {
179 self.background = Some(color);
180 self
181 }
182
183 pub fn border_color(mut self, color: Color) -> Self {
184 self.border = Some(color);
185 self
186 }
187
188 pub fn border_width(mut self, width: f32) -> Self {
189 self.border_width = Some(width.max(0.0));
190 self
191 }
192
193 pub fn border(mut self, width: f32, color: Color) -> Self {
194 self.border_width = Some(width.max(0.0));
195 self.border = Some(color);
196 self
197 }
198
199 pub fn radius(mut self, radius: f32) -> Self {
200 self.radius = Some(radius.max(0.0));
201 self
202 }
203
204 pub fn backdrop(mut self, backdrop: Backdrop) -> Self {
205 self.backdrop = Some(backdrop);
206 self
207 }
208
209 pub fn scrim_color(mut self, color: Color) -> Self {
210 self.scrim = Some(color);
211 self
212 }
213
214 pub fn scrim_backdrop(mut self, backdrop: Backdrop) -> Self {
215 self.scrim_backdrop = Some(backdrop);
216 self
217 }
218
219 pub fn dismiss(mut self, dismiss: Dismiss) -> Self {
220 self.dismiss = dismiss;
221 self
222 }
223
224 pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
225 let m = margin.into();
226 self.margin = Edges::only(
227 sanitize_margin(m.left),
228 sanitize_margin(m.top),
229 sanitize_margin(m.right),
230 sanitize_margin(m.bottom),
231 );
232 self
233 }
234
235 pub fn margin_xy(self, x: f32, y: f32) -> Self {
236 self.margin(Edges::xy(x, y))
237 }
238
239 pub fn margin_x(mut self, x: f32) -> Self {
240 let x = sanitize_margin(x);
241 self.margin.left = x;
242 self.margin.right = x;
243 self
244 }
245
246 pub fn margin_y(mut self, y: f32) -> Self {
247 let y = sanitize_margin(y);
248 self.margin.top = y;
249 self.margin.bottom = y;
250 self
251 }
252
253 pub fn margin_left(mut self, v: f32) -> Self {
254 self.margin.left = sanitize_margin(v);
255 self
256 }
257
258 pub fn margin_right(mut self, v: f32) -> Self {
259 self.margin.right = sanitize_margin(v);
260 self
261 }
262
263 pub fn margin_top(mut self, v: f32) -> Self {
264 self.margin.top = sanitize_margin(v);
265 self
266 }
267
268 pub fn margin_bottom(mut self, v: f32) -> Self {
269 self.margin.bottom = sanitize_margin(v);
270 self
271 }
272
273 pub fn scrim(&self) -> ModalScrim {
274 let style = self.style.unwrap_or_default();
275 ModalScrim::new(self.open.clone())
276 .scrim_color(self.scrim.or(style.scrim).unwrap_or(DEFAULT_SCRIM))
277 .backdrop(
278 self.scrim_backdrop
279 .or(style.backdrop)
280 .unwrap_or(Backdrop::NONE),
281 )
282 }
283
284 pub fn is_open(&self) -> bool {
285 self.open.get_untracked()
286 }
287
288 pub fn open(&self) {
289 self.open.set(true);
290 }
291
292 pub fn close(&self) {
293 self.open.set(false);
294 }
295
296 pub fn toggle(&self) {
297 self.open.set(!self.open.get_untracked());
298 }
299
300 fn key(&self) -> usize {
301 self.open.id()
302 }
303
304 fn sync_overlay(&self, ctx: &mut PaintCtx, theme: &Theme) {
305 let key = self.key();
306 if !self.open.get_untracked() {
307 if let Some(id) = take_entry(key) {
308 ctx.hide_overlay(id);
309 }
310 return;
311 }
312 if let Some(id) = get_entry(key) {
313 if !ctx.has_overlay(id) {
314 take_entry(key);
315 self.open.set(false);
316 }
317 return;
318 }
319 let Some(child) = self.child.clone() else {
320 self.open.set(false);
321 return;
322 };
323 let style = self.style.unwrap_or_default();
324 let radius = self.radius.or(style.radius).unwrap_or(theme.radius);
325 let backdrop = self.backdrop.or(style.backdrop).unwrap_or(Backdrop::MD);
326 let panel = OverlayPanel {
327 child,
328 width: self.width,
329 height: self.height,
330 padding: self.padding,
331 background: self
332 .background
333 .or(style.background)
334 .unwrap_or(theme.surface),
335 border: self.border.or(style.border).unwrap_or(theme.border),
336 border_width: self
337 .border_width
338 .or(style.border_width)
339 .unwrap_or(1.0)
340 .max(0.0),
341 radius,
342 backdrop,
343 };
344
345 let (dx, dy) = match self.side {
346 DrawerSide::Left => (-DRAWER_ENTER_X, 0.0),
347 DrawerSide::Right => (DRAWER_ENTER_X, 0.0),
348 DrawerSide::Top => (0.0, -DRAWER_ENTER_Y),
349 DrawerSide::Bottom => (0.0, DRAWER_ENTER_Y),
350 };
351
352 let enter = Presence::new(Signal::new(true))
353 .from(1.0)
354 .enter_duration(DRAWER_ENTER_SECS)
355 .enter_easing(DRAWER_ENTER_EASE)
356 .slide(dx, dy)
357 .child(panel);
358 let id = ctx.show_overlay(
359 OverlayEntry::new(enter)
360 .anchor(Anchor::Viewport)
361 .placement(self.side.placement())
362 .layer(OverlayLayer::Modal)
363 .input_mode(InputMode::Modal)
364 .dismiss(self.dismiss)
365 .overflow(OverlayOverflow::Clamp),
366 );
367 insert_entry(key, id);
368 }
369}
370
371impl View for Drawer {
372 fn body(&self) -> impl View + 'static {
373 Unit
374 }
375 fn is_leaf(&self) -> bool {
376 true
377 }
378
379 fn hit_testable(&self) -> bool {
380 false
381 }
382 fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
383 let _ = self.open.get();
384 Some(Vec::new())
385 }
386 fn layout_style(&self) -> LayoutStyle {
387 LayoutStyle {
388 width: Length::Fit,
389 height: Length::Fit,
390 margin: self.margin,
391 ..LayoutStyle::default()
392 }
393 }
394 fn on_unmount(&self) {
395 take_entry(self.key());
396 }
397 fn paint_self(&self, ctx: &mut PaintCtx) {
398 let theme = ctx.theme();
399 self.sync_overlay(ctx, &theme);
400 }
401}
402
403fn sanitize_margin(v: f32) -> f32 {
404 if v.is_finite() { v.max(0.0) } else { 0.0 }
405}