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/flex/mod_test.rs
1use std::collections::HashSet;
2 
3use super::*;
4use crate::elements::{Align, SavePosition, Stack};
5use crate::geometry::rect::RectF;
6 
7use crate::platform::WindowStyle;
8use crate::{
9 elements::{ConstrainedBox, ParentElement, Rect},
10 App, Entity, Presenter, TypedActionView, WindowId, WindowInvalidation,
11};
12 
13type RenderFn = dyn Fn(&AppContext) -> Box<dyn Element> + 'static;
14 
15struct TestDynamicView {
16 render: Box<RenderFn>,
17}
18 
19impl TestDynamicView {
20 fn new(render: impl Fn(&AppContext) -> Box<dyn Element> + 'static) -> Self {
21 Self {
22 render: Box::new(render),
23 }
24 }
25}
26 
27impl Entity for TestDynamicView {
28 type Event = ();
29}
30 
31impl crate::core::View for TestDynamicView {
32 fn ui_name() -> &'static str {
33 "Flex::tests::TestDynamicView"
34 }
35 
36 fn render(&self, app: &AppContext) -> Box<dyn Element> {
37 (self.render)(app)
38 }
39}
40 
41impl TypedActionView for TestDynamicView {
42 type Action = ();
43}
44 
45/// Asserts that the bounds of all the painted rects match that of `rects`.
46fn assert_bounds_of_rects(
47 app: &mut App,
48 window_id: WindowId,
49 rects: impl IntoIterator<Item = RectF>,
50) {
51 let presenter_ref = app
52 .presenter(window_id)
53 .expect("Test window should have a presenter since first frame is rendered.");
54 let presenter = presenter_ref.borrow();
55 let scene = presenter
56 .scene()
57 .expect("Presenter should have rendered a scene after the test_view was updated.");
58 
59 let actual_rects = scene
60 .layers()
61 .next()
62 .into_iter()
63 .flat_map(|layer| layer.rects.iter())
64 .map(|rect| rect.bounds);
65 
66 itertools::assert_equal(actual_rects, rects);
67}
68 
69struct View {
70 flex_main_axis_size: MainAxisSize,
71 flex_main_axis_alignment: MainAxisAlignment,
72 flex_cross_axis_alignment: CrossAxisAlignment,
73}
74 
75impl View {
76 fn new(
77 axis_size: MainAxisSize,
78 main_axis_alignment: MainAxisAlignment,
79 cross_axis_alignment: CrossAxisAlignment,
80 ) -> Self {
81 Self {
82 flex_main_axis_size: axis_size,
83 flex_main_axis_alignment: main_axis_alignment,
84 flex_cross_axis_alignment: cross_axis_alignment,
85 }
86 }
87}
88 
89impl Entity for View {
90 type Event = String;
91}
92 
93impl crate::core::View for View {
94 fn render<'a>(&self, _: &AppContext) -> Box<dyn Element> {
95 let flex = Flex::row()
96 .with_children([
97 SavePosition::new(
98 ConstrainedBox::new(Rect::new().finish())
99 .with_height(20.)
100 .with_width(50.)
101 .finish(),
102 "view_1",
103 )
104 .finish(),
105 SavePosition::new(
106 ConstrainedBox::new(Rect::new().finish())
107 .with_height(30.)
108 .with_width(50.)
109 .finish(),
110 "view_2",
111 )
112 .finish(),
113 SavePosition::new(
114 Flex::row()
115 .with_child(
116 ConstrainedBox::new(Rect::new().finish())
117 .with_height(50.)
118 .with_width(50.)
119 .finish(),
120 )
121 .finish(),
122 "view_3",
123 )
124 .finish(),
125 ])
126 .with_cross_axis_alignment(self.flex_cross_axis_alignment)
127 .with_main_axis_alignment(self.flex_main_axis_alignment)
128 .with_main_axis_size(self.flex_main_axis_size);
129 
130 Stack::new()
131 .with_child(
132 Align::new(SavePosition::new(flex.finish(), "flex").finish())
133 .top_left()
134 .finish(),
135 )
136 .finish()
137 }
138 
139 fn ui_name() -> &'static str {
140 "View"
141 }
142}
143 
144impl TypedActionView for View {
145 type Action = ();
146}
147 
148#[test]
149fn test_flex_main_axis_alignment() {
150 App::test((), |mut app| async move {
151 let app = &mut app;
152 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
153 View::new(
154 MainAxisSize::Min,
155 MainAxisAlignment::Start,
156 CrossAxisAlignment::Start,
157 )
158 });
159 
160 let mut presenter = Presenter::new(window_id);
161 
162 let mut updated = HashSet::new();
163 updated.insert(app.root_view_id(window_id).expect("root view should exist"));
164 let invalidation = WindowInvalidation {
165 updated,
166 ..Default::default()
167 };
168 
169 app.update(move |ctx| {
170 presenter.invalidate(invalidation.clone(), ctx);
171 let window_size = RectF::new(Vector2F::zero(), vec2f(300., 300.));
172 presenter.build_scene(window_size.size(), 1., None, ctx);
173 
174 let view_1 = presenter
175 .position_cache()
176 .get_position("view_1")
177 .expect("position should exist");
178 let view_2 = presenter
179 .position_cache()
180 .get_position("view_2")
181 .expect("position should exist");
182 let view_3 = presenter
183 .position_cache()
184 .get_position("view_3")
185 .expect("position should exist");
186 let flex = presenter
187 .position_cache()
188 .get_position("flex")
189 .expect("position should exist");
190 
191 let view_1_size = vec2f(50., 20.);
192 let view_2_size = vec2f(50., 30.);
193 let view_3_size = vec2f(50., 50.);
194 
195 // The view has a min axis size, so each element should be rendered right next to
196 // each other and the flex should take up the total size of the elements.
197 assert_eq!(view_1, RectF::new(Vector2F::zero(), view_1_size));
198 assert_eq!(view_2, RectF::new(vec2f(50., 0.), view_2_size));
199 assert_eq!(view_3, RectF::new(vec2f(100., 0.), view_3_size));
200 assert_eq!(flex, RectF::new(Vector2F::zero(), vec2f(150., 50.)));
201 
202 view.update(ctx, |view, _ctx| {
203 view.flex_main_axis_alignment = MainAxisAlignment::Start;
204 view.flex_main_axis_size = MainAxisSize::Max;
205 });
206 
207 presenter.invalidate(invalidation.clone(), ctx);
208 presenter.build_scene(window_size.size(), 1., None, ctx);
209 
210 let view_1 = presenter
211 .position_cache()
212 .get_position("view_1")
213 .expect("position should exist");
214 let view_2 = presenter
215 .position_cache()
216 .get_position("view_2")
217 .expect("position should exist");
218 let view_3 = presenter
219 .position_cache()
220 .get_position("view_3")
221 .expect("position should exist");
222 let flex = presenter
223 .position_cache()
224 .get_position("flex")
225 .expect("position should exist");
226 
227 // The view has a flex axis alignment of start, so ensure that each child element
228 // is rendered next to each other, but that the flex expands out to the max size of
229 // the window.
230 assert_eq!(view_1, RectF::new(Vector2F::zero(), view_1_size));
231 assert_eq!(view_2, RectF::new(vec2f(50., 0.), view_2_size));
232 assert_eq!(view_3, RectF::new(vec2f(100., 0.), view_3_size));
233 assert_eq!(flex, RectF::new(Vector2F::zero(), vec2f(300., 50.)));
234 
235 view.update(ctx, |view, _ctx| {
236 view.flex_main_axis_alignment = MainAxisAlignment::SpaceBetween;
237 view.flex_main_axis_size = MainAxisSize::Max;
238 });
239 
240 presenter.invalidate(invalidation.clone(), ctx);
241 presenter.build_scene(window_size.size(), 1., None, ctx);
242 
243 let view_1 = presenter
244 .position_cache()
245 .get_position("view_1")
246 .expect("position should exist");
247 let view_2 = presenter
248 .position_cache()
249 .get_position("view_2")
250 .expect("position should exist");
251 let view_3 = presenter
252 .position_cache()
253 .get_position("view_3")
254 .expect("position should exist");
255 let flex = presenter
256 .position_cache()
257 .get_position("flex")
258 .expect("position should exist");
259 
260 // Ensure that the elements are evenly spaced (with no extra space at the
261 // beginning or end) and that the flex expands out to the max size of the window.
262 assert_eq!(view_1, RectF::new(Vector2F::zero(), view_1_size));
263 assert_eq!(view_2, RectF::new(vec2f(125., 0.), view_2_size));
264 assert_eq!(view_3, RectF::new(vec2f(250., 0.), view_3_size));
265 assert_eq!(flex, RectF::new(Vector2F::zero(), vec2f(300., 50.)));
266 
267 view.update(ctx, |view, _ctx| {
268 view.flex_main_axis_alignment = MainAxisAlignment::SpaceEvenly;
269 view.flex_main_axis_size = MainAxisSize::Max;
270 });
271 
272 presenter.invalidate(invalidation.clone(), ctx);
273 presenter.build_scene(window_size.size(), 1., None, ctx);
274 
275 let view_1 = presenter
276 .position_cache()
277 .get_position("view_1")
278 .expect("position should exist");
279 let view_2 = presenter
280 .position_cache()
281 .get_position("view_2")
282 .expect("position should exist");
283 let view_3 = presenter
284 .position_cache()
285 .get_position("view_3")
286 .expect("position should exist");
287 let flex = presenter
288 .position_cache()
289 .get_position("flex")
290 .expect("position should exist");
291 
292 // Ensure that the elements are evenly spaced, including space before and after
293 // the child elements, and that the flex expands out to the max size of the window.
294 assert_eq!(view_1, RectF::new(vec2f(37.5, 0.), view_1_size));
295 assert_eq!(view_2, RectF::new(vec2f(125., 0.), view_2_size));
296 assert_eq!(view_3, RectF::new(vec2f(212.5, 0.), view_3_size));
297 assert_eq!(flex, RectF::new(Vector2F::zero(), vec2f(300., 50.)));
298 
299 view.update(ctx, |view, _ctx| {
300 view.flex_main_axis_alignment = MainAxisAlignment::End;
301 view.flex_main_axis_size = MainAxisSize::Max;
302 });
303 
304 presenter.invalidate(invalidation.clone(), ctx);
305 presenter.build_scene(window_size.size(), 1., None, ctx);
306 
307 let view_1 = presenter
308 .position_cache()
309 .get_position("view_1")
310 .expect("position should exist");
311 let view_2 = presenter
312 .position_cache()
313 .get_position("view_2")
314 .expect("position should exist");
315 let view_3 = presenter
316 .position_cache()
317 .get_position("view_3")
318 .expect("position should exist");
319 let flex = presenter
320 .position_cache()
321 .get_position("flex")
322 .expect("position should exist");
323 
324 // The view has a flex axis alignment of end, so ensure that each child element
325 // is rendered next to each other, but that the flex expands out to the max size of
326 // the window.
327 assert_eq!(view_3, RectF::new(vec2f(250., 0.), view_3_size));
328 assert_eq!(view_2, RectF::new(vec2f(200., 0.), view_2_size));
329 assert_eq!(view_1, RectF::new(vec2f(150., 0.), view_1_size));
330 assert_eq!(flex, RectF::new(Vector2F::zero(), vec2f(300., 50.)));
331 });
332 })
333}
334 
335#[test]
336fn test_flex_row_spacing() {
337 App::test((), |mut app| async move {
338 let app = &mut app;
339 
340 // Test basic row spacing
341 let (window_id, test_view) = app.add_window(WindowStyle::NotStealFocus, |_| {
342 TestDynamicView::new(|_| {
343 Flex::row()
344 .with_spacing(10.)
345 .with_children([
346 ConstrainedBox::new(Rect::new().finish())
347 .with_height(30.)
348 .with_width(50.)
349 .finish(),
350 ConstrainedBox::new(Rect::new().finish())
351 .with_height(30.)
352 .with_width(60.)
353 .finish(),
354 ConstrainedBox::new(Rect::new().finish())
355 .with_height(30.)
356 .with_width(40.)
357 .finish(),
358 ])
359 .finish()
360 })
361 });
362 test_view.update(app, |_, ctx| {
363 ctx.notify();
364 });
365 
366 // Children should have 10px spacing between them
367 assert_bounds_of_rects(
368 app,
369 window_id,
370 [
371 RectF::new(vec2f(0., 0.), vec2f(50., 30.)),
372 RectF::new(vec2f(60., 0.), vec2f(60., 30.)), // 50 + 10
373 RectF::new(vec2f(130., 0.), vec2f(40., 30.)), // 50 + 10 + 60 + 10
374 ],
375 );
376 })
377}
378 
379#[test]
380fn test_flex_column_spacing() {
381 App::test((), |mut app| async move {
382 let app = &mut app;
383 
384 // Test column spacing
385 let (window_id, test_view) = app.add_window(WindowStyle::NotStealFocus, |_| {
386 TestDynamicView::new(|_| {
387 Flex::column()
388 .with_spacing(15.)
389 .with_children([
390 ConstrainedBox::new(Rect::new().finish())
391 .with_height(30.)
392 .with_width(60.)
393 .finish(),
394 ConstrainedBox::new(Rect::new().finish())
395 .with_height(40.)
396 .with_width(80.)
397 .finish(),
398 ConstrainedBox::new(Rect::new().finish())
399 .with_height(25.)
400 .with_width(70.)
401 .finish(),
402 ])
403 .finish()
404 })
405 });
406 test_view.update(app, |_, ctx| {
407 ctx.notify();
408 });
409 
410 // Children should have 15px vertical spacing between them
411 assert_bounds_of_rects(
412 app,
413 window_id,
414 [
415 RectF::new(vec2f(0., 0.), vec2f(60., 30.)),
416 RectF::new(vec2f(0., 45.), vec2f(80., 40.)), // 30 + 15
417 RectF::new(vec2f(0., 100.), vec2f(70., 25.)), // 30 + 15 + 40 + 15
418 ],
419 );
420 })
421}
422 
423#[test]
424fn test_flex_spacing_with_center_alignment() {
425 App::test((), |mut app| async move {
426 let app = &mut app;
427 
428 // Test spacing with center alignment
429 let (window_id, test_view) = app.add_window(WindowStyle::NotStealFocus, |_| {
430 TestDynamicView::new(|_| {
431 ConstrainedBox::new(
432 Flex::row()
433 .with_spacing(20.)
434 .with_children([
435 ConstrainedBox::new(Rect::new().finish())
436 .with_height(30.)
437 .with_width(40.)
438 .finish(),
439 ConstrainedBox::new(Rect::new().finish())
440 .with_height(30.)
441 .with_width(40.)
442 .finish(),
443 ])
444 .with_main_axis_size(MainAxisSize::Max)
445 .with_main_axis_alignment(MainAxisAlignment::Center)
446 .finish(),
447 )
448 .with_width(300.)
449 .with_height(100.)
450 .finish()
451 })
452 });
453 test_view.update(app, |_, ctx| {
454 ctx.notify();
455 });
456 
457 // Total content width: 40 + 20 + 40 = 100
458 // Remaining space: 300 - 100 = 200
459 // Leading space: 200 / 2 = 100
460 assert_bounds_of_rects(
461 app,
462 window_id,
463 [
464 RectF::new(vec2f(100., 0.), vec2f(40., 30.)),
465 RectF::new(vec2f(160., 0.), vec2f(40., 30.)), // 100 + 40 + 20
466 ],
467 );
468 })
469}
470 
471#[test]
472fn test_flex_spacing_empty() {
473 App::test((), |mut app| async move {
474 let app = &mut app;
475 
476 // Test empty flex with spacing
477 let (window_id, test_view) = app.add_window(WindowStyle::NotStealFocus, |_| {
478 TestDynamicView::new(|_| Flex::row().with_spacing(15.).finish())
479 });
480 test_view.update(app, |_, ctx| {
481 ctx.notify();
482 });
483 
484 // Empty flex should render no children
485 assert_bounds_of_rects(app, window_id, []);
486 })
487}
488 
489#[test]
490fn test_flex_spacing_single_child() {
491 App::test((), |mut app| async move {
492 let app = &mut app;
493 
494 // Test single child with spacing (should have no effect)
495 let (window_id, test_view) = app.add_window(WindowStyle::NotStealFocus, |_| {
496 TestDynamicView::new(|_| {
497 Flex::row()
498 .with_spacing(20.)
499 .with_child(
500 ConstrainedBox::new(Rect::new().finish())
501 .with_height(30.)
502 .with_width(50.)
503 .finish(),
504 )
505 .finish()
506 })
507 });
508 test_view.update(app, |_, ctx| {
509 ctx.notify();
510 });
511 
512 // Single child should be positioned at origin regardless of spacing
513 assert_bounds_of_rects(app, window_id, [RectF::new(vec2f(0., 0.), vec2f(50., 30.))]);
514 })
515}
516 
517#[test]
518fn test_flex_cross_axis_alignment() {
519 App::test((), |mut app| async move {
520 let app = &mut app;
521 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| {
522 View::new(
523 MainAxisSize::Min,
524 MainAxisAlignment::Start,
525 CrossAxisAlignment::Start,
526 )
527 });
528 
529 let mut presenter = Presenter::new(window_id);
530 
531 let mut updated = HashSet::new();
532 updated.insert(app.root_view_id(window_id).expect("root view should exist"));
533 let invalidation = WindowInvalidation {
534 updated,
535 ..Default::default()
536 };
537 
538 app.update(move |ctx| {
539 presenter.invalidate(invalidation.clone(), ctx);
540 let window_size = RectF::new(Vector2F::zero(), vec2f(300., 300.));
541 presenter.build_scene(window_size.size(), 1., None, ctx);
542 
543 let view_1_size = vec2f(50., 20.);
544 let view_2_size = vec2f(50., 30.);
545 let view_3_size = vec2f(50., 50.);
546 
547 let view_1 = presenter
548 .position_cache()
549 .get_position("view_1")
550 .expect("position should exist");
551 let view_2 = presenter
552 .position_cache()
553 .get_position("view_2")
554 .expect("position should exist");
555 let view_3 = presenter
556 .position_cache()
557 .get_position("view_3")
558 .expect("position should exist");
559 let flex = presenter
560 .position_cache()
561 .get_position("flex")
562 .expect("position should exist");
563 
564 assert_eq!(view_1, RectF::new(Vector2F::zero(), view_1_size));
565 assert_eq!(view_2, RectF::new(vec2f(50., 0.), view_2_size));
566 assert_eq!(view_3, RectF::new(vec2f(100., 0.), view_3_size));
567 assert_eq!(flex, RectF::new(Vector2F::zero(), vec2f(150., 50.)));
568 
569 view.update(ctx, |view, _ctx| {
570 view.flex_cross_axis_alignment = CrossAxisAlignment::Center;
571 });
572 
573 presenter.invalidate(invalidation.clone(), ctx);
574 presenter.build_scene(window_size.size(), 1., None, ctx);
575 
576 let view_1 = presenter
577 .position_cache()
578 .get_position("view_1")
579 .expect("position should exist");
580 let view_2 = presenter
581 .position_cache()
582 .get_position("view_2")
583 .expect("position should exist");
584 let view_3 = presenter
585 .position_cache()
586 .get_position("view_3")
587 .expect("position should exist");
588 let flex = presenter
589 .position_cache()
590 .get_position("flex")
591 .expect("position should exist");
592 
593 assert_eq!(view_1, RectF::new(vec2f(0., 15.), view_1_size));
594 assert_eq!(view_2, RectF::new(vec2f(50., 10.), view_2_size));
595 assert_eq!(view_3, RectF::new(vec2f(100., 0.), view_3_size));
596 assert_eq!(flex, RectF::new(Vector2F::zero(), vec2f(150., 50.)));
597 
598 view.update(ctx, |view, _ctx| {
599 view.flex_cross_axis_alignment = CrossAxisAlignment::End;
600 });
601 
602 presenter.invalidate(invalidation.clone(), ctx);
603 presenter.build_scene(window_size.size(), 1., None, ctx);
604 
605 let view_1 = presenter
606 .position_cache()
607 .get_position("view_1")
608 .expect("position should exist");
609 let view_2 = presenter
610 .position_cache()
611 .get_position("view_2")
612 .expect("position should exist");
613 let view_3 = presenter
614 .position_cache()
615 .get_position("view_3")
616 .expect("position should exist");
617 let flex = presenter
618 .position_cache()
619 .get_position("flex")
620 .expect("position should exist");
621 
622 assert_eq!(view_1, RectF::new(vec2f(0., 30.), view_1_size));
623 assert_eq!(view_2, RectF::new(vec2f(50., 20.), view_2_size));
624 assert_eq!(view_3, RectF::new(vec2f(100., 0.), view_3_size));
625 assert_eq!(flex, RectF::new(Vector2F::zero(), vec2f(150., 50.)));
626 
627 view.update(ctx, |view, _ctx| {
628 view.flex_cross_axis_alignment = CrossAxisAlignment::Stretch;
629 });
630 
631 presenter.invalidate(invalidation.clone(), ctx);
632 presenter.build_scene(window_size.size(), 1., None, ctx);
633 
634 let view_1 = presenter
635 .position_cache()
636 .get_position("view_1")
637 .expect("position should exist");
638 let view_2 = presenter
639 .position_cache()
640 .get_position("view_2")
641 .expect("position should exist");
642 let view_3 = presenter
643 .position_cache()
644 .get_position("view_3")
645 .expect("position should exist");
646 let flex = presenter
647 .position_cache()
648 .get_position("flex")
649 .expect("position should exist");
650 
651 assert_eq!(view_1, RectF::new(vec2f(0., 0.), view_1_size));
652 assert_eq!(view_2, RectF::new(vec2f(50., 0.), view_2_size));
653 // view 3 is a Flex::row(), so applying cross-axis stretch to its
654 // parent should cause the child flex height to fill the parent's
655 // maximum height (which, in this case, is the height of the window).
656 assert_eq!(
657 view_3,
658 RectF::new(
659 vec2f(100., 0.),
660 vec2f(view_3_size.x(), window_size.height())
661 )
662 );
663 assert_eq!(
664 flex,
665 RectF::new(Vector2F::zero(), vec2f(150., window_size.height()))
666 );
667 });
668 })
669}
670