From c8919ad77b57c0db27a3313976ea847ea7f73df5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Apr 2022 21:44:37 -0500 Subject: [PATCH 1/6] add benchmark --- Cargo.toml | 4 + benches/tui_update.rs | 268 ++++++++++++++++++++++++++++++++++++ examples/tui_stress_test.rs | 260 ++++++++++++++++++++++++++++++++++ packages/tui/src/config.rs | 17 ++- packages/tui/src/lib.rs | 161 ++++++++++++++-------- 5 files changed, 650 insertions(+), 60 deletions(-) create mode 100644 benches/tui_update.rs create mode 100644 examples/tui_stress_test.rs diff --git a/Cargo.toml b/Cargo.toml index e5ec20306..8291a290c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,3 +88,7 @@ harness = false [[bench]] name = "jsframework" harness = false + +[[bench]] +name = "tui_update" +harness = false \ No newline at end of file diff --git a/benches/tui_update.rs b/benches/tui_update.rs new file mode 100644 index 000000000..5a3a70b66 --- /dev/null +++ b/benches/tui_update.rs @@ -0,0 +1,268 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use dioxus::prelude::*; +use dioxus_tui::{Config, TuiContext}; + +criterion_group!(mbenches, tui_update); +criterion_main!(mbenches); + +/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. +fn tui_update(c: &mut Criterion) { + let mut group = c.benchmark_group("Update boxes"); + + // We can also use loops to define multiple benchmarks, even over multiple dimensions. + for size in 1..=8u32 { + let parameter_string = format!("{}", (3 * size).pow(2)); + group.bench_with_input( + BenchmarkId::new("size", parameter_string), + &size, + |b, size| { + b.iter(|| match size { + 1 => dioxus::tui::launch_cfg( + app3, + Config { + headless: true, + ..Default::default() + }, + ), + 2 => dioxus::tui::launch_cfg( + app6, + Config { + headless: true, + ..Default::default() + }, + ), + 3 => dioxus::tui::launch_cfg( + app9, + Config { + headless: true, + ..Default::default() + }, + ), + 4 => dioxus::tui::launch_cfg( + app12, + Config { + headless: true, + ..Default::default() + }, + ), + 5 => dioxus::tui::launch_cfg( + app15, + Config { + headless: true, + ..Default::default() + }, + ), + 6 => dioxus::tui::launch_cfg( + app18, + Config { + headless: true, + ..Default::default() + }, + ), + 7 => dioxus::tui::launch_cfg( + app21, + Config { + headless: true, + ..Default::default() + }, + ), + 8 => dioxus::tui::launch_cfg( + app24, + Config { + headless: true, + ..Default::default() + }, + ), + _ => (), + }) + }, + ); + } +} + +#[derive(Props, PartialEq)] +struct BoxProps { + x: usize, + y: usize, + hue: f32, + alpha: f32, +} +#[allow(non_snake_case)] +fn Box(cx: Scope) -> Element { + let count = use_state(&cx, || 0); + + let x = cx.props.x * 2; + let y = cx.props.y * 2; + let hue = cx.props.hue; + let display_hue = cx.props.hue as u32 / 10; + let count = count.get(); + let alpha = cx.props.alpha + (count % 100) as f32; + + cx.render(rsx! { + div { + left: "{x}%", + top: "{y}%", + width: "100%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + align_items: "center", + p{"{display_hue:03}"} + } + }) +} + +#[derive(Props, PartialEq)] +struct GridProps { + size: usize, +} +#[allow(non_snake_case)] +fn Grid(cx: Scope) -> Element { + let size = cx.props.size; + let count = use_state(&cx, || 0); + let counts = use_ref(&cx, || vec![0; size * size]); + + let ctx: TuiContext = cx.consume_context().unwrap(); + if *count.get() + 1 >= (size * size) { + ctx.quit(); + } else { + counts.with_mut(|c| { + let i = *count.current(); + c[i] += 1; + c[i] = c[i] % 360; + }); + count.with_mut(|i| { + *i += 1; + *i = *i % (size * size); + }); + } + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..size).map(|x| + { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..size).map(|y| + { + let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; + let key = format!("{}-{}", x, y); + cx.render(rsx! { + Box{ + x: x, + y: y, + alpha: 100.0, + hue: alpha, + key: "{key}", + } + }) + } + ) + } + }) + } + ) + } + }) +} + +fn app3(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 3, + } + } + }) +} + +fn app6(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 6, + } + } + }) +} + +fn app9(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 9, + } + } + }) +} + +fn app12(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 12, + } + } + }) +} + +fn app15(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 15, + } + } + }) +} + +fn app18(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 18, + } + } + }) +} + +fn app21(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 21, + } + } + }) +} + +fn app24(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 24, + } + } + }) +} diff --git a/examples/tui_stress_test.rs b/examples/tui_stress_test.rs new file mode 100644 index 000000000..1674ac055 --- /dev/null +++ b/examples/tui_stress_test.rs @@ -0,0 +1,260 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use dioxus::prelude::*; +use dioxus_tui::{Config, TuiContext}; + +criterion_group!(mbenches, tui_update); +criterion_main!(mbenches); + +/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. +fn tui_update(c: &mut Criterion) { + let mut group = c.benchmark_group("Update boxes"); + + // We can also use loops to define multiple benchmarks, even over multiple dimensions. + for size in 1..=8u32 { + let parameter_string = format!("{}", (3 * size).pow(2)); + group.bench_with_input( + BenchmarkId::new("size", parameter_string), + &size, + |b, size| { + b.iter(|| match size { + 1 => dioxus::tui::launch_cfg( + app3, + Config { + ..Default::default() + }, + ), + 2 => dioxus::tui::launch_cfg( + app6, + Config { + ..Default::default() + }, + ), + 3 => dioxus::tui::launch_cfg( + app9, + Config { + ..Default::default() + }, + ), + 4 => dioxus::tui::launch_cfg( + app12, + Config { + ..Default::default() + }, + ), + 5 => dioxus::tui::launch_cfg( + app15, + Config { + ..Default::default() + }, + ), + 6 => dioxus::tui::launch_cfg( + app18, + Config { + ..Default::default() + }, + ), + 7 => dioxus::tui::launch_cfg( + app21, + Config { + ..Default::default() + }, + ), + 8 => dioxus::tui::launch_cfg( + app24, + Config { + ..Default::default() + }, + ), + _ => (), + }) + }, + ); + } +} + +#[derive(Props, PartialEq)] +struct BoxProps { + x: usize, + y: usize, + hue: f32, + alpha: f32, +} +#[allow(non_snake_case)] +fn Box(cx: Scope) -> Element { + let count = use_state(&cx, || 0); + + let x = cx.props.x * 2; + let y = cx.props.y * 2; + let hue = cx.props.hue; + let display_hue = cx.props.hue as u32 / 10; + let count = count.get(); + let alpha = cx.props.alpha + (count % 100) as f32; + + cx.render(rsx! { + div { + left: "{x}%", + top: "{y}%", + width: "100%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + align_items: "center", + p{"{display_hue:03}"} + } + }) +} + +#[derive(Props, PartialEq)] +struct GridProps { + size: usize, +} +#[allow(non_snake_case)] +fn Grid(cx: Scope) -> Element { + let size = cx.props.size; + let count = use_state(&cx, || 0); + let counts = use_ref(&cx, || vec![0; size * size]); + + let ctx: TuiContext = cx.consume_context().unwrap(); + if *count.get() + 1 >= (size * size) { + ctx.quit(); + } else { + counts.with_mut(|c| { + let i = *count.current(); + c[i] += 1; + c[i] = c[i] % 360; + }); + count.with_mut(|i| { + *i += 1; + *i = *i % (size * size); + }); + } + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..size).map(|x| + { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..size).map(|y| + { + let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; + let key = format!("{}-{}", x, y); + cx.render(rsx! { + Box{ + x: x, + y: y, + alpha: 100.0, + hue: alpha, + key: "{key}", + } + }) + } + ) + } + }) + } + ) + } + }) +} + +fn app3(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 3, + } + } + }) +} + +fn app6(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 6, + } + } + }) +} + +fn app9(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 9, + } + } + }) +} + +fn app12(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 12, + } + } + }) +} + +fn app15(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 15, + } + } + }) +} + +fn app18(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 18, + } + } + }) +} + +fn app21(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 21, + } + } + }) +} + +fn app24(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 24, + } + } + }) +} diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs index 46a0926b8..6e839d8a7 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -1,6 +1,21 @@ -#[derive(Default, Clone, Copy)] +#[derive(Clone, Copy)] pub struct Config { pub rendering_mode: RenderingMode, + /// Controls if the terminal quit when the user presses `ctrl+c`? + /// To handle quiting on your own, use the [crate::TuiContext] root context. + pub ctrl_c_quit: bool, + /// Controls if the terminal should dislay anything, usefull for testing. + pub headless: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + rendering_mode: Default::default(), + ctrl_c_quit: true, + headless: false, + } + } } #[derive(Clone, Copy)] diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index f90967802..71f5026fd 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -6,13 +6,19 @@ use crossterm::{ }; use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::*; -use futures::{channel::mpsc::UnboundedSender, pin_mut, StreamExt}; +use futures::{ + channel::mpsc::{UnboundedReceiver, UnboundedSender}, + pin_mut, StreamExt, +}; use std::{ collections::HashMap, io, time::{Duration, Instant}, }; -use stretch2::{prelude::Size, Stretch}; +use stretch2::{ + prelude::{Node, Size}, + Stretch, +}; use style::RinkStyle; use tui::{backend::CrosstermBackend, Terminal}; @@ -30,6 +36,16 @@ pub use hooks::*; pub use layout::*; pub use render::*; +#[derive(Clone)] +pub struct TuiContext { + tx: UnboundedSender, +} +impl TuiContext { + pub fn quit(&self) { + self.tx.unbounded_send(InputEvent::Close).unwrap(); + } +} + pub fn launch(app: Component<()>) { launch_cfg(app, Config::default()) } @@ -37,8 +53,34 @@ pub fn launch(app: Component<()>) { pub fn launch_cfg(app: Component<()>, cfg: Config) { let mut dom = VirtualDom::new(app); let (tx, rx) = unbounded(); + // Setup input handling + let (event_tx, event_rx) = unbounded(); + let event_tx_clone = event_tx.clone(); + if !cfg.headless { + std::thread::spawn(move || { + let tick_rate = Duration::from_millis(100); + let mut last_tick = Instant::now(); + loop { + // poll for tick rate duration, if no events, sent tick event. + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + + if crossterm::event::poll(timeout).unwrap() { + let evt = crossterm::event::read().unwrap(); + event_tx.unbounded_send(InputEvent::UserInput(evt)).unwrap(); + } + + if last_tick.elapsed() >= tick_rate { + event_tx.unbounded_send(InputEvent::Tick).unwrap(); + last_tick = Instant::now(); + } + } + }); + } let cx = dom.base_scope(); + cx.provide_root_context(TuiContext { tx: event_tx_clone }); let (handler, state) = RinkInputHandler::new(rx, cx); @@ -46,7 +88,7 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) { dom.rebuild(); - render_vdom(&mut dom, tx, handler, cfg).unwrap(); + render_vdom(&mut dom, event_rx, tx, handler, cfg).unwrap(); } pub struct TuiNode<'a> { @@ -56,35 +98,13 @@ pub struct TuiNode<'a> { pub node: &'a VNode<'a>, } -pub fn render_vdom( +fn render_vdom( vdom: &mut VirtualDom, + mut event_reciever: UnboundedReceiver, ctx: UnboundedSender, handler: RinkInputHandler, cfg: Config, ) -> Result<()> { - // Setup input handling - let (tx, mut rx) = unbounded(); - std::thread::spawn(move || { - let tick_rate = Duration::from_millis(100); - let mut last_tick = Instant::now(); - loop { - // poll for tick rate duration, if no events, sent tick event. - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - - if crossterm::event::poll(timeout).unwrap() { - let evt = crossterm::event::read().unwrap(); - tx.unbounded_send(InputEvent::UserInput(evt)).unwrap(); - } - - if last_tick.elapsed() >= tick_rate { - tx.unbounded_send(InputEvent::Tick).unwrap(); - last_tick = Instant::now(); - } - } - }); - tokio::runtime::Builder::new_current_thread() .enable_all() .build()? @@ -92,13 +112,17 @@ pub fn render_vdom( /* Get the terminal to calcualte the layout from */ - enable_raw_mode().unwrap(); - let mut stdout = std::io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); - let backend = CrosstermBackend::new(io::stdout()); - let mut terminal = Terminal::new(backend).unwrap(); + let mut terminal = (!cfg.headless).then(|| { + enable_raw_mode().unwrap(); + let mut stdout = std::io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + let backend = CrosstermBackend::new(io::stdout()); + Terminal::new(backend).unwrap() + }); - terminal.clear().unwrap(); + if let Some(terminal) = &mut terminal { + terminal.clear().unwrap(); + } loop { /* @@ -126,34 +150,51 @@ pub fn render_vdom( let root_layout = nodes[&node_id].layout; let mut events = Vec::new(); - terminal.draw(|frame| { - // size is guaranteed to not change when rendering - let dims = frame.size(); + fn resize(dims: tui::layout::Rect, stretch: &mut Stretch, root_layout: Node) { let width = dims.width; let height = dims.height; - layout + + stretch .compute_layout( root_layout, Size { - width: stretch2::prelude::Number::Defined(width as f32), - height: stretch2::prelude::Number::Defined(height as f32), + width: stretch2::prelude::Number::Defined((width - 1) as f32), + height: stretch2::prelude::Number::Defined((height - 1) as f32), }, ) .unwrap(); + } - // resolve events before rendering - events = handler.get_events(vdom, &layout, &mut nodes, root_node); - render::render_vnode( - frame, - &layout, - &mut nodes, - vdom, - root_node, - &RinkStyle::default(), - cfg, + if let Some(terminal) = &mut terminal { + terminal.draw(|frame| { + // size is guaranteed to not change when rendering + resize(frame.size(), &mut layout, root_layout); + + // resolve events before rendering + events = handler.get_events(vdom, &layout, &mut nodes, root_node); + render::render_vnode( + frame, + &layout, + &mut nodes, + vdom, + root_node, + &RinkStyle::default(), + cfg, + ); + assert!(nodes.is_empty()); + })?; + } else { + resize( + tui::layout::Rect { + x: 0, + y: 0, + width: 100, + height: 100, + }, + &mut layout, + root_layout, ); - assert!(nodes.is_empty()); - })?; + } for e in events { vdom.handle_message(SchedulerMsg::Event(e)); @@ -164,7 +205,7 @@ pub fn render_vdom( let wait = vdom.wait_for_work(); pin_mut!(wait); - match select(wait, rx.next()).await { + match select(wait, event_reciever.next()).await { Either::Left((_a, _b)) => { // } @@ -194,13 +235,15 @@ pub fn render_vdom( vdom.work_with_deadline(|| false); } - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; + if let Some(terminal) = &mut terminal { + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + } Ok(()) }) From 931aceca43e337568451477ecd78351292851491 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Apr 2022 21:50:49 -0500 Subject: [PATCH 2/6] remove stress test --- examples/tui_stress_test.rs | 260 ------------------------------------ 1 file changed, 260 deletions(-) delete mode 100644 examples/tui_stress_test.rs diff --git a/examples/tui_stress_test.rs b/examples/tui_stress_test.rs deleted file mode 100644 index 1674ac055..000000000 --- a/examples/tui_stress_test.rs +++ /dev/null @@ -1,260 +0,0 @@ -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use dioxus::prelude::*; -use dioxus_tui::{Config, TuiContext}; - -criterion_group!(mbenches, tui_update); -criterion_main!(mbenches); - -/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. -fn tui_update(c: &mut Criterion) { - let mut group = c.benchmark_group("Update boxes"); - - // We can also use loops to define multiple benchmarks, even over multiple dimensions. - for size in 1..=8u32 { - let parameter_string = format!("{}", (3 * size).pow(2)); - group.bench_with_input( - BenchmarkId::new("size", parameter_string), - &size, - |b, size| { - b.iter(|| match size { - 1 => dioxus::tui::launch_cfg( - app3, - Config { - ..Default::default() - }, - ), - 2 => dioxus::tui::launch_cfg( - app6, - Config { - ..Default::default() - }, - ), - 3 => dioxus::tui::launch_cfg( - app9, - Config { - ..Default::default() - }, - ), - 4 => dioxus::tui::launch_cfg( - app12, - Config { - ..Default::default() - }, - ), - 5 => dioxus::tui::launch_cfg( - app15, - Config { - ..Default::default() - }, - ), - 6 => dioxus::tui::launch_cfg( - app18, - Config { - ..Default::default() - }, - ), - 7 => dioxus::tui::launch_cfg( - app21, - Config { - ..Default::default() - }, - ), - 8 => dioxus::tui::launch_cfg( - app24, - Config { - ..Default::default() - }, - ), - _ => (), - }) - }, - ); - } -} - -#[derive(Props, PartialEq)] -struct BoxProps { - x: usize, - y: usize, - hue: f32, - alpha: f32, -} -#[allow(non_snake_case)] -fn Box(cx: Scope) -> Element { - let count = use_state(&cx, || 0); - - let x = cx.props.x * 2; - let y = cx.props.y * 2; - let hue = cx.props.hue; - let display_hue = cx.props.hue as u32 / 10; - let count = count.get(); - let alpha = cx.props.alpha + (count % 100) as f32; - - cx.render(rsx! { - div { - left: "{x}%", - top: "{y}%", - width: "100%", - height: "100%", - background_color: "hsl({hue}, 100%, 50%, {alpha}%)", - align_items: "center", - p{"{display_hue:03}"} - } - }) -} - -#[derive(Props, PartialEq)] -struct GridProps { - size: usize, -} -#[allow(non_snake_case)] -fn Grid(cx: Scope) -> Element { - let size = cx.props.size; - let count = use_state(&cx, || 0); - let counts = use_ref(&cx, || vec![0; size * size]); - - let ctx: TuiContext = cx.consume_context().unwrap(); - if *count.get() + 1 >= (size * size) { - ctx.quit(); - } else { - counts.with_mut(|c| { - let i = *count.current(); - c[i] += 1; - c[i] = c[i] % 360; - }); - count.with_mut(|i| { - *i += 1; - *i = *i % (size * size); - }); - } - - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - flex_direction: "column", - (0..size).map(|x| - { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - flex_direction: "row", - (0..size).map(|y| - { - let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; - let key = format!("{}-{}", x, y); - cx.render(rsx! { - Box{ - x: x, - y: y, - alpha: 100.0, - hue: alpha, - key: "{key}", - } - }) - } - ) - } - }) - } - ) - } - }) -} - -fn app3(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 3, - } - } - }) -} - -fn app6(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 6, - } - } - }) -} - -fn app9(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 9, - } - } - }) -} - -fn app12(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 12, - } - } - }) -} - -fn app15(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 15, - } - } - }) -} - -fn app18(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 18, - } - } - }) -} - -fn app21(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 21, - } - } - }) -} - -fn app24(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 24, - } - } - }) -} From 9d9aa33e259693580442c4d384822fb21cb299eb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 06:29:22 -0500 Subject: [PATCH 3/6] add builder pattern and update examples --- examples/tui_color_test.rs | 4 +--- packages/tui/src/config.rs | 31 ++++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/examples/tui_color_test.rs b/examples/tui_color_test.rs index 358fae1f6..1a5a67457 100644 --- a/examples/tui_color_test.rs +++ b/examples/tui_color_test.rs @@ -3,9 +3,7 @@ use dioxus::prelude::*; fn main() { dioxus::tui::launch_cfg( app, - dioxus::tui::Config { - rendering_mode: dioxus::tui::RenderingMode::Ansi, - }, + dioxus::tui::Config::default().with_rendering_mode(dioxus::tui::RenderingMode::Ansi), ); } diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs index 6e839d8a7..47ec817ee 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -1,11 +1,36 @@ #[derive(Clone, Copy)] +#[non_exhaustive] pub struct Config { - pub rendering_mode: RenderingMode, + pub(crate) rendering_mode: RenderingMode, /// Controls if the terminal quit when the user presses `ctrl+c`? /// To handle quiting on your own, use the [crate::TuiContext] root context. - pub ctrl_c_quit: bool, + pub(crate) ctrl_c_quit: bool, /// Controls if the terminal should dislay anything, usefull for testing. - pub headless: bool, + pub(crate) headless: bool, +} + +impl Config { + pub fn new() -> Self { + Self::default() + } + + pub fn with_rendering_mode(self, rendering_mode: RenderingMode) -> Self { + Self { + rendering_mode, + ..self + } + } + + pub fn with_ctrl_c_quit(self, ctrl_c_quit: bool) -> Self { + Self { + ctrl_c_quit, + ..self + } + } + + pub fn with_headless(self, headless: bool) -> Self { + Self { headless, ..self } + } } impl Default for Config { From efa682735e4244bb80dcf731a1181ba629c9b5e0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 06:33:03 -0500 Subject: [PATCH 4/6] ignore ctrl-c if ctrl_c_quit is false --- packages/tui/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 71f5026fd..1c4499e18 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -215,6 +215,7 @@ fn render_vdom( TermEvent::Key(key) => { if matches!(key.code, KeyCode::Char('C' | 'c')) && key.modifiers.contains(KeyModifiers::CONTROL) + && cfg.ctrl_c_quit { break; } From 91b21475522745eb9276172f60780b8f224f38bb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 06:44:48 -0500 Subject: [PATCH 5/6] update benchmark --- benches/tui_update.rs | 64 +++++--------------------------------- packages/tui/src/config.rs | 11 ++++--- 2 files changed, 15 insertions(+), 60 deletions(-) diff --git a/benches/tui_update.rs b/benches/tui_update.rs index 5a3a70b66..d46921a8d 100644 --- a/benches/tui_update.rs +++ b/benches/tui_update.rs @@ -17,62 +17,14 @@ fn tui_update(c: &mut Criterion) { &size, |b, size| { b.iter(|| match size { - 1 => dioxus::tui::launch_cfg( - app3, - Config { - headless: true, - ..Default::default() - }, - ), - 2 => dioxus::tui::launch_cfg( - app6, - Config { - headless: true, - ..Default::default() - }, - ), - 3 => dioxus::tui::launch_cfg( - app9, - Config { - headless: true, - ..Default::default() - }, - ), - 4 => dioxus::tui::launch_cfg( - app12, - Config { - headless: true, - ..Default::default() - }, - ), - 5 => dioxus::tui::launch_cfg( - app15, - Config { - headless: true, - ..Default::default() - }, - ), - 6 => dioxus::tui::launch_cfg( - app18, - Config { - headless: true, - ..Default::default() - }, - ), - 7 => dioxus::tui::launch_cfg( - app21, - Config { - headless: true, - ..Default::default() - }, - ), - 8 => dioxus::tui::launch_cfg( - app24, - Config { - headless: true, - ..Default::default() - }, - ), + 1 => dioxus::tui::launch_cfg(app3, Config::default().with_headless()), + 2 => dioxus::tui::launch_cfg(app6, Config::default().with_headless()), + 3 => dioxus::tui::launch_cfg(app9, Config::default().with_headless()), + 4 => dioxus::tui::launch_cfg(app12, Config::default().with_headless()), + 5 => dioxus::tui::launch_cfg(app15, Config::default().with_headless()), + 6 => dioxus::tui::launch_cfg(app18, Config::default().with_headless()), + 7 => dioxus::tui::launch_cfg(app21, Config::default().with_headless()), + 8 => dioxus::tui::launch_cfg(app24, Config::default().with_headless()), _ => (), }) }, diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs index 47ec817ee..fc2fb3da5 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -21,15 +21,18 @@ impl Config { } } - pub fn with_ctrl_c_quit(self, ctrl_c_quit: bool) -> Self { + pub fn with_ctrl_c_quit(self) -> Self { Self { - ctrl_c_quit, + ctrl_c_quit: true, ..self } } - pub fn with_headless(self, headless: bool) -> Self { - Self { headless, ..self } + pub fn with_headless(self) -> Self { + Self { + headless: true, + ..self + } } } From 159be32fd4f16f194a9d48ddaa206ef5a9555da4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 13:48:11 -0500 Subject: [PATCH 6/6] update documentation to refect tui changes --- docs/reference/src/platforms/tui.md | 49 ++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/reference/src/platforms/tui.md b/docs/reference/src/platforms/tui.md index e646bd4ba..2b5a3ef96 100644 --- a/docs/reference/src/platforms/tui.md +++ b/docs/reference/src/platforms/tui.md @@ -9,13 +9,12 @@ TUI support is currently quite experimental. Even the project name will change. ## Getting Set up -To tinker with TUI support, start by making a new package and adding our TUI package from git. +To tinker with TUI support, start by making a new package and adding our TUI feature. ```shell $ cargo new --bin demo $ cd demo -$ cargo add dioxus -$ cargo add rink --git https://github.com/DioxusLabs/rink.git +$ cargo add dioxus --features tui ``` @@ -27,10 +26,7 @@ Then, edit your `main.rs` with the basic template. use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + dioxus::tui::launch(app); } fn app(cx: Scope) -> Element { @@ -54,7 +50,44 @@ To run our app: $ cargo run ``` -Press "q" to close the app (yes, this is hardcoded, we are working on handlers to expose this in your code.) +Press "ctrl-c" to close the app. To switch from "ctrl-c" to just "q" to quit you can launch the app with a configuration to disable the default quit and use the root TuiContext to quit on your own. + +```rust +// main +use dioxus::events::{KeyCode, KeyboardEvent}; +use dioxus::prelude::*; +use dioxus::tui::TuiContext; + +fn main() { + dioxus::tui::launch_cfg( + app, + dioxus::tui::Config { + ctrl_c_quit: false, + // Some older terminals only support 16 colors or ANSI colors if your terminal is one of these change this to BaseColors or ANSI + rendering_mode: dioxus::tui::RenderingMode::Rgb, + }, + ); +} + +fn app(cx: Scope) -> Element { + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + + cx.render(rsx! { + div { + width: "100%", + height: "10px", + background_color: "red", + justify_content: "center", + align_items: "center", + onkeydown: move |k: KeyboardEvent| if let KeyCode::Q = k.data.key_code { + tui_ctx.quit(); + }, + + "Hello world!" + } + }) +} +``` ## Notes