commit aaa51179ad180cc7622ead4bea6dd8b595a0619f Author: Joe Bellus Date: Mon May 29 16:11:04 2023 -0400 refactor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7633b59 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "arkham" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.71" +crossterm = "0.26.1" +ctrlc = "3.3.1" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..f162fa3 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,138 @@ +use std::{any::Any, cell::RefCell, io::Write, marker::PhantomData, rc::Rc}; + +use crossterm::{ + cursor, + event::{Event, KeyCode, KeyEventKind, KeyModifiers}, + execute, queue, terminal, +}; + +use crate::{ + container::{Callable, Container, FromContainer, Res, State}, + context::ViewContext, + view::View, +}; + +use super::input::Keyboard; + +pub struct App +where + F: Callable, + Args: FromContainer, +{ + container: Rc>, + main_view: View, + root: F, + args: PhantomData, +} + +impl App +where + F: Callable, + Args: FromContainer, +{ + pub fn new(root: F) -> App { + let container = Rc::new(RefCell::new(Container::default())); + let size = terminal::size().unwrap(); + let main_view = View::new(size); + App { + container, + root, + main_view, + args: PhantomData::default(), + } + } + + pub fn change_root(&mut self, root: F) { + self.root = root; + } + + pub fn insert_resource(self, v: T) -> Self { + self.container.borrow_mut().bind(Res::new(v)); + self + } + + pub fn insert_state(self, v: T) -> Self { + self.container.borrow_mut().bind(State::new(v)); + self + } + + fn teardown(&self) { + let mut out = std::io::stdout(); + let _ = terminal::disable_raw_mode(); + let _ = execute!(out, terminal::LeaveAlternateScreen, cursor::Show); + } + + pub fn run(&mut self) -> anyhow::Result<()> { + self.container.borrow_mut().bind(Res::new(Terminal)); + self.container.borrow_mut().bind(Res::new(Keyboard::new())); + let _ = ctrlc::set_handler(|| { + let mut out = std::io::stdout(); + let _ = terminal::disable_raw_mode(); + let _ = execute!(out, terminal::LeaveAlternateScreen, cursor::Show); + std::process::exit(0); + }); + let mut out = std::io::stdout(); + execute!(out, terminal::EnterAlternateScreen, cursor::Hide)?; + terminal::enable_raw_mode()?; + + loop { + let mut context = + ViewContext::new(self.container.clone(), terminal::size().unwrap().into()); + + self.container.borrow().call(&mut context, &self.root); + self.main_view.apply((0, 0), context.view); + self.render()?; + + self.container + .borrow() + .get::>() + .unwrap() + .reset(); + + if let Ok(event) = crossterm::event::read() { + match event { + Event::FocusGained => todo!(), + Event::FocusLost => todo!(), + Event::Key(key_event) if key_event.code == KeyCode::Char('q') => { + break; + } + Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { + let container = self.container.borrow(); + let kb = container.get::>().unwrap(); + kb.set_key(key_event.code); + } + Event::Mouse(_) => todo!(), + Event::Paste(_) => todo!(), + Event::Resize(_, _) => todo!(), + _ => {} + } + } + } + self.teardown(); + + Ok(()) + } + + fn render(&mut self) -> anyhow::Result<()> { + let mut out = std::io::stdout(); + for (row, line) in self.main_view.iter().enumerate() { + for (col, rune) in line.iter().enumerate() { + queue!(out, cursor::MoveTo(col as u16, row as u16))?; + rune.render(&mut out)?; + } + } + out.flush()?; + Ok(()) + } +} + +pub struct Terminal; + +impl Terminal { + pub fn set_title(&self, name: &str) { + let _ = execute!(std::io::stdout(), terminal::SetTitle(name)); + } + pub fn size(&self) -> (u16, u16) { + crossterm::terminal::size().unwrap_or_default() + } +} diff --git a/src/container.rs b/src/container.rs new file mode 100644 index 0000000..8d5562a --- /dev/null +++ b/src/container.rs @@ -0,0 +1,193 @@ +use std::{ + any::{Any, TypeId}, + cell::RefCell, + collections::HashMap, + ops::Deref, + rc::Rc, +}; + +use crate::context::ViewContext; +type ArkhamResult = anyhow::Result; +pub enum ArkhamState { + Noop, +} + +#[derive(Default)] +pub struct Container { + bindings: HashMap>, +} + +impl Container { + pub fn bind(&mut self, val: T) { + self.bindings.insert(val.type_id(), Box::new(val)); + } + + pub fn get(&self) -> Option<&T> { + self.bindings + .get(&TypeId::of::()) + .and_then(|boxed| boxed.downcast_ref()) + } + + pub fn call(&self, view: &mut ViewContext, callable: &F) + where + F: Callable, + Args: FromContainer, + { + callable.call(view, Args::from_container(self)); + } +} + +pub trait Callable { + fn call(&self, view: &mut ViewContext, args: Args) -> ArkhamResult; +} + +pub trait FromContainer { + fn from_container(container: &Container) -> Self; +} + +pub struct State(Rc>); + +impl State { + pub fn new(val: T) -> Self { + State(Rc::new(RefCell::new(val))) + } + + pub fn get_mut(&self) -> std::cell::RefMut { + RefCell::borrow_mut(&self.0) + } + + pub fn get(&self) -> std::cell::Ref { + RefCell::borrow(&self.0) + } +} + +impl Clone for State { + fn clone(&self) -> State { + State(self.0.clone()) + } +} + +impl FromContainer for State { + fn from_container(container: &Container) -> Self { + container.get::().expect("type not found").clone() + } +} + +#[derive(Debug)] +pub struct Res(Rc); + +impl Res { + pub fn new(val: T) -> Self { + Res(Rc::new(val)) + } +} + +impl Res { + pub fn get(&self) -> &T { + self.0.as_ref() + } +} + +impl Clone for Res { + fn clone(&self) -> Res { + Res(self.0.clone()) + } +} + +impl Deref for Res { + type Target = Rc; + + fn deref(&self) -> &Rc { + &self.0 + } +} + +impl FromContainer for Res { + fn from_container(container: &Container) -> Self { + container.get::().expect("type not found").clone() + } +} + +impl Callable<()> for Func +where + Func: Fn(&mut ViewContext), +{ + #[inline] + fn call(&self, view: &mut ViewContext, _args: ()) -> ArkhamResult { + (self)(view); + Ok(ArkhamState::Noop) + } +} + +impl FromContainer for () { + #[inline] + fn from_container(_container: &Container) -> Self {} +} + +macro_rules! callable_tuple ({ $($param:ident)* } => { + impl Callable<($($param,)*)> for Func + where + Func: Fn(&mut ViewContext, $($param),*), + { + #[inline] + #[allow(non_snake_case)] + fn call(&self, view: &mut ViewContext , ($($param,)*): ($($param,)*)) -> ArkhamResult{ + (self)(view, $($param,)*); + Ok(ArkhamState::Noop) + } + } +}); + +// callable_tuple! {} +callable_tuple! { A } +callable_tuple! { A B } +callable_tuple! { A B C } +callable_tuple! { A B C D } +callable_tuple! { A B C D E } +callable_tuple! { A B C D E F } +callable_tuple! { A B C D E F G } +callable_tuple! { A B C D E F G H } +callable_tuple! { A B C D E F G H I } +callable_tuple! { A B C D E F G H I J } +callable_tuple! { A B C D E F G H I J K } +callable_tuple! { A B C D E F G H I J K L } + +macro_rules! tuple_from_tm { + ( $($T: ident )+ ) => { + impl<$($T: FromContainer),+> FromContainer for ($($T,)+) + { + #[inline] + fn from_container(container: &Container) -> Self { + ($($T::from_container(container),)+) + } + } + }; + } + +tuple_from_tm! { A } +tuple_from_tm! { A B } +tuple_from_tm! { A B C } +tuple_from_tm! { A B C D } +tuple_from_tm! { A B C D E } +tuple_from_tm! { A B C D E F } +tuple_from_tm! { A B C D E F G } +tuple_from_tm! { A B C D E F G H } +tuple_from_tm! { A B C D E F G H I } +tuple_from_tm! { A B C D E F G H I J } +tuple_from_tm! { A B C D E F G H I J K } +tuple_from_tm! { A B C D E F G H I J K L } + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, rc::Rc}; + + use crate::{container::Container, prelude::ViewContext}; + + #[test] + fn test_no_params() { + fn test_f(_ctx: &mut ViewContext) {} + let container = Rc::new(RefCell::new(Container::default())); + let mut ctx = ViewContext::new(container.clone(), (1, 1).into()); + container.borrow().call(&mut ctx, &test_f); + } +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..5fcbf33 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,66 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::{ + container::{Callable, FromContainer}, + widget::Widget, +}; + +use super::{ + container::Container, + geometry::{Pos, Rect, Size}, + runes::Rune, + view::View, +}; + +pub struct ViewContext { + pub view: View, + pub container: Rc>, +} + +impl std::ops::DerefMut for ViewContext { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.view + } +} + +impl std::ops::Deref for ViewContext { + type Target = View; + + fn deref(&self) -> &Self::Target { + &self.view + } +} + +impl ViewContext { + pub fn new(container: Rc>, size: Size) -> Self { + let view = View::new(size); + + Self { view, container } + } + + pub fn component(&mut self, rect: Rect, f: F) + where + F: Callable, + Args: FromContainer, + { + let mut context = ViewContext::new(self.container.clone(), rect.size); + self.container.borrow().call(&mut context, &f); + self.view.apply(rect.pos, context.view); + } + + pub fn widget(&mut self, rect: Rect, mut widget: impl Widget) { + let mut context = ViewContext::new(self.container.clone(), rect.size); + widget.ui(&mut context); + self.view.apply(rect.pos, context.view); + } + + pub fn set_rune

