├── .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 |
6 |
7 |
8 |
Rink
9 |
10 | Beautiful terminal user interfaces in Rust with Dioxus .
11 |
12 |
13 |
14 |
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 | 
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 |