1
0
Эх сурвалжийг харах

Merge pull request #299 from DioxusLabs/jk/tui

feat: add tui directly into dioxus mainline
Jon Kelley 3 жил өмнө
parent
commit
268690ff49

+ 8 - 3
Cargo.toml

@@ -17,7 +17,6 @@ dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true }
 dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true }
 dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true }
 fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true }
-# dioxus-rsx = { path = "./packages/rsx", optional = true }
 
 dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true }
 dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true }
@@ -26,12 +25,15 @@ dioxus-ssr = { path = "./packages/ssr", version = "^0.1.3", optional = true }
 dioxus-router = { path = "./packages/router", version = "^0.1.1", optional = true }
 dioxus-mobile = { path = "./packages/mobile", version = "^0.0.3", optional = true }
 dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.0.0", optional = true }
+dioxus-tui = { path = "./packages/tui", version = "^0.1.0", optional = true }
+
+# dioxus-rsx = { path = "./packages/rsx", optional = true }
 # dioxus-liveview = { path = "./packages/liveview", optional = true }
+# macro = ["dioxus-core-macro", "dioxus-rsx"]
 
 [features]
 default = ["macro", "hooks", "html"]
 
-# macro = ["dioxus-core-macro", "dioxus-rsx"]
 macro = ["dioxus-core-macro"]
 hooks = ["dioxus-hooks"]
 html = ["dioxus-html"]
@@ -40,6 +42,8 @@ web = ["dioxus-web", "dioxus-router/web"]
 desktop = ["dioxus-desktop"]
 ayatana = ["dioxus-desktop/ayatana"]
 router = ["dioxus-router"]
+tui = ["dioxus-tui"]
+
 
 [workspace]
 members = [
@@ -54,6 +58,7 @@ members = [
     "packages/mobile",
     "packages/interpreter",
     "packages/fermi",
+    "packages/tui"
 ]
 
 [dev-dependencies]
@@ -68,7 +73,7 @@ serde_json = "1.0.79"
 rand = { version = "0.8.4", features = ["small_rng"] }
 tokio = { version = "1.16.1", features = ["full"] }
 reqwest = { version = "0.11.9", features = ["json"] }
-dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi"] }
+dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi", "tui"] }
 fern = { version = "0.6.0", features = ["colored"] }
 criterion = "0.3.5"
 thiserror = "1.0.30"

+ 27 - 0
examples/tui_border.rs

@@ -0,0 +1,27 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    let radius = use_state(&cx, || 0);
+
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "100%",
+            justify_content: "center",
+            align_items: "center",
+            background_color: "hsl(248, 53%, 58%)",
+            onwheel: move |w| radius.modify(|r| (r + w.delta_y as i8).abs()),
+
+            border_style: "solid none solid double",
+            border_width: "thick",
+            border_radius: "{radius}px",
+            border_color: "#0000FF #FF00FF #FF0000 #00FF00",
+
+            "{radius}"
+        }
+    })
+}

+ 47 - 0
examples/tui_color_test.rs

@@ -0,0 +1,47 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch_cfg(
+        app,
+        dioxus::tui::Config {
+            rendering_mode: dioxus::tui::RenderingMode::Ansi,
+        },
+    );
+}
+
+fn app(cx: Scope) -> Element {
+    let steps = 50;
+    cx.render(rsx! {
+        div{
+            width: "100%",
+            height: "100%",
+            flex_direction: "column",
+            (0..=steps).map(|x|
+                {
+                    let hue = x as f32*360.0/steps as f32;
+                    cx.render(rsx! {
+                        div{
+                            width: "100%",
+                            height: "100%",
+                            flex_direction: "row",
+                            (0..=steps).map(|y|
+                                {
+                                    let alpha = y as f32*100.0/steps as f32;
+                                    cx.render(rsx! {
+                                        div {
+                                            left: "{x}px",
+                                            top: "{y}px",
+                                            width: "10%",
+                                            height: "100%",
+                                            background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
+                                        }
+                                    })
+                                }
+                            )
+                        }
+                    })
+                }
+            )
+        }
+    })
+}

+ 66 - 0
examples/tui_components.rs

@@ -0,0 +1,66 @@
+#![allow(non_snake_case)]
+
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+#[derive(Props, PartialEq)]
+struct QuadrentProps {
+    color: String,
+    text: String,
+}
+
+fn Quadrant(cx: Scope<QuadrentProps>) -> Element {
+    cx.render(rsx! {
+        div {
+            border_width: "1px",
+            width: "50%",
+            height: "100%",
+            background_color: "{cx.props.color}",
+            justify_content: "center",
+            align_items: "center",
+
+            "{cx.props.text}"
+        }
+    })
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "100%",
+            flex_direction: "column",
+
+            div {
+                width: "100%",
+                height: "50%",
+                flex_direction: "row",
+                Quadrant{
+                    color: "red".to_string(),
+                    text: "[A]".to_string()
+                },
+                Quadrant{
+                    color: "black".to_string(),
+                    text: "[B]".to_string()
+                }
+            }
+
+            div {
+                width: "100%",
+                height: "50%",
+                flex_direction: "row",
+                Quadrant{
+                    color: "green".to_string(),
+                    text: "[C]".to_string()
+                },
+                Quadrant{
+                    color: "blue".to_string(),
+                    text: "[D]".to_string()
+                }
+            }
+        }
+    })
+}

+ 82 - 0
examples/tui_frame.rs

@@ -0,0 +1,82 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "100%",
+            flex_direction: "column",
+            // justify_content: "center",
+            // align_items: "center",
+            // flex_direction: "row",
+            // background_color: "red",
+
+            p {
+                background_color: "black",
+                flex_direction: "column",
+                justify_content: "center",
+                align_items: "center",
+                // height: "10%",
+                "hi"
+                "hi"
+                "hi"
+            }
+
+            li {
+                background_color: "red",
+                flex_direction: "column",
+                justify_content: "center",
+                align_items: "center",
+                // height: "10%",
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+            }
+            li {
+                background_color: "blue",
+                flex_direction: "column",
+                justify_content: "center",
+                align_items: "center",
+                // height: "10%",
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+            }
+            p {
+                background_color: "yellow",
+                "asd"
+            }
+            p {
+                background_color: "green",
+                "asd"
+            }
+            p {
+                background_color: "white",
+                "asd"
+            }
+            p {
+                background_color: "cyan",
+                "asd"
+            }
+        }
+    })
+}

+ 101 - 0
examples/tui_hover.rs

@@ -0,0 +1,101 @@
+use std::{convert::TryInto, sync::Arc};
+
+use dioxus::{events::MouseData, prelude::*};
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    fn to_str(c: &[i32; 3]) -> String {
+        "#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::<String>()
+    }
+
+    fn get_brightness(m: Arc<MouseData>) -> i32 {
+        let b: i32 = m.buttons.count_ones().try_into().unwrap();
+        127 * b
+    }
+
+    let q1_color = use_state(&cx, || [200; 3]);
+    let q2_color = use_state(&cx, || [200; 3]);
+    let q3_color = use_state(&cx, || [200; 3]);
+    let q4_color = use_state(&cx, || [200; 3]);
+
+    let q1_color_str = to_str(q1_color);
+    let q2_color_str = to_str(q2_color);
+    let q3_color_str = to_str(q3_color);
+    let q4_color_str = to_str(q4_color);
+
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "100%",
+            flex_direction: "column",
+
+            div {
+                width: "100%",
+                height: "50%",
+                flex_direction: "row",
+                div {
+                    border_width: "1px",
+                    width: "50%",
+                    height: "100%",
+                    justify_content: "center",
+                    align_items: "center",
+                    background_color: "{q1_color_str}",
+                    onmouseenter: move |m| q1_color.set([get_brightness(m.data), 0, 0]),
+                    onmousedown: move |m| q1_color.set([get_brightness(m.data), 0, 0]),
+                    onmouseup: move |m| q1_color.set([get_brightness(m.data), 0, 0]),
+                    onwheel: move |w| q1_color.set([q1_color[0] + (10.0*w.delta_y) as i32, 0, 0]),
+                    onmouseleave: move |_| q1_color.set([200; 3]),
+                    "click me"
+                }
+                div {
+                    width: "50%",
+                    height: "100%",
+                    justify_content: "center",
+                    align_items: "center",
+                    background_color: "{q2_color_str}",
+                    onmouseenter: move |m| q2_color.set([get_brightness(m.data); 3]),
+                    onmousedown: move |m| q2_color.set([get_brightness(m.data); 3]),
+                    onmouseup: move |m| q2_color.set([get_brightness(m.data); 3]),
+                    onwheel: move |w| q2_color.set([q2_color[0] + (10.0*w.delta_y) as i32;3]),
+                    onmouseleave: move |_| q2_color.set([200; 3]),
+                    "click me"
+                }
+            }
+
+            div {
+                width: "100%",
+                height: "50%",
+                flex_direction: "row",
+                div {
+                    width: "50%",
+                    height: "100%",
+                    justify_content: "center",
+                    align_items: "center",
+                    background_color: "{q3_color_str}",
+                    onmouseenter: move |m| q3_color.set([0, get_brightness(m.data), 0]),
+                    onmousedown: move |m| q3_color.set([0, get_brightness(m.data), 0]),
+                    onmouseup: move |m| q3_color.set([0, get_brightness(m.data), 0]),
+                    onwheel: move |w| q3_color.set([0, q3_color[1] + (10.0*w.delta_y) as i32, 0]),
+                    onmouseleave: move |_| q3_color.set([200; 3]),
+                    "click me"
+                }
+                div {
+                    width: "50%",
+                    height: "100%",
+                    justify_content: "center",
+                    align_items: "center",
+                    background_color: "{q4_color_str}",
+                    onmouseenter: move |m| q4_color.set([0, 0, get_brightness(m.data)]),
+                    onmousedown: move |m| q4_color.set([0, 0, get_brightness(m.data)]),
+                    onmouseup: move |m| q4_color.set([0, 0, get_brightness(m.data)]),
+                    onwheel: move |w| q4_color.set([0, 0, q4_color[2] + (10.0*w.delta_y) as i32]),
+                    onmouseleave: move |_| q4_color.set([200; 3]),
+                    "click me"
+                }
+            }
+        }
+    })
+}

+ 58 - 0
examples/tui_keys.rs

@@ -0,0 +1,58 @@
+use dioxus::events::WheelEvent;
+use dioxus::prelude::*;
+use dioxus_html::on::{KeyboardEvent, MouseEvent};
+use dioxus_html::KeyCode;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    let key = use_state(&cx, || "".to_string());
+    let mouse = use_state(&cx, || (0, 0));
+    let count = use_state(&cx, || 0);
+    let buttons = use_state(&cx, || 0);
+    let mouse_clicked = use_state(&cx, || false);
+
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "10px",
+            background_color: "red",
+            justify_content: "center",
+            align_items: "center",
+            flex_direction: "column",
+            onkeydown: move |evt: KeyboardEvent| {
+                match evt.data.key_code {
+                    KeyCode::LeftArrow => count.set(count + 1),
+                    KeyCode::RightArrow => count.set(count - 1),
+                    KeyCode::UpArrow => count.set(count + 10),
+                    KeyCode::DownArrow => count.set(count - 10),
+                    _ => {},
+                }
+                key.set(format!("{:?} repeating: {:?}", evt.key, evt.repeat));
+            },
+            onwheel: move |evt: WheelEvent| {
+                count.set(count + evt.data.delta_y as i64);
+            },
+            ondrag: move |evt: MouseEvent| {
+                mouse.set((evt.data.screen_x, evt.data.screen_y));
+            },
+            onmousedown: move |evt: MouseEvent| {
+                mouse.set((evt.data.screen_x, evt.data.screen_y));
+                buttons.set(evt.data.buttons);
+                mouse_clicked.set(true);
+            },
+            onmouseup: move |evt: MouseEvent| {
+                buttons.set(evt.data.buttons);
+                mouse_clicked.set(false);
+            },
+
+            "count: {count:?}",
+            "key: {key}",
+            "mouse buttons: {buttons:b}",
+            "mouse pos: {mouse:?}",
+            "mouse button pressed: {mouse_clicked}"
+        }
+    })
+}

+ 28 - 0
examples/tui_list.rs

@@ -0,0 +1,28 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "100%",
+            flex_direction: "column",
+            border_width: "1px",
+
+            h1 { height: "2px", color: "green",
+                "that's awesome!"
+            }
+
+            ul {
+                flex_direction: "column",
+                padding_left: "3px",
+                (0..10).map(|i| rsx!(
+                    "> hello {i}"
+                ))
+            }
+        }
+    })
+}

+ 50 - 0
examples/tui_margin.rs

@@ -0,0 +1,50 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "100%",
+            flex_direction: "column",
+            background_color: "black",
+            // margin_right: "10px",
+
+            div {
+                width: "70%",
+                height: "70%",
+                background_color: "green",
+                // margin_left: "4px",
+
+                div {
+                    width: "100%",
+                    height: "100%",
+
+                    margin_top: "2px",
+                    margin_bottom: "2px",
+                    margin_left: "2px",
+                    margin_right: "2px",
+                    flex_shrink: "0",
+
+                    background_color: "red",
+                    justify_content: "center",
+                    align_items: "center",
+                    flex_direction: "column",
+
+                    // padding_top: "2px",
+                    // padding_bottom: "2px",
+                    // padding_left: "4px",
+                    // padding_right: "4px",
+
+                    "[A]"
+                    "[A]"
+                    "[A]"
+                    "[A]"
+                }
+            }
+        }
+    })
+}

+ 60 - 0
examples/tui_quadrants.rs

@@ -0,0 +1,60 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "100%",
+            flex_direction: "column",
+
+            div {
+                width: "100%",
+                height: "50%",
+                flex_direction: "row",
+                div {
+                    border_width: "1px",
+                    width: "50%",
+                    height: "100%",
+                    background_color: "red",
+                    justify_content: "center",
+                    align_items: "center",
+                    "[A]"
+                }
+                div {
+                    width: "50%",
+                    height: "100%",
+                    background_color: "black",
+                    justify_content: "center",
+                    align_items: "center",
+                    "[B]"
+                }
+            }
+
+            div {
+                width: "100%",
+                height: "50%",
+                flex_direction: "row",
+                div {
+                    width: "50%",
+                    height: "100%",
+                    background_color: "green",
+                    justify_content: "center",
+                    align_items: "center",
+                    "[C]"
+                }
+                div {
+                    width: "50%",
+                    height: "100%",
+                    background_color: "blue",
+                    justify_content: "center",
+                    align_items: "center",
+                    "[D]"
+                }
+            }
+        }
+    })
+}

+ 19 - 0
examples/tui_readme.rs

