//! This app demonstrates: //! //! - Using ScheduleRunnerPlugin to run the bevy app loop without a window. //! - Using the RatatuiContext resource to draw widgets to the terminal. //! - Using Events to read input and communicate between systems. //! //! Keys: //! - Left & Right: modify the counter //! - Q or Esc: quit //! - P: panic (tests the color_eyre panic hooks) use core::panic; #[cfg(not(feature = "windowed"))] use std::time::Duration; use bevy::{app::AppExit, diagnostic::FrameCount, prelude::*}; #[cfg(not(feature = "windowed"))] use bevy::{app::ScheduleRunnerPlugin, state::app::StatesPlugin}; #[cfg(not(feature = "windowed"))] use bevy_ratatui::event::KeyMessage; use bevy_ratatui::{RatatuiContext, RatatuiPlugins}; #[cfg(not(feature = "windowed"))] use ratatui::crossterm::event::KeyEventKind; use ratatui::widgets::{FrameExt, Widget}; use ratatui::{ buffer::Buffer, layout::Rect, style::{Color, Style}, text::Line, widgets::WidgetRef, }; fn main() { let mut app = App::new(); #[cfg(not(feature = "windowed"))] app.add_plugins(( MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32( 1. / 60., ))), StatesPlugin, RatatuiPlugins::default(), )) .add_systems(PreUpdate, keyboard_input_system); #[cfg(feature = "windowed")] app.add_plugins(( DefaultPlugins.set(ImagePlugin::default_nearest()), RatatuiPlugins { enable_input_forwarding: true, ..default() }, )) .add_systems(PreUpdate, keyboard_input_system_windowed); app.init_resource::() .init_resource::() .init_state::() .add_message::() .add_systems( Update, (ui_system, update_counter_system, background_color_system), ) .add_systems(OnEnter(AppState::Negative), start_background_color_timer) .add_systems(OnEnter(AppState::Positive), start_background_color_timer) .run(); } fn ui_system( mut context: ResMut, frame_count: Res, counter: Res, app_state: Res>, bg_color: Res, ) -> Result { context.draw(|frame| { let area = frame.area(); let frame_count = Line::from(format!("Frame Count: {}", frame_count.0)).right_aligned(); frame.render_widget_ref(bg_color.as_ref(), area); frame.render_widget(frame_count, area); frame.render_widget_ref(counter.as_ref(), area); frame.render_widget_ref(app_state.get(), area) })?; Ok(()) } #[cfg(not(feature = "windowed"))] fn keyboard_input_system( mut key_messages: MessageReader, mut app_exit: MessageWriter, mut counter_messages: MessageWriter, ) { use ratatui::crossterm::event::KeyCode; for message in key_messages.read() { if let KeyEventKind::Release = message.kind { continue; } match message.code { KeyCode::Char('q') | KeyCode::Esc => { app_exit.write_default(); } KeyCode::Char('p') => { panic!("Panic!"); } KeyCode::Left => { counter_messages.write(CounterMessage::Decrement); } KeyCode::Right => { counter_messages.write(CounterMessage::Increment); } _ => {} } } } #[cfg(feature = "windowed")] fn keyboard_input_system_windowed( keys: Res>, mut app_exit: MessageWriter, mut counter_messages: MessageWriter, ) { if keys.just_pressed(KeyCode::KeyQ) { app_exit.write_default(); } if keys.just_pressed(KeyCode::KeyP) { panic!("Panic!"); } if keys.pressed(KeyCode::ArrowLeft) { counter_messages.write(CounterMessage::Decrement); } if keys.pressed(KeyCode::ArrowRight) { counter_messages.write(CounterMessage::Increment); } } #[derive(Default, Resource, Debug, Deref, DerefMut)] struct Counter(i32); impl Counter { fn decrement(&mut self) { self.0 -= 1; } fn increment(&mut self) { self.0 += 1; } } #[derive(Message, Clone, Copy, PartialEq, Eq, Debug)] enum CounterMessage { Increment, Decrement, } fn update_counter_system( mut counter: ResMut, mut counter_messages: MessageReader, mut app_state: ResMut>, ) { for message in counter_messages.read() { match message { CounterMessage::Increment => counter.increment(), CounterMessage::Decrement => counter.decrement(), } // demonstrates changing something in the app state based on the counter value if counter.0 < 0 { app_state.set(AppState::Negative); } else { app_state.set(AppState::Positive); } } } impl WidgetRef for &Counter { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let counter = format!("Counter: {}", self.0); Line::from(counter).render(area, buf); } } #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] enum AppState { Negative, #[default] Positive, } impl WidgetRef for &AppState { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let state = match self { AppState::Negative => "Negative", AppState::Positive => "Positive", }; Line::from(state).centered().render(area, buf); } } #[derive(Debug, Component, Deref, DerefMut)] struct ColorChangeTimer { #[deref] timer: Timer, start_color: Color, } fn start_background_color_timer(mut commands: Commands, bg_color: Res) { commands.spawn(ColorChangeTimer { timer: Timer::from_seconds(2.0, TimerMode::Once), start_color: bg_color.0, }); } #[derive(Debug, Resource, Deref, DerefMut)] struct BackgroundColor(Color); impl Default for BackgroundColor { fn default() -> Self { BackgroundColor(Color::Rgb(0, 0, 0)) } } impl WidgetRef for &BackgroundColor { fn render_ref(&self, area: Rect, buf: &mut Buffer) { buf.set_style(area, Style::new().bg(self.0)); } } /// Change the background color over time when the app state changes from negative to positive /// or vice versa. fn background_color_system( time: Res