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::components::Image;
use crate::components::assets::*;
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 alloc::vec::Vec;

pub struct Velocity {
    pub vx: Fixed,
    pub vy: Fixed,
}

pub struct PhysicsBody {
    pub x: Fixed,
    pub y: Fixed,
}

pub struct PhysicsTime {
    pub last_tick_ms: u32,
    pub accumulator_ms: u32,
}

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

pub struct SpringLength(pub Fixed);

#[derive(Default)]
pub struct PhysicsScratch {
    pub entities: Vec<Entity>,
    pub positions: Vec<(Fixed, Fixed)>,
    pub ax: Vec<Fixed>,
    pub ay: Vec<Fixed>,
}

pub struct KickPhase(pub u32);

const PHYSICS_DT_MS: u32 = 11;

fn isqrt(n: u32) -> u32 {
    if n == 0 {
        return 0;
    }
    let mut x = n;
    let mut y = x.div_ceil(2);
    while y < x {
        x = y;
        y = (x + n / x) / 2;
    }
    x
}

#[mirui_macros::system]
pub fn physics_tick_system(world: &mut World) {
    let now_ms = world
        .resource::<MonoClock>()
        .map(|c| c.now_ms())
        .unwrap_or(0);
    let steps = {
        let Some(pt) = world.resource_mut::<PhysicsTime>() else {
            return;
        };
        let elapsed = now_ms.wrapping_sub(pt.last_tick_ms);
        pt.last_tick_ms = now_ms;
        pt.accumulator_ms = pt.accumulator_ms.saturating_add(elapsed);
        let steps = pt.accumulator_ms / PHYSICS_DT_MS;
        pt.accumulator_ms %= PHYSICS_DT_MS;
        steps
    };
    for _ in 0..steps.min(8) {
        three_body_step(world);
    }
}

fn three_body_step(world: &mut World) {
    let (bound_w, bound_h) = world
        .resource::<WorldBounds>()
        .map(|b| (b.w, b.h))
        .unwrap_or((128, 128));
    let equilibrium = world
        .resource::<SpringLength>()
        .map(|s| s.0)
        .unwrap_or(Fixed::from_int(30));

    let mut scratch = {
        let Some(s) = world.resource_mut::<PhysicsScratch>() else {
            return;
        };
        core::mem::take(s)
    };
    scratch.entities.clear();
    world
        .query::<PhysicsBody>()
        .and::<Velocity>()
        .collect_into(&mut scratch.entities);
    let n = scratch.entities.len();
    if n == 0 {
        if let Some(s) = world.resource_mut::<PhysicsScratch>() {
            *s = scratch;
        }
        return;
    }

    scratch.positions.clear();
    scratch.positions.resize(n, (Fixed::ZERO, Fixed::ZERO));
    for i in 0..n {
        if let Some(body) = world.get::<PhysicsBody>(scratch.entities[i]) {
            scratch.positions[i] = (body.x, body.y);
        }
    }

    scratch.ax.clear();
    scratch.ax.resize(n, Fixed::ZERO);
    scratch.ay.clear();
    scratch.ay.resize(n, Fixed::ZERO);
    for i in 0..n {
        for j in (i + 1)..n {
            let dx = scratch.positions[j].0 - scratch.positions[i].0;
            let dy = scratch.positions[j].1 - scratch.positions[i].1;
            let dx_int = dx.to_int();
            let dy_int = dy.to_int();
            let dist = Fixed::from_int(isqrt((dx_int * dx_int + dy_int * dy_int) as u32) as i32);
            if dist == Fixed::ZERO {
                continue;
            }
            let force = Fixed::from_int(120) * (dist - equilibrium) / dist;
            let fx = force * dx / (dist * dist);
            let fy = force * dy / (dist * dist);
            scratch.ax[i] += fx;
            scratch.ay[i] += fy;
            scratch.ax[j] -= fx;
            scratch.ay[j] -= fy;
        }
    }

    let v_max = Fixed::from_int(5);
    let v_min = Fixed::ZERO - v_max;
    let min = Fixed::from_int(8);
    let max_x = Fixed::from_int(bound_w - 8);
    let max_y = Fixed::from_int(bound_h - 8);
    for i in 0..n {
        let e = scratch.entities[i];
        if let Some(vel) = world.get_mut::<Velocity>(e) {
            vel.vx += scratch.ax[i];
            vel.vy += scratch.ay[i];
            if vel.vx > v_max {
                vel.vx = v_max;
            }
            if vel.vx < v_min {
                vel.vx = v_min;
            }
            if vel.vy > v_max {
                vel.vy = v_max;
            }
            if vel.vy < v_min {
                vel.vy = v_min;
            }
        }
        let (vx, vy) = world
            .get::<Velocity>(e)
            .map(|v| (v.vx, v.vy))
            .unwrap_or((Fixed::ZERO, Fixed::ZERO));
        if let Some(body) = world.get_mut::<PhysicsBody>(e) {
            body.x += vx;
            body.y += vy;
            if body.x < min {
                body.x = min;
            }
            if body.x > max_x {
                body.x = max_x;
            }
            if body.y < min {
                body.y = min;
            }
            if body.y > max_y {
                body.y = max_y;
            }
        }
        if let Some(body) = world.get::<PhysicsBody>(e) {
            let bx = body.x;
            let by = body.y;
            if let Some(vel) = world.get_mut::<Velocity>(e)
                && (bx <= min || bx >= max_x)
            {
                vel.vx = Fixed::ZERO - vel.vx;
            }
            if let Some(vel) = world.get_mut::<Velocity>(e)
                && (by <= min || by >= max_y)
            {
                vel.vy = Fixed::ZERO - vel.vy;
            }
        }
    }

    if let Some(s) = world.resource_mut::<PhysicsScratch>() {
        *s = scratch;
    }
}

