aboutsummaryrefslogtreecommitdiff
path: root/src/display
diff options
context:
space:
mode:
authorLibravatar Silas Bartha <silas@exvacuum.dev>2024-06-04 15:00:16 -0400
committerLibravatar Silas Bartha <silas@exvacuum.dev>2024-06-04 15:00:16 -0400
commita002e4d738535e6ca779c71231f7b84864b9a8d0 (patch)
tree8ec521b535f3820328f9ea6f2511ca29c630b82f /src/display
parent56aafda8495243fa939bdce01f36d4adbf4ec556 (diff)
Refactored + Renamed + Added Docs
Diffstat (limited to 'src/display')
-rw-r--r--src/display/components.rs72
-rw-r--r--src/display/mod.rs8
-rw-r--r--src/display/resources.rs43
-rw-r--r--src/display/systems.rs116
4 files changed, 239 insertions, 0 deletions
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<Image>,
+}
+
+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<Image> {
+ 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<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/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<Terminal>,
+ 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::<char>::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::<String>();
+ 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<Assets<Image>>,
+ mut sources: ResMut<Assets<FramebufferExtractSource>>,
+ mut event_reader: EventReader<TerminalInputEvent>,
+) {
+ 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,
+ });
+ }
+ }
+ }
+}