From 6b1098c86330b3d5b9cd8b4a9d6bd51f1a872cb1 Mon Sep 17 00:00:00 2001 From: Joe Bellus Date: Thu, 6 Jan 2022 00:46:27 -0500 Subject: [PATCH] initial UI framework --- Cargo.lock | 186 +++++++++++++++++++++++++++------ Cargo.toml | 8 +- benches/space_fill.rs | 54 ++++++++++ examples/component.rs | 29 +++++ examples/tasks.rs | 8 -- src/{ => app}/command.rs | 25 ++--- src/{ => app}/context.rs | 4 +- src/{vox.rs => app/helpers.rs} | 29 ++--- src/{app.rs => app/mod.rs} | 20 ++-- src/{ => app}/opt.rs | 2 +- src/lib.rs | 40 +++++-- src/tasks.rs | 64 ------------ src/ui/cell.rs | 93 +++++++++++++++++ src/ui/component.rs | 7 ++ src/ui/context.rs | 34 ++++++ src/ui/geometry.rs | 51 +++++++++ src/ui/mod.rs | 44 ++++++++ src/ui/view.rs | 77 ++++++++++++++ 18 files changed, 616 insertions(+), 159 deletions(-) create mode 100644 benches/space_fill.rs create mode 100644 examples/component.rs delete mode 100644 examples/tasks.rs rename src/{ => app}/command.rs (85%) rename src/{ => app}/context.rs (99%) rename src/{vox.rs => app/helpers.rs} (77%) rename src/{app.rs => app/mod.rs} (97%) rename src/{ => app}/opt.rs (99%) delete mode 100644 src/tasks.rs create mode 100644 src/ui/cell.rs create mode 100644 src/ui/component.rs create mode 100644 src/ui/context.rs create mode 100644 src/ui/geometry.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/view.rs diff --git a/Cargo.lock b/Cargo.lock index 890f8a1..add2df0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,8 +15,8 @@ dependencies = [ name = "arkham" version = "0.1.1" dependencies = [ - "console", "criterion", + "crossterm", "serde", "serde_json", "textwrap 0.14.2", @@ -42,9 +42,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" @@ -90,21 +90,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "console" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc80946b3480f421c2f17ed1cb841753a371c7c5104f51d507e13f532c856aa" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "regex", - "terminal_size", - "unicode-width", - "winapi", -] - [[package]] name = "criterion" version = "0.3.4" @@ -185,6 +170,32 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" +dependencies = [ + "bitflags", + "crossterm_winapi", + "futures-core", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "csv" version = "1.1.6" @@ -214,10 +225,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] -name = "encode_unicode" -version = "0.3.6" +name = "futures-core" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" [[package]] name = "half" @@ -234,6 +245,15 @@ dependencies = [ "libc", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itertools" version = "0.9.0" @@ -275,9 +295,18 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.88" +version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] [[package]] name = "log" @@ -303,6 +332,37 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mio" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -334,6 +394,31 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + [[package]] name = "pest" version = "2.1.3" @@ -414,6 +499,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.4.3" @@ -527,6 +621,42 @@ dependencies = [ "serde", ] +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + [[package]] name = "smawk" version = "0.3.1" @@ -544,16 +674,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "terminal_size" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index e3bb079..68c74e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,12 @@ categories = ["command-line-interface", "config"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -# indicatif = "0.15.0" -console = "0.14.0" serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true} toml = { version="0.5.8", optional = true} textwrap = "0.14.2" +crossterm = { version = "0.22.1", features=["event-stream"]} + [features] @@ -35,3 +35,7 @@ criterion = "0.3" name = "arg_parsing" harness = false +[[bench]] +name = "space_fill" +harness = false + diff --git a/benches/space_fill.rs b/benches/space_fill.rs new file mode 100644 index 0000000..73efa10 --- /dev/null +++ b/benches/space_fill.rs @@ -0,0 +1,54 @@ +use arkham::ui; +use criterion::{criterion_group, criterion_main, Criterion}; + +fn space_fill(c: &mut Criterion) { + c.bench_function("large_space_fill", |b| { + let mut space = ui::View::new(200, 200); + space.fill( + ui::Rect::new(50, 50, 100, 100), + *ui::Cell::default() + .fg(ui::Color::White) + .bg(ui::Color::Green), + ); + b.iter(|| { + let mut output: Vec = vec![]; + space + .render(ui::Pos::new(0, 0), &mut output) + .expect("couldnt render"); + }) + }); + + c.bench_function("small_space_fill", |b| { + let mut space = ui::View::new(20, 20); + space.fill( + ui::Rect::new(0, 0, 20, 20), + *ui::Cell::default() + .fg(ui::Color::White) + .bg(ui::Color::Green), + ); + b.iter(|| { + let mut output: Vec = vec![]; + space + .render(ui::Pos::new(0, 0), &mut output) + .expect("couldnt render"); + }) + }); + + c.bench_function("fill_all", |b| { + let mut space = ui::View::new(200, 200); + space.fill_all( + *ui::Cell::default() + .fg(ui::Color::White) + .bg(ui::Color::Green), + ); + b.iter(|| { + let mut output: Vec = vec![]; + space + .render(ui::Pos::new(0, 0), &mut output) + .expect("couldnt render"); + }) + }); +} + +criterion_group!(benches, space_fill); +criterion_main!(benches); diff --git a/examples/component.rs b/examples/component.rs new file mode 100644 index 0000000..33cd9b5 --- /dev/null +++ b/examples/component.rs @@ -0,0 +1,29 @@ +use arkham::{ + ui::{Cell, Color, Component, Rect, UI}, + Result, +}; + +pub struct OuterComponent; + +impl Component for OuterComponent { + fn view(&mut self, ctx: &mut arkham::ui::Context) -> Result<()> { + ctx.view + .fill_all(*Cell::default().content(' ').bg(Color::Red)); + ctx.add_component(Rect::new(20, 20, 40, 40), &mut InnerComponent)?; + Ok(()) + } +} + +pub struct InnerComponent; + +impl Component for InnerComponent { + fn view(&mut self, ctx: &mut arkham::ui::Context) -> Result<()> { + ctx.view + .fill_all(*Cell::default().content(' ').bg(Color::Green)); + Ok(()) + } +} + +fn main() { + UI::new(OuterComponent).run().expect("Couldnt run UI loop"); +} diff --git a/examples/tasks.rs b/examples/tasks.rs deleted file mode 100644 index 6f71351..0000000 --- a/examples/tasks.rs +++ /dev/null @@ -1,8 +0,0 @@ -// use arkham::TaskGroup; - -fn main() { - // let tasks = TaskGroup::new(); - // let task = tasks.start_task("task 1"); - // task.tick(); - // tasks.join(); -} diff --git a/src/command.rs b/src/app/command.rs similarity index 85% rename from src/command.rs rename to src/app/command.rs index 08b86ef..fb54553 100644 --- a/src/command.rs +++ b/src/app/command.rs @@ -1,11 +1,12 @@ use std::fmt::Debug; -use crate::{ - context::Context, +use super::{ opt::{self, Opt}, - vox, App, + App, Context, }; +use super::helpers; + pub type Handler = fn(&App, &Context, &[String]); #[derive(Clone)] @@ -58,7 +59,7 @@ impl Command { } pub(crate) fn help(app: &App, _ctx: &Context, args: &[String]) { - vox::print(app.application_header()); + helpers::print(app.application_header()); print_command_help(&app.root, args); } @@ -68,17 +69,17 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) { return print_command_help(cmd, &args[1..]); } } - vox::print(""); + helpers::print(""); if let Some(desc) = cmd.long_desc.as_ref().or_else(|| cmd.short_desc.as_ref()) { if cmd.name != "root" { - vox::header(&cmd.name.to_uppercase()); + helpers::header(&cmd.name.to_uppercase()); } - vox::print(desc); - vox::print(""); + helpers::print(desc); + helpers::print(""); } if !cmd.opts.is_empty() { - vox::header("OPTIONS"); - vox::description_list( + helpers::header("OPTIONS"); + helpers::description_list( cmd.opts .iter() .map(|o| (o.usage(), o.desc.clone().unwrap_or_default())) @@ -87,8 +88,8 @@ pub(crate) fn print_command_help(cmd: &Command, args: &[String]) { } if !cmd.commands.is_empty() { - vox::header("Commands"); - vox::description_list( + helpers::header("Commands"); + helpers::description_list( cmd.commands .iter() .map(|c| (c.name.clone(), c.short_desc.clone().unwrap_or_default())) diff --git a/src/context.rs b/src/app/context.rs similarity index 99% rename from src/context.rs rename to src/app/context.rs index 34a4aba..8f3595b 100644 --- a/src/context.rs +++ b/src/app/context.rs @@ -1,6 +1,8 @@ use crate::{Command, Opt}; use std::{collections::BTreeMap, env}; +use super::command::print_command_help; + #[derive(Clone, Debug, Default)] pub struct Map(BTreeMap); @@ -240,7 +242,7 @@ impl Context { /// Can be used to display the automatic help message for the current command. pub fn display_help(&self) { - crate::command::print_command_help(&self.cmd, &[]) + print_command_help(&self.cmd, &[]) } pub(crate) fn load_config_file(&mut self, filename: &str) { diff --git a/src/vox.rs b/src/app/helpers.rs similarity index 77% rename from src/vox.rs rename to src/app/helpers.rs index 0a09538..f3f8026 100644 --- a/src/vox.rs +++ b/src/app/helpers.rs @@ -1,4 +1,4 @@ -use console::style; +use crossterm::style::{self, style, Stylize}; use std::collections::HashMap; use std::fmt::Display; @@ -13,10 +13,9 @@ pub enum Color { Magenta, Cyan, White, - Color256(u8), } -impl From for console::Color { +impl From for style::Color { fn from(fr: Color) -> Self { match fr { Color::Black => Self::Black, @@ -27,33 +26,23 @@ impl From for console::Color { Color::Magenta => Self::Magenta, Color::Cyan => Self::Cyan, Color::White => Self::White, - Color::Color256(v) => Self::Color256(v), } } } -pub fn labeled(color: Color, label: &str, msg: &str) { - let label = format!("[{}]", label); - println!("{} {}", console::style(label).fg(color.into()).bold(), msg); -} - -pub fn message(str: T) { - println!("{}", style(str).white().bold()); +pub fn print(s: T) { + println!("{}", s); } pub fn header(str: T) { println!( "{} {} {}", - style("-=[").red().dim(), - style(str).white().bold(), - style("]=-").red().dim() + "-=[".red().dim(), + style::style(str).white().bold(), + "]=-".red().dim() ); } -pub fn note(str: T) { - println!("{}", style(str).white().dim()); -} - pub fn description_list(list: HashMap) { let mut lines = list.into_iter().collect::>(); let key_max_length = lines.iter().map(|(v, _)| v.len()).max().unwrap_or(10) + 3; @@ -80,10 +69,6 @@ pub fn description_list(list: HashMap) { println!("{}", output); } -pub fn print(s: T) { - println!("{}", s); -} - pub fn error(s: T) { println!("{}", style(s).red().bold()); } diff --git a/src/app.rs b/src/app/mod.rs similarity index 97% rename from src/app.rs rename to src/app/mod.rs index 5b58956..c83da66 100644 --- a/src/app.rs +++ b/src/app/mod.rs @@ -1,11 +1,17 @@ -use crate::{print_command_help, vox}; +mod command; +mod context; +mod helpers; +mod opt; -use super::command::{help, Command, Handler}; -use super::context::Context; -use super::opt::{Opt, OptError, OptKind}; +use command::help; +pub use command::{Command, Handler}; +pub use context::Context; +pub use opt::{Opt, OptError, OptKind}; use std::env; +use command::print_command_help; + type Result = std::result::Result; pub struct App { @@ -196,7 +202,7 @@ impl App { if let Err(e) = run_command(self, &self.root, &args, &mut ctx) { match e { OptError::InvalidOpt(opt) => { - vox::error(format!("Invalid options {}", &opt)); + helpers::error(format!("Invalid options {}", &opt)); Err(OptError::InvalidOpt(opt)) } } @@ -315,7 +321,7 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> // Automatic command help display if ignored.iter().any(|a| a == "-h" || a == "--help") { - super::command::print_command_help(cmd, &[]); + print_command_help(cmd, &[]); return Ok(()); } @@ -332,7 +338,7 @@ fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> } handler(app, ctx, &ignored); } else { - crate::vox::print(app.application_header()); + helpers::print(app.application_header()); print_command_help(cmd, &[]) } diff --git a/src/opt.rs b/src/app/opt.rs similarity index 99% rename from src/opt.rs rename to src/app/opt.rs index 444e685..61cb1ea 100644 --- a/src/opt.rs +++ b/src/app/opt.rs @@ -124,7 +124,7 @@ impl Opt { } #[derive(Clone, Debug)] -pub(crate) enum OptKind { +pub enum OptKind { Flag, String, } diff --git a/src/lib.rs b/src/lib.rs index 0e3cccd..70e1e1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,13 +2,35 @@ mod macros; mod app; -mod command; -mod context; -mod opt; -pub use console; -pub mod vox; +pub mod ui; + +use std::error::Error; +use std::fmt::{Debug, Display}; + pub use app::*; -pub use command::*; -pub use context::*; -pub use opt::*; -pub use vox::*; +pub use app::{App, Command, Opt}; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum ArkhamError { + IO(String), + Other(String), +} + +impl Error for ArkhamError {} + +impl Display for ArkhamError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Other(v) => f.write_str(v), + Self::IO(v) => f.write_str(v), + } + } +} + +impl From for ArkhamError { + fn from(fr: std::io::Error) -> Self { + Self::IO(fr.to_string()) + } +} diff --git a/src/tasks.rs b/src/tasks.rs deleted file mode 100644 index 5666838..0000000 --- a/src/tasks.rs +++ /dev/null @@ -1,64 +0,0 @@ -use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle}; -use std::{ - sync::{Arc, Mutex}, - time::Instant, -}; - -#[derive(Clone)] -pub struct TaskGroup { - mp: Arc, -} - -impl TaskGroup { - pub fn new() -> Self { - Self { - mp: Arc::new(MultiProgress::new()), - } - } - - pub fn start_task(&self, desc: &str) -> Task { - let pb = self.mp.add(ProgressBar::new(1)); - let spinner_style = ProgressStyle::default_spinner() - .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ") - .template("{prefix:.bold.dim} {spinner} {wide_msg}"); - pb.set_style(spinner_style); - Task::new(TaskState { - pb, - desc: desc.into(), - started_at: Instant::now(), - }) - } - - pub fn join(&self) { - self.mp.join().unwrap(); - } -} - -#[derive(Clone)] -pub struct Task(Arc>); - -impl Task { - pub fn new(state: TaskState) -> Self { - Self(Arc::new(Mutex::new(state))) - } - - pub fn tick(&self) { - let state = self.0.lock().unwrap(); - state.pb.set_message(&format!( - "{} [{}]", - state.desc, - style(HumanDuration(state.started_at.elapsed())).yellow() - )); - state.pb.tick(); - } - - pub fn complete(&self) { - self.0.lock().unwrap().pb.finish_with_message("OK"); - } -} - -pub struct TaskState { - pb: ProgressBar, - desc: String, - started_at: Instant, -} diff --git a/src/ui/cell.rs b/src/ui/cell.rs new file mode 100644 index 0000000..c5aec86 --- /dev/null +++ b/src/ui/cell.rs @@ -0,0 +1,93 @@ +use crossterm::queue; +use crossterm::style::{ + Attribute, Attributes, SetAttributes, SetBackgroundColor, SetForegroundColor, +}; +use crossterm::style::{Color, Print}; +use std::io::Write; + +use crate::Result; + +#[derive(Clone, Debug, Copy, PartialEq)] +pub struct Cell { + empty: bool, + fg: Color, + bg: Color, + content: char, + attributes: Attributes, +} + +impl Default for Cell { + fn default() -> Self { + Self { + empty: true, + fg: Color::Reset, + bg: Color::Reset, + content: ' ', + attributes: Attributes::default(), + } + } +} + +impl Cell { + pub fn new() -> Self { + Self::default() + } + + pub fn content(&mut self, v: char) -> &mut Cell { + self.empty = false; + self.content = v; + self + } + + pub fn fg(&mut self, color: Color) -> &mut Cell { + self.fg = color; + self + } + + pub fn bg(&mut self, color: Color) -> &mut Cell { + self.bg = color; + self + } + + pub fn bold(&mut self, v: bool) -> &mut Cell { + if v { + self.attributes.set(Attribute::Bold); + } else { + self.attributes.unset(Attribute::Bold); + } + self + } + + pub fn render(&self, output: &mut impl Write) -> Result<()> { + if !self.empty { + queue!(output, SetForegroundColor(self.fg))?; + queue!(output, SetBackgroundColor(self.bg))?; + queue!(output, SetAttributes(self.attributes))?; + queue!(output, Print(&self.content))?; + } + Ok(()) + } + + pub fn raw(&self) -> Vec { + let mut output: Vec = vec![]; + self.render(&mut output).expect("Render error"); + output + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn cell_render() { + let mut cell = Cell::default(); + cell.fg(Color::Red); + cell.bg(Color::White); + cell.content = 'T'; + cell.bold(true); + let mut output: Vec = vec![]; + cell.render(&mut output).expect("Render error"); + let out_str = String::from_utf8(output).expect("Couldnt unwrap to utf8"); + assert_eq!(out_str, "\u{1b}[38;5;9m\u{1b}[48;5;15m\u{1b}[1mT"); + } +} diff --git a/src/ui/component.rs b/src/ui/component.rs new file mode 100644 index 0000000..442717a --- /dev/null +++ b/src/ui/component.rs @@ -0,0 +1,7 @@ +use crate::Result; + +use super::Context; + +pub trait Component { + fn view(&mut self, _ctx: &mut Context) -> Result<()>; +} diff --git a/src/ui/context.rs b/src/ui/context.rs new file mode 100644 index 0000000..ded0d73 --- /dev/null +++ b/src/ui/context.rs @@ -0,0 +1,34 @@ +use crossterm::event::Event; + +use super::{Component, Rect, View}; +use crate::Result; + +pub struct Context { + pub view: View, + pub event: Option, +} + +impl Default for Context { + fn default() -> Self { + Context { + view: View::fullscreen(), + event: None, + } + } +} + +impl Context { + pub fn new(width: u16, height: u16, event: Option) -> Self { + Context { + view: View::new(width, height), + event, + } + } + + pub fn add_component(&mut self, rect: Rect, cmp: &mut impl Component) -> Result<()> { + let mut ctx = Context::new(rect.width, rect.height, self.event); + cmp.view(&mut ctx)?; + self.view.merge(rect.pos, ctx.view); + Ok(()) + } +} diff --git a/src/ui/geometry.rs b/src/ui/geometry.rs new file mode 100644 index 0000000..930e431 --- /dev/null +++ b/src/ui/geometry.rs @@ -0,0 +1,51 @@ +use std::ops::{Add, Sub}; + +pub struct Rect { + pub pos: Pos, + pub width: u16, + pub height: u16, +} + +impl Rect { + pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self { + Rect { + pos: Pos::new(x, y), + width, + height, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Pos(u16, u16); + +impl Pos { + pub fn new(x: u16, y: u16) -> Self { + Pos(x, y) + } + + pub fn x(&self) -> u16 { + self.0 + } + + pub fn y(&self) -> u16 { + self.1 + } +} + +impl Add for Pos { + type Output = Pos; + fn add(self, rhs: Self) -> Self::Output { + Pos(self.x() + rhs.x(), self.y() + rhs.y()) + } +} + +impl Sub for Pos { + type Output = Pos; + fn sub(self, rhs: Self) -> Self::Output { + Pos( + (self.x() as i32 - rhs.x() as i32).max(0) as u16, + (self.y() as i32 - rhs.y() as i32).max(0) as u16, + ) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..076e6d1 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,44 @@ +mod cell; +mod component; +mod context; +mod geometry; +mod view; + +pub use cell::Cell; +pub use component::Component; +pub use context::Context; +pub use crossterm::style::{Color, Stylize}; +use crossterm::terminal::enable_raw_mode; +pub use geometry::{Pos, Rect}; +pub use std::io::Write; +pub use view::View; + +use crate::Result; + +pub struct UI { + root: Box, +} + +impl UI { + pub fn new(root: C) -> Self { + Self { + root: Box::new(root), + } + } + pub fn run(&mut self) -> Result<()> { + let mut output = std::io::stdout(); + let mut context = Context::default(); + loop { + // enable_raw_mode(); + self.root.view(&mut context)?; + context.view.render(Pos::new(0, 0), &mut output)?; + output.flush().expect("Couldnt flush output"); + + let event = crossterm::event::read()?; + context.event = Some(event); + self.root.view(&mut context)?; + context.view.render(Pos::new(0, 0), &mut output)?; + output.flush().expect("Couldnt flush output"); + } + } +} diff --git a/src/ui/view.rs b/src/ui/view.rs new file mode 100644 index 0000000..5072205 --- /dev/null +++ b/src/ui/view.rs @@ -0,0 +1,77 @@ +use super::Rect; +use std::io::Write; + +use crossterm::{ + cursor::{MoveDown, MoveTo, MoveToColumn}, + queue, terminal, +}; + +use crate::Result; + +use super::{Cell, Pos}; + +pub struct View { + cells: Vec, + width: u16, + #[allow(dead_code)] + height: u16, +} + +impl View { + pub fn new(width: u16, height: u16) -> Self { + Self { + width, + height, + cells: vec![Cell::default(); (width * height) as usize], + } + } + + pub fn fullscreen() -> Self { + let (width, height) = terminal::size().expect("Couldnt detect terminal size"); + Self { + width, + height, + cells: vec![Cell::default(); (width * height) as usize], + } + } + + pub fn get(&self, pos: Pos) -> Option<&Cell> { + self.cells.get((pos.y() * self.width + pos.x()) as usize) + } + + pub fn set(&mut self, pos: Pos, cell: Cell) { + self.cells[(pos.y() * self.width + pos.x()) as usize] = cell; + } + + pub fn fill(&mut self, rect: Rect, cell: Cell) { + for y in rect.pos.y()..rect.pos.y() + rect.height { + for x in rect.pos.x()..rect.pos.x() + rect.width { + self.set(Pos::new(x, y), cell); + } + } + } + + pub fn fill_all(&mut self, cell: Cell) { + self.cells.fill(cell); + } + + pub fn merge(&mut self, pos: Pos, view: View) { + for (idx, line) in view.cells.chunks(view.width as usize).enumerate() { + let start = ((pos.y() + idx as u16) * self.width + pos.x()) as usize; + let end = start + line.len(); + self.cells.splice(start..end, line.iter().cloned()); + } + } + + pub fn render(&mut self, pos: Pos, output: &mut impl Write) -> Result<()> { + queue!(output, MoveTo(pos.x(), pos.y()))?; + for line in self.cells.chunks(self.width as usize) { + for cell in line { + cell.render(output)?; + } + queue!(output, MoveDown(1))?; + queue!(output, MoveToColumn(pos.x()))?; + } + Ok(()) + } +}