#[mirui_macros::system]
pub fn kick_system(world: &mut World) {
    let phase = {
        let Some(p) = world.resource_mut::<KickPhase>() else {
            return;
        };
        p.0 = p.0.wrapping_add(1);
        p.0
    };
    let mut buf = Vec::new();
    world.query::<Velocity>().collect_into(&mut buf);
    let entities = buf;
    if phase % 40 == 0 && !entities.is_empty() {
        let kick_idx = (phase / 40) as usize % entities.len();
        let kick_dir = (phase / 120) as i32;
        let e = entities[kick_idx];
        let kx = (kick_dir * 7).rem_euclid(13) - 6;
        let ky = (kick_dir * 11).rem_euclid(13) - 6;
        if let Some(vel) = world.get_mut::<Velocity>(e) {
            vel.vx += Fixed::from_int(kx) / Fixed::from_int(2);
            vel.vy += Fixed::from_int(ky) / Fixed::from_int(2);
        }
    }
}

#[mirui_macros::system]
pub fn sync_layout_system(world: &mut World) {
    let half_w = Fixed::from_int(IMG_THUMBS_UP.width as i32 / 2);
    let half_h = Fixed::from_int(IMG_THUMBS_UP.height as i32 / 2);
    let mut buf = Vec::new();
    world.query::<PhysicsBody>().collect_into(&mut buf);
    for e in buf {
        let (bx, by) = world
            .get::<PhysicsBody>(e)
            .map(|b| (b.x - half_w, b.y - half_h))
            .unwrap_or((Fixed::ZERO, Fixed::ZERO));
        widget::set_position(world, e, bx, by);
    }
}

pub fn build_widgets(
    world: &mut World,
    parent: Entity,
    view_w: u16,
    view_h: u16,
    n_bodies: usize,
    equilibrium: Fixed,
) {
    let logical_w = view_w as i32;
    let logical_h = view_h as i32;

    let now_ms = world
        .resource::<MonoClock>()
        .map(|c| c.now_ms())
        .unwrap_or(0);
    world.insert_resource(PhysicsTime {
        last_tick_ms: now_ms,
        accumulator_ms: 0,
    });
    world.insert_resource(WorldBounds {
        w: logical_w,
        h: logical_h,
    });
    world.insert_resource(SpringLength(equilibrium));
    let n = n_bodies.max(1);
    world.insert_resource(PhysicsScratch {
        entities: Vec::with_capacity(n),
        positions: Vec::with_capacity(n),
        ax: Vec::with_capacity(n),
        ay: Vec::with_capacity(n),
    });
    world.insert_resource(KickPhase(0));

    ui! {
        :(
            parent: parent
            world: world
        :)

        View (direction: FlexDirection::Column, grow: 1.0) {
            View (
                bg_color: Color::rgb(88, 166, 255),
                height: 20,
                text: "mirui",
                border_radius: 3
            ) {}
            Row (direction: FlexDirection::Row, grow: 1.0) {
                View (bg_color: Color::rgb(63, 185, 80), grow: 1.0) {}
                View (bg_color: Color::rgb(248, 81, 73), grow: 1.0) {}
            }
            View (bg_color: Color::rgb(210, 168, 255), height: 20, text: "n-body") {}
        }
    };

    let iw = IMG_THUMBS_UP.width as i32;
    let ih = IMG_THUMBS_UP.height as i32;
    let cx = Fixed::from_int(logical_w / 2);
    let cy = Fixed::from_int(logical_h / 2);
    let r = Fixed::from_int(logical_w.min(logical_h) * 35 / 100);
    let orbital = Fixed::from_int(2);

    let mut init_pos: Vec<(Fixed, Fixed, Fixed, Fixed)> = Vec::with_capacity(n);
    for i in 0..n {
        let deg = Fixed::from_int(360) * Fixed::from_int(i as i32) / Fixed::from_int(n as i32);
        let c = Fixed::cos_deg(deg);
        let s = Fixed::sin_deg(deg);
        init_pos.push((
            cx + c * r,
            cy + s * r,
            Fixed::ZERO - s * orbital,
            c * orbital,
        ));
    }

    ui! {
        :(
            parent: parent
            world: world
        :)

        walk init_pos.iter() with pos {
            View (
                position: Position::Absolute,
                left: pos.0.to_int() - iw / 2,
                top: pos.1.to_int() - ih / 2,
                width: iw,
                height: ih,
                image: Image::new(&IMG_THUMBS_UP)
            ) [
                PhysicsBody { x: pos.0, y: pos.1 },
                Velocity { vx: pos.2, vy: pos.3 },
            ] {}
        }
    };
}

#[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(physics_tick_system::system());
    app.add_system(kick_system::system());
    app.add_system(sync_layout_system::system());
    build_widgets(
        &mut app.world,
        parent,
        info.width,
        info.height,
        3,
        Fixed::from_int(30),
    );
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::widget::Children;
    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, 3, Fixed::from_int(30));
        assert!(
            world
                .get::<Children>(parent)
                .is_some_and(|c| !c.0.is_empty()),
        );
    }
}