sync commit

This commit is contained in:
Joe Bellus 2023-09-02 22:46:50 -04:00
parent 3388ae4096
commit ba7703e4c2
16 changed files with 363 additions and 66 deletions

View File

@ -23,7 +23,20 @@ path = "examples/component_params.rs"
name = "keyboard"
path = "examples/keyboard.rs"
[[example]]
name = "navigation"
path = "examples/navigation.rs"
[[example]]
name = "stack"
path = "examples/stack.rs"
[[example]]
name = "paragraph"
path = "examples/paragraph.rs"
[dependencies]
anyhow = "1.0.71"
crossterm = "0.26.1"
ctrlc = "3.3.1"
textwrap = "0.16.0"

48
examples/navigation.rs Normal file
View File

@ -0,0 +1,48 @@
use arkham::prelude::*;
#[derive(Copy, Clone)]
enum Route {
Main,
Secondary,
}
fn main() {
let _ = App::new(root).insert_state(Route::Main).run();
}
fn root(ctx: &mut ViewContext, route: State<Route>) {
let size = ctx.size();
let r = *route.get();
ctx.insert((0, 0), "Press Q to quit");
match r {
Route::Main => {
ctx.component(Rect::new((0, 1), size), main_route);
}
Route::Secondary => {
ctx.component(Rect::new((0, 1), size), secondary_route);
}
}
}
fn main_route(ctx: &mut ViewContext, kb: Res<Keyboard>, route: State<Route>) {
ctx.insert(
0,
"Welcome to the main screen, press 2 to goto the secondary screen",
);
if kb.char() == Some('2') {
*route.get_mut() = Route::Secondary;
ctx.render();
}
}
fn secondary_route(ctx: &mut ViewContext, kb: Res<Keyboard>, route: State<Route>) {
ctx.insert(
0,
"Welcome to the secondary screen, press 1 to goto the main screen",
);
if kb.char() == Some('1') {
*route.get_mut() = Route::Main;
ctx.render();
}
}

14
examples/paragraph.rs Normal file
View File

@ -0,0 +1,14 @@
use arkham::prelude::*;
fn main() {
let _ = App::new(root).run();
}
fn root(ctx: &mut ViewContext) {
let mut stack = ctx.vertical_stack(ctx.size());
let p = arkham::components::Paragraph::new("Rust is a multi-paradigm, general-purpose programming language that emphasizes performance, type safety, and concurrency. It enforces memory safety—ensuring that all references point to valid memory—without requiring the use of a garbage collector or reference counting present in other memory-safe languages.");
stack.component(Size::new(100_usize, p.height(100)), p);
let p = arkham::components::Paragraph::new("Rust is a multi-paradigm, general-purpose programming language that emphasizes performance, type safety, and concurrency. It enforces memory safety—ensuring that all references point to valid memory—without requiring the use of a garbage collector or reference counting present in other memory-safe languages.");
stack.component(Size::new(100_usize, p.height(100)), p);
ctx.component(ctx.size(), stack);
}

17
examples/stack.rs Normal file
View File

@ -0,0 +1,17 @@
use arkham::prelude::*;
fn main() {
let _ = App::new(root).run();
}
fn root(ctx: &mut ViewContext) {
let mut stack = ctx.vertical_stack(Size::new(100, 100));
for _ in 0..10 {
stack.component(Size::new(ctx.size().width, 2), list_item);
}
ctx.component(Rect::new(0, (100, 100)), stack);
}
fn list_item(ctx: &mut ViewContext) {
ctx.insert(0, "line 1");
}

View File

@ -43,15 +43,10 @@ where
container,
root,
main_view,
args: PhantomData::default(),
args: PhantomData,
}
}
/// Used to change the root function that is used during each render cycle.
pub fn change_root(&mut self, root: F) {
self.root = root;
}
/// Insert a resource which can be injected into component functions.
///
/// This resource can only be accessed immutably by reference.
@ -131,12 +126,19 @@ where
terminal::enable_raw_mode()?;
loop {
let mut context =
ViewContext::new(self.container.clone(), terminal::size().unwrap().into());
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.root
.call(&mut context, Args::from_container(&self.container.borrow()));
self.main_view.apply((0, 0), &context.view);
self.render()?;
if !context.rerender {
break;
}
}
self.container
.borrow()

2
src/components/mod.rs Normal file
View File

@ -0,0 +1,2 @@
mod paragraph;
pub use paragraph::Paragraph;

