From e3171e8303700d6829880951dbc7184413df371b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 28 Jul 2021 10:52:38 -0400 Subject: [PATCH 01/32] wip: first pass --- .gitignore | 2 + .vscode/settings.json | 3 + .vscode/spellright.dict | 2 + Cargo.toml | 17 +++ README.md | 43 +++++++ src/lib.rs | 270 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 337 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 .vscode/spellright.dict create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..96ef6c0b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..1c68f97cc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.inlayHints.enable": true +} diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict new file mode 100644 index 000000000..c30e473fc --- /dev/null +++ b/.vscode/spellright.dict @@ -0,0 +1,2 @@ +esque +Tui diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..19b62bf5b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[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] +stretch = "0.3.2" +tui = { version = "0.15.0", features = ["crossterm"] } +crossterm = "0.20.0" +anyhow = "1.0.42" +thiserror = "1.0.24" +dioxus = { path = "../../dioxus", features = ["core"] } +hecs = "0.6.0" +tui-template = { path = "../../Tinkering/tui-builder" } +# tui-template = { git = "https://github.com/jkelleyrtp/tui-builder.git" } diff --git a/README.md b/README.md new file mode 100644 index 000000000..27a4dbc3f --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Rink: Like "Ink" but for Rust and Dioxus + +The fastest portable TUIs in the west + 🔫🤠🔫 + 🐎🔥🔥🔥 + +Rink lets you build terminal user interfaces in Rust with Dioxus. + +You can use html-esque semantics with stylesheets, inline styles, tree hierarchy, components, etc, but your Tui app is probably not going to work well or look good in the web. It still technically is a limited subset of HTML, so use at your own risk. + +```rust + +static App: FC<()> = |cx| { + cx.render(rsx!{ + div { width: "100%", height: "3px", border_style: "solid", + h1 { "Hello world!" } + p { "This is a paragraph." } + } + }) +} +``` + +an image should go here + + +Rink is basically a port of [Ink]() but for Rust and Dioxus. Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful. + +## Features + +Rink features: +- Flexbox based layout system +- CSS selectors +- inline css support +- Built-in focusing system +- high-quality keyboard support +- Support for events, hooks, and callbacks +- support for a very limited subset of HTML and CSS primitives + +HTML elements supported: +- div, h1-h6, p, input, textarea, tables, nav + +CSS Supported: +- Flex diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 000000000..ad6dc8596 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,270 @@ +use anyhow::Result; +use dioxus::core::*; +use tui::backend::CrosstermBackend; +use tui::layout::{Layout, Rect}; +use tui::style::{Modifier, Style as TuiStyle}; +use tui::text::{Span, Spans}; +use tui::widgets::{Block, Paragraph}; +use tui_template::tuiapp::CrosstermFrame; +struct RinkDom { + vdom: VirtualDom, +} + +impl RinkDom { + pub fn new(app: FC<()>) -> Self { + Self { + vdom: VirtualDom::new(app), + } + } + + fn render_vnode<'a>( + &self, + f: &mut CrosstermFrame, + node: &'a VNode<'a>, + state: &mut RenderState<'a>, + ) -> Rect { + match &node.kind { + VNodeKind::Fragment(_) => todo!(), + VNodeKind::Component(_) => todo!(), + VNodeKind::Suspended { node } => todo!(), + + VNodeKind::Text(te) => { + let span = Span::styled(te.text, TuiStyle::default()); + + let mut m = Modifier::empty(); + + for style in &state.current_styles { + match style { + Styles::Bold => m = m | Modifier::BOLD, + Styles::Italic => m = m | Modifier::ITALIC, + Styles::Strikethrough => m = m | Modifier::CROSSED_OUT, + Styles::Emphasis => m = m | Modifier::ITALIC, + Styles::Underline => m = m | Modifier::UNDERLINED, + } + } + + let style = TuiStyle::default().add_modifier(m); + let span = span.styled_graphemes(style); + let cur_block = state.block_stack.last_mut().unwrap(); + + // Paragraph + + // f.render_widget(span); + } + VNodeKind::Element(el) => { + // + let mut new_layout = false; + + // all of our supported styles + + match el.tag_name { + // obviously semantically not really correct + "div" => { + state.layouts.push(Layout::default()); + new_layout = true; + } + + "title" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => { + let mut block = state.block_stack.pop().unwrap(); + let children = el.children; + + if let (1, Some(VNodeKind::Text(te))) = + (children.len(), children.get(0).map(|f| &f.kind)) + { + block = block.title(vec![Span::from(te.text)]); + } + + state.block_stack.push(block); + } + + "span" | "header" => {} + + "footer" => {} + + "p" => { + state.layouts.push(Layout::default()); + new_layout = true; + } + + // elements that style for whatever reason + "em" => state.current_styles.push(Styles::Emphasis), + "i" => state.current_styles.push(Styles::Italic), + "b" => state.current_styles.push(Styles::Bold), + "u" => state.current_styles.push(Styles::Underline), + "strike" => state.current_styles.push(Styles::Strikethrough), + + "li" => {} + "ul" => {} + "ol" => {} + "code" => {} + "hr" => {} + + // Tables + "table" => {} + "tr" => {} + "th" => {} + "td" => {} + + // Inputs + "input" => {} + "label" => {} + + _ => {} + } + + let cur_layout = state.layouts.last_mut().unwrap(); + let cur_block = state.block_stack.last_mut().unwrap(); + let mut cur_style = TuiStyle::default(); + + for attr in el.attributes { + if attr.namespace == Some("style") { + match attr.name { + "width" => {} + "height" => {} + + "background" => { + // + // cur_style.bg + // cur_block.style() + } + "background-color" => {} + + "border" => {} + "border-bottom" => {} + "border-bottom-color" => {} + "border-bottom-style" => {} + "border-bottom-width" => {} + "border-color" => {} + "border-left" => {} + "border-left-color" => {} + "border-left-style" => {} + "border-left-width" => {} + "border-right" => {} + "border-right-color" => {} + "border-right-style" => {} + "border-right-width" => {} + "border-style" => {} + "border-top" => {} + "border-top-color" => {} + "border-top-style" => {} + "border-top-width" => {} + "border-width" => {} + + "clear" => {} + "clip" => {} + "color" => {} + "cursor" => {} + "display" => {} + "filter" => {} + "float" => {} + "font" => {} + "font-family" => {} + "font-size" => {} + "font-variant" => {} + "font-weight" => {} + + "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" => {} + "overflow" => {} + "padding" => {} + "padding-bottom" => {} + "padding-left" => {} + "padding-right" => {} + "padding-top" => {} + "position" => {} + "stroke-dasharray" => {} + "stroke-dashoffset" => {} + "text-align" => {} + "text-decoration" => {} + "text-indent" => {} + "text-transform" => {} + "top" => {} + "vertical-align" => {} + "visibility" => {} + "z-index" => {} + + "page-break-after" + | "page-break-before" + | "background-position" + | "background-attachment" + | "background-image" + | "background-repeat" + | _ => {} + } + } + } + + for child in el.children {} + } + } + Rect::new(0, 0, 0, 0) + } + + fn render_text(&self, f: &mut CrosstermFrame, node: &VNode) {} + + fn render_fragment(&self, f: &mut CrosstermFrame) {} +} + +impl<'a> tui_template::tuiapp::TuiApp for RinkDom { + fn render(&mut self, frame: &mut CrosstermFrame) { + let base_scope = self.vdom.base_scope(); + let root = base_scope.root(); + + let mut render_state = RenderState::new(); + self.render_vnode(frame, root, &mut render_state); + } + + fn event_handler(&self, action: tui_template::crossterm::event::Event) -> Result<()> { + todo!() + } + + fn handle_key(&mut self, key: tui_template::crossterm::event::KeyEvent) { + todo!() + } + + fn tick(&mut self) { + todo!() + } + + fn should_quit(&self) -> bool { + todo!() + } +} +struct RenderState<'a> { + block_stack: Vec>, + + layouts: Vec, + + /// All the current styles applied through the "style" tag + current_styles: Vec, +} + +// I don't think we can do any of these? +enum Styles { + Bold, + Italic, + Strikethrough, + Emphasis, + Underline, +} + +impl<'a> RenderState<'a> { + fn new() -> Self { + let block_stack = Vec::new(); + Self { + block_stack, + current_styles: Vec::new(), + layouts: Vec::new(), + } + } +} From dc34805ee619fedaaa628a7b2cf543df047046f9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 31 Dec 2021 23:53:37 -0500 Subject: [PATCH 02/32] feat: wire up stretch into place --- .vscode/settings.json | 3 - Cargo.toml | 15 +- examples/frame.rs | 85 ++++++ examples/layouts.rs | 53 ++++ examples/list.rs | 31 +++ examples/quadrants.rs | 63 +++++ examples/strecher.rs | 34 +++ examples/text.rs | 85 ++++++ src/attributes.rs | 610 ++++++++++++++++++++++++++++++++++++++++++ src/layout.rs | 88 ++++++ src/lib.rs | 376 +++++++++----------------- src/render.rs | 97 +++++++ test.html | 45 ++++ tests/margin.rs | 102 +++++++ 14 files changed, 1430 insertions(+), 257 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 examples/frame.rs create mode 100644 examples/layouts.rs create mode 100644 examples/list.rs create mode 100644 examples/quadrants.rs create mode 100644 examples/strecher.rs create mode 100644 examples/text.rs create mode 100644 src/attributes.rs create mode 100644 src/layout.rs create mode 100644 src/render.rs create mode 100644 test.html create mode 100644 tests/margin.rs diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 1c68f97cc..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rust-analyzer.inlayHints.enable": true -} diff --git a/Cargo.toml b/Cargo.toml index 19b62bf5b..7672c10d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,12 +6,13 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -stretch = "0.3.2" -tui = { version = "0.15.0", features = ["crossterm"] } -crossterm = "0.20.0" +tui = { version = "0.16.0", features = ["crossterm"] } +crossterm = "0.22.1" anyhow = "1.0.42" thiserror = "1.0.24" -dioxus = { path = "../../dioxus", features = ["core"] } -hecs = "0.6.0" -tui-template = { path = "../../Tinkering/tui-builder" } -# tui-template = { git = "https://github.com/jkelleyrtp/tui-builder.git" } +dioxus = { path = "../../dioxus" } +hecs = "0.7.3" +ctrlc = "3.2.1" +bumpalo = { version = "3.8.0", features = ["boxed"] } +stretch2 = { path = "../../Tinkering/stretch2" } +# stretch2 = { git = "https://github.com/elbaro/stretch2.git" } diff --git a/examples/frame.rs b/examples/frame.rs new file mode 100644 index 000000000..3b913b9c9 --- /dev/null +++ b/examples/frame.rs @@ -0,0 +1,85 @@ +use dioxus::prelude::*; + +fn main() { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + + rink::render_vdom(&dom).unwrap(); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + // justify_content: "center", + // align_items: "center", + // flex_direction: "row", + // background_color: "red", + + p { + background_color: "black", + flex_direction: "column", + justify_content: "center", + align_items: "center", + // height: "10%", + "hi" + "hi" + "hi" + } + + li { + background_color: "red", + flex_direction: "column", + justify_content: "center", + align_items: "center", + // height: "10%", + "bib" + "bib" + "bib" + "bib" + "bib" + "bib" + "bib" + "bib" + } + li { + background_color: "blue", + flex_direction: "column", + justify_content: "center", + align_items: "center", + // height: "10%", + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + } + p { + background_color: "yellow", + "asd" + } + p { + background_color: "green", + "asd" + } + p { + background_color: "white", + "asd" + } + p { + background_color: "cyan", + "asd" + } + } + }) +} diff --git a/examples/layouts.rs b/examples/layouts.rs new file mode 100644 index 000000000..e4278c2b1 --- /dev/null +++ b/examples/layouts.rs @@ -0,0 +1,53 @@ +use std::collections::HashMap; + +use dioxus::{core::ElementId, prelude::*}; +use rink::TuiNode; + +fn main() { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + + let mut layout = stretch2::Stretch::new(); + let mut nodes = HashMap::new(); + rink::collect_layout(&mut layout, &mut nodes, &dom, dom.base_scope().root_node()); + + let node = nodes + .remove(&dom.base_scope().root_node().mounted_id()) + .unwrap(); + + layout + .compute_layout(node.layout, stretch2::geometry::Size::undefined()) + .unwrap(); + + for (id, node) in nodes.drain() { + println!("{:?}", layout.layout(node.layout)); + } +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + + div { + "hi" + } + div { + "bi" + "bi" + } + } + }) +} + +// fn print_layout(mut nodes: HashMap, node: &VNode) { +// match node { +// VNode::Text(_) => todo!(), +// VNode::Element(_) => todo!(), +// VNode::Fragment(_) => todo!(), +// VNode::Component(_) => todo!(), +// VNode::Placeholder(_) => todo!(), +// } +// } diff --git a/examples/list.rs b/examples/list.rs new file mode 100644 index 000000000..ebdb68e3c --- /dev/null +++ b/examples/list.rs @@ -0,0 +1,31 @@ +use dioxus::prelude::*; + +fn main() { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + + rink::render_vdom(&dom).unwrap(); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + border_width: "1px", + + h1 { height: "2px", color: "green", + "that's awesome!" + } + + ul { + flex_direction: "column", + padding_left: "3px", + (0..10).map(|i| rsx!( + "> hello {i}" + )) + } + } + }) +} diff --git a/examples/quadrants.rs b/examples/quadrants.rs new file mode 100644 index 000000000..88138eeb7 --- /dev/null +++ b/examples/quadrants.rs @@ -0,0 +1,63 @@ +use dioxus::prelude::*; + +fn main() { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + + rink::render_vdom(&dom).unwrap(); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + + div { + width: "100%", + height: "50%", + flex_direction: "row", + div { + border_width: "1px", + width: "50%", + height: "100%", + background_color: "red", + justify_content: "center", + align_items: "center", + "[A]" + } + div { + width: "50%", + height: "100%", + background_color: "black", + justify_content: "center", + align_items: "center", + "[B]" + } + } + + div { + width: "100%", + height: "50%", + flex_direction: "row", + div { + width: "50%", + height: "100%", + background_color: "green", + justify_content: "center", + align_items: "center", + "[C]" + } + div { + width: "50%", + height: "100%", + background_color: "blue", + justify_content: "center", + align_items: "center", + "[D]" + } + } + } + }) +} diff --git a/examples/strecher.rs b/examples/strecher.rs new file mode 100644 index 000000000..b1fa93050 --- /dev/null +++ b/examples/strecher.rs @@ -0,0 +1,34 @@ +use stretch2::prelude::*; + +fn main() -> Result<(), Error> { + let mut stretch = Stretch::new(); + + let child = stretch.new_node( + Style { + size: Size { + width: Dimension::Percent(0.5), + height: Dimension::Auto, + }, + ..Default::default() + }, + &[], + )?; + + let node = stretch.new_node( + Style { + size: Size { + width: Dimension::Points(100.0), + height: Dimension::Points(100.0), + }, + justify_content: JustifyContent::Center, + ..Default::default() + }, + &[child], + )?; + + stretch.compute_layout(node, Size::undefined())?; + println!("node: {:#?}", stretch.layout(node)?); + println!("child: {:#?}", stretch.layout(child)?); + + Ok(()) +} diff --git a/examples/text.rs b/examples/text.rs new file mode 100644 index 000000000..3b913b9c9 --- /dev/null +++ b/examples/text.rs @@ -0,0 +1,85 @@ +use dioxus::prelude::*; + +fn main() { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + + rink::render_vdom(&dom).unwrap(); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + // justify_content: "center", + // align_items: "center", + // flex_direction: "row", + // background_color: "red", + + p { + background_color: "black", + flex_direction: "column", + justify_content: "center", + align_items: "center", + // height: "10%", + "hi" + "hi" + "hi" + } + + li { + background_color: "red", + flex_direction: "column", + justify_content: "center", + align_items: "center", + // height: "10%", + "bib" + "bib" + "bib" + "bib" + "bib" + "bib" + "bib" + "bib" + } + li { + background_color: "blue", + flex_direction: "column", + justify_content: "center", + align_items: "center", + // height: "10%", + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + "zib" + } + p { + background_color: "yellow", + "asd" + } + p { + background_color: "green", + "asd" + } + p { + background_color: "white", + "asd" + } + p { + background_color: "cyan", + "asd" + } + } + }) +} diff --git a/src/attributes.rs b/src/attributes.rs new file mode 100644 index 000000000..fb267fa9c --- /dev/null +++ b/src/attributes.rs @@ -0,0 +1,610 @@ +/* +- [ ] pub display: Display, +- [ ] pub position_type: PositionType, +- [ ] 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, + +- [ ] pub overflow: Overflow, + +- [x] pub align_items: AlignItems, +- [x] pub align_self: AlignSelf, +- [x] pub align_content: AlignContent, + +- [ ] pub margin: Rect, +- [ ] pub padding: Rect, + +- [x] pub justify_content: JustifyContent, +- [ ] pub position: Rect, +- [ ] pub border: Rect, +- [ ] pub size: Size, + +- [ ] pub min_size: Size, +- [ ] pub max_size: Size, +- [ ] pub aspect_ratio: Number, +*/ + +use stretch2::{prelude::*, style::Style}; +use tui::style::Style as TuiStyle; + +pub struct StyleModifer { + pub style: Style, + pub tui_style: TuiStyle, +} + +enum TuiModifier { + Text, +} + +/// 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" => { + // text color + } + + "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" => {} + + "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 value.ends_with("%") { + if let Ok(pct) = value.trim_end_matches("%").parse::() { + style.style.size.height = Dimension::Percent(pct / 100.0); + } + } else if value.ends_with("px") { + if let Ok(px) = value.trim_end_matches("px").parse::() { + style.style.size.height = Dimension::Points(px); + } + } + } + "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" => {} + "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 value.ends_with("%") { + if let Ok(pct) = value.trim_end_matches("%").parse::() { + style.style.size.width = Dimension::Percent(pct / 100.0); + } + } else if value.ends_with("px") { + if let Ok(px) = value.trim_end_matches("px").parse::() { + style.style.size.width = Dimension::Points(px); + } + } + } + "word-break" => {} + "word-spacing" => {} + "word-wrap" => {} + "z-index" => {} + _ => {} + } +} + +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) { + use stretch2::style::Display; + 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" => { + use tui::style::Color; + match value { + "red" => style.tui_style.bg.replace(Color::Red), + "green" => style.tui_style.bg.replace(Color::Green), + "blue" => style.tui_style.bg.replace(Color::Blue), + "yellow" => style.tui_style.bg.replace(Color::Yellow), + "cyan" => style.tui_style.bg.replace(Color::Cyan), + "magenta" => style.tui_style.bg.replace(Color::Magenta), + "white" => style.tui_style.bg.replace(Color::White), + "black" => style.tui_style.bg.replace(Color::Black), + _ => None, + }; + } + "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) { + match name { + "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" => { + if let Ok(px) = value.trim_end_matches("px").parse::() { + // tuistyle = px; + } + } + _ => {} + } +} + +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 value.ends_with("%") { + if let Ok(pct) = value.trim_end_matches("%").parse::() { + style.style.flex_basis = Dimension::Percent(pct / 100.0); + } + } else if value.ends_with("px") { + if let Ok(px) = value.trim_end_matches("px").parse::() { + style.style.flex_basis = Dimension::Points(px); + } + } + } + "flex-flow" => {} + "flex-grow" => { + if let Ok(val) = value.parse::() { + style.style.flex_grow = val; + } + } + "flex-shrink" => { + if let Ok(px) = value.parse::() { + style.style.flex_shrink = px; + } + } + "flex-wrap" => { + use FlexWrap::*; + style.style.flex_wrap = match value { + "nowrap" => NoWrap, + "wrap" => Wrap, + "wrap-reverse" => WrapReverse, + _ => NoWrap, + }; + } + _ => {} + } +} + +fn apply_font(name: &str, value: &str, style: &mut StyleModifer) { + todo!() +} + +fn apply_padding(name: &str, value: &str, style: &mut StyleModifer) { + // // 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), + + match name { + "padding" => { + if name.ends_with("px") { + if let Ok(px) = value.trim_end_matches("px").parse::() { + style.style.padding.bottom = Dimension::Points(px); + style.style.padding.top = Dimension::Points(px); + style.style.padding.start = Dimension::Points(px); + style.style.padding.end = Dimension::Points(px); + } + } else if name.ends_with("%") { + if let Ok(pct) = value.trim_end_matches("%").parse::() { + // + } + } + } + "padding-bottom" => { + if let Ok(px) = value.trim_end_matches("px").parse::() { + style.style.padding.bottom = Dimension::Points(px); + } + } + "padding-left" => { + if let Ok(px) = value.trim_end_matches("px").parse::() { + style.style.padding.start = Dimension::Points(px); + } + } + "padding-right" => { + if let Ok(px) = value.trim_end_matches("px").parse::() { + style.style.padding.end = Dimension::Points(px); + } + } + "padding-top" => { + if let Ok(px) = value.trim_end_matches("px").parse::() { + style.style.padding.top = Dimension::Points(px); + } + } + _ => {} + } +} + +fn apply_text(name: &str, value: &str, style: &mut StyleModifer) { + 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 name { + "margin" => {} + "margin-bottom" => {} + "margin-left" => {} + "margin-right" => {} + "margin-top" => {} + _ => {} + } +} diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 000000000..c7b01046a --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,88 @@ +use dioxus::core::*; +use std::collections::HashMap; +use tui::style::Style as TuiStyle; + +use crate::{ + attributes::{apply_attributes, StyleModifer}, + TuiNode, +}; + +pub fn collect_layout<'a>( + layout: &mut stretch2::Stretch, + nodes: &mut HashMap>, + vdom: &'a VirtualDom, + node: &'a VNode<'a>, +) { + use stretch2::prelude::*; + + match node { + VNode::Text(t) => { + // + let id = t.id.get().unwrap(); + let char_len = t.text.chars().count(); + + let style = Style { + size: Size { + // characters are 1 point tall + height: Dimension::Points(1.0), + + // text is as long as it is declared + width: Dimension::Points(char_len as f32), + }, + + ..Default::default() + }; + + nodes.insert( + id, + TuiNode { + node, + block_style: tui::style::Style::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: TuiStyle::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 { + child_layout.push(nodes[&node.mounted_id()].layout) + } + } + + nodes.insert( + node.mounted_id(), + TuiNode { + node, + block_style: modifier.tui_style, + 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(_) => todo!(), + VNode::Placeholder(_) => todo!(), + }; +} diff --git a/src/lib.rs b/src/lib.rs index ad6dc8596..4a606318f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,270 +1,152 @@ use anyhow::Result; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; use dioxus::core::*; -use tui::backend::CrosstermBackend; -use tui::layout::{Layout, Rect}; -use tui::style::{Modifier, Style as TuiStyle}; -use tui::text::{Span, Spans}; -use tui::widgets::{Block, Paragraph}; -use tui_template::tuiapp::CrosstermFrame; -struct RinkDom { - vdom: VirtualDom, +use std::{collections::HashMap, io}; +use stretch2::{prelude::Size, Stretch}; +use tui::{backend::CrosstermBackend, style::Style as TuiStyle, Terminal}; + +mod attributes; +mod layout; +mod render; + +pub use attributes::*; +pub use layout::*; +pub use render::*; + +pub struct TuiNode<'a> { + pub layout: stretch2::node::Node, + pub block_style: TuiStyle, + pub node: &'a VNode<'a>, } -impl RinkDom { - pub fn new(app: FC<()>) -> Self { - Self { - vdom: VirtualDom::new(app), - } - } +pub fn render_vdom(vdom: &VirtualDom) -> Result<()> { + /* + -> collect all the nodes with their layout + -> solve their layout + -> render the nodes in the right place with tui/crosstream + -> while rendering, apply styling - fn render_vnode<'a>( - &self, - f: &mut CrosstermFrame, - node: &'a VNode<'a>, - state: &mut RenderState<'a>, - ) -> Rect { - match &node.kind { - VNodeKind::Fragment(_) => todo!(), - VNodeKind::Component(_) => todo!(), - VNodeKind::Suspended { node } => todo!(), + use simd to compare lines for diffing? - VNodeKind::Text(te) => { - let span = Span::styled(te.text, TuiStyle::default()); + */ + let mut layout = Stretch::new(); + let mut nodes = HashMap::new(); - let mut m = Modifier::empty(); + let root_node = vdom.base_scope().root_node(); + layout::collect_layout(&mut layout, &mut nodes, vdom, root_node); - for style in &state.current_styles { - match style { - Styles::Bold => m = m | Modifier::BOLD, - Styles::Italic => m = m | Modifier::ITALIC, - Styles::Strikethrough => m = m | Modifier::CROSSED_OUT, - Styles::Emphasis => m = m | Modifier::ITALIC, - Styles::Underline => m = m | Modifier::UNDERLINED, - } - } + /* + Get the terminal to calcualte the layout from + */ + enable_raw_mode().unwrap(); + ctrlc::set_handler(move || { + disable_raw_mode().unwrap(); + }) + .expect("Error setting Ctrl-C handler"); + let mut stdout = std::io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + let backend = CrosstermBackend::new(io::stdout()); + let mut terminal = Terminal::new(backend).unwrap(); - let style = TuiStyle::default().add_modifier(m); - let span = span.styled_graphemes(style); - let cur_block = state.block_stack.last_mut().unwrap(); + let dims = terminal.size().unwrap(); + let width = dims.width; + let height = dims.height; - // Paragraph + /* + Compute the layout given th terminal size + */ + let node_id = root_node.try_mounted_id().unwrap(); + let root_layout = nodes[&node_id].layout; + layout.compute_layout( + root_layout, + Size { + width: stretch2::prelude::Number::Defined(width as f32), + height: stretch2::prelude::Number::Defined(height as f32), + }, + )?; - // f.render_widget(span); - } - VNodeKind::Element(el) => { - // - let mut new_layout = false; + terminal.draw(|frame| { + // + render::render_vnode(frame, &layout, &mut nodes, vdom, root_node); + assert!(nodes.is_empty()); + })?; - // all of our supported styles + std::thread::sleep(std::time::Duration::from_millis(5000)); - match el.tag_name { - // obviously semantically not really correct - "div" => { - state.layouts.push(Layout::default()); - new_layout = true; - } + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - "title" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => { - let mut block = state.block_stack.pop().unwrap(); - let children = el.children; - - if let (1, Some(VNodeKind::Text(te))) = - (children.len(), children.get(0).map(|f| &f.kind)) - { - block = block.title(vec![Span::from(te.text)]); - } - - state.block_stack.push(block); - } - - "span" | "header" => {} - - "footer" => {} - - "p" => { - state.layouts.push(Layout::default()); - new_layout = true; - } - - // elements that style for whatever reason - "em" => state.current_styles.push(Styles::Emphasis), - "i" => state.current_styles.push(Styles::Italic), - "b" => state.current_styles.push(Styles::Bold), - "u" => state.current_styles.push(Styles::Underline), - "strike" => state.current_styles.push(Styles::Strikethrough), - - "li" => {} - "ul" => {} - "ol" => {} - "code" => {} - "hr" => {} - - // Tables - "table" => {} - "tr" => {} - "th" => {} - "td" => {} - - // Inputs - "input" => {} - "label" => {} - - _ => {} - } - - let cur_layout = state.layouts.last_mut().unwrap(); - let cur_block = state.block_stack.last_mut().unwrap(); - let mut cur_style = TuiStyle::default(); - - for attr in el.attributes { - if attr.namespace == Some("style") { - match attr.name { - "width" => {} - "height" => {} - - "background" => { - // - // cur_style.bg - // cur_block.style() - } - "background-color" => {} - - "border" => {} - "border-bottom" => {} - "border-bottom-color" => {} - "border-bottom-style" => {} - "border-bottom-width" => {} - "border-color" => {} - "border-left" => {} - "border-left-color" => {} - "border-left-style" => {} - "border-left-width" => {} - "border-right" => {} - "border-right-color" => {} - "border-right-style" => {} - "border-right-width" => {} - "border-style" => {} - "border-top" => {} - "border-top-color" => {} - "border-top-style" => {} - "border-top-width" => {} - "border-width" => {} - - "clear" => {} - "clip" => {} - "color" => {} - "cursor" => {} - "display" => {} - "filter" => {} - "float" => {} - "font" => {} - "font-family" => {} - "font-size" => {} - "font-variant" => {} - "font-weight" => {} - - "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" => {} - "overflow" => {} - "padding" => {} - "padding-bottom" => {} - "padding-left" => {} - "padding-right" => {} - "padding-top" => {} - "position" => {} - "stroke-dasharray" => {} - "stroke-dashoffset" => {} - "text-align" => {} - "text-decoration" => {} - "text-indent" => {} - "text-transform" => {} - "top" => {} - "vertical-align" => {} - "visibility" => {} - "z-index" => {} - - "page-break-after" - | "page-break-before" - | "background-position" - | "background-attachment" - | "background-image" - | "background-repeat" - | _ => {} - } - } - } - - for child in el.children {} - } - } - Rect::new(0, 0, 0, 0) - } - - fn render_text(&self, f: &mut CrosstermFrame, node: &VNode) {} - - fn render_fragment(&self, f: &mut CrosstermFrame) {} + Ok(()) } -impl<'a> tui_template::tuiapp::TuiApp for RinkDom { - fn render(&mut self, frame: &mut CrosstermFrame) { - let base_scope = self.vdom.base_scope(); - let root = base_scope.root(); +// enum InputEvent { +// UserInput(KeyEvent), +// Close, +// Tick, +// } - let mut render_state = RenderState::new(); - self.render_vnode(frame, root, &mut render_state); - } +// // Setup input handling +// let (tx, rx) = mpsc::channel(); +// let tick_rate = Duration::from_millis(100); +// thread::spawn(move || { +// 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)); - fn event_handler(&self, action: tui_template::crossterm::event::Event) -> Result<()> { - todo!() - } +// if event::poll(timeout).unwrap() { +// if let TermEvent::Key(key) = event::read().unwrap() { +// tx.send(InputEvent::UserInput(key)).unwrap(); +// } +// } - fn handle_key(&mut self, key: tui_template::crossterm::event::KeyEvent) { - todo!() - } +// // if last_tick.elapsed() >= tick_rate { +// // tx.send(InputEvent::Tick).unwrap(); +// // last_tick = Instant::now(); +// // } +// } +// }); - fn tick(&mut self) { - todo!() - } +// terminal.clear()?; - fn should_quit(&self) -> bool { - todo!() - } -} -struct RenderState<'a> { - block_stack: Vec>, +// // loop { +// terminal.draw(|frame| { +// // draw the frame +// let size = frame.size(); +// let root_node = vdom.base_scope().root_node(); +// let block = render_vnode(frame, vdom, root_node); - layouts: Vec, +// frame.render_widget(block, size); +// })?; - /// All the current styles applied through the "style" tag - current_styles: Vec, -} - -// I don't think we can do any of these? -enum Styles { - Bold, - Italic, - Strikethrough, - Emphasis, - Underline, -} - -impl<'a> RenderState<'a> { - fn new() -> Self { - let block_stack = Vec::new(); - Self { - block_stack, - current_styles: Vec::new(), - layouts: Vec::new(), - } - } -} +// // // terminal.draw(|f| ui::draw(f, &mut app))?; +// match rx.recv()? { +// InputEvent::UserInput(event) => match event.code { +// KeyCode::Char('q') => { +// disable_raw_mode()?; +// execute!( +// terminal.backend_mut(), +// LeaveAlternateScreen, +// DisableMouseCapture +// )?; +// terminal.show_cursor()?; +// // break; +// } +// _ => {} // handle event +// }, +// InputEvent::Tick => {} // tick +// InputEvent::Close => { +// // break; +// } +// }; diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 000000000..ab671bb09 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,97 @@ +use anyhow::Result; +use crossterm::{ + event, + event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use dioxus::core::*; +use std::{ + collections::HashMap, + io::{self, Stdout}, + sync::mpsc, + thread, + time::{Duration, Instant}, +}; +use stretch2::{ + geometry::Point, + prelude::{Layout, Size}, + style::Style as StretchStyle, + Stretch, +}; +use tui::{ + backend::CrosstermBackend, + buffer::Buffer, + layout::Rect, + style::Style as TuiStyle, + text::Span, + widgets::{canvas::Label, Block, BorderType, Borders, Widget}, + Terminal, +}; + +use crate::TuiNode; + +pub fn render_vnode<'a>( + frame: &mut tui::Frame>, + layout: &Stretch, + layouts: &mut HashMap>, + vdom: &'a VirtualDom, + node: &'a VNode<'a>, +) { + match node { + VNode::Fragment(f) => { + for child in f.children { + render_vnode(frame, layout, layouts, vdom, child); + } + return; + } + VNode::Component(_) => todo!(), + + VNode::Placeholder(_) | VNode::Element(_) | VNode::Text(_) => {} + } + + let id = node.try_mounted_id().unwrap(); + let 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, + } + + impl<'a> Widget for Label<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + buf.set_string(area.left(), area.top(), self.text, TuiStyle::default()); + } + } + + // let s = Span::raw(t.text); + + // Block::default(). + + let label = Label { text: t.text }; + let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); + + frame.render_widget(label, area); + } + VNode::Element(el) => { + let block = Block::default().style(node.block_style); + let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); + + frame.render_widget(block, area); + + for el in el.children { + render_vnode(frame, layout, layouts, vdom, el); + } + } + VNode::Fragment(_) => todo!(), + VNode::Component(_) => todo!(), + VNode::Placeholder(_) => todo!(), + } +} diff --git a/test.html b/test.html new file mode 100644 index 000000000..48c9541d9 --- /dev/null +++ b/test.html @@ -0,0 +1,45 @@ + + + + + Test + + + + +
+
+ hello world +
+ goodbye +
+ asdasdasd +
+
+
+
+ + + \ No newline at end of file diff --git a/tests/margin.rs b/tests/margin.rs new file mode 100644 index 000000000..0ad4720c4 --- /dev/null +++ b/tests/margin.rs @@ -0,0 +1,102 @@ +use stretch2 as stretch; + +#[test] +fn margin_and_flex_row() { + let mut stretch = stretch::Stretch::new(); + let node0 = stretch + .new_node( + stretch::style::Style { + flex_grow: 1f32, + margin: stretch::geometry::Rect { + start: stretch::style::Dimension::Points(10f32), + end: stretch::style::Dimension::Points(10f32), + ..Default::default() + }, + ..Default::default() + }, + &[], + ) + .unwrap(); + let node = stretch + .new_node( + stretch::style::Style { + size: stretch::geometry::Size { + width: stretch::style::Dimension::Points(100f32), + height: stretch::style::Dimension::Points(100f32), + ..Default::default() + }, + ..Default::default() + }, + &[node0], + ) + .unwrap(); + stretch + .compute_layout(node, stretch::geometry::Size::undefined()) + .unwrap(); + assert_eq!(stretch.layout(node).unwrap().size.width, 100f32); + assert_eq!(stretch.layout(node).unwrap().size.height, 100f32); + assert_eq!(stretch.layout(node).unwrap().location.x, 0f32); + assert_eq!(stretch.layout(node).unwrap().location.y, 0f32); + assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32); + assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32); + assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32); + assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32); +} + +#[test] +fn margin_and_flex_row2() { + let mut stretch = stretch::Stretch::new(); + let node0 = stretch + .new_node( + stretch::style::Style { + flex_grow: 1f32, + margin: stretch::geometry::Rect { + // left + start: stretch::style::Dimension::Points(10f32), + + // right? + end: stretch::style::Dimension::Points(10f32), + + // top? + // top: stretch::style::Dimension::Points(10f32), + + // bottom? + // bottom: stretch::style::Dimension::Points(10f32), + ..Default::default() + }, + ..Default::default() + }, + &[], + ) + .unwrap(); + + let node = stretch + .new_node( + stretch::style::Style { + size: stretch::geometry::Size { + width: stretch::style::Dimension::Points(100f32), + height: stretch::style::Dimension::Points(100f32), + ..Default::default() + }, + ..Default::default() + }, + &[node0], + ) + .unwrap(); + + stretch + .compute_layout(node, stretch::geometry::Size::undefined()) + .unwrap(); + + assert_eq!(stretch.layout(node).unwrap().size.width, 100f32); + assert_eq!(stretch.layout(node).unwrap().size.height, 100f32); + assert_eq!(stretch.layout(node).unwrap().location.x, 0f32); + assert_eq!(stretch.layout(node).unwrap().location.y, 0f32); + + dbg!(stretch.layout(node0)); + + // assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32); + // assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32); + // assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32); + // assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32); +} From 7b29fbbad0cffbd4ba4bd9baf1f316e4983eba6f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 1 Jan 2022 01:08:31 -0500 Subject: [PATCH 03/32] wip: add an image --- README.md | 14 +++- examples/example.png | Bin 0 -> 107349 bytes examples/margin.rs | 59 +++++++++++++ examples/readme.rs | 22 +++++ src/attributes.rs | 167 +++++++++++++++++++++++-------------- src/layout.rs | 8 +- src/lib.rs | 193 +++++++++++++++++++++---------------------- test.html | 42 ++++++++-- 8 files changed, 331 insertions(+), 174 deletions(-) create mode 100644 examples/example.png create mode 100644 examples/margin.rs create mode 100644 examples/readme.rs diff --git a/README.md b/README.md index 27a4dbc3f..9ea2cbc8c 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,21 @@ You can use html-esque semantics with stylesheets, inline styles, tree hierarchy static App: FC<()> = |cx| { cx.render(rsx!{ - div { width: "100%", height: "3px", border_style: "solid", - h1 { "Hello world!" } - p { "This is a paragraph." } + div { + width: "100%", + height: "10px", + background_color: "red", + justify_content: "center", + align_items: "center", + + + "Hello world!" } }) } ``` -an image should go here +![demo app](examples/example.png) Rink is basically a port of [Ink]() but for Rust and Dioxus. Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful. diff --git a/examples/example.png b/examples/example.png new file mode 100644 index 0000000000000000000000000000000000000000..ba45e6e99c94aea47a7419536c2e9db11d5fac29 GIT binary patch literal 107349 zcmZU(1ymf((m#xQaCf)h79hal?yxv4?gR)BAjo3D-GjS(kOX&kmk=z$Ng(*L?EZP~ zz3=3zPt{b_uWGt`X3kXg#As_Oy~3izLO?)xrJ^jai-3T7h=71phk^DYiLr9q zL_olCbCQ$OR*{pV*M9GA@8oKSfS??cVTf*|H$hrpnwp#(K`1S)daRMGiNL|oim(`G z$;2EQL1-hZ-CP=n)nG2)TvE#z>xMd)Y%8``PPo z8a$al(Swdqk0((rM7M%gB(2M?7LQ?-s5)nvDvm%hAId{bD$#2;nV6J>sEzg;oaOrl zp`;t+WAOgj3)UCgAkBV+91VCmn6b z?wN??Zn)1yBK6%pEpI&ZK>&eLpH?dq8R40BSupNFEIA5BVQm;A&4noKLOvWCNT7w# zEO2Lqib|CJmC!oCk@pk%D6*09j^`mX0pOBcI5tHwT+MC2ZNoG>WebBT5 z@scp^1C=+T$95C9=fD$`$AQ;2H`Y4uDTj%p>*I#2oAH+zKBOG6DAc+s632-)BZ~+q zln4a7L=$~Vjm9Sd7^HJ!Yw2%gk&L7N9TABg{%$1v!;oDyQGW&f?a0yX7@pSv4>rqr zIwEaxp#HQp50aH*HmC3TH|^BavcpGuZX6Z;`Vo#rs9&4pJC5lj?Ne@syL2*^>b3pq zPaa-14_B^ER)H;?n%$yVWF(ET=pZC(1oQ|bR_*>2RMrqP`ga%3$69f2+dP373mdZMnN>;nFnz4LM#egtAh3ZV7=*+!w=7N?-c7p#`maeZ2U9-#(bF{;SSP&0KUwfEkF%P zWJNf_{Y%oqn1O3(fpH0v#V!zHvP7#3k(?f*@k0jV;|xj@Mkkk5w z_^8su-Z!Dtp}6#;HBq>tclArnBjAP;?c#jJ;0Zt5<@qj2v&(Xf_D7mGjGj3nk`JG0 zn7Uae58pT96AGi1GEQoA8PjKFTt*qf1qiXj%E5}_-LYV~>#VNXaxjV8iZo^c_> zQRybsfj>W6El<@<*-YuXhJ>aolN9S+vNAJMDiKErvC@7DmP&cqN?CrHP1)-*+(q

