mirui 0.46.2

A lightweight, no_std ECS-driven UI framework for embedded, mobile, desktop, and WebAssembly
Documentation
use crate::gallery::demos::life::{self, LifeBoard};
use crate::prelude::*;
use crate::render::font::FontToken;
use crate::ui::widgets::{ParagraphStyle, Text, TextAlign, TextVerticalAlign, TextWrap};

pub const VIEWPORT: (u16, u16) = (128, 128);

fn label(align: TextAlign) -> ParagraphStyle {
    ParagraphStyle {
        wrap: TextWrap::NoWrap,
        align,
        vertical_align: TextVerticalAlign::Center,
        max_lines: Some(1),
        ..ParagraphStyle::default()
    }
}

#[compose]
pub fn build_widgets(view_w: u16, view_h: u16) {
    let (cols, rows) = life::dims_from_px(i32::from(view_w), i32::from(view_h));
    let board: LifeBoard = life::seeded_board(cols, rows);

    ui! {
        View (
            id: "compact_life_shell",
            grow: 1.0,
            direction: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                super::compact_layout::shell_direction(compact_life_shell.width, compact_life_shell.height)
            },
            padding: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                super::compact_layout::padding(compact_life_shell.width, compact_life_shell.height, 3, 4)
            },
            row_gap: 4,
            column_gap: 4,
            bg_color: ColorToken::Surface
        ) {
            View (
                id: "compact_life_header",
                width: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                    super::compact_layout::header_width(compact_life_shell.width, compact_life_shell.height, 36)
                },
                height: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                    super::compact_layout::header_height(compact_life_shell.width, compact_life_shell.height)
                },
                direction: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                    super::compact_layout::header_direction(compact_life_shell.width, compact_life_shell.height)
                },
                align: AlignItems::Center,
                justify: JustifyContent::Center,
                row_gap: 3,
                column_gap: 4
            ) {
                View (
                    id: "compact_life_marker",
                    width: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                        super::compact_layout::marker_width(compact_life_shell.width, compact_life_shell.height)
                    },
                    height: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                        super::compact_layout::marker_height(compact_life_shell.width, compact_life_shell.height)
                    },
                    bg_color: ColorToken::Success,
                    border_radius: 2
                )
                Text (
                    id: "compact_life_title",
                    "LIFE",
                    grow: 1.0,
                    width: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                        super::compact_layout::title_width(compact_life_shell.width, compact_life_shell.height)
                    },
                    height: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                        super::compact_layout::title_height(compact_life_shell.width, compact_life_shell.height)
                    },
                    font: FontToken::Default,
                    font_size: 6,
                    text_color: ColorToken::OnSurface,
                    paragraph: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                        label(super::compact_layout::text_align(compact_life_shell.width, compact_life_shell.height))
                    }
                )
                Text (
                    id: "compact_life_mode",
                    "LOOP",
                    width: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                        super::compact_layout::mode_width(compact_life_shell.width, compact_life_shell.height, 28)
                    },
                    height: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                        super::compact_layout::mode_height(compact_life_shell.width, compact_life_shell.height)
                    },
                    font: FontToken::Default,
                    font_size: 6,
                    text_color: ColorToken::OnSurfaceVariant,
                    paragraph: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                        label(super::compact_layout::mode_align(compact_life_shell.width, compact_life_shell.height))
                    }
                )
            }
            View (
                id: "compact_life_stage",
                grow: 1.0,
                min_width: 0,
                min_height: 0,
                width: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                    super::compact_layout::content_width(compact_life_shell.width, compact_life_shell.height)
                },
                height: @(id(compact_life_shell).width, id(compact_life_shell).height) {
                    super::compact_layout::content_height(compact_life_shell.width, compact_life_shell.height)
                },
                padding: Padding::all(2),
                bg_color: ColorToken::SurfaceVariant,
                border_color: ColorToken::Outline,
                border_width: 1,
                border_radius: 8
            ) {
                View (
                    id: "compact_life_board",
                    grow: 1.0,
                    width: Dimension::percent(100),
                    height: Dimension::percent(100),
                    bg_color: ColorToken::Surface,
                    border_radius: 5,
                    clip_children: true
                ) [
                    board,
                ]
            }
        }
    };
}

