Newer
Older
vengine / engine-core / src / debug.rs
use bevy::{
    app::{Plugin, Startup, Update},
    ecs::{
        archetype::Archetypes,
        component::Components,
        entity::Entity,
        error::Result,
        query::Without,
        resource::Resource,
        schedule::IntoScheduleConfigs,
        system::{Commands, Local, Query, Res, ResMut, SystemState},
        world::World,
    },
    input::{common_conditions::input_just_pressed, keyboard::KeyCode},
    log::{
        self, Level,
        tracing::{
            field::{Field, ValueSet, Visit},
            span,
        },
        tracing_subscriber::{Layer, Registry},
    },
};
use bevy_egui::{
    EguiContext, EguiContexts, EguiPrimaryContextPass, PrimaryEguiContext,
    egui::{self, Grid, ScrollArea, Widget},
};

use crate::{EXTERNAL_SYSTEMS_CMDS, spawn_egui_camera};

pub struct DebugPlugin;

#[derive(Clone, Debug)]
pub struct LogEvent {
    file: Option<String>,
    line: Option<u32>,
    module_path: Option<String>,
    message: Option<String>,
    other_fields: Vec<(String, String)>,
    level: Level,
}

impl Visit for LogEvent {
    fn record_str(&mut self, field: &Field, value: &str) {
        match field.name() {
            "message" => {
                self.message = Some(value.to_owned());
            }
            _ => self.record_debug(field, &value),
        }
    }
    fn record_debug(&mut self, field: &Field, value: &dyn core::fmt::Debug) {
        match field.name() {
            "message" => {
                self.message = Some(format!("{:?}", value));
            }
            _ => {
                self.other_fields
                    .push((field.name().to_owned(), format!("{:?}", value)));
            }
        }
    }
}

#[derive(Resource, Default)]
pub struct DebugLogList(Vec<LogEvent>);

pub struct DebugLogLayer;

impl Layer<Registry> for DebugLogLayer {
    fn on_event(
        &self,
        event: &bevy::log::tracing::Event<'_>,
        ctx: bevy::log::tracing_subscriber::layer::Context<'_, Registry>,
    ) {
        if !event.metadata().is_event() {
            return;
        }
        let Some(cmds) = EXTERNAL_SYSTEMS_CMDS.get() else {
            return;
        };

        let mut log_event = LogEvent {
            file: event.metadata().file().map(|s| s.to_owned()),
            line: event.metadata().line(),
            other_fields: vec![],
            message: None,
            module_path: event.metadata().module_path().map(|s| s.to_owned()),
            level: *event.metadata().level(),
        };

        event.record(&mut log_event);

        cmds.send(Box::new(move |mut c| {
            c.queue(move |world: &mut World| {
                let mut list = world.resource_mut::<DebugLogList>();
                list.0.push(log_event);
            });
            Ok(())
        }))
        .ok();
    }
}

#[derive(Resource, Default)]
struct DebugUiState {
    is_open: bool,
    log_window_open: bool,
    world_tree_view_open: bool,
}

impl Plugin for DebugPlugin {
    fn build(&self, app: &mut bevy::app::App) {
        app.add_systems(Startup, spawn_egui_camera)
            .insert_resource(DebugUiState::default())
            .insert_resource(DebugLogList::default())
            .add_systems(
                Update,
                toggle_debug_system.run_if(input_just_pressed(KeyCode::Backslash)),
            )
            .add_systems(
                EguiPrimaryContextPass,
                (
                    debug_ui_log_window,
                    debug_ui_system_top_panel,
                    debug_ui_world_tree_view,
                )
                    .run_if(|resource: Res<DebugUiState>| resource.into_inner().is_open),
            );
    }
}

fn toggle_debug_system(mut resource: ResMut<DebugUiState>) {
    resource.is_open ^= true;
}

fn debug_ui_system_top_panel(
    mut contexts: EguiContexts,
    mut ui_state: ResMut<DebugUiState>,
) -> Result {
    egui::TopBottomPanel::top("top_panel").show(contexts.ctx_mut()?, |ui| {
        ui.menu_button("Widgets", |ui| {
            ui.toggle_value(&mut ui_state.log_window_open, "Log window");
        })
    });

    Ok(())
}

fn debug_ui_log_window(
    mut contexts: EguiContexts,
    mut ui_state: ResMut<DebugUiState>,
    log_list: Res<DebugLogList>,
) -> Result {
    egui::Window::new("Debug")
        .open(&mut ui_state.log_window_open)
        .show(contexts.ctx_mut()?, |ui| {
            ScrollArea::vertical().show(ui, |ui| {
                Grid::new("Log Grid")
                    .striped(true)
                    .num_columns(2)
                    .show(ui, |ui| {
                        for log in &log_list.0 {
                            log.ui(ui);
                            ui.end_row();
                        }
                    })
            });
        });
    Ok(())
}

fn debug_ui_world_tree_view(
    mut contexts: EguiContexts,
    entities: Query<Entity, Without<EguiContext>>,
    archetypes: &Archetypes,
    components: &Components,
    cmds: Commands,
    mut ui_state: ResMut<DebugUiState>,
) -> Result {
    egui::Window::new("World View")
        .open(&mut ui_state.log_window_open)
        .show(contexts.ctx_mut()?, |ui| {
            ScrollArea::vertical().show(ui, |ui| {
                Grid::new("World Grid")
                    .striped(true)
                    .num_columns(1)
                    .show(ui, |ui| {
                        for entity in entities {
                            let Some(archetype) = archetypes
                                .iter()
                                .find(|a| a.entities().iter().any(|v| v.id() == entity))
                            else {
                                continue;
                            };
                            let ent_ci = archetype.components();
                            for comp in ent_ci {
                                let Some(component_info) = components.get_info(*comp) else {
                                    continue;
                                };

                                ui.label(&component_info.name().to_string());
                            }
                            ui.end_row();
                        }
                    })
            });
        });
    Ok(())
}

impl Widget for &LogEvent {
    fn ui(self, ui: &mut egui::Ui) -> egui::Response {
        ui.label(self.level.as_str());
        let response = ui.label(
            self.message
                .as_ref()
                .map(|v| v.as_str())
                .unwrap_or("NO MESSAGE"),
        );

        response
    }
}