@@ -0,0 +1,19 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "10px",
+            background_color: "red",
+            justify_content: "center",
+            align_items: "center",
+
+            "Hello world!"
+        }
+    })
+}

+ 32 - 0
examples/tui_task.rs

@@ -0,0 +1,32 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    let count = use_state(&cx, || 0);
+
+    use_future(&cx, (), move |_| {
+        let count = count.to_owned();
+        let update = cx.schedule_update();
+        async move {
+            loop {
+                count.with_mut(|f| *f += 1);
+                tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
+                update();
+            }
+        }
+    });
+
+    cx.render(rsx! {
+        div { width: "100%",
+            div { width: "50%", height: "5px", background_color: "blue", justify_content: "center", align_items: "center",
+                "Hello {count}!"
+            }
+            div { width: "50%", height: "10px", background_color: "red", justify_content: "center", align_items: "center",
+                "Hello {count}!"
+            }
+        }
+    })
+}

+ 113 - 0
examples/tui_text.rs

@@ -0,0 +1,113 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::tui::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    let alpha = use_state(&cx, || 100);
+
+    cx.render(rsx! {
+        div {
+            width: "100%",
+            height: "100%",
+            flex_direction: "column",
+            onwheel: move |evt| alpha.set((**alpha + evt.data.delta_y as i64).min(100).max(0)),
+
+            p {
+                background_color: "black",
+                flex_direction: "column",
+                justify_content: "center",
+                align_items: "center",
+                color: "green",
+                "hi"
+                "hi"
+                "hi"
+            }
+
+            li {
+                background_color: "red",
+                flex_direction: "column",
+                justify_content: "center",
+                align_items: "center",
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+                "bib"
+            }
+            li {
+                background_color: "blue",
+                flex_direction: "column",
+                justify_content: "center",
+                align_items: "center",
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+                "zib"
+            }
+            p {
+                background_color: "yellow",
+                "asd"
+            }
+            p {
+                background_color: "green",
+                "asd"
+            }
+            p {
+                background_color: "white",
+                "asd"
+            }
+            p {
+                background_color: "cyan",
+                "asd"
+            }
+            div {
+                font_weight: "bold",
+                color: "#666666",
+                p {
+                    "bold"
+                }
+                p {
+                    font_weight: "normal",
+                    " normal"
+                }
+            }
+            p {
+                font_style: "italic",
+                color: "red",
+                "italic"
+            }
+            p {
+                text_decoration: "underline",
+                color: "rgba(255, 255, 255)",
+                "underline"
+            }
+            p {
+                text_decoration: "line-through",
+                color: "hsla(10, 100%, 70%)",
+                "line-through"
+            }
+            div{
+                position: "absolute",
+                top: "1px",
+                background_color: "rgba(255, 0, 0, 50%)",
+                width: "100%",
+                p {
+                    color: "rgba(255, 255, 255, {alpha}%)",
+                    background_color: "rgba(100, 100, 100, {alpha}%)",
+                    "rgba(255, 255, 255, {alpha}%)"
+                }
+                p {
+                    color: "rgba(255, 255, 255, 100%)",
+                    "rgba(255, 255, 255, 100%)"
+                }
+            }
+        }
+    })
+}

+ 6 - 0
packages/hooks/src/usestate.rs

@@ -310,6 +310,12 @@ impl<'a, T: 'static + Display> std::fmt::Display for UseState<T> {
     }
 }
 
