├── .gitignore ├── .vscode └── spellright.dict ├── Cargo.toml ├── README.md ├── examples ├── border.rs ├── color_test.rs ├── components.rs ├── example.png ├── frame.rs ├── hover.rs ├── keys.rs ├── layouts.rs ├── list.rs ├── margin.rs ├── quadrants.rs ├── readme.rs ├── strecher.rs ├── task.rs ├── text.rs └── ui.rsx ├── src ├── attributes.rs ├── config.rs ├── hooks.rs ├── layout.rs ├── lib.rs ├── render.rs ├── style.rs └── widget.rs ├── test.html └── tests └── margin.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/spellright.dict: -------------------------------------------------------------------------------- 1 | esque 2 | Tui 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rink" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | tui = "0.17.0" 10 | crossterm = "0.22.1" 11 | anyhow = "1.0.42" 12 | thiserror = "1.0.24" 13 | dioxus = "0.1.8" 14 | dioxus-html = "0.1.6" 15 | hecs = "0.7.3" 16 | ctrlc = "3.2.1" 17 | bumpalo = { version = "3.8.0", features = ["boxed"] } 18 | tokio = { version = "1.15.0", features = ["full"] } 19 | futures = "0.3.19" 20 | stretch2 = { git = "https://github.com/DioxusLabs/stretch" } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | This package has moved to dioxus/packages/tui. 4 |

5 |
6 | 7 |
8 |

Rink

9 |

10 | Beautiful terminal user interfaces in Rust with Dioxus . 11 |

