Skip to main content

symtropy_devconsole/
lib.rs

1// Copyright (C) 2024-2026 Tristan Stoltz / Luminous Dynamics
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3//! # symtropy-devconsole
4//!
5//! Drop-in Bevy dev console plugin. Two panels (more coming):
6//!
7//! - **Scene controls** (left) — pause/resume `Time<Virtual>`, F1 toggle hint.
8//! - **Φ Inspector** (left, feature `phi-panel`) — lists every `PhysicsBody`
9//!   entity and its current Phi value.
10//!
11//! Toggle the whole console with **F1**.
12
13use bevy::prelude::*;
14use bevy_egui::{egui, EguiContexts, EguiPlugin, EguiPrimaryContextPass};
15
16/// Toggle state for the dev console.
17#[derive(Resource, Debug, Clone, Copy)]
18pub struct DevConsoleVisible(pub bool);
19
20impl Default for DevConsoleVisible {
21    fn default() -> Self {
22        Self(true)
23    }
24}
25
26/// Pause flag for downstream physics systems.
27#[derive(Resource, Debug, Clone, Copy)]
28pub struct DevConsolePaused(pub bool);
29
30impl Default for DevConsolePaused {
31    fn default() -> Self {
32        Self(false)
33    }
34}
35
36/// Drop-in dev console plugin. See crate docs.
37#[derive(Default, Clone)]
38pub struct SymtropyDevConsolePlugin;
39
40impl Plugin for SymtropyDevConsolePlugin {
41    fn build(&self, app: &mut App) {
42        if !app.is_plugin_added::<EguiPlugin>() {
43            app.add_plugins(EguiPlugin::default());
44        }
45        app.init_resource::<DevConsoleVisible>()
46            .init_resource::<DevConsolePaused>()
47            .add_systems(Update, toggle_console_keybind)
48            .add_systems(
49                EguiPrimaryContextPass,
50                (
51                    scene_controls_panel,
52                    #[cfg(feature = "phi-panel")]
53                    phi_inspector_panel,
54                )
55                    .chain(),
56            );
57    }
58}
59
60fn toggle_console_keybind(keys: Res<ButtonInput<KeyCode>>, mut vis: ResMut<DevConsoleVisible>) {
61    if keys.just_pressed(KeyCode::F1) {
62        vis.0 = !vis.0;
63    }
64}
65
66fn scene_controls_panel(
67    mut contexts: EguiContexts,
68    vis: Res<DevConsoleVisible>,
69    mut paused: ResMut<DevConsolePaused>,
70    mut time: ResMut<Time<Virtual>>,
71) {
72    if !vis.0 {
73        return;
74    }
75    let Ok(ctx) = contexts.ctx_mut() else {
76        return;
77    };
78    egui::SidePanel::left("dev_console_scene_controls")
79        .default_width(220.0)
80        .show(ctx, |ui| {
81            ui.heading("Scene");
82            ui.separator();
83            let label = if paused.0 { "▶ Resume" } else { "⏸ Pause" };
84            if ui.button(label).clicked() {
85                paused.0 = !paused.0;
86                if paused.0 {
87                    time.pause();
88                } else {
89                    time.unpause();
90                }
91            }
92            ui.separator();
93            ui.label("F1: Toggle Console");
94        });
95}
96
97#[cfg(feature = "phi-panel")]
98fn phi_inspector_panel(
99    mut contexts: EguiContexts,
100    vis: Res<DevConsoleVisible>,
101    query: Query<(Entity, &symtropy_bevy_core::PhysicsBody)>,
102) {
103    if !vis.0 {
104        return;
105    }
106    let Ok(ctx) = contexts.ctx_mut() else {
107        return;
108    };
109    egui::SidePanel::left("dev_console_phi")
110        .default_width(220.0)
111        .show(ctx, |ui| {
112            ui.heading("Φ Inspector");
113            ui.label(format!(
114                "{} body(ies)",
115                query.iter().count()
116            ));
117            ui.separator();
118            ui.label("Phi tracking decoupled to break cycles.");
119        });
120}