codecraft 0.2.0

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, a yakui-drawn UI, audio and gamepad haptics; its binary maps any folder, and the symbols of its Rust files, as a 3D wall of boxes
Documentation
//! The yakui frame: started before a scene draws, finished before the frame
//! is painted, and fed the mouse in between.
use std::cell::RefCell;
use std::rc::Rc;

use bevy_ecs::prelude::*;
use yakui::event::Event;
use yakui::font::Fonts;
use yakui::geometry::{Rect, Vec2};
use yakui::input::MouseButton;
use yakui::{WidgetId, Yakui};

use super::font::{Face, Family, Font};
use super::resources::{CursorPosition, MouseInput, PointerCapture, ScreenSize};

/// Pixels scrolled per wheel click; the figure `yakui-winit` uses.
pub const SCROLL_LINE: f32 = 100.0 / 3.0;

/// The panels drawn this frame, hit-tested by [`Ui::begin_frame`] next frame.
#[derive(Clone, Default)]
pub(crate) struct Panels(pub Rc<RefCell<Vec<WidgetId>>>);

/// The yakui instance and the frame state around it.
pub struct Ui {
    pub yakui: Yakui,
    /// Last button state told to yakui, so only edges are sent.
    left: bool,
    right: bool,
    viewport: Vec2,
    open: bool,
}

impl Default for Ui {
    fn default() -> Self {
        Self::new()
    }
}

impl Ui {
    pub fn new() -> Self {
        let yakui = Yakui::new();

        let fonts = yakui.dom().get_global_or_init(Fonts::default);
        for family in Family::ALL {
            fonts.add(family.load(), Some(family.name()));
        }
        fonts.add(Family::default().load(), Some("default"));

        Self {
            yakui,
            left: false,
            right: false,
            viewport: Vec2::ZERO,
            open: false,
        }
    }

    /// Opens the frame; everything drawn until [`end_frame`](Self::end_frame) is this frame's UI.
    pub fn begin_frame(world: &mut World) {
        let Some(mut ui) = world.remove_non_send::<Ui>() else {
            return;
        };
        if ui.open {
            ui.yakui.finish();
        }

        let screen = *world.resource::<ScreenSize>();
        ui.set_viewport(screen.width, screen.height);
        let cursor = *world.resource::<CursorPosition>();
        let mouse = *world.resource::<MouseInput>();
        ui.feed(cursor, mouse);

        // Hit-tests last frame's panels: yakui only has last frame's layout.
        let panels = ui.yakui.dom().get_global_or_init(Panels::default);
        let over_panel = panels.0.borrow().iter().any(|&id| {
            ui.yakui
                .layout_dom()
                .get(id)
                .is_some_and(|node| node.rect.contains_point(Vec2::new(cursor.x, cursor.y)))
        });
        panels.0.borrow_mut().clear();
        world.resource_mut::<PointerCapture>().over_panel = over_panel;

        let family = world
            .get_resource::<Font>()
            .map(Font::family)
            .unwrap_or_default();
        ui.yakui.dom().get_global_or_init(Face::default).set(family);

        ui.yakui.start();
        ui.open = true;
        world.insert_non_send(ui);
    }

    /// Closes the frame and lays it out. What was drawn is ready to paint.
    pub fn end_frame(world: &mut World) {
        if let Some(mut ui) = world.get_non_send_mut::<Ui>()
            && ui.open
        {
            ui.yakui.finish();
            ui.open = false;
        }
    }

    /// Where a widget drawn last frame ended up.
    pub fn rect_of(&self, id: WidgetId) -> Option<Rect> {
        self.yakui.layout_dom().get(id).map(|node| node.rect)
    }

    /// Physical pixels throughout, so the scale factor stays at one.
    fn set_viewport(&mut self, width: f32, height: f32) {
        let size = Vec2::new(width, height);
        if size == self.viewport {
            return;
        }
        self.viewport = size;
        self.yakui.set_surface_size(size);
        self.yakui
            .set_unscaled_viewport(Rect::from_pos_size(Vec2::ZERO, size));
        self.yakui.set_scale_factor(1.0);
    }

    /// Cursor goes every frame: yakui only re-hit-tests on a move, and panels can open under a still pointer.
    fn feed(&mut self, cursor: CursorPosition, mouse: MouseInput) {
        self.yakui
            .handle_event(Event::CursorMoved(Some(Vec2::new(cursor.x, cursor.y))));

        let mut button = |button: MouseButton, down: bool| {
            self.yakui
                .handle_event(Event::MouseButtonChanged { button, down });
        };
        if mouse.just_pressed {
            if self.left {
                button(MouseButton::One, false);
            }
            self.left = true;
            button(MouseButton::One, true);
        }
        if mouse.left_down != self.left {
            self.left = mouse.left_down;
            button(MouseButton::One, mouse.left_down);
        }
        if mouse.right_down != self.right {
            self.right = mouse.right_down;
            button(MouseButton::Two, mouse.right_down);
        }
        if mouse.scroll != 0.0 {
            // Content moves opposite to the wheel.
            self.yakui.handle_event(Event::MouseScroll {
                delta: Vec2::new(0.0, -mouse.scroll * SCROLL_LINE),
            });
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ecs::Application;
    use crate::ui::plugin::UiPlugin;

    fn app() -> Application {
        let mut app = Application::new();
        app.add_plugin(UiPlugin);
        app.world.insert_resource(ScreenSize {
            width: 640.0,
            height: 480.0,
        });
        app
    }

    #[test]
    fn every_face_is_registered_with_yakui() {
        let ui = Ui::new();
        let fonts = ui.yakui.dom().get_global_or_init(Fonts::default);
        for family in Family::ALL {
            assert!(
                fonts.get(&family.name().into()).is_some(),
                "{} is not registered",
                family.label()
            );
        }
        assert!(fonts.get(&"default".into()).is_some());
    }

    #[test]
    fn frames_open_and_close_cleanly() {
        let mut app = app();
        for _ in 0..3 {
            Ui::begin_frame(&mut app.world);
            crate::ui::text(14.0, "hello");
            app.update();
            Ui::end_frame(&mut app.world);
        }
        Ui::begin_frame(&mut app.world);
        Ui::begin_frame(&mut app.world);
        Ui::end_frame(&mut app.world);
        Ui::end_frame(&mut app.world);
    }

    #[test]
    fn the_pointer_over_a_panel_is_the_panel_s() {
        let mut app = app();
        let draw = |app: &mut Application| {
            Ui::begin_frame(&mut app.world);
            crate::ui::corner(yakui::Alignment::TOP_LEFT, || {
                crate::ui::panel(|| {
                    crate::ui::text(20.0, "A PANEL IN THE CORNER");
                });
            });
            app.update();
            Ui::end_frame(&mut app.world);
        };
        draw(&mut app);

        app.world
            .insert_resource(CursorPosition { x: 40.0, y: 30.0 });
        draw(&mut app);
        assert!(app.world.resource::<PointerCapture>().taken());

        app.world
            .insert_resource(CursorPosition { x: 600.0, y: 400.0 });
        draw(&mut app);
        assert!(!app.world.resource::<PointerCapture>().taken());
    }
}