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 . +

+
+ +
+ + + Crates.io version + + + + Download + + + + docs.rs docs + + + + CI status + + + + + Awesome Page + + + + Discord Link + +
+ + +
+ +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 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

+
+
+
+ + + + \ No newline at end of file diff --git a/packages/tui/tests/margin.rs b/packages/tui/tests/margin.rs new file mode 100644 index 000000000..0ad4720c4 --- /dev/null +++ b/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); +}