Merge branch 'upstream' into fix-non-str-attributes

This commit is contained in:
Evan Almloff 2022-12-23 15:53:39 -06:00
commit afd024bcb6
106 changed files with 1419 additions and 2119 deletions

View file

@ -104,7 +104,7 @@ jobs:
- uses: actions-rs/cargo@v1 - uses: actions-rs/cargo@v1
with: with:
command: clippy command: clippy
args: -- -D warnings args: --workspace -- -D warnings
# Coverage is disabled until we can fix it # Coverage is disabled until we can fix it
# coverage: # coverage:

View file

@ -15,10 +15,10 @@ members = [
"packages/liveview", "packages/liveview",
"packages/autofmt", "packages/autofmt",
"packages/rsx", "packages/rsx",
"docs/guide",
"packages/tui", "packages/tui",
"packages/native-core", "packages/native-core",
"packages/native-core-macro", "packages/native-core-macro",
"docs/guide",
] ]
# This is a "virtual package" # This is a "virtual package"

View file

@ -26,7 +26,7 @@ struct ClickableProps<'a> {
// ANCHOR: Clickable // ANCHOR: Clickable
fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element { fn Clickable<'a>(cx: Scope<'a, ClickableProps<'a>>) -> Element {
match cx.props.children { match cx.props.children {
Ok(VNode { dynamic_nodes, .. }) => { Some(VNode { dynamic_nodes, .. }) => {
todo!("render some stuff") todo!("render some stuff")
} }
_ => { _ => {

View file

@ -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 - Keep state local to a component if possible
- When sharing state through props, only pass down the specific data necessary - 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"}
```

View file

@ -25,19 +25,13 @@ cargo new --bin demo
cd 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 ```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`: Edit your `main.rs`:
```rust ```rust

View file

@ -8,7 +8,7 @@
Install [dioxus-cli](https://github.com/DioxusLabs/cli). Install [dioxus-cli](https://github.com/DioxusLabs/cli).
Enable the hot-reload feature on dioxus: Enable the hot-reload feature on dioxus:
```toml ```toml
dioxus = { version = "*", features = ["web", "hot-reload"] } dioxus = { version = "*", features = ["hot-reload"] }
``` ```
# Usage # Usage

View file

@ -52,7 +52,8 @@ path = "gen/bin/desktop.rs"
# clear all the dependencies # clear all the dependencies
[dependencies] [dependencies]
mobile-entry-point = "0.1.0" mobile-entry-point = "0.1.0"
dioxus = { version = "*", features = ["mobile"] } dioxus = { version = "*"}
dioxus-desktop = { version = "*" }
simple_logger = "*" simple_logger = "*"
``` ```
@ -62,7 +63,7 @@ Edit your `lib.rs`:
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() { fn main() {
dioxus_mobile::launch(app); dioxus_desktop::launch(app);
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {

View file

@ -36,10 +36,11 @@ cargo new --bin demo
cd app cd app
``` ```
Add Dioxus with the `ssr` feature: Add Dioxus and the `ssr` renderer feature:
```shell ```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 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 ```toml
[dependencies] [dependencies]
axum = "0.4.5" axum = "0.4.5"
dioxus = { version = "*", features = ["ssr"] } dioxus = { version = "*" }
dioxus-ssr = { version = "*" }
tokio = { version = "1.15.0", features = ["full"] } tokio = { version = "1.15.0", features = ["full"] }
``` ```

View file

@ -13,12 +13,13 @@ TUI support is currently quite experimental. Even the project name will change.
## Getting Set up ## 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 ```shell
cargo new --bin demo cargo new --bin demo
cd demo cd demo
cargo add dioxus --features tui cargo add dioxus
cargo add dioxus-tui
``` ```
Then, edit your `main.rs` with the basic template. 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 ## Notes
- Our TUI package uses flexbox for layout - 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. - 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. - If your app panics, your terminal is wrecked. This will be fixed eventually.

View file

@ -38,10 +38,11 @@ cargo new --bin demo
cd demo cd demo
``` ```
Add Dioxus as a dependency with the `web` feature: Add Dioxus as a dependency and add the web renderer:
```bash ```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": Add an `index.html` for Trunk to use. Make sure your "mount point" element has an ID of "main":

View file

@ -19,7 +19,8 @@ This is where the router crates come in handy. To make sure we're using the rout
```toml ```toml
[dependencies] [dependencies]
dioxus = { version = "0.2", features = ["desktop", "router"] } dioxus = { version = "*" }
dioxus-router = { version = "*" }
``` ```

View file

@ -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 - 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 - 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"}
```

View file

@ -29,16 +29,10 @@ cd demo
Adicione o Dioxus com o recurso `desktop` (isso irá editar o `Cargo.toml`): Adicione o Dioxus com o recurso `desktop` (isso irá editar o `Cargo.toml`):
```shell ```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`: Edite seu `main.rs`:
```rust ```rust

View file

@ -10,7 +10,7 @@ Instale o [dioxus-cli](https://github.com/DioxusLabs/cli).
Habilite o recurso de _hot-reload_ no dioxus: Habilite o recurso de _hot-reload_ no dioxus:
```toml ```toml
dioxus = { version = "*", features = ["web", "hot-reload"] } dioxus = { version = "*", features = ["hot-reload"] }
``` ```
# Usage # Usage

View file

@ -52,7 +52,8 @@ path = "gen/bin/desktop.rs"
# clear all the dependencies # clear all the dependencies
[dependencies] [dependencies]
mobile-entry-point = "0.1.0" mobile-entry-point = "0.1.0"
dioxus = { version = "*", features = ["mobile"] } dioxus = { version = "*" }
dioxus-desktop = { version = "*" }
simple_logger = "*" simple_logger = "*"
``` ```
@ -62,7 +63,7 @@ Edite seu `lib.rs`:
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() { fn main() {
dioxus::mobile::launch(app); dioxus_desktop::launch(app);
} }
fn app(cx: Scope) -> Element { fn app(cx: Scope) -> Element {

View file

@ -38,7 +38,8 @@ cd app
Adicione o Dioxus com o recurso `ssr`: Adicione o Dioxus com o recurso `ssr`:
```shell ```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 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 ```toml
[dependencies] [dependencies]
axum = "0.4.5" axum = "0.4.5"
dioxus = { version = "*", features = ["ssr"] } dioxus = { version = "*" }
dioxus-ssr = { version = "*" }
tokio = { version = "1.15.0", features = ["full"] } tokio = { version = "1.15.0", features = ["full"] }
``` ```
@ -83,7 +85,7 @@ E, em seguida, adicione nosso _endpoint_. Podemos renderizar `rsx!` diretamente:
```rust ```rust
async fn app_endpoint() -> Html<String> { async fn app_endpoint() -> Html<String> {
Html(dioxus::ssr::render_lazy(rsx! { Html(dioxus_ssr::render_lazy(rsx! {
h1 { "hello world!" } h1 { "hello world!" }
})) }))
} }
@ -99,7 +101,7 @@ async fn app_endpoint() -> Html<String> {
let mut app = VirtualDom::new(app); let mut app = VirtualDom::new(app);
let _ = app.rebuild(); let _ = app.rebuild();
Html(dioxus::ssr::render_vdom(&app)) Html(dioxus_ssr::render_vdom(&app))
} }
``` ```

View file

@ -17,7 +17,8 @@ Comece criando um novo pacote e adicionando nosso recurso TUI.
```shell ```shell
cargo new --bin demo cargo new --bin demo
cd 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. Em seguida, edite seu `main.rs` com o modelo básico.

View file

@ -43,7 +43,8 @@ cd demo
Adicione o Dioxus como uma dependência com o recurso `web`: Adicione o Dioxus como uma dependência com o recurso `web`:
```bash ```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": Adicione um `index.html` para o `Trunk` usar. Certifique-se de que seu elemento "mount point" tenha um ID de "main":

View file

@ -18,7 +18,8 @@ Cada uma dessas cenas é independente não queremos renderizar a página ini
```toml ```toml
[dependencies] [dependencies]
dioxus = { version = "0.2", features = ["desktop", "router"] } dioxus = { version = "*" }
dioxus-router = { version = "*" }
``` ```
## Usando o Roteador ## Usando o Roteador

View file

@ -15,3 +15,74 @@ Sorted roughly in order of what's possible
- [ ] Format regular exprs - [ ] Format regular exprs
- [ ] Fix prettyplease around chaining - [ ] Fix prettyplease around chaining
- [ ] Don't eat comments in prettyplease - [ ] 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
-

View file

@ -125,16 +125,26 @@ impl Buffer {
} }
pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result { pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result {
for child in children { let last_child = children.len();
// 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()?;
}
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(()) Ok(())

View file

@ -2,6 +2,7 @@ use crate::Buffer;
use dioxus_rsx::*; use dioxus_rsx::*;
use proc_macro2::Span; use proc_macro2::Span;
use std::{fmt::Result, fmt::Write}; use std::{fmt::Result, fmt::Write};
use syn::{spanned::Spanned, Expr};
#[derive(Debug)] #[derive(Debug)]
enum ShortOptimization { enum ShortOptimization {
@ -83,12 +84,15 @@ impl Buffer {
self.write_attributes(attributes, key, true)?; 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, ", ")?; write!(self.buf, ", ")?;
} }
for child in children { for (id, child) in children.iter().enumerate() {
self.write_ident(child)?; self.write_ident(child)?;
if id != children.len() - 1 && children.len() > 1 {
write!(self.buf, ", ")?;
}
} }
write!(self.buf, " ")?; write!(self.buf, " ")?;
@ -100,7 +104,7 @@ impl Buffer {
} }
self.write_attributes(attributes, key, true)?; 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, ",")?; write!(self.buf, ",")?;
} }
@ -113,7 +117,7 @@ impl Buffer {
ShortOptimization::NoOpt => { ShortOptimization::NoOpt => {
self.write_attributes(attributes, key, false)?; 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, ",")?; write!(self.buf, ",")?;
} }
@ -286,33 +290,66 @@ impl Buffer {
if attr_len > 80 { if attr_len > 80 {
None None
} else if comp.children.is_empty() {
Some(attr_len)
} else { } else {
self.is_short_children(&comp.children) None
.map(|child_len| child_len + attr_len)
} }
} }
[BodyNode::RawExpr(ref _expr)] => { [BodyNode::RawExpr(ref expr)] => {
// TODO: let rawexprs to be inlined // TODO: let rawexprs to be inlined
// let span = syn::spanned::Spanned::span(&text); get_expr_length(expr)
// let (start, end) = (span.start(), span.end());
// if start.line == end.line {
// Some(end.column - start.column)
// } else {
// None
// }
None
} }
[BodyNode::Element(ref el)] => { [BodyNode::Element(ref el)] => {
let attr_len = self.is_short_attrs(&el.attributes); let attr_len = self.is_short_attrs(&el.attributes);
if attr_len > 80 { if el.children.is_empty() && attr_len < 80 {
None return Some(el.name.to_string().len());
} else {
self.is_short_children(&el.children)
.map(|child_len| child_len + attr_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<usize> {
let span = expr.span();
let (start, end) = (span.start(), span.end());
if start.line == end.line {
Some(end.column - start.column)
} else {
None
}
}

View file

@ -16,16 +16,63 @@ impl Buffer {
let placement = exp.span(); let placement = exp.span();
let start = placement.start(); let start = placement.start();
let end = placement.end(); 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]; // print comments
let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize; // 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)?; writeln!(self.buf)?;
// trim the leading whitespace // trim the leading whitespace
let line = match id {
x if x == (end.line - start.line) - 1 => &line[..end.column],
_ => line,
};
if offset < 0 { if offset < 0 {
for _ in 0..-offset { for _ in 0..-offset {
write!(self.buf, " ")?; 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(()) Ok(())
} }
} }

View file

@ -124,7 +124,7 @@ pub fn apply_format(input: &str, block: FormattedBlock) -> String {
let (left, _) = input.split_at(start); let (left, _) = input.split_at(start);
let (_, right) = input.split_at(end); let (_, right) = input.split_at(end);
dbg!(&block.formatted); // dbg!(&block.formatted);
format!("{}{}{}", left, block.formatted, right) format!("{}{}{}", left, block.formatted, right)
} }

View file

@ -23,3 +23,10 @@ twoway! ("complex" => complex);
twoway! ("tiny" => tiny); twoway! ("tiny" => tiny);
twoway! ("tinynoopt" => tinynoopt); twoway! ("tinynoopt" => tinynoopt);
twoway! ("long" => long);
twoway! ("key" => key);
// Disabled because we can't handle comments on exprs yet
twoway! ("multirsx" => multirsx);

View file

@ -27,9 +27,7 @@ rsx! {
} }
}) })
} }
div { class: "px-4", div { class: "px-4", is_current.then(|| rsx!{ children }) }
is_current.then(|| rsx!{ children })
}
} }
// No nesting // No nesting
@ -47,4 +45,6 @@ rsx! {
let blah = 120; let blah = 120;
} }
} }
div { asdbascasdbasd, asbdasbdabsd, asbdabsdbasdbas }
} }

View file

@ -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}" }
}
}

View file

@ -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 }
})
}

View file

@ -0,0 +1,25 @@
rsx! {
// hi
div {}
// hi
div { abcd, ball, s }
//
//
//
div { abcd, ball, s }
//
//
//
div {
abcd,
ball,
s,
//
"asdasd"
}
}

View file

@ -1,9 +1,6 @@
rsx! { rsx! {
div { "hello world!" } div { "hello world!" }
div { div { "hello world!", "goodbye world!" }
"hello world!"
"goodbye world!"
}
// Simple div // Simple div
div { "hello world!" } div { "hello world!" }
@ -15,7 +12,16 @@ rsx! {
div { div { "nested" } } div { div { "nested" } }
// Nested two level // Nested two level
div { div { h1 { "highly nested" } } } div {
div { h1 { "highly nested" } }
}
// Anti-Nested two level
div {
div {
div { h1 { "highly nested" } }
}
}
// Compression // Compression
h3 { class: "mb-2 text-xl font-bold", "Invite Member" } 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: "" } img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" }
// One level compression // 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 // Components
Component { ..Props {} } Component { ..Props {} }

View file

@ -14,3 +14,5 @@ macro_rules! twoway {
twoway!("comments" => comments); twoway!("comments" => comments);
twoway!("multi" => multi); twoway!("multi" => multi);
twoway!("multiexpr" => multiexpr);

View file

@ -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 }
})

View file

@ -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 }
})

View file

@ -194,7 +194,7 @@ mod field_info {
// children field is automatically defaulted to None // children field is automatically defaulted to None
if name == "children" { if name == "children" {
builder_attr.default = builder_attr.default =
Some(syn::parse(quote!(::dioxus::core::VNode::empty()).into()).unwrap()); Some(syn::parse(quote!(Default::default()).into()).unwrap());
} }
// auto detect optional // auto detect optional

View file

@ -31,16 +31,16 @@ futures-channel = "0.3.21"
indexmap = "1.7" 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" smallbox = "0.8.1"
log = "0.4.17" log = "0.4.17"
# Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
[dev-dependencies] [dev-dependencies]
tokio = { version = "*", features = ["full"] } tokio = { version = "*", features = ["full"] }
dioxus = { path = "../dioxus" } dioxus = { path = "../dioxus" }
pretty_assertions = "1.3.0"
[features] [features]
default = [] default = []

View file

@ -81,12 +81,12 @@ impl VirtualDom {
self.ensure_drop_safety(id); self.ensure_drop_safety(id);
if let Some(root) = self.scopes[id.0].as_ref().try_root_node() { 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) self.drop_scope_inner(node)
} }
} }
if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_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) self.drop_scope_inner(node)
} }
} }
@ -115,10 +115,14 @@ impl VirtualDom {
nodes.iter().for_each(|node| self.drop_scope_inner(node)) nodes.iter().for_each(|node| self.drop_scope_inner(node))
} }
DynamicNode::Placeholder(t) => { DynamicNode::Placeholder(t) => {
self.try_reclaim(t.id.get().unwrap()); if let Some(id) = t.id.get() {
self.try_reclaim(id);
}
} }
DynamicNode::Text(t) => { 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 /// Descend through the tree, removing any borrowed props and listeners
pub(crate) fn ensure_drop_safety(&self, scope: ScopeId) { pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
let scope = &self.scopes[scope.0]; 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 // 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) // run the hooks (which hold an &mut Reference)
@ -141,10 +145,13 @@ impl VirtualDom {
let mut props = scope.borrowed_props.borrow_mut(); let mut props = scope.borrowed_props.borrow_mut();
props.drain(..).for_each(|comp| { props.drain(..).for_each(|comp| {
let comp = unsafe { &*comp }; let comp = unsafe { &*comp };
if let Some(scope_id) = comp.scope.get() { match comp.scope.get() {
self.ensure_drop_safety(scope_id); 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. // Now that all the references are gone, we can safely drop our own references in our listeners.

View file

@ -380,8 +380,8 @@ impl<'b> VirtualDom {
use RenderReturn::*; use RenderReturn::*;
match return_nodes { match return_nodes {
Sync(Ok(t)) => self.mount_component(scope, template, t, idx), Sync(Some(t)) => self.mount_component(scope, template, t, idx),
Sync(Err(_e)) => todo!("Propogate error upwards"), Sync(None) => todo!("Propogate error upwards"),
Async(_) => self.mount_component_placeholder(template, idx, scope), Async(_) => self.mount_component_placeholder(template, idx, scope),
} }
} }

View file

@ -33,26 +33,26 @@ impl<'b> VirtualDom {
use RenderReturn::{Async, Sync}; use RenderReturn::{Async, Sync};
match (old, new) { 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 // Err cases
(Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e), (Sync(Some(l)), Sync(None)) => self.diff_ok_to_err(l),
(Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r), (Sync(None), Sync(Some(r))) => self.diff_err_to_ok(r),
(Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ } (Sync(None), Sync(None)) => { /* nothing */ }
// Async // Async
(Sync(Ok(_l)), Async(_)) => todo!(), (Sync(Some(_l)), Async(_)) => todo!(),
(Sync(Err(_e)), Async(_)) => todo!(), (Sync(None), Async(_)) => todo!(),
(Async(_), Sync(Ok(_r))) => todo!(), (Async(_), Sync(Some(_r))) => todo!(),
(Async(_), Sync(Err(_e))) => { /* nothing */ } (Async(_), Sync(None)) => { /* nothing */ }
(Async(_), Async(_)) => { /* nothing */ } (Async(_), Async(_)) => { /* nothing */ }
}; };
} }
self.scope_stack.pop(); self.scope_stack.pop();
} }
fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {} fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>) {}
fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _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>) { 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 // 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 // Replace components that have different render fns
if left.render_fn != right.render_fn { if left.render_fn != right.render_fn {
let created = self.create_component_node(right_template, right, idx); return self.replace_vcomponent(right_template, right, idx, left);
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;
} }
// Make sure the new vcomponent has the right scopeid associated to it // 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. /// 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 /// 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) => { Component(comp) => {
let scope = comp.scope.get().unwrap(); let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { 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!(), _ => todo!(),
} }
} }
@ -706,11 +711,21 @@ impl<'b> VirtualDom {
fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator<Item = &'b VNode<'b>>) { fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator<Item = &'b VNode<'b>>) {
let m = self.create_children(right); let m = self.create_children(right);
let id = self.find_last_element(left); let pre_edits = self.mutations.edits.len();
self.mutations.push(Mutation::InsertAfter { id, m });
self.remove_node(left, true); 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) { 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)); r.id.set(Some(placeholder));
let id = self.find_last_element(&l[0]);
self.mutations self.mutations
.push(Mutation::CreatePlaceholder { id: placeholder }); .push(Mutation::CreatePlaceholder { id: placeholder });
self.mutations.push(Mutation::InsertAfter { id, m: 1 });
self.remove_nodes(l); 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 /// Remove these nodes from the dom
/// Wont generate mutations for the inner nodes /// Wont generate mutations for the inner nodes
fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) { 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) { 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 // 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() { for (idx, _) in node.template.roots.iter().enumerate() {
if let Some(dy) = node.dynamic_root(idx) { if let Some(dy) = node.dynamic_root(idx) {
self.remove_dynamic_node(dy, gen_muts); self.remove_dynamic_node(dy, gen_muts);
@ -748,17 +784,9 @@ impl<'b> VirtualDom {
self.reclaim(id); self.reclaim(id);
} }
} }
}
for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() { fn reclaim_attributes(&mut self, node: &VNode) {
// 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
let mut id = None; let mut id = None;
for (idx, attr) in node.dynamic_attrs.iter().enumerate() { for (idx, attr) in node.dynamic_attrs.iter().enumerate() {
// We'll clean up the root nodes either way, so don't worry // 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) { fn remove_dynamic_node(&mut self, node: &DynamicNode, gen_muts: bool) {
match node { match node {
Component(comp) => self.remove_component_node(comp, gen_muts), Component(comp) => self.remove_component_node(comp, gen_muts),
Text(t) => self.remove_text_node(t), Text(t) => self.remove_text_node(t, gen_muts),
Placeholder(t) => self.remove_placeholder(t), Placeholder(t) => self.remove_placeholder(t, gen_muts),
Fragment(nodes) => nodes Fragment(nodes) => nodes
.iter() .iter()
.for_each(|node| self.remove_node(node, gen_muts)), .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 let Some(id) = t.id.take() {
if gen_muts {
self.mutations.push(Mutation::Remove { id });
}
self.reclaim(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 let Some(id) = t.id.take() {
if gen_muts {
self.mutations.push(Mutation::Remove { id });
}
self.reclaim(id) self.reclaim(id)
} }
} }
fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) { fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) {
if let Some(scope) = comp.scope.take() { let scope = comp.scope.take().unwrap();
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 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 { let props = self.scopes[scope.0].props.take();
height: self.scopes[scope.0].height,
id: scope,
});
*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 *comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
self.ensure_drop_safety(scope);
self.scopes.remove(scope.0); // 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 { fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
@ -832,7 +880,7 @@ impl<'b> VirtualDom {
Some(Component(comp)) => { Some(Component(comp)) => {
let scope = comp.scope.get().unwrap(); let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { 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"), _ => todo!("cannot handle nonstandard nodes"),
} }
} }
@ -848,7 +896,7 @@ impl<'b> VirtualDom {
Some(Component(comp)) => { Some(Component(comp)) => {
let scope = comp.scope.get().unwrap(); let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { 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"), _ => todo!("cannot handle nonstandard nodes"),
} }
} }

View file

@ -5,7 +5,7 @@ use crate::ScopeId;
/// A boundary that will capture any errors from child components /// A boundary that will capture any errors from child components
#[allow(dead_code)] #[allow(dead_code)]
pub struct ErrorBoundary { pub struct ErrorBoundary {
error: RefCell<Option<(anyhow::Error, ScopeId)>>, error: RefCell<Option<ScopeId>>,
id: ScopeId, id: ScopeId,
} }

View file

@ -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. /// 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)] #[allow(non_upper_case_globals, non_snake_case)]
pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?; let children = cx.props.0.as_ref()?;
Ok(VNode { Some(VNode {
key: children.key, key: children.key,
parent: children.parent, parent: children.parent,
template: children.template, template: children.template,
@ -95,7 +95,7 @@ impl<'a> Properties for FragmentProps<'a> {
type Builder = FragmentBuilder<'a, false>; type Builder = FragmentBuilder<'a, false>;
const IS_STATIC: bool = false; const IS_STATIC: bool = false;
fn builder() -> Self::Builder { fn builder() -> Self::Builder {
FragmentBuilder(VNode::empty()) FragmentBuilder(None)
} }
unsafe fn memoize(&self, _other: &Self) -> bool { unsafe fn memoize(&self, _other: &Self) -> bool {
false false

View file

@ -34,10 +34,10 @@ pub(crate) mod innerlude {
pub use crate::scopes::*; pub use crate::scopes::*;
pub use crate::virtual_dom::*; 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. /// An Errored [`Element`] will propagate the error to the nearest error boundary.
pub type Element<'a> = Result<VNode<'a>, anyhow::Error>; pub type Element<'a> = Option<VNode<'a>>;
/// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`]. /// A [`Component`] is a function that takes a [`Scope`] and returns an [`Element`].
/// ///

View file

@ -58,7 +58,7 @@ pub struct VNode<'a> {
impl<'a> VNode<'a> { impl<'a> VNode<'a> {
/// Create a template with no nodes that will be skipped over during diffing /// Create a template with no nodes that will be skipped over during diffing
pub fn empty() -> Element<'a> { pub fn empty() -> Element<'a> {
Ok(VNode { Some(VNode {
key: None, key: None,
parent: None, parent: None,
root_ids: &[], 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<T> { impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option<T> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
match self { match self {
@ -624,7 +614,7 @@ impl<'a, T: IntoDynNode<'a>> IntoDynNode<'a> for Option<T> {
impl<'a> IntoDynNode<'a> for &Element<'a> { impl<'a> IntoDynNode<'a> for &Element<'a> {
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
match self.as_ref() { match self.as_ref() {
Ok(val) => val.clone().into_vnode(_cx), Some(val) => val.clone().into_vnode(_cx),
_ => DynamicNode::default(), _ => DynamicNode::default(),
} }
} }
@ -678,7 +668,7 @@ impl<'a> IntoTemplate<'a> for VNode<'a> {
impl<'a> IntoTemplate<'a> for Element<'a> { impl<'a> IntoTemplate<'a> for Element<'a> {
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> {
match self { match self {
Ok(val) => val.into_template(_cx), Some(val) => val.into_template(_cx),
_ => VNode::empty().unwrap(), _ => VNode::empty().unwrap(),
} }
} }

View file

@ -78,7 +78,7 @@ impl VirtualDom {
fiber.waiting_on.borrow_mut().remove(&id); 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_ref = &mut fiber.mutations.borrow_mut();
let mutations = &mut **mutations_ref; let mutations = &mut **mutations_ref;
let template: &VNode = unsafe { std::mem::transmute(template) }; let template: &VNode = unsafe { std::mem::transmute(template) };

View file

@ -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 /// Create a dynamic text node using [`Arguments`] and the [`ScopeState`]'s internal [`Bump`] allocator

View file

@ -477,7 +477,7 @@ impl VirtualDom {
pub fn rebuild(&mut self) -> Mutations { pub fn rebuild(&mut self) -> Mutations {
match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
// Rebuilding implies we append the created elements to the root // 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); let m = self.create_scope(ScopeId(0), node);
self.mutations.edits.push(Mutation::AppendChildren { self.mutations.edits.push(Mutation::AppendChildren {
id: ElementId(0), 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 // 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"), RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"),
} }

View file

@ -54,7 +54,7 @@ fn attrs_cycle() {
assert_eq!( assert_eq!(
dom.render_immediate().santize().edits, 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 } ReplaceWith { id: ElementId(2), m: 1 }
] ]
); );
@ -64,15 +64,10 @@ fn attrs_cycle() {
dom.render_immediate().santize().edits, dom.render_immediate().santize().edits,
[ [
LoadTemplate { name: "template", index: 0, id: ElementId(2) }, LoadTemplate { name: "template", index: 0, id: ElementId(2) },
AssignId { path: &[0], id: ElementId(1) }, AssignId { path: &[0], id: ElementId(3) },
SetAttribute { SetAttribute { name: "class", value: "3", id: ElementId(3), ns: None },
name: "class", SetAttribute { name: "id", value: "3", id: ElementId(3), ns: None },
value: "3".into_value(&bump), ReplaceWith { id: ElementId(1), m: 1 }
id: ElementId(1),
ns: None
},
SetAttribute { name: "id", value: "3".into_value(&bump), id: ElementId(1), ns: None },
ReplaceWith { id: ElementId(3), m: 1 }
] ]
); );

View file

@ -9,7 +9,7 @@ fn app(cx: Scope) -> Element {
_ => unreachable!(), _ => unreachable!(),
}; };
let value = raw.parse::<f32>()?; let value = raw.parse::<f32>().unwrap_or(123.123);
cx.render(rsx! { cx.render(rsx! {
div { "hello {value}" } div { "hello {value}" }

View file

@ -1,5 +1,6 @@
use dioxus::core::{ElementId, Mutation::*}; use dioxus::core::{ElementId, Mutation::*};
use dioxus::prelude::*; use dioxus::prelude::*;
use pretty_assertions::assert_eq;
#[test] #[test]
fn list_creates_one_by_one() { fn list_creates_one_by_one() {
@ -125,7 +126,7 @@ fn removes_one_by_one() {
assert_eq!( assert_eq!(
dom.render_immediate().santize().edits, dom.render_immediate().santize().edits,
[ [
CreatePlaceholder { id: ElementId(3) }, CreatePlaceholder { id: ElementId(4) },
ReplaceWith { id: ElementId(2), m: 1 } ReplaceWith { id: ElementId(2), m: 1 }
] ]
); );
@ -137,12 +138,12 @@ fn removes_one_by_one() {
dom.render_immediate().santize().edits, dom.render_immediate().santize().edits,
[ [
LoadTemplate { name: "template", index: 0, id: ElementId(2) }, 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) }, LoadTemplate { name: "template", index: 0, id: ElementId(5) },
HydrateText { path: &[0], value: "1", id: ElementId(6) }, HydrateText { path: &[0], value: "1", id: ElementId(6) },
LoadTemplate { name: "template", index: 0, id: ElementId(7) }, LoadTemplate { name: "template", index: 0, id: ElementId(7) },
HydrateText { path: &[0], value: "2", id: ElementId(8) }, 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!( assert_eq!(
dom.render_immediate().santize().edits, dom.render_immediate().santize().edits,
[ [
Remove { id: ElementId(4) }, CreatePlaceholder { id: ElementId(8) },
CreatePlaceholder { id: ElementId(5) }, Remove { id: ElementId(2) },
ReplaceWith { id: ElementId(2), m: 1 } ReplaceWith { id: ElementId(4), m: 1 }
] ]
); );
} }
@ -364,11 +365,11 @@ fn remove_many() {
assert_eq!( assert_eq!(
edits.edits, edits.edits,
[ [
CreatePlaceholder { id: ElementId(11,) },
Remove { id: ElementId(9,) }, Remove { id: ElementId(9,) },
Remove { id: ElementId(7,) }, Remove { id: ElementId(7,) },
Remove { id: ElementId(5,) }, Remove { id: ElementId(5,) },
Remove { id: ElementId(1,) }, Remove { id: ElementId(1,) },
CreatePlaceholder { id: ElementId(3,) },
ReplaceWith { id: ElementId(2,), m: 1 }, ReplaceWith { id: ElementId(2,), m: 1 },
] ]
); );
@ -381,8 +382,8 @@ fn remove_many() {
edits.edits, edits.edits,
[ [
LoadTemplate { name: "template", index: 0, id: ElementId(2,) }, LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
HydrateText { path: &[0,], value: "hello 0", id: ElementId(1,) }, HydrateText { path: &[0,], value: "hello 0", id: ElementId(3,) },
ReplaceWith { id: ElementId(3,), m: 1 }, ReplaceWith { id: ElementId(11,), m: 1 },
] ]
) )
} }

View file

@ -21,6 +21,7 @@ pub struct Config {
pub(crate) resource_dir: Option<PathBuf>, pub(crate) resource_dir: Option<PathBuf>,
pub(crate) custom_head: Option<String>, pub(crate) custom_head: Option<String>,
pub(crate) custom_index: Option<String>, pub(crate) custom_index: Option<String>,
pub(crate) root_name: String,
} }
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>; type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
@ -46,6 +47,7 @@ impl Config {
resource_dir: None, resource_dir: None,
custom_head: None, custom_head: None,
custom_index: None, custom_index: None,
root_name: "main".to_string(),
} }
} }
@ -126,6 +128,14 @@ impl Config {
self.custom_index = Some(index); self.custom_index = Some(index);
self 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<String>) -> Self {
self.root_name = name.into();
self
}
} }
impl Default for Config { impl Default for Config {

View file

@ -1,6 +1,6 @@
use crate::desktop_context::{DesktopContext, UserWindowEvent}; use crate::desktop_context::{DesktopContext, UserWindowEvent};
use crate::events::{decode_event, EventMessage};
use dioxus_core::*; use dioxus_core::*;
use dioxus_html::HtmlEvent;
use futures_channel::mpsc::{unbounded, UnboundedSender}; use futures_channel::mpsc::{unbounded, UnboundedSender};
use futures_util::StreamExt; use futures_util::StreamExt;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
@ -50,6 +50,7 @@ impl DesktopController {
std::thread::spawn(move || { std::thread::spawn(move || {
// We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads // 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 // 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() let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all() .enable_all()
.build() .build()
@ -69,12 +70,14 @@ impl DesktopController {
tokio::select! { tokio::select! {
_ = dom.wait_for_work() => {} _ = dom.wait_for_work() => {}
Some(json_value) = event_rx.next() => { Some(json_value) = event_rx.next() => {
if let Ok(value) = serde_json::from_value::<EventMessage>(json_value) { if let Ok(value) = serde_json::from_value::<HtmlEvent>(json_value) {
let name = value.event.clone(); let HtmlEvent {
let el_id = ElementId(value.mounted_dom_id); name,
if let Some(evt) = decode_event(value) { element,
dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name)); 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))) .render_with_deadline(tokio::time::sleep(Duration::from_millis(16)))
.await; .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); let _ = proxy.send_event(UserWindowEvent::EditsReady);
} }
}) })

View file

@ -7,6 +7,7 @@ use serde_json::Value;
use std::future::Future; use std::future::Future;
use std::future::IntoFuture; use std::future::IntoFuture;
use std::pin::Pin; use std::pin::Pin;
use wry::application::dpi::LogicalSize;
use wry::application::event_loop::ControlFlow; use wry::application::event_loop::ControlFlow;
use wry::application::event_loop::EventLoopProxy; use wry::application::event_loop::EventLoopProxy;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
@ -136,6 +137,11 @@ impl DesktopContext {
let _ = self.proxy.send_event(SetZoomLevel(scale_factor)); let _ = self.proxy.send_event(SetZoomLevel(scale_factor));
} }
/// modifies the inner size of the window
pub fn set_inner_size(&self, logical_size: LogicalSize<f64>) {
let _ = self.proxy.send_event(SetInnerSize(logical_size));
}
/// launch print modal /// launch print modal
pub fn print(&self) { pub fn print(&self) {
let _ = self.proxy.send_event(Print); let _ = self.proxy.send_event(Print);
@ -193,6 +199,7 @@ pub enum UserWindowEvent {
SetDecorations(bool), SetDecorations(bool),
SetZoomLevel(f64), SetZoomLevel(f64),
SetInnerSize(LogicalSize<f64>),
Print, Print,
DevTool, DevTool,
@ -265,6 +272,7 @@ impl DesktopController {
SetDecorations(state) => window.set_decorations(state), SetDecorations(state) => window.set_decorations(state),
SetZoomLevel(scale_factor) => webview.zoom(scale_factor), SetZoomLevel(scale_factor) => webview.zoom(scale_factor),
SetInnerSize(logical_size) => window.set_inner_size(logical_size),
Print => { Print => {
if let Err(e) = webview.print() { if let Err(e) = webview.print() {

View file

@ -1,10 +1,6 @@
//! Convert a serialized event to an event trigger //! Convert a serialized event to an event trigger
use dioxus_html::events::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::from_value;
use std::any::Any;
use std::rc::Rc;
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub(crate) struct IpcMessage { pub(crate) struct IpcMessage {
@ -31,61 +27,3 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
} }
} }
} }
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<dyn Any>
})*
_ => 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<Rc<dyn Any>> {
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)
}

View file

@ -164,6 +164,7 @@ fn build_webview(
let custom_head = cfg.custom_head.clone(); let custom_head = cfg.custom_head.clone();
let resource_dir = cfg.resource_dir.clone(); let resource_dir = cfg.resource_dir.clone();
let index_file = cfg.custom_index.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 // 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() { if cfg.window.window.window_icon.is_none() {
@ -183,36 +184,40 @@ fn build_webview(
.with_url("dioxus://index.html/") .with_url("dioxus://index.html/")
.unwrap() .unwrap()
.with_ipc_handler(move |_window: &Window, payload: String| { .with_ipc_handler(move |_window: &Window, payload: String| {
parse_ipc_message(&payload) let message = match parse_ipc_message(&payload) {
.map(|message| match message.method() { Some(message) => message,
"eval_result" => { None => {
let result = message.params(); log::error!("Failed to parse IPC message: {}", payload);
eval_sender.send(result).unwrap(); return;
} }
"user_event" => { };
_ = event_tx.unbounded_send(message.params());
} match message.method() {
"initialize" => { "eval_result" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed); let result = message.params();
let _ = proxy.send_event(UserWindowEvent::EditsReady); eval_sender.send(result).unwrap();
} }
"browser_open" => { "user_event" => {
let data = message.params(); _ = event_tx.unbounded_send(message.params());
log::trace!("Open browser: {:?}", data); }
if let Some(temp) = data.as_object() { "initialize" => {
if temp.contains_key("href") { is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
let url = temp.get("href").unwrap().as_str().unwrap(); let _ = proxy.send_event(UserWindowEvent::EditsReady);
if let Err(e) = webbrowser::open(url) { }
log::error!("Open Browser error: {:?}", e); "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| { .with_custom_protocol(String::from("dioxus"), move |r| {
protocol::desktop_handler( protocol::desktop_handler(
@ -220,6 +225,7 @@ fn build_webview(
resource_dir.clone(), resource_dir.clone(),
custom_head.clone(), custom_head.clone(),
index_file.clone(), index_file.clone(),
&root_name,
) )
}) })
.with_file_drop_handler(move |window, evet| { .with_file_drop_handler(move |window, evet| {

View file

@ -1,22 +1,34 @@
use dioxus_interpreter_js::INTERPRETER_JS;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use wry::{ use wry::{
http::{status::StatusCode, Request, Response}, http::{status::StatusCode, Request, Response},
Result, Result,
}; };
const MODULE_LOADER: &str = r#" fn module_loader(root_name: &str) -> String {
format!(
r#"
<script> <script>
import("./index.js").then(function (module) { {INTERPRETER_JS}
module.main();
}); let rootname = "{}";
let root = window.document.getElementById(rootname);
if (root != null) {{
window.interpreter = new Interpreter(root);
window.ipc.postMessage(serializeIpcMessage("initialize"));
}}
</script> </script>
"#; "#,
root_name
)
}
pub(super) fn desktop_handler( pub(super) fn desktop_handler(
request: &Request<Vec<u8>>, request: &Request<Vec<u8>>,
asset_root: Option<PathBuf>, asset_root: Option<PathBuf>,
custom_head: Option<String>, custom_head: Option<String>,
custom_index: Option<String>, custom_index: Option<String>,
root_name: &str,
) -> Result<Response<Vec<u8>>> { ) -> Result<Response<Vec<u8>>> {
// Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case". // 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. // 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 </body> tag and insert our little module loader there. // we'll look for the closing </body> tag and insert our little module loader there.
if let Some(custom_index) = custom_index { if let Some(custom_index) = custom_index {
let rendered = custom_index let rendered = custom_index
.replace("</body>", &format!("{}</body>", MODULE_LOADER)) .replace("</body>", &format!("{}</body>", module_loader(root_name)))
.into_bytes(); .into_bytes();
Response::builder() Response::builder()
.header("Content-Type", "text/html") .header("Content-Type", "text/html")
@ -42,7 +54,7 @@ pub(super) fn desktop_handler(
if let Some(custom_head) = custom_head { if let Some(custom_head) = custom_head {
template = template.replace("<!-- CUSTOM HEAD -->", &custom_head); template = template.replace("<!-- CUSTOM HEAD -->", &custom_head);
} }
template = template.replace("<!-- MODULE LOADER -->", MODULE_LOADER); template = template.replace("<!-- MODULE LOADER -->", &module_loader(root_name));
Response::builder() Response::builder()
.header("Content-Type", "text/html") .header("Content-Type", "text/html")

View file

@ -19,6 +19,7 @@ euclid = "0.22.7"
enumset = "1.0.11" enumset = "1.0.11"
keyboard-types = "0.6.2" keyboard-types = "0.6.2"
async-trait = "0.1.58" async-trait = "0.1.58"
serde-value = "0.7.0"
[dependencies.web-sys] [dependencies.web-sys]
optional = true optional = true
@ -39,7 +40,10 @@ features = [
"ClipboardEvent", "ClipboardEvent",
] ]
[dev-dependencies]
serde_json = "*"
[features] [features]
default = [] default = ["serialize"]
serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde"] serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"]
wasm-bind = ["web-sys", "wasm-bindgen"] wasm-bind = ["web-sys", "wasm-bindgen"]

View file

@ -3,7 +3,7 @@ use dioxus_core::Event;
pub type AnimationEvent = Event<AnimationData>; pub type AnimationEvent = Event<AnimationData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct AnimationData { pub struct AnimationData {
pub animation_name: String, pub animation_name: String,
pub pseudo_element: String, pub pseudo_element: String,

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type ClipboardEvent = Event<ClipboardData>; pub type ClipboardEvent = Event<ClipboardData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClipboardData { pub struct ClipboardData {
// DOMDataTransfer clipboardData // DOMDataTransfer clipboardData
} }

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type CompositionEvent = Event<CompositionData>; pub type CompositionEvent = Event<CompositionData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompositionData { pub struct CompositionData {
pub data: String, pub data: String,
} }

View file

@ -1,5 +1,3 @@
use std::any::Any;
use dioxus_core::Event; use dioxus_core::Event;
use crate::MouseData; use crate::MouseData;
@ -10,12 +8,11 @@ pub type DragEvent = Event<DragData>;
/// placing a pointer device (such as a mouse) on the touch surface and then dragging the pointer to a new location /// 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 /// (such as another DOM element). Applications are free to interpret a drag and drop interaction in an
/// application-specific way. /// application-specific way.
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct DragData { pub struct DragData {
/// Inherit mouse data /// Inherit mouse data
pub mouse: MouseData, pub mouse: MouseData,
/// And then add the rest of the drag data
pub data: Box<dyn Any>,
} }
impl_event! { impl_event! {

View file

@ -3,7 +3,7 @@ use dioxus_core::Event;
pub type FocusEvent = Event<FocusData>; pub type FocusEvent = Event<FocusData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct FocusData {/* DOMEventInner: Send + SyncTarget relatedTarget */} pub struct FocusData {/* DOMEventInner: Send + SyncTarget relatedTarget */}
impl_event! [ impl_event! [

View file

@ -16,6 +16,12 @@ pub struct FormData {
pub files: Option<Arc<dyn FileEngine>>, pub files: Option<Arc<dyn FileEngine>>,
} }
impl PartialEq for FormData {
fn eq(&self, other: &Self) -> bool {
self.value == other.value && self.values == other.values
}
}
impl Debug for FormData { impl Debug for FormData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FormEvent") f.debug_struct("FormEvent")

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type ImageEvent = Event<ImageData>; pub type ImageEvent = Event<ImageData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageData { pub struct ImageData {
pub load_error: bool, pub load_error: bool,
} }

View file

@ -7,7 +7,7 @@ use std::str::FromStr;
pub type KeyboardEvent = Event<KeyboardData>; pub type KeyboardEvent = Event<KeyboardData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone)] #[derive(Clone, PartialEq, Eq)]
pub struct KeyboardData { pub struct KeyboardData {
#[deprecated( #[deprecated(
since = "0.3.0", since = "0.3.0",

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type MediaEvent = Event<MediaData>; pub type MediaEvent = Event<MediaData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct MediaData {} pub struct MediaData {}
impl_event! [ impl_event! [

View file

@ -10,7 +10,7 @@ pub type MouseEvent = Event<MouseData>;
/// A synthetic event that wraps a web-style [`MouseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) /// 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))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Default)] #[derive(Clone, Default, PartialEq)]
/// Data associated with a mouse event /// Data associated with a mouse event
/// ///
/// Do not use the deprecated fields; they may change or become private in the future. /// Do not use the deprecated fields; they may change or become private in the future.

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type PointerEvent = Event<PointerData>; pub type PointerEvent = Event<PointerData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct PointerData { pub struct PointerData {
// Mouse only // Mouse only
pub alt_key: bool, pub alt_key: bool,

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type ScrollEvent = Event<ScrollData>; pub type ScrollEvent = Event<ScrollData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScrollData {} pub struct ScrollData {}
impl_event! { impl_event! {

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type SelectionEvent = Event<SelectionData>; pub type SelectionEvent = Event<SelectionData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectionData {} pub struct SelectionData {}
impl_event! [ impl_event! [

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type ToggleEvent = Event<ToggleData>; pub type ToggleEvent = Event<ToggleData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToggleData {} pub struct ToggleData {}
impl_event! { impl_event! {

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type TouchEvent = Event<TouchData>; pub type TouchEvent = Event<TouchData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct TouchData { pub struct TouchData {
pub alt_key: bool, pub alt_key: bool,
pub ctrl_key: bool, pub ctrl_key: bool,

View file

@ -2,7 +2,7 @@ use dioxus_core::Event;
pub type TransitionEvent = Event<TransitionData>; pub type TransitionEvent = Event<TransitionData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct TransitionData { pub struct TransitionData {
pub property_name: String, pub property_name: String,
pub pseudo_element: String, pub pseudo_element: String,

View file

@ -6,7 +6,7 @@ use crate::geometry::{LinesVector, PagesVector, PixelsVector, WheelDelta};
pub type WheelEvent = Event<WheelData>; pub type WheelEvent = Event<WheelData>;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone)] #[derive(Clone, PartialEq, Default)]
pub struct WheelData { pub struct WheelData {
#[deprecated(since = "0.3.0", note = "use delta() instead")] #[deprecated(since = "0.3.0", note = "use delta() instead")]
pub delta_mode: u32, pub delta_mode: u32,

View file

@ -22,6 +22,12 @@ mod render_template;
#[cfg(feature = "wasm-bind")] #[cfg(feature = "wasm-bind")]
mod web_sys_bind; mod web_sys_bind;
#[cfg(feature = "serialize")]
mod transit;
#[cfg(feature = "serialize")]
pub use transit::*;
pub use elements::*; pub use elements::*;
pub use events::*; pub use events::*;
pub use global_attributes::*; pub use global_attributes::*;

View file

@ -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<D>(deserializer: D) -> Result<Self, D::Error>
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<EventData, serde_value::DeserializerError> {
use EventData::*;
// a little macro-esque thing to make the code below more readable
#[inline]
fn de<'de, F>(f: serde_value::Value) -> Result<F, serde_value::DeserializerError>
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<dyn Any> {
match self {
EventData::Mouse(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Clipboard(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Composition(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Keyboard(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Focus(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Form(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Drag(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Pointer(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Selection(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Touch(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Scroll(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Wheel(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Media(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
}
}
}
#[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);
}

View file

@ -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 { class ListenerMap {
constructor(root) { constructor(root) {
// bubbling events can listen at the root element // bubbling events can listen at the root element
@ -60,7 +52,7 @@ class ListenerMap {
} }
} }
export class Interpreter { class Interpreter {
constructor(root) { constructor(root) {
this.root = root; this.root = root;
this.listeners = new ListenerMap(root); this.listeners = new ListenerMap(root);
@ -352,6 +344,9 @@ export class Interpreter {
this.RemoveEventListener(edit.id, edit.name); this.RemoveEventListener(edit.id, edit.name);
break; break;
case "NewEventListener": case "NewEventListener":
let bubbles = event_bubbles(edit.name);
// this handler is only provided on desktop implementations since this // this handler is only provided on desktop implementations since this
// method is not used by the web implementation // method is not used by the web implementation
let handler = (event) => { let handler = (event) => {
@ -435,20 +430,21 @@ export class Interpreter {
} }
window.ipc.postMessage( window.ipc.postMessage(
serializeIpcMessage("user_event", { serializeIpcMessage("user_event", {
event: edit.name, name: edit.name,
mounted_dom_id: parseInt(realId), element: parseInt(realId),
contents: contents, data: contents,
bubbles,
}) })
); );
} }
}; };
this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler); this.NewEventListener(edit.name, edit.id, bubbles, handler);
break; break;
} }
} }
} }
export function serialize_event(event) { function serialize_event(event) {
switch (event.type) { switch (event.type) {
case "copy": case "copy":
case "cut": case "cut":

View file

@ -10,43 +10,63 @@ description = "Build server-side apps with Dioxus"
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
tokio = { version = "1", features = ["full"] } tokio = { version = "1.23.0", features = ["full"] }
futures-util = { version = "0.3", default-features = false, features = [ futures-util = { version = "0.3.25", default-features = false, features = [
"sink", "sink",
] } ] }
futures-channel = { version = "0.3.17", features = ["sink"] } futures-channel = { version = "0.3.25", features = ["sink"] }
pretty_env_logger = "0.4" pretty_env_logger = "0.4.0"
tokio-stream = { version = "0.1.1", features = ["net"] } tokio-stream = { version = "0.1.11", features = ["net"] }
serde = { version = "1.0.136", features = ["derive"] } serde = { version = "1.0.151", features = ["derive"] }
serde_json = "1.0.79" serde_json = "1.0.91"
tokio-util = { version = "0.7.0", features = ["full"] } tokio-util = { version = "0.7.4", features = ["full"] }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" } dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" }
dioxus-interpreter-js = { path = "../interpreter" }
# warp # warp
warp = { version = "0.3", optional = true } warp = { version = "0.3.3", optional = true }
# axum # axum
axum = { version = "0.5.1", optional = true, features = ["ws"] } axum = { version = "0.6.1", optional = true, features = ["ws"] }
tower = { version = "0.4.12", optional = true } tower = { version = "0.4.13", optional = true }
# salvo # 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] [dev-dependencies]
tokio = { version = "1", features = ["full"] } tokio = { version = "1.23.0", features = ["full"] }
dioxus = { path = "../dioxus" } dioxus = { path = "../dioxus" }
warp = "0.3" warp = "0.3.3"
axum = { version = "0.5.1", features = ["ws"] } axum = { version = "0.6.1", features = ["ws"] }
salvo = { version = "0.32.0", features = ["affix", "ws"] } salvo = { version = "0.37.7", features = ["affix", "ws"] }
tower = "0.4.12" tower = "0.4.13"
[features] [features]
default = [] 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"]

View file

@ -1,49 +1,3 @@
# Dioxus LiveView # Dioxus LiveView
Enabling server-rendered and hybrid applications with incredibly low latency (<1ms). Server rendered apps with minimal latency
```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" }
))
}
```

View file

@ -1,32 +1,53 @@
#[cfg(not(feature = "axum"))] use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
fn main() {} 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] #[tokio::main]
async fn main() { async fn main() {
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
use dioxus_core::{Element, LazyNodes, Scope};
pretty_env_logger::init(); 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 addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = dioxus_liveview::new(addr); let view = dioxus_liveview::LiveViewPool::new();
let body = view.body("<title>Dioxus Liveview</title>");
let app = Router::new() let app = Router::new()
.route("/", get(move || async { Html(body) }))
.route( .route(
"/app", "/",
get(move || async move {
Html(format!(
r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Warp</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
))
}),
)
.route(
"/ws",
get(move |ws: WebSocketUpgrade| async move { get(move |ws: WebSocketUpgrade| async move {
ws.on_upgrade(move |socket| 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()) axum::Server::bind(&addr.to_string().parse().unwrap())
.serve(app.into_make_service()) .serve(app.into_make_service())
.await .await

View file

@ -1,55 +1,66 @@
#[cfg(not(feature = "salvo"))] use dioxus::prelude::*;
fn main() {} 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] #[tokio::main]
async fn 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(); 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() let router = Router::new()
.hoop(affix::inject(Arc::new(view))) .hoop(affix::inject(Arc::new(view)))
.get(index) .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; Server::new(TcpListener::bind(addr)).serve(router).await;
}
#[handler]
fn index(depot: &mut Depot, res: &mut Response) { #[handler]
let view = depot.obtain::<Arc<Liveview>>().unwrap(); fn index(_depot: &mut Depot, res: &mut Response) {
let body = view.body("<title>Dioxus LiveView</title>"); let addr: SocketAddr = ([127, 0, 0, 1], 3030).into();
res.render(Text::Html(body)); res.render(Text::Html(format!(
} r#"
<!DOCTYPE html>
#[handler] <html>
async fn connect( <head> <title>Dioxus LiveView with Warp</title> </head>
req: &mut Request, <body> <div id="main"></div> </body>
depot: &mut Depot, {glue}
res: &mut Response, </html>
) -> Result<(), StatusError> { "#,
let view = depot.obtain::<Arc<Liveview>>().unwrap().clone(); glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws"))
let fut = WsHandler::new().handle(req, res)?; )));
let fut = async move { }
if let Some(ws) = fut.await {
view.upgrade_salvo(ws, app).await; #[handler]
} async fn connect(
}; req: &mut Request,
tokio::task::spawn(fut); depot: &mut Depot,
Ok(()) res: &mut Response,
} ) -> Result<(), StatusError> {
let view = depot.obtain::<Arc<LiveViewPool>>().unwrap().clone();
WebSocketUpgrade::new()
.upgrade(req, res, |ws| async move {
_ = view.launch(dioxus_liveview::salvo_socket(ws), app).await;
})
.await
} }

View file

@ -1,35 +1,56 @@
#[cfg(not(feature = "warp"))] use dioxus::prelude::*;
fn main() {} 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] #[tokio::main]
async fn 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(); 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 index = warp::path::end().map(move || {
let view = liveview::new(addr); warp::reply::html(format!(
let body = view.body("<title>Dioxus LiveView</title>"); r#"
<!DOCTYPE html>
<html>
<head> <title>Dioxus LiveView with Warp</title> </head>
<body> <div id="main"></div> </body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws/"))
))
});
let routes = warp::path::end() let pool = LiveViewPool::new();
.map(move || warp::reply::html(body.clone()))
.or(warp::path("app") let ws = warp::path("ws")
.and(warp::ws()) .and(warp::ws())
.and(warp::any().map(move || view.clone())) .and(warp::any().map(move || pool.clone()))
.map(|ws: Ws, view: liveview::Liveview| { .map(move |ws: Ws, pool: LiveViewPool| {
ws.on_upgrade(|socket| async move { ws.on_upgrade(|ws| async move {
view.upgrade_warp(socket, app).await; let _ = pool.launch(warp_socket(ws), app).await;
}) })
})); });
warp::serve(routes).run(addr).await;
println!("Listening on http://{}", addr);
warp::serve(index.or(ws)).run(addr).await;
} }

View file

@ -1,94 +1,23 @@
use crate::events; use crate::{LiveViewError, LiveViewSocket};
use axum::extract::ws::{Message, WebSocket}; use axum::extract::ws::{Message, WebSocket};
use dioxus_core::prelude::*; use futures_util::{SinkExt, StreamExt};
use futures_util::{
future::{select, Either},
pin_mut, SinkExt, StreamExt,
};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_util::task::LocalPoolHandle;
impl crate::Liveview { /// Convert a warp websocket into a LiveViewSocket
pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) { ///
connect(ws, self.pool.clone(), app, ()).await; /// 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)
pub async fn upgrade_axum_with_props<T>( .with(transform_tx)
&self, .sink_map_err(|_| LiveViewError::SendingFailed)
ws: WebSocket,
app: fn(Scope<T>) -> Element,
props: T,
) where
T: Send + Sync + 'static,
{
connect(ws, self.pool.clone(), app, props).await;
}
} }
pub async fn connect<T>( fn transform_rx(message: Result<Message, axum::Error>) -> Result<String, LiveViewError> {
socket: WebSocket, message
pool: LocalPoolHandle, .map_err(|_| LiveViewError::SendingFailed)?
app: fn(Scope<T>) -> Element, .into_text()
props: T, .map_err(|_| LiveViewError::SendingFailed)
) where }
T: Send + Sync + 'static,
{ async fn transform_tx(message: String) -> Result<Message, axum::Error> {
let (mut user_ws_tx, mut user_ws_rx) = socket.split(); Ok(Message::Text(message))
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();
} }

View file

@ -1,110 +1,25 @@
use crate::events; use futures_util::{SinkExt, StreamExt};
use dioxus_core::prelude::*; use salvo::ws::{Message, WebSocket};
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;
impl crate::Liveview { use crate::{LiveViewError, LiveViewSocket};
pub async fn upgrade_salvo(&self, ws: salvo::extra::ws::WebSocket, app: fn(Scope) -> Element) {
connect(ws, self.pool.clone(), app, ()).await; /// Convert a salvo websocket into a LiveViewSocket
} ///
pub async fn upgrade_salvo_with_props<T>( /// This is required to launch a LiveView app using the warp web framework
&self, pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket {
ws: salvo::extra::ws::WebSocket, ws.map(transform_rx)
app: fn(Scope<T>) -> Element, .with(transform_tx)
props: T, .sink_map_err(|_| LiveViewError::SendingFailed)
) where
T: Send + Sync + 'static,
{
connect(ws, self.pool.clone(), app, props).await;
}
} }
pub async fn connect<T>( fn transform_rx(message: Result<Message, salvo::Error>) -> Result<String, LiveViewError> {
ws: WebSocket, let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?;
pool: LocalPoolHandle,
app: fn(Scope<T>) -> Element,
props: T,
) where
T: Send + Sync + 'static,
{
// Use a counter to assign a new unique ID for this user.
// Split the socket into a sender and receive of messages. let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?;
let (mut user_ws_tx, mut user_ws_rx) = ws.split();
let (event_tx, event_rx) = mpsc::unbounded_channel(); Ok(msg)
let (edits_tx, edits_rx) = mpsc::unbounded_channel(); }
let mut edits_rx = UnboundedReceiverStream::new(edits_rx); async fn transform_tx(message: String) -> Result<Message, salvo::Error> {
let mut event_rx = UnboundedReceiverStream::new(event_rx); Ok(Message::text(message))
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();
} }

View file

@ -1,110 +1,28 @@
use crate::events; use crate::{LiveViewError, LiveViewSocket};
use dioxus_core::prelude::*; use futures_util::{SinkExt, StreamExt};
use futures_util::{pin_mut, SinkExt, StreamExt};
use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_util::task::LocalPoolHandle;
use warp::ws::{Message, WebSocket}; use warp::ws::{Message, WebSocket};
impl crate::Liveview { /// Convert a warp websocket into a LiveViewSocket
pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) { ///
connect(ws, self.pool.clone(), app, ()).await; /// This is required to launch a LiveView app using the warp web framework
} pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket {
pub async fn upgrade_warp_with_props<T>( ws.map(transform_rx)
&self, .with(transform_tx)
ws: warp::ws::WebSocket, .sink_map_err(|_| LiveViewError::SendingFailed)
app: fn(Scope<T>) -> Element,
props: T,
) where
T: Send + Sync + 'static,
{
connect(ws, self.pool.clone(), app, props).await;
}
} }
pub async fn connect<T>( fn transform_rx(message: Result<Message, warp::Error>) -> Result<String, LiveViewError> {
ws: WebSocket, // destructure the message into the buffer we got from warp
pool: LocalPoolHandle, let msg = message
app: fn(Scope<T>) -> Element, .map_err(|_| LiveViewError::SendingFailed)?
props: T, .into_bytes();
) where
T: Send + Sync + 'static,
{
// Use a counter to assign a new unique ID for this user.
// Split the socket into a sender and receive of messages. // transform it back into a string, saving us the allocation
let (mut user_ws_tx, mut user_ws_rx) = ws.split(); let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?;
let (event_tx, event_rx) = mpsc::unbounded_channel(); Ok(msg)
let (edits_tx, edits_rx) = mpsc::unbounded_channel(); }
let mut edits_rx = UnboundedReceiverStream::new(edits_rx); async fn transform_tx(message: String) -> Result<Message, warp::Error> {
let mut event_rx = UnboundedReceiverStream::new(event_rx); Ok(Message::text(message))
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();
} }

View file

@ -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<IpcMessage> {
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<dyn Any> {
match name {
"copy" | "cut" | "paste" => {
//
Arc::new(ClipboardData {})
}
"compositionend" | "compositionstart" | "compositionupdate" => {
Arc::new(serde_json::from_value::<CompositionData>(val).unwrap())
}
"keydown" | "keypress" | "keyup" => {
let evt = serde_json::from_value::<KeyboardData>(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::<FormData>(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::<MouseData>(val).unwrap())
}
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
Arc::new(serde_json::from_value::<PointerData>(val).unwrap())
}
"select" => {
//
Arc::new(serde_json::from_value::<SelectionData>(val).unwrap())
}
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
Arc::new(serde_json::from_value::<TouchData>(val).unwrap())
}
"scroll" => Arc::new(()),
"wheel" => Arc::new(serde_json::from_value::<WheelData>(val).unwrap()),
"animationstart" | "animationend" | "animationiteration" => {
Arc::new(serde_json::from_value::<AnimationData>(val).unwrap())
}
"transitionend" => Arc::new(serde_json::from_value::<TransitionData>(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")
}
}
}

View file

@ -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;
}
}

View file

@ -1,56 +1,55 @@
#![allow(dead_code)]
pub(crate) mod events;
pub mod adapters { pub mod adapters {
#[cfg(feature = "warp")] #[cfg(feature = "warp")]
pub mod warp_adapter; pub mod warp_adapter;
#[cfg(feature = "warp")]
pub use warp_adapter::*;
#[cfg(feature = "axum")] #[cfg(feature = "axum")]
pub mod axum_adapter; pub mod axum_adapter;
#[cfg(feature = "axum")]
pub use axum_adapter::*;
#[cfg(feature = "salvo")] #[cfg(feature = "salvo")]
pub mod salvo_adapter; 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 trait WebsocketTx: SinkExt<String, Error = LiveViewError> {}
pub struct Liveview { impl<T> WebsocketTx for T where T: SinkExt<String, Error = LiveViewError> {}
pool: LocalPoolHandle,
addr: String, pub trait WebsocketRx: StreamExt<Item = Result<String, LiveViewError>> {}
impl<T> WebsocketRx for T where T: StreamExt<Item = Result<String, LiveViewError>> {}
#[derive(Debug, thiserror::Error)]
pub enum LiveViewError {
#[error("warp error")]
SendingFailed,
} }
impl Liveview { use dioxus_interpreter_js::INTERPRETER_JS;
pub fn body(&self, header: &str) -> String { static MAIN_JS: &str = include_str!("./main.js");
format!(
r#"
<!DOCTYPE html>
<html>
<head>
{header}
</head>
<body>
<div id="main"></div>
<script>
var WS_ADDR = "ws://{addr}/app";
{interpreter}
main();
</script>
</body>
</html>"#,
addr = self.addr,
interpreter = include_str!("../src/interpreter.js")
)
}
}
pub fn new(addr: impl Into<SocketAddr>) -> Liveview { /// This script that gets injected into your app connects this page to the websocket endpoint
let addr: SocketAddr = addr.into(); ///
/// Once the endpoint is connected, it will send the initial state of the app, and then start
Liveview { /// processing user events and returning edits to the liveview instance
pool: LocalPoolHandle::new(16), pub fn interpreter_glue(url: &str) -> String {
addr: addr.to_string(), format!(
} r#"
<script>
var WS_ADDR = "{url}";
{INTERPRETER_JS}
{MAIN_JS}
main();
</script>
"#
)
} }

View file

@ -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);
}
}

View file

@ -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<T: Send + 'static>(
&self,
ws: impl LiveViewSocket,
app: fn(Scope<T>) -> 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<Message, axum::Error>) -> Result<String, LiveViewError> {
/// message
/// .map_err(|_| LiveViewError::SendingFailed)?
/// .into_text()
/// .map_err(|_| LiveViewError::SendingFailed)
/// }
///
/// async fn transform_tx(message: String) -> Result<Message, axum::Error> {
/// Ok(Message::Text(message))
/// }
/// ```
pub trait LiveViewSocket:
SinkExt<String, Error = LiveViewError>
+ StreamExt<Item = Result<String, LiveViewError>>
+ Send
+ 'static
{
}
impl<S> LiveViewSocket for S where
S: SinkExt<String, Error = LiveViewError>
+ StreamExt<Item = Result<String, LiveViewError>>
+ 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<T>(
app: Component<T>,
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::<IpcMessage>(&evt) {
vdom.handle_event(&params.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?;
}
}

View file

@ -200,7 +200,7 @@ impl Cursor {
} }
change -= 1; change -= 1;
} }
c.move_col(change as i32, text); c.move_col(change, text);
}, },
data.modifiers().contains(Modifiers::SHIFT), data.modifiers().contains(Modifiers::SHIFT),
); );

View file

@ -47,5 +47,5 @@ pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element {
router.replace_route(cx.props.to, None, None); router.replace_route(cx.props.to, None, None);
} }
cx.render(rsx!(())) None
} }

View file

@ -52,6 +52,6 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
cx.render(rsx!(&cx.props.children)) cx.render(rsx!(&cx.props.children))
} else { } else {
log::debug!("Route should *not* render: {:?}", cx.scope_id()); log::debug!("Route should *not* render: {:?}", cx.scope_id());
cx.render(rsx!(())) None
} }
} }

1
packages/rsx/README.md Normal file
View file

@ -0,0 +1 @@
# The actual RSX language implemented using syn parsers.

View file

@ -171,7 +171,7 @@ impl ToTokens for Component {
toks.append_all(quote! { toks.append_all(quote! {
.children( .children(
Ok({ #renderer }) Some({ #renderer })
) )
}); });
} }

View file

@ -34,7 +34,7 @@ use syn::{
}; };
/// Fundametnally, every CallBody is a template /// Fundametnally, every CallBody is a template
#[derive(Default)] #[derive(Default, Debug)]
pub struct CallBody { pub struct CallBody {
pub roots: Vec<BodyNode>, pub roots: Vec<BodyNode>,
@ -70,7 +70,7 @@ impl ToTokens for CallBody {
if self.inline_cx { if self.inline_cx {
out_tokens.append_all(quote! { out_tokens.append_all(quote! {
Ok({ Some({
let __cx = cx; let __cx = cx;
#body #body
}) })

View file

@ -1,3 +1,5 @@
// use crate::{raw_expr::RawExprNode, text::TextNode};
use super::*; use super::*;
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::{Span, TokenStream as TokenStream2};
@ -29,7 +31,7 @@ pub enum BodyNode {
impl BodyNode { impl BodyNode {
pub fn is_litstr(&self) -> bool { pub fn is_litstr(&self) -> bool {
matches!(self, BodyNode::Text(_)) matches!(self, BodyNode::Text { .. })
} }
pub fn span(&self) -> Span { pub fn span(&self) -> Span {

View file

@ -23,7 +23,7 @@ pub fn render_lazy(f: LazyNodes<'_, '_>) -> String {
fn lazy_app<'a>(cx: Scope<'a, RootProps<'static, 'static>>) -> Element<'a> { fn lazy_app<'a>(cx: Scope<'a, RootProps<'static, 'static>>) -> Element<'a> {
let lazy = cx.props.caller.take().unwrap(); let lazy = cx.props.caller.take().unwrap();
let lazy: LazyNodes = unsafe { std::mem::transmute(lazy) }; let lazy: LazyNodes = unsafe { std::mem::transmute(lazy) };
Ok(lazy.call(cx)) Some(lazy.call(cx))
} }
let props: RootProps = unsafe { let props: RootProps = unsafe {

View file

@ -51,7 +51,7 @@ impl Renderer {
) -> std::fmt::Result { ) -> std::fmt::Result {
// We should never ever run into async or errored nodes in SSR // We should never ever run into async or errored nodes in SSR
// Error boundaries and suspense boundaries will convert these to sync // 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)? self.render_template(buf, dom, node)?
}; };
@ -89,7 +89,7 @@ impl Renderer {
let scope = dom.get_scope(id).unwrap(); let scope = dom.get_scope(id).unwrap();
let node = scope.root_node(); let node = scope.root_node();
match node { match node {
RenderReturn::Sync(Ok(node)) => { RenderReturn::Sync(Some(node)) => {
self.render_template(buf, dom, node)? self.render_template(buf, dom, node)?
} }
_ => todo!( _ => todo!(

View file

@ -17,7 +17,7 @@ fn app(cx: Scope) -> Element {
width: "100%", width: "100%",
background_color: "hsl({hue}, 70%, {brightness}%)", background_color: "hsl({hue}, 70%, {brightness}%)",
onmousemove: move |evt| { 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() { if let Some(id) = node.root_ids[0].get() {
let node = tui_query.get(id); let node = tui_query.get(id);
let Size{width, height} = node.size().unwrap(); let Size{width, height} = node.size().unwrap();

View file

@ -10,7 +10,7 @@ use dioxus_core::{ElementId, RenderReturn, Scope};
pub use input::*; pub use input::*;
pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<ElementId> { pub(crate) fn get_root_id<T>(cx: Scope<T>) -> Option<ElementId> {
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()) sync.root_ids.get(0).and_then(|id| id.get())
} else { } else {
None None

View file

@ -99,7 +99,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a
update(text.clone()); update(text.clone());
}; };
render! { cx.render(rsx! {
div{ div{
width: "{width}", width: "{width}",
height: "{height}", 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 Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start; 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 let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){ if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap(); 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 Point{ x, y } = node.pos().unwrap();
let Pos { col, row } = cursor.read().start; 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 let Ok(pos) = crossterm::cursor::position() {
if pos != (x, y){ if pos != (x, y){
execute!(stdout(), MoveTo(x, y)).unwrap(); 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}" "{text_after_second_cursor}"
} }
} })
} }

Some files were not shown because too many files have changed in this diff Show more