bevy_ratatui_camera 0.16.0

A bevy plugin for rendering your bevy app to the terminal using ratatui.
Documentation
use std::time::Duration;

use bevy::app::ScheduleRunnerPlugin;
use bevy::color::Color;
use bevy::diagnostic::DiagnosticsStore;
use bevy::diagnostic::FrameTimeDiagnosticsPlugin;
use bevy::ecs::system::SystemState;
use bevy::log::LogPlugin;
use bevy::prelude::*;
use bevy::winit::WinitPlugin;
use bevy_ratatui::RatatuiContext;
use bevy_ratatui::RatatuiPlugins;
use bevy_ratatui::event::KeyMessage;
use bevy_ratatui::kitty::KittyEnabled;
use bevy_ratatui_camera::HalfBlocksConfig;
use bevy_ratatui_camera::LuminanceConfig;
use bevy_ratatui_camera::RatatuiCamera;
use bevy_ratatui_camera::RatatuiCameraEdgeDetection;
use bevy_ratatui_camera::RatatuiCameraPlugin;
use bevy_ratatui_camera::RatatuiCameraStrategy;
use bevy_ratatui_camera::RatatuiCameraWidget;
use crossterm::event::KeyCode;
use crossterm::event::KeyEventKind;
use log::LevelFilter;
use ratatui::widgets::Widget;

mod shared;

fn main() {
    shared::setup_tui_logger(LevelFilter::Info);

    App::new()
        .add_plugins((
            DefaultPlugins
                .build()
                .disable::<WinitPlugin>()
                .disable::<LogPlugin>(),
            ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1. / 60.)),
            FrameTimeDiagnosticsPlugin {
                smoothing_factor: 1.0,
                ..default()
            },
            RatatuiPlugins::default(),
            RatatuiCameraPlugin,
        ))
        .init_resource::<shared::Flags>()
        .init_resource::<shared::InputState>()
        .insert_resource(ClearColor(Color::BLACK))
        .add_systems(Startup, setup_scene_system)
        .add_systems(Update, draw_scene_system)
        .add_systems(PreUpdate, shared::handle_input_system)
        .add_systems(Update, shared::rotate_spinners_system)
        .add_systems(Update, handle_input_system)
        .run();
}

fn setup_scene_system(
    mut commands: Commands,
    meshes: ResMut<Assets<Mesh>>,
    materials: ResMut<Assets<StandardMaterial>>,
) {
    shared::spawn_3d_scene(commands.reborrow(), meshes, materials);

    commands.spawn((
        RatatuiCamera::default(),
        Camera3d::default(),
        Transform::from_xyz(2.5, 2.5, 2.5).looking_at(Vec3::ZERO, Vec3::Z),
    ));
}

fn draw_scene_system(
    mut ratatui: ResMut<RatatuiContext>,
    mut camera_widget: Single<&mut RatatuiCameraWidget>,
    flags: Res<shared::Flags>,
    diagnostics: Res<DiagnosticsStore>,
    kitty_enabled: Option<Res<KittyEnabled>>,
) -> Result {
    ratatui.draw(|frame| {
        let area = shared::debug_frame(frame, &flags, &diagnostics, kitty_enabled.as_deref());

        camera_widget.render(area, frame.buffer_mut());
    })?;

    Ok(())
}

#[derive(Resource, Default, Clone)]
pub enum CameraState {
    #[default]
    Start,
    SwitchedStrategy,
    AddedEdges,
    ChangedCharacters,
    ChangedEdgeColor,
}

pub fn handle_input_system(
    world: &mut World,
    system_state: &mut SystemState<MessageReader<KeyMessage>>,
    mut camera_state: Local<CameraState>,
) -> Result {
    let mut message_reader = system_state.get_mut(world);
    let messages: Vec<_> = message_reader.read().cloned().collect();

    for key_message in messages.iter() {
        if let KeyEventKind::Press = key_message.kind {
            if let KeyCode::Char(' ') = key_message.code {
                match *camera_state {
                    CameraState::Start => {
                        world.run_system_cached(toggle_ratatui_camera_strategy)?;
                        *camera_state = CameraState::SwitchedStrategy;
                    }
                    CameraState::SwitchedStrategy => {
                        world.run_system_cached(toggle_edge_detection_system)?;
                        *camera_state = CameraState::AddedEdges;
                    }
                    CameraState::AddedEdges => {
                        world.run_system_cached(modify_ratatui_camera_strategy)?;
                        *camera_state = CameraState::ChangedCharacters;
                    }
                    CameraState::ChangedCharacters => {
                        world.run_system_cached(modify_edge_detection_system)?;
                        *camera_state = CameraState::ChangedEdgeColor;
                    }
                    CameraState::ChangedEdgeColor => {
                        world.run_system_cached(toggle_edge_detection_system)?;
                        world.run_system_cached(toggle_ratatui_camera_strategy)?;
                        *camera_state = CameraState::Start;
                    }
                }
            }
        }
    }

    Ok(())
}

fn toggle_edge_detection_system(
    mut commands: Commands,
    ratatui_camera: Single<(Entity, Option<&mut RatatuiCameraEdgeDetection>), With<RatatuiCamera>>,
) {
    let (entity, ref edge_detection) = *ratatui_camera;

    if edge_detection.is_some() {
        commands
            .entity(entity)
            .remove::<RatatuiCameraEdgeDetection>();
    } else {
        commands
            .entity(entity)
            .insert(RatatuiCameraEdgeDetection::default());
    }
}

fn modify_edge_detection_system(
    mut ratatui_camera_edge_detection: Single<
        Option<&mut RatatuiCameraEdgeDetection>,
        With<RatatuiCamera>,
    >,
) {
    if let Some(ref mut c) = *ratatui_camera_edge_detection {
        c.edge_color = Some(ratatui::style::Color::Magenta);
    }
}

fn modify_ratatui_camera_strategy(mut ratatui_camera_strategy: Single<&mut RatatuiCameraStrategy>) {
    let RatatuiCameraStrategy::Luminance(ref mut luminance_config) = **ratatui_camera_strategy
    else {
        return;
    };

    luminance_config.characters.list = vec!['.', 'o', 'O', '0'];
}

fn toggle_ratatui_camera_strategy(
    mut commands: Commands,
    ratatui_camera: Single<(Entity, &RatatuiCameraStrategy)>,
) {
    let (entity, strategy) = *ratatui_camera;
    commands.entity(entity).insert(match strategy {
        RatatuiCameraStrategy::HalfBlocks(_) => {
            RatatuiCameraStrategy::Luminance(LuminanceConfig::default())
        }
        RatatuiCameraStrategy::Luminance(_) => {
            RatatuiCameraStrategy::HalfBlocks(HalfBlocksConfig::default())
        }
        RatatuiCameraStrategy::None => RatatuiCameraStrategy::None,
        RatatuiCameraStrategy::Depth(_) => RatatuiCameraStrategy::None,
    });
}