├── src ├── widgets │ ├── mod.rs │ └── border.rs ├── renderer │ ├── mod.rs │ ├── cell.rs │ ├── buffer.rs │ └── render.rs ├── lib.rs ├── prelude.rs ├── math.rs ├── window.rs └── layout.rs ├── logo.png ├── devenv.nix ├── .gitignore ├── Makefile ├── .envrc ├── .github └── workflows │ └── rust.yml ├── devenv.yaml ├── Cargo.toml ├── examples ├── event_visualizer.rs ├── simple.rs ├── confirmation.rs ├── multiwidth.rs ├── inline.rs ├── cursor.rs ├── borders.rs ├── input_validator.rs ├── layout.rs └── space_invaders.rs ├── devenv.lock ├── README.md └── Cargo.lock /src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod border; 2 | pub use border::*; 3 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EmeraldPandaTurtle/ascii-forge/HEAD/logo.png -------------------------------------------------------------------------------- /src/renderer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod render; 2 | 3 | pub mod buffer; 4 | pub mod cell; 5 | -------------------------------------------------------------------------------- /devenv.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | lib, 4 | config, 5 | inputs, 6 | ... 7 | }: { 8 | languages.rust.enable = true; 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Devenv 4 | .devenv* 5 | devenv.local.nix 6 | 7 | # direnv 8 | .direnv 9 | 10 | # pre-commit 11 | .pre-commit-config.yaml 12 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod renderer; 2 | 3 | pub mod window; 4 | 5 | pub mod math; 6 | 7 | pub mod widgets; 8 | 9 | pub mod prelude; 10 | 11 | pub mod layout; 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | cargo test 3 | 4 | 5 | VERSION_FILE := Cargo.toml 6 | 7 | publish: 8 | sed -i -r "s/version=\"0\.0\.0\"/version=\"${VERSION}\"/g" $(VERSION_FILE) \ 9 | && cargo publish --allow-dirty 10 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export DIRENV_WARN_TIMEOUT=20s 2 | 3 | eval "$(devenv direnvrc)" 4 | 5 | # The use_devenv function supports passing flags to the devenv command 6 | # For example: use devenv --impure --option services.postgres.enable:bool true 7 | use devenv 8 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_imports)] 2 | pub use crate::event; 3 | pub use crate::math::*; 4 | pub use crate::render; 5 | pub use crate::renderer::{buffer::*, cell::*, render::*}; 6 | pub use crate::window::*; 7 | 8 | pub use crate::layout::*; 9 | 10 | pub use crossterm; 11 | 12 | pub use crossterm::event::*; 13 | pub use crossterm::style::*; 14 | 15 | pub use crossterm::cursor::SetCursorStyle; 16 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | - run : | 18 | VERSION="${GITHUB_REF#refs/*/}" make publish 19 | env: 20 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_KEY }} 21 | -------------------------------------------------------------------------------- /devenv.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://devenv.sh/devenv.schema.json 2 | inputs: 3 | nixpkgs: 4 | url: github:cachix/devenv-nixpkgs/rolling 5 | 6 | # If you're using non-OSS software, you can set allowUnfree to true. 7 | # allowUnfree: true 8 | 9 | # If you're willing to use a package that's vulnerable 10 | # permittedInsecurePackages: 11 | # - "openssl-1.1.1w" 12 | 13 | # If you have more than one devenv you can merge them 14 | #imports: 15 | # - ./backend 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ascii-forge" 3 | version="0.0.0" 4 | edition = "2021" 5 | description = "A Minimal TUI Ascii Application Engine that simplifies the use of crossterm" 6 | readme = "README.md" 7 | repository = "https://github.com/TheEmeraldBee/ascii-forge" 8 | license = "MIT OR Apache-2.0" 9 | 10 | [dependencies] 11 | compact_str = "0.9.0" 12 | crossterm = "0.29.0" 13 | unicode-width = "0.2.1" 14 | 15 | [dev-dependencies] 16 | regex = "1.10.3" 17 | 18 | [features] 19 | default = [] 20 | serde = ["crossterm/serde"] 21 | -------------------------------------------------------------------------------- /examples/event_visualizer.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, time::Duration}; 2 | 3 | use ascii_forge::prelude::*; 4 | 5 | fn main() -> Result<(), Box> { 6 | let mut event = Event::FocusGained; 7 | let mut window = Window::init()?; 8 | window.keyboard()?; 9 | 10 | loop { 11 | window.update(Duration::ZERO)?; 12 | 13 | if let Some(new_event) = window.events().last() { 14 | event = new_event.clone(); 15 | } 16 | 17 | if let Event::Paste(_) = &event { 18 | render!( 19 | window, 20 | vec2(0, 20) => [ "To Quit, Press Ctrl + C".red() ], 21 | vec2(0, 0) => [ format!("Paste!") ], 22 | ); 23 | } else { 24 | render!( 25 | window, 26 | vec2(0, 20) => [ "To Quit, Press Ctrl + C".red() ], 27 | vec2(0, 0) => [ format!("{:#?}", event).replace('\t', " ") ], 28 | ); 29 | } 30 | 31 | if event!(window, Event::Key(e) => 32 | *e == KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL) 33 | ) { 34 | break; 35 | } 36 | } 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use ascii_forge::prelude::*; 2 | use std::{io, time::Duration}; 3 | 4 | fn main() -> io::Result<()> { 5 | // Will init the window for you, handling all required procedures. 6 | let mut window = Window::init()?; 7 | 8 | // Ask the system to handle panics for us. 9 | handle_panics(); 10 | 11 | loop { 12 | // Ask the window to draw, handle events, and fix sizing issues. 13 | // Duration is the time for which to poll events before re-rendering. 14 | window.update(Duration::from_millis(200))?; 15 | 16 | // Render elements to the window 17 | render!( 18 | window, 19 | vec2(0, 0) => [ "Hello World!" ], 20 | vec2(0, 1) => [ "Press `Enter` to exit!".red() ], 21 | vec2(0, 2) => [ 22 | "Render ".red(), 23 | "Multiple ".yellow(), 24 | "Elements ", 25 | "In one go!".to_string() 26 | ], 27 | ); 28 | 29 | // Check if the Enter Key was pressed, and exit the app if it was. 30 | if event!(window, Event::Key(e) => e.code == KeyCode::Enter) { 31 | break; 32 | } 33 | } 34 | 35 | // Restore the window, enabling the window to function normally again 36 | // If nothing will be run after this, once the window is dropped, this will be run implicitly. 37 | window.restore() 38 | } 39 | -------------------------------------------------------------------------------- /examples/confirmation.rs: -------------------------------------------------------------------------------- 1 | use ascii_forge::prelude::*; 2 | use std::{io, time::Duration}; 3 | 4 | pub fn confirmation() -> io::Result { 5 | let mut window = Window::init_inline(1)?; 6 | window.keyboard()?; 7 | 8 | loop { 9 | render!(window, vec2(0, 0) => [ "Are You Sure? (`y` / `n`)" ]); 10 | 11 | if event!(window, Event::Key(e) => e.code == KeyCode::Char('Y') || e.code == KeyCode::Char('y')) 12 | { 13 | return Ok(true); 14 | } 15 | 16 | if event!(window, Event::Key(e) => e.code == KeyCode::Char('n') || e.code == KeyCode::Char('N')) 17 | { 18 | return Ok(false); 19 | } 20 | 21 | // Update the window 22 | window.update(Duration::from_millis(1000))?; 23 | } 24 | } 25 | 26 | pub fn standard_confirmation() -> io::Result { 27 | println!("Are you Sure? (`Y` / `N`)"); 28 | loop { 29 | let mut input = String::new(); 30 | 31 | io::stdin().read_line(&mut input)?; 32 | 33 | if input.trim().to_lowercase() == *"y" { 34 | return Ok(true); 35 | } 36 | if input.trim().to_lowercase() == *"n" { 37 | return Ok(false); 38 | } 39 | println!( 40 | "Invalid Input {}, please input either `Y` or `N`", 41 | input.trim() 42 | ); 43 | } 44 | } 45 | 46 | fn main() -> io::Result<()> { 47 | println!("State: {}", standard_confirmation()?); 48 | 49 | println!("State: {}", confirmation()?); 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /examples/multiwidth.rs: -------------------------------------------------------------------------------- 1 | use ascii_forge::prelude::*; 2 | use std::{io, time::Duration}; 3 | 4 | fn main() -> io::Result<()> { 5 | let mut window = Window::init()?; 6 | handle_panics(); 7 | 8 | let buf = Buffer::sized_element("Normal: Hello World!\nWide: 👩‍👩‍👧‍👦 and 🚀\nMixed: a👩‍👩‍👧‍👦b🚀c"); 9 | 10 | let buf_size = buf.size(); 11 | 12 | let mut pos = (0i16, 0i16); 13 | let mut vel = (1i16, 1i16); 14 | 15 | loop { 16 | window.update(Duration::from_millis(60))?; 17 | let win_size = window.size(); 18 | let max_x = win_size.x as i16; 19 | let max_y = win_size.y as i16; 20 | 21 | pos.0 += vel.0; 22 | pos.1 += vel.1; 23 | 24 | if pos.0 < 0 { 25 | pos.0 = 0; 26 | vel.0 = -vel.0; 27 | } else if pos.0 + buf_size.x as i16 >= max_x { 28 | pos.0 = max_x - buf_size.x as i16; 29 | vel.0 = -vel.0; 30 | } 31 | 32 | if pos.1 < 0 { 33 | pos.1 = 0; 34 | vel.1 = -vel.1; 35 | } else if pos.1 + buf_size.y as i16 >= max_y { 36 | pos.1 = max_y - buf_size.y as i16; 37 | vel.1 = -vel.1; 38 | } 39 | 40 | render!( 41 | window, 42 | vec2(pos.0 as u16, pos.1 as u16) => [ buf ], 43 | vec2(0, max_y.saturating_sub(2) as u16) => [ "Press `Enter` to exit!".red() ], 44 | ); 45 | 46 | if event!(window, Event::Key(e) => e.code == KeyCode::Enter) { 47 | break; 48 | } 49 | } 50 | 51 | window.restore() 52 | } 53 | -------------------------------------------------------------------------------- /src/math.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Add, AddAssign, Sub, SubAssign}; 2 | 3 | /// A 2d Vector 4 | /// Can be made from (u16, u16). 5 | /// Using a single u16.into() will create a vec2 where both values are the same. 6 | /// Basic Mathematic Operations are supported 7 | #[derive(Default, Debug, Eq, PartialEq, PartialOrd, Ord, Copy, Clone)] 8 | pub struct Vec2 { 9 | pub x: u16, 10 | pub y: u16, 11 | } 12 | 13 | impl From<(u16, u16)> for Vec2 { 14 | fn from(value: (u16, u16)) -> Self { 15 | vec2(value.0, value.1) 16 | } 17 | } 18 | 19 | impl From for Vec2 { 20 | fn from(value: u16) -> Self { 21 | vec2(value, value) 22 | } 23 | } 24 | 25 | impl> Add for Vec2 { 26 | type Output = Vec2; 27 | fn add(mut self, rhs: V) -> Self::Output { 28 | let rhs = rhs.into(); 29 | self.x += rhs.x; 30 | self.y += rhs.y; 31 | self 32 | } 33 | } 34 | 35 | impl> AddAssign for Vec2 { 36 | fn add_assign(&mut self, rhs: V) { 37 | let rhs = rhs.into(); 38 | self.x += rhs.x; 39 | self.y += rhs.y; 40 | } 41 | } 42 | 43 | impl> Sub for Vec2 { 44 | type Output = Vec2; 45 | fn sub(mut self, rhs: V) -> Self::Output { 46 | let rhs = rhs.into(); 47 | self.x -= rhs.x; 48 | self.y -= rhs.y; 49 | self 50 | } 51 | } 52 | 53 | impl> SubAssign for Vec2 { 54 | fn sub_assign(&mut self, rhs: V) { 55 | let rhs = rhs.into(); 56 | self.x -= rhs.x; 57 | self.y -= rhs.y; 58 | } 59 | } 60 | 61 | /// Creates a Vec2 from the given inputs. 62 | pub const fn vec2(x: u16, y: u16) -> Vec2 { 63 | Vec2 { x, y } 64 | } 65 | -------------------------------------------------------------------------------- /examples/inline.rs: -------------------------------------------------------------------------------- 1 | use ascii_forge::prelude::*; 2 | use std::{ 3 | io, 4 | time::{Duration, SystemTime}, 5 | }; 6 | 7 | fn progress_bar() -> io::Result<()> { 8 | let mut window = Window::init_inline(2)?; 9 | 10 | let timer = SystemTime::now(); 11 | let duration = Duration::from_secs(3); 12 | 13 | // The Inline Render Loop 14 | loop { 15 | // Render's the Window and captures events 16 | window.update(Duration::ZERO)?; 17 | 18 | let amount_done = SystemTime::now().duration_since(timer).unwrap(); 19 | 20 | let percent = amount_done.as_secs_f64() / duration.as_secs_f64(); 21 | 22 | if percent >= 1.0 { 23 | break; 24 | } 25 | 26 | let x = (window.size().x as f64 * percent).round() as u16; 27 | 28 | // Create the progress bar text 29 | let text_green = "|".repeat(x as usize).green(); 30 | let text_red = "|".repeat((window.size().x - x) as usize).red(); 31 | 32 | // Render the Progress Bar 33 | render!(window, 34 | vec2(0, 1) => [ text_green ], 35 | vec2(x, 1) => [ text_red ], 36 | vec2(0, 0) => [ "Progress" ], 37 | ); 38 | 39 | // End the loop if key is pressed early 40 | if event!(window, Event::Key(e) => *e == KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)) 41 | { 42 | break; 43 | } 44 | } 45 | 46 | window.restore() 47 | } 48 | 49 | fn main() -> io::Result<()> { 50 | // Start by asking the terminal to handle if a panic happens. 51 | handle_panics(); 52 | 53 | // Render The Progress bar. 54 | progress_bar()?; 55 | 56 | println!("Progress bar complete!"); 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /examples/cursor.rs: -------------------------------------------------------------------------------- 1 | use std::{io, time::Duration}; 2 | 3 | use ascii_forge::prelude::*; 4 | 5 | fn main() -> io::Result<()> { 6 | let mut window_a = Window::init()?; 7 | window_a.set_cursor_visible(true); 8 | window_a.set_cursor_style(SetCursorStyle::BlinkingBar); 9 | 10 | loop { 11 | window_a.update(Duration::from_millis(500))?; 12 | 13 | render!(window_a, 14 | (0, 0) => [ "Controls:" ], 15 | (0, 1) => [ "hjkl: Move Cursor".green() ], 16 | (0, 2) => [ "H: Toggle Cursor Visibility".blue() ], 17 | (0, 3) => [ "b/B: Change Cursor Style".magenta() ], 18 | (0, 4) => [ "q: Quit".red() ], 19 | ); 20 | 21 | if event!(window_a, Event::Key(k) => k.code == KeyCode::Char('q')) { 22 | break; 23 | } else if event!(window_a, Event::Key(k) => k.code == KeyCode::Char('h')) { 24 | window_a.move_cursor(-1, 0) 25 | } else if event!(window_a, Event::Key(k) => k.code == KeyCode::Char('l')) { 26 | window_a.move_cursor(1, 0) 27 | } else if event!(window_a, Event::Key(k) => k.code == KeyCode::Char('j')) { 28 | window_a.move_cursor(0, 1) 29 | } else if event!(window_a, Event::Key(k) => k.code == KeyCode::Char('k')) { 30 | window_a.move_cursor(0, -1) 31 | } else if event!(window_a, Event::Key(k) => k.code == KeyCode::Char('H')) { 32 | window_a.set_cursor_visible(!window_a.cursor_visible()) 33 | } else if event!(window_a, Event::Key(k) => k.code == KeyCode::Char('b')) { 34 | window_a.set_cursor_style(SetCursorStyle::BlinkingBar); 35 | } else if event!(window_a, Event::Key(k) => k.code == KeyCode::Char('B')) { 36 | window_a.set_cursor_style(SetCursorStyle::BlinkingBlock); 37 | } 38 | } 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /devenv.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devenv": { 4 | "locked": { 5 | "dir": "src/modules", 6 | "lastModified": 1749934215, 7 | "owner": "cachix", 8 | "repo": "devenv", 9 | "rev": "0ad2d684f722b41578b34670428161d996382e64", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "dir": "src/modules", 14 | "owner": "cachix", 15 | "repo": "devenv", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-compat": { 20 | "flake": false, 21 | "locked": { 22 | "lastModified": 1747046372, 23 | "owner": "edolstra", 24 | "repo": "flake-compat", 25 | "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "edolstra", 30 | "repo": "flake-compat", 31 | "type": "github" 32 | } 33 | }, 34 | "git-hooks": { 35 | "inputs": { 36 | "flake-compat": "flake-compat", 37 | "gitignore": "gitignore", 38 | "nixpkgs": [ 39 | "nixpkgs" 40 | ] 41 | }, 42 | "locked": { 43 | "lastModified": 1749636823, 44 | "owner": "cachix", 45 | "repo": "git-hooks.nix", 46 | "rev": "623c56286de5a3193aa38891a6991b28f9bab056", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "cachix", 51 | "repo": "git-hooks.nix", 52 | "type": "github" 53 | } 54 | }, 55 | "gitignore": { 56 | "inputs": { 57 | "nixpkgs": [ 58 | "git-hooks", 59 | "nixpkgs" 60 | ] 61 | }, 62 | "locked": { 63 | "lastModified": 1709087332, 64 | "owner": "hercules-ci", 65 | "repo": "gitignore.nix", 66 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "hercules-ci", 71 | "repo": "gitignore.nix", 72 | "type": "github" 73 | } 74 | }, 75 | "nixpkgs": { 76 | "locked": { 77 | "lastModified": 1746807397, 78 | "owner": "cachix", 79 | "repo": "devenv-nixpkgs", 80 | "rev": "c5208b594838ea8e6cca5997fbf784b7cca1ca90", 81 | "type": "github" 82 | }, 83 | "original": { 84 | "owner": "cachix", 85 | "ref": "rolling", 86 | "repo": "devenv-nixpkgs", 87 | "type": "github" 88 | } 89 | }, 90 | "root": { 91 | "inputs": { 92 | "devenv": "devenv", 93 | "git-hooks": "git-hooks", 94 | "nixpkgs": "nixpkgs", 95 | "pre-commit-hooks": [ 96 | "git-hooks" 97 | ] 98 | } 99 | } 100 | }, 101 | "root": "root", 102 | "version": 7 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/TheEmeraldBee/ascii-forge/blob/master/logo.png?raw=true) 2 | 3 | # Ascii-Forge 4 | An oppinionated terminal canvas rendering engine built off of crossterm with the goal of improving terminal UI/Games without adding any un-needed elements. 5 | 6 | # Why? 7 | Although other terminal UI Engines already exist, like [Ratatui](https://github.com/ratatui-org/ratatui), I felt there was a lot of extra elements that wasn't needed for a small application or game. 8 | 9 | As well, there aren't many bare-bones terminal canvas engines with as much flexability as would be needed to make a fun game. In order to acomplish this, all elements of the engine are available, at all times. 10 | 11 | # But What is Different? 12 | As said before, Ascii-Forge is oppinionated, you don't have a choice of the backend, crossterm is what you get, but it is the best, and one of the only fully cross-platform terminal engines. 13 | 14 | To list off some big differences: 15 | - Keeping it as small as possible while still making things easy. 16 | - Absolutely everything used to make the engine available is available to you. 17 | - This means that if the update method doesn't work as expected, you can make your own using the other methods. 18 | - Want to access the stdout that the window is using, use the `io()` method! 19 | - Most of the larger engines make their own layout system, in this engine, the layout engine is totally optional. 20 | - The layout engine is very basic, and relies on the user to handle the rendering of things. 21 | 22 | # Examples 23 | Most of the examples will be found in the [examples](https://github.com/TheEmeraldBee/ascii-forge/tree/master/examples) directory 24 | 25 | Simplest Example Included Here. 26 | ```rust 27 | use std::{io, time::Duration}; 28 | 29 | use ascii_forge::prelude::*; 30 | 31 | fn main() -> io::Result<()> { 32 | // Will init the window for you, handling all required procedures. 33 | let mut window = Window::init()?; 34 | 35 | // Ask the system to handle panics for us. 36 | handle_panics(); 37 | 38 | loop { 39 | // Ask the window to draw, handle events, and fix sizing issues. 40 | // Duration is the time for which to poll events before re-rendering. 41 | window.update(Duration::from_millis(200))?; 42 | 43 | // Render elements to the window 44 | render!(window, 45 | vec2(0, 0) => [ "Hello World!" ], 46 | vec2(0, 1) => [ "Press `Enter` to exit!".red() ], 47 | vec2(0, 2) => [ 48 | "Render ".red(), 49 | "Multiple ".yellow(), 50 | "Elements ", 51 | "In one go!".to_string() 52 | ] 53 | ); 54 | 55 | // Check if the Enter Key was pressed, and exit the app if it was. 56 | if event!(window, Event::Key(e) => e.code == KeyCode::Enter) { 57 | break; 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | # Documentation 64 | - [docs.rs](https://docs.rs/ascii-forge/latest/ascii_forge/) 65 | - See the examples for basic usage 66 | -------------------------------------------------------------------------------- /src/renderer/cell.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::prelude::*; 4 | use compact_str::{CompactString, ToCompactString}; 5 | use unicode_width::UnicodeWidthStr; 6 | 7 | /// A cell that stores a symbol, and the style that will be applied to it. 8 | #[derive(Debug, Clone, Eq, PartialEq)] 9 | pub struct Cell { 10 | text: CompactString, 11 | style: ContentStyle, 12 | width: u16, 13 | } 14 | 15 | impl Default for Cell { 16 | fn default() -> Self { 17 | Self::chr('\0') 18 | } 19 | } 20 | 21 | impl Cell { 22 | pub fn new>(text: impl Into, style: S) -> Self { 23 | let text = text.into(); 24 | Self { 25 | width: text.width() as u16, 26 | text, 27 | style: style.into(), 28 | } 29 | } 30 | 31 | pub fn string(string: impl AsRef) -> Self { 32 | let text = CompactString::new(string); 33 | Self { 34 | width: text.width() as u16, 35 | text, 36 | style: ContentStyle::default(), 37 | } 38 | } 39 | 40 | pub fn chr(chr: char) -> Self { 41 | let text = chr.to_compact_string(); 42 | Self { 43 | width: text.width() as u16, 44 | text, 45 | style: ContentStyle::default(), 46 | } 47 | } 48 | 49 | pub fn styled(content: StyledContent) -> Self { 50 | let text = CompactString::new(format!("{}", content.content())); 51 | Self { 52 | width: text.width() as u16, 53 | text, 54 | style: *content.style(), 55 | } 56 | } 57 | 58 | pub fn width(&self) -> u16 { 59 | self.width 60 | } 61 | 62 | pub fn is_empty(&self) -> bool { 63 | self.text.trim().is_empty() 64 | } 65 | 66 | pub fn text(&self) -> &str { 67 | &self.text 68 | } 69 | 70 | pub fn text_mut(&mut self) -> &mut CompactString { 71 | &mut self.text 72 | } 73 | 74 | pub fn style(&self) -> &ContentStyle { 75 | &self.style 76 | } 77 | 78 | pub fn style_mut(&mut self) -> &mut ContentStyle { 79 | &mut self.style 80 | } 81 | } 82 | 83 | impl Render for Cell { 84 | fn render(&self, loc: crate::prelude::Vec2, buffer: &mut crate::prelude::Buffer) -> Vec2 { 85 | buffer.set(loc, self.clone()); 86 | loc 87 | } 88 | } 89 | 90 | macro_rules! str_impl { 91 | ($($ty:ty)*) => { 92 | $( 93 | impl From<$ty> for Cell { 94 | fn from(value: $ty) -> Self { 95 | Self::string(value) 96 | } 97 | } 98 | )* 99 | }; 100 | } 101 | 102 | str_impl! {&str String} 103 | 104 | impl From for Cell { 105 | fn from(value: char) -> Self { 106 | Self::chr(value) 107 | } 108 | } 109 | 110 | impl From> for Cell { 111 | fn from(value: StyledContent) -> Self { 112 | Self::styled(value) 113 | } 114 | } 115 | 116 | impl Display for Cell { 117 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 118 | write!(f, "{}", StyledContent::new(self.style, &self.text)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /examples/borders.rs: -------------------------------------------------------------------------------- 1 | use ascii_forge::{prelude::*, widgets::Border}; 2 | use std::{io, time::Duration}; 3 | 4 | fn main() -> io::Result<()> { 5 | // Will init the window for you, handling all required procedures. 6 | let mut window = Window::init()?; 7 | // Ask the system to handle panics for us. 8 | handle_panics(); 9 | 10 | loop { 11 | // Ask the window to draw, handle events, and fix sizing issues. 12 | // Duration is the time for which to poll events before re-rendering. 13 | window.update(Duration::from_millis(200))?; 14 | 15 | // Create borders with different styles 16 | let square_border = Border::square(30, 5) 17 | .with_title("Square Border") 18 | .with_style(ContentStyle::new().cyan()); 19 | 20 | let rounded_border = Border::rounded(30, 5) 21 | .with_title("Rounded Border".red()) 22 | .with_style(ContentStyle::new().green()); 23 | 24 | let thick_border = Border::thick(30, 5) 25 | .with_title("Thick Border".blue()) 26 | .with_style(ContentStyle::new().yellow()); 27 | 28 | let double_border = Border::double(30, 5) 29 | .with_title("Double Border".on_green().black()) 30 | .with_style(ContentStyle::new().magenta()); 31 | 32 | // A border with a very long title to demonstrate clipping 33 | let clipped_border = Border::rounded(25, 4) 34 | .with_title("This is a very long title that will be clipped!") 35 | .with_style(ContentStyle::new().red()); 36 | 37 | // Render all the borders 38 | render!( 39 | window, 40 | vec2(2, 1) => [ square_border ], 41 | vec2(2, 7) => [ rounded_border ], 42 | vec2(2, 13) => [ thick_border ], 43 | vec2(2, 19) => [ double_border ], 44 | vec2(35, 1) => [ clipped_border ], 45 | vec2(35, 7) => [ "Borders can have:".white() ], 46 | vec2(35, 8) => [ "• Custom styles".dark_grey() ], 47 | vec2(35, 9) => [ "• Titles".dark_grey() ], 48 | vec2(35, 10) => [ "• Different border types".dark_grey() ], 49 | vec2(2, 25) => [ "Press `Enter` to exit!".red() ], 50 | ); 51 | 52 | // You can also render content inside borders 53 | let content_border = Border::square(30, 8) 54 | .with_title("Content Inside") 55 | .with_style(ContentStyle::new().blue()); 56 | 57 | let border_loc = render!(window, vec2(35, 12) => [ content_border ]); 58 | 59 | render!( 60 | window, 61 | border_loc => [ "Hello from inside!".white() ], 62 | border_loc + vec2(0, 1) => [ "Content can be".dark_grey() ], 63 | border_loc + vec2(0, 2) => [ "rendered inside".dark_grey() ], 64 | border_loc + vec2(0, 3) => [ "the border area.".dark_grey() ], 65 | ); 66 | 67 | // Check if the Enter Key was pressed, and exit the app if it was. 68 | if event!(window, Event::Key(e) => e.code == KeyCode::Enter) { 69 | break; 70 | } 71 | } 72 | 73 | // Restore the window, enabling the window to function normally again 74 | // If nothing will be run after this, once the window is dropped, this will be run implicitly. 75 | window.restore() 76 | } 77 | -------------------------------------------------------------------------------- /examples/input_validator.rs: -------------------------------------------------------------------------------- 1 | use ascii_forge::prelude::*; 2 | use regex::Regex; 3 | use std::{fmt::Display, io, time::Duration}; 4 | 5 | pub fn input(validator: T) -> io::Result 6 | where 7 | T: Fn(&str) -> Option, 8 | { 9 | let mut window = Window::init_inline(2)?; 10 | 11 | let mut text = String::new(); 12 | let mut status_text; 13 | 14 | loop { 15 | window.update(Duration::ZERO)?; 16 | 17 | if validator(&text).is_some() { 18 | status_text = "-- Valid --".green(); 19 | } else { 20 | status_text = "-- Invalid --".red(); 21 | } 22 | 23 | render!(window, 24 | vec2(0, 0) => [ status_text ], 25 | vec2(0, 1) => ["> ", text], 26 | ); 27 | 28 | for event in window.events() { 29 | if let Event::Key(e) = event { 30 | match e.code { 31 | KeyCode::Backspace => { 32 | text.pop(); 33 | } 34 | KeyCode::Enter => { 35 | if let Some(t) = validator(&text) { 36 | return Ok(t); 37 | } 38 | } 39 | KeyCode::Char(c) => text.push(c), 40 | _ => {} 41 | } 42 | } 43 | } 44 | 45 | if event!(window, Event::Key(e) => *e == KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)) 46 | { 47 | return Err(io::Error::new( 48 | io::ErrorKind::Interrupted, 49 | "Input validation canceled", 50 | )); 51 | } 52 | } 53 | } 54 | 55 | #[derive(Debug)] 56 | pub struct Email { 57 | pub prefix: String, 58 | pub suffix: String, 59 | } 60 | 61 | impl Display for Email { 62 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 63 | write!(f, "{}@{}", self.prefix, self.suffix) 64 | } 65 | } 66 | 67 | fn email(text: &str) -> Option { 68 | let mut email = Email { 69 | prefix: "".to_string(), 70 | suffix: "".to_string(), 71 | }; 72 | 73 | let regex = match Regex::new(r"^(?[\w\-\.]+)@(?[\w-]+\.+[\w-]{2,4})$") 74 | .expect("Regex should be fine") 75 | .captures(text) 76 | { 77 | Some(s) => s, 78 | None => return None, 79 | }; 80 | 81 | if let Some(item) = regex.name("prefix") { 82 | email.prefix = item.as_str().to_string(); 83 | } else { 84 | return None; 85 | } 86 | 87 | if let Some(item) = regex.name("suffix") { 88 | email.suffix = item.as_str().to_string(); 89 | } else { 90 | return None; 91 | } 92 | 93 | Some(email) 94 | } 95 | 96 | fn main() -> io::Result<()> { 97 | handle_panics(); 98 | 99 | println!("Input your age!"); 100 | let num = match input(|e| match e.parse::() { 101 | Ok(t) => Some(t), 102 | Err(_) => None, 103 | }) { 104 | Ok(t) => t, 105 | Err(_) => return Ok(()), 106 | }; 107 | 108 | println!("Input your email!"); 109 | let email = match input(email) { 110 | Ok(t) => t, 111 | Err(_) => return Ok(()), 112 | }; 113 | 114 | println!("{num}, {email}"); 115 | 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /examples/layout.rs: -------------------------------------------------------------------------------- 1 | use ascii_forge::{prelude::*, widgets::Border}; 2 | use std::{io, time::Duration}; 3 | 4 | fn main() -> io::Result<()> { 5 | // Will init the window for you, handling all required procedures. 6 | let mut window = Window::init()?; 7 | // Ask the system to handle panics for us. 8 | handle_panics(); 9 | 10 | loop { 11 | window.update(Duration::from_millis(200))?; 12 | let window_size = window.size(); 13 | 14 | // 1. Define the Layout structure, using Layout::row for both vertical and horizontal splits. 15 | let layout = Layout::new() 16 | // Row 1: Header (fixed height, single column) 17 | // Height: fixed(3) | Columns: [flexible() (100% width)] 18 | .row(fixed(3), vec![flexible()]) 19 | // Row 2: Main Content (flexible height, horizontally split into two columns) 20 | // Height: flexible() | Columns: [fixed(20) (Sidebar width), flexible() (Main Panel width)] 21 | .row(flexible(), vec![fixed(20), flexible()]) 22 | // Row 3: Footer (fixed height, single column) 23 | // Height: fixed(2) | Columns: [flexible() (100% width)] 24 | .row(fixed(2), vec![flexible()]); 25 | 26 | // 2. Calculate the Rectangles (Rects) for the entire grid layout 27 | let rects = match layout.calculate(window_size) { 28 | Ok(r) => r, 29 | Err(e) => { 30 | // Handle terminal size errors, e.g., if a fixed size is too big 31 | render!(window, vec2(0, 0) => [ format!("Layout Error: {:?}", e).red() ]); 32 | continue; 33 | } 34 | }; 35 | 36 | // --- Layout Breakdown --- 37 | // Row 0, Column 0: Header Area 38 | let header_rect = rects[0][0]; 39 | // Row 1, Column 0: Sidebar Area 40 | let sidebar_rect = rects[1][0]; 41 | // Row 1, Column 1: Main Panel Area 42 | let main_panel_rect = rects[1][1]; 43 | // Row 2, Column 0: Footer Area 44 | let footer_rect = rects[2][0]; 45 | 46 | // 3. Render all Borders and Content 47 | 48 | // --- Header (Row 0) --- 49 | let header_border = Border::double(header_rect.width, header_rect.height) 50 | .with_title(" Complex Application Header ".yellow().on_blue()); 51 | let header_inner = render!(window, header_rect.position() => [ header_border ]); 52 | render!(window, header_inner => [ "A Multi-Column Layout Example (Not Interactive)".white().bold() ]); 53 | 54 | // --- Sidebar (Row 1, Col 0) --- 55 | let sidebar_border = 56 | Border::rounded(sidebar_rect.width, sidebar_rect.height).with_title(" Nav ".magenta()); 57 | let sidebar_inner = render!(window, sidebar_rect.position() => [ sidebar_border ]); 58 | render!( 59 | window, 60 | sidebar_inner => [ "Home".bold() ], 61 | sidebar_inner + vec2(0, 1) => [ "Settings" ], 62 | sidebar_inner + vec2(0, 2) => [ "Help" ], 63 | ); 64 | 65 | // --- Main Panel (Row 1, Col 1) --- 66 | let main_panel_border = Border::square(main_panel_rect.width, main_panel_rect.height) 67 | .with_title(" Main Content Panel ".green()); 68 | let main_panel_inner = render!(window, main_panel_rect.position() => [ main_panel_border ]); 69 | render!( 70 | window, 71 | main_panel_inner => [ "This area is the main view." ], 72 | main_panel_inner + vec2(0, 1) => [ "It takes up all flexible space." ], 73 | ); 74 | 75 | // --- Footer (Row 2) --- 76 | let footer_border = 77 | Border::thick(footer_rect.width, footer_rect.height).with_title(" Status ".cyan()); 78 | let footer_inner_pos = render!(window, footer_rect.position() => [ footer_border ]); 79 | render!(window, footer_inner_pos + vec2(1, 0) => [ "Press 'Enter' to exit.".red() ]); 80 | 81 | // Check if the Enter Key was pressed, and exit the app if it was. 82 | if event!(window, Event::Key(e) => e.code == KeyCode::Enter) { 83 | break; 84 | } 85 | } 86 | 87 | // Restore the window 88 | window.restore() 89 | } 90 | -------------------------------------------------------------------------------- /src/widgets/border.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use crate::prelude::*; 4 | 5 | /// A basic border type. 6 | /// Rendering this will put the next content inside of the function 7 | /// Borders will skip rendering if their size is under a 3x3 8 | pub struct Border { 9 | pub size: Vec2, 10 | pub horizontal: &'static str, 11 | pub vertical: &'static str, 12 | pub top_left: &'static str, 13 | pub top_right: &'static str, 14 | pub bottom_left: &'static str, 15 | pub bottom_right: &'static str, 16 | 17 | pub title: Option, 18 | 19 | pub style: ContentStyle, 20 | } 21 | 22 | impl Deref for Border { 23 | type Target = ContentStyle; 24 | fn deref(&self) -> &Self::Target { 25 | &self.style 26 | } 27 | } 28 | 29 | impl DerefMut for Border { 30 | fn deref_mut(&mut self) -> &mut Self::Target { 31 | &mut self.style 32 | } 33 | } 34 | 35 | impl Border { 36 | pub const fn square(width: u16, height: u16) -> Border { 37 | Border { 38 | size: vec2(width, height), 39 | horizontal: "─", 40 | vertical: "│", 41 | top_right: "┐", 42 | top_left: "┌", 43 | bottom_left: "└", 44 | bottom_right: "┘", 45 | 46 | title: None, 47 | 48 | style: ContentStyle { 49 | foreground_color: None, 50 | background_color: None, 51 | underline_color: None, 52 | attributes: Attributes::none(), 53 | }, 54 | } 55 | } 56 | 57 | pub const fn rounded(width: u16, height: u16) -> Border { 58 | Border { 59 | size: vec2(width, height), 60 | horizontal: "─", 61 | vertical: "│", 62 | top_right: "╮", 63 | top_left: "╭", 64 | bottom_left: "╰", 65 | bottom_right: "╯", 66 | 67 | title: None, 68 | 69 | style: ContentStyle { 70 | foreground_color: None, 71 | background_color: None, 72 | underline_color: None, 73 | attributes: Attributes::none(), 74 | }, 75 | } 76 | } 77 | 78 | pub const fn thick(width: u16, height: u16) -> Border { 79 | Border { 80 | size: vec2(width, height), 81 | horizontal: "━", 82 | vertical: "┃", 83 | top_right: "┓", 84 | top_left: "┏", 85 | bottom_left: "┗", 86 | bottom_right: "┛", 87 | 88 | title: None, 89 | 90 | style: ContentStyle { 91 | foreground_color: None, 92 | background_color: None, 93 | underline_color: None, 94 | attributes: Attributes::none(), 95 | }, 96 | } 97 | } 98 | 99 | pub const fn double(width: u16, height: u16) -> Border { 100 | Border { 101 | size: vec2(width, height), 102 | horizontal: "═", 103 | vertical: "║", 104 | top_right: "╗", 105 | top_left: "╔", 106 | bottom_left: "╚", 107 | bottom_right: "╝", 108 | 109 | title: None, 110 | 111 | style: ContentStyle { 112 | foreground_color: None, 113 | background_color: None, 114 | underline_color: None, 115 | attributes: Attributes::none(), 116 | }, 117 | } 118 | } 119 | 120 | pub fn with_title(mut self, title: impl Render) -> Border { 121 | let title_buf = Buffer::sized_element(title); 122 | self.title = Some(title_buf); 123 | 124 | self 125 | } 126 | } 127 | 128 | impl Render for Border { 129 | fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2 { 130 | if self.size.x < 3 || self.size.y < 3 { 131 | return loc; 132 | } 133 | 134 | // Fill the interior with spaces 135 | for y in (loc.y + 1)..(loc.y + self.size.y.saturating_sub(1)) { 136 | for x in (loc.x + 1)..(loc.x + self.size.x.saturating_sub(1)) { 137 | buffer.set(vec2(x, y), " "); 138 | } 139 | } 140 | 141 | // Render vertical sides with style 142 | for y in (loc.y + 1)..(loc.y + self.size.y.saturating_sub(1)) { 143 | buffer.set( 144 | vec2(loc.x, y), 145 | StyledContent::new(self.style, self.vertical), 146 | ); 147 | buffer.set( 148 | vec2(loc.x + self.size.x.saturating_sub(1), y), 149 | StyledContent::new(self.style, self.vertical), 150 | ); 151 | } 152 | 153 | // Render top and bottom borders with style 154 | let horizontal_repeat = self 155 | .horizontal 156 | .repeat(self.size.x.saturating_sub(2) as usize); 157 | render!(buffer, 158 | loc => [ 159 | StyledContent::new(self.style, self.top_left), 160 | StyledContent::new(self.style, horizontal_repeat.as_str()), 161 | StyledContent::new(self.style, self.top_right) 162 | ], 163 | vec2(loc.x, loc.y + self.size.y.saturating_sub(1)) => [ 164 | StyledContent::new(self.style, self.bottom_left), 165 | StyledContent::new(self.style, self.horizontal.repeat(self.size.x.saturating_sub(2) as usize).as_str()), 166 | StyledContent::new(self.style, self.bottom_right) 167 | ] 168 | ); 169 | 170 | // Render title with clipping to fit within the border width 171 | if let Some(title) = &self.title { 172 | let max_title_width = self.size.x.saturating_sub(2); // Account for corners 173 | title.render_clipped(loc + vec2(1, 0), vec2(max_title_width, 1), buffer); 174 | } 175 | 176 | vec2(loc.x + 1, loc.y + 1) 177 | } 178 | 179 | fn size(&self) -> Vec2 { 180 | self.size 181 | } 182 | } 183 | 184 | #[cfg(test)] 185 | mod test { 186 | use crate::{ 187 | math::Vec2, 188 | render, 189 | widgets::border::Border, 190 | window::{Buffer, Render}, 191 | }; 192 | 193 | #[test] 194 | fn render_small() { 195 | let border = Border::square(0, 0); 196 | // Ensure no panics 197 | let _ = Buffer::sized_element(border); 198 | } 199 | 200 | #[test] 201 | fn check_size() { 202 | let border = Border::square(16, 16); 203 | let mut buf = Buffer::new((80, 80)); 204 | render!(buf, (0, 0) => [ border ]); 205 | buf.shrink(); 206 | assert_eq!(buf.size(), border.size()) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /examples/space_invaders.rs: -------------------------------------------------------------------------------- 1 | /* 2 | An Example of Something slightly more complex that could be achieved. 3 | This is not a complete example, it is not fun, it is purely to show how things COULD be made. 4 | 5 | Requires the "keyboard" feature to be enabled 6 | */ 7 | 8 | use std::{ 9 | io, 10 | time::{Duration, SystemTime}, 11 | }; 12 | 13 | use ascii_forge::prelude::*; 14 | 15 | pub struct Projectile { 16 | loc: (f32, f32), 17 | velocity: f32, 18 | element: E, 19 | } 20 | 21 | impl Projectile { 22 | pub fn new(loc: Vec2, velocity: f32, element: E) -> Self { 23 | Self { 24 | loc: (loc.x as f32, loc.y as f32), 25 | velocity, 26 | element, 27 | } 28 | } 29 | 30 | pub fn update(&mut self) { 31 | self.loc.1 += self.velocity; 32 | } 33 | 34 | pub fn draw_loc(&self) -> Vec2 { 35 | vec2(self.loc.0.floor() as u16, self.loc.1.floor() as u16) 36 | } 37 | 38 | pub fn draw(&self, window: &mut Window) { 39 | render!(window, 40 | self.draw_loc() => [ self.element ] 41 | ); 42 | } 43 | 44 | pub fn alive(&self, window: &Window) -> bool { 45 | self.loc.1 >= 2.0 && self.loc.1 < (window.size().y - 2) as f32 46 | } 47 | } 48 | 49 | pub struct Player { 50 | loc: Vec2, 51 | element: E, 52 | input: i32, 53 | } 54 | 55 | impl Player { 56 | pub fn new(window: &Window, element: E) -> Self { 57 | Self { 58 | loc: vec2(window.size().x / 2, window.size().y - 3), 59 | input: 0, 60 | element, 61 | } 62 | } 63 | 64 | pub fn draw(&self, window: &mut Window) { 65 | render!(window, self.loc => [ self.element ]); 66 | } 67 | 68 | pub fn update(&mut self, window: &mut Window) { 69 | self.input = 0; 70 | if event!(window, Event::Key(e) => e.code == KeyCode::Right) { 71 | self.input = 1; 72 | } 73 | if event!(window, Event::Key(e) => e.code == KeyCode::Right) { 74 | self.input = -1; 75 | } 76 | 77 | self.loc.x = (self.loc.x as i32 + self.input).clamp(0, window.size().x as i32) as u16; 78 | } 79 | 80 | pub fn hit(&mut self, projectiles: &[Projectile]) -> bool { 81 | projectiles.iter().any(|x| x.draw_loc() == self.loc) 82 | } 83 | } 84 | 85 | pub struct Enemy { 86 | loc: Vec2, 87 | right: bool, 88 | element: E, 89 | score: u32, 90 | } 91 | 92 | impl Enemy { 93 | pub fn new(loc: Vec2, element: E, score: u32) -> Self { 94 | Self { 95 | loc, 96 | right: true, 97 | element, 98 | score, 99 | } 100 | } 101 | 102 | pub fn draw(&mut self, window: &mut Window) { 103 | render!(window, self.loc => [ self.element ]); 104 | } 105 | 106 | pub fn hit(&mut self, projectiles: &[Projectile]) -> bool { 107 | projectiles.iter().any(|x| { 108 | let loc = x.draw_loc(); 109 | loc.y == self.loc.y && ((loc.x)..=(loc.x + 2)).contains(&self.loc.x) 110 | }) 111 | } 112 | 113 | pub fn enemy_move(&mut self, window: &Window) -> bool { 114 | if self.loc.y >= window.size().y - 4 { 115 | true 116 | } else { 117 | match self.right { 118 | true => { 119 | self.loc.x += 1; 120 | if self.loc.x >= window.size().x { 121 | self.right = false; 122 | self.loc.y += 1; 123 | } 124 | } 125 | false => { 126 | self.loc.x -= 1; 127 | if self.loc.x == 0 { 128 | self.right = true; 129 | self.loc.y += 1; 130 | } 131 | } 132 | } 133 | 134 | false 135 | } 136 | } 137 | } 138 | 139 | pub fn main() -> io::Result<()> { 140 | // Create the window, and ask the engine to catch a panic 141 | let mut window = Window::init()?; 142 | 143 | // Require kitty keyboard support to be enabled. 144 | window.keyboard()?; 145 | 146 | handle_panics(); 147 | 148 | // Run the application 149 | // Store the result so restore happens no matter what. 150 | let result = app(&mut window); 151 | 152 | // Restore the previous screen on the terminal 153 | // Since a print statement will come after this, we want to restore the window. 154 | window.restore()?; 155 | 156 | // Now check if error 157 | let result = result?; 158 | 159 | // Print Exit Message 160 | println!("{}", result); 161 | Ok(()) 162 | } 163 | 164 | pub fn app(window: &mut Window) -> io::Result { 165 | let mut score = 0; 166 | let mut player = Player::new(window, 'W'.green()); 167 | 168 | let mut projectiles = vec![]; 169 | 170 | let mut enemies = vec![]; 171 | 172 | let mut delta = Duration::ZERO; 173 | 174 | let mut spawner = Duration::from_millis(800); 175 | 176 | let mut move_timer = Duration::from_millis(200); 177 | 178 | let mut shoot_timer = Duration::from_millis(500); 179 | 180 | let info_text = Buffer::sized_element("Press C-q to quit"); 181 | 182 | // Main Game Loop 183 | loop { 184 | let start = SystemTime::now(); 185 | // update the window, without blocking the screen 186 | window.update(Duration::from_secs_f64(1.0 / 60.0))?; 187 | 188 | if event!(window, Event::Key(e) => e.code == KeyCode::Char(' ')) { 189 | projectiles.push(Projectile::new( 190 | vec2(player.loc.x - 1, player.loc.y - 1), 191 | -0.3, 192 | "|||".green(), 193 | )) 194 | } 195 | 196 | // Render and update projectiles 197 | projectiles.retain(|x| x.alive(window)); 198 | 199 | projectiles.iter_mut().for_each(|x| { 200 | x.update(); 201 | x.draw(window); 202 | }); 203 | 204 | // Render and update the player. 205 | player.update(window); 206 | player.draw(window); 207 | 208 | match spawner.checked_sub(delta) { 209 | Some(s) => spawner = s, 210 | None => { 211 | enemies.push(Enemy::new(vec2(0, 3), 'M'.red(), 10)); 212 | spawner = Duration::from_secs(2); 213 | } 214 | } 215 | 216 | match move_timer.checked_sub(delta) { 217 | Some(m) => move_timer = m, 218 | None => { 219 | if enemies.iter_mut().any(|x| x.enemy_move(window)) { 220 | return Ok(format!("Game Over\nScore was: {}", score)); 221 | } 222 | move_timer = Duration::from_millis(200); 223 | } 224 | } 225 | 226 | if player.hit(&projectiles) { 227 | return Ok(format!("Game Over\nScore was: {}", score)); 228 | } 229 | 230 | match shoot_timer.checked_sub(delta) { 231 | Some(s) => shoot_timer = s, 232 | None => { 233 | enemies.iter_mut().for_each(|x| { 234 | projectiles.push(Projectile::new(vec2(x.loc.x, x.loc.y + 1), 0.3, "|".red())) 235 | }); 236 | shoot_timer = Duration::from_millis(500); 237 | } 238 | } 239 | 240 | enemies.retain_mut(|x| { 241 | if x.hit(&projectiles) { 242 | score += x.score; 243 | false 244 | } else { 245 | true 246 | } 247 | }); 248 | 249 | enemies.iter_mut().for_each(|x| x.draw(window)); 250 | 251 | render!( 252 | window, 253 | vec2(0, 0) => [ format!("Score: {}", score) ], 254 | vec2(window.size().x - info_text.size().x, 0) => [ info_text ], 255 | ); 256 | 257 | if event!(window, Event::Key(e) => *e == KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)) 258 | { 259 | break; 260 | } 261 | 262 | delta = SystemTime::now().duration_since(start).unwrap(); 263 | } 264 | 265 | Ok("Game Exited".to_string()) 266 | } 267 | -------------------------------------------------------------------------------- /src/renderer/buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | /** 4 | A screen buffer that can be rendered to, has a size 5 | 6 | This is the backbone of ascii-forge 7 | 8 | `Example` 9 | ```rust 10 | # use ascii_forge::prelude::*; 11 | # fn main() { 12 | // A 30x30 buffer window 13 | let mut buffer = Buffer::new((30, 30)); 14 | 15 | // Render Hello World to the top left of the buffer 16 | render!(buffer, (0, 0) => [ "Hello World!" ]); 17 | # } 18 | ``` 19 | 20 | */ 21 | #[derive(Debug)] 22 | pub struct Buffer { 23 | size: Vec2, 24 | cells: Vec, 25 | } 26 | 27 | impl AsMut for Buffer { 28 | fn as_mut(&mut self) -> &mut Buffer { 29 | self 30 | } 31 | } 32 | 33 | impl Buffer { 34 | /// Creates a new buffer of empty cells with the given size. 35 | pub fn new(size: impl Into) -> Self { 36 | let size = size.into(); 37 | Self { 38 | size, 39 | cells: vec![Cell::default(); size.x as usize * size.y as usize], 40 | } 41 | } 42 | 43 | /// Creates a new buffer filled with the given cell type. 44 | pub fn new_filled(size: impl Into, cell: impl Into) -> Self { 45 | let cell = cell.into(); 46 | let size = size.into(); 47 | 48 | Self { 49 | size, 50 | cells: vec![cell; size.x as usize * size.y as usize], 51 | } 52 | } 53 | 54 | /// Returns the current size of the buffer. 55 | pub fn size(&self) -> Vec2 { 56 | self.size 57 | } 58 | 59 | /// Sets a cell at the given location to the given cell 60 | pub fn set>(&mut self, loc: impl Into, cell: C) { 61 | let loc = loc.into(); 62 | let idx = self.index_of(loc); 63 | 64 | // Ignore if cell is out of bounds 65 | let Some(idx) = idx else { 66 | return; 67 | }; 68 | 69 | let cell = cell.into(); 70 | 71 | for i in 1..cell.width().saturating_sub(1) { 72 | self.set(loc + vec2(i, 0), Cell::default()); 73 | } 74 | 75 | self.cells[idx] = cell; 76 | } 77 | 78 | /// Sets a cell at the given location to an empty cell ('\0') 79 | pub fn del(&mut self, loc: impl Into) { 80 | self.set(loc, '\0'); 81 | } 82 | 83 | /// Sets all cells at the given location to the given cell 84 | pub fn fill>(&mut self, cell: C) { 85 | let cell = cell.into(); 86 | for i in 0..self.cells.len() { 87 | self.cells[i] = cell.clone() 88 | } 89 | } 90 | 91 | /// Returns a reverence to the cell at the given location. 92 | pub fn get(&self, loc: impl Into) -> Option<&Cell> { 93 | let idx = self.index_of(loc)?; 94 | self.cells.get(idx) 95 | } 96 | 97 | /// Returns a mutable reference to the cell at the given location. 98 | pub fn get_mut(&mut self, loc: impl Into) -> Option<&mut Cell> { 99 | let idx = self.index_of(loc)?; 100 | self.cells.get_mut(idx) 101 | } 102 | 103 | fn index_of(&self, loc: impl Into) -> Option { 104 | let loc = loc.into(); 105 | if loc.x > self.size.x || loc.y > self.size.y { 106 | return None; 107 | } 108 | 109 | let idx = loc.y as usize * self.size.x as usize + loc.x as usize; 110 | 111 | if (idx as u16) >= self.size.x * self.size.y { 112 | return None; 113 | } 114 | 115 | Some(idx.min((self.size.x as usize * self.size.y as usize) - 1)) 116 | } 117 | 118 | /// Clears the buffer, filling it with '\0' (empty) characters 119 | pub fn clear(&mut self) { 120 | *self = Self::new(self.size); 121 | } 122 | 123 | /// Returns the cells and locations that are different between the two buffers 124 | pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(Vec2, &'a Cell)> { 125 | assert!(self.size == other.size); 126 | 127 | let mut res = vec![]; 128 | let mut skip = 0; 129 | 130 | for y in 0..self.size.y { 131 | for x in 0..self.size.x { 132 | if skip > 0 { 133 | skip -= 1; 134 | continue; 135 | } 136 | 137 | let old = self.get((x, y)); 138 | let new = other.get((x, y)); 139 | 140 | if old != new { 141 | let new = new.expect("new should be in bounds"); 142 | skip = new.width().saturating_sub(1) as usize; 143 | res.push((vec2(x, y), new)) 144 | } 145 | } 146 | } 147 | 148 | res 149 | } 150 | 151 | /// Shrinks the buffer to the given size by dropping any cells that are empty ('\0') 152 | pub fn shrink(&mut self) { 153 | let mut max_whitespace_x = 0; 154 | let mut max_whitespace_y = 0; 155 | for x in (0..self.size.x).rev() { 156 | for y in (0..self.size.y).rev() { 157 | if self.get((x, y)).expect("Cell should be in bounds").text() != "\0" { 158 | max_whitespace_x = x.max(max_whitespace_x); 159 | max_whitespace_y = y.max(max_whitespace_y); 160 | } 161 | } 162 | } 163 | 164 | self.resize(vec2(max_whitespace_x + 1, max_whitespace_y + 1)); 165 | } 166 | 167 | /// Resizes the buffer while retaining elements that have already been rendered 168 | pub fn resize(&mut self, new_size: impl Into) { 169 | let new_size = new_size.into(); 170 | if self.size == new_size { 171 | return; 172 | } 173 | 174 | let mut new_elements = vec![]; 175 | 176 | for y in 0..new_size.y { 177 | for x in 0..new_size.x { 178 | new_elements.push(self.get((x, y)).expect("Cell should be in bounds").clone()); 179 | } 180 | } 181 | 182 | self.size = new_size; 183 | self.cells = new_elements; 184 | } 185 | 186 | /// Creates a Buffer from the given element with the minimum size it could have for that element. 187 | /// Useful for if you want to store any set of render elements in a custom element. 188 | pub fn sized_element(item: R) -> Self { 189 | let mut buff = Buffer::new((100, 100)); 190 | render!(buff, vec2(0, 0) => [ item ]); 191 | buff.shrink(); 192 | buff 193 | } 194 | 195 | /// Renders a clipped region of this buffer to another buffer. 196 | pub fn render_clipped( 197 | &self, 198 | loc: impl Into, 199 | clip_size: impl Into, 200 | buffer: &mut Buffer, 201 | ) -> Vec2 { 202 | let loc = loc.into(); 203 | let clip_size = clip_size.into(); 204 | 205 | for x in 0..clip_size.x.min(self.size().x) { 206 | if x + loc.x >= buffer.size().x { 207 | break; 208 | } 209 | for y in 0..clip_size.y.min(self.size().y) { 210 | if y + loc.y >= buffer.size().y { 211 | break; 212 | } 213 | 214 | let source_pos = vec2(x, y); 215 | let dest_pos = vec2(x + loc.x, y + loc.y); 216 | 217 | if let Some(cell) = self.get(source_pos) { 218 | if cell.text() != "\0" { 219 | buffer.set(dest_pos, cell.clone()); 220 | } 221 | } 222 | } 223 | } 224 | 225 | vec2( 226 | loc.x + clip_size.x.min(self.size().x), 227 | loc.y + clip_size.y.min(self.size().y), 228 | ) 229 | } 230 | } 231 | 232 | impl Render for Buffer { 233 | fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2 { 234 | for x in 0..self.size().x { 235 | if x + loc.x >= buffer.size().x { 236 | break; 237 | } 238 | for y in 0..self.size().y { 239 | if y + loc.y >= buffer.size().y { 240 | break; 241 | } 242 | 243 | let source_pos = vec2(x, y); 244 | let dest_pos = vec2(x + loc.x, y + loc.y); 245 | 246 | if let Some(cell) = self.get(source_pos) { 247 | if cell.text() != "\0" { 248 | buffer.set(dest_pos, cell.clone()); 249 | } 250 | } 251 | } 252 | } 253 | 254 | vec2(loc.x + buffer.size().x, loc.y + buffer.size().y) 255 | } 256 | 257 | fn size(&self) -> Vec2 { 258 | self.size 259 | } 260 | 261 | fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 { 262 | self.render_clipped(loc, clip_size, buffer) 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/renderer/render.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, marker::PhantomData}; 2 | 3 | use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; 4 | 5 | use crate::prelude::*; 6 | 7 | /// A macro to simplify rendering lots of items at once. 8 | /// The Buffer can be anything that implements AsMut 9 | /// This render will return the location of which the last element finished rendering. 10 | /** 11 | `Example` 12 | ```rust 13 | # use ascii_forge::prelude::*; 14 | # fn main() -> std::io::Result<()> { 15 | // Create a buffer 16 | let mut buffer = Buffer::new((32, 32)); 17 | 18 | // Render This works! and Another Element! To the window's buffer 19 | render!( 20 | buffer, 21 | (16, 16) => [ "This works!" ], 22 | (0, 0) => [ "Another Element!" ] 23 | ); 24 | 25 | # Ok(()) 26 | # } 27 | ``` 28 | */ 29 | #[macro_export] 30 | macro_rules! render { 31 | ($buffer:expr, $( $loc:expr => [$($render:expr),* $(,)?]),* $(,)? ) => {{ 32 | #[allow(unused_mut)] 33 | let mut loc; 34 | $( 35 | loc = Vec2::from($loc); 36 | $(loc = $render.render(loc, $buffer.as_mut()));*; 37 | let _ = loc; 38 | )* 39 | loc 40 | }}; 41 | } 42 | 43 | /// The main trait that allows for rendering an element at a location to the buffer. 44 | /// Render's return type is the location the render ended at. 45 | pub trait Render { 46 | /// Render the object to the buffer at the given location. 47 | fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2; 48 | 49 | /// Returns the resulting size of the element 50 | fn size(&self) -> Vec2 { 51 | let mut buf = Buffer::new((u16::MAX, u16::MAX)); 52 | render!(buf, vec2(0, 0) => [ self ]); 53 | buf.shrink(); 54 | buf.size() 55 | } 56 | 57 | /// Render's the element into a clipped view, allowing for clipping easily 58 | fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 { 59 | let mut buff = Buffer::new((100, 100)); 60 | render!(buff, vec2(0, 0) => [ self ]); 61 | buff.shrink(); 62 | 63 | buff.render_clipped(loc, clip_size, buffer) 64 | } 65 | } 66 | 67 | /* --------------- Implementations --------------- */ 68 | impl Render for char { 69 | fn render(&self, mut loc: Vec2, buffer: &mut Buffer) -> Vec2 { 70 | buffer.set(loc, *self); 71 | loc.x += self.width().unwrap_or(1).saturating_sub(1) as u16; 72 | loc 73 | } 74 | 75 | fn size(&self) -> Vec2 { 76 | vec2(self.width().unwrap_or(1) as u16, 1) 77 | } 78 | 79 | fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 { 80 | let char_width = self.width().unwrap_or(1) as u16; 81 | 82 | // Only render if there's enough space for the character 83 | if clip_size.x >= char_width && clip_size.y >= 1 { 84 | buffer.set(loc, *self); 85 | vec2(loc.x + char_width, loc.y) 86 | } else { 87 | loc 88 | } 89 | } 90 | } 91 | 92 | impl Render for &str { 93 | fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2 { 94 | render!(buffer, loc => [ StyledContent::new(ContentStyle::default(), self) ]) 95 | } 96 | 97 | fn size(&self) -> Vec2 { 98 | StyledContent::new(ContentStyle::default(), self).size() 99 | } 100 | 101 | fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 { 102 | StyledContent::new(ContentStyle::default(), self).render_clipped(loc, clip_size, buffer) 103 | } 104 | } 105 | 106 | impl From for Box { 107 | fn from(value: R) -> Self { 108 | Box::new(value) 109 | } 110 | } 111 | 112 | impl> + Clone> Render for Vec { 113 | fn render(&self, mut loc: Vec2, buffer: &mut Buffer) -> Vec2 { 114 | let items: Vec> = self.iter().map(|x| x.clone().into()).collect(); 115 | for item in items { 116 | loc = render!(buffer, loc => [ item ]); 117 | } 118 | loc 119 | } 120 | 121 | fn render_clipped(&self, mut loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 { 122 | let start_loc = loc; 123 | let items: Vec> = self.iter().map(|x| x.clone().into()).collect(); 124 | 125 | for item in items { 126 | // Calculate remaining clip space 127 | let used_x = loc.x.saturating_sub(start_loc.x); 128 | let used_y = loc.y.saturating_sub(start_loc.y); 129 | 130 | if used_y >= clip_size.y { 131 | break; 132 | } 133 | 134 | let remaining_clip = vec2( 135 | clip_size.x.saturating_sub(used_x), 136 | clip_size.y.saturating_sub(used_y), 137 | ); 138 | 139 | if remaining_clip.x == 0 || remaining_clip.y == 0 { 140 | break; 141 | } 142 | 143 | loc = item.render_clipped(loc, remaining_clip, buffer); 144 | } 145 | loc 146 | } 147 | } 148 | 149 | /// A Render type that doesn't get split. It purely renders the one item to the screen. 150 | /// Useful for multi-character emojis. 151 | pub struct CharString> + Clone> { 152 | pub text: F, 153 | marker: PhantomData, 154 | } 155 | 156 | impl> + Clone> CharString { 157 | pub fn new(text: F) -> Self { 158 | Self { 159 | text, 160 | marker: PhantomData {}, 161 | } 162 | } 163 | } 164 | 165 | impl> + Clone> Render for CharString { 166 | fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2 { 167 | render!(buffer, loc => [ Cell::styled(self.text.clone().into()) ]) 168 | } 169 | 170 | fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 { 171 | let cell = Cell::styled(self.text.clone().into()); 172 | let cell_width = cell.width(); 173 | 174 | // Only render if there's enough space for the entire cell 175 | if clip_size.x >= cell_width && clip_size.y >= 1 { 176 | buffer.set(loc, cell); 177 | vec2(loc.x + cell_width, loc.y) 178 | } else { 179 | loc 180 | } 181 | } 182 | } 183 | 184 | impl Render for String { 185 | fn render(&self, loc: Vec2, buffer: &mut Buffer) -> Vec2 { 186 | render!(buffer, loc => [ self.as_str() ]) 187 | } 188 | 189 | fn render_clipped(&self, loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 { 190 | self.as_str().render_clipped(loc, clip_size, buffer) 191 | } 192 | } 193 | 194 | impl Render for StyledContent { 195 | fn render(&self, mut loc: Vec2, buffer: &mut Buffer) -> Vec2 { 196 | let base_x = loc.x; 197 | for line in format!("{}", self.content()).split('\n') { 198 | loc.x = base_x; 199 | for chr in line.chars().collect::>() { 200 | buffer.set(loc, StyledContent::new(*self.style(), chr)); 201 | loc.x += chr.width().unwrap_or(1) as u16; 202 | } 203 | loc.y += 1; 204 | } 205 | loc.y -= 1; 206 | loc 207 | } 208 | 209 | fn size(&self) -> Vec2 { 210 | let mut width = 0; 211 | let mut height = 0; 212 | for line in format!("{}", self.content()).split('\n') { 213 | width = line.chars().count().max(width); 214 | height += line.width() as u16; 215 | } 216 | vec2(width as u16, height) 217 | } 218 | 219 | fn render_clipped(&self, mut loc: Vec2, clip_size: Vec2, buffer: &mut Buffer) -> Vec2 { 220 | let base_x = loc.x; 221 | let start_y = loc.y; 222 | let mut lines_rendered = 0; 223 | 224 | for line in format!("{}", self.content()).split('\n') { 225 | if lines_rendered >= clip_size.y { 226 | break; 227 | } 228 | 229 | loc.x = base_x; 230 | let mut chars_rendered = 0; 231 | 232 | for chr in line.chars().collect::>() { 233 | let chr_width = chr.width().unwrap_or(1) as u16; 234 | 235 | if chars_rendered + chr_width > clip_size.x { 236 | break; 237 | } 238 | 239 | buffer.set(loc, StyledContent::new(*self.style(), chr)); 240 | loc.x += chr_width; 241 | chars_rendered += chr_width; 242 | } 243 | 244 | loc.y += 1; 245 | lines_rendered += 1; 246 | } 247 | 248 | vec2( 249 | base_x + lines_rendered.min(clip_size.x), 250 | start_y + lines_rendered.min(clip_size.y), 251 | ) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ascii-forge" 16 | version = "0.0.0" 17 | dependencies = [ 18 | "compact_str", 19 | "crossterm", 20 | "regex", 21 | "unicode-width", 22 | ] 23 | 24 | [[package]] 25 | name = "autocfg" 26 | version = "1.1.0" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 29 | 30 | [[package]] 31 | name = "bitflags" 32 | version = "1.3.2" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 35 | 36 | [[package]] 37 | name = "bitflags" 38 | version = "2.9.0" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 41 | dependencies = [ 42 | "serde", 43 | ] 44 | 45 | [[package]] 46 | name = "castaway" 47 | version = "0.2.3" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 50 | dependencies = [ 51 | "rustversion", 52 | ] 53 | 54 | [[package]] 55 | name = "cfg-if" 56 | version = "1.0.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 59 | 60 | [[package]] 61 | name = "compact_str" 62 | version = "0.9.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" 65 | dependencies = [ 66 | "castaway", 67 | "cfg-if", 68 | "itoa", 69 | "rustversion", 70 | "ryu", 71 | "static_assertions", 72 | ] 73 | 74 | [[package]] 75 | name = "convert_case" 76 | version = "0.7.1" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 79 | dependencies = [ 80 | "unicode-segmentation", 81 | ] 82 | 83 | [[package]] 84 | name = "crossterm" 85 | version = "0.29.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 88 | dependencies = [ 89 | "bitflags 2.9.0", 90 | "crossterm_winapi", 91 | "derive_more", 92 | "document-features", 93 | "mio", 94 | "parking_lot", 95 | "rustix", 96 | "serde", 97 | "signal-hook", 98 | "signal-hook-mio", 99 | "winapi", 100 | ] 101 | 102 | [[package]] 103 | name = "crossterm_winapi" 104 | version = "0.9.1" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 107 | dependencies = [ 108 | "winapi", 109 | ] 110 | 111 | [[package]] 112 | name = "derive_more" 113 | version = "2.0.1" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 116 | dependencies = [ 117 | "derive_more-impl", 118 | ] 119 | 120 | [[package]] 121 | name = "derive_more-impl" 122 | version = "2.0.1" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 125 | dependencies = [ 126 | "convert_case", 127 | "proc-macro2", 128 | "quote", 129 | "syn", 130 | ] 131 | 132 | [[package]] 133 | name = "document-features" 134 | version = "0.2.11" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 137 | dependencies = [ 138 | "litrs", 139 | ] 140 | 141 | [[package]] 142 | name = "errno" 143 | version = "0.3.11" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 146 | dependencies = [ 147 | "libc", 148 | "windows-sys", 149 | ] 150 | 151 | [[package]] 152 | name = "hermit-abi" 153 | version = "0.3.9" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 156 | 157 | [[package]] 158 | name = "itoa" 159 | version = "1.0.15" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 162 | 163 | [[package]] 164 | name = "libc" 165 | version = "0.2.172" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 168 | 169 | [[package]] 170 | name = "linux-raw-sys" 171 | version = "0.9.4" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 174 | 175 | [[package]] 176 | name = "litrs" 177 | version = "0.4.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 180 | 181 | [[package]] 182 | name = "lock_api" 183 | version = "0.4.11" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 186 | dependencies = [ 187 | "autocfg", 188 | "scopeguard", 189 | ] 190 | 191 | [[package]] 192 | name = "log" 193 | version = "0.4.20" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 196 | 197 | [[package]] 198 | name = "memchr" 199 | version = "2.7.1" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 202 | 203 | [[package]] 204 | name = "mio" 205 | version = "1.0.2" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 208 | dependencies = [ 209 | "hermit-abi", 210 | "libc", 211 | "log", 212 | "wasi", 213 | "windows-sys", 214 | ] 215 | 216 | [[package]] 217 | name = "parking_lot" 218 | version = "0.12.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 221 | dependencies = [ 222 | "lock_api", 223 | "parking_lot_core", 224 | ] 225 | 226 | [[package]] 227 | name = "parking_lot_core" 228 | version = "0.9.9" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 231 | dependencies = [ 232 | "cfg-if", 233 | "libc", 234 | "redox_syscall", 235 | "smallvec", 236 | "windows-targets 0.48.5", 237 | ] 238 | 239 | [[package]] 240 | name = "proc-macro2" 241 | version = "1.0.95" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 244 | dependencies = [ 245 | "unicode-ident", 246 | ] 247 | 248 | [[package]] 249 | name = "quote" 250 | version = "1.0.40" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 253 | dependencies = [ 254 | "proc-macro2", 255 | ] 256 | 257 | [[package]] 258 | name = "redox_syscall" 259 | version = "0.4.1" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 262 | dependencies = [ 263 | "bitflags 1.3.2", 264 | ] 265 | 266 | [[package]] 267 | name = "regex" 268 | version = "1.10.3" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 271 | dependencies = [ 272 | "aho-corasick", 273 | "memchr", 274 | "regex-automata", 275 | "regex-syntax", 276 | ] 277 | 278 | [[package]] 279 | name = "regex-automata" 280 | version = "0.4.5" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" 283 | dependencies = [ 284 | "aho-corasick", 285 | "memchr", 286 | "regex-syntax", 287 | ] 288 | 289 | [[package]] 290 | name = "regex-syntax" 291 | version = "0.8.2" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 294 | 295 | [[package]] 296 | name = "rustix" 297 | version = "1.0.7" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 300 | dependencies = [ 301 | "bitflags 2.9.0", 302 | "errno", 303 | "libc", 304 | "linux-raw-sys", 305 | "windows-sys", 306 | ] 307 | 308 | [[package]] 309 | name = "rustversion" 310 | version = "1.0.20" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 313 | 314 | [[package]] 315 | name = "ryu" 316 | version = "1.0.20" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 319 | 320 | [[package]] 321 | name = "scopeguard" 322 | version = "1.2.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 325 | 326 | [[package]] 327 | name = "serde" 328 | version = "1.0.219" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 331 | dependencies = [ 332 | "serde_derive", 333 | ] 334 | 335 | [[package]] 336 | name = "serde_derive" 337 | version = "1.0.219" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 340 | dependencies = [ 341 | "proc-macro2", 342 | "quote", 343 | "syn", 344 | ] 345 | 346 | [[package]] 347 | name = "signal-hook" 348 | version = "0.3.17" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 351 | dependencies = [ 352 | "libc", 353 | "signal-hook-registry", 354 | ] 355 | 356 | [[package]] 357 | name = "signal-hook-mio" 358 | version = "0.2.4" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 361 | dependencies = [ 362 | "libc", 363 | "mio", 364 | "signal-hook", 365 | ] 366 | 367 | [[package]] 368 | name = "signal-hook-registry" 369 | version = "1.4.1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 372 | dependencies = [ 373 | "libc", 374 | ] 375 | 376 | [[package]] 377 | name = "smallvec" 378 | version = "1.13.1" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 381 | 382 | [[package]] 383 | name = "static_assertions" 384 | version = "1.1.0" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 387 | 388 | [[package]] 389 | name = "syn" 390 | version = "2.0.101" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 393 | dependencies = [ 394 | "proc-macro2", 395 | "quote", 396 | "unicode-ident", 397 | ] 398 | 399 | [[package]] 400 | name = "unicode-ident" 401 | version = "1.0.18" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 404 | 405 | [[package]] 406 | name = "unicode-segmentation" 407 | version = "1.12.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 410 | 411 | [[package]] 412 | name = "unicode-width" 413 | version = "0.2.1" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" 416 | 417 | [[package]] 418 | name = "wasi" 419 | version = "0.11.0+wasi-snapshot-preview1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 422 | 423 | [[package]] 424 | name = "winapi" 425 | version = "0.3.9" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 428 | dependencies = [ 429 | "winapi-i686-pc-windows-gnu", 430 | "winapi-x86_64-pc-windows-gnu", 431 | ] 432 | 433 | [[package]] 434 | name = "winapi-i686-pc-windows-gnu" 435 | version = "0.4.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 438 | 439 | [[package]] 440 | name = "winapi-x86_64-pc-windows-gnu" 441 | version = "0.4.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 444 | 445 | [[package]] 446 | name = "windows-sys" 447 | version = "0.52.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 450 | dependencies = [ 451 | "windows-targets 0.52.6", 452 | ] 453 | 454 | [[package]] 455 | name = "windows-targets" 456 | version = "0.48.5" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 459 | dependencies = [ 460 | "windows_aarch64_gnullvm 0.48.5", 461 | "windows_aarch64_msvc 0.48.5", 462 | "windows_i686_gnu 0.48.5", 463 | "windows_i686_msvc 0.48.5", 464 | "windows_x86_64_gnu 0.48.5", 465 | "windows_x86_64_gnullvm 0.48.5", 466 | "windows_x86_64_msvc 0.48.5", 467 | ] 468 | 469 | [[package]] 470 | name = "windows-targets" 471 | version = "0.52.6" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 474 | dependencies = [ 475 | "windows_aarch64_gnullvm 0.52.6", 476 | "windows_aarch64_msvc 0.52.6", 477 | "windows_i686_gnu 0.52.6", 478 | "windows_i686_gnullvm", 479 | "windows_i686_msvc 0.52.6", 480 | "windows_x86_64_gnu 0.52.6", 481 | "windows_x86_64_gnullvm 0.52.6", 482 | "windows_x86_64_msvc 0.52.6", 483 | ] 484 | 485 | [[package]] 486 | name = "windows_aarch64_gnullvm" 487 | version = "0.48.5" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 490 | 491 | [[package]] 492 | name = "windows_aarch64_gnullvm" 493 | version = "0.52.6" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 496 | 497 | [[package]] 498 | name = "windows_aarch64_msvc" 499 | version = "0.48.5" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 502 | 503 | [[package]] 504 | name = "windows_aarch64_msvc" 505 | version = "0.52.6" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 508 | 509 | [[package]] 510 | name = "windows_i686_gnu" 511 | version = "0.48.5" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 514 | 515 | [[package]] 516 | name = "windows_i686_gnu" 517 | version = "0.52.6" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 520 | 521 | [[package]] 522 | name = "windows_i686_gnullvm" 523 | version = "0.52.6" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 526 | 527 | [[package]] 528 | name = "windows_i686_msvc" 529 | version = "0.48.5" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 532 | 533 | [[package]] 534 | name = "windows_i686_msvc" 535 | version = "0.52.6" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 538 | 539 | [[package]] 540 | name = "windows_x86_64_gnu" 541 | version = "0.48.5" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 544 | 545 | [[package]] 546 | name = "windows_x86_64_gnu" 547 | version = "0.52.6" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 550 | 551 | [[package]] 552 | name = "windows_x86_64_gnullvm" 553 | version = "0.48.5" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 556 | 557 | [[package]] 558 | name = "windows_x86_64_gnullvm" 559 | version = "0.52.6" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 562 | 563 | [[package]] 564 | name = "windows_x86_64_msvc" 565 | version = "0.48.5" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 568 | 569 | [[package]] 570 | name = "windows_x86_64_msvc" 571 | version = "0.52.6" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 574 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | pub use crate::prelude::*; 2 | 3 | use crossterm::{ 4 | cursor::{self, Hide, MoveTo, Show}, 5 | event, execute, queue, 6 | terminal::{self, *}, 7 | tty::IsTty, 8 | }; 9 | use std::{ 10 | io::{self, Stdout, Write}, 11 | panic::{set_hook, take_hook}, 12 | time::Duration, 13 | }; 14 | 15 | #[derive(Default)] 16 | pub struct Inline { 17 | active: bool, 18 | kitty: bool, 19 | start: u16, 20 | } 21 | 22 | impl AsMut for Window { 23 | fn as_mut(&mut self) -> &mut Buffer { 24 | self.buffer_mut() 25 | } 26 | } 27 | 28 | /// The main window behind the application. 29 | /// Represents the terminal window, allowing it to be used similar to a buffer, 30 | /// but has extra event handling. 31 | /** 32 | ```rust, no_run 33 | # use ascii_forge::prelude::*; 34 | # fn main() -> std::io::Result<()> { 35 | let mut window = Window::init()?; 36 | render!(window, (10, 10) => [ "Element Here!" ]); 37 | # Ok(()) 38 | # } 39 | ``` 40 | */ 41 | pub struct Window { 42 | io: io::Stdout, 43 | buffers: [Buffer; 2], 44 | active_buffer: usize, 45 | events: Vec, 46 | 47 | last_cursor: (bool, Vec2, SetCursorStyle), 48 | 49 | cursor_visible: bool, 50 | cursor: Vec2, 51 | cursor_style: SetCursorStyle, 52 | cursor_dirty: bool, 53 | 54 | // Input Helpers, 55 | mouse_pos: Vec2, 56 | // Inlining 57 | inline: Option, 58 | // Event Handling 59 | just_resized: bool, 60 | } 61 | 62 | impl Default for Window { 63 | fn default() -> Self { 64 | Self::init().expect("Init should have succeeded") 65 | } 66 | } 67 | 68 | impl Window { 69 | /// Creates a new window from the given stdout. 70 | /// Please prefer to use init as it will do all of the terminal init stuff. 71 | pub fn new(io: io::Stdout) -> io::Result { 72 | Ok(Self { 73 | io, 74 | buffers: [ 75 | Buffer::new_filled(size()?, ' '), 76 | Buffer::new_filled(size()?, ' '), 77 | ], 78 | active_buffer: 0, 79 | events: vec![], 80 | last_cursor: (false, vec2(0, 0), SetCursorStyle::SteadyBlock), 81 | cursor_visible: false, 82 | cursor_style: SetCursorStyle::SteadyBlock, 83 | cursor: vec2(0, 0), 84 | cursor_dirty: false, 85 | mouse_pos: vec2(0, 0), 86 | inline: None, 87 | just_resized: false, 88 | }) 89 | } 90 | 91 | /// Creates a new window built for inline using the given Stdout and height. 92 | pub fn new_inline(io: io::Stdout, height: u16) -> io::Result { 93 | let size = vec2(size()?.0, height); 94 | Ok(Self { 95 | io, 96 | buffers: [Buffer::new_filled(size, ' '), Buffer::new_filled(size, ' ')], 97 | active_buffer: 0, 98 | events: vec![], 99 | last_cursor: (false, vec2(0, 0), SetCursorStyle::SteadyBlock), 100 | cursor_visible: false, 101 | cursor_style: SetCursorStyle::SteadyBlock, 102 | cursor: vec2(0, 0), 103 | cursor_dirty: false, 104 | mouse_pos: vec2(0, 0), 105 | inline: Some(Inline::default()), 106 | just_resized: false, 107 | }) 108 | } 109 | 110 | /// Initializes a window that is prepared for inline rendering. 111 | /// Height is the number of columns that your terminal will need. 112 | pub fn init_inline(height: u16) -> io::Result { 113 | let stdout = io::stdout(); 114 | assert!(stdout.is_tty()); 115 | Window::new_inline(stdout, height) 116 | } 117 | 118 | /// Initializes the window, and returns a new Window for your use. 119 | pub fn init() -> io::Result { 120 | enable_raw_mode()?; 121 | let mut stdout = io::stdout(); 122 | assert!(stdout.is_tty()); 123 | execute!( 124 | stdout, 125 | EnterAlternateScreen, 126 | EnableMouseCapture, 127 | EnableFocusChange, 128 | EnableBracketedPaste, 129 | Hide, 130 | DisableLineWrap, 131 | )?; 132 | Window::new(stdout) 133 | } 134 | 135 | /// Enables the kitty keyboard protocol 136 | pub fn keyboard(&mut self) -> io::Result<()> { 137 | if let Ok(t) = terminal::supports_keyboard_enhancement() { 138 | if !t { 139 | return Err(io::Error::new( 140 | io::ErrorKind::Unsupported, 141 | "Terminal doesn't support the kitty keyboard protocol", 142 | )); 143 | } 144 | if let Some(inline) = &mut self.inline { 145 | inline.kitty = true; 146 | } else { 147 | execute!( 148 | self.io(), 149 | PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::all()) 150 | )?; 151 | } 152 | Ok(()) 153 | } else { 154 | Err(io::Error::new( 155 | io::ErrorKind::Unsupported, 156 | "Terminal doesn't support the kitty keyboard protocol", 157 | )) 158 | } 159 | } 160 | 161 | /// Returns the active Buffer, as a reference. 162 | pub fn buffer(&self) -> &Buffer { 163 | &self.buffers[self.active_buffer] 164 | } 165 | 166 | /// Returns the active Buffer, as a mutable reference. 167 | pub fn buffer_mut(&mut self) -> &mut Buffer { 168 | &mut self.buffers[self.active_buffer] 169 | } 170 | 171 | /// Swaps the buffers, clearing the old buffer. Used automatically by the window's update method. 172 | pub fn swap_buffers(&mut self) { 173 | self.active_buffer = 1 - self.active_buffer; 174 | self.buffers[self.active_buffer].fill(' '); 175 | } 176 | 177 | /// Returns the current known size of the buffer's window. 178 | pub fn size(&self) -> Vec2 { 179 | self.buffer().size() 180 | } 181 | 182 | /// Restores the window to it's previous state from before the window's init method. 183 | /// If the window is inline, restore the inline render 184 | pub fn restore(&mut self) -> io::Result<()> { 185 | if terminal::supports_keyboard_enhancement().is_ok() { 186 | queue!(self.io, PopKeyboardEnhancementFlags)?; 187 | } 188 | if let Some(inline) = &self.inline { 189 | execute!( 190 | self.io, 191 | DisableMouseCapture, 192 | DisableFocusChange, 193 | DisableBracketedPaste, 194 | PopKeyboardEnhancementFlags, 195 | Show, 196 | )?; 197 | if terminal::size()?.1 != inline.start + 1 { 198 | print!( 199 | "{}", 200 | "\n".repeat(self.buffers[self.active_buffer].size().y as usize) 201 | ); 202 | } 203 | disable_raw_mode()?; 204 | Ok(()) 205 | } else { 206 | execute!( 207 | self.io, 208 | PopKeyboardEnhancementFlags, 209 | LeaveAlternateScreen, 210 | DisableMouseCapture, 211 | DisableFocusChange, 212 | DisableBracketedPaste, 213 | Show, 214 | EnableLineWrap, 215 | )?; 216 | disable_raw_mode() 217 | } 218 | } 219 | 220 | /// Renders the window to the screen. should really only be used by the update method, but if you need a custom system, you can use this. 221 | pub fn render(&mut self) -> io::Result<()> { 222 | if self.inline.is_some() { 223 | if !self.inline.as_ref().expect("Inline should be some").active { 224 | // Make room for the inline render 225 | print!("{}", "\n".repeat(self.buffer().size().y as usize)); 226 | 227 | enable_raw_mode()?; 228 | 229 | execute!( 230 | self.io, 231 | EnableMouseCapture, 232 | EnableFocusChange, 233 | EnableBracketedPaste, 234 | DisableLineWrap, 235 | Hide, 236 | )?; 237 | if self.inline.as_ref().expect("Inline should be some").kitty { 238 | execute!( 239 | self.io, 240 | PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::all()) 241 | )?; 242 | } 243 | let inline = self.inline.as_mut().expect("Inline should be some"); 244 | inline.active = true; 245 | inline.start = cursor::position()?.1; 246 | } 247 | 248 | for (loc, cell) in 249 | self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer]) 250 | { 251 | queue!( 252 | self.io, 253 | cursor::MoveTo( 254 | loc.x, 255 | self.inline.as_ref().expect("Inline should be some").start 256 | - self.buffers[self.active_buffer].size().y 257 | + loc.y 258 | ), 259 | Print(cell), 260 | )?; 261 | 262 | self.cursor_dirty = true; 263 | } 264 | 265 | queue!( 266 | self.io, 267 | cursor::MoveTo( 268 | 0, 269 | self.inline.as_ref().expect("Inline should be some").start 270 | - self.buffers[self.active_buffer].size().y 271 | ) 272 | )?; 273 | } else { 274 | if self.just_resized { 275 | self.just_resized = false; 276 | let cell = self.buffers[self.active_buffer].size(); 277 | for x in 0..cell.x { 278 | for y in 0..cell.y { 279 | let cell = self.buffers[self.active_buffer] 280 | .get((x, y)) 281 | .expect("Cell should be in bounds"); 282 | queue!(self.io, cursor::MoveTo(x, y), Print(cell))?; 283 | 284 | self.cursor_dirty = true; 285 | } 286 | } 287 | } 288 | 289 | for (loc, cell) in 290 | self.buffers[1 - self.active_buffer].diff(&self.buffers[self.active_buffer]) 291 | { 292 | queue!(self.io, cursor::MoveTo(loc.x, loc.y), Print(cell))?; 293 | 294 | self.cursor_dirty = true; 295 | } 296 | } 297 | Ok(()) 298 | } 299 | 300 | /// Handles events, and renders the screen. 301 | pub fn update(&mut self, poll: Duration) -> io::Result<()> { 302 | // Render Window 303 | self.render()?; 304 | self.swap_buffers(); 305 | self.render_cursor()?; 306 | // Flush Render To Stdout 307 | self.io.flush()?; 308 | // Poll For Events 309 | self.handle_event(poll)?; 310 | Ok(()) 311 | } 312 | 313 | pub fn render_cursor(&mut self) -> io::Result<()> { 314 | // Get the current cursor position 315 | if self.cursor_style != self.last_cursor.2 316 | || self.cursor != self.last_cursor.1 317 | || self.cursor_visible != self.last_cursor.0 318 | || self.cursor_dirty 319 | { 320 | self.cursor_dirty = false; 321 | if self.cursor_visible { 322 | let cursor = self.cursor; 323 | let style = self.cursor_style; 324 | 325 | // Calculate the actual position based on inline rendering 326 | let actual_pos = if let Some(inline) = &self.inline { 327 | vec2( 328 | cursor.x, 329 | inline.start - self.buffers[self.active_buffer].size().y + cursor.y, 330 | ) 331 | } else { 332 | cursor 333 | }; 334 | 335 | queue!(self.io(), MoveTo(actual_pos.x, actual_pos.y), style, Show)?; 336 | } else { 337 | queue!(self.io(), Hide)?; 338 | } 339 | } 340 | self.last_cursor = (self.cursor_visible, self.cursor, self.cursor_style); 341 | Ok(()) 342 | } 343 | 344 | /// Handles events. Used automatically by the update method, so no need to use it unless update is being used. 345 | pub fn handle_event(&mut self, poll: Duration) -> io::Result<()> { 346 | self.events = vec![]; 347 | if event::poll(poll)? { 348 | // Get all queued events 349 | while event::poll(Duration::ZERO)? { 350 | let event = event::read()?; 351 | match event { 352 | Event::Resize(width, height) => { 353 | if self.inline.is_none() { 354 | self.buffers = [ 355 | Buffer::new_filled((width, height), ' '), 356 | Buffer::new_filled((width, height), ' '), 357 | ]; 358 | self.just_resized = true; 359 | } 360 | } 361 | Event::Mouse(MouseEvent { column, row, .. }) => { 362 | self.mouse_pos = vec2(column, row) 363 | } 364 | _ => {} 365 | } 366 | self.events.push(event); 367 | } 368 | } 369 | Ok(()) 370 | } 371 | 372 | /// Returns whether the cursor is visible 373 | pub fn cursor_visible(&self) -> bool { 374 | self.cursor_visible 375 | } 376 | 377 | /// Returns the current cursor position 378 | pub fn cursor(&self) -> Vec2 { 379 | self.cursor 380 | } 381 | 382 | /// Returns the current cursor style 383 | pub fn cursor_style(&self) -> SetCursorStyle { 384 | self.cursor_style 385 | } 386 | 387 | /// Sets the cursor visibility 388 | pub fn set_cursor_visible(&mut self, visible: bool) { 389 | self.cursor_visible = visible; 390 | } 391 | 392 | /// Sets the cursor position, clamping to window bounds 393 | pub fn set_cursor(&mut self, pos: Vec2) { 394 | let size = self.size(); 395 | self.cursor.x = pos.x.min(size.x.saturating_sub(1)); 396 | self.cursor.y = pos.y.min(size.y.saturating_sub(1)); 397 | } 398 | 399 | /// Sets the cursor style 400 | pub fn set_cursor_style(&mut self, style: SetCursorStyle) { 401 | self.cursor_style = style; 402 | } 403 | 404 | /// Move the cursor by a given distance 405 | pub fn move_cursor(&mut self, x: i16, y: i16) { 406 | let size = self.size(); 407 | self.cursor.x = self 408 | .cursor 409 | .x 410 | .saturating_add_signed(x) 411 | .min(size.x.saturating_sub(1)); 412 | self.cursor.y = self 413 | .cursor 414 | .y 415 | .saturating_add_signed(y) 416 | .min(size.y.saturating_sub(1)); 417 | } 418 | 419 | pub fn mouse_pos(&self) -> Vec2 { 420 | self.mouse_pos 421 | } 422 | 423 | /// Pushes an event into the state 424 | /// Could be usefull with a custom event loop 425 | /// or for keyboard control from elsewhere 426 | pub fn insert_event(&mut self, event: Event) { 427 | match event { 428 | Event::Resize(width, height) => { 429 | if self.inline.is_none() { 430 | self.buffers = [ 431 | Buffer::new_filled((width, height), ' '), 432 | Buffer::new_filled((width, height), ' '), 433 | ]; 434 | self.just_resized = true; 435 | } 436 | } 437 | Event::Mouse(MouseEvent { column, row, .. }) => self.mouse_pos = vec2(column, row), 438 | _ => {} 439 | } 440 | 441 | self.events.push(event); 442 | } 443 | 444 | /// Clears events, usefull for handling issues with 445 | /// custom event insertions or handlers 446 | pub fn clear_events(&mut self) { 447 | self.events.clear(); 448 | } 449 | 450 | /// Returns the current event for the frame, as a reference. 451 | pub fn events(&self) -> &Vec { 452 | &self.events 453 | } 454 | 455 | /// Returns true if the mouse cursor is hovering the given rect. 456 | pub fn hover>(&self, loc: V, size: V) -> io::Result { 457 | let loc = loc.into(); 458 | let size = size.into(); 459 | let pos: Vec2 = self.mouse_pos(); 460 | Ok(pos.x <= loc.x + size.x && pos.x >= loc.x && pos.y <= loc.y + size.y && pos.y >= loc.y) 461 | } 462 | 463 | pub fn io(&mut self) -> &mut Stdout { 464 | &mut self.io 465 | } 466 | } 467 | 468 | /// A macro that allows you to quickly check an event based off of a pattern 469 | /// Takes in the window, a pattern for the if let statement, and finally a closure. 470 | /// This closure could be anything that returns a bool. 471 | /// 472 | /// Underneath, the event! macro runs an if let on your pattern checking for any of the 473 | /// Events to be true from your given closure. 474 | /** 475 | Example 476 | ```rust, no_run 477 | # use ascii_forge::prelude::*; 478 | # fn main() -> std::io::Result<()> { 479 | # let mut window = Window::init()?; 480 | event!(window, Event::Key(e) => e.code == KeyCode::Char('q')); 481 | # Ok(()) 482 | # } 483 | ``` 484 | */ 485 | #[macro_export] 486 | macro_rules! event { 487 | ($window:expr, $event_type:pat => $($closure:tt)*) => { 488 | $window.events().iter().any(|e| { 489 | if let $event_type = e { 490 | $($closure)* 491 | } else { 492 | false 493 | } 494 | }) 495 | }; 496 | } 497 | 498 | /// Enables a panic hook to help you terminal still look pretty. 499 | pub fn handle_panics() { 500 | let original_hook = take_hook(); 501 | set_hook(Box::new(move |e| { 502 | Window::new(io::stdout()) 503 | .expect("Window should have created for panic") 504 | .restore() 505 | .expect("Window should have exited for panic"); 506 | original_hook(e); 507 | })) 508 | } 509 | 510 | impl Drop for Window { 511 | fn drop(&mut self) { 512 | self.restore().expect("Restoration should have succeded"); 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /src/layout.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | /// Defines a constraint for sizing elements within a layout. 4 | /// 5 | /// Constraints determine how much space an element should occupy relative 6 | /// to the available space or other elements. 7 | #[derive(Debug, Clone)] 8 | pub enum Constraint { 9 | /// Takes up a specified percentage of the total available space (0.0 to 100.0). 10 | /// It will shrink if necessary to fit within the available space. 11 | Percentage(f32), 12 | /// Takes up a fixed amount of space in units (e.g., characters or rows). 13 | /// If the available space is less than the fixed size, an error may occur. 14 | Fixed(u16), 15 | /// Takes up space within a specified minimum and maximum range. 16 | /// It will try to fit its content but won't go below `min` or above `max`. 17 | Range { min: u16, max: u16 }, 18 | /// Takes up at least the specified minimum space, but can grow beyond it. 19 | Min(u16), 20 | /// Takes up at most the specified maximum space, but can shrink below it. 21 | Max(u16), 22 | /// Takes up all the remaining available space after other constraints have been resolved. 23 | /// Multiple flexible constraints will share the remaining space evenly. 24 | Flexible, 25 | } 26 | 27 | /// The possible error results that can occur during layout calculation. 28 | #[derive(Debug, PartialEq, Eq)] 29 | pub enum LayoutError { 30 | /// Indicates that at least one constraint (e.g., a `Fixed` or `Range` with too high `min`) 31 | /// could not fit within the allocated space. 32 | InsufficientSpace, 33 | 34 | /// Occurs when `Percentage` constraints sum up to more than 100%, or a percentage 35 | /// value is outside the 0.0-100.0 range. 36 | InvalidPercentages, 37 | 38 | /// Reserved for potential future conflicts where constraints are logically impossible 39 | /// to satisfy simultaneously (currently not explicitly triggered by `resolve_constraints`). 40 | ConstraintConflict, 41 | } 42 | 43 | /// An area that a layout element takes up. 44 | /// 45 | /// Represents a rectangular region on the screen, defined by its top-left 46 | /// corner (x, y) and its dimensions (width, height). 47 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 48 | pub struct Rect { 49 | /// The X-coordinate of the top-left corner. 50 | pub x: u16, 51 | /// The Y-coordinate of the top-left corner. 52 | pub y: u16, 53 | /// The width of the rectangle. 54 | pub width: u16, 55 | /// The height of the rectangle. 56 | pub height: u16, 57 | } 58 | 59 | impl Rect { 60 | /// Creates a new `Rect` with the specified position and dimensions. 61 | pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self { 62 | Self { 63 | x, 64 | y, 65 | width, 66 | height, 67 | } 68 | } 69 | 70 | /// Returns the top-left position as a Vec2. 71 | pub fn position(&self) -> Vec2 { 72 | vec2(self.x, self.y) 73 | } 74 | 75 | /// Returns the size as a Vec2. 76 | pub fn size(&self) -> Vec2 { 77 | vec2(self.width, self.height) 78 | } 79 | 80 | /// Returns the bottom-right corner as a Vec2. 81 | pub fn bottom_right(&self) -> Vec2 { 82 | vec2(self.x + self.width, self.y + self.height) 83 | } 84 | 85 | /// Returns the center point as a Vec2. 86 | pub fn center(&self) -> Vec2 { 87 | vec2(self.x + self.width / 2, self.y + self.height / 2) 88 | } 89 | 90 | /// Creates a Rect from two Vec2 points. 91 | pub fn from_corners(top_left: Vec2, bottom_right: Vec2) -> Self { 92 | Self { 93 | x: top_left.x, 94 | y: top_left.y, 95 | width: bottom_right.x.saturating_sub(top_left.x), 96 | height: bottom_right.y.saturating_sub(top_left.y), 97 | } 98 | } 99 | 100 | /// Creates a Rect from a position and size. 101 | pub fn from_pos_size(pos: Vec2, size: Vec2) -> Self { 102 | Self { 103 | x: pos.x, 104 | y: pos.y, 105 | width: size.x, 106 | height: size.y, 107 | } 108 | } 109 | 110 | /// Creates a new Rect with padding applied inward. 111 | pub fn with_padding(&self, padding: u16) -> Self { 112 | Self { 113 | x: self.x + padding, 114 | y: self.y + padding, 115 | width: self.width.saturating_sub(padding * 2), 116 | height: self.height.saturating_sub(padding * 2), 117 | } 118 | } 119 | 120 | /// Creates a new Rect with specific padding on each side. 121 | pub fn with_padding_sides(&self, top: u16, right: u16, bottom: u16, left: u16) -> Self { 122 | Self { 123 | x: self.x + left, 124 | y: self.y + top, 125 | width: self.width.saturating_sub(left + right), 126 | height: self.height.saturating_sub(top + bottom), 127 | } 128 | } 129 | } 130 | 131 | impl From for Vec2 { 132 | fn from(rect: Rect) -> Self { 133 | vec2(rect.x, rect.y) 134 | } 135 | } 136 | 137 | /// Creates a `Constraint::Percentage` variant. 138 | pub fn percent(value: f32) -> Constraint { 139 | Constraint::Percentage(value) 140 | } 141 | 142 | /// Creates a `Constraint::Fixed` variant. 143 | pub fn fixed(value: u16) -> Constraint { 144 | Constraint::Fixed(value) 145 | } 146 | 147 | /// Creates a `Constraint::Range` variant. 148 | pub fn range(min_val: u16, max_val: u16) -> Constraint { 149 | Constraint::Range { 150 | min: min_val, 151 | max: max_val, 152 | } 153 | } 154 | 155 | /// Creates a `Constraint::Min` variant. 156 | pub fn min(value: u16) -> Constraint { 157 | Constraint::Min(value) 158 | } 159 | 160 | /// Creates a `Constraint::Max` variant. 161 | pub fn max(value: u16) -> Constraint { 162 | Constraint::Max(value) 163 | } 164 | 165 | /// Creates a `Constraint::Flexible` variant. 166 | pub fn flexible() -> Constraint { 167 | Constraint::Flexible 168 | } 169 | 170 | /// Defines a horizontal and vertical grid layout setup. 171 | /// 172 | /// `Layout` is used for separating a given total space (e.g., the window size) 173 | /// into easy-to-manage rectangular chunks for rendering UI elements. 174 | #[derive(Default, Debug, Clone)] 175 | pub struct Layout { 176 | /// A vector where each tuple represents a row: `(height_constraint, width_constraints_for_columns)`. 177 | rows: Vec<(Constraint, Vec)>, 178 | } 179 | 180 | impl Layout { 181 | /// Starts a new `Layout` definition. 182 | pub fn new() -> Self { 183 | Self::default() 184 | } 185 | 186 | /// Adds a new row to the layout with specified height and column width constraints. 187 | pub fn row( 188 | mut self, 189 | height_constraint: Constraint, 190 | width_constraints: Vec, 191 | ) -> Self { 192 | self.rows.push((height_constraint, width_constraints)); 193 | self 194 | } 195 | 196 | /// Creates a row that takes up the full width of the available space with a single height constraint. 197 | pub fn empty_row(self, constraint: Constraint) -> Self { 198 | self.row(constraint, vec![flexible()]) 199 | } 200 | 201 | /// Calculates the `Rect`s for all elements in the layout based on the total available space. 202 | pub fn calculate(self, space: impl Into) -> Result>, LayoutError> { 203 | calculate_layout(space, self.rows) 204 | } 205 | 206 | /// Calculates the layout and renders elements to each rect area. 207 | pub fn render( 208 | self, 209 | space: impl Into, 210 | buffer: &mut Buffer, 211 | elements: Vec>, 212 | ) -> Result>, LayoutError> { 213 | let rects = self.calculate(space)?; 214 | 215 | for (row_idx, row_rects) in rects.iter().enumerate() { 216 | if let Some(row_elements) = elements.get(row_idx) { 217 | for (col_idx, rect) in row_rects.iter().enumerate() { 218 | if let Some(element) = row_elements.get(col_idx) { 219 | element.render(rect.position(), buffer); 220 | } 221 | } 222 | } 223 | } 224 | 225 | Ok(rects) 226 | } 227 | 228 | /// Calculates the layout and renders elements with clipping to fit within each rect. 229 | pub fn render_clipped( 230 | self, 231 | space: impl Into, 232 | buffer: &mut Buffer, 233 | elements: Vec>, 234 | ) -> Result>, LayoutError> { 235 | let rects = self.calculate(space)?; 236 | 237 | for (row_idx, row_rects) in rects.iter().enumerate() { 238 | if let Some(row_elements) = elements.get(row_idx) { 239 | for (col_idx, rect) in row_rects.iter().enumerate() { 240 | if let Some(element) = row_elements.get(col_idx) { 241 | element.render_clipped(rect.position(), rect.size(), buffer); 242 | } 243 | } 244 | } 245 | } 246 | 247 | Ok(rects) 248 | } 249 | } 250 | 251 | /// A helper for working with a single calculated layout. 252 | /// 253 | /// This provides convenient methods for accessing and working with layout rects. 254 | pub struct CalculatedLayout { 255 | rects: Vec>, 256 | } 257 | 258 | impl CalculatedLayout { 259 | /// Creates a new CalculatedLayout from calculated rects. 260 | pub fn new(rects: Vec>) -> Self { 261 | Self { rects } 262 | } 263 | 264 | /// Gets a rect at the specified row and column. 265 | pub fn get(&self, row: usize, col: usize) -> Option<&Rect> { 266 | self.rects.get(row)?.get(col) 267 | } 268 | 269 | /// Gets all rects in a row. 270 | pub fn row(&self, row: usize) -> Option<&[Rect]> { 271 | self.rects.get(row).map(|r| r.as_slice()) 272 | } 273 | 274 | /// Returns the total number of rows. 275 | pub fn row_count(&self) -> usize { 276 | self.rects.len() 277 | } 278 | 279 | /// Returns the number of columns in a specific row. 280 | pub fn col_count(&self, row: usize) -> usize { 281 | self.rects.get(row).map(|r| r.len()).unwrap_or(0) 282 | } 283 | 284 | /// Iterates over all rects with their row and column indices. 285 | pub fn iter(&self) -> impl Iterator { 286 | self.rects.iter().enumerate().flat_map(|(row_idx, row)| { 287 | row.iter() 288 | .enumerate() 289 | .map(move |(col_idx, rect)| (row_idx, col_idx, rect)) 290 | }) 291 | } 292 | 293 | /// Renders an element at a specific layout position. 294 | pub fn render_at( 295 | &self, 296 | row: usize, 297 | col: usize, 298 | element: R, 299 | buffer: &mut Buffer, 300 | ) -> Option { 301 | let rect = self.get(row, col)?; 302 | Some(element.render(rect.position(), buffer)) 303 | } 304 | 305 | /// Renders an element clipped to a specific layout position. 306 | pub fn render_clipped_at( 307 | &self, 308 | row: usize, 309 | col: usize, 310 | element: R, 311 | buffer: &mut Buffer, 312 | ) -> Option { 313 | let rect = self.get(row, col)?; 314 | Some(element.render_clipped(rect.position(), rect.size(), buffer)) 315 | } 316 | } 317 | 318 | /// Calculates the layout of a grid, resolving constraints for rows and columns. 319 | pub fn calculate_layout( 320 | total_space: impl Into, 321 | rows: Vec<(Constraint, Vec)>, 322 | ) -> Result>, LayoutError> { 323 | let total_space = total_space.into(); 324 | let height_constraints: Vec = rows.iter().map(|(h, _)| h.clone()).collect(); 325 | 326 | // Resolve heights for all rows 327 | let row_heights = resolve_constraints(&height_constraints, total_space.y)?; 328 | let mut result = Vec::new(); 329 | let mut current_y = 0u16; 330 | 331 | // Iterate through rows to resolve column widths and create Rects 332 | for (row_idx, (_, width_constraints)) in rows.iter().enumerate() { 333 | let row_height = row_heights[row_idx]; 334 | let widths = resolve_constraints(width_constraints, total_space.x)?; 335 | 336 | let mut row_elements = Vec::new(); 337 | let mut current_x = 0u16; 338 | 339 | for width in widths { 340 | row_elements.push(Rect::new(current_x, current_y, width, row_height)); 341 | current_x += width; 342 | } 343 | 344 | result.push(row_elements); 345 | current_y += row_height; 346 | } 347 | 348 | Ok(result) 349 | } 350 | 351 | /// Resolves a list of `Constraint`s for a single dimension (either width or height). 352 | pub fn resolve_constraints( 353 | constraints: &[Constraint], 354 | available: u16, 355 | ) -> Result, LayoutError> { 356 | if constraints.is_empty() { 357 | return Ok(vec![]); 358 | } 359 | 360 | let mut total_percentage = 0.0f32; 361 | for constraint in constraints { 362 | if let Constraint::Percentage(pct) = constraint { 363 | if *pct < 0.0 || *pct > 100.0 { 364 | return Err(LayoutError::InvalidPercentages); 365 | } 366 | total_percentage += pct; 367 | } 368 | } 369 | 370 | if total_percentage > 100.0 { 371 | return Err(LayoutError::InvalidPercentages); 372 | } 373 | 374 | let mut allocated_sizes = vec![0u16; constraints.len()]; 375 | 376 | // Allocate fixed sizes first 377 | let mut fixed_total = 0u32; 378 | for (i, constraint) in constraints.iter().enumerate() { 379 | if let Constraint::Fixed(size) = constraint { 380 | allocated_sizes[i] = *size; 381 | fixed_total += *size as u32; 382 | } 383 | } 384 | 385 | if fixed_total > available as u32 { 386 | return Err(LayoutError::InsufficientSpace); 387 | } 388 | 389 | // Allocate percentage sizes 390 | let mut percentage_total = 0u32; 391 | for (i, constraint) in constraints.iter().enumerate() { 392 | if let Constraint::Percentage(pct) = constraint { 393 | let ideal_size = ((available as f32 * pct) / 100.0).round() as u32; 394 | allocated_sizes[i] = ideal_size as u16; 395 | percentage_total += ideal_size; 396 | } 397 | } 398 | 399 | // If combined fixed and percentage exceeds available, shrink percentages proportionally 400 | if fixed_total + percentage_total > available as u32 { 401 | let shrink_factor = (available as u32 - fixed_total) as f32 / percentage_total as f32; 402 | for (i, constraint) in constraints.iter().enumerate() { 403 | if let Constraint::Percentage(_) = constraint { 404 | allocated_sizes[i] = (allocated_sizes[i] as f32 * shrink_factor).round() as u16; 405 | } 406 | } 407 | } 408 | 409 | // Ensure minimums are met for Range and Min constraints 410 | for (i, constraint) in constraints.iter().enumerate() { 411 | match constraint { 412 | Constraint::Range { min: min_val, .. } | Constraint::Min(min_val) => { 413 | allocated_sizes[i] = allocated_sizes[i].max(*min_val); 414 | } 415 | _ => {} 416 | } 417 | } 418 | 419 | let used_space: u32 = allocated_sizes.iter().map(|&x| x as u32).sum(); 420 | 421 | if used_space > available as u32 { 422 | return Err(LayoutError::InsufficientSpace); 423 | } 424 | 425 | let mut remaining_space = (available as u32) - used_space; 426 | 427 | // Identify indices of flexible, min, max, and range constraints for expansion 428 | let mut expandable_indices: Vec<(usize, u16)> = Vec::new(); 429 | 430 | for (i, constraint) in constraints.iter().enumerate() { 431 | let max_val = match constraint { 432 | Constraint::Range { max: m, .. } => Some(*m), 433 | Constraint::Max(m) => Some(*m), 434 | Constraint::Min(_) => Some(u16::MAX), 435 | Constraint::Flexible => Some(u16::MAX), 436 | _ => None, 437 | }; 438 | 439 | if let Some(max) = max_val { 440 | expandable_indices.push((i, max)); 441 | } 442 | } 443 | 444 | // Distribute remaining space to expandable constraints 445 | if !expandable_indices.is_empty() && remaining_space > 0 { 446 | while remaining_space > 0 { 447 | let mut distributed = 0u32; 448 | let eligible: Vec<_> = expandable_indices 449 | .iter() 450 | .filter(|(idx, max_val)| allocated_sizes[*idx] < *max_val) 451 | .collect(); 452 | 453 | if eligible.is_empty() { 454 | break; 455 | } 456 | 457 | let space_per_item = std::cmp::max(1, remaining_space / eligible.len() as u32); 458 | 459 | for &&(idx, max_val) in &eligible { 460 | if remaining_space == 0 { 461 | break; 462 | } 463 | 464 | let can_add = std::cmp::min( 465 | max_val.saturating_sub(allocated_sizes[idx]) as u32, 466 | std::cmp::min(space_per_item, remaining_space), 467 | ); 468 | 469 | allocated_sizes[idx] += can_add as u16; 470 | distributed += can_add; 471 | remaining_space -= can_add; 472 | } 473 | 474 | if distributed == 0 { 475 | break; 476 | } 477 | } 478 | } 479 | 480 | Ok(allocated_sizes) 481 | } 482 | 483 | #[cfg(test)] 484 | mod tests { 485 | use super::*; 486 | 487 | #[test] 488 | fn test_percent_plus_fixed_heights() { 489 | let layout_result = Layout::new() 490 | .row(percent(100.0), vec![percent(100.0)]) 491 | .row(fixed(5), vec![percent(100.0)]) 492 | .calculate((100, 100)) 493 | .unwrap(); 494 | assert_eq!( 495 | layout_result, 496 | vec![ 497 | vec![Rect::new(0, 0, 100, 95)], 498 | vec![Rect::new(0, 95, 100, 5)] 499 | ] 500 | ); 501 | } 502 | 503 | #[test] 504 | fn test_even_flexible_split() { 505 | let layout_result = Layout::new() 506 | .row(flexible(), vec![flexible(), flexible()]) 507 | .row(flexible(), vec![flexible(), flexible()]) 508 | .calculate((100, 100)) 509 | .unwrap(); 510 | assert_eq!( 511 | layout_result, 512 | vec![ 513 | vec![Rect::new(0, 0, 50, 50), Rect::new(50, 0, 50, 50)], 514 | vec![Rect::new(0, 50, 50, 50), Rect::new(50, 50, 50, 50)] 515 | ] 516 | ); 517 | } 518 | 519 | #[test] 520 | fn test_rect_helpers() { 521 | let rect = Rect::new(10, 20, 30, 40); 522 | assert_eq!(rect.position(), vec2(10, 20)); 523 | assert_eq!(rect.size(), vec2(30, 40)); 524 | assert_eq!(rect.bottom_right(), vec2(40, 60)); 525 | assert_eq!(rect.center(), vec2(25, 40)); 526 | } 527 | 528 | #[test] 529 | fn test_rect_padding() { 530 | let rect = Rect::new(10, 10, 30, 30); 531 | let padded = rect.with_padding(5); 532 | assert_eq!(padded, Rect::new(15, 15, 20, 20)); 533 | } 534 | 535 | #[test] 536 | fn test_rect_from_corners() { 537 | let rect = Rect::from_corners(vec2(10, 20), vec2(40, 60)); 538 | assert_eq!(rect, Rect::new(10, 20, 30, 40)); 539 | } 540 | 541 | #[test] 542 | fn test_min_constraint() { 543 | let sizes = resolve_constraints(&[min(30), min(20)], 100).unwrap(); 544 | assert_eq!(sizes, vec![55, 45]); // Remaining space distributed 545 | } 546 | 547 | #[test] 548 | fn test_max_constraint() { 549 | let sizes = resolve_constraints(&[max(30), flexible()], 100).unwrap(); 550 | assert_eq!(sizes, vec![30, 70]); 551 | } 552 | 553 | #[test] 554 | fn test_min_insufficient() { 555 | let result = resolve_constraints(&[min(60), min(60)], 100); 556 | assert_eq!(result, Err(LayoutError::InsufficientSpace)); 557 | } 558 | } 559 | --------------------------------------------------------------------------------