12 |
13 | 14 |
15 | 16 | 17 | Crates.io version 19 | 20 | 21 | 22 | Download 24 | 25 | 26 | 27 | docs.rs docs 29 | 30 | 31 | 32 | CI status 34 | 35 | 36 | 37 | 38 | Awesome Page 39 | 40 | 41 | 42 | Discord Link 43 | 44 |
45 | 46 | 47 |
48 | 49 | Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus. 50 | 51 | ```rust 52 | fn app(cx: Scope) -> Element { 53 | cx.render(rsx!{ 54 | div { 55 | width: "100%", 56 | height: "10px", 57 | background_color: "red", 58 | justify_content: "center", 59 | align_items: "center", 60 | "Hello world!" 61 | } 62 | }) 63 | } 64 | ``` 65 | 66 | ![demo app](examples/example.png) 67 | 68 | ## Background 69 | 70 | 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. 71 | 72 | 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. 73 | 74 | ## Limitations 75 | 76 | - **Subset of Html** 77 | Terminals can only render a subset of HTML. We support as much as we can. 78 | - **Particular frontend design** 79 | Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers. 80 | 81 | 82 | ## Status 83 | 84 | **WARNING: Rink is currently under construction!** 85 | 86 | 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. 87 | 88 | ## Features 89 | 90 | Rink features: 91 | - [x] Flexbox based layout system 92 | - [ ] CSS selectors 93 | - [x] inline CSS support 94 | - [ ] Built-in focusing system 95 | - [ ] high-quality keyboard support 96 | - [ ] Support for events, hooks, and callbacks 97 | * [ ] Html tags1 98 | 99 | 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. 100 | 101 | 102 | -------------------------------------------------------------------------------- /examples/border.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | rink::launch(app); 5 | } 6 | 7 | fn app(cx: Scope) -> Element { 8 | let (radius, set_radius) = use_state(&cx, || 0); 9 | 10 | cx.render(rsx! { 11 | div { 12 | width: "100%", 13 | height: "100%", 14 | justify_content: "center", 15 | align_items: "center", 16 | background_color: "hsl(248, 53%, 58%)", 17 | onwheel: move |w| set_radius((radius + w.delta_y as i8).abs()), 18 | 19 | border_style: "solid none solid double", 20 | border_width: "thick", 21 | border_radius: "{radius}px", 22 | border_color: "#0000FF #FF00FF #FF0000 #00FF00", 23 | 24 | "{radius}" 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /examples/color_test.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | // rink::launch(app); 5 | rink::launch_cfg( 6 | app, 7 | rink::Config { 8 | rendering_mode: rink::RenderingMode::Ansi, 9 | }, 10 | ); 11 | } 12 | 13 | fn app(cx: Scope) -> Element { 14 | let steps = 50; 15 | cx.render(rsx! { 16 | div{ 17 | width: "100%", 18 | height: "100%", 19 | flex_direction: "column", 20 | (0..=steps).map(|x| 21 | { 22 | let hue = x as f32*360.0/steps as f32; 23 | cx.render(rsx! { 24 | div{ 25 | width: "100%", 26 | height: "100%", 27 | flex_direction: "row", 28 | (0..=steps).map(|y| 29 | { 30 | let alpha = y as f32*100.0/steps as f32; 31 | cx.render(rsx! { 32 | div { 33 | left: "{x}px", 34 | top: "{y}px", 35 | width: "10%", 36 | height: "100%", 37 | background_color: "hsl({hue}, 100%, 50%, {alpha}%)", 38 | } 39 | }) 40 | } 41 | ) 42 | } 43 | }) 44 | } 45 | ) 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /examples/components.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | rink::launch(app); 5 | } 6 | 7 | #[derive(Props, PartialEq)] 8 | struct QuadrentProps { 9 | color: String, 10 | text: String, 11 | } 12 | 13 | fn Quadrent(cx: Scope) -> Element { 14 | cx.render(rsx! { 15 | div { 16 | border_width: "1px", 17 | width: "50%", 18 | height: "100%", 19 | background_color: "{cx.props.color}", 20 | justify_content: "center", 21 | align_items: "center", 22 | 23 | "{cx.props.text}" 24 | } 25 | }) 26 | } 27 | 28 | fn app(cx: Scope) -> Element { 29 | cx.render(rsx! { 30 | div { 31 | width: "100%", 32 | height: "100%", 33 | flex_direction: "column", 34 | 35 | div { 36 | width: "100%", 37 | height: "50%", 38 | flex_direction: "row", 39 | Quadrent{ 40 | color: "red".to_string(), 41 | text: "[A]".to_string() 42 | }, 43 | Quadrent{ 44 | color: "black".to_string(), 45 | text: "[B]".to_string() 46 | } 47 | } 48 | 49 | div { 50 | width: "100%", 51 | height: "50%", 52 | flex_direction: "row", 53 | Quadrent{ 54 | color: "green".to_string(), 55 | text: "[C]".to_string() 56 | }, 57 | Quadrent{ 58 | color: "blue".to_string(), 59 | text: "[D]".to_string() 60 | } 61 | } 62 | } 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /examples/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/rink/55ca83a397d9d12938ad186a2009ed9b76e110ea/examples/example.png -------------------------------------------------------------------------------- /examples/frame.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | rink::launch(app); 5 | } 6 | 7 | fn app(cx: Scope) -> Element { 8 | cx.render(rsx! { 9 | div { 10 | width: "100%", 11 | height: "100%", 12 | flex_direction: "column", 13 | // justify_content: "center", 14 | // align_items: "center", 15 | // flex_direction: "row", 16 | // background_color: "red", 17 | 18 | p { 19 | background_color: "black", 20 | flex_direction: "column", 21 | justify_content: "center", 22 | align_items: "center", 23 | // height: "10%", 24 | "hi" 25 | "hi" 26 | "hi" 27 | } 28 | 29 | li { 30 | background_color: "red", 31 | flex_direction: "column", 32 | justify_content: "center", 33 | align_items: "center", 34 | // height: "10%", 35 | "bib" 36 | "bib" 37 | "bib" 38 | "bib" 39 | "bib" 40 | "bib" 41 | "bib" 42 | "bib" 43 | } 44 | li { 45 | background_color: "blue", 46 | flex_direction: "column", 47 | justify_content: "center", 48 | align_items: "center", 49 | // height: "10%", 50 | "zib" 51 | "zib" 52 | "zib" 53 | "zib" 54 | "zib" 55 | "zib" 56 | "zib" 57 | "zib" 58 | "zib" 59 | "zib" 60 | "zib" 61 | "zib" 62 | "zib" 63 | } 64 | p { 65 | background_color: "yellow", 66 | "asd" 67 | } 68 | p { 69 | background_color: "green", 70 | "asd" 71 | } 72 | p { 73 | background_color: "white", 74 | "asd" 75 | } 76 | p { 77 | background_color: "cyan", 78 | "asd" 79 | } 80 | } 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /examples/hover.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryInto, sync::Arc}; 2 | 3 | use dioxus::{events::MouseData, prelude::*}; 4 | 5 | fn main() { 6 | rink::launch(app); 7 | } 8 | 9 | fn app(cx: Scope) -> Element { 10 | fn to_str(c: &[i32; 3]) -> String { 11 | "#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::() 12 | } 13 | 14 | fn get_brightness(m: Arc) -> i32 { 15 | let mb = m.buttons; 16 | let b: i32 = m.buttons.count_ones().try_into().unwrap(); 17 | 127 * b 18 | } 19 | 20 | let (q1_color, set_q1_color) = use_state(&cx, || [200; 3]); 21 | let (q2_color, set_q2_color) = use_state(&cx, || [200; 3]); 22 | let (q3_color, set_q3_color) = use_state(&cx, || [200; 3]); 23 | let (q4_color, set_q4_color) = use_state(&cx, || [200; 3]); 24 | 25 | let q1_color_str = to_str(q1_color); 26 | let q2_color_str = to_str(q2_color); 27 | let q3_color_str = to_str(q3_color); 28 | let q4_color_str = to_str(q4_color); 29 | 30 | cx.render(rsx! { 31 | div { 32 | width: "100%", 33 | height: "100%", 34 | flex_direction: "column", 35 | 36 | div { 37 | width: "100%", 38 | height: "50%", 39 | flex_direction: "row", 40 | div { 41 | border_width: "1px", 42 | width: "50%", 43 | height: "100%", 44 | justify_content: "center", 45 | align_items: "center", 46 | background_color: "{q1_color_str}", 47 | onmouseenter: move |m| set_q1_color([get_brightness(m.data), 0, 0]), 48 | onmousedown: move |m| set_q1_color([get_brightness(m.data), 0, 0]), 49 | onmouseup: move |m| set_q1_color([get_brightness(m.data), 0, 0]), 50 | onwheel: move |w| set_q1_color([q1_color[0] + (10.0*w.delta_y) as i32, 0, 0]), 51 | onmouseleave: move |_| set_q1_color([200; 3]), 52 | "click me" 53 | } 54 | div { 55 | width: "50%", 56 | height: "100%", 57 | justify_content: "center", 58 | align_items: "center", 59 | background_color: "{q2_color_str}", 60 | onmouseenter: move |m| set_q2_color([get_brightness(m.data); 3]), 61 | onmousedown: move |m| set_q2_color([get_brightness(m.data); 3]), 62 | onmouseup: move |m| set_q2_color([get_brightness(m.data); 3]), 63 | onwheel: move |w| set_q2_color([q2_color[0] + (10.0*w.delta_y) as i32;3]), 64 | onmouseleave: move |_| set_q2_color([200; 3]), 65 | "click me" 66 | } 67 | } 68 | 69 | div { 70 | width: "100%", 71 | height: "50%", 72 | flex_direction: "row", 73 | div { 74 | width: "50%", 75 | height: "100%", 76 | justify_content: "center", 77 | align_items: "center", 78 | background_color: "{q3_color_str}", 79 | onmouseenter: move |m| set_q3_color([0, get_brightness(m.data), 0]), 80 | onmousedown: move |m| set_q3_color([0, get_brightness(m.data), 0]), 81 | onmouseup: move |m| set_q3_color([0, get_brightness(m.data), 0]), 82 | onwheel: move |w| set_q3_color([0, q3_color[1] + (10.0*w.delta_y) as i32, 0]), 83 | onmouseleave: move |_| set_q3_color([200; 3]), 84 | "click me" 85 | } 86 | div { 87 | width: "50%", 88 | height: "100%", 89 | justify_content: "center", 90 | align_items: "center", 91 | background_color: "{q4_color_str}", 92 | onmouseenter: move |m| set_q4_color([0, 0, get_brightness(m.data)]), 93 | onmousedown: move |m| set_q4_color([0, 0, get_brightness(m.data)]), 94 | onmouseup: move |m| set_q4_color([0, 0, get_brightness(m.data)]), 95 | onwheel: move |w| set_q4_color([0, 0, q4_color[2] + (10.0*w.delta_y) as i32]), 96 | onmouseleave: move |_| set_q4_color([200; 3]), 97 | "click me" 98 | } 99 | } 100 | } 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /examples/keys.rs: -------------------------------------------------------------------------------- 1 | use dioxus::events::WheelEvent; 2 | use dioxus::prelude::*; 3 | use dioxus_html::on::{KeyboardEvent, MouseEvent}; 4 | use dioxus_html::KeyCode; 5 | 6 | fn main() { 7 | rink::launch(app); 8 | } 9 | 10 | fn app(cx: Scope) -> Element { 11 | let (key, set_key) = use_state(&cx, || "".to_string()); 12 | let (mouse, set_mouse) = use_state(&cx, || (0, 0)); 13 | let (count, set_count) = use_state(&cx, || 0); 14 | let (buttons, set_buttons) = use_state(&cx, || 0); 15 | let (mouse_clicked, set_mouse_clicked) = use_state(&cx, || false); 16 | 17 | cx.render(rsx! { 18 | div { 19 | width: "100%", 20 | height: "10px", 21 | background_color: "red", 22 | justify_content: "center", 23 | align_items: "center", 24 | flex_direction: "column", 25 | onkeydown: move |evt: KeyboardEvent| { 26 | match evt.data.key_code { 27 | KeyCode::LeftArrow => set_count(count + 1), 28 | KeyCode::RightArrow => set_count(count - 1), 29 | KeyCode::UpArrow => set_count(count + 10), 30 | KeyCode::DownArrow => set_count(count - 10), 31 | _ => {}, 32 | } 33 | set_key(format!("{:?} repeating: {:?}", evt.key, evt.repeat)); 34 | }, 35 | onwheel: move |evt: WheelEvent| { 36 | set_count(count + evt.data.delta_y as i64); 37 | }, 38 | ondrag: move |evt: MouseEvent| { 39 | set_mouse((evt.data.screen_x, evt.data.screen_y)); 40 | }, 41 | onmousedown: move |evt: MouseEvent| { 42 | set_mouse((evt.data.screen_x, evt.data.screen_y)); 43 | set_buttons(evt.data.buttons); 44 | set_mouse_clicked(true); 45 | }, 46 | onmouseup: move |evt: MouseEvent| { 47 | set_buttons(evt.data.buttons); 48 | set_mouse_clicked(false); 49 | }, 50 | 51 | "count: {count:?}", 52 | "key: {key}", 53 | "mouse buttons: {buttons:b}", 54 | "mouse pos: {mouse:?}", 55 | "mouse button pressed: {mouse_clicked}" 56 | } 57 | }) 58 | } 59 | 60 | fn app2<'a>(cx: Scope<'a>) -> Element<'a> { 61 | let (count, set_count) = use_state(&cx, || 0); 62 | 63 | cx.render(rsx! { 64 | div { 65 | width: "100%", 66 | height: "10px", 67 | background_color: "red", 68 | justify_content: "center", 69 | align_items: "center", 70 | oninput: move |_| set_count(count + 1), 71 | "Hello world!", 72 | h1 {}, 73 | } 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /examples/layouts.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use dioxus::prelude::*; 4 | 5 | fn main() { 6 | let mut dom = VirtualDom::new(app); 7 | dom.rebuild(); 8 | 9 | let mut layout = stretch2::Stretch::new(); 10 | let mut nodes = HashMap::new(); 11 | rink::collect_layout(&mut layout, &mut nodes, &dom, dom.base_scope().root_node()); 12 | 13 | let node = nodes 14 | .remove(&dom.base_scope().root_node().mounted_id()) 15 | .unwrap(); 16 | 17 | layout 18 | .compute_layout(node.layout, stretch2::geometry::Size::undefined()) 19 | .unwrap(); 20 | 21 | for (_id, node) in nodes.drain() { 22 | println!("{:?}", layout.layout(node.layout)); 23 | } 24 | } 25 | 26 | fn app(cx: Scope) -> Element { 27 | cx.render(rsx! { 28 | div { 29 | width: "100%", 30 | height: "100%", 31 | flex_direction: "column", 32 | 33 | div { 34 | "hi" 35 | } 36 | div { 37 | "bi" 38 | "bi" 39 | } 40 | } 41 | }) 42 | } 43 | 44 | // fn print_layout(mut nodes: HashMap, node: &VNode) { 45 | // match node { 46 | // VNode::Text(_) => todo!(), 47 | // VNode::Element(_) => todo!(), 48 | // VNode::Fragment(_) => todo!(), 49 | // VNode::Component(_) => todo!(), 50 | // VNode::Placeholder(_) => todo!(), 51 | // } 52 | // } 53 | -------------------------------------------------------------------------------- /examples/list.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | rink::launch(app); 5 | } 6 | 7 | fn app(cx: Scope) -> Element { 8 | cx.render(rsx! { 9 | div { 10 | width: "100%", 11 | height: "100%", 12 | flex_direction: "column", 13 | border_width: "1px", 14 | 15 | h1 { height: "2px", color: "green", 16 | "that's awesome!" 17 | } 18 | 19 | ul { 20 | flex_direction: "column", 21 | padding_left: "3px", 22 | (0..10).map(|i| rsx!( 23 | "> hello {i}" 24 | )) 25 | } 26 | } 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /examples/margin.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | rink::launch(app); 5 | } 6 | 7 | fn app(cx: Scope) -> Element { 8 | cx.render(rsx! { 9 | div { 10 | width: "100%", 11 | height: "100%", 12 | flex_direction: "column", 13 | background_color: "black", 14 | // margin_right: "10px", 15 | 16 | div { 17 | width: "70%", 18 | height: "70%", 19 | background_color: "green", 20 | // margin_left: "4px", 21 | 22 | div { 23 | width: "100%", 24 | height: "100%", 25 | 26 | margin_top: "2px", 27 | margin_bottom: "2px", 28 | margin_left: "2px", 29 | margin_right: "2px", 30 | flex_shrink: "0", 31 | 32 | background_color: "red", 33 | justify_content: "center", 34 | align_items: "center", 35 | flex_direction: "column", 36 | 37 | // padding_top: "2px", 38 | // padding_bottom: "2px", 39 | // padding_left: "4px", 40 | // padding_right: "4px", 41 | 42 | "[A]" 43 | "[A]" 44 | "[A]" 45 | "[A]" 46 | } 47 | } 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /examples/quadrants.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | rink::launch(app); 5 | } 6 | 7 | fn app(cx: Scope) -> Element { 8 | cx.render(rsx! { 9 | div { 10 | width: "100%", 11 | height: "100%", 12 | flex_direction: "column", 13 | 14 | div { 15 | width: "100%", 16 | height: "50%", 17 | flex_direction: "row", 18 | div { 19 | border_width: "1px", 20 | width: "50%", 21 | height: "100%", 22 | background_color: "red", 23 | justify_content: "center", 24 | align_items: "center", 25 | "[A]" 26 | } 27 | div { 28 | width: "50%", 29 | height: "100%", 30 | background_color: "black", 31 | justify_content: "center", 32 | align_items: "center", 33 | "[B]" 34 | } 35 | } 36 | 37 | div { 38 | width: "100%", 39 | height: "50%", 40 | flex_direction: "row", 41 | div { 42 | width: "50%", 43 | height: "100%", 44 | background_color: "green", 45 | justify_content: "center", 46 | align_items: "center", 47 | "[C]" 48 | } 49 | div { 50 | width: "50%", 51 | height: "100%", 52 | background_color: "blue", 53 | justify_content: "center", 54 | align_items: "center", 55 | "[D]" 56 | } 57 | } 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /examples/readme.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | rink::launch(app); 5 | } 6 | 7 | fn app(cx: Scope) -> Element { 8 | cx.render(rsx! { 9 | div { 10 | width: "100%", 11 | height: "10px", 12 | background_color: "red", 13 | justify_content: "center", 14 | align_items: "center", 15 | 16 | "Hello world!" 17 | } 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /examples/strecher.rs: -------------------------------------------------------------------------------- 1 | use stretch2::prelude::*; 2 | 3 | fn main() -> Result<(), Error> { 4 | let mut stretch = Stretch::new(); 5 | 6 | let child = stretch.new_node( 7 | Style { 8 | size: Size { 9 | width: Dimension::Percent(0.5), 10 | height: Dimension::Auto, 11 | }, 12 | ..Default::default() 13 | }, 14 | &[], 15 | )?; 16 | 17 | let node = stretch.new_node( 18 | Style { 19 | size: Size { 20 | width: Dimension::Points(100.0), 21 | height: Dimension::Points(100.0), 22 | }, 23 | justify_content: JustifyContent::Center, 24 | ..Default::default() 25 | }, 26 | &[child], 27 | )?; 28 | 29 | stretch.compute_layout(node, Size::undefined())?; 30 | println!("node: {:#?}", stretch.layout(node)?); 31 | println!("child: {:#?}", stretch.layout(child)?); 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /examples/task.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | rink::launch(app); 5 | } 6 | 7 | fn app(cx: Scope) -> Element { 8 | let (count, set_count) = use_state(&cx, || 0); 9 | 10 | use_future(&cx, move || { 11 | let set_count = set_count.to_owned(); 12 | let update = cx.schedule_update(); 13 | async move { 14 | loop { 15 | set_count.with_mut(|f| *f += 1); 16 | tokio::time::sleep(std::time::Duration::from_millis(1000)).await; 17 | update(); 18 | } 19 | } 20 | }); 21 | 22 | cx.render(rsx! { 23 | div { width: "100%", 24 | div { width: "50%", height: "5px", background_color: "blue", justify_content: "center", align_items: "center", 25 | "Hello {count}!" 26 | } 27 | div { width: "50%", height: "10px", background_color: "red", justify_content: "center", align_items: "center", 28 | "Hello {count}!" 29 | } 30 | } 31 | }) 32 | } 33 | 34 | // use_future(&cx, || { 35 | // let set_count = count.setter(); 36 | // let mut mycount = 0; 37 | // let update = cx.schedule_update(); 38 | // async move { 39 | // loop { 40 | // tokio::time::sleep(std::time::Duration::from_millis(100)).await; 41 | // mycount += 1; 42 | // set_count(mycount); 43 | // update(); 44 | // } 45 | // } 46 | // }); 47 | -------------------------------------------------------------------------------- /examples/text.rs: -------------------------------------------------------------------------------- 1 | use dioxus::prelude::*; 2 | 3 | fn main() { 4 | rink::launch(app); 5 | // rink::launch_cfg( 6 | // app, 7 | // rink::Config { 8 | // rendering_mode: rink::RenderingMode::Ansi, 9 | // }, 10 | // ) 11 | } 12 | 13 | fn app(cx: Scope) -> Element { 14 | let (alpha, set_alpha) = use_state(&cx, || 100); 15 | 16 | cx.render(rsx! { 17 | div { 18 | width: "100%", 19 | height: "100%", 20 | flex_direction: "column", 21 | // justify_content: "center", 22 | // align_items: "center", 23 | // flex_direction: "row", 24 | onwheel: move |evt| { 25 | set_alpha((alpha + evt.data.delta_y as i64).min(100).max(0)); 26 | }, 27 | 28 | p { 29 | background_color: "black", 30 | flex_direction: "column", 31 | justify_content: "center", 32 | align_items: "center", 33 | // height: "10%", 34 | color: "green", 35 | "hi" 36 | "hi" 37 | "hi" 38 | } 39 | 40 | li { 41 | background_color: "red", 42 | flex_direction: "column", 43 | justify_content: "center", 44 | align_items: "center", 45 | // height: "10%", 46 | "bib" 47 | "bib" 48 | "bib" 49 | "bib" 50 | "bib" 51 | "bib" 52 | "bib" 53 | "bib" 54 | } 55 | li { 56 | background_color: "blue", 57 | flex_direction: "column", 58 | justify_content: "center", 59 | align_items: "center", 60 | // height: "10%", 61 | "zib" 62 | "zib" 63 | "zib" 64 | "zib" 65 | "zib" 66 | "zib" 67 | } 68 | p { 69 | background_color: "yellow", 70 | "asd" 71 | } 72 | p { 73 | background_color: "green", 74 | "asd" 75 | } 76 | p { 77 | background_color: "white", 78 | "asd" 79 | } 80 | p { 81 | background_color: "cyan", 82 | "asd" 83 | } 84 | div { 85 | font_weight: "bold", 86 | color: "#666666", 87 | p { 88 | "bold" 89 | } 90 | p { 91 | font_weight: "normal", 92 | " normal" 93 | } 94 | } 95 | p { 96 | font_style: "italic", 97 | color: "red", 98 | "italic" 99 | } 100 | p { 101 | text_decoration: "underline", 102 | color: "rgba(255, 255, 255)", 103 | "underline" 104 | } 105 | p { 106 | text_decoration: "line-through", 107 | color: "hsla(10, 100%, 70%)", 108 | "line-through" 109 | } 110 | div{ 111 | position: "absolute", 112 | top: "1px", 113 | background_color: "rgba(255, 0, 0, 50%)", 114 | width: "100%", 115 | p { 116 | color: "rgba(255, 255, 255, {alpha}%)", 117 | background_color: "rgba(100, 100, 100, {alpha}%)", 118 | "rgba(255, 255, 255, {alpha}%)" 119 | } 120 | p { 121 | color: "rgba(255, 255, 255, 100%)", 122 | "rgba(255, 255, 255, 100%)" 123 | } 124 | } 125 | } 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /examples/ui.rsx: -------------------------------------------------------------------------------- 1 | div { 2 | div { 3 | 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/attributes.rs: -------------------------------------------------------------------------------- 1 | /* 2 | - [ ] pub display: Display, 3 | - [x] pub position_type: PositionType, --> kinda, stretch doesnt support everything 4 | - [ ] pub direction: Direction, 5 | 6 | - [x] pub flex_direction: FlexDirection, 7 | - [x] pub flex_wrap: FlexWrap, 8 | - [x] pub flex_grow: f32, 9 | - [x] pub flex_shrink: f32, 10 | - [x] pub flex_basis: Dimension, 11 | 12 | - [x] pub overflow: Overflow, ---> kinda implemented... stretch doesnt have support for directional overflow 13 | 14 | - [x] pub align_items: AlignItems, 15 | - [x] pub align_self: AlignSelf, 16 | - [x] pub align_content: AlignContent, 17 | 18 | - [x] pub margin: Rect, 19 | - [x] pub padding: Rect, 20 | 21 | - [x] pub justify_content: JustifyContent, 22 | - [ ] pub position: Rect, 23 | - [ ] pub border: Rect, 24 | 25 | - [ ] pub size: Size, ----> ??? seems to only be relevant for input? 26 | - [ ] pub min_size: Size, 27 | - [ ] pub max_size: Size, 28 | 29 | - [ ] pub aspect_ratio: Number, 30 | */ 31 | 32 | use stretch2::{prelude::*, style::PositionType, style::Style}; 33 | 34 | use crate::style::{RinkColor, RinkStyle}; 35 | 36 | pub struct StyleModifer { 37 | pub style: Style, 38 | pub tui_style: RinkStyle, 39 | pub tui_modifier: TuiModifier, 40 | } 41 | 42 | #[derive(Default)] 43 | pub struct TuiModifier { 44 | pub borders: Borders, 45 | } 46 | 47 | #[derive(Default)] 48 | pub struct Borders { 49 | pub top: BorderEdge, 50 | pub right: BorderEdge, 51 | pub bottom: BorderEdge, 52 | pub left: BorderEdge, 53 | } 54 | 55 | impl Borders { 56 | fn slice(&mut self) -> [&mut BorderEdge; 4] { 57 | [ 58 | &mut self.top, 59 | &mut self.right, 60 | &mut self.bottom, 61 | &mut self.left, 62 | ] 63 | } 64 | } 65 | 66 | pub struct BorderEdge { 67 | pub color: Option, 68 | pub style: BorderStyle, 69 | pub width: UnitSystem, 70 | pub radius: UnitSystem, 71 | } 72 | 73 | impl Default for BorderEdge { 74 | fn default() -> Self { 75 | Self { 76 | color: None, 77 | style: BorderStyle::NONE, 78 | width: UnitSystem::Point(0.0), 79 | radius: UnitSystem::Point(0.0), 80 | } 81 | } 82 | } 83 | 84 | #[derive(Clone, Copy)] 85 | pub enum BorderStyle { 86 | DOTTED, 87 | DASHED, 88 | SOLID, 89 | DOUBLE, 90 | GROOVE, 91 | RIDGE, 92 | INSET, 93 | OUTSET, 94 | HIDDEN, 95 | NONE, 96 | } 97 | 98 | impl BorderStyle { 99 | pub fn symbol_set(&self) -> Option { 100 | use tui::symbols::line::*; 101 | const DASHED: Set = Set { 102 | horizontal: "╌", 103 | vertical: "╎", 104 | ..NORMAL 105 | }; 106 | const DOTTED: Set = Set { 107 | horizontal: "┈", 108 | vertical: "┊", 109 | ..NORMAL 110 | }; 111 | match self { 112 | BorderStyle::DOTTED => Some(DOTTED), 113 | BorderStyle::DASHED => Some(DASHED), 114 | BorderStyle::SOLID => Some(NORMAL), 115 | BorderStyle::DOUBLE => Some(DOUBLE), 116 | BorderStyle::GROOVE => Some(NORMAL), 117 | BorderStyle::RIDGE => Some(NORMAL), 118 | BorderStyle::INSET => Some(NORMAL), 119 | BorderStyle::OUTSET => Some(NORMAL), 120 | BorderStyle::HIDDEN => None, 121 | BorderStyle::NONE => None, 122 | } 123 | } 124 | } 125 | 126 | /// applies the entire html namespace defined in dioxus-html 127 | pub fn apply_attributes( 128 | // 129 | name: &str, 130 | value: &str, 131 | style: &mut StyleModifer, 132 | ) { 133 | match name { 134 | "align-content" 135 | | "align-items" 136 | | "align-self" => apply_align(name, value, style), 137 | 138 | "animation" 139 | | "animation-delay" 140 | | "animation-direction" 141 | | "animation-duration" 142 | | "animation-fill-mode" 143 | | "animation-iteration-count" 144 | | "animation-name" 145 | | "animation-play-state" 146 | | "animation-timing-function" => apply_animation(name, value, style), 147 | 148 | "backface-visibility" => {} 149 | 150 | "background" 151 | | "background-attachment" 152 | | "background-clip" 153 | | "background-color" 154 | | "background-image" 155 | | "background-origin" 156 | | "background-position" 157 | | "background-repeat" 158 | | "background-size" => apply_background(name, value, style), 159 | 160 | "border" 161 | | "border-bottom" 162 | | "border-bottom-color" 163 | | "border-bottom-left-radius" 164 | | "border-bottom-right-radius" 165 | | "border-bottom-style" 166 | | "border-bottom-width" 167 | | "border-collapse" 168 | | "border-color" 169 | | "border-image" 170 | | "border-image-outset" 171 | | "border-image-repeat" 172 | | "border-image-slice" 173 | | "border-image-source" 174 | | "border-image-width" 175 | | "border-left" 176 | | "border-left-color" 177 | | "border-left-style" 178 | | "border-left-width" 179 | | "border-radius" 180 | | "border-right" 181 | | "border-right-color" 182 | | "border-right-style" 183 | | "border-right-width" 184 | | "border-spacing" 185 | | "border-style" 186 | | "border-top" 187 | | "border-top-color" 188 | | "border-top-left-radius" 189 | | "border-top-right-radius" 190 | | "border-top-style" 191 | | "border-top-width" 192 | | "border-width" => apply_border(name, value, style), 193 | 194 | "bottom" => {} 195 | "box-shadow" => {} 196 | "box-sizing" => {} 197 | "caption-side" => {} 198 | "clear" => {} 199 | "clip" => {} 200 | 201 | "color" => { 202 | if let Ok(c) = value.parse() { 203 | style.tui_style.fg.replace(c); 204 | } 205 | } 206 | 207 | "column-count" 208 | | "column-fill" 209 | | "column-gap" 210 | | "column-rule" 211 | | "column-rule-color" 212 | | "column-rule-style" 213 | | "column-rule-width" 214 | | "column-span" 215 | // add column-width 216 | | "column-width" => apply_column(name, value, style), 217 | 218 | "columns" => {} 219 | 220 | "content" => {} 221 | "counter-increment" => {} 222 | "counter-reset" => {} 223 | 224 | "cursor" => {} 225 | "direction" => { 226 | match value { 227 | "ltr" => style.style.direction = Direction::LTR, 228 | "rtl" => style.style.direction = Direction::RTL, 229 | _ => {} 230 | } 231 | } 232 | 233 | "display" => apply_display(name, value, style), 234 | 235 | "empty-cells" => {} 236 | 237 | "flex" 238 | | "flex-basis" 239 | | "flex-direction" 240 | | "flex-flow" 241 | | "flex-grow" 242 | | "flex-shrink" 243 | | "flex-wrap" => apply_flex(name, value, style), 244 | 245 | "float" => {} 246 | 247 | "font" 248 | | "font-family" 249 | | "font-size" 250 | | "font-size-adjust" 251 | | "font-stretch" 252 | | "font-style" 253 | | "font-variant" 254 | | "font-weight" => apply_font(name, value, style), 255 | 256 | "height" => { 257 | if let Some(v) = parse_value(value){ 258 | style.style.size.height = match v { 259 | UnitSystem::Percent(v)=> Dimension::Percent(v/100.0), 260 | UnitSystem::Point(v)=> Dimension::Points(v), 261 | }; 262 | } 263 | } 264 | "justify-content" => { 265 | use JustifyContent::*; 266 | style.style.justify_content = match value { 267 | "flex-start" => FlexStart, 268 | "flex-end" => FlexEnd, 269 | "center" => Center, 270 | "space-between" => SpaceBetween, 271 | "space-around" => SpaceAround, 272 | "space-evenly" => SpaceEvenly, 273 | _ => FlexStart, 274 | }; 275 | } 276 | "left" => {} 277 | "letter-spacing" => {} 278 | "line-height" => {} 279 | 280 | "list-style" 281 | | "list-style-image" 282 | | "list-style-position" 283 | | "list-style-type" => {} 284 | 285 | "margin" 286 | | "margin-bottom" 287 | | "margin-left" 288 | | "margin-right" 289 | | "margin-top" => apply_margin(name, value, style), 290 | 291 | "max-height" => {} 292 | "max-width" => {} 293 | "min-height" => {} 294 | "min-width" => {} 295 | 296 | "opacity" => {} 297 | "order" => {} 298 | "outline" => {} 299 | 300 | "outline-color" 301 | | "outline-offset" 302 | | "outline-style" 303 | | "outline-width" => {} 304 | 305 | "overflow" 306 | | "overflow-x" 307 | | "overflow-y" => apply_overflow(name, value, style), 308 | 309 | "padding" 310 | | "padding-bottom" 311 | | "padding-left" 312 | | "padding-right" 313 | | "padding-top" => apply_padding(name, value, style), 314 | 315 | "page-break-after" 316 | | "page-break-before" 317 | | "page-break-inside" => {} 318 | 319 | "perspective" 320 | | "perspective-origin" => {} 321 | 322 | "position" => { 323 | match value { 324 | "static" => {} 325 | "relative" => style.style.position_type = PositionType::Relative, 326 | "fixed" => {} 327 | "absolute" => style.style.position_type = PositionType::Absolute, 328 | "sticky" => {} 329 | _ => {} 330 | } 331 | 332 | } 333 | 334 | "pointer-events" => {} 335 | 336 | "quotes" => {} 337 | "resize" => {} 338 | "right" => {} 339 | "tab-size" => {} 340 | "table-layout" => {} 341 | 342 | "text-align" 343 | | "text-align-last" 344 | | "text-decoration" 345 | | "text-decoration-color" 346 | | "text-decoration-line" 347 | | "text-decoration-style" 348 | | "text-indent" 349 | | "text-justify" 350 | | "text-overflow" 351 | | "text-shadow" 352 | | "text-transform" => apply_text(name, value, style), 353 | 354 | "top" => {} 355 | 356 | "transform" 357 | | "transform-origin" 358 | | "transform-style" => apply_transform(name, value, style), 359 | 360 | "transition" 361 | | "transition-delay" 362 | | "transition-duration" 363 | | "transition-property" 364 | | "transition-timing-function" => apply_transition(name, value, style), 365 | 366 | "vertical-align" => {} 367 | "visibility" => {} 368 | "white-space" => {} 369 | "width" => { 370 | if let Some(v) = parse_value(value){ 371 | style.style.size.width = match v { 372 | UnitSystem::Percent(v)=> Dimension::Percent(v/100.0), 373 | UnitSystem::Point(v)=> Dimension::Points(v), 374 | }; 375 | } 376 | } 377 | "word-break" => {} 378 | "word-spacing" => {} 379 | "word-wrap" => {} 380 | "z-index" => {} 381 | _ => {} 382 | } 383 | } 384 | 385 | #[derive(Clone, Copy)] 386 | pub enum UnitSystem { 387 | Percent(f32), 388 | Point(f32), 389 | } 390 | 391 | fn parse_value(value: &str) -> Option { 392 | if value.ends_with("px") { 393 | if let Ok(px) = value.trim_end_matches("px").parse::() { 394 | Some(UnitSystem::Point(px)) 395 | } else { 396 | None 397 | } 398 | } else if value.ends_with('%') { 399 | if let Ok(pct) = value.trim_end_matches('%').parse::() { 400 | Some(UnitSystem::Percent(pct)) 401 | } else { 402 | None 403 | } 404 | } else { 405 | None 406 | } 407 | } 408 | 409 | fn apply_overflow(name: &str, value: &str, style: &mut StyleModifer) { 410 | match name { 411 | // todo: add more overflow support to stretch2 412 | "overflow" | "overflow-x" | "overflow-y" => { 413 | style.style.overflow = match value { 414 | "auto" => Overflow::Visible, 415 | "hidden" => Overflow::Hidden, 416 | "scroll" => Overflow::Scroll, 417 | "visible" => Overflow::Visible, 418 | _ => Overflow::Visible, 419 | }; 420 | } 421 | _ => {} 422 | } 423 | } 424 | 425 | fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) { 426 | style.style.display = match value { 427 | "flex" => Display::Flex, 428 | "block" => Display::None, 429 | _ => Display::Flex, 430 | } 431 | 432 | // TODO: there are way more variants 433 | // stretch needs to be updated to handle them 434 | // 435 | // "block" => Display::Block, 436 | // "inline" => Display::Inline, 437 | // "inline-block" => Display::InlineBlock, 438 | // "inline-table" => Display::InlineTable, 439 | // "list-item" => Display::ListItem, 440 | // "run-in" => Display::RunIn, 441 | // "table" => Display::Table, 442 | // "table-caption" => Display::TableCaption, 443 | // "table-cell" => Display::TableCell, 444 | // "table-column" => Display::TableColumn, 445 | // "table-column-group" => Display::TableColumnGroup, 446 | // "table-footer-group" => Display::TableFooterGroup, 447 | // "table-header-group" => Display::TableHeaderGroup, 448 | // "table-row" => Display::TableRow, 449 | // "table-row-group" => Display::TableRowGroup, 450 | // "none" => Display::None, 451 | // _ => Display::Inline, 452 | } 453 | 454 | fn apply_background(name: &str, value: &str, style: &mut StyleModifer) { 455 | match name { 456 | "background-color" => { 457 | if let Ok(c) = value.parse() { 458 | style.tui_style.bg.replace(c); 459 | } 460 | } 461 | "background" => {} 462 | "background-attachment" => {} 463 | "background-clip" => {} 464 | "background-image" => {} 465 | "background-origin" => {} 466 | "background-position" => {} 467 | "background-repeat" => {} 468 | "background-size" => {} 469 | _ => {} 470 | } 471 | } 472 | 473 | fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { 474 | fn parse_border_style(v: &str) -> BorderStyle { 475 | match v { 476 | "dotted" => BorderStyle::DOTTED, 477 | "dashed" => BorderStyle::DASHED, 478 | "solid" => BorderStyle::SOLID, 479 | "double" => BorderStyle::DOUBLE, 480 | "groove" => BorderStyle::GROOVE, 481 | "ridge" => BorderStyle::RIDGE, 482 | "inset" => BorderStyle::INSET, 483 | "outset" => BorderStyle::OUTSET, 484 | "none" => BorderStyle::NONE, 485 | "hidden" => BorderStyle::HIDDEN, 486 | _ => todo!(), 487 | } 488 | } 489 | match name { 490 | "border" => {} 491 | "border-bottom" => {} 492 | "border-bottom-color" => { 493 | if let Ok(c) = value.parse() { 494 | style.tui_modifier.borders.bottom.color = Some(c); 495 | } 496 | } 497 | "border-bottom-left-radius" => { 498 | if let Some(v) = parse_value(value) { 499 | style.tui_modifier.borders.left.radius = v; 500 | } 501 | } 502 | "border-bottom-right-radius" => { 503 | if let Some(v) = parse_value(value) { 504 | style.tui_modifier.borders.right.radius = v; 505 | } 506 | } 507 | "border-bottom-style" => { 508 | style.tui_modifier.borders.bottom.style = parse_border_style(value) 509 | } 510 | "border-bottom-width" => { 511 | if let Some(v) = parse_value(value) { 512 | style.tui_modifier.borders.bottom.width = v; 513 | } 514 | } 515 | "border-collapse" => {} 516 | "border-color" => { 517 | let values: Vec<_> = value.split(' ').collect(); 518 | if values.len() == 1 { 519 | if let Ok(c) = values[0].parse() { 520 | style 521 | .tui_modifier 522 | .borders 523 | .slice() 524 | .iter_mut() 525 | .for_each(|b| b.color = Some(c)); 526 | } 527 | } else { 528 | for (v, b) in values 529 | .into_iter() 530 | .zip(style.tui_modifier.borders.slice().iter_mut()) 531 | { 532 | if let Ok(c) = v.parse() { 533 | b.color = Some(c); 534 | } 535 | } 536 | } 537 | } 538 | "border-image" => {} 539 | "border-image-outset" => {} 540 | "border-image-repeat" => {} 541 | "border-image-slice" => {} 542 | "border-image-source" => {} 543 | "border-image-width" => {} 544 | "border-left" => {} 545 | "border-left-color" => { 546 | if let Ok(c) = value.parse() { 547 | style.tui_modifier.borders.left.color = Some(c); 548 | } 549 | } 550 | "border-left-style" => style.tui_modifier.borders.left.style = parse_border_style(value), 551 | "border-left-width" => { 552 | if let Some(v) = parse_value(value) { 553 | style.tui_modifier.borders.left.width = v; 554 | } 555 | } 556 | "border-radius" => { 557 | let values: Vec<_> = value.split(' ').collect(); 558 | if values.len() == 1 { 559 | if let Some(r) = parse_value(values[0]) { 560 | style 561 | .tui_modifier 562 | .borders 563 | .slice() 564 | .iter_mut() 565 | .for_each(|b| b.radius = r); 566 | } 567 | } else { 568 | for (v, b) in values 569 | .into_iter() 570 | .zip(style.tui_modifier.borders.slice().iter_mut()) 571 | { 572 | if let Some(r) = parse_value(v) { 573 | b.radius = r; 574 | } 575 | } 576 | } 577 | } 578 | "border-right" => {} 579 | "border-right-color" => { 580 | if let Ok(c) = value.parse() { 581 | style.tui_modifier.borders.right.color = Some(c); 582 | } 583 | } 584 | "border-right-style" => style.tui_modifier.borders.right.style = parse_border_style(value), 585 | "border-right-width" => { 586 | if let Some(v) = parse_value(value) { 587 | style.tui_modifier.borders.right.width = v; 588 | } 589 | } 590 | "border-spacing" => {} 591 | "border-style" => { 592 | let values: Vec<_> = value.split(' ').collect(); 593 | if values.len() == 1 { 594 | let border_style = parse_border_style(values[0]); 595 | style 596 | .tui_modifier 597 | .borders 598 | .slice() 599 | .iter_mut() 600 | .for_each(|b| b.style = border_style); 601 | } else { 602 | for (v, b) in values 603 | .into_iter() 604 | .zip(style.tui_modifier.borders.slice().iter_mut()) 605 | { 606 | b.style = parse_border_style(v); 607 | } 608 | } 609 | } 610 | "border-top" => {} 611 | "border-top-color" => { 612 | if let Ok(c) = value.parse() { 613 | style.tui_modifier.borders.top.color = Some(c); 614 | } 615 | } 616 | "border-top-left-radius" => { 617 | if let Some(v) = parse_value(value) { 618 | style.tui_modifier.borders.left.radius = v; 619 | } 620 | } 621 | "border-top-right-radius" => { 622 | if let Some(v) = parse_value(value) { 623 | style.tui_modifier.borders.right.radius = v; 624 | } 625 | } 626 | "border-top-style" => style.tui_modifier.borders.top.style = parse_border_style(value), 627 | "border-top-width" => { 628 | if let Some(v) = parse_value(value) { 629 | style.tui_modifier.borders.top.width = v; 630 | } 631 | } 632 | "border-width" => { 633 | let values: Vec<_> = value.split(' ').collect(); 634 | if values.len() == 1 { 635 | if let Some(w) = parse_value(values[0]) { 636 | style 637 | .tui_modifier 638 | .borders 639 | .slice() 640 | .iter_mut() 641 | .for_each(|b| b.width = w); 642 | } 643 | } else { 644 | for (v, b) in values 645 | .into_iter() 646 | .zip(style.tui_modifier.borders.slice().iter_mut()) 647 | { 648 | if let Some(w) = parse_value(v) { 649 | b.width = w; 650 | } 651 | } 652 | } 653 | } 654 | _ => (), 655 | } 656 | } 657 | 658 | fn apply_animation(name: &str, _value: &str, _style: &mut StyleModifer) { 659 | match name { 660 | "animation" => {} 661 | "animation-delay" => {} 662 | "animation-direction =>{}" => {} 663 | "animation-duration" => {} 664 | "animation-fill-mode" => {} 665 | "animation-itera =>{}tion-count" => {} 666 | "animation-name" => {} 667 | "animation-play-state" => {} 668 | "animation-timing-function" => {} 669 | _ => {} 670 | } 671 | } 672 | 673 | fn apply_column(name: &str, _value: &str, _style: &mut StyleModifer) { 674 | match name { 675 | "column-count" => {} 676 | "column-fill" => {} 677 | "column-gap" => {} 678 | "column-rule" => {} 679 | "column-rule-color" => {} 680 | "column-rule-style" => {} 681 | "column-rule-width" => {} 682 | "column-span" => {} 683 | "column-width" => {} 684 | _ => {} 685 | } 686 | } 687 | 688 | fn apply_flex(name: &str, value: &str, style: &mut StyleModifer) { 689 | // - [x] pub flex_direction: FlexDirection, 690 | // - [x] pub flex_wrap: FlexWrap, 691 | // - [x] pub flex_grow: f32, 692 | // - [x] pub flex_shrink: f32, 693 | // - [x] pub flex_basis: Dimension, 694 | 695 | match name { 696 | "flex" => {} 697 | "flex-direction" => { 698 | use FlexDirection::*; 699 | style.style.flex_direction = match value { 700 | "row" => Row, 701 | "row-reverse" => RowReverse, 702 | "column" => Column, 703 | "column-reverse" => ColumnReverse, 704 | _ => Row, 705 | }; 706 | } 707 | "flex-basis" => { 708 | if let Some(v) = parse_value(value) { 709 | style.style.flex_basis = match v { 710 | UnitSystem::Percent(v) => Dimension::Percent(v / 100.0), 711 | UnitSystem::Point(v) => Dimension::Points(v), 712 | }; 713 | } 714 | } 715 | "flex-flow" => {} 716 | "flex-grow" => { 717 | if let Ok(val) = value.parse::() { 718 | style.style.flex_grow = val; 719 | } 720 | } 721 | "flex-shrink" => { 722 | if let Ok(px) = value.parse::() { 723 | style.style.flex_shrink = px; 724 | } 725 | } 726 | "flex-wrap" => { 727 | use FlexWrap::*; 728 | style.style.flex_wrap = match value { 729 | "nowrap" => NoWrap, 730 | "wrap" => Wrap, 731 | "wrap-reverse" => WrapReverse, 732 | _ => NoWrap, 733 | }; 734 | } 735 | _ => {} 736 | } 737 | } 738 | 739 | fn apply_font(name: &str, value: &str, style: &mut StyleModifer) { 740 | use tui::style::Modifier; 741 | match name { 742 | "font" => (), 743 | "font-family" => (), 744 | "font-size" => (), 745 | "font-size-adjust" => (), 746 | "font-stretch" => (), 747 | "font-style" => match value { 748 | "italic" => style.tui_style = style.tui_style.add_modifier(Modifier::ITALIC), 749 | "oblique" => style.tui_style = style.tui_style.add_modifier(Modifier::ITALIC), 750 | _ => (), 751 | }, 752 | "font-variant" => todo!(), 753 | "font-weight" => match value { 754 | "bold" => style.tui_style = style.tui_style.add_modifier(Modifier::BOLD), 755 | "normal" => style.tui_style = style.tui_style.remove_modifier(Modifier::BOLD), 756 | _ => (), 757 | }, 758 | _ => (), 759 | } 760 | } 761 | 762 | fn apply_padding(name: &str, value: &str, style: &mut StyleModifer) { 763 | match parse_value(value) { 764 | Some(UnitSystem::Percent(v)) => match name { 765 | "padding" => { 766 | let v = Dimension::Percent(v / 100.0); 767 | style.style.padding.top = v; 768 | style.style.padding.bottom = v; 769 | style.style.padding.start = v; 770 | style.style.padding.end = v; 771 | } 772 | "padding-bottom" => style.style.padding.bottom = Dimension::Percent(v / 100.0), 773 | "padding-left" => style.style.padding.start = Dimension::Percent(v / 100.0), 774 | "padding-right" => style.style.padding.end = Dimension::Percent(v / 100.0), 775 | "padding-top" => style.style.padding.top = Dimension::Percent(v / 100.0), 776 | _ => {} 777 | }, 778 | Some(UnitSystem::Point(v)) => match name { 779 | "padding" => { 780 | style.style.padding.top = Dimension::Points(v); 781 | style.style.padding.bottom = Dimension::Points(v); 782 | style.style.padding.start = Dimension::Points(v); 783 | style.style.padding.end = Dimension::Points(v); 784 | } 785 | "padding-bottom" => style.style.padding.bottom = Dimension::Points(v), 786 | "padding-left" => style.style.padding.start = Dimension::Points(v), 787 | "padding-right" => style.style.padding.end = Dimension::Points(v), 788 | "padding-top" => style.style.padding.top = Dimension::Points(v), 789 | _ => {} 790 | }, 791 | None => {} 792 | } 793 | } 794 | 795 | fn apply_text(name: &str, value: &str, style: &mut StyleModifer) { 796 | use tui::style::Modifier; 797 | 798 | match name { 799 | "text-align" => todo!(), 800 | "text-align-last" => todo!(), 801 | "text-decoration" | "text-decoration-line" => { 802 | for v in value.split(' ') { 803 | match v { 804 | "line-through" => { 805 | style.tui_style = style.tui_style.add_modifier(Modifier::CROSSED_OUT) 806 | } 807 | "underline" => { 808 | style.tui_style = style.tui_style.add_modifier(Modifier::UNDERLINED) 809 | } 810 | _ => (), 811 | } 812 | } 813 | } 814 | "text-decoration-color" => todo!(), 815 | "text-decoration-style" => todo!(), 816 | "text-indent" => todo!(), 817 | "text-justify" => todo!(), 818 | "text-overflow" => todo!(), 819 | "text-shadow" => todo!(), 820 | "text-transform" => todo!(), 821 | _ => todo!(), 822 | } 823 | } 824 | 825 | fn apply_transform(_name: &str, _value: &str, _style: &mut StyleModifer) { 826 | todo!() 827 | } 828 | 829 | fn apply_transition(_name: &str, _value: &str, _style: &mut StyleModifer) { 830 | todo!() 831 | } 832 | 833 | fn apply_align(name: &str, value: &str, style: &mut StyleModifer) { 834 | match name { 835 | "align-items" => { 836 | use AlignItems::*; 837 | style.style.align_items = match value { 838 | "flex-start" => FlexStart, 839 | "flex-end" => FlexEnd, 840 | "center" => Center, 841 | "baseline" => Baseline, 842 | "stretch" => Stretch, 843 | _ => FlexStart, 844 | }; 845 | } 846 | "align-content" => { 847 | use AlignContent::*; 848 | style.style.align_content = match value { 849 | "flex-start" => FlexStart, 850 | "flex-end" => FlexEnd, 851 | "center" => Center, 852 | "space-between" => SpaceBetween, 853 | "space-around" => SpaceAround, 854 | _ => FlexStart, 855 | }; 856 | } 857 | "align-self" => { 858 | use AlignSelf::*; 859 | style.style.align_self = match value { 860 | "auto" => Auto, 861 | "flex-start" => FlexStart, 862 | "flex-end" => FlexEnd, 863 | "center" => Center, 864 | "baseline" => Baseline, 865 | "stretch" => Stretch, 866 | _ => Auto, 867 | }; 868 | } 869 | _ => {} 870 | } 871 | } 872 | 873 | pub fn apply_size(_name: &str, _value: &str, _style: &mut StyleModifer) { 874 | // 875 | } 876 | 877 | pub fn apply_margin(name: &str, value: &str, style: &mut StyleModifer) { 878 | match parse_value(value) { 879 | Some(UnitSystem::Percent(v)) => match name { 880 | "margin" => { 881 | let v = Dimension::Percent(v / 100.0); 882 | style.style.margin.top = v; 883 | style.style.margin.bottom = v; 884 | style.style.margin.start = v; 885 | style.style.margin.end = v; 886 | } 887 | "margin-top" => style.style.margin.top = Dimension::Percent(v / 100.0), 888 | "margin-bottom" => style.style.margin.bottom = Dimension::Percent(v / 100.0), 889 | "margin-left" => style.style.margin.start = Dimension::Percent(v / 100.0), 890 | "margin-right" => style.style.margin.end = Dimension::Percent(v / 100.0), 891 | _ => {} 892 | }, 893 | Some(UnitSystem::Point(v)) => match name { 894 | "margin" => { 895 | style.style.margin.top = Dimension::Points(v); 896 | style.style.margin.bottom = Dimension::Points(v); 897 | style.style.margin.start = Dimension::Points(v); 898 | style.style.margin.end = Dimension::Points(v); 899 | } 900 | "margin-top" => style.style.margin.top = Dimension::Points(v), 901 | "margin-bottom" => style.style.margin.bottom = Dimension::Points(v), 902 | "margin-left" => style.style.margin.start = Dimension::Points(v), 903 | "margin-right" => style.style.margin.end = Dimension::Points(v), 904 | _ => {} 905 | }, 906 | None => {} 907 | } 908 | } 909 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default, Clone, Copy)] 2 | pub struct Config { 3 | pub rendering_mode: RenderingMode, 4 | } 5 | 6 | #[derive(Clone, Copy)] 7 | pub enum RenderingMode { 8 | /// only 16 colors by accessed by name, no alpha support 9 | BaseColors, 10 | /// 8 bit colors, will be downsampled from rgb colors 11 | Ansi, 12 | /// 24 bit colors, most terminals support this 13 | Rgb, 14 | } 15 | 16 | impl Default for RenderingMode { 17 | fn default() -> Self { 18 | RenderingMode::Rgb 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{ 2 | Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, MouseButton, MouseEventKind, 3 | }; 4 | use dioxus::core::*; 5 | 6 | use dioxus_html::{on::*, KeyCode}; 7 | use futures::{channel::mpsc::UnboundedReceiver, StreamExt}; 8 | use std::{ 9 | any::Any, 10 | cell::RefCell, 11 | collections::{HashMap, HashSet}, 12 | rc::Rc, 13 | sync::Arc, 14 | time::{Duration, Instant}, 15 | }; 16 | use stretch2::{prelude::Layout, Stretch}; 17 | 18 | use crate::TuiNode; 19 | 20 | // a wrapper around the input state for easier access 21 | // todo: fix loop 22 | // pub struct InputState(Rc>>); 23 | // impl InputState { 24 | // pub fn get(cx: &ScopeState) -> InputState { 25 | // let inner = cx 26 | // .consume_context::>>() 27 | // .expect("Rink InputState can only be used in Rink apps!"); 28 | // (**inner).borrow_mut().subscribe(cx.schedule_update()); 29 | // InputState(inner) 30 | // } 31 | 32 | // pub fn mouse(&self) -> Option { 33 | // let data = (**self.0).borrow(); 34 | // data.mouse.as_ref().map(|m| clone_mouse_data(m)) 35 | // } 36 | 37 | // pub fn wheel(&self) -> Option { 38 | // let data = (**self.0).borrow(); 39 | // data.wheel.as_ref().map(|w| clone_wheel_data(w)) 40 | // } 41 | 42 | // pub fn screen(&self) -> Option<(u16, u16)> { 43 | // let data = (**self.0).borrow(); 44 | // data.screen.as_ref().map(|m| m.clone()) 45 | // } 46 | 47 | // pub fn last_key_pressed(&self) -> Option { 48 | // let data = (**self.0).borrow(); 49 | // data.last_key_pressed 50 | // .as_ref() 51 | // .map(|k| clone_keyboard_data(&k.0)) 52 | // } 53 | // } 54 | 55 | type EventCore = (&'static str, EventData); 56 | 57 | #[derive(Debug)] 58 | enum EventData { 59 | Mouse(MouseData), 60 | Wheel(WheelData), 61 | Screen((u16, u16)), 62 | Keyboard(KeyboardData), 63 | } 64 | impl EventData { 65 | fn into_any(self) -> Arc { 66 | match self { 67 | Self::Mouse(m) => Arc::new(m), 68 | Self::Wheel(w) => Arc::new(w), 69 | Self::Screen(s) => Arc::new(s), 70 | Self::Keyboard(k) => Arc::new(k), 71 | } 72 | } 73 | } 74 | 75 | const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); 76 | 77 | pub struct InnerInputState { 78 | mouse: Option<(MouseData, Vec)>, 79 | wheel: Option, 80 | last_key_pressed: Option<(KeyboardData, Instant)>, 81 | screen: Option<(u16, u16)>, 82 | // subscribers: Vec>, 83 | } 84 | 85 | impl InnerInputState { 86 | fn new() -> Self { 87 | Self { 88 | mouse: None, 89 | wheel: None, 90 | last_key_pressed: None, 91 | screen: None, 92 | // subscribers: Vec::new(), 93 | } 94 | } 95 | 96 | // stores current input state and transforms events based on that state 97 | fn apply_event(&mut self, evt: &mut EventCore) { 98 | match evt.1 { 99 | // limitations: only two buttons may be held at once 100 | EventData::Mouse(ref mut m) => match &mut self.mouse { 101 | Some(state) => { 102 | let mut buttons = state.0.buttons; 103 | state.0 = clone_mouse_data(m); 104 | match evt.0 { 105 | // this code only runs when there are no buttons down 106 | "mouseup" => { 107 | buttons = 0; 108 | state.1 = Vec::new(); 109 | } 110 | "mousedown" => { 111 | if state.1.contains(&m.buttons) { 112 | // if we already pressed a button and there is another button released the button crossterm sends is the button remaining 113 | if state.1.len() > 1 { 114 | evt.0 = "mouseup"; 115 | state.1 = vec![m.buttons]; 116 | } 117 | // otherwise some other button was pressed. In testing it was consistantly this mapping 118 | else { 119 | match m.buttons { 120 | 0x01 => state.1.push(0x02), 121 | 0x02 => state.1.push(0x01), 122 | 0x04 => state.1.push(0x01), 123 | _ => (), 124 | } 125 | } 126 | } else { 127 | state.1.push(m.buttons); 128 | } 129 | 130 | buttons = state.1.iter().copied().reduce(|a, b| a | b).unwrap(); 131 | } 132 | _ => (), 133 | } 134 | state.0.buttons = buttons; 135 | m.buttons = buttons; 136 | } 137 | None => { 138 | self.mouse = Some(( 139 | clone_mouse_data(m), 140 | if m.buttons == 0 { 141 | Vec::new() 142 | } else { 143 | vec![m.buttons] 144 | }, 145 | )); 146 | } 147 | }, 148 | EventData::Wheel(ref w) => self.wheel = Some(clone_wheel_data(w)), 149 | EventData::Screen(ref s) => self.screen = Some(s.clone()), 150 | EventData::Keyboard(ref mut k) => { 151 | let repeat = self 152 | .last_key_pressed 153 | .as_ref() 154 | .filter(|k2| k2.0.key == k.key && k2.1.elapsed() < MAX_REPEAT_TIME) 155 | .is_some(); 156 | k.repeat = repeat; 157 | let new = clone_keyboard_data(k); 158 | self.last_key_pressed = Some((new, Instant::now())); 159 | } 160 | } 161 | } 162 | 163 | fn update<'a>( 164 | &mut self, 165 | dom: &'a VirtualDom, 166 | evts: &mut Vec, 167 | resolved_events: &mut Vec, 168 | layout: &Stretch, 169 | layouts: &mut HashMap>, 170 | node: &'a VNode<'a>, 171 | ) { 172 | struct Data<'b> { 173 | new_pos: (i32, i32), 174 | old_pos: Option<(i32, i32)>, 175 | clicked: bool, 176 | released: bool, 177 | wheel_delta: f64, 178 | mouse_data: &'b MouseData, 179 | wheel_data: &'b Option, 180 | }; 181 | 182 | fn layout_contains_point(layout: &Layout, point: (i32, i32)) -> bool { 183 | layout.location.x as i32 <= point.0 184 | && layout.location.x as i32 + layout.size.width as i32 >= point.0 185 | && layout.location.y as i32 <= point.1 186 | && layout.location.y as i32 + layout.size.height as i32 >= point.1 187 | } 188 | 189 | fn get_mouse_events<'c, 'd>( 190 | dom: &'c VirtualDom, 191 | resolved_events: &mut Vec, 192 | layout: &Stretch, 193 | layouts: &HashMap>, 194 | node: &'c VNode<'c>, 195 | data: &'d Data<'d>, 196 | ) -> HashSet<&'static str> { 197 | match node { 198 | VNode::Fragment(f) => { 199 | let mut union = HashSet::new(); 200 | for child in f.children { 201 | union = union 202 | .union(&get_mouse_events( 203 | dom, 204 | resolved_events, 205 | layout, 206 | layouts, 207 | child, 208 | data, 209 | )) 210 | .copied() 211 | .collect(); 212 | } 213 | return union; 214 | } 215 | 216 | VNode::Component(vcomp) => { 217 | let idx = vcomp.scope.get().unwrap(); 218 | let new_node = dom.get_scope(idx).unwrap().root_node(); 219 | return get_mouse_events(dom, resolved_events, layout, layouts, new_node, data); 220 | } 221 | 222 | VNode::Placeholder(_) => return HashSet::new(), 223 | 224 | VNode::Element(_) | VNode::Text(_) => {} 225 | } 226 | 227 | let id = node.try_mounted_id().unwrap(); 228 | let node = layouts.get(&id).unwrap(); 229 | 230 | let node_layout = layout.layout(node.layout).unwrap(); 231 | 232 | let previously_contained = data 233 | .old_pos 234 | .filter(|pos| layout_contains_point(node_layout, *pos)) 235 | .is_some(); 236 | let currently_contains = layout_contains_point(node_layout, data.new_pos); 237 | 238 | match node.node { 239 | VNode::Element(el) => { 240 | let mut events = HashSet::new(); 241 | if previously_contained || currently_contains { 242 | for c in el.children { 243 | events = events 244 | .union(&get_mouse_events( 245 | dom, 246 | resolved_events, 247 | layout, 248 | layouts, 249 | c, 250 | data, 251 | )) 252 | .copied() 253 | .collect(); 254 | } 255 | } 256 | let mut try_create_event = |name| { 257 | // only trigger event if the event was not triggered already by a child 258 | if events.insert(name) { 259 | resolved_events.push(UserEvent { 260 | scope_id: None, 261 | priority: EventPriority::Medium, 262 | name, 263 | element: Some(el.id.get().unwrap()), 264 | data: Arc::new(clone_mouse_data(data.mouse_data)), 265 | }) 266 | } 267 | }; 268 | if currently_contains { 269 | if !previously_contained { 270 | try_create_event("mouseenter"); 271 | try_create_event("mouseover"); 272 | } 273 | if data.clicked { 274 | try_create_event("mousedown"); 275 | } 276 | if data.released { 277 | try_create_event("mouseup"); 278 | match data.mouse_data.button { 279 | 0 => try_create_event("click"), 280 | 2 => try_create_event("contextmenu"), 281 | _ => (), 282 | } 283 | } 284 | if let Some(w) = data.wheel_data { 285 | if data.wheel_delta != 0.0 { 286 | resolved_events.push(UserEvent { 287 | scope_id: None, 288 | priority: EventPriority::Medium, 289 | name: "wheel", 290 | element: Some(el.id.get().unwrap()), 291 | data: Arc::new(clone_wheel_data(w)), 292 | }) 293 | } 294 | } 295 | } else { 296 | if previously_contained { 297 | try_create_event("mouseleave"); 298 | try_create_event("mouseout"); 299 | } 300 | } 301 | events 302 | } 303 | VNode::Text(_) => HashSet::new(), 304 | _ => todo!(), 305 | } 306 | } 307 | 308 | let previous_mouse = self 309 | .mouse 310 | .as_ref() 311 | .map(|m| (clone_mouse_data(&m.0), m.1.clone())); 312 | // println!("{previous_mouse:?}"); 313 | 314 | self.wheel = None; 315 | 316 | for e in evts.iter_mut() { 317 | self.apply_event(e); 318 | } 319 | 320 | // resolve hover events 321 | if let Some(mouse) = &self.mouse { 322 | let new_pos = (mouse.0.screen_x, mouse.0.screen_y); 323 | let old_pos = previous_mouse 324 | .as_ref() 325 | .map(|m| (m.0.screen_x, m.0.screen_y)); 326 | let clicked = 327 | (!mouse.0.buttons & previous_mouse.as_ref().map(|m| m.0.buttons).unwrap_or(0)) > 0; 328 | let released = 329 | (mouse.0.buttons & !previous_mouse.map(|m| m.0.buttons).unwrap_or(0)) > 0; 330 | let wheel_delta = self.wheel.as_ref().map_or(0.0, |w| w.delta_y); 331 | let mouse_data = &mouse.0; 332 | let wheel_data = &self.wheel; 333 | let data = Data { 334 | new_pos, 335 | old_pos, 336 | clicked, 337 | released, 338 | wheel_delta, 339 | mouse_data, 340 | wheel_data, 341 | }; 342 | get_mouse_events(dom, resolved_events, layout, layouts, node, &data); 343 | } 344 | 345 | // for s in &self.subscribers { 346 | // s(); 347 | // } 348 | } 349 | 350 | // fn subscribe(&mut self, f: Rc) { 351 | // self.subscribers.push(f) 352 | // } 353 | } 354 | 355 | pub struct RinkInputHandler { 356 | state: Rc>, 357 | queued_events: Rc>>, 358 | } 359 | 360 | impl RinkInputHandler { 361 | /// global context that handles events 362 | /// limitations: GUI key modifier is never detected, key up events are not detected, and only two mouse buttons may be pressed at once 363 | pub fn new( 364 | mut receiver: UnboundedReceiver, 365 | cx: &ScopeState, 366 | ) -> (Self, Rc>) { 367 | let queued_events = Rc::new(RefCell::new(Vec::new())); 368 | let queued_events2 = Rc::>>::downgrade(&queued_events); 369 | 370 | cx.push_future(async move { 371 | while let Some(evt) = receiver.next().await { 372 | if let Some(evt) = get_event(evt) { 373 | if let Some(v) = queued_events2.upgrade() { 374 | (*v).borrow_mut().push(evt); 375 | } else { 376 | break; 377 | } 378 | } 379 | } 380 | }); 381 | 382 | let state = Rc::new(RefCell::new(InnerInputState::new())); 383 | 384 | ( 385 | Self { 386 | state: state.clone(), 387 | queued_events, 388 | }, 389 | state, 390 | ) 391 | } 392 | 393 | pub fn get_events<'a>( 394 | &self, 395 | dom: &'a VirtualDom, 396 | layout: &Stretch, 397 | layouts: &mut HashMap>, 398 | node: &'a VNode<'a>, 399 | ) -> Vec { 400 | // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus 401 | fn inner( 402 | queue: &Vec<(&'static str, Arc)>, 403 | resolved: &mut Vec, 404 | node: &VNode, 405 | ) { 406 | match node { 407 | VNode::Fragment(frag) => { 408 | for c in frag.children { 409 | inner(queue, resolved, c); 410 | } 411 | } 412 | VNode::Element(el) => { 413 | for l in el.listeners { 414 | for (name, data) in queue.iter() { 415 | if *name == l.event { 416 | if let Some(id) = el.id.get() { 417 | resolved.push(UserEvent { 418 | scope_id: None, 419 | priority: EventPriority::Medium, 420 | name: *name, 421 | element: Some(id), 422 | data: data.clone(), 423 | }); 424 | } 425 | } 426 | } 427 | } 428 | for c in el.children { 429 | inner(queue, resolved, c); 430 | } 431 | } 432 | _ => (), 433 | } 434 | } 435 | 436 | let mut resolved_events = Vec::new(); 437 | 438 | (*self.state).borrow_mut().update( 439 | dom, 440 | &mut (*self.queued_events).borrow_mut(), 441 | &mut resolved_events, 442 | layout, 443 | layouts, 444 | node, 445 | ); 446 | 447 | let events: Vec<_> = self 448 | .queued_events 449 | .replace(Vec::new()) 450 | .into_iter() 451 | // these events were added in the update stage 452 | .filter(|e| !["mousedown", "mouseup", "mousemove", "drag", "wheel"].contains(&e.0)) 453 | .map(|e| (e.0, e.1.into_any())) 454 | .collect(); 455 | 456 | inner(&events, &mut resolved_events, node); 457 | 458 | resolved_events 459 | } 460 | } 461 | 462 | // translate crossterm events into dioxus events 463 | fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { 464 | let (name, data): (&str, EventData) = match evt { 465 | TermEvent::Key(k) => { 466 | let key = translate_key_code(k.code)?; 467 | ( 468 | "keydown", 469 | // from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent 470 | EventData::Keyboard(KeyboardData { 471 | char_code: key.raw_code(), 472 | key: format!("{key:?}"), 473 | key_code: key, 474 | alt_key: k.modifiers.contains(KeyModifiers::ALT), 475 | ctrl_key: k.modifiers.contains(KeyModifiers::CONTROL), 476 | meta_key: false, 477 | shift_key: k.modifiers.contains(KeyModifiers::SHIFT), 478 | locale: Default::default(), 479 | location: 0x00, 480 | repeat: Default::default(), 481 | which: Default::default(), 482 | }), 483 | ) 484 | } 485 | TermEvent::Mouse(m) => { 486 | let (x, y) = (m.column.into(), m.row.into()); 487 | let alt = m.modifiers.contains(KeyModifiers::ALT); 488 | let shift = m.modifiers.contains(KeyModifiers::SHIFT); 489 | let ctrl = m.modifiers.contains(KeyModifiers::CONTROL); 490 | let meta = false; 491 | 492 | let get_mouse_data = |b| { 493 | let buttons = match b { 494 | None => 0, 495 | Some(MouseButton::Left) => 1, 496 | Some(MouseButton::Right) => 2, 497 | Some(MouseButton::Middle) => 4, 498 | }; 499 | let button_state = match b { 500 | None => 0, 501 | Some(MouseButton::Left) => 0, 502 | Some(MouseButton::Middle) => 1, 503 | Some(MouseButton::Right) => 2, 504 | }; 505 | // from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent 506 | EventData::Mouse(MouseData { 507 | alt_key: alt, 508 | button: button_state, 509 | buttons, 510 | client_x: x, 511 | client_y: y, 512 | ctrl_key: ctrl, 513 | meta_key: meta, 514 | page_x: x, 515 | page_y: y, 516 | screen_x: x, 517 | screen_y: y, 518 | shift_key: shift, 519 | }) 520 | }; 521 | 522 | let get_wheel_data = |up| { 523 | // from https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent 524 | EventData::Wheel(WheelData { 525 | delta_mode: 0x01, 526 | delta_x: 0.0, 527 | delta_y: if up { -1.0 } else { 1.0 }, 528 | delta_z: 0.0, 529 | }) 530 | }; 531 | 532 | match m.kind { 533 | MouseEventKind::Down(b) => ("mousedown", get_mouse_data(Some(b))), 534 | MouseEventKind::Up(b) => ("mouseup", get_mouse_data(Some(b))), 535 | MouseEventKind::Drag(b) => ("drag", get_mouse_data(Some(b))), 536 | MouseEventKind::Moved => ("mousemove", get_mouse_data(None)), 537 | MouseEventKind::ScrollDown => ("wheel", get_wheel_data(false)), 538 | MouseEventKind::ScrollUp => ("wheel", get_wheel_data(true)), 539 | } 540 | } 541 | TermEvent::Resize(x, y) => ("resize", EventData::Screen((x, y))), 542 | }; 543 | 544 | Some((name, data)) 545 | } 546 | 547 | fn translate_key_code(c: TermKeyCode) -> Option { 548 | match c { 549 | TermKeyCode::Backspace => Some(KeyCode::Backspace), 550 | TermKeyCode::Enter => Some(KeyCode::Enter), 551 | TermKeyCode::Left => Some(KeyCode::LeftArrow), 552 | TermKeyCode::Right => Some(KeyCode::RightArrow), 553 | TermKeyCode::Up => Some(KeyCode::UpArrow), 554 | TermKeyCode::Down => Some(KeyCode::DownArrow), 555 | TermKeyCode::Home => Some(KeyCode::Home), 556 | TermKeyCode::End => Some(KeyCode::End), 557 | TermKeyCode::PageUp => Some(KeyCode::PageUp), 558 | TermKeyCode::PageDown => Some(KeyCode::PageDown), 559 | TermKeyCode::Tab => Some(KeyCode::Tab), 560 | TermKeyCode::BackTab => None, 561 | TermKeyCode::Delete => Some(KeyCode::Delete), 562 | TermKeyCode::Insert => Some(KeyCode::Insert), 563 | TermKeyCode::F(fn_num) => match fn_num { 564 | 1 => Some(KeyCode::F1), 565 | 2 => Some(KeyCode::F2), 566 | 3 => Some(KeyCode::F3), 567 | 4 => Some(KeyCode::F4), 568 | 5 => Some(KeyCode::F5), 569 | 6 => Some(KeyCode::F6), 570 | 7 => Some(KeyCode::F7), 571 | 8 => Some(KeyCode::F8), 572 | 9 => Some(KeyCode::F9), 573 | 10 => Some(KeyCode::F10), 574 | 11 => Some(KeyCode::F11), 575 | 12 => Some(KeyCode::F12), 576 | _ => None, 577 | }, 578 | TermKeyCode::Char(c) => match c.to_uppercase().next().unwrap() { 579 | 'A' => Some(KeyCode::A), 580 | 'B' => Some(KeyCode::B), 581 | 'C' => Some(KeyCode::C), 582 | 'D' => Some(KeyCode::D), 583 | 'E' => Some(KeyCode::E), 584 | 'F' => Some(KeyCode::F), 585 | 'G' => Some(KeyCode::G), 586 | 'H' => Some(KeyCode::H), 587 | 'I' => Some(KeyCode::I), 588 | 'J' => Some(KeyCode::J), 589 | 'K' => Some(KeyCode::K), 590 | 'L' => Some(KeyCode::L), 591 | 'M' => Some(KeyCode::M), 592 | 'N' => Some(KeyCode::N), 593 | 'O' => Some(KeyCode::O), 594 | 'P' => Some(KeyCode::P), 595 | 'Q' => Some(KeyCode::Q), 596 | 'R' => Some(KeyCode::R), 597 | 'S' => Some(KeyCode::S), 598 | 'T' => Some(KeyCode::T), 599 | 'U' => Some(KeyCode::U), 600 | 'V' => Some(KeyCode::V), 601 | 'W' => Some(KeyCode::W), 602 | 'X' => Some(KeyCode::X), 603 | 'Y' => Some(KeyCode::Y), 604 | 'Z' => Some(KeyCode::Z), 605 | _ => None, 606 | }, 607 | TermKeyCode::Null => None, 608 | TermKeyCode::Esc => Some(KeyCode::Escape), 609 | } 610 | } 611 | 612 | fn clone_mouse_data(m: &MouseData) -> MouseData { 613 | MouseData { 614 | client_x: m.client_x, 615 | client_y: m.client_y, 616 | page_x: m.page_x, 617 | page_y: m.page_y, 618 | screen_x: m.screen_x, 619 | screen_y: m.screen_y, 620 | alt_key: m.alt_key, 621 | ctrl_key: m.ctrl_key, 622 | meta_key: m.meta_key, 623 | shift_key: m.shift_key, 624 | button: m.button, 625 | buttons: m.buttons, 626 | } 627 | } 628 | 629 | fn clone_keyboard_data(k: &KeyboardData) -> KeyboardData { 630 | KeyboardData { 631 | char_code: k.char_code, 632 | key: k.key.clone(), 633 | key_code: k.key_code, 634 | alt_key: k.alt_key, 635 | ctrl_key: k.ctrl_key, 636 | meta_key: k.meta_key, 637 | shift_key: k.shift_key, 638 | locale: k.locale.clone(), 639 | location: k.location, 640 | repeat: k.repeat, 641 | which: k.which, 642 | } 643 | } 644 | 645 | fn clone_wheel_data(w: &WheelData) -> WheelData { 646 | WheelData { 647 | delta_mode: w.delta_mode, 648 | delta_x: w.delta_x, 649 | delta_y: w.delta_y, 650 | delta_z: w.delta_x, 651 | } 652 | } 653 | -------------------------------------------------------------------------------- /src/layout.rs: -------------------------------------------------------------------------------- 1 | use dioxus::core::*; 2 | use std::collections::HashMap; 3 | 4 | use crate::{ 5 | attributes::{apply_attributes, StyleModifer}, 6 | style::RinkStyle, 7 | TuiModifier, TuiNode, 8 | }; 9 | 10 | /* 11 | The layout system uses the lineheight as one point. 12 | 13 | stretch uses fractional points, so we can rasterize if we need too, but not with characters 14 | this means anything thats "1px" is 1 lineheight. Unfortunately, text cannot be smaller or bigger 15 | */ 16 | pub fn collect_layout<'a>( 17 | layout: &mut stretch2::Stretch, 18 | nodes: &mut HashMap>, 19 | vdom: &'a VirtualDom, 20 | node: &'a VNode<'a>, 21 | ) { 22 | use stretch2::prelude::*; 23 | 24 | match node { 25 | VNode::Text(t) => { 26 | let id = t.id.get().unwrap(); 27 | let char_len = t.text.chars().count(); 28 | 29 | let style = Style { 30 | size: Size { 31 | // characters are 1 point tall 32 | height: Dimension::Points(1.0), 33 | 34 | // text is as long as it is declared 35 | width: Dimension::Points(char_len as f32), 36 | }, 37 | ..Default::default() 38 | }; 39 | 40 | nodes.insert( 41 | id, 42 | TuiNode { 43 | node, 44 | block_style: RinkStyle::default(), 45 | tui_modifier: TuiModifier::default(), 46 | layout: layout.new_node(style, &[]).unwrap(), 47 | }, 48 | ); 49 | } 50 | VNode::Element(el) => { 51 | // gather up all the styles from the attribute list 52 | let mut modifier = StyleModifer { 53 | style: Style::default(), 54 | tui_style: RinkStyle::default(), 55 | tui_modifier: TuiModifier::default(), 56 | }; 57 | 58 | for &Attribute { name, value, .. } in el.attributes { 59 | apply_attributes(name, value, &mut modifier); 60 | } 61 | 62 | // Layout the children 63 | for child in el.children { 64 | collect_layout(layout, nodes, vdom, child); 65 | } 66 | 67 | // Set all direct nodes as our children 68 | let mut child_layout = vec![]; 69 | for el in el.children { 70 | let ite = ElementIdIterator::new(vdom, el); 71 | for node in ite { 72 | match node { 73 | VNode::Element(_) | VNode::Text(_) => { 74 | // 75 | child_layout.push(nodes[&node.mounted_id()].layout) 76 | } 77 | VNode::Placeholder(_) => {} 78 | VNode::Fragment(_) => todo!(), 79 | VNode::Component(_) => todo!(), 80 | } 81 | 82 | // child_layout.push(nodes[&node.mounted_id()].layout) 83 | } 84 | } 85 | 86 | nodes.insert( 87 | node.mounted_id(), 88 | TuiNode { 89 | node, 90 | block_style: modifier.tui_style, 91 | tui_modifier: modifier.tui_modifier, 92 | layout: layout.new_node(modifier.style, &child_layout).unwrap(), 93 | }, 94 | ); 95 | } 96 | VNode::Fragment(el) => { 97 | // 98 | for child in el.children { 99 | collect_layout(layout, nodes, vdom, child); 100 | } 101 | } 102 | VNode::Component(sc) => { 103 | // 104 | let scope = vdom.get_scope(sc.scope.get().unwrap()).unwrap(); 105 | let root = scope.root_node(); 106 | collect_layout(layout, nodes, vdom, root); 107 | } 108 | VNode::Placeholder(_) => { 109 | // 110 | } 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::{ 3 | event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers}, 4 | execute, 5 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 6 | }; 7 | use dioxus::core::exports::futures_channel::mpsc::unbounded; 8 | use dioxus::core::*; 9 | use futures::{channel::mpsc::UnboundedSender, pin_mut, StreamExt}; 10 | use std::{ 11 | collections::HashMap, 12 | io, 13 | time::{Duration, Instant}, 14 | }; 15 | use stretch2::{prelude::Size, Stretch}; 16 | use style::RinkStyle; 17 | use tui::{backend::CrosstermBackend, Terminal}; 18 | 19 | mod attributes; 20 | mod config; 21 | mod hooks; 22 | mod layout; 23 | mod render; 24 | mod style; 25 | mod widget; 26 | 27 | pub use attributes::*; 28 | pub use config::*; 29 | pub use hooks::*; 30 | pub use layout::*; 31 | pub use render::*; 32 | 33 | pub fn launch(app: Component<()>) { 34 | launch_cfg(app, Config::default()) 35 | } 36 | 37 | pub fn launch_cfg(app: Component<()>, cfg: Config) { 38 | let mut dom = VirtualDom::new(app); 39 | let (tx, rx) = unbounded(); 40 | 41 | let cx = dom.base_scope(); 42 | 43 | let (handler, state) = RinkInputHandler::new(rx, cx); 44 | 45 | cx.provide_root_context(state); 46 | 47 | dom.rebuild(); 48 | 49 | render_vdom(&mut dom, tx, handler, cfg).unwrap(); 50 | } 51 | 52 | pub struct TuiNode<'a> { 53 | pub layout: stretch2::node::Node, 54 | pub block_style: RinkStyle, 55 | pub tui_modifier: TuiModifier, 56 | pub node: &'a VNode<'a>, 57 | } 58 | 59 | pub fn render_vdom( 60 | vdom: &mut VirtualDom, 61 | ctx: UnboundedSender, 62 | handler: RinkInputHandler, 63 | cfg: Config, 64 | ) -> Result<()> { 65 | // Setup input handling 66 | let (tx, mut rx) = unbounded(); 67 | std::thread::spawn(move || { 68 | let tick_rate = Duration::from_millis(100); 69 | let mut last_tick = Instant::now(); 70 | loop { 71 | // poll for tick rate duration, if no events, sent tick event. 72 | let timeout = tick_rate 73 | .checked_sub(last_tick.elapsed()) 74 | .unwrap_or_else(|| Duration::from_secs(0)); 75 | 76 | if crossterm::event::poll(timeout).unwrap() { 77 | let evt = crossterm::event::read().unwrap(); 78 | tx.unbounded_send(InputEvent::UserInput(evt)).unwrap(); 79 | } 80 | 81 | if last_tick.elapsed() >= tick_rate { 82 | tx.unbounded_send(InputEvent::Tick).unwrap(); 83 | last_tick = Instant::now(); 84 | } 85 | } 86 | }); 87 | 88 | tokio::runtime::Builder::new_current_thread() 89 | .enable_all() 90 | .build()? 91 | .block_on(async { 92 | /* 93 | Get the terminal to calcualte the layout from 94 | */ 95 | enable_raw_mode().unwrap(); 96 | let mut stdout = std::io::stdout(); 97 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); 98 | let backend = CrosstermBackend::new(io::stdout()); 99 | let mut terminal = Terminal::new(backend).unwrap(); 100 | 101 | terminal.clear().unwrap(); 102 | 103 | loop { 104 | /* 105 | -> collect all the nodes with their layout 106 | -> solve their layout 107 | -> resolve events 108 | -> render the nodes in the right place with tui/crosstream 109 | -> while rendering, apply styling 110 | 111 | use simd to compare lines for diffing? 112 | 113 | 114 | todo: reuse the layout and node objects. 115 | our work_with_deadline method can tell us which nodes are dirty. 116 | */ 117 | let mut layout = Stretch::new(); 118 | let mut nodes = HashMap::new(); 119 | 120 | let root_node = vdom.base_scope().root_node(); 121 | layout::collect_layout(&mut layout, &mut nodes, vdom, root_node); 122 | /* 123 | Compute the layout given the terminal size 124 | */ 125 | let node_id = root_node.try_mounted_id().unwrap(); 126 | let root_layout = nodes[&node_id].layout; 127 | let mut events = Vec::new(); 128 | 129 | terminal.draw(|frame| { 130 | // size is guaranteed to not change when rendering 131 | let dims = frame.size(); 132 | let width = dims.width; 133 | let height = dims.height; 134 | layout 135 | .compute_layout( 136 | root_layout, 137 | Size { 138 | width: stretch2::prelude::Number::Defined(width as f32), 139 | height: stretch2::prelude::Number::Defined(height as f32), 140 | }, 141 | ) 142 | .unwrap(); 143 | 144 | // resolve events before rendering 145 | events = handler.get_events(vdom, &layout, &mut nodes, root_node); 146 | render::render_vnode( 147 | frame, 148 | &layout, 149 | &mut nodes, 150 | vdom, 151 | root_node, 152 | &RinkStyle::default(), 153 | cfg, 154 | ); 155 | assert!(nodes.is_empty()); 156 | })?; 157 | 158 | for e in events { 159 | vdom.handle_message(SchedulerMsg::Event(e)); 160 | } 161 | 162 | use futures::future::{select, Either}; 163 | { 164 | let wait = vdom.wait_for_work(); 165 | pin_mut!(wait); 166 | 167 | match select(wait, rx.next()).await { 168 | Either::Left((_a, _b)) => { 169 | // 170 | } 171 | Either::Right((evt, _o)) => { 172 | match evt.as_ref().unwrap() { 173 | InputEvent::UserInput(event) => match event { 174 | TermEvent::Key(key) => { 175 | if matches!(key.code, KeyCode::Char('c')) 176 | && key.modifiers.contains(KeyModifiers::CONTROL) 177 | { 178 | break; 179 | } 180 | } 181 | TermEvent::Resize(_, _) | TermEvent::Mouse(_) => {} 182 | }, 183 | InputEvent::Tick => {} // tick 184 | InputEvent::Close => break, 185 | }; 186 | 187 | if let InputEvent::UserInput(evt) = evt.unwrap() { 188 | ctx.unbounded_send(evt).unwrap(); 189 | } 190 | } 191 | } 192 | } 193 | 194 | vdom.work_with_deadline(|| false); 195 | } 196 | 197 | disable_raw_mode()?; 198 | execute!( 199 | terminal.backend_mut(), 200 | LeaveAlternateScreen, 201 | DisableMouseCapture 202 | )?; 203 | terminal.show_cursor()?; 204 | 205 | Ok(()) 206 | }) 207 | } 208 | 209 | enum InputEvent { 210 | UserInput(TermEvent), 211 | Close, 212 | Tick, 213 | } 214 | -------------------------------------------------------------------------------- /src/render.rs: -------------------------------------------------------------------------------- 1 | use dioxus::core::*; 2 | use std::{collections::HashMap, io::Stdout}; 3 | use stretch2::{ 4 | geometry::Point, 5 | prelude::{Layout, Size}, 6 | Stretch, 7 | }; 8 | use tui::{backend::CrosstermBackend, layout::Rect}; 9 | 10 | use crate::{ 11 | style::{RinkColor, RinkStyle}, 12 | widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext}, 13 | BorderEdge, BorderStyle, Config, TuiNode, UnitSystem, 14 | }; 15 | 16 | const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5]; 17 | 18 | pub fn render_vnode<'a>( 19 | frame: &mut tui::Frame>, 20 | layout: &Stretch, 21 | layouts: &mut HashMap>, 22 | vdom: &'a VirtualDom, 23 | node: &'a VNode<'a>, 24 | // this holds the accumulated syle state for styled text rendering 25 | style: &RinkStyle, 26 | cfg: Config, 27 | ) { 28 | match node { 29 | VNode::Fragment(f) => { 30 | for child in f.children { 31 | render_vnode(frame, layout, layouts, vdom, child, style, cfg); 32 | } 33 | return; 34 | } 35 | 36 | VNode::Component(vcomp) => { 37 | let idx = vcomp.scope.get().unwrap(); 38 | let new_node = vdom.get_scope(idx).unwrap().root_node(); 39 | render_vnode(frame, layout, layouts, vdom, new_node, style, cfg); 40 | return; 41 | } 42 | 43 | VNode::Placeholder(_) => return, 44 | 45 | VNode::Element(_) | VNode::Text(_) => {} 46 | } 47 | 48 | let id = node.try_mounted_id().unwrap(); 49 | let mut node = layouts.remove(&id).unwrap(); 50 | 51 | let Layout { location, size, .. } = layout.layout(node.layout).unwrap(); 52 | 53 | let Point { x, y } = location; 54 | let Size { width, height } = size; 55 | 56 | match node.node { 57 | VNode::Text(t) => { 58 | #[derive(Default)] 59 | struct Label<'a> { 60 | text: &'a str, 61 | style: RinkStyle, 62 | } 63 | 64 | impl<'a> RinkWidget for Label<'a> { 65 | fn render(self, area: Rect, mut buf: RinkBuffer) { 66 | for (i, c) in self.text.char_indices() { 67 | let mut new_cell = RinkCell::default(); 68 | new_cell.set_style(self.style); 69 | new_cell.symbol = c.to_string(); 70 | buf.set(area.left() + i as u16, area.top(), &new_cell); 71 | } 72 | } 73 | } 74 | 75 | let label = Label { 76 | text: t.text, 77 | style: *style, 78 | }; 79 | let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); 80 | 81 | // the renderer will panic if a node is rendered out of range even if the size is zero 82 | if area.width > 0 && area.height > 0 { 83 | frame.render_widget(WidgetWithContext::new(label, cfg), area); 84 | } 85 | } 86 | VNode::Element(el) => { 87 | let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); 88 | 89 | let mut new_style = node.block_style.merge(*style); 90 | node.block_style = new_style; 91 | 92 | // the renderer will panic if a node is rendered out of range even if the size is zero 93 | if area.width > 0 && area.height > 0 { 94 | frame.render_widget(WidgetWithContext::new(node, cfg), area); 95 | } 96 | 97 | // do not pass background color to children 98 | new_style.bg = None; 99 | for el in el.children { 100 | render_vnode(frame, layout, layouts, vdom, el, &new_style, cfg); 101 | } 102 | } 103 | VNode::Fragment(_) => todo!(), 104 | VNode::Component(_) => todo!(), 105 | VNode::Placeholder(_) => todo!(), 106 | } 107 | } 108 | 109 | impl<'a> RinkWidget for TuiNode<'a> { 110 | fn render(self, area: Rect, mut buf: RinkBuffer<'_>) { 111 | use tui::symbols::line::*; 112 | 113 | enum Direction { 114 | Left, 115 | Right, 116 | Up, 117 | Down, 118 | } 119 | 120 | fn draw( 121 | buf: &mut RinkBuffer, 122 | points_history: [[i32; 2]; 3], 123 | symbols: &Set, 124 | pos: [u16; 2], 125 | color: &Option, 126 | ) { 127 | let [before, current, after] = points_history; 128 | let start_dir = match [before[0] - current[0], before[1] - current[1]] { 129 | [1, 0] => Direction::Right, 130 | [-1, 0] => Direction::Left, 131 | [0, 1] => Direction::Down, 132 | [0, -1] => Direction::Up, 133 | [a, b] => { 134 | panic!( 135 | "draw({:?} {:?} {:?}) {}, {} no cell adjacent", 136 | before, current, after, a, b 137 | ) 138 | } 139 | }; 140 | let end_dir = match [after[0] - current[0], after[1] - current[1]] { 141 | [1, 0] => Direction::Right, 142 | [-1, 0] => Direction::Left, 143 | [0, 1] => Direction::Down, 144 | [0, -1] => Direction::Up, 145 | [a, b] => { 146 | panic!( 147 | "draw({:?} {:?} {:?}) {}, {} no cell adjacent", 148 | before, current, after, a, b 149 | ) 150 | } 151 | }; 152 | 153 | let mut new_cell = RinkCell::default(); 154 | if let Some(c) = color { 155 | new_cell.fg = *c; 156 | } 157 | new_cell.symbol = match [start_dir, end_dir] { 158 | [Direction::Down, Direction::Up] => symbols.vertical, 159 | [Direction::Down, Direction::Right] => symbols.top_left, 160 | [Direction::Down, Direction::Left] => symbols.top_right, 161 | [Direction::Up, Direction::Down] => symbols.vertical, 162 | [Direction::Up, Direction::Right] => symbols.bottom_left, 163 | [Direction::Up, Direction::Left] => symbols.bottom_right, 164 | [Direction::Right, Direction::Left] => symbols.horizontal, 165 | [Direction::Right, Direction::Up] => symbols.bottom_left, 166 | [Direction::Right, Direction::Down] => symbols.top_left, 167 | [Direction::Left, Direction::Up] => symbols.bottom_right, 168 | [Direction::Left, Direction::Right] => symbols.horizontal, 169 | [Direction::Left, Direction::Down] => symbols.top_right, 170 | _ => panic!( 171 | "{:?} {:?} {:?} cannont connect cell to itself", 172 | before, current, after 173 | ), 174 | } 175 | .to_string(); 176 | buf.set( 177 | (current[0] + pos[0] as i32) as u16, 178 | (current[1] + pos[1] as i32) as u16, 179 | &new_cell, 180 | ); 181 | } 182 | 183 | fn draw_arc( 184 | pos: [u16; 2], 185 | starting_angle: f32, 186 | arc_angle: f32, 187 | radius: f32, 188 | symbols: &Set, 189 | buf: &mut RinkBuffer, 190 | color: &Option, 191 | ) { 192 | if radius < 0.0 { 193 | return; 194 | } 195 | 196 | let num_points = (radius * arc_angle) as i32; 197 | let starting_point = [ 198 | (starting_angle.cos() * (radius * RADIUS_MULTIPLIER[0])) as i32, 199 | (starting_angle.sin() * (radius * RADIUS_MULTIPLIER[1])) as i32, 200 | ]; 201 | // keep track of the last 3 point to allow filling diagonals 202 | let mut points_history = [ 203 | [0, 0], 204 | { 205 | // change the x or y value based on which one is changing quicker 206 | let ddx = -starting_angle.sin(); 207 | let ddy = starting_angle.cos(); 208 | if ddx.abs() > ddy.abs() { 209 | [starting_point[0] - ddx.signum() as i32, starting_point[1]] 210 | } else { 211 | [starting_point[0], starting_point[1] - ddy.signum() as i32] 212 | } 213 | }, 214 | starting_point, 215 | ]; 216 | 217 | for i in 1..=num_points { 218 | let angle = (i as f32 / num_points as f32) * arc_angle + starting_angle; 219 | let x = angle.cos() * radius * RADIUS_MULTIPLIER[0]; 220 | let y = angle.sin() * radius * RADIUS_MULTIPLIER[1]; 221 | let new = [x as i32, y as i32]; 222 | 223 | if new != points_history[2] { 224 | points_history = [points_history[1], points_history[2], new]; 225 | 226 | let dx = points_history[2][0] - points_history[1][0]; 227 | let dy = points_history[2][1] - points_history[1][1]; 228 | // fill diagonals 229 | if dx != 0 && dy != 0 { 230 | let connecting_point = match [dx, dy] { 231 | [1, 1] => [points_history[1][0] + 1, points_history[1][1]], 232 | [1, -1] => [points_history[1][0], points_history[1][1] - 1], 233 | [-1, 1] => [points_history[1][0], points_history[1][1] + 1], 234 | [-1, -1] => [points_history[1][0] - 1, points_history[1][1]], 235 | _ => todo!(), 236 | }; 237 | draw( 238 | buf, 239 | [points_history[0], points_history[1], connecting_point], 240 | symbols, 241 | pos, 242 | color, 243 | ); 244 | points_history = [points_history[1], connecting_point, points_history[2]]; 245 | } 246 | 247 | draw(buf, points_history, symbols, pos, color); 248 | } 249 | } 250 | 251 | points_history = [points_history[1], points_history[2], { 252 | // change the x or y value based on which one is changing quicker 253 | let ddx = -(starting_angle + arc_angle).sin(); 254 | let ddy = (starting_angle + arc_angle).cos(); 255 | if ddx.abs() > ddy.abs() { 256 | [ 257 | points_history[2][0] + ddx.signum() as i32, 258 | points_history[2][1], 259 | ] 260 | } else { 261 | [ 262 | points_history[2][0], 263 | points_history[2][1] + ddy.signum() as i32, 264 | ] 265 | } 266 | }]; 267 | 268 | draw(buf, points_history, symbols, pos, color); 269 | } 270 | 271 | fn get_radius(border: &BorderEdge, area: Rect) -> f32 { 272 | match border.style { 273 | BorderStyle::HIDDEN => 0.0, 274 | BorderStyle::NONE => 0.0, 275 | _ => match border.radius { 276 | UnitSystem::Percent(p) => p * area.width as f32 / 100.0, 277 | UnitSystem::Point(p) => p, 278 | } 279 | .abs() 280 | .min((area.width as f32 / RADIUS_MULTIPLIER[0]) / 2.0) 281 | .min((area.height as f32 / RADIUS_MULTIPLIER[1]) / 2.0), 282 | } 283 | } 284 | 285 | if area.area() == 0 { 286 | return; 287 | } 288 | 289 | // todo: only render inside borders 290 | for x in area.left()..area.right() { 291 | for y in area.top()..area.bottom() { 292 | let mut new_cell = RinkCell::default(); 293 | if let Some(c) = self.block_style.bg { 294 | new_cell.bg = c; 295 | } 296 | buf.set(x, y, &new_cell); 297 | } 298 | } 299 | 300 | let borders = self.tui_modifier.borders; 301 | 302 | let last_edge = &borders.left; 303 | let current_edge = &borders.top; 304 | if let Some(symbols) = current_edge.style.symbol_set() { 305 | // the radius for the curve between this line and the next 306 | let r = get_radius(current_edge, area); 307 | let radius = [ 308 | (r * RADIUS_MULTIPLIER[0]) as u16, 309 | (r * RADIUS_MULTIPLIER[1]) as u16, 310 | ]; 311 | // the radius for the curve between this line and the last 312 | let last_r = get_radius(last_edge, area); 313 | let last_radius = [ 314 | (last_r * RADIUS_MULTIPLIER[0]) as u16, 315 | (last_r * RADIUS_MULTIPLIER[1]) as u16, 316 | ]; 317 | let color = current_edge.color.or(self.block_style.fg); 318 | let mut new_cell = RinkCell::default(); 319 | if let Some(c) = color { 320 | new_cell.fg = c; 321 | } 322 | for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) { 323 | new_cell.symbol = symbols.horizontal.to_string(); 324 | buf.set(x, area.top(), &new_cell); 325 | } 326 | draw_arc( 327 | [area.right() - radius[0] - 1, area.top() + radius[1]], 328 | std::f32::consts::FRAC_PI_2 * 3.0, 329 | std::f32::consts::FRAC_PI_2, 330 | r, 331 | &symbols, 332 | &mut buf, 333 | &color, 334 | ); 335 | } 336 | 337 | let last_edge = &borders.top; 338 | let current_edge = &borders.right; 339 | if let Some(symbols) = current_edge.style.symbol_set() { 340 | // the radius for the curve between this line and the next 341 | let r = get_radius(current_edge, area); 342 | let radius = [ 343 | (r * RADIUS_MULTIPLIER[0]) as u16, 344 | (r * RADIUS_MULTIPLIER[1]) as u16, 345 | ]; 346 | // the radius for the curve between this line and the last 347 | let last_r = get_radius(last_edge, area); 348 | let last_radius = [ 349 | (last_r * RADIUS_MULTIPLIER[0]) as u16, 350 | (last_r * RADIUS_MULTIPLIER[1]) as u16, 351 | ]; 352 | let color = current_edge.color.or(self.block_style.fg); 353 | let mut new_cell = RinkCell::default(); 354 | if let Some(c) = color { 355 | new_cell.fg = c; 356 | } 357 | for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) { 358 | new_cell.symbol = symbols.vertical.to_string(); 359 | buf.set(area.right() - 1, y, &new_cell); 360 | } 361 | draw_arc( 362 | [area.right() - radius[0] - 1, area.bottom() - radius[1] - 1], 363 | 0.0, 364 | std::f32::consts::FRAC_PI_2, 365 | r, 366 | &symbols, 367 | &mut buf, 368 | &color, 369 | ); 370 | } 371 | 372 | let last_edge = &borders.right; 373 | let current_edge = &borders.bottom; 374 | if let Some(symbols) = current_edge.style.symbol_set() { 375 | // the radius for the curve between this line and the next 376 | let r = get_radius(current_edge, area); 377 | let radius = [ 378 | (r * RADIUS_MULTIPLIER[0]) as u16, 379 | (r * RADIUS_MULTIPLIER[1]) as u16, 380 | ]; 381 | // the radius for the curve between this line and the last 382 | let last_r = get_radius(last_edge, area); 383 | let last_radius = [ 384 | (last_r * RADIUS_MULTIPLIER[0]) as u16, 385 | (last_r * RADIUS_MULTIPLIER[1]) as u16, 386 | ]; 387 | let color = current_edge.color.or(self.block_style.fg); 388 | let mut new_cell = RinkCell::default(); 389 | if let Some(c) = color { 390 | new_cell.fg = c; 391 | } 392 | for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) { 393 | new_cell.symbol = symbols.horizontal.to_string(); 394 | buf.set(x, area.bottom() - 1, &new_cell); 395 | } 396 | draw_arc( 397 | [area.left() + radius[0], area.bottom() - radius[1] - 1], 398 | std::f32::consts::FRAC_PI_2, 399 | std::f32::consts::FRAC_PI_2, 400 | r, 401 | &symbols, 402 | &mut buf, 403 | &color, 404 | ); 405 | } 406 | 407 | let last_edge = &borders.bottom; 408 | let current_edge = &borders.left; 409 | if let Some(symbols) = current_edge.style.symbol_set() { 410 | // the radius for the curve between this line and the next 411 | let r = get_radius(current_edge, area); 412 | let radius = [ 413 | (r * RADIUS_MULTIPLIER[0]) as u16, 414 | (r * RADIUS_MULTIPLIER[1]) as u16, 415 | ]; 416 | // the radius for the curve between this line and the last 417 | let last_r = get_radius(last_edge, area); 418 | let last_radius = [ 419 | (last_r * RADIUS_MULTIPLIER[0]) as u16, 420 | (last_r * RADIUS_MULTIPLIER[1]) as u16, 421 | ]; 422 | let color = current_edge.color.or(self.block_style.fg); 423 | let mut new_cell = RinkCell::default(); 424 | if let Some(c) = color { 425 | new_cell.fg = c; 426 | } 427 | for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) { 428 | new_cell.symbol = symbols.vertical.to_string(); 429 | buf.set(area.left(), y, &new_cell); 430 | } 431 | draw_arc( 432 | [area.left() + radius[0], area.top() + radius[1]], 433 | std::f32::consts::PI, 434 | std::f32::consts::FRAC_PI_2, 435 | r, 436 | &symbols, 437 | &mut buf, 438 | &color, 439 | ); 440 | } 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/style.rs: -------------------------------------------------------------------------------- 1 | use std::{num::ParseFloatError, str::FromStr}; 2 | 3 | use tui::style::{Color, Modifier, Style}; 4 | 5 | use crate::RenderingMode; 6 | 7 | #[derive(Clone, Copy, Debug)] 8 | pub struct RinkColor { 9 | pub color: Color, 10 | pub alpha: f32, 11 | } 12 | 13 | impl Default for RinkColor { 14 | fn default() -> Self { 15 | Self { 16 | color: Color::Black, 17 | alpha: 0.0, 18 | } 19 | } 20 | } 21 | 22 | impl RinkColor { 23 | pub fn blend(self, other: Color) -> Color { 24 | if self.color == Color::Reset { 25 | Color::Reset 26 | } else if self.alpha == 0.0 { 27 | other 28 | } else { 29 | let [sr, sg, sb] = to_rgb(self.color); 30 | let [or, og, ob] = to_rgb(other); 31 | let (sr, sg, sb, sa) = ( 32 | sr as f32 / 255.0, 33 | sg as f32 / 255.0, 34 | sb as f32 / 255.0, 35 | self.alpha, 36 | ); 37 | let (or, og, ob) = (or as f32 / 255.0, og as f32 / 255.0, ob as f32 / 255.0); 38 | Color::Rgb( 39 | (255.0 * (sr * sa + or * (1.0 - sa))) as u8, 40 | (255.0 * (sg * sa + og * (1.0 - sa))) as u8, 41 | (255.0 * (sb * sa + ob * (1.0 - sa))) as u8, 42 | ) 43 | } 44 | } 45 | } 46 | 47 | fn parse_value( 48 | v: &str, 49 | current_max_output: f32, 50 | required_max_output: f32, 51 | ) -> Result { 52 | if let Some(stripped) = v.strip_suffix('%') { 53 | Ok((stripped.trim().parse::()? / 100.0) * required_max_output) 54 | } else { 55 | Ok((v.trim().parse::()? / current_max_output) * required_max_output) 56 | } 57 | } 58 | 59 | pub struct ParseColorError; 60 | 61 | fn parse_hex(color: &str) -> Result { 62 | let mut values = [0, 0, 0]; 63 | let mut color_ok = true; 64 | for i in 0..values.len() { 65 | if let Ok(v) = u8::from_str_radix(&color[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) { 66 | values[i] = v; 67 | } else { 68 | color_ok = false; 69 | } 70 | } 71 | if color_ok { 72 | Ok(Color::Rgb(values[0], values[1], values[2])) 73 | } else { 74 | Err(ParseColorError) 75 | } 76 | } 77 | 78 | fn parse_rgb(color: &str) -> Result { 79 | let mut values = [0, 0, 0]; 80 | let mut color_ok = true; 81 | for (v, i) in color.split(',').zip(0..values.len()) { 82 | if let Ok(v) = parse_value(v.trim(), 255.0, 255.0) { 83 | values[i] = v as u8; 84 | } else { 85 | color_ok = false; 86 | } 87 | } 88 | if color_ok { 89 | Ok(Color::Rgb(values[0], values[1], values[2])) 90 | } else { 91 | Err(ParseColorError) 92 | } 93 | } 94 | 95 | fn parse_hsl(color: &str) -> Result { 96 | let mut values = [0.0, 0.0, 0.0]; 97 | let mut color_ok = true; 98 | for (v, i) in color.split(',').zip(0..values.len()) { 99 | if let Ok(v) = parse_value(v.trim(), if i == 0 { 360.0 } else { 100.0 }, 1.0) { 100 | values[i] = v; 101 | } else { 102 | color_ok = false; 103 | } 104 | } 105 | if color_ok { 106 | let [h, s, l] = values; 107 | let rgb = if s == 0.0 { 108 | [l as u8; 3] 109 | } else { 110 | fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 { 111 | if t < 0.0 { 112 | t += 1.0; 113 | } 114 | if t > 1.0 { 115 | t -= 1.0; 116 | } 117 | if t < 1.0 / 6.0 { 118 | p + (q - p) * 6.0 * t 119 | } else if t < 1.0 / 2.0 { 120 | q 121 | } else if t < 2.0 / 3.0 { 122 | p + (q - p) * (2.0 / 3.0 - t) * 6.0 123 | } else { 124 | p 125 | } 126 | } 127 | 128 | let q = if l < 0.5 { 129 | l * (1.0 + s) 130 | } else { 131 | l + s - l * s 132 | }; 133 | let p = 2.0 * l - q; 134 | [ 135 | (hue_to_rgb(p, q, h + 1.0 / 3.0) * 255.0) as u8, 136 | (hue_to_rgb(p, q, h) * 255.0) as u8, 137 | (hue_to_rgb(p, q, h - 1.0 / 3.0) * 255.0) as u8, 138 | ] 139 | }; 140 | 141 | Ok(Color::Rgb(rgb[0], rgb[1], rgb[2])) 142 | } else { 143 | Err(ParseColorError) 144 | } 145 | } 146 | 147 | impl FromStr for RinkColor { 148 | type Err = ParseColorError; 149 | 150 | fn from_str(color: &str) -> Result { 151 | match color { 152 | "red" => Ok(RinkColor { 153 | color: Color::Red, 154 | alpha: 1.0, 155 | }), 156 | "black" => Ok(RinkColor { 157 | color: Color::Black, 158 | alpha: 1.0, 159 | }), 160 | "green" => Ok(RinkColor { 161 | color: Color::Green, 162 | alpha: 1.0, 163 | }), 164 | "yellow" => Ok(RinkColor { 165 | color: Color::Yellow, 166 | alpha: 1.0, 167 | }), 168 | "blue" => Ok(RinkColor { 169 | color: Color::Blue, 170 | alpha: 1.0, 171 | }), 172 | "magenta" => Ok(RinkColor { 173 | color: Color::Magenta, 174 | alpha: 1.0, 175 | }), 176 | "cyan" => Ok(RinkColor { 177 | color: Color::Cyan, 178 | alpha: 1.0, 179 | }), 180 | "gray" => Ok(RinkColor { 181 | color: Color::Gray, 182 | alpha: 1.0, 183 | }), 184 | "darkgray" => Ok(RinkColor { 185 | color: Color::DarkGray, 186 | alpha: 1.0, 187 | }), 188 | // light red does not exist 189 | "orangered" => Ok(RinkColor { 190 | color: Color::LightRed, 191 | alpha: 1.0, 192 | }), 193 | "lightgreen" => Ok(RinkColor { 194 | color: Color::LightGreen, 195 | alpha: 1.0, 196 | }), 197 | "lightyellow" => Ok(RinkColor { 198 | color: Color::LightYellow, 199 | alpha: 1.0, 200 | }), 201 | "lightblue" => Ok(RinkColor { 202 | color: Color::LightBlue, 203 | alpha: 1.0, 204 | }), 205 | // light magenta does not exist 206 | "orchid" => Ok(RinkColor { 207 | color: Color::LightMagenta, 208 | alpha: 1.0, 209 | }), 210 | "lightcyan" => Ok(RinkColor { 211 | color: Color::LightCyan, 212 | alpha: 1.0, 213 | }), 214 | "white" => Ok(RinkColor { 215 | color: Color::White, 216 | alpha: 1.0, 217 | }), 218 | _ => { 219 | if color.len() == 7 && color.starts_with('#') { 220 | parse_hex(color).map(|c| RinkColor { 221 | color: c, 222 | alpha: 1.0, 223 | }) 224 | } else if let Some(stripped) = color.strip_prefix("rgb(") { 225 | let color_values = stripped.trim_end_matches(')'); 226 | if color.matches(',').count() == 3 { 227 | let (alpha, rgb_values) = 228 | color_values.rsplit_once(',').ok_or(ParseColorError)?; 229 | if let Ok(a) = alpha.parse() { 230 | parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a }) 231 | } else { 232 | Err(ParseColorError) 233 | } 234 | } else { 235 | parse_rgb(color_values).map(|c| RinkColor { 236 | color: c, 237 | alpha: 1.0, 238 | }) 239 | } 240 | } else if let Some(stripped) = color.strip_prefix("rgba(") { 241 | let color_values = stripped.trim_end_matches(')'); 242 | if color.matches(',').count() == 3 { 243 | let (rgb_values, alpha) = 244 | color_values.rsplit_once(',').ok_or(ParseColorError)?; 245 | if let Ok(a) = parse_value(alpha, 1.0, 1.0) { 246 | parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a }) 247 | } else { 248 | Err(ParseColorError) 249 | } 250 | } else { 251 | parse_rgb(color_values).map(|c| RinkColor { 252 | color: c, 253 | alpha: 1.0, 254 | }) 255 | } 256 | } else if let Some(stripped) = color.strip_prefix("hsl(") { 257 | let color_values = stripped.trim_end_matches(')'); 258 | if color.matches(',').count() == 3 { 259 | let (rgb_values, alpha) = 260 | color_values.rsplit_once(',').ok_or(ParseColorError)?; 261 | if let Ok(a) = parse_value(alpha, 1.0, 1.0) { 262 | parse_hsl(rgb_values).map(|c| RinkColor { color: c, alpha: a }) 263 | } else { 264 | Err(ParseColorError) 265 | } 266 | } else { 267 | parse_hsl(color_values).map(|c| RinkColor { 268 | color: c, 269 | alpha: 1.0, 270 | }) 271 | } 272 | } else if let Some(stripped) = color.strip_prefix("hsla(") { 273 | let color_values = stripped.trim_end_matches(')'); 274 | if color.matches(',').count() == 3 { 275 | let (rgb_values, alpha) = 276 | color_values.rsplit_once(',').ok_or(ParseColorError)?; 277 | if let Ok(a) = parse_value(alpha, 1.0, 1.0) { 278 | parse_hsl(rgb_values).map(|c| RinkColor { color: c, alpha: a }) 279 | } else { 280 | Err(ParseColorError) 281 | } 282 | } else { 283 | parse_hsl(color_values).map(|c| RinkColor { 284 | color: c, 285 | alpha: 1.0, 286 | }) 287 | } 288 | } else { 289 | Err(ParseColorError) 290 | } 291 | } 292 | } 293 | } 294 | } 295 | 296 | fn to_rgb(c: Color) -> [u8; 3] { 297 | match c { 298 | Color::Black => [0, 0, 0], 299 | Color::Red => [255, 0, 0], 300 | Color::Green => [0, 128, 0], 301 | Color::Yellow => [255, 255, 0], 302 | Color::Blue => [0, 0, 255], 303 | Color::Magenta => [255, 0, 255], 304 | Color::Cyan => [0, 255, 255], 305 | Color::Gray => [128, 128, 128], 306 | Color::DarkGray => [169, 169, 169], 307 | Color::LightRed => [255, 69, 0], 308 | Color::LightGreen => [144, 238, 144], 309 | Color::LightYellow => [255, 255, 224], 310 | Color::LightBlue => [173, 216, 230], 311 | Color::LightMagenta => [218, 112, 214], 312 | Color::LightCyan => [224, 255, 255], 313 | Color::White => [255, 255, 255], 314 | Color::Rgb(r, g, b) => [r, g, b], 315 | Color::Indexed(idx) => match idx { 316 | 16..=231 => { 317 | let v = idx - 16; 318 | // add 3 to round up 319 | let r = ((v as u16 / 36) * 255 + 3) / 5; 320 | let g = (((v as u16 % 36) / 6) * 255 + 3) / 5; 321 | let b = ((v as u16 % 6) * 255 + 3) / 5; 322 | [r as u8, g as u8, b as u8] 323 | } 324 | 232..=255 => { 325 | let l = (idx - 232) / 24; 326 | [l; 3] 327 | } 328 | // rink will never generate these colors, but they might be on the screen from another program 329 | _ => [0, 0, 0], 330 | }, 331 | Color::Reset => [0, 0, 0], 332 | } 333 | } 334 | 335 | pub fn convert(mode: RenderingMode, c: Color) -> Color { 336 | if let Color::Reset = c { 337 | c 338 | } else { 339 | match mode { 340 | crate::RenderingMode::BaseColors => match c { 341 | Color::Rgb(_, _, _) => panic!("cannot convert rgb color to base color"), 342 | Color::Indexed(_) => panic!("cannot convert Ansi color to base color"), 343 | _ => c, 344 | }, 345 | crate::RenderingMode::Rgb => { 346 | let rgb = to_rgb(c); 347 | Color::Rgb(rgb[0], rgb[1], rgb[2]) 348 | } 349 | crate::RenderingMode::Ansi => match c { 350 | Color::Indexed(_) => c, 351 | _ => { 352 | let rgb = to_rgb(c); 353 | // 16-231: 6 × 6 × 6 color cube 354 | // 232-255: 23 step grayscale 355 | if rgb[0] == rgb[1] && rgb[1] == rgb[2] { 356 | let idx = 232 + (rgb[0] as u16 * 23 / 255) as u8; 357 | Color::Indexed(idx) 358 | } else { 359 | let r = (rgb[0] as u16 * 5) / 255; 360 | let g = (rgb[1] as u16 * 5) / 255; 361 | let b = (rgb[2] as u16 * 5) / 255; 362 | let idx = 16 + r * 36 + g * 6 + b; 363 | Color::Indexed(idx as u8) 364 | } 365 | } 366 | }, 367 | } 368 | } 369 | } 370 | 371 | #[test] 372 | fn rgb_to_ansi() { 373 | for idx in 17..=231 { 374 | let idxed = Color::Indexed(idx); 375 | let rgb = to_rgb(idxed); 376 | // gray scale colors have two equivelent repersentations 377 | let color = Color::Rgb(rgb[0], rgb[1], rgb[2]); 378 | let converted = convert(RenderingMode::Ansi, color); 379 | if let Color::Indexed(i) = converted { 380 | if rgb[0] != rgb[1] || rgb[1] != rgb[2] { 381 | assert_eq!(idxed, converted); 382 | } else { 383 | assert!(i >= 232); 384 | } 385 | } else { 386 | panic!("color is not indexed") 387 | } 388 | } 389 | for idx in 232..=255 { 390 | let idxed = Color::Indexed(idx); 391 | let rgb = to_rgb(idxed); 392 | assert!(rgb[0] == rgb[1] && rgb[1] == rgb[2]); 393 | } 394 | } 395 | 396 | #[derive(Clone, Copy)] 397 | pub struct RinkStyle { 398 | pub fg: Option, 399 | pub bg: Option, 400 | pub add_modifier: Modifier, 401 | pub sub_modifier: Modifier, 402 | } 403 | 404 | impl Default for RinkStyle { 405 | fn default() -> Self { 406 | Self { 407 | fg: Some(RinkColor { 408 | color: Color::White, 409 | alpha: 1.0, 410 | }), 411 | bg: None, 412 | add_modifier: Modifier::empty(), 413 | sub_modifier: Modifier::empty(), 414 | } 415 | } 416 | } 417 | 418 | impl RinkStyle { 419 | pub fn add_modifier(mut self, m: Modifier) -> Self { 420 | self.sub_modifier.remove(m); 421 | self.add_modifier.insert(m); 422 | self 423 | } 424 | 425 | pub fn remove_modifier(mut self, m: Modifier) -> Self { 426 | self.add_modifier.remove(m); 427 | self.sub_modifier.insert(m); 428 | self 429 | } 430 | 431 | pub fn merge(mut self, other: RinkStyle) -> Self { 432 | self.fg = self.fg.or(other.fg); 433 | self.add_modifier(other.add_modifier) 434 | .remove_modifier(other.sub_modifier) 435 | } 436 | } 437 | 438 | impl Into 49 | 50 | 51 | 52 |
53 |
54 |
55 |

