diff --git a/output/src/out_impls.rs b/output/src/out_impls.rs index 38a051e..787d752 100644 --- a/output/src/out_impls.rs +++ b/output/src/out_impls.rs @@ -49,7 +49,7 @@ impl WH { } } impl XYWH { - fn zero (&self) -> Self { + fn zero () -> Self { Self(0.into(), 0.into(), 0.into(), 0.into()) } fn x2 (&self) -> N { @@ -250,8 +250,13 @@ impl Measure { } } -impl From<[O::Unit; 2]> for Measure { - fn from ([x, y]: [O::Unit; 2]) -> Self { Self::new(x, y) } +/// FIXME don't convert to u16 specifically +impl HasWH for Measure { + fn w (&self) -> O::Unit { (self.x.load(Relaxed) as u16).into() } + fn h (&self) -> O::Unit { (self.y.load(Relaxed) as u16).into() } +} +impl From> for Measure { + fn from (WH(x, y): WH) -> Self { Self::new(x, y) } } impl Layout for () { @@ -460,10 +465,10 @@ layout_op_xy!(1 opt: Expand); impl> Layout for Expand { fn layout_w (&self, to: XYWH) -> O::Unit { - self.inner().w(to).plus(self.dx().unwrap_or_default()) + self.inner().layout_w(to).plus(self.dx().unwrap_or_default()) } fn layout_h (&self, to: XYWH) -> O::Unit { - self.inner().w(to).plus(self.dy().unwrap_or_default()) + self.inner().layout_w(to).plus(self.dy().unwrap_or_default()) } } @@ -703,8 +708,8 @@ impl<'a, O, A, B, I, F, G> Layout for Map where fn layout (&self, area: XYWH) -> XYWH { let Self { get_iter, get_item, .. } = self; let mut index = 0; - let [mut min_x, mut min_y] = area.center(); - let [mut max_x, mut max_y] = area.center(); + let XY(mut min_x, mut min_y) = area.centered(); + let XY(mut max_x, mut max_y) = area.center(); for item in get_iter() { let XYWH(x, y, w, h) = get_item(item, index).layout(area); min_x = min_x.min(x); diff --git a/output/src/out_structs.rs b/output/src/out_structs.rs index b3b14bb..c267a29 100644 --- a/output/src/out_structs.rs +++ b/output/src/out_structs.rs @@ -6,7 +6,7 @@ use crate::*; /// let xy: XY = XY(0, 0); /// ``` #[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone)] pub struct XY(pub C, pub C); +#[derive(Copy, Clone, Default)] pub struct XY(pub C, pub C); /// A size (Width, Height). /// @@ -14,7 +14,7 @@ use crate::*; /// let wh: WH = WH(0, 0); /// ``` #[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone)] pub struct WH(pub C, pub C); +#[derive(Copy, Clone, Default)] pub struct WH(pub C, pub C); /// Point with size. /// @@ -25,7 +25,7 @@ use crate::*; /// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0) /// #[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone)] pub struct XYWH(pub C, pub C, pub C, pub C); +#[derive(Copy, Clone, Default)] pub struct XYWH(pub C, pub C, pub C, pub C); /// A cardinal direction. /// @@ -53,7 +53,7 @@ use crate::*; /// let measure = Measure::default(); /// ``` #[derive(Default)] pub struct Measure { - __: PhantomData, + pub __: PhantomData, pub x: Arc, pub y: Arc, } diff --git a/output/src/out_traits.rs b/output/src/out_traits.rs index 3ce8211..127d1ae 100644 --- a/output/src/out_traits.rs +++ b/output/src/out_traits.rs @@ -41,6 +41,32 @@ pub trait Out: Send + Sync + Sized { } } +/// A numeric type that can be used as coordinate. +/// +/// FIXME: Replace this ad-hoc trait with `num` crate. +pub trait Coord: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{ + fn plus (self, other: Self) -> Self; + fn minus (self, other: Self) -> Self { + if self >= other { self - other } else { 0.into() } + } + fn atomic (self) -> AtomicUsize { + AtomicUsize::new(self.into()) + } + fn zero () -> Self { + 0.into() + } +} + /// Drawable with dynamic dispatch. pub trait Draw { fn draw (&self, to: &mut O); @@ -93,32 +119,6 @@ pub trait HasContent { // TODO DOCUMENTME pub trait Content: Draw + Layout {} -/// A numeric type that can be used as coordinate. -/// -/// FIXME: Replace this ad-hoc trait with `num` crate. -pub trait Coord: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into -{ - fn plus (self, other: Self) -> Self; - fn minus (self, other: Self) -> Self { - if self >= other { self - other } else { 0.into() } - } - fn atomic (self) -> AtomicUsize { - AtomicUsize::new(self.into()) - } - fn zero () -> Self { - 0.into() - } -} - // Something that has an origin point (X, Y). pub trait HasXY { fn x (&self) -> N; diff --git a/tui/src/tui_content/_tui_focus.rs b/tui/src/.scratch.rs similarity index 68% rename from tui/src/tui_content/_tui_focus.rs rename to tui/src/.scratch.rs index 41b0f33..a6ee28e 100644 --- a/tui/src/tui_content/_tui_focus.rs +++ b/tui/src/.scratch.rs @@ -304,3 +304,131 @@ pub fn to_focus_command (input: &TuiIn) -> Option> { + pub menus: Vec>, + pub index: usize, +} +impl> MenuBar { + pub fn new () -> Self { Self { menus: vec![], index: 0 } } + pub fn add (mut self, menu: Menu) -> Self { + self.menus.push(menu); + self + } +} +pub struct Menu> { + pub title: Arc, + pub items: Vec>, + pub index: Option, +} +impl> Menu { + pub fn new (title: impl AsRef) -> Self { + Self { + title: title.as_ref().to_string(), + items: vec![], + index: None, + } + } + pub fn add (mut self, item: MenuItem) -> Self { + self.items.push(item); + self + } + pub fn sep (mut self) -> Self { + self.items.push(MenuItem::sep()); + self + } + pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self { + self.items.push(MenuItem::cmd(hotkey, text, command)); + self + } + pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self { + self.items.push(MenuItem::off(hotkey, text)); + self + } +} +pub enum MenuItem> { + /// Unused. + __(PhantomData, PhantomData), + /// A separator. Skip it. + Separator, + /// A menu item with command, description and hotkey. + Command(&'static str, &'static str, C), + /// A menu item that can't be activated but has description and hotkey + Disabled(&'static str, &'static str) +} +impl> MenuItem { + pub fn sep () -> Self { + Self::Separator + } + pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self { + Self::Command(hotkey, text, command) + } + pub fn off (hotkey: &'static str, text: &'static str) -> Self { + Self::Disabled(hotkey, text) + } +} + +//impl> Content for Result> { + //fn content (&self) -> impl Draw + '_ { + //Bsp::a(self.as_ref().ok(), self.as_ref().err().map( + //|e|Tui::fg_bg(Color::Rgb(255,255,255), Color::Rgb(32,32,32), e.to_string()) + //)) + //} +//} + +//impl> Draw for Result> { + //fn layout (&self, to: [u16;4]) -> [u16;4] { + //match self { + //Ok(content) => content.layout(to), + //Err(e) => [0, 0, to.w(), to.h()] + //} + //} + //fn draw (&self, to: &mut TuiOut) { + //match self { + //Ok(content) => content.draw(to), + //Err(e) => to.blit(&e.to_string(), 0, 0, Some(Style::default() + //.bg(Color::Rgb(32,32,32)) + //.fg(Color::Rgb(255,255,255)))) + //} + //} +//} + + //let token = token.as_ref(); + //if token.len() < 2 { + //Self { valid: false, key: None, mods: KeyModifiers::NONE } + //} else if token.chars().next() != Some('@') { + //Self { valid: false, key: None, mods: KeyModifiers::NONE } + //} else { + //Self { valid: true, key: None, mods: KeyModifiers::NONE }.next(&token[1..]) + //} + //} + //pub fn build (self) -> Option { + //if self.valid && self.key.is_some() { + //Some(Event::Key(KeyEvent::new(self.key.unwrap(), self.mods))) + //} else { + //None + //} + //} + //fn next (mut self, token: &str) -> Self { + //let mut tokens = token.split('-').peekable(); + //while let Some(token) = tokens.next() { + //if tokens.peek().is_some() { + //match token { + //"ctrl" | "Ctrl" | "c" | "C" => self.mods |= KeyModifiers::CONTROL, + //"alt" | "Alt" | "m" | "M" => self.mods |= KeyModifiers::ALT, + //"shift" | "Shift" | "s" | "S" => { + //self.mods |= KeyModifiers::SHIFT; + //// + TODO normalize character case, BackTab, etc. + //}, + //_ => panic!("unknown modifier {token}"), + //} + //} else { + //self.key = if token.len() == 1 { + //Some(KeyCode::Char(token.chars().next().unwrap())) + //} else { + //Some(Self::named_key(token).unwrap_or_else(||panic!("unknown character {token}"))) + //} + //} + //} + //self + //} diff --git a/tui/src/lib.rs b/tui/src/lib.rs index d9e16c6..976d230 100644 --- a/tui/src/lib.rs +++ b/tui/src/lib.rs @@ -1,4 +1,9 @@ #![feature(type_changing_struct_update, trait_alias)] + +use std::{time::Duration, thread::{spawn, JoinHandle}}; + +use unicode_width::*; + pub use ::{ dizzle, tengri_input, @@ -8,6 +13,7 @@ pub use ::{ palette, better_panic, }; + pub(crate) use ::{ dizzle::*, tengri_input::*, @@ -26,11 +32,10 @@ pub(crate) use ::{ crossterm::{ ExecutableCommand, terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, - event::{Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, + event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, } }; -mod tui_engine; pub use self::tui_engine::*; -mod tui_content; pub use self::tui_content::*; + #[macro_export] macro_rules! tui_main { ($expr:expr) => { fn main () -> Usually<()> { @@ -41,6 +46,256 @@ mod tui_content; pub use self::tui_content::*; }; } +#[macro_export] macro_rules! has_color { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? { + fn color (&$self) -> ItemColor { $cb } + } + } +} + +macro_rules! border { + ($($T:ident { + $nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal + $($x:tt)* + }),+) => {$( + impl BorderStyle for $T { + const NW: &'static str = $nw; + const N: &'static str = $n; + const NE: &'static str = $ne; + const W: &'static str = $w; + const E: &'static str = $e; + const SW: &'static str = $sw; + const S: &'static str = $s; + const SE: &'static str = $se; + $($x)* + fn enabled (&self) -> bool { self.0 } + } + #[derive(Copy, Clone)] pub struct $T(pub bool, pub Style); + impl Layout for $T {} + impl Draw for $T { + fn draw (&self, to: &mut TuiOut) { + if self.enabled() { let _ = BorderStyle::draw(self, to); } + } + } + )+} +} + +mod tui_structs; pub use self::tui_structs::*; +mod tui_traits; pub use self::tui_traits::*; +mod tui_impls; pub use self::tui_impls::*; + +#[cfg(feature = "dsl")] +pub fn evaluate_output_expression_tui <'a, S> ( + state: &S, output: &mut TuiOut, expr: impl Expression + 'a +) -> Usually where + S: View + + for<'b>Namespace<'b, bool> + + for<'b>Namespace<'b, u16> + + for<'b>Namespace<'b, Color> +{ + // See `tengri_output::evaluate_output_expression` + let head = expr.head()?; + let mut frags = head.src()?.unwrap_or_default().split("/"); + let args = expr.tail(); + let arg0 = args.head(); + let tail0 = args.tail(); + let arg1 = tail0.head(); + let tail1 = tail0.tail(); + let _arg2 = tail1.head(); + match frags.next() { + + Some("text") => if let Some(src) = args?.src()? { output.place(&src) }, + + Some("fg") => { + let arg0 = arg0?.expect("fg: expected arg 0 (color)"); + output.place(&Tui::fg( + Namespace::::resolve(state, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")), + Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()), + )) + }, + + Some("bg") => { + let arg0 = arg0?.expect("bg: expected arg 0 (color)"); + output.place(&Tui::bg( + Namespace::::resolve(state, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")), + Thunk::new(move|output: &mut TuiOut|state.view(output, &arg1).unwrap()), + )) + }, + + _ => return Ok(false) + + }; + Ok(true) +} + +pub fn named_key (token: &str) -> Option { + use KeyCode::*; + Some(match token { + "up" => Up, + "down" => Down, + "left" => Left, + "right" => Right, + "esc" | "escape" => Esc, + "enter" | "return" => Enter, + "delete" | "del" => Delete, + "backspace" => Backspace, + "tab" => Tab, + "space" => Char(' '), + "comma" => Char(','), + "period" => Char('.'), + "plus" => Char('+'), + "minus" | "dash" => Char('-'), + "equal" | "equals" => Char('='), + "underscore" => Char('_'), + "backtick" => Char('`'), + "lt" => Char('<'), + "gt" => Char('>'), + "cbopen" | "openbrace" => Char('{'), + "cbclose" | "closebrace" => Char('}'), + "bropen" | "openbracket" => Char('['), + "brclose" | "closebracket" => Char(']'), + "pgup" | "pageup" => PageUp, + "pgdn" | "pagedown" => PageDown, + "f1" => F(1), + "f2" => F(2), + "f3" => F(3), + "f4" => F(4), + "f5" => F(5), + "f6" => F(6), + "f7" => F(7), + "f8" => F(8), + "f9" => F(9), + "f10" => F(10), + "f11" => F(11), + "f12" => F(12), + _ => return None, + }) +} + +pub fn button_2 <'a> (key: impl Content, label: impl Content, editing: bool) -> impl Content { + Tui::bold(true, Bsp::e( + Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(Tui::g(96), &"▐")))), + When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)))) +} + +pub fn button_3 <'a> ( + key: impl Content, label: impl Content, value: impl Content, editing: bool, +) -> impl Content { + Tui::bold(true, Bsp::e( + Tui::fg_bg(Tui::orange(), Tui::g(0), + Bsp::e(Tui::fg(Tui::g(0), &"▐"), Bsp::e(key, Tui::fg(if editing { Tui::g(128) } else { Tui::g(96) }, "▐")))), + Bsp::e( + When::new(!editing, Bsp::e(Tui::fg_bg(Tui::g(255), Tui::g(96), label), Tui::fg_bg(Tui::g(128), Tui::g(96), &"▐"),)), + Bsp::e(Tui::fg_bg(Tui::g(224), Tui::g(128), value), Tui::fg_bg(Tui::g(128), Reset, &"▌"), )))) +} + +border! { + Square { + "┌" "─" "┐" + "│" "│" + "└" "─" "┘" fn style (&self) -> Option