multiple/
multiple.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, 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}