1use std::rc::Rc;
2use std::time::Duration;
3
4use geometry_core::{Rect, Transform};
5use layout_core::{
6 AlignItems, AvailableSpace, JustifyContent, LayoutError, LayoutStyle, NodeId, SizeDimension,
7};
8use motion_core::{Animated, Easing, tween};
9use platform_core::{Event, Key, NamedKey, PointerButton};
10use reactive_core::RwSignal;
11use renderer_core::{Color, RectStyle};
12use ui_tree::{Component, EventResult, RenderNode};
13
14use crate::context::{compute_layout, mark_dirty, new_container, track_layout};
15use crate::layout_item::LayoutItem;
16
17pub const DEFAULT_SCRIM: Color = Color::rgba(0.0, 0.0, 0.0, 0.35);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Edge {
28 Top,
29 Bottom,
30 Left,
31 Right,
32 Center,
33}
34
35const ENTER_MS: u64 = 200;
37const SLIDE_DISTANCE: f32 = 24.0;
38const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
39
40#[derive(Clone, Copy)]
41enum EnterMotion {
42 Slide(Edge),
43 Fade,
44}
45
46fn enter_transform(motion: EnterMotion, progress: f32) -> ([f32; 6], f32) {
49 let p = progress.clamp(0.0, 1.0);
50 let opacity = p;
51 match motion {
52 EnterMotion::Fade => (IDENTITY, opacity),
53 EnterMotion::Slide(anchor) => {
54 let d = SLIDE_DISTANCE * (1.0 - p);
55 let (dx, dy) = match anchor {
56 Edge::Top => (0.0, -d),
57 Edge::Bottom => (0.0, d),
58 Edge::Left => (-d, 0.0),
59 Edge::Right => (d, 0.0),
60 Edge::Center => (0.0, 0.0),
61 };
62 (Transform::translate(dx, dy).to_array(), opacity)
63 }
64 }
65}
66
67fn apply_enter(node: RenderNode, matrix: [f32; 6], opacity: f32) -> RenderNode {
68 let faded = if opacity < 1.0 {
69 RenderNode::layer(opacity, 0.0, [node])
70 } else {
71 node
72 };
73 if matrix == IDENTITY {
74 faded
75 } else {
76 RenderNode::transform_with(matrix, [faded])
77 }
78}
79
80#[derive(Clone)]
87pub struct SurfaceTransition {
88 progress: Animated<f32>,
89 duration: Duration,
90}
91
92impl SurfaceTransition {
93 pub fn enter() -> Self {
97 let duration = Duration::from_millis(ENTER_MS);
98 let progress = Animated::new(0.0, tween(duration, Easing::EaseOut));
99 progress.retarget(1.0);
100 Self { progress, duration }
101 }
102
103 pub fn leave(&self) {
106 self.progress.retarget(0.0);
107 }
108
109 pub fn duration(&self) -> Duration {
111 self.duration
112 }
113
114 fn get(&self) -> f32 {
115 self.progress.get()
116 }
117}
118
119pub struct SurfaceScaffold {
127 root: NodeId,
128 panel_rect: Option<RwSignal<Rect>>,
129 root_rect: Option<RwSignal<Rect>>,
130 content: Box<dyn LayoutItem>,
131 scrim: Option<Color>,
132 dismiss: Option<Rc<dyn Fn()>>,
133 edge: Edge,
134 transition: Option<SurfaceTransition>,
135}
136
137impl SurfaceScaffold {
138 pub fn new(
143 edge: Edge,
144 align: AlignItems,
145 margin: (i32, i32, i32, i32),
146 scrim: Option<Color>,
147 dismiss: Option<Rc<dyn Fn()>>,
148 content: Box<dyn LayoutItem>,
149 ) -> Result<Self, LayoutError> {
150 let panel_node = content.layout_node();
151 let (mt, mr, mb, ml) = margin;
152 let base = LayoutStyle::new()
153 .width(SizeDimension::Percent(1.0))
154 .height(SizeDimension::Percent(1.0))
155 .padding_top(mt as f32)
156 .padding_right(mr as f32)
157 .padding_bottom(mb as f32)
158 .padding_left(ml as f32);
159 let style = match edge {
160 Edge::Top => base
161 .flex_column()
162 .justify_content(JustifyContent::START)
163 .align_items(align),
164 Edge::Bottom => base
165 .flex_column()
166 .justify_content(JustifyContent::END)
167 .align_items(align),
168 Edge::Left => base
169 .flex_row()
170 .justify_content(JustifyContent::START)
171 .align_items(align),
172 Edge::Right => base
173 .flex_row()
174 .justify_content(JustifyContent::END)
175 .align_items(align),
176 Edge::Center => base
177 .flex_column()
178 .justify_content(JustifyContent::CENTER)
179 .align_items(AlignItems::CENTER),
180 };
181 let root = new_container(style, &[panel_node])?;
182 let panel_rect = track_layout(panel_node);
183 let root_rect = track_layout(root);
184 Ok(Self {
185 root,
186 panel_rect,
187 root_rect,
188 content,
189 scrim,
190 dismiss,
191 edge,
192 transition: None,
193 })
194 }
195
196 pub fn animate_in(self) -> Self {
197 self.animate(SurfaceTransition::enter())
198 }
199
200 pub fn animate(mut self, transition: SurfaceTransition) -> Self {
203 self.transition = Some(transition);
204 self
205 }
206}
207
208impl LayoutItem for SurfaceScaffold {
209 fn layout_node(&self) -> NodeId {
210 self.root
211 }
212}
213
214impl Component for SurfaceScaffold {
215 fn view(&self) -> RenderNode {
216 let content = self.content.view();
217 let (matrix, opacity) = match &self.transition {
218 Some(transition) => enter_transform(EnterMotion::Slide(self.edge), transition.get()),
219 None => (IDENTITY, 1.0),
220 };
221 let panel = apply_enter(content, matrix, opacity);
222 match (self.scrim, self.root_rect.as_ref()) {
223 (Some(color), Some(rect)) => {
224 let scrim = apply_enter(
225 RenderNode::rect(rect.get(), RectStyle::filled(color, 0.0)),
226 IDENTITY,
227 opacity,
228 );
229 RenderNode::group([scrim, panel])
230 }
231 _ => panel,
232 }
233 }
234
235 fn on_event(&mut self, event: &Event) -> EventResult {
236 match event {
237 Event::WindowResized { width, height } => {
238 mark_dirty(self.root).ok();
239 compute_layout(
240 self.root,
241 AvailableSpace::Definite(*width as f32),
242 AvailableSpace::Definite(*height as f32),
243 )
244 .ok();
245 EventResult::Handled
246 }
247 Event::PointerPressed {
248 x,
249 y,
250 button: PointerButton::Primary,
251 ..
252 } => {
253 let inside = self
254 .panel_rect
255 .as_ref()
256 .map(|r| r.get().contains(*x as f32, *y as f32))
257 .unwrap_or(false);
258 match (inside, &self.dismiss) {
259 (false, Some(dismiss)) => {
260 dismiss();
261 EventResult::Handled
262 }
263 _ => self.content.on_event(event),
264 }
265 }
266 Event::KeyPressed {
271 key: Key::Named(NamedKey::Escape),
272 ..
273 } => match (self.content.on_event(event), &self.dismiss) {
274 (EventResult::Ignored, Some(dismiss)) => {
275 dismiss();
276 EventResult::Handled
277 }
278 (result, _) => result,
279 },
280 _ => self.content.on_event(event),
281 }
282 }
283
284 fn debug_name(&self) -> &'static str {
285 "SurfaceScaffold"
286 }
287}
288
289pub struct SurfaceRoot {
290 root: NodeId,
291 content: Box<dyn LayoutItem>,
292 transition: Option<SurfaceTransition>,
293}
294
295impl SurfaceRoot {
296 pub fn new(content: Box<dyn LayoutItem>) -> Result<Self, LayoutError> {
297 let root = new_container(
298 LayoutStyle::new()
299 .flex_row()
300 .width(SizeDimension::Percent(1.0))
301 .height(SizeDimension::Percent(1.0)),
302 &[content.layout_node()],
303 )?;
304 Ok(Self {
305 root,
306 content,
307 transition: None,
308 })
309 }
310
311 pub fn animate_in(self) -> Self {
312 self.animate(SurfaceTransition::enter())
313 }
314
315 pub fn animate(mut self, transition: SurfaceTransition) -> Self {
318 self.transition = Some(transition);
319 self
320 }
321}
322
323impl LayoutItem for SurfaceRoot {
324 fn layout_node(&self) -> NodeId {
325 self.root
326 }
327}
328
329impl Component for SurfaceRoot {
330 fn view(&self) -> RenderNode {
331 let content = self.content.view();
332 match &self.transition {
333 Some(transition) => {
334 let (_, opacity) = enter_transform(EnterMotion::Fade, transition.get());
335 apply_enter(content, IDENTITY, opacity)
336 }
337 None => content,
338 }
339 }
340
341 fn on_event(&mut self, event: &Event) -> EventResult {
342 if let Event::WindowResized { width, height } = event {
343 mark_dirty(self.root).ok();
344 compute_layout(
345 self.root,
346 AvailableSpace::Definite(*width as f32),
347 AvailableSpace::Definite(*height as f32),
348 )
349 .ok();
350 return EventResult::Handled;
351 }
352 self.content.on_event(event)
353 }
354
355 fn debug_name(&self) -> &'static str {
356 "SurfaceRoot"
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use renderer_core::TextStyle;
364 use std::cell::Cell;
365
366 use platform_core::PointerSource;
367 use renderer_core::RectStyle;
368
369 use crate::StyledContainer;
370 use crate::context::reset_layout_runtime;
371 use crate::layout_item::box_item;
372
373 fn panel() -> Box<dyn LayoutItem> {
374 box_item(
375 StyledContainer::new(
376 LayoutStyle::new().width(100.0).height(40.0),
377 |_r| RectStyle::default(),
378 vec![],
379 )
380 .unwrap(),
381 )
382 }
383
384 fn press(x: f64, y: f64) -> Event {
385 Event::PointerPressed {
386 x,
387 y,
388 button: PointerButton::Primary,
389 source: PointerSource::Mouse,
390 }
391 }
392
393 #[test]
394 fn dismiss_fires_on_outside_press_only() {
395 reset_layout_runtime();
396 let fired = Rc::new(Cell::new(0u32));
397 let f = fired.clone();
398 let mut scaffold = SurfaceScaffold::new(
399 Edge::Top,
400 AlignItems::CENTER,
401 (20, 20, 20, 20),
402 Some(DEFAULT_SCRIM),
403 Some(Rc::new(move || f.set(f.get() + 1))),
404 panel(),
405 )
406 .unwrap();
407 scaffold.on_event(&Event::WindowResized {
408 width: 200,
409 height: 200,
410 });
411
412 scaffold.on_event(&press(100.0, 40.0));
413 assert_eq!(fired.get(), 0, "a press inside the panel must not dismiss");
414 scaffold.on_event(&press(10.0, 190.0));
415 assert_eq!(fired.get(), 1, "a press outside the panel dismisses");
416 }
417
418 #[test]
422 fn escape_dismisses_only_what_the_panel_left_alone() {
423 reset_layout_runtime();
424 let fired = Rc::new(Cell::new(0u32));
425 let f = fired.clone();
426 let mut scaffold = SurfaceScaffold::new(
427 Edge::Top,
428 AlignItems::CENTER,
429 (20, 20, 20, 20),
430 Some(DEFAULT_SCRIM),
431 Some(Rc::new(move || f.set(f.get() + 1))),
432 panel(),
433 )
434 .unwrap();
435 scaffold.on_event(&Event::WindowResized {
436 width: 200,
437 height: 200,
438 });
439
440 let escape = Event::KeyPressed {
441 key: Key::Named(NamedKey::Escape),
442 modifiers: Default::default(),
443 };
444 assert_eq!(scaffold.on_event(&escape), EventResult::Handled);
445 assert_eq!(
446 fired.get(),
447 1,
448 "a plain panel lets Escape close the surface"
449 );
450
451 reset_layout_runtime();
452 let untouched = Rc::new(Cell::new(0u32));
453 let u = untouched.clone();
454 let field = crate::input::Input::new(
455 reactive_core::signal(String::new()),
456 LayoutStyle::new().width(100.0).height(30.0),
457 || TextStyle::new(14.0, Color::BLACK),
458 )
459 .unwrap()
460 .autofocus();
461 let mut focused = SurfaceScaffold::new(
462 Edge::Top,
463 AlignItems::CENTER,
464 (20, 20, 20, 20),
465 Some(DEFAULT_SCRIM),
466 Some(Rc::new(move || u.set(u.get() + 1))),
467 box_item(field),
468 )
469 .unwrap();
470 focused.on_event(&Event::WindowResized {
471 width: 200,
472 height: 200,
473 });
474 focused.on_event(&press(100.0, 40.0));
475 focused.on_event(&escape);
476 assert_eq!(
477 untouched.get(),
478 0,
479 "a field that claims Escape to release its own focus keeps the surface up"
480 );
481 }
482
483 #[test]
484 fn cross_axis_margin_insets_the_panel_from_the_side_edges() {
485 reset_layout_runtime();
486 let fired = Rc::new(Cell::new(0u32));
487 let f = fired.clone();
488 let mut scaffold = SurfaceScaffold::new(
490 Edge::Top,
491 AlignItems::START,
492 (30, 10, 10, 10),
493 Some(DEFAULT_SCRIM),
494 Some(Rc::new(move || f.set(f.get() + 1))),
495 panel(),
496 )
497 .unwrap();
498 scaffold.on_event(&Event::WindowResized {
499 width: 200,
500 height: 200,
501 });
502
503 scaffold.on_event(&press(50.0, 40.0));
504 assert_eq!(
505 fired.get(),
506 0,
507 "a press on the inset panel must not dismiss"
508 );
509 scaffold.on_event(&press(4.0, 40.0));
510 assert_eq!(
511 fired.get(),
512 1,
513 "a press in the left gap (before the inset) dismisses"
514 );
515 scaffold.on_event(&press(150.0, 40.0));
516 assert_eq!(fired.get(), 2, "a press in the right gap dismisses");
517 }
518
519 #[test]
520 fn no_dismiss_when_not_configured() {
521 reset_layout_runtime();
522 let fired = Rc::new(Cell::new(0u32));
523 let f = fired.clone();
524 let mut scaffold = SurfaceScaffold::new(
526 Edge::Top,
527 AlignItems::CENTER,
528 (0, 0, 0, 0),
529 Some(DEFAULT_SCRIM),
530 None,
531 panel(),
532 )
533 .unwrap();
534 let _ = &f;
535 scaffold.on_event(&Event::WindowResized {
536 width: 200,
537 height: 200,
538 });
539
540 scaffold.on_event(&press(10.0, 190.0));
541 assert_eq!(
542 fired.get(),
543 0,
544 "no dismiss must fire when dismiss_on_outside is off"
545 );
546 }
547 #[test]
548 fn enter_transform_fade_is_opacity_only() {
549 assert_eq!(enter_transform(EnterMotion::Fade, 0.0), (IDENTITY, 0.0));
550 assert_eq!(enter_transform(EnterMotion::Fade, 0.5), (IDENTITY, 0.5));
551 assert_eq!(enter_transform(EnterMotion::Fade, 1.0), (IDENTITY, 1.0));
552 }
553
554 #[test]
555 fn enter_transform_slide_offsets_from_edge_then_settles() {
556 let (m, o) = enter_transform(EnterMotion::Slide(Edge::Top), 0.0);
557 assert_eq!(o, 0.0);
558 assert_eq!(m[5], -SLIDE_DISTANCE, "top slides down from above");
559 assert_eq!(
560 enter_transform(EnterMotion::Slide(Edge::Top), 1.0),
561 (IDENTITY, 1.0)
562 );
563 assert_eq!(
564 enter_transform(EnterMotion::Slide(Edge::Bottom), 0.0).0[5],
565 SLIDE_DISTANCE
566 );
567 assert_eq!(
568 enter_transform(EnterMotion::Slide(Edge::Left), 0.0).0[4],
569 -SLIDE_DISTANCE
570 );
571 assert_eq!(
572 enter_transform(EnterMotion::Slide(Edge::Right), 0.0).0[4],
573 SLIDE_DISTANCE
574 );
575 }
576}