diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/display/components.rs (renamed from src/components.rs) | 35 | ||||
-rw-r--r-- | src/display/mod.rs | 8 | ||||
-rw-r--r-- | src/display/resources.rs | 43 | ||||
-rw-r--r-- | src/display/systems.rs (renamed from src/systems.rs) | 87 | ||||
-rw-r--r-- | src/input/events.rs (renamed from src/events.rs) | 1 | ||||
-rw-r--r-- | src/input/mod.rs | 8 | ||||
-rw-r--r-- | src/input/resources.rs | 48 | ||||
-rw-r--r-- | src/input/systems.rs | 46 | ||||
-rw-r--r-- | src/lib.rs | 85 | ||||
-rw-r--r-- | src/resources.rs | 109 | ||||
-rw-r--r-- | src/widgets/components.rs | 14 | ||||
-rw-r--r-- | src/widgets/mod.rs | 21 | ||||
-rw-r--r-- | src/widgets/systems.rs | 35 |
13 files changed, 299 insertions, 241 deletions
diff --git a/src/components.rs b/src/display/components.rs index 3814cc7..4459325 100644 --- a/src/components.rs +++ b/src/display/components.rs @@ -1,21 +1,13 @@ +use bevy::{prelude::*, render::render_resource::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}}; +use bevy_dither_post_process::components::DitherPostProcessSettings; +use bevy_framebuffer_extract::{components::{ExtractFramebufferBundle, FramebufferExtractDestination}, render_assets::FramebufferExtractSource}; -use bevy::{ - prelude::*, - render::render_resource::{ - Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, - }, -}; -use grex_dither_post_process::components::DitherPostProcessSettings; -use grex_framebuffer_extract::{ - components::{ExtractFramebufferBundle, FramebufferExtractDestination}, - render_assets::FramebufferExtractSource, -}; - -use crate::resources::TerminalWidget; - +/// Marker component for terminal display #[derive(Component)] pub struct TerminalDisplay; +/// Bundle for terminal display, contains a handle to an image to be used as a render target to +/// render to the terminal #[derive(Bundle)] pub struct TerminalDisplayBundle { _terminal_display: TerminalDisplay, @@ -25,6 +17,9 @@ pub struct TerminalDisplayBundle { } impl TerminalDisplayBundle { + /// Create a new terminal display with the given dither level. A higher level exponentially + /// increases the size of the bayer matrix used in the ordered dithering calculations. If in + /// doubt, 3 is a good starting value to test with. pub fn new(dither_level: u32, asset_server: &AssetServer) -> Self { let terminal_size = crossterm::terminal::size().unwrap(); let size = Extent3d { @@ -69,17 +64,9 @@ impl TerminalDisplayBundle { } } + /// Retrieves the handle to this display's target image. Anything written here will be + /// displayed. pub fn image_handle(&self) -> Handle<Image> { self.image_handle.clone() } } - -#[derive(Component)] -pub struct Widget { - pub widget: Box<dyn TerminalWidget + Send + Sync>, - pub depth: u32, - pub enabled: bool, -} - -#[derive(Component)] -pub struct Tooltip(pub String); diff --git a/src/display/mod.rs b/src/display/mod.rs new file mode 100644 index 0000000..ae45445 --- /dev/null +++ b/src/display/mod.rs @@ -0,0 +1,8 @@ +/// Components for this module +pub mod components; + +/// Resources for this module +pub mod resources; + +/// Systems for this module +pub(crate) mod systems; diff --git a/src/display/resources.rs b/src/display/resources.rs new file mode 100644 index 0000000..7321aea --- /dev/null +++ b/src/display/resources.rs @@ -0,0 +1,43 @@ +use std::io::{stdout, Stdout}; + +use bevy::prelude::*; +use crossterm::{ + event::{ + DisableMouseCapture, EnableMouseCapture, KeyboardEnhancementFlags, + PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, + }, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use ratatui::backend::CrosstermBackend; + +/// Ratatui terminal instance. Enters alternate screen when constructed, and exits once dropped. +#[derive(Resource)] +pub struct Terminal(pub ratatui::Terminal<CrosstermBackend<Stdout>>); + +impl Default for Terminal { + fn default() -> Self { + stdout().execute(EnterAlternateScreen).unwrap(); + stdout().execute(EnableMouseCapture).unwrap(); + stdout() + .execute(PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::REPORT_EVENT_TYPES, + )) + .unwrap(); + enable_raw_mode().unwrap(); + let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout())) + .expect("Failed to create terminal"); + terminal.clear().expect("Failed to clear terminal"); + Self(terminal) + } +} + +impl Drop for Terminal { + fn drop(&mut self) { + let mut stdout = stdout(); + let _ = stdout.execute(PopKeyboardEnhancementFlags); + let _ = stdout.execute(DisableMouseCapture); + let _ = stdout.execute(LeaveAlternateScreen); + let _ = disable_raw_mode(); + } +} diff --git a/src/systems.rs b/src/display/systems.rs index dedef65..a768af5 100644 --- a/src/systems.rs +++ b/src/display/systems.rs @@ -2,44 +2,31 @@ use bevy::{ prelude::*, render::render_resource::{Extent3d, TextureFormat}, }; -use crossterm::event::{read, Event, KeyEventKind}; -use grex_framebuffer_extract::{ +use crossterm::event::Event; +use bevy_framebuffer_extract::{ components::FramebufferExtractDestination, render_assets::FramebufferExtractSource, }; - -use crate::{ - components::Widget, events::TerminalInputEvent, resources::{EventQueue, Terminal, TerminalInput} -}; - use ratatui::{ - prelude::*, + style::Stylize, widgets::{Paragraph, Wrap}, }; +use crate::input::events::TerminalInputEvent; + +use super::resources::Terminal; + const BRAILLE_CODE_MIN: u16 = 0x2800; const BRAILLE_CODE_MAX: u16 = 0x28FF; -const BRAILLE_DOT_BIT_POSITIONS: [u8; 8] = [0, 1, 2, 6, 3, 4, 5, 7]; -pub fn setup(event_queue: Res<EventQueue>) { - let event_queue = event_queue.0.clone(); - std::thread::spawn(move || { - loop { - // `read()` blocks until an `Event` is available - match read() { - Ok(event) => { - event_queue.lock().unwrap().push(event); - } - Err(err) => { - panic!("Error reading input events: {:?}", err); - } - } - } - }); -} +/// 0 3 +/// 1 4 +/// 2 5 +/// 6 7 +const BRAILLE_DOT_BIT_POSITIONS: [u8; 8] = [0, 1, 2, 6, 3, 4, 5, 7]; +/// Prints out the contents of a render image to the terminal as braille characters pub fn print_to_terminal( mut terminal: ResMut<Terminal>, - mut widgets: Query<&mut Widget>, image_exports: Query<&FramebufferExtractDestination>, ) { for image_export in image_exports.iter() { @@ -47,10 +34,6 @@ pub fn print_to_terminal( .0 .lock() .expect("Failed to get lock on output texture"); - //TODO: Find a better way of preventing first frame - if image.size() == UVec2::ONE { - continue; - } if image.texture_descriptor.format != TextureFormat::R8Unorm { warn_once!("Extracted framebuffer texture is not R8Unorm format. Will attempt conversion, but consider changing your render texture's format."); info_once!("{:?}", image); @@ -87,24 +70,19 @@ pub fn print_to_terminal( terminal .0 .draw(|frame| { - let area = frame.size(); frame.render_widget( Paragraph::new(string) .white() .bold() .wrap(Wrap { trim: true }), - area, + frame.size(), ); - let mut active_widgets = widgets.iter_mut().filter(|widget| widget.enabled).collect::<Vec<_>>(); - active_widgets.sort_by(|a, b| a.depth.cmp(&b.depth)); - for mut widget in active_widgets { - widget.widget.render(frame, area); - } }) .expect("Failed to draw terminal frame"); } } +/// Utility function to convert a u8 into the corresponding braille character fn braille_char(mask: u8) -> char { match char::from_u32((BRAILLE_CODE_MIN + mask as u16) as u32) { Some(character) => { @@ -117,40 +95,7 @@ fn braille_char(mask: u8) -> char { } } -pub fn widget_input_handling( - mut widgets: Query<&mut Widget>, - mut event_reader: EventReader<TerminalInputEvent>, - mut commands: Commands, -) { - for event in event_reader.read() { - for mut widget in widgets.iter_mut().filter(|widget| widget.enabled) { - widget.widget.handle_events(event, &mut commands); - } - } -} - -pub fn input_handling( - event_queue: Res<EventQueue>, - mut input: ResMut<TerminalInput>, - mut event_writer: EventWriter<TerminalInputEvent>, -) { - let mut event_queue = event_queue.0.lock().unwrap(); - while let Some(event) = event_queue.pop() { - if let Event::Key(event) = event { - match event.kind { - KeyEventKind::Press => { - input.press(event.code); - } - KeyEventKind::Release => { - input.release(event.code); - } - _ => (), - } - } - event_writer.send(TerminalInputEvent(event)); - } -} - +/// Watches for terminal resize events and resizes the render image accordingly pub fn resize_handling( mut images: ResMut<Assets<Image>>, mut sources: ResMut<Assets<FramebufferExtractSource>>, diff --git a/src/events.rs b/src/input/events.rs index 3f7057b..cf46445 100644 --- a/src/events.rs +++ b/src/input/events.rs @@ -1,5 +1,6 @@ use bevy::prelude::*; use crossterm::event::Event; +/// An event triggered when a crossterm input event is received #[derive(Event)] pub struct TerminalInputEvent(pub Event); diff --git a/src/input/mod.rs b/src/input/mod.rs new file mode 100644 index 0000000..80c36bc --- /dev/null +++ b/src/input/mod.rs @@ -0,0 +1,8 @@ +/// Events for this module +pub mod events; + +/// Resources for this module +pub mod resources; + +/// Systems for this module +pub(crate) mod systems; diff --git a/src/input/resources.rs b/src/input/resources.rs new file mode 100644 index 0000000..afe6353 --- /dev/null +++ b/src/input/resources.rs @@ -0,0 +1,48 @@ +use bevy::{prelude::*, utils::HashSet}; +use crossterm::event::{Event, KeyCode}; +use std::sync::{Arc, Mutex}; + +/// Resource containing currently pressed and released keys +#[derive(Resource, Default)] +pub struct TerminalInput { + pressed_keys: HashSet<KeyCode>, + released_keys: HashSet<KeyCode>, +} + +impl TerminalInput { + /// Gets whether the given key is pressed + pub fn is_pressed(&self, code: KeyCode) -> bool { + self.pressed_keys.contains(&code) + } + + /// Gets whether the given key is released + pub fn is_released(&self, code: KeyCode) -> bool { + self.released_keys.contains(&code) + } + + /// Sets given key to pressed + pub(super) fn press(&mut self, code: KeyCode) { + if !self.is_pressed(code) { + self.pressed_keys.insert(code); + } + } + + /// Sets given key to released and removes pressed state + pub(super) fn release(&mut self, code: KeyCode) { + if self.is_pressed(code) { + self.pressed_keys.remove(&code); + } + if !self.is_released(code) { + self.released_keys.insert(code); + } + } + + /// Clears all released keys + pub(super) fn clear_released(&mut self) { + self.released_keys.clear(); + } +} + +/// Event queue for crossterm input event thread +#[derive(Resource, Default)] +pub(crate) struct EventQueue(pub(super) Arc<Mutex<Vec<Event>>>); diff --git a/src/input/systems.rs b/src/input/systems.rs new file mode 100644 index 0000000..3b7f11d --- /dev/null +++ b/src/input/systems.rs @@ -0,0 +1,46 @@ +use bevy::prelude::*; +use crossterm::event::{read, Event, KeyEventKind}; + +use super::{events::TerminalInputEvent, resources::{EventQueue, TerminalInput}}; + +/// Initializes event queue and thread +pub fn setup_input(event_queue: Res<EventQueue>) { + let event_queue = event_queue.0.clone(); + std::thread::spawn(move || { + loop { + // `read()` blocks until an `Event` is available + match read() { + Ok(event) => { + event_queue.lock().unwrap().push(event); + } + Err(err) => { + panic!("Error reading input events: {:?}", err); + } + } + } + }); +} + +/// Reads events from queue and broadcasts corresponding `TerminalInputEvent`s +pub fn input_handling( + event_queue: Res<EventQueue>, + mut input: ResMut<TerminalInput>, + mut event_writer: EventWriter<TerminalInputEvent>, +) { + input.clear_released(); + let mut event_queue = event_queue.0.lock().unwrap(); + while let Some(event) = event_queue.pop() { + if let Event::Key(event) = event { + match event.kind { + KeyEventKind::Press => { + input.press(event.code); + } + KeyEventKind::Release => { + input.release(event.code); + } + _ => (), + } + } + event_writer.send(TerminalInputEvent(event)); + } +} @@ -1,46 +1,58 @@ -use std::{fs::OpenOptions, io::stdout, path::PathBuf, sync::{Arc, Mutex}}; +#![warn(missing_docs)] + +//! Bevy plugin which allows a camera to render to a terminal window. + +use std::{ + fs::OpenOptions, + path::PathBuf, + sync::{Arc, Mutex}, +}; use bevy::{ log::{ - tracing_subscriber::{self, Registry, prelude::*}, - LogPlugin, Level, + tracing_subscriber::{self, prelude::*, Registry}, + Level, LogPlugin, }, - prelude::*, utils::tracing::level_filters::LevelFilter, + prelude::*, + utils::tracing::level_filters::LevelFilter, }; -use crossterm::{ - event::{DisableMouseCapture, PopKeyboardEnhancementFlags}, - terminal::{disable_raw_mode, LeaveAlternateScreen}, - ExecutableCommand, -}; -use grex_dither_post_process::DitherPostProcessPlugin; -use grex_framebuffer_extract::FramebufferExtractPlugin; +use bevy_dither_post_process::DitherPostProcessPlugin; +use bevy_framebuffer_extract::FramebufferExtractPlugin; pub use crossterm; use once_cell::sync::Lazy; pub use ratatui; -pub mod components; -pub mod events; -pub mod resources; -mod systems; +/// Functions and types related to capture and display of world to terminal +pub mod display; + +/// Functions and types related to capturing and processing user keyboard input +pub mod input; + +/// Functions and types related to constructing and rendering TUI widgets +pub mod widgets; static LOG_PATH: Lazy<Arc<Mutex<PathBuf>>> = Lazy::new(|| Arc::new(Mutex::new(PathBuf::default()))); +/// Plugin providing terminal display functionality pub struct TerminalDisplayPlugin { + /// Path to redirect tracing logs to. Defaults to "debug.log" pub log_path: PathBuf, } impl Default for TerminalDisplayPlugin { fn default() -> Self { Self { - log_path: "debug.log".into() + log_path: "debug.log".into(), } } } impl Plugin for TerminalDisplayPlugin { fn build(&self, app: &mut App) { - *LOG_PATH.lock().expect("Failed to get lock on log path mutex") = self.log_path.clone(); + *LOG_PATH + .lock() + .expect("Failed to get lock on log path mutex") = self.log_path.clone(); app.add_plugins(( DitherPostProcessPlugin, FramebufferExtractPlugin, @@ -50,7 +62,12 @@ impl Plugin for TerminalDisplayPlugin { .write(true) .create(true) .truncate(true) - .open(LOG_PATH.lock().expect("Failed to get lock on log path mutex").clone()) + .open( + LOG_PATH + .lock() + .expect("Failed to get lock on log path mutex") + .clone(), + ) .unwrap(); let file_layer = tracing_subscriber::fmt::Layer::new() .with_writer(log_file) @@ -60,29 +77,23 @@ impl Plugin for TerminalDisplayPlugin { ..Default::default() }, )) - .add_systems(Startup, systems::setup) + .add_systems(Startup, input::systems::setup_input) .add_systems( Update, ( - systems::input_handling, - systems::resize_handling, - systems::print_to_terminal, - systems::widget_input_handling, + input::systems::input_handling, + display::systems::resize_handling, + ( + display::systems::print_to_terminal, + widgets::systems::draw_widgets, + ) + .chain(), + widgets::systems::widget_input_handling, ), ) - .insert_resource(resources::Terminal::default()) - .insert_resource(resources::EventQueue::default()) - .insert_resource(resources::TerminalInput::default()) - .add_event::<events::TerminalInputEvent>(); - } -} - -impl Drop for TerminalDisplayPlugin { - fn drop(&mut self) { - let mut stdout = stdout(); - let _ = stdout.execute(PopKeyboardEnhancementFlags); - let _ = stdout.execute(DisableMouseCapture); - let _ = stdout.execute(LeaveAlternateScreen); - let _ = disable_raw_mode(); + .insert_resource(display::resources::Terminal::default()) + .insert_resource(input::resources::EventQueue::default()) + .insert_resource(input::resources::TerminalInput::default()) + .add_event::<input::events::TerminalInputEvent>(); } } diff --git a/src/resources.rs b/src/resources.rs deleted file mode 100644 index a373a80..0000000 --- a/src/resources.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::{ - any::Any, io::{stdout, Stdout}, sync::{Arc, Mutex} -}; - -use bevy::{ - prelude::*, - utils::HashSet, -}; -use crossterm::{ - event::{ - EnableMouseCapture, Event, KeyCode, KeyboardEnhancementFlags, PushKeyboardEnhancementFlags, - }, - terminal::{enable_raw_mode, EnterAlternateScreen}, - ExecutableCommand, -}; -use ratatui::{backend::CrosstermBackend, layout::Rect, Frame}; - -use crate::events::TerminalInputEvent; - -#[derive(Resource, Default)] -pub struct TerminalInput { - pressed_keys: HashSet<KeyCode>, - released_keys: HashSet<KeyCode>, -} - -impl TerminalInput { - pub fn is_pressed(&self, code: KeyCode) -> bool { - self.pressed_keys.contains(&code) - } - - pub fn is_released(&self, code: KeyCode) -> bool { - self.released_keys.contains(&code) - } - - pub(super) fn press(&mut self, code: KeyCode) { - if !self.is_pressed(code) { - self.pressed_keys.insert(code); - } - } - - pub(super) fn release(&mut self, code: KeyCode) { - if self.is_pressed(code) { - self.pressed_keys.remove(&code); - } - if !self.is_released(code) { - self.released_keys.insert(code); - } - } -} - -#[derive(Resource, Default)] -pub(super) struct EventQueue(pub(super) Arc<Mutex<Vec<Event>>>); - -#[derive(Resource)] -pub struct Terminal(pub ratatui::Terminal<CrosstermBackend<Stdout>>); - -impl Default for Terminal { - fn default() -> Self { - stdout().execute(EnterAlternateScreen).unwrap(); - stdout().execute(EnableMouseCapture).unwrap(); - stdout() - .execute(PushKeyboardEnhancementFlags( - KeyboardEnhancementFlags::REPORT_EVENT_TYPES, - )) - .unwrap(); - enable_raw_mode().unwrap(); - let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout())) - .expect("Failed to create terminal"); - terminal.clear().expect("Failed to clear terminal"); - Self(terminal) - } -} - -// #[derive(Resource, Default)] -// pub struct TerminalUI { -// widgets: HashMap<Uuid, Box<dyn TerminalWidget + Sync + Send>>, -// } -// -// impl TerminalUI { -// pub fn insert_widget(&mut self, widget: Box<dyn TerminalWidget + Sync + Send>) -> Uuid { -// let id = Uuid::new_v4(); -// self.widgets.insert(id, widget); -// id -// } -// -// pub fn get_widget(&mut self, id: Uuid) -> Option<&mut Box<dyn TerminalWidget + Sync + Send>> { -// self.widgets.get_mut(&id) -// } -// -// pub fn destroy_widget(&mut self, id: Uuid) { -// self.widgets.remove(&id); -// } -// -// pub fn widgets(&mut self) -> Vec<&mut Box<dyn TerminalWidget + Sync + Send>> { -// let mut vec = self.widgets.values_mut().collect::<Vec<_>>(); -// vec.sort_by(|a, b| a.depth().cmp(&b.depth()).reverse()); -// vec -// } -// } - -pub trait TerminalWidget: Any { - fn init(&mut self) {} - fn update(&mut self) {} - fn render(&mut self, frame: &mut Frame, rect: Rect); - fn handle_events(&mut self, _event: &TerminalInputEvent, _commands: &mut Commands) {} - fn depth(&self) -> u32 { - 0 - } -} diff --git a/src/widgets/components.rs b/src/widgets/components.rs new file mode 100644 index 0000000..b6ff925 --- /dev/null +++ b/src/widgets/components.rs @@ -0,0 +1,14 @@ +use bevy::prelude::*; + +use super::TerminalWidget; + +/// Component representing a terminal widget. +#[derive(Component)] +pub struct Widget { + /// The widget instance itself, containing rendering and input logic + pub widget: Box<dyn TerminalWidget + Send + Sync>, + /// Depth to render widget at + pub depth: u32, + /// Whether this widget is currently enabled or should be hidden + pub enabled: bool, +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs new file mode 100644 index 0000000..46bfa20 --- /dev/null +++ b/src/widgets/mod.rs @@ -0,0 +1,21 @@ +use bevy::prelude::*; +use downcast_rs::{impl_downcast, DowncastSync}; +use ratatui::{layout::Rect, Frame}; + +use crate::input::events::TerminalInputEvent; + +/// Components for this module +pub mod components; + +/// Systems for this module +pub(crate) mod systems; + +/// Trait which defines an interface for terminal widgets +pub trait TerminalWidget: DowncastSync { + /// Called every frame to render the widget + fn render(&mut self, frame: &mut Frame, rect: Rect); + + /// Called when a terminal input event is invoked to update any state accordingly + fn handle_events(&mut self, _event: &TerminalInputEvent, _commands: &mut Commands) {} +} +impl_downcast!(sync TerminalWidget); diff --git a/src/widgets/systems.rs b/src/widgets/systems.rs new file mode 100644 index 0000000..69a84e3 --- /dev/null +++ b/src/widgets/systems.rs @@ -0,0 +1,35 @@ +use bevy::prelude::*; + +use crate::{display::resources::Terminal, input::events::TerminalInputEvent}; + +use super::components::Widget; + +/// Invokes every enabled widget's `render` method +pub fn draw_widgets(mut terminal: ResMut<Terminal>, mut widgets: Query<&mut Widget>) { + terminal + .0 + .draw(|frame| { + let mut active_widgets = widgets + .iter_mut() + .filter(|widget| widget.enabled) + .collect::<Vec<_>>(); + active_widgets.sort_by(|a, b| a.depth.cmp(&b.depth)); + for mut widget in active_widgets { + widget.widget.render(frame, frame.size()); + } + }) + .unwrap(); +} + +/// Invokes every enabled widget's `handle_events` methods for each incoming input event +pub fn widget_input_handling( + mut widgets: Query<&mut Widget>, + mut event_reader: EventReader<TerminalInputEvent>, + mut commands: Commands, +) { + for event in event_reader.read() { + for mut widget in widgets.iter_mut().filter(|widget| widget.enabled) { + widget.widget.handle_events(event, &mut commands); + } + } +} |