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, PointerButton};
10use reactive_core::RwSignal;
11use renderer_core::{Color, RectStyle, TextStyle};
12use ui_tree::{Component, EventResult, RenderNode};
13
14use crate::context::{compute_layout, mark_dirty, new_container, track_layout};
15use crate::layout_item::{LayoutItem, box_item};
16use crate::styled_container::StyledContainer;
17use crate::text::Text;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum SurfaceRole {
24 Drawer,
26 Popup,
28 Osd,
30 Float,
32 Overlay,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum KeyboardMode {
47 #[default]
49 None,
50 OnDemand,
52 Exclusive,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum SurfaceAnchor {
59 Top,
60 Bottom,
61 Left,
62 Right,
63 Center,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum SurfaceAlign {
69 Start,
70 Center,
71 End,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum SurfaceSize {
77 Fixed(u32, u32),
78 Auto,
79}
80
81#[derive(Debug, Clone)]
86pub struct SurfacePlacement {
87 pub role: SurfaceRole,
88 pub anchor: SurfaceAnchor,
89 pub align: SurfaceAlign,
90 pub size: SurfaceSize,
91 pub margin: (i32, i32, i32, i32),
95 pub scrim: bool,
97 pub dismiss_on_outside: bool,
99 pub timeout: Option<Duration>,
101 pub input_transparent: bool,
103 pub keyboard: KeyboardMode,
107 pub output: Option<String>,
109}
110
111impl SurfacePlacement {
112 pub fn new(role: SurfaceRole, anchor: SurfaceAnchor) -> Self {
113 Self {
114 role,
115 anchor,
116 align: SurfaceAlign::Center,
117 size: SurfaceSize::Auto,
118 margin: (0, 0, 0, 0),
119 scrim: false,
120 dismiss_on_outside: false,
121 timeout: None,
122 input_transparent: false,
123 keyboard: KeyboardMode::None,
124 output: None,
125 }
126 }
127
128 pub fn overlay() -> Self {
131 Self {
132 scrim: true,
133 dismiss_on_outside: true,
134 keyboard: KeyboardMode::Exclusive,
135 ..Self::new(SurfaceRole::Overlay, SurfaceAnchor::Center)
136 }
137 }
138
139 pub fn drawer(anchor: SurfaceAnchor) -> Self {
140 Self {
141 scrim: true,
142 dismiss_on_outside: true,
143 ..Self::new(SurfaceRole::Drawer, anchor)
144 }
145 }
146
147 pub fn osd() -> Self {
148 Self {
149 input_transparent: true,
150 ..Self::new(SurfaceRole::Osd, SurfaceAnchor::Top)
151 }
152 }
153
154 pub fn float() -> Self {
155 Self::new(SurfaceRole::Float, SurfaceAnchor::Center)
156 }
157
158 pub fn align(mut self, align: SurfaceAlign) -> Self {
159 self.align = align;
160 self
161 }
162
163 pub fn size(mut self, size: SurfaceSize) -> Self {
164 self.size = size;
165 self
166 }
167
168 pub fn margin(mut self, margin: (i32, i32, i32, i32)) -> Self {
169 self.margin = margin;
170 self
171 }
172
173 pub fn inset(mut self, px: i32) -> Self {
174 let (t, r, b, l) = self.margin;
175 self.margin = match self.anchor {
176 SurfaceAnchor::Top => (px, r, b, l),
177 SurfaceAnchor::Bottom => (t, r, px, l),
178 SurfaceAnchor::Left => (t, r, b, px),
179 SurfaceAnchor::Right => (t, px, b, l),
180 SurfaceAnchor::Center => (t, r, b, l),
181 };
182 self
183 }
184
185 pub fn scrim(mut self, scrim: bool) -> Self {
186 self.scrim = scrim;
187 self
188 }
189
190 pub fn dismiss_on_outside(mut self, dismiss: bool) -> Self {
191 self.dismiss_on_outside = dismiss;
192 self
193 }
194
195 pub fn timeout(mut self, timeout: Duration) -> Self {
196 self.timeout = Some(timeout);
197 self
198 }
199
200 pub fn input_transparent(mut self, transparent: bool) -> Self {
201 self.input_transparent = transparent;
202 self
203 }
204
205 pub fn keyboard(mut self, wants_keyboard: bool) -> Self {
209 self.keyboard = if wants_keyboard {
210 KeyboardMode::OnDemand
211 } else {
212 KeyboardMode::None
213 };
214 self
215 }
216
217 pub fn keyboard_mode(mut self, mode: KeyboardMode) -> Self {
219 self.keyboard = mode;
220 self
221 }
222
223 pub fn wants_keyboard(&self) -> bool {
225 self.keyboard != KeyboardMode::None
226 }
227
228 pub fn output(mut self, output: Option<String>) -> Self {
229 self.output = output;
230 self
231 }
232
233 pub fn needs_scaffold(&self) -> bool {
236 self.scrim || self.dismiss_on_outside
237 }
238}
239
240pub const DEFAULT_SCRIM: Color = Color::rgba(0.0, 0.0, 0.0, 0.35);
243
244fn cross_align(align: SurfaceAlign) -> AlignItems {
245 match align {
246 SurfaceAlign::Start => AlignItems::START,
247 SurfaceAlign::Center => AlignItems::CENTER,
248 SurfaceAlign::End => AlignItems::END,
249 }
250}
251
252const ENTER_MS: u64 = 200;
254const SLIDE_DISTANCE: f32 = 24.0;
255const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
256
257#[derive(Clone, Copy)]
258enum EnterMotion {
259 Slide(SurfaceAnchor),
260 Fade,
261}
262
263fn enter_transform(motion: EnterMotion, progress: f32) -> ([f32; 6], f32) {
266 let p = progress.clamp(0.0, 1.0);
267 let opacity = p;
268 match motion {
269 EnterMotion::Fade => (IDENTITY, opacity),
270 EnterMotion::Slide(anchor) => {
271 let d = SLIDE_DISTANCE * (1.0 - p);
272 let (dx, dy) = match anchor {
273 SurfaceAnchor::Top => (0.0, -d),
274 SurfaceAnchor::Bottom => (0.0, d),
275 SurfaceAnchor::Left => (-d, 0.0),
276 SurfaceAnchor::Right => (d, 0.0),
277 SurfaceAnchor::Center => (0.0, 0.0),
278 };
279 (Transform::translate(dx, dy).to_array(), opacity)
280 }
281 }
282}
283
284fn apply_enter(node: RenderNode, matrix: [f32; 6], opacity: f32) -> RenderNode {
285 let faded = if opacity < 1.0 {
286 RenderNode::layer(opacity, 0.0, [node])
287 } else {
288 node
289 };
290 if matrix == IDENTITY {
291 faded
292 } else {
293 RenderNode::transform_with(matrix, [faded])
294 }
295}
296
297fn enter_animation() -> Animated<f32> {
298 let anim = Animated::new(0.0, tween(Duration::from_millis(ENTER_MS), Easing::EaseOut));
299 anim.retarget(1.0);
300 anim
301}
302
303pub struct SurfaceScaffold {
311 root: NodeId,
312 panel_rect: Option<RwSignal<Rect>>,
313 root_rect: Option<RwSignal<Rect>>,
314 content: Box<dyn LayoutItem>,
315 scrim: Option<Color>,
316 dismiss: Option<Rc<dyn Fn()>>,
317 anchor: SurfaceAnchor,
318 enter: Option<Animated<f32>>,
319}
320
321impl SurfaceScaffold {
322 pub fn new(
323 placement: &SurfacePlacement,
324 content: Box<dyn LayoutItem>,
325 dismiss: Option<Rc<dyn Fn()>>,
326 ) -> Result<Self, LayoutError> {
327 let panel_node = content.layout_node();
328 let (mt, mr, mb, ml) = placement.margin;
329 let cross = cross_align(placement.align);
330 let base = LayoutStyle::new()
333 .width(SizeDimension::Percent(1.0))
334 .height(SizeDimension::Percent(1.0))
335 .padding_top(mt as f32)
336 .padding_right(mr as f32)
337 .padding_bottom(mb as f32)
338 .padding_left(ml as f32);
339 let style = match placement.anchor {
340 SurfaceAnchor::Top => base
341 .flex_column()
342 .justify_content(JustifyContent::START)
343 .align_items(cross),
344 SurfaceAnchor::Bottom => base
345 .flex_column()
346 .justify_content(JustifyContent::END)
347 .align_items(cross),
348 SurfaceAnchor::Left => base
349 .flex_row()
350 .justify_content(JustifyContent::START)
351 .align_items(cross),
352 SurfaceAnchor::Right => base
353 .flex_row()
354 .justify_content(JustifyContent::END)
355 .align_items(cross),
356 SurfaceAnchor::Center => base
357 .flex_column()
358 .justify_content(JustifyContent::CENTER)
359 .align_items(AlignItems::CENTER),
360 };
361 let root = new_container(style, &[panel_node])?;
362 let panel_rect = track_layout(panel_node);
363 let root_rect = track_layout(root);
364 let dismiss = if placement.dismiss_on_outside {
365 dismiss
366 } else {
367 None
368 };
369 Ok(Self {
370 root,
371 panel_rect,
372 root_rect,
373 content,
374 scrim: placement.scrim.then_some(DEFAULT_SCRIM),
375 dismiss,
376 anchor: placement.anchor,
377 enter: None,
378 })
379 }
380
381 pub fn animate_in(mut self) -> Self {
382 self.enter = Some(enter_animation());
383 self
384 }
385}
386
387impl LayoutItem for SurfaceScaffold {
388 fn layout_node(&self) -> NodeId {
389 self.root
390 }
391}
392
393impl Component for SurfaceScaffold {
394 fn view(&self) -> RenderNode {
395 let content = self.content.view();
396 let (matrix, opacity) = match &self.enter {
397 Some(anim) => enter_transform(EnterMotion::Slide(self.anchor), anim.get()),
398 None => (IDENTITY, 1.0),
399 };
400 let panel = apply_enter(content, matrix, opacity);
401 match (self.scrim, self.root_rect.as_ref()) {
402 (Some(color), Some(rect)) => {
403 let scrim = apply_enter(
404 RenderNode::rect(rect.get(), RectStyle::filled(color, 0.0)),
405 IDENTITY,
406 opacity,
407 );
408 RenderNode::group([scrim, panel])
409 }
410 _ => panel,
411 }
412 }
413
414 fn on_event(&mut self, event: &Event) -> EventResult {
415 match event {
416 Event::WindowResized { width, height } => {
417 mark_dirty(self.root).ok();
418 compute_layout(
419 self.root,
420 AvailableSpace::Definite(*width as f32),
421 AvailableSpace::Definite(*height as f32),
422 )
423 .ok();
424 EventResult::Handled
425 }
426 Event::PointerPressed {
427 x,
428 y,
429 button: PointerButton::Primary,
430 ..
431 } => {
432 let inside = self
433 .panel_rect
434 .as_ref()
435 .map(|r| r.get().contains(*x as f32, *y as f32))
436 .unwrap_or(false);
437 match (inside, &self.dismiss) {
438 (false, Some(dismiss)) => {
439 dismiss();
440 EventResult::Handled
441 }
442 _ => self.content.on_event(event),
443 }
444 }
445 _ => self.content.on_event(event),
446 }
447 }
448
449 fn debug_name(&self) -> &'static str {
450 "SurfaceScaffold"
451 }
452}
453
454pub struct SurfaceRoot {
455 root: NodeId,
456 content: Box<dyn LayoutItem>,
457 enter: Option<Animated<f32>>,
458}
459
460impl SurfaceRoot {
461 pub fn new(content: Box<dyn LayoutItem>) -> Result<Self, LayoutError> {
462 let root = new_container(
463 LayoutStyle::new()
464 .flex_row()
465 .width(SizeDimension::Percent(1.0))
466 .height(SizeDimension::Percent(1.0)),
467 &[content.layout_node()],
468 )?;
469 Ok(Self {
470 root,
471 content,
472 enter: None,
473 })
474 }
475
476 pub fn animate_in(mut self) -> Self {
477 self.enter = Some(enter_animation());
478 self
479 }
480}
481
482impl LayoutItem for SurfaceRoot {
483 fn layout_node(&self) -> NodeId {
484 self.root
485 }
486}
487
488impl Component for SurfaceRoot {
489 fn view(&self) -> RenderNode {
490 let content = self.content.view();
491 match &self.enter {
492 Some(anim) => {
493 let (_, opacity) = enter_transform(EnterMotion::Fade, anim.get());
494 apply_enter(content, IDENTITY, opacity)
495 }
496 None => content,
497 }
498 }
499
500 fn on_event(&mut self, event: &Event) -> EventResult {
501 if let Event::WindowResized { width, height } = event {
502 mark_dirty(self.root).ok();
503 compute_layout(
504 self.root,
505 AvailableSpace::Definite(*width as f32),
506 AvailableSpace::Definite(*height as f32),
507 )
508 .ok();
509 return EventResult::Handled;
510 }
511 self.content.on_event(event)
512 }
513
514 fn debug_name(&self) -> &'static str {
515 "SurfaceRoot"
516 }
517}
518
519#[derive(Debug, Clone, Copy)]
520pub struct SurfaceFrameStyle {
521 pub background: Color,
522 pub title_bar: Color,
523 pub title_text: Color,
524 pub close: Color,
525 pub radius: f32,
526 pub font_size: f32,
527}
528
529pub fn surface_frame(
530 title: impl Into<String>,
531 style: SurfaceFrameStyle,
532 close: std::rc::Rc<dyn Fn()>,
533 body: Box<dyn LayoutItem>,
534) -> Result<Box<dyn LayoutItem>, LayoutError> {
535 let title = title.into();
536 let title_color = style.title_text;
537 let font_size = style.font_size;
538 let title_label = box_item(Text::auto(
539 move || title.clone(),
540 LayoutStyle::new(),
541 move || TextStyle::new(font_size, title_color),
542 )?);
543
544 let close_color = style.close;
545 let close_label = box_item(Text::auto(
546 || "\u{2715}".to_string(),
547 LayoutStyle::new(),
548 move || TextStyle::new(font_size, close_color),
549 )?);
550 let close_button = box_item(
551 StyledContainer::new(
552 LayoutStyle::new()
553 .align_items(AlignItems::CENTER)
554 .justify_content(JustifyContent::CENTER)
555 .padding_horizontal(8.0)
556 .padding_vertical(2.0),
557 |_| RectStyle::default(),
558 vec![close_label],
559 )?
560 .on_press(move || close()),
561 );
562
563 let title_bar_color = style.title_bar;
564 let title_bar = box_item(StyledContainer::new(
565 LayoutStyle::new()
566 .flex_row()
567 .align_items(AlignItems::CENTER)
568 .justify_content(JustifyContent::SPACE_BETWEEN)
569 .width(SizeDimension::Percent(1.0))
570 .padding_horizontal(12.0)
571 .padding_vertical(8.0),
572 move |_| RectStyle::filled(title_bar_color, 0.0),
573 vec![title_label, close_button],
574 )?);
575
576 let body_area = box_item(StyledContainer::new(
577 LayoutStyle::new()
578 .flex_column()
579 .flex_grow(1.0)
580 .width(SizeDimension::Percent(1.0))
581 .align_items(AlignItems::CENTER)
582 .justify_content(JustifyContent::CENTER)
583 .padding_all(12.0),
584 |_| RectStyle::default(),
585 vec![body],
586 )?);
587
588 let background = style.background;
589 let radius = style.radius;
590 let card = StyledContainer::new(
591 LayoutStyle::new()
592 .flex_column()
593 .width(SizeDimension::Percent(1.0))
594 .height(SizeDimension::Percent(1.0)),
595 move |_| RectStyle::filled(background, radius),
596 vec![title_bar, body_area],
597 )?;
598 Ok(box_item(card))
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use std::cell::Cell;
605
606 use platform_core::PointerSource;
607 use renderer_core::RectStyle;
608
609 use crate::StyledContainer;
610 use crate::context::reset_layout_runtime;
611 use crate::layout_item::box_item;
612
613 fn panel() -> Box<dyn LayoutItem> {
614 box_item(
615 StyledContainer::new(
616 LayoutStyle::new().width(100.0).height(40.0),
617 |_r| RectStyle::default(),
618 vec![],
619 )
620 .unwrap(),
621 )
622 }
623
624 fn press(x: f64, y: f64) -> Event {
625 Event::PointerPressed {
626 x,
627 y,
628 button: PointerButton::Primary,
629 source: PointerSource::Mouse,
630 }
631 }
632
633 #[test]
634 fn dismiss_fires_on_outside_press_only() {
635 reset_layout_runtime();
636 let fired = Rc::new(Cell::new(0u32));
637 let f = fired.clone();
638 let placement = SurfacePlacement::drawer(SurfaceAnchor::Top).inset(20);
639 let mut scaffold = SurfaceScaffold::new(
640 &placement,
641 panel(),
642 Some(Rc::new(move || f.set(f.get() + 1))),
643 )
644 .unwrap();
645 scaffold.on_event(&Event::WindowResized {
646 width: 200,
647 height: 200,
648 });
649
650 scaffold.on_event(&press(100.0, 40.0));
651 assert_eq!(fired.get(), 0, "a press inside the panel must not dismiss");
652 scaffold.on_event(&press(10.0, 190.0));
653 assert_eq!(fired.get(), 1, "a press outside the panel dismisses");
654 }
655
656 #[test]
657 fn cross_axis_margin_insets_the_panel_from_the_side_edges() {
658 reset_layout_runtime();
659 let fired = Rc::new(Cell::new(0u32));
660 let f = fired.clone();
661 let placement = SurfacePlacement::drawer(SurfaceAnchor::Top)
663 .align(SurfaceAlign::Start)
664 .margin((30, 10, 10, 10));
665 let mut scaffold = SurfaceScaffold::new(
666 &placement,
667 panel(),
668 Some(Rc::new(move || f.set(f.get() + 1))),
669 )
670 .unwrap();
671 scaffold.on_event(&Event::WindowResized {
672 width: 200,
673 height: 200,
674 });
675
676 scaffold.on_event(&press(50.0, 40.0));
677 assert_eq!(
678 fired.get(),
679 0,
680 "a press on the inset panel must not dismiss"
681 );
682 scaffold.on_event(&press(4.0, 40.0));
683 assert_eq!(
684 fired.get(),
685 1,
686 "a press in the left gap (before the inset) dismisses"
687 );
688 scaffold.on_event(&press(150.0, 40.0));
689 assert_eq!(fired.get(), 2, "a press in the right gap dismisses");
690 }
691
692 #[test]
693 fn no_dismiss_when_not_configured() {
694 reset_layout_runtime();
695 let fired = Rc::new(Cell::new(0u32));
696 let f = fired.clone();
697 let placement = SurfacePlacement::new(SurfaceRole::Drawer, SurfaceAnchor::Top).scrim(true);
698 let mut scaffold = SurfaceScaffold::new(
699 &placement,
700 panel(),
701 Some(Rc::new(move || f.set(f.get() + 1))),
702 )
703 .unwrap();
704 scaffold.on_event(&Event::WindowResized {
705 width: 200,
706 height: 200,
707 });
708
709 scaffold.on_event(&press(10.0, 190.0));
710 assert_eq!(
711 fired.get(),
712 0,
713 "no dismiss must fire when dismiss_on_outside is off"
714 );
715 }
716
717 #[test]
718 fn enter_transform_fade_is_opacity_only() {
719 assert_eq!(enter_transform(EnterMotion::Fade, 0.0), (IDENTITY, 0.0));
720 assert_eq!(enter_transform(EnterMotion::Fade, 0.5), (IDENTITY, 0.5));
721 assert_eq!(enter_transform(EnterMotion::Fade, 1.0), (IDENTITY, 1.0));
722 }
723
724 #[test]
725 fn enter_transform_slide_offsets_from_edge_then_settles() {
726 let (m, o) = enter_transform(EnterMotion::Slide(SurfaceAnchor::Top), 0.0);
727 assert_eq!(o, 0.0);
728 assert_eq!(m[5], -SLIDE_DISTANCE, "top slides down from above");
729 assert_eq!(
730 enter_transform(EnterMotion::Slide(SurfaceAnchor::Top), 1.0),
731 (IDENTITY, 1.0)
732 );
733 assert_eq!(
734 enter_transform(EnterMotion::Slide(SurfaceAnchor::Bottom), 0.0).0[5],
735 SLIDE_DISTANCE
736 );
737 assert_eq!(
738 enter_transform(EnterMotion::Slide(SurfaceAnchor::Left), 0.0).0[4],
739 -SLIDE_DISTANCE
740 );
741 assert_eq!(
742 enter_transform(EnterMotion::Slide(SurfaceAnchor::Right), 0.0).0[4],
743 SLIDE_DISTANCE
744 );
745 }
746}