(&mut self, pos: P, rune: Rune) + where + P: Into, + { + let Pos { x, y } = pos.into(); + if let Some(r) = self.view.get_mut(y).and_then(|row| row.get_mut(x)) { + *r = rune; + } + } +} diff --git a/src/geometry.rs b/src/geometry.rs new file mode 100644 index 0000000..773a51c --- /dev/null +++ b/src/geometry.rs @@ -0,0 +1,134 @@ +use std::ops::{Add, Sub}; + +#[derive(Debug, Clone, Copy)] +pub struct Pos { + pub x: usize, + pub y: usize, +} + +impl From<(usize, usize)> for Pos { + fn from(value: (usize, usize)) -> Self { + Self { + x: value.0, + y: value.1, + } + } +} + +impl From for Pos { + fn from(value: usize) -> Self { + Self { x: value, y: value } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Size { + pub width: usize, + pub height: usize, +} + +impl Add for Size { + type Output = Size; + + fn add(self, rhs: Size) -> Self::Output { + Self { + width: self.width + rhs.width, + height: self.height + rhs.height, + } + } +} + +impl Sub for Size { + type Output = Size; + + fn sub(self, rhs: Size) -> Self::Output { + Self { + width: self.width - rhs.width, + height: self.height - rhs.height, + } + } +} + +impl Add for Size { + type Output = Size; + + fn add(self, rhs: i32) -> Self::Output { + Self { + width: self.width + rhs as usize, + height: self.height + rhs as usize, + } + } +} + +impl Sub for Size { + type Output = Size; + + fn sub(self, rhs: i32) -> Self::Output { + Self { + width: self.width - rhs as usize, + height: self.height - rhs as usize, + } + } +} + +impl From<(usize, usize)> for Size { + fn from(value: (usize, usize)) -> Self { + Self { + width: value.0, + height: value.1, + } + } +} + +impl From<(u16, u16)> for Size { + fn from(value: (u16, u16)) -> Self { + Self { + width: value.0 as usize, + height: value.1 as usize, + } + } +} + +impl From<(i32, i32)> for Size { + fn from(value: (i32, i32)) -> Self { + Self { + width: value.0 as usize, + height: value.1 as usize, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Rect { + pub pos: Pos, + pub size: Size, +} + +impl Rect { + pub fn new(pos: P, size: S) -> Self + where + P: Into, + S: Into, + { + Self { + pos: pos.into(), + size: size.into(), + } + } + + pub fn with_size(size: S) -> Self + where + S: Into, + { + Rect { + pos: (0, 0).into(), + size: size.into(), + } + } +} + +impl From for Rect { + fn from(value: Size) -> Self { + Rect::with_size(value) + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..014383f --- /dev/null +++ b/src/input.rs @@ -0,0 +1,34 @@ +use std::{cell::RefCell, rc::Rc}; + +use crossterm::event::KeyCode; + +#[derive(Debug, Default)] +pub struct Keyboard { + key: Rc>>, +} + +impl Keyboard { + pub fn new() -> Self { + Self::default() + } + + pub fn set_key(&self, k: KeyCode) { + *self.key.borrow_mut() = Some(k); + } + + pub fn reset(&self) { + *self.key.borrow_mut() = None; + } + + pub fn code(&self) -> Option { + *self.key.borrow() + } + + pub fn char(&self) -> Option { + if let Some(KeyCode::Char(c)) = *self.key.borrow() { + Some(c) + } else { + None + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f537f57 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,25 @@ +mod app; +mod container; +mod context; +mod geometry; +mod input; +mod runes; +pub mod symbols; +mod theme; +mod view; +mod widget; + +pub mod prelude { + pub use super::{ + app::{App, Terminal}, + container::{Callable, FromContainer, Res, State}, + context::ViewContext, + geometry::{Pos, Rect, Size}, + input::Keyboard, + runes::{Rune, Runes, ToRuneExt}, + theme::Theme, + widget::Widget, + }; + pub use crossterm::event::KeyCode; + pub use crossterm::style::Color; +} diff --git a/src/runes.rs b/src/runes.rs new file mode 100644 index 0000000..3476d52 --- /dev/null +++ b/src/runes.rs @@ -0,0 +1,122 @@ +use crossterm::{ + queue, + style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor}, +}; + +#[derive(Clone, Copy, Default, Eq, PartialEq)] +pub struct Rune { + pub content: Option, + pub fg: Option, + pub bg: Option, + pub bold: bool, +} + +impl std::fmt::Debug for Rune { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(&format!("Rn({})", self.content.unwrap_or_default())) + .finish() + } +} + +impl Rune { + pub fn new() -> Self { + Self::default() + } + pub fn content(mut self, content: char) -> Self { + self.content = Some(content); + self + } + pub fn bg(mut self, bg: Color) -> Self { + self.bg = Some(bg); + self + } + + pub fn fg(mut self, fg: Color) -> Self { + self.fg = Some(fg); + self + } + + pub fn bold(mut self) -> Self { + self.bold = true; + self + } + + pub fn render(self, out: &mut W) -> anyhow::Result<()> + where + W: std::io::Write, + { + if let Some(content) = self.content { + if let Some(c) = self.fg { + queue!(out, SetForegroundColor(c))?; + } + if let Some(c) = self.bg { + queue!(out, SetBackgroundColor(c))?; + } + if self.bold { + queue!(out, SetAttribute(Attribute::Bold))?; + } + queue!(out, Print(content))?; + } + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct Runes(Vec); + +impl std::ops::Deref for Runes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Runes { + fn from(value: T) -> Self { + Runes( + value + .to_string() + .chars() + .map(|c| Rune::new().content(c)) + .collect(), + ) + } +} + +impl Runes { + pub fn fg(mut self, color: Color) -> Self { + for r in self.0.iter_mut() { + r.fg = Some(color); + } + self + } + pub fn bg(mut self, color: Color) -> Self { + for r in self.0.iter_mut() { + r.bg = Some(color); + } + self + } + pub fn bold(mut self) -> Self { + for r in self.0.iter_mut() { + r.bold = true; + } + self + } +} + +pub trait ToRuneExt { + fn to_runes(&self) -> Runes; +} + +impl ToRuneExt for String { + fn to_runes(&self) -> Runes { + Runes::from(self.to_string()) + } +} + +impl ToRuneExt for &str { + fn to_runes(&self) -> Runes { + Runes::from(self.to_string()) + } +} diff --git a/src/symbols.rs b/src/symbols.rs new file mode 100644 index 0000000..68fe138 --- /dev/null +++ b/src/symbols.rs @@ -0,0 +1,94 @@ +#![allow(dead_code)] + +#[cfg(any(not(windows), all(windows)))] +mod universal { + pub const TICK: char = '✔'; + pub const CROSS: char = '✖'; + pub const STAR: char = '★'; + pub const SQUARE: char = '▇'; + pub const SQUARE_SMALL: char = '◻'; + pub const SQUARE_SMALL_FILLED: char = '◼'; + pub const PLAY: char = '▶'; + pub const CIRCLE: char = '◯'; + pub const CIRCLE_FILLED: char = '◉'; + pub const CIRCLE_DOTTED: char = '◌'; + pub const CIRCLE_DOUBLE: char = '◎'; + pub const CIRCLE_CIRCLE: char = 'ⓞ'; + pub const CIRCLE_CROSS: char = 'ⓧ'; + pub const CIRCLE_PIPE: char = 'Ⓘ'; + pub const CIRCLE_QUESTION_MARK: char = '?'; + pub const BULLET: char = '●'; + pub const DOT: char = '․'; + pub const LINE: char = '─'; + pub const ELLIPSIS: char = '…'; + pub const POINTER: char = '❯'; + pub const POINTER_SMALL: char = '›'; + pub const INFO: char = 'ℹ'; + pub const WARNING: char = '⚠'; + pub const HAMBURGER: char = '☰'; + pub const SMILEY: char = '㋡'; + pub const MUSTACHE: char = '෴'; + pub const HEART: char = '♥'; + pub const NODEJS: char = '⬢'; + pub const ARROW_UP: char = '↑'; + pub const ARROW_DOWN: char = '↓'; + pub const ARROW_LEFT: char = '←'; + pub const ARROW_RIGHT: char = '→'; + pub const RADIO_ON: char = '◉'; + pub const RADIO_OFF: char = '◯'; + pub const CHECKBOX_ON: char = '☒'; + pub const CHECKBOX_OFF: char = '☐'; + pub const CHECKBOX_CIRCLE_ON: char = 'ⓧ'; + pub const CHECKBOX_CIRCLE_OFF: char = 'Ⓘ'; + pub const QUESTION_MARK_PREFIX: char = '?'; + pub const ONE_HALF: char = '½'; + pub const ONE_THIRD: char = '⅓'; + pub const ONE_QUARTER: char = '¼'; + pub const ONE_FIFTH: char = '⅕'; + pub const ONE_SIXTH: char = '⅙'; + pub const ONE_SEVENTH: char = '⅐'; + pub const ONE_EIGHTH: char = '⅛'; + pub const ONE_NINTH: char = '⅑'; + pub const ONE_TENTH: char = '⅒'; + pub const TWO_THIRDS: char = '⅔'; + pub const TWO_FIFTHS: char = '⅖'; + pub const THREE_QUARTERS: char = '¾'; + pub const THREE_FIFTHS: char = '⅗'; + pub const THREE_EIGHTHS: char = '⅜'; + pub const FOUR_FIFTHS: char = '⅘'; + pub const FIVE_SIXTHS: char = '⅚'; + pub const FIVE_EIGHTHS: char = '⅝'; + pub const SEVEN_EIGHTHS: char = '⅞'; +} + +#[cfg(any(not(windows), all(windows)))] +pub use universal::*; + +#[cfg(all(windows))] +mod win { + pub const TICK: char = '√'; + pub const CROSS: char = '×'; + pub const STAR: char = '*'; + pub const SQUARE: char = '█'; + pub const PLAY: char = '►'; + pub const BULLET: char = '*'; + pub const DOT: char = '.'; + pub const LINE: char = '─'; + pub const POINTER: char = '>'; + pub const POINTER_SMALL: char = '»'; + pub const INFO: char = 'i'; + pub const WARNING: char = '‼'; + pub const HAMBURGER: char = '≡'; + pub const SMILEY: char = '☺'; + pub const HEART: char = '♥'; + pub const NODEJS: char = '♦'; + pub const ARROW_UP: char = '↑'; + pub const ARROW_DOWN: char = '↓'; + pub const ARROW_LEFT: char = '←'; + pub const ARROW_RIGHT: char = '→'; + pub const QUESTION_MARK_PREFIX: char = '?'; + pub const ONE_HALF: char = ' '; +} + +#[cfg(all(windows))] +pub use win::*; diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..e68d515 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,45 @@ +use crossterm::style::Color; + +#[derive(Debug)] +pub struct Theme { + pub bg_primary: Color, + pub bg_secondary: Color, + pub bg_tertiary: Color, + pub bg_selection: Color, + pub fg_selection: Color, + pub fg: Color, + pub accent: Color, +} + +impl Default for Theme { + fn default() -> Self { + Self { + bg_primary: Color::Rgb { + r: 36, + g: 39, + b: 58, + }, + + bg_secondary: Color::Rgb { + r: 20, + g: 22, + b: 30, + }, + + bg_tertiary: Color::Rgb { + r: 76, + g: 79, + b: 98, + }, + + bg_selection: Color::Rgb { r: 60, g: 0, b: 60 }, + fg_selection: Color::White, + fg: Color::White, + accent: Color::Rgb { + r: 150, + g: 0, + b: 150, + }, + } + } +} diff --git a/src/view.rs b/src/view.rs new file mode 100644 index 0000000..2f40fdd --- /dev/null +++ b/src/view.rs @@ -0,0 +1,178 @@ +use crate::{ + geometry::{Pos, Rect, Size}, + runes::{Rune, Runes}, +}; + +#[derive(Clone)] +pub struct View(pub Vec>); + +impl std::ops::DerefMut for View { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::ops::Deref for View { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl View { + pub fn new(size: T) -> Self + where + T: Into, + { + let size: Size = size.into(); + Self(vec![vec![Rune::default(); size.width]; size.height]) + } + + pub fn iter(&self) -> impl Iterator> { + self.0.iter() + } + + pub fn apply>(&mut self, pos: P, view: View) { + let pos = pos.into(); + for (y, line) in view.0.iter().enumerate() { + if self.0.len() > y + pos.y { + for (x, rune) in line.iter().enumerate() { + if rune.content.is_some() && self.0[y].len() > x + pos.x { + let _ = std::mem::replace(&mut self.0[y + pos.y][x + pos.x], *rune); + } + } + } + } + } + + pub fn width(&self) -> usize { + self.0.first().unwrap().len() + } + pub fn height(&self) -> usize { + self.0.len() + } + pub fn size(&self) -> Size { + (self.width(), self.height()).into() + } + pub fn fill(&mut self, rect: Rect, rune: Rune) { + for y in rect.pos.y..(rect.size.height + rect.pos.y).min(self.0.len()) { + for x in rect.pos.x..(rect.size.width + rect.pos.x).min(self.0[y].len()) { + let _ = std::mem::replace(&mut self.0[y][x], rune); + } + } + } + + pub fn insert, S: Into>(&mut self, pos: P, value: S) { + let Pos { x, y } = pos.into(); + let runes: Runes = value.into(); + if let Some(line) = self.0.get_mut(y) { + let line_len = line.len() as i32; + for (i, c) in runes + .iter() + .take((line_len - x as i32).max(0) as usize) + .enumerate() + { + let _ = std::mem::replace(&mut line[x + i], *c); + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::{geometry::Rect, runes::Rune}; + + use super::View; + + #[test] + pub fn test_insert_pos() { + let mut view = View::new((5, 3)); + view.insert((1, 2), "test"); + dbg!(&view.0); + assert_eq!(view.0[2][1].content, Some('t')); + assert_eq!(view.0[2][2].content, Some('e')); + assert_eq!(view.0[2][3].content, Some('s')); + assert_eq!(view.0[2][4].content, Some('t')); + } + + #[test] + pub fn test_fill() { + let mut view = View::new((3, 3)); + view.fill(Rect::new((1, 1), (2, 2)), Rune::new().content('X')); + dbg!(&view.0); + assert_eq!(view.0[0][0].content, None); + assert_eq!(view.0[0][1].content, None); + assert_eq!(view.0[0][2].content, None); + + assert_eq!(view.0[1][0].content, None); + assert_eq!(view.0[1][1].content, Some('X')); + assert_eq!(view.0[1][2].content, Some('X')); + + assert_eq!(view.0[2][0].content, None); + assert_eq!(view.0[2][1].content, Some('X')); + assert_eq!(view.0[2][2].content, Some('X')); + } + + #[test] + pub fn test_fill_overflow() { + let mut view = View::new((3, 3)); + view.fill(Rect::new((1, 1), (4, 4)), Rune::new().content('X')); + dbg!(&view.0); + assert_eq!(view.0[0][0].content, None); + assert_eq!(view.0[0][1].content, None); + assert_eq!(view.0[0][2].content, None); + + assert_eq!(view.0[1][0].content, None); + assert_eq!(view.0[1][1].content, Some('X')); + assert_eq!(view.0[1][2].content, Some('X')); + + assert_eq!(view.0[2][0].content, None); + assert_eq!(view.0[2][1].content, Some('X')); + assert_eq!(view.0[2][2].content, Some('X')); + } + + #[test] + pub fn test_apply() { + let mut view1 = View::new((3, 4)); + view1.fill(Rect::new((1, 1), (2, 2)), Rune::new().content('X')); + let mut view2 = View::new((3, 4)); + view2.apply((0, 1), view1); + dbg!(&view2.0); + assert_eq!(view2.0[0][0].content, None); + assert_eq!(view2.0[0][1].content, None); + assert_eq!(view2.0[0][2].content, None); + + assert_eq!(view2.0[1][0].content, None); + assert_eq!(view2.0[1][1].content, None); + assert_eq!(view2.0[1][2].content, None); + + assert_eq!(view2.0[2][0].content, None); + assert_eq!(view2.0[2][1].content, Some('X')); + assert_eq!(view2.0[2][2].content, Some('X')); + + assert_eq!(view2.0[3][0].content, None); + assert_eq!(view2.0[3][1].content, Some('X')); + assert_eq!(view2.0[3][2].content, Some('X')); + } + + #[test] + pub fn test_apply_overflow() { + let mut view0 = View::new((5, 5)); + view0.fill(Rect::new((1, 1), (4, 4)), Rune::new().content('X')); + let mut view = View::new((3, 3)); + view.apply((0, 0), view0); + dbg!(&view.0); + assert_eq!(view.0[0][0].content, None); + assert_eq!(view.0[0][1].content, None); + assert_eq!(view.0[0][2].content, None); + + assert_eq!(view.0[1][0].content, None); + assert_eq!(view.0[1][1].content, Some('X')); + assert_eq!(view.0[1][2].content, Some('X')); + + assert_eq!(view.0[2][0].content, None); + assert_eq!(view.0[2][1].content, Some('X')); + assert_eq!(view.0[2][2].content, Some('X')); + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs new file mode 100644 index 0000000..40bf396 --- /dev/null +++ b/src/widget/mod.rs @@ -0,0 +1,13 @@ +use crate::prelude::{Res, Runes, Theme, ViewContext}; + +pub trait Widget { + fn ui(&mut self, ctx: &mut ViewContext); +} + +fn list(items: Vec, selection_index: usize) -> impl FnOnce(&mut ViewContext, Res) { + move |ctx: &mut ViewContext, theme: Res| { + for (idx, item) in items.into_iter().enumerate() { + ctx.insert((0, idx), item.clone()); + } + } +}