nightshade 0.48.0

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::world::World;
use winit::event::WindowEvent;

/// Builds the egui host once the window exists, storing the `egui_winit::State`
/// (which owns the `egui::Context`) on `world.resources.egui`. Called from the
/// winit `resumed` handler after the window handle is set.
pub(crate) fn initialize(world: &mut World) {
    let Some(window_handle) = world.resources.window.handle.as_ref() else {
        return;
    };
    let window_handle = window_handle.clone();

    let gui_context = egui::Context::default();

    #[cfg(target_arch = "wasm32")]
    gui_context.set_pixels_per_point(1.0);

    let viewport_id = gui_context.viewport_id();
    let gui_state = egui_winit::State::new(
        gui_context,
        viewport_id,
        &window_handle,
        Some(window_handle.scale_factor() as _),
        Some(winit::window::Theme::Dark),
        None,
    );

    egui_extras::install_image_loaders(gui_state.egui_ctx());

    world.resources.egui.state = Some(gui_state);

    #[cfg(target_arch = "wasm32")]
    {
        use wasm_bindgen::JsCast;
        use winit::platform::web::WindowExtWebSys;
        if let Some(canvas) = window_handle.canvas() {
            let queue = std::sync::Arc::clone(&world.resources.egui.wasm_paste_queue);
            let closure = wasm_bindgen::closure::Closure::wrap(Box::new(
                move |event: web_sys::ClipboardEvent| {
                    if let Some(data) = event.clipboard_data()
                        && let Ok(text) = data.get_data("text/plain")
                        && !text.is_empty()
                        && let Ok(mut queue) = queue.lock()
                    {
                        queue.push(text);
                        event.prevent_default();
                    }
                },
            )
                as Box<dyn FnMut(web_sys::ClipboardEvent)>);
            let _ =
                canvas.add_event_listener_with_callback("paste", closure.as_ref().unchecked_ref());
            closure.forget();
        }
    }
}

/// Feeds a raw window event to egui and records whether egui consumed it so the
/// engine input systems can skip events the UI handled. Called first in the
/// event handler, before `input_event_system`.
pub(crate) fn handle_window_event(world: &mut World, window_event: &WindowEvent) {
    let Some(gui_state) = world.resources.egui.state.as_mut() else {
        return;
    };
    let Some(window_handle) = world.resources.window.handle.as_ref() else {
        return;
    };
    world.resources.user_interface.consumed_event = gui_state
        .on_window_event(window_handle, window_event)
        .consumed;
}

/// Opens the egui pass for the frame: takes accumulated input and begins the
/// context so per-frame systems can draw with [`egui_context`](crate::ecs::ui::egui_state::egui_context).
/// Runs before the user systems each frame; a no-op unless egui is enabled.
pub(crate) fn begin_frame(world: &mut World) {
    world.resources.egui.frame_active = false;
    if !world.resources.egui.enabled {
        return;
    }

    let Some(gui_state) = world.resources.egui.state.as_mut() else {
        return;
    };
    let Some(window_handle) = world.resources.window.handle.as_ref() else {
        return;
    };

    #[cfg(not(target_arch = "wasm32"))]
    {
        let scale_factor = world
            .resources
            .render_settings
            .ui_scale
            .unwrap_or_else(|| window_handle.scale_factor() as f32);
        gui_state.egui_ctx().set_pixels_per_point(scale_factor);
    }

    #[cfg(not(target_arch = "wasm32"))]
    let gui_input = gui_state.take_egui_input(window_handle);

    #[cfg(target_arch = "wasm32")]
    let gui_input = {
        use winit::platform::web::WindowExtWebSys;
        let mut input = gui_state.take_egui_input(window_handle);
        if let Some(canvas) = window_handle.canvas() {
            let dpr = window_handle.scale_factor() as f32;
            let canvas_size = egui::vec2(canvas.width() as f32 / dpr, canvas.height() as f32 / dpr);
            input.screen_rect = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, canvas_size));
        }
        if let Ok(mut queue) = world.resources.egui.wasm_paste_queue.lock() {
            for text in queue.drain(..) {
                input.events.push(egui::Event::Paste(text));
            }
        }
        input
    };

    gui_state.egui_ctx().begin_pass(gui_input);
    world.resources.egui.frame_active = true;
}

/// Closes the egui pass: tessellates what the systems drew and stashes the
/// output on `world.resources.egui` for the renderer. Runs after the user
/// systems each frame; a no-op unless the pass was opened.
pub(crate) fn finish_frame(world: &mut World) {
    if !world.resources.egui.frame_active {
        return;
    }

    let Some(gui_state) = world.resources.egui.state.as_mut() else {
        return;
    };
    let Some(window_handle) = world.resources.window.handle.as_ref() else {
        return;
    };

    let context = gui_state.egui_ctx().clone();
    let output = context.end_pass();
    gui_state.handle_platform_output(window_handle, output.platform_output.clone());
    let paint_jobs = context.tessellate(output.shapes.clone(), output.pixels_per_point);

    world.resources.egui.frame_active = false;
    world.resources.egui.frame_output = Some((output, paint_jobs));
}