Merge pull request #3 from selimaj-dev/layout-and-outlines

Layout and outlines
This commit is contained in:
2026-07-15 01:44:32 +02:00
committed by GitHub
8 changed files with 233 additions and 53 deletions
+30 -14
View File
@@ -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
View File
@@ -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 -8
View File
@@ -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
View File
@@ -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()
}
}
+29 -1
View File
@@ -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) {
(*self).render(scope);
}
}
+25
View File
@@ -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), "┘");
}
}
+50
View File
@@ -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
View File
@@ -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)),
]),
);
}
}