Hello World

56 |

This is a test

57 |
58 |
59 |
60 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /tests/margin.rs: -------------------------------------------------------------------------------- 1 | use stretch2 as stretch; 2 | 3 | #[test] 4 | fn margin_and_flex_row() { 5 | let mut stretch = stretch::Stretch::new(); 6 | let node0 = stretch 7 | .new_node( 8 | stretch::style::Style { 9 | flex_grow: 1f32, 10 | margin: stretch::geometry::Rect { 11 | start: stretch::style::Dimension::Points(10f32), 12 | end: stretch::style::Dimension::Points(10f32), 13 | ..Default::default() 14 | }, 15 | ..Default::default() 16 | }, 17 | &[], 18 | ) 19 | .unwrap(); 20 | let node = stretch 21 | .new_node( 22 | stretch::style::Style { 23 | size: stretch::geometry::Size { 24 | width: stretch::style::Dimension::Points(100f32), 25 | height: stretch::style::Dimension::Points(100f32), 26 | ..Default::default() 27 | }, 28 | ..Default::default() 29 | }, 30 | &[node0], 31 | ) 32 | .unwrap(); 33 | stretch 34 | .compute_layout(node, stretch::geometry::Size::undefined()) 35 | .unwrap(); 36 | assert_eq!(stretch.layout(node).unwrap().size.width, 100f32); 37 | assert_eq!(stretch.layout(node).unwrap().size.height, 100f32); 38 | assert_eq!(stretch.layout(node).unwrap().location.x, 0f32); 39 | assert_eq!(stretch.layout(node).unwrap().location.y, 0f32); 40 | assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32); 41 | assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32); 42 | assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32); 43 | assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32); 44 | } 45 | 46 | #[test] 47 | fn margin_and_flex_row2() { 48 | let mut stretch = stretch::Stretch::new(); 49 | let node0 = stretch 50 | .new_node( 51 | stretch::style::Style { 52 | flex_grow: 1f32, 53 | margin: stretch::geometry::Rect { 54 | // left 55 | start: stretch::style::Dimension::Points(10f32), 56 | 57 | // right? 58 | end: stretch::style::Dimension::Points(10f32), 59 | 60 | // top? 61 | // top: stretch::style::Dimension::Points(10f32), 62 | 63 | // bottom? 64 | // bottom: stretch::style::Dimension::Points(10f32), 65 | ..Default::default() 66 | }, 67 | ..Default::default() 68 | }, 69 | &[], 70 | ) 71 | .unwrap(); 72 | 73 | let node = stretch 74 | .new_node( 75 | stretch::style::Style { 76 | size: stretch::geometry::Size { 77 | width: stretch::style::Dimension::Points(100f32), 78 | height: stretch::style::Dimension::Points(100f32), 79 | ..Default::default() 80 | }, 81 | ..Default::default() 82 | }, 83 | &[node0], 84 | ) 85 | .unwrap(); 86 | 87 | stretch 88 | .compute_layout(node, stretch::geometry::Size::undefined()) 89 | .unwrap(); 90 | 91 | assert_eq!(stretch.layout(node).unwrap().size.width, 100f32); 92 | assert_eq!(stretch.layout(node).unwrap().size.height, 100f32); 93 | assert_eq!(stretch.layout(node).unwrap().location.x, 0f32); 94 | assert_eq!(stretch.layout(node).unwrap().location.y, 0f32); 95 | 96 | dbg!(stretch.layout(node0)); 97 | 98 | // assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32); 99 | // assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32); 100 | // assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32); 101 | // assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32); 102 | } 103 | --------------------------------------------------------------------------------