oPRG!R6c57GXb+sm49iEX_G7~ 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�&=*CxM7=N;Qn<8{39UQazNEy$W{smux7_m#G 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 Element { + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + background_color: "black", + // margin_right: "10px", + + + + div { + width: "70%", + height: "70%", + // margin_left: "4px", + background_color: "green", + + div { + width: "100%", + height: "100%", + + + margin_top: "2px", + margin_bottom: "2px", + margin_left: "2px", + margin_right: "2px", + // flex_shrink: "0", + + background_color: "red", + justify_content: "center", + align_items: "center", + flex_direction: "column", + + + // padding_top: "2px", + // padding_bottom: "2px", + // padding_left: "4px", + // padding_right: "4px", + + + "[A]" + "[A]" + "[A]" + "[A]" + } + } + + } + }) +} diff --git a/examples/readme.rs b/examples/readme.rs new file mode 100644 index 000000000..2edb2ac98 --- /dev/null +++ b/examples/readme.rs @@ -0,0 +1,22 @@ +use dioxus::prelude::*; + +fn main() { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + + rink::render_vdom(&dom).unwrap(); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + width: "100%", + height: "10px", + background_color: "red", + justify_content: "center", + align_items: "center", + + "Hello world!" + } + }) +} diff --git a/src/attributes.rs b/src/attributes.rs index fb267fa9c..637d9e9d3 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -1,6 +1,6 @@ /* - [ ] pub display: Display, -- [ ] pub position_type: PositionType, +- [x] pub position_type: PositionType, --> kinda, stretch doesnt support everything - [ ] pub direction: Direction, - [x] pub flex_direction: FlexDirection, @@ -9,26 +9,27 @@ - [x] pub flex_shrink: f32, - [x] pub flex_basis: Dimension, -- [ ] pub overflow: Overflow, +- [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, -- [ ] pub margin: Rect, -- [ ] pub padding: Rect, +- [x] pub margin: Rect, +- [x] pub padding: Rect, - [x] pub justify_content: JustifyContent, - [ ] pub position: Rect, - [ ] pub border: Rect, -- [ ] pub size: Size, +- [ ] pub size: Size, ----> ??? seems to only be relevant for input? - [ ] pub min_size: Size, - [ ] pub max_size: Size, + - [ ] pub aspect_ratio: Number, */ -use stretch2::{prelude::*, style::Style}; +use stretch2::{prelude::*, style::PositionType, style::Style}; use tui::style::Style as TuiStyle; pub struct StyleModifer { @@ -137,7 +138,13 @@ pub fn apply_attributes( "counter-reset" => {} "cursor" => {} - "direction" => {} + "direction" => { + match value { + "ltr" => style.style.direction = Direction::LTR, + "rtl" => style.style.direction = Direction::RTL, + _ => {} + } + } "display" => apply_display(name, value, style), @@ -231,8 +238,20 @@ pub fn apply_attributes( "perspective" | "perspective-origin" => {} - "position" => {} + "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" => {} @@ -285,6 +304,29 @@ pub fn apply_attributes( } } +enum UnitSystem { + Percent(f32), + Point(f32), +} + +fn parse_value(value: &str) -> Option { + if value.ends_with("px") { + if let Ok(px) = value.trim_end_matches("px").parse::() { + Some(UnitSystem::Point(px)) + } else { + None + } + } else if value.ends_with("%") { + if let Ok(pct) = value.trim_end_matches("%").parse::() { + Some(UnitSystem::Percent(pct)) + } else { + None + } + } else { + None + } +} + fn apply_overflow(name: &str, value: &str, style: &mut StyleModifer) { match name { // todo: add more overflow support to stretch2 @@ -491,54 +533,35 @@ fn apply_font(name: &str, value: &str, style: &mut StyleModifer) { } fn apply_padding(name: &str, value: &str, style: &mut StyleModifer) { - // // 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), - - match name { - "padding" => { - if name.ends_with("px") { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.padding.bottom = Dimension::Points(px); - style.style.padding.top = Dimension::Points(px); - style.style.padding.start = Dimension::Points(px); - style.style.padding.end = Dimension::Points(px); - } - } else if name.ends_with("%") { - if let Ok(pct) = value.trim_end_matches("%").parse::() { - // - } + 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" => { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.padding.bottom = Dimension::Points(px); + "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-left" => { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.padding.start = Dimension::Points(px); - } - } - "padding-right" => { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.padding.end = Dimension::Points(px); - } - } - "padding-top" => { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.padding.top = Dimension::Points(px); - } - } - _ => {} + "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 => {} } } @@ -599,12 +622,34 @@ pub fn apply_size(name: &str, value: &str, style: &mut StyleModifer) { } pub fn apply_margin(name: &str, value: &str, style: &mut StyleModifer) { - match name { - "margin" => {} - "margin-bottom" => {} - "margin-left" => {} - "margin-right" => {} - "margin-top" => {} - _ => {} + match parse_value(value) { + Some(UnitSystem::Percent(v)) => match name { + "margin" => { + let v = Dimension::Percent(v / 100.0); + style.style.margin.top = v; + style.style.margin.bottom = v; + style.style.margin.start = v; + style.style.margin.end = v; + } + "margin-top" => style.style.margin.top = Dimension::Percent(v / 100.0), + "margin-bottom" => style.style.margin.bottom = Dimension::Percent(v / 100.0), + "margin-left" => style.style.margin.start = Dimension::Percent(v / 100.0), + "margin-right" => style.style.margin.end = Dimension::Percent(v / 100.0), + _ => {} + }, + Some(UnitSystem::Point(v)) => match name { + "margin" => { + style.style.margin.top = Dimension::Points(v); + style.style.margin.bottom = Dimension::Points(v); + style.style.margin.start = Dimension::Points(v); + style.style.margin.end = Dimension::Points(v); + } + "margin-top" => style.style.margin.top = Dimension::Points(v), + "margin-bottom" => style.style.margin.bottom = Dimension::Points(v), + "margin-left" => style.style.margin.start = Dimension::Points(v), + "margin-right" => style.style.margin.end = Dimension::Points(v), + _ => {} + }, + None => {} } } diff --git a/src/layout.rs b/src/layout.rs index c7b01046a..d5443c6c1 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -7,6 +7,12 @@ use crate::{ 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>, @@ -17,7 +23,6 @@ pub fn collect_layout<'a>( match node { VNode::Text(t) => { - // let id = t.id.get().unwrap(); let char_len = t.text.chars().count(); @@ -29,7 +34,6 @@ pub fn collect_layout<'a>( // text is as long as it is declared width: Dimension::Points(char_len as f32), }, - ..Default::default() }; diff --git a/src/lib.rs b/src/lib.rs index 4a606318f..9acb3d487 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,16 @@ use anyhow::Result; use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use dioxus::core::*; -use std::{collections::HashMap, io}; +use std::{ + collections::HashMap, + io, + sync::mpsc, + time::{Duration, Instant}, +}; use stretch2::{prelude::Size, Stretch}; use tui::{backend::CrosstermBackend, style::Style as TuiStyle, Terminal}; @@ -24,21 +29,6 @@ pub struct TuiNode<'a> { } pub fn render_vdom(vdom: &VirtualDom) -> Result<()> { - /* - -> collect all the nodes with their layout - -> solve their layout - -> render the nodes in the right place with tui/crosstream - -> while rendering, apply styling - - use simd to compare lines for diffing? - - */ - 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); - /* Get the terminal to calcualte the layout from */ @@ -52,30 +42,90 @@ pub fn render_vdom(vdom: &VirtualDom) -> Result<()> { let backend = CrosstermBackend::new(io::stdout()); let mut terminal = Terminal::new(backend).unwrap(); - let dims = terminal.size().unwrap(); - let width = dims.width; - let height = dims.height; + // Setup input handling + let (tx, rx) = mpsc::channel(); + 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)); - /* - Compute the layout given th terminal size - */ - let node_id = root_node.try_mounted_id().unwrap(); - let root_layout = nodes[&node_id].layout; - layout.compute_layout( - root_layout, - Size { - width: stretch2::prelude::Number::Defined(width as f32), - height: stretch2::prelude::Number::Defined(height as f32), - }, - )?; + if event::poll(timeout).unwrap() { + if let TermEvent::Key(key) = event::read().unwrap() { + tx.send(InputEvent::UserInput(key)).unwrap(); + } + } - terminal.draw(|frame| { - // - render::render_vnode(frame, &layout, &mut nodes, vdom, root_node); - assert!(nodes.is_empty()); - })?; + if last_tick.elapsed() >= tick_rate { + tx.send(InputEvent::Tick).unwrap(); + last_tick = Instant::now(); + } + } + }); - std::thread::sleep(std::time::Duration::from_millis(5000)); + terminal.clear().unwrap(); + + loop { + let dims = terminal.size().unwrap(); + let width = dims.width; + let height = dims.height; + + /* + -> collect all the nodes with their layout + -> solve their layout + -> render the nodes in the right place with tui/crosstream + -> while rendering, apply styling + + use simd to compare lines for diffing? + + */ + 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 th terminal size + */ + let node_id = root_node.try_mounted_id().unwrap(); + let root_layout = nodes[&node_id].layout; + layout.compute_layout( + root_layout, + Size { + width: stretch2::prelude::Number::Defined(width as f32), + height: stretch2::prelude::Number::Defined(height as f32), + }, + )?; + + terminal.draw(|frame| { + // + render::render_vnode(frame, &layout, &mut nodes, vdom, root_node); + assert!(nodes.is_empty()); + })?; + + match rx.recv()? { + InputEvent::UserInput(event) => match event.code { + KeyCode::Char('q') => { + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + break; + } + _ => {} // handle event + }, + InputEvent::Tick => {} // tick + InputEvent::Close => { + break; + } + }; + } disable_raw_mode()?; execute!( @@ -88,65 +138,8 @@ pub fn render_vdom(vdom: &VirtualDom) -> Result<()> { Ok(()) } -// enum InputEvent { -// UserInput(KeyEvent), -// Close, -// Tick, -// } - -// // Setup input handling -// let (tx, rx) = mpsc::channel(); -// let tick_rate = Duration::from_millis(100); -// thread::spawn(move || { -// 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 event::poll(timeout).unwrap() { -// if let TermEvent::Key(key) = event::read().unwrap() { -// tx.send(InputEvent::UserInput(key)).unwrap(); -// } -// } - -// // if last_tick.elapsed() >= tick_rate { -// // tx.send(InputEvent::Tick).unwrap(); -// // last_tick = Instant::now(); -// // } -// } -// }); - -// terminal.clear()?; - -// // loop { -// terminal.draw(|frame| { -// // draw the frame -// let size = frame.size(); -// let root_node = vdom.base_scope().root_node(); -// let block = render_vnode(frame, vdom, root_node); - -// frame.render_widget(block, size); -// })?; - -// // // terminal.draw(|f| ui::draw(f, &mut app))?; -// match rx.recv()? { -// InputEvent::UserInput(event) => match event.code { -// KeyCode::Char('q') => { -// disable_raw_mode()?; -// execute!( -// terminal.backend_mut(), -// LeaveAlternateScreen, -// DisableMouseCapture -// )?; -// terminal.show_cursor()?; -// // break; -// } -// _ => {} // handle event -// }, -// InputEvent::Tick => {} // tick -// InputEvent::Close => { -// // break; -// } -// }; +enum InputEvent { + UserInput(KeyEvent), + Close, + Tick, +} diff --git a/test.html b/test.html index 48c9541d9..5298c723e 100644 --- a/test.html +++ b/test.html @@ -14,32 +14,60 @@ height: 100%; display: flex; flex-direction: column; - justify-content: center; - align-items: center; + background-color: black; + /* justify-content: center; + align-items: center; */ /* margin: auto; */ } .smaller { - height: 50%; + 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; - color: violet; + flex-direction: column; + /* margin: 20px; */ + /* margin: 20px; */ }

+
+
+

Hello World

+

This is a test

+
+
+
+ \ No newline at end of file From 2b4e3ae6e9df82f952c178a656507a21eb0d0121 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 1 Jan 2022 01:14:08 -0500 Subject: [PATCH 04/32] docs: clarify readme --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9ea2cbc8c..c1202b659 100644 --- a/README.md +++ b/README.md @@ -31,19 +31,21 @@ static App: FC<()> = |cx| { Rink is basically a port of [Ink]() but for Rust and Dioxus. Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful. +## Status + + +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: -- Flexbox based layout system -- CSS selectors -- inline css support -- Built-in focusing system -- high-quality keyboard support -- Support for events, hooks, and callbacks -- support for a very limited subset of HTML and CSS primitives +- [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 elements supported: -- div, h1-h6, p, input, textarea, tables, nav - -CSS Supported: -- Flex +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. From c884f8c1b850230114e390af0c057b28d090429c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 1 Jan 2022 09:49:08 -0500 Subject: [PATCH 05/32] feat: add full async suport --- Cargo.toml | 2 + README.md | 1 - examples/frame.rs | 2 +- examples/list.rs | 2 +- examples/margin.rs | 2 +- examples/quadrants.rs | 2 +- examples/readme.rs | 2 +- examples/task.rs | 51 +++++++++++ examples/text.rs | 2 +- src/lib.rs | 191 ++++++++++++++++++++++++------------------ 10 files changed, 168 insertions(+), 89 deletions(-) create mode 100644 examples/task.rs diff --git a/Cargo.toml b/Cargo.toml index 7672c10d6..2b67f00c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,6 @@ hecs = "0.7.3" ctrlc = "3.2.1" bumpalo = { version = "3.8.0", features = ["boxed"] } stretch2 = { path = "../../Tinkering/stretch2" } +tokio = { version = "1.15.0", features = ["full"] } +futures = "0.3.19" # stretch2 = { git = "https://github.com/elbaro/stretch2.git" } diff --git a/README.md b/README.md index c1202b659..064a262cf 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ static App: FC<()> = |cx| { justify_content: "center", align_items: "center", - "Hello world!" } }) diff --git a/examples/frame.rs b/examples/frame.rs index 3b913b9c9..20bbbfb36 100644 --- a/examples/frame.rs +++ b/examples/frame.rs @@ -4,7 +4,7 @@ fn main() { let mut dom = VirtualDom::new(app); dom.rebuild(); - rink::render_vdom(&dom).unwrap(); + rink::render_vdom(&mut dom).unwrap(); } fn app(cx: Scope) -> Element { diff --git a/examples/list.rs b/examples/list.rs index ebdb68e3c..354f3f008 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -4,7 +4,7 @@ fn main() { let mut dom = VirtualDom::new(app); dom.rebuild(); - rink::render_vdom(&dom).unwrap(); + rink::render_vdom(&mut dom).unwrap(); } fn app(cx: Scope) -> Element { diff --git a/examples/margin.rs b/examples/margin.rs index db08ff802..853721027 100644 --- a/examples/margin.rs +++ b/examples/margin.rs @@ -4,7 +4,7 @@ fn main() { let mut dom = VirtualDom::new(app); dom.rebuild(); - rink::render_vdom(&dom).unwrap(); + rink::render_vdom(&mut dom).unwrap(); } fn app(cx: Scope) -> Element { diff --git a/examples/quadrants.rs b/examples/quadrants.rs index 88138eeb7..4e9642b6e 100644 --- a/examples/quadrants.rs +++ b/examples/quadrants.rs @@ -4,7 +4,7 @@ fn main() { let mut dom = VirtualDom::new(app); dom.rebuild(); - rink::render_vdom(&dom).unwrap(); + rink::render_vdom(&mut dom).unwrap(); } fn app(cx: Scope) -> Element { diff --git a/examples/readme.rs b/examples/readme.rs index 2edb2ac98..7f68e884c 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -4,7 +4,7 @@ fn main() { let mut dom = VirtualDom::new(app); dom.rebuild(); - rink::render_vdom(&dom).unwrap(); + rink::render_vdom(&mut dom).unwrap(); } fn app(cx: Scope) -> Element { diff --git a/examples/task.rs b/examples/task.rs new file mode 100644 index 000000000..f8a8fe111 --- /dev/null +++ b/examples/task.rs @@ -0,0 +1,51 @@ +use dioxus::prelude::*; + +fn main() { + let mut dom = VirtualDom::new(app); + dom.rebuild(); + + rink::render_vdom(&mut dom).unwrap(); +} + +fn app(cx: Scope) -> Element { + let count = use_state(&cx, || 0); + + 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(50)).await; + mycount += 1; + set_count(mycount); + update(); + } + } + }); + + cx.render(rsx! { + div { width: "100%", + div { width: "50%", height: "5px", background_color: "blue", justify_content: "center", align_items: "center", + "Hello {count}!" + } + div { width: "50%", height: "10px", background_color: "red", justify_content: "center", align_items: "center", + "Hello {count}!" + } + } + }) +} + +// use_future(&cx, || { +// let set_count = count.setter(); +// let mut mycount = 0; +// let update = cx.schedule_update(); +// async move { +// loop { +// tokio::time::sleep(std::time::Duration::from_millis(100)).await; +// mycount += 1; +// set_count(mycount); +// update(); +// } +// } +// }); diff --git a/examples/text.rs b/examples/text.rs index 3b913b9c9..20bbbfb36 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -4,7 +4,7 @@ fn main() { let mut dom = VirtualDom::new(app); dom.rebuild(); - rink::render_vdom(&dom).unwrap(); + rink::render_vdom(&mut dom).unwrap(); } fn app(cx: Scope) -> Element { diff --git a/src/lib.rs b/src/lib.rs index 9acb3d487..6b737538d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,15 @@ use anyhow::Result; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent}, + event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; +use dioxus::core::exports::futures_channel::mpsc::unbounded; use dioxus::core::*; +use futures::{future::Either, pin_mut, StreamExt}; use std::{ collections::HashMap, io, - sync::mpsc, time::{Duration, Instant}, }; use stretch2::{prelude::Size, Stretch}; @@ -28,22 +29,10 @@ pub struct TuiNode<'a> { pub node: &'a VNode<'a>, } -pub fn render_vdom(vdom: &VirtualDom) -> Result<()> { - /* - Get the terminal to calcualte the layout from - */ - enable_raw_mode().unwrap(); - ctrlc::set_handler(move || { - disable_raw_mode().unwrap(); - }) - .expect("Error setting Ctrl-C handler"); - let mut stdout = std::io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); - let backend = CrosstermBackend::new(io::stdout()); - let mut terminal = Terminal::new(backend).unwrap(); - +pub fn render_vdom(vdom: &mut VirtualDom) -> Result<()> { // Setup input handling - let (tx, rx) = mpsc::channel(); + let (tx, mut rx) = unbounded(); + std::thread::spawn(move || { let tick_rate = Duration::from_millis(100); let mut last_tick = Instant::now(); @@ -53,93 +42,131 @@ pub fn render_vdom(vdom: &VirtualDom) -> Result<()> { .checked_sub(last_tick.elapsed()) .unwrap_or_else(|| Duration::from_secs(0)); - if event::poll(timeout).unwrap() { - if let TermEvent::Key(key) = event::read().unwrap() { - tx.send(InputEvent::UserInput(key)).unwrap(); - } + 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.send(InputEvent::Tick).unwrap(); + tx.unbounded_send(InputEvent::Tick).unwrap(); last_tick = Instant::now(); } } }); - terminal.clear().unwrap(); + 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(); - loop { - let dims = terminal.size().unwrap(); - let width = dims.width; - let height = dims.height; + terminal.clear().unwrap(); - /* - -> collect all the nodes with their layout - -> solve their layout - -> render the nodes in the right place with tui/crosstream - -> while rendering, apply styling + loop { + let dims = terminal.size().unwrap(); + let width = dims.width; + let height = dims.height; - use simd to compare lines for diffing? + /* + -> collect all the nodes with their layout + -> solve their layout + -> render the nodes in the right place with tui/crosstream + -> while rendering, apply styling - */ - let mut layout = Stretch::new(); - let mut nodes = HashMap::new(); + use simd to compare lines for diffing? - let root_node = vdom.base_scope().root_node(); - layout::collect_layout(&mut layout, &mut nodes, vdom, root_node); - /* - Compute the layout given th terminal size - */ - let node_id = root_node.try_mounted_id().unwrap(); - let root_layout = nodes[&node_id].layout; - layout.compute_layout( - root_layout, - Size { - width: stretch2::prelude::Number::Defined(width as f32), - height: stretch2::prelude::Number::Defined(height as f32), - }, - )?; - terminal.draw(|frame| { - // - render::render_vnode(frame, &layout, &mut nodes, vdom, root_node); - assert!(nodes.is_empty()); - })?; + 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(); - match rx.recv()? { - InputEvent::UserInput(event) => match event.code { - KeyCode::Char('q') => { - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - break; + let root_node = vdom.base_scope().root_node(); + layout::collect_layout(&mut layout, &mut nodes, vdom, root_node); + /* + Compute the layout given th terminal size + */ + let node_id = root_node.try_mounted_id().unwrap(); + let root_layout = nodes[&node_id].layout; + layout.compute_layout( + root_layout, + Size { + width: stretch2::prelude::Number::Defined(width as f32), + height: stretch2::prelude::Number::Defined(height as f32), + }, + )?; + + terminal.draw(|frame| { + // + render::render_vnode(frame, &layout, &mut nodes, vdom, root_node); + assert!(nodes.is_empty()); + })?; + + use futures::future::{select, Either}; + { + let wait = vdom.wait_for_work(); + pin_mut!(wait); + + match select(wait, rx.next()).await { + Either::Left((a, b)) => { + // println!("work"); + } + Either::Right((evt, o)) => { + // println!("event"); + match evt.unwrap() { + InputEvent::UserInput(event) => match event { + TermEvent::Key(key) => { + match key.code { + KeyCode::Char('q') => { + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + break; + } + _ => {} // handle event + } + } + TermEvent::Mouse(_) => {} + TermEvent::Resize(_, _) => {} + }, + InputEvent::Tick => {} // tick + InputEvent::Close => { + break; + } + }; + } + } } - _ => {} // handle event - }, - InputEvent::Tick => {} // tick - InputEvent::Close => { - break; + + vdom.work_with_deadline(|| false); } - }; - } - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; - Ok(()) + Ok(()) + }) } enum InputEvent { - UserInput(KeyEvent), + UserInput(TermEvent), Close, Tick, } From 1dbe244bbeabf03f4bdf23dd298d26a18dd34a34 Mon Sep 17 00:00:00 2001 From: Amin Yahyaabadi Date: Tue, 4 Jan 2022 14:26:36 -0800 Subject: [PATCH 06/32] Fix Ink link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 064a262cf..ceb8ff010 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ static App: FC<()> = |cx| { ![demo app](examples/example.png) -Rink is basically a port of [Ink]() but for Rust and Dioxus. Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful. +Rink is basically a port of [Ink](https://github.com/vadimdemedes/ink) but for Rust and Dioxus. Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful. ## Status From 395558232e2b1e2a2e5985adb723f12e3bd76f78 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 12 Jan 2022 09:40:36 -0500 Subject: [PATCH 07/32] updates: move to stable versions --- Cargo.toml | 7 +-- examples/frame.rs | 5 +- examples/keys.rs | 59 ++++++++++++++++++++++++ examples/list.rs | 5 +- examples/margin.rs | 15 ++---- examples/quadrants.rs | 5 +- examples/readme.rs | 5 +- examples/task.rs | 5 +- examples/text.rs | 5 +- examples/ui.rsx | 5 ++ src/hooks.rs | 104 ++++++++++++++++++++++++++++++++++++++++++ src/layout.rs | 23 ++++++++-- src/lib.rs | 55 ++++++++++++---------- 13 files changed, 233 insertions(+), 65 deletions(-) create mode 100644 examples/keys.rs create mode 100644 examples/ui.rsx create mode 100644 src/hooks.rs diff --git a/Cargo.toml b/Cargo.toml index 2b67f00c3..1d2084f1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,12 @@ tui = { version = "0.16.0", features = ["crossterm"] } crossterm = "0.22.1" anyhow = "1.0.42" thiserror = "1.0.24" -dioxus = { path = "../../dioxus" } +# dioxus = { path = "../../dioxus" } +dioxus = { version = "0.1.7" } hecs = "0.7.3" ctrlc = "3.2.1" bumpalo = { version = "3.8.0", features = ["boxed"] } -stretch2 = { path = "../../Tinkering/stretch2" } +# stretch2 = { path = "../../Tinkering/stretch2" } tokio = { version = "1.15.0", features = ["full"] } futures = "0.3.19" -# stretch2 = { git = "https://github.com/elbaro/stretch2.git" } +stretch2 = { git = "https://github.com/DioxusLabs/stretch" } diff --git a/examples/frame.rs b/examples/frame.rs index 20bbbfb36..db49cd733 100644 --- a/examples/frame.rs +++ b/examples/frame.rs @@ -1,10 +1,7 @@ use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + rink::launch(app); } fn app(cx: Scope) -> Element { diff --git a/examples/keys.rs b/examples/keys.rs new file mode 100644 index 000000000..58236a4d5 --- /dev/null +++ b/examples/keys.rs @@ -0,0 +1,59 @@ +use std::cell::RefCell; + +use crossterm::event::KeyEvent; +use dioxus::prelude::*; +use rink::InputHandler; + +fn main() { + rink::launch(app); +} + +fn app(cx: Scope) -> Element { + let count = use_state(&cx, || 0); + + cx.render(rsx! { + div { + width: "100%", + height: "10px", + background_color: "red", + justify_content: "center", + align_items: "center", + "Hello world!", + + // todo: enabling this will panic + // rink::InputHandler { + // onkeydown: move |evt: KeyEvent| { + // use crossterm::event::KeyCode::*; + // match evt.code { + // Left => count += 1, + // Right => count -= 1, + // Up => count += 10, + // Down => count -= 10, + // _ => {}, + // } + // }, + // onmousedown: move |evt| {}, + // onresize: move |dims| { + // println!("{:?}", dims); + // }, + // } + } + }) +} + +fn app2<'a>(cx: Scope<'a>) -> Element<'a> { + let mut count = use_state(&cx, || 0); + + cx.render(rsx! { + div { + width: "100%", + height: "10px", + background_color: "red", + justify_content: "center", + align_items: "center", + oninput: move |_| count += 1, + "Hello world!", + h1 {}, + } + }) +} diff --git a/examples/list.rs b/examples/list.rs index 354f3f008..ff219dcc6 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -1,10 +1,7 @@ use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + rink::launch(app); } fn app(cx: Scope) -> Element { diff --git a/examples/margin.rs b/examples/margin.rs index 853721027..edc2b659f 100644 --- a/examples/margin.rs +++ b/examples/margin.rs @@ -1,10 +1,7 @@ use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + rink::launch(app); } fn app(cx: Scope) -> Element { @@ -16,44 +13,38 @@ fn app(cx: Scope) -> Element { background_color: "black", // margin_right: "10px", - - div { width: "70%", height: "70%", - // margin_left: "4px", 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", + flex_shrink: "0", background_color: "red", justify_content: "center", align_items: "center", flex_direction: "column", - // padding_top: "2px", // padding_bottom: "2px", // padding_left: "4px", // padding_right: "4px", - "[A]" "[A]" "[A]" "[A]" } } - } }) } diff --git a/examples/quadrants.rs b/examples/quadrants.rs index 4e9642b6e..db0b92f19 100644 --- a/examples/quadrants.rs +++ b/examples/quadrants.rs @@ -1,10 +1,7 @@ use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + rink::launch(app); } fn app(cx: Scope) -> Element { diff --git a/examples/readme.rs b/examples/readme.rs index 7f68e884c..95a306686 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -1,10 +1,7 @@ use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + rink::launch(app); } fn app(cx: Scope) -> Element { diff --git a/examples/task.rs b/examples/task.rs index f8a8fe111..a9b757612 100644 --- a/examples/task.rs +++ b/examples/task.rs @@ -1,10 +1,7 @@ use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + rink::launch(app); } fn app(cx: Scope) -> Element { diff --git a/examples/text.rs b/examples/text.rs index 20bbbfb36..db49cd733 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -1,10 +1,7 @@ use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + rink::launch(app); } fn app(cx: Scope) -> Element { diff --git a/examples/ui.rsx b/examples/ui.rsx new file mode 100644 index 000000000..962dbc3f1 --- /dev/null +++ b/examples/ui.rsx @@ -0,0 +1,5 @@ +div { + div { + + } +} diff --git a/src/hooks.rs b/src/hooks.rs new file mode 100644 index 000000000..1400acd62 --- /dev/null +++ b/src/hooks.rs @@ -0,0 +1,104 @@ +use anyhow::Result; +use crossterm::{ + event::{ + DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent, MouseEvent, + }, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use dioxus::{core::exports::futures_channel::mpsc::unbounded, prelude::Props}; +use dioxus::{core::*, prelude::*}; +use futures::{channel::mpsc::UnboundedReceiver, future::Either, pin_mut, StreamExt}; +use std::{ + cell::{Cell, RefCell}, + collections::HashMap, + io, + rc::Rc, + time::{Duration, Instant}, +}; +use stretch2::{prelude::Size, Stretch}; +use tui::{backend::CrosstermBackend, style::Style as TuiStyle, Terminal}; + +pub struct RinkContext { + last_event: RefCell>, + receiver: Rc>>>, +} + +impl RinkContext { + pub fn new(receiver: UnboundedReceiver) -> Self { + Self { + last_event: RefCell::new(None), + receiver: Rc::new(Cell::new(Some(receiver))), + } + } + pub fn subscribe_to_events(&self, scope: ScopeId) { + // + } +} + +#[derive(Props)] +pub struct AppHandlerProps<'a> { + // #[props(setter(strip_option), default)] + onkeydown: EventHandler<'a, KeyEvent>, + + // #[props(setter(strip_option), default)] + onmousedown: EventHandler<'a, MouseEvent>, + + #[props(setter(strip_option), default)] + onresize: Option>, +} + +/// This component lets you handle input events +/// +/// Once attached to the DOM, it will listen for input events from the terminal +/// +/// +pub fn InputHandler<'a>(cx: Scope<'a, AppHandlerProps<'a>>) -> Element { + let rcx = cx.use_hook(|_| { + let rcx = cx + .consume_context::() + .unwrap_or_else(|| panic!("Rink InputHandlers can only be used in Rink apps!")); + + // our component will only re-render if new events are received ... or if the parent is updated + // todo: if update was not caused by a new event, we should not re-render + // perhaps add some tracking to context? + rcx.subscribe_to_events(cx.scope_id()); + + let mut rec = rcx.receiver.take().unwrap(); + let updater = cx.schedule_update(); + let rc2 = rcx.clone(); + cx.push_future(async move { + while let Some(evt) = rec.next().await { + rc2.last_event.borrow_mut().replace(evt); + println!("{:?}", evt); + updater(); + } + // + }); + + rcx + }); + + if let Some(evet) = rcx.last_event.borrow().as_ref() { + match evet { + TermEvent::Key(key) => { + cx.props.onkeydown.call(key.clone()); + // let mut handler = cx.props.keydown.borrow_mut(); + // handler(*key); + // if let Some(handler) = cx.props.onkeydown { + // handler(*key); + // } + } + TermEvent::Mouse(mouse) => { + cx.props.onmousedown.call(mouse.clone()); + } + TermEvent::Resize(x, y) => { + // if let Some(handler) = cx.props.onresize { + // handler((*x, *y)); + // } + } + } + } + + None +} diff --git a/src/layout.rs b/src/layout.rs index d5443c6c1..94f750990 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -67,7 +67,17 @@ pub fn collect_layout<'a>( for el in el.children { let ite = ElementIdIterator::new(vdom, el); for node in ite { - child_layout.push(nodes[&node.mounted_id()].layout) + 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) } } @@ -86,7 +96,14 @@ pub fn collect_layout<'a>( collect_layout(layout, nodes, vdom, child); } } - VNode::Component(_) => todo!(), - VNode::Placeholder(_) => todo!(), + VNode::Component(sc) => { + // + let scope = vdom.get_scope(sc.scope.get().unwrap()).unwrap(); + let root = scope.root_node(); + collect_layout(layout, nodes, vdom, root); + } + VNode::Placeholder(_) => { + // + } }; } diff --git a/src/lib.rs b/src/lib.rs index 6b737538d..801d5e068 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,35 +4,52 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use dioxus::core::exports::futures_channel::mpsc::unbounded; -use dioxus::core::*; -use futures::{future::Either, pin_mut, StreamExt}; +use dioxus::{core::exports::futures_channel::mpsc::unbounded, prelude::Props}; +use dioxus::{core::*, prelude::*}; +use futures::{ + channel::mpsc::{UnboundedReceiver, UnboundedSender}, + future::Either, + pin_mut, StreamExt, +}; use std::{ + cell::Cell, collections::HashMap, io, + rc::Rc, time::{Duration, Instant}, }; use stretch2::{prelude::Size, Stretch}; use tui::{backend::CrosstermBackend, style::Style as TuiStyle, Terminal}; mod attributes; +mod hooks; mod layout; mod render; pub use attributes::*; +pub use hooks::*; pub use layout::*; pub use render::*; +pub fn launch(app: Component<()>) { + let mut dom = VirtualDom::new(app); + let (tx, rx) = unbounded(); + + dom.base_scope().provide_context(RinkContext::new(rx)); + dom.rebuild(); + + render_vdom(&mut dom, tx).unwrap(); +} + pub struct TuiNode<'a> { pub layout: stretch2::node::Node, pub block_style: TuiStyle, pub node: &'a VNode<'a>, } -pub fn render_vdom(vdom: &mut VirtualDom) -> Result<()> { +pub fn render_vdom(vdom: &mut VirtualDom, ctx: UnboundedSender) -> 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(); @@ -117,35 +134,27 @@ pub fn render_vdom(vdom: &mut VirtualDom) -> Result<()> { match select(wait, rx.next()).await { Either::Left((a, b)) => { - // println!("work"); + // } Either::Right((evt, o)) => { - // println!("event"); - match evt.unwrap() { + // + match evt.as_ref().unwrap() { InputEvent::UserInput(event) => match event { TermEvent::Key(key) => { match key.code { - KeyCode::Char('q') => { - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - break; - } + KeyCode::Char('q') => break, _ => {} // handle event } } - TermEvent::Mouse(_) => {} - TermEvent::Resize(_, _) => {} + TermEvent::Resize(_, _) | TermEvent::Mouse(_) => {} }, InputEvent::Tick => {} // tick - InputEvent::Close => { - break; - } + InputEvent::Close => break, }; + + if let InputEvent::UserInput(evt) = evt.unwrap() { + ctx.unbounded_send(evt).unwrap(); + } } } } From 6c8fd6dccbc306b4124c023fc028f715781801af Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Thu, 13 Jan 2022 21:15:02 +0100 Subject: [PATCH 08/32] Cleaning up the README --- README.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ceb8ff010..592df5009 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,23 @@ The fastest portable TUIs in the west 🔫🤠🔫 🐎🔥🔥🔥 -Rink lets you build terminal user interfaces in Rust with Dioxus. +Rink lets you build terminal user interfaces in Rust with [`Dioxus`](https://dioxuslabs.com/). -You can use html-esque semantics with stylesheets, inline styles, tree hierarchy, components, etc, but your Tui app is probably not going to work well or look good in the web. It still technically is a limited subset of HTML, so use at your own risk. +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. + +## Example + +Let's print `Hello world!` in the center of the screen. ```rust - static App: FC<()> = |cx| { cx.render(rsx!{ div { @@ -28,12 +39,9 @@ static App: FC<()> = |cx| { ![demo app](examples/example.png) -Rink is basically a port of [Ink](https://github.com/vadimdemedes/ink) but for Rust and Dioxus. Rink doesn't depend on Node.js or any other JavaScript runtime, so your binaries are portable and beautiful. - ## Status - -Rink is currently under construction! +**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. @@ -42,9 +50,12 @@ Rendering a VirtualDom works fine, but the ecosystem of hooks is not yet ready. Rink features: - [x] Flexbox based layout system - [ ] CSS selectors -- [x] inline css support +- [x] inline CSS support - [ ] Built-in focusing system - [ ] high-quality keyboard support - [ ] Support for events, hooks, and callbacks +* [ ] Html tags1 + +1 Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality. + -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. From 2700806ca93e3facf6fa6fe62c17eb5a9bf71e78 Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Thu, 13 Jan 2022 21:39:02 +0100 Subject: [PATCH 09/32] Preventing default loading of termion through tui --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1d2084f1a..4ef184010 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tui = { version = "0.16.0", features = ["crossterm"] } +tui = { version = "0.16.0", features = ["crossterm"], default-features = false } crossterm = "0.22.1" anyhow = "1.0.42" thiserror = "1.0.24" From 99c9085bed87975dccca1db8f037aa11c2f68b08 Mon Sep 17 00:00:00 2001 From: Raimundo Saona <37874270+saona-raimundo@users.noreply.github.com> Date: Thu, 13 Jan 2022 21:54:21 +0100 Subject: [PATCH 10/32] Cleaning up Cargo.toml --- Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4ef184010..c493bcd19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,10 @@ tui = { version = "0.16.0", features = ["crossterm"], default-features = false } crossterm = "0.22.1" anyhow = "1.0.42" thiserror = "1.0.24" -# dioxus = { path = "../../dioxus" } -dioxus = { version = "0.1.7" } +dioxus = "0.1.7" hecs = "0.7.3" ctrlc = "3.2.1" bumpalo = { version = "3.8.0", features = ["boxed"] } -# stretch2 = { path = "../../Tinkering/stretch2" } tokio = { version = "1.15.0", features = ["full"] } futures = "0.3.19" stretch2 = { git = "https://github.com/DioxusLabs/stretch" } From 82b1735f25346e27e9cd47a9371e0f69ecbc64aa Mon Sep 17 00:00:00 2001 From: Demonthos <66571940+Demonthos@users.noreply.github.com> Date: Thu, 3 Feb 2022 14:45:05 -0600 Subject: [PATCH 11/32] added rgb color for background-color --- src/attributes.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/attributes.rs b/src/attributes.rs index 637d9e9d3..1f8013575 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -386,7 +386,30 @@ fn apply_background(name: &str, value: &str, style: &mut StyleModifer) { "magenta" => style.tui_style.bg.replace(Color::Magenta), "white" => style.tui_style.bg.replace(Color::White), "black" => style.tui_style.bg.replace(Color::Black), - _ => None, + _ => { + if value.len() == 7{ + 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(&value[(1+2*i)..(1+2*(i+1))], 16){ + values[i] = v; + } + else{ + color_ok = false; + } + } + if color_ok{ + let color = Color::Rgb(values[0], values[1], values[2]); + style.tui_style.bg.replace(color) + } + else{ + None + } + } + else{ + None + } + }, }; } "background" => {} From e2f6d87d8ee370001031f6604db3b651abbbdfe2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Feb 2022 07:24:02 -0600 Subject: [PATCH 12/32] added component rendering and updated dioxus version --- Cargo.toml | 2 +- examples/components.rs | 64 ++++++++++++++++++++++++++++++++++++++++++ examples/keys.rs | 14 ++++----- examples/task.rs | 12 ++++---- src/hooks.rs | 6 ++-- src/render.rs | 8 +++++- 6 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 examples/components.rs diff --git a/Cargo.toml b/Cargo.toml index c493bcd19..fde5cd137 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ tui = { version = "0.16.0", features = ["crossterm"], default-features = false } crossterm = "0.22.1" anyhow = "1.0.42" thiserror = "1.0.24" -dioxus = "0.1.7" +dioxus = "0.1.8" hecs = "0.7.3" ctrlc = "3.2.1" bumpalo = { version = "3.8.0", features = ["boxed"] } diff --git a/examples/components.rs b/examples/components.rs new file mode 100644 index 000000000..1c12b4c33 --- /dev/null +++ b/examples/components.rs @@ -0,0 +1,64 @@ +use dioxus::prelude::*; + +fn main() { + rink::launch(app); +} + +#[derive(Props, PartialEq)] +struct QuadrentProps { + color: String, + text: String +} + +fn Quadrent(cx: Scope) -> Element{ + cx.render(rsx! { + div { + border_width: "1px", + width: "50%", + height: "100%", + background_color: "{cx.props.color}", + justify_content: "center", + align_items: "center", + + "{cx.props.text}" + } + }) +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + + div { + width: "100%", + height: "50%", + flex_direction: "row", + Quadrent{ + color: "red".to_string(), + text: "[A]".to_string() + }, + Quadrent{ + color: "black".to_string(), + text: "[B]".to_string() + } + } + + div { + width: "100%", + height: "50%", + flex_direction: "row", + Quadrent{ + color: "green".to_string(), + text: "[C]".to_string() + }, + Quadrent{ + color: "blue".to_string(), + text: "[D]".to_string() + } + } + } + }) +} diff --git a/examples/keys.rs b/examples/keys.rs index 58236a4d5..1270c5ea7 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -9,7 +9,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx! { div { @@ -25,10 +25,10 @@ fn app(cx: Scope) -> Element { // onkeydown: move |evt: KeyEvent| { // use crossterm::event::KeyCode::*; // match evt.code { - // Left => count += 1, - // Right => count -= 1, - // Up => count += 10, - // Down => count -= 10, + // Left => set_count(count + 1), + // Right => set_count(count - 1), + // Up => set_count(count + 10), + // Down => set_count(count - 10), // _ => {}, // } // }, @@ -42,7 +42,7 @@ fn app(cx: Scope) -> Element { } fn app2<'a>(cx: Scope<'a>) -> Element<'a> { - let mut count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx! { div { @@ -51,7 +51,7 @@ fn app2<'a>(cx: Scope<'a>) -> Element<'a> { background_color: "red", justify_content: "center", align_items: "center", - oninput: move |_| count += 1, + oninput: move |_| set_count(count + 1), "Hello world!", h1 {}, } diff --git a/examples/task.rs b/examples/task.rs index a9b757612..cc39d6711 100644 --- a/examples/task.rs +++ b/examples/task.rs @@ -5,17 +5,15 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); - use_future(&cx, || { - let set_count = count.setter(); - let mut mycount = 0; + use_future(&cx, move || { + let set_count = set_count.to_owned(); let update = cx.schedule_update(); async move { loop { - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - mycount += 1; - set_count(mycount); + set_count.with_mut(|f| *f += 1); + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; update(); } } diff --git a/src/hooks.rs b/src/hooks.rs index 1400acd62..045325761 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -38,13 +38,13 @@ impl RinkContext { #[derive(Props)] pub struct AppHandlerProps<'a> { - // #[props(setter(strip_option), default)] + #[props(default)] onkeydown: EventHandler<'a, KeyEvent>, - // #[props(setter(strip_option), default)] + #[props(default)] onmousedown: EventHandler<'a, MouseEvent>, - #[props(setter(strip_option), default)] + #[props(default)] onresize: Option>, } diff --git a/src/render.rs b/src/render.rs index ab671bb09..9189e4eaa 100644 --- a/src/render.rs +++ b/src/render.rs @@ -45,7 +45,13 @@ pub fn render_vnode<'a>( } return; } - VNode::Component(_) => todo!(), + + 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); + return; + } VNode::Placeholder(_) | VNode::Element(_) | VNode::Text(_) => {} } From 3f739d351b631992c8dfc8f38ad594bbb0b6637e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Feb 2022 14:52:01 -0600 Subject: [PATCH 13/32] finished InputHandler --- examples/keys.rs | 40 +++++++++++----------- src/hooks.rs | 89 ++++++++++++++++++++++++++++-------------------- src/lib.rs | 5 ++- src/render.rs | 4 ++- 4 files changed, 81 insertions(+), 57 deletions(-) diff --git a/examples/keys.rs b/examples/keys.rs index 1270c5ea7..be9319126 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -9,7 +9,9 @@ fn main() { } fn app(cx: Scope) -> Element { - let (count, set_count) = use_state(&cx, || 0); + let (key, set_key) = use_state(&cx, || "".to_string()); + let (mouse, set_mouse) = use_state(&cx, || "".to_string()); + let (size, set_size) = use_state(&cx, || "".to_string()); cx.render(rsx! { div { @@ -18,25 +20,25 @@ fn app(cx: Scope) -> Element { background_color: "red", justify_content: "center", align_items: "center", - "Hello world!", - // todo: enabling this will panic - // rink::InputHandler { - // onkeydown: move |evt: KeyEvent| { - // use crossterm::event::KeyCode::*; - // match evt.code { - // Left => set_count(count + 1), - // Right => set_count(count - 1), - // Up => set_count(count + 10), - // Down => set_count(count - 10), - // _ => {}, - // } - // }, - // onmousedown: move |evt| {}, - // onresize: move |dims| { - // println!("{:?}", dims); - // }, - // } + rink::InputHandler { + onkeydown: move |evt: KeyEvent| { + set_key(format!("{evt:?}")); + }, + }, + rink::InputHandler { + onmousedown: move |evt| { + set_mouse(format!("{evt:?}")); + }, + }, + rink::InputHandler { + onresize: move |dims| { + set_size(format!("{dims:?}")); + }, + }, + "keyboard: {key} + mouse: {mouse} + resize: {size}", } }) } diff --git a/src/hooks.rs b/src/hooks.rs index 045325761..9ec8d47a6 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -17,22 +17,51 @@ use std::{ time::{Duration, Instant}, }; use stretch2::{prelude::Size, Stretch}; +use tokio::sync::broadcast::Receiver; use tui::{backend::CrosstermBackend, style::Style as TuiStyle, Terminal}; pub struct RinkContext { - last_event: RefCell>, - receiver: Rc>>>, + last_event: Rc>>, + subscribers: Rc>>, } impl RinkContext { - pub fn new(receiver: UnboundedReceiver) -> Self { + pub fn new(mut receiver: UnboundedReceiver, cx: &ScopeState) -> Self { + let updater = cx.schedule_update_any(); + let last_event = Rc::new(Cell::new(None)); + let last_event2 = last_event.clone(); + let subscribers = Rc::new(RefCell::new(HashMap::new())); + let subscribers2 = subscribers.clone(); + + cx.push_future(async move { + while let Some(evt) = receiver.next().await { + last_event2.replace(Some(evt)); + for (subscriber, received) in subscribers2.borrow_mut().iter_mut() { + updater(*subscriber); + *received = false; + } + } + }); + Self { - last_event: RefCell::new(None), - receiver: Rc::new(Cell::new(Some(receiver))), + last_event: last_event, + subscribers: subscribers, } } + pub fn subscribe_to_events(&self, scope: ScopeId) { - // + self.subscribers.borrow_mut().insert(scope, false); + } + + pub fn get_event(&self, scope: ScopeId) -> Option { + let mut subscribers = self.subscribers.borrow_mut(); + let received = subscribers.get_mut(&scope)?; + if !*received { + *received = true; + self.last_event.get() + } else { + None + } } } @@ -45,7 +74,7 @@ pub struct AppHandlerProps<'a> { onmousedown: EventHandler<'a, MouseEvent>, #[props(default)] - onresize: Option>, + onresize: EventHandler<'a, (u16, u16)>, } /// This component lets you handle input events @@ -64,38 +93,26 @@ pub fn InputHandler<'a>(cx: Scope<'a, AppHandlerProps<'a>>) -> Element { // perhaps add some tracking to context? rcx.subscribe_to_events(cx.scope_id()); - let mut rec = rcx.receiver.take().unwrap(); - let updater = cx.schedule_update(); - let rc2 = rcx.clone(); - cx.push_future(async move { - while let Some(evt) = rec.next().await { - rc2.last_event.borrow_mut().replace(evt); - println!("{:?}", evt); - updater(); - } - // - }); - rcx }); - if let Some(evet) = rcx.last_event.borrow().as_ref() { - match evet { - TermEvent::Key(key) => { - cx.props.onkeydown.call(key.clone()); - // let mut handler = cx.props.keydown.borrow_mut(); - // handler(*key); - // if let Some(handler) = cx.props.onkeydown { - // handler(*key); - // } - } - TermEvent::Mouse(mouse) => { - cx.props.onmousedown.call(mouse.clone()); - } - TermEvent::Resize(x, y) => { - // if let Some(handler) = cx.props.onresize { - // handler((*x, *y)); - // } + { + if let Some(evet) = rcx.get_event(cx.scope_id()) { + match evet { + TermEvent::Key(key) => { + cx.props.onkeydown.call(key.clone()); + // let mut handler = cx.props.keydown.borrow_mut(); + // handler(*key); + // if let Some(handler) = cx.props.onkeydown { + // handler(*key); + // } + } + TermEvent::Mouse(mouse) => { + cx.props.onmousedown.call(mouse.clone()); + } + TermEvent::Resize(x, y) => { + cx.props.onresize.call((x, y)); + } } } } diff --git a/src/lib.rs b/src/lib.rs index 801d5e068..89206b934 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,7 +35,10 @@ pub fn launch(app: Component<()>) { let mut dom = VirtualDom::new(app); let (tx, rx) = unbounded(); - dom.base_scope().provide_context(RinkContext::new(rx)); + let cx = dom.base_scope(); + + cx.provide_root_context(RinkContext::new(rx, cx)); + dom.rebuild(); render_vdom(&mut dom, tx).unwrap(); diff --git a/src/render.rs b/src/render.rs index 9189e4eaa..21cc6e01f 100644 --- a/src/render.rs +++ b/src/render.rs @@ -53,7 +53,9 @@ pub fn render_vnode<'a>( return; } - VNode::Placeholder(_) | VNode::Element(_) | VNode::Text(_) => {} + VNode::Placeholder(_) => return, + + VNode::Element(_) | VNode::Text(_) => {} } let id = node.try_mounted_id().unwrap(); From 981c3689330e58bd731ebaa3298a85d18167d38f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Feb 2022 15:03:55 -0600 Subject: [PATCH 14/32] reformated and center aligned example --- examples/keys.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/examples/keys.rs b/examples/keys.rs index be9319126..e30f13bdb 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; -use crossterm::event::KeyEvent; +use crossterm::event::{KeyCode, KeyEvent, MouseEvent}; use dioxus::prelude::*; use rink::InputHandler; @@ -9,9 +9,9 @@ fn main() { } fn app(cx: Scope) -> Element { - let (key, set_key) = use_state(&cx, || "".to_string()); - let (mouse, set_mouse) = use_state(&cx, || "".to_string()); - let (size, set_size) = use_state(&cx, || "".to_string()); + let (key, set_key) = use_state(&cx, || KeyCode::Null); + let (mouse, set_mouse) = use_state(&cx, || (0, 0)); + let (size, set_size) = use_state(&cx, || (0, 0)); cx.render(rsx! { div { @@ -20,25 +20,26 @@ fn app(cx: Scope) -> Element { background_color: "red", justify_content: "center", align_items: "center", + flex_direction: "column", rink::InputHandler { onkeydown: move |evt: KeyEvent| { - set_key(format!("{evt:?}")); + set_key(evt.code); }, }, rink::InputHandler { - onmousedown: move |evt| { - set_mouse(format!("{evt:?}")); + onmousedown: move |evt: MouseEvent| { + set_mouse((evt.row, evt.column)); }, }, rink::InputHandler { onresize: move |dims| { - set_size(format!("{dims:?}")); + set_size(dims); }, }, - "keyboard: {key} - mouse: {mouse} - resize: {size}", + "keyboard: {key:?}", + "mouse: {mouse:?}", + "resize: {size:?}", } }) } From be535c558c3bbb4d63f874783118e878c6311a9a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Feb 2022 15:09:26 -0600 Subject: [PATCH 15/32] combined input handlers --- examples/keys.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/keys.rs b/examples/keys.rs index e30f13bdb..ef9df9750 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -12,6 +12,7 @@ fn app(cx: Scope) -> Element { let (key, set_key) = use_state(&cx, || KeyCode::Null); let (mouse, set_mouse) = use_state(&cx, || (0, 0)); let (size, set_size) = use_state(&cx, || (0, 0)); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx! { div { @@ -24,20 +25,25 @@ fn app(cx: Scope) -> Element { rink::InputHandler { onkeydown: move |evt: KeyEvent| { + use crossterm::event::KeyCode::*; + match evt.code { + Left => set_count(count + 1), + Right => set_count(count - 1), + Up => set_count(count + 10), + Down => set_count(count - 10), + _ => {}, + } set_key(evt.code); }, - }, - rink::InputHandler { onmousedown: move |evt: MouseEvent| { set_mouse((evt.row, evt.column)); }, - }, - rink::InputHandler { onresize: move |dims| { set_size(dims); }, }, - "keyboard: {key:?}", + "count: {count:?}", + "key: {key:?}", "mouse: {mouse:?}", "resize: {size:?}", } From 86535e150f4c3ccef9f03d601d58a466b4ad8478 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 17:57:00 -0500 Subject: [PATCH 16/32] fix: cargo fmt and readme update --- README.md | 36 ++++++++++++++++-------------------- examples/components.rs | 4 ++-- examples/keys.rs | 3 --- examples/layouts.rs | 5 ++--- src/attributes.rs | 41 ++++++++++++++++++++--------------------- src/hooks.rs | 24 ++++++------------------ src/lib.rs | 26 ++++++++++---------------- src/render.rs | 20 ++------------------ 8 files changed, 58 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 592df5009..5f17c7b79 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,13 @@ # Rink: Like "Ink" but for Rust and Dioxus -The fastest portable TUIs in the west - 🔫🤠🔫 - 🐎🔥🔥🔥 +Rink lets you build terminal user interfaces in Rust with [`Dioxus`](https://dioxuslabs.com/). -Rink lets you build terminal user interfaces in Rust with [`Dioxus`](https://dioxuslabs.com/). -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. - -## Example - -Let's print `Hello world!` in the center of the screen. ```rust static App: FC<()> = |cx| { cx.render(rsx!{ - div { - width: "100%", + div { + width: "100%", height: "10px", background_color: "red", justify_content: "center", @@ -38,6 +21,19 @@ static App: FC<()> = |cx| { ![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 diff --git a/examples/components.rs b/examples/components.rs index 1c12b4c33..4086a5855 100644 --- a/examples/components.rs +++ b/examples/components.rs @@ -7,10 +7,10 @@ fn main() { #[derive(Props, PartialEq)] struct QuadrentProps { color: String, - text: String + text: String, } -fn Quadrent(cx: Scope) -> Element{ +fn Quadrent(cx: Scope) -> Element { cx.render(rsx! { div { border_width: "1px", diff --git a/examples/keys.rs b/examples/keys.rs index ef9df9750..77bf7683d 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -1,8 +1,5 @@ -use std::cell::RefCell; - use crossterm::event::{KeyCode, KeyEvent, MouseEvent}; use dioxus::prelude::*; -use rink::InputHandler; fn main() { rink::launch(app); diff --git a/examples/layouts.rs b/examples/layouts.rs index e4278c2b1..c6d661e5b 100644 --- a/examples/layouts.rs +++ b/examples/layouts.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; -use dioxus::{core::ElementId, prelude::*}; -use rink::TuiNode; +use dioxus::prelude::*; fn main() { let mut dom = VirtualDom::new(app); @@ -19,7 +18,7 @@ fn main() { .compute_layout(node.layout, stretch2::geometry::Size::undefined()) .unwrap(); - for (id, node) in nodes.drain() { + for (_id, node) in nodes.drain() { println!("{:?}", layout.layout(node.layout)); } } diff --git a/src/attributes.rs b/src/attributes.rs index 1f8013575..d84779406 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -343,7 +343,7 @@ fn apply_overflow(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_display(name: &str, value: &str, style: &mut StyleModifer) { +fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) { use stretch2::style::Display; style.style.display = match value { "flex" => Display::Flex, @@ -387,29 +387,28 @@ fn apply_background(name: &str, value: &str, style: &mut StyleModifer) { "white" => style.tui_style.bg.replace(Color::White), "black" => style.tui_style.bg.replace(Color::Black), _ => { - if value.len() == 7{ + if value.len() == 7 { 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(&value[(1+2*i)..(1+2*(i+1))], 16){ + for i in 0..values.len() { + if let Ok(v) = + u8::from_str_radix(&value[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) + { values[i] = v; - } - else{ + } else { color_ok = false; } } - if color_ok{ + if color_ok { let color = Color::Rgb(values[0], values[1], values[2]); style.tui_style.bg.replace(color) - } - else{ + } else { None } - } - else{ + } else { None } - }, + } }; } "background" => {} @@ -424,7 +423,7 @@ fn apply_background(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { +fn apply_border(name: &str, value: &str, _style: &mut StyleModifer) { match name { "border" => {} "border-bottom" => {} @@ -459,7 +458,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { "border-top-style" => {} "border-top-width" => {} "border-width" => { - if let Ok(px) = value.trim_end_matches("px").parse::() { + if let Ok(_px) = value.trim_end_matches("px").parse::() { // tuistyle = px; } } @@ -467,7 +466,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_animation(name: &str, value: &str, style: &mut StyleModifer) { +fn apply_animation(name: &str, _value: &str, _style: &mut StyleModifer) { match name { "animation" => {} "animation-delay" => {} @@ -482,7 +481,7 @@ fn apply_animation(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_column(name: &str, value: &str, style: &mut StyleModifer) { +fn apply_column(name: &str, _value: &str, _style: &mut StyleModifer) { match name { "column-count" => {} "column-fill" => {} @@ -551,7 +550,7 @@ fn apply_flex(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_font(name: &str, value: &str, style: &mut StyleModifer) { +fn apply_font(_name: &str, _value: &str, _style: &mut StyleModifer) { todo!() } @@ -588,15 +587,15 @@ fn apply_padding(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_text(name: &str, value: &str, style: &mut StyleModifer) { +fn apply_text(_name: &str, _value: &str, _style: &mut StyleModifer) { todo!() } -fn apply_transform(name: &str, value: &str, style: &mut StyleModifer) { +fn apply_transform(_name: &str, _value: &str, _style: &mut StyleModifer) { todo!() } -fn apply_transition(name: &str, value: &str, style: &mut StyleModifer) { +fn apply_transition(_name: &str, _value: &str, _style: &mut StyleModifer) { todo!() } @@ -640,7 +639,7 @@ fn apply_align(name: &str, value: &str, style: &mut StyleModifer) { } } -pub fn apply_size(name: &str, value: &str, style: &mut StyleModifer) { +pub fn apply_size(_name: &str, _value: &str, _style: &mut StyleModifer) { // } diff --git a/src/hooks.rs b/src/hooks.rs index 9ec8d47a6..b34068c19 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -1,24 +1,12 @@ -use anyhow::Result; -use crossterm::{ - event::{ - DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent, MouseEvent, - }, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; -use dioxus::{core::exports::futures_channel::mpsc::unbounded, prelude::Props}; -use dioxus::{core::*, prelude::*}; -use futures::{channel::mpsc::UnboundedReceiver, future::Either, pin_mut, StreamExt}; +use crossterm::event::{Event as TermEvent, KeyEvent, MouseEvent}; +use dioxus::core::*; +use dioxus::prelude::Props; +use futures::{channel::mpsc::UnboundedReceiver, StreamExt}; use std::{ cell::{Cell, RefCell}, collections::HashMap, - io, rc::Rc, - time::{Duration, Instant}, }; -use stretch2::{prelude::Size, Stretch}; -use tokio::sync::broadcast::Receiver; -use tui::{backend::CrosstermBackend, style::Style as TuiStyle, Terminal}; pub struct RinkContext { last_event: Rc>>, @@ -44,8 +32,8 @@ impl RinkContext { }); Self { - last_event: last_event, - subscribers: subscribers, + last_event, + subscribers, } } diff --git a/src/lib.rs b/src/lib.rs index 89206b934..0e56089bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,21 +1,15 @@ use anyhow::Result; use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent}, + 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, prelude::Props}; -use dioxus::{core::*, prelude::*}; -use futures::{ - channel::mpsc::{UnboundedReceiver, UnboundedSender}, - future::Either, - pin_mut, StreamExt, -}; +use dioxus::core::exports::futures_channel::mpsc::unbounded; +use dioxus::core::*; +use futures::{channel::mpsc::UnboundedSender, pin_mut, StreamExt}; use std::{ - cell::Cell, collections::HashMap, io, - rc::Rc, time::{Duration, Instant}, }; use stretch2::{prelude::Size, Stretch}; @@ -136,17 +130,17 @@ pub fn render_vdom(vdom: &mut VirtualDom, ctx: UnboundedSender) -> Re pin_mut!(wait); match select(wait, rx.next()).await { - Either::Left((a, b)) => { + Either::Left((_a, _b)) => { // } - Either::Right((evt, o)) => { - // + Either::Right((evt, _o)) => { match evt.as_ref().unwrap() { InputEvent::UserInput(event) => match event { TermEvent::Key(key) => { - match key.code { - KeyCode::Char('q') => break, - _ => {} // handle event + if matches!(key.code, KeyCode::Char('c')) + && key.modifiers.contains(KeyModifiers::CONTROL) + { + break; } } TermEvent::Resize(_, _) | TermEvent::Mouse(_) => {} diff --git a/src/render.rs b/src/render.rs index 21cc6e01f..46e590f17 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,22 +1,8 @@ -use anyhow::Result; -use crossterm::{ - event, - event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, KeyCode, KeyEvent}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, -}; use dioxus::core::*; -use std::{ - collections::HashMap, - io::{self, Stdout}, - sync::mpsc, - thread, - time::{Duration, Instant}, -}; +use std::{collections::HashMap, io::Stdout}; use stretch2::{ geometry::Point, prelude::{Layout, Size}, - style::Style as StretchStyle, Stretch, }; use tui::{ @@ -24,9 +10,7 @@ use tui::{ buffer::Buffer, layout::Rect, style::Style as TuiStyle, - text::Span, - widgets::{canvas::Label, Block, BorderType, Borders, Widget}, - Terminal, + widgets::{Block, Widget}, }; use crate::TuiNode; From 828fa2f0630597bb99494175cdcac23f3c98a542 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 18:01:53 -0500 Subject: [PATCH 17/32] docs: update readme --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f17c7b79..3c29107d4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,46 @@ -# Rink: Like "Ink" but for Rust and Dioxus +
+

Rink

+

+ Beautiful terminal user interfaces in Rust with [`Dioxus`](https://dioxuslabs.com/). +

+
-Rink lets you build terminal user interfaces in Rust with [`Dioxus`](https://dioxuslabs.com/). +
+ + + Crates.io version + + + + Download + + + + docs.rs docs + + + + CI status + + + + Awesome Page + + + + Discord Link + +
+ + +
+ +Leverage React-like patterns to build beautiful, portable, terminal user interfaces with Dioxus and Rust. ```rust static App: FC<()> = |cx| { From 90d8995645b63cb407e8ac299bf4cd704eb487d4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 18:02:34 -0500 Subject: [PATCH 18/32] fix: readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c29107d4..92a8cc4cb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Rink

- Beautiful terminal user interfaces in Rust with [`Dioxus`](https://dioxuslabs.com/). + Beautiful terminal user interfaces in Rust with Dioxus .

From 198d883547c5b2750d2ea7671da7eba5fb29c0d9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 18:05:03 -0500 Subject: [PATCH 19/32] docs: update readme --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 92a8cc4cb..20adaeb6c 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,10 @@
-Leverage React-like patterns to build beautiful, portable, terminal user interfaces with Dioxus and Rust. +Leverage React-like patterns, CSS, HTML, and Rust to build beautiful, portable, terminal user interfaces with Dioxus. ```rust -static App: FC<()> = |cx| { +fn app(cx: Scope) -> Element { cx.render(rsx!{ div { width: "100%", @@ -51,7 +51,6 @@ static App: FC<()> = |cx| { background_color: "red", justify_content: "center", align_items: "center", - "Hello world!" } }) From 7df8499da78e15e9072b3b41f7bb9f47cd4d8812 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Feb 2022 20:19:17 -0600 Subject: [PATCH 20/32] fixed race condition and zero sized node rendering --- src/lib.rs | 25 +++++++++++++------------ src/render.rs | 10 ++++++++-- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 89206b934..ccebf2b37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,10 +90,6 @@ pub fn render_vdom(vdom: &mut VirtualDom, ctx: UnboundedSender) -> Re terminal.clear().unwrap(); loop { - let dims = terminal.size().unwrap(); - let width = dims.width; - let height = dims.height; - /* -> collect all the nodes with their layout -> solve their layout @@ -116,16 +112,21 @@ pub fn render_vdom(vdom: &mut VirtualDom, ctx: UnboundedSender) -> Re */ let node_id = root_node.try_mounted_id().unwrap(); let root_layout = nodes[&node_id].layout; - layout.compute_layout( - root_layout, - Size { - width: stretch2::prelude::Number::Defined(width as f32), - height: stretch2::prelude::Number::Defined(height as f32), - }, - )?; 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(); render::render_vnode(frame, &layout, &mut nodes, vdom, root_node); assert!(nodes.is_empty()); })?; diff --git a/src/render.rs b/src/render.rs index 21cc6e01f..0f76c23a1 100644 --- a/src/render.rs +++ b/src/render.rs @@ -86,13 +86,19 @@ pub fn render_vnode<'a>( let label = Label { text: t.text }; let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); - frame.render_widget(label, area); + // 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(label, area); + } } VNode::Element(el) => { let block = Block::default().style(node.block_style); let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); - frame.render_widget(block, area); + // 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(block, area); + } for el in el.children { render_vnode(frame, layout, layouts, vdom, el); From 03ff72dddc83d6f16126e4549a19c8d1d2a28c3a Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 5 Feb 2022 16:28:19 -0600 Subject: [PATCH 21/32] added html native events --- Cargo.toml | 1 + examples/keys.rs | 51 ++--- src/hooks.rs | 496 +++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 15 +- 4 files changed, 450 insertions(+), 113 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fde5cd137..8b8b96616 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ 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"] } diff --git a/examples/keys.rs b/examples/keys.rs index 77bf7683d..8df6cbfb5 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -1,15 +1,17 @@ -use crossterm::event::{KeyCode, KeyEvent, MouseEvent}; 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, || KeyCode::Null); + let (key, set_key) = use_state(&cx, || KeyCode::Space); let (mouse, set_mouse) = use_state(&cx, || (0, 0)); - let (size, set_size) = 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 { @@ -19,30 +21,31 @@ fn app(cx: Scope) -> Element { justify_content: "center", align_items: "center", flex_direction: "column", - - rink::InputHandler { - onkeydown: move |evt: KeyEvent| { - use crossterm::event::KeyCode::*; - match evt.code { - Left => set_count(count + 1), - Right => set_count(count - 1), - Up => set_count(count + 10), - Down => set_count(count - 10), - _ => {}, - } - set_key(evt.code); - }, - onmousedown: move |evt: MouseEvent| { - set_mouse((evt.row, evt.column)); - }, - onresize: move |dims| { - set_size(dims); - }, + 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(evt.key_code); }, + 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: {mouse:?}", - "resize: {size:?}", + "mouse buttons: {buttons:b}", + "mouse pos: {mouse:?}", + "mouse button pressed: {mouse_clicked}" } }) } diff --git a/src/hooks.rs b/src/hooks.rs index b34068c19..8871c121e 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -1,109 +1,433 @@ -use crossterm::event::{Event as TermEvent, KeyEvent, MouseEvent}; +use crossterm::event::{ + Event as TermEvent, KeyCode as TermKeyCode, KeyModifiers, MouseButton, MouseEventKind, +}; use dioxus::core::*; -use dioxus::prelude::Props; + +use dioxus_html::{on::*, KeyCode}; use futures::{channel::mpsc::UnboundedReceiver, StreamExt}; use std::{ - cell::{Cell, RefCell}, - collections::HashMap, + any::Any, + borrow::BorrowMut, + cell::RefCell, rc::Rc, + sync::Arc, + time::{Duration, Instant}, }; -pub struct RinkContext { - last_event: Rc>>, - subscribers: Rc>>, +// a wrapper around the input state for easier access +// todo: fix loop +// pub struct InputState(Rc>>); +// impl InputState { +// pub fn get(cx: &ScopeState) -> InputState { +// let inner = cx +// .consume_context::>>() +// .expect("Rink InputState can only be used in Rink apps!"); +// (**inner).borrow_mut().subscribe(cx.schedule_update()); +// InputState(inner) +// } + +// pub fn mouse(&self) -> Option { +// let data = (**self.0).borrow(); +// data.mouse.as_ref().map(|m| clone_mouse_data(m)) +// } + +// pub fn wheel(&self) -> Option { +// let data = (**self.0).borrow(); +// data.wheel.as_ref().map(|w| clone_wheel_data(w)) +// } + +// pub fn screen(&self) -> Option<(u16, u16)> { +// let data = (**self.0).borrow(); +// data.screen.as_ref().map(|m| m.clone()) +// } + +// pub fn last_key_pressed(&self) -> Option { +// let data = (**self.0).borrow(); +// data.last_key_pressed +// .as_ref() +// .map(|k| clone_keyboard_data(&k.0)) +// } +// } + +type EventCore = (&'static str, EventData); + +#[derive(Debug)] +enum EventData { + Mouse(MouseData), + Wheel(WheelData), + Screen((u16, u16)), + Keyboard(KeyboardData), +} +impl EventData { + fn into_any(self) -> Arc { + match self { + Self::Mouse(m) => Arc::new(m), + Self::Wheel(w) => Arc::new(w), + Self::Screen(s) => Arc::new(s), + Self::Keyboard(k) => Arc::new(k), + } + } } -impl RinkContext { - pub fn new(mut receiver: UnboundedReceiver, cx: &ScopeState) -> Self { - let updater = cx.schedule_update_any(); - let last_event = Rc::new(Cell::new(None)); - let last_event2 = last_event.clone(); - let subscribers = Rc::new(RefCell::new(HashMap::new())); - let subscribers2 = subscribers.clone(); +const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); + +pub struct InnerInputState { + mouse: Option, + wheel: Option, + last_key_pressed: Option<(KeyboardData, Instant)>, + screen: Option<(u16, u16)>, + // subscribers: Vec>, +} + +impl InnerInputState { + fn new() -> Self { + Self { + mouse: None, + wheel: None, + last_key_pressed: None, + screen: None, + // subscribers: Vec::new(), + } + } + + // stores current input state and transforms events based on that state + fn apply_event(&mut self, evt: &mut EventCore) { + match evt.1 { + EventData::Mouse(ref mut m) => match &mut self.mouse { + Some(state) => { + *state = clone_mouse_data(m); + // crossterm always outputs the left mouse button on mouse up + // let mut buttons = state.buttons; + // *state = clone_mouse_data(m); + // match evt.0 { + // "mouseup" => { + // buttons &= !m.buttons; + // } + // "mousedown" => { + // buttons |= m.buttons; + // } + // _ => (), + // } + // state.buttons = buttons; + // m.buttons = buttons; + } + None => { + self.mouse = Some(clone_mouse_data(m)); + } + }, + 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 mut new = clone_keyboard_data(k); + new.repeat = repeat; + self.last_key_pressed = Some((new, Instant::now())); + } + } + } + + fn update(&mut self, evts: &mut [EventCore]) { + for e in evts { + self.apply_event(e) + } + // for s in &self.subscribers { + // s(); + // } + } + + // fn subscribe(&mut self, f: Rc) { + // self.subscribers.push(f) + // } +} + +pub struct RinkInputHandler { + state: Rc>, + queued_events: Rc>>, +} + +impl RinkInputHandler { + /// global context that handles events + /// limitations: GUI key modifier is never detected + pub fn new( + mut receiver: UnboundedReceiver, + cx: &ScopeState, + ) -> (Self, Rc>) { + let queued_events = Rc::new(RefCell::new(Vec::new())); + let queued_events2 = Rc::>>::downgrade(&queued_events); cx.push_future(async move { while let Some(evt) = receiver.next().await { - last_event2.replace(Some(evt)); - for (subscriber, received) in subscribers2.borrow_mut().iter_mut() { - updater(*subscriber); - *received = false; + if let Some(evt) = get_event(evt) { + if let Some(v) = queued_events2.upgrade() { + (*v).borrow_mut().push(evt); + } else { + break; + } } } }); - Self { - last_event, - subscribers, - } + let state = Rc::new(RefCell::new(InnerInputState::new())); + + ( + Self { + state: state.clone(), + queued_events, + }, + state, + ) } - pub fn subscribe_to_events(&self, scope: ScopeId) { - self.subscribers.borrow_mut().insert(scope, false); - } - - pub fn get_event(&self, scope: ScopeId) -> Option { - let mut subscribers = self.subscribers.borrow_mut(); - let received = subscribers.get_mut(&scope)?; - if !*received { - *received = true; - self.last_event.get() - } else { - None - } - } -} - -#[derive(Props)] -pub struct AppHandlerProps<'a> { - #[props(default)] - onkeydown: EventHandler<'a, KeyEvent>, - - #[props(default)] - onmousedown: EventHandler<'a, MouseEvent>, - - #[props(default)] - onresize: EventHandler<'a, (u16, u16)>, -} - -/// This component lets you handle input events -/// -/// Once attached to the DOM, it will listen for input events from the terminal -/// -/// -pub fn InputHandler<'a>(cx: Scope<'a, AppHandlerProps<'a>>) -> Element { - let rcx = cx.use_hook(|_| { - let rcx = cx - .consume_context::() - .unwrap_or_else(|| panic!("Rink InputHandlers can only be used in Rink apps!")); - - // our component will only re-render if new events are received ... or if the parent is updated - // todo: if update was not caused by a new event, we should not re-render - // perhaps add some tracking to context? - rcx.subscribe_to_events(cx.scope_id()); - - rcx - }); - - { - if let Some(evet) = rcx.get_event(cx.scope_id()) { - match evet { - TermEvent::Key(key) => { - cx.props.onkeydown.call(key.clone()); - // let mut handler = cx.props.keydown.borrow_mut(); - // handler(*key); - // if let Some(handler) = cx.props.onkeydown { - // handler(*key); - // } + pub fn resolve_events(&self, dom: &mut VirtualDom) { + // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus + fn inner( + queue: &Vec<(&'static str, Arc)>, + resolved: &mut Vec, + node: &VNode, + ) { + match node { + VNode::Fragment(frag) => { + for c in frag.children { + inner(queue, resolved, c); + } } - TermEvent::Mouse(mouse) => { - cx.props.onmousedown.call(mouse.clone()); - } - TermEvent::Resize(x, y) => { - cx.props.onresize.call((x, y)); + 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); + } } + _ => (), } } - } - None + let mut resolved_events = Vec::new(); + + (*self.state) + .borrow_mut() + .update(&mut (*self.queued_events).borrow_mut()); + + let events: Vec<_> = self + .queued_events + .replace(Vec::new()) + .into_iter() + .map(|e| (e.0, e.1.into_any())) + .collect(); + + inner(&events, &mut resolved_events, dom.base_scope().root_node()); + + for e in resolved_events { + dom.handle_message(SchedulerMsg::Event(e)); + } + } +} + +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 => ("scroll", get_wheel_data(false)), + MouseEventKind::ScrollUp => ("scroll", get_wheel_data(true)), + } + } + TermEvent::Resize(x, y) => ("resize", EventData::Screen((x, y))), + }; + + Some((name, data)) +} + +fn translate_key_code(c: TermKeyCode) -> Option { + match c { + TermKeyCode::Backspace => Some(KeyCode::Backspace), + TermKeyCode::Enter => Some(KeyCode::Enter), + TermKeyCode::Left => Some(KeyCode::LeftArrow), + TermKeyCode::Right => Some(KeyCode::RightArrow), + TermKeyCode::Up => Some(KeyCode::UpArrow), + TermKeyCode::Down => Some(KeyCode::DownArrow), + TermKeyCode::Home => Some(KeyCode::Home), + TermKeyCode::End => Some(KeyCode::End), + TermKeyCode::PageUp => Some(KeyCode::PageUp), + TermKeyCode::PageDown => Some(KeyCode::PageDown), + TermKeyCode::Tab => Some(KeyCode::Tab), + TermKeyCode::BackTab => None, + TermKeyCode::Delete => Some(KeyCode::Delete), + TermKeyCode::Insert => Some(KeyCode::Insert), + TermKeyCode::F(fn_num) => match fn_num { + 1 => Some(KeyCode::F1), + 2 => Some(KeyCode::F2), + 3 => Some(KeyCode::F3), + 4 => Some(KeyCode::F4), + 5 => Some(KeyCode::F5), + 6 => Some(KeyCode::F6), + 7 => Some(KeyCode::F7), + 8 => Some(KeyCode::F8), + 9 => Some(KeyCode::F9), + 10 => Some(KeyCode::F10), + 11 => Some(KeyCode::F11), + 12 => Some(KeyCode::F12), + _ => None, + }, + TermKeyCode::Char(c) => match c.to_uppercase().next().unwrap() { + 'A' => Some(KeyCode::A), + 'B' => Some(KeyCode::B), + 'C' => Some(KeyCode::C), + 'D' => Some(KeyCode::D), + 'E' => Some(KeyCode::E), + 'F' => Some(KeyCode::F), + 'G' => Some(KeyCode::G), + 'H' => Some(KeyCode::H), + 'I' => Some(KeyCode::I), + 'J' => Some(KeyCode::J), + 'K' => Some(KeyCode::K), + 'L' => Some(KeyCode::L), + 'M' => Some(KeyCode::M), + 'N' => Some(KeyCode::N), + 'O' => Some(KeyCode::O), + 'P' => Some(KeyCode::P), + 'Q' => Some(KeyCode::Q), + 'R' => Some(KeyCode::R), + 'S' => Some(KeyCode::S), + 'T' => Some(KeyCode::T), + 'U' => Some(KeyCode::U), + 'V' => Some(KeyCode::V), + 'W' => Some(KeyCode::W), + 'X' => Some(KeyCode::X), + 'Y' => Some(KeyCode::Y), + 'Z' => Some(KeyCode::Z), + _ => None, + }, + TermKeyCode::Null => None, + TermKeyCode::Esc => Some(KeyCode::Escape), + } +} + +fn clone_mouse_data(m: &MouseData) -> MouseData { + MouseData { + client_x: m.client_x, + client_y: m.client_y, + page_x: m.page_x, + page_y: m.page_y, + screen_x: m.screen_x, + screen_y: m.screen_y, + alt_key: m.alt_key, + ctrl_key: m.ctrl_key, + meta_key: m.meta_key, + shift_key: m.shift_key, + button: m.button, + buttons: m.buttons, + } +} + +fn clone_keyboard_data(k: &KeyboardData) -> KeyboardData { + KeyboardData { + char_code: k.char_code, + key: k.key.clone(), + key_code: k.key_code, + alt_key: k.alt_key, + ctrl_key: k.ctrl_key, + meta_key: k.meta_key, + shift_key: k.shift_key, + locale: k.locale.clone(), + location: k.location, + repeat: k.repeat, + which: k.which, + } +} + +fn clone_wheel_data(w: &WheelData) -> WheelData { + WheelData { + delta_mode: w.delta_mode, + delta_x: w.delta_x, + delta_y: w.delta_y, + delta_z: w.delta_x, + } } diff --git a/src/lib.rs b/src/lib.rs index 7a8e581f4..df24c0b37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,11 +31,13 @@ pub fn launch(app: Component<()>) { let cx = dom.base_scope(); - cx.provide_root_context(RinkContext::new(rx, cx)); + let (handler, state) = RinkInputHandler::new(rx, cx); + + cx.provide_root_context(state); dom.rebuild(); - render_vdom(&mut dom, tx).unwrap(); + render_vdom(&mut dom, tx, handler).unwrap(); } pub struct TuiNode<'a> { @@ -44,7 +46,11 @@ pub struct TuiNode<'a> { pub node: &'a VNode<'a>, } -pub fn render_vdom(vdom: &mut VirtualDom, ctx: UnboundedSender) -> Result<()> { +pub fn render_vdom( + vdom: &mut VirtualDom, + ctx: UnboundedSender, + handler: RinkInputHandler, +) -> Result<()> { // Setup input handling let (tx, mut rx) = unbounded(); std::thread::spawn(move || { @@ -84,6 +90,9 @@ pub fn render_vdom(vdom: &mut VirtualDom, ctx: UnboundedSender) -> Re terminal.clear().unwrap(); loop { + // resolve events before rendering + handler.resolve_events(vdom); + /* -> collect all the nodes with their layout -> solve their layout From 6f50da162ba0e8e8c2c72727f642a42914cec871 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 5 Feb 2022 16:30:04 -0600 Subject: [PATCH 22/32] updated comment --- src/hooks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks.rs b/src/hooks.rs index 8871c121e..e67b1c935 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -152,7 +152,7 @@ pub struct RinkInputHandler { impl RinkInputHandler { /// global context that handles events - /// limitations: GUI key modifier is never detected + /// limitations: GUI key modifier is never detected, key up events are not detected, and mouse up events are not specific to a key pub fn new( mut receiver: UnboundedReceiver, cx: &ScopeState, From ff92df73bdae6ca0fe9e7c74587d53f85d2e5a7d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 5 Feb 2022 16:45:40 -0600 Subject: [PATCH 23/32] changed onscroll to onwheel --- examples/keys.rs | 7 +++++++ src/hooks.rs | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/keys.rs b/examples/keys.rs index 8df6cbfb5..44c2dd745 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -1,3 +1,4 @@ +use dioxus::events::WheelEvent; use dioxus::prelude::*; use dioxus_html::on::{KeyboardEvent, MouseEvent}; use dioxus_html::KeyCode; @@ -31,6 +32,12 @@ fn app(cx: Scope) -> Element { } set_key(evt.key_code); }, + 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); diff --git a/src/hooks.rs b/src/hooks.rs index e67b1c935..143957e05 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -315,8 +315,8 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> { 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 => ("scroll", get_wheel_data(false)), - MouseEventKind::ScrollUp => ("scroll", get_wheel_data(true)), + MouseEventKind::ScrollDown => ("wheel", get_wheel_data(false)), + MouseEventKind::ScrollUp => ("wheel", get_wheel_data(true)), } } TermEvent::Resize(x, y) => ("resize", EventData::Screen((x, y))), From a03ab3edfb00f104c4b9f1031c5f5f1897905a4e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 6 Feb 2022 07:08:15 -0600 Subject: [PATCH 24/32] fixed mouse --- examples/keys.rs | 6 ++--- src/hooks.rs | 63 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/examples/keys.rs b/examples/keys.rs index 44c2dd745..509199b5b 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -8,7 +8,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let (key, set_key) = use_state(&cx, || KeyCode::Space); + 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); @@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element { KeyCode::DownArrow => set_count(count - 10), _ => {}, } - set_key(evt.key_code); + set_key(format!("{:?} repeating: {:?}", evt.key, evt.repeat)); }, onwheel: move |evt: WheelEvent| { set_count(count + evt.data.delta_y as i64); @@ -49,7 +49,7 @@ fn app(cx: Scope) -> Element { }, "count: {count:?}", - "key: {key:?}", + "key: {key}", "mouse buttons: {buttons:b}", "mouse pos: {mouse:?}", "mouse button pressed: {mouse_clicked}" diff --git a/src/hooks.rs b/src/hooks.rs index 143957e05..21858a662 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -72,7 +72,7 @@ impl EventData { const MAX_REPEAT_TIME: Duration = Duration::from_millis(100); pub struct InnerInputState { - mouse: Option, + mouse: Option<(MouseData, Vec)>, wheel: Option, last_key_pressed: Option<(KeyboardData, Instant)>, screen: Option<(u16, u16)>, @@ -93,26 +93,53 @@ impl InnerInputState { // 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) => { - *state = clone_mouse_data(m); - // crossterm always outputs the left mouse button on mouse up - // let mut buttons = state.buttons; - // *state = clone_mouse_data(m); - // match evt.0 { - // "mouseup" => { - // buttons &= !m.buttons; - // } - // "mousedown" => { - // buttons |= m.buttons; - // } - // _ => (), - // } - // state.buttons = buttons; - // m.buttons = buttons; + 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 { + 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; + // println!("{buttons}") } None => { - self.mouse = Some(clone_mouse_data(m)); + 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)), @@ -152,7 +179,7 @@ pub struct RinkInputHandler { impl RinkInputHandler { /// global context that handles events - /// limitations: GUI key modifier is never detected, key up events are not detected, and mouse up events are not specific to a key + /// limitations: GUI key modifier is never detected, key up events are not detected, and only two mouse buttons may be pressed at once pub fn new( mut receiver: UnboundedReceiver, cx: &ScopeState, From 7a1fdeb673eee58e63902b36cb1515a72e4d2c00 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 7 Feb 2022 05:57:57 -0600 Subject: [PATCH 25/32] added more mouse events --- examples/hover.rs | 102 +++++++++++++++++++++ src/hooks.rs | 222 ++++++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 14 ++- 3 files changed, 319 insertions(+), 19 deletions(-) create mode 100644 examples/hover.rs diff --git a/examples/hover.rs b/examples/hover.rs new file mode 100644 index 000000000..10916ba2e --- /dev/null +++ b/examples/hover.rs @@ -0,0 +1,102 @@ +use std::{convert::TryInto, sync::Arc}; + +use dioxus::{events::MouseData, prelude::*}; + +fn main() { + rink::launch(app); +} + +fn app(cx: Scope) -> Element { + fn to_str(c: &[i32; 3]) -> String { + "#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::() + } + + fn get_brightness(m: Arc) -> i32 { + let mb = m.buttons; + let b: i32 = m.buttons.count_ones().try_into().unwrap(); + 127 * b + } + + let (q1_color, set_q1_color) = use_state(&cx, || [200; 3]); + let (q2_color, set_q2_color) = use_state(&cx, || [200; 3]); + let (q3_color, set_q3_color) = use_state(&cx, || [200; 3]); + let (q4_color, set_q4_color) = use_state(&cx, || [200; 3]); + + let q1_color_str = to_str(q1_color); + let q2_color_str = to_str(q2_color); + let q3_color_str = to_str(q3_color); + let q4_color_str = to_str(q4_color); + + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + + div { + width: "100%", + height: "50%", + flex_direction: "row", + div { + border_width: "1px", + width: "50%", + height: "100%", + justify_content: "center", + align_items: "center", + background_color: "{q1_color_str}", + onmouseenter: move |m| set_q1_color([get_brightness(m.data), 0, 0]), + onmousedown: move |m| set_q1_color([get_brightness(m.data), 0, 0]), + onmouseup: move |m| set_q1_color([get_brightness(m.data), 0, 0]), + onwheel: move |w| set_q1_color([q1_color[0] + (10.0*w.delta_y) as i32, 0, 0]), + onmouseleave: move |_| set_q1_color([200; 3]), + "click me" + } + div { + width: "50%", + height: "100%", + justify_content: "center", + align_items: "center", + background_color: "{q2_color_str}", + onmouseenter: move |m| set_q2_color([get_brightness(m.data); 3]), + onmousedown: move |m| set_q2_color([get_brightness(m.data); 3]), + onmouseup: move |m| set_q2_color([get_brightness(m.data); 3]), + onwheel: move |w| set_q2_color([q2_color[0] + (10.0*w.delta_y) as i32;3]), + onmouseleave: move |_| set_q2_color([200; 3]), + "click me" + } + } + + div { + width: "100%", + height: "50%", + flex_direction: "row", + div { + width: "50%", + height: "100%", + justify_content: "center", + align_items: "center", + background_color: "{q3_color_str}", + onmouseenter: move |m| set_q3_color([0, get_brightness(m.data), 0]), + onmousedown: move |m| set_q3_color([0, get_brightness(m.data), 0]), + onmouseup: move |m| set_q3_color([0, get_brightness(m.data), 0]), + onwheel: move |w| set_q3_color([0, q3_color[1] + (10.0*w.delta_y) as i32, 0]), + onmouseleave: move |_| set_q3_color([200; 3]), + "click me" + } + div { + width: "50%", + height: "100%", + justify_content: "center", + align_items: "center", + background_color: "{q4_color_str}", + onmouseenter: move |m| set_q4_color([0, 0, get_brightness(m.data)]), + onmousedown: move |m| set_q4_color([0, 0, get_brightness(m.data)]), + onmouseup: move |m| set_q4_color([0, 0, get_brightness(m.data)]), + onwheel: move |w| set_q4_color([0, 0, q4_color[2] + (10.0*w.delta_y) as i32]), + onmouseleave: move |_| set_q4_color([200; 3]), + "click me" + } + } + } + }) +} diff --git a/src/hooks.rs b/src/hooks.rs index 21858a662..dd0469ab1 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -7,12 +7,15 @@ use dioxus_html::{on::*, KeyCode}; use futures::{channel::mpsc::UnboundedReceiver, StreamExt}; use std::{ any::Any, - borrow::BorrowMut, 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 @@ -108,6 +111,7 @@ impl InnerInputState { 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 @@ -129,7 +133,6 @@ impl InnerInputState { } state.0.buttons = buttons; m.buttons = buttons; - // println!("{buttons}") } None => { self.mouse = Some(( @@ -151,17 +154,194 @@ impl InnerInputState { .filter(|k2| k2.0.key == k.key && k2.1.elapsed() < MAX_REPEAT_TIME) .is_some(); k.repeat = repeat; - let mut new = clone_keyboard_data(k); - new.repeat = repeat; + let new = clone_keyboard_data(k); self.last_key_pressed = Some((new, Instant::now())); } } } - fn update(&mut self, evts: &mut [EventCore]) { - for e in evts { - self.apply_event(e) + fn update<'a>( + &mut self, + dom: &'a VirtualDom, + evts: &mut Vec, + resolved_events: &mut Vec, + layout: &Stretch, + layouts: &mut HashMap>, + node: &'a VNode<'a>, + ) { + struct Data<'b> { + new_pos: (i32, i32), + old_pos: Option<(i32, i32)>, + clicked: bool, + released: bool, + wheel_delta: f64, + mouse_data: &'b MouseData, + wheel_data: &'b Option, + }; + + fn layout_contains_point(layout: &Layout, point: (i32, i32)) -> bool { + layout.location.x as i32 <= point.0 + && layout.location.x as i32 + layout.size.width as i32 >= point.0 + && layout.location.y as i32 <= point.1 + && layout.location.y as i32 + layout.size.height as i32 >= point.1 } + + fn get_mouse_events<'c, 'd>( + dom: &'c VirtualDom, + resolved_events: &mut Vec, + layout: &Stretch, + layouts: &HashMap>, + node: &'c VNode<'c>, + data: &'d Data<'d>, + ) -> HashSet<&'static str> { + match node { + VNode::Fragment(f) => { + let mut union = HashSet::new(); + for child in f.children { + union = union + .union(&get_mouse_events( + dom, + resolved_events, + layout, + layouts, + child, + data, + )) + .copied() + .collect(); + } + return union; + } + + VNode::Component(vcomp) => { + let idx = vcomp.scope.get().unwrap(); + let new_node = dom.get_scope(idx).unwrap().root_node(); + return get_mouse_events(dom, resolved_events, layout, layouts, new_node, data); + } + + VNode::Placeholder(_) => return HashSet::new(), + + VNode::Element(_) | VNode::Text(_) => {} + } + + let id = node.try_mounted_id().unwrap(); + let node = layouts.get(&id).unwrap(); + + let node_layout = layout.layout(node.layout).unwrap(); + + let previously_contained = data + .old_pos + .filter(|pos| layout_contains_point(node_layout, *pos)) + .is_some(); + let currently_contains = layout_contains_point(node_layout, data.new_pos); + + match node.node { + VNode::Element(el) => { + let mut events = HashSet::new(); + if previously_contained || currently_contains { + for c in el.children { + events = events + .union(&get_mouse_events( + dom, + resolved_events, + layout, + layouts, + c, + data, + )) + .copied() + .collect(); + } + } + let mut try_create_event = |name| { + // only trigger event if the event was not triggered already by a child + if events.insert(name) { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name, + element: Some(el.id.get().unwrap()), + data: Arc::new(clone_mouse_data(data.mouse_data)), + }) + } + }; + if currently_contains { + if !previously_contained { + try_create_event("mouseenter"); + try_create_event("mouseover"); + } + if data.clicked { + try_create_event("mousedown"); + } + if data.released { + try_create_event("mouseup"); + match data.mouse_data.button { + 0 => try_create_event("click"), + 2 => try_create_event("contextmenu"), + _ => (), + } + } + if let Some(w) = data.wheel_data { + if data.wheel_delta != 0.0 { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: "wheel", + element: Some(el.id.get().unwrap()), + data: Arc::new(clone_wheel_data(w)), + }) + } + } + } else { + if previously_contained { + try_create_event("mouseleave"); + try_create_event("mouseout"); + } + } + events + } + VNode::Text(_) => HashSet::new(), + _ => todo!(), + } + } + + let previous_mouse = self + .mouse + .as_ref() + .map(|m| (clone_mouse_data(&m.0), m.1.clone())); + // println!("{previous_mouse:?}"); + + self.wheel = None; + + for e in evts.iter_mut() { + self.apply_event(e); + } + + // resolve hover events + if let Some(mouse) = &self.mouse { + let new_pos = (mouse.0.screen_x, mouse.0.screen_y); + let old_pos = previous_mouse + .as_ref() + .map(|m| (m.0.screen_x, m.0.screen_y)); + let clicked = + (!mouse.0.buttons & previous_mouse.as_ref().map(|m| m.0.buttons).unwrap_or(0)) > 0; + let released = + (mouse.0.buttons & !previous_mouse.map(|m| m.0.buttons).unwrap_or(0)) > 0; + let wheel_delta = self.wheel.as_ref().map_or(0.0, |w| w.delta_y); + let mouse_data = &mouse.0; + let wheel_data = &self.wheel; + let data = Data { + new_pos, + old_pos, + clicked, + released, + wheel_delta, + mouse_data, + wheel_data, + }; + get_mouse_events(dom, resolved_events, layout, layouts, node, &data); + } + // for s in &self.subscribers { // s(); // } @@ -210,7 +390,13 @@ impl RinkInputHandler { ) } - pub fn resolve_events(&self, dom: &mut VirtualDom) { + pub fn get_events<'a>( + &self, + dom: &'a VirtualDom, + layout: &Stretch, + layouts: &mut HashMap>, + node: &'a VNode<'a>, + ) -> Vec { // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus fn inner( queue: &Vec<(&'static str, Arc)>, @@ -249,25 +435,31 @@ impl RinkInputHandler { let mut resolved_events = Vec::new(); - (*self.state) - .borrow_mut() - .update(&mut (*self.queued_events).borrow_mut()); + (*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, dom.base_scope().root_node()); + inner(&events, &mut resolved_events, node); - for e in resolved_events { - dom.handle_message(SchedulerMsg::Event(e)); - } + 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) => { diff --git a/src/lib.rs b/src/lib.rs index df24c0b37..145e59d1b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,12 +90,10 @@ pub fn render_vdom( terminal.clear().unwrap(); loop { - // resolve events before rendering - handler.resolve_events(vdom); - /* -> 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 @@ -111,10 +109,11 @@ pub fn render_vdom( let root_node = vdom.base_scope().root_node(); layout::collect_layout(&mut layout, &mut nodes, vdom, root_node); /* - Compute the layout given th terminal size + 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 @@ -130,10 +129,17 @@ pub fn render_vdom( }, ) .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); 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(); From f6ef9981dd9204cab0eae364c3609ad821807331 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 12 Feb 2022 14:03:08 -0600 Subject: [PATCH 26/32] added border and more attributes --- Cargo.toml | 2 +- examples/border.rs | 30 +++ examples/text.rs | 33 +++- src/attributes.rs | 444 +++++++++++++++++++++++++++++++++++---------- src/layout.rs | 5 +- src/lib.rs | 10 +- src/render.rs | 336 +++++++++++++++++++++++++++++++++- 7 files changed, 745 insertions(+), 115 deletions(-) create mode 100644 examples/border.rs diff --git a/Cargo.toml b/Cargo.toml index 8b8b96616..1837ad5ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tui = { version = "0.16.0", features = ["crossterm"], default-features = false } +tui = "0.17.0" crossterm = "0.22.1" anyhow = "1.0.42" thiserror = "1.0.24" diff --git a/examples/border.rs b/examples/border.rs new file mode 100644 index 000000000..2e267b271 --- /dev/null +++ b/examples/border.rs @@ -0,0 +1,30 @@ +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()), + + // the border can either be solid, double, thick, OR rounded + // if multable are set only the last style is appiled + // to skip a side set the style to none + border_style: "solid none solid double", + border_width: "thick", + border_radius: "{radius}px", + border_color: "#0000FF #FF00FF #FF0000 #00FF00", + + "{radius}" + } + }) +} diff --git a/examples/text.rs b/examples/text.rs index db49cd733..21a7654ad 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -53,13 +53,6 @@ fn app(cx: Scope) -> Element { "zib" "zib" "zib" - "zib" - "zib" - "zib" - "zib" - "zib" - "zib" - "zib" } p { background_color: "yellow", @@ -77,6 +70,32 @@ fn app(cx: Scope) -> Element { 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: "rgb(50, 100, 255)", + "underline" + } + p { + text_decoration: "line-through", + color: "hsl(10, 100%, 70%)", + "line-through" + } } }) } diff --git a/src/attributes.rs b/src/attributes.rs index d84779406..b4a5691f3 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -30,15 +30,155 @@ */ use stretch2::{prelude::*, style::PositionType, style::Style}; -use tui::style::Style as TuiStyle; +use tui::style::{Color, Style as TuiStyle}; pub struct StyleModifer { pub style: Style, pub tui_style: TuiStyle, + pub tui_modifier: TuiModifier, } -enum TuiModifier { - Text, +pub struct TuiModifier { + // border arrays start at the top and proceed clockwise + pub border_colors: [Option; 4], + pub border_types: [BorderType; 4], + pub border_widths: [UnitSystem; 4], + pub border_radi: [UnitSystem; 4], +} + +#[derive(Clone, Copy)] +pub enum BorderType { + DOTTED, + DASHED, + SOLID, + DOUBLE, + GROOVE, + RIDGE, + INSET, + OUTSET, + HIDDEN, + NONE, +} + +impl Default for TuiModifier { + fn default() -> Self { + Self { + border_colors: [None; 4], + border_types: [BorderType::NONE; 4], + border_widths: [UnitSystem::Point(0.0); 4], + border_radi: [UnitSystem::Point(0.0); 4], + } + } +} + +fn parse_color(color: &str) -> Option { + match color { + "red" => Some(Color::Red), + "green" => Some(Color::Green), + "blue" => Some(Color::Blue), + "yellow" => Some(Color::Yellow), + "cyan" => Some(Color::Cyan), + "magenta" => Some(Color::Magenta), + "white" => Some(Color::White), + "black" => Some(Color::Black), + _ => { + if color.len() == 7 && color.starts_with('#') { + 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 { + Some(Color::Rgb(values[0], values[1], values[2])) + } else { + None + } + } else if color.starts_with("rgb(") { + let mut values = [0, 0, 0]; + let mut color_ok = true; + for (v, i) in color[4..] + .trim_end_matches(')') + .split(',') + .zip(0..values.len()) + { + if let Ok(v) = v.trim().parse() { + values[i] = v; + } else { + color_ok = false; + } + } + if color_ok { + Some(Color::Rgb(values[0], values[1], values[2])) + } else { + None + } + } else if color.starts_with("hsl(") { + let mut values = [0, 0, 0]; + let mut color_ok = true; + for (v, i) in color[4..] + .trim_end_matches(')') + .split(',') + .zip(0..values.len()) + { + if let Ok(v) = v.trim_end_matches('%').trim().parse() { + values[i] = v; + } else { + color_ok = false; + } + } + if color_ok { + let [h, s, l] = [ + values[0] as f32 / 360.0, + values[1] as f32 / 100.0, + values[2] as f32 / 100.0, + ]; + 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, + ] + }; + + Some(Color::Rgb(rgb[0], rgb[1], rgb[2])) + } else { + None + } + } else { + None + } + } + } } /// applies the entire html namespace defined in dioxus-html @@ -117,7 +257,9 @@ pub fn apply_attributes( "clip" => {} "color" => { - // text color + if let Some(c) = parse_color(value) { + style.tui_style.fg.replace(c); + } } "column-count" @@ -170,14 +312,11 @@ pub fn apply_attributes( | "font-weight" => apply_font(name, value, style), "height" => { - if value.ends_with("%") { - if let Ok(pct) = value.trim_end_matches("%").parse::() { - style.style.size.height = Dimension::Percent(pct / 100.0); - } - } else if value.ends_with("px") { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.size.height = Dimension::Points(px); - } + 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" => { @@ -286,14 +425,11 @@ pub fn apply_attributes( "visibility" => {} "white-space" => {} "width" => { - if value.ends_with("%") { - if let Ok(pct) = value.trim_end_matches("%").parse::() { - style.style.size.width = Dimension::Percent(pct / 100.0); - } - } else if value.ends_with("px") { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.size.width = Dimension::Points(px); - } + 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" => {} @@ -304,7 +440,8 @@ pub fn apply_attributes( } } -enum UnitSystem { +#[derive(Clone, Copy)] +pub enum UnitSystem { Percent(f32), Point(f32), } @@ -344,7 +481,6 @@ fn apply_overflow(name: &str, value: &str, style: &mut StyleModifer) { } fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) { - use stretch2::style::Display; style.style.display = match value { "flex" => Display::Flex, "block" => Display::None, @@ -376,40 +512,9 @@ fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) { fn apply_background(name: &str, value: &str, style: &mut StyleModifer) { match name { "background-color" => { - use tui::style::Color; - match value { - "red" => style.tui_style.bg.replace(Color::Red), - "green" => style.tui_style.bg.replace(Color::Green), - "blue" => style.tui_style.bg.replace(Color::Blue), - "yellow" => style.tui_style.bg.replace(Color::Yellow), - "cyan" => style.tui_style.bg.replace(Color::Cyan), - "magenta" => style.tui_style.bg.replace(Color::Magenta), - "white" => style.tui_style.bg.replace(Color::White), - "black" => style.tui_style.bg.replace(Color::Black), - _ => { - if value.len() == 7 { - 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(&value[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) - { - values[i] = v; - } else { - color_ok = false; - } - } - if color_ok { - let color = Color::Rgb(values[0], values[1], values[2]); - style.tui_style.bg.replace(color) - } else { - None - } - } else { - None - } - } - }; + if let Some(c) = parse_color(value) { + style.tui_style.bg.replace(c); + } } "background" => {} "background-attachment" => {} @@ -423,17 +528,63 @@ fn apply_background(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_border(name: &str, value: &str, _style: &mut StyleModifer) { +fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { + fn parse_border_type(v: &str) -> BorderType { + match v { + "dotted" => BorderType::DOTTED, + "dashed" => BorderType::DASHED, + "solid" => BorderType::SOLID, + "double" => BorderType::DOUBLE, + "groove" => BorderType::GROOVE, + "ridge" => BorderType::RIDGE, + "inset" => BorderType::INSET, + "outset" => BorderType::OUTSET, + "none" => BorderType::NONE, + "hidden" => BorderType::HIDDEN, + _ => todo!(), + } + } match name { "border" => {} "border-bottom" => {} - "border-bottom-color" => {} - "border-bottom-left-radius" => {} - "border-bottom-right-radius" => {} - "border-bottom-style" => {} - "border-bottom-width" => {} + "border-bottom-color" => { + if let Some(c) = parse_color(value) { + style.tui_modifier.border_colors[2] = Some(c); + } + } + "border-bottom-left-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_radi[2] = v; + } + } + "border-bottom-right-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_radi[1] = v; + } + } + "border-bottom-style" => style.tui_modifier.border_types[2] = parse_border_type(value), + "border-bottom-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_widths[2] = v; + } + } "border-collapse" => {} - "border-color" => {} + "border-color" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + if let Some(c) = parse_color(values[0]) { + for i in 0..4 { + style.tui_modifier.border_colors[i] = Some(c); + } + } + } else { + for (i, v) in values.into_iter().enumerate() { + if let Some(c) = parse_color(v) { + style.tui_modifier.border_colors[i] = Some(c); + } + } + } + } "border-image" => {} "border-image-outset" => {} "border-image-repeat" => {} @@ -441,28 +592,89 @@ fn apply_border(name: &str, value: &str, _style: &mut StyleModifer) { "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" => { - if let Ok(_px) = value.trim_end_matches("px").parse::() { - // tuistyle = px; + "border-left-color" => { + if let Some(c) = parse_color(value) { + style.tui_modifier.border_colors[3] = Some(c); } } - _ => {} + "border-left-style" => style.tui_modifier.border_types[3] = parse_border_type(value), + "border-left-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_widths[3] = v; + } + } + "border-radius" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + if let Some(r) = parse_value(values[0]) { + for i in 0..4 { + style.tui_modifier.border_radi[i] = r; + } + } + } else { + for (i, v) in values.into_iter().enumerate() { + if let Some(r) = parse_value(v) { + style.tui_modifier.border_radi[i] = r; + } + } + } + } + "border-right" => {} + "border-right-color" => { + if let Some(c) = parse_color(value) { + style.tui_modifier.border_colors[1] = Some(c); + } + } + "border-right-style" => style.tui_modifier.border_types[1] = parse_border_type(value), + "border-right-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_widths[1] = v; + } + } + "border-spacing" => {} + "border-style" => { + let values: Vec<_> = value.split(' ').collect(); + if values.len() == 1 { + let border = parse_border_type(values[0]); + for i in 0..4 { + style.tui_modifier.border_types[i] = border; + } + } else { + for (i, v) in values.into_iter().enumerate() { + style.tui_modifier.border_types[i] = parse_border_type(v); + } + } + } + "border-top" => {} + "border-top-color" => { + if let Some(c) = parse_color(value) { + style.tui_modifier.border_colors[0] = Some(c); + } + } + "border-top-left-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_radi[3] = v; + } + } + "border-top-right-radius" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_radi[0] = v; + } + } + "border-top-style" => style.tui_modifier.border_types[0] = parse_border_type(value), + "border-top-width" => { + if let Some(v) = parse_value(value) { + style.tui_modifier.border_widths[0] = v; + } + } + "border-width" => { + if let Some(v) = parse_value(value) { + for i in 0..4 { + style.tui_modifier.border_widths[i] = v; + } + } + } + _ => (), } } @@ -516,14 +728,11 @@ fn apply_flex(name: &str, value: &str, style: &mut StyleModifer) { }; } "flex-basis" => { - if value.ends_with("%") { - if let Ok(pct) = value.trim_end_matches("%").parse::() { - style.style.flex_basis = Dimension::Percent(pct / 100.0); - } - } else if value.ends_with("px") { - if let Ok(px) = value.trim_end_matches("px").parse::() { - style.style.flex_basis = Dimension::Points(px); - } + 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" => {} @@ -550,8 +759,27 @@ fn apply_flex(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_font(_name: &str, _value: &str, _style: &mut StyleModifer) { - todo!() +fn apply_font(name: &str, value: &str, style: &mut StyleModifer) { + use tui::style::Modifier; + match name { + "font" => todo!(), + "font-family" => todo!(), + "font-size" => todo!(), + "font-size-adjust" => todo!(), + "font-stretch" => todo!(), + "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) { @@ -587,8 +815,34 @@ fn apply_padding(name: &str, value: &str, style: &mut StyleModifer) { } } -fn apply_text(_name: &str, _value: &str, _style: &mut StyleModifer) { - todo!() +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) { diff --git a/src/layout.rs b/src/layout.rs index 94f750990..d9cf6ad21 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -4,7 +4,7 @@ use tui::style::Style as TuiStyle; use crate::{ attributes::{apply_attributes, StyleModifer}, - TuiNode, + TuiModifier, TuiNode, }; /* @@ -42,6 +42,7 @@ pub fn collect_layout<'a>( TuiNode { node, block_style: tui::style::Style::default(), + tui_modifier: TuiModifier::default(), layout: layout.new_node(style, &[]).unwrap(), }, ); @@ -51,6 +52,7 @@ pub fn collect_layout<'a>( let mut modifier = StyleModifer { style: Style::default(), tui_style: TuiStyle::default(), + tui_modifier: TuiModifier::default(), }; for &Attribute { name, value, .. } in el.attributes { @@ -86,6 +88,7 @@ pub fn collect_layout<'a>( TuiNode { node, block_style: modifier.tui_style, + tui_modifier: modifier.tui_modifier, layout: layout.new_node(modifier.style, &child_layout).unwrap(), }, ); diff --git a/src/lib.rs b/src/lib.rs index 145e59d1b..c3cba6970 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ pub fn launch(app: Component<()>) { pub struct TuiNode<'a> { pub layout: stretch2::node::Node, pub block_style: TuiStyle, + pub tui_modifier: TuiModifier, pub node: &'a VNode<'a>, } @@ -132,7 +133,14 @@ pub fn render_vdom( // resolve events before rendering events = handler.get_events(vdom, &layout, &mut nodes, root_node); - render::render_vnode(frame, &layout, &mut nodes, vdom, root_node); + render::render_vnode( + frame, + &layout, + &mut nodes, + vdom, + root_node, + &TuiStyle::default(), + ); assert!(nodes.is_empty()); })?; diff --git a/src/render.rs b/src/render.rs index c997d5302..83b30046c 100644 --- a/src/render.rs +++ b/src/render.rs @@ -9,11 +9,320 @@ use tui::{ backend::CrosstermBackend, buffer::Buffer, layout::Rect, - style::Style as TuiStyle, - widgets::{Block, Widget}, + style::{Color, Style as TuiStyle}, + widgets::Widget, }; -use crate::TuiNode; +use crate::{BorderType, TuiNode, UnitSystem}; + +const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5]; + +impl<'a> Widget for TuiNode<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + use tui::symbols::line::*; + + enum Direction { + Left, + Right, + Up, + Down, + } + + fn draw( + buf: &mut Buffer, + points_history: [[i32; 2]; 3], + symbols: &Set, + pos: [u16; 2], + color: &Option, + ) { + let [before, current, after] = points_history; + let start_dir = match [before[0] - current[0], before[1] - current[1]] { + [1, 0] => Direction::Right, + [-1, 0] => Direction::Left, + [0, 1] => Direction::Down, + [0, -1] => Direction::Up, + [a, b] => { + panic!( + "draw({:?} {:?} {:?}) {}, {} no cell adjacent", + before, current, after, a, b + ) + } + }; + let end_dir = match [after[0] - current[0], after[1] - current[1]] { + [1, 0] => Direction::Right, + [-1, 0] => Direction::Left, + [0, 1] => Direction::Down, + [0, -1] => Direction::Up, + [a, b] => { + panic!( + "draw({:?} {:?} {:?}) {}, {} no cell adjacent", + before, current, after, a, b + ) + } + }; + + let cell = buf.get_mut( + (current[0] + pos[0] as i32) as u16, + (current[1] + pos[1] as i32) as u16, + ); + if let Some(c) = color { + cell.fg = *c; + } + 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(); + } + + fn draw_arc( + pos: [u16; 2], + starting_angle: f32, + arc_angle: f32, + radius: f32, + symbols: &Set, + buf: &mut Buffer, + color: &Option, + ) { + if radius < 0.0 { + return; + } + + let num_points = (radius * arc_angle) as i32; + let starting_point = [ + (starting_angle.cos() * (radius * RADIUS_MULTIPLIER[0])) as i32, + (starting_angle.sin() * (radius * RADIUS_MULTIPLIER[1])) as i32, + ]; + 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); + } + + if area.area() == 0 { + return; + } + + for i in 0..4 { + // the radius for the curve between this line and the next + let r = match self.tui_modifier.border_types[(i + 1) % 4] { + BorderType::HIDDEN => 0.0, + BorderType::NONE => 0.0, + _ => match self.tui_modifier.border_radi[i] { + 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), + }; + 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_idx = if i == 0 { 3 } else { i - 1 }; + let last_r = match self.tui_modifier.border_types[last_idx] { + BorderType::HIDDEN => 0.0, + BorderType::NONE => 0.0, + _ => match self.tui_modifier.border_radi[last_idx] { + 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), + }; + let last_radius = [ + (last_r * RADIUS_MULTIPLIER[0]) as u16, + (last_r * RADIUS_MULTIPLIER[1]) as u16, + ]; + + let symbols = match self.tui_modifier.border_types[i] { + BorderType::DOTTED => NORMAL, + BorderType::DASHED => NORMAL, + BorderType::SOLID => NORMAL, + BorderType::DOUBLE => DOUBLE, + BorderType::GROOVE => NORMAL, + BorderType::RIDGE => NORMAL, + BorderType::INSET => NORMAL, + BorderType::OUTSET => NORMAL, + BorderType::HIDDEN => continue, + BorderType::NONE => continue, + }; + + let color = self.tui_modifier.border_colors[i].or(self.block_style.fg); + + match i { + 0 => { + for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) { + let cell = buf.get_mut(x, area.top()); + if let Some(c) = color { + cell.fg = c; + } + cell.symbol = symbols.horizontal.to_string(); + } + } + 1 => { + for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) { + let cell = buf.get_mut(area.right() - 1, y); + if let Some(c) = color { + cell.fg = c; + } + cell.symbol = symbols.vertical.to_string(); + } + } + 2 => { + for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) { + let cell = buf.get_mut(x, area.bottom() - 1); + if let Some(c) = color { + cell.fg = c; + } + cell.symbol = symbols.horizontal.to_string(); + } + } + 3 => { + for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) { + let cell = buf.get_mut(area.left(), y); + if let Some(c) = color { + cell.fg = c; + } + cell.symbol = symbols.vertical.to_string(); + } + } + _ => (), + } + + match i { + 0 => 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, + buf, + &color, + ), + 1 => draw_arc( + [area.right() - radius[0] - 1, area.bottom() - radius[1] - 1], + 0.0, + std::f32::consts::FRAC_PI_2, + r, + &symbols, + buf, + &color, + ), + 2 => 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, + buf, + &color, + ), + 3 => draw_arc( + [area.left() + radius[0], area.top() + radius[1]], + std::f32::consts::PI, + std::f32::consts::FRAC_PI_2, + r, + &symbols, + buf, + &color, + ), + _ => panic!("more than 4 sides?"), + } + } + + // todo: only render inside borders + for x in area.left()..area.right() { + for y in area.top()..area.bottom() { + let cell = buf.get_mut(x, y); + if let Some(c) = self.block_style.bg { + cell.bg = c; + } + } + } + } +} pub fn render_vnode<'a>( frame: &mut tui::Frame>, @@ -21,11 +330,13 @@ pub fn render_vnode<'a>( layouts: &mut HashMap>, vdom: &'a VirtualDom, node: &'a VNode<'a>, + // this holds the parents syle state for styled text rendering and potentially transparentcy + style: &TuiStyle, ) { match node { VNode::Fragment(f) => { for child in f.children { - render_vnode(frame, layout, layouts, vdom, child); + render_vnode(frame, layout, layouts, vdom, child, style); } return; } @@ -33,7 +344,7 @@ pub fn render_vnode<'a>( 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); + render_vnode(frame, layout, layouts, vdom, new_node, style); return; } @@ -55,11 +366,12 @@ pub fn render_vnode<'a>( #[derive(Default)] struct Label<'a> { text: &'a str, + style: TuiStyle, } impl<'a> Widget for Label<'a> { fn render(self, area: Rect, buf: &mut Buffer) { - buf.set_string(area.left(), area.top(), self.text, TuiStyle::default()); + buf.set_string(area.left(), area.top(), self.text, self.style); } } @@ -67,7 +379,10 @@ pub fn render_vnode<'a>( // Block::default(). - let label = Label { text: t.text }; + 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 @@ -76,16 +391,17 @@ pub fn render_vnode<'a>( } } VNode::Element(el) => { - let block = Block::default().style(node.block_style); let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); + let new_style = style.patch(node.block_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(block, area); + frame.render_widget(node, area); } for el in el.children { - render_vnode(frame, layout, layouts, vdom, el); + render_vnode(frame, layout, layouts, vdom, el, &new_style); } } VNode::Fragment(_) => todo!(), From 7a1d8f05320dbcaef410e7df99634b7b0d228107 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 17 Feb 2022 16:06:28 -0600 Subject: [PATCH 27/32] added alpha channel --- examples/border.rs | 3 - examples/color_test.rs | 48 +++++ examples/text.rs | 34 +++- src/attributes.rs | 132 ++---------- src/config.rs | 20 ++ src/layout.rs | 7 +- src/lib.rs | 19 +- src/render.rs | 286 +++++++++++++------------- src/style.rs | 453 +++++++++++++++++++++++++++++++++++++++++ src/widget.rs | 96 +++++++++ 10 files changed, 821 insertions(+), 277 deletions(-) create mode 100644 examples/color_test.rs create mode 100644 src/config.rs create mode 100644 src/style.rs create mode 100644 src/widget.rs diff --git a/examples/border.rs b/examples/border.rs index 2e267b271..1d9f681b7 100644 --- a/examples/border.rs +++ b/examples/border.rs @@ -16,9 +16,6 @@ fn app(cx: Scope) -> Element { background_color: "hsl(248, 53%, 58%)", onwheel: move |w| set_radius((radius + w.delta_y as i8).abs()), - // the border can either be solid, double, thick, OR rounded - // if multable are set only the last style is appiled - // to skip a side set the style to none border_style: "solid none solid double", border_width: "thick", border_radius: "{radius}px", diff --git a/examples/color_test.rs b/examples/color_test.rs new file mode 100644 index 000000000..4aeefee8c --- /dev/null +++ b/examples/color_test.rs @@ -0,0 +1,48 @@ +use dioxus::prelude::*; + +fn main() { + // rink::launch(app); + rink::launch_cfg( + app, + rink::Config { + rendering_mode: rink::RenderingMode::Ansi, + }, + ); +} + +fn app(cx: Scope) -> Element { + let steps = 50; + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..=steps).map(|x| + { + let hue = x as f32*360.0/steps as f32; + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..=steps).map(|y| + { + let alpha = y as f32*100.0/steps as f32; + cx.render(rsx! { + div { + left: "{x}px", + top: "{y}px", + width: "10%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + } + }) + } + ) + } + }) + } + ) + } + }) +} diff --git a/examples/text.rs b/examples/text.rs index 21a7654ad..a38dc0b8d 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -2,9 +2,17 @@ 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%", @@ -13,7 +21,9 @@ fn app(cx: Scope) -> Element { // justify_content: "center", // align_items: "center", // flex_direction: "row", - // background_color: "red", + onwheel: move |evt| { + set_alpha((alpha + evt.data.delta_y as i64).min(100).max(0)); + }, p { background_color: "black", @@ -21,6 +31,7 @@ fn app(cx: Scope) -> Element { justify_content: "center", align_items: "center", // height: "10%", + color: "green", "hi" "hi" "hi" @@ -73,7 +84,7 @@ fn app(cx: Scope) -> Element { div { font_weight: "bold", color: "#666666", - p{ + p { "bold" } p { @@ -88,14 +99,29 @@ fn app(cx: Scope) -> Element { } p { text_decoration: "underline", - color: "rgb(50, 100, 255)", + color: "rgba(255, 255, 255)", "underline" } p { text_decoration: "line-through", - color: "hsl(10, 100%, 70%)", + color: "hsla(10, 100%, 70%)", "line-through" } + div{ + position: "absolute", + top: "1px", + background_color: "rgba(255, 0, 0, 50%)", + width: "100%", + p { + color: "rgba(255, 255, 255, {alpha}%)", + background_color: "rgba(100, 100, 100, {alpha}%)", + "rgba(255, 255, 255, {alpha}%)" + } + p { + color: "rgba(255, 255, 255, 100%)", + "rgba(255, 255, 255, 100%)" + } + } } }) } diff --git a/src/attributes.rs b/src/attributes.rs index b4a5691f3..8ef9af46a 100644 --- a/src/attributes.rs +++ b/src/attributes.rs @@ -32,15 +32,17 @@ use stretch2::{prelude::*, style::PositionType, style::Style}; use tui::style::{Color, Style as TuiStyle}; +use crate::style::{RinkColor, RinkStyle}; + pub struct StyleModifer { pub style: Style, - pub tui_style: TuiStyle, + pub tui_style: RinkStyle, pub tui_modifier: TuiModifier, } pub struct TuiModifier { // border arrays start at the top and proceed clockwise - pub border_colors: [Option; 4], + pub border_colors: [Option; 4], pub border_types: [BorderType; 4], pub border_widths: [UnitSystem; 4], pub border_radi: [UnitSystem; 4], @@ -71,116 +73,6 @@ impl Default for TuiModifier { } } -fn parse_color(color: &str) -> Option { - match color { - "red" => Some(Color::Red), - "green" => Some(Color::Green), - "blue" => Some(Color::Blue), - "yellow" => Some(Color::Yellow), - "cyan" => Some(Color::Cyan), - "magenta" => Some(Color::Magenta), - "white" => Some(Color::White), - "black" => Some(Color::Black), - _ => { - if color.len() == 7 && color.starts_with('#') { - 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 { - Some(Color::Rgb(values[0], values[1], values[2])) - } else { - None - } - } else if color.starts_with("rgb(") { - let mut values = [0, 0, 0]; - let mut color_ok = true; - for (v, i) in color[4..] - .trim_end_matches(')') - .split(',') - .zip(0..values.len()) - { - if let Ok(v) = v.trim().parse() { - values[i] = v; - } else { - color_ok = false; - } - } - if color_ok { - Some(Color::Rgb(values[0], values[1], values[2])) - } else { - None - } - } else if color.starts_with("hsl(") { - let mut values = [0, 0, 0]; - let mut color_ok = true; - for (v, i) in color[4..] - .trim_end_matches(')') - .split(',') - .zip(0..values.len()) - { - if let Ok(v) = v.trim_end_matches('%').trim().parse() { - values[i] = v; - } else { - color_ok = false; - } - } - if color_ok { - let [h, s, l] = [ - values[0] as f32 / 360.0, - values[1] as f32 / 100.0, - values[2] as f32 / 100.0, - ]; - 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, - ] - }; - - Some(Color::Rgb(rgb[0], rgb[1], rgb[2])) - } else { - None - } - } else { - None - } - } - } -} - /// applies the entire html namespace defined in dioxus-html pub fn apply_attributes( // @@ -257,7 +149,7 @@ pub fn apply_attributes( "clip" => {} "color" => { - if let Some(c) = parse_color(value) { + if let Ok(c) = value.parse() { style.tui_style.fg.replace(c); } } @@ -512,7 +404,7 @@ fn apply_display(_name: &str, value: &str, style: &mut StyleModifer) { fn apply_background(name: &str, value: &str, style: &mut StyleModifer) { match name { "background-color" => { - if let Some(c) = parse_color(value) { + if let Ok(c) = value.parse() { style.tui_style.bg.replace(c); } } @@ -548,7 +440,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { "border" => {} "border-bottom" => {} "border-bottom-color" => { - if let Some(c) = parse_color(value) { + if let Ok(c) = value.parse() { style.tui_modifier.border_colors[2] = Some(c); } } @@ -572,14 +464,14 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { "border-color" => { let values: Vec<_> = value.split(' ').collect(); if values.len() == 1 { - if let Some(c) = parse_color(values[0]) { + if let Ok(c) = values[0].parse() { for i in 0..4 { style.tui_modifier.border_colors[i] = Some(c); } } } else { for (i, v) in values.into_iter().enumerate() { - if let Some(c) = parse_color(v) { + if let Ok(c) = v.parse() { style.tui_modifier.border_colors[i] = Some(c); } } @@ -593,7 +485,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { "border-image-width" => {} "border-left" => {} "border-left-color" => { - if let Some(c) = parse_color(value) { + if let Ok(c) = value.parse() { style.tui_modifier.border_colors[3] = Some(c); } } @@ -621,7 +513,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { } "border-right" => {} "border-right-color" => { - if let Some(c) = parse_color(value) { + if let Ok(c) = value.parse() { style.tui_modifier.border_colors[1] = Some(c); } } @@ -647,7 +539,7 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) { } "border-top" => {} "border-top-color" => { - if let Some(c) = parse_color(value) { + if let Ok(c) = value.parse() { style.tui_modifier.border_colors[0] = Some(c); } } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 000000000..46a0926b8 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,20 @@ +#[derive(Default, Clone, Copy)] +pub struct Config { + pub rendering_mode: RenderingMode, +} + +#[derive(Clone, Copy)] +pub enum RenderingMode { + /// only 16 colors by accessed by name, no alpha support + BaseColors, + /// 8 bit colors, will be downsampled from rgb colors + Ansi, + /// 24 bit colors, most terminals support this + Rgb, +} + +impl Default for RenderingMode { + fn default() -> Self { + RenderingMode::Rgb + } +} diff --git a/src/layout.rs b/src/layout.rs index d9cf6ad21..21b75db42 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -4,6 +4,7 @@ use tui::style::Style as TuiStyle; use crate::{ attributes::{apply_attributes, StyleModifer}, + style::RinkStyle, TuiModifier, TuiNode, }; @@ -41,7 +42,7 @@ pub fn collect_layout<'a>( id, TuiNode { node, - block_style: tui::style::Style::default(), + block_style: RinkStyle::default(), tui_modifier: TuiModifier::default(), layout: layout.new_node(style, &[]).unwrap(), }, @@ -51,7 +52,7 @@ pub fn collect_layout<'a>( // gather up all the styles from the attribute list let mut modifier = StyleModifer { style: Style::default(), - tui_style: TuiStyle::default(), + tui_style: RinkStyle::default(), tui_modifier: TuiModifier::default(), }; @@ -87,7 +88,7 @@ pub fn collect_layout<'a>( node.mounted_id(), TuiNode { node, - block_style: modifier.tui_style, + block_style: modifier.tui_style.into(), tui_modifier: modifier.tui_modifier, layout: layout.new_node(modifier.style, &child_layout).unwrap(), }, diff --git a/src/lib.rs b/src/lib.rs index c3cba6970..16d861435 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,19 +13,28 @@ use std::{ time::{Duration, Instant}, }; use stretch2::{prelude::Size, Stretch}; -use tui::{backend::CrosstermBackend, style::Style as TuiStyle, Terminal}; +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(); @@ -37,12 +46,12 @@ pub fn launch(app: Component<()>) { dom.rebuild(); - render_vdom(&mut dom, tx, handler).unwrap(); + render_vdom(&mut dom, tx, handler, cfg).unwrap(); } pub struct TuiNode<'a> { pub layout: stretch2::node::Node, - pub block_style: TuiStyle, + pub block_style: RinkStyle, pub tui_modifier: TuiModifier, pub node: &'a VNode<'a>, } @@ -51,6 +60,7 @@ pub fn render_vdom( vdom: &mut VirtualDom, ctx: UnboundedSender, handler: RinkInputHandler, + cfg: Config, ) -> Result<()> { // Setup input handling let (tx, mut rx) = unbounded(); @@ -139,7 +149,8 @@ pub fn render_vdom( &mut nodes, vdom, root_node, - &TuiStyle::default(), + &RinkStyle::default(), + cfg, ); assert!(nodes.is_empty()); })?; diff --git a/src/render.rs b/src/render.rs index 83b30046c..1dac33f86 100644 --- a/src/render.rs +++ b/src/render.rs @@ -5,20 +5,111 @@ use stretch2::{ prelude::{Layout, Size}, Stretch, }; -use tui::{ - backend::CrosstermBackend, - buffer::Buffer, - layout::Rect, - style::{Color, Style as TuiStyle}, - widgets::Widget, -}; +use tui::{backend::CrosstermBackend, layout::Rect}; -use crate::{BorderType, TuiNode, UnitSystem}; +use crate::{ + style::{RinkColor, RinkStyle}, + widget::{RinkBuffer, RinkCell, RinkWidget, WidgetWithContext}, + BorderType, Config, TuiNode, UnitSystem, +}; const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5]; -impl<'a> Widget for TuiNode<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { +pub fn render_vnode<'a>( + frame: &mut tui::Frame>, + layout: &Stretch, + layouts: &mut HashMap>, + vdom: &'a VirtualDom, + node: &'a VNode<'a>, + // this holds the accumulated syle state for styled text rendering + style: &RinkStyle, + cfg: Config, +) { + match node { + VNode::Fragment(f) => { + for child in f.children { + render_vnode(frame, layout, layouts, vdom, child, style, cfg); + } + return; + } + + VNode::Component(vcomp) => { + let idx = vcomp.scope.get().unwrap(); + let new_node = vdom.get_scope(idx).unwrap().root_node(); + render_vnode(frame, layout, layouts, vdom, new_node, style, cfg); + return; + } + + VNode::Placeholder(_) => return, + + VNode::Element(_) | VNode::Text(_) => {} + } + + let id = node.try_mounted_id().unwrap(); + let mut node = layouts.remove(&id).unwrap(); + + let Layout { location, size, .. } = layout.layout(node.layout).unwrap(); + + let Point { x, y } = location; + let Size { width, height } = size; + + match node.node { + VNode::Text(t) => { + #[derive(Default)] + struct Label<'a> { + text: &'a str, + style: RinkStyle, + } + + impl<'a> RinkWidget for Label<'a> { + fn render(self, area: Rect, mut buf: RinkBuffer) { + for (i, c) in self.text.char_indices() { + let mut new_cell = RinkCell::default(); + new_cell.set_style(self.style); + new_cell.symbol = c.to_string(); + buf.set(area.left() + i as u16, area.top(), &new_cell); + } + } + } + + // let s = Span::raw(t.text); + + // Block::default(). + + 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 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); + } + + 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 { @@ -29,11 +120,11 @@ impl<'a> Widget for TuiNode<'a> { } fn draw( - buf: &mut Buffer, + buf: &mut RinkBuffer, points_history: [[i32; 2]; 3], symbols: &Set, pos: [u16; 2], - color: &Option, + color: &Option, ) { let [before, current, after] = points_history; let start_dir = match [before[0] - current[0], before[1] - current[1]] { @@ -61,14 +152,11 @@ impl<'a> Widget for TuiNode<'a> { } }; - let cell = buf.get_mut( - (current[0] + pos[0] as i32) as u16, - (current[1] + pos[1] as i32) as u16, - ); + let mut new_cell = RinkCell::default(); if let Some(c) = color { - cell.fg = *c; + new_cell.fg = *c; } - cell.symbol = match [start_dir, end_dir] { + 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, @@ -87,6 +175,11 @@ impl<'a> Widget for TuiNode<'a> { ), } .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( @@ -95,8 +188,8 @@ impl<'a> Widget for TuiNode<'a> { arc_angle: f32, radius: f32, symbols: &Set, - buf: &mut Buffer, - color: &Option, + mut buf: &mut RinkBuffer, + color: &Option, ) { if radius < 0.0 { return; @@ -143,7 +236,7 @@ impl<'a> Widget for TuiNode<'a> { _ => todo!(), }; draw( - buf, + &mut buf, [points_history[0], points_history[1], connecting_point], &symbols, pos, @@ -152,7 +245,7 @@ impl<'a> Widget for TuiNode<'a> { points_history = [points_history[1], connecting_point, points_history[2]]; } - draw(buf, points_history, &symbols, pos, color); + draw(&mut buf, points_history, &symbols, pos, color); } } @@ -173,13 +266,24 @@ impl<'a> Widget for TuiNode<'a> { } }]; - draw(buf, points_history, &symbols, pos, color); + draw(&mut buf, points_history, &symbols, pos, color); } 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); + } + } + for i in 0..4 { // the radius for the curve between this line and the next let r = match self.tui_modifier.border_types[(i + 1) % 4] { @@ -231,41 +335,33 @@ impl<'a> Widget for TuiNode<'a> { let color = self.tui_modifier.border_colors[i].or(self.block_style.fg); + let mut new_cell = RinkCell::default(); + if let Some(c) = color { + new_cell.fg = c; + } match i { 0 => { for x in (area.left() + last_radius[0] + 1)..(area.right() - radius[0]) { - let cell = buf.get_mut(x, area.top()); - if let Some(c) = color { - cell.fg = c; - } - cell.symbol = symbols.horizontal.to_string(); + new_cell.symbol = symbols.horizontal.to_string(); + buf.set(x, area.top(), &new_cell); } } 1 => { for y in (area.top() + last_radius[1] + 1)..(area.bottom() - radius[1]) { - let cell = buf.get_mut(area.right() - 1, y); - if let Some(c) = color { - cell.fg = c; - } - cell.symbol = symbols.vertical.to_string(); + new_cell.symbol = symbols.vertical.to_string(); + buf.set(area.right() - 1, y, &new_cell); } } 2 => { for x in (area.left() + radius[0])..(area.right() - last_radius[0] - 1) { - let cell = buf.get_mut(x, area.bottom() - 1); - if let Some(c) = color { - cell.fg = c; - } - cell.symbol = symbols.horizontal.to_string(); + new_cell.symbol = symbols.horizontal.to_string(); + buf.set(x, area.bottom() - 1, &new_cell); } } 3 => { for y in (area.top() + radius[1])..(area.bottom() - last_radius[1] - 1) { - let cell = buf.get_mut(area.left(), y); - if let Some(c) = color { - cell.fg = c; - } - cell.symbol = symbols.vertical.to_string(); + new_cell.symbol = symbols.vertical.to_string(); + buf.set(area.left(), y, &new_cell); } } _ => (), @@ -278,7 +374,7 @@ impl<'a> Widget for TuiNode<'a> { std::f32::consts::FRAC_PI_2, r, &symbols, - buf, + &mut buf, &color, ), 1 => draw_arc( @@ -287,7 +383,7 @@ impl<'a> Widget for TuiNode<'a> { std::f32::consts::FRAC_PI_2, r, &symbols, - buf, + &mut buf, &color, ), 2 => draw_arc( @@ -296,7 +392,7 @@ impl<'a> Widget for TuiNode<'a> { std::f32::consts::FRAC_PI_2, r, &symbols, - buf, + &mut buf, &color, ), 3 => draw_arc( @@ -305,107 +401,11 @@ impl<'a> Widget for TuiNode<'a> { std::f32::consts::FRAC_PI_2, r, &symbols, - buf, + &mut buf, &color, ), _ => panic!("more than 4 sides?"), } } - - // todo: only render inside borders - for x in area.left()..area.right() { - for y in area.top()..area.bottom() { - let cell = buf.get_mut(x, y); - if let Some(c) = self.block_style.bg { - cell.bg = c; - } - } - } - } -} - -pub fn render_vnode<'a>( - frame: &mut tui::Frame>, - layout: &Stretch, - layouts: &mut HashMap>, - vdom: &'a VirtualDom, - node: &'a VNode<'a>, - // this holds the parents syle state for styled text rendering and potentially transparentcy - style: &TuiStyle, -) { - match node { - VNode::Fragment(f) => { - for child in f.children { - render_vnode(frame, layout, layouts, vdom, child, style); - } - 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); - return; - } - - VNode::Placeholder(_) => return, - - VNode::Element(_) | VNode::Text(_) => {} - } - - let id = node.try_mounted_id().unwrap(); - let 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: TuiStyle, - } - - impl<'a> Widget for Label<'a> { - fn render(self, area: Rect, buf: &mut Buffer) { - buf.set_string(area.left(), area.top(), self.text, self.style); - } - } - - // let s = Span::raw(t.text); - - // Block::default(). - - 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(label, area); - } - } - VNode::Element(el) => { - let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16); - - let new_style = style.patch(node.block_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(node, area); - } - - for el in el.children { - render_vnode(frame, layout, layouts, vdom, el, &new_style); - } - } - VNode::Fragment(_) => todo!(), - VNode::Component(_) => todo!(), - VNode::Placeholder(_) => todo!(), } } diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 000000000..ffe3aa568 --- /dev/null +++ b/src/style.rs @@ -0,0 +1,453 @@ +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 if other == Color::Reset { + self.color + } 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); + let c = 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, + ); + c + } + } + } +} + +fn parse_value( + v: &str, + current_max_output: f32, + required_max_output: f32, +) -> Result { + if v.ends_with('%') { + Ok((v[..v.len() - 1].trim().parse::()? / 100.0) * required_max_output) + } else { + Ok((v.trim().parse::()? / current_max_output) * required_max_output) + } +} + +pub struct ParseColorError; + +fn parse_hex(color: &str) -> Result { + let mut values = [0, 0, 0]; + let mut color_ok = true; + for i in 0..values.len() { + if let Ok(v) = u8::from_str_radix(&color[(1 + 2 * i)..(1 + 2 * (i + 1))], 16) { + values[i] = v; + } else { + color_ok = false; + } + } + if color_ok { + Ok(Color::Rgb(values[0], values[1], values[2])) + } else { + Err(ParseColorError) + } +} + +fn parse_rgb(color: &str) -> Result { + let mut values = [0, 0, 0]; + let mut color_ok = true; + for (v, i) in color.split(',').zip(0..values.len()) { + if let Ok(v) = parse_value(v.trim(), 255.0, 255.0) { + values[i] = v as u8; + } else { + color_ok = false; + } + } + if color_ok { + Ok(Color::Rgb(values[0], values[1], values[2])) + } else { + Err(ParseColorError) + } +} + +fn parse_hsl(color: &str) -> Result { + let mut values = [0.0, 0.0, 0.0]; + let mut color_ok = true; + for (v, i) in color.split(',').zip(0..values.len()) { + if let Ok(v) = parse_value(v.trim(), if i == 0 { 360.0 } else { 100.0 }, 1.0) { + values[i] = v; + } else { + color_ok = false; + } + } + if color_ok { + let [h, s, l] = values; + let rgb = if s == 0.0 { + [l as u8; 3] + } else { + fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 { + if t < 0.0 { + t += 1.0; + } + if t > 1.0 { + t -= 1.0; + } + if t < 1.0 / 6.0 { + p + (q - p) * 6.0 * t + } else if t < 1.0 / 2.0 { + q + } else if t < 2.0 / 3.0 { + p + (q - p) * (2.0 / 3.0 - t) * 6.0 + } else { + p + } + } + + let q = if l < 0.5 { + l * (1.0 + s) + } else { + l + s - l * s + }; + let p = 2.0 * l - q; + [ + (hue_to_rgb(p, q, h + 1.0 / 3.0) * 255.0) as u8, + (hue_to_rgb(p, q, h) * 255.0) as u8, + (hue_to_rgb(p, q, h - 1.0 / 3.0) * 255.0) as u8, + ] + }; + + Ok(Color::Rgb(rgb[0], rgb[1], rgb[2])) + } else { + Err(ParseColorError) + } +} + +impl FromStr for RinkColor { + type Err = ParseColorError; + + fn from_str(color: &str) -> Result { + match color { + "red" => Ok(RinkColor { + color: Color::Red, + alpha: 1.0, + }), + "black" => Ok(RinkColor { + color: Color::Black, + alpha: 1.0, + }), + "green" => Ok(RinkColor { + color: Color::Green, + alpha: 1.0, + }), + "yellow" => Ok(RinkColor { + color: Color::Yellow, + alpha: 1.0, + }), + "blue" => Ok(RinkColor { + color: Color::Blue, + alpha: 1.0, + }), + "magenta" => Ok(RinkColor { + color: Color::Magenta, + alpha: 1.0, + }), + "cyan" => Ok(RinkColor { + color: Color::Cyan, + alpha: 1.0, + }), + "gray" => Ok(RinkColor { + color: Color::Gray, + alpha: 1.0, + }), + "darkgray" => Ok(RinkColor { + color: Color::DarkGray, + alpha: 1.0, + }), + // light red does not exist + "orangered" => Ok(RinkColor { + color: Color::LightRed, + alpha: 1.0, + }), + "lightgreen" => Ok(RinkColor { + color: Color::LightGreen, + alpha: 1.0, + }), + "lightyellow" => Ok(RinkColor { + color: Color::LightYellow, + alpha: 1.0, + }), + "lightblue" => Ok(RinkColor { + color: Color::LightBlue, + alpha: 1.0, + }), + // light magenta does not exist + "orchid" => Ok(RinkColor { + color: Color::LightMagenta, + alpha: 1.0, + }), + "lightcyan" => Ok(RinkColor { + color: Color::LightCyan, + alpha: 1.0, + }), + "white" => Ok(RinkColor { + color: Color::White, + alpha: 1.0, + }), + _ => { + if color.len() == 7 && color.starts_with('#') { + parse_hex(color).map(|c| RinkColor { + color: c, + alpha: 1.0, + }) + } else if color.starts_with("rgb(") { + let color_values = color[4..].trim_end_matches(')'); + if color.matches(',').count() == 4 { + 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 color.starts_with("rgba(") { + let color_values = color[5..].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 color.starts_with("hsl(") { + let color_values = color[4..].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 color.starts_with("hsla(") { + let color_values = color[5..].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) / 6; + let g = (((v as u16 % 36) / 6) * 255 + 3) / 6; + let b = ((v as u16 % 6) * 255 + 3) / 6; + let vals = [v / 36, (v % 36) / 6, v % 6]; + [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], + }, + _ => todo!("{c:?}"), + } +} + +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: 24 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 * 6) / 255; + let g = (rgb[1] as u16 * 6) / 255; + let b = (rgb[2] as u16 * 6) / 255; + let idx = 16 + r * 36 + g * 6 + b; + Color::Indexed(idx as u8) + } + } + }, + } + } +} + +#[test] +fn rgb_to_ansi() { + for idx in 16..=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 && i <= 255); + } + } else { + panic!("color is not indexed") + } + } + for idx in 232..=255 { + let idxed = Color::Indexed(idx); + let rgb = to_rgb(idxed); + assert!(rgb[0] == rgb[1] && rgb[1] == rgb[2]); + } +} + +#[derive(Clone, Copy)] +pub struct RinkStyle { + pub fg: Option, + pub bg: Option, + pub add_modifier: Modifier, + pub sub_modifier: Modifier, +} + +impl Default for RinkStyle { + fn default() -> Self { + Self { + fg: Some(RinkColor { + color: Color::White, + alpha: 1.0, + }), + bg: None, + add_modifier: Modifier::empty(), + sub_modifier: Modifier::empty(), + } + } +} + +impl RinkStyle { + pub fn add_modifier(mut self, m: Modifier) -> Self { + self.sub_modifier.remove(m); + self.add_modifier.insert(m); + self + } + + pub fn remove_modifier(mut self, m: Modifier) -> Self { + self.add_modifier.remove(m); + self.sub_modifier.insert(m); + self + } + + pub fn merge(mut self, other: RinkStyle) -> Self { + self.fg = self.fg.or(other.fg); + self.add_modifier(other.add_modifier) + .remove_modifier(other.sub_modifier) + } +} + +impl Into