cube/
cube.rs

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}