+impl<'a, T: std::fmt::Binary> std::fmt::Binary for UseState<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{:b}", self.current_val.as_ref())
+    }
+}
+
 impl<T: PartialEq> PartialEq<T> for UseState<T> {
     fn eq(&self, other: &T) -> bool {
         self.current_val.as_ref() == other

+ 2 - 0
packages/tui/.gitignore

@@ -0,0 +1,2 @@
+/target
+Cargo.lock

+ 2 - 0
packages/tui/.vscode/spellright.dict

@@ -0,0 +1,2 @@
+esque
+Tui

+ 23 - 0
packages/tui/Cargo.toml

@@ -0,0 +1,23 @@
+[package]
+name = "dioxus-tui"
+version = "0.1.0"
+edition = "2018"
+
+
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+tui = "0.17.0"
+crossterm = "0.23.0"
+anyhow = "1.0.42"
+thiserror = "1.0.24"
+dioxus-core = { path = "../core" }
+dioxus-html = { path = "../html" }
+hecs = "0.7.3"
+ctrlc = "3.2.1"
+bumpalo = { version = "3.8.0", features = ["boxed"] }
+tokio = { version = "1.15.0", features = ["full"] }
+futures = "0.3.19"
+stretch2 = "0.4.0"
+

+ 95 - 0
packages/tui/README.md

@@ -0,0 +1,95 @@
+<div align="center">
+  <h1>Rink</h1>
+  <p>
+    <strong>Beautiful terminal user interfaces in Rust with <a href="https://dioxuslabs.com/">Dioxus </a>.</strong>
+  </p>
+</div>
+
+<div align="center">
+  <!-- Crates version -->
+  <a href="https://crates.io/crates/dioxus">
+    <img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
+    alt="Crates.io version" />
+  </a>
+  <!-- Downloads -->
+  <a href="https://crates.io/crates/dioxus">
+    <img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
+      alt="Download" />
+  </a>
+  <!-- docs -->
+  <a href="https://docs.rs/dioxus">
+    <img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
+      alt="docs.rs docs" />
+  </a>
+  <!-- CI -->
+  <a href="https://github.com/jkelleyrtp/dioxus/actions">
+    <img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
+      alt="CI status" />
+  </a>
+
+  <!--Awesome -->
+  <a href="https://github.com/dioxuslabs/awesome-dioxus">
+    <img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
+  </a>
+  <!-- Discord -->
+  <a href="https://discord.gg/XgGxMSkvUM">
+    <img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
+  </a>
+</div>
+
+
+<br/>
+
+Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus.
+
+```rust
+fn app(cx: Scope) -> Element {
+    cx.render(rsx!{
+        div {
+            width: "100%",
+            height: "10px",
+            background_color: "red",
+            justify_content: "center",
+            align_items: "center",
+            "Hello world!"
+        }
+    })
+}
+```
+
+![demo app](examples/example.png)
+
+## Background
+
+You can use Html-like semantics with stylesheets, inline styles, tree hierarchy, components, and more in your  [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application.
+
+Rink is basically a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful.
+
+## Limitations
+
+- **Subset of Html**
+Terminals can only render a subset of HTML. We support as much as we can.
+- **Particular frontend design**
+Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
+
+
+## Status
+
+**WARNING: Rink is currently under construction!**
+
+Rendering a VirtualDom works fine, but the ecosystem of hooks is not yet ready. Additionally, some bugs in the flexbox implementation might be quirky at times.
+
+## Features
+
+Rink features:
+- [x] Flexbox based layout system
+- [ ] CSS selectors
+- [x] inline CSS support
+- [ ] Built-in focusing system
+- [ ] high-quality keyboard support
+- [ ] Support for events, hooks, and callbacks
+* [ ] Html tags<sup>1</sup>
+
+<sup>1</sup> Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality.
+
+

BIN
packages/tui/examples/example.png


+ 5 - 0
packages/tui/examples/ui.rsx

@@ -0,0 +1,5 @@
+div {
+    div {
+
+    }    
+}

+ 908 - 0
packages/tui/src/attributes.rs

@@ -0,0 +1,908 @@
+/*
+- [ ] pub display: Display,
+- [x] pub position_type: PositionType,  --> kinda, stretch doesnt support everything
+- [ ] pub direction: Direction,
+
+- [x] pub flex_direction: FlexDirection,
+- [x] pub flex_wrap: FlexWrap,
+- [x] pub flex_grow: f32,
+- [x] pub flex_shrink: f32,
+- [x] pub flex_basis: Dimension,
+
+- [x] pub overflow: Overflow, ---> kinda implemented... stretch doesnt have support for directional overflow
+
+- [x] pub align_items: AlignItems,
+- [x] pub align_self: AlignSelf,
+- [x] pub align_content: AlignContent,
+
+- [x] pub margin: Rect<Dimension>,
+- [x] pub padding: Rect<Dimension>,
+
+- [x] pub justify_content: JustifyContent,
+- [ ] pub position: Rect<Dimension>,
+- [ ] pub border: Rect<Dimension>,
+
+- [ ] pub size: Size<Dimension>, ----> ??? seems to only be relevant for input?
+- [ ] pub min_size: Size<Dimension>,
+- [ ] pub max_size: Size<Dimension>,
+
+- [ ] pub aspect_ratio: Number,
+*/
+
+use stretch2::{prelude::*, style::PositionType, style::Style};
+
+use crate::style::{RinkColor, RinkStyle};
+
+pub struct StyleModifer {
+    pub style: Style,
+    pub tui_style: RinkStyle,
+    pub tui_modifier: TuiModifier,
+}
+
+#[derive(Default)]
+pub struct TuiModifier {
+    pub borders: Borders,
+}
+
+#[derive(Default)]
+pub struct Borders {
+    pub top: BorderEdge,
+    pub right: BorderEdge,
+    pub bottom: BorderEdge,
+    pub left: BorderEdge,
+}
+
+impl Borders {
+    fn slice(&mut self) -> [&mut BorderEdge; 4] {
+        [
+            &mut self.top,
+            &mut self.right,
+            &mut self.bottom,
+            &mut self.left,
+        ]
+    }
+}
+
+pub struct BorderEdge {
+    pub color: Option<RinkColor>,
+    pub style: BorderStyle,
+    pub width: UnitSystem,
+    pub radius: UnitSystem,
+}
+
+impl Default for BorderEdge {
+    fn default() -> Self {
+        Self {
+            color: None,
+            style: BorderStyle::NONE,
+            width: UnitSystem::Point(0.0),
+            radius: UnitSystem::Point(0.0),
+        }
+    }
+}
+
+#[derive(Clone, Copy)]
+pub enum BorderStyle {
+    DOTTED,
+    DASHED,
+    SOLID,
+    DOUBLE,
+    GROOVE,
+    RIDGE,
+    INSET,
+    OUTSET,
+    HIDDEN,
+    NONE,
+}
+
+impl BorderStyle {
+    pub fn symbol_set(&self) -> Option<tui::symbols::line::Set> {
+        use tui::symbols::line::*;
+        const DASHED: Set = Set {
+            horizontal: "╌",
+            vertical: "╎",
+            ..NORMAL
+        };
+        const DOTTED: Set = Set {
+            horizontal: "┈",
+            vertical: "┊",
+            ..NORMAL
+        };
+        match self {
+            BorderStyle::DOTTED => Some(DOTTED),
+            BorderStyle::DASHED => Some(DASHED),
+            BorderStyle::SOLID => Some(NORMAL),
+            BorderStyle::DOUBLE => Some(DOUBLE),
+            BorderStyle::GROOVE => Some(NORMAL),
+            BorderStyle::RIDGE => Some(NORMAL),
+            BorderStyle::INSET => Some(NORMAL),
+            BorderStyle::OUTSET => Some(NORMAL),
+            BorderStyle::HIDDEN => None,
+            BorderStyle::NONE => None,
+        }
+    }
+}
+
+/// applies the entire html namespace defined in dioxus-html
+pub fn apply_attributes(
+    //
+    name: &str,
+    value: &str,
+    style: &mut StyleModifer,
+) {
+    match name {
+        "align-content"
+        | "align-items"
+        | "align-self" => apply_align(name, value, style),
+
+        "animation"
+        | "animation-delay"
+        | "animation-direction"
+        | "animation-duration"
+        | "animation-fill-mode"
+        | "animation-iteration-count"
+        | "animation-name"
+        | "animation-play-state"
+        | "animation-timing-function" => apply_animation(name, value, style),
+
+        "backface-visibility" => {}
+
+        "background"
+        | "background-attachment"
+        | "background-clip"
+        | "background-color"
+        | "background-image"
+        | "background-origin"
+        | "background-position"
+        | "background-repeat"
+        | "background-size" => apply_background(name, value, style),
+
+        "border"
+        | "border-bottom"
+        | "border-bottom-color"
+        | "border-bottom-left-radius"
+        | "border-bottom-right-radius"
+        | "border-bottom-style"
+        | "border-bottom-width"
+        | "border-collapse"
+        | "border-color"
+        | "border-image"
+        | "border-image-outset"
+        | "border-image-repeat"
+        | "border-image-slice"
+        | "border-image-source"
+        | "border-image-width"
+        | "border-left"
+        | "border-left-color"
+        | "border-left-style"
+        | "border-left-width"
+        | "border-radius"
+        | "border-right"
+        | "border-right-color"
+        | "border-right-style"
+        | "border-right-width"
+        | "border-spacing"
+        | "border-style"
+        | "border-top"
+        | "border-top-color"
+        | "border-top-left-radius"
+        | "border-top-right-radius"
+        | "border-top-style"
+        | "border-top-width"
+        | "border-width" => apply_border(name, value, style),
+
+        "bottom" => {}
+        "box-shadow" => {}
+        "box-sizing" => {}
+        "caption-side" => {}
+        "clear" => {}
+        "clip" => {}
+
+        "color" => {
+            if let Ok(c) = value.parse() {
+                style.tui_style.fg.replace(c);
+            }
+        }
+
+        "column-count"
+        | "column-fill"
+        | "column-gap"
+        | "column-rule"
+        | "column-rule-color"
+        | "column-rule-style"
+        | "column-rule-width"
+        | "column-span"
+        // add column-width
+        | "column-width" => apply_column(name, value, style),
+
+        "columns" => {}
+
+        "content" => {}
+        "counter-increment" => {}
+        "counter-reset" => {}
+
+        "cursor" => {}
+        "direction" => {
+            match value {
+                "ltr" => style.style.direction = Direction::LTR,
+                "rtl" => style.style.direction = Direction::RTL,
+                _ => {}
+            }
+        }
+
+        "display" => apply_display(name, value, style),
+
+        "empty-cells" => {}
+
+        "flex"
+        | "flex-basis"
+        | "flex-direction"
+        | "flex-flow"
+        | "flex-grow"
+        | "flex-shrink"
+        | "flex-wrap" => apply_flex(name, value, style),
+
+        "float" => {}
+
+        "font"
+        | "font-family"
+        | "font-size"
+        | "font-size-adjust"
+        | "font-stretch"
+        | "font-style"
+        | "font-variant"
+        | "font-weight" => apply_font(name, value, style),
+
+        "height" => {
+            if let Some(v) = parse_value(value){
+                style.style.size.height = match v {
+                    UnitSystem::Percent(v)=> Dimension::Percent(v/100.0),
+                    UnitSystem::Point(v)=> Dimension::Points(v),
+                };
+            }
+        }
+        "justify-content" => {
+            use JustifyContent::*;
+            style.style.justify_content = match value {
+                "flex-start" => FlexStart,
+                "flex-end" => FlexEnd,
+                "center" => Center,
+                "space-between" => SpaceBetween,
+                "space-around" => SpaceAround,
+                "space-evenly" => SpaceEvenly,
+                _ => FlexStart,
+            };
+        }
+        "left" => {}
+        "letter-spacing" => {}
+        "line-height" => {}
+
+        "list-style"
+        | "list-style-image"
+        | "list-style-position"
+        | "list-style-type" => {}
+
+        "margin"
+        | "margin-bottom"
+        | "margin-left"
+        | "margin-right"
+        | "margin-top" => apply_margin(name, value, style),
+
+        "max-height" => {}
+        "max-width" => {}
+        "min-height" => {}
+        "min-width" => {}
+
+        "opacity" => {}
+        "order" => {}
+        "outline" => {}
+
+        "outline-color"
+        | "outline-offset"
+        | "outline-style"
+        | "outline-width" => {}
+
+        "overflow"
+        | "overflow-x"
+        | "overflow-y" => apply_overflow(name, value, style),
+
+        "padding"
+        | "padding-bottom"
+        | "padding-left"
+        | "padding-right"
+        | "padding-top" => apply_padding(name, value, style),
+
+        "page-break-after"
+        | "page-break-before"
+        | "page-break-inside" => {}
+
+        "perspective"
+        | "perspective-origin" => {}
+
+        "position" => {
+            match value {
+                "static" => {}
+                "relative" => style.style.position_type = PositionType::Relative,
+                "fixed" => {}
+                "absolute" => style.style.position_type = PositionType::Absolute,
+                "sticky" => {}
+                _ => {}
+            }
+
+        }
+
+        "pointer-events" => {}
+
+        "quotes" => {}
+        "resize" => {}
+        "right" => {}
+        "tab-size" => {}
+        "table-layout" => {}
+
+        "text-align"
+        | "text-align-last"
+        | "text-decoration"
+        | "text-decoration-color"
+        | "text-decoration-line"
+        | "text-decoration-style"
+        | "text-indent"
+        | "text-justify"
+        | "text-overflow"
+        | "text-shadow"
+        | "text-transform" => apply_text(name, value, style),
+
+        "top" => {}
+
+        "transform"
+        | "transform-origin"
+        | "transform-style" => apply_transform(name, value, style),
+
+        "transition"
+        | "transition-delay"
+        | "transition-duration"
+        | "transition-property"
+        | "transition-timing-function" => apply_transition(name, value, style),
+
+        "vertical-align" => {}
+        "visibility" => {}
+        "white-space" => {}
+        "width" => {
+            if let Some(v) = parse_value(value){
+                style.style.size.width = match v {
+                    UnitSystem::Percent(v)=> Dimension::Percent(v/100.0),
+                    UnitSystem::Point(v)=> Dimension::Points(v),
+                };
+            }
+        }
+        "word-break" => {}
+        "word-spacing" => {}
+        "word-wrap" => {}
+        "z-index" => {}
+        _ => {}
+    }
+}
+
+#[derive(Clone, Copy)]
+pub enum UnitSystem {
+    Percent(f32),
+    Point(f32),
+}
+
+fn parse_value(value: &str) -> Option<UnitSystem> {
+    if value.ends_with("px") {
+        if let Ok(px) = value.trim_end_matches("px").parse::<f32>() {
+            Some(UnitSystem::Point(px))
+        } else {
+            None
+        }
+    } else if value.ends_with('%') {
+        if let Ok(pct) = value.trim_end_matches('%').parse::<f32>() {
+            Some(UnitSystem::Percent(pct))
+        } else {
+            None
+        }
+    } else {
+        None
+    }
+}
+
+fn apply_overflow(name: &str, value: &str, style: &mut StyleModifer) {
+    match name {
+        // todo: add more overflow support to stretch2
+        "overflow" | "overflow-x" | "overflow-y" => {
+            style.style.overflow = match value {
+                "auto" => Overflow::Visible,
+                "hidden" => Overflow::Hidden,
+                "scroll" => Overflow::Scroll,
+                "visible" => Overflow::Visible,
+                _ => Overflow::Visible,
+            };
+        }
+        _ => {}
+    }
+}
+
+fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) {
+    style.style.display = match value {
+        "flex" => Display::Flex,
+        "block" => Display::None,
+        _ => Display::Flex,
+    }
+
+    // TODO: there are way more variants
+    // stretch needs to be updated to handle them
+    //
+    // "block" => Display::Block,
+    // "inline" => Display::Inline,
+    // "inline-block" => Display::InlineBlock,
+    // "inline-table" => Display::InlineTable,
+    // "list-item" => Display::ListItem,
+    // "run-in" => Display::RunIn,
+    // "table" => Display::Table,
+    // "table-caption" => Display::TableCaption,
+    // "table-cell" => Display::TableCell,
+    // "table-column" => Display::TableColumn,
+    // "table-column-group" => Display::TableColumnGroup,
+    // "table-footer-group" => Display::TableFooterGroup,
+    // "table-header-group" => Display::TableHeaderGroup,
+    // "table-row" => Display::TableRow,
+    // "table-row-group" => Display::TableRowGroup,
+    // "none" => Display::None,
+    // _ => Display::Inline,
+}
+
+fn apply_background(name: &str, value: &str, style: &mut StyleModifer) {
+    match name {
+        "background-color" => {
+            if let Ok(c) = value.parse() {
+                style.tui_style.bg.replace(c);
+            }
+        }
+        "background" => {}
+        "background-attachment" => {}
+        "background-clip" => {}
+        "background-image" => {}
+        "background-origin" => {}
+        "background-position" => {}
+        "background-repeat" => {}
+        "background-size" => {}
+        _ => {}
+    }
+}
+
+fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
+    fn parse_border_style(v: &str) -> BorderStyle {
+        match v {
+            "dotted" => BorderStyle::DOTTED,
+            "dashed" => BorderStyle::DASHED,
+            "solid" => BorderStyle::SOLID,
+            "double" => BorderStyle::DOUBLE,
+            "groove" => BorderStyle::GROOVE,
+            "ridge" => BorderStyle::RIDGE,
+            "inset" => BorderStyle::INSET,
+            "outset" => BorderStyle::OUTSET,
+            "none" => BorderStyle::NONE,
+            "hidden" => BorderStyle::HIDDEN,
+            _ => todo!(),
+        }
+    }
+    match name {
+        "border" => {}
+        "border-bottom" => {}
+        "border-bottom-color" => {
+            if let Ok(c) = value.parse() {
+                style.tui_modifier.borders.bottom.color = Some(c);
+            }
+        }
+        "border-bottom-left-radius" => {
+            if let Some(v) = parse_value(value) {
+                style.tui_modifier.borders.left.radius = v;
+            }
+        }
+        "border-bottom-right-radius" => {
+            if let Some(v) = parse_value(value) {
+                style.tui_modifier.borders.right.radius = v;
+            }
+        }
+        "border-bottom-style" => {
+            style.tui_modifier.borders.bottom.style = parse_border_style(value)
+        }
+        "border-bottom-width" => {
+            if let Some(v) = parse_value(value) {
+                style.tui_modifier.borders.bottom.width = v;
+            }
+        }
+        "border-collapse" => {}
+        "border-color" => {
+            let values: Vec<_> = value.split(' ').collect();
+            if values.len() == 1 {
+                if let Ok(c) = values[0].parse() {
+                    style
+                        .tui_modifier
+                        .borders
+                        .slice()
+                        .iter_mut()
+                        .for_each(|b| b.color = Some(c));
+                }
+            } else {
+                for (v, b) in values
+                    .into_iter()
+                    .zip(style.tui_modifier.borders.slice().iter_mut())
+                {
+                    if let Ok(c) = v.parse() {
+                        b.color = Some(c);
+                    }
+                }
+            }
+        }
+        "border-image" => {}
+        "border-image-outset" => {}
+        "border-image-repeat" => {}
+        "border-image-slice" => {}
+        "border-image-source" => {}
+        "border-image-width" => {}
+        "border-left" => {}
+        "border-left-color" => {
+            if let Ok(c) = value.parse() {
+                style.tui_modifier.borders.left.color = Some(c);
+            }
+        }
+        "border-left-style" => style.tui_modifier.borders.left.style = parse_border_style(value),
+        "border-left-width" => {
+            if let Some(v) = parse_value(value) {
+                style.tui_modifier.borders.left.width = v;
+            }
+        }
+        "border-radius" => {
+            let values: Vec<_> = value.split(' ').collect();
+            if values.len() == 1 {
+                if let Some(r) = parse_value(values[0]) {
+                    style
+                        .tui_modifier
+                        .borders
+                        .slice()
+                        .iter_mut()
+                        .for_each(|b| b.radius = r);
+                }
+            } else {
+                for (v, b) in values
+                    .into_iter()
+                    .zip(style.tui_modifier.borders.slice().iter_mut())
+                {
+                    if let Some(r) = parse_value(v) {
+                        b.radius = r;
+                    }
+                }
+            }
+        }
+        "border-right" => {}
+        "border-right-color" => {
+            if let Ok(c) = value.parse() {
+                style.tui_modifier.borders.right.color = Some(c);
+            }
+        }
+        "border-right-style" => style.tui_modifier.borders.right.style = parse_border_style(value),
+        "border-right-width" => {
+            if let Some(v) = parse_value(value) {
+                style.tui_modifier.borders.right.width = v;
+            }
+        }
+        "border-spacing" => {}
+        "border-style" => {
+            let values: Vec<_> = value.split(' ').collect();
+            if values.len() == 1 {
+                let border_style = parse_border_style(values[0]);
+                style
+                    .tui_modifier
+                    .borders
+                    .slice()
+                    .iter_mut()
+                    .for_each(|b| b.style = border_style);
+            } else {
+                for (v, b) in values
+                    .into_iter()
+                    .zip(style.tui_modifier.borders.slice().iter_mut())
+                {
+                    b.style = parse_border_style(v);
+                }
+            }
+        }
+        "border-top" => {}
+        "border-top-color" => {
+            if let Ok(c) = value.parse() {
+                style.tui_modifier.borders.top.color = Some(c);
+            }
+        }
+        "border-top-left-radius" => {
+            if let Some(v) = parse_value(value) {
+                style.tui_modifier.borders.left.radius = v;
+            }
+        }
+        "border-top-right-radius" => {
+            if let Some(v) = parse_value(value) {
+                style.tui_modifier.borders.right.radius = v;
+            }
+        }
+        "border-top-style" => style.tui_modifier.borders.top.style = parse_border_style(value),
+        "border-top-width" => {
+            if let Some(v) = parse_value(value) {
+                style.tui_modifier.borders.top.width = v;
+            }
+        }
+        "border-width" => {
+            let values: Vec<_> = value.split(' ').collect();
+            if values.len() == 1 {
+                if let Some(w) = parse_value(values[0]) {
+                    style
+                        .tui_modifier
+                        .borders
+                        .slice()
+                        .iter_mut()
+                        .for_each(|b| b.width = w);
+                }
+            } else {
+                for (v, b) in values
+                    .into_iter()
+                    .zip(style.tui_modifier.borders.slice().iter_mut())
+                {
+                    if let Some(w) = parse_value(v) {
+                        b.width = w;
+                    }
+                }
+            }
+        }
+        _ => (),
+    }
+}
+
+fn apply_animation(name: &str, _value: &str, _style: &mut StyleModifer) {
+    match name {
+        "animation" => {}
+        "animation-delay" => {}
+        "animation-direction =>{}" => {}
+        "animation-duration" => {}
+        "animation-fill-mode" => {}
+        "animation-itera =>{}tion-count" => {}
+        "animation-name" => {}
+        "animation-play-state" => {}
+        "animation-timing-function" => {}
+        _ => {}
+    }
+}
+
+fn apply_column(name: &str, _value: &str, _style: &mut StyleModifer) {
+    match name {
+        "column-count" => {}
+        "column-fill" => {}
+        "column-gap" => {}
+        "column-rule" => {}
+        "column-rule-color" => {}
+        "column-rule-style" => {}
+        "column-rule-width" => {}
+        "column-span" => {}
+        "column-width" => {}
+        _ => {}
+    }
+}
+
+fn apply_flex(name: &str, value: &str, style: &mut StyleModifer) {
+    // - [x] pub flex_direction: FlexDirection,
+    // - [x] pub flex_wrap: FlexWrap,
+    // - [x] pub flex_grow: f32,
+    // - [x] pub flex_shrink: f32,
+    // - [x] pub flex_basis: Dimension,
+
+    match name {
+        "flex" => {}
+        "flex-direction" => {
+            use FlexDirection::*;
+            style.style.flex_direction = match value {
+                "row" => Row,
+                "row-reverse" => RowReverse,
+                "column" => Column,
+                "column-reverse" => ColumnReverse,
+                _ => Row,
+            };
+        }
+        "flex-basis" => {
+            if let Some(v) = parse_value(value) {
+                style.style.flex_basis = match v {
+                    UnitSystem::Percent(v) => Dimension::Percent(v / 100.0),
+                    UnitSystem::Point(v) => Dimension::Points(v),
+                };
+            }
+        }
+        "flex-flow" => {}
+        "flex-grow" => {
+            if let Ok(val) = value.parse::<f32>() {
+                style.style.flex_grow = val;
+            }
+        }
+        "flex-shrink" => {
+            if let Ok(px) = value.parse::<f32>() {
+                style.style.flex_shrink = px;
+            }
+        }
+        "flex-wrap" => {
+            use FlexWrap::*;
+            style.style.flex_wrap = match value {
+                "nowrap" => NoWrap,
+                "wrap" => Wrap,
+                "wrap-reverse" => WrapReverse,
+                _ => NoWrap,
+            };
+        }
+        _ => {}
+    }
+}
+
+fn apply_font(name: &str, value: &str, style: &mut StyleModifer) {
+    use tui::style::Modifier;
+    match name {
+        "font" => (),
+        "font-family" => (),
+        "font-size" => (),
+        "font-size-adjust" => (),
+        "font-stretch" => (),
+        "font-style" => match value {
+            "italic" => style.tui_style = style.tui_style.add_modifier(Modifier::ITALIC),
+            "oblique" => style.tui_style = style.tui_style.add_modifier(Modifier::ITALIC),
+            _ => (),
+        },
+        "font-variant" => todo!(),
+        "font-weight" => match value {
+            "bold" => style.tui_style = style.tui_style.add_modifier(Modifier::BOLD),
+            "normal" => style.tui_style = style.tui_style.remove_modifier(Modifier::BOLD),
+            _ => (),
+        },
+        _ => (),
+    }
+}
+
+fn apply_padding(name: &str, value: &str, style: &mut StyleModifer) {
+    match parse_value(value) {
+        Some(UnitSystem::Percent(v)) => match name {
+            "padding" => {
+                let v = Dimension::Percent(v / 100.0);
+                style.style.padding.top = v;
+                style.style.padding.bottom = v;
+                style.style.padding.start = v;
+                style.style.padding.end = v;
+            }
+            "padding-bottom" => style.style.padding.bottom = Dimension::Percent(v / 100.0),
+            "padding-left" => style.style.padding.start = Dimension::Percent(v / 100.0),
+            "padding-right" => style.style.padding.end = Dimension::Percent(v / 100.0),
+            "padding-top" => style.style.padding.top = Dimension::Percent(v / 100.0),
+            _ => {}
+        },
+        Some(UnitSystem::Point(v)) => match name {
+            "padding" => {
+                style.style.padding.top = Dimension::Points(v);
+                style.style.padding.bottom = Dimension::Points(v);
+                style.style.padding.start = Dimension::Points(v);
+                style.style.padding.end = Dimension::Points(v);
+            }
+            "padding-bottom" => style.style.padding.bottom = Dimension::Points(v),
+            "padding-left" => style.style.padding.start = Dimension::Points(v),
+            "padding-right" => style.style.padding.end = Dimension::Points(v),
+            "padding-top" => style.style.padding.top = Dimension::Points(v),
+            _ => {}
+        },
+        None => {}
+    }
+}
+
+fn apply_text(name: &str, value: &str, style: &mut StyleModifer) {
+    use tui::style::Modifier;
+
+    match name {
+        "text-align" => todo!(),
+        "text-align-last" => todo!(),
+        "text-decoration" | "text-decoration-line" => {
+            for v in value.split(' ') {
+                match v {
+                    "line-through" => {
+                        style.tui_style = style.tui_style.add_modifier(Modifier::CROSSED_OUT)
+                    }
+                    "underline" => {
+                        style.tui_style = style.tui_style.add_modifier(Modifier::UNDERLINED)
+                    }
+                    _ => (),
+                }
+            }
+        }
+        "text-decoration-color" => todo!(),
+        "text-decoration-style" => todo!(),
+        "text-indent" => todo!(),
+        "text-justify" => todo!(),
+        "text-overflow" => todo!(),
+        "text-shadow" => todo!(),
+        "text-transform" => todo!(),
+        _ => todo!(),
+    }
+}
+
+fn apply_transform(_name: &str, _value: &str, _style: &mut StyleModifer) {
+    todo!()
+}
+
+fn apply_transition(_name: &str, _value: &str, _style: &mut StyleModifer) {
+    todo!()
+}
+
+fn apply_align(name: &str, value: &str, style: &mut StyleModifer) {
+    match name {
+        "align-items" => {
+            use AlignItems::*;
+            style.style.align_items = match value {
+                "flex-start" => FlexStart,
+                "flex-end" => FlexEnd,
+                "center" => Center,
+                "baseline" => Baseline,
+                "stretch" => Stretch,
+                _ => FlexStart,
+            };
+        }
+        "align-content" => {
+            use AlignContent::*;
+            style.style.align_content = match value {
+                "flex-start" => FlexStart,
+                "flex-end" => FlexEnd,
+                "center" => Center,
+                "space-between" => SpaceBetween,
+                "space-around" => SpaceAround,
+                _ => FlexStart,
+            };
+        }
+        "align-self" => {
+            use AlignSelf::*;
+            style.style.align_self = match value {
+                "auto" => Auto,
+                "flex-start" => FlexStart,
+                "flex-end" => FlexEnd,
+                "center" => Center,
+                "baseline" => Baseline,
+                "stretch" => Stretch,
+                _ => Auto,
+            };
+        }
+        _ => {}
+    }
+}
+
+pub fn apply_size(_name: &str, _value: &str, _style: &mut StyleModifer) {
+    //
+}
+
+pub fn apply_margin(name: &str, value: &str, style: &mut StyleModifer) {
+    match parse_value(value) {
+        Some(UnitSystem::Percent(v)) => match name {
+            "margin" => {
+                let v = Dimension::Percent(v / 100.0);
+                style.style.margin.top = v;
+                style.style.margin.bottom = v;
+                style.style.margin.start = v;
+                style.style.margin.end = v;
+            }
+            "margin-top" => style.style.margin.top = Dimension::Percent(v / 100.0),
+            "margin-bottom" => style.style.margin.bottom = Dimension::Percent(v / 100.0),
+            "margin-left" => style.style.margin.start = Dimension::Percent(v / 100.0),
+            "margin-right" => style.style.margin.end = Dimension::Percent(v / 100.0),
+            _ => {}
+        },
+        Some(UnitSystem::Point(v)) => match name {
+            "margin" => {
+                style.style.margin.top = Dimension::Points(v);
+                style.style.margin.bottom = Dimension::Points(v);
+                style.style.margin.start = Dimension::Points(v);
+                style.style.margin.end = Dimension::Points(v);
+            }
+            "margin-top" => style.style.margin.top = Dimension::Points(v),
+            "margin-bottom" => style.style.margin.bottom = Dimension::Points(v),
+            "margin-left" => style.style.margin.start = Dimension::Points(v),
+            "margin-right" => style.style.margin.end = Dimension::Points(v),
+            _ => {}
+        },
+        None => {}
+    }
+}

+ 20 - 0
packages/tui/src/config.rs

@@ -0,0 +1,20 @@
+#[derive(Default, Clone, Copy)]
+pub struct Config {
+    pub rendering_mode: RenderingMode,
+}
+
+#[derive(Clone, Copy)]
+pub enum RenderingMode {
+    /// only 16 colors by accessed by name, no alpha support
+    BaseColors,
+    /// 8 bit colors, will be downsampled from rgb colors
+    Ansi,
+    /// 24 bit colors, most terminals support this
+    Rgb,
+}
+
+impl Default for RenderingMode {
+    fn default() -> Self {
+        RenderingMode::Rgb
+    }
+}

+ 650 - 0
packages/tui/src/hooks.rs

@@ -0,0 +1,650 @@
+use crossterm::event::{
+    Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, MouseButton, MouseEventKind,
+};
+use dioxus_core::*;
+
+use dioxus_html::{on::*, KeyCode};
+use futures::{channel::mpsc::UnboundedReceiver, StreamExt};
+use std::{
+    any::Any,
+    cell::RefCell,
+    collections::{HashMap, HashSet},
+    rc::Rc,
+    sync::Arc,
+    time::{Duration, Instant},
+};
+use stretch2::{prelude::Layout, Stretch};
+
+use crate::TuiNode;
+
+// a wrapper around the input state for easier access
+// todo: fix loop
+// pub struct InputState(Rc<Rc<RefCell<InnerInputState>>>);
+// impl InputState {
+//     pub fn get(cx: &ScopeState) -> InputState {
+//         let inner = cx
+//             .consume_context::<Rc<RefCell<InnerInputState>>>()
+//             .expect("Rink InputState can only be used in Rink apps!");
+//         (**inner).borrow_mut().subscribe(cx.schedule_update());
+//         InputState(inner)
+//     }
+
+//     pub fn mouse(&self) -> Option<MouseData> {
+//         let data = (**self.0).borrow();
+//         data.mouse.as_ref().map(|m| clone_mouse_data(m))
+//     }
+
+//     pub fn wheel(&self) -> Option<WheelData> {
+//         let data = (**self.0).borrow();
+//         data.wheel.as_ref().map(|w| clone_wheel_data(w))
+//     }
+
+//     pub fn screen(&self) -> Option<(u16, u16)> {
+//         let data = (**self.0).borrow();
+//         data.screen.as_ref().map(|m| m.clone())
+//     }
+
+//     pub fn last_key_pressed(&self) -> Option<KeyboardData> {
+//         let data = (**self.0).borrow();
+//         data.last_key_pressed
+//             .as_ref()
+//             .map(|k| clone_keyboard_data(&k.0))
+//     }
+// }
+
+type EventCore = (&'static str, EventData);
+
+#[derive(Debug)]
+enum EventData {
+    Mouse(MouseData),
+    Wheel(WheelData),
+    Screen((u16, u16)),
+    Keyboard(KeyboardData),
+}
+impl EventData {
+    fn into_any(self) -> Arc<dyn Any + Send + Sync> {
+        match self {
+            Self::Mouse(m) => Arc::new(m),
+            Self::Wheel(w) => Arc::new(w),
+            Self::Screen(s) => Arc::new(s),
+            Self::Keyboard(k) => Arc::new(k),
+        }
+    }
+}
+
+const MAX_REPEAT_TIME: Duration = Duration::from_millis(100);
+
+pub struct InnerInputState {
+    mouse: Option<(MouseData, Vec<u16>)>,
+    wheel: Option<WheelData>,
+    last_key_pressed: Option<(KeyboardData, Instant)>,
+    screen: Option<(u16, u16)>,
+    // subscribers: Vec<Rc<dyn Fn() + 'static>>,
+}
+
+impl InnerInputState {
+    fn new() -> Self {
+        Self {
+            mouse: None,
+            wheel: None,
+            last_key_pressed: None,
+            screen: None,
+            // subscribers: Vec::new(),
+        }
+    }
+
+    // stores current input state and transforms events based on that state
+    fn apply_event(&mut self, evt: &mut EventCore) {
+        match evt.1 {
+            // limitations: only two buttons may be held at once
+            EventData::Mouse(ref mut m) => match &mut self.mouse {
+                Some(state) => {
+                    let mut buttons = state.0.buttons;
+                    state.0 = clone_mouse_data(m);
+                    match evt.0 {
+                        // this code only runs when there are no buttons down
+                        "mouseup" => {
+                            buttons = 0;
+                            state.1 = Vec::new();
+                        }
+                        "mousedown" => {
+                            if state.1.contains(&m.buttons) {
+                                // if we already pressed a button and there is another button released the button crossterm sends is the button remaining
+                                if state.1.len() > 1 {
+                                    evt.0 = "mouseup";
+                                    state.1 = vec![m.buttons];
+                                }
+                                // otherwise some other button was pressed. In testing it was consistantly this mapping
+                                else {
+                                    match m.buttons {
+                                        0x01 => state.1.push(0x02),
+                                        0x02 => state.1.push(0x01),
+                                        0x04 => state.1.push(0x01),
+                                        _ => (),
+                                    }
+                                }
+                            } else {
+                                state.1.push(m.buttons);
+                            }
+
+                            buttons = state.1.iter().copied().reduce(|a, b| a | b).unwrap();
+                        }
+                        _ => (),
+                    }
+                    state.0.buttons = buttons;
+                    m.buttons = buttons;
+                }
+                None => {
+                    self.mouse = Some((
+                        clone_mouse_data(m),
+                        if m.buttons == 0 {
+                            Vec::new()
+                        } else {
+                            vec![m.buttons]
+                        },
+                    ));
+                }
+            },
+            EventData::Wheel(ref w) => self.wheel = Some(clone_wheel_data(w)),
+            EventData::Screen(ref s) => self.screen = Some(*s),
+            EventData::Keyboard(ref mut k) => {
+                let repeat = self
+                    .last_key_pressed
+                    .as_ref()
+                    .filter(|k2| k2.0.key == k.key && k2.1.elapsed() < MAX_REPEAT_TIME)
+                    .is_some();
+                k.repeat = repeat;
+                let new = clone_keyboard_data(k);
+                self.last_key_pressed = Some((new, Instant::now()));
+            }
+        }
+    }
+
+    fn update<'a>(
+        &mut self,
+        dom: &'a VirtualDom,
+        evts: &mut Vec<EventCore>,
+        resolved_events: &mut Vec<UserEvent>,
+        layout: &Stretch,
+        layouts: &mut HashMap<ElementId, TuiNode<'a>>,
+        node: &'a VNode<'a>,
+    ) {
+        struct Data<'b> {
+            new_pos: (i32, i32),
+            old_pos: Option<(i32, i32)>,
+            clicked: bool,
+            released: bool,
+            wheel_delta: f64,
+            mouse_data: &'b MouseData,
+            wheel_data: &'b Option<WheelData>,
+        }
+
+        fn layout_contains_point(layout: &Layout, point: (i32, i32)) -> bool {
+            layout.location.x as i32 <= point.0
+                && layout.location.x as i32 + layout.size.width as i32 >= point.0
+                && layout.location.y as i32 <= point.1
+                && layout.location.y as i32 + layout.size.height as i32 >= point.1
+        }
+
+        fn get_mouse_events<'c, 'd>(
+            dom: &'c VirtualDom,
+            resolved_events: &mut Vec<UserEvent>,
+            layout: &Stretch,
+            layouts: &HashMap<ElementId, TuiNode<'c>>,
+            node: &'c VNode<'c>,
+            data: &'d Data<'d>,
+        ) -> HashSet<&'static str> {
+            match node {
+                VNode::Fragment(f) => {
+                    let mut union = HashSet::new();
+                    for child in f.children {
+                        union = union
+                            .union(&get_mouse_events(
+                                dom,
+                                resolved_events,
+                                layout,
+                                layouts,
+                                child,
+                                data,
+                            ))
+                            .copied()
+                            .collect();
+                    }
+                    return union;
+                }
+
+                VNode::Component(vcomp) => {
+                    let idx = vcomp.scope.get().unwrap();
+                    let new_node = dom.get_scope(idx).unwrap().root_node();
+                    return get_mouse_events(dom, resolved_events, layout, layouts, new_node, data);
+                }
+
+                VNode::Placeholder(_) => return HashSet::new(),
+
+                VNode::Element(_) | VNode::Text(_) => {}
+            }
+
+            let id = node.try_mounted_id().unwrap();
+            let node = layouts.get(&id).unwrap();
+
+            let node_layout = layout.layout(node.layout).unwrap();
+
+            let previously_contained = data
+                .old_pos
+                .filter(|pos| layout_contains_point(node_layout, *pos))
+                .is_some();
+            let currently_contains = layout_contains_point(node_layout, data.new_pos);
+
+            match node.node {
+                VNode::Element(el) => {
+                    let mut events = HashSet::new();
+                    if previously_contained || currently_contains {
+                        for c in el.children {
+                            events = events
+                                .union(&get_mouse_events(
+                                    dom,
+                                    resolved_events,
+                                    layout,
+                                    layouts,
+                                    c,
+                                    data,
+                                ))
+                                .copied()
+                                .collect();
+                        }
+                    }
+                    let mut try_create_event = |name| {
+                        // only trigger event if the event was not triggered already by a child
+                        if events.insert(name) {
+                            resolved_events.push(UserEvent {
+                                scope_id: None,
+                                priority: EventPriority::Medium,
+                                name,
+                                element: Some(el.id.get().unwrap()),
+                                data: Arc::new(clone_mouse_data(data.mouse_data)),
+                            })
+                        }
+                    };
+                    if currently_contains {
+                        if !previously_contained {
+                            try_create_event("mouseenter");
+                            try_create_event("mouseover");
+                        }
+                        if data.clicked {
+                            try_create_event("mousedown");
+                        }
+                        if data.released {
+                            try_create_event("mouseup");
+                            match data.mouse_data.button {
+                                0 => try_create_event("click"),
+                                2 => try_create_event("contextmenu"),
+                                _ => (),
+                            }
+                        }
+                        if let Some(w) = data.wheel_data {
+                            if data.wheel_delta != 0.0 {
+                                resolved_events.push(UserEvent {
+                                    scope_id: None,
+                                    priority: EventPriority::Medium,
+                                    name: "wheel",
+                                    element: Some(el.id.get().unwrap()),
+                                    data: Arc::new(clone_wheel_data(w)),
+                                })
+                            }
+                        }
+                    } else if previously_contained {
+                        try_create_event("mouseleave");
+                        try_create_event("mouseout");
+                    }
+                    events
+                }
+                VNode::Text(_) => HashSet::new(),
+                _ => todo!(),
+            }
+        }
+
+        let previous_mouse = self
+            .mouse
+            .as_ref()
+            .map(|m| (clone_mouse_data(&m.0), m.1.clone()));
+        // println!("{previous_mouse:?}");
+
+        self.wheel = None;
+
+        for e in evts.iter_mut() {
+            self.apply_event(e);
+        }
+
+        // resolve hover events
+        if let Some(mouse) = &self.mouse {
+            let new_pos = (mouse.0.screen_x, mouse.0.screen_y);
+            let old_pos = previous_mouse
+                .as_ref()
+                .map(|m| (m.0.screen_x, m.0.screen_y));
+            let clicked =
+                (!mouse.0.buttons & previous_mouse.as_ref().map(|m| m.0.buttons).unwrap_or(0)) > 0;
+            let released =
+                (mouse.0.buttons & !previous_mouse.map(|m| m.0.buttons).unwrap_or(0)) > 0;
+            let wheel_delta = self.wheel.as_ref().map_or(0.0, |w| w.delta_y);
+            let mouse_data = &mouse.0;
+            let wheel_data = &self.wheel;
+            let data = Data {
+                new_pos,
+                old_pos,
+                clicked,
+                released,
+                wheel_delta,
+                mouse_data,
+                wheel_data,
+            };
+            get_mouse_events(dom, resolved_events, layout, layouts, node, &data);
+        }
+
+        // for s in &self.subscribers {
+        //     s();
+        // }
+    }
+
+    // fn subscribe(&mut self, f: Rc<dyn Fn() + 'static>) {
+    //     self.subscribers.push(f)
+    // }
+}
+
+pub struct RinkInputHandler {
+    state: Rc<RefCell<InnerInputState>>,
+    queued_events: Rc<RefCell<Vec<EventCore>>>,
+}
+
+impl RinkInputHandler {
+    /// global context that handles events
+    /// limitations: GUI key modifier is never detected, key up events are not detected, and only two mouse buttons may be pressed at once
+    pub fn new(
+        mut receiver: UnboundedReceiver<TermEvent>,
+        cx: &ScopeState,
+    ) -> (Self, Rc<RefCell<InnerInputState>>) {
+        let queued_events = Rc::new(RefCell::new(Vec::new()));
+        let queued_events2 = Rc::<RefCell<std::vec::Vec<_>>>::downgrade(&queued_events);
+
+        cx.push_future(async move {
+            while let Some(evt) = receiver.next().await {
+                if let Some(evt) = get_event(evt) {
+                    if let Some(v) = queued_events2.upgrade() {
+                        (*v).borrow_mut().push(evt);
+                    } else {
+                        break;
+                    }
+                }
+            }
+        });
+
+        let state = Rc::new(RefCell::new(InnerInputState::new()));
+
+        (
+            Self {
+                state: state.clone(),
+                queued_events,
+            },
+            state,
+        )
+    }
+
+    pub fn get_events<'a>(
+        &self,
+        dom: &'a VirtualDom,
+        layout: &Stretch,
+        layouts: &mut HashMap<ElementId, TuiNode<'a>>,
+        node: &'a VNode<'a>,
+    ) -> Vec<UserEvent> {
+        // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus
+        fn inner(
+            queue: &[(&'static str, Arc<dyn Any + Send + Sync>)],
+            resolved: &mut Vec<UserEvent>,
+            node: &VNode,
+        ) {
+            match node {
+                VNode::Fragment(frag) => {
+                    for c in frag.children {
+                        inner(queue, resolved, c);
+                    }
+                }
+                VNode::Element(el) => {
+                    for l in el.listeners {
+                        for (name, data) in queue.iter() {
+                            if *name == l.event {
+                                if let Some(id) = el.id.get() {
+                                    resolved.push(UserEvent {
+                                        scope_id: None,
+                                        priority: EventPriority::Medium,
+                                        name: *name,
+                                        element: Some(id),
+                                        data: data.clone(),
+                                    });
+                                }
+                            }
+                        }
+                    }
+                    for c in el.children {
+                        inner(queue, resolved, c);
+                    }
+                }
+                _ => (),
+            }
+        }
+
+        let mut resolved_events = Vec::new();
+
+        (*self.state).borrow_mut().update(
+            dom,
+            &mut (*self.queued_events).borrow_mut(),
+            &mut resolved_events,
+            layout,
+            layouts,
+            node,
+        );
+
+        let events: Vec<_> = self
+            .queued_events
+            .replace(Vec::new())
+            .into_iter()
+            // these events were added in the update stage
+            .filter(|e| !["mousedown", "mouseup", "mousemove", "drag", "wheel"].contains(&e.0))
+            .map(|e| (e.0, e.1.into_any()))
+            .collect();
+
+        inner(&events, &mut resolved_events, node);
+
+        resolved_events
+    }
+}
+
+// translate crossterm events into dioxus events
+fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> {
+    let (name, data): (&str, EventData) = match evt {
+        TermEvent::Key(k) => {
+            let key = translate_key_code(k.code)?;
+            (
+                "keydown",
+                // from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
+                EventData::Keyboard(KeyboardData {
+                    char_code: key.raw_code(),
+                    key: format!("{key:?}"),
+                    key_code: key,
+                    alt_key: k.modifiers.contains(KeyModifiers::ALT),
+                    ctrl_key: k.modifiers.contains(KeyModifiers::CONTROL),
+                    meta_key: false,
+                    shift_key: k.modifiers.contains(KeyModifiers::SHIFT),
+                    locale: Default::default(),
+                    location: 0x00,
+                    repeat: Default::default(),
+                    which: Default::default(),
+                }),
+            )
+        }
+        TermEvent::Mouse(m) => {
+            let (x, y) = (m.column.into(), m.row.into());
+            let alt = m.modifiers.contains(KeyModifiers::ALT);
+            let shift = m.modifiers.contains(KeyModifiers::SHIFT);
+            let ctrl = m.modifiers.contains(KeyModifiers::CONTROL);
+            let meta = false;
+
+            let get_mouse_data = |b| {
+                let buttons = match b {
+                    None => 0,
+                    Some(MouseButton::Left) => 1,
+                    Some(MouseButton::Right) => 2,
+                    Some(MouseButton::Middle) => 4,
+                };
+                let button_state = match b {
+                    None => 0,
+                    Some(MouseButton::Left) => 0,
+                    Some(MouseButton::Middle) => 1,
+                    Some(MouseButton::Right) => 2,
+                };
+                // from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
+                EventData::Mouse(MouseData {
+                    alt_key: alt,
+                    button: button_state,
+                    buttons,
+                    client_x: x,
+                    client_y: y,
+                    ctrl_key: ctrl,
+                    meta_key: meta,
+                    page_x: x,
+                    page_y: y,
+                    screen_x: x,
+                    screen_y: y,
+                    shift_key: shift,
+                })
+            };
+
+            let get_wheel_data = |up| {
+                // from https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
+                EventData::Wheel(WheelData {
+                    delta_mode: 0x01,
+                    delta_x: 0.0,
+                    delta_y: if up { -1.0 } else { 1.0 },
+                    delta_z: 0.0,
+                })
+            };
+
+            match m.kind {
+                MouseEventKind::Down(b) => ("mousedown", get_mouse_data(Some(b))),
+                MouseEventKind::Up(b) => ("mouseup", get_mouse_data(Some(b))),
+                MouseEventKind::Drag(b) => ("drag", get_mouse_data(Some(b))),
+                MouseEventKind::Moved => ("mousemove", get_mouse_data(None)),
+                MouseEventKind::ScrollDown => ("wheel", get_wheel_data(false)),
+                MouseEventKind::ScrollUp => ("wheel", get_wheel_data(true)),
+            }
+        }
+        TermEvent::Resize(x, y) => ("resize", EventData::Screen((x, y))),
+    };
+
+    Some((name, data))
+}
+
+fn translate_key_code(c: TermKeyCode) -> Option<KeyCode> {
+    match c {
+        TermKeyCode::Backspace => Some(KeyCode::Backspace),
+        TermKeyCode::Enter => Some(KeyCode::Enter),
+        TermKeyCode::Left => Some(KeyCode::LeftArrow),
+        TermKeyCode::Right => Some(KeyCode::RightArrow),
+        TermKeyCode::Up => Some(KeyCode::UpArrow),
+        TermKeyCode::Down => Some(KeyCode::DownArrow),
+        TermKeyCode::Home => Some(KeyCode::Home),
+        TermKeyCode::End => Some(KeyCode::End),
+        TermKeyCode::PageUp => Some(KeyCode::PageUp),
+        TermKeyCode::PageDown => Some(KeyCode::PageDown),
+        TermKeyCode::Tab => Some(KeyCode::Tab),
+        TermKeyCode::BackTab => None,
+        TermKeyCode::Delete => Some(KeyCode::Delete),
+        TermKeyCode::Insert => Some(KeyCode::Insert),
+        TermKeyCode::F(fn_num) => match fn_num {
+            1 => Some(KeyCode::F1),
+            2 => Some(KeyCode::F2),
+            3 => Some(KeyCode::F3),
+            4 => Some(KeyCode::F4),
+            5 => Some(KeyCode::F5),
+            6 => Some(KeyCode::F6),
+            7 => Some(KeyCode::F7),
+            8 => Some(KeyCode::F8),
+            9 => Some(KeyCode::F9),
+            10 => Some(KeyCode::F10),
+            11 => Some(KeyCode::F11),
+            12 => Some(KeyCode::F12),
+            _ => None,
+        },
+        TermKeyCode::Char(c) => match c.to_uppercase().next().unwrap() {
+            'A' => Some(KeyCode::A),
+            'B' => Some(KeyCode::B),
+            'C' => Some(KeyCode::C),
+            'D' => Some(KeyCode::D),
+            'E' => Some(KeyCode::E),
+            'F' => Some(KeyCode::F),
+            'G' => Some(KeyCode::G),
+            'H' => Some(KeyCode::H),
+            'I' => Some(KeyCode::I),
+            'J' => Some(KeyCode::J),
+            'K' => Some(KeyCode::K),
+            'L' => Some(KeyCode::L),
+            'M' => Some(KeyCode::M),
+            'N' => Some(KeyCode::N),
+            'O' => Some(KeyCode::O),
+            'P' => Some(KeyCode::P),
+            'Q' => Some(KeyCode::Q),
+            'R' => Some(KeyCode::R),
+            'S' => Some(KeyCode::S),
+            'T' => Some(KeyCode::T),
+            'U' => Some(KeyCode::U),
+            'V' => Some(KeyCode::V),
+            'W' => Some(KeyCode::W),
+            'X' => Some(KeyCode::X),
+            'Y' => Some(KeyCode::Y),
+            'Z' => Some(KeyCode::Z),
+            _ => None,
+        },
+        TermKeyCode::Null => None,
+        TermKeyCode::Esc => Some(KeyCode::Escape),
+    }
+}
+
+fn clone_mouse_data(m: &MouseData) -> MouseData {
+    MouseData {
+        client_x: m.client_x,
+        client_y: m.client_y,
+        page_x: m.page_x,
+        page_y: m.page_y,
+        screen_x: m.screen_x,
+        screen_y: m.screen_y,
+        alt_key: m.alt_key,
+        ctrl_key: m.ctrl_key,
+        meta_key: m.meta_key,
+        shift_key: m.shift_key,
+        button: m.button,
+        buttons: m.buttons,
+    }
+}
+
+fn clone_keyboard_data(k: &KeyboardData) -> KeyboardData {
+    KeyboardData {
+        char_code: k.char_code,
+        key: k.key.clone(),
+        key_code: k.key_code,
+        alt_key: k.alt_key,
+        ctrl_key: k.ctrl_key,
+        meta_key: k.meta_key,
+        shift_key: k.shift_key,
+        locale: k.locale.clone(),
+        location: k.location,
+        repeat: k.repeat,
+        which: k.which,
+    }
+}
+
+fn clone_wheel_data(w: &WheelData) -> WheelData {
+    WheelData {
+        delta_mode: w.delta_mode,
+        delta_x: w.delta_x,
+        delta_y: w.delta_y,
+        delta_z: w.delta_x,
+    }
+}

+ 112 - 0
packages/tui/src/layout.rs

@@ -0,0 +1,112 @@
+use dioxus_core::*;
+use std::collections::HashMap;
+
+use crate::{
+    attributes::{apply_attributes, StyleModifer},
+    style::RinkStyle,
+    TuiModifier, TuiNode,
+};
+
+/*
+The layout system uses the lineheight as one point.
+
+stretch uses fractional points, so we can rasterize if we need too, but not with characters
+this means anything thats "1px" is 1 lineheight. Unfortunately, text cannot be smaller or bigger
+*/
+pub fn collect_layout<'a>(
+    layout: &mut stretch2::Stretch,
+    nodes: &mut HashMap<ElementId, TuiNode<'a>>,
+    vdom: &'a VirtualDom,
+    node: &'a VNode<'a>,
+) {
+    use stretch2::prelude::*;
+
+    match node {
+        VNode::Text(t) => {
+            let id = t.id.get().unwrap();
+            let char_len = t.text.chars().count();
+
+            let style = Style {
+                size: Size {
+                    // characters are 1 point tall
+                    height: Dimension::Points(1.0),
+
+                    // text is as long as it is declared
+                    width: Dimension::Points(char_len as f32),
+                },
+                ..Default::default()
+            };
+
+            nodes.insert(
+                id,
+                TuiNode {
+                    node,
+                    block_style: RinkStyle::default(),
+                    tui_modifier: TuiModifier::default(),
+                    layout: layout.new_node(style, &[]).unwrap(),
+                },
+            );
+        }
+        VNode::Element(el) => {
+            // gather up all the styles from the attribute list
+            let mut modifier = StyleModifer {
+                style: Style::default(),
+                tui_style: RinkStyle::default(),
+                tui_modifier: TuiModifier::default(),
+            };
+
+            for &Attribute { name, value, .. } in el.attributes {
+                apply_attributes(name, value, &mut modifier);
+            }
+
+            // Layout the children
+            for child in el.children {
+                collect_layout(layout, nodes, vdom, child);
+            }
+
+            // Set all direct nodes as our children
+            let mut child_layout = vec![];
+            for el in el.children {
+                let ite = ElementIdIterator::new(vdom, el);
+                for node in ite {
+                    match node {
+                        VNode::Element(_) | VNode::Text(_) => {
+                            //
+                            child_layout.push(nodes[&node.mounted_id()].layout)
+                        }
+                        VNode::Placeholder(_) => {}
+                        VNode::Fragment(_) => todo!(),
+                        VNode::Component(_) => todo!(),
+                    }
+
+                    // child_layout.push(nodes[&node.mounted_id()].layout)
+                }
+            }
+
+            nodes.insert(
+                node.mounted_id(),
+                TuiNode {
+                    node,
+                    block_style: modifier.tui_style,
+                    tui_modifier: modifier.tui_modifier,
+                    layout: layout.new_node(modifier.style, &child_layout).unwrap(),
+                },
+            );
+        }
+        VNode::Fragment(el) => {
+            //
+            for child in el.children {
+                collect_layout(layout, nodes, vdom, child);
+            }
+        }
+        VNode::Component(sc) => {
+            //
+            let scope = vdom.get_scope(sc.scope.get().unwrap()).unwrap();
+            let root = scope.root_node();
+            collect_layout(layout, nodes, vdom, root);
+        }
+        VNode::Placeholder(_) => {
+            //
+        }
+    };
+}

