1#![no_std]
3#![doc = include_str!("../README.md")]
4
5#[cfg(feature = "framebuffer")]
6pub mod buffered;
7mod common;
8mod draw;
9mod element;
10mod layout;
11pub mod style;
12mod target;
13mod theme;
14mod tree;
15
16use embedded_graphics::{pixelcolor::Rgb565, prelude::PixelColor};
17
18pub use element::{
19 BuildError, CustomBuilder, CustomElement, DivStyle, ElementBuilder, Font, NoCustomElement,
20 ParentElement, StyledFlexContainer, TextStyle, TextStyledElement,
21};
22use heapless::VecView;
23pub use style::{Insets, Style, StyledElement};
24pub use target::{DirectTarget, DisplayTarget};
25pub use theme::Theme;
26
27use crate::{
28 common::{NodeIndex, TextRange},
29 layout::Constraints,
30 tree::{FrameTree, Node},
31};
32
33pub struct FrameStorage<C, const N: usize = 64, const T: usize = 1024, CE = NoCustomElement>
38where
39 C: PixelColor,
40{
41 nodes: heapless::Vec<Node<C, CE>, N>,
42 text: heapless::Vec<u8, T>,
46}
47
48impl<C, const N: usize, const T: usize, CE> Default for FrameStorage<C, N, T, CE>
49where
50 C: PixelColor,
51{
52 fn default() -> Self {
53 Self { nodes: heapless::Vec::new(), text: heapless::Vec::new() }
54 }
55}
56
57#[derive(Debug, Clone, Copy)]
59#[cfg_attr(feature = "defmt", derive(defmt::Format))]
60pub struct FrameUsage {
61 pub nodes: usize,
63 pub text: usize,
65}
66
67#[derive(Debug, Clone, Copy)]
69#[cfg_attr(feature = "defmt", derive(defmt::Format))]
70pub struct FrameCapacity {
71 pub nodes: usize,
73 pub text: usize,
75}
76
77impl<C, const N: usize, const T: usize, CE> FrameStorage<C, N, T, CE>
78where
79 C: PixelColor,
80{
81 pub const fn view(&mut self) -> StorageView<'_, C, CE> {
83 StorageView { nodes: &mut self.nodes, text: &mut self.text }
84 }
85
86 pub fn clear(&mut self) {
88 self.nodes.clear();
89 self.text.clear();
90 }
91
92 pub const fn size(&self) -> usize {
94 core::mem::size_of::<Self>()
95 }
96
97 pub fn usage(&self) -> FrameUsage {
99 FrameUsage { nodes: self.nodes.len(), text: self.text.len() }
100 }
101
102 pub const fn capacity(&self) -> FrameCapacity {
104 FrameCapacity { nodes: N, text: T }
105 }
106}
107
108pub struct StorageView<'frame, C, CE>
110where
111 C: PixelColor,
112{
113 nodes: &'frame mut VecView<Node<C, CE>>,
114 text: &'frame mut VecView<u8>,
115}
116
117pub struct Ui<D, const N: usize = 64, const T: usize = 1024, CE = NoCustomElement>
120where
121 D: DisplayTarget,
122{
123 display: D,
124 storage: FrameStorage<D::Color, N, T, CE>,
125 theme: Theme<D::Color>,
126}
127
128#[derive(Debug, thiserror::Error)]
130#[cfg_attr(feature = "defmt", derive(defmt::Format))]
131pub enum RenderError<E> {
132 #[error(transparent)]
134 Build(#[from] BuildError),
135 #[error("display drawing failed")]
137 Draw(E),
138}
139
140impl<D, const N: usize, const T: usize, CE> Ui<D, N, T, CE>
141where
142 D: DisplayTarget,
143{
144 pub const fn with_theme(
149 display: D,
150 storage: FrameStorage<D::Color, N, T, CE>,
151 theme: Theme<D::Color>,
152 ) -> Self {
153 Self { display, storage, theme }
154 }
155
156 pub const fn with_background(mut self, background: D::Color) -> Self {
159 self.theme.background = background;
160 self
161 }
162
163 pub const fn with_foreground(mut self, foreground: D::Color) -> Self {
165 self.theme.foreground = foreground;
166 self
167 }
168
169 pub fn render<V>(&mut self, view: &V) -> Result<(), RenderError<D::Error>>
171 where
172 CE: CustomElement<D::Color>,
173 V: Render<D::Color, CE>,
174 {
175 self.storage.clear();
176
177 let frame = self.storage.view();
178 let cx = Context::new(frame);
179
180 let root = view.render(&cx).try_build()?;
181
182 let viewport = Constraints::max(self.display.size());
184
185 let mut tree = FrameTree::new(cx.storage.into_inner());
187 tree.layout(root, viewport);
188
189 tree.draw(&self.theme, &mut self.display).map_err(RenderError::Draw)?;
190
191 Ok(())
192 }
193
194 pub const fn display(&self) -> &D {
196 &self.display
197 }
198
199 pub const fn display_mut(&mut self) -> &mut D {
201 &mut self.display
202 }
203
204 pub fn into_display(self) -> D {
206 self.display
207 }
208
209 pub const fn theme(&self) -> &Theme<D::Color> {
211 &self.theme
212 }
213
214 pub const fn storage(&self) -> &FrameStorage<D::Color, N, T, CE> {
216 &self.storage
217 }
218}
219
220impl<D, const N: usize, const T: usize, CE> Ui<D, N, T, CE>
221where
222 D: DisplayTarget,
223 Theme<D::Color>: Default,
224{
225 pub fn new(display: D, storage: FrameStorage<D::Color, N, T, CE>) -> Self {
227 Self::with_theme(display, storage, Theme::default())
228 }
229}
230
231pub trait FluentBuilder {
233 fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
235 where
236 Self: Sized,
237 {
238 f(self)
239 }
240
241 fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
243 where
244 Self: Sized,
245 {
246 self.map(|this| if condition { then(this) } else { this })
247 }
248
249 fn when_else(
251 self,
252 condition: bool,
253 then: impl FnOnce(Self) -> Self,
254 else_fn: impl FnOnce(Self) -> Self,
255 ) -> Self
256 where
257 Self: Sized,
258 {
259 self.map(|this| if condition { then(this) } else { else_fn(this) })
260 }
261
262 fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
264 where
265 Self: Sized,
266 {
267 self.map(|this| if let Some(value) = option { then(this, value) } else { this })
268 }
269 fn when_none<T>(self, option: &Option<T>, then: impl FnOnce(Self) -> Self) -> Self
271 where
272 Self: Sized,
273 {
274 self.map(|this| if option.is_some() { this } else { then(this) })
275 }
276}
277
278impl<T: ElementBuilder> FluentBuilder for T {}
280
281pub trait Render<C = Rgb565, CE = NoCustomElement>
288where
289 C: PixelColor,
290{
291 fn render(&self, cx: &Context<'_, C, CE>) -> impl ElementBuilder;
293}
294
295pub struct Context<'frame, C: PixelColor = Rgb565, CE = NoCustomElement> {
297 storage: core::cell::RefCell<StorageView<'frame, C, CE>>,
298}
299
300impl<'frame, C: PixelColor, CE> Context<'frame, C, CE> {
301 const fn new(storage: StorageView<'frame, C, CE>) -> Self {
303 Self { storage: core::cell::RefCell::new(storage) }
304 }
305
306 fn link_sibling(&self, node: NodeIndex, sibling: NodeIndex) {
308 let mut storage = self.storage.borrow_mut();
309
310 storage.nodes[node].set_sibling(sibling);
311 }
312
313 fn insert(&self, node: Node<C, CE>) -> Result<NodeIndex, BuildError> {
315 let mut storage = self.storage.borrow_mut();
316 let index = storage.nodes.len();
317 storage.nodes.push(node).map_err(|_| BuildError::NodeCapacity)?;
318
319 Ok(index)
320 }
321
322 fn store_text(&self, content: &str) -> Result<TextRange, BuildError> {
325 let mut storage = self.storage.borrow_mut();
326
327 let offset = storage.text.len();
328 let len = content.len();
329 let end = offset.checked_add(len).ok_or(BuildError::TextCapacity)?;
330
331 if end > storage.text.capacity() {
332 return Err(BuildError::TextCapacity);
333 }
334
335 storage.text.extend_from_slice(content.as_bytes()).map_err(|_| BuildError::TextCapacity)?;
337
338 Ok(TextRange { offset, len })
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use embedded_graphics::{
345 mock_display::MockDisplay,
346 pixelcolor::{BinaryColor, Rgb565},
347 prelude::{Point, RgbColor as _, Size},
348 primitives::Rectangle,
349 };
350
351 use crate::{element::DivStyle, style::FlexDirection, tree::NodeKind};
352
353 use super::*;
354
355 struct Dashboard {
356 text: &'static str,
357 }
358
359 impl Render for Dashboard {
360 fn render(&self, cx: &Context<'_>) -> impl ElementBuilder {
361 cx.row()
362 .child(cx.text(self.text))
363 .child(cx.row().child(cx.text("Nested")))
364 .when(true, |row| row.child(cx.text("Conditional")))
365 .children([cx.text("Copyright"), cx.text("ACME Corp")])
366 }
367 }
368
369 fn child_count<C: PixelColor>(tree: &FrameTree<'_, C>, parent: NodeIndex) -> usize {
370 let mut count = 0;
371 let mut child = tree.node(parent).child;
372
373 while let Some(index) = child {
374 count += 1;
375 child = tree.node(index).sibling;
376 }
377
378 count
379 }
380
381 fn nth_child<C: PixelColor>(
382 tree: &FrameTree<'_, C>,
383 parent: NodeIndex,
384 position: usize,
385 ) -> Option<NodeIndex> {
386 let mut child = tree.node(parent).child;
387
388 for _ in 0..position {
389 child = child.and_then(|index| tree.node(index).sibling);
390 }
391
392 child
393 }
394
395 fn text_content<'a, C: PixelColor>(tree: &'a FrameTree<'_, C>, index: NodeIndex) -> &'a str {
396 match &tree.node(index).kind {
397 NodeKind::Text(text) => text.content(tree.storage.text),
398 _ => panic!("expected a text node"),
399 }
400 }
401
402 #[test]
403 fn transparent_root_requires_clear() {
404 let viewport = Size::new(20, 20);
405 let mut storage = FrameStorage::<Rgb565, 1, 1>::default();
406 let cx = Context::new(storage.view());
407 let root = cx.column().size(viewport).try_build().unwrap();
408 let mut tree = FrameTree::new(cx.storage.into_inner());
409
410 tree.layout(root, Constraints::max(viewport));
411
412 assert!(tree.needs_clear(viewport));
413 }
414
415 #[test]
416 fn partial_opaque_root_requires_clear() {
417 let viewport = Size::new(20, 20);
418 let mut storage = FrameStorage::<Rgb565, 1, 1>::default();
419 let cx = Context::new(storage.view());
420 let root =
421 cx.column().background(Rgb565::BLACK).size(Size::new(10, 20)).try_build().unwrap();
422 let mut tree = FrameTree::new(cx.storage.into_inner());
423
424 tree.layout(root, Constraints::max(viewport));
425
426 assert!(tree.needs_clear(viewport));
427 }
428
429 #[test]
430 fn full_viewport_opaque_root_does_not_require_clear() {
431 let viewport = Size::new(20, 20);
432 let mut storage = FrameStorage::<Rgb565, 1, 1>::default();
433 let cx = Context::new(storage.view());
434 let root = cx.column().background(Rgb565::BLACK).size(viewport).try_build().unwrap();
435 let mut tree = FrameTree::new(cx.storage.into_inner());
436
437 tree.layout(root, Constraints::max(viewport));
438
439 assert!(!tree.needs_clear(viewport));
440 }
441
442 #[test]
443 fn row_composes_heterogeneous_and_conditional_children() {
444 let dashboard = Dashboard { text: "Hello, World!" };
445 let mut storage = FrameStorage::<Rgb565, 10, 64>::default();
446 let cx = Context::new(storage.view());
447
448 let root = dashboard.render(&cx).try_build().unwrap();
449 let tree = FrameTree::new(cx.storage.into_inner());
450
451 assert!(matches!(tree.node(root).kind, NodeKind::Div(_)));
452 assert_eq!(child_count(&tree, root), 5);
453
454 let nested = nth_child(&tree, root, 1).unwrap();
455 assert!(matches!(tree.node(nested).kind, NodeKind::Div(_)));
456
457 let conditional = nth_child(&tree, root, 2).unwrap();
458 assert_eq!(text_content(&tree, conditional), "Conditional");
459 }
460
461 #[test]
462 fn column_composes_heterogeneous_and_conditional_children() {
463 let mut storage = FrameStorage::<Rgb565, 10, 64>::default();
464 let cx = Context::new(storage.view());
465
466 let root = cx
467 .column()
468 .child(cx.text("First"))
469 .child(cx.row().child(cx.text("Nested")))
470 .when(true, |column| column.child(cx.text("Conditional")))
471 .children([cx.text("Fourth"), cx.text("Fifth")])
472 .try_build()
473 .unwrap();
474 let tree = FrameTree::new(cx.storage.into_inner());
475
476 assert!(matches!(tree.node(root).kind, NodeKind::Div(_)));
477 assert_eq!(child_count(&tree, root), 5);
478
479 let nested = nth_child(&tree, root, 1).unwrap();
480 assert!(matches!(tree.node(nested).kind, NodeKind::Div(_)));
481
482 let conditional = nth_child(&tree, root, 2).unwrap();
483 assert_eq!(text_content(&tree, conditional), "Conditional");
484 }
485
486 #[test]
487 fn column_stacks_children_vertically_and_uses_the_widest_child() {
488 let mut storage = FrameStorage::<Rgb565, 8, 8>::default();
489 let cx = Context::new(storage.view());
490
491 let root = cx
492 .column()
493 .child(
494 cx.row()
495 .child(cx.text("a").size(Size::new(10, 5)))
496 .child(cx.text("b").size(Size::new(20, 7))),
497 )
498 .child(cx.text("c").size(Size::new(15, 9)))
499 .try_build()
500 .unwrap();
501
502 let mut tree = FrameTree::new(cx.storage.into_inner());
503 tree.layout(root, Constraints::exact(Size::new(100, 100)).loosen());
504
505 assert_eq!(tree.node(root).layout.outer_size, Size::new(30, 16));
506
507 let row = tree.node(root).child.expect("column should have a row child");
508 let last_text = tree.node(row).sibling.expect("column should have a text child");
509 assert_eq!(tree.node(row).layout.offset, Point::zero());
510 assert_eq!(tree.node(last_text).layout.offset, Point::new(0, 7));
511
512 let first_text = tree.node(row).child.expect("row should have a text child");
513 let second_text = tree.node(first_text).sibling.expect("row should have two children");
514 assert_eq!(tree.node(first_text).layout.offset, Point::zero());
515 assert_eq!(tree.node(second_text).layout.offset, Point::new(10, 0));
516 }
517
518 #[test]
519 fn flex_gaps_apply_on_the_main_axis_and_contribute_to_size() {
520 let mut row_storage = FrameStorage::<Rgb565, 3, 1>::default();
521 let row_cx = Context::new(row_storage.view());
522 let row = row_cx
523 .row()
524 .gap(4)
525 .child(row_cx.text("").size(Size::new(10, 5)))
526 .child(row_cx.text("").size(Size::new(20, 7)))
527 .try_build()
528 .unwrap();
529 let mut row_tree = FrameTree::new(row_cx.storage.into_inner());
530 row_tree.layout(row, Constraints::exact(Size::new(100, 100)).loosen());
531
532 let row_first = row_tree.node(row).child.unwrap();
533 let row_second = row_tree.node(row_first).sibling.unwrap();
534 assert_eq!(row_tree.node(row_second).layout.offset, Point::new(14, 0));
535 assert_eq!(row_tree.node(row).layout.outer_size, Size::new(34, 7));
536
537 let mut column_storage = FrameStorage::<Rgb565, 3, 1>::default();
538 let column_cx = Context::new(column_storage.view());
539 let column = column_cx
540 .column()
541 .gap(3)
542 .child(column_cx.text("").size(Size::new(10, 5)))
543 .child(column_cx.text("").size(Size::new(20, 7)))
544 .try_build()
545 .unwrap();
546 let mut column_tree = FrameTree::new(column_cx.storage.into_inner());
547 column_tree.layout(column, Constraints::exact(Size::new(100, 100)).loosen());
548
549 let column_first = column_tree.node(column).child.unwrap();
550 let column_second = column_tree.node(column_first).sibling.unwrap();
551 assert_eq!(column_tree.node(column_second).layout.offset, Point::new(0, 8));
552 assert_eq!(column_tree.node(column).layout.outer_size, Size::new(20, 15));
553 }
554
555 #[test]
556 fn column_draws_its_styled_border_box() {
557 let column = NodeKind::<Rgb565, NoCustomElement>::Div(Style {
558 border: 1.into(),
559 border_color: Some(Rgb565::BLUE),
560 background: Some(Rgb565::RED),
561 specific: DivStyle {
562 gap: Size::zero(),
563 direction: FlexDirection::Column,
564 ..Default::default()
565 },
566 ..Default::default()
567 });
568 let layout = layout::BorderBox {
569 border: Rectangle::new(Point::new(1, 1), Size::new(4, 4)),
570 content: Rectangle::new(Point::new(2, 2), Size::new(2, 2)),
571 };
572 let mut display = MockDisplay::new();
573 display.set_allow_overdraw(true);
574
575 column.draw(&layout, &Theme::default(), &mut display).unwrap();
576
577 assert_eq!(display.get_pixel(Point::new(1, 1)), Some(Rgb565::BLUE));
578 assert_eq!(display.get_pixel(Point::new(2, 2)), Some(Rgb565::RED));
579 assert_eq!(display.get_pixel(Point::zero()), None);
580 }
581
582 #[test]
583 fn asymmetric_insets_drive_size_and_offsets() {
584 let mut storage = FrameStorage::<Rgb565, 1, 1>::default();
585 let cx = Context::new(storage.view());
586
587 let root = cx
588 .text("")
589 .margin(Insets::new(1, 2, 3, 4))
590 .border(Insets::new(1, 2, 3, 4))
591 .padding(Insets::new(5, 6, 7, 8))
592 .size(Size::new(30, 25))
593 .try_build()
594 .unwrap();
595
596 let mut tree = FrameTree::new(cx.storage.into_inner());
597 tree.layout(root, Constraints::exact(Size::new(100, 100)).loosen());
598 let layout = &tree.node(root).layout;
599
600 assert_eq!(layout.border_size, Size::new(30, 25));
601 assert_eq!(layout.outer_size, Size::new(36, 29));
602 assert_eq!(layout.content_size, Size::new(10, 9));
603 assert_eq!(layout.border_offset, Point::new(4, 1));
604 assert_eq!(layout.content_offset, Point::new(16, 7));
605 }
606
607 #[test]
608 fn adjacent_margins_add_in_rows() {
609 let mut storage = FrameStorage::<Rgb565, 3, 1>::default();
610 let cx = Context::new(storage.view());
611
612 let root = cx
613 .row()
614 .child(cx.text("").margin(Insets::new(0, 2, 0, 0)).size(Size::new(10, 10)))
615 .child(cx.text("").margin(Insets::new(0, 0, 0, 3)).size(Size::new(10, 10)))
616 .try_build()
617 .unwrap();
618
619 let mut tree = FrameTree::new(cx.storage.into_inner());
620 tree.layout(root, Constraints::exact(Size::new(100, 100)).loosen());
621
622 let first = tree.node(root).child.expect("row should have children");
623 let second = tree.node(first).sibling.expect("row should have two children");
624
625 assert_eq!(tree.node(first).layout.outer_size, Size::new(12, 10));
626 assert_eq!(tree.node(second).layout.offset, Point::new(12, 0));
627
628 let first_box = tree.node(first).layout.resolve(Point::zero()).border;
629 let second_box = tree.node(second).layout.resolve(Point::zero()).border;
630 assert_eq!(first_box.top_left.x + 10, 10);
631 assert_eq!(second_box.top_left.x, 15);
632 }
633
634 #[test]
635 fn explicit_size_grows_for_insets_but_hard_constraints_win() {
636 let mut loose_storage = FrameStorage::<Rgb565, 1, 1>::default();
637 let loose_cx = Context::new(loose_storage.view());
638 let loose =
639 loose_cx.text("").padding(4).border(2).size(Size::new(5, 5)).try_build().unwrap();
640 let mut loose_tree = FrameTree::new(loose_cx.storage.into_inner());
641 loose_tree.layout(loose, Constraints::exact(Size::new(100, 100)).loosen());
642
643 assert_eq!(loose_tree.node(loose).layout.border_size, Size::new(12, 12));
644 assert_eq!(loose_tree.node(loose).layout.content_size, Size::zero());
645
646 let mut constrained_storage = FrameStorage::<Rgb565, 1, 1>::default();
647 let constrained_cx = Context::new(constrained_storage.view());
648 let constrained =
649 constrained_cx.text("").padding(4).border(2).size(Size::new(5, 5)).try_build().unwrap();
650 let mut constrained_tree = FrameTree::new(constrained_cx.storage.into_inner());
651 constrained_tree.layout(constrained, Constraints::exact(Size::new(8, 8)));
652
653 assert_eq!(constrained_tree.node(constrained).layout.border_size, Size::new(8, 8));
654 assert_eq!(constrained_tree.node(constrained).layout.content_size, Size::zero());
655 }
656
657 #[test]
658 fn width_and_height_leave_the_other_axis_automatic() {
659 let mut width_storage = FrameStorage::<Rgb565, 2, 1>::default();
660 let width_cx = Context::new(width_storage.view());
661 let width_root = width_cx
662 .column()
663 .width(30)
664 .child(width_cx.text("").size(Size::new(10, 7)))
665 .try_build()
666 .unwrap();
667 let mut width_tree = FrameTree::new(width_cx.storage.into_inner());
668 width_tree.layout(width_root, Constraints::max(Size::new(100, 100)));
669
670 assert_eq!(width_tree.node(width_root).layout.border_size, Size::new(30, 7));
671
672 let mut height_storage = FrameStorage::<Rgb565, 2, 1>::default();
673 let height_cx = Context::new(height_storage.view());
674 let height_root = height_cx
675 .row()
676 .height(25)
677 .child(height_cx.text("").size(Size::new(10, 7)))
678 .try_build()
679 .unwrap();
680 let mut height_tree = FrameTree::new(height_cx.storage.into_inner());
681 height_tree.layout(height_root, Constraints::max(Size::new(100, 100)));
682
683 assert_eq!(height_tree.node(height_root).layout.border_size, Size::new(10, 25));
684 }
685
686 #[cfg(feature = "flexbox")]
687 #[test]
688 fn stretch_only_changes_an_automatic_cross_axis() {
689 let mut row_storage = FrameStorage::<Rgb565, 3, 1>::default();
690 let row_cx = Context::new(row_storage.view());
691 let row = row_cx
692 .row()
693 .size(Size::new(100, 40))
694 .child(row_cx.text("").width(10))
695 .child(row_cx.text("").size(Size::new(10, 12)))
696 .try_build()
697 .unwrap();
698 let mut row_tree = FrameTree::new(row_cx.storage.into_inner());
699 row_tree.layout(row, Constraints::max(Size::new(100, 40)));
700
701 let row_auto = row_tree.node(row).child.unwrap();
702 let row_explicit = row_tree.node(row_auto).sibling.unwrap();
703 assert_eq!(row_tree.node(row_auto).layout.border_size, Size::new(10, 40));
704 assert_eq!(row_tree.node(row_explicit).layout.border_size, Size::new(10, 12));
705
706 let mut column_storage = FrameStorage::<Rgb565, 3, 1>::default();
707 let column_cx = Context::new(column_storage.view());
708 let column = column_cx
709 .column()
710 .size(Size::new(40, 100))
711 .child(column_cx.text("").height(10))
712 .child(column_cx.text("").size(Size::new(12, 10)))
713 .try_build()
714 .unwrap();
715 let mut column_tree = FrameTree::new(column_cx.storage.into_inner());
716 column_tree.layout(column, Constraints::max(Size::new(40, 100)));
717
718 let column_auto = column_tree.node(column).child.unwrap();
719 let column_explicit = column_tree.node(column_auto).sibling.unwrap();
720 assert_eq!(column_tree.node(column_auto).layout.border_size, Size::new(40, 10));
721 assert_eq!(column_tree.node(column_explicit).layout.border_size, Size::new(12, 10));
722 }
723
724 #[cfg(feature = "flexbox")]
725 #[test]
726 fn flex_and_flex_grow_use_zero_and_auto_bases() {
727 let mut flex_storage = FrameStorage::<Rgb565, 3, 1>::default();
728 let flex_cx = Context::new(flex_storage.view());
729 let flex = flex_cx
730 .row()
731 .size(Size::new(100, 20))
732 .child(flex_cx.text("").size(Size::new(10, 5)).flex(1))
733 .child(flex_cx.text("").size(Size::new(20, 5)).flex(1))
734 .try_build()
735 .unwrap();
736 let mut flex_tree = FrameTree::new(flex_cx.storage.into_inner());
737 flex_tree.layout(flex, Constraints::max(Size::new(100, 20)));
738
739 let first = flex_tree.node(flex).child.unwrap();
740 let second = flex_tree.node(first).sibling.unwrap();
741 assert_eq!(flex_tree.node(first).layout.border_size.width, 50);
742 assert_eq!(flex_tree.node(second).layout.border_size.width, 50);
743
744 let mut grow_storage = FrameStorage::<Rgb565, 3, 1>::default();
745 let grow_cx = Context::new(grow_storage.view());
746 let grow = grow_cx
747 .row()
748 .size(Size::new(100, 20))
749 .child(grow_cx.text("").size(Size::new(10, 5)).flex_grow(1))
750 .child(grow_cx.text("").size(Size::new(20, 5)).flex_grow(1))
751 .try_build()
752 .unwrap();
753 let mut grow_tree = FrameTree::new(grow_cx.storage.into_inner());
754 grow_tree.layout(grow, Constraints::max(Size::new(100, 20)));
755
756 let first = grow_tree.node(grow).child.unwrap();
757 let second = grow_tree.node(first).sibling.unwrap();
758 assert_eq!(grow_tree.node(first).layout.border_size.width, 45);
759 assert_eq!(grow_tree.node(second).layout.border_size.width, 55);
760 }
761
762 #[cfg(feature = "flexbox")]
763 #[test]
764 fn flex_distributes_remainders_after_gaps_and_margins() {
765 let mut storage = FrameStorage::<Rgb565, 4, 1>::default();
766 let cx = Context::new(storage.view());
767 let root = cx
768 .row()
769 .size(Size::new(101, 20))
770 .gap(5)
771 .child(cx.text("").margin((0, 2)).flex(1))
772 .child(cx.text("").margin((0, 1)).flex(1))
773 .child(cx.text("").flex(1))
774 .try_build()
775 .unwrap();
776 let mut tree = FrameTree::new(cx.storage.into_inner());
777 tree.layout(root, Constraints::max(Size::new(101, 20)));
778
779 let first = tree.node(root).child.unwrap();
780 let second = tree.node(first).sibling.unwrap();
781 let third = tree.node(second).sibling.unwrap();
782
783 assert_eq!(tree.node(first).layout.border_size.width, 28);
785 assert_eq!(tree.node(second).layout.border_size.width, 28);
786 assert_eq!(tree.node(third).layout.border_size.width, 29);
787 assert_eq!(tree.node(second).layout.offset.x, 37);
788 assert_eq!(tree.node(third).layout.offset.x, 72);
789 }
790
791 #[cfg(feature = "flexbox")]
792 #[test]
793 fn flex_grow_and_stretch_are_resolved_together_in_columns() {
794 let mut storage = FrameStorage::<Rgb565, 3, 1>::default();
795 let cx = Context::new(storage.view());
796 let root = cx
797 .column()
798 .size(Size::new(40, 100))
799 .child(cx.text("").height(10).flex(1))
800 .child(cx.text("").size(Size::new(12, 10)).flex(2))
801 .try_build()
802 .unwrap();
803 let mut tree = FrameTree::new(cx.storage.into_inner());
804 tree.layout(root, Constraints::max(Size::new(40, 100)));
805
806 let first = tree.node(root).child.unwrap();
807 let second = tree.node(first).sibling.unwrap();
808 assert_eq!(tree.node(first).layout.border_size, Size::new(40, 33));
809 assert_eq!(tree.node(second).layout.border_size, Size::new(12, 67));
810 assert_eq!(tree.node(second).layout.offset, Point::new(0, 33));
811 }
812
813 #[test]
814 fn asymmetric_borders_are_painted_inside_the_border_box() {
815 let column = NodeKind::<Rgb565, NoCustomElement>::Div(Style {
816 border: Insets::new(1, 2, 3, 4),
817 border_color: Some(Rgb565::BLUE),
818 background: Some(Rgb565::RED),
819 specific: DivStyle { direction: FlexDirection::Column, ..Default::default() },
820 ..Style::default()
821 });
822 let layout = layout::BorderBox {
823 border: Rectangle::new(Point::new(1, 1), Size::new(7, 7)),
824 content: Rectangle::new(Point::new(5, 2), Size::new(1, 3)),
825 };
826 let mut display = MockDisplay::new();
827 display.set_allow_overdraw(true);
828
829 column.draw(&layout, &Theme::default(), &mut display).unwrap();
830
831 assert_eq!(display.get_pixel(Point::new(5, 1)), Some(Rgb565::BLUE));
832 assert_eq!(display.get_pixel(Point::new(6, 3)), Some(Rgb565::BLUE));
833 assert_eq!(display.get_pixel(Point::new(5, 5)), Some(Rgb565::BLUE));
834 assert_eq!(display.get_pixel(Point::new(4, 3)), Some(Rgb565::BLUE));
835 assert_eq!(display.get_pixel(Point::new(5, 3)), Some(Rgb565::RED));
836 assert_eq!(display.get_pixel(Point::zero()), None);
837 assert_eq!(display.get_pixel(Point::new(8, 3)), None);
838 }
839
840 #[test]
841 fn box_painting_supports_binary_color() {
842 let column = NodeKind::<BinaryColor, NoCustomElement>::Div(Style {
843 border: (1, 2, 1, 2).into(),
844 border_color: Some(BinaryColor::On),
845 background: Some(BinaryColor::Off),
846 specific: DivStyle { direction: FlexDirection::Column, ..Default::default() },
847 ..Style::default()
848 });
849 let layout = layout::BorderBox {
850 border: Rectangle::new(Point::zero(), Size::new(6, 4)),
851 content: Rectangle::new(Point::new(2, 1), Size::new(2, 2)),
852 };
853 let mut display = MockDisplay::new();
854 display.set_allow_overdraw(true);
855
856 column.draw(&layout, &Theme::default(), &mut display).unwrap();
857
858 assert_eq!(display.get_pixel(Point::zero()), Some(BinaryColor::On));
859 assert_eq!(display.get_pixel(Point::new(3, 2)), Some(BinaryColor::Off));
860 }
861}