Merge pull request #3 from selimaj-dev/layout-and-outlines
Layout and outlines
This commit is contained in:
+30
-14
@@ -4,20 +4,22 @@ use crate::unit::{Rect, Size};
|
||||
pub enum LayoutItem {
|
||||
Rows { unit: Size, items: Vec<LayoutItem> },
|
||||
Columns { unit: Size, items: Vec<LayoutItem> },
|
||||
|
||||
Frame { padding: u16, item: Box<LayoutItem> },
|
||||
Widget(Size),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Allocation {
|
||||
pub widget_alloc: Vec<Rect>,
|
||||
pub frame_alloc: Vec<Rect>,
|
||||
pub widgets: Vec<Rect>,
|
||||
pub frame: Vec<Rect>,
|
||||
}
|
||||
|
||||
impl LayoutItem {
|
||||
pub fn allocate(&self, alloc: &Rect, frame: &mut Vec<Rect>, widgets: &mut Vec<Rect>) {
|
||||
pub fn allocate(&self, alloc: &Rect, full_alloc: &mut Allocation) {
|
||||
match self {
|
||||
Self::Rows { items, .. } | Self::Columns { items, .. } => {
|
||||
frame.push(*alloc);
|
||||
full_alloc.frame.push(*alloc);
|
||||
|
||||
let is_row = matches!(self, Self::Rows { .. });
|
||||
|
||||
@@ -59,12 +61,26 @@ impl LayoutItem {
|
||||
|
||||
curr += unit_alloc;
|
||||
|
||||
item.allocate(&item_alloc, frame, widgets);
|
||||
item.allocate(&item_alloc, full_alloc);
|
||||
}
|
||||
}
|
||||
|
||||
Self::Frame { padding, item } => {
|
||||
full_alloc.frame.push(*alloc);
|
||||
|
||||
let mut item_alloc = alloc.clone();
|
||||
|
||||
item_alloc.width -= padding * 2;
|
||||
item_alloc.height -= padding * 2;
|
||||
|
||||
item_alloc.x += padding;
|
||||
item_alloc.y += padding;
|
||||
|
||||
item.allocate(&item_alloc, full_alloc);
|
||||
}
|
||||
|
||||
Self::Widget(_) => {
|
||||
widgets.push(*alloc);
|
||||
full_alloc.widgets.push(*alloc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,26 +90,26 @@ impl LayoutItem {
|
||||
Self::Rows { unit, .. } => unit,
|
||||
Self::Columns { unit, .. } => unit,
|
||||
Self::Widget(unit) => unit,
|
||||
Self::Frame { item, .. } => item.get_size(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn allocate(&self, layout: &LayoutItem) -> Allocation {
|
||||
let mut widget_alloc = Vec::new();
|
||||
let mut frame_alloc = Vec::new();
|
||||
let mut alloc = Allocation {
|
||||
widgets: Vec::new(),
|
||||
frame: Vec::new(),
|
||||
};
|
||||
|
||||
layout.allocate(self, &mut frame_alloc, &mut widget_alloc);
|
||||
layout.allocate(self, &mut alloc);
|
||||
|
||||
Allocation {
|
||||
widget_alloc,
|
||||
frame_alloc,
|
||||
}
|
||||
alloc
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout(rows: Vec<LayoutItem>) -> LayoutItem {
|
||||
LayoutItem::Rows {
|
||||
LayoutItem::Columns {
|
||||
unit: Size::Percent(100),
|
||||
items: rows,
|
||||
}
|
||||
|
||||
+43
-16
@@ -11,48 +11,59 @@ pub mod widget;
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait App {
|
||||
// State
|
||||
async fn init(&mut self, ctx: state::Context);
|
||||
async fn update(&mut self, event: Box<dyn Any>);
|
||||
async fn init(&mut self, ctx: &state::Context);
|
||||
async fn update(&mut self, ctx: &state::Context, event: Box<dyn Any + Send + Sync>);
|
||||
|
||||
// Rendering
|
||||
async fn layout(&self) -> layout::LayoutItem;
|
||||
async fn render(&mut self, allocation: layout::Allocation);
|
||||
|
||||
async fn refresh(&mut self) {
|
||||
crossterm::execute!(
|
||||
std::io::stdout(),
|
||||
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
crossterm::execute!(
|
||||
std::io::stdout(),
|
||||
crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge)
|
||||
)
|
||||
.unwrap();
|
||||
clear();
|
||||
|
||||
crossterm::execute!(std::io::stdout(), crossterm::cursor::Hide).unwrap();
|
||||
|
||||
self.render(get_screen().allocate(&self.layout().await))
|
||||
.await;
|
||||
|
||||
crossterm::execute!(std::io::stdout()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run<A: App>(create_app: impl FnOnce(&state::Context) -> A) {
|
||||
let (tx, mut rx) = mpsc::channel(100);
|
||||
|
||||
let ctx = state::Context { tx };
|
||||
let ctx = state::Context { tx: tx.clone() };
|
||||
|
||||
crossterm::terminal::enable_raw_mode().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tx.send(Box::new(crossterm::event::read().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let mut app = create_app(&ctx);
|
||||
|
||||
app.init(ctx).await;
|
||||
app.init(&ctx).await;
|
||||
|
||||
app.refresh().await;
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
app.update(event).await;
|
||||
if let Some(state::Close) = event.downcast_ref() {
|
||||
break;
|
||||
}
|
||||
|
||||
app.update(&ctx, event).await;
|
||||
app.refresh().await;
|
||||
}
|
||||
|
||||
crossterm::terminal::disable_raw_mode().unwrap();
|
||||
crossterm::execute!(std::io::stdout(), crossterm::cursor::Show).unwrap();
|
||||
|
||||
clear();
|
||||
}
|
||||
|
||||
pub fn get_screen() -> unit::Rect {
|
||||
@@ -65,3 +76,19 @@ pub fn get_screen() -> unit::Rect {
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear() {
|
||||
crossterm::execute!(
|
||||
std::io::stdout(),
|
||||
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
crossterm::execute!(
|
||||
std::io::stdout(),
|
||||
crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge)
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
crossterm::execute!(std::io::stdout(), crossterm::cursor::MoveTo(0, 0)).unwrap();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::{
|
||||
|
||||
pub enum Instr {
|
||||
DrawText(Point, String),
|
||||
DrawOutline(Rect),
|
||||
}
|
||||
|
||||
pub struct RenderScope {
|
||||
@@ -21,10 +20,6 @@ impl RenderScope {
|
||||
self.draw_instructions
|
||||
.push(Instr::DrawText(at.into(), text.to_string()));
|
||||
}
|
||||
|
||||
pub fn draw_outline<P: Into<Point>>(&mut self, rect: Rect) {
|
||||
self.draw_instructions.push(Instr::DrawOutline(rect));
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderScope {
|
||||
@@ -62,7 +57,13 @@ impl From<Rect> for Point {
|
||||
|
||||
impl Allocation {
|
||||
pub fn draw<W: Widget>(&self, index: usize, widget: W) {
|
||||
let mut scope = RenderScope::new(self.widget_alloc[index]);
|
||||
let mut scope = RenderScope::new(self.widgets[index]);
|
||||
widget.render(&mut scope);
|
||||
scope.draw();
|
||||
}
|
||||
|
||||
pub fn draw_frame<W: Widget>(&self, index: usize, widget: W) {
|
||||
let mut scope = RenderScope::new(self.frame[index]);
|
||||
widget.render(&mut scope);
|
||||
scope.draw();
|
||||
}
|
||||
@@ -81,7 +82,6 @@ impl RenderScope {
|
||||
);
|
||||
}
|
||||
}
|
||||
Instr::DrawOutline(rect) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,5 +89,5 @@ impl RenderScope {
|
||||
|
||||
pub fn print_at(x: u16, y: u16, text: &str) {
|
||||
crossterm::execute!(stdout(), crossterm::cursor::MoveTo(x, y)).unwrap();
|
||||
println!("{text}");
|
||||
print!("{text}");
|
||||
}
|
||||
|
||||
+20
-5
@@ -3,25 +3,26 @@ use std::{any::Any, sync::Arc};
|
||||
use tokio::sync::{Mutex, MutexGuard, mpsc::Sender};
|
||||
|
||||
pub struct Refresh;
|
||||
pub struct Close;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub(crate) tx: Sender<Box<dyn Any>>,
|
||||
pub(crate) tx: Sender<Box<dyn Any + Send + Sync>>,
|
||||
}
|
||||
|
||||
pub struct State<T> {
|
||||
pub value: Arc<Mutex<T>>,
|
||||
tx: Sender<Box<dyn Any>>,
|
||||
tx: Sender<Box<dyn Any + Send + Sync>>,
|
||||
}
|
||||
|
||||
pub struct StateGuard<'a, T> {
|
||||
is_mutated: bool,
|
||||
value: MutexGuard<'a, T>,
|
||||
tx: &'a Sender<Box<dyn Any>>,
|
||||
tx: &'a Sender<Box<dyn Any + Send + Sync>>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn use_state<T>(&self, v: T) -> State<T> {
|
||||
pub fn use_state<T: Send + Sync>(&self, v: T) -> State<T> {
|
||||
State {
|
||||
value: Arc::new(Mutex::new(v)),
|
||||
tx: self.tx.clone(),
|
||||
@@ -39,6 +40,12 @@ impl<T> State<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Display> State<T> {
|
||||
pub async fn display(&self) -> String {
|
||||
self.lock().await.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> std::ops::Deref for StateGuard<'a, T> {
|
||||
type Target = T;
|
||||
|
||||
@@ -58,7 +65,15 @@ impl<'a, T> std::ops::DerefMut for StateGuard<'a, T> {
|
||||
impl<'a, T> Drop for StateGuard<'a, T> {
|
||||
fn drop(&mut self) {
|
||||
if self.is_mutated {
|
||||
self.tx.blocking_send(Box::new(Refresh)).unwrap()
|
||||
let tx = self.tx.clone();
|
||||
|
||||
tokio::spawn(async move { tx.send(Box::new(Refresh)).await.unwrap() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub async fn close(&self) {
|
||||
self.tx.send(Box::new(Close)).await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,39 @@
|
||||
pub mod outline;
|
||||
pub mod spaced;
|
||||
|
||||
use crate::render::RenderScope;
|
||||
|
||||
pub trait Widget {
|
||||
fn render(&self, scope: &mut RenderScope);
|
||||
}
|
||||
|
||||
impl Widget for &str {
|
||||
macro_rules! display_widget {
|
||||
($t:ty) => {
|
||||
impl Widget for $t {
|
||||
fn render(&self, scope: &mut RenderScope) {
|
||||
scope.draw_text(0, self);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
display_widget!(&str);
|
||||
display_widget!(String);
|
||||
display_widget!(i8);
|
||||
display_widget!(i16);
|
||||
display_widget!(i32);
|
||||
display_widget!(i64);
|
||||
display_widget!(i128);
|
||||
display_widget!(u8);
|
||||
display_widget!(u16);
|
||||
display_widget!(u32);
|
||||
display_widget!(u64);
|
||||
display_widget!(u128);
|
||||
display_widget!(f32);
|
||||
display_widget!(f64);
|
||||
|
||||
impl Widget for &dyn Widget {
|
||||
fn render(&self, scope: &mut RenderScope) {
|
||||
scope.draw_text(0, self);
|
||||
(*self).render(scope);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::widget::Widget;
|
||||
|
||||
pub struct Outline;
|
||||
|
||||
impl Widget for Outline {
|
||||
fn render(&self, scope: &mut crate::render::RenderScope) {
|
||||
scope.draw_text(0, "─".repeat(scope.rect.width as usize));
|
||||
|
||||
scope.draw_text((0, 1), "│\n".repeat(scope.rect.height as usize - 2));
|
||||
scope.draw_text(
|
||||
(scope.rect.width - 1, 1),
|
||||
"│\n".repeat(scope.rect.height as usize - 2),
|
||||
);
|
||||
|
||||
scope.draw_text(
|
||||
(0, scope.rect.height - 1),
|
||||
"─".repeat(scope.rect.width as usize),
|
||||
);
|
||||
|
||||
scope.draw_text(0, "┌");
|
||||
scope.draw_text((scope.rect.width - 1, 0), "┐");
|
||||
scope.draw_text((0, scope.rect.height - 1), "└");
|
||||
scope.draw_text((scope.rect.width - 1, scope.rect.height - 1), "┘");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use crate::{layout::LayoutItem, unit::Size::Percent, widget::Widget};
|
||||
|
||||
pub struct SpacedRows(pub Vec<(LayoutItem, Box<dyn Widget>)>);
|
||||
|
||||
impl Widget for SpacedRows {
|
||||
fn render(&self, scope: &mut crate::render::RenderScope) {
|
||||
let alloc = scope.rect.allocate(&crate::layout::LayoutItem::Rows {
|
||||
unit: Percent(100),
|
||||
items: self.0.iter().map(|v| v.0.clone()).collect(),
|
||||
});
|
||||
|
||||
for i in 0..alloc.widgets.len() {
|
||||
let item = &self.0[i];
|
||||
|
||||
if i > 0 {
|
||||
let rect = &alloc.widgets[i];
|
||||
|
||||
scope.draw_text((0, rect.y - 2), "─".repeat(rect.width as usize));
|
||||
}
|
||||
|
||||
alloc.draw(i, &*item.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SpacedColumns(pub Vec<(LayoutItem, Box<dyn Widget>)>);
|
||||
|
||||
impl Widget for SpacedColumns {
|
||||
fn render(&self, scope: &mut crate::render::RenderScope) {
|
||||
let mut alloc = scope.rect.allocate(&crate::layout::LayoutItem::Columns {
|
||||
unit: Percent(100),
|
||||
items: self.0.iter().map(|v| v.0.clone()).collect(),
|
||||
});
|
||||
|
||||
for i in 0..alloc.widgets.len() {
|
||||
let item = &self.0[i];
|
||||
|
||||
if i > 0 {
|
||||
let rect = &mut alloc.widgets[i];
|
||||
|
||||
for i in 0..rect.height {
|
||||
scope.draw_text((rect.x - 1, i), "│");
|
||||
}
|
||||
rect.x += 1;
|
||||
}
|
||||
|
||||
alloc.draw(i, &*item.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-8
@@ -1,8 +1,11 @@
|
||||
use std::any::Any;
|
||||
|
||||
use pulse_ui::{
|
||||
App,
|
||||
layout::{LayoutItem, layout},
|
||||
state::State,
|
||||
state::{Refresh, State},
|
||||
unit::Size,
|
||||
widget::{outline::Outline, spaced::SpacedRows},
|
||||
};
|
||||
|
||||
pub struct PulseTradeApp {
|
||||
@@ -10,19 +13,35 @@ pub struct PulseTradeApp {
|
||||
}
|
||||
|
||||
impl App for PulseTradeApp {
|
||||
async fn init(&mut self, ctx: pulse_ui::state::Context) {}
|
||||
async fn init(&mut self, ctx: &pulse_ui::state::Context) {}
|
||||
|
||||
async fn update(&mut self, event: Box<dyn std::any::Any>) {}
|
||||
async fn update(&mut self, ctx: &pulse_ui::state::Context, event: Box<dyn Any + Send + Sync>) {
|
||||
if let Some(Refresh) = event.downcast_ref() {
|
||||
return;
|
||||
}
|
||||
|
||||
*self.count.lock().await += 1;
|
||||
}
|
||||
|
||||
async fn layout(&self) -> pulse_ui::layout::LayoutItem {
|
||||
layout(vec![
|
||||
LayoutItem::Widget(Size::Flex(1)),
|
||||
LayoutItem::Widget(Size::Flex(1)),
|
||||
])
|
||||
layout(vec![LayoutItem::Frame {
|
||||
padding: 1,
|
||||
item: Box::new(LayoutItem::Widget(Size::Flex(1))),
|
||||
}])
|
||||
}
|
||||
|
||||
async fn render(&mut self, layout: pulse_ui::layout::Allocation) {
|
||||
layout.draw(1, "Yo");
|
||||
layout.draw_frame(0, Outline);
|
||||
|
||||
layout.draw(
|
||||
0,
|
||||
SpacedRows(vec![
|
||||
(LayoutItem::Widget(Size::Flex(1)), Box::new("one")),
|
||||
(LayoutItem::Widget(Size::Flex(1)), Box::new("two")),
|
||||
(LayoutItem::Widget(Size::Flex(1)), Box::new("three")),
|
||||
(LayoutItem::Widget(Size::Flex(1)), Box::new(self.count.display().await)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user