From e3171e8303700d6829880951dbc7184413df371b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 28 Jul 2021 10:52:38 -0400 Subject: [PATCH 001/310] 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 002/310] 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 003/310] 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 z_X8b=5st5@Wr_70ah!Ix5d;ODMAE|-hpO|fYpU}{SNckYoXP{EZ5UZS)@w_X)@y~w zSA1aRI6p6IPxfo6MM`vfpXy_rMvFz={)J-bq2&MOHJ%Z_4Yd2Wft1|e1Q)(8m5zpq z7S<+4Kd*?&!6x{rx5shfKA+34uiJDGm2P>WQjk$8AWE&{liC{%i<3ztu_6djC*}Wm zNqX$x?>CfNv+7*GpDL+FSY2Ca(2Ne+Qe74u+N@{d(n#t&B?zHw_51Ufp#ueWY`5D# zU8rO4u4LB&nEA%=*uqk?jL!!z+LZxn52oS+vTf}NP2d~nuMqU&{ju>t@y5ZQ%&*38 z3w!;Kg#of3$wB8CraqJYoNbmOzeABnD(LvPp*_l&bX`LUH>R ziiPCu=57GKHq8;Jd06F~9>L$wuyUo`&~;=LvJlGUe^6XF;EgL~gtizoec&$-1z^l_ z?;1?_+Sg;-hip#3@1T8R^gC#SjTH9ZY8_D}$GuTP->8jm?_M)c;FuE*TXGa(i5aU) zuM2X&x{nq}oLz?KLOoVlUP$iDcW&XL*+)g_cM@GoZ4Zg6V=u+F7TPse$3$y{$`3+IeyqvAz0gCa7uL{7^#{w% z5!Gz2dDlPLQ7>0|Baxr-!8C;S_@|S9C1qKGpVpVnw~5;JxP2#opHT78Uni7XwE?gH zdwsds$jK2~@ECRp%zw*EQleqb>~%?F(kfmDmtL8a;6EGl@-0ptI+#X%G)NhfU|f({ zL0)OF1usr8HSCex)@NUXBdW}Fvq6Ljn~mK}w#u+CRbRvhZ-J>x@LHnEBX$Ujo@Og3 zyL_i7?Np_v8iuECFUc>u3e1{2Zm?c;XS9aKcC&|9q73gqoZ|)OJ!n<8<|Lv#ei{vGunG{hA0#@WBXFfV3(U$**3KU+ul` zvZSzrlX<`UQj~QpW5jnnvZje2w4R3KWEM>XiyaRbpZBBW@|O`om#yBr?=_ zj>@NolTckpY1ed$>1KN^mMwd#i{J*P7pmzpzC%-dYM_!_&OS?|8sT#8ICQ&XL|6?;sd$mVR8I>0h+zLG{;Ky?n^mj zZf&B*;xYbNq0=ptn|@vLWR9(cvkudCLyJQ%xmh?DB>z0zB#|_KeCX{6yE?JJay@eE z{8_EUa+hlC$g1u;f^Bb#^$NA9rdj3_@6)h;bNEB|g%A6+t%)~Ze9#OHiIW7a*H#5E ziTruV?pCJln|<@l!S@8)r23%K`kK|%nS3ECkBLX~Fte3~S2Klchc)Ka>IVesgP!oK z2_ZIcqM@awOQDyDr9{!k$`4DN9R_0>UZR2sW zD_@9vqZ{W-6vLvNBKU`JD>j%u zeEJwzr4>u3wxNfw&f)9l5ieh-Ywx?-(m3PXNY|YM6X%kZkbUnk)h(7!LQEw=aVJh* zb$qnikg3k&-R~Z{Fn77VRE35%FF2$Aqip|zA_VE-yZ$A!dA$gzz7O*Jj`X1~r*HPE zd=t!&ulBCqNXl3_lk?5@R(a!II3*F@_=J9mYMFg*+A^h%X1h?RB-rft6n+a6FFK^G zpx3TS?)j|9kJJZ9I7?kOlEc)U`TL~%Dm^~(IGZuaYRgQK6W#;PAb2HlLfi>f;d;e0 zJ-_i!{b7IuQWwdqT*`7|Xyi4vXc?W=tI{A_)dvL}?ypC+P`*^|*^x5Q1Hy`xLRKTV znhlnD=w3%F)^7eI7wfmkXGteBL==0X&uS%Xl0I4MTb|Uu zKjeo@w5z-5opmxVZ7=5MSuM6xO;wEoe-b>Je2{yK9kz8iT4Ti*{@&>C}n&=Lusg4MFo;cU&_?jn91^9 zeth5Ou1{P0BAAuyjmAZg>UB=iR>7><`GaTYD%w*=J8BLyJjz7ko3q(#Q>o0{y!wgd z%#ZN7jx7Ibmq6WS+4#Ze*wkwFu&RSTm&a9dKo?b&a3`+^kItqdjXKto; z2D&QAzFXdm87|t~@Mz=aAscQkG+s}IBP*v3M+-F|@k#4uhKm{t1>EKQ?^WrD^%tmk z4lcV$3fnsVM0Woo$>i3FpB{4RNEB>J?mD;8ML>l)Z9wQ=4!%duaK*L&o8#mbUSTVbozqY%dDJ14unAj;1jcG_%M(?cKKL% zSR>LS3*l6pf525(4sx*w0;uh?g)6Xn+;gO@?@SiE)KLVnFuKq_u(b?#=UC>MEE_^# z7Lo$MwoHc5UKn9(R1tZg&w!wyc<&f_3-^O$=Q=)ve*m4ka5JiUVLEj)4PrIX$62ux zpa-jdoRYPdFV!-@s%WJAu3uaDkt43m-F?&MQ2)g~rPT+|+V`xPXBon3GjS4ytkmOv z?xhSPd7^^roOp77(IZV)ywT{QT}ho(JLDs$T@dP z*E=swByh`H&bA^(5DcFteYn#gL z(W^xeb!zh(@4643mV&TFa~+723|JzlpYR?D=#!;y~UpLqQ#G?V=r$RsG2e#!a=Xjw=! zj`?mq^?hjn(zC9LrN(-cXFF^l=NP#xptf{0g0z}e)i-STn?TFROc5G_t7W@0w`c>AjYbpiZ>8fGPS_noXzfv@|zO=*5g-8Y5!6-QM z3vH$3QC@-KwC4-Fqv;xS@r%@7EwX3V&UAFqJ!VPYVah!|-uS#Sq;gd0$W@JcIQ23Y zb8yPX4UJ0{Jwt?&OjRHBVDkT*)Jjnq@t&xmobvQWj%A7!bl4v0wi_7P@~YwBWacMyQ{Wz7#9 zw1~`o%}Z;G&I~znpLPVA*n2?SAZa?uw`&t%x~hWL5{qze#4#xes$z0ioo&A`nsLB1 zKe~Od-<_7@Y|}Tw;x1{QNj5;+3mqXx7Mtrk7TWa{Q%;ounEsFJRM9%nj!7$K&(^vH z=4kWor9>YQ>Lj6v7q2?I=XOK~f(|Dw$1I=aR^!358LkPi0?AUUwg%l;<83xw5x)@m zK9Q~?y0)yX%9mF?{En6DfzwlDRoa(t5m_~Tgat2|u3MqtRwV1YE0Z=cMzWeK@RB6h zy1rANWm}(D=_#G5zndHN$4`%lt=g}#`SrcgMID|=u{Ngm>`Ww5TX8OBWZLISJu#mN zfv{zmo-rMH<#{@yDpXs1?lH314^fXXIWUt>N6foB28gpdB$+%v;zTx>FGgQ>wJ%_+ z8v__~XunO}6|Hw|lS{8uro`PxiA4l&??iat1GKAyGl%HLxBY7OqC}2K1htwNs!w|P z{|zr3k5135T^*R4#wN`I>7#E*Ls)xg{vB!@w^D|KWBa0}hSPAqXyNuz0&9|1Iq^DA zez0w|_ryGh%xgB2uoh50IxF-vv^p&yj6&P$XQ}PO&3_tQU(7TA!$IyuDWoaODD*e&>#Qli#^xDKSABTD@_-w|ldH=lJv7w-P9_ zwYY#-iv77xXy2!0^Ep_gDJJf@%V6}-BkkV1;Ue@bpe4bFRAZ!MR<>#Z0-p4hPxaW; zAj)GaYiMez(UnRL$m&G(6&OTm{u?vZ9!;WB?3O-9qjS-VN-)$q%@DQ*k=cZ6zq_QDUT3PZ~JCcQ!5IA=fFVTyf^nh@1t1TGycjO)Qlya-0a4#HDR7PdGDb_tpgSh|V&z=Dy2;x{9O zLV~n!3sfXr4xN9wI!mC)XRtBglbKUl7@>L(e{DEO=>*4ak{bQpd-2IMc!T9(4&d;_ z>IKnK*f`m#m8+>T2zz&>V2p9)*c(aJ;m9Rd)bcM}cA^GZ!mu6?ODI21}?yP@%OThd&} z^%0aRI(TGaZ)WXaiPp)kGQKW_K(oWc;Z)jV=3r0MgNrIN zyT#X(uUuTCwj+feKe(EH@4i^3Y>{8GmBVC&B>O92>6SH^xkO;sx|+_No)0-JEO;!- z2s&cF!3oN?wNe?kaIFF8aHHg^@Hok8_ErT+sG`1LkHVHMvHrrG_95Qz&JP+odB!@K zddN@aI#ornPM=or3{J21gTBq5U+c_zs_YeNGi*%&{l@p|p35l!b;D(G!|6;@Gw=Ks#OA z!^lXo>1@N=JzgF4K}=H`B<>kg%6k`RtW7EyW}*0)E~UV|^sw8~$*;?RyZ_Ela&$+a z}n8DXNb`H)8aT1TQ zssj;sQrG^RHzW3x?mq#6Vr|V%)tRL znn6{ale#s8`7Mxm?q_hq0Vg@f=7pBA%>CbDR|cF>d6DV7kzGJ{Xea39TE7CLL&2|f zNXxNOeeAqSG1mf^KbUceQjJk8P;Y$Pxsv|O^^@_3vd|rV0`$Rv# zxcQl2L-?F!*nYb^5byN68{e-qDSL1CuX0THEDJLFdkfYQ%SK#=-Y0*JtsEanS??~Q z{s$E3C6;E6dwP#UoG+;u5amuX=f|rRhMdQ_tBzl!xgysrXDk|&X9mfid83vT0H%zd z%-B-hKrKu~Y|hC|FobCyRqE1JQ1*^6re84l^vh$MPX;>eMjORbmupGc>X*<8vcUNg zD8ie#)v=L3(9l|)_}$je=|$9$Y3MFBHH}PnZ%?p3BP9h*p`xNpoQDRs(*>LCID)%G zghK9?_s!>j0r_uIP~4ohnahhN8bs-!lwSKTHII^WMU=T;H!9g^j3m4C#J z!>tDMhd2e}r+EK(Jz`nwz0OmNLYx!SYDy@Jn6#ymz3L8i)ijE~%gL}(clR4NP+x~G zf^v^-McEQXR*)l`*SRna$@4oc1zZ}rbf1GEP=&9z6qnbv?KX$u0iV2si;tS-?m_nr z#;P;Fx0_n+F>rTiXHCMSzk%vLb}bfHqf@_0Uf5S4;@u#$xm%R=Y-@*S5qd8<6)`*W zzSbIoAwjM7m>1XtGrPSlsCTR<>QdF{KxZ2r%rCfmU=ZF9rgc=?*?%y(@O zM%fq7&FSoWv`D1jlgdEaoR&rjdboFp(3!fTf>}h*r5io1$I=C&FJwcLDS4IB8zkBm zdUl9wyiAxT;;t-1Iqs8Ud0cs(5sD=Svl)u!8WJt<)@g+->Cul&)Px9dCDKX|ycJE@ zT*u3@00!h8MB6kfG~mFs_lRBX>RV3b%u)ErE0^+8k|RsjGhHAx4M8z~c5VDXHyj~y zTaGMwp+|YUH1WAN4An3y?{kqJhPKNLLqQ^+{I|MLTb})EaVl_Y`TZ5>cZ@h$1ZHtE zrBE%`D&2fV=ax0%dsjrY25t38@5SH@ud6(?`+C-`9S<~zqfEn|%w-EU@mDq6Y zq1zFGVnNG3@y$7|cE7}YDQ)fgaU$Eg`^XuL`nOrlENcs4K33iLJ#QkFPX&dT^otz>wzq{>1gknjS619`92YKJB)IRAS|JB~Rl)TS}zufasj z6-QTCsU7JPaO7!f#m#Yhh6ZSkA0j>tQNsmeVqJYH|yCkE`n=e<9UR>`$F>J6~##~Fnj#I zZ0g)Rpz031esde=TA|AG;*R0%t$4|kdQ#5V2{~7^;kmDBY?=4$po`M`gqRnHE+_=t zaZ4B$Id0}KKl{46cY^wdoO(&jm`1NnvLPVd=4;X_?FE`|w8HYDnU9F?Xz4=G_b25( z{$ts>sxt;#O;f6Pj7@t0xdv4N8UtaNm8wH=(n?!EQDWK$$FCMp<}v$ft4und=Pi|I z$AL?xva8`5tHk#aJFKp3tY;Ed`esS1WW^9(BK$dN6}eu6IvgI!{9uTXm;>IGFiM0m zLoO5iA2&L!Uys%ClL?h2!7ml;0Wy@y9^z7OSx~y6>fn@T^P#dJQ6#NCK%L;(wbp)B zEE=76@XSa)s5H0F7GE~kXpk$kHRgUy6wIB+9DZWRKU*BtTuZ?8lO9TxX2*e)&PZKi zCiUU#g1rM0JZMRjGw+Gv9qV$yZT3Ls4a#*&NkRv(pf2+{GN|L^_GC!&6)(U;WR7$U z>T#C{K>;;K=D!&H(7LDHP;ChIrv@R@?yxAqX?&(sGDvn+8Qv|JtG4iZ(w$+p7 z%^dz@E|fcMh988T=EASkL)+AFi?rzwT#*nv3SYPlXSypF4abd64*8s4D~7v)p0kTp$kKAxsk;6_@FSKRK>g#dDD(7YTB|JipV+AccvB;h#|mwCv3)tAH#qsq>kA^Cg=R zF8GotUB8AiGz@KPgJq3l^l@n@Pic>qCcAKjqu}%HZ4BFtLPziI4xpgsHSX5pABi{D zO=WVIjwr@O{V~(PLlNuH{Y-k)e;%<_+>|$SoKKvskG9E<-UGatJz0~^*DAd%t}>YS zpyZEBaQFMZ)-znzqaIvpd09=`(E`+6xG2r%*UoJ7F3yp15YM};u3N|~cQkuu4h%O2 zIy1^2vMYO%WJNWo<9Kkg-~-r99D@3ZF&2J@yJvadh;hB>B(yrmqU7Q`_w^GMbW2bW z%*y1XUyxe9qL2<~i#@wQL9r`+1(pCBrqbX&c(IHRwKKQC%XLiV68yB2vT9zL8MJ8x zoxSr5T+<0TEtt_`UX5L*3D|-;*N;e+ICHARe4}590w!L_R`z{?Ij<8e)x&8k4zRAN z0HL8TA7*VWx*qQ@^Ak4wazBCqc8wsU`WC*mRJ<`T=NM7`$>=hdb6^j2(>+q75#o@l zsQg;eKbkLO#ICHoyoCWG9g$xBzE|t)A23oVaf&f^wp|t0t5&pdf}STj=|09JB3(^` zG4th;4HyG7(M-4UVTaAr$_697H7=yPwRy=?Q(*oSDUMuy1o70`MNr+P?!GSWZ;VlS zLORiQmGTaoi>HKqS{}FW-zj@=n{lI_eRb1TN^l+M4#^echJCp3d=5PTpmFMv%mL`l z$npB|yErq$uWjPh7>WuLLX)V{NZtX?ULn#9)nAVx?|pK%fPK|Orwt#}P@L}~+?QKCdmY^^Kr z!|&N#1~K_RZi>ARy~z3E3Jmy=R#X-sj)}9Kn_Vl-re37_*b#uvjzF*1 zs0kOq)k_w^sRaYL6=}q03L3AD>6_nZ;54Lvh(7%7~8nIN@kF_)gK8~L>?zH!+j?$MePCDI0S zZm{w;s!XzbU}yN50G>(HEZ)9@^hAvwn-)p=GqI6ll|;{0{n6KDdot)jF%Mx~kky z7N6LKG&zOfwKzG?77ZYjeb>kHJ+oSFIm3L@r?a?}+KmUhqDz%>4pQ{@1n0k&61F_; zw5$V`D{<)&RDa?=KY8?Xe{H+QmcP>Uj~A+ZDB$+Oz#FLmY9@zdJqp*Ty4Z7UJLw6$ z+_PL4!$0(>4nHLxY@@u0<4c=wbzShlwAByM@^PWEe${-h zwq}t5@;T-OY5nh1friPm1GoQ-yC(AieA?kyQk^C-hwuSXl!;G>1uYCVVx<6 z)?oX-5=WGrvqnsZXpYztFsHq<2B7!e-bk4p4H?;`EFNje)Ph1g1At*}DGjCysO;*G&oSmAj2B$-4%6yRAH_8ZlE$HAEOHI;(f zLja+5O7H69a4wc#ex9OTu`C8lX5{NdJ+-ZiT4X(C7&i=}wx*{>7qvwV&jYFHI3KN# z;r)&n9^;WVKLbI`Ov=)G-_D;jf*pD-bPkXlXhrK2m%=EzU%fUh zn8`FbbbCa|hV`LGHn(nDxaQYYYBV3nzZWKSE29Cb5kA{H6j5}xzpgn~iQynxr&W5K zSSR=-Kwg8{wtXYy>z0t@9)q_p%pS9)-}x>nm0>Cz@CGbY~#&4f?_vSCv* z*G!_51*3fHkBaEh_@}nc*xy=h`Y5y?wp+x1`s-S4Eg(Z0^qpUcqoCjlbydrV^)bP? zLe#zV7F+&pS<&F_A1{ljCDYm>TvI4Gf^e9Sskj-5A2l?ZtylyE6Dg{xIZo`^Ou|L@ zsem6}_pjUJ&M1|XR#WxlkepdSvR|uJ+9DpU_8lDD(Jx(H8Fx z>28`uf4fzZ@B@v6c0vX~;51m*5&Bpm>nO*!^=$SzzD(8|f2bmi*n4xOSfA~AFtVp+ z=Uc^sD66;K+O9{Y@#i=gM1Ve^W~Y>0AWPOI)+IZw_pX0hoLzNzu(>ZN($E9cZy1Sg z=q2ft!cP^%3X=wvN~|jU>^l5*UikL94lTcP`4stnM5yJ1^lOwO)0mEAKAuT0Y}A{& z3TECSbvkPB;}QVxO}4a9nL=KO-c?=ZAbPeMw=%NEj3tAiDo?v_6ZlC}&jS4Ayg8=h z?l719YW#b1i#CWA<^-Vl-_S3&xi5!S?C(J?j@9Pvf1olx=zZ9;b_FPm1HEFm^@o*; z?kUM$MC$)K>b$LqQN~M&uHpQTyu}{(4D8Bv>0KEyH=AdGN1tOM<*?|x=7K#LPo1fi z)%qVJR5{j?Ab=V_r10%`U>JvITer8B&nUHo+RNYlxGBnub;YinXUd|#mp7X(-9%w2 zkC26Vyc;1ad%%r`-`dqgMf`r8hWM5|>Ih`bzhZ<0HDdG*hsS6*78xUsCapKw8jivI ztn#`Eu|v$NfEu!;lSuKy_XaD%B|QQVTwgZ35$`AYE|MtlY84WHCao|HJaj=H)2C;1 z;HT^9U7LD&x&7+pcbkh{Di3^f&}7h08i;2{XQUsF)##!&)T%&gfrup>;+U%SKz&;p zVfN#6$zB1M$Cplw*77%NT1X&h`2BMfg<~kZnbZ0mjNVggu!x^bF5*IiC4%gHS#9`1 z!oKTgfehWj^Qr?gRx1G3PVHYJ%5c`~-uv>VN7y?~$VK-D$e8?*X}R;pgh9Ex>6WDb6LGG(cDt}ofcuh|N` zR4Lu%>IG<=0c95WfX&BOMV#&?ZzqnT<%!BV;dg**BxTCaFHE0}_QR)_#%k`^MUmZB z%K%40ijOH@Vj0Nr`3=D@V0Jz<2qc zljrHr;w%?kvo|N?m;4L)RVvH;d%*#9r7t=9hCTItNgOl}6=B@@d0PrO6zpL#tWX=4 zn{tLR*&L4vO)ABucs}pVcGuC3zvRFagsXygI$b(oam=Obb@F;}>h0NcdA>flln|Gb zkVCIuLF9V?Ew1AgNhb}eMediMAiIsXqsEjygbuD>SJ{R}bw8E38Ut1dr955=f|aic zgLy(eJlh)FRGwE;W#6oEyTD{Mm(&pl60Vtw*JD22gv9r;Q|7zQ-CQ?g@jaJh^Sjc= zaH$EVrL8Lr*QC&;f_&*gGrDgKJMA_^HDgII!c_`&(~AmM&(^hVVLb+Jfgm0+N{&?> z?(h>@O}ruMV?{B0bgEjsL8Q~ z2xk*knh(-0z?}djEx8T%#c*U z_Oim{yH01fo6IdZNo#3DyyFwygW1}- zJ<(J3?A$96h1<*VpTT-OLyUZz_aF1X8r$;Q_btipo~RvDOY@i0_wU(vFi->_qXabs zEfZwD`ai#*Wn@o1;qOzqA`%QURiu^;?Oe<*+^uy-eMKj&vTn4xvZqoT7qR>)o@ij_ z?VgS;D^stW{(fpOY|$*<4`Wrvf1^n1?%-zuy1UlElXuSdjU6&~&wkn$5Sx9qL0wRD zf9TBVG-<0#2Y)#DTzg@&ZPyf@>7_7CxLB=0z(zsm4I9&><@?6J<-?bEP^JSbJrQ8- zskY+3Wy4lD#B?s5e(mwsen;LP55URXB}W80_V|4+@NzJ)ce1MPwoX5cPtHq4eG2Q- zq1v4p=DwG6#S2|;=8H>s+?OY#D_7H4ClCePqNnQ!!h+P!s8yXBnH&?Z8j!i$g{ikrY&(jTY& z?0x0)9sRL^fPO5zG0_!5-E-{0c@dd`lo}hvuZ;!0#MwT5WsPN9cVkxb8!Ielk@b-4^{d);q{wn`7 zVGId|$@~?u1q^kY)g-Wa{#o z+(rk{!-<~g)}s#!HZfG~x#h1)=Ka32R4Skcr;)$RO9^N;5Bkdnz+?YvN&>~*&m~oz z{v{%VxE?xPNIcyun|qWjLT4q*(ySfNwLJN9uG;dtbc3Z?>hPwxO9YW_>>I7XY4rdr zGiZVt!yo<Gv#x z+t@iSJ6k{B|U zwcfCmBiDyyySy6}q0bgP+lXWsa)at4h$Wj|M({G z(-rbNPKP-oc1X%`IVf^25HEAG zVf}({n=I_8U3kA}XAxjJDX6fqp|LNY+7=`11yTv{1RYJ++T|18_dKqTVILf zH-C!UV)%3NbQc}FwOSstF^)ss7T_ST$QRr(cn?LRRVJ>iB$uN#w-s=N zW599#Z44b{{UhM#oL72hd#;(}&v5v^RtId7@BNe4Ju6O3LEZfAe+K;y&$){os-rNK z>Zv!E3P&-2x`_4c@jGO61EU^W=r8gDZ=N~({E%xN`^J4;sBWSfJzmys);;U6$JEHU zrTTX4{lmll`joOOFh$&O$pIL3FPyA~9UJ1Rgh{78kuODSxVKB$4PfI*=lT3yJn(K&M%?YKRuwT@;Wd#tUEO?=jw5hm#qKVM+&C|lA%i%6u%#= z$D4tiKY&WiEVE;bx#8n$a{_Nu%;<@zI9Lls;sE{V<$oJh&v!W@!B4jGbk!($kmF-L zGAot?FIv4{O7r@(WS{%Mzuoauo3`X$jFglua7}~4je(nP{y$HgfAqTTp^Nvbv5{O8 z7T-)~{iIP-CM!?5bz{Qy+?bDpOYbYJ4T>9UaNVML+w!woR4F)Vl~h&o6C%PR@eh4D za^Fg2w z9QM}kjfAKqC%hLz4Zne9N}jx0m#7%M=1{+0pu%PMLY5Z0_Rpd7^V2}^`3?zd#8iq@ z9X4#-eX&nZDcZLE+;ZU`*^M_n`MZmrDFD+zagVIJ>nKH~h5St5zGB)W_@oC9S*>oP zc-*QAHPkqUK0tr2{rsOzwf$)z@#j+tr8Y0_UwM$oT=>J;*^EbX@bg_Mo7+iu-|Esb zK51W7Yc7|>Hrag9T*z|L;8@AB7ay!wxDeZVkC$xsgos^p$w=o8T8nBhI{Yub$v9#k zD=W&6wBik3JI0My>bUzh$OANec|a|UTHs~;vYllfV*^|I=)+d$oiPEMb(fZWss&(F zNpl3@OpEsBzwP=p8jt-)S@i45dh8y?c~}#s9?SnEb!p(yA2LjdM*iS+=|7kM7-hHO z*;8vRl8`t48@AQ{P-r3`2;ylvpWgOwbKUEK*V2R5?n)$wVJekV@64?ov&tz`lWE5e z+kR@Km?hpq-;u&*Wz}O3me=6yu<0~&Gp$0byaPjkUKaADD~i72F-06#P+70C#h?9S zyCC>yp8_z1+8#l%7RlJib*Iecn@nkrwc|M*rK=6<`{ zMEayT#Y^lf#MDvB8nKcyRIQRBLJ|9x$bVuRkN>xgSe6ge;-CY{N#OObHQ%gyV{fiH z`Q2K4PzdWB(mmm7&NF69SYrFPuRnneNclEqJ{2ZZz?>2E_~?) zB6hOO(V3&D#TFbvxHt#oyyv(6EsXI$!+2>T<-wMVcnhZ5#k4{e#W9oQdu%9XHI-xW z#A|E-QI`G&q*3(RVQsL{@7q$pDloU)dSl?^6ypH>>iK`Wl%peb+c6V@3s2ajQqr}f zfk8S557+`Xs5a32%`@5UjxU5a1h|L>n5aL{PRPH6U4B!dSb#fM1|KhEV_nZT)%Tw7 z;lH^~CKvG8idf^hk(MVp|7#fR*pqd{9xLnm!RfH-Lt1|0`5OI4$O{hwTJ^0wd|mS6bu-)cNf_-7!);M*y;(dEZ!gIhXtHVHZOUkpdG=Fe z|6ihR+GN{n0bGI}4rrH0tGJ1xVr`n+lLA1#j-y>-+%OIbOmFRo0@2sLf2Ds=_t#wj zNRk>3pPMVSL788(9_?yB_Wv0B_IM`y|Nrjp4m#XPB~fvgBqSsxOlLPa+z2^sYBzFR zlH*Qy=Y!Z?lsPU5Ar@i@TPhYdOJOm#BAY|AxXjGhe%JckpZoj${Jzci`;W&SdpvB{ z`*pnz&)4($x?byTJGqmj8v$vVTdFMHdxzcX>y!C9yN5*G1jN?YMhl8p%+A{E?0!%#@%vJz^|5_1ba1)y>AaBR*VJHs>gANr-A`Msxu%AY6wCEIe@BZ7 zG|?tbS=%mB!?WB^HIjTx6EI4!N*_Ei{>?rGmlxT0&AYnHq9r`kPG=HNu#_W--ie?v zC%gp$;{;U#%qD(cp(s;;@n^H+Z@*mw;x#zy!kZgUFO0l@x3kgDtv0>TRc*TJ7WmxL zswbPPGd?|0bCPXWt{jZLH^9H zqHmqrD$h@WNtD8;do%ARechRv-U=>BB)orO8U?vITH>Y8=UJ*xg^FG>CTpwfVM^fY znv@<=U|x8pQsDYKlM5XO?fAg;RXr7NG2efrC+%o_mj9mIbjAgGrn$il=y-|VZvt1E zizZO}F?Vm%{n1k&NvO)M_fNnyepSyM7rhOusR^`SQ(hB-Btzll}({c zyeijAnuEd)pEwbXw>j}@ZX$9WPP}F7KRc9ua2JM;ECv^x7ThE$2g0xFO!%3`jPzNU z1QVpXzTdi3XB>MOnI(txeBahtvKViqoxe%eaaROtAN*&R?vFmwWpT0^S9jd)-8;k0 z@ZKHtzJ1fD_=J0x5<~BKpIZSjzis9X`-vtA-j_5$nQa1X@tj(pxwduZ&rJ)K<+Ov^ zR}-9efP)cLtye_n=lgdEXQ-K9GIl*HVmw@gNC=TQbeNKhzg#3#8{C?*r|G6S{n5P8 zs+lRHi^o?2@2PEYT3C68J|T?XgX#R@?`AX%c%RZxcY{eBbHD9*Uix-6xZz=hYO%G( z?SwG*)q(jw*4$$k!GV|`RjlvtqMaVxc~#H7OU2_~JCbEpSHZZT!+-I_eM z4cb)U)V)`2ylPDXrMvsSaTvGkbn7#C*%Q5|Go|?pE^nE|%h5?oYFuT;z7{wpeWfA9 zemLV%WVKF6{}&{Ou@OVBXE(j^EzUET^Cnyx`eIM4ap-lqAGi!BetXqL@ingD6KK$x~~4;I9t@`Z3;R5pHAmE8wrQElJnGKmqnz zbCc#;7Q#6&6%ef%In|GV8+rHfgpQiTO8;}5*AOp6USh%pvkFWyMuNkn>ch2J# zxJ+rZ?cd99e-JN!9^|YCkut$Zn)pV=?l*hfK;6*`Cn6$N+O2Qf(AvED%{qfA#~MBU zjy(2e{8lxt(+#glmm!0riNvKs@Q>fq#1hm*jwFUMNwoQ4AWiERc% z%Bpef$D-^JUg$kNzD}e6L)5sLpv!x6Tyk7n#$STvdMkvH^j@FP5Ky(|7Ht)n46g_r zJT;@xMqqaV`5Tsi-$Pmzm@igcn%IOF?M?sS6p|hQyW7(c0m9qn>CX`lxw%7w&YoXl z&)0c=d>;zV>F>${Z$zn}J^@HAE@6#{@TaP?4=>=Zf1_tXxO`;651+n?lXm#s#~abO z1n`T~+D0=fb60&Wu@5|78pnqQ>fhc2K=~5y57q)RDE@lo=?O0$eWBexQ_Aw>kmRx< zZl^1LxErA()x9NvFvD#V-Q~o_3+UO_VrCb%8dofAN=p~ zN3S2bp`CK>Ui~gwSc-i%IK-&a)9)dAkJ+jhG%GY*$cW4N8b4w&0FD2 zh#@+zKAhk8~%W$#!&SjDY9D<~^_T3y|Ar048} z*9uuY&93}^`+_uM+k{EyaxCu^s^bLI-BAKQuO*r{<(|JTvWD|h4-1^`?$_!PgNsKT zAEi4WT4s+%v1@WGlqKtr^z;Sgg{J^PNgOzNHIu>`mv@AMV+udh7zn z<#7GjX!+B+mwSv7)Pz+JKU%EkrtUI=v-c!$h%4yyTP8STBj|z%#CGv7YFym@fg65@ zjjmPdt>=~<8|P`7#pn(MCI}q=GU|Ghee;`U$8lufE7VDDYA_@(e2bdaLXOCgI|~my znf2svADUG?o0nVw{&G2-(CfJ>P&-(>*A=RP^ez0z8>oAGtP8>%FWX~82RwtYpy~jk z)#W%7#5DNZ5mZ*}RMfoYdaII$fA4}(;G1AzflWa|r>JlTE$oW@Z5Ll~zc1&t>oczg zY1_%^;iX!~V$?>uK8~Ha{KOl|HBbFUxc$YyduD)G6~taw=xD`0!J}t^KWMV^ig-=O z?>{Y2I%C$#{i(wmHs=A~y>e2|=rlAGwShCFLAWAvOe7_bW`3pYBG=`e_w9HNvGG_x z;_Ku4DttY+=!G#t69B&9UFttBz|S4zsL0dZ=K%W3UAoWY?Ekb79CPEt8Du9geM(oM;`4Hzrq67DQmmbK(%8GKTB zgkyH3=k3o>P2kv1{oA|$=&g&hSiiGP_p|@8jwcS)gE8fe!4Z{ir7ed!e+-pvYz^wB z`5LCtqu99L&Hwix*uXWr*KWfFL5sWd;kv2Vn`0fHZEj6!!q^V*Y;ny;$W0+xZE0d4 zd?w4Up9Jj)_%C#?g%hvdvLHhP_PXHg9t=C?%Xt9~OhpspvY;kd0b2Q(4s-}2-1W#V z53=HGM!44IqZR$E688;)59Kw-Bq6X@b6_W{(pso=3!E$Zv6F)DPFY^OwhFM*OnB5b zvaT)ZQbH>y3rS=34K3C3AIkp?_EAkeo##(-S&6O@mKW5MJYe_iPUKY|%L|W;0=IRo zZ#}+Y!4%E>-u>+HfYWqcp}%}FA%qOq1fNMPBpLMD;@y#U67M;ayoQ6;uhmN@u;xMH6hp6Lh9Hdw9eNjh9N38Jf0w@_SQd~f1% zNwMp=79mFeYcTbOs_SDHYD9#`t}NfzX(3b}SNDJkuZ2AkCig^nN;bb&_--G_JTH~S z7#MujK}_$Wj+rJj1dH#UL0B5JC7GIlL`YldOB% zBc9Wxb6)LB(&wPopon#e_O6Uc&%e_3viL{C3fN8av>V8wtY+dKc3Z1&CiR+SbfhvE zM!djyTKd|%^4S3ePI8g_oeRs1_rQsFRp@qEJDaZ2-OkIp4Mp$UNi3Yd6mR7 zbp(Kpiu>Xq@7*MqoKy=3hweWi*EWIBfTLNMNv&U(L^-hRe}l~6{X7Y7+&ArLIG?ia zeZDaVdL#Y$c-rPo}Rn z!JP13Dkj{ms}?L(RC%cq)B}%p-Ai8q6y7+old-+$)Sg@1%;-V_b+19YPvX_dS|+L_ z`bY)5ah+vyE^7+$8`u&PsO_IU!j7 z9uSn|@tM|}Ks%w&_%a>dIvMX0c)vyC3-lZG(cu$4%~b`?p4o<)VS(PFz9jaECr_Wv z803X#>nvz&Be}7?lmqkiUOdR%7$Uqx>#<4g^!GE;|E9CY{t0ffE_}qrKF!6jfs``n zRPVYGF0C={+KBB+y$Di-_Vm)o6@l)ms}?fH@49k^Y-OPKe4SiH%*@q1c0Rcp&q#|2 z3pDiqOSDXfD0*C9H+hc0^Al9>=C^xdzZ;^JLQxv>ZE#b`x03hQlLWn8D)r_<`ge}^ zbBde^c!Q5CTu4(qD3don0!_fY;5<(3_!n-Vc4xNz2o(BPm#K-(Ha(z{6m;x!zvu}% z^`^Agzjvdg4G!uY@+A2N@8Y*G$oSqtQ{}&~WJYpmv{?b%?d^ZvZ|g@iQsbDKup{2! zo&@e`F5TB7B3F3t8L$9Fmlc3J7*Cm2xJ|&COAA8P?~HLNk`@+P_)^!^rhVVdN$AL7 zWa`pKExtoO=-i1&C~WO)gL%)Mv^o!#3)n25x}6}*0XbJ|)N;aZY41;)ZY{8)_Q9U- z92_OlIR3G@woiX@^dKjJPPkVe%?T=qRkkgwF5M3lS)wUoP zt=W1!iQ+Irg|kB@7fd&DA`}c)t6FKlGv1{YIo_EY`0#dlk6jeIYsxLp;l$Y|I6(re z={~~r4)i>^;H1}SU9b=Qt`^YOa<~4xU=V!IIG|e4kv(wf$uwtJxp76z$leN>auWQGo;{l_g)@%GT4fh`V7Z%z50@0s> zj&JRAe0I;9wgD}yy1QdWS3A;OOTOyJ+Z3_Fy>P3b0N~Q&Qn#XirNte+c76de{o3CM;bAcG$~)wOY8{&S zfARDQ*QpEgA$ouF^e=vgu5HWm+ecdJTXphXXW5&hF4;DtFH&z~E6VED(>XsY})XyqJPg z0Xs$fL8dgm;{X5X?_c(5^06OpLT5VBH8R`q(@c__+b+)Y8ayViHO7H ze|=!7B$Y97RHp6kB6S*vjq+>_dS1Kk$CsR{6s|?1rif8hv6(*$i@T0Yh0HxNN0qvN z2-p7$chlXNqV!2?)gVhy1ADPQNG}Ul4Tnesop{9K_WyYUpvA8(pYuatc260<^lA-Ks#9=TA6AB|(}Mdn&A9n#bo-GE z&%DBGrZI#i#~ zZTz?w&xo@uHqnZbWWT1Kque|ZHqV^L@7pe!m>Gc7jV?c*<4Pb4{r&#X%W|nJjl*{1 zag=43FaOYEm$dJB>S2sc5F_hPghye)%SHCFp^;Hu^HH2;o^gY4cM?7LoYo-Pp->{E zoiAgTOXmtQKL`dY=VrpzuuKRm*Zd$3Kr`+qz4UCH8*B3t>PPEkt!_^r3i3o~9Fk-| z7R6q0MBCsQ!TRT5*7fy*BwAqP?79xDwKc1sjp_0Crw{}Ac-W>;1(xku0{W<_C-wpr z)y8tUEW-C+7S=99M{fdsGg++FWjQ+;bcaJ-XGz`#PtBi*i?aS283|d%J;Av7V-w^$ z1V{p;fvCBdCFA1zJ<#!NoMd%3@^hvJL6CpXBFeEK+^00A``~yD&K^zg?$XTkOqqCX z5=-Ntdi+7pvrenjTc)eP3{@}hH=@X`iBy#GXBUpiW#LAe?dkzjKVr_L`(Z+!ge1`9{=TMcVa1#^EEbwO-2Z`!5L7YR+2HbiD(c~Ra8i1Ld8AE{o@gXiV?j7}a z$(TDgtA>T{v1uO2Y(&}M`92R1i~Gzt-I~!=b@U8~RHG3UyMmk4uD}%SyFqV|%?e9f zDWcK><(=r#?qPRvQl4*1KPJxF9(o`>5X%Al1l|#dc>wM zjlMQo=RZ^8-!XPEdS$pO4F)Bb%t;REWua;V|AMR&ZzM7f%^iIE0IeTb75|ivN0cV} zPXN6(&xxE+zH2M@z$GziS}G+>tj9rRvx80f^9O#M82@}pR02Prk^<%zL5wj$_MU7; z4*c4-s9{-ACuZd+{LWly2dbu0^0lqKcQ5B!`j|6+;!~u>y-Lt{*G3%wBa8o&7F@PJ z4Fpw?XKT3H#)QGC337*8j{6(7u4CP&L9&f15dB2UjICKR5xR#7y9P}3eKaA07Pz3m z0!3@V;()DBb0VKE_%^6r*41Ir;c|0s%Nx>XYo^Jo*}&fke3e!gpfXB>=9MgF)Q!!| zgh9^5i9~qmu!mljo%aO=5n&{^ZLnSzjUr;e$jR1d_9!`=xpAg-5FSJ;_MZ1Sjhya; zB|r1xi_D|};ZSW|o_hqF0d3{BjboYE?i(6_-j^R{*F_hjSAn0+OA$LZizp58AKZki zc_Q`HVA(Ssx2zvzvbglI2_dZ5#Fpm&W1sz$y%;Q+8CZrHSK{mOja|Y# z>QXrV!D%Tsqu3>80LWP0h{6p^^4;HYUe7=pq{@Fz$4|ZCG&NN?^n9VhOlL@NNX(Z1 zIQBoGcP{c`*(GsX8^=I~SxaM1&sZkGmJAPeX~z>UG%OuWVs|_V?oG1}ju2}_wd}V| ziGSX_my1Z%$0G{DA$EFMA~Ppr*lK45A08zCaL^-BMbn46e{1!<&prZZ6njCh4VRIc z#w@W%`9KvqCvh)xym3xxD7r#Zit86$Khlj?qs8r2`*EU--+qyPS=>d14eI#*B37v( zeMG5~MUD;o_5TuxQL)d?QGD)o;K*bL=d<(Gzn$PxO&xk3-B(c5a((2vQ9lrvYs$La zQj~v5yvxdTF|hgrfae`8mG)LIGlk1kME73ezM(Pt$9pUM!a_+gm1%TQmQh`aiPl(O z|GZ6Z;?yit%Se;YwEuB>Sb{kgP!}Ne@IZ5Hu@mOoCpB{zWnGloSF^({2{tYWb<~OB zQi|r|z+NA8Q|>S?y2Es_tm3yaaB049f{B3Hj+e91kJ%ClHnat6wSp^iI&S-8*PFW~ zuOipDV>n96%X*KD3|W5%L6)`hwOwhySAMT#_HEcTK@aot%vrAMw)s<5f-3a7_gY3N zby-SWq5&Sa6%fVJ)N&OBVYY|dTHqzEY*jlgCLac70>W%JW+YA>eGIjiD1G$xaxRM* zt#LV<15b3mF+GR+MPzyXbydHJ<=PrVOmlj%|9=4C?~e27t2_uj;E+iXh*nVzD;X}F z=&yegNbik)o(^ZGh*$Re?t-0`j*N?E8hvb+hwYA(AT)7|SduGTIy#FM_Onc|vs#Td zKQ>DCA@H&xakSu_PcyEe>#$f_;tmrew^@S_zQUK> zm4VPXcicOR?67pIr(8;oCXYu~&sb?H*JUBpY1qd?K3?m*g6wq8S&lGi%3;TQBP~*Wt&P3|1?& zv6dAVp!bHX=`8Y8?kJi_?fq8f>{L~J5VF&`ac=vc*xk1g6c%P=AifVRnXQPW1YevFLT^}iRu%5oFR>d!??+E&3}p_}MM#w*ct-)rOfHq3j3F*@$5=CT=wp4F$l z6hyX<{Is`O`OT=bN=rSP5Bi;*=L*MDUn+H0!L3a%&3p|r0ZcH#ok>5ebuW9C)~w_j8DVj8Hx0)b;N{|2tTw~(65Das%$7n@ zZeWFY^*FrVrR4%8*Soxn)kUD3jGp!PyRU_%pI#WUgM}Y0E`bK_2~2GiSN73HowUyq z)(hDwDaFL)fY@>Ac+&NB5&BLo%?Y-B76(>N7R#uvbWBwJWGk@hh|`ZPyFW{w7Me$K z<9I}ZB;=cB74j*B`2evdza3R-BFjHu2V?J^l$_d5RSOJ1zfZa+<$6Dz9^8l;u~g9i zxvv15Davhmnz7;RWwqr1QG~`sBR}^+0R3!oXG(FJQ&@Go#@qgBlVW1ei{2w2T1_zf zn^VMK-W>aALb&c`-lQm`g1>hzUfu${VB6e?RnSxRCGqt`sB2@@?ihz*(+eC?&O}7P z#@BYpTFf(G(?zKXM4AW);ix&Y%i?)wy{v(#?f(?G|Ni2lsT`xYXkq6K!T*?b2G-1O z3dN#ZQ6FOKjOwVT{7mu0QH2g47oSJC>Rg1 z2%}09ZRJOIWo*pkS&8PXedmp+K4mjqd?YiT0kUOYuStrrVMKQ#>; zR8<`P-)pniJ*@4nz*HmB>i2frIRiq=8(K>0&s++^H!9a`1tO{|3^35TGNGnnp=)8g70Re5=4s?dU^4Qsdnte4bGx{X$U!2Fcf>v9Oa z`!3cjli?XlK=*k2a#`Qb_ZQn}_VEkd%puZV;~WK8#a`ZY0av>lQA@bd*$}qH?8E3= zA0<~p)KGn-qBEEjVSY2~h#8bVG~0F#lDpZ8d#_Uck^&3-3!UN8D_bm{Y`~x%RUeq$ zIjeVvaCyc&n|I*&Daz{6Dx1UN5U9EHWpRm#EFo?y7-GxE$EBJdv>L#&zAMT3#}P4e zOn&y=fvIAVRNO|oQ^fG?Pv`e8h9#hAw=K;|VFuxh+}YAQ(pTC8R=BL}n9g@Y9=M*?`k&2u<+6qiKjBN! zFQt%7uL({kw!ekPXnoMXgh)W9bk9Hz%S;e{W1poc%$$(RDgtx(k*0e;&f&R-<<-3e z{Lx!HgJktfpPe!6bE4DPWy-Han$0fmz2h@4{L6b-ABNB599aU8RA?}Y1&3?=m0hc4DPa!)vnDgZ( zSXDD_w4oXIdhNo@kf631g)Lpl_7!Fhw{)Uk6u-1K6&5?AAi%G9feIRXVdX4x6Q_tN z`{|t~E_bHx`F>BmERcs-CX1yNr6|CH?S_97977!COX6Nl+u(_$Md!-G$q0gV)MK?dg6ktB^>~z109NiJiHJ6%_IvPZNwO zvom4!f0S;seKox?%jmTzw^+??>7)&Tk+N~zt90*d2h*!%MHyvdNXuOOalNe0^9qWZ z`}f>f5S~kkPc;!=IBBdE7<@b+_jIBrkbSlbR@WU6^woBNylzH?%y4P?&FX%b6Tim! zex(=#1bUfq;W>466sr6GrTtc;QA$b#5}pzJ_)&H~P+L7~;dM&O#*)YzF}#deffh1< zu6Fb-C4O8#kJ^)X{FCj*Vl!Z>3L>55k5T)(>Xf^XSec8V7@^xZM`;<>{jux8ROI~E zJJkFHQw2}bHw7qGvjEop^vv06N$5!=Oe=x@%|?>AR*P<}|41v0hpurs3^E5%aqAoB zVwM6RPXRKluc8$MVU@g-CPJh>hs4`=bG*)KYK8Nu+ud=wN?isaT0bGjT8pcl`=?iw zMLRN9bmS6jGKqjU?3qzMp|)QWZKUa7{ydh zet=Cn?G3XaT>lgT(KRR>30g9&{upGOHE|Ovyfx!y1+-TEh25Kx(d8Sl)WOqn00{h)tI1n|Bkc;ZQ@frVVTxzJXX|?mS7hOrQkG^m?*z#^ zs+>~4nZ=VxXUw;Gy&d7-XzCuL4vuL(maOI?5D_1N)k>KQ2U~w{EBeLR&e3hN z`!H^s$z8=E=BjV0Z}jl6CEsvYC-`wJBmXV9I9D09e#*cNI zanrZWO=Z>&qWeycLM_~9G~?Rdp`Y&_I3}&KrTQ!0WvL?f z@|82|oS{d}s5kuRHCnIdckU?@!bI@*H@;E_lO$hDwBE)XN6&W0Mcn}#e1185-Wv{* zfvXjfw{>#zc4$a2v8SSGKA{sYh(aQ3oBV}T^k%~AsqU)X=7dJ{ZINEaIMcMPIxDe)W7(`s{b+&(0^_!VUD)rfg_{(G z6zzNGq7N2&n=7_x>gsM^W0~;{#<_;rR+km%YvU5KDY>lk%*wvv9b{FW`2aGIQqYf4<(a=jWyu!N|?tsF~UTj$e7Y_-Qd^ zyPVDjS!5c8*@%*!kaW-7uQ2*~N+a7%qW2hgY?62!=xH6VGO`M5Lv4nZTUApp*Y%%l z11X^aX?XFT$8b7`#~R=u4X z4X4(F7I>$?0^iY@bi&PCeC^&V9PXK?9p|X6&Sdmj&NaY~sx+2<2wh{h%a-2O9AXSJ zoPN4FiM90%eC_kWjBL6xW21g5FL ziNEcW^G2Xd2MuX}V^ni_tAfvJx+V%mvCZFX92x2fPsZUs$G#aN7#$iMyJ1^ya+pmY zfq|p2QIcowaLnW93V3pNd6{%E1D_lK#IZ#eB}!ZaKX88c)1evm>zNP-)>as)%?ydu z@P870BS30?Njw&(HEJ0jQIJx}nsDi5DO|8j+>Y^)?wVTr5=a z!G(~nn~Y%1C4kLC0nYglR(Vh|HEX-5cd%nUR!xlxf>5wY`ou{QcSb3|g0+7W97Irq zF@~`QdAl(x4{%<%PYv&d8y_1b+EwL-xKCzR4`JWv49@Kz_^i=}r#O`akndHBw-ki4 znfn@1CW@rv@BK39k&`*S%e}Sr5Z;N(^sq z+s@AFTjmLw+YK|c7+^!~qoAn8hstjim{b5Y#NM5Rl$G54uQVl^sUriPyS{8TjHyAp zJ#W|Nwb=HRNL;)rgnGWLxWBu_&76ZEe=TW7J&6XTnE28EL;KCYB=+(UqGfYUtQ&8U z7a$~08F%GazAjH_-4OWAFpnrrd~iP`OsI-{NMIaeD9ur~R|n-{x~VV zAdnU7$oM<(G7I(jAG_W+19=ru&m`JP4=e^e1B0Y-c33Gwf3;RwEG#)xFs4mZCkMln z2l6xQu1>-E^^9rz->~)j$0I?-qdWit>X$4NSiFKD1XXa*S|A^9unTr&NV1YXc)4u; z)E}cA>+=$I_}T#of6MCbViqJXCS}BOTQ2~Z_3^f>VZ?F}V2W;VGaniFA&y#fu}a>M zfcn;34v1AJh5FXEXhy})7`6jt^~WlIE3Ux>L>}s@xTuadxIAor%ae-dd`f~fgw=l> zlvKtL-r=Ahw(35sa5mEjxn57d-ql|RK_X#~$4M|F! z(G3=RfcRBI)JU;sF!|m~^}vW(ebsltb+eLo@e4-RfFK@Jpvi&8vb=5*9P?JuAcLhG zVVc4~EkSHvIcXWnNJGW2E(53$bAxtadK_GW;%ISpepzYRu(jf2;(R=6{^uA%;~bwJ zTo8UOOF?H1R;%eo)K@&iBCN>rcY=@2Q9`pz=xfpeGkW1-F@FD2AZd}b!k9cCDJe)A zj1qyZSg#3TJ}#7IaWeSF9LxUF?*;Jfi{`n#R|rtkxO!ZcL;qONO3q-U`U~8&pBJXw z!ggcgrcWS5zoJO|3i*E8NSp=$HXx6t97}(9e+?$O#oE6!QYL6h164#Y>jI^e0>JRO zAZ94+LUqud>8el~2%Ky0Zv~1!DR3`PuT-b;>aszI{T$u?8UCE}rl49nhH=!m1DY&~ z;4l2Sw*9dSXLipXQ12-M`%c?18Wna~(Y(d?kseC@oDEy2=D96+xi*mqBhvFy>2t(j zG@hM!zlm%595(^c`7oatRO^ZG!b;ufBXLpQm^riS`~@L9s3r=BN)zwZra3L+wVgLD zwMc$|cJ4%49VjGg(s-t-&y6&pY|%09UJLZiVOz~yek6kOwY;3etc;}@tu|B8{Dgl` z3#GO%;N&5GQ6rKz-Z<-bnQVC*?9-a(aHCt$2~}?e@S9s;X%#IUGj;nlV=XKQOMwEO z0xiTv2NV`$dm&(&5uw_`jpEv==w)LK=bVxmqK?+A{UCK%)^#B4*EkmS5jkiqRN2XZ zI`esx6&K8#23WnmPYDoE}u!@YxXe_)ziCfN5sxg40K>}ew-o?e)3u(r?x>xSu8q$XJA7(8l1h;fqz0T zyNB8?6;}Kv?K%tb?Lkim21jHdUbn>*O~;=jWjXYK^9ZjmD4+y&zw@(u6IVTzR>?$6estV*s>d^fl%&>H%lKgdM%ER{EC-PPrXOV;N}%<;PJ(pAPL-t5^LHy4 z6#1kk$3RtSPFxPiRB?{yLJ%>qny^)9j-xYiv%ulL{1@ z3xbUTfj|*u%r@+3vUr0cuI)R%il(A=4oW!C`jFzpYk=f-OBJ>kAV<0!LOF%DBsvps z*hmH*cO7438}sIF8A5%Sc~%;^5xB7j z#nuKPT=J7*)~L`p$BnV`<45M3`x`*vkflgC z)RQ+}GxDiCB}Nf_Tv!MaCiUlQ0C;(Y1Wg--D?=RqDN7uonxD3C?z#MmXeF z)y(0Z|BHMB60(3miQlY}fNSS8;PhD=Ouug6wJ@tR)3-7ECFOY^9euJJHVj~u%a9R zgjASa0K0@nlZ_8EWKLH<=XZ^ly7JNJFJ`YrU&jui_5qLo!aT_l^*~mP;MA!7PD=o$ zs(g|Zgu>*8qrvGL4=wMC?i}p+en(Xz)?Y+_^}BVB*$jQ`FPuT+^5_h8>RhMIT-*p?CW9a zmBj%K4IVgVQI^Vs;0Mq=B+|Os?ktRsxrL*4$G6kpk^KkvCgyk98nZTzft15Ba2&|e zNiWMyk#hAnW(I$87d$#t3*zq&&5+jl4a*g_fnrfw!k>Lb^ zWRP{mZ(9*bd!sK=3rEC07&paJ8pKc1=;stoBZAcmA|Hzp2l4kUA9XLiKKxi?59;7#0jxl{1iP-b_5~+P4p3 z+!Rridoi-#@iVf|@QA1*cdV5m9{z;#jtmkOsyt)dOhnjGJDK2IcHv@C)9W?~`K=$f&`4#T3JxOi z`RV;#LWvJZ;$O|%`Qx0K?IceNaG_lsjZ{CsFCN!pbBYl5&f>ajFu4mabIr*<nVH zci$OI+G}p9fmM@_v~#Nh$Ww|)+~OrQ%3K&o7218uMP%@Y+QrWq0S#*9#S>zPPr`cY z$=1SZpWt7C{%! zQ;5d7*S|tu3w!l!b*9u`2qf(@HEv0t^BeqZdlPOSlx@<&Hi&1ziHXd{IZRQS)5L-_ zO%aYF-Kd4NK^G+~N3X7?Kmy4jb^Yo6@ZII=H0I@=avl9NhCdL&H?gQ=WrK7wI82Zp zKps|P3XsnRX?+-_WDZGL$(5u`*N!@_q60ZdOm3Mp9bV=+7s^EaLb9~XR_4;8td+Qu zL~yPjCD~vFMn4>!9WqfI{ojQMa2{C;)IidGmyKxOK}^hUd82$~b#IZIdD3!Linylt zqpB^j|FUGZtXs1nyd0eD2nGohb=!$yA<-6U-AhdUaavRY2kYch>PlQwnbdaCqD3;9 z^|oK4{=H!9Yk$I@{=VM4`D*ngs*OLJf5I=jmH8$wqOOay zwO9Sv+MhC1-~0qC>Mry+daS8CGWd=GzSy~`c(hPO<;{wthjyx%@X#1WRd+9vaNu5+ zuZQI1Lyl9$_3CbD(R9(cRtwQHw|_S=JKNbkxXr60NYYme_!uV7Q%sMGd_J7CBk0bW z?JY-N;PoNvqpURx=q*jpYA~MpAG)XgF?|tnJib9)Oh?M1#Y(^a`J+ECbC*JbCg0m| zkPVw%o@k233)N`8`0~frRK6G!R@JOg!TU**9CpQ@>K~PhxOEQV@(5$gZx=M8$xQAh zqvQ`?CdJQUXR~pjR&aNV%E3iHEm`}6KO6OoPa0D~8|y14F~&q)U<3JPKtEyMr39Bx zF-h0Q9C$#t4gVT{v^@{KVhBmPK-I^SRDsmwq>%Y%Wq8u&nwnE4u=@Sme|QG)T)UkE z9tW_!Xui_^-C9Unz`x5zSu!Gi=jKY;bdY)MXeWRR8q7<_cwT;azaS-%M!vJIofdC^3# zqHU_~3M+je1YcjQM?@D$m+X{PzGY;XCNF~eAFT4n9QWbvg}DnB;%+Jm2d zUvh0FH!NBF;tUkW{N-be_oW2oLG+)oqc_MJ@2oikHNr-G#XiBu>BK7V??sDOZ3L|g z^KYO70kqdd1;PQcWyt-yzEDk1aC^5uFv?m2hfTMa@NuI6TCyXP; zob%_e{U~VU?>*grG4GNXs<+w3x&E)NfeSjL{Ot+C$1EVx7Iz}7Ot%4e&YV3T*Ozr! z92r2c&3{7HNv21mZf$dB$clk#C1_c1eVN+J z^rXEntO${B`4IaZz{IMb9D4TeQf$~GYT&_%1>yI-ob`7SeLL}9;*7pb9DkD)K-@o@ z-1?E&Q_Y*vL3!b-yBeGiFoo7!SG_EQ)Dy07Hn*quMVp}i|CB$jt>WS&R?w`Wd+reOuxFl~ABXh;k*R&E0Y7tcG<5j_XT0fEB@YD>E9gh6B zOygCLB0Omf>DuSV6^7)WU`W<`f&nz1UO5q9ldDf16Fyur?6XafhwO`-&-rAtEX1dJ zw!!C^mKYUxJV!J}w@VhUl6yGEPH%H?z;%Tl9&mBnWRUoyUR$g7KHPHmk8JeOy0^@z8)K(bVD}k3;Y5rdq+_4CH&$x?$cXR|vlPfm zO#+{++NHSCtT`4i6TtSQjkTO%E*ll@E}E+5jJ?0m%p1eMJ}&km*QI_FY;5QKW^0&C z4H=wFfSC|$cLi!MRqPLl2HtM&EwKR){DuA25_?` zhqdY(sKQ^dR9K|8l*>;%5L;>jK2hONreRZhIF|VPV1HE3FAfjJUr*Zf-}~kYhl)mO z@OD5`t&2unu@M3j#O!x7A3f8AQW?0R@K*iy{Z>&OlM}8E_fy-{VsBK9`KKSs!E>(1 zl}C&!O?IgqYJfOJoH(H$7`+V)JCJ#q^Vs{3>%KJHZjAZOK{@evR232F4>Cw?W7t7z z3QKFb5V1Q#^1@h>Ge+^P<{tSVZ&kc+$KoSPfzs0HV7s>Fh^D!g zjNOpyg6^%gi7UEU#x_1vYI@c|3M%s)cj+ zeHgSs^89jUu*l^B@7JYkBs}4>-~uj2B+|{q$l{7FFuTWHL=WPe=z|0wNf8VC*L!8_ zI%xEze{RINXgMYzoT@(N04|0O-|#T3%WXv$l7Uc3Y=5U(My)mLG)E*`1qlRDp8dmT z$-fx6pOVC1*bxy4bqE^^M*DcuM3_Z>|KvRj(jxHLxp_d$llH67WYMw}0P5fQ<>jlT z*H&{?OBwzqS_w#d`d~(b;qtMFUuOH#!9n^tGsJF#FEJ{>y^2vBY|&8|s{MU@+RoIZ zB|zgF&3_|>=haIh?*~Y~@`^6VPSy7P{A_X?y+!%4Q(WB{=>u*t;cd%<#nA&BI@V5)Ia`H_DI)W($~=hg~E10cBZaP{xmzn7-rX=y`|II z^uu|}zg|AXK$ieN&o9c@`0vZ!Z~fiDJ+MuvHJ^jEyc*F<0ISRKSI5Y1PJxzGnn+*q4`zNz-v+{07)+Yj36&m@Jm#;QRid+giYlAP zxdA?Cy6&muDu}Q82Aw$uPf7P?OyYtyl_w4HAQzaW9|&)(&s5?DWA**C|6TVjDqg;8 zoNEx|y#HA%?LCWq|9uwc0%O}6{RcLW6JEHS<46jprb#TV{CO98f3B}&?pMNzXzvF( z(s7}PHu3-qogV0(5+QEN*3{YNAS=&61MlTd*a*i*3IIJDk)<$m{Xb~yV%8B58B5>X z9F*G0Ky1%~Et_Iq^lNEu@1<;chIM&rE}Og~T@LT}@S>-a`^sE8LV9AC045pBLG-<9 zBaKi6W=Hq`hP+8vUaoDdzchoJ`GGuhZ{vx-zY4f1v z<3Fc=<+{F|`-XEF$rBLXBa__{nQ=)0QX(w%eVA0gAc>Ot-rv_^+D~+$v#6}#+4gS#Zsrz9Rlvh@(1-dx#wy@pBE2pJ~#gkb4RB{7mW zdq?ZWTB$bVK*4pJ?Pk*e)4a0M+70YvgJn)I-nxZ)*SVwJUpGoE{?=bE(#Ed@KsuCF4x>7uSTx;<4*C$RwAgB9I z(XK%Uy{wp3+^(7ibahU6druCDNDC=g@oyKGgZ4EC@Yrw2#NS!PnUuVvyP$cd7E+x-x(Ws_dP?Jwv>mB>yHvW2iyg}(X}Csh^)U; z9!qaLnvj+&^)IevHavvY*iP88FgfV=MMd285X)IDnUh|_I3gcQVF;b4*Zip)$NuSC z8ntNZCgEYYdBU)#Uq{W911`c<4})zD z3aGYbX39@h=WCo0JLHa^EM59%v#i&sV2XleGyWUo?X#AreBIr6FKNFrSJ-EEL$s=H z&F%fPC8N3gxZVpy!#^f}W%D{pcH-C)pqp{STc}>0_KOkrmvhtqB*L;C=l*J(LvF43 zT^i;F{OW6h$RghLRFZ`HI+fq+@~O4P+z=&DJn6C-*<&pG$=3eV`6bW5IwdhGni`t+ zOBkXQ=14!-7EJY0YIup%+4`ZuLhJ-j4=qU@$9Tloem09J<+imUq&Rxa~)|3k@<9(cUjEFFg`G3@Xc|4SB`2K0tuH|T<=$uZAR6|0- zsGL$1MUi!sM7F7988c%ll{QU_En+GmiOD))7All&vQCVdNiq|I!5CxA;`fY7=XB0= zzQ5(4-#>l$=k4{p&vQTbecjh}-C=!+uw$P)B)qM7bfh$Rq&rHze^cn*7()hRKSnV2 zc&J_Do~m@Va%AysH(3Jiy-e#IY@{$MtixX7`j>jg04q-`l5#Yvo!mg-{&DDno8N7M z`RgR5#>4$ViOIo~A`>!}3K8vkFG##Cp{QBil9m zQq_snnkA@%_1@C*+BA5!V>fF^FU8;{Ot?QC!NWwS;w6h_zsAMX?=YeHYL6|!ET%jk zN#~>vr5Y5l!Y2<<^Xg;A*i(GyLu)Y`QuvT88tE=cMm)5QbC7i>Ui=y|DRngER(A=v zuqt)Tu2*`b3r4{iZCStfIDPIk{wVxt7s*{>?X>qnEb+}hwF{ff zjE{lJw6x3v!aTKtip<4t2@g?)Z{b}x^QULV;of_v1xnkSp(lK}S({OPw{7$T9K0qn z-+AT_)T*`qkQU;|GT}fY&&x9PtmRYGr;5kr7l5xmfoPbr7OKB`1;srg}#?YclZ`8Bxf&)_D+y7Pgp$dbk)yT!9vxitse6@*L2AKWK5SknXGYx6MH{& z=5Nu3hEF*0bJ;MTv1=dRvg&2^k5Jfc-LU1T-ek#=sYOE=hyEdWQ&+7Y-V|T!E$X6! z_L41bK4sl|(M(M#0dHAjhHXw#o5eN~?!)w$`%7GA3T(GdY_9oB9l-_Un&KOUF{rAr zYWX;P5`D9;P^v?tNR%n+Z|Kiac}SD9*x!1kovmW&us_Vz31)^EQ}?Yrxyd6ylhKYj z`mh`&J%4H<>1vxbInhm)6jn@f`~K7`^qW_!mj*m|rj zOsC(^+M7o;ebqH4;mL9u*i!ZQRF+>LOMqXTXVQN@9kDa2;GRrEeLVCMX4&;1$wxnq zMhSbtzcf(DAJ*uH+ww1s;f4DIgH7f#gE3^~&4)-SqNOboeYqe#tnTq)QL$fDg-rwE zbT8P4FiB^+gGraJ73TApWtTDrPG~tfk$27C`Uij+5C8%*^+;uZu6(g1WO(K#?#{;*PO=hCqN)9Cu^VF9uc()7IC+}^+>eCTm+rtqOi zoQK)p|A-ZrScqW+zNK6zq|P=tu19=Z|~w5on6EzCHD zCF&gklOp5e@z0yY1x@{;@s%1Q^DMf$gHU}$X*f$u51W0s2ay16uhXoFF|uK~Vq94x z?;CH#poY5`(mN#<^Yw-|({c_c)NAubM0Oct;UCKi8ay%P7MLLnC-TwU@7L%2X#n#= zmk~|u$Fsy$#xz^bItJY+J9J*dbxU|OX)gZ=bT9~KCJlXic$xi8h8Vzqmq73S(RU#2 z#igm+N{N(bf=mpC0VfmJ*@lN|v2l_)8ERv-VZ5Q@4LvAPRhE(UXxms+yt3^1(lIW@ zSFZu4jgh!cX>Dm4-iEa+aXPuwuVKtzOFZTl8!heR_r0YoUngw)ob9xplaLAMHVhwR z(WQ3Bl(D(6lR(&FS(6sFJ6;weo!mZc`1(}8(9-nGTQq%iTXN5(XZY*U((!wRXi*_) zf{l3AFc@NHI-)$+hc@IejGyH?uvSo%)Fa>sK6w`KATMsK^q!y*Um1;5Fb`c4ZZMRtO9>8(6r~;M z6$PWrDFH==x9inhF(e0@BENi>7Ks`qz8)g!kqjW`dU<-%xyB66=%m?EgfX`uwoI@D zebU)Us=$%*nK=PYJlJ0<)m@fW|N98$ww;%X4_Lk z5ntSb<~rGqmgBD#gokFexXCoJmU|)2BU@CS;3xn=^__{D*<|+M;vlnY2hx4o$?hk{A$@^s zKMRj~^jRTM$DO_aomsuT83O~wJqQtZllGR0Y(L^R?d{qaYmJVgNyG9z0zZc5FF$n7 zL@e#;9Q|zJO_umbYP$k(ck4m=?r<`p+@ftCp=Zf-Q!mQat(&cClF2D^yY)tAQ(kvx zVcg=qJ&Np}_!vFmv|4Iwuu28O>!S!pgBz6MWzUivf@};kliAOvVN^rEgYk>kX&=|d zONujfa2o3?YeS&$jn^77pYNYn9~f?x+!$%ztMl4C#wXGnp1A{)XqH&eYL(Kf8E8}qFu1HOiwAVm30x8r@fr(Q@9YXn4RX{_`TVIkS z4I4fqJr}&UW)Y=^_YCr08W)iQ{{#A-@6}SKfpA>xTswn&8iixE(25Nd0%B zl7z_vZ_AWZ9T#-#Nl*O1Q!t)--F-hw@0_s2qRbV)S9e&5;TXlEJ za>kqm)u!AoI|E6l5`P3ax6#uSJ@JVI+h~J0*X%PpsG^5aJ=701=4juiTA%fJ$r`wL zq4AwmYs@hVjELhJPA=r38-}J)t-A5l>D`q=v+=Jaf+Tdl6HFcdTC}cS$yD(OJ!+CxcU2N6r;3;C_e9FH8$b|Tk z9n<)XSpTrJ%x>9TQ|QvGr0LzcI5G`U1j&~^aYqlX)W+o&{?ZF)Q@bimov4^obPd;NrG9}Gi^#Ozr% z=Bx(y2%UGvh+mj4>c?6%P{zbj>VhUs6WPgh#JlkNA+?^AtT8u8aMN&?PdZ|xBCQJ| zy)bs+v8Xf%b zodL(n6Nt{o#|DcK+VK28EZwrSU7U={Yb9DYndvvA@h)1~_;w^RR`9wxF(awM7{GEK zXEeR5>Bde@!O_KRbr3|H7ps0d!{=Wu02enD-_~rl4>K@LQ#uNPjUoEbxTwAq(~GUc z)?>eF-RgKs!OgX%ymECNtu*bz7T(Sv)iwx3y-QMubWIDzX$-FGfK5}*WN|U>bbki0 zFj!#MA<4riH^9;Ryvd@gC!aodd5CO*_N=lE80OcXZKz~O1}ts)_c--KnTtPRlBbD0 zK-^JXAIIWSxt8SL^(w4KY^`8k9>_3erLd=fM5S2O&V+PPJT6wUm-ng zRWn$bzA%OGaP-FQ2tjkIpzmvo04{VgJ}*oRJyzwXJ``I**>`N!BV_mATLk>6eC4Tp zzKhqcZ)MB8T@TdWje~tGyGt-Qk}XW#Jt@zxfrmI@0YRR;Ars=F;;lu>b+T59RaZKe zC7t%1=ayoYZbSNA)+%B!B|ZDAb!QSzB?s?U;x7gKw$ z_;B+nTAzv8W4lJO^nJSa9e61`W&nAt0ot^$FNgw_*#KhI*J58^V*V`*eMnvM`QqIM zDOlW4S`@jZvZakfmc41XPI1qu!o>n?s(I}TA<1@|P{TGr98vzlz#(Mxy?Gz`m}|1E zDmIn{$tO>(kdgB%IwKr39IjxLcZORj+P zqzlc>w3iuC67NjoD3Kl3wHXiZ1n`sXo&38h3f zlrKtO7TF710j%JP3v5Q)c?8VBz_6^%c>QP#Is9LQ8r309wK4C*v z<^CI!UL&p5URRXJyFjirLAgqN0g=Q?I3xBt`-e-*h7?~_i&_P7;e*A5DO=>LFDP*p-JMI+1iM1*>vy196={FO`>=`91!S}d~NmW zRJu8SPX`U)V-;sw)W>=bYo=eUCi>y|M@ATNwk?v}v5(Y`g?Zf=$*o2&l|W{yqmbUhabH^ap4<~N_I@= zhrW-dAG?Hfy+ow*#W-C|b9X*i*k1j|tSpr-ADoNXSePfWQ>zs>5LIw=*cZ?D3KXnG zRWdL#Sc}D|2FA+5(ZRH_8BzKSdTcb2#fBhy(f5I7^V#G5Evd!Pvs-xqb3v;ofmKE2 zPFhTMD#VqC&CH|gqZzE!$3hk3(E$VXYRo`v$_c?Ccx9Keu~C?)(Wi6r_nc8%eyQgl zbG;WriHcO>4!kgQ#V=XCoiG*Z$qy?Q1YZy?eSEU1Ti6Y;Oe*D^f)dJ?=64RS*7C?b zExHh$W5^XAtStXKRK7!rAL^ZXgD~G{g^??_ZpjT$f%jCDmqi`Ya@FeMy&YCpw(9CI zHB5;hT3he}AIGfhvN{d6{d8WSIwNff9y;W;(J#4+`0T1I@LXlt=fXqm<8ONTV*%dK z&t~0g_ffihGMR;$vv~fVP>7y}>~_A#=tkBHq@P4;OnV1_B{zsEgrwAd7rHS8iwKb} zQH`&}{5B-E|7bD|!*>+Zl&D1mZZav>aak&4PbAt@rpwB~%zdQ77x8w*<0g*9_7DB? z_J!bV!7kPAMrV~b!qln*GCwdHmAIyFFi6_a(noC8Q^(dH zH!GDG;&3W_m3tGo_6JzOxu$K_%3=5@_2VAC%u!`cO=4K06z1XOFW9fg>5ZKtTlkJf zWaF$82QdEB6bgk{flNIf*xtaRLm)%BISaRoTW@|UWIm@Nh6Vdi>3&NG&z1O31~JB> zGNIEwZG#p(IjF3UWLfs9&}iQ26G=s>)}~CqqvLPeV`Tbyxa)LLV|X3*jWu#3;~|%=wkY>zy_dm6P7~{V09Q zs=hrDL_Po?#^gIiBD6egy_-HCR7pi%Qsv#`?mI9!P$Pz?xr5mHsGQi>;HQISUk|`- z>9c_`CDmDn-N{io2bX~RWd6i`^6hT^_UE79HA~(+H9X;6t@S4l>PmiYcHf0pG^hKu z1WF9>vo*hkzd55@aMIh!(MB_2_Q~F%WxS3~{GFqUIVgE@4oY{o-|)JE*XS1SYR(Ij zq|5-7Z!JUOk^OsnHohRpjQ{KkDDybZTJ^P!XR!{}_qCB{t@5`<9w}0VKke2kh2QJw}aCkGe<-KHX8@vy-qjj&>r3<`#wb*N8 z>x(kuXysL~#Pcf@zVU_e9kx!Y!k<;gF@A7CFoT_Gpxl2uEST&SrtRin_vHJ}@c391PI`9Ui8UU(nNh;8pRJziD8R=-wAQ#!Rr9?e1@uh3$vM1!MZnqx@)9o@GCaON)xM%u#p($013P2vn}O)Stx0 z=7pNM_9U$W=y>r=l$Pzw(JL&ySaRzD@{OTlfg$v@QVL+B>txcaV1k76nVH3v{e|Wh zE!7?m6|XZ+zRv4Ck|o6s(4fk}p&4%lq#$ZQj(@W6W2Hgp0~dk%+u{hGz|h1KauhX{ z?+3ftucnRfi7ZIFqq|6FTx9(@2fzRtQm-BwuDS5ji5e6RLdx78jx?lgZ zQ(NWlPd>&}Pp2G%Dh)13;+_wP>g!EBViM7H=`o>_c{wmVn36LPc}eEsY#77?`o$#+ z_1M>EDUPGBP zVhWLh5@W-*a+O-xkXcmQU2J9d{ylLk~#V7q$c*s_EulWwE7+ z4_L^mw!|P@QL0EMR$KTGDI z!;A0vcAfm=#DH4pZ#WQbSFh-gV`oj!7x_u4gh+3{lRfH`LF8d4S^TA;YkWT~wC)7K zPkB{b@btgHp+9Vuao!*=I;T#z4B#_X_K%pG?$~3COz=Yfi$l-)dbgdWcDTZCgi#EX z_^YGr|H|dUJ!}K_!zk0P(%e{qhTKj0tH?wuet|bSr zcc+}7C6&bXlSoye$GEkJsqX60Ifa`Gf7N6ACCSgDO8}NA3EVV0@`nZZ{;T-crJLeR z`wp|daQrx*i6&e$UA@`qdU>a@$>xzgpGgdO|H)%n#_m=_fw=rAP{Mx<@HP;{Qv98v zxMnLHzZqbo&`y5<)-jHIeQ$3}5O}_PVG#d7F0zv^1DBec`r#9;JbJ5u;<%Jq`?1wp zLd<>weHonJxsqv>kJtlaH|N)S?i?Mc-@1Q8984%DeZg~&F$cb*(%=7rFF$QKBws9m zLMc}o9Yu6JEc`3ik2JNX?(st?7RH`bcjU6qxkQ2x8LX~5-bb<2Ot@^Lf95Ay$&0T= zkAwuP+>Pb5i+TcLRQh{uM*T~^)H}$FgX-+qc>d}Zg7T0kuooS4&osaodPg7VZ(Bzl z;Pew^k{;#rKHn&ZDXPd2a9b6nI7|M1@Ck!hh-@lIVdO|i=7m|5*J` zL^y+3Pi0Us7r^t07Sfhb)G^ zt@xy6bQUIi>Tg$V`I9GuO$F#4c35$89-znF53Z^iN@~0?tbdToX+A(dqm4%zpW?N( zk!tVedH4_h+6+}+u{R+hwjP`bT#O2Tg_)J&@3cn#osOXUu8H2zJ;Q_562k=r)6rX_ zw($?T%i;}U^j*#lA`^B>6F2JvHdxBQy8UaG%w)S@6{n9!XU@^6*2s;e#FV&Lr}O(N z5yf`TlV;OGFi1boqUV`0IahPNhUR4!5OyGHy{;v6S2vrg(yxi$rsVl+vGf04y#FKo z9FcRFvGp(=oY_c==b`87rIOl}uI)wmDEv&|PrL?y1AFiH%LIO(Bq%F$>Zm(X#Qijg zK!OI0XbpHY;wO>q$Y0axdmx=|R)Hb*AQxmXifdA+z}G~&J5l_rdZkYjs@kResbP6z zEX7e9zvcoW&0S$4^W1H`BwBkDDPs&>5D_1dGPf;ySeRtU;cTmdbzgTJARXp2SGWVJ zzFv+mi0#I>3!=B>(?A5Ng#)MF1wV}jpxSC`>PW)y)_EjHaHn^zzKsnb&HXt0!i8bS z%G3b&XK`4NOOFKgEpHQhmLW7fa{Wt*eGDeq5qhj|pq6gWE@)5~ZO)a1$t4ihN927k zjcTq@jb+Cwci!~ROb1)BY9sxR(Lq-Rw}sT&_}P-3Tx-euf)R_pY|QqBvfRpK3=P{BfdT(V5LQftUD^$Ij%_Kof?q z`da*8PqSXzs-XN9c5d`9Wc~M1bpyBVl?=&J-@ zBr(r-23)Tvnc%c8Q;!zy_TswGwXq*o!R_%vgTqdAbz{B?)R;fr=q1RZX}@J}V?v4p*!))dJkO=?j|O(B~}^ z4c&j7Ya8&oVKR`J-@C*K%q})f2Q`*?bD=BSJA+mAq|HEBMAF$ETWHifa2q7T^7Ogz zi4Lq+#>#!U$IhIKC@<*mPbHy#DZ$0DHD{EA8wLg&I3IU&{J5ne7^Vb z(zBQ-^Oz*F0N#Lk-{sGBsslyku}?@O18PUGci-~r*aNP2JNqX(o6x(bfBaw*PX|S3 zX}E}41?&AS`7&xIeU(1%GSAR9c%Jg704VUfptFMy#a$^>b#O_5|^`^=OX^~Z&hSshCW)TV9l-A~Fu1!gp z)BEGro2V|t3zlwLO{#G&CUA|<{EP;1&t?oe1oelXW27iwkjpHnxh87S zm`qc+GPC8CGbr>uK1yx$)t^<^c7~8#qkeHYUlh@Ix>ykVj%m%Wbyep|RWsf}Zw9T) z42pqitKIEX`}V5e23@ML@^b zU2Q(|zD@HQ>c>f>T9OI=zR;)U1grz(BKp5pr`_u2h1V>FVnqanI zDO4`749$6;e?HWt>bKBdT>4y%K<}63QxIxL!bMiUm_lw~sgc%>s@R6|&T&8*D9Dr# zO>l7k5msiafE`E+9|Af?vyss_#{t((faZE#l}tY@hot>nLh*Uhy1eA)TNmUo^d1M_tKys{ z;BkXAcIH&ld$*0s)hk71Y?U@AjQXRqEM8Y_G?nCn`{X_ty`l($Ic%WCPWj1`y}n`5 z2jy6FZE@#`P;2iU$b=`H0G}s*hH*3#^%ueZeLz#vy4e6R%SQuy^Kng$Wme171g*4h z8j}ADB-+lh<+a0>&EIQvDV#D<^5%5jzK)kOd% z_75#Zxz;i560g2FgoBl- zR}}Xsjy#rQb=}>ksgz)_;a>%Y58ZiVPGn9Wn6_gFa-&B>p^ja3g|Ek9lu_`!K56bp zVpNU;!Od0Z4*+Nlo&V8=B`?>$vGWtzDFe5&gQuM3JxfQ=?e<%UM&SDdJ+6MdSAj_ywlsTj|A}KFZ9X=KGD`sz5u|7#FOp7`%*F1@ar77Q6TM zs1_U9UZ{TLVd1VWW)fB96-V1;Rh}TrFV4LGHX(y1R+}xYdU0kbfiDdeagTTG1ZJcM z+yA3t$uCE_=-|^5=C)aC^V*A)XN0e4N6zcu-a4{^7t1i@BYJzQb#vUp8ZL+g4IGJX zK>-*|`2Ht|i`*W-6jMe_s(#5Yde6We(qdMv;yObD@|$=bhx!tb4Cc~gfIttAyeTM` z#5aKv=8bU>wqjTa7!G`+JmMzR{(D+|{d-V~eL<c#*D2XIH1;UhhY6ni_^FZ zo&-BqrLz{9<=Oj*e1Z<#hbJ(4(APxnx0K|rhL7Y;{#T%f_4Cf#7iQqTft_}im8&eN z4-17OtAfpjydpzfIxHKR%mmJWkzVjTw&gnf+cP6DI~I2LQypt<+}o``BoNYQ!Ff4J?~4MO_;KdIhhyKS)gam9`}#2s z_r)WL{Nd=r-Yp6xvn!u#oNvD%>Tng10K)CD0CBUHp34;W93|y@IoCF<3dde#(xuaJ zYuJ^#>_1^b2g{j~sz0D1(PQ=55CR-`F!XD@l$#H+B8LL!KFC!WZ9=X9!jh_8obK z#6`Y6^HpYk89-MmKY^|s<+U#Lm=@cb?bQZk<`)M=2YsCU^UdgBI)=kWjX-wtR4YxN zyPgB*Tsh1nAT!TDk&7Py+EySsp^7BW;vi@KD-fVa@=jvTpuV@g1+%!iv`~H9?iMEH z?BQ{hxz2W7uMl4#GY68Au}NW>ZeamufQF|mQZc&na0UPhfwa}cp{I2_DXDWlX_(r0 z8M{sIXvcZt+-IezwdZh4$}2eKYrpHvhc|jxE3KcPQU4>XTpH&}?D#4)-zOKECl!fz z&1LmIRV#|+7i6KF;N*RIx+QW@iLE$9zmS7e*XWt$M9@OkG+CtLO>{~}s;oO-q*`gR z`iebB;tjzhqEmyWkpj!?)$3hfdkg&ox^j~* zASeep&`S}6j(QpavDw}?bp)OG#nl;C-I!fJ2?=sETdrA7!WfVM>)8vQQTwM?mS?S~ zuy!zZ^pPD3-b4(pSQ^OP9l2qg!vW0Ma!!|mBAibLa^=@QEvNxj7btiEgaZJhYA@Om zy7$v6S9tiyg$EBvF*#jrOSRbU1v%@|Ivjdrx7id zJ8~%T!Qip_kvH#)Clv)ZHjI^x+%$}Tg3Zu_+ zYk6WDopbPu6B8n|xRHeHv;1I!CEV68H}6c2==C0e@UEKI(3q5De4<%+p#6;mwvB#l z{R*xpF+!EU@R;IwAwSh}x07Y?n_2g79CB$#CC54U2G$_Qh|>q4>+S^36<%@iD-ijv zK8K&H?~xZc1nIh|5~g8cNfz zUN!QD-o%xDp>B-Uh{6DWDd+TW!0qnIssE^09No!TakPe->3yr?gr^uv7fWyUn?wu* z?s;}oSk-r5L>vxcFLThQ07=+rSAcAp6CEHfzKRi-aSDrEpk#fb9pTc`XZ=)#*LgcI zE4Hw;Tex|>u!aQ8nCku}QQG8Py(C~lSn&@G2RspA12RT)9M!tB6nO}ry*Q;$@N!Ck zM|Zvzv9D!8HRlSHn;n)MLn0i1dHfTR>4B#*Geo9%K#kRMH^MPPq zn^)CA-#`YM9dp75MuT8RqfusOBk$^Otr-OpJ)?ECgc|>0XqiOkO}OZrZ0G1ye(g`X z*QN4S3NV=uO%3@iV|C~46wbRgq~vz-Z=rd+iCF`3IZX|JBr)DdPS>CvH5` z;?~Adk1ziRa{Fy>d>5{~dGZ_VJLf5sy_RVq1+A7ui7JLihAIj7fwha7&+blkFu z({;Pld?&9rEZllgKxSIR&&-^)64###k8=GoJh*2!|lbbXXeAxT8$q=0=SMG-QT1k zz`N@&=1jbhpCB~rL1D-O9Z;ptL}1SJuFds*Ji#i}f8*k($a+_~H`4)J1%!XjYr`wO zl3Kc$WQ1KdZn>5cwckurt?>DGhX40UX#A%QasndL2ROztfg-7b-;WwLteQ0vq=`0g zqZQ)ggzrptB9!@__EfsksHA%cT3n4YRM*&>0Nm05%0}?ZJr*_`QKF}CWhTp8O@E<+ zANoQCw|~$#Zp^egPuM`rjjaf!l&{ajCawh1n>t zZ#{uNZ*9O9e|yn}og>{EdXxhrQSXrssfA%hy`Xb76mQR$h_p%YzlIuj24WN0vTVw#8UK;h8Cg z!5sUAVy8_pEZhSoaw;ZJ%5CZ7A{1iMh9JKOuGBMET*aQ=ugnwUpAT$jPH%Q2kJF$# z0y^%-ly~Hv77_7r_Tt5Q>?zR`a=jn8cIQfVj`LJkeq}#U zjy(&#Aa=HAB9Q^!Iq9@G!0_AxD9}I0a^*H!Rlu9+I-S0pS;W7z;$u%J&Evq;o~qtN zA3c$hSCS15+8PscsFJIgCDXvC~JgFPNj)rmjpY91JSHDbwGF zjq%S$OcOTaBGxg4$fAJG061-}C=TTj(N_g)s=0>H{K=E$T7B8;?$em4f~d#~e1AZJ zzU%moxu#pxrt0l9j~$3r>Pt*0rXlVWRPRrE*;SO@l#t}$18gh=2-QQ>`JP$9& zBgGfxm)J>~7E-%E*H#X#%U4d>g#Mef5u2laxh8OU@ogwtj<2ehm5 z;P~EsOA0_*b$W3m^I_Al{yOTi{ikgcTSm>@opekKqng1c)Ccx?Kks7#=K4W^p9Rb? z72lZE4Df?xueF?o(Sv#q!3e!i(E~cRuls|TiLBcE=WX59)tB&YZ#ElNx1Lv4O zwRvZM5)$NGNX*CA031$m)yCQ95e|F)zJ;dCkvMybEYSzG$b@m`b5UUO?ikQd#Bpr3 zORqi-0P z+gt5^nsthNa9`GN5u1c@4G+=y^=;8*6KGDR4&{U%TW<;AW*aK*rjBb|zBrhs4Sq0rN;`7hMi|21wj7+0swIus^5JE5)~VRQm$r=N^xsT9vz)+5#8T zJA-fP1HAf7WN#SrO&nLb(nc2M6NGGp#HuDST(q##Cug}Vwn>we9CoUpdZ*-LTfd4XX1e^xk`}D_nyQ&*fAmA5FkM zzI*L0mt9JXPSiYK`0}jZ7cbM$w9UglLA}7FfkiKVwzi_QDekVoJfRb%->Z$7MgC=8;Mrd=%@ykS^JLpR$@ z9@~7Cn$Bp1$%;;kP~hgSbpkNKXjF0U40iC{hb#WUEd6@y0WAa|bhB#25X{E&h(q^I zQs6$cG4G=xM%R5z2l(Z%!KeK$euh^FwtnjMv4w$|{Cl^;h+C`{&1Ta~R290(iA>ub%Yt>^R-}0tJ|J8R+Y({PUF)wvRvZs@xuq6B%y-lNSFOFb!MKZo8D$7k2PfaB=tLzD{DM z7OOb{vZ!~?|Bj{U2@C3=xOY+e$T?=h zPJA~YNB#PfC(FrXm}7z$rWQ_>yGs#-wtc+48@=F*1StQx`5Tq^MHN$owKWW`@m?qP z8tbFI9d)yVg<&nvEHzMJ>X8>tq>xsm!Jz1CNnoDEFQoGrLu6RM^7-v4>;5@>woszJkx zIP~smT!0_U`KXd5wh+a?17rLKD@WoqkA)cVYT=w#iwCAZXa?^`D9$s0AaNF(TLr_m zR>3@R=`K6qE(6gKc}uceNp$DY9#GB08w;)59}?|qD%w1r#k=oAi!!G1y?>7j-ljMt zj3d~lr#}Kfct0c#DF5kCbr-~Q?*)WqPgGb=dOU{CZbs3;RaKyeNabtB;e4H+ ztMEAzP(l2v^V1?)?mTYN2inBou7BZw%XOjX3XQ&!ymFlvXq|2|D8D9(%Y1eX&Z#XW zVeIaMW5Hcq&1C|uOy?@L`v2s@0_EDpY5YoxxRgk4AZ~$f9`--im#CMI-$B~{2*PV3 zB{4GgN0N_2i;DTp{4?$rU%V9eDMl3|ewQN)O5IBQ#c_nwp$ONnQvd>0h+qhSzAGBR zm%Vr^mgX5@4DJ)emDdnp6?Q0I1z>*!di#Lem-NndWblq1{LS^xKEJc357cFn;H#hU zrVF+~9->yH-EEP>F|d8y;X!-G^ZY>jWl$l26ACvd?hg0Zf`oj+TFxPfxhasBBwiRo z?eKdXIdD+)`Dn4Dqzd`*AU|Ko`|QJc=f+U!NG<$k`v0(;c=10@HC}~pQJ#>jwRY8E z`>m{XYwVFOe5NUW2T}RjahF#n$LtgQpiVPC_=N9Y<_DbDkb=U3$nl%h@?@kczpqrG z*yh72IXonHE3*M^We{W96s;`;;8w;jJT|gC)#qXXGk-3B2Cyp=gkszuVt`#)SO6IY zN=#qpJ%IVJ{y(Y|a&sJB3ux_g&m(8DO`L_3jy_+7q8>%9SeVfv%`x(X;z@~!-HWT? zj@p#4(fgS4W_^INBq&B%fPQ%vn}d&|-E|0YT~aK*>s^LK98ctWWEG5(Zd9TJ@N_lz zv-gS{)HPa~<6ya}{CIE!mEBj|K?P5o!;Z!f^w@k`M8A5I&$C_WNK$`^^S#i_MR+7I zp*4I&B{V-UjTk_@7@NG(pRic$=%|X+RFnJpf}1$ym11VzIDZZ>^Nh!s3#!F~Bl_2Q zQ$KgsZ442fHJL21@-Y-3k6jrCp8qxTfGRLIfn=?NnKS4H2JATJ0i_grX({o)<{+T( zU`uPdzVY6RINoKrMW~MXjv1?0FGduMh=8aW#i|h92v1fwm z$Vv+Pie?L!8jq)K>YKga#6CAU#`hmd(hcPFZ-;XD-d9essK&_SAuYV+!38o zPVs$Qrf>~Ty_cs_`BA`hXK&f3K)xHZvNo#3`Djr=(2Ycq{c@7+GO=Fq@Ko|eUaw2@ zBB$W2P<8&3sG8`vDWCo#4GdVD zv`cKMT%BY5v^tMP$me46wTX#k`M4282Ly3=B&%TgYkIoAa#vMbQL|{@9#dD*!55NkKUjkge%6tFYzraN7~)SZ+Ud(I?&OCx8X%6R4f zt2qE<7Yh~`&C#dD2KJW+n6D%)P5j~WP|7wEb<6Q4b+x5F!<-rG|&Emy}O zO9xX54AGjjEm~eoR=hA@4e)(|Kl}1X`eEqx15i_MzpBh2FY&4f5*Im?}&~leQqRH;H_8&}J<7<42 za9%HtH?^Pta%7}!Aqo8(T(x+$KIc18wk1>z^KK45QF zm^O958-z|BCVf>R_5+j_Eo3t9$NP5cu`M;#YRdi#zm;=oHUo3UkHh*Agg(^Kh^@uU zz-5Pi7oS0L9QVs6#iuoq&$f2g*de^pOu=C8`O7Z?8ysn#h~`7P;k(+Rd&$mA#8%tHcqBIHu#3=P`>lT6v5 z=Za#)?Jt6F{m1u|k0L%l201^5(jHq6&@M10 zw~`36t~ECb7r4GiEhhxEd8~qhp`*ny%?z)#S0aZcvR*@Vd(F8c_G?+>LRV<*z+oxUTK=+=+WA&c@w<8|5#B z_R*vrP}5cKOv|cZR2cMi6-+nIR9yx(U;g_g|2`UR{>ZMW>?#-z=tebUM)0*?2IV>> zCkDZaD~>qp#^Em^U`~hD;nLW2rn`R-!I0niZ6X+I!N1O7F0q>GN|BKUc^$j7S=M>* z@hQ1fFeR7B#TYhwm%;AGt+C)_~Kedlv3by)WacK;xLcYvRVuq8Gz@Hlxc zUR*2A?mrcm;w!^~kwlcw3IG1dW=K>ggGtn!&2iWg)b)mn@IPT^Yp;;3qBbdX%F1zbFX3P&mn1+wUH{KE4cU^!SE=vnineSkbN`PX)C+_p0c6zp=(vGyawO3<}G04(rv>jx%*PPpXs>h@{~+ z1MU}P?#vfiT(C zA={`Ngnh$QzQSM?^!q6=juMwnx}|q#FeXL#j<~z~Hf=shVE5FO@43#DHjMW6=wpP7 z7F2Rhm`F6zc=)WGa-tjgZsEj;+rRx#Qakm8HfCUu7V^f<49Xu)%Hib8auPc-qxLg| z*{V;n^*x_u8D>AY#M65iY59RR}X@XAw* zz~Ot!t5^bI{rM+zGJj63m{F#imG&8##I!CY96)D1E%S>gwB}_zFb&Jja=$wgtuLVh zK0XbiF%Le5f{}zm(@nA!TW0)i4JLw234X(^QdH{4SO{jtc|_7V+}qIFs?R1y1?Tl{ zP{r<&hL=Jprp4_dPsawAwsW*W&~@sjo2=l1XsZr}_gd0_X7#_HTNuo(8Q40nx2pEM zk~GF$+lmd0Tv9Mhy_0QjUF$c+X+9;-XE*K^BRiy^JIo383SHaOl>3TM4C3pPsQpDF z3f|U#>eF)6CtfUwUIWc5(NO=3+~`` zfwKa4I1u&s_7=|?(sG5D1TDsWwdqhI=V&nA5t>{;$@~G^(j{-Q#Kz zaj0OaSfCQCM-h=CdO$@*_xr?bqCXEYPz|*; zDeyx1w&_Ep(u`#*1H)C}K+7_bXXlPvJjtC>Ns2m(!Y?gLpn4oP&(!B#qUm;DB&?&y z8E#*F?jD7(WI=NyYA8TC*b;HrEVMFlH>Mu-DmgOdSccD2$f>CQ{o+Q`Qu^X1Muj*B z1bnuyP@bU#<_CNx z9Gr6^WWK&csd4pgr);g#LtHCpOdRqKp+|L@bav`2M4xzd;=l+%8y@c*_Mk%mszzgR zm&bNpi1|eO>uY?KU&Bdy{?a3TN2%ik!t;TGauBt*5p+oiW0oWZW>#PIF&-mUTtA<4 zF^lT`N=rQ{F3xaBN9FVYZO-Z6peZhTE_66SvBRg`8jYhqa=jtq7(sXB2pV!? zRUsJC+FPqL>4fYcn%id7$bwdKPllDWBZ`@-ewF`-Ly&9TA2QvD$0w^Rq91a7fgaPr z-&@UjRJ-vc*avzKSJKu#2+R(l1SN< zl3%l^ANOOG7UkluiTQ4WjA{+zRd5t$cYCMjIt~?-{JQ-QrVoDi*UTm1j`Y;8^~+hJ z`V0?>^R*fJ!Z+R`4oTkW<|Lm84`7yEVk}a6^i$*h;g4T~h1KBevv_!Sx zEzNk;*-p%}>$@8#cmJ4#E9&1{x5}%&L)WD~6y&yrQmM_aqkN}EuzuBZ!HZd9a|pBw z^oe9y-$ROk!GGpI?v!_K=Adg(`t2fctS49=Tn^)9f?J^m2oMq>JLNFx5 zyFfGMfV>-M{lP7B1&h0mzv@lE!iPncd`1N+;=AYT_yxrI9ek$tP^eud7X?PbX)`qX zU@$2bv8S2EYicRzgJ&S`ZuU~|V2%o2FW#g_hmAM*l`*pfcT=mGhKV##T6o~BBGn_o zkov&<2BI{f;zn|(9JvQ}=hffDJ>i}ijdiOURl z32p3EYBVP|)u+G|m}ordE6llQI=vSi-U*V32c|vGhGMqTp7q*?i_Wo=5Os3qKOjJYNOmk#F+%((PZ}R(?bZ6OYh^%Hj+kl{H@JW_J90+AusnwXn zTGUSB`b?Au#rVDDU3re?XE(G%r>K>+G;T_ews&cXY1f1dXCOy+BDLLHf5=hGG?Cxz zf%>(Wmt<{Brg_}B_(AEq4HKN^2S&6r%Ksrp<)VT*$}ptYN=sW2Dw0{->Bm0cI(t=5 zMxQ?0xaczPF@+sYe|JWbt75h1>^GKvqEXunMIWwIx>a8v`k7#4z^6HF=>WZUh-rF` zN9S|9wOUlgb%ez-Ydh<13|VI2__`P)+2uzJNn%I%!*^8cz$fY|OdZC7BBI_Rv}bM8 zl-^XCZqxW1L2?sWq&wfY#$Dth`kB#pOw?t zxi;`tSEKY{8npS)Oj<;|YtYgrKz-;q?G`7&>k0&&h6@g1vpCK&`HIjsv~^U;%F%+# zDIL0~khg#3VCj+eWiHm)y0-D$SYs$o?g!%cA4u_={@6k|mD|M=rLza2<3N<}YSw1C z=)!q!N2&~(!IeGqH5WLqtaH=A{y=>=29goo1Ux|iw`u)`g%q<^cEIV;Npnv@-s7~y&kRX>bvddk>C%f}&cQ&V@Or9S<5FWxYk0$<}{ z7IgO&kqf);{}z#}+w?I&6q9Q?&CFC%*zwDK-j z2fqGV!2k1Z|MDZmb{06t5iH~HuvGOISURBl087V*X~~+F%4Eo?=Tq=i8yz+S#y!Lk zzo+qTJN$2W$HIH3*^C(`dv@*c`1V6v02AVcOu>|pKvQmy7*Q9L3ecg%LpRQI6;2jF z+Y%3Vi!By6%6I9?RIJP{QWI!>Mo{}omZK}u(=bmGbLm{2Hhz)NYdPB~fqT?E&f%XT zf&xw$I1_%R>Z;)tM@6OST?AZgQ{#K4wn_Tgd|;6(p`0gJ zN4ifqlBvJ`SU`n|0GOKB!{gU@;$0K-dca$t{_9V3n_@8mB!EmOF7PeI+#brwi~kuk z-SY=D<$Db-ejuk#=JSYc>?cmwzR{WVJ6_9=#s}y9AE@co6zPlcalY-4lYQ9!WhhY> zcX1i-P4zD-yAzA*cp)_FA;J)0we)<~jxDFbZOflvj!!nCzQNOeu0XJ*Zd2$~C}4S=5ot|7jsnQn z=aC9$_acaN@+U0NH$cIE*7 zs=wzn$-hgR;-<@xaO9UpY_t=HVbP$8R8Z&*OwyYIj z**%CT*u?5q2y?22j#?TVr7ZVDSDf6Fs zHD?HAUS^z`)Z_s;mmkjXo3AQy4yzAZ8{R{XRT(;NuY~ZadtJ6SBXyD0>%7~m;$g44 zH;})F#_5^peK)O3RvV-*B{~Q%=m`()lYo1?xI?OI%Dm&Il%T3R^XoN-+7x8JNZF&emPwOsM}-$-*{n%T{}a$u zm3b(ENZsNZ~PwDj!(mwT6JB)R_d zCBvs+^aNo1^JJiwR+~eWYRBi>r#ArZbA?urV=&mdThPcX)CFe4qFtp&2b-DElHV+9 z9Y->nZI8ySWfwe>wu-;!M8}K|zZ*GzwRrQI(6lcRELEX(5`fFF7PvV|mGf5|#unel z|DBQ^I81A)v?St7)HNNLImCQ|t{Huky;SjYolYW&Tl_}4#R)UsHT~s2<~mNfUJt?A zQ2&}la=8sxi7Z;-wIu$^R~M+zR?YNe_bK2b_>> z?@bf1DI08PzBL+OOcO7EZ@68;Rn>)BM`lx6n2hcZlr(q!#`&$`ys?cVZzu}`en45X zeFM%xpgl^(2CzRHkF-C_T=EAc{ptfHjo3vQIzAG+oMtp!urDRCAkgK<=bo!EvDEuF zYjj<3+FqBB8W7?)b|t`GAh$XBR8o?rD$`aLz4tx(-p7~@65}(jfS@wwUEbbiab~Su^Xs*VhO1=lPLdqOphSq9r03 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 004/310] 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 005/310] 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 006/310] 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 45ebcf6f0e9f1f86601e83d83a55797e21628a85 Mon Sep 17 00:00:00 2001 From: JtotheThree Date: Fri, 7 Jan 2022 20:01:52 -0600 Subject: [PATCH 007/310] form prevent_default --- packages/html/src/elements.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 1a2f36e57..4b53f91f0 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1047,7 +1047,7 @@ impl input { /// - `text` /// - `time` /// - `url` - /// - `week` + /// - `week` pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { cx.attr("type", val, None, false) } @@ -1106,6 +1106,12 @@ impl a { } } +impl form { + pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { + cx.attr("dioxus-prevent-default", val, None, false) + } +} + builder_constructors! { // SVG components /// Build a From e69fbb4baa3de3902a746a0cb2b06338e33dcaa9 Mon Sep 17 00:00:00 2001 From: JtotheThree Date: Fri, 7 Jan 2022 20:20:52 -0600 Subject: [PATCH 008/310] login_form example --- examples/login_form.rs | 89 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 examples/login_form.rs diff --git a/examples/login_form.rs b/examples/login_form.rs new file mode 100644 index 000000000..fdd1843ce --- /dev/null +++ b/examples/login_form.rs @@ -0,0 +1,89 @@ +//! This example demonstrates the following: +//! Futures in a callback, Router, and Forms + +use dioxus::events::*; +use dioxus::prelude::*; +use dioxus::router::{Link, Router, Route, RouterService}; + +fn main() { + dioxus::desktop::launch(APP); +} + +static APP: Component = |cx| { + cx.render(rsx!{ + Router { + Route { to: "/", home() } + Route { to: "/login", login() } + } + }) +}; + +fn home(cx: Scope) -> Element { + cx.render(rsx! { + h1 { "Welcome Home" } + Link { to: "/login", "Login" } + }) +} + + +fn login(cx: Scope) -> Element { + let username = use_state(&cx, String::new); + let password = use_state(&cx, String::new); + + let service = cx.consume_context::()?; + + let onsubmit = move |_| { + cx.push_future({ + let (username, password) = (username.get().clone(), password.get().clone()); + let service = service.clone(); + + async move { + let params = [ + ("username", username.to_string()), + ("password", password.to_string()) + ]; + + let resp = reqwest::Client::new() + .post("http://localhost/login") + .form(¶ms) + .send() + .await; + + match resp { + Ok(data) => { + // Parse data from here, such as storing a response token + service.push_route("/"); + } + Err(err) => {} //Handle any errors from the fetch here + } + } + }); + }; + + cx.render(rsx!{ + h1 { "Login" } + form { + onsubmit: onsubmit, + // Prevent the default behavior of
to post + prevent_default: "onsubmit", + input { + oninput: move |evt| username.set(evt.value.clone()) + } + label { + "Username" + } + br {} + input { + oninput: move |evt| password.set(evt.value.clone()), + r#type: "password" + } + label { + "Password" + } + br {} + button { + "Login" + } + } + }) +} \ No newline at end of file From 395558232e2b1e2a2e5985adb723f12e3bd76f78 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 12 Jan 2022 09:40:36 -0500 Subject: [PATCH 009/310] 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 010/310] 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 011/310] 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 012/310] 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 abfac0d59b620cdf3af77c80aadb07266e0d651b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 17 Jan 2022 16:37:44 -0500 Subject: [PATCH 013/310] wip: move macro lib out of proc macro crate --- Cargo.toml | 5 +- examples/rsx_autocomplete.rs | 27 ++++++ packages/core-macro/Cargo.toml | 1 + packages/core-macro/src/lib.rs | 7 +- packages/macro-inner/Cargo.toml | 13 +++ .../{core-macro => macro-inner}/src/htm.rs | 0 .../{core-macro => macro-inner}/src/ifmt.rs | 0 .../src/inlineprops.rs | 0 packages/macro-inner/src/lib.rs | 5 + .../src/props/mod.rs | 0 .../{core-macro => macro-inner}/src/router.rs | 0 .../src/rsx/component.rs | 8 +- .../src/rsx/element.rs | 93 ++++++++++++------- .../src/rsx/mod.rs | 4 +- .../src/rsx/node.rs | 0 .../src/rsxtemplate.rs | 0 16 files changed, 116 insertions(+), 47 deletions(-) create mode 100644 examples/rsx_autocomplete.rs create mode 100644 packages/macro-inner/Cargo.toml rename packages/{core-macro => macro-inner}/src/htm.rs (100%) rename packages/{core-macro => macro-inner}/src/ifmt.rs (100%) rename packages/{core-macro => macro-inner}/src/inlineprops.rs (100%) create mode 100644 packages/macro-inner/src/lib.rs rename packages/{core-macro => macro-inner}/src/props/mod.rs (100%) rename packages/{core-macro => macro-inner}/src/router.rs (100%) rename packages/{core-macro => macro-inner}/src/rsx/component.rs (98%) rename packages/{core-macro => macro-inner}/src/rsx/element.rs (80%) rename packages/{core-macro => macro-inner}/src/rsx/mod.rs (97%) rename packages/{core-macro => macro-inner}/src/rsx/node.rs (100%) rename packages/{core-macro => macro-inner}/src/rsxtemplate.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 884513a5b..98da05bcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "./packages/core", version = "^0.1.7" } dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.6", optional = true } +dioxus-macro-inner = { path = "./packages/macro-inner", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } @@ -27,7 +28,7 @@ dioxus-mobile = { path = "./packages/mobile", version = "^0.0.3", optional = tru [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro"] +macro = ["dioxus-core-macro", "dioxus-macro-inner"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] @@ -35,6 +36,7 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] + # "dioxus-router/web" # "dioxus-router/desktop" # desktop = ["dioxus-desktop", "dioxus-router/desktop"] @@ -46,6 +48,7 @@ router = ["dioxus-router"] members = [ "packages/core", "packages/core-macro", + "packages/macro-inner", "packages/html", "packages/hooks", "packages/web", diff --git a/examples/rsx_autocomplete.rs b/examples/rsx_autocomplete.rs new file mode 100644 index 000000000..99afe7521 --- /dev/null +++ b/examples/rsx_autocomplete.rs @@ -0,0 +1,27 @@ +//! This example shows that autocomplete works in RSX + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + onclick: move |_| { + } + // class: "asd", + // style { + // media: "Ad", + // } + // div { + + // } + // { + // let t = String::new(); + // t. + // } + } + }) +} diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index ee3dc2883..8f2b81cba 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] +dioxus-macro-inner = { path = "../macro-inner" } once_cell = "1.8" proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 4ad7543a2..3579039b5 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,13 +1,8 @@ +use dioxus_macro_inner::*; use proc_macro::TokenStream; use quote::ToTokens; use syn::parse_macro_input; -pub(crate) mod ifmt; -pub(crate) mod inlineprops; -pub(crate) mod props; -pub(crate) mod router; -pub(crate) mod rsx; - #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { use ifmt::*; diff --git a/packages/macro-inner/Cargo.toml b/packages/macro-inner/Cargo.toml new file mode 100644 index 000000000..63fcbbd9b --- /dev/null +++ b/packages/macro-inner/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dioxus-macro-inner" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +once_cell = "1.8" +proc-macro-error = "1.0.4" +proc-macro2 = { version = "1.0.6" } +quote = "1.0" +syn = { version = "1.0.11", features = ["full", "extra-traits"] } diff --git a/packages/core-macro/src/htm.rs b/packages/macro-inner/src/htm.rs similarity index 100% rename from packages/core-macro/src/htm.rs rename to packages/macro-inner/src/htm.rs diff --git a/packages/core-macro/src/ifmt.rs b/packages/macro-inner/src/ifmt.rs similarity index 100% rename from packages/core-macro/src/ifmt.rs rename to packages/macro-inner/src/ifmt.rs diff --git a/packages/core-macro/src/inlineprops.rs b/packages/macro-inner/src/inlineprops.rs similarity index 100% rename from packages/core-macro/src/inlineprops.rs rename to packages/macro-inner/src/inlineprops.rs diff --git a/packages/macro-inner/src/lib.rs b/packages/macro-inner/src/lib.rs new file mode 100644 index 000000000..69e4b743f --- /dev/null +++ b/packages/macro-inner/src/lib.rs @@ -0,0 +1,5 @@ +pub mod ifmt; +pub mod inlineprops; +pub mod props; +pub mod router; +pub mod rsx; diff --git a/packages/core-macro/src/props/mod.rs b/packages/macro-inner/src/props/mod.rs similarity index 100% rename from packages/core-macro/src/props/mod.rs rename to packages/macro-inner/src/props/mod.rs diff --git a/packages/core-macro/src/router.rs b/packages/macro-inner/src/router.rs similarity index 100% rename from packages/core-macro/src/router.rs rename to packages/macro-inner/src/router.rs diff --git a/packages/core-macro/src/rsx/component.rs b/packages/macro-inner/src/rsx/component.rs similarity index 98% rename from packages/core-macro/src/rsx/component.rs rename to packages/macro-inner/src/rsx/component.rs index 5257022a4..26305359d 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/macro-inner/src/rsx/component.rs @@ -23,10 +23,10 @@ use syn::{ }; pub struct Component { - name: syn::Path, - body: Vec, - children: Vec, - manual_props: Option, + pub name: syn::Path, + pub body: Vec, + pub children: Vec, + pub manual_props: Option, } impl Parse for Component { diff --git a/packages/core-macro/src/rsx/element.rs b/packages/macro-inner/src/rsx/element.rs similarity index 80% rename from packages/core-macro/src/rsx/element.rs rename to packages/macro-inner/src/rsx/element.rs index ebc401b6e..c612e9f1b 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/macro-inner/src/rsx/element.rs @@ -4,19 +4,18 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, - Expr, Ident, LitStr, Result, Token, + Expr, ExprClosure, Ident, LitStr, Result, Token, }; // ======================================= // Parse the VNode::Element type // ======================================= pub struct Element { - name: Ident, - key: Option, - attributes: Vec, - listeners: Vec, - children: Vec, - _is_static: bool, + pub name: Ident, + pub key: Option, + pub attributes: Vec, + pub children: Vec, + pub _is_static: bool, } impl Parse for Element { @@ -28,7 +27,6 @@ impl Parse for Element { syn::braced!(content in stream); let mut attributes: Vec = vec![]; - let mut listeners: Vec = vec![]; let mut children: Vec = vec![]; let mut key = None; let mut _el_ref = None; @@ -54,6 +52,7 @@ impl Parse for Element { }); } else { let value = content.parse::()?; + attributes.push(ElementAttrNamed { el_name: el_name.clone(), attr: ElementAttr::CustomAttrExpression { name, value }, @@ -82,13 +81,24 @@ impl Parse for Element { content.parse::()?; if name_str.starts_with("on") { - listeners.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, - }, - }); + if content.fork().parse::().is_ok() { + // + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventClosure { + name, + closure: content.parse()?, + }, + }); + } else { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventTokens { + name, + tokens: content.parse()?, + }, + }); + } } else { match name_str.as_str() { "key" => { @@ -182,7 +192,7 @@ impl Parse for Element { name: el_name, attributes, children, - listeners, + // listeners, _is_static: false, }) } @@ -193,14 +203,29 @@ impl ToTokens for Element { let name = &self.name; let children = &self.children; - let listeners = &self.listeners; - let attr = &self.attributes; + // let listeners = &self.listeners; let key = match &self.key { Some(ty) => quote! { Some(format_args_f!(#ty)) }, None => quote! { None }, }; + let listeners = self.attributes.iter().filter(|f| { + if let ElementAttr::EventTokens { .. } = f.attr { + true + } else { + false + } + }); + + let attr = self.attributes.iter().filter(|f| { + if let ElementAttr::EventTokens { .. } = f.attr { + false + } else { + true + } + }); + tokens.append_all(quote! { __cx.element( dioxus_elements::#name, @@ -213,29 +238,29 @@ impl ToTokens for Element { } } -enum ElementAttr { - // attribute: "valuee {}" +pub enum ElementAttr { + /// attribute: "valuee {}" AttrText { name: Ident, value: LitStr }, - // attribute: true, + /// attribute: true, AttrExpression { name: Ident, value: Expr }, - // "attribute": "value {}" + /// "attribute": "value {}" CustomAttrText { name: LitStr, value: LitStr }, - // "attribute": true, + /// "attribute": true, CustomAttrExpression { name: LitStr, value: Expr }, - // // onclick: move |_| {} - // EventClosure { name: Ident, closure: ExprClosure }, + /// onclick: move |_| {} + EventClosure { name: Ident, closure: ExprClosure }, - // onclick: {} + /// onclick: {} EventTokens { name: Ident, tokens: Expr }, } -struct ElementAttrNamed { - el_name: Ident, - attr: ElementAttr, +pub struct ElementAttrNamed { + pub el_name: Ident, + pub attr: ElementAttr, } impl ToTokens for ElementAttrNamed { @@ -263,11 +288,11 @@ impl ToTokens for ElementAttrNamed { __cx.attr( #name, format_args_f!(#value), None, false ) } } - // ElementAttr::EventClosure { name, closure } => { - // quote! { - // dioxus_elements::on::#name(__cx, #closure) - // } - // } + ElementAttr::EventClosure { name, closure } => { + quote! { + dioxus_elements::on::#name(__cx, #closure) + } + } ElementAttr::EventTokens { name, tokens } => { quote! { dioxus_elements::on::#name(__cx, #tokens) diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/macro-inner/src/rsx/mod.rs similarity index 97% rename from packages/core-macro/src/rsx/mod.rs rename to packages/macro-inner/src/rsx/mod.rs index 5ff5d85f7..87b324dfc 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/macro-inner/src/rsx/mod.rs @@ -29,8 +29,8 @@ use syn::{ }; pub struct CallBody { - custom_context: Option, - roots: Vec, + pub custom_context: Option, + pub roots: Vec, } impl Parse for CallBody { diff --git a/packages/core-macro/src/rsx/node.rs b/packages/macro-inner/src/rsx/node.rs similarity index 100% rename from packages/core-macro/src/rsx/node.rs rename to packages/macro-inner/src/rsx/node.rs diff --git a/packages/core-macro/src/rsxtemplate.rs b/packages/macro-inner/src/rsxtemplate.rs similarity index 100% rename from packages/core-macro/src/rsxtemplate.rs rename to packages/macro-inner/src/rsxtemplate.rs From 174d2870625c8f052b041207c15c1062e8356c14 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 17 Jan 2022 23:02:36 -0500 Subject: [PATCH 014/310] wip: extract to rsx module --- Cargo.toml | 4 +- .../{macro-inner => core-macro}/src/ifmt.rs | 0 .../src/inlineprops.rs | 0 .../src/props/mod.rs | 0 packages/macro-inner/src/router.rs | 214 ------------------ packages/macro-inner/src/rsxtemplate.rs | 84 ------- packages/{macro-inner => rsx}/Cargo.toml | 2 +- packages/{macro-inner => rsx}/src/htm.rs | 0 packages/{macro-inner => rsx}/src/lib.rs | 1 - .../{macro-inner => rsx}/src/rsx/component.rs | 0 .../{macro-inner => rsx}/src/rsx/element.rs | 0 packages/{macro-inner => rsx}/src/rsx/mod.rs | 0 packages/{macro-inner => rsx}/src/rsx/node.rs | 0 13 files changed, 3 insertions(+), 302 deletions(-) rename packages/{macro-inner => core-macro}/src/ifmt.rs (100%) rename packages/{macro-inner => core-macro}/src/inlineprops.rs (100%) rename packages/{macro-inner => core-macro}/src/props/mod.rs (100%) delete mode 100644 packages/macro-inner/src/router.rs delete mode 100644 packages/macro-inner/src/rsxtemplate.rs rename packages/{macro-inner => rsx}/Cargo.toml (91%) rename packages/{macro-inner => rsx}/src/htm.rs (100%) rename packages/{macro-inner => rsx}/src/lib.rs (79%) rename packages/{macro-inner => rsx}/src/rsx/component.rs (100%) rename packages/{macro-inner => rsx}/src/rsx/element.rs (100%) rename packages/{macro-inner => rsx}/src/rsx/mod.rs (100%) rename packages/{macro-inner => rsx}/src/rsx/node.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 98da05bcc..c8075c545 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "./packages/core", version = "^0.1.7" } dioxus-html = { path = "./packages/html", version = "^0.1.4", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.6", optional = true } -dioxus-macro-inner = { path = "./packages/macro-inner", optional = true } +dioxus-macro-inner = { path = "./packages/rsx", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.6", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.4", optional = true } @@ -48,7 +48,7 @@ router = ["dioxus-router"] members = [ "packages/core", "packages/core-macro", - "packages/macro-inner", + "packages/rsx", "packages/html", "packages/hooks", "packages/web", diff --git a/packages/macro-inner/src/ifmt.rs b/packages/core-macro/src/ifmt.rs similarity index 100% rename from packages/macro-inner/src/ifmt.rs rename to packages/core-macro/src/ifmt.rs diff --git a/packages/macro-inner/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs similarity index 100% rename from packages/macro-inner/src/inlineprops.rs rename to packages/core-macro/src/inlineprops.rs diff --git a/packages/macro-inner/src/props/mod.rs b/packages/core-macro/src/props/mod.rs similarity index 100% rename from packages/macro-inner/src/props/mod.rs rename to packages/core-macro/src/props/mod.rs diff --git a/packages/macro-inner/src/router.rs b/packages/macro-inner/src/router.rs deleted file mode 100644 index 3edad75e3..000000000 --- a/packages/macro-inner/src/router.rs +++ /dev/null @@ -1,214 +0,0 @@ -#![allow(dead_code)] - -use proc_macro2::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::{Data, DeriveInput, Fields, Ident, LitStr, Variant}; - -const AT_ATTR_IDENT: &str = "at"; -const NOT_FOUND_ATTR_IDENT: &str = "not_found"; - -pub struct Routable { - ident: Ident, - ats: Vec, - variants: Punctuated, - not_found_route: Option, -} - -impl Parse for Routable { - fn parse(input: ParseStream) -> syn::Result { - let DeriveInput { ident, data, .. } = input.parse()?; - - let data = match data { - Data::Enum(data) => data, - Data::Struct(s) => { - return Err(syn::Error::new( - s.struct_token.span(), - "expected enum, found struct", - )) - } - Data::Union(u) => { - return Err(syn::Error::new( - u.union_token.span(), - "expected enum, found union", - )) - } - }; - - let (not_found_route, ats) = parse_variants_attributes(&data.variants)?; - - Ok(Self { - ident, - variants: data.variants, - ats, - not_found_route, - }) - } -} - -fn parse_variants_attributes( - variants: &Punctuated, -) -> syn::Result<(Option, Vec)> { - let mut not_founds = vec![]; - let mut ats: Vec = vec![]; - - let mut not_found_attrs = vec![]; - - for variant in variants.iter() { - if let Fields::Unnamed(ref field) = variant.fields { - return Err(syn::Error::new( - field.span(), - "only named fields are supported", - )); - } - - let attrs = &variant.attrs; - let at_attrs = attrs - .iter() - .filter(|attr| attr.path.is_ident(AT_ATTR_IDENT)) - .collect::>(); - - let attr = match at_attrs.len() { - 1 => *at_attrs.first().unwrap(), - 0 => { - return Err(syn::Error::new( - variant.span(), - format!( - "{} attribute must be present on every variant", - AT_ATTR_IDENT - ), - )) - } - _ => { - return Err(syn::Error::new_spanned( - quote! { #(#at_attrs)* }, - format!("only one {} attribute must be present", AT_ATTR_IDENT), - )) - } - }; - - let lit = attr.parse_args::()?; - ats.push(lit); - - for attr in attrs.iter() { - if attr.path.is_ident(NOT_FOUND_ATTR_IDENT) { - not_found_attrs.push(attr); - not_founds.push(variant.ident.clone()) - } - } - } - - if not_founds.len() > 1 { - return Err(syn::Error::new_spanned( - quote! { #(#not_found_attrs)* }, - format!("there can only be one {}", NOT_FOUND_ATTR_IDENT), - )); - } - - Ok((not_founds.into_iter().next(), ats)) -} - -impl Routable { - // fn build_from_path(&self) -> TokenStream { - // let from_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { - // let ident = &variant.ident; - // let right = match &variant.fields { - // Fields::Unit => quote! { Self::#ident }, - // Fields::Named(field) => { - // let fields = field.named.iter().map(|it| { - // //named fields have idents - // it.ident.as_ref().unwrap() - // }); - // quote! { Self::#ident { #(#fields: params.get(stringify!(#fields))?.parse().ok()?,)* } } - // } - // Fields::Unnamed(_) => unreachable!(), // already checked - // }; - - // let left = self.ats.get(i).unwrap(); - // quote! { - // #left => ::std::option::Option::Some(#right) - // } - // }); - - // quote! { - // fn from_path(path: &str, params: &::std::collections::HashMap<&str, &str>) -> ::std::option::Option { - // match path { - // #(#from_path_matches),*, - // _ => ::std::option::Option::None, - // } - // } - // } - // } - - // fn build_to_path(&self) -> TokenStream { - // let to_path_matches = self.variants.iter().enumerate().map(|(i, variant)| { - // let ident = &variant.ident; - // let mut right = self.ats.get(i).unwrap().value(); - - // match &variant.fields { - // Fields::Unit => quote! { Self::#ident => ::std::string::ToString::to_string(#right) }, - // Fields::Named(field) => { - // let fields = field - // .named - // .iter() - // .map(|it| it.ident.as_ref().unwrap()) - // .collect::>(); - - // for field in fields.iter() { - // // :param -> {param} - // // so we can pass it to `format!("...", param)` - // right = right.replace(&format!(":{}", field), &format!("{{{}}}", field)) - // } - - // quote! { - // Self::#ident { #(#fields),* } => ::std::format!(#right, #(#fields = #fields),*) - // } - // } - // Fields::Unnamed(_) => unreachable!(), // already checked - // } - // }); - - // quote! { - // fn to_path(&self) -> ::std::string::String { - // match self { - // #(#to_path_matches),*, - // } - // } - // } - // } -} - -pub fn routable_derive_impl(input: Routable) -> TokenStream { - let Routable { - // ats, - // not_found_route, - // ident, - .. - } = &input; - - // let from_path = input.build_from_path(); - // let to_path = input.build_to_path(); - - quote! { - // #[automatically_derived] - // impl ::dioxus::router::Routable for #ident { - - // fn recognize(pathname: &str) -> ::std::option::Option { - // todo!() - // // ::std::thread_local! { - // // static ROUTER: ::dioxus::router::__macro::Router = ::dioxus::router::__macro::build_router::<#ident>(); - // // } - // // let route = ROUTER.with(|router| ::dioxus::router::__macro::recognize_with_router(router, pathname)); - // // { - // // let route = ::std::clone::Clone::clone(&route); - // // #cache_thread_local_ident.with(move |val| { - // // *val.borrow_mut() = route; - // // }); - // // } - // // route - // } - // } - } -} diff --git a/packages/macro-inner/src/rsxtemplate.rs b/packages/macro-inner/src/rsxtemplate.rs deleted file mode 100644 index 9222d7117..000000000 --- a/packages/macro-inner/src/rsxtemplate.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::{rsx::RsxBody, util::is_valid_svg_tag}; - -use { - proc_macro::TokenStream, - proc_macro2::{Span, TokenStream as TokenStream2}, - quote::{quote, ToTokens, TokenStreamExt}, - syn::{ - ext::IdentExt, - parse::{Parse, ParseStream}, - token, Error, Expr, ExprClosure, Ident, LitBool, LitStr, Path, Result, Token, - }, -}; - -// ============================================== -// Parse any stream coming from the html! macro -// ============================================== -pub struct RsxTemplate { - inner: RsxBody, -} - -impl Parse for RsxTemplate { - fn parse(s: ParseStream) -> Result { - if s.peek(LitStr) { - use std::str::FromStr; - - let lit = s.parse::()?; - let g = lit.span(); - let mut value = lit.value(); - if value.ends_with('\n') { - value.pop(); - if value.ends_with('\r') { - value.pop(); - } - } - let lit = LitStr::new(&value, lit.span()); - - // panic!("{:#?}", lit); - match lit.parse::() { - Ok(r) => Ok(Self { inner: r }), - Err(e) => Err(e), - } - } else { - panic!("Not a str lit") - } - // let t = s.parse::()?; - - // let new_stream = TokenStream::from(t.to_s) - - // let cx: Ident = s.parse()?; - // s.parse::()?; - // if elements are in an array, return a bumpalo::collections::Vec rather than a Node. - // let kind = if s.peek(token::Bracket) { - // let nodes_toks; - // syn::bracketed!(nodes_toks in s); - // let mut nodes: Vec> = vec![nodes_toks.parse()?]; - // while nodes_toks.peek(Token![,]) { - // nodes_toks.parse::()?; - // nodes.push(nodes_toks.parse()?); - // } - // NodeOrList::List(NodeList(nodes)) - // } else { - // NodeOrList::Node(s.parse()?) - // }; - // Ok(HtmlRender { kind }) - } -} - -impl ToTokens for RsxTemplate { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - self.inner.to_tokens(out_tokens); - // let new_toks = ToToksCtx::new(&self.kind).to_token_stream(); - - // // create a lazy tree that accepts a bump allocator - // let final_tokens = quote! { - // dioxus::prelude::LazyNodes::new(move |cx| { - // let bump = &cx.bump(); - - // #new_toks - // }) - // }; - - // final_tokens.to_tokens(out_tokens); - } -} diff --git a/packages/macro-inner/Cargo.toml b/packages/rsx/Cargo.toml similarity index 91% rename from packages/macro-inner/Cargo.toml rename to packages/rsx/Cargo.toml index 63fcbbd9b..4cad9064c 100644 --- a/packages/macro-inner/Cargo.toml +++ b/packages/rsx/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dioxus-macro-inner" +name = "dioxus-rsx" version = "0.0.0" edition = "2018" diff --git a/packages/macro-inner/src/htm.rs b/packages/rsx/src/htm.rs similarity index 100% rename from packages/macro-inner/src/htm.rs rename to packages/rsx/src/htm.rs diff --git a/packages/macro-inner/src/lib.rs b/packages/rsx/src/lib.rs similarity index 79% rename from packages/macro-inner/src/lib.rs rename to packages/rsx/src/lib.rs index 69e4b743f..b9911dedc 100644 --- a/packages/macro-inner/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -1,5 +1,4 @@ pub mod ifmt; pub mod inlineprops; pub mod props; -pub mod router; pub mod rsx; diff --git a/packages/macro-inner/src/rsx/component.rs b/packages/rsx/src/rsx/component.rs similarity index 100% rename from packages/macro-inner/src/rsx/component.rs rename to packages/rsx/src/rsx/component.rs diff --git a/packages/macro-inner/src/rsx/element.rs b/packages/rsx/src/rsx/element.rs similarity index 100% rename from packages/macro-inner/src/rsx/element.rs rename to packages/rsx/src/rsx/element.rs diff --git a/packages/macro-inner/src/rsx/mod.rs b/packages/rsx/src/rsx/mod.rs similarity index 100% rename from packages/macro-inner/src/rsx/mod.rs rename to packages/rsx/src/rsx/mod.rs diff --git a/packages/macro-inner/src/rsx/node.rs b/packages/rsx/src/rsx/node.rs similarity index 100% rename from packages/macro-inner/src/rsx/node.rs rename to packages/rsx/src/rsx/node.rs From aef414ac0f48e938bbd93f5dba79aa32aaf444fa Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 01:44:15 -0500 Subject: [PATCH 015/310] wip: use_coroutine new behavior --- packages/hooks/src/usecoroutine.rs | 178 ++++++++++++++--------------- 1 file changed, 86 insertions(+), 92 deletions(-) diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 7faf507ed..593371b23 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -1,115 +1,93 @@ -use dioxus_core::{ScopeState, TaskId}; +use bumpalo::boxed::Box as BumpBox; +use dioxus_core::exports::bumpalo; +use dioxus_core::exports::futures_channel; +use dioxus_core::{LazyNodes, ScopeState, TaskId}; use std::future::Future; use std::{cell::Cell, rc::Rc}; -/* - - -let g = use_coroutine(&cx, || { - // clone the items in - async move { - - } -}) - - - -*/ -pub fn use_coroutine(cx: &ScopeState, create_future: impl FnOnce() -> F) -> CoroutineHandle<'_> +/// Maintain a handle over a future that can be paused, resumed, and canceled. +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +pub fn use_coroutine<'a, F>( + cx: &'a ScopeState, + create_future: impl FnOnce() -> F + 'a, +) -> UseCoroutine where F: Future + 'static, { - let state = cx.use_hook(move |_| { - let f = create_future(); - let id = cx.push_future(f); - State { - running: Default::default(), - _id: id - // pending_fut: Default::default(), - // running_fut: Default::default(), - } + let state = cx.use_hook(move |_| CoroutineInner { + _id: None, + running: Default::default(), }); - // state.pending_fut.set(Some(Box::pin(f))); + let mut bump = None; - // if let Some(fut) = state.running_fut.as_mut() { - // cx.push_future(fut); - // } + // as an optimization, we use the bump arena to allocate the callback instead of boxes + // that way we don't always call the constructor, but it's still efficient + cx.render(LazyNodes::new(move |f| { + bump.replace(f.bump()); + f.static_text("") + })); - // if let Some(fut) = state.running_fut.take() { - // state.running.set(true); - // fut.resume(); - // } + let mut slot = Some(create_future); - // let submit: Box = Box::new(move || { - // let g = async move { - // running.set(true); - // create_future().await; - // running.set(false); - // }; - // let p: Pin>> = Box::pin(g); - // fut_slot - // .borrow_mut() - // .replace(unsafe { std::mem::transmute(p) }); - // }); + // safety: bumpalo is limited in constructing unsized box types, so we have to do it through dynamic dispatch + let boxed: BumpBox<'a, dyn FnMut() -> F + 'a> = unsafe { + BumpBox::from_raw(bump.unwrap().alloc(move || { + let inner = slot.take().unwrap(); + inner() + })) + }; - // let submit = unsafe { std::mem::transmute(submit) }; - // state.submit.get_mut().replace(submit); - - // if state.running.get() { - // // let mut fut = state.fut.borrow_mut(); - // // cx.push_task(|| fut.as_mut().unwrap().as_mut()); - // } else { - // // make sure to drop the old future - // if let Some(fut) = state.fut.borrow_mut().take() { - // drop(fut); - // } - // } - CoroutineHandle { cx, inner: state } -} - -struct State { - running: Rc>, - _id: TaskId, - // the way this is structure, you can toggle the coroutine without re-rendering the comppnent - // this means every render *generates* the future, which is a bit of a waste - // todo: allocate pending futures in the bump allocator and then have a true promotion - // pending_fut: Cell + 'static>>>>, - // running_fut: Option + 'static>>>, - // running_fut: Rc + 'static>>>>> -} - -pub struct CoroutineHandle<'a> { - cx: &'a ScopeState, - inner: &'a State, -} - -impl Clone for CoroutineHandle<'_> { - fn clone(&self) -> Self { - CoroutineHandle { - cx: self.cx, - inner: self.inner, - } + UseCoroutine { + inner: state, + create_fut: Cell::new(Some(boxed)), + cx, } } -impl Copy for CoroutineHandle<'_> {} -impl<'a> CoroutineHandle<'a> { - #[allow(clippy::needless_return)] +struct CoroutineInner { + running: Rc>, + _id: Option, +} + +pub struct UseCoroutine<'a, F: Future + 'static> { + create_fut: Cell F + 'a>>>, + inner: &'a CoroutineInner, + cx: &'a ScopeState, +} + +impl<'a, F: Future + 'static> UseCoroutine<'a, F> { + pub fn auto_start(&self, start: bool) -> &Self { + todo!() + } + pub fn start(&self) { - if self.is_running() { - return; + if !self.is_running() { + if let Some(mut fut) = self.create_fut.take() { + let fut = fut(); + self.cx.push_future(fut); + } } - - // if let Some(submit) = self.inner.pending_fut.take() { - // submit(); - // let inner = self.inner; - // self.cx.push_task(submit()); - // } } pub fn is_running(&self) -> bool { - self.inner.running.get() + // self.inner.running.get() + false } pub fn resume(&self) { @@ -120,3 +98,19 @@ impl<'a> CoroutineHandle<'a> { pub fn restart(&self) {} } + +#[test] +fn it_works() { + use dioxus_core::prelude::*; + fn app(cx: Scope) -> Element { + let poll_tasks = use_coroutine(&cx, || async { + loop { + println!("polling tasks"); + } + }); + + poll_tasks.auto_start(true); + + todo!() + } +} From bd1535a0eda26b448b157d0ff5c8d5f2e10967ed Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 02:29:42 -0500 Subject: [PATCH 016/310] feat: use_coroutine happily compiles --- packages/hooks/Cargo.toml | 6 +- packages/hooks/src/usecoroutine.rs | 98 ++++++++++++++++++++---------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/packages/hooks/Cargo.toml b/packages/hooks/Cargo.toml index 375fef087..23af30627 100644 --- a/packages/hooks/Cargo.toml +++ b/packages/hooks/Cargo.toml @@ -12,4 +12,8 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../../packages/core", version ="^0.1.7"} +dioxus-core = { path = "../../packages/core", version = "^0.1.7" } + + +[dev-dependencies] +futures-util = { version = "0.3", default-features = false } diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 593371b23..5636b4031 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -1,6 +1,7 @@ +#![warn(clippy::pedantic)] + use bumpalo::boxed::Box as BumpBox; use dioxus_core::exports::bumpalo; -use dioxus_core::exports::futures_channel; use dioxus_core::{LazyNodes, ScopeState, TaskId}; use std::future::Future; use std::{cell::Cell, rc::Rc}; @@ -25,34 +26,35 @@ use std::{cell::Cell, rc::Rc}; pub fn use_coroutine<'a, F>( cx: &'a ScopeState, create_future: impl FnOnce() -> F + 'a, -) -> UseCoroutine +) -> UseCoroutine<'a, F> where F: Future + 'static, { let state = cx.use_hook(move |_| CoroutineInner { - _id: None, - running: Default::default(), + task_id: Cell::new(None), + running: std::rc::Rc::default(), + run_count: Cell::new(0), }); - let mut bump = None; - // as an optimization, we use the bump arena to allocate the callback instead of boxes // that way we don't always call the constructor, but it's still efficient - cx.render(LazyNodes::new(move |f| { - bump.replace(f.bump()); - f.static_text("") - })); - - let mut slot = Some(create_future); - // safety: bumpalo is limited in constructing unsized box types, so we have to do it through dynamic dispatch let boxed: BumpBox<'a, dyn FnMut() -> F + 'a> = unsafe { - BumpBox::from_raw(bump.unwrap().alloc(move || { - let inner = slot.take().unwrap(); + let mut bump = None; + cx.render(LazyNodes::new(move |f| { + bump.replace(f.bump()); + f.static_text("") + })); + let mut slot = Some(create_future); + let bump = bump.expect("bump is assigned during render"); + BumpBox::from_raw(bump.alloc(move || { + let inner = slot.take().expect("closure to not be called twice"); inner() })) }; + state.run_count.set(state.run_count.get() + 1); + UseCoroutine { inner: state, create_fut: Cell::new(Some(boxed)), @@ -62,7 +64,8 @@ where struct CoroutineInner { running: Rc>, - _id: Option, + task_id: Cell>, + run_count: Cell, } pub struct UseCoroutine<'a, F: Future + 'static> { @@ -73,35 +76,50 @@ pub struct UseCoroutine<'a, F: Future + 'static> { impl<'a, F: Future + 'static> UseCoroutine<'a, F> { pub fn auto_start(&self, start: bool) -> &Self { - todo!() + if start && self.inner.run_count.get() == 1 { + self.start(); + } + self + } + + pub fn is_running(&self) -> bool { + self.inner.running.get() } pub fn start(&self) { if !self.is_running() { if let Some(mut fut) = self.create_fut.take() { let fut = fut(); - self.cx.push_future(fut); + let ready_handle = self.inner.running.clone(); + + let task = self.cx.push_future(async move { + ready_handle.set(true); + fut.await; + ready_handle.set(false); + }); + + self.inner.task_id.set(Some(task)); } } } - pub fn is_running(&self) -> bool { - // self.inner.running.get() - false - } + // todo: wire these up, either into the task system or into the coroutine system itself + // we would have change how we poll the coroutine and how its awaken - pub fn resume(&self) { - // self.cx.push_task(fut) - } - - pub fn stop(&self) {} - - pub fn restart(&self) {} + // pub fn resume(&self) {} + // pub fn stop(&self) {} + // pub fn restart(&self) {} } -#[test] -fn it_works() { +#[cfg(test)] +mod tests { + #![allow(unused)] + + use super::*; + use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::prelude::*; + use futures_util::StreamExt; + fn app(cx: Scope) -> Element { let poll_tasks = use_coroutine(&cx, || async { loop { @@ -113,4 +131,22 @@ fn it_works() { todo!() } + + fn app_with_channel(cx: Scope) -> Element { + let (tx, mut rx) = unbounded(); + + let tx = cx.use_hook(|_| tx); + + let poll_tasks = use_coroutine(&cx, move || async move { + while let Some(msg) = rx.next().await { + println!("polling tasks: {}", msg); + } + }); + + poll_tasks.auto_start(true); + + tx.unbounded_send("asd").unwrap(); + + todo!() + } } 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 017/310] 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 018/310] 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 019/310] 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 020/310] 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 021/310] 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 022/310] 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 023/310] 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 024/310] 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 025/310] 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 ce17574aa0223b9cfb5d6eab6565a48fe302b21c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 4 Feb 2022 18:05:55 -0500 Subject: [PATCH 026/310] wip: add tracking for tasks --- packages/core/src/scopes.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 00c3024bf..1e58dcfd2 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -7,7 +7,7 @@ use std::{ any::{Any, TypeId}, borrow::Borrow, cell::{Cell, RefCell}, - collections::HashMap, + collections::{HashMap, HashSet}, future::Future, pin::Pin, rc::Rc, @@ -179,6 +179,9 @@ impl ScopeArena { log::trace!("removing scope {:?}", id); self.ensure_drop_safety(id); + // + let tasks = self.tasks.tasks.borrow_mut(); + // Safety: // - ensure_drop_safety ensures that no references to this scope are in use // - this raw pointer is removed from the map @@ -933,14 +936,17 @@ impl BumpFrame { pub(crate) struct TaskQueue { pub(crate) tasks: RefCell>, + pub(crate) task_map: RefCell>>, gen: Cell, sender: UnboundedSender, } + pub(crate) type InnerTask = Pin>>; impl TaskQueue { fn new(sender: UnboundedSender) -> Rc { Rc::new(Self { tasks: RefCell::new(FxHashMap::default()), + task_map: RefCell::new(FxHashMap::default()), gen: Cell::new(0), sender, }) @@ -957,10 +963,6 @@ impl TaskQueue { fn remove_fut(&self, id: TaskId) { if let Ok(mut tasks) = self.tasks.try_borrow_mut() { let _ = tasks.remove(&id); - } else { - // todo: it should be okay to remote a fut while the queue is being polled - // However, it's not currently possible to do that. - log::trace!("Unable to remove task from task queue. This is probably a bug."); } } pub(crate) fn has_tasks(&self) -> bool { From 7df8499da78e15e9072b3b41f7bb9f47cd4d8812 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 4 Feb 2022 20:19:17 -0600 Subject: [PATCH 027/310] 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 028/310] 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 029/310] 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 030/310] 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 031/310] 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 032/310] 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 9f60dfa8b00243442b7614b7a18514027cc1f8f7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 11 Feb 2022 15:50:07 -0500 Subject: [PATCH 033/310] feat: add all the css --- examples/all_css.rs | 411 +++++++++++++++ packages/html/src/global_attributes.rs | 671 ++++++++++--------------- 2 files changed, 687 insertions(+), 395 deletions(-) create mode 100644 examples/all_css.rs diff --git a/examples/all_css.rs b/examples/all_css.rs new file mode 100644 index 000000000..baf6294a4 --- /dev/null +++ b/examples/all_css.rs @@ -0,0 +1,411 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + align_content: "a", + align_items: "a", + align_self: "a", + alignment_adjust: "a", + alignment_baseline: "a", + all: "a", + alt: "a", + animation: "a", + animation_delay: "a", + animation_direction: "a", + animation_duration: "a", + animation_fill_mode: "a", + animation_iteration_count: "a", + animation_name: "a", + animation_play_state: "a", + animation_timing_function: "a", + azimuth: "a", + backface_visibility: "a", + background: "a", + background_attachment: "a", + background_clip: "a", + background_color: "a", + background_image: "a", + background_origin: "a", + background_position: "a", + background_repeat: "a", + background_size: "a", + background_blend_mode: "a", + baseline_shift: "a", + bleed: "a", + bookmark_label: "a", + bookmark_level: "a", + bookmark_state: "a", + border: "a", + border_color: "a", + border_style: "a", + border_width: "a", + border_bottom: "a", + border_bottom_color: "a", + border_bottom_style: "a", + border_bottom_width: "a", + border_left: "a", + border_left_color: "a", + border_left_style: "a", + border_left_width: "a", + border_right: "a", + border_right_color: "a", + border_right_style: "a", + border_right_width: "a", + border_top: "a", + border_top_color: "a", + border_top_style: "a", + border_top_width: "a", + border_collapse: "a", + border_image: "a", + border_image_outset: "a", + border_image_repeat: "a", + border_image_slice: "a", + border_image_source: "a", + border_image_width: "a", + border_radius: "a", + border_bottom_left_radius: "a", + border_bottom_right_radius: "a", + border_top_left_radius: "a", + border_top_right_radius: "a", + border_spacing: "a", + bottom: "a", + box_decoration_break: "a", + box_shadow: "a", + box_sizing: "a", + box_snap: "a", + break_after: "a", + break_before: "a", + break_inside: "a", + buffered_rendering: "a", + caption_side: "a", + clear: "a", + clear_side: "a", + clip: "a", + clip_path: "a", + clip_rule: "a", + color: "a", + color_adjust: "a", + color_correction: "a", + color_interpolation: "a", + color_interpolation_filters: "a", + color_profile: "a", + color_rendering: "a", + column_fill: "a", + column_gap: "a", + column_rule: "a", + column_rule_color: "a", + column_rule_style: "a", + column_rule_width: "a", + column_span: "a", + columns: "a", + column_count: "a", + column_width: "a", + contain: "a", + content: "a", + counter_increment: "a", + counter_reset: "a", + counter_set: "a", + cue: "a", + cue_after: "a", + cue_before: "a", + cursor: "a", + direction: "a", + display: "a", + display_inside: "a", + display_outside: "a", + display_extras: "a", + display_box: "a", + dominant_baseline: "a", + elevation: "a", + empty_cells: "a", + enable_background: "a", + fill: "a", + fill_opacity: "a", + fill_rule: "a", + filter: "a", + float: "a", + float_defer_column: "a", + float_defer_page: "a", + float_offset: "a", + float_wrap: "a", + flow_into: "a", + flow_from: "a", + flex: "a", + flex_basis: "a", + flex_grow: "a", + flex_shrink: "a", + flex_flow: "a", + flex_direction: "a", + flex_wrap: "a", + flood_color: "a", + flood_opacity: "a", + font: "a", + font_family: "a", + font_size: "a", + font_stretch: "a", + font_style: "a", + font_weight: "a", + font_feature_settings: "a", + font_kerning: "a", + font_language_override: "a", + font_size_adjust: "a", + font_synthesis: "a", + font_variant: "a", + font_variant_alternates: "a", + font_variant_caps: "a", + font_variant_east_asian: "a", + font_variant_ligatures: "a", + font_variant_numeric: "a", + font_variant_position: "a", + footnote_policy: "a", + glyph_orientation_horizontal: "a", + glyph_orientation_vertical: "a", + grid: "a", + grid_auto_flow: "a", + grid_auto_columns: "a", + grid_auto_rows: "a", + grid_template: "a", + grid_template_areas: "a", + grid_template_columns: "a", + grid_template_rows: "a", + grid_area: "a", + grid_column: "a", + grid_column_start: "a", + grid_column_end: "a", + grid_row: "a", + grid_row_start: "a", + grid_row_end: "a", + hanging_punctuation: "a", + height: "a", + hyphenate_character: "a", + hyphenate_limit_chars: "a", + hyphenate_limit_last: "a", + hyphenate_limit_lines: "a", + hyphenate_limit_zone: "a", + hyphens: "a", + icon: "a", + image_orientation: "a", + image_resolution: "a", + image_rendering: "a", + ime: "a", + ime_align: "a", + ime_mode: "a", + ime_offset: "a", + ime_width: "a", + initial_letters: "a", + inline_box_align: "a", + isolation: "a", + justify_content: "a", + justify_items: "a", + justify_self: "a", + kerning: "a", + left: "a", + letter_spacing: "a", + lighting_color: "a", + line_box_contain: "a", + line_break: "a", + line_grid: "a", + line_height: "a", + line_slack: "a", + line_snap: "a", + list_style: "a", + list_style_image: "a", + list_style_position: "a", + list_style_type: "a", + margin: "a", + margin_bottom: "a", + margin_left: "a", + margin_right: "a", + margin_top: "a", + marker: "a", + marker_end: "a", + marker_mid: "a", + marker_pattern: "a", + marker_segment: "a", + marker_start: "a", + marker_knockout_left: "a", + marker_knockout_right: "a", + marker_side: "a", + marks: "a", + marquee_direction: "a", + marquee_play_count: "a", + marquee_speed: "a", + marquee_style: "a", + mask: "a", + mask_image: "a", + mask_repeat: "a", + mask_position: "a", + mask_clip: "a", + mask_origin: "a", + mask_size: "a", + mask_box: "a", + mask_box_outset: "a", + mask_box_repeat: "a", + mask_box_slice: "a", + mask_box_source: "a", + mask_box_width: "a", + mask_type: "a", + max_height: "a", + max_lines: "a", + max_width: "a", + min_height: "a", + min_width: "a", + mix_blend_mode: "a", + nav_down: "a", + nav_index: "a", + nav_left: "a", + nav_right: "a", + nav_up: "a", + object_fit: "a", + object_position: "a", + offset_after: "a", + offset_before: "a", + offset_end: "a", + offset_start: "a", + opacity: "a", + order: "a", + orphans: "a", + outline: "a", + outline_color: "a", + outline_style: "a", + outline_width: "a", + outline_offset: "a", + overflow: "a", + overflow_x: "a", + overflow_y: "a", + overflow_style: "a", + overflow_wrap: "a", + padding: "a", + padding_bottom: "a", + padding_left: "a", + padding_right: "a", + padding_top: "a", + page: "a", + page_break_after: "a", + page_break_before: "a", + page_break_inside: "a", + paint_order: "a", + pause: "a", + pause_after: "a", + pause_before: "a", + perspective: "a", + perspective_origin: "a", + pitch: "a", + pitch_range: "a", + play_during: "a", + pointer_events: "a", + position: "a", + quotes: "a", + region_fragment: "a", + resize: "a", + rest: "a", + rest_after: "a", + rest_before: "a", + richness: "a", + right: "a", + ruby_align: "a", + ruby_merge: "a", + ruby_position: "a", + scroll_behavior: "a", + scroll_snap_coordinate: "a", + scroll_snap_destination: "a", + scroll_snap_points_x: "a", + scroll_snap_points_y: "a", + scroll_snap_type: "a", + shape_image_threshold: "a", + shape_inside: "a", + shape_margin: "a", + shape_outside: "a", + shape_padding: "a", + shape_rendering: "a", + size: "a", + speak: "a", + speak_as: "a", + speak_header: "a", + speak_numeral: "a", + speak_punctuation: "a", + speech_rate: "a", + stop_color: "a", + stop_opacity: "a", + stress: "a", + string_set: "a", + stroke: "a", + stroke_dasharray: "a", + stroke_dashoffset: "a", + stroke_linecap: "a", + stroke_linejoin: "a", + stroke_miterlimit: "a", + stroke_opacity: "a", + stroke_width: "a", + tab_size: "a", + table_layout: "a", + text_align: "a", + text_align_all: "a", + text_align_last: "a", + text_anchor: "a", + text_combine_upright: "a", + text_decoration: "a", + text_decoration_color: "a", + text_decoration_line: "a", + text_decoration_style: "a", + text_decoration_skip: "a", + text_emphasis: "a", + text_emphasis_color: "a", + text_emphasis_style: "a", + text_emphasis_position: "a", + text_emphasis_skip: "a", + text_height: "a", + text_indent: "a", + text_justify: "a", + text_orientation: "a", + text_overflow: "a", + text_rendering: "a", + text_shadow: "a", + text_size_adjust: "a", + text_space_collapse: "a", + text_spacing: "a", + text_transform: "a", + text_underline_position: "a", + text_wrap: "a", + top: "a", + touch_action: "a", + transform: "a", + transform_box: "a", + transform_origin: "a", + transform_style: "a", + transition: "a", + transition_delay: "a", + transition_duration: "a", + transition_property: "a", + unicode_bidi: "a", + vector_effect: "a", + vertical_align: "a", + visibility: "a", + voice_balance: "a", + voice_duration: "a", + voice_family: "a", + voice_pitch: "a", + voice_range: "a", + voice_rate: "a", + voice_stress: "a", + voice_volumn: "a", + volume: "a", + white_space: "a", + widows: "a", + width: "a", + will_change: "a", + word_break: "a", + word_spacing: "a", + word_wrap: "a", + wrap_flow: "a", + wrap_through: "a", + writing_mode: "a", + z_index: "a", + } + }) +} diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index 151a1539b..05b2df744 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -118,524 +118,405 @@ pub trait GlobalAttributes { // // This roughly follows the html spec style_trait_methods! { - /// Specifies the alignment of flexible container's items within the flex container. align_content: "align-content", - - /// Specifies the default alignment for items within the flex container. align_items: "align-items", - - /// Specifies the alignment for selected items within the flex container. align_self: "align-self", - - /// Specifies the keyframe_based animations. + alignment_adjust: "alignment-adjust", + alignment_baseline: "alignment-baseline", + all: "all", + alt: "alt", animation: "animation", - - /// Specifies when the animation will start. animation_delay: "animation-delay", - - /// Specifies whether the animation should play in reverse on alternate cycles or not. animation_direction: "animation-direction", - - /// Specifies the number of seconds or milliseconds an animation should take to complete one cycle animation_duration: "animation-duration", - - /// Specifies how a CSS animation should apply styles to its target before and after it is executing animation_fill_mode: "animation-fill-mode", - - /// Specifies the number of times an animation cycle should be played before stopping. animation_iteration_count: "animation-iteration-count", - - /// Specifies the name of @keyframes defined animations that should be applied to the selected element animation_name: "animation-name", - - /// Specifies whether the animation is running or paused. animation_play_state: "animation-play-state", - - /// Specifies how a CSS animation should progress over the duration of each cycle. animation_timing_function: "animation-timing-function", - - /// Specifies whether or not the "back" side of a transformed element is visible when facing the user. + azimuth: "azimuth", backface_visibility: "backface-visibility", - - /// Defines a variety of background properties within one declaration. background: "background", - - /// Specify whether the background image is fixed in the viewport or scrolls. background_attachment: "background-attachment", - - /// Specifies the painting area of the background. background_clip: "background-clip", - - /// Defines an element's background color. background_color: "background-color", - - /// Defines an element's background image. background_image: "background-image", - - /// Specifies the positioning area of the background images. background_origin: "background-origin", - - /// Defines the origin of a background image. background_position: "background-position", - - /// Specify whether/how the background image is tiled. background_repeat: "background-repeat", - - /// Specifies the size of the background images. background_size: "background-size", - - /// Sets the width, style, and color for all four sides of an element's border. + background_blend_mode: "background-blend-mode", + baseline_shift: "baseline-shift", + bleed: "bleed", + bookmark_label: "bookmark-label", + bookmark_level: "bookmark-level", + bookmark_state: "bookmark-state", border: "border", - - /// Sets the width, style, and color of the bottom border of an element. - border_bottom: "border-bottom", - - /// Sets the color of the bottom border of an element. - border_bottom_color: "border-bottom-color", - - /// Defines the shape of the bottom_left border corner of an element. - border_bottom_left_radius: "border-bottom-left-radius", - - /// Defines the shape of the bottom_right border corner of an element. - border_bottom_right_radius: "border-bottom-right-radius", - - /// Sets the style of the bottom border of an element. - border_bottom_style: "border-bottom-style", - - /// Sets the width of the bottom border of an element. - border_bottom_width: "border-bottom-width", - - /// Specifies whether table cell borders are connected or separated. - border_collapse: "border-collapse", - - /// Sets the color of the border on all the four sides of an element. border_color: "border-color", - - /// Specifies how an image is to be used in place of the border styles. - border_image: "border-image", - - /// Specifies the amount by which the border image area extends beyond the border box. - border_image_outset: "border-image-outset", - - /// Specifies whether the image_border should be repeated, rounded or stretched. - border_image_repeat: "border-image-repeat", - - /// Specifies the inward offsets of the image_border. - border_image_slice: "border-image-slice", - - /// Specifies the location of the image to be used as a border. - border_image_source: "border-image-source", - - /// Specifies the width of the image_border. - border_image_width: "border-image-width", - - /// Sets the width, style, and color of the left border of an element. - border_left: "border-left", - - /// Sets the color of the left border of an element. - border_left_color: "border-left-color", - - /// Sets the style of the left border of an element. - border_left_style: "border-left-style", - - /// Sets the width of the left border of an element. - border_left_width: "border-left-width", - - /// Defines the shape of the border corners of an element. - border_radius: "border-radius", - - /// Sets the width, style, and color of the right border of an element. - border_right: "border-right", - - /// Sets the color of the right border of an element. - border_right_color: "border-right-color", - - /// Sets the style of the right border of an element. - border_right_style: "border-right-style", - - /// Sets the width of the right border of an element. - border_right_width: "border-right-width", - - /// Sets the spacing between the borders of adjacent table cells. - border_spacing: "border-spacing", - - /// Sets the style of the border on all the four sides of an element. border_style: "border-style", - - /// Sets the width, style, and color of the top border of an element. - border_top: "border-top", - - /// Sets the color of the top border of an element. - border_top_color: "border-top-color", - - /// Defines the shape of the top_left border corner of an element. - border_top_left_radius: "border-top-left-radius", - - /// Defines the shape of the top_right border corner of an element. - border_top_right_radius: "border-top-right-radius", - - /// Sets the style of the top border of an element. - border_top_style: "border-top-style", - - /// Sets the width of the top border of an element. - border_top_width: "border-top-width", - - /// Sets the width of the border on all the four sides of an element. border_width: "border-width", - - /// Specify the location of the bottom edge of the positioned element. + border_bottom: "border-bottom", + border_bottom_color: "border-bottom-color", + border_bottom_style: "border-bottom-style", + border_bottom_width: "border-bottom-width", + border_left: "border-left", + border_left_color: "border-left-color", + border_left_style: "border-left-style", + border_left_width: "border-left-width", + border_right: "border-right", + border_right_color: "border-right-color", + border_right_style: "border-right-style", + border_right_width: "border-right-width", + border_top: "border-top", + border_top_color: "border-top-color", + border_top_style: "border-top-style", + border_top_width: "border-top-width", + border_collapse: "border-collapse", + border_image: "border-image", + border_image_outset: "border-image-outset", + border_image_repeat: "border-image-repeat", + border_image_slice: "border-image-slice", + border_image_source: "border-image-source", + border_image_width: "border-image-width", + border_radius: "border-radius", + border_bottom_left_radius: "border-bottom-left-radius", + border_bottom_right_radius: "border-bottom-right-radius", + border_top_left_radius: "border-top-left-radius", + border_top_right_radius: "border-top-right-radius", + border_spacing: "border-spacing", bottom: "bottom", - - /// Applies one or more drop_shadows to the element's box. + box_decoration_break: "box-decoration-break", box_shadow: "box-shadow", - - /// Alter the default CSS box model. box_sizing: "box-sizing", - - /// Specify the position of table's caption. + box_snap: "box-snap", + break_after: "break-after", + break_before: "break-before", + break_inside: "break-inside", + buffered_rendering: "buffered-rendering", caption_side: "caption-side", - - /// Specifies the placement of an element in relation to floating elements. clear: "clear", - - /// Defines the clipping region. + clear_side: "clear-side", clip: "clip", - - /// Specify the color of the text of an element. + clip_path: "clip-path", + clip_rule: "clip-rule", color: "color", - - /// Specifies the number of columns in a multi_column element. - column_count: "column-count", - - /// Specifies how columns will be filled. + color_adjust: "color-adjust", + color_correction: "color-correction", + color_interpolation: "color-interpolation", + color_interpolation_filters: "color-interpolation-filters", + color_profile: "color-profile", + color_rendering: "color-rendering", column_fill: "column-fill", - - /// Specifies the gap between the columns in a multi_column element. column_gap: "column-gap", - - /// Specifies a straight line, or "rule", to be drawn between each column in a multi_column element. column_rule: "column-rule", - - /// Specifies the color of the rules drawn between columns in a multi_column layout. column_rule_color: "column-rule-color", - - /// Specifies the style of the rule drawn between the columns in a multi_column layout. column_rule_style: "column-rule-style", - - /// Specifies the width of the rule drawn between the columns in a multi_column layout. column_rule_width: "column-rule-width", - - /// Specifies how many columns an element spans across in a multi_column layout. column_span: "column-span", - - /// Specifies the optimal width of the columns in a multi_column element. - column_width: "column-width", - - /// A shorthand property for setting column_width and column_count properties. columns: "columns", - - /// Inserts generated content. + column_count: "column-count", + column_width: "column-width", + contain: "contain", content: "content", - - /// Increments one or more counter values. counter_increment: "counter-increment", - - /// Creates or resets one or more counters. counter_reset: "counter-reset", - - /// Specify the type of cursor. + counter_set: "counter-set", + cue: "cue", + cue_after: "cue-after", + cue_before: "cue-before", cursor: "cursor", - - /// Define the text direction/writing direction. direction: "direction", - - /// Specifies how an element is displayed onscreen. display: "display", - - /// Show or hide borders and backgrounds of empty table cells. + display_inside: "display-inside", + display_outside: "display-outside", + display_extras: "display-extras", + display_box: "display-box", + dominant_baseline: "dominant-baseline", + elevation: "elevation", empty_cells: "empty-cells", - - /// Specifies the components of a flexible length. - flex: "flex", - - /// Specifies the initial main size of the flex item. - flex_basis: "flex-basis", - - /// Specifies the direction of the flexible items. - flex_direction: "flex-direction", - - /// A shorthand property for the flex_direction and the flex_wrap properties. - flex_flow: "flex-flow", - - /// Specifies how the flex item will grow relative to the other items inside the flex container. - flex_grow: "flex-grow", - - /// Specifies how the flex item will shrink relative to the other items inside the flex container - flex_shrink: "flex-shrink", - - /// Specifies whether the flexible items should wrap or not. - flex_wrap: "flex-wrap", - - /// Specifies whether or not a box should float. + enable_background: "enable-background", + fill: "fill", + fill_opacity: "fill-opacity", + fill_rule: "fill-rule", + filter: "filter", float: "float", - - /// Defines a variety of font properties within one declaration. + float_defer_column: "float-defer-column", + float_defer_page: "float-defer-page", + float_offset: "float-offset", + float_wrap: "float-wrap", + flow_into: "flow-into", + flow_from: "flow-from", + flex: "flex", + flex_basis: "flex-basis", + flex_grow: "flex-grow", + flex_shrink: "flex-shrink", + flex_flow: "flex-flow", + flex_direction: "flex-direction", + flex_wrap: "flex-wrap", + flood_color: "flood-color", + flood_opacity: "flood-opacity", font: "font", - - /// Defines a list of fonts for element. font_family: "font-family", - - /// Defines the font size for the text. font_size: "font-size", - - /// Preserves the readability of text when font fallback occurs. - font_size_adjust: "font-size-adjust", - - /// Selects a normal, condensed, or expanded face from a font. font_stretch: "font-stretch", - - /// Defines the font style for the text. font_style: "font-style", - - /// Specify the font variant. - font_variant: "font-variant", - - /// Specify the font weight of the text. font_weight: "font-weight", - - /// Sets gaps (gutters) between rows and columns. Shorthand for row_gap and column_gap. - gap: "gap", - - /// Specify the height of an element. + font_feature_settings: "font-feature-settings", + font_kerning: "font-kerning", + font_language_override: "font-language-override", + font_size_adjust: "font-size-adjust", + font_synthesis: "font-synthesis", + font_variant: "font-variant", + font_variant_alternates: "font-variant-alternates", + font_variant_caps: "font-variant-caps", + font_variant_east_asian: "font-variant-east-asian", + font_variant_ligatures: "font-variant-ligatures", + font_variant_numeric: "font-variant-numeric", + font_variant_position: "font-variant-position", + footnote_policy: "footnote-policy", + glyph_orientation_horizontal: "glyph-orientation-horizontal", + glyph_orientation_vertical: "glyph-orientation-vertical", + grid: "grid", + grid_auto_flow: "grid-auto-flow", + grid_auto_columns: "grid-auto-columns", + grid_auto_rows: "grid-auto-rows", + grid_template: "grid-template", + grid_template_areas: "grid-template-areas", + grid_template_columns: "grid-template-columns", + grid_template_rows: "grid-template-rows", + grid_area: "grid-area", + grid_column: "grid-column", + grid_column_start: "grid-column-start", + grid_column_end: "grid-column-end", + grid_row: "grid-row", + grid_row_start: "grid-row-start", + grid_row_end: "grid-row-end", + hanging_punctuation: "hanging-punctuation", height: "height", - - /// Specifies how flex items are aligned along the main axis of the flex container after any flexible lengths and auto margins have been resolved. + hyphenate_character: "hyphenate-character", + hyphenate_limit_chars: "hyphenate-limit-chars", + hyphenate_limit_last: "hyphenate-limit-last", + hyphenate_limit_lines: "hyphenate-limit-lines", + hyphenate_limit_zone: "hyphenate-limit-zone", + hyphens: "hyphens", + icon: "icon", + image_orientation: "image-orientation", + image_resolution: "image-resolution", + image_rendering: "image-rendering", + ime: "ime", + ime_align: "ime-align", + ime_mode: "ime-mode", + ime_offset: "ime-offset", + ime_width: "ime-width", + initial_letters: "initial-letters", + inline_box_align: "inline-box-align", + isolation: "isolation", justify_content: "justify-content", - - /// Specify the location of the left edge of the positioned element. + justify_items: "justify-items", + justify_self: "justify-self", + kerning: "kerning", left: "left", - - /// Sets the extra spacing between letters. letter_spacing: "letter-spacing", - - /// Sets the height between lines of text. + lighting_color: "lighting-color", + line_box_contain: "line-box-contain", + line_break: "line-break", + line_grid: "line-grid", line_height: "line-height", - - /// Defines the display style for a list and list elements. + line_slack: "line-slack", + line_snap: "line-snap", list_style: "list-style", - - /// Specifies the image to be used as a list_item marker. list_style_image: "list-style-image", - - /// Specifies the position of the list_item marker. list_style_position: "list-style-position", - - /// Specifies the marker style for a list_item. - list_styler_type: "list-style-type", - - /// Sets the margin on all four sides of the element. + list_style_type: "list-style-type", margin: "margin", - - /// Sets the bottom margin of the element. margin_bottom: "margin-bottom", - - /// Sets the left margin of the element. margin_left: "margin-left", - - /// Sets the right margin of the element. margin_right: "margin-right", - - /// Sets the top margin of the element. margin_top: "margin-top", - - /// Specify the maximum height of an element. + marker: "marker", + marker_end: "marker-end", + marker_mid: "marker-mid", + marker_pattern: "marker-pattern", + marker_segment: "marker-segment", + marker_start: "marker-start", + marker_knockout_left: "marker-knockout-left", + marker_knockout_right: "marker-knockout-right", + marker_side: "marker-side", + marks: "marks", + marquee_direction: "marquee-direction", + marquee_play_count: "marquee-play-count", + marquee_speed: "marquee-speed", + marquee_style: "marquee-style", + mask: "mask", + mask_image: "mask-image", + mask_repeat: "mask-repeat", + mask_position: "mask-position", + mask_clip: "mask-clip", + mask_origin: "mask-origin", + mask_size: "mask-size", + mask_box: "mask-box", + mask_box_outset: "mask-box-outset", + mask_box_repeat: "mask-box-repeat", + mask_box_slice: "mask-box-slice", + mask_box_source: "mask-box-source", + mask_box_width: "mask-box-width", + mask_type: "mask-type", max_height: "max-height", - - /// Specify the maximum width of an element. + max_lines: "max-lines", max_width: "max-width", - - /// Specify the minimum height of an element. min_height: "min-height", - - /// Specify the minimum width of an element. min_width: "min-width", - - /// Specifies the transparency of an element. + mix_blend_mode: "mix-blend-mode", + nav_down: "nav-down", + nav_index: "nav-index", + nav_left: "nav-left", + nav_right: "nav-right", + nav_up: "nav-up", + object_fit: "object-fit", + object_position: "object-position", + offset_after: "offset-after", + offset_before: "offset-before", + offset_end: "offset-end", + offset_start: "offset-start", opacity: "opacity", - - /// Specifies the order in which a flex items are displayed and laid out within a flex container. order: "order", - - /// Sets the width, style, and color for all four sides of an element's outline. + orphans: "orphans", outline: "outline", - - /// Sets the color of the outline. outline_color: "outline-color", - - /// Set the space between an outline and the border edge of an element. - outline_offset: "outline-offset", - - /// Sets a style for an outline. outline_style: "outline-style", - - /// Sets the width of the outline. outline_width: "outline-width", - - /// Specifies the treatment of content that overflows the element's box. + outline_offset: "outline-offset", overflow: "overflow", - - /// Specifies the treatment of content that overflows the element's box horizontally. overflow_x: "overflow-x", - - /// Specifies the treatment of content that overflows the element's box vertically. overflow_y: "overflow-y", - - /// Sets the padding on all four sides of the element. + overflow_style: "overflow-style", + overflow_wrap: "overflow-wrap", padding: "padding", - - /// Sets the padding to the bottom side of an element. padding_bottom: "padding-bottom", - - /// Sets the padding to the left side of an element. padding_left: "padding-left", - - /// Sets the padding to the right side of an element. padding_right: "padding-right", - - /// Sets the padding to the top side of an element. padding_top: "padding-top", - - /// Insert a page breaks after an element. + page: "page", page_break_after: "page-break-after", - - /// Insert a page breaks before an element. page_break_before: "page-break-before", - - /// Insert a page breaks inside an element. page_break_inside: "page-break-inside", - - /// Defines the perspective from which all child elements of the object are viewed. + paint_order: "paint-order", + pause: "pause", + pause_after: "pause-after", + pause_before: "pause-before", perspective: "perspective", - - /// Defines the origin (the vanishing point for the 3D space) for the perspective property. perspective_origin: "perspective-origin", - - /// Specifies how an element is positioned. - position: "position", - - /// The pointer-events CSS property sets under what circumstances (if any) a particular graphic element can - /// become the target of pointer events. - /// - /// MDN: [`pointer_events`](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events) + pitch: "pitch", + pitch_range: "pitch-range", + play_during: "play-during", pointer_events: "pointer-events", - - /// Specifies quotation marks for embedded quotations. + position: "position", quotes: "quotes", - - /// Specifies whether or not an element is resizable by the user. + region_fragment: "region-fragment", resize: "resize", - - /// Specify the location of the right edge of the positioned element. + rest: "rest", + rest_after: "rest-after", + rest_before: "rest-before", + richness: "richness", right: "right", - - /// Specifies the gap between the rows in a multi_column element. - row_gap: "row-gap", - - /// Specifies the length of the tab character. + ruby_align: "ruby-align", + ruby_merge: "ruby-merge", + ruby_position: "ruby-position", + scroll_behavior: "scroll-behavior", + scroll_snap_coordinate: "scroll-snap-coordinate", + scroll_snap_destination: "scroll-snap-destination", + scroll_snap_points_x: "scroll-snap-points-x", + scroll_snap_points_y: "scroll-snap-points-y", + scroll_snap_type: "scroll-snap-type", + shape_image_threshold: "shape-image-threshold", + shape_inside: "shape-inside", + shape_margin: "shape-margin", + shape_outside: "shape-outside", + shape_padding: "shape-padding", + shape_rendering: "shape-rendering", + size: "size", + speak: "speak", + speak_as: "speak-as", + speak_header: "speak-header", + speak_numeral: "speak-numeral", + speak_punctuation: "speak-punctuation", + speech_rate: "speech-rate", + stop_color: "stop-color", + stop_opacity: "stop-opacity", + stress: "stress", + string_set: "string-set", + stroke: "stroke", + stroke_dasharray: "stroke-dasharray", + stroke_dashoffset: "stroke-dashoffset", + stroke_linecap: "stroke-linecap", + stroke_linejoin: "stroke-linejoin", + stroke_miterlimit: "stroke-miterlimit", + stroke_opacity: "stroke-opacity", + stroke_width: "stroke-width", tab_size: "tab-size", - - /// Specifies a table layout algorithm. table_layout: "table-layout", - - /// Sets the horizontal alignment of inline content. text_align: "text-align", - - /// Specifies how the last line of a block or a line right before a forced line break is aligned when is justify.", + text_align_all: "text-align-all", text_align_last: "text-align-last", - - /// Specifies the decoration added to text. + text_anchor: "text-anchor", + text_combine_upright: "text-combine-upright", text_decoration: "text-decoration", - - /// Specifies the color of the text_decoration_line. text_decoration_color: "text-decoration-color", - - /// Specifies what kind of line decorations are added to the element. text_decoration_line: "text-decoration-line", - - /// Specifies the style of the lines specified by the text_decoration_line property text_decoration_style: "text-decoration-style", - - /// Indent the first line of text. + text_decoration_skip: "text-decoration-skip", + text_emphasis: "text-emphasis", + text_emphasis_color: "text-emphasis-color", + text_emphasis_style: "text-emphasis-style", + text_emphasis_position: "text-emphasis-position", + text_emphasis_skip: "text-emphasis-skip", + text_height: "text-height", text_indent: "text-indent", - - /// Specifies the justification method to use when the text_align property is set to justify. text_justify: "text-justify", - - /// Specifies how the text content will be displayed, when it overflows the block containers. + text_orientation: "text-orientation", text_overflow: "text-overflow", - - /// Applies one or more shadows to the text content of an element. + text_rendering: "text-rendering", text_shadow: "text-shadow", - - /// Transforms the case of the text. + text_size_adjust: "text-size-adjust", + text_space_collapse: "text-space-collapse", + text_spacing: "text-spacing", text_transform: "text-transform", - - /// Specify the location of the top edge of the positioned element. + text_underline_position: "text-underline-position", + text_wrap: "text-wrap", top: "top", - - /// Applies a 2D or 3D transformation to an element. + touch_action: "touch-action", transform: "transform", - - /// Defines the origin of transformation for an element. + transform_box: "transform-box", transform_origin: "transform-origin", - - /// Specifies how nested elements are rendered in 3D space. transform_style: "transform-style", - - /// Defines the transition between two states of an element. transition: "transition", - - /// Specifies when the transition effect will start. transition_delay: "transition-delay", - - /// Specifies the number of seconds or milliseconds a transition effect should take to complete. transition_duration: "transition-duration", - - /// Specifies the names of the CSS properties to which a transition effect should be applied. transition_property: "transition-property", - - /// Specifies the speed curve of the transition effect. - transition_timing_function: "transition-timing-function", - - /// The user-select CSS property controls whether the user can select text. - /// This doesn't have any effect on content loaded as part of a browser's user interface (its chrome), except in textboxes. - user_select: "user-select", - webkit_user_select: "-webkit-user-select", - - /// Sets the vertical positioning of an element relative to the current text baseline. + unicode_bidi: "unicode-bidi", + vector_effect: "vector-effect", vertical_align: "vertical-align", - - /// Specifies whether or not an element is visible. visibility: "visibility", - - /// Specifies how white space inside the element is handled. + voice_balance: "voice-balance", + voice_duration: "voice-duration", + voice_family: "voice-family", + voice_pitch: "voice-pitch", + voice_range: "voice-range", + voice_rate: "voice-rate", + voice_stress: "voice-stress", + voice_volumn: "voice-volumn", + volume: "volume", white_space: "white-space", - - /// Specify the width of an element. + widows: "widows", width: "width", - - /// Specifies how to break lines within words. + will_change: "will-change", word_break: "word-break", - - /// Sets the spacing between words. word_spacing: "word-spacing", - - /// Specifies whether to break words when the content overflows the boundaries of its container. word_wrap: "word-wrap", - - /// Specifies a layering or stacking order for positioned elements. - z_index : "z-index", - + wrap_flow: "wrap-flow", + wrap_through: "wrap-through", + writing_mode: "writing-mode", + z_index: "z-index", } aria_trait_methods! { aria_current: "aria-current", From b64574810d56a5968919e44686e997219d8cd279 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sat, 12 Feb 2022 01:43:36 +0100 Subject: [PATCH 034/310] Clean up desktop's index.html Initially I wanted to delete only the duplicated `` opening tag, but then decided to make it a fully valid HTML document. Passes https://validator.w3.org/nu/#textarea check. --- packages/desktop/src/index.html | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 88fcba12a..e244b6faa 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -1,22 +1,15 @@ - - - - - - - - - -
-
- - - - + + Dioxus app + + + +
+ + From 2d1371167f05f3e5ffc53470093ff03c984a44d9 Mon Sep 17 00:00:00 2001 From: Aster Date: Sat, 12 Feb 2022 21:22:05 +0800 Subject: [PATCH 035/310] Use `===` when rhs is string --- packages/interpreter/src/interpreter.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 95c88d294..f0acd0802 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -102,15 +102,15 @@ export class Interpreter { SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; - if (ns == "style") { + if (ns === "style") { // @ts-ignore node.style[name] = value; - } else if (ns != null || ns != undefined) { + } else if (ns != null || ns !== undefined) { node.setAttributeNS(ns, name, value); } else { switch (name) { case "value": - if (value != node.value) { + if (value !== node.value) { node.value = value; } break; @@ -125,7 +125,7 @@ export class Interpreter { break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { + if (value === "false" && bool_attrs.hasOwnProperty(name)) { node.removeAttribute(name); } else { node.setAttribute(name, value); @@ -142,7 +142,7 @@ export class Interpreter { node.checked = false; } else if (name === "selected") { node.selected = false; - } else if (name == "dangerous_inner_html") { + } else if (name === "dangerous_inner_html") { node.innerHTML = ""; } else { node.removeAttribute(name); @@ -200,10 +200,10 @@ export class Interpreter { `dioxus-prevent-default` ); - if (event.type == "click") { + if (event.type === "click") { // todo call prevent default if it's the right type of event if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { + if (target.tagName === "A") { event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { @@ -213,7 +213,7 @@ export class Interpreter { } // also prevent buttons from submitting - if (target.tagName == "BUTTON") { + if (target.tagName === "BUTTON") { event.preventDefault(); } } @@ -237,16 +237,16 @@ export class Interpreter { if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); } - if (event.type == "submit") { + if (event.type === "submit") { event.preventDefault(); } - if (target.tagName == "FORM") { + if (target.tagName === "FORM") { for (let x = 0; x < target.elements.length; x++) { let element = target.elements[x]; let name = element.getAttribute("name"); if (name != null) { - if (element.getAttribute("type") == "checkbox") { + if (element.getAttribute("type") === "checkbox") { // @ts-ignore contents.values[name] = element.checked ? "true" : "false"; } else { @@ -349,7 +349,7 @@ export function serialize_event(event) { case "submit": { let target = event.target; let value = target.value ?? target.textContent; - if (target.type == "checkbox") { + if (target.type === "checkbox") { value = target.checked ? "true" : "false"; } return { From aad055cd84e7d592aba9606b0ff6447b620bcaf1 Mon Sep 17 00:00:00 2001 From: Aster Date: Sat, 12 Feb 2022 21:45:42 +0800 Subject: [PATCH 036/310] Fix typo `WryProtocol` --- packages/desktop/src/cfg.rs | 14 ++++++++++---- packages/desktop/src/lib.rs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 5baa46f5d..25e280a0f 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,4 @@ +use wry::application::window::Icon; use wry::{ application::{ event_loop::EventLoop, @@ -13,12 +14,12 @@ pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); pub struct DesktopConfig { pub window: WindowBuilder, pub file_drop_handler: Option bool>>, - pub protocos: Vec, + pub protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, } -pub type WryProtocl = ( +pub type WryProtocol = ( String, Box WryResult + 'static>, ); @@ -31,7 +32,7 @@ impl DesktopConfig { Self { event_handler: None, window, - protocos: Vec::new(), + protocols: Vec::new(), file_drop_handler: None, pre_rendered: None, } @@ -75,7 +76,12 @@ impl DesktopConfig { where F: Fn(&HttpRequest) -> WryResult + 'static, { - self.protocos.push((name, Box::new(handler))); + self.protocols.push((name, Box::new(handler))); + self + } + + pub fn with_icon(&mut self, icon: Icon) -> &mut Self { + self.window.window.window_icon = Some(icon); self } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index a58996f89..596d9c260 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -261,7 +261,7 @@ pub fn launch_with_props( .unwrap_or_default() }); - for (name, handler) in cfg.protocos.drain(..) { + for (name, handler) in cfg.protocols.drain(..) { webview = webview.with_custom_protocol(name, handler) } From 0596088312b32a3e9279cb09df119c82ead0b376 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Feb 2022 10:57:57 -0500 Subject: [PATCH 037/310] docs: fix lifetimes on children --- docs/guide/src/components/component_children.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/src/components/component_children.md b/docs/guide/src/components/component_children.md index ea9b1fc45..809ae3d9b 100644 --- a/docs/guide/src/components/component_children.md +++ b/docs/guide/src/components/component_children.md @@ -30,7 +30,7 @@ struct ClickableProps<'a> { title: &'a str } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}" @@ -64,7 +64,7 @@ struct ClickableProps<'a> { body: Element<'a> } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn Clickable(cx: Scope) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -125,7 +125,7 @@ While technically allowed, it's an antipattern to pass children more than once i However, because the `Element` is transparently a `VNode`, we can actually match on it to extract the nodes themselves, in case we are expecting a specific format: ```rust -fn clickable(cx: Scope) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { match cx.props.children { Some(VNode::Text(text)) => { // ... @@ -184,7 +184,7 @@ struct ClickableProps<'a> { onclick: EventHandler<'a, MouseEvent> } -fn clickable(cx: Scope) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { cx.render(rsx!( a { onclick: move |evt| cx.props.onclick.call(evt) From 4981e4c26b17ec20950cb895d31a44c76aceeb73 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Feb 2022 11:02:05 -0500 Subject: [PATCH 038/310] ci: make codecov be quieter --- codecov.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..69cb76019 --- /dev/null +++ b/codecov.yml @@ -0,0 +1 @@ +comment: false From a325c03dd93b0cfb042487d228ad493adc7aebd0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Feb 2022 11:02:55 -0500 Subject: [PATCH 039/310] fix: lifetimes again --- docs/guide/src/components/component_children.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guide/src/components/component_children.md b/docs/guide/src/components/component_children.md index 809ae3d9b..1259422af 100644 --- a/docs/guide/src/components/component_children.md +++ b/docs/guide/src/components/component_children.md @@ -30,7 +30,7 @@ struct ClickableProps<'a> { title: &'a str } -fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}" @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn Clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -125,7 +125,7 @@ While technically allowed, it's an antipattern to pass children more than once i However, because the `Element` is transparently a `VNode`, we can actually match on it to extract the nodes themselves, in case we are expecting a specific format: ```rust -fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { match cx.props.children { Some(VNode::Text(text)) => { // ... @@ -160,7 +160,7 @@ struct ClickableProps<'a> { attributes: Attributes<'a> } -fn clickable(cx: Scope) -> Element { +fn clickable(cx: Scope>) -> Element { cx.render(rsx!( a { ..cx.props.attributes, @@ -184,7 +184,7 @@ struct ClickableProps<'a> { onclick: EventHandler<'a, MouseEvent> } -fn clickable<'a>(cx: Scope<'a, ClickableProps>) -> Element { +fn clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { cx.render(rsx!( a { onclick: move |evt| cx.props.onclick.call(evt) From 78ac592c0aaec89041c6e0210edf3573b5334ce4 Mon Sep 17 00:00:00 2001 From: Aster Date: Sun, 13 Feb 2022 00:34:45 +0800 Subject: [PATCH 040/310] Custom for icon --- packages/desktop/Cargo.toml | 1 + packages/desktop/src/cfg.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index fa1bc88dc..fcb3a19e9 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -32,6 +32,7 @@ dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } +image = "0.24.0" [features] diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 25e280a0f..a177c8a4b 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,3 +1,6 @@ +use image::io::Reader as ImageReader; +use image::ImageFormat; +use std::io::Cursor; use wry::application::window::Icon; use wry::{ application::{ @@ -86,6 +89,19 @@ impl DesktopConfig { } } +impl DesktopConfig { + pub(crate) fn with_default_icon(mut self) -> Self { + let png: &[u8] = include_bytes!("default_icon.png"); + let mut reader = ImageReader::new(Cursor::new(png)); + reader.set_format(ImageFormat::Png); + let icon = reader.decode().expect("image parse failed"); + let rgba = Icon::from_rgba(icon.as_bytes().to_owned(), icon.width(), icon.height()) + .expect("image parse failed"); + self.window.window.window_icon = Some(rgba); + self + } +} + impl Default for DesktopConfig { fn default() -> Self { Self::new() From f6b30d26b969e77df57fd20084d11057d7307e6b Mon Sep 17 00:00:00 2001 From: Aster Date: Sun, 13 Feb 2022 00:35:02 +0800 Subject: [PATCH 041/310] Add default desktop icon --- packages/desktop/src/default_icon.png | Bin 0 -> 77196 bytes packages/desktop/src/lib.rs | 56 ++------------------------ packages/desktop/src/readme.md | 51 +++++++++++++++++++++++ 3 files changed, 55 insertions(+), 52 deletions(-) create mode 100644 packages/desktop/src/default_icon.png create mode 100644 packages/desktop/src/readme.md diff --git a/packages/desktop/src/default_icon.png b/packages/desktop/src/default_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d0742ea8a067fdcdbd24b45866ec7794dfa4a399 GIT binary patch literal 77196 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

M;Ig0xsnp{B?j30@}Tazxg}1i|Tbn@eJK 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 E0YnYs?f?J) literal 0 HcmV?d00001 diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 596d9c260..f4e28ca2a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -1,54 +1,6 @@ -//! Dioxus Desktop Renderer -//! -//! Render the Dioxus VirtualDom using the platform's native WebView implementation. -//! -//! # Desktop -//! -//! One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. -//! -//! Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. -//! -//! -//! ## Getting Set up -//! -//! Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project: -//! -//! ```shell -//! $ cargo new --bin demo -//! $ cd app -//! ``` -//! -//! Add Dioxus with the `desktop` feature: -//! -//! ```shell -//! $ cargo add dioxus --features desktop -//! ``` -//! -//! Edit your `main.rs`: -//! -//! ```rust -//! // main.rs -//! use dioxus::prelude::*; -//! -//! fn main() { -//! dioxus::desktop::launch(app); -//! } -//! -//! fn app(cx: Scope) -> Element { -//! cx.render(rsx!{ -//! div { -//! "hello world!" -//! } -//! }) -//! } -//! ``` -//! -//! -//! To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/). -//! -//! ## Future Steps -//! -//! Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! +#![doc = include_str!("readme.md")] +#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] +#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] pub mod cfg; pub mod desktop_context; @@ -152,7 +104,7 @@ pub fn launch_with_props( props: P, builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig, ) { - let mut cfg = DesktopConfig::default(); + let mut cfg = DesktopConfig::default().with_default_icon(); builder(&mut cfg); let event_loop = EventLoop::with_user_event(); diff --git a/packages/desktop/src/readme.md b/packages/desktop/src/readme.md new file mode 100644 index 000000000..dc034fe67 --- /dev/null +++ b/packages/desktop/src/readme.md @@ -0,0 +1,51 @@ +Dioxus Desktop Renderer + +Render the Dioxus VirtualDom using the platform's native WebView implementation. + +# Desktop + +One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. + +Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over keyboard shortcuts, menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao)) directly. The next major release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc. + + +## Getting Set up + +Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project: + +```shell +$ cargo new --bin demo +$ cd app +``` + +Add Dioxus with the `desktop` feature: + +```shell +$ cargo add dioxus --features desktop +``` + +Edit your `main.rs`: + +```rust +// main.rs +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx!{ + div { + "hello world!" + } + }) +} +``` + + +To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/). + +## Future Steps + +Make sure to read the [Dioxus Guide](https://dioxuslabs.com/guide) if you already haven't! \ No newline at end of file From 21b436b7bfac41894d01066abcdb8734cc55b5af Mon Sep 17 00:00:00 2001 From: Aster Date: Sun, 13 Feb 2022 01:34:41 +0800 Subject: [PATCH 042/310] Remove image at runtime --- packages/desktop/Cargo.toml | 2 +- packages/desktop/src/cfg.rs | 36 +++++++++++++++++++------- packages/desktop/src/default_icon.bin | Bin 0 -> 846400 bytes 3 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 packages/desktop/src/default_icon.bin diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index fcb3a19e9..805c46f3b 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -32,7 +32,6 @@ dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } -image = "0.24.0" [features] @@ -42,3 +41,4 @@ tokio_runtime = ["tokio"] [dev-dependencies] dioxus-hooks = { path = "../hooks" } +image = "0.24.0" \ No newline at end of file diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index a177c8a4b..84786a133 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,6 +1,3 @@ -use image::io::Reader as ImageReader; -use image::ImageFormat; -use std::io::Cursor; use wry::application::window::Icon; use wry::{ application::{ @@ -91,12 +88,8 @@ impl DesktopConfig { impl DesktopConfig { pub(crate) fn with_default_icon(mut self) -> Self { - let png: &[u8] = include_bytes!("default_icon.png"); - let mut reader = ImageReader::new(Cursor::new(png)); - reader.set_format(ImageFormat::Png); - let icon = reader.decode().expect("image parse failed"); - let rgba = Icon::from_rgba(icon.as_bytes().to_owned(), icon.width(), icon.height()) - .expect("image parse failed"); + let bin: &[u8] = include_bytes!("default_icon.bin"); + let rgba = Icon::from_rgba(bin.to_owned(), 460, 460).expect("image parse failed"); self.window.window.window_icon = Some(rgba); self } @@ -107,3 +100,28 @@ impl Default for DesktopConfig { Self::new() } } + +// dirty trick, avoid introducing `image` at runtime +// TODO: use serde when `Icon` impl serde +#[test] +#[ignore] +fn prepare_default_icon() { + use image::io::Reader as ImageReader; + use image::ImageFormat; + use std::fs::File; + use std::io::Cursor; + use std::io::Write; + use std::path::PathBuf; + let png: &[u8] = include_bytes!("default_icon.png"); + let mut reader = ImageReader::new(Cursor::new(png)); + reader.set_format(ImageFormat::Png); + let icon = reader.decode().unwrap(); + let bin = PathBuf::from(file!()) + .parent() + .unwrap() + .join("default_icon.bin"); + println!("{:?}", bin); + let mut file = File::create(bin).unwrap(); + file.write_all(icon.as_bytes()).unwrap(); + println!("({}, {})", icon.width(), icon.height()) +} diff --git a/packages/desktop/src/default_icon.bin b/packages/desktop/src/default_icon.bin new file mode 100644 index 0000000000000000000000000000000000000000..2d4898f0cabc546d8fd1dc1127f8fe8e648f5aaa GIT binary patch literal 846400 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 V2#A0Ph=2%)fCz|y2-GKme*uRv2jc($ literal 0 HcmV?d00001 From f6ef9981dd9204cab0eae364c3609ad821807331 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 12 Feb 2022 14:03:08 -0600 Subject: [PATCH 043/310] 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 ab99bc6c8d44d0788a124f41d67e45ff254f16f6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:35:07 -0500 Subject: [PATCH 044/310] chore: comment out image generator when not in use --- packages/desktop/Cargo.toml | 2 +- packages/desktop/src/cfg.rs | 47 ++++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 805c46f3b..0d7c13e3f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -41,4 +41,4 @@ tokio_runtime = ["tokio"] [dev-dependencies] dioxus-hooks = { path = "../hooks" } -image = "0.24.0" \ No newline at end of file +# image = "0.24.0" # enable this when generating a new desktop image diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 84786a133..49a26ae78 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -103,25 +103,28 @@ impl Default for DesktopConfig { // dirty trick, avoid introducing `image` at runtime // TODO: use serde when `Icon` impl serde -#[test] -#[ignore] -fn prepare_default_icon() { - use image::io::Reader as ImageReader; - use image::ImageFormat; - use std::fs::File; - use std::io::Cursor; - use std::io::Write; - use std::path::PathBuf; - let png: &[u8] = include_bytes!("default_icon.png"); - let mut reader = ImageReader::new(Cursor::new(png)); - reader.set_format(ImageFormat::Png); - let icon = reader.decode().unwrap(); - let bin = PathBuf::from(file!()) - .parent() - .unwrap() - .join("default_icon.bin"); - println!("{:?}", bin); - let mut file = File::create(bin).unwrap(); - file.write_all(icon.as_bytes()).unwrap(); - println!("({}, {})", icon.width(), icon.height()) -} +// +// This function should only be enabled when generating new icons. +// +// #[test] +// #[ignore] +// fn prepare_default_icon() { +// use image::io::Reader as ImageReader; +// use image::ImageFormat; +// use std::fs::File; +// use std::io::Cursor; +// use std::io::Write; +// use std::path::PathBuf; +// let png: &[u8] = include_bytes!("default_icon.png"); +// let mut reader = ImageReader::new(Cursor::new(png)); +// reader.set_format(ImageFormat::Png); +// let icon = reader.decode().unwrap(); +// let bin = PathBuf::from(file!()) +// .parent() +// .unwrap() +// .join("default_icon.bin"); +// println!("{:?}", bin); +// let mut file = File::create(bin).unwrap(); +// file.write_all(icon.as_bytes()).unwrap(); +// println!("({}, {})", icon.width(), icon.height()) +// } From e06f6dc0305d90b4a3dc53ec876e879612c5dd79 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:37:39 -0500 Subject: [PATCH 045/310] fix: docs deploys should only be done on master commits --- .github/workflows/docs.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f0d2a4b80..0e19ecaec 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,7 +5,6 @@ on: paths: - docs/** - .github/workflows/docs.yml - pull_request: branches: - master From 542b48a59cb57aec0813f41cb54928199a7c62d9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:40:25 -0500 Subject: [PATCH 046/310] ci: disable on draft prs --- .github/workflows/macos.yml | 3 +++ .github/workflows/main.yml | 4 ++++ .github/workflows/windows.yml | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 7003b3925..16e306f6c 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -10,10 +10,13 @@ on: - lib.rs - Cargo.toml pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master jobs: + JOB: + if: github.event.pull_request.draft == false test: name: Test Suite runs-on: macos-latest diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 029e50e88..d4ebdd48e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,14 @@ on: - lib.rs - Cargo.toml pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master jobs: + JOB: + if: github.event.pull_request.draft == false + check: name: Check runs-on: ubuntu-latest diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index cee707dde..41e98f9ee 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -10,10 +10,14 @@ on: - lib.rs - Cargo.toml pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - master jobs: + JOB: + if: github.event.pull_request.draft == false + test: runs-on: windows-latest name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) From 8d2e23bde626eaed6e5b13384cc7ac38bab5ea66 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:41:38 -0500 Subject: [PATCH 047/310] ci: fix draft request --- .github/workflows/macos.yml | 3 +-- .github/workflows/main.yml | 4 +--- .github/workflows/windows.yml | 4 +--- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 16e306f6c..d1351712a 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -15,9 +15,8 @@ on: - master jobs: - JOB: - if: github.event.pull_request.draft == false test: + if: github.event.pull_request.draft == false name: Test Suite runs-on: macos-latest steps: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d4ebdd48e..3e1ec15c0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,10 +15,8 @@ on: - master jobs: - JOB: - if: github.event.pull_request.draft == false - check: + if: github.event.pull_request.draft == false name: Check runs-on: ubuntu-latest steps: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 41e98f9ee..518da37b3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -15,10 +15,8 @@ on: - master jobs: - JOB: - if: github.event.pull_request.draft == false - test: + if: github.event.pull_request.draft == false runs-on: windows-latest name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) env: From 3c54ce06f2c2359aadf6f1c4ad0a4eeca4bcb403 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:43:15 -0500 Subject: [PATCH 048/310] fix: disable more --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e1ec15c0..07ccd8069 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,6 +34,7 @@ jobs: command: check test: + if: github.event.pull_request.draft == false name: Test Suite runs-on: ubuntu-latest steps: @@ -55,6 +56,7 @@ jobs: args: tests fmt: + if: github.event.pull_request.draft == false name: Rustfmt runs-on: ubuntu-latest steps: @@ -72,6 +74,7 @@ jobs: args: --all -- --check clippy: + if: github.event.pull_request.draft == false name: Clippy runs-on: ubuntu-latest steps: @@ -91,6 +94,7 @@ jobs: args: -- -D warnings coverage: + if: github.event.pull_request.draft == false name: Coverage runs-on: ubuntu-latest container: From e03f8ca90c324be7a9051ca3c138a5171a5ebbd5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 13 Feb 2022 12:44:17 -0500 Subject: [PATCH 049/310] ci: no more codecov failing --- codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/codecov.yml b/codecov.yml index 69cb76019..325c05ec8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1 +1,2 @@ comment: false +fail_ci_if_error: false From c5025357247465fc9ca2e85f97e81b2c35da8805 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 18:59:15 +0100 Subject: [PATCH 050/310] Update dependencies Specifically set wry to 0.13; this has breaking changes (notably: RPC -> IPC). --- Cargo.toml | 12 ++++++------ packages/desktop/Cargo.toml | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e3a0a032..e1561cb59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,15 +57,15 @@ members = [ ] [dev-dependencies] -futures-util = "0.3.17" +futures-util = "0.3.21" log = "0.4.14" num-format = "0.4.0" separator = "0.4.1" -serde = { version = "1.0.131", features = ["derive"] } +serde = { version = "1.0.136", features = ["derive"] } im-rc = "15.0.0" -anyhow = "1.0.51" -serde_json = "1.0.73" +anyhow = "1.0.53" +serde_json = "1.0.79" rand = { version = "0.8.4", features = ["small_rng"] } -tokio = { version = "1.14.0", features = ["full"] } -reqwest = { version = "0.11.8", features = ["json"] } +tokio = { version = "1.16.1", features = ["full"] } +reqwest = { version = "0.11.9", features = ["json"] } dioxus = { path = ".", features = ["desktop", "ssr", "router"] } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 0d7c13e3f..d1f8b8697 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -13,15 +13,15 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } -argh = "0.1.4" -serde = "1.0.120" -serde_json = "1.0.61" -thiserror = "1.0.23" -log = "0.4.13" +argh = "0.1.7" +serde = "1.0.136" +serde_json = "1.0.79" +thiserror = "1.0.30" +log = "0.4.14" html-escape = "0.2.9" -wry = "0.12.2" -futures-channel = "0.3" -tokio = { version = "1.12.0", features = [ +wry = { version = "0.13.1" } +futures-channel = "0.3.21" +tokio = { version = "1.16.1", features = [ "sync", "rt-multi-thread", "rt", From ee2b869e9957312f1141065102f9fa00e3405ee7 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:39:47 +0100 Subject: [PATCH 051/310] Add optional feature flags of wry Check wry's documentation for each. Some of them are platform dependent or have platform dependent effects. (mostly MacOS and Linux) --- Cargo.toml | 7 +++++++ packages/desktop/Cargo.toml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e1561cb59..3f0d50187 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,13 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] +devtool = ["dioxus-desktop/devtool"] +fullscreen = ["dioxus-desktop/fullscreen"] +transparent = ["dioxus-desktop/transparent"] + +tray = ["dioxus-desktop/tray"] +ayatana = ["dioxus-desktop/ayatana"] + # "dioxus-router/web" # "dioxus-router/desktop" # desktop = ["dioxus-desktop", "dioxus-router/desktop"] diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index d1f8b8697..2622355eb 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -38,6 +38,13 @@ dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0" } default = ["tokio_runtime"] tokio_runtime = ["tokio"] +devtool = ["wry/devtool"] +fullscreen = ["wry/fullscreen"] +transparent = ["wry/transparent"] + +tray = ["wry/tray"] +ayatana = ["wry/ayatana"] + [dev-dependencies] dioxus-hooks = { path = "../hooks" } From c40d225d7db2490f47967d1fea5c65e1a524954d Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:50:29 +0100 Subject: [PATCH 052/310] Fix typo --- packages/desktop/src/cfg.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 49a26ae78..210ec9637 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -12,9 +12,9 @@ use wry::{ pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); pub struct DesktopConfig { - pub window: WindowBuilder, - pub file_drop_handler: Option bool>>, - pub protocols: Vec, + pub(crate) window: WindowBuilder, + pub(crate) file_drop_handler: Option bool>>, + pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, pub(crate) event_handler: Option>, } From a5bf25ce18d97ad58fa10fd0e9be5cb69f632cbd Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:51:37 +0100 Subject: [PATCH 053/310] Adjust visibility --- packages/desktop/src/cfg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 210ec9637..90fd0f1ae 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -19,7 +19,7 @@ pub struct DesktopConfig { pub(crate) event_handler: Option>, } -pub type WryProtocol = ( +pub(crate) type WryProtocol = ( String, Box WryResult + 'static>, ); From afa5a301c7ddae707130f0f8fb27a9446f4a879d Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:52:05 +0100 Subject: [PATCH 054/310] Fix typo --- packages/interpreter/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/interpreter/src/lib.rs b/packages/interpreter/src/lib.rs index ebca3cbf4..076a89197 100644 --- a/packages/interpreter/src/lib.rs +++ b/packages/interpreter/src/lib.rs @@ -1,4 +1,4 @@ -pub static INTERPRTER_JS: &str = include_str!("./interpreter.js"); +pub static INTERPRETER_JS: &str = include_str!("./interpreter.js"); #[cfg(feature = "web")] mod bindings; From 594a794f0538749f56f8fb56d011e46aead7ca1e Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:56:11 +0100 Subject: [PATCH 055/310] Switch from RPC to IPC --- packages/desktop/src/events.rs | 40 +++++++++++++++++------ packages/desktop/src/lib.rs | 42 ++++++++++++------------- packages/interpreter/src/interpreter.js | 12 ++++--- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 760bf7662..a991ec788 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -1,5 +1,4 @@ -//! Convert a serialized event to an event Trigger -//! +//! Convert a serialized event to an event trigger use std::any::Any; use std::sync::Arc; @@ -7,27 +6,50 @@ use std::sync::Arc; use dioxus_core::{ElementId, EventPriority, UserEvent}; use dioxus_html::on::*; +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct IpcMessage { + method: String, + params: serde_json::Value, +} + +impl IpcMessage { + pub(crate) fn method(&self) -> &str { + self.method.as_str() + } + + pub(crate) fn params(self) -> serde_json::Value { + self.params + } +} + +pub(crate) fn parse_ipc_message(payload: &str) -> Option { + let mm = serde_json::from_str(payload); + match mm { + Ok(message) => Some(message), + Err(e) => { + log::error!("could not parse IPC message, error: {e}"); + None + } + } +} + #[derive(serde::Serialize, serde::Deserialize)] struct ImEvent { event: String, mounted_dom_id: u64, - // scope: u64, contents: serde_json::Value, } pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { - let ims: Vec = serde_json::from_value(val).unwrap(); - let ImEvent { event, mounted_dom_id, contents, - } = ims.into_iter().next().unwrap(); + } = serde_json::from_value(val).unwrap(); - // let scope_id = ScopeId(scope as usize); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); - let name = event_name_from_typ(&event); + let name = event_name_from_type(&event); let event = make_synthetic_event(&event, contents); UserEvent { @@ -105,7 +127,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc &'static str { +fn event_name_from_type(typ: &str) -> &'static str { match typ { "copy" => "copy", "cut" => "cut", diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index f4e28ca2a..90fb4a3b0 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -132,23 +132,24 @@ pub fn launch_with_props( .with_transparent(cfg.window.window.transparent) .with_url("dioxus://index.html/") .unwrap() - .with_rpc_handler(move |_window: &Window, req: RpcRequest| { - match req.method.as_str() { - "user_event" => { - let event = events::trigger_from_serialized(req.params.unwrap()); - log::trace!("User event: {:?}", event); - sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); - } - "initialize" => { - is_ready.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy.send_event(UserWindowEvent::Update); - } - "browser_open" => { - println!("browser_open"); - let data = req.params.unwrap(); - log::trace!("Open browser: {:?}", data); - if let Some(arr) = data.as_array() { - if let Some(temp) = arr[0].as_object() { + .with_ipc_handler(move |_window: &Window, payload: String| { + parse_ipc_message(&payload) + .map(|message| match message.method() { + "user_event" => { + let event = trigger_from_serialized(message.params()); + log::trace!("User event: {:?}", event); + sender.unbounded_send(SchedulerMsg::Event(event)).unwrap(); + } + "initialize" => { + is_ready.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = proxy + .send_event(user_window_events::UserWindowEvent::Update); + } + "browser_open" => { + println!("browser_open"); + let data = message.params(); + log::trace!("Open browser: {:?}", data); + if let Some(temp) = data.as_object() { if temp.contains_key("href") { let url = temp.get("href").unwrap().as_str().unwrap(); if let Err(e) = webbrowser::open(url) { @@ -157,11 +158,8 @@ pub fn launch_with_props( } } } - } - _ => {} - } - None - }) + _ => (), + }) .with_custom_protocol(String::from("dioxus"), move |request| { // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" // For now, we only serve two pieces of content which get included as bytes into the final binary. diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f0acd0802..f042b74c9 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -2,7 +2,7 @@ export function main() { let root = window.document.getElementById("main"); if (root != null) { window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); + window.ipc.postMessage(serializeIpcMessage("initialize")) } } export class Interpreter { @@ -207,7 +207,7 @@ export class Interpreter { event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { - window.rpc.call("browser_open", { href }); + window.ipc.postMessage(serializeIpcMessage("browser_open", { href })) } } } @@ -261,11 +261,12 @@ export class Interpreter { if (realId == null) { return; } - window.rpc.call("user_event", { + window.ipc.postMessage(serializeIpcMessage( + "user_event", { event: edit.event_name, mounted_dom_id: parseInt(realId), contents: contents, - }); + })); } }; this.NewEventListener(edit.event_name, edit.root, handler); @@ -544,6 +545,9 @@ export function serialize_event(event) { } } } +function serializeIpcMessage(method, params = {}) { + return JSON.stringify({ method, params }); +} const bool_attrs = { allowfullscreen: true, allowpaymentrequest: true, From 73ce79bd2a31e409fc34d0673c8efbd08fc01d31 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:57:46 +0100 Subject: [PATCH 056/310] Extract protocol hander into module --- packages/desktop/src/lib.rs | 47 +++----------------------------- packages/desktop/src/protocol.rs | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 packages/desktop/src/protocol.rs diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 90fb4a3b0..31812f680 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -160,50 +160,11 @@ pub fn launch_with_props( } _ => (), }) - .with_custom_protocol(String::from("dioxus"), move |request| { - // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" - // For now, we only serve two pieces of content which get included as bytes into the final binary. - let path = request.uri().replace("dioxus://", ""); - - // all assets shouldbe called from index.html - let trimmed = path.trim_start_matches("index.html/"); - - if trimmed.is_empty() { - wry::http::ResponseBuilder::new() - .mimetype("text/html") - .body(include_bytes!("./index.html").to_vec()) - } else if trimmed == "index.js" { - wry::http::ResponseBuilder::new() - .mimetype("text/javascript") - .body(dioxus_interpreter_js::INTERPRTER_JS.as_bytes().to_vec()) - } else { - // Read the file content from file path - use std::fs::read; - - let path_buf = std::path::Path::new(trimmed).canonicalize()?; - let cur_path = std::path::Path::new(".").canonicalize()?; - - if !path_buf.starts_with(cur_path) { - return wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::FORBIDDEN) - .body(String::from("Forbidden").into_bytes()); - } - - if !path_buf.exists() { - return wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::NOT_FOUND) - .body(String::from("Not Found").into_bytes()); - } - - let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); - - // do not let path searching to go two layers beyond the caller level - let data = read(path_buf)?; - let meta = format!("{}", mime); - - wry::http::ResponseBuilder::new().mimetype(&meta).body(data) - } + .unwrap_or_else(|| { + log::warn!("invalid IPC message received"); + }); }) + .with_custom_protocol(String::from("dioxus"), protocol::desktop_handler) .with_file_drop_handler(move |window, evet| { file_handler .as_ref() diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs new file mode 100644 index 000000000..c8f007c1d --- /dev/null +++ b/packages/desktop/src/protocol.rs @@ -0,0 +1,47 @@ +use std::path::Path; +use wry::{ + http::{status::StatusCode, Request, Response, ResponseBuilder}, + Result, +}; + +pub(super) fn desktop_handler(request: &Request) -> Result { + // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case". + // For now, we only serve two pieces of content which get included as bytes into the final binary. + let path = request.uri().replace("dioxus://", ""); + + // all assets should be called from index.html + let trimmed = path.trim_start_matches("index.html/"); + + if trimmed.is_empty() { + ResponseBuilder::new() + .mimetype("text/html") + .body(include_bytes!("./index.html").to_vec()) + } else if trimmed == "index.js" { + ResponseBuilder::new() + .mimetype("text/javascript") + .body(dioxus_interpreter_js::INTERPRETER_JS.as_bytes().to_vec()) + } else { + let path_buf = Path::new(trimmed).canonicalize()?; + let cur_path = Path::new(".").canonicalize()?; + + if !path_buf.starts_with(cur_path) { + return ResponseBuilder::new() + .status(StatusCode::FORBIDDEN) + .body(String::from("Forbidden").into_bytes()); + } + + if !path_buf.exists() { + return ResponseBuilder::new() + .status(StatusCode::NOT_FOUND) + .body(String::from("Not Found").into_bytes()); + } + + let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); + + // do not let path searching to go two layers beyond the caller level + let data = std::fs::read(path_buf)?; + let meta = format!("{}", mime); + + ResponseBuilder::new().mimetype(&meta).body(data) + } +} From cf543ab1dfe0a0af423c37d3850af04432cb075a Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:58:37 +0100 Subject: [PATCH 057/310] Extract controller into module --- packages/desktop/src/controller.rs | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/desktop/src/controller.rs diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs new file mode 100644 index 000000000..7d9a2f432 --- /dev/null +++ b/packages/desktop/src/controller.rs @@ -0,0 +1,106 @@ +use crate::desktop_context::DesktopContext; +use crate::user_window_events::UserWindowEvent; +use dioxus_core::*; +use std::{ + collections::{HashMap, VecDeque}, + sync::atomic::AtomicBool, + sync::{Arc, RwLock}, +}; +use wry::{ + self, + application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId}, + webview::WebView, +}; + +pub(super) struct DesktopController { + pub(super) webviews: HashMap, + pub(super) sender: futures_channel::mpsc::UnboundedSender, + pub(super) pending_edits: Arc>>, + pub(super) quit_app_on_close: bool, + pub(super) is_ready: Arc, +} + +impl DesktopController { + // Launch the virtualdom on its own thread managed by tokio + // returns the desktop state + pub(super) fn new_on_tokio( + root: Component

, + props: P, + proxy: EventLoopProxy, + ) -> Self { + let edit_queue = Arc::new(RwLock::new(VecDeque::new())); + let pending_edits = edit_queue.clone(); + + let (sender, receiver) = futures_channel::mpsc::unbounded::(); + let return_sender = sender.clone(); + + let desktop_context_proxy = proxy.clone(); + std::thread::spawn(move || { + // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + runtime.block_on(async move { + let mut dom = + VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); + + let window_context = DesktopContext::new(desktop_context_proxy); + + dom.base_scope().provide_context(window_context); + + let edits = dom.rebuild(); + + edit_queue + .write() + .unwrap() + .push_front(serde_json::to_string(&edits.edits).unwrap()); + + loop { + dom.wait_for_work().await; + let mut muts = dom.work_with_deadline(|| false); + + while let Some(edit) = muts.pop() { + edit_queue + .write() + .unwrap() + .push_front(serde_json::to_string(&edit.edits).unwrap()); + } + + let _ = proxy.send_event(UserWindowEvent::Update); + } + }) + }); + + Self { + pending_edits, + sender: return_sender, + webviews: HashMap::new(), + is_ready: Arc::new(AtomicBool::new(false)), + quit_app_on_close: true, + } + } + + pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { + self.webviews.remove(&window_id); + + if self.webviews.is_empty() && self.quit_app_on_close { + *control_flow = ControlFlow::Exit; + } + } + + pub(super) fn try_load_ready_webviews(&mut self) { + if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { + let mut queue = self.pending_edits.write().unwrap(); + let (_id, view) = self.webviews.iter_mut().next().unwrap(); + + while let Some(edit) = queue.pop_back() { + view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) + .unwrap(); + } + } else { + println!("waiting for ready"); + } + } +} From e7a0e5f1d9fb03e9aa1ef8aa9978898f4c6ba56f Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:58:50 +0100 Subject: [PATCH 058/310] Extract user window events into module --- packages/desktop/src/user_window_events.rs | 72 ++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/desktop/src/user_window_events.rs diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs new file mode 100644 index 000000000..93eeaccf1 --- /dev/null +++ b/packages/desktop/src/user_window_events.rs @@ -0,0 +1,72 @@ +use wry::application::event_loop::ControlFlow; +use wry::application::window::Fullscreen as WryFullscreen; + +use crate::controller::DesktopController; + +pub(crate) enum UserWindowEvent { + Update, + + CloseWindow, + DragWindow, + FocusWindow, + + Visible(bool), + Minimize(bool), + Maximize(bool), + MaximizeToggle, + Resizable(bool), + AlwaysOnTop(bool), + Fullscreen(bool), + + CursorVisible(bool), + CursorGrab(bool), + + SetTitle(String), + SetDecorations(bool), + + DevTool, +} + +use UserWindowEvent::*; + +pub(super) fn handler( + user_event: UserWindowEvent, + desktop: &mut DesktopController, + control_flow: &mut ControlFlow, +) { + // currently dioxus-desktop supports a single window only, + // so we can grab the only webview from the map; + let webview = desktop.webviews.values().next().unwrap(); + let window = webview.window(); + + match user_event { + Update => desktop.try_load_ready_webviews(), + CloseWindow => *control_flow = ControlFlow::Exit, + DragWindow => { + // if the drag_window has any errors, we don't do anything + window.fullscreen().is_none().then(|| window.drag_window()); + } + Visible(state) => window.set_visible(state), + Minimize(state) => window.set_minimized(state), + Maximize(state) => window.set_maximized(state), + MaximizeToggle => window.set_maximized(!window.is_maximized()), + Fullscreen(state) => { + window.current_monitor().map(|handle| { + window.set_fullscreen(state.then(|| WryFullscreen::Borderless(Some(handle)))); + }); + } + FocusWindow => window.set_focus(), + Resizable(state) => window.set_resizable(state), + AlwaysOnTop(state) => window.set_always_on_top(state), + + CursorVisible(state) => window.set_cursor_visible(state), + CursorGrab(state) => { + let _ = window.set_cursor_grab(state); + } + + SetTitle(content) => window.set_title(&content), + SetDecorations(state) => window.set_decorations(state), + + DevTool => webview.devtool(), + } +} From 2828f45e12e129a29006bdacbf0ccbec38fefbfe Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 19:59:25 +0100 Subject: [PATCH 059/310] Clean up desktop's lib.rs --- packages/desktop/src/lib.rs | 246 ++---------------------------------- 1 file changed, 11 insertions(+), 235 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 31812f680..371adced5 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -3,31 +3,28 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] pub mod cfg; +mod controller; pub mod desktop_context; pub mod escape; pub mod events; +mod protocol; +mod user_window_events; use cfg::DesktopConfig; +use controller::DesktopController; pub use desktop_context::use_window; -use desktop_context::DesktopContext; use dioxus_core::*; -use std::{ - collections::{HashMap, VecDeque}, - sync::atomic::AtomicBool, - sync::{Arc, RwLock}, -}; +use events::parse_ipc_message; use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, - window::{Window, WindowId}, + window::Window, }; pub use wry; pub use wry::application as tao; -use wry::{ - application::{event_loop::EventLoopProxy, window::Fullscreen}, - webview::RpcRequest, - webview::{WebView, WebViewBuilder}, -}; +use wry::webview::WebViewBuilder; + +use crate::events::trigger_from_serialized; /// Launch the WebView and run the event loop. /// @@ -194,114 +191,8 @@ pub fn launch_with_props( _ => {} }, - Event::UserEvent(_evt) => { - // - match _evt { - UserWindowEvent::Update => desktop.try_load_ready_webviews(), - UserWindowEvent::DragWindow => { - // this loop just run once, because dioxus-desktop is unsupport multi-window. - for webview in desktop.webviews.values() { - let window = webview.window(); - // start to drag the window. - // if the drag_window have any err. we don't do anything. - - if window.fullscreen().is_some() { - return; - } - - let _ = window.drag_window(); - } - } - UserWindowEvent::CloseWindow => { - // close window - *control_flow = ControlFlow::Exit; - } - UserWindowEvent::Visible(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_visible(state); - } - } - UserWindowEvent::Minimize(state) => { - // this loop just run once, because dioxus-desktop is unsupport multi-window. - for webview in desktop.webviews.values() { - let window = webview.window(); - // change window minimized state. - window.set_minimized(state); - } - } - UserWindowEvent::Maximize(state) => { - // this loop just run once, because dioxus-desktop is unsupport multi-window. - for webview in desktop.webviews.values() { - let window = webview.window(); - // change window maximized state. - window.set_maximized(state); - } - } - UserWindowEvent::Fullscreen(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - - let current_monitor = window.current_monitor(); - - if current_monitor.is_none() { - return; - } - - let fullscreen = if state { - Some(Fullscreen::Borderless(current_monitor)) - } else { - None - }; - - window.set_fullscreen(fullscreen); - } - } - UserWindowEvent::FocusWindow => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_focus(); - } - } - UserWindowEvent::Resizable(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_resizable(state); - } - } - UserWindowEvent::AlwaysOnTop(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_always_on_top(state); - } - } - - UserWindowEvent::CursorVisible(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_cursor_visible(state); - } - } - UserWindowEvent::CursorGrab(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - let _ = window.set_cursor_grab(state); - } - } - - UserWindowEvent::SetTitle(content) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_title(&content); - } - } - UserWindowEvent::SetDecorations(state) => { - for webview in desktop.webviews.values() { - let window = webview.window(); - window.set_decorations(state); - } - } - } + Event::UserEvent(user_event) => { + user_window_events::handler(user_event, &mut desktop, control_flow) } Event::MainEventsCleared => {} Event::Resumed => {} @@ -312,118 +203,3 @@ pub fn launch_with_props( } }) } - -pub enum UserWindowEvent { - Update, - DragWindow, - CloseWindow, - FocusWindow, - Visible(bool), - Minimize(bool), - Maximize(bool), - Resizable(bool), - AlwaysOnTop(bool), - Fullscreen(bool), - - CursorVisible(bool), - CursorGrab(bool), - - SetTitle(String), - SetDecorations(bool), -} - -pub struct DesktopController { - pub proxy: EventLoopProxy, - pub webviews: HashMap, - pub sender: futures_channel::mpsc::UnboundedSender, - pub pending_edits: Arc>>, - pub quit_app_on_close: bool, - pub is_ready: Arc, -} - -impl DesktopController { - // Launch the virtualdom on its own thread managed by tokio - // returns the desktop state - pub fn new_on_tokio( - root: Component

, - props: P, - evt: EventLoopProxy, - ) -> Self { - let edit_queue = Arc::new(RwLock::new(VecDeque::new())); - let pending_edits = edit_queue.clone(); - - let (sender, receiver) = futures_channel::mpsc::unbounded::(); - let return_sender = sender.clone(); - let proxy = evt.clone(); - - let desktop_context_proxy = proxy.clone(); - std::thread::spawn(move || { - // We create the runtime as multithreaded, so you can still "spawn" onto multiple threads - let runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap(); - - runtime.block_on(async move { - let mut dom = - VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); - - let window_context = DesktopContext::new(desktop_context_proxy); - - dom.base_scope().provide_context(window_context); - - let edits = dom.rebuild(); - - edit_queue - .write() - .unwrap() - .push_front(serde_json::to_string(&edits.edits).unwrap()); - - loop { - dom.wait_for_work().await; - let mut muts = dom.work_with_deadline(|| false); - - while let Some(edit) = muts.pop() { - edit_queue - .write() - .unwrap() - .push_front(serde_json::to_string(&edit.edits).unwrap()); - } - - let _ = evt.send_event(UserWindowEvent::Update); - } - }) - }); - - Self { - pending_edits, - sender: return_sender, - proxy, - webviews: HashMap::new(), - is_ready: Arc::new(AtomicBool::new(false)), - quit_app_on_close: true, - } - } - - pub fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { - self.webviews.remove(&window_id); - - if self.webviews.is_empty() && self.quit_app_on_close { - *control_flow = ControlFlow::Exit; - } - } - - pub fn try_load_ready_webviews(&mut self) { - if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - let mut queue = self.pending_edits.write().unwrap(); - let (_id, view) = self.webviews.iter_mut().next().unwrap(); - - while let Some(edit) = queue.pop_back() { - view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) - .unwrap(); - } - } else { - println!("waiting for ready"); - } - } -} From 934d5998dbc5bc8e14cce10d3b4f1332205f6ae8 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 20:00:45 +0100 Subject: [PATCH 060/310] Support maximize toggle and devtool --- packages/desktop/src/desktop_context.rs | 47 +++++++++++++------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d180e1235..e1b726d73 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,7 +3,8 @@ use std::rc::Rc; use dioxus_core::ScopeState; use wry::application::event_loop::EventLoopProxy; -use crate::UserWindowEvent; +use crate::user_window_events::UserWindowEvent; +use UserWindowEvent::*; type ProxyType = EventLoopProxy; @@ -35,75 +36,77 @@ impl DesktopContext { /// onmousedown: move |_| { desktop.drag_window(); } /// ``` pub fn drag(&self) { - let _ = self.proxy.send_event(UserWindowEvent::DragWindow); + let _ = self.proxy.send_event(DragWindow); } /// set window minimize state pub fn set_minimized(&self, minimized: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Minimize(minimized)); + let _ = self.proxy.send_event(Minimize(minimized)); } /// set window maximize state pub fn set_maximized(&self, maximized: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Maximize(maximized)); + let _ = self.proxy.send_event(Maximize(maximized)); + } + + /// toggle window maximize state + pub fn toggle_maximized(&self) { + let _ = self.proxy.send_event(MaximizeToggle); } /// set window visible or not pub fn set_visible(&self, visible: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Visible(visible)); + let _ = self.proxy.send_event(Visible(visible)); } /// close window pub fn close(&self) { - let _ = self.proxy.send_event(UserWindowEvent::CloseWindow); + let _ = self.proxy.send_event(CloseWindow); } /// set window to focus pub fn focus(&self) { - let _ = self.proxy.send_event(UserWindowEvent::FocusWindow); + let _ = self.proxy.send_event(FocusWindow); } /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { - let _ = self - .proxy - .send_event(UserWindowEvent::Fullscreen(fullscreen)); + let _ = self.proxy.send_event(Fullscreen(fullscreen)); } /// set resizable state pub fn set_resizable(&self, resizable: bool) { - let _ = self.proxy.send_event(UserWindowEvent::Resizable(resizable)); + let _ = self.proxy.send_event(Resizable(resizable)); } /// set the window always on top pub fn set_always_on_top(&self, top: bool) { - let _ = self.proxy.send_event(UserWindowEvent::AlwaysOnTop(top)); + let _ = self.proxy.send_event(AlwaysOnTop(top)); } // set cursor visible or not pub fn set_cursor_visible(&self, visible: bool) { - let _ = self - .proxy - .send_event(UserWindowEvent::CursorVisible(visible)); + let _ = self.proxy.send_event(CursorVisible(visible)); } // set cursor grab pub fn set_cursor_grab(&self, grab: bool) { - let _ = self.proxy.send_event(UserWindowEvent::CursorGrab(grab)); + let _ = self.proxy.send_event(CursorGrab(grab)); } /// set window title pub fn set_title(&self, title: &str) { - let _ = self - .proxy - .send_event(UserWindowEvent::SetTitle(String::from(title))); + let _ = self.proxy.send_event(SetTitle(String::from(title))); } /// change window to borderless pub fn set_decorations(&self, decoration: bool) { - let _ = self - .proxy - .send_event(UserWindowEvent::SetDecorations(decoration)); + let _ = self.proxy.send_event(SetDecorations(decoration)); + } + + /// opens DevTool window + pub fn devtool(&self) { + let _ = self.proxy.send_event(DevTool); } } From 932ad0164454c65335f7c62078d3a7250a8608c3 Mon Sep 17 00:00:00 2001 From: Christoph Grabo Date: Sun, 13 Feb 2022 20:57:30 +0100 Subject: [PATCH 061/310] Make clippy happy --- packages/desktop/src/user_window_events.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/user_window_events.rs b/packages/desktop/src/user_window_events.rs index 93eeaccf1..aab14561b 100644 --- a/packages/desktop/src/user_window_events.rs +++ b/packages/desktop/src/user_window_events.rs @@ -51,9 +51,9 @@ pub(super) fn handler( Maximize(state) => window.set_maximized(state), MaximizeToggle => window.set_maximized(!window.is_maximized()), Fullscreen(state) => { - window.current_monitor().map(|handle| { + if let Some(handle) = window.current_monitor() { window.set_fullscreen(state.then(|| WryFullscreen::Borderless(Some(handle)))); - }); + } } FocusWindow => window.set_focus(), Resizable(state) => window.set_resizable(state), From dd9f0f362ec8e7193a4920c2ee46246f1cbe67c9 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 14 Feb 2022 16:52:16 +0800 Subject: [PATCH 062/310] fix: statement problem --- packages/interpreter/src/interpreter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f0acd0802..a631d0368 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -102,10 +102,11 @@ export class Interpreter { SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; + console.log(ns); if (ns === "style") { // @ts-ignore node.style[name] = value; - } else if (ns != null || ns !== undefined) { + } else if (ns != null || ns != undefined) { node.setAttributeNS(ns, name, value); } else { switch (name) { From e3e5f22bc7605b7019d5f92eeff719abcbf512c5 Mon Sep 17 00:00:00 2001 From: mrxiaozhuox Date: Mon, 14 Feb 2022 16:53:35 +0800 Subject: [PATCH 063/310] feat: move `default_icon` to assets --- packages/desktop/src/{ => assets}/default_icon.bin | Bin packages/desktop/src/{ => assets}/default_icon.png | Bin packages/desktop/src/cfg.rs | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/desktop/src/{ => assets}/default_icon.bin (100%) rename packages/desktop/src/{ => assets}/default_icon.png (100%) diff --git a/packages/desktop/src/default_icon.bin b/packages/desktop/src/assets/default_icon.bin similarity index 100% rename from packages/desktop/src/default_icon.bin rename to packages/desktop/src/assets/default_icon.bin diff --git a/packages/desktop/src/default_icon.png b/packages/desktop/src/assets/default_icon.png similarity index 100% rename from packages/desktop/src/default_icon.png rename to packages/desktop/src/assets/default_icon.png diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 49a26ae78..2a1067ded 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -88,7 +88,7 @@ impl DesktopConfig { impl DesktopConfig { pub(crate) fn with_default_icon(mut self) -> Self { - let bin: &[u8] = include_bytes!("default_icon.bin"); + let bin: &[u8] = include_bytes!("./assets/default_icon.bin"); let rgba = Icon::from_rgba(bin.to_owned(), 460, 460).expect("image parse failed"); self.window.window.window_icon = Some(rgba); self From 3317b3aac51f2e1225ffc873cfa0ed25b3096f3d Mon Sep 17 00:00:00 2001 From: dazmaks Date: Mon, 14 Feb 2022 17:13:09 +0300 Subject: [PATCH 064/310] fix: typo --- docs/reference/src/mobile/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/src/mobile/index.md b/docs/reference/src/mobile/index.md index 2e8344ed1..aa3470761 100644 --- a/docs/reference/src/mobile/index.md +++ b/docs/reference/src/mobile/index.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo moble init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. From 8ca505b65beba1edf74eddf47d4da5a8e7be7033 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 14 Feb 2022 09:30:08 -0500 Subject: [PATCH 065/310] feat: better link --- packages/router/src/components/link.rs | 48 +++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 18e9fd92e..b2b69108f 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -34,6 +34,17 @@ pub struct LinkProps<'a> { #[props(default, strip_option)] title: Option<&'a str>, + #[props(default = true)] + autodetect: bool, + + /// Is this link an external link? + #[props(default = false)] + external: bool, + + /// New tab? + #[props(default = false)] + new_tab: bool, + children: Element<'a>, #[props(default)] @@ -41,17 +52,38 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - // log::trace!("render Link to {}", cx.props.to); if let Some(service) = cx.consume_context::() { + let LinkProps { + to, + href, + class, + id, + title, + autodetect, + external, + new_tab, + children, + .. + } = cx.props; + + let is_http = to.starts_with("http") || to.starts_with("https"); + let outerlink = (*autodetect && is_http) || *external; + + let prevent_default = if outerlink { "" } else { "onclick" }; + return cx.render(rsx! { a { - href: "{cx.props.to}", - class: format_args!("{}", cx.props.class.unwrap_or("")), - id: format_args!("{}", cx.props.id.unwrap_or("")), - title: format_args!("{}", cx.props.title.unwrap_or("")), - - prevent_default: "onclick", - onclick: move |_| service.push_route(cx.props.to), + href: "{to}", + class: format_args!("{}", class.unwrap_or("")), + id: format_args!("{}", id.unwrap_or("")), + title: format_args!("{}", title.unwrap_or("")), + prevent_default: "{prevent_default}", + target: format_args!("{}", if *new_tab { "_blank" } else { "" }), + onclick: move |_| { + if !outerlink { + service.push_route(to); + } + }, &cx.props.children } From 5a908d1e8b92d712c35c7f05af5956926acec29e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 14 Feb 2022 12:23:30 -0500 Subject: [PATCH 066/310] fix: remove console log --- packages/interpreter/src/interpreter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index a631d0368..5b81d3a9f 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -102,7 +102,6 @@ export class Interpreter { SetAttribute(root, field, value, ns) { const name = field; const node = this.nodes[root]; - console.log(ns); if (ns === "style") { // @ts-ignore node.style[name] = value; From 1413631d306065b76a9d2dce1c6e709c0def0bd4 Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Mon, 14 Feb 2022 19:08:10 -0500 Subject: [PATCH 067/310] update: match DioxusLabs/docs#3 --- README.md | 8 ++++---- docs/guide/src/README.md | 12 ++++++------ docs/guide/src/setup.md | 2 +- docs/reference/src/SUMMARY.md | 11 ++++++----- .../src/{desktop/index.md => platforms/desktop.md} | 2 +- docs/reference/src/platforms/index.md | 10 ++++++++++ .../src/{mobile/index.md => platforms/mobile.md} | 2 +- .../reference/src/{ssr/index.md => platforms/ssr.md} | 0 .../reference/src/{tui/index.md => platforms/tui.md} | 2 +- .../reference/src/{web/index.md => platforms/web.md} | 2 +- 10 files changed, 31 insertions(+), 20 deletions(-) rename docs/reference/src/{desktop/index.md => platforms/desktop.md} (98%) create mode 100644 docs/reference/src/platforms/index.md rename docs/reference/src/{mobile/index.md => platforms/mobile.md} (99%) rename docs/reference/src/{ssr/index.md => platforms/ssr.md} (100%) rename docs/reference/src/{tui/index.md => platforms/tui.md} (98%) rename docs/reference/src/{web/index.md => platforms/web.md} (98%) diff --git a/README.md b/README.md index 02857ed18..23cd9c516 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,10 @@ cargo run --example EXAMPLE - - - - + + + +
TutorialWebDesktopSSRMobileWebDesktopSSRMobile State
diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index bbbd9f85f..da6c0156f 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -18,7 +18,7 @@ fn app(cx: Scope) -> Element { In general, Dioxus and React share many functional similarities. If this guide is lacking in any general concept or an error message is confusing, React's documentation might be more helpful. We are dedicated to providing a *familiar* toolkit for UI in Rust, so we've chosen to follow in the footsteps of popular UI frameworks (React, Redux, etc). If you know React, then you already know Dioxus. If you don't know either, this guide will still help you! -> This is an introduction book! For advanced topics, check out the [Reference](https://dioxuslabs.com/reference) instead. +> This is an introduction book! For advanced topics, check out the [Reference](/reference) instead. ## Multiplatform @@ -37,7 +37,7 @@ The Web is the best-supported target platform for Dioxus. To run on the Web, you Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features. -[Jump to the getting started guide for the web.]() +[Jump to the getting started guide for the web.](/reference/platforms/web) Examples: - [TodoMVC](https://github.com/DioxusLabs/example-projects/tree/master/todomvc) @@ -55,7 +55,7 @@ For rendering statically to an `.html` file or from a WebServer, then you'll wan let contents = dioxus::ssr::render_vdom(&dom); ``` -[Jump to the getting started guide for SSR.]() +[Jump to the getting started guide for SSR.](/reference/platforms/ssr) Examples: - [Example DocSite](https://github.com/dioxusLabs/docsite) @@ -68,13 +68,13 @@ The desktop is a powerful target for Dioxus, but is currently limited in capabil Desktop APIs will likely be in flux as we figure out better patterns than our ElectronJS counterpart. -[Jump to the getting started guide for Desktop.]() +[Jump to the getting started guide for Desktop.](/reference/platforms/desktop) Examples: - [File explorer](https://github.com/dioxusLabs/file-explorer/) - [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) -[![File ExplorerExample](https://github.com/DioxusLabs/file-explorer-example/raw/master/image.png)](https://github.com/dioxusLabs/file-explorer/) +[![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer) ### Mobile Support --- @@ -82,7 +82,7 @@ Mobile is currently the least-supported renderer target for Dioxus. Mobile apps Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets. -[Jump to the getting started guide for Mobile.]() +[Jump to the getting started guide for Mobile.](/reference/platforms/mobile) Examples: - [Todo App](https://github.com/DioxusLabs/example-projects/blob/master/ios_demo) diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 1dc9434c5..75cfc6606 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -8,7 +8,7 @@ We'll learn about: - Suggested cargo extensions -For platform-specific guides, check out the [Platform Specific Guides](../platforms/00-index.md). +For platform-specific guides, check out the [Platform Specific Guides](/reference/platforms/index.md). # Setting up Dioxus diff --git a/docs/reference/src/SUMMARY.md b/docs/reference/src/SUMMARY.md index 3feb9fe36..18f351bfa 100644 --- a/docs/reference/src/SUMMARY.md +++ b/docs/reference/src/SUMMARY.md @@ -2,11 +2,12 @@ - [Introduction](README.md) -- [Web](web/index.md) -- [Desktop](desktop/index.md) -- [Mobile](mobile/index.md) -- [SSR](ssr/index.md) -- [TUI](tui/index.md) +- [Platforms](platforms/index.md) + - [Web](platforms/web.md) + - [Server Side Rendering](platforms/ssr.md) + - [Desktop](platforms/desktop.md) + - [Mobile](platforms/mobile.md) + - [TUI](platforms/tui.md) - [Advanced Guides](guide/index.md) - [RSX in Depth](guide/rsx_in_depth.md) diff --git a/docs/reference/src/desktop/index.md b/docs/reference/src/platforms/desktop.md similarity index 98% rename from docs/reference/src/desktop/index.md rename to docs/reference/src/platforms/desktop.md index 8011f1eed..18393f132 100644 --- a/docs/reference/src/desktop/index.md +++ b/docs/reference/src/platforms/desktop.md @@ -1,4 +1,4 @@ -# Desktop +# Getting Started: Desktop One of Dioxus' killer features is the ability to quickly build a native desktop app that looks and feels the same across platforms. Apps built with Dioxus are typically <5mb in size and use existing system resources, so they won't hog extreme amounts of RAM or memory. diff --git a/docs/reference/src/platforms/index.md b/docs/reference/src/platforms/index.md new file mode 100644 index 000000000..fbbabd35d --- /dev/null +++ b/docs/reference/src/platforms/index.md @@ -0,0 +1,10 @@ +# Platforms + +Dioxus supports many different platforms. Below are a list of guides that walk you through setting up Dioxus for a specific platform. + +### Setup Guides +- [Web](web.md) +- [Server Side Rendering](ssr.md) +- [Desktop](desktop.md) +- [Mobile](mobile.md) +- [TUI](tui.md) \ No newline at end of file diff --git a/docs/reference/src/mobile/index.md b/docs/reference/src/platforms/mobile.md similarity index 99% rename from docs/reference/src/mobile/index.md rename to docs/reference/src/platforms/mobile.md index 2e8344ed1..5a41ffe60 100644 --- a/docs/reference/src/mobile/index.md +++ b/docs/reference/src/platforms/mobile.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo moble init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. diff --git a/docs/reference/src/ssr/index.md b/docs/reference/src/platforms/ssr.md similarity index 100% rename from docs/reference/src/ssr/index.md rename to docs/reference/src/platforms/ssr.md diff --git a/docs/reference/src/tui/index.md b/docs/reference/src/platforms/tui.md similarity index 98% rename from docs/reference/src/tui/index.md rename to docs/reference/src/platforms/tui.md index 3947883d4..e646bd4ba 100644 --- a/docs/reference/src/tui/index.md +++ b/docs/reference/src/platforms/tui.md @@ -1,4 +1,4 @@ -# TUI +# Getting Started: TUI TUI support is currently quite experimental. Even the project name will change. But, if you're willing to venture into the realm of the unknown, this guide will get you started. diff --git a/docs/reference/src/web/index.md b/docs/reference/src/platforms/web.md similarity index 98% rename from docs/reference/src/web/index.md rename to docs/reference/src/platforms/web.md index 062cba177..2ff5f621e 100644 --- a/docs/reference/src/web/index.md +++ b/docs/reference/src/platforms/web.md @@ -1,4 +1,4 @@ -# Getting Started: Dioxus for Web +# Getting Started: Web [*"Pack your things, we're going on an adventure!"*](https://trunkrs.dev) From 7c2d911fbf1abb23a216f3ef5c235bc75766ee4a Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 15 Feb 2022 09:54:32 -0600 Subject: [PATCH 068/310] Fix closure signature for use_ref example in interactivity docs The example showed the closure accepting a single argument, but it accepts none. --- docs/guide/src/interactivity/hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index a90afe2dc..a84a0b129 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/docs/guide/src/interactivity/hooks.md @@ -140,7 +140,7 @@ fn Child(cx: Scope, name: String) -> Element { // ✅ Or, use a hashmap with use_ref ```rust -let ages = use_ref(&cx, |_| HashMap::new()); +let ages = use_ref(&cx, || HashMap::new()); names.iter().map(|name| { let age = ages.get(name).unwrap(); From e43a8a9b6a68bd2344bc86c6613ab046d89da40b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 11:03:13 -0500 Subject: [PATCH 069/310] feat: remove old async channel for new channel in ric raf web code --- packages/web/Cargo.toml | 2 +- packages/web/src/dom.rs | 7 ++----- packages/web/src/lib.rs | 2 +- packages/web/src/ric_raf.rs | 21 +++++++++++---------- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7a3baa955..31a7ff6be 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -23,13 +23,13 @@ wasm-logger = "0.2.0" console_error_panic_hook = { version = "0.1.7", optional = true } wasm-bindgen-test = "0.3.29" once_cell = "1.9.0" -async-channel = "1.6.1" anyhow = "1.0.53" gloo-timers = { version = "0.2.3", features = ["futures"] } futures-util = "0.3.19" smallstr = "0.2.0" dioxus-interpreter-js = { path = "../interpreter", version = "^0.0.0", features = ["web"] } serde-wasm-bindgen = "0.4.2" +futures-channel = "0.3.21" [dependencies.web-sys] version = "0.3.56" diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 48fb25e6e..b2eb65d08 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -87,14 +87,11 @@ impl WebsysDom { } }); + // a match here in order to avoid some error during runtime browser test let document = load_document(); let root = match document.get_element_by_id(&cfg.rootname) { Some(root) => root, - // a match here in order to avoid some error during runtime browser test - None => { - let body = document.create_element("body").ok().unwrap(); - body - } + None => document.create_element("body").ok().unwrap(), }; Self { diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index cef1bc14c..90fe84d04 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -211,7 +211,7 @@ pub async fn run_with_props(root: Component, root_props: T websys_dom.apply_edits(edits.edits); } - let work_loop = ric_raf::RafLoop::new(); + let mut work_loop = ric_raf::RafLoop::new(); loop { log::trace!("waiting for work"); diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 2ac0668b9..89819fff0 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -7,6 +7,7 @@ //! Because RIC doesn't work on Safari, we polyfill using the "ricpolyfill.js" file and use some basic detection to see //! if RIC is available. +use futures_util::StreamExt; use gloo_timers::future::TimeoutFuture; use js_sys::Function; use wasm_bindgen::{prelude::Closure, JsCast, JsValue}; @@ -14,21 +15,21 @@ use web_sys::{window, Window}; pub(crate) struct RafLoop { window: Window, - ric_receiver: async_channel::Receiver, - raf_receiver: async_channel::Receiver<()>, + ric_receiver: futures_channel::mpsc::UnboundedReceiver, + raf_receiver: futures_channel::mpsc::UnboundedReceiver<()>, ric_closure: Closure, raf_closure: Closure, } impl RafLoop { pub fn new() -> Self { - let (raf_sender, raf_receiver) = async_channel::unbounded(); + let (raf_sender, raf_receiver) = futures_channel::mpsc::unbounded(); let raf_closure: Closure = Closure::wrap(Box::new(move |_v: JsValue| { - raf_sender.try_send(()).unwrap() + raf_sender.unbounded_send(()).unwrap() })); - let (ric_sender, ric_receiver) = async_channel::unbounded(); + let (ric_sender, ric_receiver) = futures_channel::mpsc::unbounded(); let has_idle_callback = { let bo = window().unwrap().dyn_into::().unwrap(); @@ -45,7 +46,7 @@ impl RafLoop { 10 }; - ric_sender.try_send(time_remaining).unwrap() + ric_sender.unbounded_send(time_remaining).unwrap() })); // execute the polyfill for safari @@ -64,16 +65,16 @@ impl RafLoop { } } /// waits for some idle time and returns a timeout future that expires after the idle time has passed - pub async fn wait_for_idle_time(&self) -> TimeoutFuture { + pub async fn wait_for_idle_time(&mut self) -> TimeoutFuture { let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap(); - let deadline = self.ric_receiver.recv().await.unwrap(); + let deadline = self.ric_receiver.next().await.unwrap(); TimeoutFuture::new(deadline) } - pub async fn wait_for_raf(&self) { + pub async fn wait_for_raf(&mut self) { let raf_fn = self.raf_closure.as_ref().dyn_ref::().unwrap(); let _id: i32 = self.window.request_animation_frame(raf_fn).unwrap(); - self.raf_receiver.recv().await.unwrap(); + self.raf_receiver.next().await.unwrap(); } } From 06ea624eecc132dc2a27fa8f6b2e2605126caeb4 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 15 Feb 2022 10:27:11 -0600 Subject: [PATCH 070/310] Make all packages which require futures-channel ask for the same version If they aren't the same then Cargo cannot resolve a working version for some reason. --- packages/core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 03b378e88..4126eb47f 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -32,7 +32,7 @@ smallvec = "1.6" slab = "0.4" -futures-channel = "0.3" +futures-channel = "0.3.21" # used for noderefs once_cell = "1.8" From a6cbe233b7f40763faa8e247b4264fba07f09c08 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 13:38:06 -0500 Subject: [PATCH 071/310] fix: empty values on desktop --- packages/desktop/src/controller.rs | 2 -- packages/desktop/src/events.rs | 3 +-- packages/html/src/events.rs | 2 ++ 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 7d9a2f432..8497a42e9 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -99,8 +99,6 @@ impl DesktopController { view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) .unwrap(); } - } else { - println!("waiting for ready"); } } } diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index a991ec788..7be4273e9 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -23,8 +23,7 @@ impl IpcMessage { } pub(crate) fn parse_ipc_message(payload: &str) -> Option { - let mm = serde_json::from_str(payload); - match mm { + match serde_json::from_str(payload) { Ok(message) => Some(message), Err(e) => { log::error!("could not parse IPC message, error: {e}"); diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 8067e6314..4336d2476 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -486,6 +486,8 @@ pub mod on { #[derive(Debug)] pub struct FormData { pub value: String, + + #[serde(default)] pub values: HashMap, /* DOMEvent: Send + SyncTarget relatedTarget */ } From ba17b57cdd244946ca780e3183cd7c9a43648380 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 13:44:05 -0500 Subject: [PATCH 072/310] fix: also include values in onchange --- packages/interpreter/src/interpreter.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index b1271b303..76da73752 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -2,7 +2,7 @@ export function main() { let root = window.document.getElementById("main"); if (root != null) { window.interpreter = new Interpreter(root); - window.ipc.postMessage(serializeIpcMessage("initialize")) + window.ipc.postMessage(serializeIpcMessage("initialize")); } } export class Interpreter { @@ -207,7 +207,9 @@ export class Interpreter { event.preventDefault(); const href = target.getAttribute("href"); if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage(serializeIpcMessage("browser_open", { href })) + window.ipc.postMessage( + serializeIpcMessage("browser_open", { href }) + ); } } } @@ -261,12 +263,13 @@ export class Interpreter { if (realId == null) { return; } - window.ipc.postMessage(serializeIpcMessage( - "user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - })); + window.ipc.postMessage( + serializeIpcMessage("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }) + ); } }; this.NewEventListener(edit.event_name, edit.root, handler); @@ -342,6 +345,7 @@ export function serialize_event(event) { } return { value: value, + values: {}, }; } case "input": From 61f9b9dd83a3151b3ee4054792e7b7bc5eb2af95 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 15 Feb 2022 13:46:14 -0500 Subject: [PATCH 073/310] fix: remove serde attr --- packages/html/src/events.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 4336d2476..8067e6314 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -486,8 +486,6 @@ pub mod on { #[derive(Debug)] pub struct FormData { pub value: String, - - #[serde(default)] pub values: HashMap, /* DOMEvent: Send + SyncTarget relatedTarget */ } From ee2e986a307b385e88ad82ca604dd2d34e06ec29 Mon Sep 17 00:00:00 2001 From: Denis Richartz Date: Wed, 16 Feb 2022 15:34:43 +0100 Subject: [PATCH 074/310] fix unnecessary div --- packages/router/src/components/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index bf23645f5..df1daab78 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -33,6 +33,6 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { } cx.render(rsx!( - div { &cx.props.children } + &cx.props.children )) } From b71cf6ed4ad4367da264827b55fbec537f631d41 Mon Sep 17 00:00:00 2001 From: Denis Richartz Date: Wed, 16 Feb 2022 16:11:57 +0100 Subject: [PATCH 075/310] cargo fmt --- packages/router/src/components/router.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index df1daab78..c117c6650 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -32,7 +32,5 @@ pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { cx.props.onchange.call(path.to_string()); } - cx.render(rsx!( - &cx.props.children - )) + cx.render(rsx!(&cx.props.children)) } From aa60971c5c9209d42b3f39b636e757c910a6f61e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:03:53 -0500 Subject: [PATCH 076/310] fix: remove preventdefault on form --- packages/html/src/elements.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 3d160211e..c6b1cb7e0 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1101,12 +1101,6 @@ impl label { } } -impl form { - pub fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> { - cx.attr("dioxus-prevent-default", val, None, false) - } -} - builder_constructors! { // SVG components /// Build a From c6bdb5db76629e7fae77c110a21509d07ece2e89 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:08:57 -0500 Subject: [PATCH 077/310] fix: login form --- examples/login_form.rs | 68 +++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 44 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index fdd1843ce..62c6f982b 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -3,14 +3,14 @@ use dioxus::events::*; use dioxus::prelude::*; -use dioxus::router::{Link, Router, Route, RouterService}; +use dioxus::router::{Link, Route, Router, RouterService}; fn main() { dioxus::desktop::launch(APP); } static APP: Component = |cx| { - cx.render(rsx!{ + cx.render(rsx! { Router { Route { to: "/", home() } Route { to: "/login", login() } @@ -25,65 +25,45 @@ fn home(cx: Scope) -> Element { }) } - fn login(cx: Scope) -> Element { - let username = use_state(&cx, String::new); - let password = use_state(&cx, String::new); - let service = cx.consume_context::()?; - let onsubmit = move |_| { - cx.push_future({ - let (username, password) = (username.get().clone(), password.get().clone()); - let service = service.clone(); + let onsubmit = move |evt: FormEvent| { + to_owned![service]; + let username = evt.values["username"].clone(); + let password = evt.values["password"].clone(); - async move { - let params = [ - ("username", username.to_string()), - ("password", password.to_string()) - ]; + cx.spawn(async move { + let resp = reqwest::Client::new() + .post("http://localhost/login") + .form(&[("username", username), ("password", password)]) + .send() + .await; - let resp = reqwest::Client::new() - .post("http://localhost/login") - .form(¶ms) - .send() - .await; + match resp { + // Parse data from here, such as storing a response token + Ok(data) => service.push_route("/"), - match resp { - Ok(data) => { - // Parse data from here, such as storing a response token - service.push_route("/"); - } - Err(err) => {} //Handle any errors from the fetch here - } + //Handle any errors from the fetch here + Err(err) => {} } }); }; - cx.render(rsx!{ + cx.render(rsx! { h1 { "Login" } form { onsubmit: onsubmit, - // Prevent the default behavior of to post - prevent_default: "onsubmit", - input { - oninput: move |evt| username.set(evt.value.clone()) - } - label { - "Username" - } + prevent_default: "onsubmit", // Prevent the default behavior of to post + input { r#type: "text" } + label { "Username" } br {} - input { - oninput: move |evt| password.set(evt.value.clone()), - r#type: "password" - } + input { r#type: "password" } label { "Password" } br {} - button { - "Login" - } + button { "Login" } } }) -} \ No newline at end of file +} From babe8627394c47491348b7e3cec54ce02a31e3b2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:11:31 -0500 Subject: [PATCH 078/310] fix: login example to use proper methods --- examples/login_form.rs | 23 +++++++++++------------ packages/router/src/hooks/use_route.rs | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index 62c6f982b..cdf05d7c3 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -3,20 +3,20 @@ use dioxus::events::*; use dioxus::prelude::*; -use dioxus::router::{Link, Route, Router, RouterService}; +use dioxus::router::{use_router, Link, Route, Router}; fn main() { - dioxus::desktop::launch(APP); + dioxus::desktop::launch(app); } -static APP: Component = |cx| { +fn app(cx: Scope) -> Element { cx.render(rsx! { Router { Route { to: "/", home() } Route { to: "/login", login() } } }) -}; +} fn home(cx: Scope) -> Element { cx.render(rsx! { @@ -26,7 +26,7 @@ fn home(cx: Scope) -> Element { } fn login(cx: Scope) -> Element { - let service = cx.consume_context::()?; + let service = use_router(&cx); let onsubmit = move |evt: FormEvent| { to_owned![service]; @@ -42,10 +42,10 @@ fn login(cx: Scope) -> Element { match resp { // Parse data from here, such as storing a response token - Ok(data) => service.push_route("/"), + Ok(_data) => service.push_route("/"), //Handle any errors from the fetch here - Err(err) => {} + Err(_err) => {} } }); }; @@ -55,13 +55,12 @@ fn login(cx: Scope) -> Element { form { onsubmit: onsubmit, prevent_default: "onsubmit", // Prevent the default behavior of to post - input { r#type: "text" } + + input { "type": "text" } label { "Username" } br {} - input { r#type: "password" } - label { - "Password" - } + input { "type": "password" } + label { "Password" } br {} button { "Login" } } diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 31a157a8f..4de6dff12 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -106,7 +106,7 @@ impl Drop for UseRouteListener { } /// This hook provides access to the `RouterService` for the app. -pub fn use_router(cx: &ScopeState) -> &RouterService { +pub fn use_router(cx: &ScopeState) -> &Rc { cx.use_hook(|_| { cx.consume_context::() .expect("Cannot call use_route outside the scope of a Router component") From 4d965c8571774c59c5c6d6a3223a747301da4949 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:16:03 -0500 Subject: [PATCH 079/310] feat: simplify login example --- examples/login_form.rs | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index cdf05d7c3..1613cdc63 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -10,39 +10,22 @@ fn main() { } fn app(cx: Scope) -> Element { - cx.render(rsx! { - Router { - Route { to: "/", home() } - Route { to: "/login", login() } - } - }) -} - -fn home(cx: Scope) -> Element { - cx.render(rsx! { - h1 { "Welcome Home" } - Link { to: "/login", "Login" } - }) -} - -fn login(cx: Scope) -> Element { - let service = use_router(&cx); - let onsubmit = move |evt: FormEvent| { - to_owned![service]; - let username = evt.values["username"].clone(); - let password = evt.values["password"].clone(); - cx.spawn(async move { let resp = reqwest::Client::new() .post("http://localhost/login") - .form(&[("username", username), ("password", password)]) + .form(&[ + ("username", &evt.values["username"]), + ("password", &evt.values["password"]), + ]) .send() .await; match resp { // Parse data from here, such as storing a response token - Ok(_data) => service.push_route("/"), + Ok(_data) => { + println!("Login successful"); + } //Handle any errors from the fetch here Err(_err) => {} From b57987cfb1964d761eeb85f4019cabfcd7625f2c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:17:47 -0500 Subject: [PATCH 080/310] chore: clean up example a bit --- examples/login_form.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/login_form.rs b/examples/login_form.rs index 1613cdc63..7fd91262d 100644 --- a/examples/login_form.rs +++ b/examples/login_form.rs @@ -3,7 +3,6 @@ use dioxus::events::*; use dioxus::prelude::*; -use dioxus::router::{use_router, Link, Route, Router}; fn main() { dioxus::desktop::launch(app); @@ -23,12 +22,10 @@ fn app(cx: Scope) -> Element { match resp { // Parse data from here, such as storing a response token - Ok(_data) => { - println!("Login successful"); - } + Ok(_data) => println!("Login successful"), //Handle any errors from the fetch here - Err(_err) => {} + Err(_err) => println!("Login failed"), } }); }; From 3873cd1a60e6bc6371e73fe7c378cc2fb2c60407 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 16 Feb 2022 14:40:45 -0500 Subject: [PATCH 081/310] fix: remove unused depds --- packages/core-macro/Cargo.toml | 1 - packages/desktop/Cargo.toml | 4 +--- packages/router/Cargo.toml | 2 +- packages/web/Cargo.toml | 5 ++--- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index eb8210923..8026bcdaf 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,7 +15,6 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] -once_cell = "1.8" proc-macro-error = "1.0.4" proc-macro2 = { version = "1.0.6" } quote = "1.0" diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 2622355eb..6fd277ba4 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -13,12 +13,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { path = "../core", version = "^0.1.9", features = ["serialize"] } -argh = "0.1.7" serde = "1.0.136" serde_json = "1.0.79" thiserror = "1.0.30" log = "0.4.14" -html-escape = "0.2.9" wry = { version = "0.13.1" } futures-channel = "0.3.21" tokio = { version = "1.16.1", features = [ @@ -27,7 +25,6 @@ tokio = { version = "1.16.1", features = [ "rt", "time", ], optional = true, default-features = false } -dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.6" } webbrowser = "0.5.5" mime_guess = "2.0.3" @@ -47,5 +44,6 @@ ayatana = ["wry/ayatana"] [dev-dependencies] +dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } dioxus-hooks = { path = "../hooks" } # image = "0.24.0" # enable this when generating a new desktop image diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 239a3c5d8..221372b50 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -16,8 +16,8 @@ dioxus-html = { path = "../html", version = "^0.1.6", default-features = false } dioxus-core-macro = { path = "../core-macro", version = "^0.1.7" } serde = "1" -url = "2.2.2" serde_urlencoded = "0.7" +# url = "2.2.2" # for wasm web-sys = { version = "0.3", features = [ diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 31a7ff6be..333838398 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -15,13 +15,10 @@ dioxus-core = { path = "../core", version = "^0.1.9" } dioxus-html = { path = "../html", version = "^0.1.6" } js-sys = "0.3.56" wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] } -lazy_static = "1.4.0" wasm-bindgen-futures = "0.4.29" log = { version = "0.4.14", features = ["release_max_level_off"] } fxhash = "0.2.1" -wasm-logger = "0.2.0" console_error_panic_hook = { version = "0.1.7", optional = true } -wasm-bindgen-test = "0.3.29" once_cell = "1.9.0" anyhow = "1.0.53" gloo-timers = { version = "0.2.3", features = ["futures"] } @@ -80,3 +77,5 @@ panic_hook = ["console_error_panic_hook"] dioxus-core-macro = { path = "../core-macro" } wasm-bindgen-test = "0.3.29" dioxus-ssr = { path = "../ssr" } +wasm-logger = "0.2.0" + From 96bfa4a90e81fa0ce669e6012f5a68afbb6d966d Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Thu, 17 Feb 2022 19:40:20 +0800 Subject: [PATCH 082/310] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23cd9c516..18109018a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ |
Guide (0.1.8) | - Guide (Master) + Guide (Nightly) From d461ffc01131e76e8dc8044f3476db056ee127e7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 10:38:51 -0500 Subject: [PATCH 083/310] feat: integrate fermi --- Cargo.toml | 18 +--- docs/fermi/.gitignore | 1 + docs/fermi/book.toml | 6 ++ docs/fermi/src/SUMMARY.md | 3 + docs/fermi/src/chapter_1.md | 1 + examples/fermi.rs | 30 ++++++ packages/fermi/Cargo.toml | 14 +++ packages/fermi/README.md | 92 ++++++++++++++++++ packages/fermi/src/atoms/atom.rs | 28 ++++++ packages/fermi/src/atoms/atomfamily.rs | 25 +++++ packages/fermi/src/atoms/atomref.rs | 25 +++++ packages/fermi/src/atoms/selector.rs | 0 packages/fermi/src/atoms/selectorfamily.rs | 0 packages/fermi/src/callback.rs | 51 ++++++++++ packages/fermi/src/hooks/atom_ref.rs | 62 +++++++++++++ packages/fermi/src/hooks/atom_root.rs | 11 +++ packages/fermi/src/hooks/init_atom_root.rs | 11 +++ packages/fermi/src/hooks/read.rs | 36 +++++++ packages/fermi/src/hooks/set.rs | 13 +++ packages/fermi/src/lib.rs | 59 ++++++++++++ packages/fermi/src/root.rs | 103 +++++++++++++++++++++ src/lib.rs | 3 + tests/fermi.rs | 53 +++++++++++ 23 files changed, 630 insertions(+), 15 deletions(-) create mode 100644 docs/fermi/.gitignore create mode 100644 docs/fermi/book.toml create mode 100644 docs/fermi/src/SUMMARY.md create mode 100644 docs/fermi/src/chapter_1.md create mode 100644 examples/fermi.rs create mode 100644 packages/fermi/Cargo.toml create mode 100644 packages/fermi/README.md create mode 100644 packages/fermi/src/atoms/atom.rs create mode 100644 packages/fermi/src/atoms/atomfamily.rs create mode 100644 packages/fermi/src/atoms/atomref.rs create mode 100644 packages/fermi/src/atoms/selector.rs create mode 100644 packages/fermi/src/atoms/selectorfamily.rs create mode 100644 packages/fermi/src/callback.rs create mode 100644 packages/fermi/src/hooks/atom_ref.rs create mode 100644 packages/fermi/src/hooks/atom_root.rs create mode 100644 packages/fermi/src/hooks/init_atom_root.rs create mode 100644 packages/fermi/src/hooks/read.rs create mode 100644 packages/fermi/src/hooks/set.rs create mode 100644 packages/fermi/src/lib.rs create mode 100644 packages/fermi/src/root.rs create mode 100644 tests/fermi.rs diff --git a/Cargo.toml b/Cargo.toml index 3f0d50187..bd15590ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ dioxus-core = { path = "./packages/core", version = "^0.1.9" } dioxus-html = { path = "./packages/html", version = "^0.1.6", optional = true } dioxus-core-macro = { path = "./packages/core-macro", version = "^0.1.7", optional = true } dioxus-hooks = { path = "./packages/hooks", version = "^0.1.7", optional = true } +fermi = { path = "./packages/fermi", version = "^0.1.0", optional = true } dioxus-web = { path = "./packages/web", version = "^0.0.5", optional = true } dioxus-desktop = { path = "./packages/desktop", version = "^0.1.6", optional = true } @@ -36,20 +37,6 @@ web = ["dioxus-web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] -devtool = ["dioxus-desktop/devtool"] -fullscreen = ["dioxus-desktop/fullscreen"] -transparent = ["dioxus-desktop/transparent"] - -tray = ["dioxus-desktop/tray"] -ayatana = ["dioxus-desktop/ayatana"] - -# "dioxus-router/web" -# "dioxus-router/desktop" -# desktop = ["dioxus-desktop", "dioxus-router/desktop"] -# mobile = ["dioxus-mobile"] -# liveview = ["dioxus-liveview"] - - [workspace] members = [ "packages/core", @@ -61,6 +48,7 @@ members = [ "packages/desktop", "packages/mobile", "packages/interpreter", + "packages/fermi", ] [dev-dependencies] @@ -75,4 +63,4 @@ serde_json = "1.0.79" rand = { version = "0.8.4", features = ["small_rng"] } tokio = { version = "1.16.1", features = ["full"] } reqwest = { version = "0.11.9", features = ["json"] } -dioxus = { path = ".", features = ["desktop", "ssr", "router"] } +dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi"] } diff --git a/docs/fermi/.gitignore b/docs/fermi/.gitignore new file mode 100644 index 000000000..7585238ef --- /dev/null +++ b/docs/fermi/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/fermi/book.toml b/docs/fermi/book.toml new file mode 100644 index 000000000..dd6d2eacb --- /dev/null +++ b/docs/fermi/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Jonathan Kelley"] +language = "en" +multilingual = false +src = "src" +title = "Fermi Guide" diff --git a/docs/fermi/src/SUMMARY.md b/docs/fermi/src/SUMMARY.md new file mode 100644 index 000000000..7390c8289 --- /dev/null +++ b/docs/fermi/src/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/docs/fermi/src/chapter_1.md b/docs/fermi/src/chapter_1.md new file mode 100644 index 000000000..b743fda35 --- /dev/null +++ b/docs/fermi/src/chapter_1.md @@ -0,0 +1 @@ +# Chapter 1 diff --git a/examples/fermi.rs b/examples/fermi.rs new file mode 100644 index 000000000..2be01d019 --- /dev/null +++ b/examples/fermi.rs @@ -0,0 +1,30 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use fermi::prelude::*; + +fn main() { + dioxus::desktop::launch(app) +} + +static NAME: Atom = |_| "world".to_string(); + +fn app(cx: Scope) -> Element { + let name = use_read(&cx, NAME); + + cx.render(rsx! { + div { "hello {name}!" } + Child {} + }) +} + +fn Child(cx: Scope) -> Element { + let set_name = use_set(&cx, NAME); + + cx.render(rsx! { + button { + onclick: move |_| set_name("dioxus".to_string()), + "reset name" + } + }) +} diff --git a/packages/fermi/Cargo.toml b/packages/fermi/Cargo.toml new file mode 100644 index 000000000..48fe377c7 --- /dev/null +++ b/packages/fermi/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fermi" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus-core = { path = "../core" } +im-rc = { version = "15.0.0", features = ["serde"] } +log = "0.4.14" + +[dev-dependencies] +closure = "0.3.0" diff --git a/packages/fermi/README.md b/packages/fermi/README.md new file mode 100644 index 000000000..0923aa6f6 --- /dev/null +++ b/packages/fermi/README.md @@ -0,0 +1,92 @@ + +

+

Fermi ⚛

+

+ Atom-based global state management solution for Dioxus +

+
+ + +
+ + + Crates.io version + + + + Download + + + + docs.rs docs + + + + CI status + +
+ +----- + +Fermi is a global state management solution for Dioxus that's as easy as `use_state`. + +Inspired by atom-based state management solutions, all state in Fermi starts as an `atom`: + +```rust +static NAME: Atom<&str> = |_| "Dioxus"; +``` + +From anywhere in our app, we can read our the value of our atom: + +```rust +fn NameCard(cx: Scope) -> Element { + let name = use_read(&cx, NAME); + cx.render(rsx!{ h1 { "Hello, {name}"} }) +} +``` + +We can also set the value of our atom, also from anywhere in our app: + +```rust +fn NameCard(cx: Scope) -> Element { + let set_name = use_set(&cx, NAME); + cx.render(rsx!{ + button { + onclick: move |_| set_name("Fermi"), + "Set name to fermi" + } + }) +} +``` + +It's that simple! + +## Installation +Fermi is currently under construction, so you have to use the `master` branch to get started. + +```rust +[depdencies] +fermi = { git = "https://github.com/dioxuslabs/fermi" } +``` + + +## Running examples + +The examples here use Dioxus Desktop to showcase their functionality. To run an example, use +``` +$ cargo run --example EXAMPLE +``` + +## Features + +Broadly our feature set to required to be released includes: +- [x] Support for Atoms +- [x] Support for AtomRef (for values that aren't clone) +- [ ] Support for Atom Families +- [ ] Support for memoized Selectors +- [ ] Support for memoized SelectorFamilies +- [ ] Support for UseFermiCallback for access to fermi from async diff --git a/packages/fermi/src/atoms/atom.rs b/packages/fermi/src/atoms/atom.rs new file mode 100644 index 000000000..2b99174b5 --- /dev/null +++ b/packages/fermi/src/atoms/atom.rs @@ -0,0 +1,28 @@ +use crate::{AtomId, AtomRoot, Readable, Writable}; + +pub type Atom = fn(AtomBuilder) -> T; +pub struct AtomBuilder; + +impl Readable for Atom { + fn read(&self, _root: AtomRoot) -> Option { + todo!() + } + fn init(&self) -> V { + (*self)(AtomBuilder) + } + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +impl Writable for Atom { + fn write(&self, _root: AtomRoot, _value: V) { + todo!() + } +} + +#[test] +fn atom_compiles() { + static TEST_ATOM: Atom<&str> = |_| "hello"; + dbg!(TEST_ATOM.init()); +} diff --git a/packages/fermi/src/atoms/atomfamily.rs b/packages/fermi/src/atoms/atomfamily.rs new file mode 100644 index 000000000..60fc22bcb --- /dev/null +++ b/packages/fermi/src/atoms/atomfamily.rs @@ -0,0 +1,25 @@ +use crate::{AtomId, AtomRoot, Readable, Writable}; +use im_rc::HashMap as ImMap; + +pub struct AtomFamilyBuilder; +pub type AtomFamily = fn(AtomFamilyBuilder) -> ImMap; + +impl Readable> for AtomFamily { + fn read(&self, _root: AtomRoot) -> Option> { + todo!() + } + + fn init(&self) -> ImMap { + (*self)(AtomFamilyBuilder) + } + + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +impl Writable> for AtomFamily { + fn write(&self, _root: AtomRoot, _value: ImMap) { + todo!() + } +} diff --git a/packages/fermi/src/atoms/atomref.rs b/packages/fermi/src/atoms/atomref.rs new file mode 100644 index 000000000..59e412031 --- /dev/null +++ b/packages/fermi/src/atoms/atomref.rs @@ -0,0 +1,25 @@ +use crate::{AtomId, AtomRoot, Readable}; +use std::cell::RefCell; + +pub struct AtomRefBuilder; +pub type AtomRef = fn(AtomRefBuilder) -> T; + +impl Readable> for AtomRef { + fn read(&self, _root: AtomRoot) -> Option> { + todo!() + } + + fn init(&self) -> RefCell { + RefCell::new((*self)(AtomRefBuilder)) + } + + fn unique_id(&self) -> AtomId { + *self as *const () + } +} + +#[test] +fn atom_compiles() { + static TEST_ATOM: AtomRef> = |_| vec![]; + dbg!(TEST_ATOM.init()); +} diff --git a/packages/fermi/src/atoms/selector.rs b/packages/fermi/src/atoms/selector.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/fermi/src/atoms/selectorfamily.rs b/packages/fermi/src/atoms/selectorfamily.rs new file mode 100644 index 000000000..e69de29bb diff --git a/packages/fermi/src/callback.rs b/packages/fermi/src/callback.rs new file mode 100644 index 000000000..284fb6439 --- /dev/null +++ b/packages/fermi/src/callback.rs @@ -0,0 +1,51 @@ +use std::{future::Future, rc::Rc}; + +use dioxus_core::prelude::*; + +use crate::{Atom, AtomRoot, Readable, Writable}; + +#[derive(Clone)] +pub struct CallbackApi { + root: Rc, +} + +impl CallbackApi { + // get the current value of the atom + pub fn get(&self, atom: impl Readable) -> &V { + todo!() + } + + // get the current value of the atom in its RC container + pub fn get_rc(&self, atom: impl Readable) -> &Rc { + todo!() + } + + // set the current value of the atom + pub fn set(&self, atom: impl Writable, value: V) { + todo!() + } +} + +pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi { + todo!() +} + +macro_rules! use_callback { + (&$cx:ident, [$($cap:ident),*], move || $body:expr) => { + move || { + $( + #[allow(unused_mut)] + let mut $cap = $cap.to_owned(); + )* + $cx.spawn($body); + } + }; +} + +#[macro_export] +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} diff --git a/packages/fermi/src/hooks/atom_ref.rs b/packages/fermi/src/hooks/atom_ref.rs new file mode 100644 index 000000000..da9ad9367 --- /dev/null +++ b/packages/fermi/src/hooks/atom_ref.rs @@ -0,0 +1,62 @@ +use crate::{use_atom_root, AtomId, AtomRef, AtomRoot, Readable}; +use dioxus_core::{ScopeId, ScopeState}; +use std::{ + cell::{Ref, RefCell, RefMut}, + rc::Rc, +}; + +/// +/// +/// +/// +/// +/// +/// +/// +pub fn use_atom_ref(cx: &ScopeState, atom: AtomRef) -> &UseAtomRef { + let root = use_atom_root(cx); + + cx.use_hook(|_| { + root.initialize(atom); + UseAtomRef { + ptr: atom.unique_id(), + root: root.clone(), + scope_id: cx.scope_id(), + value: root.register(atom, cx.scope_id()), + } + }) +} + +pub struct UseAtomRef { + ptr: AtomId, + value: Rc>, + root: Rc, + scope_id: ScopeId, +} + +impl UseAtomRef { + pub fn read(&self) -> Ref { + self.value.borrow() + } + + pub fn write(&self) -> RefMut { + self.root.force_update(self.ptr); + self.value.borrow_mut() + } + + pub fn write_silent(&self) -> RefMut { + self.root.force_update(self.ptr); + self.value.borrow_mut() + } + + pub fn set(&self, new: T) { + self.root.force_update(self.ptr); + self.root.set(self.ptr, new); + } +} + +impl Drop for UseAtomRef { + fn drop(&mut self) { + self.root.unsubscribe(self.ptr, self.scope_id) + } +} diff --git a/packages/fermi/src/hooks/atom_root.rs b/packages/fermi/src/hooks/atom_root.rs new file mode 100644 index 000000000..d2945025b --- /dev/null +++ b/packages/fermi/src/hooks/atom_root.rs @@ -0,0 +1,11 @@ +use crate::AtomRoot; +use dioxus_core::ScopeState; +use std::rc::Rc; + +// Returns the atom root, initiaizing it at the root of the app if it does not exist. +pub fn use_atom_root(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| match cx.consume_context::() { + Some(root) => root, + None => cx.provide_root_context(AtomRoot::new(cx.schedule_update_any())), + }) +} diff --git a/packages/fermi/src/hooks/init_atom_root.rs b/packages/fermi/src/hooks/init_atom_root.rs new file mode 100644 index 000000000..edb8aafb8 --- /dev/null +++ b/packages/fermi/src/hooks/init_atom_root.rs @@ -0,0 +1,11 @@ +use crate::AtomRoot; +use dioxus_core::ScopeState; +use std::rc::Rc; + +// Initializes the atom root and retuns it; +pub fn use_init_atom_root(cx: &ScopeState) -> &Rc { + cx.use_hook(|_| match cx.consume_context::() { + Some(ctx) => ctx, + None => cx.provide_context(AtomRoot::new(cx.schedule_update_any())), + }) +} diff --git a/packages/fermi/src/hooks/read.rs b/packages/fermi/src/hooks/read.rs new file mode 100644 index 000000000..b231c74d3 --- /dev/null +++ b/packages/fermi/src/hooks/read.rs @@ -0,0 +1,36 @@ +use crate::{use_atom_root, AtomId, AtomRoot, Readable}; +use dioxus_core::{ScopeId, ScopeState}; +use std::rc::Rc; + +pub fn use_read<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable) -> &'a V { + use_read_rc(cx, f).as_ref() +} + +pub fn use_read_rc<'a, V: 'static>(cx: &'a ScopeState, f: impl Readable) -> &'a Rc { + let root = use_atom_root(cx); + + struct UseReadInner { + root: Rc, + id: AtomId, + scope_id: ScopeId, + value: Option>, + } + + impl Drop for UseReadInner { + fn drop(&mut self) { + self.root.unsubscribe(self.id, self.scope_id) + } + } + + let inner = cx.use_hook(|_| UseReadInner { + value: None, + root: root.clone(), + scope_id: cx.scope_id(), + id: f.unique_id(), + }); + + let value = inner.root.register(f, cx.scope_id()); + + inner.value = Some(value); + inner.value.as_ref().unwrap() +} diff --git a/packages/fermi/src/hooks/set.rs b/packages/fermi/src/hooks/set.rs new file mode 100644 index 000000000..6d77fbf8c --- /dev/null +++ b/packages/fermi/src/hooks/set.rs @@ -0,0 +1,13 @@ +use crate::{use_atom_root, Writable}; +use dioxus_core::ScopeState; +use std::rc::Rc; + +pub fn use_set<'a, T: 'static>(cx: &'a ScopeState, f: impl Writable) -> &'a Rc { + let root = use_atom_root(cx); + cx.use_hook(|_| { + let id = f.unique_id(); + let root = root.clone(); + root.initialize(f); + Rc::new(move |new| root.set(id, new)) as Rc + }) +} diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs new file mode 100644 index 000000000..023b5d286 --- /dev/null +++ b/packages/fermi/src/lib.rs @@ -0,0 +1,59 @@ +#![doc = include_str!("../README.md")] + +pub mod prelude { + pub use crate::*; +} + +mod root; +mod callback; + + +pub use atoms::*; +pub use hooks::*; +pub use root::*; +pub use callback::*; + +mod atoms { + mod atom; + mod atomfamily; + mod atomref; + mod selector; + mod selectorfamily; + + pub use atom::*; + pub use atomfamily::*; + pub use atomref::*; + pub use selector::*; + pub use selectorfamily::*; +} + +pub mod hooks { + mod atom_ref; + mod atom_root; + mod init_atom_root; + mod read; + mod set; + pub use atom_ref::*; + pub use atom_root::*; + pub use init_atom_root::*; + pub use read::*; + pub use set::*; +} + +/// All Atoms are `Readable` - they support reading their value. +/// +/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors. +/// It is not very useful for your own code, but could be used to build new Atom primitives. +pub trait Readable { + fn read(&self, root: AtomRoot) -> Option; + fn init(&self) -> V; + fn unique_id(&self) -> AtomId; +} + +/// All Atoms are `Writable` - they support writing their value. +/// +/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors. +/// This trait lets Dioxus abstract over Atoms, AtomFamilies, AtomRefs, and Selectors +pub trait Writable: Readable { + fn write(&self, root: AtomRoot, value: V); +} diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs new file mode 100644 index 000000000..5c07083be --- /dev/null +++ b/packages/fermi/src/root.rs @@ -0,0 +1,103 @@ +use std::{any::Any, cell::RefCell, collections::HashMap, rc::Rc}; + +use dioxus_core::ScopeId; +use im_rc::HashSet; + +use crate::Readable; + +pub type AtomId = *const (); + +pub struct AtomRoot { + pub atoms: RefCell>, + pub update_any: Rc, +} + +pub struct Slot { + pub value: Rc, + pub subscribers: HashSet, +} + +impl AtomRoot { + pub fn new(update_any: Rc) -> Self { + Self { + update_any, + atoms: RefCell::new(HashMap::new()), + } + } + + pub fn initialize(&self, f: impl Readable) { + let id = f.unique_id(); + if self.atoms.borrow().get(&id).is_none() { + self.atoms.borrow_mut().insert( + id, + Slot { + value: Rc::new(f.init()), + subscribers: HashSet::new(), + }, + ); + } + } + + pub fn register(&self, f: impl Readable, scope: ScopeId) -> Rc { + log::trace!("registering atom {:?}", f.unique_id()); + + let mut atoms = self.atoms.borrow_mut(); + + // initialize the value if it's not already initialized + if let Some(slot) = atoms.get_mut(&f.unique_id()) { + slot.subscribers.insert(scope); + slot.value.clone().downcast().unwrap() + } else { + let value = Rc::new(f.init()); + let mut subscribers = HashSet::new(); + subscribers.insert(scope); + + atoms.insert( + f.unique_id(), + Slot { + value: value.clone(), + subscribers, + }, + ); + value + } + } + + pub fn set(&self, ptr: AtomId, value: V) { + let mut atoms = self.atoms.borrow_mut(); + + if let Some(slot) = atoms.get_mut(&ptr) { + slot.value = Rc::new(value); + log::trace!("found item with subscribers {:?}", slot.subscribers); + + for scope in &slot.subscribers { + log::trace!("updating subcsriber"); + (self.update_any)(*scope); + } + } else { + log::trace!("no atoms found for {:?}", ptr); + } + } + + pub fn unsubscribe(&self, ptr: AtomId, scope: ScopeId) { + let mut atoms = self.atoms.borrow_mut(); + + if let Some(slot) = atoms.get_mut(&ptr) { + slot.subscribers.remove(&scope); + } + } + + // force update of all subscribers + pub fn force_update(&self, ptr: AtomId) { + if let Some(slot) = self.atoms.borrow_mut().get(&ptr) { + for scope in slot.subscribers.iter() { + log::trace!("updating subcsriber"); + (self.update_any)(*scope); + } + } + } + + pub fn read(&self, _f: impl Readable) -> &V { + todo!() + } +} diff --git a/src/lib.rs b/src/lib.rs index d7b479a07..2db210bee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -340,6 +340,9 @@ pub use dioxus_web as web; #[cfg(feature = "desktop")] pub use dioxus_desktop as desktop; +#[cfg(feature = "fermi")] +pub use fermi; + // #[cfg(feature = "mobile")] // pub use dioxus_mobile as mobile; diff --git a/tests/fermi.rs b/tests/fermi.rs new file mode 100644 index 000000000..efef5c641 --- /dev/null +++ b/tests/fermi.rs @@ -0,0 +1,53 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use fermi::*; + +#[test] +fn test_fermi() { + let mut app = VirtualDom::new(App); + app.rebuild(); +} + +static TITLE: Atom = |_| "".to_string(); +static USERS: AtomFamily = |_| Default::default(); + +fn App(cx: Scope) -> Element { + cx.render(rsx!( + Leaf { id: 0 } + Leaf { id: 1 } + Leaf { id: 2 } + )) +} + +#[derive(Debug, PartialEq, Props)] +struct LeafProps { + id: u32, +} + +fn Leaf(cx: Scope) -> Element { + let user = use_read(&cx, TITLE); + let user = use_read(&cx, USERS); + + use_coroutine(&cx, || { + // + async move { + // + } + }); + + rsx!(cx, div { + button { + onclick: move |_| {}, + "Start" + } + button { + onclick: move |_| {}, + "Stop" + } + button { + onclick: move |_| {}, + "Reverse" + } + }) +} From d095d8c407ed8bf3b57a4944922b61918dd17995 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 10:43:34 -0500 Subject: [PATCH 084/310] fix: clippy is happy --- packages/fermi/src/callback.rs | 6 ++++-- tests/fermi.rs | 11 ++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/fermi/src/callback.rs b/packages/fermi/src/callback.rs index 284fb6439..a7ca9155f 100644 --- a/packages/fermi/src/callback.rs +++ b/packages/fermi/src/callback.rs @@ -1,8 +1,10 @@ -use std::{future::Future, rc::Rc}; +#![allow(clippy::all, unused)] + +use std::rc::Rc; use dioxus_core::prelude::*; -use crate::{Atom, AtomRoot, Readable, Writable}; +use crate::{AtomRoot, Readable, Writable}; #[derive(Clone)] pub struct CallbackApi { diff --git a/tests/fermi.rs b/tests/fermi.rs index efef5c641..1149539f0 100644 --- a/tests/fermi.rs +++ b/tests/fermi.rs @@ -26,15 +26,8 @@ struct LeafProps { } fn Leaf(cx: Scope) -> Element { - let user = use_read(&cx, TITLE); - let user = use_read(&cx, USERS); - - use_coroutine(&cx, || { - // - async move { - // - } - }); + let _user = use_read(&cx, TITLE); + let _user = use_read(&cx, USERS); rsx!(cx, div { button { From a4df07f338cc08a3b71cd5730abf4a31e7d9766a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:16:04 -0500 Subject: [PATCH 085/310] fix: rustmft --- packages/fermi/src/atoms/selector.rs | 1 + packages/fermi/src/atoms/selectorfamily.rs | 1 + packages/fermi/src/lib.rs | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/fermi/src/atoms/selector.rs b/packages/fermi/src/atoms/selector.rs index e69de29bb..8b1378917 100644 --- a/packages/fermi/src/atoms/selector.rs +++ b/packages/fermi/src/atoms/selector.rs @@ -0,0 +1 @@ + diff --git a/packages/fermi/src/atoms/selectorfamily.rs b/packages/fermi/src/atoms/selectorfamily.rs index e69de29bb..8b1378917 100644 --- a/packages/fermi/src/atoms/selectorfamily.rs +++ b/packages/fermi/src/atoms/selectorfamily.rs @@ -0,0 +1 @@ + diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs index 023b5d286..8ac4f1ccf 100644 --- a/packages/fermi/src/lib.rs +++ b/packages/fermi/src/lib.rs @@ -4,14 +4,13 @@ pub mod prelude { pub use crate::*; } -mod root; mod callback; - +mod root; pub use atoms::*; +pub use callback::*; pub use hooks::*; pub use root::*; -pub use callback::*; mod atoms { mod atom; From f4686150d7e2d62b6607a6a761c72cfe1b451479 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:19:28 -0500 Subject: [PATCH 086/310] ci: make workflows only run on PRs --- .github/workflows/macos.yml | 9 ++++----- .github/workflows/main.yml | 9 ++++----- .github/workflows/windows.yml | 9 ++++----- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index d1351712a..718922a57 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,7 +1,10 @@ name: macOS tests on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master paths: - packages/** - examples/** @@ -9,10 +12,6 @@ on: - .github/** - lib.rs - Cargo.toml - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: - - master jobs: test: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e1ec15c0..9f8e0bac3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,10 @@ name: Rust CI on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master paths: - packages/** - examples/** @@ -9,10 +12,6 @@ on: - .github/** - lib.rs - Cargo.toml - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: - - master jobs: check: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 518da37b3..75953807a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,7 +1,10 @@ name: windows on: - push: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: + - master paths: - packages/** - examples/** @@ -9,10 +12,6 @@ on: - .github/** - lib.rs - Cargo.toml - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: - - master jobs: test: From 6bc9dd36f9ae1d698282147f1a6fe08a46f8f6e5 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:27:13 -0500 Subject: [PATCH 087/310] fix: coverage --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9f8e0bac3..8521dd176 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -101,8 +101,9 @@ jobs: - name: Generate code coverage run: | apt-get update &&\ + apt-get install build-essential &&\ apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --verbose --tests --all-features --workspace --timeout 120 --out Xml + cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v2 with: From 7aeed4ccae47d0e28c667dde9779de8e4babdf47 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 17 Feb 2022 11:30:25 -0500 Subject: [PATCH 088/310] ci: disable coverage --- .github/workflows/main.yml | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8521dd176..128dc5179 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,22 +89,23 @@ jobs: command: clippy args: -- -D warnings - coverage: - name: Coverage - runs-on: ubuntu-latest - container: - image: xd009642/tarpaulin:develop-nightly - options: --security-opt seccomp=unconfined - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Generate code coverage - run: | - apt-get update &&\ - apt-get install build-essential &&\ - apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ - cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml - - name: Upload to codecov.io - uses: codecov/codecov-action@v2 - with: - fail_ci_if_error: false + # Coverage is disabled until we can fix it + # coverage: + # name: Coverage + # runs-on: ubuntu-latest + # container: + # image: xd009642/tarpaulin:develop-nightly + # options: --security-opt seccomp=unconfined + # steps: + # - name: Checkout repository + # uses: actions/checkout@v2 + # - name: Generate code coverage + # run: | + # apt-get update &&\ + # apt-get install build-essential &&\ + # apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev -y &&\ + # cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml + # - name: Upload to codecov.io + # uses: codecov/codecov-action@v2 + # with: + # fail_ci_if_error: false From 7a1d8f05320dbcaef410e7df99634b7b0d228107 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 17 Feb 2022 16:06:28 -0600 Subject: [PATCH 089/310] 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".into()) + }); + + dioxus::desktop::launch_cfg(app, |c| { + c.with_custom_index( + r#" + + + + Dioxus app + + + + +
+ + + "# + .into(), + ) + }); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 {"hello world!"} + } + }) +} diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 6028706bc..cc154942b 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -21,6 +21,8 @@ pub struct DesktopConfig { pub(crate) event_handler: Option>, pub(crate) disable_context_menu: bool, pub(crate) resource_dir: Option, + pub(crate) custom_head: Option, + pub(crate) custom_index: Option, } pub(crate) type WryProtocol = ( @@ -42,6 +44,8 @@ impl DesktopConfig { pre_rendered: None, disable_context_menu: !cfg!(debug_assertions), resource_dir: None, + custom_head: None, + custom_index: None, } } @@ -100,10 +104,32 @@ impl DesktopConfig { self } + + /// Add a custom icon for this application pub fn with_icon(&mut self, icon: Icon) -> &mut Self { self.window.window.window_icon = Some(icon); self } + + + /// Inject additional content into the document's HEAD. + /// + /// This is useful for loading CSS libraries, JS libraries, etc. + pub fn with_custom_head(&mut self, head: String) -> &mut Self { + self.custom_head = Some(head); + self + } + + /// Use a custom index.html instead of the default Dioxus one. + /// + /// Make sure your index.html is valid HTML. + /// + /// Dioxus injects some loader code into the closing body tag. Your document + /// must include a body element! + pub fn with_custom_index(&mut self, index: String) -> &mut Self { + self.custom_index = Some(index); + self + } } impl DesktopConfig { diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 4ad2ff3a7..155f7af1b 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -201,7 +201,7 @@ pub(super) fn handler( /// Get a closure that executes any JavaScript in the WebView context. pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) { - let desktop = use_window(&cx).clone(); + let desktop = use_window(cx).clone(); cx.use_hook(|_| move |script| desktop.eval(script)) } diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 292a1ae6a..0704fd0f0 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -3,13 +3,10 @@ Dioxus app +
- + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 596956c53..89222f271 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -125,8 +125,9 @@ pub fn launch_with_props( let proxy = proxy.clone(); let file_handler = cfg.file_drop_handler.take(); - + let custom_head = cfg.custom_head.clone(); let resource_dir = cfg.resource_dir.clone(); + let index_file = cfg.custom_index.clone(); let mut webview = WebViewBuilder::new(window) .unwrap() @@ -164,7 +165,12 @@ pub fn launch_with_props( }); }) .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler(r, resource_dir.clone()) + protocol::desktop_handler( + r, + resource_dir.clone(), + custom_head.clone(), + index_file.clone(), + ) }) .with_file_drop_handler(move |window, evet| { file_handler diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index bd8bb3d84..085c3c32d 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -4,7 +4,20 @@ use wry::{ Result, }; -pub(super) fn desktop_handler(request: &Request, asset_root: Option) -> Result { +const MODULE_LOADER: &str = r#" + +"#; + +pub(super) fn desktop_handler( + request: &Request, + asset_root: Option, + custom_head: Option, + custom_index: Option, +) -> Result { // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case". // For now, we only serve two pieces of content which get included as bytes into the final binary. let path = request.uri().replace("dioxus://", ""); @@ -13,9 +26,25 @@ pub(super) fn desktop_handler(request: &Request, asset_root: Option) -> let trimmed = path.trim_start_matches("index.html/"); if trimmed.is_empty() { - ResponseBuilder::new() - .mimetype("text/html") - .body(include_bytes!("./index.html").to_vec()) + // If a custom index is provided, just defer to that, expecting the user to know what they're doing. + // we'll look for the closing tag and insert our little module loader there. + if let Some(custom_index) = custom_index { + let rendered = custom_index + .replace("", &format!("{}", MODULE_LOADER)) + .into_bytes(); + ResponseBuilder::new().mimetype("text/html").body(rendered) + } else { + // Otherwise, we'll serve the default index.html and apply a custom head if that's specified. + let mut template = include_str!("./index.html").to_string(); + if let Some(custom_head) = custom_head { + template = template.replace("", &custom_head); + } + template = template.replace("", MODULE_LOADER); + + ResponseBuilder::new() + .mimetype("text/html") + .body(template.into_bytes()) + } } else if trimmed == "index.js" { ResponseBuilder::new() .mimetype("text/javascript") From 29bda8aa487e9f49515ec498a23b83aa20ee2418 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 16 Apr 2022 16:59:41 -0400 Subject: [PATCH 294/310] docs: use tailwindv3 in examples --- examples/tailwind.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/tailwind.rs b/examples/tailwind.rs index 6611fc47b..e0cb2339a 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind.rs @@ -11,12 +11,13 @@ use dioxus::prelude::*; fn main() { - dioxus::desktop::launch(app); + dioxus::desktop::launch_cfg(app, |c| { + c.with_custom_head("".to_string()) + }); } pub fn app(cx: Scope) -> Element { cx.render(rsx!( - link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } div { header { class: "text-gray-400 bg-gray-900 body-font", div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", From fd2a5f5a6be10659ee49d64c656b3c529a7e1fe4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 16 Apr 2022 17:02:57 -0400 Subject: [PATCH 295/310] fmt --- packages/desktop/src/cfg.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index cc154942b..84ac3a72a 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -104,14 +104,12 @@ impl DesktopConfig { self } - /// Add a custom icon for this application pub fn with_icon(&mut self, icon: Icon) -> &mut Self { self.window.window.window_icon = Some(icon); self } - /// Inject additional content into the document's HEAD. /// /// This is useful for loading CSS libraries, JS libraries, etc. From 9c0963abe74fb1d283b781a29859e61d09922ec5 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Sun, 17 Apr 2022 11:56:30 +0800 Subject: [PATCH 296/310] Update ZH_CN.md --- notes/README/ZH_CN.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 79f2b51aa..0a4832443 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -65,12 +65,12 @@ Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平 ```rust fn app(cx: Scope) -> Element { - let (count, set_count) = use_state(&cx, || 0); + let count = use_state(&cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| set_count(count + 1), "Up high!" } - button { onclick: move |_| set_count(count - 1), "Down low!" } + button { onclick: move |_| count.set(count + 1), "Up high!" } + button { onclick: move |_| count.set(count - 1), "Down low!" } )) } ``` From 9d285d842d1411c93919f27ba68760c0886c5cad Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Sun, 17 Apr 2022 11:59:29 +0800 Subject: [PATCH 297/310] Update ZH_CN.md --- notes/README/ZH_CN.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 0a4832443..4da075c7d 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -65,13 +65,13 @@ Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平 ```rust fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let mut count = use_state(&cx, || 0); - cx.render(rsx!( + cx.render(rsx! { h1 { "High-Five counter: {count}" } - button { onclick: move |_| count.set(count + 1), "Up high!" } - button { onclick: move |_| count.set(count - 1), "Down low!" } - )) + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + }) } ``` From c9537a4c41f08c809eb000426cc9a9ed1a83e755 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Sun, 17 Apr 2022 12:27:41 +0800 Subject: [PATCH 298/310] Update localstate.md --- docs/guide/src/state/localstate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/state/localstate.md b/docs/guide/src/state/localstate.md index 72dcdb619..788399cb5 100644 --- a/docs/guide/src/state/localstate.md +++ b/docs/guide/src/state/localstate.md @@ -19,8 +19,8 @@ cx.render(rsx!{ ul { todos.read().iter().enumerate().map(|(id, todo)| rsx!{ li { + onhover: move |_| *todos.write()[id].is_hovered = true, h1 { "{todo.contents}" } - onhover: move |_| *todos.write()[id].is_hovered = true; } }) } From 078287301d675e911531063bc1a6fc4e6baa7e8b Mon Sep 17 00:00:00 2001 From: Michael Mroz Date: Sun, 17 Apr 2022 01:51:02 -0700 Subject: [PATCH 299/310] Fix query parsing: the router was silently throwing away queries not prefixed with an extra '?' --- packages/router/src/hooks/use_route.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index cfb1b0447..a37c6f102 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -51,7 +51,7 @@ impl UseRoute { #[cfg(feature = "query")] pub fn query(&self) -> Option { let query = self.url().query()?; - serde_urlencoded::from_str(query.strip_prefix('?').unwrap_or("")).ok() + serde_urlencoded::from_str(query).ok() } /// Get the first query parameter given the parameter name. From a1c644d80bd1d8ea69eb5d071ac73458fb575f5c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 19 Apr 2022 23:48:17 -0400 Subject: [PATCH 300/310] feat: allow header injection for liveview --- packages/liveview/examples/warp.rs | 2 +- packages/liveview/src/lib.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index a02a5231a..e20f07964 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -13,7 +13,7 @@ async fn main() { // todo: compactify this routing under one liveview::app method let view = liveview::new(addr); - let body = view.body(); + let body = view.body("Dioxus LiveView"); let routes = warp::path::end() .map(move || warp::reply::html(body.clone())) diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 340a3e4ea..f3ad90511 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -31,14 +31,13 @@ pub struct Liveview { } impl Liveview { - pub fn body(&self) -> String { + pub fn body(&self, header: &str) -> String { format!( r#" - Dioxus app - + {header}
From 5ff87f637521200e4b11164a62d17f2db6ac229c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 20 Apr 2022 00:52:10 -0400 Subject: [PATCH 301/310] fix: liveview handler should filter only for events --- packages/liveview/src/adapters/warp_adapter.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index 948cffe5b..0c6f3e892 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -65,8 +65,10 @@ pub async fn connect(ws: WebSocket, pool: LocalPoolHandle, app: fn(Scope) -> Ele Either::Left((l, _)) => { if let Some(Ok(msg)) = l { if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); + if msg.method == "user_event" { + let user_event = events::trigger_from_serialized(msg.params); + event_tx.send(user_event).unwrap(); + } } else { break; } From 366a0a8026f98e5fc61a52a26db05d278bb651a4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 20 Apr 2022 09:45:34 -0400 Subject: [PATCH 302/310] feat: read works on fermi root --- packages/fermi/src/root.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs index d3f10afa9..c7c234ddb 100644 --- a/packages/fermi/src/root.rs +++ b/packages/fermi/src/root.rs @@ -95,7 +95,22 @@ impl AtomRoot { } } - pub fn read(&self, _f: impl Readable) -> &V { - todo!() + pub fn read(&self, f: impl Readable) -> Rc { + let mut atoms = self.atoms.borrow_mut(); + + // initialize the value if it's not already initialized + if let Some(slot) = atoms.get_mut(&f.unique_id()) { + slot.value.clone().downcast().unwrap() + } else { + let value = Rc::new(f.init()); + atoms.insert( + f.unique_id(), + Slot { + value: value.clone(), + subscribers: HashSet::new(), + }, + ); + value + } } } From c8919ad77b57c0db27a3313976ea847ea7f73df5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Apr 2022 21:44:37 -0500 Subject: [PATCH 303/310] add benchmark --- Cargo.toml | 4 + benches/tui_update.rs | 268 ++++++++++++++++++++++++++++++++++++ examples/tui_stress_test.rs | 260 ++++++++++++++++++++++++++++++++++ packages/tui/src/config.rs | 17 ++- packages/tui/src/lib.rs | 161 ++++++++++++++-------- 5 files changed, 650 insertions(+), 60 deletions(-) create mode 100644 benches/tui_update.rs create mode 100644 examples/tui_stress_test.rs diff --git a/Cargo.toml b/Cargo.toml index e5ec20306..8291a290c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,3 +88,7 @@ harness = false [[bench]] name = "jsframework" harness = false + +[[bench]] +name = "tui_update" +harness = false \ No newline at end of file diff --git a/benches/tui_update.rs b/benches/tui_update.rs new file mode 100644 index 000000000..5a3a70b66 --- /dev/null +++ b/benches/tui_update.rs @@ -0,0 +1,268 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use dioxus::prelude::*; +use dioxus_tui::{Config, TuiContext}; + +criterion_group!(mbenches, tui_update); +criterion_main!(mbenches); + +/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. +fn tui_update(c: &mut Criterion) { + let mut group = c.benchmark_group("Update boxes"); + + // We can also use loops to define multiple benchmarks, even over multiple dimensions. + for size in 1..=8u32 { + let parameter_string = format!("{}", (3 * size).pow(2)); + group.bench_with_input( + BenchmarkId::new("size", parameter_string), + &size, + |b, size| { + b.iter(|| match size { + 1 => dioxus::tui::launch_cfg( + app3, + Config { + headless: true, + ..Default::default() + }, + ), + 2 => dioxus::tui::launch_cfg( + app6, + Config { + headless: true, + ..Default::default() + }, + ), + 3 => dioxus::tui::launch_cfg( + app9, + Config { + headless: true, + ..Default::default() + }, + ), + 4 => dioxus::tui::launch_cfg( + app12, + Config { + headless: true, + ..Default::default() + }, + ), + 5 => dioxus::tui::launch_cfg( + app15, + Config { + headless: true, + ..Default::default() + }, + ), + 6 => dioxus::tui::launch_cfg( + app18, + Config { + headless: true, + ..Default::default() + }, + ), + 7 => dioxus::tui::launch_cfg( + app21, + Config { + headless: true, + ..Default::default() + }, + ), + 8 => dioxus::tui::launch_cfg( + app24, + Config { + headless: true, + ..Default::default() + }, + ), + _ => (), + }) + }, + ); + } +} + +#[derive(Props, PartialEq)] +struct BoxProps { + x: usize, + y: usize, + hue: f32, + alpha: f32, +} +#[allow(non_snake_case)] +fn Box(cx: Scope) -> Element { + let count = use_state(&cx, || 0); + + let x = cx.props.x * 2; + let y = cx.props.y * 2; + let hue = cx.props.hue; + let display_hue = cx.props.hue as u32 / 10; + let count = count.get(); + let alpha = cx.props.alpha + (count % 100) as f32; + + cx.render(rsx! { + div { + left: "{x}%", + top: "{y}%", + width: "100%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + align_items: "center", + p{"{display_hue:03}"} + } + }) +} + +#[derive(Props, PartialEq)] +struct GridProps { + size: usize, +} +#[allow(non_snake_case)] +fn Grid(cx: Scope) -> Element { + let size = cx.props.size; + let count = use_state(&cx, || 0); + let counts = use_ref(&cx, || vec![0; size * size]); + + let ctx: TuiContext = cx.consume_context().unwrap(); + if *count.get() + 1 >= (size * size) { + ctx.quit(); + } else { + counts.with_mut(|c| { + let i = *count.current(); + c[i] += 1; + c[i] = c[i] % 360; + }); + count.with_mut(|i| { + *i += 1; + *i = *i % (size * size); + }); + } + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..size).map(|x| + { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..size).map(|y| + { + let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; + let key = format!("{}-{}", x, y); + cx.render(rsx! { + Box{ + x: x, + y: y, + alpha: 100.0, + hue: alpha, + key: "{key}", + } + }) + } + ) + } + }) + } + ) + } + }) +} + +fn app3(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 3, + } + } + }) +} + +fn app6(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 6, + } + } + }) +} + +fn app9(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 9, + } + } + }) +} + +fn app12(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 12, + } + } + }) +} + +fn app15(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 15, + } + } + }) +} + +fn app18(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 18, + } + } + }) +} + +fn app21(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 21, + } + } + }) +} + +fn app24(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 24, + } + } + }) +} diff --git a/examples/tui_stress_test.rs b/examples/tui_stress_test.rs new file mode 100644 index 000000000..1674ac055 --- /dev/null +++ b/examples/tui_stress_test.rs @@ -0,0 +1,260 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use dioxus::prelude::*; +use dioxus_tui::{Config, TuiContext}; + +criterion_group!(mbenches, tui_update); +criterion_main!(mbenches); + +/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. +fn tui_update(c: &mut Criterion) { + let mut group = c.benchmark_group("Update boxes"); + + // We can also use loops to define multiple benchmarks, even over multiple dimensions. + for size in 1..=8u32 { + let parameter_string = format!("{}", (3 * size).pow(2)); + group.bench_with_input( + BenchmarkId::new("size", parameter_string), + &size, + |b, size| { + b.iter(|| match size { + 1 => dioxus::tui::launch_cfg( + app3, + Config { + ..Default::default() + }, + ), + 2 => dioxus::tui::launch_cfg( + app6, + Config { + ..Default::default() + }, + ), + 3 => dioxus::tui::launch_cfg( + app9, + Config { + ..Default::default() + }, + ), + 4 => dioxus::tui::launch_cfg( + app12, + Config { + ..Default::default() + }, + ), + 5 => dioxus::tui::launch_cfg( + app15, + Config { + ..Default::default() + }, + ), + 6 => dioxus::tui::launch_cfg( + app18, + Config { + ..Default::default() + }, + ), + 7 => dioxus::tui::launch_cfg( + app21, + Config { + ..Default::default() + }, + ), + 8 => dioxus::tui::launch_cfg( + app24, + Config { + ..Default::default() + }, + ), + _ => (), + }) + }, + ); + } +} + +#[derive(Props, PartialEq)] +struct BoxProps { + x: usize, + y: usize, + hue: f32, + alpha: f32, +} +#[allow(non_snake_case)] +fn Box(cx: Scope) -> Element { + let count = use_state(&cx, || 0); + + let x = cx.props.x * 2; + let y = cx.props.y * 2; + let hue = cx.props.hue; + let display_hue = cx.props.hue as u32 / 10; + let count = count.get(); + let alpha = cx.props.alpha + (count % 100) as f32; + + cx.render(rsx! { + div { + left: "{x}%", + top: "{y}%", + width: "100%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + align_items: "center", + p{"{display_hue:03}"} + } + }) +} + +#[derive(Props, PartialEq)] +struct GridProps { + size: usize, +} +#[allow(non_snake_case)] +fn Grid(cx: Scope) -> Element { + let size = cx.props.size; + let count = use_state(&cx, || 0); + let counts = use_ref(&cx, || vec![0; size * size]); + + let ctx: TuiContext = cx.consume_context().unwrap(); + if *count.get() + 1 >= (size * size) { + ctx.quit(); + } else { + counts.with_mut(|c| { + let i = *count.current(); + c[i] += 1; + c[i] = c[i] % 360; + }); + count.with_mut(|i| { + *i += 1; + *i = *i % (size * size); + }); + } + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..size).map(|x| + { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..size).map(|y| + { + let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; + let key = format!("{}-{}", x, y); + cx.render(rsx! { + Box{ + x: x, + y: y, + alpha: 100.0, + hue: alpha, + key: "{key}", + } + }) + } + ) + } + }) + } + ) + } + }) +} + +fn app3(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 3, + } + } + }) +} + +fn app6(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 6, + } + } + }) +} + +fn app9(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 9, + } + } + }) +} + +fn app12(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 12, + } + } + }) +} + +fn app15(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 15, + } + } + }) +} + +fn app18(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 18, + } + } + }) +} + +fn app21(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 21, + } + } + }) +} + +fn app24(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 24, + } + } + }) +} diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs index 46a0926b8..6e839d8a7 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -1,6 +1,21 @@ -#[derive(Default, Clone, Copy)] +#[derive(Clone, Copy)] pub struct Config { pub rendering_mode: RenderingMode, + /// Controls if the terminal quit when the user presses `ctrl+c`? + /// To handle quiting on your own, use the [crate::TuiContext] root context. + pub ctrl_c_quit: bool, + /// Controls if the terminal should dislay anything, usefull for testing. + pub headless: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + rendering_mode: Default::default(), + ctrl_c_quit: true, + headless: false, + } + } } #[derive(Clone, Copy)] diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index f90967802..71f5026fd 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -6,13 +6,19 @@ use crossterm::{ }; use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::*; -use futures::{channel::mpsc::UnboundedSender, pin_mut, StreamExt}; +use futures::{ + channel::mpsc::{UnboundedReceiver, UnboundedSender}, + pin_mut, StreamExt, +}; use std::{ collections::HashMap, io, time::{Duration, Instant}, }; -use stretch2::{prelude::Size, Stretch}; +use stretch2::{ + prelude::{Node, Size}, + Stretch, +}; use style::RinkStyle; use tui::{backend::CrosstermBackend, Terminal}; @@ -30,6 +36,16 @@ pub use hooks::*; pub use layout::*; pub use render::*; +#[derive(Clone)] +pub struct TuiContext { + tx: UnboundedSender, +} +impl TuiContext { + pub fn quit(&self) { + self.tx.unbounded_send(InputEvent::Close).unwrap(); + } +} + pub fn launch(app: Component<()>) { launch_cfg(app, Config::default()) } @@ -37,8 +53,34 @@ pub fn launch(app: Component<()>) { pub fn launch_cfg(app: Component<()>, cfg: Config) { let mut dom = VirtualDom::new(app); let (tx, rx) = unbounded(); + // Setup input handling + let (event_tx, event_rx) = unbounded(); + let event_tx_clone = event_tx.clone(); + if !cfg.headless { + std::thread::spawn(move || { + let tick_rate = Duration::from_millis(100); + let mut last_tick = Instant::now(); + loop { + // poll for tick rate duration, if no events, sent tick event. + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + + if crossterm::event::poll(timeout).unwrap() { + let evt = crossterm::event::read().unwrap(); + event_tx.unbounded_send(InputEvent::UserInput(evt)).unwrap(); + } + + if last_tick.elapsed() >= tick_rate { + event_tx.unbounded_send(InputEvent::Tick).unwrap(); + last_tick = Instant::now(); + } + } + }); + } let cx = dom.base_scope(); + cx.provide_root_context(TuiContext { tx: event_tx_clone }); let (handler, state) = RinkInputHandler::new(rx, cx); @@ -46,7 +88,7 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) { dom.rebuild(); - render_vdom(&mut dom, tx, handler, cfg).unwrap(); + render_vdom(&mut dom, event_rx, tx, handler, cfg).unwrap(); } pub struct TuiNode<'a> { @@ -56,35 +98,13 @@ pub struct TuiNode<'a> { pub node: &'a VNode<'a>, } -pub fn render_vdom( +fn render_vdom( vdom: &mut VirtualDom, + mut event_reciever: UnboundedReceiver, ctx: UnboundedSender, handler: RinkInputHandler, cfg: Config, ) -> Result<()> { - // Setup input handling - let (tx, mut rx) = unbounded(); - std::thread::spawn(move || { - let tick_rate = Duration::from_millis(100); - let mut last_tick = Instant::now(); - loop { - // poll for tick rate duration, if no events, sent tick event. - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - - if crossterm::event::poll(timeout).unwrap() { - let evt = crossterm::event::read().unwrap(); - tx.unbounded_send(InputEvent::UserInput(evt)).unwrap(); - } - - if last_tick.elapsed() >= tick_rate { - tx.unbounded_send(InputEvent::Tick).unwrap(); - last_tick = Instant::now(); - } - } - }); - tokio::runtime::Builder::new_current_thread() .enable_all() .build()? @@ -92,13 +112,17 @@ pub fn render_vdom( /* 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(); + let mut terminal = (!cfg.headless).then(|| { + enable_raw_mode().unwrap(); + let mut stdout = std::io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + let backend = CrosstermBackend::new(io::stdout()); + Terminal::new(backend).unwrap() + }); - terminal.clear().unwrap(); + if let Some(terminal) = &mut terminal { + terminal.clear().unwrap(); + } loop { /* @@ -126,34 +150,51 @@ pub fn render_vdom( let root_layout = nodes[&node_id].layout; let mut events = Vec::new(); - terminal.draw(|frame| { - // size is guaranteed to not change when rendering - let dims = frame.size(); + fn resize(dims: tui::layout::Rect, stretch: &mut Stretch, root_layout: Node) { let width = dims.width; let height = dims.height; - layout + + stretch .compute_layout( root_layout, Size { - width: stretch2::prelude::Number::Defined(width as f32), - height: stretch2::prelude::Number::Defined(height as f32), + width: stretch2::prelude::Number::Defined((width - 1) as f32), + height: stretch2::prelude::Number::Defined((height - 1) as f32), }, ) .unwrap(); + } - // resolve events before rendering - events = handler.get_events(vdom, &layout, &mut nodes, root_node); - render::render_vnode( - frame, - &layout, - &mut nodes, - vdom, - root_node, - &RinkStyle::default(), - cfg, + if let Some(terminal) = &mut terminal { + terminal.draw(|frame| { + // size is guaranteed to not change when rendering + resize(frame.size(), &mut layout, root_layout); + + // resolve events before rendering + events = handler.get_events(vdom, &layout, &mut nodes, root_node); + render::render_vnode( + frame, + &layout, + &mut nodes, + vdom, + root_node, + &RinkStyle::default(), + cfg, + ); + assert!(nodes.is_empty()); + })?; + } else { + resize( + tui::layout::Rect { + x: 0, + y: 0, + width: 100, + height: 100, + }, + &mut layout, + root_layout, ); - assert!(nodes.is_empty()); - })?; + } for e in events { vdom.handle_message(SchedulerMsg::Event(e)); @@ -164,7 +205,7 @@ pub fn render_vdom( let wait = vdom.wait_for_work(); pin_mut!(wait); - match select(wait, rx.next()).await { + match select(wait, event_reciever.next()).await { Either::Left((_a, _b)) => { // } @@ -194,13 +235,15 @@ pub fn render_vdom( vdom.work_with_deadline(|| false); } - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; + if let Some(terminal) = &mut terminal { + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + } Ok(()) }) From 931aceca43e337568451477ecd78351292851491 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Apr 2022 21:50:49 -0500 Subject: [PATCH 304/310] remove stress test --- examples/tui_stress_test.rs | 260 ------------------------------------ 1 file changed, 260 deletions(-) delete mode 100644 examples/tui_stress_test.rs diff --git a/examples/tui_stress_test.rs b/examples/tui_stress_test.rs deleted file mode 100644 index 1674ac055..000000000 --- a/examples/tui_stress_test.rs +++ /dev/null @@ -1,260 +0,0 @@ -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use dioxus::prelude::*; -use dioxus_tui::{Config, TuiContext}; - -criterion_group!(mbenches, tui_update); -criterion_main!(mbenches); - -/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. -fn tui_update(c: &mut Criterion) { - let mut group = c.benchmark_group("Update boxes"); - - // We can also use loops to define multiple benchmarks, even over multiple dimensions. - for size in 1..=8u32 { - let parameter_string = format!("{}", (3 * size).pow(2)); - group.bench_with_input( - BenchmarkId::new("size", parameter_string), - &size, - |b, size| { - b.iter(|| match size { - 1 => dioxus::tui::launch_cfg( - app3, - Config { - ..Default::default() - }, - ), - 2 => dioxus::tui::launch_cfg( - app6, - Config { - ..Default::default() - }, - ), - 3 => dioxus::tui::launch_cfg( - app9, - Config { - ..Default::default() - }, - ), - 4 => dioxus::tui::launch_cfg( - app12, - Config { - ..Default::default() - }, - ), - 5 => dioxus::tui::launch_cfg( - app15, - Config { - ..Default::default() - }, - ), - 6 => dioxus::tui::launch_cfg( - app18, - Config { - ..Default::default() - }, - ), - 7 => dioxus::tui::launch_cfg( - app21, - Config { - ..Default::default() - }, - ), - 8 => dioxus::tui::launch_cfg( - app24, - Config { - ..Default::default() - }, - ), - _ => (), - }) - }, - ); - } -} - -#[derive(Props, PartialEq)] -struct BoxProps { - x: usize, - y: usize, - hue: f32, - alpha: f32, -} -#[allow(non_snake_case)] -fn Box(cx: Scope) -> Element { - let count = use_state(&cx, || 0); - - let x = cx.props.x * 2; - let y = cx.props.y * 2; - let hue = cx.props.hue; - let display_hue = cx.props.hue as u32 / 10; - let count = count.get(); - let alpha = cx.props.alpha + (count % 100) as f32; - - cx.render(rsx! { - div { - left: "{x}%", - top: "{y}%", - width: "100%", - height: "100%", - background_color: "hsl({hue}, 100%, 50%, {alpha}%)", - align_items: "center", - p{"{display_hue:03}"} - } - }) -} - -#[derive(Props, PartialEq)] -struct GridProps { - size: usize, -} -#[allow(non_snake_case)] -fn Grid(cx: Scope) -> Element { - let size = cx.props.size; - let count = use_state(&cx, || 0); - let counts = use_ref(&cx, || vec![0; size * size]); - - let ctx: TuiContext = cx.consume_context().unwrap(); - if *count.get() + 1 >= (size * size) { - ctx.quit(); - } else { - counts.with_mut(|c| { - let i = *count.current(); - c[i] += 1; - c[i] = c[i] % 360; - }); - count.with_mut(|i| { - *i += 1; - *i = *i % (size * size); - }); - } - - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - flex_direction: "column", - (0..size).map(|x| - { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - flex_direction: "row", - (0..size).map(|y| - { - let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; - let key = format!("{}-{}", x, y); - cx.render(rsx! { - Box{ - x: x, - y: y, - alpha: 100.0, - hue: alpha, - key: "{key}", - } - }) - } - ) - } - }) - } - ) - } - }) -} - -fn app3(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 3, - } - } - }) -} - -fn app6(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 6, - } - } - }) -} - -fn app9(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 9, - } - } - }) -} - -fn app12(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 12, - } - } - }) -} - -fn app15(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 15, - } - } - }) -} - -fn app18(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 18, - } - } - }) -} - -fn app21(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 21, - } - } - }) -} - -fn app24(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 24, - } - } - }) -} From 9d9aa33e259693580442c4d384822fb21cb299eb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 06:29:22 -0500 Subject: [PATCH 305/310] add builder pattern and update examples --- examples/tui_color_test.rs | 4 +--- packages/tui/src/config.rs | 31 ++++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/examples/tui_color_test.rs b/examples/tui_color_test.rs index 358fae1f6..1a5a67457 100644 --- a/examples/tui_color_test.rs +++ b/examples/tui_color_test.rs @@ -3,9 +3,7 @@ use dioxus::prelude::*; fn main() { dioxus::tui::launch_cfg( app, - dioxus::tui::Config { - rendering_mode: dioxus::tui::RenderingMode::Ansi, - }, + dioxus::tui::Config::default().with_rendering_mode(dioxus::tui::RenderingMode::Ansi), ); } diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs index 6e839d8a7..47ec817ee 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -1,11 +1,36 @@ #[derive(Clone, Copy)] +#[non_exhaustive] pub struct Config { - pub rendering_mode: RenderingMode, + pub(crate) rendering_mode: RenderingMode, /// Controls if the terminal quit when the user presses `ctrl+c`? /// To handle quiting on your own, use the [crate::TuiContext] root context. - pub ctrl_c_quit: bool, + pub(crate) ctrl_c_quit: bool, /// Controls if the terminal should dislay anything, usefull for testing. - pub headless: bool, + pub(crate) headless: bool, +} + +impl Config { + pub fn new() -> Self { + Self::default() + } + + pub fn with_rendering_mode(self, rendering_mode: RenderingMode) -> Self { + Self { + rendering_mode, + ..self + } + } + + pub fn with_ctrl_c_quit(self, ctrl_c_quit: bool) -> Self { + Self { + ctrl_c_quit, + ..self + } + } + + pub fn with_headless(self, headless: bool) -> Self { + Self { headless, ..self } + } } impl Default for Config { From efa682735e4244bb80dcf731a1181ba629c9b5e0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 06:33:03 -0500 Subject: [PATCH 306/310] ignore ctrl-c if ctrl_c_quit is false --- packages/tui/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 71f5026fd..1c4499e18 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -215,6 +215,7 @@ fn render_vdom( TermEvent::Key(key) => { if matches!(key.code, KeyCode::Char('C' | 'c')) && key.modifiers.contains(KeyModifiers::CONTROL) + && cfg.ctrl_c_quit { break; } From 91b21475522745eb9276172f60780b8f224f38bb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 06:44:48 -0500 Subject: [PATCH 307/310] update benchmark --- benches/tui_update.rs | 64 +++++--------------------------------- packages/tui/src/config.rs | 11 ++++--- 2 files changed, 15 insertions(+), 60 deletions(-) diff --git a/benches/tui_update.rs b/benches/tui_update.rs index 5a3a70b66..d46921a8d 100644 --- a/benches/tui_update.rs +++ b/benches/tui_update.rs @@ -17,62 +17,14 @@ fn tui_update(c: &mut Criterion) { &size, |b, size| { b.iter(|| match size { - 1 => dioxus::tui::launch_cfg( - app3, - Config { - headless: true, - ..Default::default() - }, - ), - 2 => dioxus::tui::launch_cfg( - app6, - Config { - headless: true, - ..Default::default() - }, - ), - 3 => dioxus::tui::launch_cfg( - app9, - Config { - headless: true, - ..Default::default() - }, - ), - 4 => dioxus::tui::launch_cfg( - app12, - Config { - headless: true, - ..Default::default() - }, - ), - 5 => dioxus::tui::launch_cfg( - app15, - Config { - headless: true, - ..Default::default() - }, - ), - 6 => dioxus::tui::launch_cfg( - app18, - Config { - headless: true, - ..Default::default() - }, - ), - 7 => dioxus::tui::launch_cfg( - app21, - Config { - headless: true, - ..Default::default() - }, - ), - 8 => dioxus::tui::launch_cfg( - app24, - Config { - headless: true, - ..Default::default() - }, - ), + 1 => dioxus::tui::launch_cfg(app3, Config::default().with_headless()), + 2 => dioxus::tui::launch_cfg(app6, Config::default().with_headless()), + 3 => dioxus::tui::launch_cfg(app9, Config::default().with_headless()), + 4 => dioxus::tui::launch_cfg(app12, Config::default().with_headless()), + 5 => dioxus::tui::launch_cfg(app15, Config::default().with_headless()), + 6 => dioxus::tui::launch_cfg(app18, Config::default().with_headless()), + 7 => dioxus::tui::launch_cfg(app21, Config::default().with_headless()), + 8 => dioxus::tui::launch_cfg(app24, Config::default().with_headless()), _ => (), }) }, diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs index 47ec817ee..fc2fb3da5 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -21,15 +21,18 @@ impl Config { } } - pub fn with_ctrl_c_quit(self, ctrl_c_quit: bool) -> Self { + pub fn with_ctrl_c_quit(self) -> Self { Self { - ctrl_c_quit, + ctrl_c_quit: true, ..self } } - pub fn with_headless(self, headless: bool) -> Self { - Self { headless, ..self } + pub fn with_headless(self) -> Self { + Self { + headless: true, + ..self + } } } From 159be32fd4f16f194a9d48ddaa206ef5a9555da4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 13:48:11 -0500 Subject: [PATCH 308/310] update documentation to refect tui changes --- docs/reference/src/platforms/tui.md | 49 ++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/reference/src/platforms/tui.md b/docs/reference/src/platforms/tui.md index e646bd4ba..2b5a3ef96 100644 --- a/docs/reference/src/platforms/tui.md +++ b/docs/reference/src/platforms/tui.md @@ -9,13 +9,12 @@ TUI support is currently quite experimental. Even the project name will change. ## Getting Set up -To tinker with TUI support, start by making a new package and adding our TUI package from git. +To tinker with TUI support, start by making a new package and adding our TUI feature. ```shell $ cargo new --bin demo $ cd demo -$ cargo add dioxus -$ cargo add rink --git https://github.com/DioxusLabs/rink.git +$ cargo add dioxus --features tui ``` @@ -27,10 +26,7 @@ Then, edit your `main.rs` with the basic template. use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + dioxus::tui::launch(app); } fn app(cx: Scope) -> Element { @@ -54,7 +50,44 @@ To run our app: $ cargo run ``` -Press "q" to close the app (yes, this is hardcoded, we are working on handlers to expose this in your code.) +Press "ctrl-c" to close the app. To switch from "ctrl-c" to just "q" to quit you can launch the app with a configuration to disable the default quit and use the root TuiContext to quit on your own. + +```rust +// main +use dioxus::events::{KeyCode, KeyboardEvent}; +use dioxus::prelude::*; +use dioxus::tui::TuiContext; + +fn main() { + dioxus::tui::launch_cfg( + app, + dioxus::tui::Config { + ctrl_c_quit: false, + // Some older terminals only support 16 colors or ANSI colors if your terminal is one of these change this to BaseColors or ANSI + rendering_mode: dioxus::tui::RenderingMode::Rgb, + }, + ); +} + +fn app(cx: Scope) -> Element { + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + + cx.render(rsx! { + div { + width: "100%", + height: "10px", + background_color: "red", + justify_content: "center", + align_items: "center", + onkeydown: move |k: KeyboardEvent| if let KeyCode::Q = k.data.key_code { + tui_ctx.quit(); + }, + + "Hello world!" + } + }) +} +``` ## Notes From 0105218db9f9949c825c7c7cb837d58e2d0f6c8f Mon Sep 17 00:00:00 2001 From: Benjamin Lemelin Date: Thu, 21 Apr 2022 15:44:43 -0400 Subject: [PATCH 309/310] Removed alerts when right click on release. --- packages/desktop/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 89222f271..1e3898f05 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -189,12 +189,10 @@ pub fn launch_with_props( r#" if (document.addEventListener) { document.addEventListener('contextmenu', function(e) { - alert("You've tried to open context menu"); e.preventDefault(); }, false); } else { document.attachEvent('oncontextmenu', function() { - alert("You've tried to open context menu"); window.event.returnValue = false; }); } From 5e162695322b40a4470c61309bf9eea1ad6dd0c3 Mon Sep 17 00:00:00 2001 From: Aleksei Zolotov Date: Sat, 23 Apr 2022 16:21:07 +0200 Subject: [PATCH 310/310] Add PartialEq to events::KeyCode --- packages/html/src/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 7e95375cf..79c90e8ab 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -611,7 +611,7 @@ pub mod on { feature = "serialize", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr) )] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] #[repr(u8)] pub enum KeyCode { // That key has no keycode, = 0