1
0
Evan Almloff 3 жил өмнө
parent
commit
c8919ad77b

+ 4 - 0
Cargo.toml

@@ -88,3 +88,7 @@ harness = false
 [[bench]]
 name = "jsframework"
 harness = false
+
+[[bench]]
+name = "tui_update"
+harness = false

+ 268 - 0
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<BoxProps>) -> 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<GridProps>) -> 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,
+            }
+        }
+    })
+}

+ 260 - 0
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<BoxProps>) -> 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<GridProps>) -> 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,
+            }
+        }
+    })
+}

+ 16 - 1
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)]

+ 102 - 59
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<InputEvent>,
+}
+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<InputEvent>,
     ctx: UnboundedSender<TermEvent>,
     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();
+                }
+
+                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,
+                        // 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(())
         })