#![no_std]
#![doc = include_str!("../README.md")]
#[cfg(feature = "framebuffer")]
pub mod buffered;
mod common;
mod draw;
mod element;
mod layout;
pub mod style;
mod target;
mod theme;
mod tree;
use embedded_graphics::{pixelcolor::Rgb565, prelude::PixelColor};
pub use element::{
BuildError, CustomBuilder, CustomElement, DivStyle, ElementBuilder, Font, NoCustomElement,
ParentElement, StyledFlexContainer, TextStyle, TextStyledElement,
};
use heapless::VecView;
pub use style::{Insets, Style, StyledElement};
pub use target::{DirectTarget, DisplayTarget};
pub use theme::Theme;
use crate::{
common::{NodeIndex, TextRange},
layout::Constraints,
tree::{FrameTree, Node},
};
pub struct FrameStorage<C, const N: usize = 64, const T: usize = 1024, CE = NoCustomElement>
where
C: PixelColor,
{
nodes: heapless::Vec<Node<C, CE>, N>,
text: heapless::Vec<u8, T>,
}
impl<C, const N: usize, const T: usize, CE> Default for FrameStorage<C, N, T, CE>
where
C: PixelColor,
{
fn default() -> Self {
Self { nodes: heapless::Vec::new(), text: heapless::Vec::new() }
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct FrameUsage {
pub nodes: usize,
pub text: usize,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct FrameCapacity {
pub nodes: usize,
pub text: usize,
}
impl<C, const N: usize, const T: usize, CE> FrameStorage<C, N, T, CE>
where
C: PixelColor,
{
pub const fn view(&mut self) -> StorageView<'_, C, CE> {
StorageView { nodes: &mut self.nodes, text: &mut self.text }
}
pub fn clear(&mut self) {
self.nodes.clear();
self.text.clear();
}
pub const fn size(&self) -> usize {
core::mem::size_of::<Self>()
}
pub fn usage(&self) -> FrameUsage {
FrameUsage { nodes: self.nodes.len(), text: self.text.len() }
}
pub const fn capacity(&self) -> FrameCapacity {
FrameCapacity { nodes: N, text: T }
}
}
pub struct StorageView<'frame, C, CE>
where
C: PixelColor,
{
nodes: &'frame mut VecView<Node<C, CE>>,
text: &'frame mut VecView<u8>,
}
pub struct Ui<D, const N: usize = 64, const T: usize = 1024, CE = NoCustomElement>
where
D: DisplayTarget,
{
display: D,
storage: FrameStorage<D::Color, N, T, CE>,
theme: Theme<D::Color>,
}
#[derive(Debug, thiserror::Error)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum RenderError<E> {
#[error(transparent)]
Build(#[from] BuildError),
#[error("display drawing failed")]
Draw(E),
}
impl<D, const N: usize, const T: usize, CE> Ui<D, N, T, CE>
where
D: DisplayTarget,
{
pub const fn with_theme(
display: D,
storage: FrameStorage<D::Color, N, T, CE>,
theme: Theme<D::Color>,
) -> Self {
Self { display, storage, theme }
}
pub const fn with_background(mut self, background: D::Color) -> Self {
self.theme.background = background;
self
}
pub const fn with_foreground(mut self, foreground: D::Color) -> Self {
self.theme.foreground = foreground;
self
}
pub fn render<V>(&mut self, view: &V) -> Result<(), RenderError<D::Error>>
where
CE: CustomElement<D::Color>,
V: Render<D::Color, CE>,
{
self.storage.clear();
let frame = self.storage.view();
let cx = Context::new(frame);
let root = view.render(&cx).try_build()?;
let viewport = Constraints::max(self.display.size());
let mut tree = FrameTree::new(cx.storage.into_inner());
tree.layout(root, viewport);
tree.draw(&self.theme, &mut self.display).map_err(RenderError::Draw)?;
Ok(())
}
pub const fn display(&self) -> &D {
&self.display
}
pub const fn display_mut(&mut self) -> &mut D {
&mut self.display
}
pub fn into_display(self) -> D {
self.display
}
pub const fn theme(&self) -> &Theme<D::Color> {
&self.theme
}
pub const fn storage(&self) -> &FrameStorage<D::Color, N, T, CE> {
&self.storage
}
}
impl<D, const N: usize, const T: usize, CE> Ui<D, N, T, CE>
where
D: DisplayTarget,
Theme<D::Color>: Default,
{
pub fn new(display: D, storage: FrameStorage<D::Color, N, T, CE>) -> Self {
Self::with_theme(display, storage, Theme::default())
}
}
pub trait FluentBuilder {
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
where
Self: Sized,
{
f(self)
}
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| if condition { then(this) } else { this })
}
fn when_else(
self,
condition: bool,
then: impl FnOnce(Self) -> Self,
else_fn: impl FnOnce(Self) -> Self,
) -> Self
where
Self: Sized,
{
self.map(|this| if condition { then(this) } else { else_fn(this) })
}
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| if let Some(value) = option { then(this, value) } else { this })
}
fn when_none<T>(self, option: &Option<T>, then: impl FnOnce(Self) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| if option.is_some() { this } else { then(this) })
}
}
impl<T: ElementBuilder> FluentBuilder for T {}
pub trait Render<C = Rgb565, CE = NoCustomElement>
where
C: PixelColor,
{
fn render(&self, cx: &Context<'_, C, CE>) -> impl ElementBuilder;
}
pub struct Context<'frame, C: PixelColor = Rgb565, CE = NoCustomElement> {
storage: core::cell::RefCell<StorageView<'frame, C, CE>>,
}
impl<'frame, C: PixelColor, CE> Context<'frame, C, CE> {
const fn new(storage: StorageView<'frame, C, CE>) -> Self {
Self { storage: core::cell::RefCell::new(storage) }
}
fn link_sibling(&self, node: NodeIndex, sibling: NodeIndex) {
let mut storage = self.storage.borrow_mut();
storage.nodes[node].set_sibling(sibling);
}
fn insert(&self, node: Node<C, CE>) -> Result<NodeIndex, BuildError> {
let mut storage = self.storage.borrow_mut();
let index = storage.nodes.len();
storage.nodes.push(node).map_err(|_| BuildError::NodeCapacity)?;
Ok(index)
}
fn store_text(&self, content: &str) -> Result<TextRange, BuildError> {
let mut storage = self.storage.borrow_mut();
let offset = storage.text.len();
let len = content.len();
let end = offset.checked_add(len).ok_or(BuildError::TextCapacity)?;
if end > storage.text.capacity() {
return Err(BuildError::TextCapacity);
}
storage.text.extend_from_slice(content.as_bytes()).map_err(|_| BuildError::TextCapacity)?;
Ok(TextRange { offset, len })
}
}
#[cfg(test)]
mod tests {
use embedded_graphics::{
mock_display::MockDisplay,
pixelcolor::{BinaryColor, Rgb565},
prelude::{Point, RgbColor as _, Size},
primitives::Rectangle,
};
use crate::{element::DivStyle, style::FlexDirection, tree::NodeKind};
use super::*;
struct Dashboard {
text: &'static str,
}
impl Render for Dashboard {
fn render(&self, cx: &Context<'_>) -> impl ElementBuilder {
cx.row()
.child(cx.text(self.text))
.child(cx.row().child(cx.text("Nested")))
.when(true, |row| row.child(cx.text("Conditional")))
.children([cx.text("Copyright"), cx.text("ACME Corp")])
}
}
fn child_count<C: PixelColor>(tree: &FrameTree<'_, C>, parent: NodeIndex) -> usize {
let mut count = 0;
let mut child = tree.node(parent).child;
while let Some(index) = child {
count += 1;
child = tree.node(index).sibling;
}
count
}
fn nth_child<C: PixelColor>(
tree: &FrameTree<'_, C>,
parent: NodeIndex,
position: usize,
) -> Option<NodeIndex> {
let mut child = tree.node(parent).child;
for _ in 0..position {
child = child.and_then(|index| tree.node(index).sibling);
}
child
}
fn text_content<'a, C: PixelColor>(tree: &'a FrameTree<'_, C>, index: NodeIndex) -> &'a str {
match &tree.node(index).kind {
NodeKind::Text(text) => text.content(tree.storage.text),
_ => panic!("expected a text node"),
}
}
#[test]
fn transparent_root_requires_clear() {
let viewport = Size::new(20, 20);
let mut storage = FrameStorage::<Rgb565, 1, 1>::default();
let cx = Context::new(storage.view());
let root = cx.column().size(viewport).try_build().unwrap();
let mut tree = FrameTree::new(cx.storage.into_inner());
tree.layout(root, Constraints::max(viewport));
assert!(tree.needs_clear(viewport));
}
#[test]
fn partial_opaque_root_requires_clear() {
let viewport = Size::new(20, 20);
let mut storage = FrameStorage::<Rgb565, 1, 1>::default();
let cx = Context::new(storage.view());
let root =
cx.column().background(Rgb565::BLACK).size(Size::new(10, 20)).try_build().unwrap();
let mut tree = FrameTree::new(cx.storage.into_inner());
tree.layout(root, Constraints::max(viewport));
assert!(tree.needs_clear(viewport));
}
#[test]
fn full_viewport_opaque_root_does_not_require_clear() {
let viewport = Size::new(20, 20);
let mut storage = FrameStorage::<Rgb565, 1, 1>::default();
let cx = Context::new(storage.view());
let root = cx.column().background(Rgb565::BLACK).size(viewport).try_build().unwrap();
let mut tree = FrameTree::new(cx.storage.into_inner());
tree.layout(root, Constraints::max(viewport));
assert!(!tree.needs_clear(viewport));
}
#[test]
fn row_composes_heterogeneous_and_conditional_children() {
let dashboard = Dashboard { text: "Hello, World!" };
let mut storage = FrameStorage::<Rgb565, 10, 64>::default();
let cx = Context::new(storage.view());
let root = dashboard.render(&cx).try_build().unwrap();
let tree = FrameTree::new(cx.storage.into_inner());
assert!(matches!(tree.node(root).kind, NodeKind::Div(_)));
assert_eq!(child_count(&tree, root), 5);
let nested = nth_child(&tree, root, 1).unwrap();
assert!(matches!(tree.node(nested).kind, NodeKind::Div(_)));
let conditional = nth_child(&tree, root, 2).unwrap();
assert_eq!(text_content(&tree, conditional), "Conditional");
}
#[test]
fn column_composes_heterogeneous_and_conditional_children() {
let mut storage = FrameStorage::<Rgb565, 10, 64>::default();
let cx = Context::new(storage.view());
let root = cx
.column()
.child(cx.text("First"))
.child(cx.row().child(cx.text("Nested")))
.when(true, |column| column.child(cx.text("Conditional")))
.children([cx.text("Fourth"), cx.text("Fifth")])
.try_build()
.unwrap();
let tree = FrameTree::new(cx.storage.into_inner());
assert!(matches!(tree.node(root).kind, NodeKind::Div(_)));
assert_eq!(child_count(&tree, root), 5);
let nested = nth_child(&tree, root, 1).unwrap();
assert!(matches!(tree.node(nested).kind, NodeKind::Div(_)));
let conditional = nth_child(&tree, root, 2).unwrap();
assert_eq!(text_content(&tree, conditional), "Conditional");
}
#[test]
fn column_stacks_children_vertically_and_uses_the_widest_child() {
let mut storage = FrameStorage::<Rgb565, 8, 8>::default();
let cx = Context::new(storage.view());
let root = cx
.column()
.child(
cx.row()
.child(cx.text("a").size(Size::new(10, 5)))
.child(cx.text("b").size(Size::new(20, 7))),
)
.child(cx.text("c").size(Size::new(15, 9)))
.try_build()
.unwrap();
let mut tree = FrameTree::new(cx.storage.into_inner());
tree.layout(root, Constraints::exact(Size::new(100, 100)).loosen());
assert_eq!(tree.node(root).layout.outer_size, Size::new(30, 16));
let row = tree.node(root).child.expect("column should have a row child");
let last_text = tree.node(row).sibling.expect("column should have a text child");
assert_eq!(tree.node(row).layout.offset, Point::zero());
assert_eq!(tree.node(last_text).layout.offset, Point::new(0, 7));
let first_text = tree.node(row).child.expect("row should have a text child");
let second_text = tree.node(first_text).sibling.expect("row should have two children");
assert_eq!(tree.node(first_text).layout.offset, Point::zero());
assert_eq!(tree.node(second_text).layout.offset, Point::new(10, 0));
}
#[test]
fn flex_gaps_apply_on_the_main_axis_and_contribute_to_size() {
let mut row_storage = FrameStorage::<Rgb565, 3, 1>::default();
let row_cx = Context::new(row_storage.view());
let row = row_cx
.row()
.gap(4)
.child(row_cx.text("").size(Size::new(10, 5)))
.child(row_cx.text("").size(Size::new(20, 7)))
.try_build()
.unwrap();
let mut row_tree = FrameTree::new(row_cx.storage.into_inner());
row_tree.layout(row, Constraints::exact(Size::new(100, 100)).loosen());
let row_first = row_tree.node(row).child.unwrap();
let row_second = row_tree.node(row_first).sibling.unwrap();
assert_eq!(row_tree.node(row_second).layout.offset, Point::new(14, 0));
assert_eq!(row_tree.node(row).layout.outer_size, Size::new(34, 7));
let mut column_storage = FrameStorage::<Rgb565, 3, 1>::default();
let column_cx = Context::new(column_storage.view());
let column = column_cx
.column()
.gap(3)
.child(column_cx.text("").size(Size::new(10, 5)))
.child(column_cx.text("").size(Size::new(20, 7)))
.try_build()
.unwrap();
let mut column_tree = FrameTree::new(column_cx.storage.into_inner());
column_tree.layout(column, Constraints::exact(Size::new(100, 100)).loosen());
let column_first = column_tree.node(column).child.unwrap();
let column_second = column_tree.node(column_first).sibling.unwrap();
assert_eq!(column_tree.node(column_second).layout.offset, Point::new(0, 8));
assert_eq!(column_tree.node(column).layout.outer_size, Size::new(20, 15));
}
#[test]
fn column_draws_its_styled_border_box() {
let column = NodeKind::<Rgb565, NoCustomElement>::Div(Style {
border: 1.into(),
border_color: Some(Rgb565::BLUE),
background: Some(Rgb565::RED),
specific: DivStyle {
gap: Size::zero(),
direction: FlexDirection::Column,
..Default::default()
},
..Default::default()
});
let layout = layout::BorderBox {
border: Rectangle::new(Point::new(1, 1), Size::new(4, 4)),
content: Rectangle::new(Point::new(2, 2), Size::new(2, 2)),
};
let mut display = MockDisplay::new();
display.set_allow_overdraw(true);
column.draw(&layout, &Theme::default(), &mut display).unwrap();
assert_eq!(display.get_pixel(Point::new(1, 1)), Some(Rgb565::BLUE));
assert_eq!(display.get_pixel(Point::new(2, 2)), Some(Rgb565::RED));
assert_eq!(display.get_pixel(Point::zero()), None);
}
#[test]
fn asymmetric_insets_drive_size_and_offsets() {
let mut storage = FrameStorage::<Rgb565, 1, 1>::default();
let cx = Context::new(storage.view());
let root = cx
.text("")
.margin(Insets::new(1, 2, 3, 4))
.border(Insets::new(1, 2, 3, 4))
.padding(Insets::new(5, 6, 7, 8))
.size(Size::new(30, 25))
.try_build()
.unwrap();
let mut tree = FrameTree::new(cx.storage.into_inner());
tree.layout(root, Constraints::exact(Size::new(100, 100)).loosen());
let layout = &tree.node(root).layout;
assert_eq!(layout.border_size, Size::new(30, 25));
assert_eq!(layout.outer_size, Size::new(36, 29));
assert_eq!(layout.content_size, Size::new(10, 9));
assert_eq!(layout.border_offset, Point::new(4, 1));
assert_eq!(layout.content_offset, Point::new(16, 7));
}
#[test]
fn adjacent_margins_add_in_rows() {
let mut storage = FrameStorage::<Rgb565, 3, 1>::default();
let cx = Context::new(storage.view());
let root = cx
.row()
.child(cx.text("").margin(Insets::new(0, 2, 0, 0)).size(Size::new(10, 10)))
.child(cx.text("").margin(Insets::new(0, 0, 0, 3)).size(Size::new(10, 10)))
.try_build()
.unwrap();
let mut tree = FrameTree::new(cx.storage.into_inner());
tree.layout(root, Constraints::exact(Size::new(100, 100)).loosen());
let first = tree.node(root).child.expect("row should have children");
let second = tree.node(first).sibling.expect("row should have two children");
assert_eq!(tree.node(first).layout.outer_size, Size::new(12, 10));
assert_eq!(tree.node(second).layout.offset, Point::new(12, 0));
let first_box = tree.node(first).layout.resolve(Point::zero()).border;
let second_box = tree.node(second).layout.resolve(Point::zero()).border;
assert_eq!(first_box.top_left.x + 10, 10);
assert_eq!(second_box.top_left.x, 15);
}
#[test]
fn explicit_size_grows_for_insets_but_hard_constraints_win() {
let mut loose_storage = FrameStorage::<Rgb565, 1, 1>::default();
let loose_cx = Context::new(loose_storage.view());
let loose =
loose_cx.text("").padding(4).border(2).size(Size::new(5, 5)).try_build().unwrap();
let mut loose_tree = FrameTree::new(loose_cx.storage.into_inner());
loose_tree.layout(loose, Constraints::exact(Size::new(100, 100)).loosen());
assert_eq!(loose_tree.node(loose).layout.border_size, Size::new(12, 12));
assert_eq!(loose_tree.node(loose).layout.content_size, Size::zero());
let mut constrained_storage = FrameStorage::<Rgb565, 1, 1>::default();
let constrained_cx = Context::new(constrained_storage.view());
let constrained =
constrained_cx.text("").padding(4).border(2).size(Size::new(5, 5)).try_build().unwrap();
let mut constrained_tree = FrameTree::new(constrained_cx.storage.into_inner());
constrained_tree.layout(constrained, Constraints::exact(Size::new(8, 8)));
assert_eq!(constrained_tree.node(constrained).layout.border_size, Size::new(8, 8));
assert_eq!(constrained_tree.node(constrained).layout.content_size, Size::zero());
}
#[test]
fn width_and_height_leave_the_other_axis_automatic() {
let mut width_storage = FrameStorage::<Rgb565, 2, 1>::default();
let width_cx = Context::new(width_storage.view());
let width_root = width_cx
.column()
.width(30)
.child(width_cx.text("").size(Size::new(10, 7)))
.try_build()
.unwrap();
let mut width_tree = FrameTree::new(width_cx.storage.into_inner());
width_tree.layout(width_root, Constraints::max(Size::new(100, 100)));
assert_eq!(width_tree.node(width_root).layout.border_size, Size::new(30, 7));
let mut height_storage = FrameStorage::<Rgb565, 2, 1>::default();
let height_cx = Context::new(height_storage.view());
let height_root = height_cx
.row()
.height(25)
.child(height_cx.text("").size(Size::new(10, 7)))
.try_build()
.unwrap();
let mut height_tree = FrameTree::new(height_cx.storage.into_inner());
height_tree.layout(height_root, Constraints::max(Size::new(100, 100)));
assert_eq!(height_tree.node(height_root).layout.border_size, Size::new(10, 25));
}
#[cfg(feature = "flexbox")]
#[test]
fn stretch_only_changes_an_automatic_cross_axis() {
let mut row_storage = FrameStorage::<Rgb565, 3, 1>::default();
let row_cx = Context::new(row_storage.view());
let row = row_cx
.row()
.size(Size::new(100, 40))
.child(row_cx.text("").width(10))
.child(row_cx.text("").size(Size::new(10, 12)))
.try_build()
.unwrap();
let mut row_tree = FrameTree::new(row_cx.storage.into_inner());
row_tree.layout(row, Constraints::max(Size::new(100, 40)));
let row_auto = row_tree.node(row).child.unwrap();
let row_explicit = row_tree.node(row_auto).sibling.unwrap();
assert_eq!(row_tree.node(row_auto).layout.border_size, Size::new(10, 40));
assert_eq!(row_tree.node(row_explicit).layout.border_size, Size::new(10, 12));
let mut column_storage = FrameStorage::<Rgb565, 3, 1>::default();
let column_cx = Context::new(column_storage.view());
let column = column_cx
.column()
.size(Size::new(40, 100))
.child(column_cx.text("").height(10))
.child(column_cx.text("").size(Size::new(12, 10)))
.try_build()
.unwrap();
let mut column_tree = FrameTree::new(column_cx.storage.into_inner());
column_tree.layout(column, Constraints::max(Size::new(40, 100)));
let column_auto = column_tree.node(column).child.unwrap();
let column_explicit = column_tree.node(column_auto).sibling.unwrap();
assert_eq!(column_tree.node(column_auto).layout.border_size, Size::new(40, 10));
assert_eq!(column_tree.node(column_explicit).layout.border_size, Size::new(12, 10));
}
#[cfg(feature = "flexbox")]
#[test]
fn flex_and_flex_grow_use_zero_and_auto_bases() {
let mut flex_storage = FrameStorage::<Rgb565, 3, 1>::default();
let flex_cx = Context::new(flex_storage.view());
let flex = flex_cx
.row()
.size(Size::new(100, 20))
.child(flex_cx.text("").size(Size::new(10, 5)).flex(1))
.child(flex_cx.text("").size(Size::new(20, 5)).flex(1))
.try_build()
.unwrap();
let mut flex_tree = FrameTree::new(flex_cx.storage.into_inner());
flex_tree.layout(flex, Constraints::max(Size::new(100, 20)));
let first = flex_tree.node(flex).child.unwrap();
let second = flex_tree.node(first).sibling.unwrap();
assert_eq!(flex_tree.node(first).layout.border_size.width, 50);
assert_eq!(flex_tree.node(second).layout.border_size.width, 50);
let mut grow_storage = FrameStorage::<Rgb565, 3, 1>::default();
let grow_cx = Context::new(grow_storage.view());
let grow = grow_cx
.row()
.size(Size::new(100, 20))
.child(grow_cx.text("").size(Size::new(10, 5)).flex_grow(1))
.child(grow_cx.text("").size(Size::new(20, 5)).flex_grow(1))
.try_build()
.unwrap();
let mut grow_tree = FrameTree::new(grow_cx.storage.into_inner());
grow_tree.layout(grow, Constraints::max(Size::new(100, 20)));
let first = grow_tree.node(grow).child.unwrap();
let second = grow_tree.node(first).sibling.unwrap();
assert_eq!(grow_tree.node(first).layout.border_size.width, 45);
assert_eq!(grow_tree.node(second).layout.border_size.width, 55);
}
#[cfg(feature = "flexbox")]
#[test]
fn flex_distributes_remainders_after_gaps_and_margins() {
let mut storage = FrameStorage::<Rgb565, 4, 1>::default();
let cx = Context::new(storage.view());
let root = cx
.row()
.size(Size::new(101, 20))
.gap(5)
.child(cx.text("").margin((0, 2)).flex(1))
.child(cx.text("").margin((0, 1)).flex(1))
.child(cx.text("").flex(1))
.try_build()
.unwrap();
let mut tree = FrameTree::new(cx.storage.into_inner());
tree.layout(root, Constraints::max(Size::new(101, 20)));
let first = tree.node(root).child.unwrap();
let second = tree.node(first).sibling.unwrap();
let third = tree.node(second).sibling.unwrap();
assert_eq!(tree.node(first).layout.border_size.width, 28);
assert_eq!(tree.node(second).layout.border_size.width, 28);
assert_eq!(tree.node(third).layout.border_size.width, 29);
assert_eq!(tree.node(second).layout.offset.x, 37);
assert_eq!(tree.node(third).layout.offset.x, 72);
}
#[cfg(feature = "flexbox")]
#[test]
fn flex_grow_and_stretch_are_resolved_together_in_columns() {
let mut storage = FrameStorage::<Rgb565, 3, 1>::default();
let cx = Context::new(storage.view());
let root = cx
.column()
.size(Size::new(40, 100))
.child(cx.text("").height(10).flex(1))
.child(cx.text("").size(Size::new(12, 10)).flex(2))
.try_build()
.unwrap();
let mut tree = FrameTree::new(cx.storage.into_inner());
tree.layout(root, Constraints::max(Size::new(40, 100)));
let first = tree.node(root).child.unwrap();
let second = tree.node(first).sibling.unwrap();
assert_eq!(tree.node(first).layout.border_size, Size::new(40, 33));
assert_eq!(tree.node(second).layout.border_size, Size::new(12, 67));
assert_eq!(tree.node(second).layout.offset, Point::new(0, 33));
}
#[test]
fn asymmetric_borders_are_painted_inside_the_border_box() {
let column = NodeKind::<Rgb565, NoCustomElement>::Div(Style {
border: Insets::new(1, 2, 3, 4),
border_color: Some(Rgb565::BLUE),
background: Some(Rgb565::RED),
specific: DivStyle { direction: FlexDirection::Column, ..Default::default() },
..Style::default()
});
let layout = layout::BorderBox {
border: Rectangle::new(Point::new(1, 1), Size::new(7, 7)),
content: Rectangle::new(Point::new(5, 2), Size::new(1, 3)),
};
let mut display = MockDisplay::new();
display.set_allow_overdraw(true);
column.draw(&layout, &Theme::default(), &mut display).unwrap();
assert_eq!(display.get_pixel(Point::new(5, 1)), Some(Rgb565::BLUE));
assert_eq!(display.get_pixel(Point::new(6, 3)), Some(Rgb565::BLUE));
assert_eq!(display.get_pixel(Point::new(5, 5)), Some(Rgb565::BLUE));
assert_eq!(display.get_pixel(Point::new(4, 3)), Some(Rgb565::BLUE));
assert_eq!(display.get_pixel(Point::new(5, 3)), Some(Rgb565::RED));
assert_eq!(display.get_pixel(Point::zero()), None);
assert_eq!(display.get_pixel(Point::new(8, 3)), None);
}
#[test]
fn box_painting_supports_binary_color() {
let column = NodeKind::<BinaryColor, NoCustomElement>::Div(Style {
border: (1, 2, 1, 2).into(),
border_color: Some(BinaryColor::On),
background: Some(BinaryColor::Off),
specific: DivStyle { direction: FlexDirection::Column, ..Default::default() },
..Style::default()
});
let layout = layout::BorderBox {
border: Rectangle::new(Point::zero(), Size::new(6, 4)),
content: Rectangle::new(Point::new(2, 1), Size::new(2, 2)),
};
let mut display = MockDisplay::new();
display.set_allow_overdraw(true);
column.draw(&layout, &Theme::default(), &mut display).unwrap();
assert_eq!(display.get_pixel(Point::zero()), Some(BinaryColor::On));
assert_eq!(display.get_pixel(Point::new(3, 2)), Some(BinaryColor::Off));
}
}