make debug console top level

This commit is contained in:
Schrottkatze 2025-06-18 17:13:52 +02:00
parent d79475ae2a
commit 4c24f67cd5
Signed by: schrottkatze
SSH key fingerprint: SHA256:FPOYVeBy3QP20FEM42uWF1Wa/Qhlk+L3S2+Wuau/Auo
9 changed files with 111 additions and 68 deletions

View file

@ -1,19 +0,0 @@
use bevy::prelude::*;
use clap::Subcommand;
mod console;
#[derive(Event, Debug, Subcommand)]
enum DebugEvent {
/// Close the debug console.
#[command(name = "close", aliases = ["close-console"])]
CloseDebugConsole,
/// Output a string to the console.
#[command(name = "echo", aliases = ["print", "print-to-console"])]
PrintToConsole { text: String },
}
pub(super) fn plugin(app: &mut App) {
app.add_plugins(console::plugin).add_event::<DebugEvent>();
}

View file

@ -1,105 +0,0 @@
use bevy::prelude::*;
use ui::{
DebugConsole, autofocus, autoscroll, handle_open_console, process_prompt, update_content,
update_scroll_position,
};
use crate::{AppState, cleanup::despawn, game::GameplaySet};
use super::DebugEvent;
mod cli;
mod ui;
pub(super) fn plugin(app: &mut App) {
app.init_state::<ConsoleState>()
.add_systems(
OnEnter(ConsoleState::Open),
(open_console, (autofocus, autoscroll).after(open_console)),
)
.add_systems(
Update,
(
handle_open_console
.in_set(GameplaySet)
.run_if(in_state(ConsoleState::Closed).and(in_state(AppState::Ingame))),
update_content.run_if(resource_changed::<ConsoleLog>),
(
update_scroll_position,
process_prompt,
execute_console_events,
)
.run_if(in_state(ConsoleState::Open)),
),
)
.add_systems(OnExit(ConsoleState::Open), despawn::<DebugConsole>)
.insert_resource::<ConsoleLog>(ConsoleLog::new());
}
/// the usize is a read index
#[derive(Resource, Clone)]
struct ConsoleLog(Vec<ConsoleEvent>, usize);
impl ConsoleLog {
fn new() -> Self {
Self(
vec![ConsoleEvent::Output(String::from(
"Welcome to the dev console :3\n (i should probably put some proper info into this some time)",
))],
1,
)
}
fn unread(&mut self) -> &[ConsoleEvent] {
let res = &self.0[self.1..];
self.1 = self.0.len();
res
}
pub fn input(&mut self, content: &str) {
self.0.push(ConsoleEvent::Input(content.to_string()))
}
pub fn output(&mut self, content: &str) {
self.0.push(ConsoleEvent::Output(content.to_string()))
}
pub fn error(&mut self, content: &str) {
self.0.push(ConsoleEvent::Error(content.to_string()))
}
}
#[derive(Clone, Debug)]
enum ConsoleEvent {
Input(String),
Output(String),
Error(String),
}
const OPEN_CONSOLE_DEFAULT: KeyCode = KeyCode::KeyC;
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash, Reflect)]
enum ConsoleState {
#[default]
Closed,
Open,
}
fn open_console(mut c: Commands, console_data: Res<ConsoleLog>, asset_server: Res<AssetServer>) {
log::info!("opening debug console...");
c.spawn(ui::console(console_data.clone(), asset_server.clone()));
}
fn execute_console_events(
mut ev_reader: EventReader<DebugEvent>,
mut log: ResMut<ConsoleLog>,
mut next_state: ResMut<NextState<ConsoleState>>,
) {
for ev in ev_reader.read() {
match ev {
DebugEvent::CloseDebugConsole => next_state.set(ConsoleState::Closed),
DebugEvent::PrintToConsole { text } => log.output(text),
_ => todo!(),
};
}
}

View file

