mirui 0.28.1

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

#[cfg(feature = "std")]
use crate::app::{App, RendererFactory};
use crate::ecs::{Entity, MonoClock, World};
#[cfg(feature = "std")]
use crate::plugins::StdInstantClockPlugin;
use crate::prelude::*;
#[cfg(feature = "std")]
use crate::surface::Surface;
use crate::widget;
use crate::widget::dirty::Dirty;
use crate::widget::{Children, Parent, Style};
use alloc::vec::Vec;

pub struct Particle {
    pub x: Fixed,
    pub y: Fixed,
    pub vx: Fixed,
    pub vy: Fixed,
    pub phase: Fixed,
}

pub struct PulseRing {
    pub radius: Fixed,
    pub grow_speed: Fixed,
    pub max_radius: Fixed,
}

pub struct BouncingBar {
    pub pos: Fixed,
    pub speed: Fixed,
    pub vertical: bool,
}

pub struct ParticleBounds {
    pub w: i32,
    pub h: i32,
}

#[mirui_macros::system(order = ANIMATION)]
pub fn particle_system(world: &mut World) {
    let (bw, bh) = world
        .resource::<ParticleBounds>()
        .map(|b| (b.w, b.h))
        .unwrap_or((128, 128));
    let mut buf = Vec::new();
    world.query::<Particle>().collect_into(&mut buf);
    for e in buf {
        let (new_x, new_y) = {
            let Some(p) = world.get_mut::<Particle>(e) else {
                continue;
            };
            p.x += p.vx;
            p.y += p.vy;
            p.phase += Fixed::from_raw(5);

            if p.x < Fixed::from_int(2) || p.x > Fixed::from_int(bw - 6) {
                p.vx = Fixed::ZERO - p.vx;
                p.x = p.x.max(Fixed::from_int(2)).min(Fixed::from_int(bw - 6));
            }
            if p.y < Fixed::from_int(2) || p.y > Fixed::from_int(bh - 6) {
                p.vy = Fixed::ZERO - p.vy;
                p.y = p.y.max(Fixed::from_int(2)).min(Fixed::from_int(bh - 6));
            }
            (p.x, p.y)
        };
        widget::set_position(world, e, new_x, new_y);
    }
}

#[mirui_macros::system(order = ANIMATION)]
pub fn pulse_ring_system(world: &mut World) {
    let (bw, bh) = world
        .resource::<ParticleBounds>()
        .map(|b| (b.w, b.h))
        .unwrap_or((128, 128));
    let mut buf = Vec::new();
    world.query::<PulseRing>().collect_into(&mut buf);
    for e in buf {
        let new_radius = {
            let Some(ring) = world.get_mut::<PulseRing>(e) else {
                continue;
            };
            ring.radius += ring.grow_speed;
            if ring.radius > ring.max_radius {
                ring.radius = Fixed::from_int(2);
            }
            ring.radius
        };
        if let Some(style) = world.get_mut::<Style>(e) {
            let center_x = Fixed::from_int(bw / 2);
            let center_y = Fixed::from_int(bh / 2);
            style.layout.left = Dimension::Px(center_x - new_radius);
            style.layout.top = Dimension::Px(center_y - new_radius);
            style.layout.width = Dimension::Px(new_radius * 2);
            style.layout.height = Dimension::Px(new_radius * 2);
            style.border_radius = Fixed::ZERO;
        }
        world.insert(e, Dirty);
    }
}

#[mirui_macros::system(order = ANIMATION)]
pub fn bar_system(world: &mut World) {
    let (bw, bh) = world
        .resource::<ParticleBounds>()
        .map(|b| (b.w, b.h))
        .unwrap_or((128, 128));
    let mut buf = Vec::new();
    world.query::<BouncingBar>().collect_into(&mut buf);
    for e in buf {
        let (new_x, new_y) = {
            let Some(bar) = world.get_mut::<BouncingBar>(e) else {
                continue;
            };
            bar.pos += bar.speed;
            let max = if bar.vertical {
                Fixed::from_int(bh - 20)
            } else {
                Fixed::from_int(bw - 30)
            };
            if bar.pos < Fixed::from_int(4) || bar.pos > max {
                bar.speed = Fixed::ZERO - bar.speed;
                bar.pos = bar.pos.max(Fixed::from_int(4)).min(max);
            }
            if bar.vertical {
                (Fixed::from_int(4), bar.pos)
            } else {
                (bar.pos, Fixed::from_int(4))
            }
        };
        widget::set_position(world, e, new_x, new_y);
    }
}

