1use std::io;
2use std::time::Duration;
3
4use bevy::app::AppExit;
5use bevy::app::ScheduleRunnerPlugin;
6use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
7use bevy::log::LogPlugin;
8use bevy::prelude::*;
9use bevy::utils::error;
10use bevy::winit::WinitPlugin;
11use bevy_ratatui::event::KeyEvent;
12use bevy_ratatui::kitty::KittyEnabled;
13use bevy_ratatui::terminal::RatatuiContext;
14use bevy_ratatui::RatatuiPlugins;
15use bevy_ratatui_render::{RatatuiRenderContext, RatatuiRenderPlugin};
16use crossterm::event::{KeyCode, KeyEventKind};
17use ratatui::layout::Alignment;
18use ratatui::style::Style;
19use ratatui::style::Stylize;
20use ratatui::widgets::Block;
21
22#[derive(Component)]
23pub struct Cube;
24
25#[derive(Resource, Default)]
26pub struct Flags {
27 debug: bool,
28}
29
30fn main() {
31 App::new()
32 .add_plugins((
33 DefaultPlugins
34 .build()
35 .disable::<WinitPlugin>()
36 .disable::<LogPlugin>(),
37 ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1. / 60.)),
38 FrameTimeDiagnosticsPlugin,
39 RatatuiPlugins::default(),
40 RatatuiRenderPlugin::new("main", (256, 256)).autoresize(),
41 ))
42 .insert_resource(Flags::default())
43 .insert_resource(InputState::Idle)
44 .insert_resource(ClearColor(Color::BLACK))
45 .add_systems(Startup, setup_scene_system)
46 .add_systems(Update, draw_scene_system.map(error))
47 .add_systems(PreUpdate, handle_input_system)
48 .add_systems(Update, rotate_cube_system.after(handle_input_system))
49 .run();
50}
51
52fn setup_scene_system(
53 mut commands: Commands,
54 mut meshes: ResMut<Assets<Mesh>>,
55 mut materials: ResMut<Assets<StandardMaterial>>,
56 ratatui_render: Res<RatatuiRenderContext>,
57) {
58 commands.spawn((
59 Cube,
60 Mesh3d(meshes.add(Cuboid::default())),
61 MeshMaterial3d(materials.add(StandardMaterial {
62 base_color: Color::srgb(0.4, 0.54, 0.7),
63 ..Default::default()
64 })),
65 ));
66 commands.spawn((
67 Mesh3d(meshes.add(Cuboid::new(15., 15., 1.))),
68 Transform::from_xyz(0., 0., -6.),
69 ));
70 commands.spawn((
71 PointLight {
72 shadows_enabled: true,
73 ..Default::default()
74 },
75 Transform::from_xyz(3., 4., 6.),
76 ));
77 commands.spawn((
78 Camera3d::default(),
79 Camera {
80 target: ratatui_render.target("main").unwrap(),
81 ..default()
82 },
83 Transform::from_xyz(3., 3., 3.).looking_at(Vec3::ZERO, Vec3::Z),
84 ));
85}
86
87fn draw_scene_system(
88 mut ratatui: ResMut<RatatuiContext>,
89 rat_render: Res<RatatuiRenderContext>,
90 flags: Res<Flags>,
91 diagnostics: Res<DiagnosticsStore>,
92 kitty_enabled: Option<Res<KittyEnabled>>,
93) -> io::Result<()> {
94 ratatui.draw(|frame| {
95 let mut block = Block::bordered()
96 .bg(ratatui::style::Color::Rgb(0, 0, 0))
97 .border_style(Style::default().bg(ratatui::style::Color::Rgb(0, 0, 0)))
98 .title_bottom("[q for quit]")
99 .title_bottom("[d for debug]")
100 .title_bottom("[p for panic]")
101 .title_alignment(Alignment::Center);
102
103 let inner = block.inner(frame.area());
104
105 if flags.debug {
106 block = block.title_top(format!(
107 "[kitty protocol: {}]",
108 if kitty_enabled.is_some() {
109 "enabled"
110 } else {
111 "disabled"
112 }
113 ));
114
115 if let Some(value) = diagnostics
116 .get(&FrameTimeDiagnosticsPlugin::FPS)
117 .and_then(|fps| fps.smoothed())
118 {
119 block = block.title_top(format!("[fps: {value:.0}]"));
120 }
121 }
122
123 frame.render_widget(block, frame.area());
124 frame.render_widget(rat_render.widget("main").unwrap(), inner);
125 })?;
126
127 Ok(())
128}
129
130#[derive(Resource)]
131pub enum InputState {
132 None,
133 Idle,
134 Left(f32),
135 Right(f32),
136}
137
138pub fn handle_input_system(
139 mut rat_events: EventReader<KeyEvent>,
140 mut exit: EventWriter<AppExit>,
141 mut flags: ResMut<Flags>,
142 mut input: ResMut<InputState>,
143) {
144 for key_event in rat_events.read() {
145 match key_event.kind {
146 KeyEventKind::Press | KeyEventKind::Repeat => match key_event.code {
147 KeyCode::Char('q') => {
148 exit.send_default();
149 }
150 KeyCode::Char('p') => {
151 panic!("Panic!");
152 }
153 KeyCode::Char('d') => {
154 flags.debug = !flags.debug;
155 }
156 KeyCode::Left => {
157 *input = InputState::Left(0.75);
158 }
159 KeyCode::Right => {
160 *input = InputState::Right(0.75);
161 }
162 _ => {}
163 },
164 KeyEventKind::Release => match key_event.code {
165 KeyCode::Left => {
166 if let InputState::Left(_) = *input {
167 *input = InputState::None;
168 }
169 }
170 KeyCode::Right => {
171 if let InputState::Right(_) = *input {
172 *input = InputState::None;
173 }
174 }
175 _ => {}
176 },
177 }
178 }
179}
180
181fn rotate_cube_system(
182 time: Res<Time>,
183 mut cube: Query<&mut Transform, With<Cube>>,
184 mut input: ResMut<InputState>,
185) {
186 match *input {
187 InputState::Idle => {
188 cube.single_mut().rotate_z(time.delta_secs());
189 }
190 InputState::Left(duration) => {
191 cube.single_mut()
192 .rotate_z(-time.delta_secs() * duration.min(0.25) * 4.);
193 let new_duration = (duration - time.delta_secs()).max(0.);
194 *input = if new_duration > 0. {
195 InputState::Left(new_duration)
196 } else {
197 InputState::None
198 }
199 }
200 InputState::Right(duration) => {
201 cube.single_mut()
202 .rotate_z(time.delta_secs() * duration.min(0.25) * 4.);
203 let new_duration = (duration - time.delta_secs()).max(0.);
204 *input = if new_duration > 0. {
205 InputState::Right(new_duration)
206 } else {
207 InputState::None
208 }
209 }
210 InputState::None => {}
211 }
212}