aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/docs.yml50
-rw-r--r--.github/workflows/rust.yml24
-rw-r--r--Cargo.toml14
-rw-r--r--README.md16
-rw-r--r--src/display/components.rs (renamed from src/components.rs)35
-rw-r--r--src/display/mod.rs8
-rw-r--r--src/display/resources.rs43
-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.rs8
-rw-r--r--src/input/resources.rs48
-rw-r--r--src/input/systems.rs46
-rw-r--r--src/lib.rs85
-rw-r--r--src/resources.rs109
-rw-r--r--src/widgets/components.rs14
-rw-r--r--src/widgets/mod.rs21
-rw-r--r--src/widgets/systems.rs35
17 files changed, 388 insertions, 256 deletions
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..e0044de
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,50 @@
+name: Docs
+on:
+ push:
+ branches: [master]
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+concurrency:
+ group: deploy
+ cancel-in-progress: false
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ - name: Install Dependencies
+ run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+ - name: Configure cache
+ uses: Swatinem/rust-cache@v2
+ - name: Setup pages
+ id: pages
+ uses: actions/configure-pages@v4
+ - name: Clean docs folder
+ run: cargo clean --doc
+ - name: Build docs
+ run: cargo doc --no-deps
+ - name: Add redirect
+ run: echo '<meta http-equiv="refresh" content="0;url=bevy_terminal_display/index.html">' > target/doc/index.html
+ - name: Remove lock file
+ run: rm target/doc/.lock
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: target/doc
+ deploy:
+ name: Deploy
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
new file mode 100644
index 0000000..e38739d
--- /dev/null
+++ b/.github/workflows/rust.yml
@@ -0,0 +1,24 @@
+name: Rust
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install Dependencies
+ run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
+ - name: Build
+ run: cargo build --verbose
+ - name: Run tests
+ run: cargo test --verbose
diff --git a/Cargo.toml b/Cargo.toml
index e1b8dda..782c22e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,24 +1,24 @@
[package]
-name = "grex_terminal_display"
-version = "0.1.5"
+name = "bevy_terminal_display"
+version = "0.2.0"
edition = "2021"
[dependencies]
crossterm = "0.27.0"
+downcast-rs = "1.2.1"
once_cell = "1.19.0"
[dependencies.bevy]
version = "0.13"
-[dependencies.grex_framebuffer_extract]
-git = "https://github.com/exvacuum/grex_framebuffer_extract"
+[dependencies.bevy_framebuffer_extract]
+git = "https://github.com/exvacuum/bevy_framebuffer_extract"
tag = "v0.1.1"
-[dependencies.grex_dither_post_process]
-git = "https://github.com/exvacuum/grex_dither_post_process"
+[dependencies.bevy_dither_post_process]
+git = "https://github.com/exvacuum/bevy_dither_post_process"
tag = "v0.1.3"
[dependencies.ratatui]
version = "0.26.2"
-features = ["unstable-widget-ref"]
diff --git a/README.md b/README.md
index 64ce199..44bee7c 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# grex_terminal_display
+# bevy_terminal_display
A (very experimental) plugin for the [Bevy](https://bevyengine.org) engine which allows for rendering to a terminal window.
@@ -10,7 +10,7 @@ Features Include:
- Post-process dithers colors to pure black and white, which are then printed as braille characters to the terminal
- Responsiveness to terminal window resizing
- `TerminalInput` resource which keeps track of pressed & released keys
-- `TerminalUI` resource for rendering ratatui TUI widgets
+- `Widget` component for rendering ratatui TUI widgets
- `TerminalWidget` trait for creating custom TUI widget components
- Logging redirected to `output.log`
@@ -20,14 +20,14 @@ Features Include:
| Crate Version | Bevy Version |
|--- |--- |
-| 0.1 | 0.13 |
+| 0.2 | 0.13 |
## Installation
### Using git URL in Cargo.toml
```toml
-[dependencies.grex_terminal_display]
-git = "https://github.com/exvacuum/grex_terminal_display.git"
+[dependencies.bevy_terminal_display]
+git = "https://github.com/exvacuum/bevy_terminal_display.git"
```
## Example Usage
@@ -35,14 +35,14 @@ git = "https://github.com/exvacuum/grex_terminal_display.git"
In `main.rs`:
```rs
use bevy::prelude::*;
-use grex_terminal_display;
+use bevy_terminal_display;
fn main() {
App::new()
.add_plugins((
DefaultPlugins.build().disable::<WinitPlugin>().disable::<LogPlugin>,
ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32(1.0 / 60.0)),
- grex_terminal_display::TerminalDisplayPlugin,
+ bevy_terminal_display::TerminalDisplayPlugin,
))
.insert_resource(Msaa::Off) // For post-process
.run();
@@ -51,7 +51,7 @@ fn main() {
When spawning a camera:
```rs
-let terminal_display_bundle = grex_terminal_display::components::TerminalDisplayBundle::new(3, &asset_server);
+let terminal_display_bundle = bevy_terminal_display::display::components::TerminalDisplayBundle::new(3, &asset_server);
commands.spawn((
Camera3dBundle {
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));
+ }
+}
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<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);
+ }
+ }
+}