From a002e4d738535e6ca779c71231f7b84864b9a8d0 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Tue, 4 Jun 2024 15:00:16 -0400 Subject: Refactored + Renamed + Added Docs --- src/components.rs | 85 ----------------------- src/display/components.rs | 72 +++++++++++++++++++ src/display/mod.rs | 8 +++ src/display/resources.rs | 43 ++++++++++++ src/display/systems.rs | 116 +++++++++++++++++++++++++++++++ src/events.rs | 5 -- src/input/events.rs | 6 ++ src/input/mod.rs | 8 +++ src/input/resources.rs | 48 +++++++++++++ src/input/systems.rs | 46 +++++++++++++ src/lib.rs | 85 +++++++++++++---------- src/resources.rs | 109 ----------------------------- src/systems.rs | 171 ---------------------------------------------- src/widgets/components.rs | 14 ++++ src/widgets/mod.rs | 21 ++++++ src/widgets/systems.rs | 35 ++++++++++ 16 files changed, 465 insertions(+), 407 deletions(-) delete mode 100644 src/components.rs create mode 100644 src/display/components.rs create mode 100644 src/display/mod.rs create mode 100644 src/display/resources.rs create mode 100644 src/display/systems.rs delete mode 100644 src/events.rs create mode 100644 src/input/events.rs create mode 100644 src/input/mod.rs create mode 100644 src/input/resources.rs create mode 100644 src/input/systems.rs delete mode 100644 src/resources.rs delete mode 100644 src/systems.rs create mode 100644 src/widgets/components.rs create mode 100644 src/widgets/mod.rs create mode 100644 src/widgets/systems.rs (limited to 'src') diff --git a/src/components.rs b/src/components.rs deleted file mode 100644 index 3814cc7..0000000 --- a/src/components.rs +++ /dev/null @@ -1,85 +0,0 @@ - -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; - -#[derive(Component)] -pub struct TerminalDisplay; - -#[derive(Bundle)] -pub struct TerminalDisplayBundle { - _terminal_display: TerminalDisplay, - extract_framebuffer_bundle: ExtractFramebufferBundle, - dither_post_process_settings: DitherPostProcessSettings, - image_handle: Handle, -} - -impl TerminalDisplayBundle { - pub fn new(dither_level: u32, asset_server: &AssetServer) -> Self { - let terminal_size = crossterm::terminal::size().unwrap(); - let size = Extent3d { - width: (terminal_size.0 as u32) * 2, - height: (terminal_size.1 as u32) * 4, - depth_or_array_layers: 1, - }; - - let mut image = Image { - texture_descriptor: TextureDescriptor { - label: None, - size, - dimension: TextureDimension::D2, - format: TextureFormat::R8Unorm, - mip_level_count: 1, - sample_count: 1, - usage: TextureUsages::TEXTURE_BINDING - | TextureUsages::COPY_SRC - | TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }, - ..default() - }; - - image.resize(size); - let image_handle = asset_server.add(image); - - let framebuffer_extract_source = - asset_server.add(FramebufferExtractSource(image_handle.clone())); - - Self { - _terminal_display: TerminalDisplay, - extract_framebuffer_bundle: ExtractFramebufferBundle { - source: framebuffer_extract_source, - dest: FramebufferExtractDestination::default(), - }, - image_handle, - dither_post_process_settings: DitherPostProcessSettings::new( - dither_level, - asset_server, - ), - } - } - - pub fn image_handle(&self) -> Handle { - self.image_handle.clone() - } -} - -#[derive(Component)] -pub struct Widget { - pub widget: Box, - pub depth: u32, - pub enabled: bool, -} - -#[derive(Component)] -pub struct Tooltip(pub String); diff --git a/src/display/components.rs b/src/display/components.rs new file mode 100644 index 0000000..4459325 --- /dev/null +++ b/src/display/components.rs @@ -0,0 +1,72 @@ +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}; + +/// 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, + extract_framebuffer_bundle: ExtractFramebufferBundle, + dither_post_process_settings: DitherPostProcessSettings, + image_handle: Handle, +} + +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 { + width: (terminal_size.0 as u32) * 2, + height: (terminal_size.1 as u32) * 4, + depth_or_array_layers: 1, + }; + + let mut image = Image { + texture_descriptor: TextureDescriptor { + label: None, + size, + dimension: TextureDimension::D2, + format: TextureFormat::R8Unorm, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC + | TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ..default() + }; + + image.resize(size); + let image_handle = asset_server.add(image); + + let framebuffer_extract_source = + asset_server.add(FramebufferExtractSource(image_handle.clone())); + + Self { + _terminal_display: TerminalDisplay, + extract_framebuffer_bundle: ExtractFramebufferBundle { + source: framebuffer_extract_source, + dest: FramebufferExtractDestination::default(), + }, + image_handle, + dither_post_process_settings: DitherPostProcessSettings::new( + dither_level, + asset_server, + ), + } + } + + /// Retrieves the handle to this display's target image. Anything written here will be + /// displayed. + pub fn image_handle(&self) -> Handle { + self.image_handle.clone() + } +} 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>); + +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/display/systems.rs b/src/display/systems.rs new file mode 100644 index 0000000..a768af5 --- /dev/null +++ b/src/display/systems.rs @@ -0,0 +1,116 @@ +use bevy::{ + prelude::*, + render::render_resource::{Extent3d, TextureFormat}, +}; +use crossterm::event::Event; +use bevy_framebuffer_extract::{ + components::FramebufferExtractDestination, render_assets::FramebufferExtractSource, +}; +use ratatui::{ + 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; + +/// 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, + image_exports: Query<&FramebufferExtractDestination>, +) { + for image_export in image_exports.iter() { + let mut image = image_export + .0 + .lock() + .expect("Failed to get lock on output texture"); + 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); + match image.convert(TextureFormat::R8Unorm) { + Some(img) => *image = img, + None => error_once!( + "Could not convert to R8Unorm texture format. Unexpected output may occur." + ), + }; + } + + let mut output_buffer = Vec::::new(); + let width = image.width(); + let height = image.height(); + let data = &image.data; + for character_y in (0..height).step_by(4) { + for character_x in (0..width).step_by(2) { + let mut mask: u8 = 0; + for offset_x in 0..2 { + for offset_y in 0..4 { + let x = character_x + offset_x; + let y = character_y + offset_y; + if x < width && y < height && data[(y * width + x) as usize] == 0xFF { + mask |= 1 + << (BRAILLE_DOT_BIT_POSITIONS[(offset_x * 4 + offset_y) as usize]); + } + } + } + output_buffer.push(braille_char(mask)); + } + } + + let string = output_buffer.into_iter().collect::(); + terminal + .0 + .draw(|frame| { + frame.render_widget( + Paragraph::new(string) + .white() + .bold() + .wrap(Wrap { trim: true }), + frame.size(), + ); + }) + .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) => { + if character as u16 > BRAILLE_CODE_MAX { + panic!("Number too big!") + } + character + } + None => panic!("Error converting character!"), + } +} + +/// Watches for terminal resize events and resizes the render image accordingly +pub fn resize_handling( + mut images: ResMut>, + mut sources: ResMut>, + mut event_reader: EventReader, +) { + for event in event_reader.read() { + if let Event::Resize(w, h) = event.0 { + for source in sources.iter_mut() { + let image = images.get_mut(&source.1 .0).unwrap(); + image.resize(Extent3d { + width: w as u32 * 2, + height: h as u32 * 4, + depth_or_array_layers: 1, + }); + } + } + } +} diff --git a/src/events.rs b/src/events.rs deleted file mode 100644 index 3f7057b..0000000 --- a/src/events.rs +++ /dev/null @@ -1,5 +0,0 @@ -use bevy::prelude::*; -use crossterm::event::Event; - -#[derive(Event)] -pub struct TerminalInputEvent(pub Event); diff --git a/src/input/events.rs b/src/input/events.rs new file mode 100644 index 0000000..cf46445 --- /dev/null +++ b/src/input/events.rs @@ -0,0 +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, + released_keys: HashSet, +} + +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>>); 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) { + 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, + mut input: ResMut, + mut event_writer: EventWriter, +) { + 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)); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0a1a925..280e413 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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>> = 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::(); - } -} - -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::(); } } 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, - released_keys: HashSet, -} - -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>>); - -#[derive(Resource)] -pub struct Terminal(pub ratatui::Terminal>); - -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>, -// } -// -// impl TerminalUI { -// pub fn insert_widget(&mut self, widget: Box) -> Uuid { -// let id = Uuid::new_v4(); -// self.widgets.insert(id, widget); -// id -// } -// -// pub fn get_widget(&mut self, id: Uuid) -> Option<&mut Box> { -// 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> { -// let mut vec = self.widgets.values_mut().collect::>(); -// 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/systems.rs b/src/systems.rs deleted file mode 100644 index dedef65..0000000 --- a/src/systems.rs +++ /dev/null @@ -1,171 +0,0 @@ -use bevy::{ - prelude::*, - render::render_resource::{Extent3d, TextureFormat}, -}; -use crossterm::event::{read, Event, KeyEventKind}; -use grex_framebuffer_extract::{ - components::FramebufferExtractDestination, render_assets::FramebufferExtractSource, -}; - -use crate::{ - components::Widget, events::TerminalInputEvent, resources::{EventQueue, Terminal, TerminalInput} -}; - -use ratatui::{ - prelude::*, - widgets::{Paragraph, Wrap}, -}; - -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) { - 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); - } - } - } - }); -} - -pub fn print_to_terminal( - mut terminal: ResMut, - mut widgets: Query<&mut Widget>, - image_exports: Query<&FramebufferExtractDestination>, -) { - for image_export in image_exports.iter() { - let mut image = image_export - .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); - match image.convert(TextureFormat::R8Unorm) { - Some(img) => *image = img, - None => error_once!( - "Could not convert to R8Unorm texture format. Unexpected output may occur." - ), - }; - } - - let mut output_buffer = Vec::::new(); - let width = image.width(); - let height = image.height(); - let data = &image.data; - for character_y in (0..height).step_by(4) { - for character_x in (0..width).step_by(2) { - let mut mask: u8 = 0; - for offset_x in 0..2 { - for offset_y in 0..4 { - let x = character_x + offset_x; - let y = character_y + offset_y; - if x < width && y < height && data[(y * width + x) as usize] == 0xFF { - mask |= 1 - << (BRAILLE_DOT_BIT_POSITIONS[(offset_x * 4 + offset_y) as usize]); - } - } - } - output_buffer.push(braille_char(mask)); - } - } - - let string = output_buffer.into_iter().collect::(); - terminal - .0 - .draw(|frame| { - let area = frame.size(); - frame.render_widget( - Paragraph::new(string) - .white() - .bold() - .wrap(Wrap { trim: true }), - area, - ); - let mut active_widgets = widgets.iter_mut().filter(|widget| widget.enabled).collect::>(); - 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"); - } -} - -fn braille_char(mask: u8) -> char { - match char::from_u32((BRAILLE_CODE_MIN + mask as u16) as u32) { - Some(character) => { - if character as u16 > BRAILLE_CODE_MAX { - panic!("Number too big!") - } - character - } - None => panic!("Error converting character!"), - } -} - -pub fn widget_input_handling( - mut widgets: Query<&mut Widget>, - mut event_reader: EventReader, - 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, - mut input: ResMut, - mut event_writer: EventWriter, -) { - 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)); - } -} - -pub fn resize_handling( - mut images: ResMut>, - mut sources: ResMut>, - mut event_reader: EventReader, -) { - for event in event_reader.read() { - if let Event::Resize(w, h) = event.0 { - for source in sources.iter_mut() { - let image = images.get_mut(&source.1 .0).unwrap(); - image.resize(Extent3d { - width: w as u32 * 2, - height: h as u32 * 4, - depth_or_array_layers: 1, - }); - } - } - } -} 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, + /// 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, mut widgets: Query<&mut Widget>) { + terminal + .0 + .draw(|frame| { + let mut active_widgets = widgets + .iter_mut() + .filter(|widget| widget.enabled) + .collect::>(); + 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, + 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); + } + } +} -- cgit v1.2.3