This commit is contained in:
Joe Bellus 2023-05-29 16:11:04 -04:00
commit aaa51179ad
13 changed files with 1055 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/Cargo.lock

11
Cargo.toml Normal file
View File

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

138
src/app.rs Normal file
View File

@ -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<F, Args>
where
F: Callable<Args>,
Args: FromContainer,
{
container: Rc<RefCell<Container>>,
main_view: View,
root: F,
args: PhantomData<Args>,
}
impl<F, Args> App<F, Args>
where
F: Callable<Args>,
Args: FromContainer,
{
pub fn new(root: F) -> App<F, Args> {
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<T: Any>(self, v: T) -> Self {
self.container.borrow_mut().bind(Res::new(v));
self
}
pub fn insert_state<T: Any>(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::<Res<Keyboard>>()
.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::<Res<Keyboard>>().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()
}
}

193
src/container.rs Normal file
View File

@ -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<ArkhamState>;
pub enum ArkhamState {
Noop,
}
#[derive(Default)]
pub struct Container {
bindings: HashMap<TypeId, Box<dyn Any>>,
}
impl Container {
pub fn bind<T: Any>(&mut self, val: T) {
self.bindings.insert(val.type_id(), Box::new(val));
}
pub fn get<T: Any>(&self) -> Option<&T> {
self.bindings
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref())
}
pub fn call<F, Args>(&self, view: &mut ViewContext, callable: &F)
where
F: Callable<Args>,
Args: FromContainer,
{
callable.call(view, Args::from_container(self));
}
}
pub trait Callable<Args> {
fn call(&self, view: &mut ViewContext, args: Args) -> ArkhamResult;
}
pub trait FromContainer {
fn from_container(container: &Container) -> Self;
}
pub struct State<T: ?Sized>(Rc<RefCell<T>>);
impl<T> State<T> {
pub fn new(val: T) -> Self {
State(Rc::new(RefCell::new(val)))
}
pub fn get_mut(&self) -> std::cell::RefMut<T> {
RefCell::borrow_mut(&self.0)
}
pub fn get(&self) -> std::cell::Ref<T> {
RefCell::borrow(&self.0)
}
}
impl<T: ?Sized> Clone for State<T> {
fn clone(&self) -> State<T> {
State(self.0.clone())
}
}
impl<T: ?Sized + 'static> FromContainer for State<T> {
fn from_container(container: &Container) -> Self {
container.get::<Self>().expect("type not found").clone()
}
}
#[derive(Debug)]
pub struct Res<T: ?Sized>(Rc<T>);
impl<T> Res<T> {
pub fn new(val: T) -> Self {
Res(Rc::new(val))
}
}
impl<T: ?Sized> Res<T> {
pub fn get(&self) -> &T {
self.0.as_ref()
}
}
impl<T: ?Sized> Clone for Res<T> {
fn clone(&self) -> Res<T> {
Res(self.0.clone())
}
}
impl<T: ?Sized> Deref for Res<T> {
type Target = Rc<T>;
fn deref(&self) -> &Rc<T> {
&self.0
}
}
impl<T: ?Sized + 'static> FromContainer for Res<T> {
fn from_container(container: &Container) -> Self {
container.get::<Self>().expect("type not found").clone()
}
}
impl<Func> 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<Func, $($param,)*> 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);
}
}

66
src/context.rs Normal file
View File

@ -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<RefCell<Container>>,
}
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<RefCell<Container>>, size: Size) -> Self {
let view = View::new(size);
Self { view, container }
}
pub fn component<F, Args>(&mut self, rect: Rect, f: F)
where
F: Callable<Args>,
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<P>(&mut self, pos: P, rune: Rune)
where
P: Into<Pos>,
{
let Pos { x, y } = pos.into();
if let Some(r) = self.view.get_mut(y).and_then(|row| row.get_mut(x)) {
*r = rune;
}
}
}

134
src/geometry.rs Normal file
View File

@ -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<usize> 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<Size> 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<Size> 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<i32> 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<i32> 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<P, S>(pos: P, size: S) -> Self
where
P: Into<Pos>,
S: Into<Size>,
{
Self {
pos: pos.into(),
size: size.into(),
}
}
pub fn with_size<S>(size: S) -> Self
where
S: Into<Size>,
{
Rect {
pos: (0, 0).into(),
size: size.into(),
}
}
}
impl From<Size> for Rect {
fn from(value: Size) -> Self {
Rect::with_size(value)
}
}

34
src/input.rs Normal file
View File

@ -0,0 +1,34 @@
use std::{cell::RefCell, rc::Rc};
use crossterm::event::KeyCode;
#[derive(Debug, Default)]
pub struct Keyboard {
key: Rc<RefCell<Option<KeyCode>>>,
}
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<KeyCode> {
*self.key.borrow()
}
pub fn char(&self) -> Option<char> {
if let Some(KeyCode::Char(c)) = *self.key.borrow() {
Some(c)
} else {
None
}
}
}

25
src/lib.rs Normal file
View File

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

122
src/runes.rs Normal file
View File

@ -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<char>,
pub fg: Option<Color>,
pub bg: Option<Color>,
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<W>(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<Rune>);
impl std::ops::Deref for Runes {
type Target = Vec<Rune>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T: ToString> From<T> 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())
}
}

94
src/symbols.rs Normal file
View File

@ -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::*;

45
src/theme.rs Normal file
View File

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

178
src/view.rs Normal file
View File

@ -0,0 +1,178 @@
use crate::{
geometry::{Pos, Rect, Size},
runes::{Rune, Runes},
};
#[derive(Clone)]
pub struct View(pub Vec<Vec<Rune>>);
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<Vec<Rune>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl View {
pub fn new<T>(size: T) -> Self
where
T: Into<Size>,
{
let size: Size = size.into();
Self(vec![vec![Rune::default(); size.width]; size.height])
}
pub fn iter(&self) -> impl Iterator<Item = &Vec<Rune>> {
self.0.iter()
}
pub fn apply<P: Into<Pos>>(&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<P: Into<Pos>, S: Into<Runes>>(&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'));
}
}

13
src/widget/mod.rs Normal file
View File

@ -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<Runes>, selection_index: usize) -> impl FnOnce(&mut ViewContext, Res<Theme>) {
move |ctx: &mut ViewContext, theme: Res<Theme>| {
for (idx, item) in items.into_iter().enumerate() {
ctx.insert((0, idx), item.clone());
}
}
}