diff --git a/Cargo.toml b/Cargo.toml index e442ec2..b0fd6fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,9 @@ version = "0.15.0" description = "UI metaframework." [features] -default = ["lang", "sing", "draw", "play", "tui", "text", "time", "rand", "okhsl"] +default = ["lang", "sing", "draw", "play", "term", "text", "time", "rand", "okhsl"] bumpalo = ["dep:bumpalo"] draw = [] -dsl = ["dep:dizzle"] gui = ["draw", "dep:winit"] lang = ["dep:dizzle"] okhsl = ["dep:palette"] @@ -17,7 +16,7 @@ rand = ["dep:rand"] sing = ["dep:jack"] text = ["dep:unicode-width"] time = ["dep:quanta"] -tui = ["draw", "dep:ratatui", "dep:crossterm"] +term = ["draw", "dep:ratatui", "dep:crossterm"] [dependencies] anyhow = { version = "1.0" } diff --git a/dizzle b/dizzle index 068e26d..44b2be5 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit 068e26dd50699e51e9db01ac23fc0778074647bd +Subproject commit 44b2be57ca8d1f69a95f2eb02f4b5474ec77ac0a diff --git a/src/.scratch.rs b/src/.scratch.rs index beb03ea..1b68884 100644 --- a/src/.scratch.rs +++ b/src/.scratch.rs @@ -1359,3 +1359,105 @@ impl> MenuItem { ////} ////} ////} + +////impl> TuiOut for T { fn tui_out (&mut self) -> &mut Buffer { self.as_mut() } } + +//impl Tui { + ///// True if done + //pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) } + ///// Prepare before run + //pub fn setup (&self) -> Usually<()> { tui_setup(&mut*self.backend.write().unwrap()) } + ///// Clean up after run + //pub fn teardown (&self) -> Usually<()> { tui_teardown(&mut*self.backend.write().unwrap()) } + ///// Apply changes to the display buffer. + //pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { tui_flip(self, self.buffer, buffer, size) } + ///// Create the engine. + //pub fn new (output: Box) -> Usually { + //let backend = CrosstermBackend::new(output); + //let Size { width, height } = backend.size()?; + //Ok(Self { + //exited: Arc::new(AtomicBool::new(false)), + //buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }).into(), + //area: XYWH(0, 0, width, height), + //perf: Default::default(), + //backend: backend.into(), + //event: None, + //error: None, + //}) + //} + ///// Run an app in the engine. + //pub fn run (mut self, join: bool, state: &Arc>) -> Usually> where + //T: Act + Draw + Send + Sync + 'static + //{ + //self.setup()?; + //let tui = Arc::new(self); + //let input_poll = Duration::from_millis(100); + //let output_sleep = Duration::from_millis(10); // == 1/MAXFPS (?) + //let _input_thread = tui_input(&tui, state, input_poll)?; + //let render_thread = tui_output(&tui, state, output_sleep)?; + //if join { // Run until render thread ends: + //let result = render_thread.join(); + //tui.teardown()?; + //match result { + //Ok(result) => println!("\n\rRan successfully: {result:?}\n\r"), + //Err(error) => panic!("\n\rDraw thread failed: error={error:?}.\n\r"), + //} + //} + //Ok(tui) + //} +//} + +//impl Layout for &str { + //fn layout (&self, to: XYWH) -> XYWH { + //to.centered_xy([width_chars_max(to.w(), self), 1]) + //} +//} + +//impl Layout for String { + //fn layout (&self, to: XYWH) -> XYWH { + //self.as_str().layout(to) + //} +//} + +//impl Layout for Arc { + //fn layout (&self, to: XYWH) -> XYWH { + //self.as_ref().layout(to) + //} +//} + +//impl<'a, T: AsRef> Layout for TrimString { + //fn layout (&self, to: XYWH) -> XYWH { + //Layout::layout(&self.as_ref(), to) + //} +//} + +//impl<'a, T: AsRef> Layout for TrimStringRef<'a, T> { + //fn layout (&self, to: XYWH) -> XYWH { + //XYWH(to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()) + //} +//} + +///// TUI helper defs. +//impl Tui { + ////pub const fn null () -> Color { Color::Reset } + ////pub const fn red () -> Color { Color::Rgb(255,0, 0) } + ////pub const fn orange () -> Color { Color::Rgb(255,128,0) } + ////pub const fn yellow () -> Color { Color::Rgb(255,255,0) } + ////pub const fn brown () -> Color { Color::Rgb(128,255,0) } + ////pub const fn green () -> Color { Color::Rgb(0,255,0) } + ////pub const fn electric () -> Color { Color::Rgb(0,255,128) } + ////pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) } + ////fn bg0 () -> Color { Color::Rgb(20, 20, 20) } + ////fn bg () -> Color { Color::Rgb(28, 35, 25) } + ////fn border_bg () -> Color { Color::Rgb(40, 50, 30) } + ////fn border_fg (f: bool) -> Color { if f { Self::bo1() } else { Self::bo2() } } + ////fn title_fg (f: bool) -> Color { if f { Self::ti1() } else { Self::ti2() } } + ////fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) } + ////fn mode_bg () -> Color { Color::Rgb(150, 160, 90) } + ////fn mode_fg () -> Color { Color::Rgb(255, 255, 255) } + ////fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) } + ////fn bo1 () -> Color { Color::Rgb(100, 110, 40) } + ////fn bo2 () -> Color { Color::Rgb(70, 80, 50) } + ////fn ti1 () -> Color { Color::Rgb(150, 160, 90) } + ////fn ti2 () -> Color { Color::Rgb(120, 130, 100) } +//} diff --git a/src/color.rs b/src/color.rs index 3f0d48a..cab02c2 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,11 +1,23 @@ -pub(crate) use ::palette::Okhsl; +use ::ratatui::style::Color; +use crate::lang::impl_from; +pub(crate) use ::palette::{Okhsl, Srgb, OklabHue, okhsl::UniformOkhsl}; + +pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor { todo!(); } + +pub fn okhsl_to_rgb (color: Okhsl) -> Color { + let Srgb { red, green, blue, .. }: Srgb = Srgb::from_color_unclamped(color); + Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,) +} + +pub fn rgb_to_okhsl (color: Color) -> Okhsl { + if let Color::Rgb(r, g, b) = color { + Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)) + } else { + unreachable!("only Color::Rgb is supported") + } +} pub trait HasColor { fn color (&self) -> ItemColor; } - -pub struct ItemColor {} - -pub struct ItemTheme {} - #[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),*>)? { @@ -14,6 +26,107 @@ pub struct ItemTheme {} } } -pub fn rgb (r: u8, g: u8, b: u8) -> ItemColor { - todo!(); +pub struct ItemColor {} +impl_from!(ItemColor: |rgb: Color| Self { rgb, okhsl: rgb_to_okhsl(rgb) }); +impl_from!(ItemColor: |okhsl: Okhsl| Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); +// A single color within item theme parameters, in OKHSL and RGB representations. +impl ItemColor { + #[cfg(feature = "tui")] pub const fn from_tui (rgb: Color) -> Self { + Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) } + } + pub fn random () -> Self { + let mut rng = ::rand::thread_rng(); + let lo = Okhsl::new(-180.0, 0.01, 0.25); + let hi = Okhsl::new( 180.0, 0.9, 0.5); + UniformOkhsl::new(lo, hi).sample(&mut rng).into() + } + pub fn random_dark () -> Self { + let mut rng = ::rand::thread_rng(); + let lo = Okhsl::new(-180.0, 0.025, 0.075); + let hi = Okhsl::new( 180.0, 0.5, 0.150); + UniformOkhsl::new(lo, hi).sample(&mut rng).into() + } + pub fn random_near (color: Self, distance: f32) -> Self { + color.mix(Self::random(), distance) + } + pub fn mix (&self, other: Self, distance: f32) -> Self { + if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); } + self.okhsl.mix(other.okhsl, distance).into() + } +} + +pub struct ItemTheme {} +impl_from!(ItemTheme: |base: ItemColor| Self::from_item_color(base)); +impl_from!(ItemTheme: |base: Color| Self::from_tui_color(base)); +impl ItemTheme { + #[cfg(feature = "tui")] pub const G: [Self;256] = { + let mut builder = konst::array::ArrayBuilder::new(); + while !builder.is_full() { + let index = builder.len() as u8; + let light = (index as f64 * 1.15) as u8; + let lighter = (index as f64 * 1.7) as u8; + let lightest = (index as f64 * 1.85) as u8; + let dark = (index as f64 * 0.9) as u8; + let darker = (index as f64 * 0.6) as u8; + let darkest = (index as f64 * 0.3) as u8; + builder.push(ItemTheme { + base: ItemColor::from_tui(Color::Rgb(index, index, index )), + light: ItemColor::from_tui(Color::Rgb(light, light, light, )), + lighter: ItemColor::from_tui(Color::Rgb(lighter, lighter, lighter, )), + lightest: ItemColor::from_tui(Color::Rgb(lightest, lightest, lightest, )), + dark: ItemColor::from_tui(Color::Rgb(dark, dark, dark, )), + darker: ItemColor::from_tui(Color::Rgb(darker, darker, darker, )), + darkest: ItemColor::from_tui(Color::Rgb(darkest, darkest, darkest, )), + }); + } + builder.build() + }; + pub fn random () -> Self { ItemColor::random().into() } + pub fn random_near (color: Self, distance: f32) -> Self { + color.base.mix(ItemColor::random(), distance).into() + } + pub const G00: Self = { + let color: ItemColor = ItemColor { + okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 }, + rgb: Color::Rgb(0, 0, 0) + }; + Self { + base: color, + light: color, + lighter: color, + lightest: color, + dark: color, + darker: color, + darkest: color, + } + }; + #[cfg(feature = "tui")] pub fn from_tui_color (base: Color) -> Self { + Self::from_item_color(ItemColor::from_tui(base)) + } + pub fn from_item_color (base: ItemColor) -> Self { + let mut light = base.okhsl; + light.lightness = (light.lightness * 1.3).min(1.0); + let mut lighter = light; + lighter.lightness = (lighter.lightness * 1.3).min(1.0); + let mut lightest = base.okhsl; + lightest.lightness = 0.95; + let mut dark = base.okhsl; + dark.lightness = (dark.lightness * 0.75).max(0.0); + dark.saturation = (dark.saturation * 0.75).max(0.0); + let mut darker = dark; + darker.lightness = (darker.lightness * 0.66).max(0.0); + darker.saturation = (darker.saturation * 0.66).max(0.0); + let mut darkest = darker; + darkest.lightness = 0.1; + darkest.saturation = (darkest.saturation * 0.50).max(0.0); + Self { + base, + light: light.into(), + lighter: lighter.into(), + lightest: lightest.into(), + dark: dark.into(), + darker: darker.into(), + darkest: darkest.into(), + } + } } diff --git a/src/lang.rs b/src/lang.rs new file mode 100644 index 0000000..54aa0c2 --- /dev/null +++ b/src/lang.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "tui")] impl TuiKey { + + #[cfg(feature = "lang")] + pub fn from_dsl (dsl: impl Language) -> Usually { + if let Some(word) = dsl.word()? { + let word = word.trim(); + Ok(if word == ":char" { + Self(None, KeyModifiers::NONE) + } else if word.chars().nth(0) == Some('@') { + let mut key = None; + let mut modifiers = KeyModifiers::NONE; + let mut tokens = word[1..].split(Self::SPLIT).peekable(); + while let Some(token) = tokens.next() { + if tokens.peek().is_some() { + match token { + "ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL, + "alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT, + "shift" | "Shift" | "s" | "S" => { + modifiers |= KeyModifiers::SHIFT; + // + TODO normalize character case, BackTab, etc. + }, + _ => panic!("unknown modifier {token}"), + } + } else { + key = if token.len() == 1 { + Some(KeyCode::Char(token.chars().next().unwrap())) + } else { + Some(named_key(token).unwrap_or_else(||panic!("unknown character {token}"))) + } + } + } + Self(key, modifiers) + } else { + return Err(format!("TuiKey: unexpected: {word}").into()) + }) + } else { + return Err(format!("TuiKey: unspecified").into()) + } + } + +} diff --git a/src/lib.rs b/src/lib.rs index ecde0e6..2fad25c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,9 +35,9 @@ pub(crate) use ::{ #[cfg(feature = "draw")] pub mod draw; #[cfg(feature = "draw")] pub mod color; #[cfg(feature = "text")] pub mod text; -#[cfg(feature = "tui")] pub mod tui; -#[cfg(feature = "tui")] pub extern crate ratatui; -#[cfg(feature = "tui")] pub extern crate crossterm; +#[cfg(feature = "term")] pub mod term; +#[cfg(feature = "term")] pub extern crate ratatui; +#[cfg(feature = "term")] pub extern crate crossterm; /// Define a trait an implement it for various mutation-enabled wrapper types. */ #[macro_export] macro_rules! flex_trait_mut ( diff --git a/src/term.rs b/src/term.rs new file mode 100644 index 0000000..d88cc8c --- /dev/null +++ b/src/term.rs @@ -0,0 +1,792 @@ +use crate::{*, lang::*, play::*, draw::*, color::*, text::*}; +use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; +use rand::distributions::uniform::UniformSampler; +use ::{ + std::{ + io::{stdout, Write}, + time::Duration + }, + better_panic::{Settings, Verbosity}, + ratatui::{ + prelude::{Color, Style, Buffer, Position, Backend}, + style::{Modifier, Color::*}, + backend::{CrosstermBackend, ClearType}, + layout::{Size, Rect}, + buffer::Cell + }, + crossterm::{ + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, + } +}; + +/// Marker trait for structs that may be root of TUI app. +pub trait Tui: Draw + Do> {} + +/// `Tui` is automatically implemented. +impl + Do>> Tui for T {} + +/// Spawn the TUI input thread which reads keys from the terminal. +pub fn tui_input + Send + Sync + 'static> ( + exited: &Arc, state: &Arc>, poll: Duration +) -> Result { + let exited = exited.clone(); + let state = state.clone(); + Thread::new_poll(exited.clone(), poll, move |_| { + let event = read().unwrap(); + match event { + + // Hardcoded exit. + Event::Key(KeyEvent { + modifiers: KeyModifiers::CONTROL, + code: KeyCode::Char('c'), + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) => { exited.store(true, Relaxed); }, + + // Handle all other events by the state: + _ => { + let event = TuiEvent::from_crossterm(event); + if let Err(e) = state.write().unwrap().handle(&event) { + panic!("{e}") + } + } + } + }) +} + +/// TUI input loop event. +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] +pub struct TuiEvent(pub Event); +impl_from!(TuiEvent: |e: Event| TuiEvent(e)); +impl_from!(TuiEvent: |c: char| TuiEvent(Event::Key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)))); +impl TuiEvent { + #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps { + Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self)) + } +} +impl Ord for TuiEvent { + fn cmp (&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other) + .unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf + } +} + +/// TUI key spec. +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)] +pub struct TuiKey(pub Option, pub KeyModifiers); + +impl TuiKey { + const SPLIT: char = '/'; + pub fn to_crossterm (&self) -> Option { + self.0.map(|code|Event::Key(KeyEvent { + code, + modifiers: self.1, + kind: KeyEventKind::Press, + state: KeyEventState::NONE, + })) + } + pub fn named (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, + }) + } +} + +/// TUI works in u16 coordinates. +impl Coord for u16 { fn plus (self, other: Self) -> Self { self.saturating_add(other) } } + +impl Draw for u64 { + fn draw (&self, _to: &mut Buffer) { todo!() } +} +impl Draw for f64 { + fn draw (&self, _to: &mut Buffer) { todo!() } +} +impl Draw for &str { + fn draw (&self, to: &mut Buffer) { + let XYWH(x, y, w, ..) = to.centered_xy([width_chars_max(to.w(), self), 1]); + to.text(&self, x, y, w) + } +} +impl Draw for String { + fn draw (&self, to: &mut Buffer) { self.as_str().draw(to) } +} +impl Draw for Arc { + fn draw (&self, to: &mut Buffer) { self.as_ref().draw(to) } +} + +mod phat { + use super::*; + pub const LO: &'static str = "▄"; + pub const HI: &'static str = "▀"; + /// A phat line + pub fn lo (fg: Color, bg: Color) -> impl Draw { + H::fixed(1, Tui::fg_bg(fg, bg, X::repeat(self::phat::LO))) + } + /// A phat line + pub fn hi (fg: Color, bg: Color) -> impl Draw { + H::fixed(1, Tui::fg_bg(fg, bg, X::repeat(self::phat::HI))) + } +} + +mod scroll { + pub const ICON_DEC_V: &[char] = &['▲']; + pub const ICON_INC_V: &[char] = &['▼']; + pub const ICON_DEC_H: &[char] = &[' ', '🞀', ' ']; + pub const ICON_INC_H: &[char] = &[' ', '🞂', ' ']; +} + +fn x_repeat (c: char) { + move|to: &mut Buffer|{ + let XYWH(x, y, w, h) = to.area(); + for x in x..x+w { + if let Some(cell) = to.cell_mut(Position::from((x, y))) { + cell.set_symbol(&c); + } + } + } +} + +fn y_repeat (c: char) { + move|to: &mut Buffer|{ + let XYWH(x, y, w, h) = to.area(); + for y in y..y+h { + if let Some(cell) = to.cell_mut(Position::from((x, y))) { + cell.set_symbol(&c); + } + } + } +} + +fn xy_repeat (c: char) { + move|to: &mut Buffer|{ + let XYWH(x, y, w, h) = to.area(); + let a = c.len(); + for (_v, y) in (y..y+h).enumerate() { + for (u, x) in (x..x+w).enumerate() { + if let Some(cell) = to.cell_mut(Position::from((x, y))) { + let u = u % a; + cell.set_symbol(&c[u..u+1]); + } + } + } + } +} + +/// ``` +/// let _ = tengri::button_2("", "", true); +/// let _ = tengri::button_2("", "", false); +/// ``` +pub fn button_2 <'a> (key: impl Draw, label: impl Draw, editing: bool) -> impl Draw { + 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(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)))) +} + +/// ``` +/// let _ = tengri::button_3("", "", "", true); +/// let _ = tengri::button_3("", "", "", false); +/// ``` +pub fn button_3 <'a> ( + key: impl Draw, label: impl Draw, value: impl Draw, editing: bool, +) -> impl Draw { + 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(!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, &"▌"), )))) +} + +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 Buffer) { + if self.enabled() { let _ = BorderStyle::draw(self, to); } + } + } + )+} +} + +border! { + Square { + "┌" "─" "┐" + "│" "│" + "└" "─" "┘" fn style (&self) -> Option