View File

@ -0,0 +1,38 @@
use std::ops::Deref;
use crossterm::style::Color;
use crate::prelude::{Callable, ToRuneExt};
#[derive(Debug)]
pub struct Paragraph {
content: String,
fg: Option<Color>,
bg: Option<Color>,
}
impl Paragraph {
pub fn new(content: &str) -> Self {
Self {
content: content.to_string(),
fg: None,
bg: None,
}
}
pub fn height(&self, width: usize) -> usize {
textwrap::wrap(&self.content, width).len()
}
}
impl Callable<()> for Paragraph {
fn call(&self, view: &mut crate::prelude::ViewContext, _args: ()) {
let lines = textwrap::wrap(&self.content, view.width());
let mut stack = view.vertical_stack(view.size());
for line in lines.iter() {
let l = line.deref().to_runes();
stack.insert(line);
}
view.component(view.size(), stack);
}
}

View File

@ -7,14 +7,10 @@ use std::{
};
use crate::context::ViewContext;
type ArkhamResult = anyhow::Result<ArkhamState>;
pub enum ArkhamState {
Noop,
}
/// The container stores typed resource and state objects and provides
/// them to component functions.
#[derive(Default)]
#[derive(Default, Debug)]
pub struct Container {
bindings: HashMap<TypeId, Box<dyn Any>>,
}
@ -36,15 +32,6 @@ impl Container {
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref())
}
/// Call a function while providing dependency injcetion.
pub fn call<F, Args>(&self, view: &mut ViewContext, callable: &F)
where
F: Callable<Args>,
Args: FromContainer,
{
let _ = callable.call(view, Args::from_container(self));
}
}
/// A wrapper for state objcets. This internally holds a reference counted
@ -136,24 +123,23 @@ impl<T: ?Sized + 'static> FromContainer for Res<T> {
}
}
/// Callable must be implemented for functions that can be used as component
/// functions. They are given a ViewContext for the component function and
/// injectable arguments.
pub trait Callable<Args> {
fn call(&self, view: &mut ViewContext, args: Args);
}
impl<Func> Callable<()> for Func
where
Func: Fn(&mut ViewContext),
{
#[inline]
fn call(&self, view: &mut ViewContext, _args: ()) -> ArkhamResult {
fn call(&self, view: &mut ViewContext, _args: ()) {
(self)(view);
Ok(ArkhamState::Noop)
}
}
/// Callable must be implemented for functions that can be used as component
/// functions. They are given a ViewContext for the component function and
/// injectable arguments.
pub trait Callable<Args> {
fn call(&self, view: &mut ViewContext, args: Args) -> ArkhamResult;
}
/// FromContainer must be implmented for objects that can be injected into
/// component functions. This includes the Res and State structs.
pub trait FromContainer {
@ -172,9 +158,8 @@ macro_rules! callable_tuple ({ $($param:ident)* } => {
{
#[inline]
#[allow(non_snake_case)]
fn call(&self, view: &mut ViewContext , ($($param,)*): ($($param,)*)) -> ArkhamResult{
fn call(&self, view: &mut ViewContext , ($($param,)*): ($($param,)*)) {
(self)(view, $($param,)*);
Ok(ArkhamState::Noop)
}
}
});
@ -217,18 +202,3 @@ 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);
}
}

View File

