diff --git a/examples/tui_00.rs b/examples/tui_00.rs index 94bf069..80e7acd 100644 --- a/examples/tui_00.rs +++ b/examples/tui_00.rs @@ -4,7 +4,6 @@ namespace!(State: bool {}); namespace!(State: u16 {}); namespace!(State: Color {}); handle!(TuiIn: |self: State, input|Action::from(input).eval(self).map(|_|None)); - from!(Action: |input: &TuiIn| todo!()); view!(State: TuiOut: [ evaluate_output_expression, evaluate_output_expression_tui ]); draw!(State: TuiOut: [ draw_example ]); #[derive(Debug, Default)] struct State { @@ -12,6 +11,7 @@ namespace!(State: Color {}); /** Command history (undo/redo) */ history: Vec, /** User-controllable value */ cursor: usize, } +impl_from!(Action: |input: &TuiIn| todo!()); #[derive(Debug)] enum Action { /** Increment cursor */ Next, /** Decrement cursor */ Prev diff --git a/src/tengri.rs b/src/tengri.rs index fd0560e..7c1968b 100644 --- a/src/tengri.rs +++ b/src/tengri.rs @@ -36,7 +36,7 @@ pub extern crate unicode_width; pub(crate) use unicode_width::*; }; pub(crate) use ::std::{ io::{stdout, Stdout, Write}, - sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}}, + sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}}, fmt::{Debug, Display}, ops::{Add, Sub, Mul, Div}, marker::PhantomData, @@ -156,8 +156,6 @@ pub(crate) use ::std::{ } } - - /// Define layout operation. /// /// ``` diff --git a/src/tengri_impl.rs b/src/tengri_impl.rs index 3b3d1ea..0a865c2 100644 --- a/src/tengri_impl.rs +++ b/src/tengri_impl.rs @@ -37,7 +37,7 @@ impl<'a, T: AsRef> TrimString { fn as_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) } } impl> ErrorBoundary { - pub fn new (content: Perhaps) -> Self { Self(Default::default(), content) } + pub fn new (content: Perhaps) -> Self { Self(content, Default::default()) } } impl, V: Content> HasContent for FieldH { fn content (&self) -> impl Content { Bsp::e(&self.1, &self.2) } @@ -89,9 +89,6 @@ impl> Command for Option { Ok(None) } } -impl, F: Fn()->T> Lazy { - pub const fn new (thunk: F) -> Self { Self(thunk, PhantomData) } -} impl Layout for Measure {} impl>> Measured for T { fn measure (&self) -> &Measure { self.as_ref() } @@ -102,14 +99,16 @@ impl Clone for Measure { } } impl Thunk { - pub const fn new (draw: F) -> Self { Self(PhantomData, draw) } + pub const fn new (draw: F) -> Self { Self(draw, PhantomData) } } impl Draw for Thunk { - fn draw (&self, to: &mut O) { (self.1)(to) } + fn draw (&self, to: &mut O) { (self.0)(to) } } impl Layout for Thunk {} impl Memo { - pub fn new (value: T, view: U) -> Self { Self { value, view: Arc::new(view.into()) } } + pub fn new (value: T, view: U) -> Self { + Self { value, view: Arc::new(view.into()) } + } pub fn update (&mut self, newval: T, draw: impl Fn(&mut U, &T, &T)->R) -> Option { if newval != self.value { let result = draw(&mut*self.view.write().unwrap(), &newval, &self.value); @@ -528,7 +527,7 @@ impl, Tail: Layout> Layout for Bsp { fn layout_w (&self, area: XYWH) -> O::Unit { match self.0 { Above | Below | North | South => self.1.layout_w(area).max(self.2.layout_w(area)), - East | West => self.1.layout_w_min(area).plus(self.2.layout_w(area)), + East | West => self.1.layout_w_min(area).plus(self.2.layout_w(area)), } } fn layout_w_min (&self, area: XYWH) -> O::Unit { @@ -546,7 +545,7 @@ impl, Tail: Layout> Layout for Bsp { fn layout_h (&self, area: XYWH) -> O::Unit { match self.0 { Above | Below | East | West => self.1.layout_h(area).max(self.2.layout_h(area)), - North | South => self.1.layout_h(area).plus(self.2.layout_h(area)), + North | South => self.1.layout_h(area).plus(self.2.layout_h(area)), } } fn layout_h_min (&self, area: XYWH) -> O::Unit { @@ -558,7 +557,7 @@ impl, Tail: Layout> Layout for Bsp { fn layout_h_max (&self, area: XYWH) -> O::Unit { match self.0 { Above | Below | North | South => self.1.layout_h_max(area).max(self.2.layout_h_max(area)), - East | West => self.1.layout_h_max(area).plus(self.2.layout_h_max(area)), + East | West => self.1.layout_h_max(area).plus(self.2.layout_h_max(area)), } } fn layout (&self, area: XYWH) -> XYWH { @@ -871,60 +870,119 @@ mod xywh { impl_from!(ItemColor: |okhsl: Okhsl| Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); impl_debug!(BigBuffer |self, f| { write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len()) }); 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_resized(&mut*self.backend.write().unwrap(), &mut*self.buffer.write().unwrap(), size); + tui_redrawn(&mut*self.backend.write().unwrap(), &mut*self.buffer.write().unwrap(), &mut buffer); + buffer + } + /// 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 }), - area: [0, 0, width, height], + buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }).into(), + area: XYWH(0, 0, width, height), perf: Default::default(), - backend, + backend: backend.into(), + event: None, + error: None, }) } - /// Create and launch a terminal user interface. - pub fn run (self, join: bool, state: &Arc>) -> Usually>> where - T: Handle + Draw + Send + Sync + 'static + /// Run an amm in the engine. + pub fn run (mut self, join: bool, state: &Arc>) -> Usually> where + T: Handle + Draw + Send + Sync + 'static { - let tui = Arc::new(RwLock::new(self)); - let _input_thread = tui_input(tui.clone(), state, Duration::from_millis(100)); - tui.write().unwrap().setup()?; - let render_thread = tui_output(tui.clone(), state, Duration::from_millis(10))?; + self.setup()?; + let tui = Arc::new(self); + let _input_thread = tui_input(&tui, state, Duration::from_millis(100))?; + let render_thread = tui_output(&tui, state, Duration::from_millis(10))?; if join { - match render_thread.join() { - Ok(result) => { - tui.write().unwrap().teardown()?; - println!("\n\rRan successfully: {result:?}\n\r"); - }, - Err(error) => { - tui.write().unwrap().teardown()?; - panic!("\n\rDraw thread failed: error={error:?}.\n\r") - }, + 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) } - /// True if done - pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) } - /// Prepare before run - pub fn setup (&mut self) -> Usually<()> { tui_setup(&mut self.backend) } - /// Clean up after run - pub fn teardown (&mut self) -> Usually<()> { tui_teardown(&mut self.backend) } - /// Apply changes to the display buffer. - pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { - tui_resized(&mut self.backend, &mut self.buffer, size); - tui_redrawn(&mut self.backend, &mut self.buffer, &mut buffer); - buffer + } + + /// Spawn the input thread. + pub fn tui_input + Send + Sync + 'static> ( + engine: &Arc, state: &Arc>, poll: Duration + ) -> Result { + let state = state.clone(); + let engine = engine.clone(); + TuiThread::new_poll(engine.exited.clone(), poll, move |_| { + let event = read().unwrap(); + match event { + Event::Key(KeyEvent { + modifiers: KeyModifiers::CONTROL, + code: KeyCode::Char('c'), + kind: KeyEventKind::Press, + state: KeyEventState::NONE + }) => { + engine.exited.store(true, Relaxed); + }, + _ => { + let event = TuiEvent::from_crossterm(event); + if let Err(e) = state.write().unwrap().handle(&engine) { + panic!("{e}") + } + } + } + }) + } + + /// Spawn the output thread. + pub fn tui_output + Send + Sync + 'static> ( + engine: &Arc, state: &Arc>, sleep: Duration + ) -> Result { + let state = state.clone(); + let mut engine = engine.clone(); + let WH(width, height) = tui_wh(&engine); + let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height }); + TuiThread::new_sleep(engine.exited.clone(), sleep, move |perf| { + let WH(width, height) = tui_wh(&engine); + if let Ok(state) = state.try_read() { + let size = Rect { x: 0, y: 0, width, height }; + if buffer.area != size { + engine.backend.write().unwrap() + .clear_region(ClearType::All).expect("pre-frame clear region failed"); + buffer.resize(size); + buffer.reset(); + } + state.draw(&engine); + buffer = engine.flip(*engine.buffer.write().unwrap(), size); + } + let timer = format!("{:>3.3}ms", perf.used.load(Relaxed)); + buffer.set_string(0, 0, &timer, Style::default()); + }) + } + + fn tui_wh (engine: &Tui) -> WH { + let Size { width, height } = engine.backend.read().unwrap().size().expect("get size failed"); + WH(width, height) + } + + impl Input for Tui { + type Event = TuiEvent; + type Handled = bool; + fn event (&self) -> &TuiEvent { + self.event.as_ref().expect("input.event called outside of input handler") } } - impl Input for TuiIn { - type Event = TuiEvent; - type Handled = bool; - fn event (&self) -> &TuiEvent { &self.event } - } - - impl Done for TuiIn { + impl Done for Tui { fn done (&self) { self.exited.store(true, Relaxed); } fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) } } @@ -1003,7 +1061,7 @@ mod xywh { } } - impl Out for TuiOut { + impl Out for Tui { type Unit = u16; #[inline] fn area (&self) -> XYWH { self.area } #[inline] fn area_mut (&mut self) -> &mut XYWH { &mut self.area } @@ -1015,9 +1073,11 @@ mod xywh { } } - impl TuiOut { + impl Tui { #[inline] pub fn with_rect (&mut self, area: XYWH) -> &mut Self { self.area = area; self } - pub fn update (&mut self, area: XYWH, callback: &impl Fn(&mut Cell, u16, u16)) { tui_update(&mut self.buffer, area, callback); } + pub fn update (&mut self, area: XYWH, callback: &impl Fn(&mut Cell, u16, u16)) { + tui_update(&mut*self.buffer.write().unwrap(), area, callback); + } pub fn fill_char (&mut self, area: XYWH, c: char) { self.update(area, &|cell,_,_|{cell.set_char(c);}) } pub fn fill_bg (&mut self, area: XYWH, color: Color) { self.update(area, &|cell,_,_|{cell.set_bg(color);}) } pub fn fill_fg (&mut self, area: XYWH, color: Color) { self.update(area, &|cell,_,_|{cell.set_fg(color);}) } @@ -1044,7 +1104,7 @@ mod xywh { } } pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) { - for cell in self.buffer.content.iter_mut() { + for cell in self.buffer.write().unwrap().content.iter_mut() { cell.fg = fg; cell.bg = bg; cell.modifier = modifier; @@ -1052,8 +1112,8 @@ mod xywh { } pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option