diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index de6b34274..ee5a763c7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -104,7 +104,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings + args: --workspace -- -D warnings # Coverage is disabled until we can fix it # coverage: diff --git a/Cargo.toml b/Cargo.toml index 8f3a33d96..5955451f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,10 @@ members = [ "packages/liveview", "packages/autofmt", "packages/rsx", - "docs/guide", "packages/tui", "packages/native-core", "packages/native-core-macro", + "docs/guide", ] # This is a "virtual package" diff --git a/docs/guide/examples/component_children_inspect.rs b/docs/guide/examples/component_children_inspect.rs index 880c64358..a79b068a3 100644 --- a/docs/guide/examples/component_children_inspect.rs +++ b/docs/guide/examples/component_children_inspect.rs @@ -26,7 +26,7 @@ struct ClickableProps<'a> { // ANCHOR: Clickable fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { match cx.props.children { - Ok(VNode { dynamic_nodes, .. }) => { + Some(VNode { dynamic_nodes, .. }) => { todo!("render some stuff") } _ => { diff --git a/docs/guide/src/en/best_practices/index.md b/docs/guide/src/en/best_practices/index.md index 2270508f1..6b9f0ed1d 100644 --- a/docs/guide/src/en/best_practices/index.md +++ b/docs/guide/src/en/best_practices/index.md @@ -12,18 +12,3 @@ While it is possible to share state between components, this should only be done - Keep state local to a component if possible - When sharing state through props, only pass down the specific data necessary - -## Reusable Libraries - -When publishing a library designed to work with Dioxus, we highly advise using only the core feature on the `dioxus` crate. This makes your crate compile faster, makes it more stable, and avoids bringing in incompatible libraries that might make it not compile on unsupported platforms. - - -❌ Don't include unnecessary dependencies in libraries: -```toml -dioxus = { version = "...", features = ["web", "desktop", "full"]} -``` - -✅ Only add the features you need: -```toml -dioxus = { version = "...", features = "core"} -``` diff --git a/docs/guide/src/en/getting_started/desktop.md b/docs/guide/src/en/getting_started/desktop.md index c91aa634a..0a963ff61 100644 --- a/docs/guide/src/en/getting_started/desktop.md +++ b/docs/guide/src/en/getting_started/desktop.md @@ -25,19 +25,13 @@ cargo new --bin demo cd demo ``` -Add Dioxus with the `desktop` feature (this will edit `Cargo.toml`): +Add Dioxus and the `desktop` renderer (this will edit `Cargo.toml`): ```shell -cargo add dioxus --features desktop +cargo add dioxus +cargo add dioxus-desktop ``` -> If your system does not provide the `libappindicator3` library, like Debian/bullseye, you can enable the replacement `ayatana` with an additional flag: -> ->```shell -># On Debian/bullseye use: ->cargo add dioxus --features desktop --features ayatana ->``` - Edit your `main.rs`: ```rust diff --git a/docs/guide/src/en/getting_started/hot_reload.md b/docs/guide/src/en/getting_started/hot_reload.md index e5d7b0327..7434fdb9c 100644 --- a/docs/guide/src/en/getting_started/hot_reload.md +++ b/docs/guide/src/en/getting_started/hot_reload.md @@ -8,7 +8,7 @@ Install [dioxus-cli](https://github.com/DioxusLabs/cli). Enable the hot-reload feature on dioxus: ```toml -dioxus = { version = "*", features = ["web", "hot-reload"] } +dioxus = { version = "*", features = ["hot-reload"] } ``` # Usage diff --git a/docs/guide/src/en/getting_started/mobile.md b/docs/guide/src/en/getting_started/mobile.md index 56fb891e1..f8dd5f4ff 100644 --- a/docs/guide/src/en/getting_started/mobile.md +++ b/docs/guide/src/en/getting_started/mobile.md @@ -52,7 +52,8 @@ path = "gen/bin/desktop.rs" # clear all the dependencies [dependencies] mobile-entry-point = "0.1.0" -dioxus = { version = "*", features = ["mobile"] } +dioxus = { version = "*"} +dioxus-desktop = { version = "*" } simple_logger = "*" ``` @@ -62,7 +63,7 @@ Edit your `lib.rs`: use dioxus::prelude::*; fn main() { - dioxus_mobile::launch(app); + dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { diff --git a/docs/guide/src/en/getting_started/ssr.md b/docs/guide/src/en/getting_started/ssr.md index e4f825f0e..7e28cd304 100644 --- a/docs/guide/src/en/getting_started/ssr.md +++ b/docs/guide/src/en/getting_started/ssr.md @@ -36,10 +36,11 @@ cargo new --bin demo cd app ``` -Add Dioxus with the `ssr` feature: +Add Dioxus and the `ssr` renderer feature: ```shell -cargo add dioxus --features ssr +cargo add dioxus +cargo add dioxus-ssr ``` Next, add all the Axum dependencies. This will be different if you're using a different Web Framework @@ -54,7 +55,8 @@ Your dependencies should look roughly like this: ```toml [dependencies] axum = "0.4.5" -dioxus = { version = "*", features = ["ssr"] } +dioxus = { version = "*" } +dioxus-ssr = { version = "*" } tokio = { version = "1.15.0", features = ["full"] } ``` diff --git a/docs/guide/src/en/getting_started/tui.md b/docs/guide/src/en/getting_started/tui.md index 67f366e18..980af5706 100644 --- a/docs/guide/src/en/getting_started/tui.md +++ b/docs/guide/src/en/getting_started/tui.md @@ -13,12 +13,13 @@ TUI support is currently quite experimental. Even the project name will change. ## Getting Set up -Start by making a new package and adding our TUI feature. +Start by making a new package and adding our TUI renderer. ```shell cargo new --bin demo cd demo -cargo add dioxus --features tui +cargo add dioxus +cargo add dioxus-tui ``` Then, edit your `main.rs` with the basic template. @@ -42,5 +43,6 @@ Press "ctrl-c" to close the app. To switch from "ctrl-c" to just "q" to quit yo ## Notes - Our TUI package uses flexbox for layout +- Regular widgets will not work in the tui render, but the tui renderer has it's own widget components (see [the widgets example](https://github.com/DioxusLabs/dioxus/blob/master/packages/tui/examples/tui_widgets.rs)). - 1px is one character lineheight. Your regular CSS px does not translate. - If your app panics, your terminal is wrecked. This will be fixed eventually. diff --git a/docs/guide/src/en/getting_started/web.md b/docs/guide/src/en/getting_started/web.md index 119f9b8e5..311cf277a 100644 --- a/docs/guide/src/en/getting_started/web.md +++ b/docs/guide/src/en/getting_started/web.md @@ -38,10 +38,11 @@ cargo new --bin demo cd demo ``` -Add Dioxus as a dependency with the `web` feature: +Add Dioxus as a dependency and add the web renderer: ```bash -cargo add dioxus --features web +cargo add dioxus +cargo add dioxus-web ``` Add an `index.html` for Trunk to use. Make sure your "mount point" element has an ID of "main": diff --git a/docs/guide/src/en/interactivity/router.md b/docs/guide/src/en/interactivity/router.md index e1d170ec3..cf629f6d2 100644 --- a/docs/guide/src/en/interactivity/router.md +++ b/docs/guide/src/en/interactivity/router.md @@ -19,7 +19,8 @@ This is where the router crates come in handy. To make sure we're using the rout ```toml [dependencies] -dioxus = { version = "0.2", features = ["desktop", "router"] } +dioxus = { version = "*" } +dioxus-router = { version = "*" } ``` diff --git a/docs/guide/src/pt-br/best_practices/index.md b/docs/guide/src/pt-br/best_practices/index.md index 3330c4327..ee8506241 100644 --- a/docs/guide/src/pt-br/best_practices/index.md +++ b/docs/guide/src/pt-br/best_practices/index.md @@ -12,19 +12,3 @@ Embora seja possível compartilhar o estado entre os componentes, isso só deve - Mantenha o estado local para um componente, se possível - Ao compartilhar o estado por meio de adereços, passe apenas os dados específicos necessários - -## Bibliotecas Reutilizáveis - -Ao publicar uma biblioteca projetada para funcionar com o Dioxus, é altamente recomendável usar apenas o recurso principal na `crate` `dioxus`. Isso faz com que sua `crate` seja compilada mais rapidamente, mais estável e evita a inclusão de bibliotecas incompatíveis que podem fazer com que ela não seja compilada em plataformas não suportadas. - -❌ Não inclua dependências desnecessárias nas bibliotecas: - -```toml -dioxus = { version = "...", features = ["web", "desktop", "full"]} -``` - -✅ Adicione apenas os recursos que você precisa: - -```toml -dioxus = { version = "...", features = "core"} -``` diff --git a/docs/guide/src/pt-br/getting_started/desktop.md b/docs/guide/src/pt-br/getting_started/desktop.md index abb7c9309..5c6940b96 100644 --- a/docs/guide/src/pt-br/getting_started/desktop.md +++ b/docs/guide/src/pt-br/getting_started/desktop.md @@ -29,16 +29,10 @@ cd demo Adicione o Dioxus com o recurso `desktop` (isso irá editar o `Cargo.toml`): ```shell -cargo add dioxus --features desktop +cargo add dioxus +cargo add dioxus-desktop ``` -> Se seu sistema não fornece a biblioteca `libappindicator3`, como Debian/bullseye, você pode habilitar a substituta `ayatana` com um _flag_ adicional: -> -> ```shell -> # On Debian/bullseye use: -> cargo add dioxus --features desktop --features ayatana -> ``` - Edite seu `main.rs`: ```rust diff --git a/docs/guide/src/pt-br/getting_started/hot_reload.md b/docs/guide/src/pt-br/getting_started/hot_reload.md index e56499c5f..2dabde536 100644 --- a/docs/guide/src/pt-br/getting_started/hot_reload.md +++ b/docs/guide/src/pt-br/getting_started/hot_reload.md @@ -10,7 +10,7 @@ Instale o [dioxus-cli](https://github.com/DioxusLabs/cli). Habilite o recurso de _hot-reload_ no dioxus: ```toml -dioxus = { version = "*", features = ["web", "hot-reload"] } +dioxus = { version = "*", features = ["hot-reload"] } ``` # Usage diff --git a/docs/guide/src/pt-br/getting_started/mobile.md b/docs/guide/src/pt-br/getting_started/mobile.md index 57c2a6ccf..edcfbfba7 100644 --- a/docs/guide/src/pt-br/getting_started/mobile.md +++ b/docs/guide/src/pt-br/getting_started/mobile.md @@ -52,7 +52,8 @@ path = "gen/bin/desktop.rs" # clear all the dependencies [dependencies] mobile-entry-point = "0.1.0" -dioxus = { version = "*", features = ["mobile"] } +dioxus = { version = "*" } +dioxus-desktop = { version = "*" } simple_logger = "*" ``` @@ -62,7 +63,7 @@ Edite seu `lib.rs`: use dioxus::prelude::*; fn main() { - dioxus::mobile::launch(app); + dioxus_desktop::launch(app); } fn app(cx: Scope) -> Element { diff --git a/docs/guide/src/pt-br/getting_started/ssr.md b/docs/guide/src/pt-br/getting_started/ssr.md index f3e3caf8e..ddb02a289 100644 --- a/docs/guide/src/pt-br/getting_started/ssr.md +++ b/docs/guide/src/pt-br/getting_started/ssr.md @@ -38,7 +38,8 @@ cd app Adicione o Dioxus com o recurso `ssr`: ```shell -cargo add dioxus --features ssr +cargo add dioxus +cargo add dioxus-ssr ``` Em seguida, adicione todas as dependências do Axum. Isso será diferente se você estiver usando um Web Framework diferente @@ -53,7 +54,8 @@ Suas dependências devem ficar mais ou menos assim: ```toml [dependencies] axum = "0.4.5" -dioxus = { version = "*", features = ["ssr"] } +dioxus = { version = "*" } +dioxus-ssr = { version = "*" } tokio = { version = "1.15.0", features = ["full"] } ``` @@ -83,7 +85,7 @@ E, em seguida, adicione nosso _endpoint_. Podemos renderizar `rsx!` diretamente: ```rust async fn app_endpoint() -> Html { - Html(dioxus::ssr::render_lazy(rsx! { + Html(dioxus_ssr::render_lazy(rsx! { h1 { "hello world!" } })) } @@ -99,7 +101,7 @@ async fn app_endpoint() -> Html { let mut app = VirtualDom::new(app); let _ = app.rebuild(); - Html(dioxus::ssr::render_vdom(&app)) + Html(dioxus_ssr::render_vdom(&app)) } ``` diff --git a/docs/guide/src/pt-br/getting_started/tui.md b/docs/guide/src/pt-br/getting_started/tui.md index aa99ca763..deaf060b6 100644 --- a/docs/guide/src/pt-br/getting_started/tui.md +++ b/docs/guide/src/pt-br/getting_started/tui.md @@ -17,7 +17,8 @@ Comece criando um novo pacote e adicionando nosso recurso TUI. ```shell cargo new --bin demo cd demo -cargo add dioxus --features tui +cargo add dioxus +cargo add dioxus-tui ``` Em seguida, edite seu `main.rs` com o modelo básico. diff --git a/docs/guide/src/pt-br/getting_started/web.md b/docs/guide/src/pt-br/getting_started/web.md index 87fb4f76b..ffb322a25 100644 --- a/docs/guide/src/pt-br/getting_started/web.md +++ b/docs/guide/src/pt-br/getting_started/web.md @@ -43,7 +43,8 @@ cd demo Adicione o Dioxus como uma dependência com o recurso `web`: ```bash -cargo add dioxus --features web +cargo add dioxus +cargo add dioxus-web ``` Adicione um `index.html` para o `Trunk` usar. Certifique-se de que seu elemento "mount point" tenha um ID de "main": diff --git a/docs/guide/src/pt-br/interactivity/router.md b/docs/guide/src/pt-br/interactivity/router.md index c7453bddc..d3ac7abb9 100644 --- a/docs/guide/src/pt-br/interactivity/router.md +++ b/docs/guide/src/pt-br/interactivity/router.md @@ -18,7 +18,8 @@ Cada uma dessas cenas é independente – não queremos renderizar a página ini ```toml [dependencies] -dioxus = { version = "0.2", features = ["desktop", "router"] } +dioxus = { version = "*" } +dioxus-router = { version = "*" } ``` ## Usando o Roteador diff --git a/packages/autofmt/README.md b/packages/autofmt/README.md index f1c425c56..eec3b590e 100644 --- a/packages/autofmt/README.md +++ b/packages/autofmt/README.md @@ -15,3 +15,74 @@ Sorted roughly in order of what's possible - [ ] Format regular exprs - [ ] Fix prettyplease around chaining - [ ] Don't eat comments in prettyplease + + +# Technique + + +div { + div {} + div {} +} + + +div + +possible line break +div +div + + + +string of possible items within a nesting +div { + attr_pair + expr + text + comment +} +a nesting is either a component or an element + +idea: +collect all items into a queue +q +```rust +section { + div { + h1 { p { "asdasd" } } + h1 { p { "asdasd" } } + } +} + +section {} +``` + + +// space +// space +// space + + +3 - section +3 - section div +3 - section div h1 +3 - section div h1 p +3 - section div h1 p text +3 - section +3 - section div +3 - section div h1 +3 - section div h1 p +3 - section div h1 p text + +block + +- when we hit the end of a trail, we can make a decision what needs to be hard breaked +- most nestings cannot be merged into a single one, so at some point we need to write the line break +- this is the scan section. we scan forward until it's obvious where to place a hard break +- when a line is finished, we can print it out by unloading our queued items +- never double nested + + +Terms +- break is a whitespace than can flex, dependent on the situation +- ‹⁠› diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs index 79ad7dfb7..fd0b6b60d 100644 --- a/packages/autofmt/src/buffer.rs +++ b/packages/autofmt/src/buffer.rs @@ -125,16 +125,26 @@ impl Buffer { } pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result { - for child in children { - // Exprs handle their own indenting/line breaks - if !matches!(child, BodyNode::RawExpr(_)) { - if self.current_span_is_primary(child.span()) { - self.write_comments(child.span())?; - } - self.tabbed_line()?; - } + let last_child = children.len(); - self.write_ident(child)?; + for (idx, child) in children.iter().enumerate() { + match child { + // check if the expr is a short + BodyNode::RawExpr { .. } => { + self.tabbed_line()?; + self.write_ident(child)?; + if idx != last_child - 1 { + write!(self.buf, ",")?; + } + } + _ => { + if self.current_span_is_primary(child.span()) { + self.write_comments(child.span())?; + } + self.tabbed_line()?; + self.write_ident(child)?; + } + } } Ok(()) diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index b5f1c2f84..310641121 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -2,6 +2,7 @@ use crate::Buffer; use dioxus_rsx::*; use proc_macro2::Span; use std::{fmt::Result, fmt::Write}; +use syn::{spanned::Spanned, Expr}; #[derive(Debug)] enum ShortOptimization { @@ -83,12 +84,15 @@ impl Buffer { self.write_attributes(attributes, key, true)?; - if !children.is_empty() && !attributes.is_empty() { + if !children.is_empty() && (!attributes.is_empty() || key.is_some()) { write!(self.buf, ", ")?; } - for child in children { + for (id, child) in children.iter().enumerate() { self.write_ident(child)?; + if id != children.len() - 1 && children.len() > 1 { + write!(self.buf, ", ")?; + } } write!(self.buf, " ")?; @@ -100,7 +104,7 @@ impl Buffer { } self.write_attributes(attributes, key, true)?; - if !children.is_empty() && !attributes.is_empty() { + if !children.is_empty() && (!attributes.is_empty() || key.is_some()) { write!(self.buf, ",")?; } @@ -113,7 +117,7 @@ impl Buffer { ShortOptimization::NoOpt => { self.write_attributes(attributes, key, false)?; - if !children.is_empty() && !attributes.is_empty() { + if !children.is_empty() && (!attributes.is_empty() || key.is_some()) { write!(self.buf, ",")?; } @@ -286,33 +290,66 @@ impl Buffer { if attr_len > 80 { None + } else if comp.children.is_empty() { + Some(attr_len) } else { - self.is_short_children(&comp.children) - .map(|child_len| child_len + attr_len) + None } } - [BodyNode::RawExpr(ref _expr)] => { + [BodyNode::RawExpr(ref expr)] => { // TODO: let rawexprs to be inlined - // let span = syn::spanned::Spanned::span(&text); - // let (start, end) = (span.start(), span.end()); - // if start.line == end.line { - // Some(end.column - start.column) - // } else { - // None - // } - None + get_expr_length(expr) } [BodyNode::Element(ref el)] => { let attr_len = self.is_short_attrs(&el.attributes); - if attr_len > 80 { - None - } else { - self.is_short_children(&el.children) - .map(|child_len| child_len + attr_len) + if el.children.is_empty() && attr_len < 80 { + return Some(el.name.to_string().len()); } + + if el.children.len() == 1 { + if let BodyNode::Text(ref text) = el.children[0] { + let value = text.source.as_ref().unwrap().value(); + + if value.len() + el.name.to_string().len() + attr_len < 80 { + return Some(value.len() + el.name.to_string().len() + attr_len); + } + } + } + + None + } + // todo, allow non-elements to be on the same line + items => { + let mut total_count = 0; + + for item in items { + match item { + BodyNode::Component(_) | BodyNode::Element(_) => return None, + BodyNode::Text(text) => { + total_count += text.source.as_ref().unwrap().value().len() + } + BodyNode::RawExpr(expr) => match get_expr_length(expr) { + Some(len) => total_count += len, + None => return None, + }, + BodyNode::ForLoop(_) => todo!(), + BodyNode::IfChain(_) => todo!(), + } + } + + Some(total_count) } - _ => None, } } } + +fn get_expr_length(expr: &Expr) -> Option { + let span = expr.span(); + let (start, end) = (span.start(), span.end()); + if start.line == end.line { + Some(end.column - start.column) + } else { + None + } +} diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs index d89a5cb39..48ea6fcc7 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -16,16 +16,63 @@ impl Buffer { let placement = exp.span(); let start = placement.start(); let end = placement.end(); - let num_spaces_desired = (self.indent * 4) as isize; + // let num_spaces_desired = (self.indent * 4) as isize; - let first = &self.src[start.line - 1]; - let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize; + // print comments + // let mut queued_comments = vec![]; + // let mut offset = 2; + // loop { + // let line = &self.src[start.line - offset]; + // if line.trim_start().starts_with("//") { + // queued_comments.push(line); + // } else { + // break; + // } - let offset = num_spaces_real - num_spaces_desired; + // offset += 1; + // } + // let had_comments = !queued_comments.is_empty(); + // for comment in queued_comments.into_iter().rev() { + // writeln!(self.buf, "{}", comment)?; + // } - for line in &self.src[start.line - 1..end.line] { + // if the expr is on one line, just write it directly + if start.line == end.line { + write!( + self.buf, + "{}", + &self.src[start.line - 1][start.column - 1..end.column].trim() + )?; + return Ok(()); + } + + // If the expr is multiline, we want to collect all of its lines together and write them out properly + // This involves unshifting the first line if it's aligned + let first_line = &self.src[start.line - 1]; + write!( + self.buf, + "{}", + &first_line[start.column - 1..first_line.len()].trim() + )?; + + let first_prefix = &self.src[start.line - 1][..start.column]; + let offset = match first_prefix.trim() { + "" => 0, + _ => first_prefix + .chars() + .rev() + .take_while(|c| c.is_whitespace()) + .count() as isize, + }; + + for (id, line) in self.src[start.line..end.line].iter().enumerate() { writeln!(self.buf)?; // trim the leading whitespace + let line = match id { + x if x == (end.line - start.line) - 1 => &line[..end.column], + _ => line, + }; + if offset < 0 { for _ in 0..-offset { write!(self.buf, " ")?; @@ -39,6 +86,32 @@ impl Buffer { } } + // let first = &self.src[start.line - 1]; + // let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize; + // let offset = num_spaces_real - num_spaces_desired; + + // for (row, line) in self.src[start.line - 1..end.line].iter().enumerate() { + // let line = match row { + // 0 => &line[start.column - 1..], + // a if a == (end.line - start.line) => &line[..end.column - 1], + // _ => line, + // }; + + // writeln!(self.buf)?; + // // trim the leading whitespace + // if offset < 0 { + // for _ in 0..-offset { + // write!(self.buf, " ")?; + // } + + // write!(self.buf, "{}", line)?; + // } else { + // let offset = offset as usize; + // let right = &line[offset..]; + // write!(self.buf, "{}", right)?; + // } + // } + Ok(()) } } diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 7a14eae3e..aca9f64b0 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -124,7 +124,7 @@ pub fn apply_format(input: &str, block: FormattedBlock) -> String { let (left, _) = input.split_at(start); let (_, right) = input.split_at(end); - dbg!(&block.formatted); + // dbg!(&block.formatted); format!("{}{}{}", left, block.formatted, right) } diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs index 29332b7da..1f9c359c1 100644 --- a/packages/autofmt/tests/samples.rs +++ b/packages/autofmt/tests/samples.rs @@ -23,3 +23,10 @@ twoway! ("complex" => complex); twoway! ("tiny" => tiny); twoway! ("tinynoopt" => tinynoopt); + +twoway! ("long" => long); + +twoway! ("key" => key); + +// Disabled because we can't handle comments on exprs yet +twoway! ("multirsx" => multirsx); diff --git a/packages/autofmt/tests/samples/complex.rsx b/packages/autofmt/tests/samples/complex.rsx index d3e45c67b..598d08a82 100644 --- a/packages/autofmt/tests/samples/complex.rsx +++ b/packages/autofmt/tests/samples/complex.rsx @@ -27,9 +27,7 @@ rsx! { } }) } - div { class: "px-4", - is_current.then(|| rsx!{ children }) - } + div { class: "px-4", is_current.then(|| rsx!{ children }) } } // No nesting @@ -47,4 +45,6 @@ rsx! { let blah = 120; } } + + div { asdbascasdbasd, asbdasbdabsd, asbdabsdbasdbas } } diff --git a/packages/autofmt/tests/samples/key.rsx b/packages/autofmt/tests/samples/key.rsx new file mode 100644 index 000000000..c97b6db38 --- /dev/null +++ b/packages/autofmt/tests/samples/key.rsx @@ -0,0 +1,9 @@ +rsx! { + li { key: "{link}", + Link { class: "py-1 px-2 {hover} {hover_bg}", to: "{link}", "{name}" } + } + + li { key: "{link}", asd: "asd", + Link { class: "py-1 px-2 {hover} {hover_bg}", to: "{link}", "{name}" } + } +} diff --git a/packages/autofmt/tests/samples/long.rsx b/packages/autofmt/tests/samples/long.rsx new file mode 100644 index 000000000..2464a1905 --- /dev/null +++ b/packages/autofmt/tests/samples/long.rsx @@ -0,0 +1,38 @@ +use dioxus::prelude::*; + +#[inline_props] +pub fn Explainer<'a>( + cx: Scope<'a>, + invert: bool, + title: &'static str, + content: Element<'a>, + flasher: Element<'a>, +) -> Element { + // pt-5 sm:pt-24 lg:pt-24 + + let mut right = rsx! { + div { class: "relative w-1/2", flasher } + }; + + let align = match invert { + true => "mr-auto ml-16", + false => "ml-auto mr-16", + }; + + let mut left = rsx! { + div { class: "relative w-1/2 {align} max-w-md leading-8", + h2 { class: "mb-6 text-3xl leading-tight md:text-4xl md:leading-tight lg:text-3xl lg:leading-tight font-heading font-mono font-bold", + "{title}" + } + content + } + }; + + if *invert { + std::mem::swap(&mut left, &mut right); + } + + cx.render(rsx! { + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } + }) +} diff --git a/packages/autofmt/tests/samples/multirsx.rsx b/packages/autofmt/tests/samples/multirsx.rsx new file mode 100644 index 000000000..380ef3487 --- /dev/null +++ b/packages/autofmt/tests/samples/multirsx.rsx @@ -0,0 +1,25 @@ +rsx! { + + // hi + div {} + + // hi + div { abcd, ball, s } + + // + // + // + div { abcd, ball, s } + + // + // + // + div { + abcd, + ball, + s, + + // + "asdasd" + } +} diff --git a/packages/autofmt/tests/samples/simple.rsx b/packages/autofmt/tests/samples/simple.rsx index a60d69f2a..54ff5f867 100644 --- a/packages/autofmt/tests/samples/simple.rsx +++ b/packages/autofmt/tests/samples/simple.rsx @@ -1,9 +1,6 @@ rsx! { div { "hello world!" } - div { - "hello world!" - "goodbye world!" - } + div { "hello world!", "goodbye world!" } // Simple div div { "hello world!" } @@ -15,7 +12,16 @@ rsx! { div { div { "nested" } } // Nested two level - div { div { h1 { "highly nested" } } } + div { + div { h1 { "highly nested" } } + } + + // Anti-Nested two level + div { + div { + div { h1 { "highly nested" } } + } + } // Compression h3 { class: "mb-2 text-xl font-bold", "Invite Member" } @@ -30,7 +36,9 @@ rsx! { img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" } // One level compression - div { a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" } } + div { + a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" } + } // Components Component { ..Props {} } diff --git a/packages/autofmt/tests/wrong.rs b/packages/autofmt/tests/wrong.rs index 4666da79c..acf930b40 100644 --- a/packages/autofmt/tests/wrong.rs +++ b/packages/autofmt/tests/wrong.rs @@ -14,3 +14,5 @@ macro_rules! twoway { twoway!("comments" => comments); twoway!("multi" => multi); + +twoway!("multiexpr" => multiexpr); diff --git a/packages/autofmt/tests/wrong/multiexpr.rsx b/packages/autofmt/tests/wrong/multiexpr.rsx new file mode 100644 index 000000000..1c3f38434 --- /dev/null +++ b/packages/autofmt/tests/wrong/multiexpr.rsx @@ -0,0 +1,3 @@ +cx.render(rsx! { + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } +}) diff --git a/packages/autofmt/tests/wrong/multiexpr.wrong.rsx b/packages/autofmt/tests/wrong/multiexpr.wrong.rsx new file mode 100644 index 000000000..1c3f38434 --- /dev/null +++ b/packages/autofmt/tests/wrong/multiexpr.wrong.rsx @@ -0,0 +1,3 @@ +cx.render(rsx! { + div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right } +}) diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index b614acb43..56a3407f7 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -194,7 +194,7 @@ mod field_info { // children field is automatically defaulted to None if name == "children" { builder_attr.default = - Some(syn::parse(quote!(::dioxus::core::VNode::empty()).into()).unwrap()); + Some(syn::parse(quote!(Default::default()).into()).unwrap()); } // auto detect optional diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 69e460044..8701005ec 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -31,16 +31,16 @@ futures-channel = "0.3.21" indexmap = "1.7" -# Serialize the Edits for use in Webview/Liveview instances -serde = { version = "1", features = ["derive"], optional = true } -anyhow = "1.0.66" - smallbox = "0.8.1" log = "0.4.17" +# Serialize the Edits for use in Webview/Liveview instances +serde = { version = "1", features = ["derive"], optional = true } + [dev-dependencies] tokio = { version = "*", features = ["full"] } dioxus = { path = "../dioxus" } +pretty_assertions = "1.3.0" [features] default = [] diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 380cfd90c..4cb5d3964 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -81,12 +81,12 @@ impl VirtualDom { self.ensure_drop_safety(id); if let Some(root) = self.scopes[id.0].as_ref().try_root_node() { - if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } { + if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { self.drop_scope_inner(node) } } if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } { - if let RenderReturn::Sync(Ok(node)) = unsafe { root.extend_lifetime_ref() } { + if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { self.drop_scope_inner(node) } } @@ -115,10 +115,14 @@ impl VirtualDom { nodes.iter().for_each(|node| self.drop_scope_inner(node)) } DynamicNode::Placeholder(t) => { - self.try_reclaim(t.id.get().unwrap()); + if let Some(id) = t.id.get() { + self.try_reclaim(id); + } } DynamicNode::Text(t) => { - self.try_reclaim(t.id.get().unwrap()); + if let Some(id) = t.id.get() { + self.try_reclaim(id); + } } }); @@ -132,8 +136,8 @@ impl VirtualDom { } /// Descend through the tree, removing any borrowed props and listeners - pub(crate) fn ensure_drop_safety(&self, scope: ScopeId) { - let scope = &self.scopes[scope.0]; + pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) { + let scope = &self.scopes[scope_id.0]; // make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we // run the hooks (which hold an &mut Reference) @@ -141,10 +145,13 @@ impl VirtualDom { let mut props = scope.borrowed_props.borrow_mut(); props.drain(..).for_each(|comp| { let comp = unsafe { &*comp }; - if let Some(scope_id) = comp.scope.get() { - self.ensure_drop_safety(scope_id); + match comp.scope.get() { + Some(child) if child != scope_id => self.ensure_drop_safety(child), + _ => (), + } + if let Ok(mut props) = comp.props.try_borrow_mut() { + *props = None; } - drop(comp.props.take()); }); // Now that all the references are gone, we can safely drop our own references in our listeners. diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 0b8eff3d2..423c923db 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -380,8 +380,8 @@ impl<'b> VirtualDom { use RenderReturn::*; match return_nodes { - Sync(Ok(t)) => self.mount_component(scope, template, t, idx), - Sync(Err(_e)) => todo!("Propogate error upwards"), + Sync(Some(t)) => self.mount_component(scope, template, t, idx), + Sync(None) => todo!("Propogate error upwards"), Async(_) => self.mount_component_placeholder(template, idx, scope), } } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 5e0ec299f..af262948e 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -33,26 +33,26 @@ impl<'b> VirtualDom { use RenderReturn::{Async, Sync}; match (old, new) { - (Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r), + (Sync(Some(l)), Sync(Some(r))) => self.diff_node(l, r), // Err cases - (Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e), - (Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r), - (Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ } + (Sync(Some(l)), Sync(None)) => self.diff_ok_to_err(l), + (Sync(None), Sync(Some(r))) => self.diff_err_to_ok(r), + (Sync(None), Sync(None)) => { /* nothing */ } // Async - (Sync(Ok(_l)), Async(_)) => todo!(), - (Sync(Err(_e)), Async(_)) => todo!(), - (Async(_), Sync(Ok(_r))) => todo!(), - (Async(_), Sync(Err(_e))) => { /* nothing */ } + (Sync(Some(_l)), Async(_)) => todo!(), + (Sync(None), Async(_)) => todo!(), + (Async(_), Sync(Some(_r))) => todo!(), + (Async(_), Sync(None)) => { /* nothing */ } (Async(_), Async(_)) => { /* nothing */ } }; } self.scope_stack.pop(); } - fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {} - fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {} + fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>) {} + fn diff_err_to_ok(&mut self, _l: &'b VNode<'b>) {} fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { // If the templates are the same, we don't need to do anything, nor do we want to @@ -149,22 +149,7 @@ impl<'b> VirtualDom { // Replace components that have different render fns if left.render_fn != right.render_fn { - let created = self.create_component_node(right_template, right, idx); - let head = unsafe { - self.scopes[left.scope.get().unwrap().0] - .root_node() - .extend_lifetime_ref() - }; - let last = match head { - RenderReturn::Sync(Ok(node)) => self.find_last_element(node), - _ => todo!(), - }; - self.mutations.push(Mutation::InsertAfter { - id: last, - m: created, - }); - self.remove_component_node(left, true); - return; + return self.replace_vcomponent(right_template, right, idx, left); } // Make sure the new vcomponent has the right scopeid associated to it @@ -197,6 +182,26 @@ impl<'b> VirtualDom { }); } + fn replace_vcomponent( + &mut self, + right_template: &'b VNode<'b>, + right: &'b VComponent<'b>, + idx: usize, + left: &'b VComponent<'b>, + ) { + let m = self.create_component_node(right_template, right, idx); + + self.remove_component_node(left, true); + + // We want to optimize the replace case to use one less mutation if possible + // Since mutations are done in reverse, the last node removed will be the first in the stack + // Instead of *just* removing it, we can use the replace mutation + match self.mutations.edits.pop().unwrap() { + Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m }), + at => panic!("Expected remove mutation from remove_node {:#?}", at), + }; + } + /// Lightly diff the two templates, checking only their roots. /// /// The goal here is to preserve any existing component state that might exist. This is to preserve some React-like @@ -668,7 +673,7 @@ impl<'b> VirtualDom { Component(comp) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node), + RenderReturn::Sync(Some(node)) => self.push_all_real_nodes(node), _ => todo!(), } } @@ -706,11 +711,21 @@ impl<'b> VirtualDom { fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator>) { let m = self.create_children(right); - let id = self.find_last_element(left); - - self.mutations.push(Mutation::InsertAfter { id, m }); + let pre_edits = self.mutations.edits.len(); self.remove_node(left, true); + + // We should always have a remove mutation + // Eventually we don't want to generate placeholders, so this might not be true. But it's true today + assert!(self.mutations.edits.len() > pre_edits); + + // We want to optimize the replace case to use one less mutation if possible + // Since mutations are done in reverse, the last node removed will be the first in the stack + // Instead of *just* removing it, we can use the replace mutation + match self.mutations.edits.pop().unwrap() { + Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m }), + _ => panic!("Expected remove mutation from remove_node"), + }; } fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) { @@ -719,24 +734,45 @@ impl<'b> VirtualDom { r.id.set(Some(placeholder)); - let id = self.find_last_element(&l[0]); - self.mutations .push(Mutation::CreatePlaceholder { id: placeholder }); - self.mutations.push(Mutation::InsertAfter { id, m: 1 }); - self.remove_nodes(l); + + // We want to optimize the replace case to use one less mutation if possible + // Since mutations are done in reverse, the last node removed will be the first in the stack + // Instead of *just* removing it, we can use the replace mutation + match self.mutations.edits.pop().unwrap() { + Mutation::Remove { id } => self.mutations.push(Mutation::ReplaceWith { id, m: 1 }), + _ => panic!("Expected remove mutation from remove_node"), + }; } /// Remove these nodes from the dom /// Wont generate mutations for the inner nodes fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) { - nodes.iter().for_each(|node| self.remove_node(node, true)); + nodes + .iter() + .rev() + .for_each(|node| self.remove_node(node, true)); } fn remove_node(&mut self, node: &'b VNode<'b>, gen_muts: bool) { + // Clean up any attributes that have claimed a static node as dynamic for mount/unmounta + // Will not generate mutations! + self.reclaim_attributes(node); + + // Remove the nested dynamic nodes + // We don't generate mutations for these, as they will be removed by the parent (in the next line) + // But we still need to make sure to reclaim them from the arena and drop their hooks, etc + self.remove_nested_dyn_nodes(node); + // Clean up the roots, assuming we need to generate mutations for these + // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim) + self.reclaim_roots(node, gen_muts); + } + + fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) { for (idx, _) in node.template.roots.iter().enumerate() { if let Some(dy) = node.dynamic_root(idx) { self.remove_dynamic_node(dy, gen_muts); @@ -748,17 +784,9 @@ impl<'b> VirtualDom { self.reclaim(id); } } + } - for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() { - // Roots are cleaned up automatically above - if node.template.node_paths[idx].len() == 1 { - continue; - } - - self.remove_dynamic_node(dyn_node, false); - } - - // we clean up nodes with dynamic attributes, provided the node is unique and not a root node + fn reclaim_attributes(&mut self, node: &VNode) { let mut id = None; for (idx, attr) in node.dynamic_attrs.iter().enumerate() { // We'll clean up the root nodes either way, so don't worry @@ -778,49 +806,69 @@ impl<'b> VirtualDom { } } + fn remove_nested_dyn_nodes(&mut self, node: &VNode) { + for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() { + // Roots are cleaned up automatically above + if node.template.node_paths[idx].len() == 1 { + continue; + } + + self.remove_dynamic_node(dyn_node, false); + } + } + fn remove_dynamic_node(&mut self, node: &DynamicNode, gen_muts: bool) { match node { Component(comp) => self.remove_component_node(comp, gen_muts), - Text(t) => self.remove_text_node(t), - Placeholder(t) => self.remove_placeholder(t), + Text(t) => self.remove_text_node(t, gen_muts), + Placeholder(t) => self.remove_placeholder(t, gen_muts), Fragment(nodes) => nodes .iter() .for_each(|node| self.remove_node(node, gen_muts)), }; } - fn remove_placeholder(&mut self, t: &VPlaceholder) { + fn remove_placeholder(&mut self, t: &VPlaceholder, gen_muts: bool) { if let Some(id) = t.id.take() { + if gen_muts { + self.mutations.push(Mutation::Remove { id }); + } self.reclaim(id) } } - fn remove_text_node(&mut self, t: &VText) { + fn remove_text_node(&mut self, t: &VText, gen_muts: bool) { if let Some(id) = t.id.take() { + if gen_muts { + self.mutations.push(Mutation::Remove { id }); + } self.reclaim(id) } } fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) { - if let Some(scope) = comp.scope.take() { - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(t)) => self.remove_node(t, gen_muts), - _ => todo!("cannot handle nonstandard nodes"), - }; + let scope = comp.scope.take().unwrap(); - let props = self.scopes[scope.0].props.take(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Some(t)) => { + println!("Removing component node sync {:?}", gen_muts); + self.remove_node(t, gen_muts) + } + _ => todo!("cannot handle nonstandard nodes"), + }; - self.dirty_scopes.remove(&DirtyScope { - height: self.scopes[scope.0].height, - id: scope, - }); + let props = self.scopes[scope.0].props.take(); - *comp.props.borrow_mut() = unsafe { std::mem::transmute(props) }; + self.dirty_scopes.remove(&DirtyScope { + height: self.scopes[scope.0].height, + id: scope, + }); - // make sure to wipe any of its props and listeners - self.ensure_drop_safety(scope); - self.scopes.remove(scope.0); - } + *comp.props.borrow_mut() = unsafe { std::mem::transmute(props) }; + + // make sure to wipe any of its props and listeners + self.ensure_drop_safety(scope); + self.scopes.remove(scope.0); } fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { @@ -832,7 +880,7 @@ impl<'b> VirtualDom { Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(t)) => self.find_first_element(t), + RenderReturn::Sync(Some(t)) => self.find_first_element(t), _ => todo!("cannot handle nonstandard nodes"), } } @@ -848,7 +896,7 @@ impl<'b> VirtualDom { Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(t)) => self.find_last_element(t), + RenderReturn::Sync(Some(t)) => self.find_last_element(t), _ => todo!("cannot handle nonstandard nodes"), } } diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index 6f40c2484..fd8810f1a 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -5,7 +5,7 @@ use crate::ScopeId; /// A boundary that will capture any errors from child components #[allow(dead_code)] pub struct ErrorBoundary { - error: RefCell>, + error: RefCell>, id: ScopeId, } diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index 776d3e37a..d36031556 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -27,8 +27,8 @@ use crate::innerlude::*; /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. #[allow(non_upper_case_globals, non_snake_case)] pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { - let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?; - Ok(VNode { + let children = cx.props.0.as_ref()?; + Some(VNode { key: children.key, parent: children.parent, template: children.template, @@ -95,7 +95,7 @@ impl<'a> Properties for FragmentProps<'a> { type Builder = FragmentBuilder<'a, false>; const IS_STATIC: bool = false; fn builder() -> Self::Builder { - FragmentBuilder(VNode::empty()) + FragmentBuilder(None) } unsafe fn memoize(&self, _other: &Self) -> bool { false diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 15c59c137..d6ae70c04 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -34,10 +34,10 @@ pub(crate) mod innerlude { pub use crate::scopes::*; pub use crate::virtual_dom::*; - /// An [`Element`] is a possibly-errored [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`]. + /// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`Scope`] or [`ScopeState`]. /// /// An Errored [`Element`] will propagate the error to the nearest error boundary. - pub type Element<'a> = Result, anyhow::Error>; + pub type Element<'a> = Option>; /// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`]. /// diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 33f2e5cf2..8fa33e526 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -58,7 +58,7 @@ pub struct VNode<'a> { impl<'a> VNode<'a> { /// Create a template with no nodes that will be skipped over during diffing pub fn empty() -> Element<'a> { - Ok(VNode { + Some(VNode { key: None, parent: None, root_ids: &[], @@ -602,16 +602,6 @@ impl<'a> IntoDynNode<'a> for DynamicNode<'a> { } } -// An element that's an error is currently lost into the ether -impl<'a> IntoDynNode<'a> for Element<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - match self { - Ok(val) => val.into_vnode(_cx), - _ => DynamicNode::default(), - } - } -} - impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { match self { @@ -624,7 +614,7 @@ impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { impl<'a> IntoDynNode<'a> for &Element<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { match self.as_ref() { - Ok(val) => val.clone().into_vnode(_cx), + Some(val) => val.clone().into_vnode(_cx), _ => DynamicNode::default(), } } @@ -678,7 +668,7 @@ impl<'a> IntoTemplate<'a> for VNode<'a> { impl<'a> IntoTemplate<'a> for Element<'a> { fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { match self { - Ok(val) => val.into_template(_cx), + Some(val) => val.into_template(_cx), _ => VNode::empty().unwrap(), } } diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 8ae320e70..8dff17b01 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -78,7 +78,7 @@ impl VirtualDom { fiber.waiting_on.borrow_mut().remove(&id); - if let RenderReturn::Sync(Ok(template)) = ret { + if let RenderReturn::Sync(Some(template)) = ret { let mutations_ref = &mut fiber.mutations.borrow_mut(); let mutations = &mut **mutations_ref; let template: &VNode = unsafe { std::mem::transmute(template) }; diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 84891384b..83428fd7d 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -390,7 +390,7 @@ impl<'src> ScopeState { } } - Ok(element) + Some(element) } /// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index ea6556b38..c862a3e39 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -477,7 +477,7 @@ impl VirtualDom { pub fn rebuild(&mut self) -> Mutations { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root - RenderReturn::Sync(Ok(node)) => { + RenderReturn::Sync(Some(node)) => { let m = self.create_scope(ScopeId(0), node); self.mutations.edits.push(Mutation::AppendChildren { id: ElementId(0), @@ -485,7 +485,7 @@ impl VirtualDom { }); } // If an error occurs, we should try to render the default error component and context where the error occured - RenderReturn::Sync(Err(e)) => panic!("Cannot catch errors during rebuild {:?}", e), + RenderReturn::Sync(None) => panic!("Cannot catch errors during rebuild"), RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"), } diff --git a/packages/core/tests/attr_cleanup.rs b/packages/core/tests/attr_cleanup.rs index c8e1baa1f..89f79abfb 100644 --- a/packages/core/tests/attr_cleanup.rs +++ b/packages/core/tests/attr_cleanup.rs @@ -54,7 +54,7 @@ fn attrs_cycle() { assert_eq!( dom.render_immediate().santize().edits, [ - LoadTemplate { name: "template", index: 0, id: ElementId(3) }, + LoadTemplate { name: "template", index: 0, id: ElementId(1) }, ReplaceWith { id: ElementId(2), m: 1 } ] ); @@ -64,15 +64,10 @@ fn attrs_cycle() { dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2) }, - AssignId { path: &[0], id: ElementId(1) }, - SetAttribute { - name: "class", - value: "3".into_value(&bump), - id: ElementId(1), - ns: None - }, - SetAttribute { name: "id", value: "3".into_value(&bump), id: ElementId(1), ns: None }, - ReplaceWith { id: ElementId(3), m: 1 } + AssignId { path: &[0], id: ElementId(3) }, + SetAttribute { name: "class", value: "3", id: ElementId(3), ns: None }, + SetAttribute { name: "id", value: "3", id: ElementId(3), ns: None }, + ReplaceWith { id: ElementId(1), m: 1 } ] ); diff --git a/packages/core/tests/bubble_error.rs b/packages/core/tests/bubble_error.rs index ebbbc613c..7d6966c46 100644 --- a/packages/core/tests/bubble_error.rs +++ b/packages/core/tests/bubble_error.rs @@ -9,7 +9,7 @@ fn app(cx: Scope) -> Element { _ => unreachable!(), }; - let value = raw.parse::()?; + let value = raw.parse::().unwrap_or(123.123); cx.render(rsx! { div { "hello {value}" } diff --git a/packages/core/tests/diff_unkeyed_list.rs b/packages/core/tests/diff_unkeyed_list.rs index 84fd65604..4934f6080 100644 --- a/packages/core/tests/diff_unkeyed_list.rs +++ b/packages/core/tests/diff_unkeyed_list.rs @@ -1,5 +1,6 @@ use dioxus::core::{ElementId, Mutation::*}; use dioxus::prelude::*; +use pretty_assertions::assert_eq; #[test] fn list_creates_one_by_one() { @@ -125,7 +126,7 @@ fn removes_one_by_one() { assert_eq!( dom.render_immediate().santize().edits, [ - CreatePlaceholder { id: ElementId(3) }, + CreatePlaceholder { id: ElementId(4) }, ReplaceWith { id: ElementId(2), m: 1 } ] ); @@ -137,12 +138,12 @@ fn removes_one_by_one() { dom.render_immediate().santize().edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2) }, - HydrateText { path: &[0], value: "0", id: ElementId(4) }, + HydrateText { path: &[0], value: "0", id: ElementId(3) }, LoadTemplate { name: "template", index: 0, id: ElementId(5) }, HydrateText { path: &[0], value: "1", id: ElementId(6) }, LoadTemplate { name: "template", index: 0, id: ElementId(7) }, HydrateText { path: &[0], value: "2", id: ElementId(8) }, - ReplaceWith { id: ElementId(3), m: 3 } + ReplaceWith { id: ElementId(4), m: 3 } ] ); } @@ -264,9 +265,9 @@ fn removes_one_by_one_multiroot() { assert_eq!( dom.render_immediate().santize().edits, [ - Remove { id: ElementId(4) }, - CreatePlaceholder { id: ElementId(5) }, - ReplaceWith { id: ElementId(2), m: 1 } + CreatePlaceholder { id: ElementId(8) }, + Remove { id: ElementId(2) }, + ReplaceWith { id: ElementId(4), m: 1 } ] ); } @@ -364,11 +365,11 @@ fn remove_many() { assert_eq!( edits.edits, [ + CreatePlaceholder { id: ElementId(11,) }, Remove { id: ElementId(9,) }, Remove { id: ElementId(7,) }, Remove { id: ElementId(5,) }, Remove { id: ElementId(1,) }, - CreatePlaceholder { id: ElementId(3,) }, ReplaceWith { id: ElementId(2,), m: 1 }, ] ); @@ -381,8 +382,8 @@ fn remove_many() { edits.edits, [ LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, - HydrateText { path: &[0,], value: "hello 0", id: ElementId(1,) }, - ReplaceWith { id: ElementId(3,), m: 1 }, + HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) }, + ReplaceWith { id: ElementId(11,), m: 1 }, ] ) } diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 060e95ce2..4456eb2d7 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -21,6 +21,7 @@ pub struct Config { pub(crate) resource_dir: Option, pub(crate) custom_head: Option, pub(crate) custom_index: Option, + pub(crate) root_name: String, } type DropHandler = Box bool>; @@ -46,6 +47,7 @@ impl Config { resource_dir: None, custom_head: None, custom_index: None, + root_name: "main".to_string(), } } @@ -126,6 +128,14 @@ impl Config { self.custom_index = Some(index); self } + + /// Set the name of the element that Dioxus will use as the root. + /// + /// This is akint to calling React.render() on the element with the specified name. + pub fn with_root_name(mut self, name: impl Into) -> Self { + self.root_name = name.into(); + self + } } impl Default for Config { diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 9037189b1..c732f5a8b 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -1,6 +1,6 @@ use crate::desktop_context::{DesktopContext, UserWindowEvent}; -use crate::events::{decode_event, EventMessage}; use dioxus_core::*; +use dioxus_html::HtmlEvent; use futures_channel::mpsc::{unbounded, UnboundedSender}; use futures_util::StreamExt; #[cfg(target_os = "ios")] @@ -50,6 +50,7 @@ impl DesktopController { std::thread::spawn(move || { // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads // I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it + let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() @@ -69,12 +70,14 @@ impl DesktopController { tokio::select! { _ = dom.wait_for_work() => {} Some(json_value) = event_rx.next() => { - if let Ok(value) = serde_json::from_value::(json_value) { - let name = value.event.clone(); - let el_id = ElementId(value.mounted_dom_id); - if let Some(evt) = decode_event(value) { - dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name)); - } + if let Ok(value) = serde_json::from_value::(json_value) { + let HtmlEvent { + name, + element, + bubbles, + data + } = value; + dom.handle_event(&name, data.into_any(), element, bubbles); } } } @@ -83,7 +86,10 @@ impl DesktopController { .render_with_deadline(tokio::time::sleep(Duration::from_millis(16))) .await; - edit_queue.lock().unwrap().push(serde_json::to_string(&muts).unwrap()); + edit_queue + .lock() + .unwrap() + .push(serde_json::to_string(&muts).unwrap()); let _ = proxy.send_event(UserWindowEvent::EditsReady); } }) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 756814de0..7b41d33e0 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -7,6 +7,7 @@ use serde_json::Value; use std::future::Future; use std::future::IntoFuture; use std::pin::Pin; +use wry::application::dpi::LogicalSize; use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] @@ -136,6 +137,11 @@ impl DesktopContext { let _ = self.proxy.send_event(SetZoomLevel(scale_factor)); } + /// modifies the inner size of the window + pub fn set_inner_size(&self, logical_size: LogicalSize) { + let _ = self.proxy.send_event(SetInnerSize(logical_size)); + } + /// launch print modal pub fn print(&self) { let _ = self.proxy.send_event(Print); @@ -193,6 +199,7 @@ pub enum UserWindowEvent { SetDecorations(bool), SetZoomLevel(f64), + SetInnerSize(LogicalSize), Print, DevTool, @@ -265,6 +272,7 @@ impl DesktopController { SetDecorations(state) => window.set_decorations(state), SetZoomLevel(scale_factor) => webview.zoom(scale_factor), + SetInnerSize(logical_size) => window.set_inner_size(logical_size), Print => { if let Err(e) = webview.print() { diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index b82d6f749..f9b3fe5cb 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -1,10 +1,6 @@ //! Convert a serialized event to an event trigger -use dioxus_html::events::*; use serde::{Deserialize, Serialize}; -use serde_json::from_value; -use std::any::Any; -use std::rc::Rc; #[derive(Deserialize, Serialize)] pub(crate) struct IpcMessage { @@ -31,61 +27,3 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option { } } } - -macro_rules! match_data { - ( - $m:ident; - $name:ident; - $( - $tip:ty => $($mname:literal)|* ; - )* - ) => { - match $name { - $( $($mname)|* => { - let val: $tip = from_value::<$tip>($m).ok()?; - Rc::new(val) as Rc - })* - _ => return None, - } - }; -} - -#[derive(Deserialize)] -pub struct EventMessage { - pub contents: serde_json::Value, - pub event: String, - pub mounted_dom_id: usize, -} - -pub fn decode_event(value: EventMessage) -> Option> { - let val = value.contents; - let name = value.event.as_str(); - type DragData = MouseData; - - let evt = match_data! { val; name; - MouseData => "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup"; - ClipboardData => "copy" | "cut" | "paste"; - CompositionData => "compositionend" | "compositionstart" | "compositionupdate"; - KeyboardData => "keydown" | "keypress" | "keyup"; - FocusData => "blur" | "focus" | "focusin" | "focusout"; - FormData => "change" | "input" | "invalid" | "reset" | "submit"; - DragData => "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop"; - PointerData => "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" | "lostpointercapture"; - SelectionData => "selectstart" | "selectionchange" | "select"; - TouchData => "touchcancel" | "touchend" | "touchmove" | "touchstart"; - ScrollData => "scroll"; - WheelData => "wheel"; - MediaData => "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" - | "encrypted" | "ended" | "interruptbegin" | "interruptend" | "loadeddata" - | "loadedmetadata" | "loadstart" | "pause" | "play" | "playing" | "progress" - | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" | "timeupdate" - | "volumechange" | "waiting" | "error" | "load" | "loadend" | "timeout"; - AnimationData => "animationstart" | "animationend" | "animationiteration"; - TransitionData => "transitionend"; - ToggleData => "toggle"; - // ImageData => "load" | "error"; - // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange"; - }; - - Some(evt) -} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index a635e3f4e..0080e671b 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -164,6 +164,7 @@ fn build_webview( let custom_head = cfg.custom_head.clone(); let resource_dir = cfg.resource_dir.clone(); let index_file = cfg.custom_index.clone(); + let root_name = cfg.root_name.clone(); // We assume that if the icon is None in cfg, then the user just didnt set it if cfg.window.window.window_icon.is_none() { @@ -183,36 +184,40 @@ fn build_webview( .with_url("dioxus://index.html/") .unwrap() .with_ipc_handler(move |_window: &Window, payload: String| { - parse_ipc_message(&payload) - .map(|message| match message.method() { - "eval_result" => { - let result = message.params(); - eval_sender.send(result).unwrap(); - } - "user_event" => { - _ = event_tx.unbounded_send(message.params()); - } - "initialize" => { - is_ready.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = proxy.send_event(UserWindowEvent::EditsReady); - } - "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) { - log::error!("Open Browser error: {:?}", e); - } + let message = match parse_ipc_message(&payload) { + Some(message) => message, + None => { + log::error!("Failed to parse IPC message: {}", payload); + return; + } + }; + + match message.method() { + "eval_result" => { + let result = message.params(); + eval_sender.send(result).unwrap(); + } + "user_event" => { + _ = event_tx.unbounded_send(message.params()); + } + "initialize" => { + is_ready.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = proxy.send_event(UserWindowEvent::EditsReady); + } + "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) { + log::error!("Open Browser error: {:?}", e); } } } - _ => (), - }) - .unwrap_or_else(|| { - log::warn!("invalid IPC message received"); - }); + } + _ => (), + } }) .with_custom_protocol(String::from("dioxus"), move |r| { protocol::desktop_handler( @@ -220,6 +225,7 @@ fn build_webview( resource_dir.clone(), custom_head.clone(), index_file.clone(), + &root_name, ) }) .with_file_drop_handler(move |window, evet| { diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index a544aa305..8835078f2 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -1,22 +1,34 @@ +use dioxus_interpreter_js::INTERPRETER_JS; use std::path::{Path, PathBuf}; use wry::{ http::{status::StatusCode, Request, Response}, Result, }; -const MODULE_LOADER: &str = r#" +fn module_loader(root_name: &str) -> String { + format!( + r#" -"#; +"#, + root_name + ) +} pub(super) fn desktop_handler( request: &Request>, asset_root: Option, custom_head: Option, custom_index: Option, + root_name: &str, ) -> 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. @@ -30,7 +42,7 @@ pub(super) fn desktop_handler( // 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)) + .replace("", &format!("{}", module_loader(root_name))) .into_bytes(); Response::builder() .header("Content-Type", "text/html") @@ -42,7 +54,7 @@ pub(super) fn desktop_handler( if let Some(custom_head) = custom_head { template = template.replace("", &custom_head); } - template = template.replace("", MODULE_LOADER); + template = template.replace("", &module_loader(root_name)); Response::builder() .header("Content-Type", "text/html") diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index 446da9303..3436dcc93 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -19,6 +19,7 @@ euclid = "0.22.7" enumset = "1.0.11" keyboard-types = "0.6.2" async-trait = "0.1.58" +serde-value = "0.7.0" [dependencies.web-sys] optional = true @@ -39,7 +40,10 @@ features = [ "ClipboardEvent", ] +[dev-dependencies] +serde_json = "*" + [features] -default = [] -serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde"] +default = ["serialize"] +serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"] wasm-bind = ["web-sys", "wasm-bindgen"] diff --git a/packages/html/src/events/animation.rs b/packages/html/src/events/animation.rs index fb6dc64ca..4495191fc 100644 --- a/packages/html/src/events/animation.rs +++ b/packages/html/src/events/animation.rs @@ -3,7 +3,7 @@ use dioxus_core::Event; pub type AnimationEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct AnimationData { pub animation_name: String, pub pseudo_element: String, diff --git a/packages/html/src/events/clipboard.rs b/packages/html/src/events/clipboard.rs index e7031d061..54390c6b0 100644 --- a/packages/html/src/events/clipboard.rs +++ b/packages/html/src/events/clipboard.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type ClipboardEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ClipboardData { // DOMDataTransfer clipboardData } diff --git a/packages/html/src/events/composition.rs b/packages/html/src/events/composition.rs index 59009c1f0..480868f24 100644 --- a/packages/html/src/events/composition.rs +++ b/packages/html/src/events/composition.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type CompositionEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CompositionData { pub data: String, } diff --git a/packages/html/src/events/drag.rs b/packages/html/src/events/drag.rs index 5dd7f5c71..f8ee6ff92 100644 --- a/packages/html/src/events/drag.rs +++ b/packages/html/src/events/drag.rs @@ -1,5 +1,3 @@ -use std::any::Any; - use dioxus_core::Event; use crate::MouseData; @@ -10,12 +8,11 @@ pub type DragEvent = Event; /// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location /// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an /// application-specific way. +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq)] pub struct DragData { /// Inherit mouse data pub mouse: MouseData, - - /// And then add the rest of the drag data - pub data: Box, } impl_event! { diff --git a/packages/html/src/events/focus.rs b/packages/html/src/events/focus.rs index b2a4f5205..2092c972e 100644 --- a/packages/html/src/events/focus.rs +++ b/packages/html/src/events/focus.rs @@ -3,7 +3,7 @@ use dioxus_core::Event; pub type FocusEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FocusData {/* DOMEventInner: Send + SyncTarget relatedTarget */} impl_event! [ diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 5e0b6a17e..3e4e00e0e 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -16,6 +16,12 @@ pub struct FormData { pub files: Option>, } +impl PartialEq for FormData { + fn eq(&self, other: &Self) -> bool { + self.value == other.value && self.values == other.values + } +} + impl Debug for FormData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("FormEvent") diff --git a/packages/html/src/events/image.rs b/packages/html/src/events/image.rs index 06430c4c2..bf5228409 100644 --- a/packages/html/src/events/image.rs +++ b/packages/html/src/events/image.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type ImageEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ImageData { pub load_error: bool, } diff --git a/packages/html/src/events/keyboard.rs b/packages/html/src/events/keyboard.rs index ddf720770..ee803d948 100644 --- a/packages/html/src/events/keyboard.rs +++ b/packages/html/src/events/keyboard.rs @@ -7,7 +7,7 @@ use std::str::FromStr; pub type KeyboardEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub struct KeyboardData { #[deprecated( since = "0.3.0", diff --git a/packages/html/src/events/media.rs b/packages/html/src/events/media.rs index 0b2fe70e0..d2ba71c08 100644 --- a/packages/html/src/events/media.rs +++ b/packages/html/src/events/media.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type MediaEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct MediaData {} impl_event! [ diff --git a/packages/html/src/events/mouse.rs b/packages/html/src/events/mouse.rs index a0fda7be6..f8446b7d1 100644 --- a/packages/html/src/events/mouse.rs +++ b/packages/html/src/events/mouse.rs @@ -10,7 +10,7 @@ pub type MouseEvent = Event; /// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Default)] +#[derive(Clone, Default, PartialEq)] /// Data associated with a mouse event /// /// Do not use the deprecated fields; they may change or become private in the future. diff --git a/packages/html/src/events/pointer.rs b/packages/html/src/events/pointer.rs index 23b0aba71..4134ac852 100644 --- a/packages/html/src/events/pointer.rs +++ b/packages/html/src/events/pointer.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type PointerEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct PointerData { // Mouse only pub alt_key: bool, diff --git a/packages/html/src/events/scroll.rs b/packages/html/src/events/scroll.rs index 0c2dd3143..798218b02 100644 --- a/packages/html/src/events/scroll.rs +++ b/packages/html/src/events/scroll.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type ScrollEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ScrollData {} impl_event! { diff --git a/packages/html/src/events/selection.rs b/packages/html/src/events/selection.rs index ff1496c96..249353e63 100644 --- a/packages/html/src/events/selection.rs +++ b/packages/html/src/events/selection.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type SelectionEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SelectionData {} impl_event! [ diff --git a/packages/html/src/events/toggle.rs b/packages/html/src/events/toggle.rs index 1f0d3f6e7..4f225a8e6 100644 --- a/packages/html/src/events/toggle.rs +++ b/packages/html/src/events/toggle.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type ToggleEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ToggleData {} impl_event! { diff --git a/packages/html/src/events/touch.rs b/packages/html/src/events/touch.rs index 1f219b22f..1e795b6da 100644 --- a/packages/html/src/events/touch.rs +++ b/packages/html/src/events/touch.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type TouchEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TouchData { pub alt_key: bool, pub ctrl_key: bool, diff --git a/packages/html/src/events/transition.rs b/packages/html/src/events/transition.rs index 095496664..ecc3e1636 100644 --- a/packages/html/src/events/transition.rs +++ b/packages/html/src/events/transition.rs @@ -2,7 +2,7 @@ use dioxus_core::Event; pub type TransitionEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct TransitionData { pub property_name: String, pub pseudo_element: String, diff --git a/packages/html/src/events/wheel.rs b/packages/html/src/events/wheel.rs index e6e06501b..758fa7936 100644 --- a/packages/html/src/events/wheel.rs +++ b/packages/html/src/events/wheel.rs @@ -6,7 +6,7 @@ use crate::geometry::{LinesVector, PagesVector, PixelsVector, WheelDelta}; pub type WheelEvent = Event; #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone)] +#[derive(Clone, PartialEq, Default)] pub struct WheelData { #[deprecated(since = "0.3.0", note = "use delta() instead")] pub delta_mode: u32, diff --git a/packages/html/src/lib.rs b/packages/html/src/lib.rs index ab2e49f1e..577899fe1 100644 --- a/packages/html/src/lib.rs +++ b/packages/html/src/lib.rs @@ -22,6 +22,12 @@ mod render_template; #[cfg(feature = "wasm-bind")] mod web_sys_bind; +#[cfg(feature = "serialize")] +mod transit; + +#[cfg(feature = "serialize")] +pub use transit::*; + pub use elements::*; pub use events::*; pub use global_attributes::*; diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs new file mode 100644 index 000000000..651b7e4f6 --- /dev/null +++ b/packages/html/src/transit.rs @@ -0,0 +1,217 @@ +use std::{any::Any, rc::Rc}; + +use crate::events::*; +use dioxus_core::ElementId; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct HtmlEvent { + pub element: ElementId, + pub name: String, + pub bubbles: bool, + pub data: EventData, +} + +impl<'de> Deserialize<'de> for HtmlEvent { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize, Debug, Clone)] + struct Inner { + element: ElementId, + name: String, + bubbles: bool, + data: serde_value::Value, + } + + let Inner { + element, + name, + bubbles, + data, + } = Inner::deserialize(deserializer)?; + + Ok(HtmlEvent { + data: fun_name(&name, data).unwrap(), + element, + bubbles, + name, + }) + } +} + +fn fun_name( + name: &str, + data: serde_value::Value, +) -> Result { + use EventData::*; + + // a little macro-esque thing to make the code below more readable + #[inline] + fn de<'de, F>(f: serde_value::Value) -> Result + where + F: Deserialize<'de>, + { + F::deserialize(f) + } + + let data = match name { + // Mouse + "click" | "contextmenu" | "dblclick" | "doubleclick" | "mousedown" | "mouseenter" + | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => Mouse(de(data)?), + + // Clipboard + "copy" | "cut" | "paste" => Clipboard(de(data)?), + + // Composition + "compositionend" | "compositionstart" | "compositionupdate" => Composition(de(data)?), + + // Keyboard + "keydown" | "keypress" | "keyup" => Keyboard(de(data)?), + + // Focus + "blur" | "focus" | "focusin" | "focusout" => Focus(de(data)?), + + // Form + "change" | "input" | "invalid" | "reset" | "submit" => Form(de(data)?), + + // Drag + "drag" | "dragend" | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart" + | "drop" => Drag(de(data)?), + + // Pointer + "pointerlockchange" | "pointerlockerror" | "pointerdown" | "pointermove" | "pointerup" + | "pointerover" | "pointerout" | "pointerenter" | "pointerleave" | "gotpointercapture" + | "lostpointercapture" => Pointer(de(data)?), + + // Selection + "selectstart" | "selectionchange" | "select" => Selection(de(data)?), + + // Touch + "touchcancel" | "touchend" | "touchmove" | "touchstart" => Touch(de(data)?), + + // Srcoll + "scroll" => Scroll(de(data)?), + + // Wheel + "wheel" => Wheel(de(data)?), + + // Media + "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" + | "ended" | "interruptbegin" | "interruptend" | "loadeddata" | "loadedmetadata" + | "loadstart" | "pause" | "play" | "playing" | "progress" | "ratechange" | "seeked" + | "seeking" | "stalled" | "suspend" | "timeupdate" | "volumechange" | "waiting" + | "error" | "load" | "loadend" | "timeout" => Media(de(data)?), + + // Animation + "animationstart" | "animationend" | "animationiteration" => Animation(de(data)?), + + // Transition + "transitionend" => Transition(de(data)?), + + // Toggle + "toggle" => Toggle(de(data)?), + + // ImageData => "load" | "error"; + // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange"; + other => { + return Err(serde_value::DeserializerError::UnknownVariant( + other.to_string(), + &[], + )) + } + }; + + Ok(data) +} + +impl HtmlEvent { + pub fn bubbles(&self) -> bool { + event_bubbles(&self.name) + } +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(untagged)] +pub enum EventData { + Mouse(MouseData), + Clipboard(ClipboardData), + Composition(CompositionData), + Keyboard(KeyboardData), + Focus(FocusData), + Form(FormData), + Drag(DragData), + Pointer(PointerData), + Selection(SelectionData), + Touch(TouchData), + Scroll(ScrollData), + Wheel(WheelData), + Media(MediaData), + Animation(AnimationData), + Transition(TransitionData), + Toggle(ToggleData), +} + +impl EventData { + pub fn into_any(self) -> Rc { + match self { + EventData::Mouse(data) => Rc::new(data) as Rc, + EventData::Clipboard(data) => Rc::new(data) as Rc, + EventData::Composition(data) => Rc::new(data) as Rc, + EventData::Keyboard(data) => Rc::new(data) as Rc, + EventData::Focus(data) => Rc::new(data) as Rc, + EventData::Form(data) => Rc::new(data) as Rc, + EventData::Drag(data) => Rc::new(data) as Rc, + EventData::Pointer(data) => Rc::new(data) as Rc, + EventData::Selection(data) => Rc::new(data) as Rc, + EventData::Touch(data) => Rc::new(data) as Rc, + EventData::Scroll(data) => Rc::new(data) as Rc, + EventData::Wheel(data) => Rc::new(data) as Rc, + EventData::Media(data) => Rc::new(data) as Rc, + EventData::Animation(data) => Rc::new(data) as Rc, + EventData::Transition(data) => Rc::new(data) as Rc, + EventData::Toggle(data) => Rc::new(data) as Rc, + } + } +} + +#[test] +fn test_back_and_forth() { + let data = HtmlEvent { + element: ElementId(0), + data: EventData::Mouse(MouseData::default()), + name: "click".to_string(), + bubbles: true, + }; + + println!("{}", serde_json::to_string_pretty(&data).unwrap()); + + let o = r#" +{ + "element": 0, + "name": "click", + "bubbles": true, + "data": { + "alt_key": false, + "button": 0, + "buttons": 0, + "client_x": 0, + "client_y": 0, + "ctrl_key": false, + "meta_key": false, + "offset_x": 0, + "offset_y": 0, + "page_x": 0, + "page_y": 0, + "screen_x": 0, + "screen_y": 0, + "shift_key": false + } +} + "#; + + let p: HtmlEvent = serde_json::from_str(o).unwrap(); + + assert_eq!(data, p); +} diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index 3ac52a8f1..e8cc4d039 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -1,11 +1,3 @@ -export function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.ipc.postMessage(serializeIpcMessage("initialize")); - } -} - class ListenerMap { constructor(root) { // bubbling events can listen at the root element @@ -60,7 +52,7 @@ class ListenerMap { } } -export class Interpreter { +class Interpreter { constructor(root) { this.root = root; this.listeners = new ListenerMap(root); @@ -352,6 +344,9 @@ export class Interpreter { this.RemoveEventListener(edit.id, edit.name); break; case "NewEventListener": + + let bubbles = event_bubbles(edit.name); + // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event) => { @@ -435,20 +430,21 @@ export class Interpreter { } window.ipc.postMessage( serializeIpcMessage("user_event", { - event: edit.name, - mounted_dom_id: parseInt(realId), - contents: contents, + name: edit.name, + element: parseInt(realId), + data: contents, + bubbles, }) ); } }; - this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler); + this.NewEventListener(edit.name, edit.id, bubbles, handler); break; } } } -export function serialize_event(event) { +function serialize_event(event) { switch (event.type) { case "copy": case "cut": diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 90f956289..99e43da08 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -10,43 +10,63 @@ description = "Build server-side apps with Dioxus" license = "MIT/Apache-2.0" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { version = "1", features = ["full"] } -futures-util = { version = "0.3", default-features = false, features = [ +tokio = { version = "1.23.0", features = ["full"] } +futures-util = { version = "0.3.25", default-features = false, features = [ "sink", ] } -futures-channel = { version = "0.3.17", features = ["sink"] } -pretty_env_logger = "0.4" -tokio-stream = { version = "0.1.1", features = ["net"] } +futures-channel = { version = "0.3.25", features = ["sink"] } +pretty_env_logger = "0.4.0" +tokio-stream = { version = "0.1.11", features = ["net"] } -serde = { version = "1.0.136", features = ["derive"] } -serde_json = "1.0.79" -tokio-util = { version = "0.7.0", features = ["full"] } +serde = { version = "1.0.151", features = ["derive"] } +serde_json = "1.0.91" +tokio-util = { version = "0.7.4", features = ["full"] } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" } dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" } - +dioxus-interpreter-js = { path = "../interpreter" } # warp -warp = { version = "0.3", optional = true } +warp = { version = "0.3.3", optional = true } # axum -axum = { version = "0.5.1", optional = true, features = ["ws"] } -tower = { version = "0.4.12", optional = true } +axum = { version = "0.6.1", optional = true, features = ["ws"] } +tower = { version = "0.4.13", optional = true } # salvo -salvo = { version = "0.32.0", optional = true, features = ["ws"] } +salvo = { version = "0.37.7", optional = true, features = ["ws"] } +thiserror = "1.0.38" +uuid = { version = "1.2.2", features = ["v4"] } +anyhow = "1.0.68" + +# actix is ... complicated? +# actix-files = { version = "0.6.2", optional = true } +# actix-web = { version = "4.2.1", optional = true } +# actix-ws = { version = "0.2.5", optional = true } [dev-dependencies] -tokio = { version = "1", features = ["full"] } +tokio = { version = "1.23.0", features = ["full"] } dioxus = { path = "../dioxus" } -warp = "0.3" -axum = { version = "0.5.1", features = ["ws"] } -salvo = { version = "0.32.0", features = ["affix", "ws"] } -tower = "0.4.12" +warp = "0.3.3" +axum = { version = "0.6.1", features = ["ws"] } +salvo = { version = "0.37.7", features = ["affix", "ws"] } +tower = "0.4.13" [features] -default = [] \ No newline at end of file +default = [] +# actix = ["actix-files", "actix-web", "actix-ws"] + +[[example]] +name = "axum" +required-features = ["axum"] + +[[example]] +name = "salvo" +required-features = ["salvo"] + +[[example]] +name = "warp" +required-features = ["warp"] diff --git a/packages/liveview/README.md b/packages/liveview/README.md index 6cdd57dcd..46ceabf04 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -1,49 +1,3 @@ # Dioxus LiveView -Enabling server-rendered and hybrid applications with incredibly low latency (<1ms). - -```rust -#[async_std::main] -async fn main() -> tide::Result<()> { - let liveview_pool = dioxus::liveview::pool::default(); - let mut app = tide::new(); - - // serve the liveview client - app.at("/").get(dioxus::liveview::liveview_frontend); - - // and then connect the client to the backend - app.at("/app").get(|req| dioxus::liveview::launch(App, Props { req })) - - app.listen("127.0.0.1:8080").await?; - - Ok(()) -} -``` - -Dioxus LiveView runs your Dioxus apps on the server - - - -```rust -use soyuz::prelude::*; - -#[tokio::main] -async fn main() { - let mut app = soyuz::new(); - app.at("/app").get(websocket(handler)); - app.listen("127.0.0.1:8080").await.unwrap(); -} - -async fn order_shoes(mut req: WebsocketRequest) -> Response { - let stream = req.upgrade(); - dioxus::liveview::launch(App, stream).await; -} - -fn App(cx: Scope) -> Element { - let mut count = use_state(cx, || 0); - cx.render(rsx!( - button { onclick: move |_| count += 1, "Incr" } - button { onclick: move |_| count -= 1, "Decr" } - )) -} -``` +Server rendered apps with minimal latency diff --git a/packages/liveview/examples/axum.rs b/packages/liveview/examples/axum.rs index 450af30ef..410c0e9a4 100644 --- a/packages/liveview/examples/axum.rs +++ b/packages/liveview/examples/axum.rs @@ -1,32 +1,53 @@ -#[cfg(not(feature = "axum"))] -fn main() {} +use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello axum! {num}" + button { onclick: move |_| num += 1, "Increment" } + } + }) +} -#[cfg(feature = "axum")] #[tokio::main] async fn main() { - use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; - use dioxus_core::{Element, LazyNodes, Scope}; pretty_env_logger::init(); - fn app(cx: Scope) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); - let view = dioxus_liveview::new(addr); - let body = view.body("Dioxus Liveview"); + let view = dioxus_liveview::LiveViewPool::new(); let app = Router::new() - .route("/", get(move || async { Html(body) })) .route( - "/app", + "/", + get(move || async move { + Html(format!( + r#" + + + Dioxus LiveView with Warp +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) + )) + }), + ) + .route( + "/ws", get(move |ws: WebSocketUpgrade| async move { ws.on_upgrade(move |socket| async move { - view.upgrade_axum(socket, app).await; + _ = view.launch(dioxus_liveview::axum_socket(socket), app).await; }) }), ); + + println!("Listening on http://{}", addr); + axum::Server::bind(&addr.to_string().parse().unwrap()) .serve(app.into_make_service()) .await diff --git a/packages/liveview/examples/salvo.rs b/packages/liveview/examples/salvo.rs index 132a62633..8cd2c0ca4 100644 --- a/packages/liveview/examples/salvo.rs +++ b/packages/liveview/examples/salvo.rs @@ -1,55 +1,66 @@ -#[cfg(not(feature = "salvo"))] -fn main() {} +use dioxus::prelude::*; +use dioxus_liveview::LiveViewPool; +use salvo::affix; +use salvo::prelude::*; +use std::net::SocketAddr; +use std::sync::Arc; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello salvo! {num}" + button { onclick: move |_| num += 1, "Increment" } + } + }) +} -#[cfg(feature = "salvo")] #[tokio::main] async fn main() { - use std::sync::Arc; - - use dioxus_core::{Element, LazyNodes, Scope}; - use dioxus_liveview as liveview; - use dioxus_liveview::Liveview; - use salvo::extra::affix; - use salvo::extra::ws::WsHandler; - use salvo::prelude::*; - - fn app(cx: Scope) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - pretty_env_logger::init(); - let addr = ([127, 0, 0, 1], 3030); + let addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); + + let view = LiveViewPool::new(); - // todo: compactify this routing under one liveview::app method - let view = liveview::new(addr); let router = Router::new() .hoop(affix::inject(Arc::new(view))) .get(index) - .push(Router::with_path("app").get(connect)); + .push(Router::with_path("ws").get(connect)); + + println!("Listening on http://{}", addr); + Server::new(TcpListener::bind(addr)).serve(router).await; - - #[handler] - fn index(depot: &mut Depot, res: &mut Response) { - let view = depot.obtain::>().unwrap(); - let body = view.body("Dioxus LiveView"); - res.render(Text::Html(body)); - } - - #[handler] - async fn connect( - req: &mut Request, - depot: &mut Depot, - res: &mut Response, - ) -> Result<(), StatusError> { - let view = depot.obtain::>().unwrap().clone(); - let fut = WsHandler::new().handle(req, res)?; - let fut = async move { - if let Some(ws) = fut.await { - view.upgrade_salvo(ws, app).await; - } - }; - tokio::task::spawn(fut); - Ok(()) - } +} + +#[handler] +fn index(_depot: &mut Depot, res: &mut Response) { + let addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); + res.render(Text::Html(format!( + r#" + + + Dioxus LiveView with Warp +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) + ))); +} + +#[handler] +async fn connect( + req: &mut Request, + depot: &mut Depot, + res: &mut Response, +) -> Result<(), StatusError> { + let view = depot.obtain::>().unwrap().clone(); + + WebSocketUpgrade::new() + .upgrade(req, res, |ws| async move { + _ = view.launch(dioxus_liveview::salvo_socket(ws), app).await; + }) + .await } diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index 7b2ddfef5..7e759ea90 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -1,35 +1,56 @@ -#[cfg(not(feature = "warp"))] -fn main() {} +use dioxus::prelude::*; +use dioxus_liveview::adapters::warp_adapter::warp_socket; +use dioxus_liveview::LiveViewPool; +use std::net::SocketAddr; +use warp::ws::Ws; +use warp::Filter; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello warp! {num}" + button { + onclick: move |_| num += 1, + "Increment" + } + } + }) +} -#[cfg(feature = "warp")] #[tokio::main] async fn main() { - use dioxus_core::{Element, LazyNodes, Scope}; - use dioxus_liveview as liveview; - use warp::ws::Ws; - use warp::Filter; - - fn app(cx: Scope) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - pretty_env_logger::init(); - let addr = ([127, 0, 0, 1], 3030); + let addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); - // todo: compactify this routing under one liveview::app method - let view = liveview::new(addr); - let body = view.body("Dioxus LiveView"); + let index = warp::path::end().map(move || { + warp::reply::html(format!( + r#" + + + Dioxus LiveView with Warp +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws/")) + )) + }); - let routes = warp::path::end() - .map(move || warp::reply::html(body.clone())) - .or(warp::path("app") - .and(warp::ws()) - .and(warp::any().map(move || view.clone())) - .map(|ws: Ws, view: liveview::Liveview| { - ws.on_upgrade(|socket| async move { - view.upgrade_warp(socket, app).await; - }) - })); - warp::serve(routes).run(addr).await; + let pool = LiveViewPool::new(); + + let ws = warp::path("ws") + .and(warp::ws()) + .and(warp::any().map(move || pool.clone())) + .map(move |ws: Ws, pool: LiveViewPool| { + ws.on_upgrade(|ws| async move { + let _ = pool.launch(warp_socket(ws), app).await; + }) + }); + + println!("Listening on http://{}", addr); + + warp::serve(index.or(ws)).run(addr).await; } diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index c60b267ea..646cde38e 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -1,94 +1,23 @@ -use crate::events; +use crate::{LiveViewError, LiveViewSocket}; use axum::extract::ws::{Message, WebSocket}; -use dioxus_core::prelude::*; -use futures_util::{ - future::{select, Either}, - pin_mut, SinkExt, StreamExt, -}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tokio_util::task::LocalPoolHandle; +use futures_util::{SinkExt, StreamExt}; -impl crate::Liveview { - pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) { - connect(ws, self.pool.clone(), app, ()).await; - } - - pub async fn upgrade_axum_with_props( - &self, - ws: WebSocket, - app: fn(Scope) -> Element, - props: T, - ) where - T: Send + Sync + 'static, - { - connect(ws, self.pool.clone(), app, props).await; - } +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) } -pub async fn connect( - socket: WebSocket, - pool: LocalPoolHandle, - app: fn(Scope) -> Element, - props: T, -) where - T: Send + Sync + 'static, -{ - let (mut user_ws_tx, mut user_ws_rx) = socket.split(); - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (edits_tx, edits_rx) = mpsc::unbounded_channel(); - let mut edits_rx = UnboundedReceiverStream::new(edits_rx); - let mut event_rx = UnboundedReceiverStream::new(event_rx); - let vdom_fut = pool.clone().spawn_pinned(move || async move { - let mut vdom = VirtualDom::new_with_props(app, props); - let edits = vdom.rebuild(); - let serialized = serde_json::to_string(&edits.edits).unwrap(); - edits_tx.send(serialized).unwrap(); - loop { - let new_event = { - let vdom_fut = vdom.wait_for_work(); - pin_mut!(vdom_fut); - match select(event_rx.next(), vdom_fut).await { - Either::Left((l, _)) => l, - Either::Right((_, _)) => None, - } - }; - if let Some(new_event) = new_event { - vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); - } else { - let mutations = vdom.work_with_deadline(|| false); - for mutation in mutations { - let edits = serde_json::to_string(&mutation.edits).unwrap(); - edits_tx.send(edits).unwrap(); - } - } - } - }); - loop { - match select(user_ws_rx.next(), edits_rx.next()).await { - Either::Left((l, _)) => { - if let Some(Ok(msg)) = l { - if let Ok(Some(msg)) = msg.to_text().map(events::parse_ipc_message) { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); - } else { - break; - } - } else { - break; - } - } - Either::Right((edits, _)) => { - if let Some(edits) = edits { - // send the edits to the client - if user_ws_tx.send(Message::Text(edits)).await.is_err() { - break; - } - } else { - break; - } - } - } - } - vdom_fut.abort(); +fn transform_rx(message: Result) -> Result { + message + .map_err(|_| LiveViewError::SendingFailed)? + .into_text() + .map_err(|_| LiveViewError::SendingFailed) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::Text(message)) } diff --git a/packages/liveview/src/adapters/salvo_adapter.rs b/packages/liveview/src/adapters/salvo_adapter.rs index 8a28784b8..2c8912a57 100644 --- a/packages/liveview/src/adapters/salvo_adapter.rs +++ b/packages/liveview/src/adapters/salvo_adapter.rs @@ -1,110 +1,25 @@ -use crate::events; -use dioxus_core::prelude::*; -use futures_util::{pin_mut, SinkExt, StreamExt}; -use salvo::extra::ws::{Message, WebSocket}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tokio_util::task::LocalPoolHandle; +use futures_util::{SinkExt, StreamExt}; +use salvo::ws::{Message, WebSocket}; -impl crate::Liveview { - pub async fn upgrade_salvo(&self, ws: salvo::extra::ws::WebSocket, app: fn(Scope) -> Element) { - connect(ws, self.pool.clone(), app, ()).await; - } - pub async fn upgrade_salvo_with_props( - &self, - ws: salvo::extra::ws::WebSocket, - app: fn(Scope) -> Element, - props: T, - ) where - T: Send + Sync + 'static, - { - connect(ws, self.pool.clone(), app, props).await; - } +use crate::{LiveViewError, LiveViewSocket}; + +/// Convert a salvo websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) } -pub async fn connect( - ws: WebSocket, - pool: LocalPoolHandle, - app: fn(Scope) -> Element, - props: T, -) where - T: Send + Sync + 'static, -{ - // Use a counter to assign a new unique ID for this user. +fn transform_rx(message: Result) -> Result { + let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?; - // Split the socket into a sender and receive of messages. - let (mut user_ws_tx, mut user_ws_rx) = ws.split(); + let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?; - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (edits_tx, edits_rx) = mpsc::unbounded_channel(); - - let mut edits_rx = UnboundedReceiverStream::new(edits_rx); - let mut event_rx = UnboundedReceiverStream::new(event_rx); - - let vdom_fut = pool.spawn_pinned(move || async move { - let mut vdom = VirtualDom::new_with_props(app, props); - - let edits = vdom.rebuild(); - - let serialized = serde_json::to_string(&edits.edits).unwrap(); - edits_tx.send(serialized).unwrap(); - - loop { - use futures_util::future::{select, Either}; - - let new_event = { - let vdom_fut = vdom.wait_for_work(); - - pin_mut!(vdom_fut); - - match select(event_rx.next(), vdom_fut).await { - Either::Left((l, _)) => l, - Either::Right((_, _)) => None, - } - }; - - if let Some(new_event) = new_event { - vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); - } else { - let mutations = vdom.work_with_deadline(|| false); - for mutation in mutations { - let edits = serde_json::to_string(&mutation.edits).unwrap(); - edits_tx.send(edits).unwrap(); - } - } - } - }); - - loop { - use futures_util::future::{select, Either}; - - match select(user_ws_rx.next(), edits_rx.next()).await { - Either::Left((l, _)) => { - if let Some(Ok(msg)) = l { - if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) { - if msg.method == "user_event" { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); - } - } else { - break; - } - } else { - break; - } - } - Either::Right((edits, _)) => { - if let Some(edits) = edits { - // send the edits to the client - if user_ws_tx.send(Message::text(edits)).await.is_err() { - break; - } - } else { - break; - } - } - } - } - - vdom_fut.abort(); + Ok(msg) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) } diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index 398c7cdd9..e5c821ce3 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -1,110 +1,28 @@ -use crate::events; -use dioxus_core::prelude::*; -use futures_util::{pin_mut, SinkExt, StreamExt}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tokio_util::task::LocalPoolHandle; +use crate::{LiveViewError, LiveViewSocket}; +use futures_util::{SinkExt, StreamExt}; use warp::ws::{Message, WebSocket}; -impl crate::Liveview { - pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) { - connect(ws, self.pool.clone(), app, ()).await; - } - pub async fn upgrade_warp_with_props( - &self, - ws: warp::ws::WebSocket, - app: fn(Scope) -> Element, - props: T, - ) where - T: Send + Sync + 'static, - { - connect(ws, self.pool.clone(), app, props).await; - } +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) } -pub async fn connect( - ws: WebSocket, - pool: LocalPoolHandle, - app: fn(Scope) -> Element, - props: T, -) where - T: Send + Sync + 'static, -{ - // Use a counter to assign a new unique ID for this user. +fn transform_rx(message: Result) -> Result { + // destructure the message into the buffer we got from warp + let msg = message + .map_err(|_| LiveViewError::SendingFailed)? + .into_bytes(); - // Split the socket into a sender and receive of messages. - let (mut user_ws_tx, mut user_ws_rx) = ws.split(); + // transform it back into a string, saving us the allocation + let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?; - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (edits_tx, edits_rx) = mpsc::unbounded_channel(); - - let mut edits_rx = UnboundedReceiverStream::new(edits_rx); - let mut event_rx = UnboundedReceiverStream::new(event_rx); - - let vdom_fut = pool.spawn_pinned(move || async move { - let mut vdom = VirtualDom::new_with_props(app, props); - - let edits = vdom.rebuild(); - - let serialized = serde_json::to_string(&edits.edits).unwrap(); - edits_tx.send(serialized).unwrap(); - - loop { - use futures_util::future::{select, Either}; - - let new_event = { - let vdom_fut = vdom.wait_for_work(); - - pin_mut!(vdom_fut); - - match select(event_rx.next(), vdom_fut).await { - Either::Left((l, _)) => l, - Either::Right((_, _)) => None, - } - }; - - if let Some(new_event) = new_event { - vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); - } else { - let mutations = vdom.work_with_deadline(|| false); - for mutation in mutations { - let edits = serde_json::to_string(&mutation.edits).unwrap(); - edits_tx.send(edits).unwrap(); - } - } - } - }); - - loop { - use futures_util::future::{select, Either}; - - match select(user_ws_rx.next(), edits_rx.next()).await { - Either::Left((l, _)) => { - if let Some(Ok(msg)) = l { - if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) { - if msg.method == "user_event" { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); - } - } else { - break; - } - } else { - break; - } - } - Either::Right((edits, _)) => { - if let Some(edits) = edits { - // send the edits to the client - if user_ws_tx.send(Message::text(edits)).await.is_err() { - break; - } - } else { - break; - } - } - } - } - - vdom_fut.abort(); + Ok(msg) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) } diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs deleted file mode 100644 index 2c5f97cc7..000000000 --- a/packages/liveview/src/events.rs +++ /dev/null @@ -1,207 +0,0 @@ -#![allow(dead_code)] - -//! Convert a serialized event to an event trigger - -use std::any::Any; -use std::sync::Arc; - -use dioxus_core::ElementId; -// use dioxus_html::event_bubbles; -use dioxus_html::events::*; - -#[derive(serde::Serialize, serde::Deserialize)] -pub(crate) struct IpcMessage { - pub method: String, - pub params: serde_json::Value, -} - -pub(crate) fn parse_ipc_message(payload: &str) -> Option { - match serde_json::from_str(payload) { - Ok(message) => Some(message), - Err(_) => None, - } -} - -#[derive(serde::Serialize, serde::Deserialize)] -struct ImEvent { - event: String, - mounted_dom_id: ElementId, - contents: serde_json::Value, -} - -pub fn trigger_from_serialized(_val: serde_json::Value) { - todo!() - // let ImEvent { - // event, - // mounted_dom_id, - // contents, - // } = serde_json::from_value(val).unwrap(); - - // let mounted_dom_id = Some(mounted_dom_id); - - // let name = event_name_from_type(&event); - // let event = make_synthetic_event(&event, contents); - - // UserEvent { - // name, - // scope_id: None, - // element: mounted_dom_id, - // data: event, - // bubbles: event_bubbles(name), - // } -} - -fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc { - match name { - "copy" | "cut" | "paste" => { - // - Arc::new(ClipboardData {}) - } - "compositionend" | "compositionstart" | "compositionupdate" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "keydown" | "keypress" | "keyup" => { - let evt = serde_json::from_value::(val).unwrap(); - Arc::new(evt) - } - "focus" | "blur" | "focusout" | "focusin" => { - // - Arc::new(FocusData {}) - } - - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - "change" | "input" | "invalid" | "reset" | "submit" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" - | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" - | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "select" => { - // - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "touchcancel" | "touchend" | "touchmove" | "touchstart" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "scroll" => Arc::new(()), - - "wheel" => Arc::new(serde_json::from_value::(val).unwrap()), - - "animationstart" | "animationend" | "animationiteration" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "transitionend" => Arc::new(serde_json::from_value::(val).unwrap()), - - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => { - // - Arc::new(MediaData {}) - } - - "toggle" => Arc::new(ToggleData {}), - - _ => Arc::new(()), - } -} - -fn event_name_from_type(typ: &str) -> &'static str { - match typ { - "copy" => "copy", - "cut" => "cut", - "paste" => "paste", - "compositionend" => "compositionend", - "compositionstart" => "compositionstart", - "compositionupdate" => "compositionupdate", - "keydown" => "keydown", - "keypress" => "keypress", - "keyup" => "keyup", - "focus" => "focus", - "focusout" => "focusout", - "focusin" => "focusin", - "blur" => "blur", - "change" => "change", - "input" => "input", - "invalid" => "invalid", - "reset" => "reset", - "submit" => "submit", - "click" => "click", - "contextmenu" => "contextmenu", - "doubleclick" => "doubleclick", - "drag" => "drag", - "dragend" => "dragend", - "dragenter" => "dragenter", - "dragexit" => "dragexit", - "dragleave" => "dragleave", - "dragover" => "dragover", - "dragstart" => "dragstart", - "drop" => "drop", - "mousedown" => "mousedown", - "mouseenter" => "mouseenter", - "mouseleave" => "mouseleave", - "mousemove" => "mousemove", - "mouseout" => "mouseout", - "mouseover" => "mouseover", - "mouseup" => "mouseup", - "pointerdown" => "pointerdown", - "pointermove" => "pointermove", - "pointerup" => "pointerup", - "pointercancel" => "pointercancel", - "gotpointercapture" => "gotpointercapture", - "lostpointercapture" => "lostpointercapture", - "pointerenter" => "pointerenter", - "pointerleave" => "pointerleave", - "pointerover" => "pointerover", - "pointerout" => "pointerout", - "select" => "select", - "touchcancel" => "touchcancel", - "touchend" => "touchend", - "touchmove" => "touchmove", - "touchstart" => "touchstart", - "scroll" => "scroll", - "wheel" => "wheel", - "animationstart" => "animationstart", - "animationend" => "animationend", - "animationiteration" => "animationiteration", - "transitionend" => "transitionend", - "abort" => "abort", - "canplay" => "canplay", - "canplaythrough" => "canplaythrough", - "durationchange" => "durationchange", - "emptied" => "emptied", - "encrypted" => "encrypted", - "ended" => "ended", - "error" => "error", - "loadeddata" => "loadeddata", - "loadedmetadata" => "loadedmetadata", - "loadstart" => "loadstart", - "pause" => "pause", - "play" => "play", - "playing" => "playing", - "progress" => "progress", - "ratechange" => "ratechange", - "seeked" => "seeked", - "seeking" => "seeking", - "stalled" => "stalled", - "suspend" => "suspend", - "timeupdate" => "timeupdate", - "volumechange" => "volumechange", - "waiting" => "waiting", - "toggle" => "toggle", - _ => { - panic!("unsupported event type") - } - } -} diff --git a/packages/liveview/src/interpreter.js b/packages/liveview/src/interpreter.js deleted file mode 100644 index 7e99745f2..000000000 --- a/packages/liveview/src/interpreter.js +++ /dev/null @@ -1,973 +0,0 @@ -function main() { - let root = window.document.getElementById("main"); - - if (root != null) { - // create a new ipc - window.ipc = new IPC(root); - - window.ipc.send(serializeIpcMessage("initialize")); - } -} - -class IPC { - constructor(root) { - // connect to the websocket - window.interpreter = new Interpreter(root); - - this.ws = new WebSocket(WS_ADDR); - - this.ws.onopen = () => { - console.log("Connected to the websocket"); - }; - - this.ws.onerror = (err) => { - console.error("Error: ", err); - }; - - this.ws.onmessage = (event) => { - let edits = JSON.parse(event.data); - window.interpreter.handleEdits(edits); - }; - } - - send(msg) { - this.ws.send(msg); - } -} - -class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = root; - } - - create(event_name, element, handler, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.global[event_name].callback = handler; - this.root.addEventListener(event_name, handler); - } else { - this.global[event_name].active++; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - this.local[id][event_name] = handler; - element.addEventListener(event_name, handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, handler); - } - } -} - -class Interpreter { - constructor(root) { - this.root = root; - this.lastNode = root; - this.listeners = new ListenerMap(root); - this.handlers = {}; - this.nodes = [root]; - this.parents = []; - } - checkAppendParent() { - if (this.parents.length > 0) { - const lastParent = this.parents[this.parents.length - 1]; - lastParent[1]--; - if (lastParent[1] === 0) { - this.parents.pop(); - } - lastParent[0].appendChild(this.lastNode); - } - } - AppendChildren(root, children) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - for (let i = 0; i < children.length; i++) { - node.appendChild(this.nodes[children[i]]); - } - } - ReplaceWith(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.replaceWith(...els); - } - InsertAfter(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.after(...els); - } - InsertBefore(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.before(...els); - } - Remove(root) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (node !== undefined) { - node.remove(); - } - } - CreateTextNode(text, root) { - this.lastNode = document.createTextNode(text); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - } - CreateElement(tag, root, children) { - this.lastNode = document.createElement(tag); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - if (children > 0) { - this.parents.push([this.lastNode, children]); - } - } - CreateElementNs(tag, root, ns, children) { - this.lastNode = document.createElementNS(ns, tag); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - if (children > 0) { - this.parents.push([this.lastNode, children]); - } - } - CreatePlaceholder(root) { - this.lastNode = document.createElement("pre"); - this.lastNode.hidden = true; - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - } - NewEventListener(event_name, root, handler, bubbles) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.setAttribute("data-dioxus-id", `${root}`); - this.listeners.create(event_name, node, handler, bubbles); - } - RemoveEventListener(root, event_name, bubbles) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.removeAttribute(`data-dioxus-id`); - this.listeners.remove(node, event_name, bubbles); - } - SetText(root, text) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.data = text; - } - SetAttribute(root, field, value, ns) { - const name = field; - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (ns === "style") { - // @ts-ignore - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - 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)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root, field, ns) { - const name = field; - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (ns == "style") { - node.style.removeProperty(name); - } else if (ns !== null || ns !== undefined) { - node.removeAttributeNS(ns, name); - } else if (name === "value") { - node.value = ""; - } else if (name === "checked") { - node.checked = false; - } else if (name === "selected") { - node.selected = false; - } else if (name === "dangerous_inner_html") { - node.innerHTML = ""; - } else { - node.removeAttribute(name); - } - } - CloneNode(old, new_id) { - let node; - if (old === null) { - node = this.lastNode; - } else { - node = this.nodes[old]; - } - this.nodes[new_id] = node.cloneNode(true); - } - CloneNodeChildren(old, new_ids) { - let node; - if (old === null) { - node = this.lastNode; - } else { - node = this.nodes[old]; - } - const old_node = node.cloneNode(true); - let i = 0; - for (let node = old_node.firstChild; i < new_ids.length; node = node.nextSibling) { - this.nodes[new_ids[i++]] = node; - } - } - FirstChild() { - this.lastNode = this.lastNode.firstChild; - } - NextSibling() { - this.lastNode = this.lastNode.nextSibling; - } - ParentNode() { - this.lastNode = this.lastNode.parentNode; - } - StoreWithId(id) { - this.nodes[id] = this.lastNode; - } - SetLastNode(root) { - this.lastNode = this.nodes[root]; - } - handleEdits(edits) { - for (let edit of edits) { - this.handleEdit(edit); - } - } - handleEdit(edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.root, edit.children); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.nodes); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.nodes); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.nodes); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root, edit.children); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns, edit.children); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event) => { - let target = event.target; - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - let shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - if (event.type === "click") { - // todo call prevent default if it's the right type of event - if (shouldPreventDefault !== `onclick`) { - if (target.tagName === "A") { - event.preventDefault(); - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - serializeIpcMessage("browser_open", { href }) - ); - } - } - } - - // also prevent buttons from submitting - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - } - // walk the tree to find the real element - while (realId == null) { - // we've reached the root we don't want to send an event - if (target.parentElement === null) { - return; - } - - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - - shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - let contents = serialize_event(event); - - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - 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") { - // @ts-ignore - contents.values[name] = element.checked ? "true" : "false"; - } else if (element.getAttribute("type") === "radio") { - if (element.checked) { - contents.values[name] = element.value; - } - } else { - // @ts-ignore - contents.values[name] = - element.value ?? element.textContent; - } - } - } - } - - if (realId === null) { - return; - } - realId = parseInt(realId); - window.ipc.send( - serializeIpcMessage("user_event", { - event: edit.event_name, - mounted_dom_id: realId, - contents: contents, - }) - ); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name)); - - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name, edit.ns); - break; - case "CloneNode": - this.CloneNode(edit.id, edit.new_id); - break; - case "CloneNodeChildren": - this.CloneNodeChildren(edit.id, edit.new_ids); - break; - case "FirstChild": - this.FirstChild(); - break; - case "NextSibling": - this.NextSibling(); - break; - case "ParentNode": - this.ParentNode(); - break; - case "StoreWithId": - this.StoreWithId(BigInt(edit.id)); - break; - case "SetLastNode": - this.SetLastNode(BigInt(edit.id)); - break; - } - } -} - -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { - charCode, - key, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, - location, - repeat, - which, - code, - } = event; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - code, - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - return { - value: value, - values: {}, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - - if (target.type === "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - values: {}, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "dblclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - offsetX, - offsetY, - pageX, - pageY, - screenX, - screenY, - shiftKey, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - offset_x: offsetX, - offset_y: offsetY, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - pointerId, - width, - height, - pressure, - tangentialPressure, - tiltX, - tiltY, - twist, - pointerType, - isPrimary, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -function serializeIpcMessage(method, params = {}) { - return JSON.stringify({ method, params }); -} -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; - -function is_element_node(node) { - return node.nodeType == 1; -} - -function event_bubbles(event) { - switch (event) { - case "copy": - return true; - case "cut": - return true; - case "paste": - return true; - case "compositionend": - return true; - case "compositionstart": - return true; - case "compositionupdate": - return true; - case "keydown": - return true; - case "keypress": - return true; - case "keyup": - return true; - case "focus": - return false; - case "focusout": - return true; - case "focusin": - return true; - case "blur": - return false; - case "change": - return true; - case "input": - return true; - case "invalid": - return true; - case "reset": - return true; - case "submit": - return true; - case "click": - return true; - case "contextmenu": - return true; - case "doubleclick": - return true; - case "dblclick": - return true; - case "drag": - return true; - case "dragend": - return true; - case "dragenter": - return false; - case "dragexit": - return false; - case "dragleave": - return true; - case "dragover": - return true; - case "dragstart": - return true; - case "drop": - return true; - case "mousedown": - return true; - case "mouseenter": - return false; - case "mouseleave": - return false; - case "mousemove": - return true; - case "mouseout": - return true; - case "scroll": - return false; - case "mouseover": - return true; - case "mouseup": - return true; - case "pointerdown": - return true; - case "pointermove": - return true; - case "pointerup": - return true; - case "pointercancel": - return true; - case "gotpointercapture": - return true; - case "lostpointercapture": - return true; - case "pointerenter": - return false; - case "pointerleave": - return false; - case "pointerover": - return true; - case "pointerout": - return true; - case "select": - return true; - case "touchcancel": - return true; - case "touchend": - return true; - case "touchmove": - return true; - case "touchstart": - return true; - case "wheel": - return true; - case "abort": - return false; - case "canplay": - return false; - case "canplaythrough": - return false; - case "durationchange": - return false; - case "emptied": - return false; - case "encrypted": - return true; - case "ended": - return false; - case "error": - return false; - case "loadeddata": - return false; - case "loadedmetadata": - return false; - case "loadstart": - return false; - case "pause": - return false; - case "play": - return false; - case "playing": - return false; - case "progress": - return false; - case "ratechange": - return false; - case "seeked": - return false; - case "seeking": - return false; - case "stalled": - return false; - case "suspend": - return false; - case "timeupdate": - return false; - case "volumechange": - return false; - case "waiting": - return false; - case "animationstart": - return true; - case "animationend": - return true; - case "animationiteration": - return true; - case "transitionend": - return true; - case "toggle": - return true; - } -} \ No newline at end of file diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 540d29974..ffddf0272 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -1,56 +1,55 @@ -#![allow(dead_code)] - -pub(crate) mod events; pub mod adapters { #[cfg(feature = "warp")] pub mod warp_adapter; + #[cfg(feature = "warp")] + pub use warp_adapter::*; #[cfg(feature = "axum")] pub mod axum_adapter; + #[cfg(feature = "axum")] + pub use axum_adapter::*; #[cfg(feature = "salvo")] pub mod salvo_adapter; + + #[cfg(feature = "salvo")] + pub use salvo_adapter::*; } -use std::net::SocketAddr; +pub use adapters::*; -use tokio_util::task::LocalPoolHandle; +pub mod pool; +use futures_util::{SinkExt, StreamExt}; +pub use pool::*; -#[derive(Clone)] -pub struct Liveview { - pool: LocalPoolHandle, - addr: String, +pub trait WebsocketTx: SinkExt {} +impl WebsocketTx for T where T: SinkExt {} + +pub trait WebsocketRx: StreamExt> {} +impl WebsocketRx for T where T: StreamExt> {} + +#[derive(Debug, thiserror::Error)] +pub enum LiveViewError { + #[error("warp error")] + SendingFailed, } -impl Liveview { - pub fn body(&self, header: &str) -> String { - format!( - r#" - - - - {header} - - -
- - -"#, - addr = self.addr, - interpreter = include_str!("../src/interpreter.js") - ) - } -} +use dioxus_interpreter_js::INTERPRETER_JS; +static MAIN_JS: &str = include_str!("./main.js"); -pub fn new(addr: impl Into) -> Liveview { - let addr: SocketAddr = addr.into(); - - Liveview { - pool: LocalPoolHandle::new(16), - addr: addr.to_string(), - } +/// This script that gets injected into your app connects this page to the websocket endpoint +/// +/// Once the endpoint is connected, it will send the initial state of the app, and then start +/// processing user events and returning edits to the liveview instance +pub fn interpreter_glue(url: &str) -> String { + format!( + r#" + + "# + ) } diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js new file mode 100644 index 000000000..bdf656b50 --- /dev/null +++ b/packages/liveview/src/main.js @@ -0,0 +1,34 @@ +function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.ipc = new IPC(root); + } +} + +class IPC { + constructor(root) { + // connect to the websocket + window.interpreter = new Interpreter(root); + + let ws = new WebSocket(WS_ADDR); + + ws.onopen = () => { + ws.send(serializeIpcMessage("initialize")); + }; + + ws.onerror = (err) => { + // todo: retry the connection + }; + + ws.onmessage = (event) => { + let edits = JSON.parse(event.data); + window.interpreter.handleEdits(edits); + }; + + this.ws = ws; + } + + postMessage(msg) { + this.ws.send(msg); + } +} diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs new file mode 100644 index 000000000..a25673d84 --- /dev/null +++ b/packages/liveview/src/pool.rs @@ -0,0 +1,149 @@ +use crate::LiveViewError; +use dioxus_core::prelude::*; +use dioxus_html::HtmlEvent; +use futures_util::{pin_mut, SinkExt, StreamExt}; +use std::time::Duration; +use tokio_util::task::LocalPoolHandle; + +#[derive(Clone)] +pub struct LiveViewPool { + pub(crate) pool: LocalPoolHandle, +} + +impl Default for LiveViewPool { + fn default() -> Self { + Self::new() + } +} + +impl LiveViewPool { + pub fn new() -> Self { + LiveViewPool { + pool: LocalPoolHandle::new(16), + } + } + + pub async fn launch( + &self, + ws: impl LiveViewSocket, + app: fn(Scope<()>) -> Element, + ) -> Result<(), LiveViewError> { + self.launch_with_props(ws, app, ()).await + } + + pub async fn launch_with_props( + &self, + ws: impl LiveViewSocket, + app: fn(Scope) -> Element, + props: T, + ) -> Result<(), LiveViewError> { + match self.pool.spawn_pinned(move || run(app, props, ws)).await { + Ok(Ok(_)) => Ok(()), + Ok(Err(e)) => Err(e), + Err(_) => Err(LiveViewError::SendingFailed), + } + } +} + +/// A LiveViewSocket is a Sink and Stream of Strings that Dioxus uses to communicate with the client +/// +/// Most websockets from most HTTP frameworks can be converted into a LiveViewSocket using the appropriate adapter. +/// +/// You can also convert your own socket into a LiveViewSocket by implementing this trait. This trait is an auto trait, +/// meaning that as long as your type implements Stream and Sink, you can use it as a LiveViewSocket. +/// +/// For example, the axum implementation is a really small transform: +/// +/// ```rust, ignore +/// pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket { +/// ws.map(transform_rx) +/// .with(transform_tx) +/// .sink_map_err(|_| LiveViewError::SendingFailed) +/// } +/// +/// fn transform_rx(message: Result) -> Result { +/// message +/// .map_err(|_| LiveViewError::SendingFailed)? +/// .into_text() +/// .map_err(|_| LiveViewError::SendingFailed) +/// } +/// +/// async fn transform_tx(message: String) -> Result { +/// Ok(Message::Text(message)) +/// } +/// ``` +pub trait LiveViewSocket: + SinkExt + + StreamExt> + + Send + + 'static +{ +} + +impl LiveViewSocket for S where + S: SinkExt + + StreamExt> + + Send + + 'static +{ +} + +/// The primary event loop for the VirtualDom waiting for user input +/// +/// This function makes it easy to integrate Dioxus LiveView with any socket-based framework. +/// +/// As long as your framework can provide a Sink and Stream of Strings, you can use this function. +/// +/// You might need to transform the error types of the web backend into the LiveView error type. +pub async fn run( + app: Component, + props: T, + ws: impl LiveViewSocket, +) -> Result<(), LiveViewError> +where + T: Send + 'static, +{ + let mut vdom = VirtualDom::new_with_props(app, props); + + // todo: use an efficient binary packed format for this + let edits = serde_json::to_string(&vdom.rebuild()).unwrap(); + + // pin the futures so we can use select! + pin_mut!(ws); + + // send the initial render to the client + ws.send(edits).await?; + + // desktop uses this wrapper struct thing around the actual event itself + // this is sorta driven by tao/wry + #[derive(serde::Deserialize)] + struct IpcMessage { + params: HtmlEvent, + } + + loop { + tokio::select! { + // poll any futures or suspense + _ = vdom.wait_for_work() => {} + + evt = ws.next() => { + match evt { + Some(Ok(evt)) => { + if let Ok(IpcMessage { params }) = serde_json::from_str::(&evt) { + vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles); + } + } + // log this I guess? when would we get an error here? + Some(Err(_e)) => {}, + None => return Ok(()), + } + } + } + + let edits = vdom + .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) + .await; + + ws.send(serde_json::to_string(&edits).unwrap()).await?; + } +} diff --git a/packages/native-core/src/utils/cursor.rs b/packages/native-core/src/utils/cursor.rs index 86bcb71b1..c042d4cbf 100644 --- a/packages/native-core/src/utils/cursor.rs +++ b/packages/native-core/src/utils/cursor.rs @@ -200,7 +200,7 @@ impl Cursor { } change -= 1; } - c.move_col(change as i32, text); + c.move_col(change, text); }, data.modifiers().contains(Modifiers::SHIFT), ); diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs index 3bbe50b7e..a1858c318 100644 --- a/packages/router/src/components/redirect.rs +++ b/packages/router/src/components/redirect.rs @@ -47,5 +47,5 @@ pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element { router.replace_route(cx.props.to, None, None); } - cx.render(rsx!(())) + None } diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index cf665d2e1..37dbe4d26 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -52,6 +52,6 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { cx.render(rsx!(&cx.props.children)) } else { log::debug!("Route should *not* render: {:?}", cx.scope_id()); - cx.render(rsx!(())) + None } } diff --git a/packages/rsx/README.md b/packages/rsx/README.md new file mode 100644 index 000000000..920fcd88c --- /dev/null +++ b/packages/rsx/README.md @@ -0,0 +1 @@ +# The actual RSX language implemented using syn parsers. diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 2dddb1533..d3783c292 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -171,7 +171,7 @@ impl ToTokens for Component { toks.append_all(quote! { .children( - Ok({ #renderer }) + Some({ #renderer }) ) }); } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index cd2778428..a795c2081 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -34,7 +34,7 @@ use syn::{ }; /// Fundametnally, every CallBody is a template -#[derive(Default)] +#[derive(Default, Debug)] pub struct CallBody { pub roots: Vec, @@ -70,7 +70,7 @@ impl ToTokens for CallBody { if self.inline_cx { out_tokens.append_all(quote! { - Ok({ + Some({ let __cx = cx; #body }) diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index cb604654e..29f18fa31 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -1,3 +1,5 @@ +// use crate::{raw_expr::RawExprNode, text::TextNode}; + use super::*; use proc_macro2::{Span, TokenStream as TokenStream2}; @@ -29,7 +31,7 @@ pub enum BodyNode { impl BodyNode { pub fn is_litstr(&self) -> bool { - matches!(self, BodyNode::Text(_)) + matches!(self, BodyNode::Text { .. }) } pub fn span(&self) -> Span { diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 46587d6ad..7d2e87da4 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -23,7 +23,7 @@ pub fn render_lazy(f: LazyNodes<'_, '_>) -> String { fn lazy_app<'a>(cx: Scope<'a, RootProps<'static, 'static>>) -> Element<'a> { let lazy = cx.props.caller.take().unwrap(); let lazy: LazyNodes = unsafe { std::mem::transmute(lazy) }; - Ok(lazy.call(cx)) + Some(lazy.call(cx)) } let props: RootProps = unsafe { diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index cc015d570..608c6a6d4 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -51,7 +51,7 @@ impl Renderer { ) -> std::fmt::Result { // We should never ever run into async or errored nodes in SSR // Error boundaries and suspense boundaries will convert these to sync - if let RenderReturn::Sync(Ok(node)) = dom.get_scope(scope).unwrap().root_node() { + if let RenderReturn::Sync(Some(node)) = dom.get_scope(scope).unwrap().root_node() { self.render_template(buf, dom, node)? }; @@ -89,7 +89,7 @@ impl Renderer { let scope = dom.get_scope(id).unwrap(); let node = scope.root_node(); match node { - RenderReturn::Sync(Ok(node)) => { + RenderReturn::Sync(Some(node)) => { self.render_template(buf, dom, node)? } _ => todo!( diff --git a/packages/tui/examples/tui_colorpicker.rs b/packages/tui/examples/tui_colorpicker.rs index 951c4d1e5..244eea94b 100644 --- a/packages/tui/examples/tui_colorpicker.rs +++ b/packages/tui/examples/tui_colorpicker.rs @@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element { width: "100%", background_color: "hsl({hue}, 70%, {brightness}%)", onmousemove: move |evt| { - if let RenderReturn::Sync(Ok(node)) = cx.root_node() { + if let RenderReturn::Sync(Some(node)) = cx.root_node() { if let Some(id) = node.root_ids[0].get() { let node = tui_query.get(id); let Size{width, height} = node.size().unwrap(); diff --git a/packages/tui/src/widgets/mod.rs b/packages/tui/src/widgets/mod.rs index 25201bddb..36306b180 100644 --- a/packages/tui/src/widgets/mod.rs +++ b/packages/tui/src/widgets/mod.rs @@ -10,7 +10,7 @@ use dioxus_core::{ElementId, RenderReturn, Scope}; pub use input::*; pub(crate) fn get_root_id(cx: Scope) -> Option { - if let RenderReturn::Sync(Ok(sync)) = cx.root_node() { + if let RenderReturn::Sync(Some(sync)) = cx.root_node() { sync.root_ids.get(0).and_then(|id| id.get()) } else { None diff --git a/packages/tui/src/widgets/number.rs b/packages/tui/src/widgets/number.rs index 05cb2d678..2df1f512b 100644 --- a/packages/tui/src/widgets/number.rs +++ b/packages/tui/src/widgets/number.rs @@ -99,7 +99,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a update(text.clone()); }; - render! { + cx.render(rsx! { div{ width: "{width}", height: "{height}", @@ -120,7 +120,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a let Point{ x, y } = node.pos().unwrap(); let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); if let Ok(pos) = crossterm::cursor::position() { if pos != (x, y){ execute!(stdout(), MoveTo(x, y)).unwrap(); @@ -172,7 +172,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a let Point{ x, y } = node.pos().unwrap(); let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); if let Ok(pos) = crossterm::cursor::position() { if pos != (x, y){ execute!(stdout(), MoveTo(x, y)).unwrap(); @@ -205,5 +205,5 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a "{text_after_second_cursor}" } - } + }) } diff --git a/packages/tui/src/widgets/password.rs b/packages/tui/src/widgets/password.rs index 7f8455d71..f82d0a346 100644 --- a/packages/tui/src/widgets/password.rs +++ b/packages/tui/src/widgets/password.rs @@ -83,40 +83,44 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { "solid" }; - render! { - div{ + let onkeydown = move |k: KeyboardEvent| { + if k.key() == Key::Enter { + return; + } + let mut text = text_ref.write(); + cursor.write().handle_input(&k, &mut text, max_len); + if let Some(input_handler) = &cx.props.raw_oninput { + input_handler.call(FormData { + value: text.clone(), + values: HashMap::new(), + files: None, + }); + } + + let node = tui_query.get(get_root_id(cx).unwrap()); + let Point { x, y } = node.pos().unwrap(); + + let Pos { col, row } = cursor.read().start; + let (x, y) = ( + col as u16 + x as u16 + u16::from(border != "none"), + row as u16 + y as u16 + u16::from(border != "none"), + ); + if let Ok(pos) = crossterm::cursor::position() { + if pos != (x, y) { + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + } else { + execute!(stdout(), MoveTo(x, y)).unwrap(); + } + }; + + cx.render(rsx! { + div { width: "{width}", height: "{height}", border_style: "{border}", - onkeydown: move |k| { - if k.key()== Key::Enter { - return; - } - let mut text = text_ref.write(); - cursor.write().handle_input(&k, &mut text, max_len); - if let Some(input_handler) = &cx.props.raw_oninput{ - input_handler.call(FormData{ - value: text.clone(), - values: HashMap::new(), - files: None - }); - } - - let node = tui_query.get(get_root_id(cx).unwrap()); - let Point{ x, y } = node.pos().unwrap(); - - let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); - if let Ok(pos) = crossterm::cursor::position() { - if pos != (x, y){ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - } - else{ - execute!(stdout(), MoveTo(x, y)).unwrap(); - } - }, + onkeydown: onkeydown, onmousemove: move |evt| { if *dragging.get() { @@ -133,6 +137,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { } } }, + onmousedown: move |evt| { let offset = evt.data.element_coordinates(); let mut new = Pos::new(offset.x as usize, offset.y as usize); @@ -149,7 +154,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { let Point{ x, y } = node.pos().unwrap(); let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); if let Ok(pos) = crossterm::cursor::position() { if pos != (x, y){ execute!(stdout(), MoveTo(x, y)).unwrap(); @@ -182,5 +187,5 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { "{text_after_second_cursor}" } - } + }) } diff --git a/packages/tui/src/widgets/slider.rs b/packages/tui/src/widgets/slider.rs index 43f0ac77e..0d5dbca13 100644 --- a/packages/tui/src/widgets/slider.rs +++ b/packages/tui/src/widgets/slider.rs @@ -42,13 +42,12 @@ pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> { .and_then(|v| v.parse().ok()) .unwrap_or(size / 10.0); - let current_value = if let Some(value) = value { - value - } else { - *value_state.get() + let current_value = match value { + Some(value) => value, + None => *value_state.get(), } - .max(min) - .min(max); + .clamp(min, max); + let fst_width = 100.0 * (current_value - min) / size; let snd_width = 100.0 * (max - current_value) / size; assert!(fst_width + snd_width > 99.0 && fst_width + snd_width < 101.0); @@ -63,7 +62,7 @@ pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> { } }; - render! { + cx.render(rsx! { div{ width: "{width}", height: "{height}", @@ -72,11 +71,11 @@ pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> { onkeydown: move |event| { match event.key() { Key::ArrowLeft => { - value_state.set((current_value - step).max(min).min(max)); + value_state.set((current_value - step).clamp(min, max)); update(value_state.current().to_string()); } Key::ArrowRight => { - value_state.set((current_value + step).max(min).min(max)); + value_state.set((current_value + step).clamp(min, max)); update(value_state.current().to_string()); } _ => () @@ -104,5 +103,5 @@ pub(crate) fn Slider<'a>(cx: Scope<'a, SliderProps>) -> Element<'a> { background_color: "rgba(10,10,10,0.5)", } } - } + }) } diff --git a/packages/tui/src/widgets/textbox.rs b/packages/tui/src/widgets/textbox.rs index 8628fca29..4a58725bc 100644 --- a/packages/tui/src/widgets/textbox.rs +++ b/packages/tui/src/widgets/textbox.rs @@ -79,7 +79,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { "solid" }; - render! { + cx.render(rsx! { div{ width: "{width}", height: "{height}", @@ -103,7 +103,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { let Point{ x, y } = node.pos().unwrap(); let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); if let Ok(pos) = crossterm::cursor::position() { if pos != (x, y){ execute!(stdout(), MoveTo(x, y)).unwrap(); @@ -145,7 +145,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { let Point{ x, y } = node.pos().unwrap(); let Pos { col, row } = cursor.read().start; - let (x, y) = (col as u16 + x as u16 + if border == "none" {0} else {1}, row as u16 + y as u16 + if border == "none" {0} else {1}); + let (x, y) = (col as u16 + x as u16 + u16::from(border != "none"), row as u16 + y as u16 + u16::from(border != "none")); if let Ok(pos) = crossterm::cursor::position() { if pos != (x, y){ execute!(stdout(), MoveTo(x, y)).unwrap(); @@ -178,5 +178,5 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { "{text_after_second_cursor}" } - } + }) } diff --git a/packages/web/.gitignore b/packages/web/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/packages/web/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 8fe1d7eb1..ae449a176 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -351,8 +351,6 @@ fn read_input_to_data(target: Element) -> Rc { } fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> { - use wasm_bindgen::JsCast; - let mut target = event .target() .expect("missing target") diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index ec42eb5d5..3628322b8 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -54,19 +54,21 @@ // - Do DOM work in the next requestAnimationFrame callback pub use crate::cfg::Config; -use crate::dom::virtual_event_from_websys_event; pub use crate::util::{use_eval, EvalResult}; -use dioxus_core::{Element, ElementId, Scope, VirtualDom}; +use dioxus_core::{Element, Scope, VirtualDom}; use futures_util::{pin_mut, FutureExt, StreamExt}; mod cache; mod cfg; mod dom; mod hot_reload; -// mod rehydrate; -mod ric_raf; mod util; +// Currently disabled since it actually slows down immediate rendering +// todo: only schedule non-immediate renders through ric/raf +// mod ric_raf; +// mod rehydrate; + /// Launch the VirtualDOM given a root component and a configuration. /// /// This function expects the root component to not have root props. To launch the root component with root props, use @@ -197,8 +199,6 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // the mutations come back with nothing - we need to actually mount them websys_dom.mount(); - let _work_loop = ric_raf::RafLoop::new(); - loop { log::debug!("waiting for work");