@ -1,6 +1,9 @@
use std::{cell::RefCell, rc::Rc};
use crate::container::{Callable, FromContainer};
use crate::{
container::{Callable, FromContainer},
stack::Stack,
};
use super::{
container::Container,
@ -15,6 +18,7 @@ use super::{
pub struct ViewContext {
pub view: View,
pub container: Rc<RefCell<Container>>,
pub(crate) rerender: bool,
}
impl std::ops::DerefMut for ViewContext {
@ -38,7 +42,35 @@ impl ViewContext {
pub fn new(container: Rc<RefCell<Container>>, size: Size) -> Self {
let view = View::new(size);
Self { view, container }
Self {
view,
container,
rerender: false,
}
}
/// Notify the application to rerender the view. This is useful after a
/// state change that might affect other views.
pub fn render(&mut self) {
self.rerender = true;
}
pub fn vertical_stack(&self, size: Size) -> Stack {
Stack {
direction: crate::stack::StackDirection::Vertical,
container: self.container.clone(),
view: View::new(size),
position: Pos::from(0),
}
}
pub fn horizontal_stack(&self, size: Size) -> Stack {
Stack {
direction: crate::stack::StackDirection::Horizontal,
container: self.container.clone(),
view: View::new(size),
position: Pos::from(0),
}
}
/// Execute a component function. The passed function will receive a new
@ -53,8 +85,10 @@ impl ViewContext {
{
let rect = rect.into();
let mut context = ViewContext::new(self.container.clone(), rect.size);
self.container.borrow().call(&mut context, &f);
self.view.apply(rect.pos, context.view);
let args = Args::from_container(&self.container.borrow());
f.call(&mut context, args);
self.view.apply(rect.pos, &context.view);
self.rerender = context.rerender;
}
/// Set a specific rune to a specific position. This function can be used

View File

@ -1,4 +1,4 @@
use std::ops::{Add, Sub};
use std::ops::{Add, AddAssign, Sub};
/// Pos represents a coordinate position within the termianl screen.
#[derive(Debug, Clone, Copy)]
@ -7,6 +7,12 @@ pub struct Pos {
pub y: usize,
}
impl Pos {
pub fn new(x: usize, y: usize) -> Self {
Self { x, y }
}
}
impl From<(usize, usize)> for Pos {
fn from(value: (usize, usize)) -> Self {
Self {
@ -22,6 +28,23 @@ impl From<usize> for Pos {
}
}
impl Add<Pos> for Pos {
type Output = Pos;
fn add(mut self, rhs: Pos) -> Self::Output {
self.x += rhs.x;
self.y += rhs.y;
self
}
}
impl AddAssign<Pos> for Pos {
fn add_assign(&mut self, rhs: Pos) {
self.x += rhs.x;
self.y += rhs.y;
}
}
// An area that can be operated on.
#[derive(Debug, Clone, Copy)]
pub struct Size {
@ -29,6 +52,12 @@ pub struct Size {
pub height: usize,
}
impl Size {
pub fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
}
impl Add<Size> for Size {
type Output = Size;
@ -100,6 +129,15 @@ impl From<(i32, i32)> for Size {
}
}
impl From<i32> for Size {
fn from(value: i32) -> Self {
Self {
width: value as usize,
height: value as usize,
}
}
}
/// An area of the screen with a given size and postiion. The position
/// represents the top-left corner of the rectangle.
#[derive(Debug, Clone, Copy)]
@ -120,6 +158,10 @@ impl Rect {
}
}
pub fn zero() -> Self {
Self::new(0, 0)
}
pub fn with_size<S>(size: S) -> Self
where
S: Into<Size>,

View File

@ -1,9 +1,12 @@
mod app;
pub mod components;
mod container;
mod context;
mod geometry;
mod input;
mod plugins;
mod runes;
mod stack;
pub mod symbols;
mod theme;
mod view;

1
src/plugins/keybind.rs Normal file
View File

@ -0,0 +1 @@
pub struct KeybindPlugin;

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

@ -0,0 +1,13 @@
use crate::prelude::ViewContext;
pub mod keybind;
pub trait Pluigin {
fn build(ctx: ViewContext);
#[allow(unused_variables)]
fn before_render(ctx: ViewContext) {}
#[allow(unused_variables)]
fn after_render(ctx: ViewContext) {}
}

View File

@ -1,6 +1,8 @@
use crossterm::{
queue,
style::{Attribute, Color, Print, SetAttribute, SetBackgroundColor, SetForegroundColor},
style::{
Attribute, Color, Print, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor,
},
};
/// Rune repesents the state of the screen at a specific position. It stores
@ -20,6 +22,25 @@ impl std::fmt::Debug for Rune {
}
}
impl std::ops::Add<Rune> for Rune {
type Output = Rune;
fn add(self, mut rhs: Rune) -> Self::Output {
rhs.fg = rhs.fg.or(self.fg);
rhs.bg = rhs.bg.or(self.bg);
rhs
}
}
impl From<char> for Rune {
fn from(value: char) -> Self {
Rune {
content: Some(value),
..Default::default()
}
}
}
impl Rune {
pub fn new() -> Self {
Self::default()
@ -48,6 +69,7 @@ impl Rune {
W: std::io::Write,
{
if let Some(content) = self.content {
queue!(out, ResetColor)?;
if let Some(c) = self.fg {
queue!(out, SetForegroundColor(c))?;
}
@ -65,7 +87,7 @@ impl Rune {
/// Runes represetns a series of runes. This is generally used to convert
/// strings into Runes and apply styling information to them.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct Runes(Vec<Rune>);
impl std::ops::Deref for Runes {
@ -76,6 +98,12 @@ impl std::ops::Deref for Runes {
}
}
impl From<Rune> for Runes {
fn from(value: Rune) -> Self {
Runes::new(vec![value])
}
}
impl<T: ToString> From<T> for Runes {
fn from(value: T) -> Self {
Runes(
@ -89,12 +117,20 @@ impl<T: ToString> From<T> for Runes {
}
impl Runes {
pub fn fg(mut self, color: Color) -> Self {
pub fn new(runes: Vec<Rune>) -> Self {
Self(runes)
}
pub fn fg(self, color: Color) -> Self {
self.set_fg(Some(color))
}
pub fn set_fg(mut self, color: Option<Color>) -> Self {
for r in self.0.iter_mut() {
r.fg = Some(color);
r.fg = color;
}
self
}
pub fn bg(mut self, color: Color) -> Self {
for r in self.0.iter_mut() {
r.bg = Some(color);
@ -107,6 +143,13 @@ impl Runes {
}
self
}
pub fn add<R>(&mut self, runes: R)
where
R: Into<Runes>,
{
self.0.append(&mut runes.into().0);
}
}
pub trait ToRuneExt {

55
src/stack.rs Normal file
View File

@ -0,0 +1,55 @@
use std::{cell::RefCell, rc::Rc};
use crate::{
container::Container,
prelude::{Callable, Pos, Runes, Size, ViewContext},
view::View,
};
#[derive(Debug, Clone, Copy)]
pub enum StackDirection {
Vertical,
Horizontal,
}
#[derive(Debug)]
pub struct Stack {
pub(crate) direction: StackDirection,
pub(crate) container: Rc<RefCell<Container>>,
pub(crate) view: View,
pub(crate) position: Pos,
}
impl Stack {
pub fn component<F, Args, S>(&mut self, size: S, f: F)
where
F: crate::prelude::Callable<Args>,
Args: crate::prelude::FromContainer,
S: Into<Size>,
{
let size = size.into();
let mut context = ViewContext::new(self.container.clone(), size);
f.call(&mut context, Args::from_container(&self.container.borrow()));
self.view.apply(self.position, &context.view);
self.position += match self.direction {
StackDirection::Vertical => Pos::new(0, size.height),
StackDirection::Horizontal => Pos::new(size.width, 0),
};
}
pub fn insert<R: Into<Runes>>(&mut self, value: R) {
let runes: Runes = value.into();
let l = runes.len();
self.view.insert(self.position, runes);
self.position += match self.direction {
StackDirection::Vertical => Pos::new(0, 1),
StackDirection::Horizontal => Pos::new(l, 0),
};
}
}
impl Callable<()> for Stack {
fn call(&self, ctx: &mut ViewContext, _args: ()) {
ctx.apply((0, 0), &self.view);
}
}

View File

@ -8,7 +8,7 @@ use crate::{
/// A renderable region. View stores the renderable state of an area of the
/// screen. Views can be combined together to achieve a finalized view that
/// repsresents the entire screens next render.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct View(pub Vec<Vec<Rune>>);
impl std::ops::DerefMut for View {
@ -41,13 +41,14 @@ impl View {
}
/// Apply another view onto this view at a given position.
pub fn apply<P: Into<Pos>>(&mut self, pos: P, view: View) {
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);
let rune = (self.0[y + pos.y][x + pos.x]) + *rune;
let _ = std::mem::replace(&mut self.0[y + pos.y][x + pos.x], rune);
}
}
}
@ -108,7 +109,8 @@ impl View {
.take((line_len - x as i32).max(0) as usize)
.enumerate()
{
let _ = std::mem::replace(&mut line[x + i], *c);
let rune = line[x + i] + *c;
let _ = std::mem::replace(&mut line[x + i], rune);
}
}
}
@ -172,7 +174,7 @@ mod tests {
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);
view2.apply((0, 1), &view1);
dbg!(&view2.0);
assert_eq!(view2.0[0][0].content, None);
assert_eq!(view2.0[0][1].content, None);
@ -196,7 +198,7 @@ mod tests {
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);
view.apply((0, 0), &view0);
dbg!(&view.0);
assert_eq!(view.0[0][0].content, None);
assert_eq!(view.0[0][1].content, None);