diff --git a/packages/tui/.gitignore b/packages/tui/.gitignore
new file mode 100644
index 000000000..96ef6c0b9
--- /dev/null
+++ b/packages/tui/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock
diff --git a/packages/tui/.vscode/spellright.dict b/packages/tui/.vscode/spellright.dict
new file mode 100644
index 000000000..c30e473fc
--- /dev/null
+++ b/packages/tui/.vscode/spellright.dict
@@ -0,0 +1,2 @@
+esque
+Tui
diff --git a/packages/tui/Cargo.toml b/packages/tui/Cargo.toml
new file mode 100644
index 000000000..1837ad5ef
--- /dev/null
+++ b/packages/tui/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "rink"
+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.22.1"
+anyhow = "1.0.42"
+thiserror = "1.0.24"
+dioxus = "0.1.8"
+dioxus-html = "0.1.6"
+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 = { git = "https://github.com/DioxusLabs/stretch" }
diff --git a/packages/tui/README.md b/packages/tui/README.md
new file mode 100644
index 000000000..20adaeb6c
--- /dev/null
+++ b/packages/tui/README.md
@@ -0,0 +1,95 @@
+
+
Rink
+
+ Beautiful terminal user interfaces in Rust with Dioxus .
+
+
+
+
+
+
+
+
+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!"
+ }
+ })
+}
+```
+
+
+
+## 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 tags1
+
+1 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.
+
+
diff --git a/packages/tui/examples/border.rs b/packages/tui/examples/border.rs
new file mode 100644
index 000000000..1d9f681b7
--- /dev/null
+++ b/packages/tui/examples/border.rs
@@ -0,0 +1,27 @@
+use dioxus::prelude::*;
+
+fn main() {
+ rink::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+ let (radius, set_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| set_radius((radius + 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}"
+ }
+ })
+}
diff --git a/packages/tui/examples/color_test.rs b/packages/tui/examples/color_test.rs
new file mode 100644
index 000000000..4aeefee8c
--- /dev/null
+++ b/packages/tui/examples/color_test.rs
@@ -0,0 +1,48 @@
+use dioxus::prelude::*;
+
+fn main() {
+ // rink::launch(app);
+ rink::launch_cfg(
+ app,
+ rink::Config {
+ rendering_mode: rink::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}%)",
+ }
+ })
+ }
+ )
+ }
+ })
+ }
+ )
+ }
+ })
+}
diff --git a/packages/tui/examples/components.rs b/packages/tui/examples/components.rs
new file mode 100644
index 000000000..4086a5855
--- /dev/null
+++ b/packages/tui/examples/components.rs
@@ -0,0 +1,64 @@
+use dioxus::prelude::*;
+
+fn main() {
+ rink::launch(app);
+}
+
+#[derive(Props, PartialEq)]
+struct QuadrentProps {
+ color: String,
+ text: String,
+}
+
+fn Quadrent(cx: Scope) -> 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",
+ Quadrent{
+ color: "red".to_string(),
+ text: "[A]".to_string()
+ },
+ Quadrent{
+ color: "black".to_string(),
+ text: "[B]".to_string()
+ }
+ }
+
+ div {
+ width: "100%",
+ height: "50%",
+ flex_direction: "row",
+ Quadrent{
+ color: "green".to_string(),
+ text: "[C]".to_string()
+ },
+ Quadrent{
+ color: "blue".to_string(),
+ text: "[D]".to_string()
+ }
+ }
+ }
+ })
+}
diff --git a/packages/tui/examples/example.png b/packages/tui/examples/example.png
new file mode 100644
index 000000000..ba45e6e99
Binary files /dev/null and b/packages/tui/examples/example.png differ
diff --git a/packages/tui/examples/frame.rs b/packages/tui/examples/frame.rs
new file mode 100644
index 000000000..db49cd733
--- /dev/null
+++ b/packages/tui/examples/frame.rs
@@ -0,0 +1,82 @@
+use dioxus::prelude::*;
+
+fn main() {
+ rink::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"
+ }
+ }
+ })
+}
diff --git a/packages/tui/examples/hover.rs b/packages/tui/examples/hover.rs
new file mode 100644
index 000000000..10916ba2e
--- /dev/null
+++ b/packages/tui/examples/hover.rs
@@ -0,0 +1,102 @@
+use std::{convert::TryInto, sync::Arc};
+
+use dioxus::{events::MouseData, prelude::*};
+
+fn main() {
+ rink::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+ fn to_str(c: &[i32; 3]) -> String {
+ "#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::()
+ }
+
+ fn get_brightness(m: Arc) -> i32 {
+ let mb = m.buttons;
+ let b: i32 = m.buttons.count_ones().try_into().unwrap();
+ 127 * b
+ }
+
+ let (q1_color, set_q1_color) = use_state(&cx, || [200; 3]);
+ let (q2_color, set_q2_color) = use_state(&cx, || [200; 3]);
+ let (q3_color, set_q3_color) = use_state(&cx, || [200; 3]);
+ let (q4_color, set_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| set_q1_color([get_brightness(m.data), 0, 0]),
+ onmousedown: move |m| set_q1_color([get_brightness(m.data), 0, 0]),
+ onmouseup: move |m| set_q1_color([get_brightness(m.data), 0, 0]),
+ onwheel: move |w| set_q1_color([q1_color[0] + (10.0*w.delta_y) as i32, 0, 0]),
+ onmouseleave: move |_| set_q1_color([200; 3]),
+ "click me"
+ }
+ div {
+ width: "50%",
+ height: "100%",
+ justify_content: "center",
+ align_items: "center",
+ background_color: "{q2_color_str}",
+ onmouseenter: move |m| set_q2_color([get_brightness(m.data); 3]),
+ onmousedown: move |m| set_q2_color([get_brightness(m.data); 3]),
+ onmouseup: move |m| set_q2_color([get_brightness(m.data); 3]),
+ onwheel: move |w| set_q2_color([q2_color[0] + (10.0*w.delta_y) as i32;3]),
+ onmouseleave: move |_| set_q2_color([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| set_q3_color([0, get_brightness(m.data), 0]),
+ onmousedown: move |m| set_q3_color([0, get_brightness(m.data), 0]),
+ onmouseup: move |m| set_q3_color([0, get_brightness(m.data), 0]),
+ onwheel: move |w| set_q3_color([0, q3_color[1] + (10.0*w.delta_y) as i32, 0]),
+ onmouseleave: move |_| set_q3_color([200; 3]),
+ "click me"
+ }
+ div {
+ width: "50%",
+ height: "100%",
+ justify_content: "center",
+ align_items: "center",
+ background_color: "{q4_color_str}",
+ onmouseenter: move |m| set_q4_color([0, 0, get_brightness(m.data)]),
+ onmousedown: move |m| set_q4_color([0, 0, get_brightness(m.data)]),
+ onmouseup: move |m| set_q4_color([0, 0, get_brightness(m.data)]),
+ onwheel: move |w| set_q4_color([0, 0, q4_color[2] + (10.0*w.delta_y) as i32]),
+ onmouseleave: move |_| set_q4_color([200; 3]),
+ "click me"
+ }
+ }
+ }
+ })
+}
diff --git a/packages/tui/examples/keys.rs b/packages/tui/examples/keys.rs
new file mode 100644
index 000000000..509199b5b
--- /dev/null
+++ b/packages/tui/examples/keys.rs
@@ -0,0 +1,75 @@
+use dioxus::events::WheelEvent;
+use dioxus::prelude::*;
+use dioxus_html::on::{KeyboardEvent, MouseEvent};
+use dioxus_html::KeyCode;
+
+fn main() {
+ rink::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+ let (key, set_key) = use_state(&cx, || "".to_string());
+ let (mouse, set_mouse) = use_state(&cx, || (0, 0));
+ let (count, set_count) = use_state(&cx, || 0);
+ let (buttons, set_buttons) = use_state(&cx, || 0);
+ let (mouse_clicked, set_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 => set_count(count + 1),
+ KeyCode::RightArrow => set_count(count - 1),
+ KeyCode::UpArrow => set_count(count + 10),
+ KeyCode::DownArrow => set_count(count - 10),
+ _ => {},
+ }
+ set_key(format!("{:?} repeating: {:?}", evt.key, evt.repeat));
+ },
+ onwheel: move |evt: WheelEvent| {
+ set_count(count + evt.data.delta_y as i64);
+ },
+ ondrag: move |evt: MouseEvent| {
+ set_mouse((evt.data.screen_x, evt.data.screen_y));
+ },
+ onmousedown: move |evt: MouseEvent| {
+ set_mouse((evt.data.screen_x, evt.data.screen_y));
+ set_buttons(evt.data.buttons);
+ set_mouse_clicked(true);
+ },
+ onmouseup: move |evt: MouseEvent| {
+ set_buttons(evt.data.buttons);
+ set_mouse_clicked(false);
+ },
+
+ "count: {count:?}",
+ "key: {key}",
+ "mouse buttons: {buttons:b}",
+ "mouse pos: {mouse:?}",
+ "mouse button pressed: {mouse_clicked}"
+ }
+ })
+}
+
+fn app2<'a>(cx: Scope<'a>) -> Element<'a> {
+ let (count, set_count) = use_state(&cx, || 0);
+
+ cx.render(rsx! {
+ div {
+ width: "100%",
+ height: "10px",
+ background_color: "red",
+ justify_content: "center",
+ align_items: "center",
+ oninput: move |_| set_count(count + 1),
+ "Hello world!",
+ h1 {},
+ }
+ })
+}
diff --git a/packages/tui/examples/layouts.rs b/packages/tui/examples/layouts.rs
new file mode 100644
index 000000000..c6d661e5b
--- /dev/null
+++ b/packages/tui/examples/layouts.rs
@@ -0,0 +1,52 @@
+use std::collections::HashMap;
+
+use dioxus::prelude::*;
+
+fn main() {
+ let mut dom = VirtualDom::new(app);
+ dom.rebuild();
+
+ let mut layout = stretch2::Stretch::new();
+ let mut nodes = HashMap::new();
+ rink::collect_layout(&mut layout, &mut nodes, &dom, dom.base_scope().root_node());
+
+ let node = nodes
+ .remove(&dom.base_scope().root_node().mounted_id())
+ .unwrap();
+
+ layout
+ .compute_layout(node.layout, stretch2::geometry::Size::undefined())
+ .unwrap();
+
+ for (_id, node) in nodes.drain() {
+ println!("{:?}", layout.layout(node.layout));
+ }
+}
+
+fn app(cx: Scope) -> Element {
+ cx.render(rsx! {
+ div {
+ width: "100%",
+ height: "100%",
+ flex_direction: "column",
+
+ div {
+ "hi"
+ }
+ div {
+ "bi"
+ "bi"
+ }
+ }
+ })
+}
+
+// fn print_layout(mut nodes: HashMap, node: &VNode) {
+// match node {
+// VNode::Text(_) => todo!(),
+// VNode::Element(_) => todo!(),
+// VNode::Fragment(_) => todo!(),
+// VNode::Component(_) => todo!(),
+// VNode::Placeholder(_) => todo!(),
+// }
+// }
diff --git a/packages/tui/examples/list.rs b/packages/tui/examples/list.rs
new file mode 100644
index 000000000..ff219dcc6
--- /dev/null
+++ b/packages/tui/examples/list.rs
@@ -0,0 +1,28 @@
+use dioxus::prelude::*;
+
+fn main() {
+ rink::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}"
+ ))
+ }
+ }
+ })
+}
diff --git a/packages/tui/examples/margin.rs b/packages/tui/examples/margin.rs
new file mode 100644
index 000000000..edc2b659f
--- /dev/null
+++ b/packages/tui/examples/margin.rs
@@ -0,0 +1,50 @@
+use dioxus::prelude::*;
+
+fn main() {
+ rink::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]"
+ }
+ }
+ }
+ })
+}
diff --git a/packages/tui/examples/quadrants.rs b/packages/tui/examples/quadrants.rs
new file mode 100644
index 000000000..db0b92f19
--- /dev/null
+++ b/packages/tui/examples/quadrants.rs
@@ -0,0 +1,60 @@
+use dioxus::prelude::*;
+
+fn main() {
+ rink::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]"
+ }
+ }
+ }
+ })
+}
diff --git a/packages/tui/examples/readme.rs b/packages/tui/examples/readme.rs
new file mode 100644
index 000000000..95a306686
--- /dev/null
+++ b/packages/tui/examples/readme.rs
@@ -0,0 +1,19 @@
+use dioxus::prelude::*;
+
+fn main() {
+ rink::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!"
+ }
+ })
+}
diff --git a/packages/tui/examples/strecher.rs b/packages/tui/examples/strecher.rs
new file mode 100644
index 000000000..b1fa93050
--- /dev/null
+++ b/packages/tui/examples/strecher.rs
@@ -0,0 +1,34 @@
+use stretch2::prelude::*;
+
+fn main() -> Result<(), Error> {
+ let mut stretch = Stretch::new();
+
+ let child = stretch.new_node(
+ Style {
+ size: Size {
+ width: Dimension::Percent(0.5),
+ height: Dimension::Auto,
+ },
+ ..Default::default()
+ },
+ &[],
+ )?;
+
+ let node = stretch.new_node(
+ Style {
+ size: Size {
+ width: Dimension::Points(100.0),
+ height: Dimension::Points(100.0),
+ },
+ justify_content: JustifyContent::Center,
+ ..Default::default()
+ },
+ &[child],
+ )?;
+
+ stretch.compute_layout(node, Size::undefined())?;
+ println!("node: {:#?}", stretch.layout(node)?);
+ println!("child: {:#?}", stretch.layout(child)?);
+
+ Ok(())
+}
diff --git a/packages/tui/examples/task.rs b/packages/tui/examples/task.rs
new file mode 100644
index 000000000..cc39d6711
--- /dev/null
+++ b/packages/tui/examples/task.rs
@@ -0,0 +1,46 @@
+use dioxus::prelude::*;
+
+fn main() {
+ rink::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+ let (count, set_count) = use_state(&cx, || 0);
+
+ use_future(&cx, move || {
+ let set_count = set_count.to_owned();
+ let update = cx.schedule_update();
+ async move {
+ loop {
+ set_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}!"
+ }
+ }
+ })
+}
+
+// use_future(&cx, || {
+// let set_count = count.setter();
+// let mut mycount = 0;
+// let update = cx.schedule_update();
+// async move {
+// loop {
+// tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+// mycount += 1;
+// set_count(mycount);
+// update();
+// }
+// }
+// });
diff --git a/packages/tui/examples/text.rs b/packages/tui/examples/text.rs
new file mode 100644
index 000000000..a38dc0b8d
--- /dev/null
+++ b/packages/tui/examples/text.rs
@@ -0,0 +1,127 @@
+use dioxus::prelude::*;
+
+fn main() {
+ rink::launch(app);
+ // rink::launch_cfg(
+ // app,
+ // rink::Config {
+ // rendering_mode: rink::RenderingMode::Ansi,
+ // },
+ // )
+}
+
+fn app(cx: Scope) -> Element {
+ let (alpha, set_alpha) = use_state(&cx, || 100);
+
+ cx.render(rsx! {
+ div {
+ width: "100%",
+ height: "100%",
+ flex_direction: "column",
+ // justify_content: "center",
+ // align_items: "center",
+ // flex_direction: "row",
+ onwheel: move |evt| {
+ set_alpha((alpha + evt.data.delta_y as i64).min(100).max(0));
+ },
+
+ p {
+ background_color: "black",
+ flex_direction: "column",
+ justify_content: "center",
+ align_items: "center",
+ // height: "10%",
+ color: "green",
+ "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"
+ }
+ 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%)"
+ }
+ }
+ }
+ })
+}
diff --git a/packages/tui/examples/ui.rsx b/packages/tui/examples/ui.rsx
new file mode 100644
index 000000000..962dbc3f1
--- /dev/null
+++ b/packages/tui/examples/ui.rsx
@@ -0,0 +1,5 @@
+div {
+ div {
+
+ }
+}
diff --git a/packages/tui/src/attributes.rs b/packages/tui/src/attributes.rs
new file mode 100644
index 000000000..a1ffcb5ea
--- /dev/null
+++ b/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,
+- [x] pub padding: Rect,
+
+- [x] pub justify_content: JustifyContent,
+- [ ] pub position: Rect,
+- [ ] pub border: Rect,
+
+- [ ] pub size: Size, ----> ??? seems to only be relevant for input?
+- [ ] pub min_size: Size,
+- [ ] pub max_size: Size,
+
+- [ ] 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,
+ 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 {
+ 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 {
+ if value.ends_with("px") {
+ if let Ok(px) = value.trim_end_matches("px").parse::() {
+ Some(UnitSystem::Point(px))
+ } else {
+ None
+ }
+ } else if value.ends_with('%') {
+ if let Ok(pct) = value.trim_end_matches('%').parse::() {
+ 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::() {
+ style.style.flex_grow = val;
+ }
+ }
+ "flex-shrink" => {
+ if let Ok(px) = value.parse::() {
+ 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 => {}
+ }
+}
diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs
new file mode 100644
index 000000000..46a0926b8
--- /dev/null
+++ b/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
+ }
+}
diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs
new file mode 100644
index 000000000..dd0469ab1
--- /dev/null
+++ b/packages/tui/src/hooks.rs
@@ -0,0 +1,652 @@
+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>>);
+// impl InputState {
+// pub fn get(cx: &ScopeState) -> InputState {
+// let inner = cx
+// .consume_context::>>()
+// .expect("Rink InputState can only be used in Rink apps!");
+// (**inner).borrow_mut().subscribe(cx.schedule_update());
+// InputState(inner)
+// }
+
+// pub fn mouse(&self) -> Option {
+// let data = (**self.0).borrow();
+// data.mouse.as_ref().map(|m| clone_mouse_data(m))
+// }
+
+// pub fn wheel(&self) -> Option {
+// 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 {
+// 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 {
+ 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)>,
+ wheel: Option,
+ last_key_pressed: Option<(KeyboardData, Instant)>,
+ screen: Option<(u16, u16)>,
+ // subscribers: Vec>,
+}
+
+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.clone()),
+ 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,
+ resolved_events: &mut Vec,
+ layout: &Stretch,
+ layouts: &mut HashMap>,
+ 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,
+ };
+
+ 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,
+ layout: &Stretch,
+ layouts: &HashMap>,
+ 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) {
+ // self.subscribers.push(f)
+ // }
+}
+
+pub struct RinkInputHandler {
+ state: Rc>,
+ queued_events: Rc>>,
+}
+
+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,
+ cx: &ScopeState,
+ ) -> (Self, Rc>) {
+ let queued_events = Rc::new(RefCell::new(Vec::new()));
+ let queued_events2 = Rc::>>::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>,
+ node: &'a VNode<'a>,
+ ) -> Vec {
+ // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus
+ fn inner(
+ queue: &Vec<(&'static str, Arc)>,
+ resolved: &mut Vec,
+ 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 {
+ 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,
+ }
+}
diff --git a/packages/tui/src/layout.rs b/packages/tui/src/layout.rs
new file mode 100644
index 000000000..53b406797
--- /dev/null
+++ b/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>,
+ 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(_) => {
+ //
+ }
+ };
+}
diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs
new file mode 100644
index 000000000..16d861435
--- /dev/null
+++ b/packages/tui/src/lib.rs
@@ -0,0 +1,213 @@
+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,
+ 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),
+ Close,
+ Tick,
+}
diff --git a/packages/tui/src/render.rs b/packages/tui/src/render.rs
new file mode 100644
index 000000000..59de80460
--- /dev/null
+++ b/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>,
+ layout: &Stretch,
+ layouts: &mut HashMap>,
+ 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,
+ ) {
+ 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,
+ ) {
+ 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,
+ );
+ }
+ }
+}
diff --git a/packages/tui/src/style.rs b/packages/tui/src/style.rs
new file mode 100644
index 000000000..943c88aed
--- /dev/null
+++ b/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 {
+ if let Some(stripped) = v.strip_suffix('%') {
+ Ok((stripped.trim().parse::()? / 100.0) * required_max_output)
+ } else {
+ Ok((v.trim().parse::()? / current_max_output) * required_max_output)
+ }
+}
+
+pub struct ParseColorError;
+
+fn parse_hex(color: &str) -> Result {
+ 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 {
+ 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 {
+ 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 {
+ 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,
+ pub bg: Option,
+ 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 Into
+
+
+
+
+
+
+
Hello World
+
This is a test
+
+
+
+
+
+
+