+ 215 - 0
packages/tui/src/lib.rs

@@ -0,0 +1,215 @@
+use anyhow::Result;
+use crossterm::{
+    event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
+    execute,
+    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
+};
+use dioxus_core::exports::futures_channel::mpsc::unbounded;
+use dioxus_core::*;
+use futures::{channel::mpsc::UnboundedSender, pin_mut, StreamExt};
+use std::{
+    collections::HashMap,
+    io,
+    time::{Duration, Instant},
+};
+use stretch2::{prelude::Size, Stretch};
+use style::RinkStyle;
+use tui::{backend::CrosstermBackend, Terminal};
+
+mod attributes;
+mod config;
+mod hooks;
+mod layout;
+mod render;
+mod style;
+mod widget;
+
+pub use attributes::*;
+pub use config::*;
+pub use hooks::*;
+pub use layout::*;
+pub use render::*;
+
+pub fn launch(app: Component<()>) {
+    launch_cfg(app, Config::default())
+}
+
+pub fn launch_cfg(app: Component<()>, cfg: Config) {
+    let mut dom = VirtualDom::new(app);
+    let (tx, rx) = unbounded();
+
+    let cx = dom.base_scope();
+
+    let (handler, state) = RinkInputHandler::new(rx, cx);
+
+    cx.provide_root_context(state);
+
+    dom.rebuild();
+
+    render_vdom(&mut dom, tx, handler, cfg).unwrap();
+}
+
+pub struct TuiNode<'a> {
+    pub layout: stretch2::node::Node,
+    pub block_style: RinkStyle,
+    pub tui_modifier: TuiModifier,
+    pub node: &'a VNode<'a>,
+}
+
+pub fn render_vdom(
+    vdom: &mut VirtualDom,
+    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()?
+        .block_on(async {
+            /*
+            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();
+
+            terminal.clear().unwrap();
+
+            loop {
+                /*
+                -> collect all the nodes with their layout
+                -> solve their layout
+                -> resolve events
+                -> render the nodes in the right place with tui/crosstream
+                -> while rendering, apply styling
+
+                use simd to compare lines for diffing?
+
+
+                todo: reuse the layout and node objects.
+                our work_with_deadline method can tell us which nodes are dirty.
+                */
+                let mut layout = Stretch::new();
+                let mut nodes = HashMap::new();
+
+                let root_node = vdom.base_scope().root_node();
+                layout::collect_layout(&mut layout, &mut nodes, vdom, root_node);
+                /*
+                Compute the layout given the terminal size
+                */
+                let node_id = root_node.try_mounted_id().unwrap();
+                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();
+                    let width = dims.width;
+                    let height = dims.height;
+                    layout
+                        .compute_layout(
+                            root_layout,
+                            Size {
+                                width: stretch2::prelude::Number::Defined(width as f32),
+                                height: stretch2::prelude::Number::Defined(height 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,
+                    );
+                    assert!(nodes.is_empty());
+                })?;
+
+                for e in events {
+                    vdom.handle_message(SchedulerMsg::Event(e));
+                }
+
+                use futures::future::{select, Either};
+                {
+                    let wait = vdom.wait_for_work();
+                    pin_mut!(wait);
+
+                    match select(wait, rx.next()).await {
+                        Either::Left((_a, _b)) => {
+                            //
+                        }
+                        Either::Right((evt, _o)) => {
+                            match evt.as_ref().unwrap() {
+                                InputEvent::UserInput(event) => match event {
+                                    TermEvent::Key(key) => {
+                                        if matches!(key.code, KeyCode::Char('c'))
+                                            && key.modifiers.contains(KeyModifiers::CONTROL)
+                                        {
+                                            break;
+                                        }
+                                    }
+                                    TermEvent::Resize(_, _) | TermEvent::Mouse(_) => {}
+                                },
+                                InputEvent::Tick => {} // tick
+                                InputEvent::Close => break,
+                            };
+
+                            if let InputEvent::UserInput(evt) = evt.unwrap() {
+                                ctx.unbounded_send(evt).unwrap();
+                            }
+                        }
+                    }
+                }
+
+                vdom.work_with_deadline(|| false);
+            }
+
+            disable_raw_mode()?;
+            execute!(
+                terminal.backend_mut(),
+                LeaveAlternateScreen,
+                DisableMouseCapture
+            )?;
+            terminal.show_cursor()?;
+
+            Ok(())
+        })
+}
+
+enum InputEvent {
+    UserInput(TermEvent),
+    Tick,
+
+    #[allow(dead_code)]
+    Close,
+}

