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, Constraint, Direction, Layout};
18use ratatui::style::Style;
19use ratatui::style::Stylize;
20use ratatui::widgets::{Block, Padding};
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 .set(ImagePlugin::default_nearest())
35 .disable::<WinitPlugin>()
36 .disable::<LogPlugin>(),
37 ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1. / 60.)),
38 FrameTimeDiagnosticsPlugin,
39 RatatuiPlugins::default(),
40 RatatuiRenderPlugin::new("top_left", (128, 128)),
41 RatatuiRenderPlugin::new("top_right", (128, 128)),
42 RatatuiRenderPlugin::new("bottom", (256, 128)),
43 ))
44 .insert_resource(Flags::default())
45 .insert_resource(InputState::Idle)
46 .insert_resource(ClearColor(Color::BLACK))
47 .add_systems(Startup, setup_scene_system)
48 .add_systems(Update, draw_scene_system.map(error))
49 .add_systems(Update, handle_input_system)
50 .add_systems(Update, rotate_cube_system.after(handle_input_system))
51 .run();
52}
53
54fn setup_scene_system(
55 mut commands: Commands,
56 mut meshes: ResMut<Assets<Mesh>>,
57 mut materials: ResMut<Assets<StandardMaterial>>,
58 ratatui_render: Res<RatatuiRenderContext>,
59) {
60 commands.spawn((
61 Cube,
62 Mesh3d(meshes.add(Cuboid::default())),
63 MeshMaterial3d(materials.add(StandardMaterial {
64 base_color: Color::srgb(0.4, 0.54, 0.7),
65 ..Default::default()
66 })),
67 ));
68 commands.spawn((
69 PointLight {
70 shadows_enabled: true,
71 ..Default::default()
72 },
73 Transform::from_xyz(3., 4., 6.),
74 ));
75 commands.spawn((
76 Camera3d::default(),
77 Camera {
78 target: ratatui_render.target("top_left").unwrap(),
79 ..default()
80 },
81 Transform::from_xyz(0., 3., 0.).looking_at(Vec3::ZERO, Vec3::Z),
82 ));
83 commands.spawn((
84 Camera3d::default(),
85 Camera {
86 target: ratatui_render.target("top_right").unwrap(),
87 ..default()
88 },
89 Transform::from_xyz(0., 0., 3.).looking_at(Vec3::ZERO, Vec3::Z),
90 ));
91 commands.spawn((
92 Camera3d::default(),
93 Camera {
94 target: ratatui_render.target("bottom").unwrap(),
95 ..default()
96 },
97 Transform::from_xyz(2., 2., 2.).looking_at(Vec3::ZERO, Vec3::Z),
98 ));
99}
100
101fn draw_scene_system(
102 mut ratatui: ResMut<RatatuiContext>,
103 ratatui_render: Res<RatatuiRenderContext>,
104 flags: Res<Flags>,
105 diagnostics: Res<DiagnosticsStore>,
106 kitty_enabled: Option<Res<KittyEnabled>>,
107) -> io::Result<()> {
108 ratatui.draw(|frame| {
109 let mut block = Block::bordered()
110 .bg(ratatui::style::Color::Rgb(0, 0, 0))
111 .border_style(Style::default().bg(ratatui::style::Color::Rgb(0, 0, 0)));
112
113 let bottom_block = block.clone();
114 let top_left_block = block.clone();
115 let top_right_block = block.clone();
116
117 block = block
118 .padding(Padding::proportional(1))
119 .title_bottom("[q for quit]")
120 .title_bottom("[d for debug]")
121 .title_alignment(Alignment::Center);
122
123 if flags.debug {
124 block = block.title_top(format!(
125 "[kitty protocol: {}]",
126 if kitty_enabled.is_some() {
127 "enabled"
128 } else {
129 "disabled"
130 }
131 ));
132
133 if let Some(value) = diagnostics
134 .get(&FrameTimeDiagnosticsPlugin::FPS)
135 .and_then(|fps| fps.smoothed())
136 {
137 block = block.title_top(format!("[fps: {value:.0}]"));
138 }
139 }
140
141 let layout = Layout::default()
142 .direction(Direction::Vertical)
143 .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
144 .split(block.inner(frame.area()));
145
146 let top_layout = Layout::default()
147 .direction(Direction::Horizontal)
148 .constraints(vec![
149 Constraint::Percentage(50),
150 Constraint::Length(1),
151 Constraint::Percentage(50),
152 ])
153 .split(layout[0]);
154
155 let top_left = top_layout[0];
156 let top_right = top_layout[2];
157 let bottom = layout[1];
158
159 let inner_top_left = top_left_block.inner(top_left);
160 let inner_top_right = top_right_block.inner(top_right);
161 let inner_bottom = bottom_block.inner(bottom);
162
163 let top_left_widget = ratatui_render.widget("top_left").unwrap();
164 let top_right_widget = ratatui_render.widget("top_right").unwrap();
165 let bottom_widget = ratatui_render.widget("bottom").unwrap();
166
167 frame.render_widget(block, frame.area());
168 frame.render_widget(top_left_block, top_left);
169 frame.render_widget(bottom_block, top_right);
170 frame.render_widget(top_right_block, bottom);
171 frame.render_widget(top_left_widget, inner_top_left);
172 frame.render_widget(top_right_widget, inner_top_right);
173 frame.render_widget(bottom_widget, inner_bottom);
174 })?;
175
176 Ok(())
177}
178
179#[derive(Resource)]
180pub enum InputState {
181 None,
182 Idle,
183 Left(f32),
184 Right(f32),
185}
186
187pub fn handle_input_system(
188 mut ratatui_events: EventReader<KeyEvent>,
189 mut exit: EventWriter<AppExit>,
190 mut flags: ResMut<Flags>,
191 mut input: ResMut<InputState>,
192) {
193 for key_event in ratatui_events.read() {
194 match key_event.kind {
195 KeyEventKind::Press | KeyEventKind::Repeat => match key_event.code {
196 KeyCode::Char('q') => {
197 exit.send_default();
198 }
199 KeyCode::Char('d') => {
200 flags.debug = !flags.debug;
201 }
202 KeyCode::Left => {
203 *input = InputState::Left(0.75);
204 }
205 KeyCode::Right => {
206 *input = InputState::Right(0.75);
207 }
208 _ => {}
209 },
210 KeyEventKind::Release => match key_event.code {
211 KeyCode::Left => {
212 if let InputState::Left(_) = *input {
213 *input = InputState::None;
214 }
215 }
216 KeyCode::Right => {
217 if let InputState::Right(_) = *input {
218 *input = InputState::None;
219 }
220 }
221 _ => {}
222 },
223 }
224 }
225}
226
227fn rotate_cube_system(
228 time: Res<Time>,
229 mut cube: Query<&mut Transform, With<Cube>>,
230 mut input: ResMut<InputState>,
231) {
232 match *input {
233 InputState::Idle => {
234 cube.single_mut().rotate_z(time.delta_secs());
235 }
236 InputState::Left(duration) => {
237 cube.single_mut()
238 .rotate_z(-time.delta_secs() * duration.min(0.25) * 4.);
239 let new_duration = (duration - time.delta_secs()).max(0.);
240 *input = if new_duration > 0. {
241 InputState::Left(new_duration)
242 } else {
243 InputState::None
244 }
245 }
246 InputState::Right(duration) => {
247 cube.single_mut()
248 .rotate_z(time.delta_secs() * duration.min(0.25) * 4.);
249 let new_duration = (duration - time.delta_secs()).max(0.);
250 *input = if new_duration > 0. {
251 InputState::Right(new_duration)
252 } else {
253 InputState::None
254 }
255 }
256 InputState::None => {}
257 }
258}