initial UI framework

This commit is contained in:
Joe Bellus 2022-01-06 00:46:27 -05:00
parent 93195bab1b
commit 6b1098c863
18 changed files with 616 additions and 159 deletions

186
Cargo.lock generated
View File

@ -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"

View File

@ -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

54
benches/space_fill.rs Normal file
View File

@ -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<u8> = 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<u8> = 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<u8> = vec![];
space
.render(ui::Pos::new(0, 0), &mut output)
.expect("couldnt render");
})
});
}
criterion_group!(benches, space_fill);
criterion_main!(benches);

29
examples/component.rs Normal file
View File

@ -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");
}

View File

@ -1,8 +0,0 @@
// use arkham::TaskGroup;
fn main() {
// let tasks = TaskGroup::new();
// let task = tasks.start_task("task 1");
// task.tick();
// tasks.join();
}

View File

@ -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()))

View File

@ -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<String, ContextValue>);
@ -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) {

View File

@ -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<Color> for console::Color {
impl From<Color> for style::Color {
fn from(fr: Color) -> Self {
match fr {
Color::Black => Self::Black,
@ -27,33 +26,23 @@ impl From<Color> 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<T: Display>(str: T) {
println!("{}", style(str).white().bold());
pub fn print<T: Display>(s: T) {
println!("{}", s);
}
pub fn header<T: Display>(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<T: Display>(str: T) {
println!("{}", style(str).white().dim());
}
pub fn description_list(list: HashMap<String, String>) {
let mut lines = list.into_iter().collect::<Vec<(String, String)>>();
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<String, String>) {
println!("{}", output);
}
pub fn print<T: Display>(s: T) {
println!("{}", s);
}
pub fn error<T: Display>(s: T) {
println!("{}", style(s).red().bold());
}

View File

@ -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<T> = std::result::Result<T, OptError>;
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, &[])
}

View File

@ -124,7 +124,7 @@ impl Opt {
}
#[derive(Clone, Debug)]
pub(crate) enum OptKind {
pub enum OptKind {
Flag,
String,
}

View File

@ -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<T> = std::result::Result<T, ArkhamError>;
#[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<std::io::Error> for ArkhamError {
fn from(fr: std::io::Error) -> Self {
Self::IO(fr.to_string())
}
}

View File

@ -1,64 +0,0 @@
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
use std::{
sync::{Arc, Mutex},
time::Instant,
};
#[derive(Clone)]
pub struct TaskGroup {
mp: Arc<MultiProgress>,
}
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<Mutex<TaskState>>);
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,
}

93
src/ui/cell.rs Normal file
View File

@ -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<u8> {
let mut output: Vec<u8> = 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<u8> = 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");
}
}

7
src/ui/component.rs Normal file
View File

@ -0,0 +1,7 @@
use crate::Result;
use super::Context;
pub trait Component {
fn view(&mut self, _ctx: &mut Context) -> Result<()>;
}

34
src/ui/context.rs Normal file
View File

@ -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<Event>,
}
impl Default for Context {
fn default() -> Self {
Context {
view: View::fullscreen(),
event: None,
}
}
}
impl Context {
pub fn new(width: u16, height: u16, event: Option<Event>) -> 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(())
}
}

51
src/ui/geometry.rs Normal file
View File

@ -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,
)
}
}

44
src/ui/mod.rs Normal file
View File

@ -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<dyn Component>,
}
impl UI {
pub fn new<C: Component + 'static>(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");
}
}
}

77
src/ui/view.rs Normal file
View File

@ -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<Cell>,
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(())
}
}