Feat: merge rink into dioxus mainline

This commit is contained in:
Jonathan Kelley committed 2022-03-09 12:57:20 -05:00
commit 40c88eeb4b
30 files changed
+4003

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
+2
View File
@@ -0,0 +1,2 @@
esque
Tui
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "rink"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tui = "0.17.0"
crossterm = "0.22.1"
anyhow = "1.0.42"
thiserror = "1.0.24"
dioxus = "0.1.8"
dioxus-html = "0.1.6"
hecs = "0.7.3"
ctrlc = "3.2.1"
bumpalo = { version = "3.8.0", features = ["boxed"] }
tokio = { version = "1.15.0", features = ["full"] }
futures = "0.3.19"
stretch2 = { git = "https://github.com/DioxusLabs/stretch" }
+95
View File
@@ -0,0 +1,95 @@
<div align="center">
<h1>Rink</h1>
<p>
<strong>Beautiful terminal user interfaces in Rust with <a href="https://dioxuslabs.com/">Dioxus </a>.</strong>
</p>
</div>
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs -->
<a href="https://docs.rs/dioxus">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
<!-- CI -->
<a href="https://github.com/jkelleyrtp/dioxus/actions">
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
alt="CI status" />
</a>
<!--Awesome -->
<a href="https://github.com/dioxuslabs/awesome-dioxus">
<img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg" alt="Awesome Page" />
</a>
<!-- Discord -->
<a href="https://discord.gg/XgGxMSkvUM">
<img src="https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square" alt="Discord Link" />
</a>
</div>
<br/>
Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus.
```rust
fn app(cx: Scope) -> Element {
cx.render(rsx!{
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
"Hello world!"
}
})
}
```
![demo app](examples/example.png)
## Background
You can use Html-like semantics with stylesheets, inline styles, tree hierarchy, components, and more in your [`text-based user interface (TUI)`](https://en.wikipedia.org/wiki/Text-based_user_interface) application.
Rink is basically a port of [Ink](https://github.com/vadimdemedes/ink) but for [`Rust`](https://www.rust-lang.org/) and [`Dioxus`](https://dioxuslabs.com/). Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful.
## Limitations
- **Subset of Html**
Terminals can only render a subset of HTML. We support as much as we can.
- **Particular frontend design**
Terminals and browsers are and look different. Therefore, the same design might not be the best to cover both renderers.
## Status
**WARNING: Rink is currently under construction!**
Rendering a VirtualDom works fine, but the ecosystem of hooks is not yet ready. Additionally, some bugs in the flexbox implementation might be quirky at times.
## Features
Rink features:
- [x] Flexbox based layout system
- [ ] CSS selectors
- [x] inline CSS support
- [ ] Built-in focusing system
- [ ] high-quality keyboard support
- [ ] Support for events, hooks, and callbacks
* [ ] Html tags<sup>1</sup>
<sup>1</sup> Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality.
+27
View File
@@ -0,0 +1,27 @@
use dioxus::prelude::*;
fn main() {
rink::launch(app);
}
fn app(cx: Scope) -> Element {
let (radius, set_radius) = use_state(&cx, || 0);
cx.render(rsx! {
div {
width: "100%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "hsl(248, 53%, 58%)",
onwheel: move |w| set_radius((radius + w.delta_y as i8).abs()),
border_style: "solid none solid double",
border_width: "thick",
border_radius: "{radius}px",
border_color: "#0000FF #FF00FF #FF0000 #00FF00",
"{radius}"
}
})
}
+48
View File
@@ -0,0 +1,48 @@
use dioxus::prelude::*;
fn main() {
// rink::launch(app);
rink::launch_cfg(
app,
rink::Config {
rendering_mode: rink::RenderingMode::Ansi,
},
);
}
fn app(cx: Scope) -> Element {
let steps = 50;
cx.render(rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "column",
(0..=steps).map(|x|
{
let hue = x as f32*360.0/steps as f32;
cx.render(rsx! {
div{
width: "100%",
height: "100%",
flex_direction: "row",
(0..=steps).map(|y|
{
let alpha = y as f32*100.0/steps as f32;
cx.render(rsx! {
div {
left: "{x}px",
top: "{y}px",
width: "10%",
height: "100%",
background_color: "hsl({hue}, 100%, 50%, {alpha}%)",
}
})
}
)
}
})
}
)
}
})
}
+64
View File
@@ -0,0 +1,64 @@
use dioxus::prelude::*;
fn main() {
rink::launch(app);
}
#[derive(Props, PartialEq)]
struct QuadrentProps {
color: String,
text: String,
}
fn Quadrent(cx: Scope<QuadrentProps>) -> Element {
cx.render(rsx! {
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "{cx.props.color}",
justify_content: "center",
align_items: "center",
"{cx.props.text}"
}
})
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrent{
color: "red".to_string(),
text: "[A]".to_string()
},
Quadrent{
color: "black".to_string(),
text: "[B]".to_string()
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrent{
color: "green".to_string(),
text: "[C]".to_string()
},
Quadrent{
color: "blue".to_string(),
text: "[D]".to_string()
}
}
}
})
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+82
View File
@@ -0,0 +1,82 @@
use dioxus::prelude::*;
fn main() {
rink::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
// justify_content: "center",
// align_items: "center",
// flex_direction: "row",
// background_color: "red",
p {
background_color: "black",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"hi"
"hi"
"hi"
}
li {
background_color: "red",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
}
li {
background_color: "blue",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
}
p {
background_color: "yellow",
"asd"
}
p {
background_color: "green",
"asd"
}
p {
background_color: "white",
"asd"
}
p {
background_color: "cyan",
"asd"
}
}
})
}
+102
View File
@@ -0,0 +1,102 @@
use std::{convert::TryInto, sync::Arc};
use dioxus::{events::MouseData, prelude::*};
fn main() {
rink::launch(app);
}
fn app(cx: Scope) -> Element {
fn to_str(c: &[i32; 3]) -> String {
"#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::<String>()
}
fn get_brightness(m: Arc<MouseData>) -> i32 {
let mb = m.buttons;
let b: i32 = m.buttons.count_ones().try_into().unwrap();
127 * b
}
let (q1_color, set_q1_color) = use_state(&cx, || [200; 3]);
let (q2_color, set_q2_color) = use_state(&cx, || [200; 3]);
let (q3_color, set_q3_color) = use_state(&cx, || [200; 3]);
let (q4_color, set_q4_color) = use_state(&cx, || [200; 3]);
let q1_color_str = to_str(q1_color);
let q2_color_str = to_str(q2_color);
let q3_color_str = to_str(q3_color);
let q4_color_str = to_str(q4_color);
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
border_width: "1px",
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{q1_color_str}",
onmouseenter: move |m| set_q1_color([get_brightness(m.data), 0, 0]),
onmousedown: move |m| set_q1_color([get_brightness(m.data), 0, 0]),
onmouseup: move |m| set_q1_color([get_brightness(m.data), 0, 0]),
onwheel: move |w| set_q1_color([q1_color[0] + (10.0*w.delta_y) as i32, 0, 0]),
onmouseleave: move |_| set_q1_color([200; 3]),
"click me"
}
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{q2_color_str}",
onmouseenter: move |m| set_q2_color([get_brightness(m.data); 3]),
onmousedown: move |m| set_q2_color([get_brightness(m.data); 3]),
onmouseup: move |m| set_q2_color([get_brightness(m.data); 3]),
onwheel: move |w| set_q2_color([q2_color[0] + (10.0*w.delta_y) as i32;3]),
onmouseleave: move |_| set_q2_color([200; 3]),
"click me"
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{q3_color_str}",
onmouseenter: move |m| set_q3_color([0, get_brightness(m.data), 0]),
onmousedown: move |m| set_q3_color([0, get_brightness(m.data), 0]),
onmouseup: move |m| set_q3_color([0, get_brightness(m.data), 0]),
onwheel: move |w| set_q3_color([0, q3_color[1] + (10.0*w.delta_y) as i32, 0]),
onmouseleave: move |_| set_q3_color([200; 3]),
"click me"
}
div {
width: "50%",
height: "100%",
justify_content: "center",
align_items: "center",
background_color: "{q4_color_str}",
onmouseenter: move |m| set_q4_color([0, 0, get_brightness(m.data)]),
onmousedown: move |m| set_q4_color([0, 0, get_brightness(m.data)]),
onmouseup: move |m| set_q4_color([0, 0, get_brightness(m.data)]),
onwheel: move |w| set_q4_color([0, 0, q4_color[2] + (10.0*w.delta_y) as i32]),
onmouseleave: move |_| set_q4_color([200; 3]),
"click me"
}
}
}
})
}
+75
View File
@@ -0,0 +1,75 @@
use dioxus::events::WheelEvent;
use dioxus::prelude::*;
use dioxus_html::on::{KeyboardEvent, MouseEvent};
use dioxus_html::KeyCode;
fn main() {
rink::launch(app);
}
fn app(cx: Scope) -> Element {
let (key, set_key) = use_state(&cx, || "".to_string());
let (mouse, set_mouse) = use_state(&cx, || (0, 0));
let (count, set_count) = use_state(&cx, || 0);
let (buttons, set_buttons) = use_state(&cx, || 0);
let (mouse_clicked, set_mouse_clicked) = use_state(&cx, || false);
cx.render(rsx! {
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
flex_direction: "column",
onkeydown: move |evt: KeyboardEvent| {
match evt.data.key_code {
KeyCode::LeftArrow => set_count(count + 1),
KeyCode::RightArrow => set_count(count - 1),
KeyCode::UpArrow => set_count(count + 10),
KeyCode::DownArrow => set_count(count - 10),
_ => {},
}
set_key(format!("{:?} repeating: {:?}", evt.key, evt.repeat));
},
onwheel: move |evt: WheelEvent| {
set_count(count + evt.data.delta_y as i64);
},
ondrag: move |evt: MouseEvent| {
set_mouse((evt.data.screen_x, evt.data.screen_y));
},
onmousedown: move |evt: MouseEvent| {
set_mouse((evt.data.screen_x, evt.data.screen_y));
set_buttons(evt.data.buttons);
set_mouse_clicked(true);
},
onmouseup: move |evt: MouseEvent| {
set_buttons(evt.data.buttons);
set_mouse_clicked(false);
},
"count: {count:?}",
"key: {key}",
"mouse buttons: {buttons:b}",
"mouse pos: {mouse:?}",
"mouse button pressed: {mouse_clicked}"
}
})
}
fn app2<'a>(cx: Scope<'a>) -> Element<'a> {
let (count, set_count) = use_state(&cx, || 0);
cx.render(rsx! {
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
oninput: move |_| set_count(count + 1),
"Hello world!",
h1 {},
}
})
}
+52
View File
@@ -0,0 +1,52 @@
use std::collections::HashMap;
use dioxus::prelude::*;
fn main() {
let mut dom = VirtualDom::new(app);
dom.rebuild();
let mut layout = stretch2::Stretch::new();
let mut nodes = HashMap::new();
rink::collect_layout(&mut layout, &mut nodes, &dom, dom.base_scope().root_node());
let node = nodes
.remove(&dom.base_scope().root_node().mounted_id())
.unwrap();
layout
.compute_layout(node.layout, stretch2::geometry::Size::undefined())
.unwrap();
for (_id, node) in nodes.drain() {
println!("{:?}", layout.layout(node.layout));
}
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
"hi"
}
div {
"bi"
"bi"
}
}
})
}
// fn print_layout(mut nodes: HashMap<ElementId, TuiNode>, node: &VNode) {
// match node {
// VNode::Text(_) => todo!(),
// VNode::Element(_) => todo!(),
// VNode::Fragment(_) => todo!(),
// VNode::Component(_) => todo!(),
// VNode::Placeholder(_) => todo!(),
// }
// }
+28
View File
@@ -0,0 +1,28 @@
use dioxus::prelude::*;
fn main() {
rink::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
border_width: "1px",
h1 { height: "2px", color: "green",
"that's awesome!"
}
ul {
flex_direction: "column",
padding_left: "3px",
(0..10).map(|i| rsx!(
"> hello {i}"
))
}
}
})
}
+50
View File
@@ -0,0 +1,50 @@
use dioxus::prelude::*;
fn main() {
rink::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
background_color: "black",
// margin_right: "10px",
div {
width: "70%",
height: "70%",
background_color: "green",
// margin_left: "4px",
div {
width: "100%",
height: "100%",
margin_top: "2px",
margin_bottom: "2px",
margin_left: "2px",
margin_right: "2px",
flex_shrink: "0",
background_color: "red",
justify_content: "center",
align_items: "center",
flex_direction: "column",
// padding_top: "2px",
// padding_bottom: "2px",
// padding_left: "4px",
// padding_right: "4px",
"[A]"
"[A]"
"[A]"
"[A]"
}
}
}
})
}
+60
View File
@@ -0,0 +1,60 @@
use dioxus::prelude::*;
fn main() {
rink::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "red",
justify_content: "center",
align_items: "center",
"[A]"
}
div {
width: "50%",
height: "100%",
background_color: "black",
justify_content: "center",
align_items: "center",
"[B]"
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
div {
width: "50%",
height: "100%",
background_color: "green",
justify_content: "center",
align_items: "center",
"[C]"
}
div {
width: "50%",
height: "100%",
background_color: "blue",
justify_content: "center",
align_items: "center",
"[D]"
}
}
}
})
}
+19
View File
@@ -0,0 +1,19 @@
use dioxus::prelude::*;
fn main() {
rink::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "10px",
background_color: "red",
justify_content: "center",
align_items: "center",
"Hello world!"
}
})
}
+34
View File
@@ -0,0 +1,34 @@
use stretch2::prelude::*;
fn main() -> Result<(), Error> {
let mut stretch = Stretch::new();
let child = stretch.new_node(
Style {
size: Size {
width: Dimension::Percent(0.5),
height: Dimension::Auto,
},
..Default::default()
},
&[],
)?;
let node = stretch.new_node(
Style {
size: Size {
width: Dimension::Points(100.0),
height: Dimension::Points(100.0),
},
justify_content: JustifyContent::Center,
..Default::default()
},
&[child],
)?;
stretch.compute_layout(node, Size::undefined())?;
println!("node: {:#?}", stretch.layout(node)?);
println!("child: {:#?}", stretch.layout(child)?);
Ok(())
}
+46
View File
@@ -0,0 +1,46 @@
use dioxus::prelude::*;
fn main() {
rink::launch(app);
}
fn app(cx: Scope) -> Element {
let (count, set_count) = use_state(&cx, || 0);
use_future(&cx, move || {
let set_count = set_count.to_owned();
let update = cx.schedule_update();
async move {
loop {
set_count.with_mut(|f| *f += 1);
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
update();
}
}
});
cx.render(rsx! {
div { width: "100%",
div { width: "50%", height: "5px", background_color: "blue", justify_content: "center", align_items: "center",
"Hello {count}!"
}
div { width: "50%", height: "10px", background_color: "red", justify_content: "center", align_items: "center",
"Hello {count}!"
}
}
})
}
// use_future(&cx, || {
// let set_count = count.setter();
// let mut mycount = 0;
// let update = cx.schedule_update();
// async move {
// loop {
// tokio::time::sleep(std::time::Duration::from_millis(100)).await;
// mycount += 1;
// set_count(mycount);
// update();
// }
// }
// });
+127
View File
@@ -0,0 +1,127 @@
use dioxus::prelude::*;
fn main() {
rink::launch(app);
// rink::launch_cfg(
// app,
// rink::Config {
// rendering_mode: rink::RenderingMode::Ansi,
// },
// )
}
fn app(cx: Scope) -> Element {
let (alpha, set_alpha) = use_state(&cx, || 100);
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
// justify_content: "center",
// align_items: "center",
// flex_direction: "row",
onwheel: move |evt| {
set_alpha((alpha + evt.data.delta_y as i64).min(100).max(0));
},
p {
background_color: "black",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
color: "green",
"hi"
"hi"
"hi"
}
li {
background_color: "red",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
"bib"
}
li {
background_color: "blue",
flex_direction: "column",
justify_content: "center",
align_items: "center",
// height: "10%",
"zib"
"zib"
"zib"
"zib"
"zib"
"zib"
}
p {
background_color: "yellow",
"asd"
}
p {
background_color: "green",
"asd"
}
p {
background_color: "white",
"asd"
}
p {
background_color: "cyan",
"asd"
}
div {
font_weight: "bold",
color: "#666666",
p {
"bold"
}
p {
font_weight: "normal",
" normal"
}
}
p {
font_style: "italic",
color: "red",
"italic"
}
p {
text_decoration: "underline",
color: "rgba(255, 255, 255)",
"underline"
}
p {
text_decoration: "line-through",
color: "hsla(10, 100%, 70%)",
"line-through"
}
div{
position: "absolute",
top: "1px",
background_color: "rgba(255, 0, 0, 50%)",
width: "100%",
p {
color: "rgba(255, 255, 255, {alpha}%)",
background_color: "rgba(100, 100, 100, {alpha}%)",
"rgba(255, 255, 255, {alpha}%)"
}
p {
color: "rgba(255, 255, 255, 100%)",
"rgba(255, 255, 255, 100%)"
}
}
}
})
}
+5
View File
@@ -0,0 +1,5 @@
div {
div {
}
}
+908
View File
@@ -0,0 +1,908 @@
/*
- [ ] pub display: Display,
- [x] pub position_type: PositionType, --> kinda, stretch doesnt support everything
- [ ] pub direction: Direction,
- [x] pub flex_direction: FlexDirection,
- [x] pub flex_wrap: FlexWrap,
- [x] pub flex_grow: f32,
- [x] pub flex_shrink: f32,
- [x] pub flex_basis: Dimension,
- [x] pub overflow: Overflow, ---> kinda implemented... stretch doesnt have support for directional overflow
- [x] pub align_items: AlignItems,
- [x] pub align_self: AlignSelf,
- [x] pub align_content: AlignContent,
- [x] pub margin: Rect<Dimension>,
- [x] pub padding: Rect<Dimension>,
- [x] pub justify_content: JustifyContent,
- [ ] pub position: Rect<Dimension>,
- [ ] pub border: Rect<Dimension>,
- [ ] pub size: Size<Dimension>, ----> ??? seems to only be relevant for input?
- [ ] pub min_size: Size<Dimension>,
- [ ] pub max_size: Size<Dimension>,
- [ ] pub aspect_ratio: Number,
*/
use stretch2::{prelude::*, style::PositionType, style::Style};
use crate::style::{RinkColor, RinkStyle};
pub struct StyleModifer {
pub style: Style,
pub tui_style: RinkStyle,
pub tui_modifier: TuiModifier,
}
#[derive(Default)]
pub struct TuiModifier {
pub borders: Borders,
}
#[derive(Default)]
pub struct Borders {
pub top: BorderEdge,
pub right: BorderEdge,
pub bottom: BorderEdge,
pub left: BorderEdge,
}
impl Borders {
fn slice(&mut self) -> [&mut BorderEdge; 4] {
[
&mut self.top,
&mut self.right,
&mut self.bottom,
&mut self.left,
]
}
}
pub struct BorderEdge {
pub color: Option<RinkColor>,
pub style: BorderStyle,
pub width: UnitSystem,
pub radius: UnitSystem,
}
impl Default for BorderEdge {
fn default() -> Self {
Self {
color: None,
style: BorderStyle::NONE,
width: UnitSystem::Point(0.0),
radius: UnitSystem::Point(0.0),
}
}
}
#[derive(Clone, Copy)]
pub enum BorderStyle {
DOTTED,
DASHED,
SOLID,
DOUBLE,
GROOVE,
RIDGE,
INSET,
OUTSET,
HIDDEN,
NONE,
}
impl BorderStyle {
pub fn symbol_set(&self) -> Option<tui::symbols::line::Set> {
use tui::symbols::line::*;
const DASHED: Set = Set {
horizontal: "╌",
vertical: "╎",
..NORMAL
};
const DOTTED: Set = Set {
horizontal: "┈",
vertical: "┊",
..NORMAL
};
match self {
BorderStyle::DOTTED => Some(DOTTED),
BorderStyle::DASHED => Some(DASHED),
BorderStyle::SOLID => Some(NORMAL),
BorderStyle::DOUBLE => Some(DOUBLE),
BorderStyle::GROOVE => Some(NORMAL),
BorderStyle::RIDGE => Some(NORMAL),
BorderStyle::INSET => Some(NORMAL),
BorderStyle::OUTSET => Some(NORMAL),
BorderStyle::HIDDEN => None,
BorderStyle::NONE => None,
}
}
}
/// applies the entire html namespace defined in dioxus-html
pub fn apply_attributes(
//
name: &str,
value: &str,
style: &mut StyleModifer,
) {
match name {
"align-content"
| "align-items"
| "align-self" => apply_align(name, value, style),
"animation"
| "animation-delay"
| "animation-direction"
| "animation-duration"
| "animation-fill-mode"
| "animation-iteration-count"
| "animation-name"
| "animation-play-state"
| "animation-timing-function" => apply_animation(name, value, style),
"backface-visibility" => {}
"background"
| "background-attachment"
| "background-clip"
| "background-color"
| "background-image"
| "background-origin"
| "background-position"
| "background-repeat"
| "background-size" => apply_background(name, value, style),
"border"
| "border-bottom"
| "border-bottom-color"
| "border-bottom-left-radius"
| "border-bottom-right-radius"
| "border-bottom-style"
| "border-bottom-width"
| "border-collapse"
| "border-color"
| "border-image"
| "border-image-outset"
| "border-image-repeat"
| "border-image-slice"
| "border-image-source"
| "border-image-width"
| "border-left"
| "border-left-color"
| "border-left-style"
| "border-left-width"
| "border-radius"
| "border-right"
| "border-right-color"
| "border-right-style"
| "border-right-width"
| "border-spacing"
| "border-style"
| "border-top"
| "border-top-color"
| "border-top-left-radius"
| "border-top-right-radius"
| "border-top-style"
| "border-top-width"
| "border-width" => apply_border(name, value, style),
"bottom" => {}
"box-shadow" => {}
"box-sizing" => {}
"caption-side" => {}
"clear" => {}
"clip" => {}
"color" => {
if let Ok(c) = value.parse() {
style.tui_style.fg.replace(c);
}
}
"column-count"
| "column-fill"
| "column-gap"
| "column-rule"
| "column-rule-color"
| "column-rule-style"
| "column-rule-width"
| "column-span"
// add column-width
| "column-width" => apply_column(name, value, style),
"columns" => {}
"content" => {}
"counter-increment" => {}
"counter-reset" => {}
"cursor" => {}
"direction" => {
match value {
"ltr" => style.style.direction = Direction::LTR,
"rtl" => style.style.direction = Direction::RTL,
_ => {}
}
}
"display" => apply_display(name, value, style),
"empty-cells" => {}
"flex"
| "flex-basis"
| "flex-direction"
| "flex-flow"
| "flex-grow"
| "flex-shrink"
| "flex-wrap" => apply_flex(name, value, style),
"float" => {}
"font"
| "font-family"
| "font-size"
| "font-size-adjust"
| "font-stretch"
| "font-style"
| "font-variant"
| "font-weight" => apply_font(name, value, style),
"height" => {
if let Some(v) = parse_value(value){
style.style.size.height = match v {
UnitSystem::Percent(v)=> Dimension::Percent(v/100.0),
UnitSystem::Point(v)=> Dimension::Points(v),
};
}
}
"justify-content" => {
use JustifyContent::*;
style.style.justify_content = match value {
"flex-start" => FlexStart,
"flex-end" => FlexEnd,
"center" => Center,
"space-between" => SpaceBetween,
"space-around" => SpaceAround,
"space-evenly" => SpaceEvenly,
_ => FlexStart,
};
}
"left" => {}
"letter-spacing" => {}
"line-height" => {}
"list-style"
| "list-style-image"
| "list-style-position"
| "list-style-type" => {}
"margin"
| "margin-bottom"
| "margin-left"
| "margin-right"
| "margin-top" => apply_margin(name, value, style),
"max-height" => {}
"max-width" => {}
"min-height" => {}
"min-width" => {}
"opacity" => {}
"order" => {}
"outline" => {}
"outline-color"
| "outline-offset"
| "outline-style"
| "outline-width" => {}
"overflow"
| "overflow-x"
| "overflow-y" => apply_overflow(name, value, style),
"padding"
| "padding-bottom"
| "padding-left"
| "padding-right"
| "padding-top" => apply_padding(name, value, style),
"page-break-after"
| "page-break-before"
| "page-break-inside" => {}
"perspective"
| "perspective-origin" => {}
"position" => {
match value {
"static" => {}
"relative" => style.style.position_type = PositionType::Relative,
"fixed" => {}
"absolute" => style.style.position_type = PositionType::Absolute,
"sticky" => {}
_ => {}
}
}
"pointer-events" => {}
"quotes" => {}
"resize" => {}
"right" => {}
"tab-size" => {}
"table-layout" => {}
"text-align"
| "text-align-last"
| "text-decoration"
| "text-decoration-color"
| "text-decoration-line"
| "text-decoration-style"
| "text-indent"
| "text-justify"
| "text-overflow"
| "text-shadow"
| "text-transform" => apply_text(name, value, style),
"top" => {}
"transform"
| "transform-origin"
| "transform-style" => apply_transform(name, value, style),
"transition"
| "transition-delay"
| "transition-duration"
| "transition-property"
| "transition-timing-function" => apply_transition(name, value, style),
"vertical-align" => {}
"visibility" => {}
"white-space" => {}
"width" => {
if let Some(v) = parse_value(value){
style.style.size.width = match v {
UnitSystem::Percent(v)=> Dimension::Percent(v/100.0),
UnitSystem::Point(v)=> Dimension::Points(v),
};
}
}
"word-break" => {}
"word-spacing" => {}
"word-wrap" => {}
"z-index" => {}
_ => {}
}
}
#[derive(Clone, Copy)]
pub enum UnitSystem {
Percent(f32),
Point(f32),
}
fn parse_value(value: &str) -> Option<UnitSystem> {
if value.ends_with("px") {
if let Ok(px) = value.trim_end_matches("px").parse::<f32>() {
Some(UnitSystem::Point(px))
} else {
None
}
} else if value.ends_with('%') {
if let Ok(pct) = value.trim_end_matches('%').parse::<f32>() {
Some(UnitSystem::Percent(pct))
} else {
None
}
} else {
None
}
}
fn apply_overflow(name: &str, value: &str, style: &mut StyleModifer) {
match name {
// todo: add more overflow support to stretch2
"overflow" | "overflow-x" | "overflow-y" => {
style.style.overflow = match value {
"auto" => Overflow::Visible,
"hidden" => Overflow::Hidden,
"scroll" => Overflow::Scroll,
"visible" => Overflow::Visible,
_ => Overflow::Visible,
};
}
_ => {}
}
}
fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) {
style.style.display = match value {
"flex" => Display::Flex,
"block" => Display::None,
_ => Display::Flex,
}
// TODO: there are way more variants
// stretch needs to be updated to handle them
//
// "block" => Display::Block,
// "inline" => Display::Inline,
// "inline-block" => Display::InlineBlock,
// "inline-table" => Display::InlineTable,
// "list-item" => Display::ListItem,
// "run-in" => Display::RunIn,
// "table" => Display::Table,
// "table-caption" => Display::TableCaption,
// "table-cell" => Display::TableCell,
// "table-column" => Display::TableColumn,
// "table-column-group" => Display::TableColumnGroup,
// "table-footer-group" => Display::TableFooterGroup,
// "table-header-group" => Display::TableHeaderGroup,
// "table-row" => Display::TableRow,
// "table-row-group" => Display::TableRowGroup,
// "none" => Display::None,
// _ => Display::Inline,
}
fn apply_background(name: &str, value: &str, style: &mut StyleModifer) {
match name {
"background-color" => {
if let Ok(c) = value.parse() {
style.tui_style.bg.replace(c);
}
}
"background" => {}
"background-attachment" => {}
"background-clip" => {}
"background-image" => {}
"background-origin" => {}
"background-position" => {}
"background-repeat" => {}
"background-size" => {}
_ => {}
}
}
fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
fn parse_border_style(v: &str) -> BorderStyle {
match v {
"dotted" => BorderStyle::DOTTED,
"dashed" => BorderStyle::DASHED,
"solid" => BorderStyle::SOLID,
"double" => BorderStyle::DOUBLE,
"groove" => BorderStyle::GROOVE,
"ridge" => BorderStyle::RIDGE,
"inset" => BorderStyle::INSET,
"outset" => BorderStyle::OUTSET,
"none" => BorderStyle::NONE,
"hidden" => BorderStyle::HIDDEN,
_ => todo!(),
}
}
match name {
"border" => {}
"border-bottom" => {}
"border-bottom-color" => {
if let Ok(c) = value.parse() {
style.tui_modifier.borders.bottom.color = Some(c);
}
}
"border-bottom-left-radius" => {
if let Some(v) = parse_value(value) {
style.tui_modifier.borders.left.radius = v;
}
}
"border-bottom-right-radius" => {
if let Some(v) = parse_value(value) {
style.tui_modifier.borders.right.radius = v;
}
}
"border-bottom-style" => {
style.tui_modifier.borders.bottom.style = parse_border_style(value)
}
"border-bottom-width" => {
if let Some(v) = parse_value(value) {
style.tui_modifier.borders.bottom.width = v;
}
}
"border-collapse" => {}
"border-color" => {
let values: Vec<_> = value.split(' ').collect();
if values.len() == 1 {
if let Ok(c) = values[0].parse() {
style
.tui_modifier
.borders
.slice()
.iter_mut()
.for_each(|b| b.color = Some(c));
}
} else {
for (v, b) in values
.into_iter()
.zip(style.tui_modifier.borders.slice().iter_mut())
{
if let Ok(c) = v.parse() {
b.color = Some(c);
}
}
}
}
"border-image" => {}
"border-image-outset" => {}
"border-image-repeat" => {}
"border-image-slice" => {}
"border-image-source" => {}
"border-image-width" => {}
"border-left" => {}
"border-left-color" => {
if let Ok(c) = value.parse() {
style.tui_modifier.borders.left.color = Some(c);
}
}
"border-left-style" => style.tui_modifier.borders.left.style = parse_border_style(value),
"border-left-width" => {
if let Some(v) = parse_value(value) {
style.tui_modifier.borders.left.width = v;
}
}
"border-radius" => {
let values: Vec<_> = value.split(' ').collect();
if values.len() == 1 {
if let Some(r) = parse_value(values[0]) {
style
.tui_modifier
.borders
.slice()
.iter_mut()
.for_each(|b| b.radius = r);
}
} else {
for (v, b) in values
.into_iter()
.zip(style.tui_modifier.borders.slice().iter_mut())
{
if let Some(r) = parse_value(v) {
b.radius = r;
}
}
}
}
"border-right" => {}
"border-right-color" => {
if let Ok(c) = value.parse() {
style.tui_modifier.borders.right.color = Some(c);
}
}
"border-right-style" => style.tui_modifier.borders.right.style = parse_border_style(value),
"border-right-width" => {
if let Some(v) = parse_value(value) {
style.tui_modifier.borders.right.width = v;
}
}
"border-spacing" => {}
"border-style" => {
let values: Vec<_> = value.split(' ').collect();
if values.len() == 1 {
let border_style = parse_border_style(values[0]);
style
.tui_modifier
.borders
.slice()
.iter_mut()
.for_each(|b| b.style = border_style);
} else {
for (v, b) in values
.into_iter()
.zip(style.tui_modifier.borders.slice().iter_mut())
{
b.style = parse_border_style(v);
}
}
}
"border-top" => {}
"border-top-color" => {
if let Ok(c) = value.parse() {
style.tui_modifier.borders.top.color = Some(c);
}
}
"border-top-left-radius" => {
if let Some(v) = parse_value(value) {
style.tui_modifier.borders.left.radius = v;
}
}
"border-top-right-radius" => {
if let Some(v) = parse_value(value) {
style.tui_modifier.borders.right.radius = v;
}
}
"border-top-style" => style.tui_modifier.borders.top.style = parse_border_style(value),
"border-top-width" => {
if let Some(v) = parse_value(value) {
style.tui_modifier.borders.top.width = v;
}
}
"border-width" => {
let values: Vec<_> = value.split(' ').collect();
if values.len() == 1 {
if let Some(w) = parse_value(values[0]) {
style
.tui_modifier
.borders
.slice()
.iter_mut()
.for_each(|b| b.width = w);
}
} else {
for (v, b) in values
.into_iter()
.zip(style.tui_modifier.borders.slice().iter_mut())
{
if let Some(w) = parse_value(v) {
b.width = w;
}
}
}
}
_ => (),
}
}
fn apply_animation(name: &str, _value: &str, _style: &mut StyleModifer) {
match name {
"animation" => {}
"animation-delay" => {}
"animation-direction =>{}" => {}
"animation-duration" => {}
"animation-fill-mode" => {}
"animation-itera =>{}tion-count" => {}
"animation-name" => {}
"animation-play-state" => {}
"animation-timing-function" => {}
_ => {}
}
}
fn apply_column(name: &str, _value: &str, _style: &mut StyleModifer) {
match name {
"column-count" => {}
"column-fill" => {}
"column-gap" => {}
"column-rule" => {}
"column-rule-color" => {}
"column-rule-style" => {}
"column-rule-width" => {}
"column-span" => {}
"column-width" => {}
_ => {}
}
}
fn apply_flex(name: &str, value: &str, style: &mut StyleModifer) {
// - [x] pub flex_direction: FlexDirection,
// - [x] pub flex_wrap: FlexWrap,
// - [x] pub flex_grow: f32,
// - [x] pub flex_shrink: f32,
// - [x] pub flex_basis: Dimension,
match name {
"flex" => {}
"flex-direction" => {
use FlexDirection::*;
style.style.flex_direction = match value {
"row" => Row,
"row-reverse" => RowReverse,
"column" => Column,
"column-reverse" => ColumnReverse,
_ => Row,
};
}
"flex-basis" => {
if let Some(v) = parse_value(value) {
style.style.flex_basis = match v {
UnitSystem::Percent(v) => Dimension::Percent(v / 100.0),
UnitSystem::Point(v) => Dimension::Points(v),
};
}
}
"flex-flow" => {}
"flex-grow" => {
if let Ok(val) = value.parse::<f32>() {
style.style.flex_grow = val;
}
}
"flex-shrink" => {
if let Ok(px) = value.parse::<f32>() {
style.style.flex_shrink = px;
}
}
"flex-wrap" => {
use FlexWrap::*;
style.style.flex_wrap = match value {
"nowrap" => NoWrap,
"wrap" => Wrap,
"wrap-reverse" => WrapReverse,
_ => NoWrap,
};
}
_ => {}
}
}
fn apply_font(name: &str, value: &str, style: &mut StyleModifer) {
use tui::style::Modifier;
match name {
"font" => (),
"font-family" => (),
"font-size" => (),
"font-size-adjust" => (),
"font-stretch" => (),
"font-style" => match value {
"italic" => style.tui_style = style.tui_style.add_modifier(Modifier::ITALIC),
"oblique" => style.tui_style = style.tui_style.add_modifier(Modifier::ITALIC),
_ => (),
},
"font-variant" => todo!(),
"font-weight" => match value {
"bold" => style.tui_style = style.tui_style.add_modifier(Modifier::BOLD),
"normal" => style.tui_style = style.tui_style.remove_modifier(Modifier::BOLD),
_ => (),
},
_ => (),
}
}
fn apply_padding(name: &str, value: &str, style: &mut StyleModifer) {
match parse_value(value) {
Some(UnitSystem::Percent(v)) => match name {
"padding" => {
let v = Dimension::Percent(v / 100.0);
style.style.padding.top = v;
style.style.padding.bottom = v;
style.style.padding.start = v;
style.style.padding.end = v;
}
"padding-bottom" => style.style.padding.bottom = Dimension::Percent(v / 100.0),
"padding-left" => style.style.padding.start = Dimension::Percent(v / 100.0),
"padding-right" => style.style.padding.end = Dimension::Percent(v / 100.0),
"padding-top" => style.style.padding.top = Dimension::Percent(v / 100.0),
_ => {}
},
Some(UnitSystem::Point(v)) => match name {
"padding" => {
style.style.padding.top = Dimension::Points(v);
style.style.padding.bottom = Dimension::Points(v);
style.style.padding.start = Dimension::Points(v);
style.style.padding.end = Dimension::Points(v);
}
"padding-bottom" => style.style.padding.bottom = Dimension::Points(v),
"padding-left" => style.style.padding.start = Dimension::Points(v),
"padding-right" => style.style.padding.end = Dimension::Points(v),
"padding-top" => style.style.padding.top = Dimension::Points(v),
_ => {}
},
None => {}
}
}
fn apply_text(name: &str, value: &str, style: &mut StyleModifer) {
use tui::style::Modifier;
match name {
"text-align" => todo!(),
"text-align-last" => todo!(),
"text-decoration" | "text-decoration-line" => {
for v in value.split(' ') {
match v {
"line-through" => {
style.tui_style = style.tui_style.add_modifier(Modifier::CROSSED_OUT)
}
"underline" => {
style.tui_style = style.tui_style.add_modifier(Modifier::UNDERLINED)
}
_ => (),
}
}
}
"text-decoration-color" => todo!(),
"text-decoration-style" => todo!(),
"text-indent" => todo!(),
"text-justify" => todo!(),
"text-overflow" => todo!(),
"text-shadow" => todo!(),
"text-transform" => todo!(),
_ => todo!(),
}
}
fn apply_transform(_name: &str, _value: &str, _style: &mut StyleModifer) {
todo!()
}
fn apply_transition(_name: &str, _value: &str, _style: &mut StyleModifer) {
todo!()
}
fn apply_align(name: &str, value: &str, style: &mut StyleModifer) {
match name {
"align-items" => {
use AlignItems::*;
style.style.align_items = match value {
"flex-start" => FlexStart,
"flex-end" => FlexEnd,
"center" => Center,
"baseline" => Baseline,
"stretch" => Stretch,
_ => FlexStart,
};
}
"align-content" => {
use AlignContent::*;
style.style.align_content = match value {
"flex-start" => FlexStart,
"flex-end" => FlexEnd,
"center" => Center,
"space-between" => SpaceBetween,
"space-around" => SpaceAround,
_ => FlexStart,
};
}
"align-self" => {
use AlignSelf::*;
style.style.align_self = match value {
"auto" => Auto,
"flex-start" => FlexStart,
"flex-end" => FlexEnd,
"center" => Center,
"baseline" => Baseline,
"stretch" => Stretch,
_ => Auto,
};
}
_ => {}
}
}
pub fn apply_size(_name: &str, _value: &str, _style: &mut StyleModifer) {
//
}
pub fn apply_margin(name: &str, value: &str, style: &mut StyleModifer) {
match parse_value(value) {
Some(UnitSystem::Percent(v)) => match name {
"margin" => {
let v = Dimension::Percent(v / 100.0);
style.style.margin.top = v;
style.style.margin.bottom = v;
style.style.margin.start = v;
style.style.margin.end = v;
}
"margin-top" => style.style.margin.top = Dimension::Percent(v / 100.0),
"margin-bottom" => style.style.margin.bottom = Dimension::Percent(v / 100.0),
"margin-left" => style.style.margin.start = Dimension::Percent(v / 100.0),
"margin-right" => style.style.margin.end = Dimension::Percent(v / 100.0),
_ => {}
},
Some(UnitSystem::Point(v)) => match name {
"margin" => {
style.style.margin.top = Dimension::Points(v);
style.style.margin.bottom = Dimension::Points(v);
style.style.margin.start = Dimension::Points(v);
style.style.margin.end = Dimension::Points(v);
}
"margin-top" => style.style.margin.top = Dimension::Points(v),
"margin-bottom" => style.style.margin.bottom = Dimension::Points(v),
"margin-left" => style.style.margin.start = Dimension::Points(v),
"margin-right" => style.style.margin.end = Dimension::Points(v),
_ => {}
},
None => {}
}
}
+20
View File
@@ -0,0 +1,20 @@
#[derive(Default, Clone, Copy)]
pub struct Config {
pub rendering_mode: RenderingMode,
}
#[derive(Clone, Copy)]
pub enum RenderingMode {
/// only 16 colors by accessed by name, no alpha support
BaseColors,
/// 8 bit colors, will be downsampled from rgb colors
Ansi,
/// 24 bit colors, most terminals support this
Rgb,
}
impl Default for RenderingMode {
fn default() -> Self {
RenderingMode::Rgb
}
}
+652
View File
@@ -0,0 +1,652 @@
use crossterm::event::{
Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, MouseButton, MouseEventKind,
};
use dioxus::core::*;
use dioxus_html::{on::*, KeyCode};
use futures::{channel::mpsc::UnboundedReceiver, StreamExt};
use std::{
any::Any,
cell::RefCell,
collections::{HashMap, HashSet},
rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
use stretch2::{prelude::Layout, Stretch};
use crate::TuiNode;
// a wrapper around the input state for easier access
// todo: fix loop
// pub struct InputState(Rc<Rc<RefCell<InnerInputState>>>);
// impl InputState {
// pub fn get(cx: &ScopeState) -> InputState {
// let inner = cx
// .consume_context::<Rc<RefCell<InnerInputState>>>()
// .expect("Rink InputState can only be used in Rink apps!");
// (**inner).borrow_mut().subscribe(cx.schedule_update());
// InputState(inner)
// }
// pub fn mouse(&self) -> Option<MouseData> {
// let data = (**self.0).borrow();
// data.mouse.as_ref().map(|m| clone_mouse_data(m))
// }
// pub fn wheel(&self) -> Option<WheelData> {
// let data = (**self.0).borrow();
// data.wheel.as_ref().map(|w| clone_wheel_data(w))
// }
// pub fn screen(&self) -> Option<(u16, u16)> {
// let data = (**self.0).borrow();
// data.screen.as_ref().map(|m| m.clone())
// }
// pub fn last_key_pressed(&self) -> Option<KeyboardData> {
// let data = (**self.0).borrow();
// data.last_key_pressed
// .as_ref()
// .map(|k| clone_keyboard_data(&k.0))
// }
// }
type EventCore = (&'static str, EventData);
#[derive(Debug)]
enum EventData {
Mouse(MouseData),
Wheel(WheelData),
Screen((u16, u16)),
Keyboard(KeyboardData),
}
impl EventData {
fn into_any(self) -> Arc<dyn Any + Send + Sync> {
match self {
Self::Mouse(m) => Arc::new(m),
Self::Wheel(w) => Arc::new(w),
Self::Screen(s) => Arc::new(s),
Self::Keyboard(k) => Arc::new(k),
}
}
}
const MAX_REPEAT_TIME: Duration = Duration::from_millis(100);
pub struct InnerInputState {
mouse: Option<(MouseData, Vec<u16>)>,
wheel: Option<WheelData>,
last_key_pressed: Option<(KeyboardData, Instant)>,
screen: Option<(u16, u16)>,
// subscribers: Vec<Rc<dyn Fn() + 'static>>,
}
impl InnerInputState {
fn new() -> Self {
Self {
mouse: None,
wheel: None,
last_key_pressed: None,
screen: None,
// subscribers: Vec::new(),
}
}
// stores current input state and transforms events based on that state
fn apply_event(&mut self, evt: &mut EventCore) {
match evt.1 {
// limitations: only two buttons may be held at once
EventData::Mouse(ref mut m) => match &mut self.mouse {
Some(state) => {
let mut buttons = state.0.buttons;
state.0 = clone_mouse_data(m);
match evt.0 {
// this code only runs when there are no buttons down
"mouseup" => {
buttons = 0;
state.1 = Vec::new();
}
"mousedown" => {
if state.1.contains(&m.buttons) {
// if we already pressed a button and there is another button released the button crossterm sends is the button remaining
if state.1.len() > 1 {
evt.0 = "mouseup";
state.1 = vec![m.buttons];
}
// otherwise some other button was pressed. In testing it was consistantly this mapping
else {
match m.buttons {
0x01 => state.1.push(0x02),
0x02 => state.1.push(0x01),
0x04 => state.1.push(0x01),
_ => (),
}
}
} else {
state.1.push(m.buttons);
}
buttons = state.1.iter().copied().reduce(|a, b| a | b).unwrap();
}
_ => (),
}
state.0.buttons = buttons;
m.buttons = buttons;
}
None => {
self.mouse = Some((
clone_mouse_data(m),
if m.buttons == 0 {
Vec::new()
} else {
vec![m.buttons]
},
));
}
},
EventData::Wheel(ref w) => self.wheel = Some(clone_wheel_data(w)),
EventData::Screen(ref s) => self.screen = Some(s.clone()),
EventData::Keyboard(ref mut k) => {
let repeat = self
.last_key_pressed
.as_ref()
.filter(|k2| k2.0.key == k.key && k2.1.elapsed() < MAX_REPEAT_TIME)
.is_some();
k.repeat = repeat;
let new = clone_keyboard_data(k);
self.last_key_pressed = Some((new, Instant::now()));
}
}
}
fn update<'a>(
&mut self,
dom: &'a VirtualDom,
evts: &mut Vec<EventCore>,
resolved_events: &mut Vec<UserEvent>,
layout: &Stretch,
layouts: &mut HashMap<ElementId, TuiNode<'a>>,
node: &'a VNode<'a>,
) {
struct Data<'b> {
new_pos: (i32, i32),
old_pos: Option<(i32, i32)>,
clicked: bool,
released: bool,
wheel_delta: f64,
mouse_data: &'b MouseData,
wheel_data: &'b Option<WheelData>,
};
fn layout_contains_point(layout: &Layout, point: (i32, i32)) -> bool {
layout.location.x as i32 <= point.0
&& layout.location.x as i32 + layout.size.width as i32 >= point.0
&& layout.location.y as i32 <= point.1
&& layout.location.y as i32 + layout.size.height as i32 >= point.1
}
fn get_mouse_events<'c, 'd>(
dom: &'c VirtualDom,
resolved_events: &mut Vec<UserEvent>,
layout: &Stretch,
layouts: &HashMap<ElementId, TuiNode<'c>>,
node: &'c VNode<'c>,
data: &'d Data<'d>,
) -> HashSet<&'static str> {
match node {
VNode::Fragment(f) => {
let mut union = HashSet::new();
for child in f.children {
union = union
.union(&get_mouse_events(
dom,
resolved_events,
layout,
layouts,
child,
data,
))
.copied()
.collect();
}
return union;
}
VNode::Component(vcomp) => {
let idx = vcomp.scope.get().unwrap();
let new_node = dom.get_scope(idx).unwrap().root_node();
return get_mouse_events(dom, resolved_events, layout, layouts, new_node, data);
}
VNode::Placeholder(_) => return HashSet::new(),
VNode::Element(_) | VNode::Text(_) => {}
}
let id = node.try_mounted_id().unwrap();
let node = layouts.get(&id).unwrap();
let node_layout = layout.layout(node.layout).unwrap();
let previously_contained = data
.old_pos
.filter(|pos| layout_contains_point(node_layout, *pos))
.is_some();
let currently_contains = layout_contains_point(node_layout, data.new_pos);
match node.node {
VNode::Element(el) => {
let mut events = HashSet::new();
if previously_contained || currently_contains {
for c in el.children {
events = events
.union(&get_mouse_events(
dom,
resolved_events,
layout,
layouts,
c,
data,
))
.copied()
.collect();
}
}
let mut try_create_event = |name| {
// only trigger event if the event was not triggered already by a child
if events.insert(name) {
resolved_events.push(UserEvent {
scope_id: None,
priority: EventPriority::Medium,
name,
element: Some(el.id.get().unwrap()),
data: Arc::new(clone_mouse_data(data.mouse_data)),
})
}
};
if currently_contains {
if !previously_contained {
try_create_event("mouseenter");
try_create_event("mouseover");
}
if data.clicked {
try_create_event("mousedown");
}
if data.released {
try_create_event("mouseup");
match data.mouse_data.button {
0 => try_create_event("click"),
2 => try_create_event("contextmenu"),
_ => (),
}
}
if let Some(w) = data.wheel_data {
if data.wheel_delta != 0.0 {
resolved_events.push(UserEvent {
scope_id: None,
priority: EventPriority::Medium,
name: "wheel",
element: Some(el.id.get().unwrap()),
data: Arc::new(clone_wheel_data(w)),
})
}
}
} else {
if previously_contained {
try_create_event("mouseleave");
try_create_event("mouseout");
}
}
events
}
VNode::Text(_) => HashSet::new(),
_ => todo!(),
}
}
let previous_mouse = self
.mouse
.as_ref()
.map(|m| (clone_mouse_data(&m.0), m.1.clone()));
// println!("{previous_mouse:?}");
self.wheel = None;
for e in evts.iter_mut() {
self.apply_event(e);
}
// resolve hover events
if let Some(mouse) = &self.mouse {
let new_pos = (mouse.0.screen_x, mouse.0.screen_y);
let old_pos = previous_mouse
.as_ref()
.map(|m| (m.0.screen_x, m.0.screen_y));
let clicked =
(!mouse.0.buttons & previous_mouse.as_ref().map(|m| m.0.buttons).unwrap_or(0)) > 0;
let released =
(mouse.0.buttons & !previous_mouse.map(|m| m.0.buttons).unwrap_or(0)) > 0;
let wheel_delta = self.wheel.as_ref().map_or(0.0, |w| w.delta_y);
let mouse_data = &mouse.0;
let wheel_data = &self.wheel;
let data = Data {
new_pos,
old_pos,
clicked,
released,
wheel_delta,
mouse_data,
wheel_data,
};
get_mouse_events(dom, resolved_events, layout, layouts, node, &data);
}
// for s in &self.subscribers {
// s();
// }
}
// fn subscribe(&mut self, f: Rc<dyn Fn() + 'static>) {
// self.subscribers.push(f)
// }
}
pub struct RinkInputHandler {
state: Rc<RefCell<InnerInputState>>,
queued_events: Rc<RefCell<Vec<EventCore>>>,
}
impl RinkInputHandler {
/// global context that handles events
/// limitations: GUI key modifier is never detected, key up events are not detected, and only two mouse buttons may be pressed at once
pub fn new(
mut receiver: UnboundedReceiver<TermEvent>,
cx: &ScopeState,
) -> (Self, Rc<RefCell<InnerInputState>>) {
let queued_events = Rc::new(RefCell::new(Vec::new()));
let queued_events2 = Rc::<RefCell<std::vec::Vec<_>>>::downgrade(&queued_events);
cx.push_future(async move {
while let Some(evt) = receiver.next().await {
if let Some(evt) = get_event(evt) {
if let Some(v) = queued_events2.upgrade() {
(*v).borrow_mut().push(evt);
} else {
break;
}
}
}
});
let state = Rc::new(RefCell::new(InnerInputState::new()));
(
Self {
state: state.clone(),
queued_events,
},
state,
)
}
pub fn get_events<'a>(
&self,
dom: &'a VirtualDom,
layout: &Stretch,
layouts: &mut HashMap<ElementId, TuiNode<'a>>,
node: &'a VNode<'a>,
) -> Vec<UserEvent> {
// todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus
fn inner(
queue: &Vec<(&'static str, Arc<dyn Any + Send + Sync>)>,
resolved: &mut Vec<UserEvent>,
node: &VNode,
) {
match node {
VNode::Fragment(frag) => {
for c in frag.children {
inner(queue, resolved, c);
}
}
VNode::Element(el) => {
for l in el.listeners {
for (name, data) in queue.iter() {
if *name == l.event {
if let Some(id) = el.id.get() {
resolved.push(UserEvent {
scope_id: None,
priority: EventPriority::Medium,
name: *name,
element: Some(id),
data: data.clone(),
});
}
}
}
}
for c in el.children {
inner(queue, resolved, c);
}
}
_ => (),
}
}
let mut resolved_events = Vec::new();
(*self.state).borrow_mut().update(
dom,
&mut (*self.queued_events).borrow_mut(),
&mut resolved_events,
layout,
layouts,
node,
);
let events: Vec<_> = self
.queued_events
.replace(Vec::new())
.into_iter()
// these events were added in the update stage
.filter(|e| !["mousedown", "mouseup", "mousemove", "drag", "wheel"].contains(&e.0))
.map(|e| (e.0, e.1.into_any()))
.collect();
inner(&events, &mut resolved_events, node);
resolved_events
}
}
// translate crossterm events into dioxus events
fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> {
let (name, data): (&str, EventData) = match evt {
TermEvent::Key(k) => {
let key = translate_key_code(k.code)?;
(
"keydown",
// from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
EventData::Keyboard(KeyboardData {
char_code: key.raw_code(),
key: format!("{key:?}"),
key_code: key,
alt_key: k.modifiers.contains(KeyModifiers::ALT),
ctrl_key: k.modifiers.contains(KeyModifiers::CONTROL),
meta_key: false,
shift_key: k.modifiers.contains(KeyModifiers::SHIFT),
locale: Default::default(),
location: 0x00,
repeat: Default::default(),
which: Default::default(),
}),
)
}
TermEvent::Mouse(m) => {
let (x, y) = (m.column.into(), m.row.into());
let alt = m.modifiers.contains(KeyModifiers::ALT);
let shift = m.modifiers.contains(KeyModifiers::SHIFT);
let ctrl = m.modifiers.contains(KeyModifiers::CONTROL);
let meta = false;
let get_mouse_data = |b| {
let buttons = match b {
None => 0,
Some(MouseButton::Left) => 1,
Some(MouseButton::Right) => 2,
Some(MouseButton::Middle) => 4,
};
let button_state = match b {
None => 0,
Some(MouseButton::Left) => 0,
Some(MouseButton::Middle) => 1,
Some(MouseButton::Right) => 2,
};
// from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
EventData::Mouse(MouseData {
alt_key: alt,
button: button_state,
buttons,
client_x: x,
client_y: y,
ctrl_key: ctrl,
meta_key: meta,
page_x: x,
page_y: y,
screen_x: x,
screen_y: y,
shift_key: shift,
})
};
let get_wheel_data = |up| {
// from https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
EventData::Wheel(WheelData {
delta_mode: 0x01,
delta_x: 0.0,
delta_y: if up { -1.0 } else { 1.0 },
delta_z: 0.0,
})
};
match m.kind {
MouseEventKind::Down(b) => ("mousedown", get_mouse_data(Some(b))),
MouseEventKind::Up(b) => ("mouseup", get_mouse_data(Some(b))),
MouseEventKind::Drag(b) => ("drag", get_mouse_data(Some(b))),
MouseEventKind::Moved => ("mousemove", get_mouse_data(None)),
MouseEventKind::ScrollDown => ("wheel", get_wheel_data(false)),
MouseEventKind::ScrollUp => ("wheel", get_wheel_data(true)),
}
}
TermEvent::Resize(x, y) => ("resize", EventData::Screen((x, y))),
};
Some((name, data))
}
fn translate_key_code(c: TermKeyCode) -> Option<KeyCode> {
match c {
TermKeyCode::Backspace => Some(KeyCode::Backspace),
TermKeyCode::Enter => Some(KeyCode::Enter),
TermKeyCode::Left => Some(KeyCode::LeftArrow),
TermKeyCode::Right => Some(KeyCode::RightArrow),
TermKeyCode::Up => Some(KeyCode::UpArrow),
TermKeyCode::Down => Some(KeyCode::DownArrow),
TermKeyCode::Home => Some(KeyCode::Home),
TermKeyCode::End => Some(KeyCode::End),
TermKeyCode::PageUp => Some(KeyCode::PageUp),
TermKeyCode::PageDown => Some(KeyCode::PageDown),
TermKeyCode::Tab => Some(KeyCode::Tab),
TermKeyCode::BackTab => None,
TermKeyCode::Delete => Some(KeyCode::Delete),
TermKeyCode::Insert => Some(KeyCode::Insert),
TermKeyCode::F(fn_num) => match fn_num {
1 => Some(KeyCode::F1),
2 => Some(KeyCode::F2),
3 => Some(KeyCode::F3),
4 => Some(KeyCode::F4),
5 => Some(KeyCode::F5),
6 => Some(KeyCode::F6),
7 => Some(KeyCode::F7),
8 => Some(KeyCode::F8),
9 => Some(KeyCode::F9),
10 => Some(KeyCode::F10),
11 => Some(KeyCode::F11),
12 => Some(KeyCode::F12),
_ => None,
},
TermKeyCode::Char(c) => match c.to_uppercase().next().unwrap() {
'A' => Some(KeyCode::A),
'B' => Some(KeyCode::B),
'C' => Some(KeyCode::C),
'D' => Some(KeyCode::D),
'E' => Some(KeyCode::E),
'F' => Some(KeyCode::F),
'G' => Some(KeyCode::G),
'H' => Some(KeyCode::H),
'I' => Some(KeyCode::I),
'J' => Some(KeyCode::J),
'K' => Some(KeyCode::K),
'L' => Some(KeyCode::L),
'M' => Some(KeyCode::M),
'N' => Some(KeyCode::N),
'O' => Some(KeyCode::O),
'P' => Some(KeyCode::P),
'Q' => Some(KeyCode::Q),
'R' => Some(KeyCode::R),
'S' => Some(KeyCode::S),
'T' => Some(KeyCode::T),
'U' => Some(KeyCode::U),
'V' => Some(KeyCode::V),
'W' => Some(KeyCode::W),
'X' => Some(KeyCode::X),
'Y' => Some(KeyCode::Y),
'Z' => Some(KeyCode::Z),
_ => None,
},
TermKeyCode::Null => None,
TermKeyCode::Esc => Some(KeyCode::Escape),
}
}
fn clone_mouse_data(m: &MouseData) -> MouseData {
MouseData {
client_x: m.client_x,
client_y: m.client_y,
page_x: m.page_x,
page_y: m.page_y,
screen_x: m.screen_x,
screen_y: m.screen_y,
alt_key: m.alt_key,
ctrl_key: m.ctrl_key,
meta_key: m.meta_key,
shift_key: m.shift_key,
button: m.button,
buttons: m.buttons,
}
}
fn clone_keyboard_data(k: &KeyboardData) -> KeyboardData {
KeyboardData {
char_code: k.char_code,
key: k.key.clone(),
key_code: k.key_code,
alt_key: k.alt_key,
ctrl_key: k.ctrl_key,
meta_key: k.meta_key,
shift_key: k.shift_key,
locale: k.locale.clone(),
location: k.location,
repeat: k.repeat,
which: k.which,
}
}
fn clone_wheel_data(w: &WheelData) -> WheelData {
WheelData {
delta_mode: w.delta_mode,
delta_x: w.delta_x,
delta_y: w.delta_y,
delta_z: w.delta_x,
}
}
+112
View File
@@ -0,0 +1,112 @@
use dioxus::core::*;
use std::collections::HashMap;
use crate::{
attributes::{apply_attributes, StyleModifer},
style::RinkStyle,
TuiModifier, TuiNode,
};
/*
The layout system uses the lineheight as one point.
stretch uses fractional points, so we can rasterize if we need too, but not with characters
this means anything thats "1px" is 1 lineheight. Unfortunately, text cannot be smaller or bigger
*/
pub fn collect_layout<'a>(
layout: &mut stretch2::Stretch,
nodes: &mut HashMap<ElementId, TuiNode<'a>>,
vdom: &'a VirtualDom,
node: &'a VNode<'a>,
) {
use stretch2::prelude::*;
match node {
VNode::Text(t) => {
let id = t.id.get().unwrap();
let char_len = t.text.chars().count();
let style = Style {
size: Size {
// characters are 1 point tall
height: Dimension::Points(1.0),
// text is as long as it is declared
width: Dimension::Points(char_len as f32),
},
..Default::default()
};
nodes.insert(
id,
TuiNode {
node,
block_style: RinkStyle::default(),
tui_modifier: TuiModifier::default(),
layout: layout.new_node(style, &[]).unwrap(),
},
);
}
VNode::Element(el) => {
// gather up all the styles from the attribute list
let mut modifier = StyleModifer {
style: Style::default(),
tui_style: RinkStyle::default(),
tui_modifier: TuiModifier::default(),
};
for &Attribute { name, value, .. } in el.attributes {
apply_attributes(name, value, &mut modifier);
}
// Layout the children
for child in el.children {
collect_layout(layout, nodes, vdom, child);
}
// Set all direct nodes as our children
let mut child_layout = vec![];
for el in el.children {
let ite = ElementIdIterator::new(vdom, el);
for node in ite {
match node {
VNode::Element(_) | VNode::Text(_) => {
//
child_layout.push(nodes[&node.mounted_id()].layout)
}
VNode::Placeholder(_) => {}
VNode::Fragment(_) => todo!(),
VNode::Component(_) => todo!(),
}
// child_layout.push(nodes[&node.mounted_id()].layout)
}
}
nodes.insert(
node.mounted_id(),
TuiNode {
node,
block_style: modifier.tui_style,
tui_modifier: modifier.tui_modifier,
layout: layout.new_node(modifier.style, &child_layout).unwrap(),
},
);
}
VNode::Fragment(el) => {
//
for child in el.children {
collect_layout(layout, nodes, vdom, child);
}
}
VNode::Component(sc) => {
//
let scope = vdom.get_scope(sc.scope.get().unwrap()).unwrap();
let root = scope.root_node();
collect_layout(layout, nodes, vdom, root);
}
VNode::Placeholder(_) => {
//
}
};
}
+213
View File
@@ -0,0 +1,213 @@
use anyhow::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use dioxus::core::exports::futures_channel::mpsc::unbounded;
use dioxus::core::*;
use futures::{channel::mpsc::UnboundedSender, pin_mut, StreamExt};
use std::{
collections::HashMap,
io,
time::{Duration, Instant},
};
use stretch2::{prelude::Size, Stretch};
use style::RinkStyle;
use tui::{backend::CrosstermBackend, Terminal};
mod attributes;
mod config;
mod hooks;
mod layout;
mod render;
mod style;
mod widget;
pub use attributes::*;
pub use config::*;
pub use hooks::*;
pub use layout::*;
pub use render::*;
pub fn launch(app: Component<()>) {
launch_cfg(app, Config::default())
}
pub fn launch_cfg(app: Component<()>, cfg: Config) {
let mut dom = VirtualDom::new(app);
let (tx, rx) = unbounded();
let cx = dom.base_scope();
let (handler, state) = RinkInputHandler::new(rx, cx);
cx.provide_root_context(state);
dom.rebuild();
render_vdom(&mut dom, tx, handler, cfg).unwrap();
}
pub struct TuiNode<'a> {
pub layout: stretch2::node::Node,
pub block_style: RinkStyle,
pub tui_modifier: TuiModifier,
pub node: &'a VNode<'a>,
}
pub fn render_vdom(
vdom: &mut VirtualDom,
ctx: UnboundedSender<TermEvent>,
handler: RinkInputHandler,
cfg: Config,
) -> Result<()> {
// Setup input handling
let (tx, mut rx) = unbounded();
std::thread::spawn(move || {
let tick_rate = Duration::from_millis(100);
let mut last_tick = Instant::now();
loop {
// poll for tick rate duration, if no events, sent tick event.
let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout).unwrap() {
let evt = crossterm::event::read().unwrap();
tx.unbounded_send(InputEvent::UserInput(evt)).unwrap();
}
if last_tick.elapsed() >= tick_rate {
tx.unbounded_send(InputEvent::Tick).unwrap();
last_tick = Instant::now();
}
}
});
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(async {
/*
Get the terminal to calcualte the layout from
*/
enable_raw_mode().unwrap();
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap();
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend).unwrap();
terminal.clear().unwrap();
loop {
/*
-> collect all the nodes with their layout
-> solve their layout
-> resolve events
-> render the nodes in the right place with tui/crosstream
-> while rendering, apply styling
use simd to compare lines for diffing?
todo: reuse the layout and node objects.
our work_with_deadline method can tell us which nodes are dirty.
*/
let mut layout = Stretch::new();
let mut nodes = HashMap::new();
let root_node = vdom.base_scope().root_node();
layout::collect_layout(&mut layout, &mut nodes, vdom, root_node);
/*
Compute the layout given the terminal size
*/
let node_id = root_node.try_mounted_id().unwrap();
let root_layout = nodes[&node_id].layout;
let mut events = Vec::new();
terminal.draw(|frame| {
// size is guaranteed to not change when rendering
let dims = frame.size();
let width = dims.width;
let height = dims.height;
layout
.compute_layout(
root_layout,
Size {
width: stretch2::prelude::Number::Defined(width as f32),
height: stretch2::prelude::Number::Defined(height as f32),
},
)
.unwrap();
// resolve events before rendering
events = handler.get_events(vdom, &layout, &mut nodes, root_node);
render::render_vnode(
frame,
&layout,
&mut nodes,
vdom,
root_node,
&RinkStyle::default(),
cfg,
);
assert!(nodes.is_empty());
})?;
for e in events {
vdom.handle_message(SchedulerMsg::Event(e));
}
use futures::future::{select, Either};
{
let wait = vdom.wait_for_work();
pin_mut!(wait);
match select(wait, rx.next()).await {
Either::Left((_a, _b)) => {
//
}
Either::Right((evt, _o)) => {
match evt.as_ref().unwrap() {
InputEvent::UserInput(event) => match event {
TermEvent::Key(key) => {
if matches!(key.code, KeyCode::Char('c'))
&& key.modifiers.contains(KeyModifiers::CONTROL)
{
break;
}
}
TermEvent::Resize(_, _) | TermEvent::Mouse(_) => {}
},
InputEvent::Tick => {} // tick
InputEvent::Close => break,
};
if let InputEvent::UserInput(evt) = evt.unwrap() {
ctx.unbounded_send(evt).unwrap();
}
}
}
}
vdom.work_with_deadline(|| false);
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
})
}
enum InputEvent {
UserInput(TermEvent),
Close,
Tick,
}
+442
View File
@@ -0,0 +1,442 @@
use dioxus::core::*;
use std::{collections::HashMap, io::Stdout};
use stretch2::{
geometry::Point,
prelude::{Layout, Size},
Stretch,
};
use tui::{backend::CrosstermBackend, layout::Rect};
use crate::{
style::{RinkColor, RinkStyle},
widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext},
BorderEdge, BorderStyle, Config, TuiNode, UnitSystem,
};
const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
pub fn render_vnode<'a>(
frame: &mut tui::Frame<CrosstermBackend<Stdout>>,
layout: &Stretch,
layouts: &mut HashMap<ElementId, TuiNode<'a>>,
vdom: &'a VirtualDom,
node: &'a VNode<'a>,
// this holds the accumulated syle state for styled text rendering
style: &RinkStyle,
cfg: Config,
) {
match node {
VNode::Fragment(f) => {
for child in f.children {
render_vnode(frame, layout, layouts, vdom, child, style, cfg);
}
return;
}
VNode::Component(vcomp) => {
let idx = vcomp.scope.get().unwrap();
let new_node = vdom.get_scope(idx).unwrap().root_node();
render_vnode(frame, layout, layouts, vdom, new_node, style, cfg);
return;
}
VNode::Placeholder(_) => return,
VNode::Element(_) | VNode::Text(_) => {}
}
let id = node.try_mounted_id().unwrap();
let mut node = layouts.remove(&id).unwrap();
let Layout { location, size, .. } = layout.layout(node.layout).unwrap();
let Point { x, y } = location;
let Size { width, height } = size;
match node.node {
VNode::Text(t) => {
#[derive(Default)]
struct Label<'a> {
text: &'a str,
style: RinkStyle,
}
impl<'a> RinkWidget for Label<'a> {
fn render(self, area: Rect, mut buf: RinkBuffer) {
for (i, c) in self.text.char_indices() {
let mut new_cell = RinkCell::default();
new_cell.set_style(self.style);
new_cell.symbol = c.to_string();
buf.set(area.left() + i as u16, area.top(), &new_cell);
}
}
}
let label = Label {
text: t.text,
style: *style,
};
let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16);
// the renderer will panic if a node is rendered out of range even if the size is zero
if area.width > 0 && area.height > 0 {
frame.render_widget(WidgetWithContext::new(label, cfg), area);
}
}
VNode::Element(el) => {
let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16);
let mut new_style = node.block_style.merge(*style);
node.block_style = new_style;
// the renderer will panic if a node is rendered out of range even if the size is zero
if area.width > 0 && area.height > 0 {
frame.render_widget(WidgetWithContext::new(node, cfg), area);
}
// do not pass background color to children
new_style.bg = None;
for el in el.children {
render_vnode(frame, layout, layouts, vdom, el, &new_style, cfg);
}
}
VNode::Fragment(_) => todo!(),
VNode::Component(_) => todo!(),
VNode::Placeholder(_) => todo!(),
}
}
impl<'a> RinkWidget for TuiNode<'a> {
fn render(self, area: Rect, mut buf: RinkBuffer<'_>) {
use tui::symbols::line::*;
enum Direction {
Left,
Right,
Up,
Down,
}
fn draw(
buf: &mut RinkBuffer,
points_history: [[i32; 2]; 3],
symbols: &Set,
pos: [u16; 2],
color: &Option<RinkColor>,
) {
let [before, current, after] = points_history;
let start_dir = match [before[0] - current[0], before[1] - current[1]] {
[1, 0] => Direction::Right,
[-1, 0] => Direction::Left,
[0, 1] => Direction::Down,
[0, -1] => Direction::Up,
[a, b] => {
panic!(
"draw({:?} {:?} {:?}) {}, {} no cell adjacent",
before, current, after, a, b
)
}
};
let end_dir = match [after[0] - current[0], after[1] - current[1]] {
[1, 0] => Direction::Right,
[-1, 0] => Direction::Left,
[0, 1] => Direction::Down,
[0, -1] => Direction::Up,
[a, b] => {
panic!(
"draw({:?} {:?} {:?}) {}, {} no cell adjacent",
before, current, after, a, b
)
}
};
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = *c;
}
new_cell.symbol = match [start_dir, end_dir] {
[Direction::Down, Direction::Up] => symbols.vertical,
[Direction::Down, Direction::Right] => symbols.top_left,
[Direction::Down, Direction::Left] => symbols.top_right,
[Direction::Up, Direction::Down] => symbols.vertical,
[Direction::Up, Direction::Right] => symbols.bottom_left,
[Direction::Up, Direction::Left] => symbols.bottom_right,
[Direction::Right, Direction::Left] => symbols.horizontal,
[Direction::Right, Direction::Up] => symbols.bottom_left,
[Direction::Right, Direction::Down] => symbols.top_left,
[Direction::Left, Direction::Up] => symbols.bottom_right,
[Direction::Left, Direction::Right] => symbols.horizontal,
[Direction::Left, Direction::Down] => symbols.top_right,
_ => panic!(
"{:?} {:?} {:?} cannont connect cell to itself",
before, current, after
),
}
.to_string();
buf.set(
(current[0] + pos[0] as i32) as u16,
(current[1] + pos[1] as i32) as u16,
&new_cell,
);
}
fn draw_arc(
pos: [u16; 2],
starting_angle: f32,
arc_angle: f32,
radius: f32,
symbols: &Set,
buf: &mut RinkBuffer,
color: &Option<RinkColor>,
) {
if radius < 0.0 {
return;
}
let num_points = (radius * arc_angle) as i32;
let starting_point = [
(starting_angle.cos() * (radius * RADIUS_MULTIPLIER[0])) as i32,
(starting_angle.sin() * (radius * RADIUS_MULTIPLIER[1])) as i32,
];
// keep track of the last 3 point to allow filling diagonals
let mut points_history = [
[0, 0],
{
// change the x or y value based on which one is changing quicker
let ddx = -starting_angle.sin();
let ddy = starting_angle.cos();
if ddx.abs() > ddy.abs() {
[starting_point[0] - ddx.signum() as i32, starting_point[1]]
} else {
[starting_point[0], starting_point[1] - ddy.signum() as i32]
}
},
starting_point,
];
for i in 1..=num_points {
let angle = (i as f32 / num_points as f32) * arc_angle + starting_angle;
let x = angle.cos() * radius * RADIUS_MULTIPLIER[0];
let y = angle.sin() * radius * RADIUS_MULTIPLIER[1];
let new = [x as i32, y as i32];
if new != points_history[2] {
points_history = [points_history[1], points_history[2], new];
let dx = points_history[2][0] - points_history[1][0];
let dy = points_history[2][1] - points_history[1][1];
// fill diagonals
if dx != 0 && dy != 0 {
let connecting_point = match [dx, dy] {
[1, 1] => [points_history[1][0] + 1, points_history[1][1]],
[1, -1] => [points_history[1][0], points_history[1][1] - 1],
[-1, 1] => [points_history[1][0], points_history[1][1] + 1],
[-1, -1] => [points_history[1][0] - 1, points_history[1][1]],
_ => todo!(),
};
draw(
buf,
[points_history[0], points_history[1], connecting_point],
symbols,
pos,
color,
);
points_history = [points_history[1], connecting_point, points_history[2]];
}
draw(buf, points_history, symbols, pos, color);
}
}
points_history = [points_history[1], points_history[2], {
// change the x or y value based on which one is changing quicker
let ddx = -(starting_angle + arc_angle).sin();
let ddy = (starting_angle + arc_angle).cos();
if ddx.abs() > ddy.abs() {
[
points_history[2][0] + ddx.signum() as i32,
points_history[2][1],
]
} else {
[
points_history[2][0],
points_history[2][1] + ddy.signum() as i32,
]
}
}];
draw(buf, points_history, symbols, pos, color);
}
fn get_radius(border: &BorderEdge, area: Rect) -> f32 {
match border.style {
BorderStyle::HIDDEN => 0.0,
BorderStyle::NONE => 0.0,
_ => match border.radius {
UnitSystem::Percent(p) => p * area.width as f32 / 100.0,
UnitSystem::Point(p) => p,
}
.abs()
.min((area.width as f32 / RADIUS_MULTIPLIER[0]) / 2.0)
.min((area.height as f32 / RADIUS_MULTIPLIER[1]) / 2.0),
}
}
if area.area() == 0 {
return;
}
// todo: only render inside borders
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
let mut new_cell = RinkCell::default();
if let Some(c) = self.block_style.bg {
new_cell.bg = c;
}
buf.set(x, y, &new_cell);
}
}
let borders = self.tui_modifier.borders;
let last_edge = &borders.left;
let current_edge = &borders.top;
if let Some(symbols) = current_edge.style.symbol_set() {
// the radius for the curve between this line and the next
let r = get_radius(current_edge, area);
let radius = [
(r * RADIUS_MULTIPLIER[0]) as u16,
(r * RADIUS_MULTIPLIER[1]) as u16,
];
// the radius for the curve between this line and the last
let last_r = get_radius(last_edge, area);
let last_radius = [
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(self.block_style.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
}
for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) {
new_cell.symbol = symbols.horizontal.to_string();
buf.set(x, area.top(), &new_cell);
}
draw_arc(
[area.right() - radius[0] - 1, area.top() + radius[1]],
std::f32::consts::FRAC_PI_2 * 3.0,
std::f32::consts::FRAC_PI_2,
r,
&symbols,
&mut buf,
&color,
);
}
let last_edge = &borders.top;
let current_edge = &borders.right;
if let Some(symbols) = current_edge.style.symbol_set() {
// the radius for the curve between this line and the next
let r = get_radius(current_edge, area);
let radius = [
(r * RADIUS_MULTIPLIER[0]) as u16,
(r * RADIUS_MULTIPLIER[1]) as u16,
];
// the radius for the curve between this line and the last
let last_r = get_radius(last_edge, area);
let last_radius = [
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(self.block_style.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
}
for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) {
new_cell.symbol = symbols.vertical.to_string();
buf.set(area.right() - 1, y, &new_cell);
}
draw_arc(
[area.right() - radius[0] - 1, area.bottom() - radius[1] - 1],
0.0,
std::f32::consts::FRAC_PI_2,
r,
&symbols,
&mut buf,
&color,
);
}
let last_edge = &borders.right;
let current_edge = &borders.bottom;
if let Some(symbols) = current_edge.style.symbol_set() {
// the radius for the curve between this line and the next
let r = get_radius(current_edge, area);
let radius = [
(r * RADIUS_MULTIPLIER[0]) as u16,
(r * RADIUS_MULTIPLIER[1]) as u16,
];
// the radius for the curve between this line and the last
let last_r = get_radius(last_edge, area);
let last_radius = [
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(self.block_style.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
}
for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) {
new_cell.symbol = symbols.horizontal.to_string();
buf.set(x, area.bottom() - 1, &new_cell);
}
draw_arc(
[area.left() + radius[0], area.bottom() - radius[1] - 1],
std::f32::consts::FRAC_PI_2,
std::f32::consts::FRAC_PI_2,
r,
&symbols,
&mut buf,
&color,
);
}
let last_edge = &borders.bottom;
let current_edge = &borders.left;
if let Some(symbols) = current_edge.style.symbol_set() {
// the radius for the curve between this line and the next
let r = get_radius(current_edge, area);
let radius = [
(r * RADIUS_MULTIPLIER[0]) as u16,
(r * RADIUS_MULTIPLIER[1]) as u16,
];
// the radius for the curve between this line and the last
let last_r = get_radius(last_edge, area);
let last_radius = [
(last_r * RADIUS_MULTIPLIER[0]) as u16,
(last_r * RADIUS_MULTIPLIER[1]) as u16,
];
let color = current_edge.color.or(self.block_style.fg);
let mut new_cell = RinkCell::default();
if let Some(c) = color {
new_cell.fg = c;
}
for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) {
new_cell.symbol = symbols.vertical.to_string();
buf.set(area.left(), y, &new_cell);
}
draw_arc(
[area.left() + radius[0], area.top() + radius[1]],
std::f32::consts::PI,
std::f32::consts::FRAC_PI_2,
r,
&symbols,
&mut buf,
&color,
);
}
}
}
+447
View File
@@ -0,0 +1,447 @@
use std::{num::ParseFloatError, str::FromStr};
use tui::style::{Color, Modifier, Style};
use crate::RenderingMode;
#[derive(Clone, Copy, Debug)]
pub struct RinkColor {
pub color: Color,
pub alpha: f32,
}
impl Default for RinkColor {
fn default() -> Self {
Self {
color: Color::Black,
alpha: 0.0,
}
}
}
impl RinkColor {
pub fn blend(self, other: Color) -> Color {
if self.color == Color::Reset {
Color::Reset
} else if self.alpha == 0.0 {
other
} else {
let [sr, sg, sb] = to_rgb(self.color);
let [or, og, ob] = to_rgb(other);
let (sr, sg, sb, sa) = (
sr as f32 / 255.0,
sg as f32 / 255.0,
sb as f32 / 255.0,
self.alpha,
);
let (or, og, ob) = (or as f32 / 255.0, og as f32 / 255.0, ob as f32 / 255.0);
Color::Rgb(
(255.0 * (sr * sa + or * (1.0 - sa))) as u8,
(255.0 * (sg * sa + og * (1.0 - sa))) as u8,
(255.0 * (sb * sa + ob * (1.0 - sa))) as u8,
)
}
}
}
fn parse_value(
v: &str,
current_max_output: f32,
required_max_output: f32,
) -> Result<f32, ParseFloatError> {
if let Some(stripped) = v.strip_suffix('%') {
Ok((stripped.trim().parse::<f32>()? / 100.0) * required_max_output)
} else {
Ok((v.trim().parse::<f32>()? / current_max_output) * required_max_output)
}
}
pub struct ParseColorError;
fn parse_hex(color: &str) -> Result<Color, ParseColorError> {
let mut values = [0, 0, 0];
let mut color_ok = true;
for i in 0..values.len() {
if let Ok(v) = u8::from_str_radix(&color[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) {
values[i] = v;
} else {
color_ok = false;
}
}
if color_ok {
Ok(Color::Rgb(values[0], values[1], values[2]))
} else {
Err(ParseColorError)
}
}
fn parse_rgb(color: &str) -> Result<Color, ParseColorError> {
let mut values = [0, 0, 0];
let mut color_ok = true;
for (v, i) in color.split(',').zip(0..values.len()) {
if let Ok(v) = parse_value(v.trim(), 255.0, 255.0) {
values[i] = v as u8;
} else {
color_ok = false;
}
}
if color_ok {
Ok(Color::Rgb(values[0], values[1], values[2]))
} else {
Err(ParseColorError)
}
}
fn parse_hsl(color: &str) -> Result<Color, ParseColorError> {
let mut values = [0.0, 0.0, 0.0];
let mut color_ok = true;
for (v, i) in color.split(',').zip(0..values.len()) {
if let Ok(v) = parse_value(v.trim(), if i == 0 { 360.0 } else { 100.0 }, 1.0) {
values[i] = v;
} else {
color_ok = false;
}
}
if color_ok {
let [h, s, l] = values;
let rgb = if s == 0.0 {
[l as u8; 3]
} else {
fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
if t < 0.0 {
t += 1.0;
}
if t > 1.0 {
t -= 1.0;
}
if t < 1.0 / 6.0 {
p + (q - p) * 6.0 * t
} else if t < 1.0 / 2.0 {
q
} else if t < 2.0 / 3.0 {
p + (q - p) * (2.0 / 3.0 - t) * 6.0
} else {
p
}
}
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - l * s
};
let p = 2.0 * l - q;
[
(hue_to_rgb(p, q, h + 1.0 / 3.0) * 255.0) as u8,
(hue_to_rgb(p, q, h) * 255.0) as u8,
(hue_to_rgb(p, q, h - 1.0 / 3.0) * 255.0) as u8,
]
};
Ok(Color::Rgb(rgb[0], rgb[1], rgb[2]))
} else {
Err(ParseColorError)
}
}
impl FromStr for RinkColor {
type Err = ParseColorError;
fn from_str(color: &str) -> Result<Self, Self::Err> {
match color {
"red" => Ok(RinkColor {
color: Color::Red,
alpha: 1.0,
}),
"black" => Ok(RinkColor {
color: Color::Black,
alpha: 1.0,
}),
"green" => Ok(RinkColor {
color: Color::Green,
alpha: 1.0,
}),
"yellow" => Ok(RinkColor {
color: Color::Yellow,
alpha: 1.0,
}),
"blue" => Ok(RinkColor {
color: Color::Blue,
alpha: 1.0,
}),
"magenta" => Ok(RinkColor {
color: Color::Magenta,
alpha: 1.0,
}),
"cyan" => Ok(RinkColor {
color: Color::Cyan,
alpha: 1.0,
}),
"gray" => Ok(RinkColor {
color: Color::Gray,
alpha: 1.0,
}),
"darkgray" => Ok(RinkColor {
color: Color::DarkGray,
alpha: 1.0,
}),
// light red does not exist
"orangered" => Ok(RinkColor {
color: Color::LightRed,
alpha: 1.0,
}),
"lightgreen" => Ok(RinkColor {
color: Color::LightGreen,
alpha: 1.0,
}),
"lightyellow" => Ok(RinkColor {
color: Color::LightYellow,
alpha: 1.0,
}),
"lightblue" => Ok(RinkColor {
color: Color::LightBlue,
alpha: 1.0,
}),
// light magenta does not exist
"orchid" => Ok(RinkColor {
color: Color::LightMagenta,
alpha: 1.0,
}),
"lightcyan" => Ok(RinkColor {
color: Color::LightCyan,
alpha: 1.0,
}),
"white" => Ok(RinkColor {
color: Color::White,
alpha: 1.0,
}),
_ => {
if color.len() == 7 && color.starts_with('#') {
parse_hex(color).map(|c| RinkColor {
color: c,
alpha: 1.0,
})
} else if let Some(stripped) = color.strip_prefix("rgb(") {
let color_values = stripped.trim_end_matches(')');
if color.matches(',').count() == 3 {
let (alpha, rgb_values) =
color_values.rsplit_once(',').ok_or(ParseColorError)?;
if let Ok(a) = alpha.parse() {
parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a })
} else {
Err(ParseColorError)
}
} else {
parse_rgb(color_values).map(|c| RinkColor {
color: c,
alpha: 1.0,
})
}
} else if let Some(stripped) = color.strip_prefix("rgba(") {
let color_values = stripped.trim_end_matches(')');
if color.matches(',').count() == 3 {
let (rgb_values, alpha) =
color_values.rsplit_once(',').ok_or(ParseColorError)?;
if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
parse_rgb(rgb_values).map(|c| RinkColor { color: c, alpha: a })
} else {
Err(ParseColorError)
}
} else {
parse_rgb(color_values).map(|c| RinkColor {
color: c,
alpha: 1.0,
})
}
} else if let Some(stripped) = color.strip_prefix("hsl(") {
let color_values = stripped.trim_end_matches(')');
if color.matches(',').count() == 3 {
let (rgb_values, alpha) =
color_values.rsplit_once(',').ok_or(ParseColorError)?;
if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
parse_hsl(rgb_values).map(|c| RinkColor { color: c, alpha: a })
} else {
Err(ParseColorError)
}
} else {
parse_hsl(color_values).map(|c| RinkColor {
color: c,
alpha: 1.0,
})
}
} else if let Some(stripped) = color.strip_prefix("hsla(") {
let color_values = stripped.trim_end_matches(')');
if color.matches(',').count() == 3 {
let (rgb_values, alpha) =
color_values.rsplit_once(',').ok_or(ParseColorError)?;
if let Ok(a) = parse_value(alpha, 1.0, 1.0) {
parse_hsl(rgb_values).map(|c| RinkColor { color: c, alpha: a })
} else {
Err(ParseColorError)
}
} else {
parse_hsl(color_values).map(|c| RinkColor {
color: c,
alpha: 1.0,
})
}
} else {
Err(ParseColorError)
}
}
}
}
}
fn to_rgb(c: Color) -> [u8; 3] {
match c {
Color::Black => [0, 0, 0],
Color::Red => [255, 0, 0],
Color::Green => [0, 128, 0],
Color::Yellow => [255, 255, 0],
Color::Blue => [0, 0, 255],
Color::Magenta => [255, 0, 255],
Color::Cyan => [0, 255, 255],
Color::Gray => [128, 128, 128],
Color::DarkGray => [169, 169, 169],
Color::LightRed => [255, 69, 0],
Color::LightGreen => [144, 238, 144],
Color::LightYellow => [255, 255, 224],
Color::LightBlue => [173, 216, 230],
Color::LightMagenta => [218, 112, 214],
Color::LightCyan => [224, 255, 255],
Color::White => [255, 255, 255],
Color::Rgb(r, g, b) => [r, g, b],
Color::Indexed(idx) => match idx {
16..=231 => {
let v = idx - 16;
// add 3 to round up
let r = ((v as u16 / 36) * 255 + 3) / 5;
let g = (((v as u16 % 36) / 6) * 255 + 3) / 5;
let b = ((v as u16 % 6) * 255 + 3) / 5;
[r as u8, g as u8, b as u8]
}
232..=255 => {
let l = (idx - 232) / 24;
[l; 3]
}
// rink will never generate these colors, but they might be on the screen from another program
_ => [0, 0, 0],
},
Color::Reset => [0, 0, 0],
}
}
pub fn convert(mode: RenderingMode, c: Color) -> Color {
if let Color::Reset = c {
c
} else {
match mode {
crate::RenderingMode::BaseColors => match c {
Color::Rgb(_, _, _) => panic!("cannot convert rgb color to base color"),
Color::Indexed(_) => panic!("cannot convert Ansi color to base color"),
_ => c,
},
crate::RenderingMode::Rgb => {
let rgb = to_rgb(c);
Color::Rgb(rgb[0], rgb[1], rgb[2])
}
crate::RenderingMode::Ansi => match c {
Color::Indexed(_) => c,
_ => {
let rgb = to_rgb(c);
// 16-231: 6 × 6 × 6 color cube
// 232-255: 23 step grayscale
if rgb[0] == rgb[1] && rgb[1] == rgb[2] {
let idx = 232 + (rgb[0] as u16 * 23 / 255) as u8;
Color::Indexed(idx)
} else {
let r = (rgb[0] as u16 * 5) / 255;
let g = (rgb[1] as u16 * 5) / 255;
let b = (rgb[2] as u16 * 5) / 255;
let idx = 16 + r * 36 + g * 6 + b;
Color::Indexed(idx as u8)
}
}
},
}
}
}
#[test]
fn rgb_to_ansi() {
for idx in 17..=231 {
let idxed = Color::Indexed(idx);
let rgb = to_rgb(idxed);
// gray scale colors have two equivelent repersentations
let color = Color::Rgb(rgb[0], rgb[1], rgb[2]);
let converted = convert(RenderingMode::Ansi, color);
if let Color::Indexed(i) = converted {
if rgb[0] != rgb[1] || rgb[1] != rgb[2] {
assert_eq!(idxed, converted);
} else {
assert!(i >= 232);
}
} else {
panic!("color is not indexed")
}
}
for idx in 232..=255 {
let idxed = Color::Indexed(idx);
let rgb = to_rgb(idxed);
assert!(rgb[0] == rgb[1] && rgb[1] == rgb[2]);
}
}
#[derive(Clone, Copy)]
pub struct RinkStyle {
pub fg: Option<RinkColor>,
pub bg: Option<RinkColor>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
impl Default for RinkStyle {
fn default() -> Self {
Self {
fg: Some(RinkColor {
color: Color::White,
alpha: 1.0,
}),
bg: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
}
}
impl RinkStyle {
pub fn add_modifier(mut self, m: Modifier) -> Self {
self.sub_modifier.remove(m);
self.add_modifier.insert(m);
self
}
pub fn remove_modifier(mut self, m: Modifier) -> Self {
self.add_modifier.remove(m);
self.sub_modifier.insert(m);
self
}
pub fn merge(mut self, other: RinkStyle) -> Self {
self.fg = self.fg.or(other.fg);
self.add_modifier(other.add_modifier)
.remove_modifier(other.sub_modifier)
}
}
impl Into<Style> for RinkStyle {
fn into(self) -> Style {
Style {
fg: self.fg.map(|c| c.color),
bg: self.bg.map(|c| c.color),
add_modifier: self.add_modifier,
sub_modifier: self.sub_modifier,
}
}
}
+96
View File
@@ -0,0 +1,96 @@
use tui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier},
widgets::Widget,
};
use crate::{
style::{convert, RinkColor, RinkStyle},
Config,
};
pub struct RinkBuffer<'a> {
buf: &'a mut Buffer,
cfg: Config,
}
impl<'a> RinkBuffer<'a> {
fn new(buf: &'a mut Buffer, cfg: Config) -> RinkBuffer<'a> {
Self { buf, cfg }
}
pub fn set(&mut self, x: u16, y: u16, new: &RinkCell) {
let mut cell = self.buf.get_mut(x, y);
cell.bg = convert(self.cfg.rendering_mode, new.bg.blend(cell.bg));
if new.symbol.is_empty() {
if !cell.symbol.is_empty() {
// allows text to "shine through" transparent backgrounds
cell.fg = convert(self.cfg.rendering_mode, new.bg.blend(cell.fg));
}
} else {
cell.modifier = new.modifier;
cell.symbol = new.symbol.clone();
cell.fg = convert(self.cfg.rendering_mode, new.fg.blend(cell.bg));
}
}
}
pub trait RinkWidget {
fn render(self, area: Rect, buf: RinkBuffer);
}
pub struct WidgetWithContext<T: RinkWidget> {
widget: T,
config: Config,
}
impl<T: RinkWidget> WidgetWithContext<T> {
pub fn new(widget: T, config: Config) -> WidgetWithContext<T> {
WidgetWithContext { widget, config }
}
}
impl<T: RinkWidget> Widget for WidgetWithContext<T> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.widget.render(area, RinkBuffer::new(buf, self.config))
}
}
#[derive(Clone)]
pub struct RinkCell {
pub symbol: String,
pub bg: RinkColor,
pub fg: RinkColor,
pub modifier: Modifier,
}
impl Default for RinkCell {
fn default() -> Self {
Self {
symbol: "".to_string(),
fg: RinkColor {
color: Color::Rgb(0, 0, 0),
alpha: 0.0,
},
bg: RinkColor {
color: Color::Rgb(0, 0, 0),
alpha: 0.0,
},
modifier: Modifier::empty(),
}
}
}
impl RinkCell {
pub fn set_style(&mut self, style: RinkStyle) {
if let Some(c) = style.fg {
self.fg = c;
}
if let Some(c) = style.bg {
self.bg = c;
}
self.modifier = style.add_modifier;
self.modifier.remove(style.sub_modifier);
}
}
+73
View File
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style>
html,
body {
height: 100%;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: black;
/* justify-content: center;
align-items: center; */
/* margin: auto; */
}
.smaller {
height: 70%;
width: 70%;
background-color: green;
/* justify-content: center; */
/* align-items: center; */
}
.superinner {
height: 100%;
width: 100%;
/* display: flex; */
/* */
margin-top: 20px;
margin-bottom: 20px;
margin-left: 20px;
margin-right: 20px;
/* */
background-color: red;
justify-content: center;
align-items: center;
flex-direction: column;
/* margin: 20px; */
/* margin: 20px; */
}
</style>
</head>
<body>
<div class="container">
<div class="smaller">
<div class="superinner">
<h1>Hello World</h1>
<p>This is a test</p>
</div>
</div>
</div>
<!-- <div class="container">
<div class="smaller">
hello world
<div style="color: green; margin: 40px;">
goodbye
<div style="color:red;">
asdasdasd
</div>
</div>
</div>
</div> -->
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
use stretch2 as stretch;
#[test]
fn margin_and_flex_row() {
let mut stretch = stretch::Stretch::new();
let node0 = stretch
.new_node(
stretch::style::Style {
flex_grow: 1f32,
margin: stretch::geometry::Rect {
start: stretch::style::Dimension::Points(10f32),
end: stretch::style::Dimension::Points(10f32),
..Default::default()
},
..Default::default()
},
&[],
)
.unwrap();
let node = stretch
.new_node(
stretch::style::Style {
size: stretch::geometry::Size {
width: stretch::style::Dimension::Points(100f32),
height: stretch::style::Dimension::Points(100f32),
..Default::default()
},
..Default::default()
},
&[node0],
)
.unwrap();
stretch
.compute_layout(node, stretch::geometry::Size::undefined())
.unwrap();
assert_eq!(stretch.layout(node).unwrap().size.width, 100f32);
assert_eq!(stretch.layout(node).unwrap().size.height, 100f32);
assert_eq!(stretch.layout(node).unwrap().location.x, 0f32);
assert_eq!(stretch.layout(node).unwrap().location.y, 0f32);
assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32);
assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32);
assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32);
assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32);
}
#[test]
fn margin_and_flex_row2() {
let mut stretch = stretch::Stretch::new();
let node0 = stretch
.new_node(
stretch::style::Style {
flex_grow: 1f32,
margin: stretch::geometry::Rect {
// left
start: stretch::style::Dimension::Points(10f32),
// right?
end: stretch::style::Dimension::Points(10f32),
// top?
// top: stretch::style::Dimension::Points(10f32),
// bottom?
// bottom: stretch::style::Dimension::Points(10f32),
..Default::default()
},
..Default::default()
},
&[],
)
.unwrap();
let node = stretch
.new_node(
stretch::style::Style {
size: stretch::geometry::Size {
width: stretch::style::Dimension::Points(100f32),
height: stretch::style::Dimension::Points(100f32),
..Default::default()
},
..Default::default()
},
&[node0],
)
.unwrap();
stretch
.compute_layout(node, stretch::geometry::Size::undefined())
.unwrap();
assert_eq!(stretch.layout(node).unwrap().size.width, 100f32);
assert_eq!(stretch.layout(node).unwrap().size.height, 100f32);
assert_eq!(stretch.layout(node).unwrap().location.x, 0f32);
assert_eq!(stretch.layout(node).unwrap().location.y, 0f32);
dbg!(stretch.layout(node0));
// assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32);
// assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32);
// assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32);
// assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32);
}