pub fn install<B, F>(app: &mut App<B, F>, parent: Entity)
where
    B: Surface,
    F: RendererFactory<B>,
{
    let info = app.backend.display_info();
    app.compose(parent, |cx| build_widgets(cx, info.width, info.height));
}

#[cfg(feature = "std")]
pub fn setup_app<B, F>(app: &mut App<B, F>, parent: Entity)
where
    B: Surface,
    F: RendererFactory<B>,
{
    life::install_runtime(app);
    install(app, parent);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::surface::FramebufferAccess;
    use crate::types::Viewport;
    use crate::ui::ComputedRect;

    fn compact_layout(width: u16, height: u16) -> (Rect, Rect) {
        let mut app = App::headless(width, height);
        app.with_default_widgets()
            .with_default_systems()
            .with_widget(life::life_view());
        let root = app.spawn_root().id();
        install(&mut app, root);
        app.set_root(root);
        app.render().unwrap();

        let header = app.world.find_by_id("compact_life_header").unwrap();
        let stage = app.world.find_by_id("compact_life_stage").unwrap();
        (
            app.world.get::<ComputedRect>(header).unwrap().0,
            app.world.get::<ComputedRect>(stage).unwrap().0,
        )
    }

    #[test]
    fn compact_life_reflows_between_portrait_square_and_landscape() {
        let (portrait_header, portrait_stage) = compact_layout(96, 160);
        let (square_header, square_stage) = compact_layout(128, 128);
        let (landscape_header, landscape_stage) = compact_layout(160, 96);

        assert!(portrait_stage.y >= portrait_header.y + portrait_header.h);
        assert!(square_stage.y >= square_header.y + square_header.h);
        assert!(landscape_stage.x >= landscape_header.x + landscape_header.w);
        assert!(landscape_stage.y <= landscape_header.y + Fixed::from_int(1));
        assert!(portrait_stage.h > landscape_stage.h);
        assert!(landscape_stage.w > portrait_stage.w);
    }

    #[test]
    fn compact_life_reflows_after_live_viewport_changes() {
        let mut app = App::headless(128, 128);
        app.with_default_widgets()
            .with_default_systems()
            .with_widget(life::life_view());
        let root = app.spawn_root().id();
        install(&mut app, root);
        app.set_root(root);
        app.render().unwrap();

        crate::ui::render_system::update_layout(
            &mut app.world,
            root,
            &Viewport::new(160, 96, Fixed::ONE),
        );
        let shell = app.world.find_by_id("compact_life_shell").unwrap();
        assert_eq!(
            app.world.get::<Style>(shell).unwrap().layout.direction,
            FlexDirection::Row,
        );

        crate::ui::render_system::update_layout(
            &mut app.world,
            root,
            &Viewport::new(96, 160, Fixed::ONE),
        );
        assert_eq!(
            app.world.get::<Style>(shell).unwrap().layout.direction,
            FlexDirection::Column,
        );
    }

    #[test]
    fn compact_life_renders_at_native_size() {
        let mut app = App::headless(VIEWPORT.0, VIEWPORT.1);
        app.with_default_widgets()
            .with_default_systems()
            .with_widget(life::life_view());
        let root = app.spawn_root().id();
        install(&mut app, root);
        app.set_root(root);
        app.render().unwrap();

        let board = app.world.find_by_id("compact_life_board").unwrap();
        let rect = app.world.get::<ComputedRect>(board).unwrap().0;
        assert!(rect.w > Fixed::from_int(80));
        assert!(rect.h > Fixed::from_int(80));
        assert!(
            app.backend
                .framebuffer()
                .buf
                .as_slice()
                .iter()
                .any(|byte| *byte != 0),
        );
    }
}