mirui 0.39.2

A lightweight, no_std ECS-driven UI framework for embedded, desktop, and WebAssembly
Documentation
extern crate alloc;

use alloc::vec::Vec;

use crate::core::reactive::{Computed, Signal};
use crate::prelude::*;

const ITEMS: &[&str] = &["buy milk", "write docs", "ship release"];

fn row_color(done: bool) -> Color {
    if done {
        Color::rgb(63, 185, 80)
    } else {
        Color::rgb(80, 80, 96)
    }
}

#[compose]
pub fn build_widgets() {
    let outer_parent = cx.parent();
    let dones: Vec<Signal<bool>> = ITEMS.iter().map(|_| Signal::new(false)).collect();
    let remaining = {
        let dones = dones.clone();
        Computed::new(move || dones.iter().filter(|d| !d.get()).count() as i32)
    };
    let summary = remaining.clone();

    let root = WidgetBuilder::new(cx.world_mut())
        .layout(crate::ui::layout::LayoutStyle {
            direction: FlexDirection::Column,
            align: AlignItems::Center,
            padding: Padding::all(12),
            grow: Fixed::ONE,
            ..Default::default()
        })
        .id();
    cx.world_mut().insert(root, crate::ui::Parent(outer_parent));
    if let Some(c) = cx.world_mut().get_mut::<crate::ui::Children>(outer_parent) {
        c.0.push(root);
    }

    let world = cx.world_mut();
    let _ = ui! {
        :(
            parent: root
            world: world
        :)

        summary_label (
            text: ${ alloc::format!("{} remaining", summary.get()) },
            height: 32
        )
    };

    for (i, label) in ITEMS.iter().enumerate() {
        let toggle = dones[i].clone();
        let bg = dones[i].clone();
        let text = *label;
        ui! {
            :(
                parent: root
                world: world
            :)

            row (
                bg_color: ${ row_color(bg.get()) },
                width: 220,
                height: 32,
                border_radius: 6,
                text: text
            ) on Tap { toggle.update(|d| *d = !*d); }
        };
    }
}

#[cfg(feature = "std")]
pub fn setup_app<B, F>(app: &mut App<B, F>, parent: Entity)
where
    B: Surface,
    F: RendererFactory<B>,
{
    use crate::app::plugins::StdInstantClockPlugin;
    app.add_plugin(StdInstantClockPlugin);
    app.compose(parent, build_widgets);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::reactive::flush_signal_dirty;
    use crate::input::event::GestureHandler;
    use crate::input::event::gesture::GestureEvent;
    use crate::ui::Children;
    use crate::ui::IdMap;
    use crate::ui::UiScope;
    use crate::ui::widgets::text::Text;

    fn label_text(world: &World, label: Entity) -> alloc::string::String {
        let t = world.get::<Text>(label).expect("label has Text");
        t.resolve(world).into_owned()
    }

    #[test]
    fn toggling_a_row_updates_remaining_count() {
        let mut world = World::new();
        world.insert_resource(IdMap::new());
        let parent = WidgetBuilder::new(&mut world).id();
        let mut cx = UiScope::new(&mut world, parent);
        build_widgets(&mut cx);
        drop(cx);

        let root = world.get::<Children>(parent).unwrap().0[0];
        let kids = world.get::<Children>(root).unwrap().0.clone();
        let summary = kids[0];
        let first_row = kids[1];

        assert_eq!(label_text(&world, summary), "3 remaining");

        GestureHandler::trigger(
            &mut world,
            first_row,
            &GestureEvent::Tap {
                x: Fixed::ZERO,
                y: Fixed::ZERO,
                target: first_row,
            },
        );
        flush_signal_dirty(&mut world);
        assert_eq!(label_text(&world, summary), "2 remaining");
    }
}