+ 442 - 0
packages/tui/src/render.rs

@@ -0,0 +1,442 @@
+use dioxus_core::*;
+use std::{collections::HashMap, io::Stdout};
+use stretch2::{
+    geometry::Point,
+    prelude::{Layout, Size},
+    Stretch,
+};
+use tui::{backend::CrosstermBackend, layout::Rect};
+
+use crate::{
+    style::{RinkColor, RinkStyle},
+    widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext},
+    BorderEdge, BorderStyle, Config, TuiNode, UnitSystem,
+};
+
+const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
+
+pub fn render_vnode<'a>(
+    frame: &mut tui::Frame<CrosstermBackend<Stdout>>,
+    layout: &Stretch,
+    layouts: &mut HashMap<ElementId, TuiNode<'a>>,
+    vdom: &'a VirtualDom,
+    node: &'a VNode<'a>,
+    // this holds the accumulated syle state for styled text rendering
+    style: &RinkStyle,
+    cfg: Config,
+) {
+    match node {
+        VNode::Fragment(f) => {
+            for child in f.children {
+                render_vnode(frame, layout, layouts, vdom, child, style, cfg);
+            }
+            return;
+        }
+
+        VNode::Component(vcomp) => {
+            let idx = vcomp.scope.get().unwrap();
+            let new_node = vdom.get_scope(idx).unwrap().root_node();
+            render_vnode(frame, layout, layouts, vdom, new_node, style, cfg);
+            return;
+        }
+
+        VNode::Placeholder(_) => return,
+
+        VNode::Element(_) | VNode::Text(_) => {}
+    }
+
+    let id = node.try_mounted_id().unwrap();
+    let mut node = layouts.remove(&id).unwrap();
+
+    let Layout { location, size, .. } = layout.layout(node.layout).unwrap();
+
+    let Point { x, y } = location;
+    let Size { width, height } = size;
+
+    match node.node {
+        VNode::Text(t) => {
+            #[derive(Default)]
+            struct Label<'a> {
+                text: &'a str,
+                style: RinkStyle,
+            }
+
+            impl<'a> RinkWidget for Label<'a> {
+                fn render(self, area: Rect, mut buf: RinkBuffer) {
+                    for (i, c) in self.text.char_indices() {
+                        let mut new_cell = RinkCell::default();
+                        new_cell.set_style(self.style);
+                        new_cell.symbol = c.to_string();
+                        buf.set(area.left() + i as u16, area.top(), &new_cell);
+                    }
+                }
+            }
+
+            let label = Label {
+                text: t.text,
+                style: *style,
+            };
+            let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16);
+
+            // the renderer will panic if a node is rendered out of range even if the size is zero
+            if area.width > 0 && area.height > 0 {
+                frame.render_widget(WidgetWithContext::new(label, cfg), area);
+            }
+        }
+        VNode::Element(el) => {
+            let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16);
+
+            let mut new_style = node.block_style.merge(*style);
+            node.block_style = new_style;
+
+            // the renderer will panic if a node is rendered out of range even if the size is zero
+            if area.width > 0 && area.height > 0 {
+                frame.render_widget(WidgetWithContext::new(node, cfg), area);
+            }
+
+            // do not pass background color to children
+            new_style.bg = None;
+            for el in el.children {
+                render_vnode(frame, layout, layouts, vdom, el, &new_style, cfg);
+            }
+        }
+        VNode::Fragment(_) => todo!(),
+        VNode::Component(_) => todo!(),
+        VNode::Placeholder(_) => todo!(),
+    }
+}
+
+impl<'a> RinkWidget for TuiNode<'a> {
+    fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
+        use tui::symbols::line::*;
+
+        enum Direction {
+            Left,
+            Right,
+            Up,
+            Down,
+        }
+
+        fn draw(
+            buf: &mut RinkBuffer,
+            points_history: [[i32; 2]; 3],
+            symbols: &Set,
+            pos: [u16; 2],
+            color: &Option<RinkColor>,
+        ) {
+            let [before, current, after] = points_history;
+            let start_dir = match [before[0] - current[0], before[1] - current[1]] {
+                [1, 0] => Direction::Right,
+                [-1, 0] => Direction::Left,
+                [0, 1] => Direction::Down,
+                [0, -1] => Direction::Up,
+                [a, b] => {
+                    panic!(
+                        "draw({:?} {:?} {:?}) {}, {} no cell adjacent",
+                        before, current, after, a, b
+                    )
+                }
+            };
+            let end_dir = match [after[0] - current[0], after[1] - current[1]] {
+                [1, 0] => Direction::Right,
+                [-1, 0] => Direction::Left,
+                [0, 1] => Direction::Down,
+                [0, -1] => Direction::Up,
+                [a, b] => {
+                    panic!(
+                        "draw({:?} {:?} {:?}) {}, {} no cell adjacent",
+                        before, current, after, a, b
+                    )
+                }
+            };
+
+            let mut new_cell = RinkCell::default();
+            if let Some(c) = color {
+                new_cell.fg = *c;
+            }
+            new_cell.symbol = match [start_dir, end_dir] {
+                [Direction::Down, Direction::Up] => symbols.vertical,
+                [Direction::Down, Direction::Right] => symbols.top_left,
+                [Direction::Down, Direction::Left] => symbols.top_right,
+                [Direction::Up, Direction::Down] => symbols.vertical,
+                [Direction::Up, Direction::Right] => symbols.bottom_left,
+                [Direction::Up, Direction::Left] => symbols.bottom_right,
+                [Direction::Right, Direction::Left] => symbols.horizontal,
+                [Direction::Right, Direction::Up] => symbols.bottom_left,
+                [Direction::Right, Direction::Down] => symbols.top_left,
+                [Direction::Left, Direction::Up] => symbols.bottom_right,
+                [Direction::Left, Direction::Right] => symbols.horizontal,
+                [Direction::Left, Direction::Down] => symbols.top_right,
+                _ => panic!(
+                    "{:?} {:?} {:?} cannont connect cell to itself",
+                    before, current, after
+                ),
+            }
+            .to_string();
+            buf.set(
+                (current[0] + pos[0] as i32) as u16,
+                (current[1] + pos[1] as i32) as u16,
+                &new_cell,
+            );
+        }
+
+        fn draw_arc(
+            pos: [u16; 2],
+            starting_angle: f32,
+            arc_angle: f32,
+            radius: f32,
+            symbols: &Set,
+            buf: &mut RinkBuffer,
+            color: &Option<RinkColor>,
+        ) {
+            if radius < 0.0 {
+                return;
+            }
+
+            let num_points = (radius * arc_angle) as i32;
+            let starting_point = [
+                (starting_angle.cos() * (radius * RADIUS_MULTIPLIER[0])) as i32,
+                (starting_angle.sin() * (radius * RADIUS_MULTIPLIER[1])) as i32,
+            ];
+            // keep track of the last 3 point to allow filling diagonals
+            let mut points_history = [
+                [0, 0],
+                {
+                    // change the x or y value based on which one is changing quicker
+                    let ddx = -starting_angle.sin();
+                    let ddy = starting_angle.cos();
+                    if ddx.abs() > ddy.abs() {
+                        [starting_point[0] - ddx.signum() as i32, starting_point[1]]
+                    } else {
+                        [starting_point[0], starting_point[1] - ddy.signum() as i32]
+                    }
+                },
+                starting_point,
+            ];
+
+            for i in 1..=num_points {
+                let angle = (i as f32 / num_points as f32) * arc_angle + starting_angle;
+                let x = angle.cos() * radius * RADIUS_MULTIPLIER[0];
+                let y = angle.sin() * radius * RADIUS_MULTIPLIER[1];
+                let new = [x as i32, y as i32];
+
+                if new != points_history[2] {
+                    points_history = [points_history[1], points_history[2], new];
+
+                    let dx = points_history[2][0] - points_history[1][0];
+                    let dy = points_history[2][1] - points_history[1][1];
+                    // fill diagonals
+                    if dx != 0 && dy != 0 {
+                        let connecting_point = match [dx, dy] {
+                            [1, 1] => [points_history[1][0] + 1, points_history[1][1]],
+                            [1, -1] => [points_history[1][0], points_history[1][1] - 1],
+                            [-1, 1] => [points_history[1][0], points_history[1][1] + 1],
+                            [-1, -1] => [points_history[1][0] - 1, points_history[1][1]],
+                            _ => todo!(),
+                        };
+                        draw(
+                            buf,
+                            [points_history[0], points_history[1], connecting_point],
+                            symbols,
+                            pos,
+                            color,
+                        );
+                        points_history = [points_history[1], connecting_point, points_history[2]];
+                    }
+
+                    draw(buf, points_history, symbols, pos, color);
+                }
+            }
+
+            points_history = [points_history[1], points_history[2], {
+                // change the x or y value based on which one is changing quicker
+                let ddx = -(starting_angle + arc_angle).sin();
+                let ddy = (starting_angle + arc_angle).cos();
+                if ddx.abs() > ddy.abs() {
+                    [
+                        points_history[2][0] + ddx.signum() as i32,
+                        points_history[2][1],
+                    ]
+                } else {
+                    [
+                        points_history[2][0],
+                        points_history[2][1] + ddy.signum() as i32,
+                    ]
+                }
+            }];
+
+            draw(buf, points_history, symbols, pos, color);
+        }
+
+        fn get_radius(border: &BorderEdge, area: Rect) -> f32 {
+            match border.style {
+                BorderStyle::HIDDEN => 0.0,
+                BorderStyle::NONE => 0.0,
+                _ => match border.radius {
+                    UnitSystem::Percent(p) => p * area.width as f32 / 100.0,
+                    UnitSystem::Point(p) => p,
+                }
+                .abs()
+                .min((area.width as f32 / RADIUS_MULTIPLIER[0]) / 2.0)
+                .min((area.height as f32 / RADIUS_MULTIPLIER[1]) / 2.0),
+            }
+        }
+
+        if area.area() == 0 {
+            return;
+        }
+
+        // todo: only render inside borders
+        for x in area.left()..area.right() {
+            for y in area.top()..area.bottom() {
+                let mut new_cell = RinkCell::default();
+                if let Some(c) = self.block_style.bg {
+                    new_cell.bg = c;
+                }
+                buf.set(x, y, &new_cell);
+            }
+        }
+
+        let borders = self.tui_modifier.borders;
+
+        let last_edge = &borders.left;
+        let current_edge = &borders.top;
+        if let Some(symbols) = current_edge.style.symbol_set() {
+            // the radius for the curve between this line and the next
+            let r = get_radius(current_edge, area);
+            let radius = [
+                (r * RADIUS_MULTIPLIER[0]) as u16,
+                (r * RADIUS_MULTIPLIER[1]) as u16,
+            ];
+            // the radius for the curve between this line and the last
+            let last_r = get_radius(last_edge, area);
+            let last_radius = [
+                (last_r * RADIUS_MULTIPLIER[0]) as u16,
+                (last_r * RADIUS_MULTIPLIER[1]) as u16,
+            ];
+            let color = current_edge.color.or(self.block_style.fg);
+            let mut new_cell = RinkCell::default();
+            if let Some(c) = color {
+                new_cell.fg = c;
+            }
+            for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) {
+                new_cell.symbol = symbols.horizontal.to_string();
+                buf.set(x, area.top(), &new_cell);
+            }
+            draw_arc(
+                [area.right() - radius[0] - 1, area.top() + radius[1]],
+                std::f32::consts::FRAC_PI_2 * 3.0,
+                std::f32::consts::FRAC_PI_2,
+                r,
+                &symbols,
+                &mut buf,
+                &color,
+            );
+        }
+
+        let last_edge = &borders.top;
+        let current_edge = &borders.right;
+        if let Some(symbols) = current_edge.style.symbol_set() {
+            // the radius for the curve between this line and the next
+            let r = get_radius(current_edge, area);
+            let radius = [
+                (r * RADIUS_MULTIPLIER[0]) as u16,
+                (r * RADIUS_MULTIPLIER[1]) as u16,
+            ];
+            // the radius for the curve between this line and the last
+            let last_r = get_radius(last_edge, area);
+            let last_radius = [
+                (last_r * RADIUS_MULTIPLIER[0]) as u16,
+                (last_r * RADIUS_MULTIPLIER[1]) as u16,
+            ];
+            let color = current_edge.color.or(self.block_style.fg);
+            let mut new_cell = RinkCell::default();
+            if let Some(c) = color {
+                new_cell.fg = c;
+            }
+            for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) {
+                new_cell.symbol = symbols.vertical.to_string();
+                buf.set(area.right() - 1, y, &new_cell);
+            }
+            draw_arc(
+                [area.right() - radius[0] - 1, area.bottom() - radius[1] - 1],
+                0.0,
+                std::f32::consts::FRAC_PI_2,
+                r,
+                &symbols,
+                &mut buf,
+                &color,
+            );
+        }
+
+        let last_edge = &borders.right;
+        let current_edge = &borders.bottom;
+        if let Some(symbols) = current_edge.style.symbol_set() {
+            // the radius for the curve between this line and the next
+            let r = get_radius(current_edge, area);
+            let radius = [
+                (r * RADIUS_MULTIPLIER[0]) as u16,
+                (r * RADIUS_MULTIPLIER[1]) as u16,
+            ];
+            // the radius for the curve between this line and the last
+            let last_r = get_radius(last_edge, area);
+            let last_radius = [
+                (last_r * RADIUS_MULTIPLIER[0]) as u16,
+                (last_r * RADIUS_MULTIPLIER[1]) as u16,
+            ];
+            let color = current_edge.color.or(self.block_style.fg);
+            let mut new_cell = RinkCell::default();
+            if let Some(c) = color {
+                new_cell.fg = c;
+            }
+            for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) {
+                new_cell.symbol = symbols.horizontal.to_string();
+                buf.set(x, area.bottom() - 1, &new_cell);
+            }
+            draw_arc(
+                [area.left() + radius[0], area.bottom() - radius[1] - 1],
+                std::f32::consts::FRAC_PI_2,
+                std::f32::consts::FRAC_PI_2,
+                r,
+                &symbols,
+                &mut buf,
+                &color,
+            );
+        }
+
+        let last_edge = &borders.bottom;
+        let current_edge = &borders.left;
+        if let Some(symbols) = current_edge.style.symbol_set() {
+            // the radius for the curve between this line and the next
+            let r = get_radius(current_edge, area);
+            let radius = [
+                (r * RADIUS_MULTIPLIER[0]) as u16,
+                (r * RADIUS_MULTIPLIER[1]) as u16,
+            ];
+            // the radius for the curve between this line and the last
+            let last_r = get_radius(last_edge, area);
+            let last_radius = [
+                (last_r * RADIUS_MULTIPLIER[0]) as u16,
+                (last_r * RADIUS_MULTIPLIER[1]) as u16,
+            ];
+            let color = current_edge.color.or(self.block_style.fg);
+            let mut new_cell = RinkCell::default();
+            if let Some(c) = color {
+                new_cell.fg = c;
+            }
+            for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) {
+                new_cell.symbol = symbols.vertical.to_string();
+                buf.set(area.left(), y, &new_cell);
+            }
+            draw_arc(
+                [area.left() + radius[0], area.top() + radius[1]],
+                std::f32::consts::PI,
+                std::f32::consts::FRAC_PI_2,
+                r,
+                &symbols,
+                &mut buf,
+                &color,
+            );
+        }
+    }
+}

