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