@ -1,28 +0,0 @@
use clap::{Parser, Subcommand};
use crate::game::debug::DebugEvent;
pub(super) fn respond(line: &str) -> Result<DebugEvent, String> {
let args = shlex::split(line).ok_or("Invalid Quoting")?;
let cli = Cli::try_parse_from(args).map_err(|e| e.to_string());
// help detection
let command_contains_help = (line.contains("help") || line.contains("-h"));
let some_line_starts_with_usage = cli
.as_ref()
.is_err_and(|item| item.lines().any(|line| line.starts_with("Usage: ")));
if command_contains_help && some_line_starts_with_usage {
let Err(text) = cli else { unreachable!() };
return Ok(DebugEvent::PrintToConsole { text });
}
Ok(cli?.cmd)
}
#[derive(Debug, Parser)]
#[command(multicall = true)]
struct Cli {
#[command(subcommand)]
cmd: DebugEvent,
}

View file

@ -1,112 +0,0 @@
use bevy::{
input::mouse::{MouseScrollUnit, MouseWheel},
input_focus::InputFocus,
picking::hover::HoverMap,
prelude::*,
};
mod components;
#[derive(Component)]
pub(super) struct DebugConsole;
#[derive(Component)]
struct TextInput;
use bevy_ui_text_input::TextSubmitEvent;
pub(super) use components::console;
use components::{Content, Prompt};
use crate::game::debug::DebugEvent;
use super::{ConsoleEvent, ConsoleLog, ConsoleState, OPEN_CONSOLE_DEFAULT, cli::respond};
// stolen from scrolling example
pub fn update_scroll_position(
mut mouse_wheel_events: EventReader<MouseWheel>,
hover_map: Res<HoverMap>,
mut scrolled_node_query: Query<&mut ScrollPosition>,
) {
for mouse_wheel_event in mouse_wheel_events.read() {
let (dx, dy) = match mouse_wheel_event.unit {
MouseScrollUnit::Line => (mouse_wheel_event.x * 18., mouse_wheel_event.y * 18.),
MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y),
};
for (_pointer, pointer_map) in hover_map.iter() {
for (entity, _hit) in pointer_map.iter() {
if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) {
scroll_position.offset_x -= dx;
scroll_position.offset_y -= dy;
}
}
}
}
}
pub fn autofocus(mut input_focus: ResMut<InputFocus>, prompt: Single<Entity, With<Prompt>>) {
input_focus.set(*prompt);
}
pub fn autoscroll(mut scrollables: Single<&mut ScrollPosition, With<Content>>) {
scrollables.offset_y = f32::MAX;
}
pub fn handle_open_console(
kb_input: Res<ButtonInput<KeyCode>>,
mut console_open_state: ResMut<NextState<ConsoleState>>,
) {
if kb_input.pressed(OPEN_CONSOLE_DEFAULT) {
console_open_state.set(ConsoleState::Open);
}
}
pub fn process_prompt(
mut log: ResMut<ConsoleLog>,
mut submit_reader: EventReader<TextSubmitEvent>,
mut debug_event_writer: EventWriter<DebugEvent>,
prompt: Single<Entity, With<Prompt>>,
) {
for submit in submit_reader.read() {
if submit.entity == *prompt && !submit.text.trim().is_empty() {
if submit.text.trim() != "close" {
log.input(&submit.text);
}
match respond(&submit.text) {
Ok(debug_ev) => {
debug_event_writer.write(debug_ev);
}
Err(e) => log.error(&e),
}
}
}
}
pub fn update_content(
mut c: Commands,
content: Single<Entity, With<Content>>,
mut log: ResMut<ConsoleLog>,
asset_server: Res<AssetServer>,
mut scrollables: Single<&mut ScrollPosition, With<Content>>,
) {
c.entity(*content).with_children(|parent| {
for item in log.unread() {
match dbg!(item) {
ConsoleEvent::Input(s) => {
parent.spawn(components::input(s.to_string(), &asset_server));
}
ConsoleEvent::Output(s) => {
parent.spawn(components::output(s.to_string(), &asset_server));
}
ConsoleEvent::Error(s) => {
parent.spawn(components::error(
// dirty hack so they're not called subcommands
s.replace("subcommand", "command").to_string(),
&asset_server,
));
}
_ => {}
};
}
});
scrollables.offset_y = f32::MAX;
}

View file