+ 447 - 0
packages/tui/src/style.rs

@@ -0,0 +1,447 @@
+use std::{num::ParseFloatError, str::FromStr};
+
+use tui::style::{Color, Modifier, Style};
+
+use crate::RenderingMode;
+
+#[derive(Clone, Copy, Debug)]
+pub struct RinkColor {
+    pub color: Color,
+    pub alpha: f32,
+}
+
+impl Default for RinkColor {
+    fn default() -> Self {
+        Self {
+            color: Color::Black,
+            alpha: 0.0,
+        }
+    }
+}
+
+impl RinkColor {
+    pub fn blend(self, other: Color) -> Color {
+        if self.color == Color::Reset {
+            Color::Reset
+        } else if self.alpha == 0.0 {
+            other
+        } else {
+            let [sr, sg, sb] = to_rgb(self.color);
+            let [or, og, ob] = to_rgb(other);
+            let (sr, sg, sb, sa) = (
+                sr as f32 / 255.0,
+                sg as f32 / 255.0,
+                sb as f32 / 255.0,
+                self.alpha,
+            );
+            let (or, og, ob) = (or as f32 / 255.0, og as f32 / 255.0, ob as f32 / 255.0);
+            Color::Rgb(
+                (255.0 * (sr * sa + or * (1.0 - sa))) as u8,
+                (255.0 * (sg * sa + og * (1.0 - sa))) as u8,
+                (255.0 * (sb * sa + ob * (1.0 - sa))) as u8,
+            )
+        }
+    }
+}
+
+fn parse_value(
+    v: &str,
+    current_max_output: f32,
+    required_max_output: f32,
+) -> Result<f32, ParseFloatError> {
+    if let Some(stripped) = v.strip_suffix('%') {
+        Ok((stripped.trim().parse::<f32>()? / 100.0) * required_max_output)
+    } else {
+        Ok((v.trim().parse::<f32>()? / current_max_output) * required_max_output)
+    }
+}
+
+pub struct ParseColorError;
+
+fn parse_hex(color: &str) -> Result<Color, ParseColorError> {
+    let mut values = [0, 0, 0];
+    let mut color_ok = true;
+    for i in 0..values.len() {
+        if let Ok(v) = u8::from_str_radix(&color[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) {
+            values[i] = v;
+        } else {
+            color_ok = false;
+        }
+    }
+    if color_ok {
+        Ok(Color::Rgb(values[0], values[1], values[2]))
+    } else {
+        Err(ParseColorError)
+    }
+}
+
+fn parse_rgb(color: &str) -> Result<Color, ParseColorError> {
+    let mut values = [0, 0, 0];
+    let mut color_ok = true;
+    for (v, i) in color.split(',').zip(0..values.len()) {
+        if let Ok(v) = parse_value(v.trim(), 255.0, 255.0) {
+            values[i] = v as u8;
+        } else {
+            color_ok = false;
+        }
+    }
+    if color_ok {
+        Ok(Color::Rgb(values[0], values[1], values[2]))
+    } else {
+        Err(ParseColorError)
+    }
+}
+
+fn parse_hsl(color: &str) -> Result<Color, ParseColorError> {
+    let mut values = [0.0, 0.0, 0.0];
+    let mut color_ok = true;
+    for (v, i) in color.split(',').zip(0..values.len()) {
+        if let Ok(v) = parse_value(v.trim(), if i == 0 { 360.0 } else { 100.0 }, 1.0) {
+            values[i] = v;
+        } else {
+            color_ok = false;
+        }
+    }
+    if color_ok {
+        let [h, s, l] = values;
+        let rgb = if s == 0.0 {
+            [l as u8; 3]
+        } else {
+            fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
+                if t < 0.0 {
+                    t += 1.0;
+                }
+                if t > 1.0 {
+                    t -= 1.0;
+                }
+                if t < 1.0 / 6.0 {
+                    p + (q - p) * 6.0 * t
+                } else if t < 1.0 / 2.0 {
+                    q
+                } else if t < 2.0 / 3.0 {
+                    p + (q - p) * (2.0 / 3.0 - t) * 6.0
+                } else {
+                    p
+                }
+            }
+
+            let q = if l < 0.5 {
+                l * (1.0 + s)
+            } else {
+                l + s - l * s
+            };
+            let p = 2.0 * l - q;
+            [
+                (hue_to_rgb(p, q, h + 1.0 / 3.0) * 255.0) as u8,
+                (hue_to_rgb(p, q, h) * 255.0) as u8,
+                (hue_to_rgb(p, q, h - 1.0 / 3.0) * 255.0) as u8,
+            ]
+        };
+
+        Ok(Color::Rgb(rgb[0], rgb[1], rgb[2]))
+    } else {
+        Err(ParseColorError)
+    }
+}
+
+impl FromStr for RinkColor {
+    type Err = ParseColorError;
+
+    fn from_str(color: &str) -> Result<Self, Self::Err> {
+        match color {
+            "red" => Ok(RinkColor {
+                color: Color::Red,
+                alpha: 1.0,
+            }),
+            "black" => Ok(RinkColor {
+                color: Color::Black,
+                alpha: 1.0,
+            }),
+            "green" => Ok(RinkColor {
+                color: Color::Green,
+                alpha: 1.0,
+            }),
+            "yellow" => Ok(RinkColor {
+                color: Color::Yellow,
+                alpha: 1.0,
+            }),
+            "blue" => Ok(RinkColor {
+                color: Color::Blue,
+                alpha: 1.0,
+            }),
+            "magenta" => Ok(RinkColor {
+                color: Color::Magenta,
+                alpha: 1.0,
+            }),
+            "cyan" => Ok(RinkColor {
+                color: Color::Cyan,
+                alpha: 1.0,
+            }),
+            "gray" => Ok(RinkColor {
+                color: Color::Gray,
+                alpha: 1.0,
+            }),
+            "darkgray" => Ok(RinkColor {
+                color: Color::DarkGray,
+                alpha: 1.0,
+            }),
+            // light red does not exist
+            "orangered" => Ok(RinkColor {
+                color: Color::LightRed,
+                alpha: 1.0,
+            }),
+            "lightgreen" => Ok(RinkColor {
+                color: Color::LightGreen,
+                alpha: 1.0,
+            }),
+            "lightyellow" => Ok(RinkColor {
+                color: Color::LightYellow,
+                alpha: 1.0,
+            }),
+            "lightblue" => Ok(RinkColor {
+                color: Color::LightBlue,
+                alpha: 1.0,
+            }),
+            // light magenta does not exist
+            "orchid" => Ok(RinkColor {
+                color: Color::LightMagenta,
+                alpha: 1.0,
+            }),
+            "lightcyan" => Ok(RinkColor {
+                color: Color::LightCyan,
+                alpha: 1.0,
+            }),
+            "white" => Ok(RinkColor {
+                color: Color::White,
+                alpha: 1.0,
+            }),
+            _ => {
+                if color.len() == 7 && color.starts_with('#') {
+                    parse_hex(color).map(|c| RinkColor {
+                        color: c,
+                        alpha: 1.0,
+                    })
+                } else if let Some(stripped) = color.strip_prefix("rgb(") {
+                    let color_values = stripped.trim_end_matches(')');
+                    if color.matches(',').count() == 3 {
+                        let (alpha, rgb_values) =
+                            color_values.rsplit_once(',').ok_or(ParseColorError)?;
+                        if let Ok(a) = alpha.parse() {
+                            parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a })
+                        } else {
+                            Err(ParseColorError)
+                        }
+                    } else {
+                        parse_rgb(color_values).map(|c| RinkColor {
+                            color: c,
+                            alpha: 1.0,
+                        })
+                    }
+                } else if let Some(stripped) = color.strip_prefix("rgba(") {
+                    let color_values = stripped.trim_end_matches(')');
+                    if color.matches(',').count() == 3 {
+                        let (rgb_values, alpha) =
+                            color_values.rsplit_once(',').ok_or(ParseColorError)?;
+                        if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
+                            parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a })
+                        } else {
+                            Err(ParseColorError)
+                        }
+                    } else {
+                        parse_rgb(color_values).map(|c| RinkColor {
+                            color: c,
+                            alpha: 1.0,
+                        })
+                    }
+                } else if let Some(stripped) = color.strip_prefix("hsl(") {
+                    let color_values = stripped.trim_end_matches(')');
+                    if color.matches(',').count() == 3 {
+                        let (rgb_values, alpha) =
+                            color_values.rsplit_once(',').ok_or(ParseColorError)?;
+                        if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
+                            parse_hsl(rgb_values).map(|c| RinkColor { color: c, alpha: a })
+                        } else {
+                            Err(ParseColorError)
+                        }
+                    } else {
+                        parse_hsl(color_values).map(|c| RinkColor {
+                            color: c,
+                            alpha: 1.0,
+                        })
+                    }
+                } else if let Some(stripped) = color.strip_prefix("hsla(") {
+                    let color_values = stripped.trim_end_matches(')');
+                    if color.matches(',').count() == 3 {
+                        let (rgb_values, alpha) =
+                            color_values.rsplit_once(',').ok_or(ParseColorError)?;
+                        if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
+                            parse_hsl(rgb_values).map(|c| RinkColor { color: c, alpha: a })
+                        } else {
+                            Err(ParseColorError)
+                        }
+                    } else {
+                        parse_hsl(color_values).map(|c| RinkColor {
+                            color: c,
+                            alpha: 1.0,
+                        })
+                    }
+                } else {
+                    Err(ParseColorError)
+                }
+            }
+        }
+    }
+}
+
+fn to_rgb(c: Color) -> [u8; 3] {
+    match c {
+        Color::Black => [0, 0, 0],
+        Color::Red => [255, 0, 0],
+        Color::Green => [0, 128, 0],
+        Color::Yellow => [255, 255, 0],
+        Color::Blue => [0, 0, 255],
+        Color::Magenta => [255, 0, 255],
+        Color::Cyan => [0, 255, 255],
+        Color::Gray => [128, 128, 128],
+        Color::DarkGray => [169, 169, 169],
+        Color::LightRed => [255, 69, 0],
+        Color::LightGreen => [144, 238, 144],
+        Color::LightYellow => [255, 255, 224],
+        Color::LightBlue => [173, 216, 230],
+        Color::LightMagenta => [218, 112, 214],
+        Color::LightCyan => [224, 255, 255],
+        Color::White => [255, 255, 255],
+        Color::Rgb(r, g, b) => [r, g, b],
+        Color::Indexed(idx) => match idx {
+            16..=231 => {
+                let v = idx - 16;
+                // add 3 to round up
+                let r = ((v as u16 / 36) * 255 + 3) / 5;
+                let g = (((v as u16 % 36) / 6) * 255 + 3) / 5;
+                let b = ((v as u16 % 6) * 255 + 3) / 5;
+                [r as u8, g as u8, b as u8]
+            }
+            232..=255 => {
+                let l = (idx - 232) / 24;
+                [l; 3]
+            }
+            // rink will never generate these colors, but they might be on the screen from another program
+            _ => [0, 0, 0],
+        },
+        Color::Reset => [0, 0, 0],
+    }
+}
+
+pub fn convert(mode: RenderingMode, c: Color) -> Color {
+    if let Color::Reset = c {
+        c
+    } else {
+        match mode {
+            crate::RenderingMode::BaseColors => match c {
+                Color::Rgb(_, _, _) => panic!("cannot convert rgb color to base color"),
+                Color::Indexed(_) => panic!("cannot convert Ansi color to base color"),
+                _ => c,
+            },
+            crate::RenderingMode::Rgb => {
+                let rgb = to_rgb(c);
+                Color::Rgb(rgb[0], rgb[1], rgb[2])
+            }
+            crate::RenderingMode::Ansi => match c {
+                Color::Indexed(_) => c,
+                _ => {
+                    let rgb = to_rgb(c);
+                    // 16-231: 6 × 6 × 6 color cube
+                    // 232-255: 23 step grayscale
+                    if rgb[0] == rgb[1] && rgb[1] == rgb[2] {
+                        let idx = 232 + (rgb[0] as u16 * 23 / 255) as u8;
+                        Color::Indexed(idx)
+                    } else {
+                        let r = (rgb[0] as u16 * 5) / 255;
+                        let g = (rgb[1] as u16 * 5) / 255;
+                        let b = (rgb[2] as u16 * 5) / 255;
+                        let idx = 16 + r * 36 + g * 6 + b;
+                        Color::Indexed(idx as u8)
+                    }
+                }
+            },
+        }
+    }
+}
+
+#[test]
+fn rgb_to_ansi() {
+    for idx in 17..=231 {
+        let idxed = Color::Indexed(idx);
+        let rgb = to_rgb(idxed);
+        // gray scale colors have two equivelent repersentations
+        let color = Color::Rgb(rgb[0], rgb[1], rgb[2]);
+        let converted = convert(RenderingMode::Ansi, color);
+        if let Color::Indexed(i) = converted {
+            if rgb[0] != rgb[1] || rgb[1] != rgb[2] {
+                assert_eq!(idxed, converted);
+            } else {
+                assert!(i >= 232);
+            }
+        } else {
+            panic!("color is not indexed")
+        }
+    }
+    for idx in 232..=255 {
+        let idxed = Color::Indexed(idx);
+        let rgb = to_rgb(idxed);
+        assert!(rgb[0] == rgb[1] && rgb[1] == rgb[2]);
+    }
+}
+
+#[derive(Clone, Copy)]
+pub struct RinkStyle {
+    pub fg: Option<RinkColor>,
+    pub bg: Option<RinkColor>,
+    pub add_modifier: Modifier,
+    pub sub_modifier: Modifier,
+}
+
+impl Default for RinkStyle {
+    fn default() -> Self {
+        Self {
+            fg: Some(RinkColor {
+                color: Color::White,
+                alpha: 1.0,
+            }),
+            bg: None,
+            add_modifier: Modifier::empty(),
+            sub_modifier: Modifier::empty(),
+        }
+    }
+}
+
+impl RinkStyle {
+    pub fn add_modifier(mut self, m: Modifier) -> Self {
+        self.sub_modifier.remove(m);
+        self.add_modifier.insert(m);
+        self
+    }
+
+    pub fn remove_modifier(mut self, m: Modifier) -> Self {
+        self.add_modifier.remove(m);
+        self.sub_modifier.insert(m);
+        self
+    }
+
+    pub fn merge(mut self, other: RinkStyle) -> Self {
+        self.fg = self.fg.or(other.fg);
+        self.add_modifier(other.add_modifier)
+            .remove_modifier(other.sub_modifier)
+    }
+}
+
+impl From<RinkStyle> for Style {
+    fn from(val: RinkStyle) -> Self {
+        Style {
+            fg: val.fg.map(|c| c.color),
+            bg: val.bg.map(|c| c.color),
+            add_modifier: val.add_modifier,
+            sub_modifier: val.sub_modifier,
+        }
+    }
+}