pub fn build_widgets(world: &mut World, parent: Entity, view_w: u16, view_h: u16) {
    let bw = view_w as i32;
    let bh = view_h as i32;
    world.insert_resource(ParticleBounds { w: bw, h: bh });

    let ring_colors = [
        Color::rgba(80, 200, 255, 60),
        Color::rgba(255, 100, 200, 40),
        Color::rgba(100, 255, 150, 50),
    ];
    let ring_speeds = [Fixed::from_raw(12), Fixed::from_raw(8), Fixed::from_raw(15)];
    let ring_max = [
        Fixed::from_int(20),
        Fixed::from_int(16),
        Fixed::from_int(22),
    ];

    for i in 0..3 {
        let ring = WidgetBuilder::new(world)
            .bg_color(ring_colors[i])
            .border(ring_colors[i], Fixed::from_int(2))
            .border_radius(Fixed::from_int(10))
            .layout(LayoutStyle {
                position: Position::Absolute,
                left: Dimension::px(bw / 2 - 10),
                top: Dimension::px(bh / 2 - 10),
                width: Dimension::px(20),
                height: Dimension::px(20),
                ..Default::default()
            })
            .id();
        world.insert(
            ring,
            PulseRing {
                radius: Fixed::from_int(5 + i as i32 * 8),
                grow_speed: ring_speeds[i],
                max_radius: ring_max[i],
            },
        );
        world.insert(ring, Parent(parent));
        if let Some(ch) = world.get_mut::<Children>(parent) {
            ch.0.push(ring);
        }
    }

    let bar_configs: [(Color, Fixed, Fixed, bool, i32, i32); 3] = [
        (
            Color::rgba(255, 200, 50, 180),
            Fixed::from_raw(45),
            Fixed::from_int(10),
            false,
            30,
            6,
        ),
        (
            Color::rgba(50, 255, 200, 160),
            Fixed::from_raw(33),
            Fixed::from_int(80),
            false,
            25,
            5,
        ),
        (
            Color::rgba(200, 50, 255, 140),
            Fixed::from_raw(55),
            Fixed::from_int(20),
            true,
            5,
            40,
        ),
    ];

    for (color, speed, start, vertical, ww, hh) in bar_configs {
        let bar = WidgetBuilder::new(world)
            .bg_color(color)
            .border_radius(Fixed::ZERO)
            .layout(LayoutStyle {
                position: Position::Absolute,
                left: Dimension::px(4),
                top: Dimension::px(4),
                width: Dimension::px(ww),
                height: Dimension::px(hh),
                ..Default::default()
            })
            .id();
        world.insert(
            bar,
            BouncingBar {
                pos: start,
                speed,
                vertical,
            },
        );
        world.insert(bar, Parent(parent));
        if let Some(ch) = world.get_mut::<Children>(parent) {
            ch.0.push(bar);
        }
    }

    let mut rng_state: u32 = world
        .resource::<MonoClock>()
        .map(|c| c.now_ms())
        .unwrap_or(0)
        .wrapping_add(0x9E37_79B9);
    if rng_state == 0 {
        rng_state = 1;
    }
    let mut rng = || -> i32 {
        rng_state ^= rng_state << 13;
        rng_state ^= rng_state >> 17;
        rng_state ^= rng_state << 5;
        (rng_state % 256) as i32
    };

    let particle_colors = [
        Color::rgb(255, 80, 80),
        Color::rgb(80, 255, 80),
        Color::rgb(80, 80, 255),
        Color::rgb(255, 255, 80),
        Color::rgb(255, 80, 255),
        Color::rgb(80, 255, 255),
    ];

    for color in particle_colors {
        let px = Fixed::from_raw(rng() % (100 * 256));
        let py = Fixed::from_raw(rng() % (100 * 256));
        let vx = Fixed::from_raw(rng() % 200 - 100);
        let vy = Fixed::from_raw(rng() % 200 - 100);

        let particle = WidgetBuilder::new(world)
            .bg_color(color)
            .border_radius(Fixed::ZERO)
            .layout(LayoutStyle {
                position: Position::Absolute,
                left: Dimension::Px(px),
                top: Dimension::Px(py),
                width: Dimension::px(4),
                height: Dimension::px(4),
                ..Default::default()
            })
            .id();
        world.insert(
            particle,
            Particle {
                x: px,
                y: py,
                vx,
                vy,
                phase: Fixed::ZERO,
            },
        );
        world.insert(particle, Parent(parent));
        if let Some(ch) = world.get_mut::<Children>(parent) {
            ch.0.push(particle);
        }
    }
}

#[cfg(feature = "std")]
pub fn setup_app<B, F>(app: &mut App<B, F>, parent: Entity)
where
    B: Surface,
    F: RendererFactory<B>,
{
    let info = app.backend.display_info();
    app.add_plugin(StdInstantClockPlugin);
    app.add_system(particle_system::system());
    app.add_system(pulse_ring_system::system());
    app.add_system(bar_system::system());
    build_widgets(&mut app.world, parent, info.width, info.height);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::widget::IdMap;
    use crate::widget::builder::WidgetBuilder;

    #[test]
    fn build_widgets_smoke() {
        let mut world = World::new();
        world.insert_resource(IdMap::new());
        let parent = WidgetBuilder::new(&mut world).id();
        build_widgets(&mut world, parent, 128, 128);
        assert!(
            world
                .get::<Children>(parent)
                .is_some_and(|c| !c.0.is_empty()),
        );
    }
}