Skip to main content

guillotine/
lib.rs

1//! # Guillotine
2#![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
33/// Fixed-capacity storage for the nodes and text needed to render one frame.
34///
35/// `N` is the maximum node count, `T` is the maximum number of UTF-8 text bytes, and `CE` is the
36/// custom element type stored inline in each custom node.
37pub 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    /// UTF-8 text content is kept outside [`Node`] to reduce memory usage. Since [`Node`] is a
43    /// fixed-size struct, storing a `T`-byte text buffer inside it would reserve that space for
44    /// every node, including nodes that don't contain text.
45    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/// Tracks the usage of a [`FrameStorage`] buffer.
58#[derive(Debug, Clone, Copy)]
59#[cfg_attr(feature = "defmt", derive(defmt::Format))]
60pub struct FrameUsage {
61    /// The number of used nodes.
62    pub nodes: usize,
63    /// The number of bytes used for text content.
64    pub text: usize,
65}
66
67/// Tracks the capacity of a [`FrameStorage`] buffer.
68#[derive(Debug, Clone, Copy)]
69#[cfg_attr(feature = "defmt", derive(defmt::Format))]
70pub struct FrameCapacity {
71    /// The maximum number of nodes.
72    pub nodes: usize,
73    /// The maximum number of bytes for text content.
74    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    /// Returns a mutable view into this storage buffer.
82    pub const fn view(&mut self) -> StorageView<'_, C, CE> {
83        StorageView { nodes: &mut self.nodes, text: &mut self.text }
84    }
85
86    /// Clears all nodes and text from this storage buffer.
87    pub fn clear(&mut self) {
88        self.nodes.clear();
89        self.text.clear();
90    }
91
92    /// Returns the size of this storage buffer in bytes.
93    pub const fn size(&self) -> usize {
94        core::mem::size_of::<Self>()
95    }
96
97    /// Returns the number of nodes and text bytes used by the most recent frame.
98    pub fn usage(&self) -> FrameUsage {
99        FrameUsage { nodes: self.nodes.len(), text: self.text.len() }
100    }
101
102    /// Returns the capacity of this storage buffer.
103    pub const fn capacity(&self) -> FrameCapacity {
104        FrameCapacity { nodes: N, text: T }
105    }
106}
107
108/// A capacity-erased mutable view into a [`FrameStorage`] buffer.
109pub 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
117/// The [`Ui`] struct is the main entrypoint for the Guillotine UI framework.
118/// It manages the display and renders the tree produced by [`Render`] with [`Self::render`].
119pub 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/// An error encountered while building or drawing a frame.
129#[derive(Debug, thiserror::Error)]
130#[cfg_attr(feature = "defmt", derive(defmt::Format))]
131pub enum RenderError<E> {
132    /// Building the frame exceeded one of its fixed-capacity arenas.
133    #[error(transparent)]
134    Build(#[from] BuildError),
135    /// The display returned an error while clearing or drawing the frame.
136    #[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    /// Creates a new [`Ui`] instance with an explicit theme.
145    ///
146    /// This constructor supports arbitrary custom [`PixelColor`] implementations. For the
147    /// standard embedded-graphics color types, [`Ui::new`] supplies a black and white theme.
148    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    /// Returns a new [`Ui`] instance with the given background color. This background color will
157    /// be used to clear dirty regions before rendering.
158    pub const fn with_background(mut self, background: D::Color) -> Self {
159        self.theme.background = background;
160        self
161    }
162
163    /// Returns a new [`Ui`] instance with the given default text color.
164    pub const fn with_foreground(mut self, foreground: D::Color) -> Self {
165        self.theme.foreground = foreground;
166        self
167    }
168
169    /// Renders the given `view` onto the display.
170    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        // Create the viewport constraints.
183        let viewport = Constraints::max(self.display.size());
184
185        // Build and resolve the frame tree.
186        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    /// Returns a reference to the display.
195    pub const fn display(&self) -> &D {
196        &self.display
197    }
198
199    /// Returns a mutable reference to the display.
200    pub const fn display_mut(&mut self) -> &mut D {
201        &mut self.display
202    }
203
204    /// Returns the display and consumes the [`Ui`] instance.
205    pub fn into_display(self) -> D {
206        self.display
207    }
208
209    /// Returns the UI theme.
210    pub const fn theme(&self) -> &Theme<D::Color> {
211        &self.theme
212    }
213
214    /// Returns a reference to the frame storage.
215    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    /// Creates a new [`Ui`] instance with a black background and white foreground.
226    pub fn new(display: D, storage: FrameStorage<D::Color, N, T, CE>) -> Self {
227        Self::with_theme(display, storage, Theme::default())
228    }
229}
230
231/// A helper trait for building complex objects with imperative conditionals in a fluent style.
232pub trait FluentBuilder {
233    /// Imperatively modify self with the given closure.
234    fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
235    where
236        Self: Sized,
237    {
238        f(self)
239    }
240
241    /// Conditionally modify self with the given closure.
242    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    /// Conditionally modify self with the given closure.
250    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    /// Conditionally unwrap and modify self with the given closure, if the given option is Some.
263    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    /// Conditionally unwrap and modify self with the given closure, if the given option is None.
270    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
278// Make fluent conditionals available on every element builder.
279impl<T: ElementBuilder> FluentBuilder for T {}
280
281/// Converts a view into an element tree for the current frame.
282///
283/// `C` defaults to [`Rgb565`] to preserve the simple API for existing views. A view for another
284/// display color declares that color once in its implementation, for example
285/// `impl Render<BinaryColor> for MyView`. Element constructors inside `render` infer the color from
286/// its return type and don't need explicit generic arguments.
287pub trait Render<C = Rgb565, CE = NoCustomElement>
288where
289    C: PixelColor,
290{
291    /// Builds this view's root element using the given [`Context`].
292    fn render(&self, cx: &Context<'_, C, CE>) -> impl ElementBuilder;
293}
294
295/// Per-frame context used to create element builders backed by fixed-capacity storage.
296pub 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    /// Creates a new [`Context`] with the given storage view.
302    const fn new(storage: StorageView<'frame, C, CE>) -> Self {
303        Self { storage: core::cell::RefCell::new(storage) }
304    }
305
306    /// Links two sibling nodes by setting `node`'s next sibling to `sibling`.
307    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    /// Inserts a node into storage and returns its index.
314    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    /// Stores the given text content in the storage, returning a [`TextRange`] that can be used to
323    /// retrieve the text later.
324    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        // Store the content as UTF-8 bytes.
336        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        // 101 - 10 gap pixels - 6 margin pixels = 85 flexible pixels.
784        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}