+ 96 - 0
packages/tui/src/widget.rs

@@ -0,0 +1,96 @@
+use tui::{
+    buffer::Buffer,
+    layout::Rect,
+    style::{Color, Modifier},
+    widgets::Widget,
+};
+
+use crate::{
+    style::{convert, RinkColor, RinkStyle},
+    Config,
+};
+
+pub struct RinkBuffer<'a> {
+    buf: &'a mut Buffer,
+    cfg: Config,
+}
+
+impl<'a> RinkBuffer<'a> {
+    fn new(buf: &'a mut Buffer, cfg: Config) -> RinkBuffer<'a> {
+        Self { buf, cfg }
+    }
+
+    pub fn set(&mut self, x: u16, y: u16, new: &RinkCell) {
+        let mut cell = self.buf.get_mut(x, y);
+        cell.bg = convert(self.cfg.rendering_mode, new.bg.blend(cell.bg));
+        if new.symbol.is_empty() {
+            if !cell.symbol.is_empty() {
+                // allows text to "shine through" transparent backgrounds
+                cell.fg = convert(self.cfg.rendering_mode, new.bg.blend(cell.fg));
+            }
+        } else {
+            cell.modifier = new.modifier;
+            cell.symbol = new.symbol.clone();
+            cell.fg = convert(self.cfg.rendering_mode, new.fg.blend(cell.bg));
+        }
+    }
+}
+
+pub trait RinkWidget {
+    fn render(self, area: Rect, buf: RinkBuffer);
+}
+
+pub struct WidgetWithContext<T: RinkWidget> {
+    widget: T,
+    config: Config,
+}
+
+impl<T: RinkWidget> WidgetWithContext<T> {
+    pub fn new(widget: T, config: Config) -> WidgetWithContext<T> {
+        WidgetWithContext { widget, config }
+    }
+}
+
+impl<T: RinkWidget> Widget for WidgetWithContext<T> {
+    fn render(self, area: Rect, buf: &mut Buffer) {
+        self.widget.render(area, RinkBuffer::new(buf, self.config))
+    }
+}
+
+#[derive(Clone)]
+pub struct RinkCell {
+    pub symbol: String,
+    pub bg: RinkColor,
+    pub fg: RinkColor,
+    pub modifier: Modifier,
+}
+
+impl Default for RinkCell {
+    fn default() -> Self {
+        Self {
+            symbol: "".to_string(),
+            fg: RinkColor {
+                color: Color::Rgb(0, 0, 0),
+                alpha: 0.0,
+            },
+            bg: RinkColor {
+                color: Color::Rgb(0, 0, 0),
+                alpha: 0.0,
+            },
+            modifier: Modifier::empty(),
+        }
+    }
+}
+
+impl RinkCell {
+    pub fn set_style(&mut self, style: RinkStyle) {
+        if let Some(c) = style.fg {
+            self.fg = c;
+        }
+        if let Some(c) = style.bg {
+            self.bg = c;
+        }
+        self.modifier = style.add_modifier;
+        self.modifier.remove(style.sub_modifier);
+    }
+}

+ 73 - 0
packages/tui/test.html

@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+    <title>Test</title>
+    <style>
+        html,
+        body {
+            height: 100%;
+        }
+        
+        .container {
+            width: 100%;
+            height: 100%;
+            display: flex;
+            flex-direction: column;
+            background-color: black;
+            /* justify-content: center;
+            align-items: center; */
+            /* margin: auto; */
+        }
+        
+        .smaller {
+            height: 70%;
+            width: 70%;
+            background-color: green;
+            /* justify-content: center; */
+            /* align-items: center; */
+        }
+        
+        .superinner {
+            height: 100%;
+            width: 100%;
+            /* display: flex; */
+            /*  */
+            margin-top: 20px;
+            margin-bottom: 20px;
+            margin-left: 20px;
+            margin-right: 20px;
+            /*  */
+            background-color: red;
+            justify-content: center;
+            align-items: center;
+            flex-direction: column;
+            /* margin: 20px; */
+            /* margin: 20px; */
+        }
+    </style>
+</head>
+
+<body>
+    <div class="container">
+        <div class="smaller">
+            <div class="superinner">
+                <h1>Hello World</h1>
+                <p>This is a test</p>
+            </div>
+        </div>
+    </div>
+    <!-- <div class="container">
+        <div class="smaller">
+            hello world
+            <div style="color: green; margin: 40px;">
+                goodbye
+                <div style="color:red;">
+                    asdasdasd
+                </div>
+            </div>
+        </div>
+    </div> -->
+</body>
+
+</html>

+ 102 - 0
packages/tui/tests/margin.rs

@@ -0,0 +1,102 @@
+use stretch2 as stretch;
+
+#[test]
+fn margin_and_flex_row() {
+    let mut stretch = stretch::Stretch::new();
+    let node0 = stretch
+        .new_node(
+            stretch::style::Style {
+                flex_grow: 1f32,
+                margin: stretch::geometry::Rect {
+                    start: stretch::style::Dimension::Points(10f32),
+                    end: stretch::style::Dimension::Points(10f32),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            &[],
+        )
+        .unwrap();
+    let node = stretch
+        .new_node(
+            stretch::style::Style {
+                size: stretch::geometry::Size {
+                    width: stretch::style::Dimension::Points(100f32),
+                    height: stretch::style::Dimension::Points(100f32),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            &[node0],
+        )
+        .unwrap();
+    stretch
+        .compute_layout(node, stretch::geometry::Size::undefined())
+        .unwrap();
+    assert_eq!(stretch.layout(node).unwrap().size.width, 100f32);
+    assert_eq!(stretch.layout(node).unwrap().size.height, 100f32);
+    assert_eq!(stretch.layout(node).unwrap().location.x, 0f32);
+    assert_eq!(stretch.layout(node).unwrap().location.y, 0f32);
+    assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32);
+    assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32);
+    assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32);
+    assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32);
+}
+
+#[test]
+fn margin_and_flex_row2() {
+    let mut stretch = stretch::Stretch::new();
+    let node0 = stretch
+        .new_node(
+            stretch::style::Style {
+                flex_grow: 1f32,
+                margin: stretch::geometry::Rect {
+                    // left
+                    start: stretch::style::Dimension::Points(10f32),
+
+                    // right?
+                    end: stretch::style::Dimension::Points(10f32),
+
+                    // top?
+                    // top: stretch::style::Dimension::Points(10f32),
+
+                    // bottom?
+                    // bottom: stretch::style::Dimension::Points(10f32),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            &[],
+        )
+        .unwrap();
+
+    let node = stretch
+        .new_node(
+            stretch::style::Style {
+                size: stretch::geometry::Size {
+                    width: stretch::style::Dimension::Points(100f32),
+                    height: stretch::style::Dimension::Points(100f32),
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            &[node0],
+        )
+        .unwrap();
+
+    stretch
+        .compute_layout(node, stretch::geometry::Size::undefined())
+        .unwrap();
+
+    assert_eq!(stretch.layout(node).unwrap().size.width, 100f32);
+    assert_eq!(stretch.layout(node).unwrap().size.height, 100f32);
+    assert_eq!(stretch.layout(node).unwrap().location.x, 0f32);
+    assert_eq!(stretch.layout(node).unwrap().location.y, 0f32);
+
+    dbg!(stretch.layout(node0));
+
+    // assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32);
+    // assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32);
+    // assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32);
+    // assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32);
+}

+ 3 - 0
src/lib.rs

@@ -17,6 +17,9 @@ pub use dioxus_web as web;
 #[cfg(feature = "desktop")]
 pub use dioxus_desktop as desktop;
 
+#[cfg(feature = "tui")]
+pub use dioxus_tui as tui;
+
 #[cfg(feature = "fermi")]
 pub use fermi;