1use std::cell::{Cell, RefCell};
2use std::rc::Rc;
3use std::time::Duration;
4
5use geometry_core::{Rect, Transform};
6use layout_core::{
7 AlignItems, AvailableSpace, JustifyContent, LayoutError, LayoutStyle, NodeId, SizeDimension,
8};
9use motion_core::{Animated, Easing, tween};
10use platform_core::{Event, Key, NamedKey, PointerButton};
11use reactive_core::RwSignal;
12use renderer_core::{Color, RectStyle, TextStyle};
13use ui_tree::{Component, EventResult, RenderNode};
14
15use crate::context::{compute_layout, mark_dirty, new_container, track_layout};
16use crate::layout_item::{LayoutItem, box_item};
17use crate::styled_container::StyledContainer;
18use crate::text::Text;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum SurfaceRole {
25 Drawer,
27 Popup,
29 Osd,
31 Float,
33 Overlay,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum KeyboardMode {
48 #[default]
50 None,
51 OnDemand,
53 Exclusive,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum SurfaceAnchor {
60 Top,
61 Bottom,
62 Left,
63 Right,
64 Center,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum SurfaceAlign {
70 Start,
71 Center,
72 End,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq)]
77pub enum SurfaceSize {
78 Fixed(u32, u32),
79 Auto,
80}
81
82#[derive(Debug, Clone)]
87pub struct SurfacePlacement {
88 pub role: SurfaceRole,
89 pub anchor: SurfaceAnchor,
90 pub align: SurfaceAlign,
91 pub size: SurfaceSize,
92 pub margin: (i32, i32, i32, i32),
96 pub scrim: bool,
98 pub dismiss_on_outside: bool,
100 pub timeout: Option<Duration>,
102 pub input_transparent: bool,
104 pub keyboard: KeyboardMode,
108 pub output: Option<String>,
110}
111
112impl SurfacePlacement {
113 pub fn new(role: SurfaceRole, anchor: SurfaceAnchor) -> Self {
114 Self {
115 role,
116 anchor,
117 align: SurfaceAlign::Center,
118 size: SurfaceSize::Auto,
119 margin: (0, 0, 0, 0),
120 scrim: false,
121 dismiss_on_outside: false,
122 timeout: None,
123 input_transparent: false,
124 keyboard: KeyboardMode::None,
125 output: None,
126 }
127 }
128
129 pub fn overlay() -> Self {
132 Self {
133 scrim: true,
134 dismiss_on_outside: true,
135 keyboard: KeyboardMode::Exclusive,
136 ..Self::new(SurfaceRole::Overlay, SurfaceAnchor::Center)
137 }
138 }
139
140 pub fn drawer(anchor: SurfaceAnchor) -> Self {
141 Self {
142 scrim: true,
143 dismiss_on_outside: true,
144 ..Self::new(SurfaceRole::Drawer, anchor)
145 }
146 }
147
148 pub fn osd() -> Self {
149 Self {
150 input_transparent: true,
151 ..Self::new(SurfaceRole::Osd, SurfaceAnchor::Top)
152 }
153 }
154
155 pub fn float() -> Self {
156 Self::new(SurfaceRole::Float, SurfaceAnchor::Center)
157 }
158
159 pub fn align(mut self, align: SurfaceAlign) -> Self {
160 self.align = align;
161 self
162 }
163
164 pub fn size(mut self, size: SurfaceSize) -> Self {
165 self.size = size;
166 self
167 }
168
169 pub fn margin(mut self, margin: (i32, i32, i32, i32)) -> Self {
170 self.margin = margin;
171 self
172 }
173
174 pub fn inset(mut self, px: i32) -> Self {
175 let (t, r, b, l) = self.margin;
176 self.margin = match self.anchor {
177 SurfaceAnchor::Top => (px, r, b, l),
178 SurfaceAnchor::Bottom => (t, r, px, l),
179 SurfaceAnchor::Left => (t, r, b, px),
180 SurfaceAnchor::Right => (t, px, b, l),
181 SurfaceAnchor::Center => (t, r, b, l),
182 };
183 self
184 }
185
186 pub fn scrim(mut self, scrim: bool) -> Self {
187 self.scrim = scrim;
188 self
189 }
190
191 pub fn dismiss_on_outside(mut self, dismiss: bool) -> Self {
192 self.dismiss_on_outside = dismiss;
193 self
194 }
195
196 pub fn timeout(mut self, timeout: Duration) -> Self {
197 self.timeout = Some(timeout);
198 self
199 }
200
201 pub fn input_transparent(mut self, transparent: bool) -> Self {
202 self.input_transparent = transparent;
203 self
204 }
205
206 pub fn keyboard(mut self, wants_keyboard: bool) -> Self {
210 self.keyboard = if wants_keyboard {
211 KeyboardMode::OnDemand
212 } else {
213 KeyboardMode::None
214 };
215 self
216 }
217
218 pub fn keyboard_mode(mut self, mode: KeyboardMode) -> Self {
220 self.keyboard = mode;
221 self
222 }
223
224 pub fn wants_keyboard(&self) -> bool {
226 self.keyboard != KeyboardMode::None
227 }
228
229 pub fn output(mut self, output: Option<String>) -> Self {
230 self.output = output;
231 self
232 }
233
234 pub fn needs_scaffold(&self) -> bool {
237 self.scrim || self.dismiss_on_outside
238 }
239}
240
241pub const DEFAULT_SCRIM: Color = Color::rgba(0.0, 0.0, 0.0, 0.35);
244
245fn cross_align(align: SurfaceAlign) -> AlignItems {
246 match align {
247 SurfaceAlign::Start => AlignItems::START,
248 SurfaceAlign::Center => AlignItems::CENTER,
249 SurfaceAlign::End => AlignItems::END,
250 }
251}
252
253const ENTER_MS: u64 = 200;
255const SLIDE_DISTANCE: f32 = 24.0;
256const IDENTITY: [f32; 6] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
257
258#[derive(Clone, Copy)]
259enum EnterMotion {
260 Slide(SurfaceAnchor),
261 Fade,
262}
263
264fn enter_transform(motion: EnterMotion, progress: f32) -> ([f32; 6], f32) {
267 let p = progress.clamp(0.0, 1.0);
268 let opacity = p;
269 match motion {
270 EnterMotion::Fade => (IDENTITY, opacity),
271 EnterMotion::Slide(anchor) => {
272 let d = SLIDE_DISTANCE * (1.0 - p);
273 let (dx, dy) = match anchor {
274 SurfaceAnchor::Top => (0.0, -d),
275 SurfaceAnchor::Bottom => (0.0, d),
276 SurfaceAnchor::Left => (-d, 0.0),
277 SurfaceAnchor::Right => (d, 0.0),
278 SurfaceAnchor::Center => (0.0, 0.0),
279 };
280 (Transform::translate(dx, dy).to_array(), opacity)
281 }
282 }
283}
284
285fn apply_enter(node: RenderNode, matrix: [f32; 6], opacity: f32) -> RenderNode {
286 let faded = if opacity < 1.0 {
287 RenderNode::layer(opacity, 0.0, [node])
288 } else {
289 node
290 };
291 if matrix == IDENTITY {
292 faded
293 } else {
294 RenderNode::transform_with(matrix, [faded])
295 }
296}
297
298#[derive(Clone)]
305pub struct SurfaceTransition {
306 progress: Animated<f32>,
307 duration: Duration,
308}
309
310impl SurfaceTransition {
311 pub fn enter() -> Self {
315 let duration = Duration::from_millis(ENTER_MS);
316 let progress = Animated::new(0.0, tween(duration, Easing::EaseOut));
317 progress.retarget(1.0);
318 Self { progress, duration }
319 }
320
321 pub fn leave(&self) {
324 self.progress.retarget(0.0);
325 }
326
327 pub fn duration(&self) -> Duration {
329 self.duration
330 }
331
332 fn get(&self) -> f32 {
333 self.progress.get()
334 }
335}
336
337pub struct SurfaceScaffold {
345 root: NodeId,
346 panel_rect: Option<RwSignal<Rect>>,
347 root_rect: Option<RwSignal<Rect>>,
348 content: Box<dyn LayoutItem>,
349 scrim: Option<Color>,
350 dismiss: Option<Rc<dyn Fn()>>,
351 anchor: SurfaceAnchor,
352 transition: Option<SurfaceTransition>,
353}
354
355impl SurfaceScaffold {
356 pub fn new(
357 placement: &SurfacePlacement,
358 content: Box<dyn LayoutItem>,
359 dismiss: Option<Rc<dyn Fn()>>,
360 ) -> Result<Self, LayoutError> {
361 let panel_node = content.layout_node();
362 let (mt, mr, mb, ml) = placement.margin;
363 let cross = cross_align(placement.align);
364 let base = LayoutStyle::new()
367 .width(SizeDimension::Percent(1.0))
368 .height(SizeDimension::Percent(1.0))
369 .padding_top(mt as f32)
370 .padding_right(mr as f32)
371 .padding_bottom(mb as f32)
372 .padding_left(ml as f32);
373 let style = match placement.anchor {
374 SurfaceAnchor::Top => base
375 .flex_column()
376 .justify_content(JustifyContent::START)
377 .align_items(cross),
378 SurfaceAnchor::Bottom => base
379 .flex_column()
380 .justify_content(JustifyContent::END)
381 .align_items(cross),
382 SurfaceAnchor::Left => base
383 .flex_row()
384 .justify_content(JustifyContent::START)
385 .align_items(cross),
386 SurfaceAnchor::Right => base
387 .flex_row()
388 .justify_content(JustifyContent::END)
389 .align_items(cross),
390 SurfaceAnchor::Center => base
391 .flex_column()
392 .justify_content(JustifyContent::CENTER)
393 .align_items(AlignItems::CENTER),
394 };
395 let root = new_container(style, &[panel_node])?;
396 let panel_rect = track_layout(panel_node);
397 let root_rect = track_layout(root);
398 let dismiss = if placement.dismiss_on_outside {
399 dismiss
400 } else {
401 None
402 };
403 Ok(Self {
404 root,
405 panel_rect,
406 root_rect,
407 content,
408 scrim: placement.scrim.then_some(DEFAULT_SCRIM),
409 dismiss,
410 anchor: placement.anchor,
411 transition: None,
412 })
413 }
414
415 pub fn animate_in(self) -> Self {
416 self.animate(SurfaceTransition::enter())
417 }
418
419 pub fn animate(mut self, transition: SurfaceTransition) -> Self {
422 self.transition = Some(transition);
423 self
424 }
425}
426
427impl LayoutItem for SurfaceScaffold {
428 fn layout_node(&self) -> NodeId {
429 self.root
430 }
431}
432
433impl Component for SurfaceScaffold {
434 fn view(&self) -> RenderNode {
435 let content = self.content.view();
436 let (matrix, opacity) = match &self.transition {
437 Some(transition) => enter_transform(EnterMotion::Slide(self.anchor), transition.get()),
438 None => (IDENTITY, 1.0),
439 };
440 let panel = apply_enter(content, matrix, opacity);
441 match (self.scrim, self.root_rect.as_ref()) {
442 (Some(color), Some(rect)) => {
443 let scrim = apply_enter(
444 RenderNode::rect(rect.get(), RectStyle::filled(color, 0.0)),
445 IDENTITY,
446 opacity,
447 );
448 RenderNode::group([scrim, panel])
449 }
450 _ => panel,
451 }
452 }
453
454 fn on_event(&mut self, event: &Event) -> EventResult {
455 match event {
456 Event::WindowResized { width, height } => {
457 mark_dirty(self.root).ok();
458 compute_layout(
459 self.root,
460 AvailableSpace::Definite(*width as f32),
461 AvailableSpace::Definite(*height as f32),
462 )
463 .ok();
464 EventResult::Handled
465 }
466 Event::PointerPressed {
467 x,
468 y,
469 button: PointerButton::Primary,
470 ..
471 } => {
472 let inside = self
473 .panel_rect
474 .as_ref()
475 .map(|r| r.get().contains(*x as f32, *y as f32))
476 .unwrap_or(false);
477 match (inside, &self.dismiss) {
478 (false, Some(dismiss)) => {
479 dismiss();
480 EventResult::Handled
481 }
482 _ => self.content.on_event(event),
483 }
484 }
485 Event::KeyPressed {
490 key: Key::Named(NamedKey::Escape),
491 ..
492 } => match (self.content.on_event(event), &self.dismiss) {
493 (EventResult::Ignored, Some(dismiss)) => {
494 dismiss();
495 EventResult::Handled
496 }
497 (result, _) => result,
498 },
499 _ => self.content.on_event(event),
500 }
501 }
502
503 fn debug_name(&self) -> &'static str {
504 "SurfaceScaffold"
505 }
506}
507
508pub struct SurfaceRoot {
509 root: NodeId,
510 content: Box<dyn LayoutItem>,
511 transition: Option<SurfaceTransition>,
512}
513
514impl SurfaceRoot {
515 pub fn new(content: Box<dyn LayoutItem>) -> Result<Self, LayoutError> {
516 let root = new_container(
517 LayoutStyle::new()
518 .flex_row()
519 .width(SizeDimension::Percent(1.0))
520 .height(SizeDimension::Percent(1.0)),
521 &[content.layout_node()],
522 )?;
523 Ok(Self {
524 root,
525 content,
526 transition: None,
527 })
528 }
529
530 pub fn animate_in(self) -> Self {
531 self.animate(SurfaceTransition::enter())
532 }
533
534 pub fn animate(mut self, transition: SurfaceTransition) -> Self {
537 self.transition = Some(transition);
538 self
539 }
540}
541
542impl LayoutItem for SurfaceRoot {
543 fn layout_node(&self) -> NodeId {
544 self.root
545 }
546}
547
548impl Component for SurfaceRoot {
549 fn view(&self) -> RenderNode {
550 let content = self.content.view();
551 match &self.transition {
552 Some(transition) => {
553 let (_, opacity) = enter_transform(EnterMotion::Fade, transition.get());
554 apply_enter(content, IDENTITY, opacity)
555 }
556 None => content,
557 }
558 }
559
560 fn on_event(&mut self, event: &Event) -> EventResult {
561 if let Event::WindowResized { width, height } = event {
562 mark_dirty(self.root).ok();
563 compute_layout(
564 self.root,
565 AvailableSpace::Definite(*width as f32),
566 AvailableSpace::Definite(*height as f32),
567 )
568 .ok();
569 return EventResult::Handled;
570 }
571 self.content.on_event(event)
572 }
573
574 fn debug_name(&self) -> &'static str {
575 "SurfaceRoot"
576 }
577}
578
579#[derive(Debug, Clone, Copy)]
580pub struct SurfaceFrameStyle {
581 pub background: Color,
582 pub title_bar: Color,
583 pub title_text: Color,
584 pub close: Color,
585 pub radius: f32,
586 pub font_size: f32,
587}
588
589pub const MIN_FRAME_SIZE: (f32, f32) = (180.0, 120.0);
592
593const GRIP_SIZE: f32 = 14.0;
596
597type DeferredRect = Rc<RefCell<Option<RwSignal<Rect>>>>;
601
602fn resize_grip(
610 color: Color,
611 card_rect: DeferredRect,
612 resize: Rc<dyn Fn(f32, f32)>,
613) -> Result<Box<dyn LayoutItem>, LayoutError> {
614 let grip = StyledContainer::new(
615 LayoutStyle::new().width(GRIP_SIZE).height(GRIP_SIZE),
616 move |_| RectStyle::filled(color, 2.0),
617 vec![],
618 )?;
619 let grip_rect = track_layout(grip.layout_node());
620 let grab: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
621 let release = Rc::clone(&grab);
622 Ok(box_item(
623 grip.on_drag(move |local_x, local_y| {
624 let (Some(grip_rect), Some(card_rect)) = (&grip_rect, card_rect.borrow().clone())
625 else {
626 return;
627 };
628 let (grip, card) = (grip_rect.get(), card_rect.get());
629 let (x, y) = (grip.x + local_x, grip.y + local_y);
630 let (offset_x, offset_y) = match grab.get() {
631 Some(offset) => offset,
632 None => {
633 let offset = (x - (card.x + card.width), y - (card.y + card.height));
634 grab.set(Some(offset));
635 offset
636 }
637 };
638 resize(
639 (x - offset_x - card.x).max(MIN_FRAME_SIZE.0),
640 (y - offset_y - card.y).max(MIN_FRAME_SIZE.1),
641 );
642 })
643 .on_drag_end(move |_, _| release.set(None)),
644 ))
645}
646
647pub fn surface_frame(
653 title: impl Into<String>,
654 style: SurfaceFrameStyle,
655 close: std::rc::Rc<dyn Fn()>,
656 body: Box<dyn LayoutItem>,
657 resize: Option<Rc<dyn Fn(f32, f32)>>,
658) -> Result<Box<dyn LayoutItem>, LayoutError> {
659 let title = title.into();
660 let title_color = style.title_text;
661 let font_size = style.font_size;
662 let title_label = box_item(Text::auto(
663 move || title.clone(),
664 LayoutStyle::new(),
665 move || TextStyle::new(font_size, title_color),
666 )?);
667
668 let close_color = style.close;
669 let close_label = box_item(Text::auto(
670 || "\u{2715}".to_string(),
671 LayoutStyle::new(),
672 move || TextStyle::new(font_size, close_color),
673 )?);
674 let close_button = box_item(
675 StyledContainer::new(
676 LayoutStyle::new()
677 .align_items(AlignItems::CENTER)
678 .justify_content(JustifyContent::CENTER)
679 .padding_horizontal(8.0)
680 .padding_vertical(2.0),
681 |_| RectStyle::default(),
682 vec![close_label],
683 )?
684 .on_press(move || close()),
685 );
686
687 let title_bar_color = style.title_bar;
688 let title_bar = box_item(StyledContainer::new(
689 LayoutStyle::new()
690 .flex_row()
691 .align_items(AlignItems::CENTER)
692 .justify_content(JustifyContent::SPACE_BETWEEN)
693 .width(SizeDimension::Percent(1.0))
694 .padding_horizontal(12.0)
695 .padding_vertical(8.0),
696 move |_| RectStyle::filled(title_bar_color, 0.0),
697 vec![title_label, close_button],
698 )?);
699
700 let body_area = box_item(StyledContainer::new(
702 LayoutStyle::new()
703 .flex_column()
704 .flex_grow(1.0)
705 .min_height(0.0)
706 .width(SizeDimension::Percent(1.0))
707 .align_items(AlignItems::CENTER)
708 .justify_content(JustifyContent::CENTER)
709 .padding_all(12.0),
710 |_| RectStyle::default(),
711 vec![body],
712 )?);
713
714 let card_rect: DeferredRect = Rc::new(RefCell::new(None));
715 let mut children = vec![title_bar, body_area];
716 if let Some(resize) = resize {
717 children.push(box_item(StyledContainer::new(
718 LayoutStyle::new()
719 .flex_row()
720 .width(SizeDimension::Percent(1.0))
721 .flex_shrink(0.0)
722 .justify_content(JustifyContent::END)
723 .padding_horizontal(4.0)
724 .padding_bottom(4.0),
725 |_| RectStyle::default(),
726 vec![resize_grip(style.close, Rc::clone(&card_rect), resize)?],
727 )?));
728 }
729
730 let background = style.background;
731 let radius = style.radius;
732 let card = StyledContainer::new(
733 LayoutStyle::new()
734 .flex_column()
735 .width(SizeDimension::Percent(1.0))
736 .height(SizeDimension::Percent(1.0)),
737 move |_| RectStyle::filled(background, radius),
738 children,
739 )?;
740 *card_rect.borrow_mut() = track_layout(card.layout_node());
741 Ok(box_item(card))
742}
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747 use std::cell::Cell;
748
749 use platform_core::PointerSource;
750 use renderer_core::RectStyle;
751
752 use crate::StyledContainer;
753 use crate::context::reset_layout_runtime;
754 use crate::layout_item::box_item;
755
756 fn panel() -> Box<dyn LayoutItem> {
757 box_item(
758 StyledContainer::new(
759 LayoutStyle::new().width(100.0).height(40.0),
760 |_r| RectStyle::default(),
761 vec![],
762 )
763 .unwrap(),
764 )
765 }
766
767 fn press(x: f64, y: f64) -> Event {
768 Event::PointerPressed {
769 x,
770 y,
771 button: PointerButton::Primary,
772 source: PointerSource::Mouse,
773 }
774 }
775
776 #[test]
777 fn dismiss_fires_on_outside_press_only() {
778 reset_layout_runtime();
779 let fired = Rc::new(Cell::new(0u32));
780 let f = fired.clone();
781 let placement = SurfacePlacement::drawer(SurfaceAnchor::Top).inset(20);
782 let mut scaffold = SurfaceScaffold::new(
783 &placement,
784 panel(),
785 Some(Rc::new(move || f.set(f.get() + 1))),
786 )
787 .unwrap();
788 scaffold.on_event(&Event::WindowResized {
789 width: 200,
790 height: 200,
791 });
792
793 scaffold.on_event(&press(100.0, 40.0));
794 assert_eq!(fired.get(), 0, "a press inside the panel must not dismiss");
795 scaffold.on_event(&press(10.0, 190.0));
796 assert_eq!(fired.get(), 1, "a press outside the panel dismisses");
797 }
798
799 #[test]
803 fn escape_dismisses_only_what_the_panel_left_alone() {
804 reset_layout_runtime();
805 let fired = Rc::new(Cell::new(0u32));
806 let f = fired.clone();
807 let placement = SurfacePlacement::drawer(SurfaceAnchor::Top).inset(20);
808 let mut scaffold = SurfaceScaffold::new(
809 &placement,
810 panel(),
811 Some(Rc::new(move || f.set(f.get() + 1))),
812 )
813 .unwrap();
814 scaffold.on_event(&Event::WindowResized {
815 width: 200,
816 height: 200,
817 });
818
819 let escape = Event::KeyPressed {
820 key: Key::Named(NamedKey::Escape),
821 modifiers: Default::default(),
822 };
823 assert_eq!(scaffold.on_event(&escape), EventResult::Handled);
824 assert_eq!(
825 fired.get(),
826 1,
827 "a plain panel lets Escape close the surface"
828 );
829
830 reset_layout_runtime();
831 let untouched = Rc::new(Cell::new(0u32));
832 let u = untouched.clone();
833 let field = crate::input::Input::new(
834 reactive_core::signal(String::new()),
835 LayoutStyle::new().width(100.0).height(30.0),
836 || TextStyle::new(14.0, Color::BLACK),
837 )
838 .unwrap()
839 .autofocus();
840 let mut focused = SurfaceScaffold::new(
841 &placement,
842 box_item(field),
843 Some(Rc::new(move || u.set(u.get() + 1))),
844 )
845 .unwrap();
846 focused.on_event(&Event::WindowResized {
847 width: 200,
848 height: 200,
849 });
850 focused.on_event(&press(100.0, 40.0));
851 focused.on_event(&escape);
852 assert_eq!(
853 untouched.get(),
854 0,
855 "a field that claims Escape to release its own focus keeps the surface up"
856 );
857 }
858
859 #[test]
860 fn cross_axis_margin_insets_the_panel_from_the_side_edges() {
861 reset_layout_runtime();
862 let fired = Rc::new(Cell::new(0u32));
863 let f = fired.clone();
864 let placement = SurfacePlacement::drawer(SurfaceAnchor::Top)
866 .align(SurfaceAlign::Start)
867 .margin((30, 10, 10, 10));
868 let mut scaffold = SurfaceScaffold::new(
869 &placement,
870 panel(),
871 Some(Rc::new(move || f.set(f.get() + 1))),
872 )
873 .unwrap();
874 scaffold.on_event(&Event::WindowResized {
875 width: 200,
876 height: 200,
877 });
878
879 scaffold.on_event(&press(50.0, 40.0));
880 assert_eq!(
881 fired.get(),
882 0,
883 "a press on the inset panel must not dismiss"
884 );
885 scaffold.on_event(&press(4.0, 40.0));
886 assert_eq!(
887 fired.get(),
888 1,
889 "a press in the left gap (before the inset) dismisses"
890 );
891 scaffold.on_event(&press(150.0, 40.0));
892 assert_eq!(fired.get(), 2, "a press in the right gap dismisses");
893 }
894
895 #[test]
896 fn no_dismiss_when_not_configured() {
897 reset_layout_runtime();
898 let fired = Rc::new(Cell::new(0u32));
899 let f = fired.clone();
900 let placement = SurfacePlacement::new(SurfaceRole::Drawer, SurfaceAnchor::Top).scrim(true);
901 let mut scaffold = SurfaceScaffold::new(
902 &placement,
903 panel(),
904 Some(Rc::new(move || f.set(f.get() + 1))),
905 )
906 .unwrap();
907 scaffold.on_event(&Event::WindowResized {
908 width: 200,
909 height: 200,
910 });
911
912 scaffold.on_event(&press(10.0, 190.0));
913 assert_eq!(
914 fired.get(),
915 0,
916 "no dismiss must fire when dismiss_on_outside is off"
917 );
918 }
919
920 #[test]
927 fn the_grip_resizes_by_the_distance_dragged_not_to_the_pointer() {
928 use std::cell::RefCell;
929 reset_layout_runtime();
930
931 let asked: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
932 let sink = Rc::clone(&asked);
933 let style = SurfaceFrameStyle {
934 background: Color::TRANSPARENT,
935 title_bar: Color::TRANSPARENT,
936 title_text: Color::TRANSPARENT,
937 close: Color::TRANSPARENT,
938 radius: 0.0,
939 font_size: 12.0,
940 };
941 let mut frame = surface_frame(
942 "Settings",
943 style,
944 Rc::new(|| {}),
945 panel(),
946 Some(Rc::new(move |w, h| sink.borrow_mut().push((w, h)))),
947 )
948 .unwrap();
949 compute_layout(
950 frame.layout_node(),
951 AvailableSpace::Definite(400.0),
952 AvailableSpace::Definite(300.0),
953 )
954 .unwrap();
955
956 let grip = Rect {
958 x: 400.0 - 4.0 - GRIP_SIZE,
959 y: 300.0 - 4.0 - GRIP_SIZE,
960 width: GRIP_SIZE,
961 height: GRIP_SIZE,
962 };
963 let (start_x, start_y) = (grip.x + GRIP_SIZE / 2.0, grip.y + GRIP_SIZE / 2.0);
964 frame.on_event(&press(start_x as f64, start_y as f64));
965 frame.on_event(&Event::PointerMoved {
966 x: (start_x + 60.0) as f64,
967 y: (start_y + 40.0) as f64,
968 source: PointerSource::Mouse,
969 });
970
971 let asked = asked.borrow();
972 assert_eq!(
973 asked.first().copied(),
974 Some((400.0, 300.0)),
975 "grabbing the grip without moving must ask for the size the window already is"
976 );
977 assert_eq!(
978 asked.last().copied(),
979 Some((460.0, 340.0)),
980 "the window grows by what the pointer travelled, not to where the pointer is"
981 );
982 }
983
984 #[test]
991 fn the_grip_stays_inside_a_window_whose_body_wants_all_of_it() {
992 reset_layout_runtime();
993
994 const SURFACE: (f32, f32) = (920.0, 680.0);
995 let style = SurfaceFrameStyle {
996 background: Color::TRANSPARENT,
997 title_bar: Color::TRANSPARENT,
998 title_text: Color::TRANSPARENT,
999 close: Color::TRANSPARENT,
1000 radius: 0.0,
1001 font_size: 12.0,
1002 };
1003 let hungry = box_item(
1005 StyledContainer::new(
1006 LayoutStyle::new().width(600.0).height(SURFACE.1),
1007 |_r| RectStyle::default(),
1008 vec![],
1009 )
1010 .unwrap(),
1011 );
1012 let asked: Rc<std::cell::RefCell<Vec<(f32, f32)>>> =
1013 Rc::new(std::cell::RefCell::new(Vec::new()));
1014 let sink = Rc::clone(&asked);
1015 let mut frame = surface_frame(
1016 "Settings",
1017 style,
1018 Rc::new(|| {}),
1019 hungry,
1020 Some(Rc::new(move |w, h| sink.borrow_mut().push((w, h)))),
1021 )
1022 .unwrap();
1023 compute_layout(
1024 frame.layout_node(),
1025 AvailableSpace::Definite(SURFACE.0),
1026 AvailableSpace::Definite(SURFACE.1),
1027 )
1028 .unwrap();
1029
1030 let (x, y) = (
1032 SURFACE.0 - 4.0 - GRIP_SIZE / 2.0,
1033 SURFACE.1 - 4.0 - GRIP_SIZE / 2.0,
1034 );
1035 frame.on_event(&press(x as f64, y as f64));
1036 frame.on_event(&Event::PointerMoved {
1037 x: (x + 40.0) as f64,
1038 y: (y + 30.0) as f64,
1039 source: PointerSource::Mouse,
1040 });
1041
1042 let asked = asked.borrow();
1043 assert!(
1044 !asked.is_empty(),
1045 "nothing at the window's bottom-right corner answered a drag — a body that refuses to shrink \
1046 pushes the grip row off the surface, where it lays out perfectly and is never seen"
1047 );
1048 assert_eq!(
1049 asked.last().copied(),
1050 Some((SURFACE.0 + 40.0, SURFACE.1 + 30.0)),
1051 "and once it is on screen it still resizes by what the pointer travelled"
1052 );
1053 }
1054
1055 #[test]
1056 fn a_frame_without_a_resize_callback_draws_no_grip() {
1057 reset_layout_runtime();
1058 let style = SurfaceFrameStyle {
1059 background: Color::TRANSPARENT,
1060 title_bar: Color::TRANSPARENT,
1061 title_text: Color::TRANSPARENT,
1062 close: Color::TRANSPARENT,
1063 radius: 0.0,
1064 font_size: 12.0,
1065 };
1066 assert!(surface_frame("Clock", style, Rc::new(|| {}), panel(), None).is_ok());
1068 }
1069
1070 #[test]
1071 fn enter_transform_fade_is_opacity_only() {
1072 assert_eq!(enter_transform(EnterMotion::Fade, 0.0), (IDENTITY, 0.0));
1073 assert_eq!(enter_transform(EnterMotion::Fade, 0.5), (IDENTITY, 0.5));
1074 assert_eq!(enter_transform(EnterMotion::Fade, 1.0), (IDENTITY, 1.0));
1075 }
1076
1077 #[test]
1078 fn enter_transform_slide_offsets_from_edge_then_settles() {
1079 let (m, o) = enter_transform(EnterMotion::Slide(SurfaceAnchor::Top), 0.0);
1080 assert_eq!(o, 0.0);
1081 assert_eq!(m[5], -SLIDE_DISTANCE, "top slides down from above");
1082 assert_eq!(
1083 enter_transform(EnterMotion::Slide(SurfaceAnchor::Top), 1.0),
1084 (IDENTITY, 1.0)
1085 );
1086 assert_eq!(
1087 enter_transform(EnterMotion::Slide(SurfaceAnchor::Bottom), 0.0).0[5],
1088 SLIDE_DISTANCE
1089 );
1090 assert_eq!(
1091 enter_transform(EnterMotion::Slide(SurfaceAnchor::Left), 0.0).0[4],
1092 -SLIDE_DISTANCE
1093 );
1094 assert_eq!(
1095 enter_transform(EnterMotion::Slide(SurfaceAnchor::Right), 0.0).0[4],
1096 SLIDE_DISTANCE
1097 );
1098 }
1099}