@ -1,181 +0,0 @@
use bevy::{ecs::spawn::SpawnWith, prelude::*};
use bevy_ui_text_input::{TextInputMode, TextInputNode, TextInputPrompt};
use crate::game::debug::console::{ConsoleEvent, ConsoleLog};
use super::DebugConsole;
#[derive(Component)]
pub struct Prompt;
#[derive(Component)]
pub struct Content;
pub fn console(data: ConsoleLog, asset_server: AssetServer) -> impl Bundle {
(
DebugConsole,
Node {
justify_self: JustifySelf::Center,
width: Val::Percent(85.),
max_height: Val::Percent(100. / 3.),
flex_direction: FlexDirection::Column,
border: UiRect {
left: Val::Px(2.),
right: Val::Px(2.),
top: Val::Px(0.),
bottom: Val::Px(2.),
},
..default()
},
BorderRadius::bottom(Val::Px(10.)),
BorderColor(Color::BLACK),
ZIndex(i32::MAX),
BackgroundColor(Color::Srgba(Srgba::BLACK.with_alpha(0.2))),
children![
(
Content,
Node {
padding: UiRect::all(Val::Px(10.)),
overflow: Overflow::scroll_y(),
flex_direction: FlexDirection::Column,
..default()
},
Children::spawn(SpawnWith({
let asset_server = asset_server.clone();
move |parent: &mut ChildSpawner| {
for item in data.0 {
match item {
ConsoleEvent::Input(s) => {
parent.spawn(input(s.to_string(), &asset_server))
}
ConsoleEvent::Output(s) => {
parent.spawn(output(s.to_string(), &asset_server))
}
ConsoleEvent::Error(s) => {
parent.spawn(error(s.to_string(), &asset_server))
}
};
}
}
}))
),
(
Node {
width: Val::Percent(100.),
padding: UiRect {
left: Val::Px(10.),
right: Val::Px(10.),
top: Val::Px(10.),
bottom: Val::Px(10.)
},
..default()
},
BackgroundColor(Color::Srgba(Srgba::gray(0.2).with_alpha(0.3))),
children![
(
Text::new("> "),
TextFont {
font: asset_server
.load("fonts/DepartureMono-1.500/DepartureMono-Regular.otf"),
font_size: 14.,
..default()
}
),
(
Prompt,
Node {
width: Val::Auto,
flex_grow: 1.,
..default()
},
TextInputNode {
clear_on_submit: true,
mode: TextInputMode::SingleLine,
is_enabled: true,
focus_on_pointer_down: true,
..default()
},
TextInputPrompt {
text: String::new(),
..default()
},
TextFont {
font: asset_server
.load("fonts/DepartureMono-1.500/DepartureMono-Regular.otf"),
font_size: 14.,
..default()
},
)
]
),
],
)
}
pub fn input(content: String, asset_server: &AssetServer) -> impl Bundle {
console_output(
Srgba::GREEN.with_alpha(0.05),
UiRect::all(Val::Px(1.)),
Srgba::GREEN,
format!("> {content}"),
asset_server,
)
}
pub fn output(content: String, asset_server: &AssetServer) -> impl Bundle {
console_output(
Srgba::WHITE.with_alpha(0.05),
UiRect::all(Val::ZERO),
Srgba::NONE,
content,
asset_server,
)
}
pub fn error(content: String, asset_server: &AssetServer) -> impl Bundle {
console_output(
Srgba::RED.with_alpha(0.025),
UiRect::all(Val::Px(2.)),
Srgba::RED.with_alpha(0.75),
content,
asset_server,
)
}
fn console_output(
background_color: Srgba,
border_width: UiRect,
border_color: Srgba,
content: String,
asset_server: &AssetServer,
) -> impl Bundle {
(
Node {
margin: UiRect {
left: Val::Px(0.),
right: Val::Px(0.),
top: Val::Px(0.),
bottom: Val::Px(10.),
},
padding: UiRect::all(Val::Px(7.5)),
border: border_width,
..default()
},
BorderRadius::all(Val::Px(7.5)),
BorderColor(Color::Srgba(border_color)),
BackgroundColor(Color::Srgba(background_color)),
Pickable {
should_block_lower: false,
..default()
},
children![(
Text(content),
TextFont {
font: asset_server.load("fonts/DepartureMono-1.500/DepartureMono-Regular.otf"),
font_size: 14.,
..default()
},
Pickable {
should_block_lower: false,
..default()
},
)],
)
}