Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/stack/mod_test.rs
1use std::{
2 cell::RefCell,
3 collections::{HashMap, HashSet},
4 rc::Rc,
5};
6 
7use itertools::Itertools;
8use pathfinder_geometry::rect::RectF;
9 
10use super::*;
11 
12use crate::{
13 elements::{Clipped, DispatchEventResult},
14 platform::WindowStyle,
15 TypedActionView,
16};
17use crate::{
18 elements::{ConstrainedBox, EventHandler, ParentElement, Rect, ZIndex},
19 App, AppContext, Entity, Event, Presenter, ViewContext, ViewHandle, WindowId,
20 WindowInvalidation,
21};
22 
23#[derive(Default)]
24struct View {
25 // maps view id to number of mouse downs
26 mouse_downs: HashMap<usize, u32>,
27 mouse_ups: HashMap<usize, u32>,
28 mouse_dragged: HashMap<usize, u32>,
29}
30 
31pub fn init(app: &mut AppContext) {
32 app.add_action("test_view:mouse_down", View::mouse_down);
33 app.add_action("test_view:mouse_up", View::mouse_up);
34 app.add_action("test_view:mouse_dragged", View::mouse_dragged);
35}
36 
37impl View {
38 fn mouse_down(&mut self, view_id: &usize, _ctx: &mut ViewContext<Self>) -> bool {
39 log::info!("Recording mouse_down on view_id {view_id}");
40 let entry = self.mouse_downs.entry(*view_id).or_insert(0);
41 *entry += 1;
42 true
43 }
44 
45 fn mouse_up(&mut self, view_id: &usize, _ctx: &mut ViewContext<Self>) -> bool {
46 log::info!("Recording mouse_up on view_id {view_id}");
47 let entry = self.mouse_ups.entry(*view_id).or_insert(0);
48 *entry += 1;
49 true
50 }
51 
52 fn mouse_dragged(&mut self, view_id: &usize, _ctx: &mut ViewContext<Self>) -> bool {
53 log::info!("Recording mouse_dragged on view_id {view_id}");
54 let entry = self.mouse_dragged.entry(*view_id).or_insert(0);
55 *entry += 1;
56 true
57 }
58}
59 
60impl TypedActionView for View {
61 type Action = ();
62}
63 
64impl Entity for View {
65 type Event = String;
66}
67 
68impl crate::core::View for View {
69 fn render<'a>(&self, _: &AppContext) -> Box<dyn Element> {
70 let mut s = Stack::new();
71 s.add_child(
72 EventHandler::new(
73 ConstrainedBox::new(Rect::new().finish())
74 .with_height(50.)
75 .with_width(50.)
76 .finish(),
77 )
78 .on_left_mouse_down(|evt_ctx, _ctx, _position| {
79 evt_ctx.dispatch_action("test_view:mouse_down", 0usize);
80 DispatchEventResult::StopPropagation
81 })
82 .on_left_mouse_up(|evt_ctx, _ctx, _position| {
83 evt_ctx.dispatch_action("test_view:mouse_up", 0usize);
84 DispatchEventResult::StopPropagation
85 })
86 .on_mouse_dragged(|evt_ctx, _ctx, _position| {
87 evt_ctx.dispatch_action("test_view:mouse_dragged", 0usize);
88 DispatchEventResult::StopPropagation
89 })
90 .finish(),
91 );
92 s.add_child(
93 Positioned::new(
94 EventHandler::new(
95 ConstrainedBox::new(Rect::new().finish())
96 .with_height(50.)
97 .with_width(50.)
98 .finish(),
99 )
100 .on_left_mouse_down(|evt_ctx, _ctx, _position| {
101 evt_ctx.dispatch_action("test_view:mouse_down", 1usize);
102 DispatchEventResult::StopPropagation
103 })
104 .on_left_mouse_up(|evt_ctx, _ctx, _position| {
105 evt_ctx.dispatch_action("test_view:mouse_up", 1usize);
106 DispatchEventResult::StopPropagation
107 })
108 .on_mouse_dragged(|evt_ctx, _ctx, _position| {
109 evt_ctx.dispatch_action("test_view:mouse_dragged", 1usize);
110 DispatchEventResult::StopPropagation
111 })
112 .finish(),
113 )
114 .with_offset(OffsetPositioning::offset_from_parent(
115 vec2f(25., 25.),
116 ParentOffsetBounds::Unbounded,
117 ParentAnchor::TopLeft,
118 ChildAnchor::TopLeft,
119 ))
120 .finish(),
121 );
122 s.add_child(
123 Positioned::new(
124 Clipped::sized(
125 EventHandler::new(
126 ConstrainedBox::new(Rect::new().finish())
127 .with_height(50.)
128 .with_width(50.)
129 .finish(),
130 )
131 .on_left_mouse_down(|evt_ctx, _ctx, _position| {
132 evt_ctx.dispatch_action("test_view:mouse_down", 2usize);
133 DispatchEventResult::StopPropagation
134 })
135 .on_left_mouse_up(|evt_ctx, _ctx, _position| {
136 evt_ctx.dispatch_action("test_view:mouse_up", 2usize);
137 DispatchEventResult::StopPropagation
138 })
139 .on_mouse_dragged(|evt_ctx, _ctx, _position| {
140 evt_ctx.dispatch_action("test_view:mouse_dragged", 2usize);
141 DispatchEventResult::StopPropagation
142 })
143 .finish(),
144 vec2f(25., 25.),
145 )
146 .finish(),
147 )
148 .with_offset(OffsetPositioning::offset_from_parent(
149 vec2f(100., 100.),
150 ParentOffsetBounds::Unbounded,
151 ParentAnchor::TopLeft,
152 ChildAnchor::TopLeft,
153 ))
154 .finish(),
155 );
156 s.finish()
157 }
158 
159 fn ui_name() -> &'static str {
160 "View"
161 }
162}
163 
164const FIRST_CHILD_POSITION_ID: &str = "RelativePositionedView::first_child_position_id";
165 
166/// A view for testing that renders the second child in a stack based on what's specified in
167/// `second_child_positioning`.
168#[derive(Default)]
169struct RelativePositionedView {
170 second_child_positioning: Option<OffsetPositioning>,
171 second_child_size: Option<Vector2F>,
172}
173 
174impl RelativePositionedView {
175 fn new() -> Self {
176 Self {
177 second_child_positioning: None,
178 second_child_size: None,
179 }
180 }
181 
182 fn first_child_position_id() -> &'static str {
183 FIRST_CHILD_POSITION_ID
184 }
185}
186 
187impl Entity for RelativePositionedView {
188 type Event = String;
189}
190 
191impl crate::core::View for RelativePositionedView {
192 fn render<'a>(&self, _: &AppContext) -> Box<dyn Element> {
193 let mut s = Stack::new();
194 s.add_child(
195 SavePosition::new(
196 ConstrainedBox::new(Rect::new().finish())
197 .with_height(50.)
198 .with_width(50.)
199 .finish(),
200 FIRST_CHILD_POSITION_ID,
201 )
202 .finish(),
203 );
204 
205 if let Some(second_child_positioning) = &self.second_child_positioning {
206 s.add_child(
207 Positioned::new(if let Some(second_child_size) = &self.second_child_size {
208 ConstrainedBox::new(Rect::new().finish())
209 .with_width(second_child_size.x())
210 .with_height(second_child_size.y())
211 .finish()
212 } else {
213 ConstrainedBox::new(Rect::new().finish())
214 .with_height(50.)
215 .with_width(50.)
216 .finish()
217 })
218 .with_offset(second_child_positioning.clone())
219 .finish(),
220 );
221 }
222 
223 // Force the Stack to take up the full size of the window by pulling
224 // the minimum size constraint up to the size of the window.
225 ConstrainedBox::new(s.finish())
226 .with_min_width(f32::MAX)
227 .with_min_height(f32::MAX)
228 .finish()
229 }
230 
231 fn ui_name() -> &'static str {
232 "View"
233 }
234}
235 
236impl TypedActionView for RelativePositionedView {
237 type Action = ();
238}
239 
240#[test]
241fn test_paint_sets_z_index() {
242 App::test((), |mut app| async move {
243 let app = &mut app;
244 app.update(init);
245 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| View::default());
246 
247 let mut presenter = Presenter::new(window_id);
248 
249 let mut updated = HashSet::new();
250 updated.insert(app.root_view_id(window_id).unwrap());
251 let invalidation = WindowInvalidation {
252 updated,
253 ..Default::default()
254 };
255 
256 app.update(move |ctx| {
257 presenter.invalidate(invalidation, ctx);
258 let scene = presenter.build_scene(vec2f(300., 300.), 1., None, ctx);
259 assert_eq!(scene.z_index(), ZIndex::new(0));
260 assert_eq!(scene.layer_count(), 5);
261 let presenter = Rc::new(RefCell::new(presenter));
262 
263 // Fire event on first child
264 ctx.simulate_window_event(
265 Event::LeftMouseDown {
266 position: vec2f(15., 15.),
267 modifiers: Default::default(),
268 click_count: 1,
269 is_first_mouse: false,
270 },
271 window_id,
272 presenter.clone(),
273 );
274 ctx.simulate_window_event(
275 Event::LeftMouseUp {
276 position: vec2f(15., 15.),
277 modifiers: Default::default(),
278 },
279 window_id,
280 presenter.clone(),
281 );
282 ctx.simulate_window_event(
283 Event::LeftMouseDragged {
284 position: vec2f(15., 15.),
285 modifiers: Default::default(),
286 },
287 window_id,
288 presenter.clone(),
289 );
290 
291 // Fire event on second child
292 ctx.simulate_window_event(
293 Event::LeftMouseDown {
294 position: vec2f(30., 30.),
295 modifiers: Default::default(),
296 click_count: 1,
297 is_first_mouse: false,
298 },
299 window_id,
300 presenter.clone(),
301 );
302 ctx.simulate_window_event(
303 Event::LeftMouseUp {
304 position: vec2f(30., 30.),
305 modifiers: Default::default(),
306 },
307 window_id,
308 presenter.clone(),
309 );
310 ctx.simulate_window_event(
311 Event::LeftMouseDragged {
312 position: vec2f(30., 30.),
313 modifiers: Default::default(),
314 },
315 window_id,
316 presenter.clone(),
317 );
318 
319 // Fire event on third child
320 ctx.simulate_window_event(
321 Event::LeftMouseDown {
322 position: vec2f(120., 120.),
323 modifiers: Default::default(),
324 click_count: 1,
325 is_first_mouse: false,
326 },
327 window_id,
328 presenter.clone(),
329 );
330 ctx.simulate_window_event(
331 Event::LeftMouseUp {
332 position: vec2f(120., 120.),
333 modifiers: Default::default(),
334 },
335 window_id,
336 presenter.clone(),
337 );
338 ctx.simulate_window_event(
339 Event::LeftMouseDragged {
340 position: vec2f(120., 120.),
341 modifiers: Default::default(),
342 },
343 window_id,
344 presenter.clone(),
345 );
346 
347 // Fire event on clipped part of third child
348 ctx.simulate_window_event(
349 Event::LeftMouseDown {
350 position: vec2f(140., 140.),
351 modifiers: Default::default(),
352 click_count: 1,
353 is_first_mouse: false,
354 },
355 window_id,
356 presenter.clone(),
357 );
358 ctx.simulate_window_event(
359 Event::LeftMouseUp {
360 position: vec2f(140., 140.),
361 modifiers: Default::default(),
362 },
363 window_id,
364 presenter.clone(),
365 );
366 ctx.simulate_window_event(
367 Event::LeftMouseDragged {
368 position: vec2f(140., 140.),
369 modifiers: Default::default(),
370 },
371 window_id,
372 presenter,
373 );
374 });
375 
376 view.read(app, |view, _ctx| {
377 assert_eq!(1, *view.mouse_downs.get(&0).unwrap());
378 assert_eq!(1, *view.mouse_downs.get(&1).unwrap());
379 assert_eq!(1, *view.mouse_downs.get(&2).unwrap());
380 assert_eq!(1, *view.mouse_ups.get(&0).unwrap());
381 assert_eq!(1, *view.mouse_ups.get(&1).unwrap());
382 assert_eq!(1, *view.mouse_ups.get(&2).unwrap());
383 assert_eq!(1, *view.mouse_dragged.get(&0).unwrap());
384 assert_eq!(1, *view.mouse_dragged.get(&1).unwrap());
385 assert_eq!(1, *view.mouse_dragged.get(&2).unwrap());
386 });
387 })
388}
389 
390#[test]
391fn test_relative_positioning() {
392 App::test((), |mut app| async move {
393 let app = &mut app;
394 
395 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
396 RelativePositionedView::new()
397 });
398 
399 position_child_and_assert_location(
400 OffsetPositioning::offset_from_save_position_element(
401 RelativePositionedView::first_child_position_id(),
402 vec2f(25., 25.),
403 PositionedElementOffsetBounds::Unbounded,
404 PositionedElementAnchor::TopLeft,
405 ChildAnchor::TopLeft,
406 ),
407 RectF::new(vec2f(25., 25.), vec2f(50., 50.)),
408 app,
409 window_id,
410 view.clone(),
411 );
412 
413 // Update the view to position the top right of the child offset from the top right of
414 // the parent (this should mean part of the child is clipped offscreen on the left).
415 position_child_and_assert_location(
416 OffsetPositioning::offset_from_save_position_element(
417 RelativePositionedView::first_child_position_id(),
418 vec2f(25., 25.),
419 PositionedElementOffsetBounds::Unbounded,
420 PositionedElementAnchor::TopLeft,
421 ChildAnchor::TopRight,
422 ),
423 RectF::new(vec2f(-25., 25.), vec2f(50., 50.)),
424 app,
425 window_id,
426 view.clone(),
427 );
428 
429 // Offset with the same position, but bound horizontally to the parent so the element is
430 // no longer clipped past the left side of the screen.
431 position_child_and_assert_location(
432 OffsetPositioning::from_axes(
433 PositioningAxis::relative_to_stack_child(
434 RelativePositionedView::first_child_position_id(),
435 PositionedElementOffsetBounds::ParentByPosition,
436 OffsetType::Pixel(25.),
437 AnchorPair::new(XAxisAnchor::Left, XAxisAnchor::Right),
438 ),
439 PositioningAxis::relative_to_stack_child(
440 RelativePositionedView::first_child_position_id(),
441 PositionedElementOffsetBounds::Unbounded,
442 OffsetType::Pixel(25.),
443 AnchorPair::new(YAxisAnchor::Top, YAxisAnchor::Top),
444 ),
445 ),
446 RectF::new(vec2f(0., 25.), vec2f(50., 50.)),
447 app,
448 window_id,
449 view.clone(),
450 );
451 
452 // Now just bound vertically to the parent. This should not change the positioning since
453 // the element is already bound vertically within the parent.
454 position_child_and_assert_location(
455 OffsetPositioning::from_axes(
456 PositioningAxis::relative_to_stack_child(
457 RelativePositionedView::first_child_position_id(),
458 PositionedElementOffsetBounds::Unbounded,
459 OffsetType::Pixel(25.),
460 AnchorPair::new(XAxisAnchor::Left, XAxisAnchor::Right),
461 ),
462 PositioningAxis::relative_to_stack_child(
463 RelativePositionedView::first_child_position_id(),
464 PositionedElementOffsetBounds::ParentByPosition,
465 OffsetType::Pixel(25.),
466 AnchorPair::new(YAxisAnchor::Top, YAxisAnchor::Top),
467 ),
468 ),
469 RectF::new(vec2f(-25., 25.), vec2f(50., 50.)),
470 app,
471 window_id,
472 view.clone(),
473 );
474 
475 // Update the view to position the top left of the child offset from the top right of the
476 // parent.
477 position_child_and_assert_location(
478 OffsetPositioning::from_axes(
479 PositioningAxis::relative_to_stack_child(
480 RelativePositionedView::first_child_position_id(),
481 PositionedElementOffsetBounds::Unbounded,
482 OffsetType::Pixel(25.),
483 AnchorPair::new(XAxisAnchor::Right, XAxisAnchor::Left),
484 ),
485 PositioningAxis::relative_to_stack_child(
486 RelativePositionedView::first_child_position_id(),
487 PositionedElementOffsetBounds::Unbounded,
488 OffsetType::Pixel(25.),
489 AnchorPair::new(YAxisAnchor::Top, YAxisAnchor::Top),
490 ),
491 ),
492 RectF::new(vec2f(75., 25.), vec2f(50., 50.)),
493 app,
494 window_id,
495 view.clone(),
496 );
497 
498 // Now, bound vertically with the parent--this should have no effect here since the
499 // child is fully contained within its parent.
500 let new_positioning = OffsetPositioning::from_axes(
501 PositioningAxis::relative_to_stack_child(
502 RelativePositionedView::first_child_position_id(),
503 PositionedElementOffsetBounds::Unbounded,
504 OffsetType::Pixel(25.),
505 AnchorPair::new(XAxisAnchor::Right, XAxisAnchor::Left),
506 ),
507 PositioningAxis::relative_to_stack_child(
508 RelativePositionedView::first_child_position_id(),
509 PositionedElementOffsetBounds::ParentByPosition,
510 OffsetType::Pixel(25.),
511 AnchorPair::new(YAxisAnchor::Top, YAxisAnchor::Top),
512 ),
513 );
514 
515 position_child_and_assert_location(
516 new_positioning,
517 RectF::new(vec2f(75., 25.), vec2f(50., 50.)),
518 app,
519 window_id,
520 view.clone(),
521 );
522 
523 // Position the child's bottom right corner on the parent's bottom right corner. With
524 // no offset this means they should be stacked directly on top of each other.
525 position_child_and_assert_location(
526 OffsetPositioning::from_axes(
527 PositioningAxis::relative_to_stack_child(
528 RelativePositionedView::first_child_position_id(),
529 PositionedElementOffsetBounds::Unbounded,
530 OffsetType::Pixel(0.),
531 AnchorPair::new(XAxisAnchor::Right, XAxisAnchor::Right),
532 ),
533 PositioningAxis::relative_to_stack_child(
534 RelativePositionedView::first_child_position_id(),
535 PositionedElementOffsetBounds::Unbounded,
536 OffsetType::Pixel(0.),
537 AnchorPair::new(YAxisAnchor::Bottom, YAxisAnchor::Bottom),
538 ),
539 ),
540 RectF::new(vec2f(0., 0.), vec2f(50., 50.)),
541 app,
542 window_id,
543 view.clone(),
544 );
545 
546 // Align the child vertically from the parent and horizontally from the child.
547 position_child_and_assert_location(
548 OffsetPositioning::from_axes(
549 PositioningAxis::relative_to_stack_child(
550 RelativePositionedView::first_child_position_id(),
551 PositionedElementOffsetBounds::Unbounded,
552 OffsetType::Pixel(5.),
553 AnchorPair::new(XAxisAnchor::Right, XAxisAnchor::Left),
554 ),
555 PositioningAxis::relative_to_parent(
556 ParentOffsetBounds::Unbounded,
557 OffsetType::Pixel(5.),
558 AnchorPair::new(YAxisAnchor::Top, YAxisAnchor::Top),
559 ),
560 ),
561 RectF::new(vec2f(55., 5.), vec2f(50., 50.)),
562 app,
563 window_id,
564 view,
565 );
566 })
567}
568 
569#[test]
570fn test_relative_positioning_bound_to_window_by_size() {
571 App::test((), |mut app| async move {
572 let app = &mut app;
573 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
574 RelativePositionedView::new()
575 });
576 let window_size = view.update(app, |_, ctx| {
577 ctx.notify();
578 ctx.windows()
579 .platform_window(window_id)
580 .expect("Window should exist for platform.")
581 .size()
582 });
583 
584 let offset = vec2f(25., 25.);
585 let positioning = OffsetPositioning::offset_from_save_position_element(
586 RelativePositionedView::first_child_position_id(),
587 offset,
588 PositionedElementOffsetBounds::WindowBySize,
589 PositionedElementAnchor::BottomRight,
590 ChildAnchor::TopLeft,
591 );
592 view.update(app, |view, ctx| {
593 view.second_child_positioning = Some(positioning);
594 
595 // Set the offset-positioned child's size to the window size so the bounding
596 // behavior is actually tested.
597 view.second_child_size = Some(window_size);
598 ctx.notify();
599 });
600 
601 // Simulate a render frame to ensure the scene is built.
602 app.update(|ctx| ctx.simulate_render_frame(window_id));
603 
604 let presenter_ref = app
605 .presenter(window_id)
606 .expect("Test window should have a presenter since first frame is rendered.");
607 let presenter = presenter_ref.borrow();
608 let scene = presenter
609 .scene()
610 .expect("Presenter should have rendered a scene after the view was updated.");
611 
612 // The expected bounds should go from the anchor position with offset to the edge of
613 // the window bounds. Note the usage of `RectF::from_points`, which specifies top-left
614 // and bottom-right coordinates, rather than the default `RectF::new()` constructor.
615 let expected_bounds = RectF::from_points(vec2f(75., 75.), window_size);
616 assert_eq!(
617 scene
618 .layers()
619 .collect_vec()
620 .get(2)
621 .unwrap()
622 .rects
623 .iter()
624 .map(|r| { r.bounds })
625 .collect::<Vec<_>>(),
626 vec![expected_bounds]
627 );
628 })
629}
630 
631#[test]
632fn test_relative_positioning_bound_to_window_by_position() {
633 App::test((), |mut app| async move {
634 let app = &mut app;
635 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
636 RelativePositionedView::new()
637 });
638 let window_size = view.update(app, |_, ctx| {
639 ctx.notify();
640 ctx.windows()
641 .platform_window(window_id)
642 .expect("Window should exist for platform.")
643 .size()
644 });
645 
646 let offset = vec2f(25., 25.);
647 let positioning = OffsetPositioning::offset_from_save_position_element(
648 RelativePositionedView::first_child_position_id(),
649 offset,
650 PositionedElementOffsetBounds::WindowByPosition,
651 PositionedElementAnchor::BottomRight,
652 ChildAnchor::TopLeft,
653 );
654 view.update(app, |view, ctx| {
655 view.second_child_positioning = Some(positioning);
656 
657 // Set the offset-positioned child's size to the window size so the bounding
658 // behavior is actually tested.
659 view.second_child_size = Some(window_size);
660 ctx.notify();
661 });
662 
663 let presenter_ref = app
664 .presenter(window_id)
665 .expect("Test window should have a presenter since first frame is rendered.");
666 let presenter = presenter_ref.borrow();
667 let scene = presenter
668 .scene()
669 .expect("Presenter should have rendered a scene after the view was updated.");
670 
671 // The expected bounds should have a modified position to accomodate the size of the
672 // positioned child (it should be moved back to (0,0) from it's 'default' (75, 75).
673 //
674 // Note the usage of `RectF::from_points`, which specifies top-left
675 // and bottom-right coordinates, rather than the default `RectF::new()` constructor.
676 let expected_bounds = RectF::from_points(vec2f(0., 0.), window_size);
677 assert_eq!(
678 scene
679 .layers()
680 .collect_vec()
681 .get(2)
682 .unwrap()
683 .rects
684 .iter()
685 .map(|r| { r.bounds })
686 .collect::<Vec<_>>(),
687 vec![expected_bounds]
688 );
689 })
690}
691 
692#[test]
693fn test_relative_positioning_bound_to_missing_anchor() {
694 App::test((), |mut app| async move {
695 let (window_id, _) = app.add_window(WindowStyle::NotStealFocus, |_| {
696 let mut view = RelativePositionedView::new();
697 
698 view.second_child_positioning = Some(OffsetPositioning::from_axes(
699 PositioningAxis::relative_to_stack_child(
700 "nonexistent_anchor",
701 PositionedElementOffsetBounds::WindowBySize,
702 OffsetType::Pixel(0.),
703 AnchorPair::new(XAxisAnchor::Middle, XAxisAnchor::Middle),
704 )
705 .with_conditional_anchor(),
706 PositioningAxis::relative_to_stack_child(
707 "nonexistent_anchor",
708 PositionedElementOffsetBounds::WindowBySize,
709 OffsetType::Pixel(0.),
710 AnchorPair::new(YAxisAnchor::Middle, YAxisAnchor::Middle),
711 )
712 .with_conditional_anchor(),
713 ));
714 
715 view
716 });
717 
718 let mut presenter = Presenter::new(window_id);
719 
720 let invalidation = WindowInvalidation {
721 updated: HashSet::from([app.root_view_id(window_id).expect("Root view must exist")]),
722 ..Default::default()
723 };
724 
725 app.update(move |ctx| {
726 presenter.invalidate(invalidation, ctx);
727 
728 let window_size = RectF::new(Vector2F::zero(), vec2f(300., 300.));
729 let scene = presenter.build_scene(window_size.size(), 1., None, ctx);
730 
731 assert_eq!(scene.z_index(), ZIndex::new(0));
732 assert_eq!(scene.layer_count(), 3);
733 
734 let stack_layer = scene.layers().nth(2).expect("Should be 3 layers");
735 assert!(
736 stack_layer.rects.is_empty(),
737 "Relative-positioned element should not have been laid out"
738 );
739 // In addition to the assertion that there's no rect for the second
740 // child, this implicitly tests that we don't panic during layout.
741 });
742 });
743}
744 
745/// Positions the second child using the positioning and asserts the child is at bounds
746/// indicated within `expected_child_bounds`.
747fn position_child_and_assert_location(
748 positioning: OffsetPositioning,
749 expected_child_bounds: RectF,
750 app: &mut App,
751 window_id: WindowId,
752 view: ViewHandle<RelativePositionedView>,
753) {
754 view.update(app, |view, _| {
755 view.second_child_positioning = Some(positioning);
756 });
757 
758 let mut presenter = Presenter::new(window_id);
759 
760 let mut updated = HashSet::new();
761 updated.insert(app.root_view_id(window_id).unwrap());
762 let invalidation = WindowInvalidation {
763 updated,
764 ..Default::default()
765 };
766 
767 app.update(move |ctx| {
768 presenter.invalidate(invalidation, ctx);
769 let window_size = RectF::new(Vector2F::zero(), vec2f(300., 300.));
770 let scene = presenter.build_scene(window_size.size(), 1., None, ctx);
771 
772 assert_eq!(scene.z_index(), ZIndex::new(0));
773 assert_eq!(scene.layer_count(), 3);
774 
775 assert_eq!(
776 scene
777 .layers()
778 .nth(1)
779 .unwrap()
780 .rects
781 .iter()
782 .map(|r| r.bounds)
783 .collect::<Vec<_>>(),
784 vec![RectF::new(Vector2F::zero(), vec2f(50., 50.))]
785 );
786 assert_eq!(
787 scene
788 .layers()
789 .nth(2)
790 .unwrap()
791 .rects
792 .iter()
793 .map(|r| { r.bounds })
794 .collect::<Vec<_>>(),
795 vec![expected_child_bounds]
796 );
797 });
798}
799