diff --git a/Cargo.toml b/Cargo.toml index 86a952e7f..7266b6cee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com" documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] -rust-version = "1.56.0" +rust-version = "1.60.0" [dependencies] dioxus-core = { path = "./packages/core", version = "^0.2.1" } @@ -26,6 +26,8 @@ dioxus-router = { path = "./packages/router", version = "^0.2.3", optional = tru dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.2.1", optional = true } dioxus-tui = { path = "./packages/tui", version = "^0.2.2", optional = true } +dioxus-rsx = { path = "./packages/rsx", optional = true } +dioxus-rsx-interpreter = { path = "./packages/rsx_interpreter", optional = true } dioxus-liveview = { path = "./packages/liveview", version = "^0.1.0", optional = true } dioxus-native-core = { path = "./packages/native-core", version = "^0.2.0", optional = true } @@ -38,15 +40,16 @@ dioxus-native-core-macro = { path = "./packages/native-core-macro", version = "^ [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro"] +macro = ["dioxus-core-macro", "dioxus-rsx"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] -web = ["dioxus-web", "dioxus-router/web"] +web = ["dioxus-web", "dioxus-router?/web"] desktop = ["dioxus-desktop"] router = ["dioxus-router"] tui = ["dioxus-tui"] liveview = ["dioxus-liveview"] +hot-reload = ["dioxus-core-macro/hot-reload", "dioxus-rsx-interpreter", "dioxus-desktop?/hot-reload", "dioxus-web?/hot-reload"] native-core = ["dioxus-native-core", "dioxus-native-core-macro"] @@ -64,6 +67,8 @@ members = [ "packages/fermi", "packages/tui", "packages/liveview", + "packages/rsx", + "packages/rsx_interpreter", "packages/native-core", "packages/native-core-macro", ] @@ -86,6 +91,7 @@ criterion = "0.3.5" thiserror = "1.0.30" env_logger = "0.9.0" + [[bench]] name = "create" harness = false diff --git a/benches/tui_update.rs b/benches/tui_update.rs index d46921a8d..811c40739 100644 --- a/benches/tui_update.rs +++ b/benches/tui_update.rs @@ -80,11 +80,11 @@ fn Grid(cx: Scope) -> Element { counts.with_mut(|c| { let i = *count.current(); c[i] += 1; - c[i] = c[i] % 360; + c[i] %= 360; }); count.with_mut(|i| { *i += 1; - *i = *i % (size * size); + *i %= size * size; }); } diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index c20258066..778cabe4a 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -71,7 +71,7 @@ Desktop APIs will likely be in flux as we figure out better patterns than our El [Jump to the getting started guide for Desktop.](/reference/platforms/desktop) Examples: -- [File explorer](https://github.com/dioxusLabs/file-explorer/) +- [File explorer](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) - [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) [![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer) diff --git a/docs/guide/src/interactivity/index.md b/docs/guide/src/interactivity/index.md index 02c39b5b1..670ca5a40 100644 --- a/docs/guide/src/interactivity/index.md +++ b/docs/guide/src/interactivity/index.md @@ -64,7 +64,7 @@ The most common hook you'll use for storing state is `use_state`. `use_state` pr ```rust fn App(cx: Scope)-> Element { - let (post, set_post) = use_state(&cx, || { + let post = use_state(&cx, || { PostData { id: Uuid::new_v4(), score: 10, @@ -112,11 +112,11 @@ For example, let's say we provide a button to generate a new post. Whenever the ```rust fn App(cx: Scope)-> Element { - let (post, set_post) = use_state(&cx, || PostData::new()); + let post = use_state(&cx, || PostData::new()); cx.render(rsx!{ button { - onclick: move |_| set_post(PostData::random()) + onclick: move |_| post.set(PostData::random()) "Generate a random post" } Post { props: &post } @@ -141,19 +141,19 @@ We can use tasks in our components to build a tiny stopwatch that ticks every se ```rust fn App(cx: Scope)-> Element { - let (elapsed, set_elapsed) = use_state(&cx, || 0); - - use_future(&cx, || { - to_owned![set_elapsed]; // explicitly capture this hook for use in async + let elapsed = use_state(&cx, || 0); + + use_future(&cx, (), |()| { + to_owned![elapsed]; // explicitly capture this hook for use in async async move { loop { - TimeoutFuture::from_ms(1000).await; - set_elapsed.modify(|i| i + 1) + gloo_timers::future::TimeoutFuture::new(1_000).await; + elapsed.modify(|i| i + 1) } } }); - rsx!(cx, div { "Current stopwatch time: {sec_elapsed}" }) + rsx!(cx, div { "Current stopwatch time: {elapsed}" }) } ``` diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 5e4c9e347..3244ce301 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -59,7 +59,7 @@ When using Debian/bullseye `libappindicator3-dev` is no longer available but rep sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev ``` -If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/en/docs/get-started/setup-linux). +If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/v1/guides/getting-started/prerequisites#setting-up-linux). ### macOS diff --git a/docs/reference/src/SUMMARY.md b/docs/reference/src/SUMMARY.md index 18f351bfa..3918268d3 100644 --- a/docs/reference/src/SUMMARY.md +++ b/docs/reference/src/SUMMARY.md @@ -21,6 +21,7 @@ - [Custom Renderer](guide/custom_renderer.md) - [Server-side components](guide/server_side_components.md) - [Bundling and Distributing](guide/bundline.md) + - [Hot Reloading Rsx](guide/hot_reloading.md) - [Reference Guide](reference/reference.md) - [Anti-patterns](reference/anti.md) diff --git a/docs/reference/src/guide/hot_reloading.md b/docs/reference/src/guide/hot_reloading.md new file mode 100644 index 000000000..8560c62f9 --- /dev/null +++ b/docs/reference/src/guide/hot_reloading.md @@ -0,0 +1,24 @@ +# Hot Reloading +1. Hot reloading allows much faster iteration times inside of rsx calls by interperting them and streaming the edits. +2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program. +3. Currently the cli only implements hot reloading for the web renderer. + +# Setup +Install [dioxus-cli](https://github.com/DioxusLabs/cli). +Enable the hot-reload feature on dioxus: +```toml +dioxus = { version = "*", features = ["web", "hot-reload"] } +``` + +# Usage +1. run: +``` +dioxus serve --hot-reload +``` +2. change some code within a rsx macro +3. open your localhost in a browser +4. save and watch the style change without recompiling + +# Limitations +1. The interperter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will trigger a full recompile to capture the expression. +2. Components and Iterators can contain abritary rust code, and will trigger a full recompile when changed. \ No newline at end of file diff --git a/docs/reference/src/platforms/desktop.md b/docs/reference/src/platforms/desktop.md index 18393f132..985fbe6d9 100644 --- a/docs/reference/src/platforms/desktop.md +++ b/docs/reference/src/platforms/desktop.md @@ -11,7 +11,7 @@ Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Ca ```shell $ cargo new --bin demo -$ cd app +$ cd demo ``` Add Dioxus with the `desktop` feature: diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 490aea844..774eb0ead 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -46,6 +46,9 @@ fn main() { /// This type alias specifies the type for you so you don't need to write "None as Option<()>" const NONE_ELEMENT: Option<()> = None; +use core::{fmt, str::FromStr}; +use std::fmt::Display; + use baller::Baller; use dioxus::prelude::*; @@ -187,6 +190,15 @@ fn app(cx: Scope) -> Element { text: "using functionc all syntax" ) + // Components can be geneirc too + // This component takes i32 type to give you typed input + TypedInput::> {} + // Type inference can be used too + TypedInput { initial: 10.0 } + // geneircs with the `inline_props` macro + label(text: "hello geneirc world!") + label(text: 99.9) + // helper functions // Single values must be wrapped in braces or `Some` to satisfy `IntoIterator` [helper(&cx, "hello world!")] @@ -227,9 +239,35 @@ pub fn Taller<'a>(cx: Scope<'a, TallerProps<'a>>) -> Element { }) } +#[derive(Props, PartialEq)] +pub struct TypedInputProps { + #[props(optional, default)] + initial: Option, +} + +#[allow(non_snake_case)] +pub fn TypedInput(_: Scope>) -> Element +where + T: FromStr + fmt::Display, + ::Err: std::fmt::Display, +{ + todo!() +} + #[inline_props] fn with_inline<'a>(cx: Scope<'a>, text: &'a str) -> Element { cx.render(rsx! { p { "{text}" } }) } + +// generic component with inline_props too +#[inline_props] +fn label(cx: Scope, text: T) -> Element +where + T: Display, +{ + cx.render(rsx! { + p { "{text}" } + }) +} diff --git a/examples/svg_basic.rs b/examples/svg_basic.rs new file mode 100644 index 000000000..40eb80b23 --- /dev/null +++ b/examples/svg_basic.rs @@ -0,0 +1,75 @@ +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + cx.render(rsx!( svg { + width: "200", + height: "250", + xmlns: "http://www.w3.org/2000/svg", + version: "1.1", + rect { + x: "10", + y: "10", + width: "30", + height: "30", + stroke: "black", + fill: "transparent", + stroke_width: "5", + } + rect { + x: "60", + y: "10", + width: "30", + height: "30", + stroke: "black", + fill: "transparent", + stroke_width: "5", + } + circle { + cx: "25", + cy: "75", + r: "20", + stroke: "red", + fill: "transparent", + stroke_width: "5", + } + ellipse { + cx: "75", + cy: "75", + rx: "20", + ry: "5", + stroke: "red", + fill: "transparent", + stroke_width: "5", + } + line { + x1: "10", + x2: "50", + y1: "110", + y2: "150", + stroke: "orange", + stroke_width: "5", + } + polyline { + points: "60 110 65 120 70 115 75 130 80 125 85 140 90 135 95 150 100 145", + stroke: "orange", + fill: "transparent", + stroke_width: "5", + } + polygon { + points: "50 160 55 180 70 180 60 190 65 205 50 195 35 205 40 190 30 180 45 180", + stroke: "green", + fill: "transparent", + stroke_width: "5", + } + path { + d: "M20,230 Q40,205 50,230 T90,230", + fill: "none", + stroke: "blue", + stroke_width: "5", + } + })) +} + +fn main() { + dioxus::desktop::launch(app); +} diff --git a/examples/tui_buttons.rs b/examples/tui_buttons.rs new file mode 100644 index 000000000..31d2e1ce0 --- /dev/null +++ b/examples/tui_buttons.rs @@ -0,0 +1,93 @@ +use dioxus::{events::KeyCode, prelude::*}; + +fn main() { + dioxus::tui::launch(app); +} + +#[derive(PartialEq, Props)] +struct ButtonProps { + color_offset: u32, + layer: u16, +} + +#[allow(non_snake_case)] +fn Button(cx: Scope) -> Element { + let toggle = use_state(&cx, || false); + let hovered = use_state(&cx, || false); + + let hue = cx.props.color_offset % 255; + let saturation = if *toggle.get() { 50 } else { 25 } + if *hovered.get() { 50 } else { 25 }; + let brightness = saturation / 2; + let color = format!("hsl({hue}, {saturation}, {brightness})"); + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + background_color: "{color}", + tabindex: "{cx.props.layer}", + onkeydown: |e| { + if let KeyCode::Space = e.data.key_code{ + toggle.modify(|f| !f); + } + }, + onclick: |_| { + toggle.modify(|f| !f); + }, + onmouseenter: |_|{ + hovered.set(true); + }, + onmouseleave: |_|{ + hovered.set(false); + }, + justify_content: "center", + align_items: "center", + display: "flex", + flex_direction: "column", + + p{"tabindex: {cx.props.layer}"} + } + }) +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + display: "flex", + flex_direction: "column", + width: "100%", + height: "100%", + + (1..8).map(|y| + cx.render(rsx!{ + div{ + display: "flex", + flex_direction: "row", + width: "100%", + height: "100%", + (1..8).map(|x|{ + if (x + y) % 2 == 0{ + cx.render(rsx!{ + div{ + width: "100%", + height: "100%", + background_color: "rgb(100, 100, 100)", + } + }) + } + else{ + let layer = (x + y) % 3; + cx.render(rsx!{ + Button{ + color_offset: x * y, + layer: layer as u16, + } + }) + } + }) + } + }) + ) + } + }) +} diff --git a/examples/tui_keys.rs b/examples/tui_keys.rs index 2417a3077..f947f97d0 100644 --- a/examples/tui_keys.rs +++ b/examples/tui_keys.rs @@ -11,9 +11,9 @@ fn main() { fn app(cx: Scope) -> Element { let key = use_state(&cx, || "".to_string()); - let mouse = use_state(&cx, || ScreenPoint::zero()); + let mouse = use_state(&cx, ScreenPoint::zero); let count = use_state(&cx, || 0); - let buttons = use_state(&cx, || MouseButtonSet::empty()); + let buttons = use_state(&cx, MouseButtonSet::empty); let mouse_clicked = use_state(&cx, || false); let key_down_handler = move |evt: KeyboardEvent| { diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index 358f5edf0..f6b352689 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -15,12 +15,17 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] proc-macro = true [dependencies] -proc-macro-error = "1" -proc-macro2 = { version = "1.0.6" } +proc-macro2 = { version = "1.0" } quote = "1.0" -syn = { version = "1.0.11", features = ["full", "extra-traits"] } +syn = { version = "1.0", features = ["full", "extra-traits"] } +dioxus-rsx = { path = "../rsx" } +dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true } # testing [dev-dependencies] rustversion = "1.0" trybuild = "1.0" + +[features] +default = [] +hot-reload = ["dioxus-rsx-interpreter"] \ No newline at end of file diff --git a/packages/core-macro/src/ifmt.rs b/packages/core-macro/src/ifmt.rs deleted file mode 100644 index 912d78f7e..000000000 --- a/packages/core-macro/src/ifmt.rs +++ /dev/null @@ -1,228 +0,0 @@ -use ::quote::{quote, ToTokens}; -use ::std::ops::Not; -use ::syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - *, -}; -use proc_macro2::TokenStream; - -pub fn format_args_f_impl(input: IfmtInput) -> Result { - let IfmtInput { - mut format_literal, - mut positional_args, - mut named_args, - } = input; - - let s = format_literal.value(); - let out_format_literal = &mut String::with_capacity(s.len()); - - let mut iterator = s.char_indices().peekable(); - while let Some((i, c)) = iterator.next() { - out_format_literal.push(c); - if c != '{' { - continue; - } - // encountered `{`, let's see if it was `{{` - if let Some(&(_, '{')) = iterator.peek() { - let _ = iterator.next(); - out_format_literal.push('{'); - continue; - } - let (end, colon_or_closing_brace) = - iterator - .find(|&(_, c)| c == '}' || c == ':') - .expect(concat!( - "Invalid format string literal\n", - "note: if you intended to print `{`, ", - "you can escape it using `{{`", - )); - // We use defer to ensure all the `continue`s append the closing char. - let mut out_format_literal = defer(&mut *out_format_literal, |it| { - it.push(colon_or_closing_brace) - }); - let out_format_literal: &mut String = *out_format_literal; - let mut arg = s[i + 1..end].trim(); - if let Some("=") = arg.get(arg.len().saturating_sub(1)..) { - assert_eq!( - out_format_literal.pop(), // Remove the opening brace - Some('{'), - ); - arg = &arg[..arg.len() - 1]; - out_format_literal.push_str(arg); - out_format_literal.push_str(" = {"); - } - if arg.is_empty() { - continue; - } - - enum Segment { - Ident(Ident), - LitInt(LitInt), - } - let segments: Vec = { - impl Parse for Segment { - fn parse(input: ParseStream<'_>) -> Result { - let lookahead = input.lookahead1(); - if lookahead.peek(Ident) { - input.parse().map(Segment::Ident) - } else if lookahead.peek(LitInt) { - input.parse().map(Segment::LitInt) - } else { - Err(lookahead.error()) - } - } - } - match ::syn::parse::Parser::parse_str( - Punctuated::::parse_separated_nonempty, - arg, - ) { - Ok(segments) => segments.into_iter().collect(), - Err(err) => return Err(err), - } - }; - match segments.len() { - 0 => unreachable!("`parse_separated_nonempty` returned empty"), - 1 => { - out_format_literal.push_str(arg); - match { segments }.pop().unwrap() { - Segment::LitInt(_) => { - // found something like `{0}`, let `format_args!` - // handle it. - continue; - } - Segment::Ident(ident) => { - // if `ident = ...` is not yet among the extra args - if named_args.iter().all(|(it, _)| *it != ident) { - named_args.push(( - ident.clone(), - parse_quote!(#ident), // Expr - )); - } - } - } - } - _ => { - ::std::fmt::Write::write_fmt( - out_format_literal, - format_args!("{}", positional_args.len()), - ) - .expect("`usize` or `char` Display impl cannot panic"); - let segments: Punctuated = segments - .into_iter() - .map(|it| match it { - Segment::Ident(ident) => ident.into_token_stream(), - Segment::LitInt(literal) => literal.into_token_stream(), - }) - .collect(); - positional_args.push(parse_quote! { - #segments - }) - } - } - } - - let named_args = named_args.into_iter().map(|(ident, expr)| { - quote! { - #ident = #expr - } - }); - format_literal = LitStr::new(out_format_literal, format_literal.span()); - - Ok(quote! { - format_args!( - #format_literal - #(, #positional_args)* - #(, #named_args)* - ) - }) -} - -#[allow(dead_code)] // dumb compiler does not see the struct being used... -pub struct IfmtInput { - format_literal: LitStr, - positional_args: Vec, - named_args: Vec<(Ident, Expr)>, -} - -impl Parse for IfmtInput { - fn parse(input: ParseStream) -> Result { - let format_literal = input.parse()?; - let mut positional_args = vec![]; - loop { - if input.parse::>()?.is_none() { - return Ok(Self { - format_literal, - positional_args, - named_args: vec![], - }); - } - if input.peek(Ident) && input.peek2(Token![=]) && input.peek3(Token![=]).not() { - // Found a positional parameter - break; - } - positional_args.push(input.parse()?); - } - let named_args = Punctuated::<_, Token![,]>::parse_terminated_with(input, |input| { - Ok({ - let name: Ident = input.parse()?; - let _: Token![=] = input.parse()?; - let expr: Expr = input.parse()?; - (name, expr) - }) - })? - .into_iter() - .collect(); - Ok(Self { - format_literal, - positional_args, - named_args, - }) - } -} - -pub fn defer<'a, T: 'a, Drop: 'a>(x: T, drop: Drop) -> impl ::core::ops::DerefMut + 'a -where - Drop: FnOnce(T), -{ - use ::core::mem::ManuallyDrop; - struct Ret(ManuallyDrop, ManuallyDrop) - where - Drop: FnOnce(T); - impl ::core::ops::Drop for Ret - where - Drop: FnOnce(T), - { - fn drop(&'_ mut self) { - use ::core::ptr; - unsafe { - // # Safety - // - // - This is the canonical example of using `ManuallyDrop`. - let value = ManuallyDrop::into_inner(ptr::read(&self.0)); - let drop = ManuallyDrop::into_inner(ptr::read(&self.1)); - drop(value); - } - } - } - impl ::core::ops::Deref for Ret - where - Drop: FnOnce(T), - { - type Target = T; - #[inline] - fn deref(&'_ self) -> &'_ Self::Target { - &self.0 - } - } - impl ::core::ops::DerefMut for Ret - where - Drop: FnOnce(T), - { - #[inline] - fn deref_mut(&'_ mut self) -> &'_ mut Self::Target { - &mut self.0 - } - } - Ret(ManuallyDrop::new(x), ManuallyDrop::new(drop)) -} diff --git a/packages/core-macro/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs index 782d13a48..05d9d5240 100644 --- a/packages/core-macro/src/inlineprops.rs +++ b/packages/core-macro/src/inlineprops.rs @@ -17,6 +17,7 @@ pub struct InlinePropsBody { pub inputs: Punctuated, // pub fields: FieldsNamed, pub output: ReturnType, + pub where_clause: Option, pub block: Box, } @@ -28,7 +29,7 @@ impl Parse for InlinePropsBody { let fn_token = input.parse()?; let ident = input.parse()?; - let generics = input.parse()?; + let generics: Generics = input.parse()?; let content; let paren_token = syn::parenthesized!(content in input); @@ -47,6 +48,11 @@ impl Parse for InlinePropsBody { let output = input.parse()?; + let where_clause = input + .peek(syn::token::Where) + .then(|| input.parse()) + .transpose()?; + let block = input.parse()?; Ok(Self { @@ -57,6 +63,7 @@ impl Parse for InlinePropsBody { paren_token, inputs, output, + where_clause, block, cx_token, attrs, @@ -73,6 +80,7 @@ impl ToTokens for InlinePropsBody { generics, inputs, output, + where_clause, block, cx_token, attrs, @@ -136,12 +144,16 @@ impl ToTokens for InlinePropsBody { out_tokens.append_all(quote! { #modifiers #[allow(non_camel_case_types)] - #vis struct #struct_name #struct_generics { + #vis struct #struct_name #struct_generics + #where_clause + { #(#fields),* } #(#attrs)* - #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output { + #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output + #where_clause + { let #struct_name { #(#field_names),* } = &cx.props; #block } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 931933386..35298703c 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -2,14 +2,15 @@ use proc_macro::TokenStream; use quote::ToTokens; use syn::parse_macro_input; -mod ifmt; mod inlineprops; mod props; -mod rsx; + +// mod rsx; +use dioxus_rsx as rsx; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { - use ifmt::*; + use rsx::*; let item = parse_macro_input!(input as IfmtInput); format_args_f_impl(item) .unwrap_or_else(|err| err.to_compile_error()) @@ -29,159 +30,45 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// /// ## Complete Reference Guide: /// ``` -/// const Example: Component = |cx| { -/// let formatting = "formatting!"; -/// let formatting_tuple = ("a", "b"); -/// let lazy_fmt = format_args!("lazily formatted text"); -/// cx.render(rsx! { -/// div { -/// // Elements -/// div {} -/// h1 {"Some text"} -/// h1 {"Some text with {formatting}"} -/// h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"} -/// h2 { -/// "Multiple" -/// "Text" -/// "Blocks" -/// "Use comments as separators in html" -/// } -/// div { -/// h1 {"multiple"} -/// h2 {"nested"} -/// h3 {"elements"} -/// } -/// div { -/// class: "my special div" -/// h1 {"Headers and attributes!"} -/// } -/// div { -/// // pass simple rust expressions in -/// class: lazy_fmt, -/// id: format_args!("attributes can be passed lazily with std::fmt::Arguments"), -/// div { -/// class: { -/// const WORD: &str = "expressions"; -/// format_args!("Arguments can be passed in through curly braces for complex {}", WORD) -/// } -/// } -/// } -/// -/// // Expressions can be used in element position too: -/// {rsx!(p { "More templating!" })} -/// {html!(

"Even HTML templating!!"

)} -/// -/// // Iterators -/// {(0..10).map(|i| rsx!(li { "{i}" }))} -/// {{ -/// let data = std::collections::HashMap::<&'static str, &'static str>::new(); -/// // Iterators *should* have keys when you can provide them. -/// // Keys make your app run faster. Make sure your keys are stable, unique, and predictable. -/// // Using an "ID" associated with your data is a good idea. -/// data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" })) -/// }} -/// -/// // Matching -/// {match true { -/// true => rsx!(h1 {"Top text"}), -/// false => rsx!(h1 {"Bottom text"}) -/// }} -/// -/// // Conditional rendering -/// // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals. -/// // You can convert a bool condition to rsx! with .then and .or -/// {true.then(|| rsx!(div {}))} -/// -/// // True conditions -/// {if true { -/// rsx!(h1 {"Top text"}) -/// } else { -/// rsx!(h1 {"Bottom text"}) -/// }} -/// -/// // returning "None" is a bit noisy... but rare in practice -/// {None as Option<()>} -/// -/// // Use the Dioxus type-alias for less noise -/// {NONE_ELEMENT} -/// -/// // can also just use empty fragments -/// Fragment {} -/// -/// // Fragments let you insert groups of nodes without a parent. -/// // This lets you make components that insert elements as siblings without a container. -/// div {"A"} -/// Fragment { -/// div {"B"} -/// div {"C"} -/// Fragment { -/// "D" -/// Fragment { -/// "heavily nested fragments is an antipattern" -/// "they cause Dioxus to do unnecessary work" -/// "don't use them carelessly if you can help it" -/// } -/// } -/// } -/// -/// // Components -/// // Can accept any paths -/// // Notice how you still get syntax highlighting and IDE support :) -/// Baller {} -/// baller::Baller { } -/// crate::baller::Baller {} -/// -/// // Can take properties -/// Taller { a: "asd" } -/// -/// // Can take optional properties -/// Taller { a: "asd" } -/// -/// // Can pass in props directly as an expression -/// {{ -/// let props = TallerProps {a: "hello"}; -/// rsx!(Taller { ..props }) -/// }} -/// -/// // Spreading can also be overridden manually -/// Taller { -/// ..TallerProps { a: "ballin!" } -/// a: "not ballin!" -/// } -/// -/// // Can take children too! -/// Taller { a: "asd", div {"hello world!"} } -/// } -/// }) -/// }; -/// -/// mod baller { -/// use super::*; -/// pub struct BallerProps {} -/// -/// /// This component totally balls -/// pub fn Baller(cx: Scope) -> DomTree { -/// todo!() -/// } -/// } -/// -/// #[derive(Debug, PartialEq, Props)] -/// pub struct TallerProps { -/// a: &'static str, -/// } -/// -/// /// This component is taller than most :) -/// pub fn Taller(cx: Scope) -> DomTree { -/// let b = true; -/// todo!() -/// } +#[doc = include_str!("../../../examples/rsx_usage.rs")] /// ``` -#[proc_macro_error::proc_macro_error] #[proc_macro] pub fn rsx(s: TokenStream) -> TokenStream { + #[cfg(feature = "hot-reload")] + let rsx_text = s.to_string(); match syn::parse::(s) { Err(err) => err.to_compile_error().into(), - Ok(stream) => stream.to_token_stream().into(), + Ok(body) => { + #[cfg(feature = "hot-reload")] + { + use dioxus_rsx_interpreter::captuered_context::CapturedContextBuilder; + + match CapturedContextBuilder::from_call_body(body) { + Ok(captured) => { + let lazy = quote::quote! { + LazyNodes::new(move |__cx|{ + let code_location = get_line_num!(); + let captured = #captured; + let text = #rsx_text; + + resolve_scope(code_location, text, captured, __cx) + }) + }; + if let Some(cx) = captured.custom_context { + quote::quote! { + #cx.render(#lazy) + } + .into() + } else { + lazy.into() + } + } + Err(err) => err.into_compile_error().into(), + } + } + #[cfg(not(feature = "hot-reload"))] + body.to_token_stream().into() + } } } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 6c65c5944..92772df00 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -653,7 +653,9 @@ Finally, call `.build()` to create the instance of `{name}`. } } - impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{ + impl #impl_generics dioxus::prelude::Properties for #name #ty_generics + #b_generics_where_extras_predicates + { type Builder = #builder_name #generics_with_empty; const IS_STATIC: bool = #is_static; fn builder() -> Self::Builder { diff --git a/packages/core-macro/src/rsx/errors.rs b/packages/core-macro/src/rsx/errors.rs deleted file mode 100644 index 27e2c7f98..000000000 --- a/packages/core-macro/src/rsx/errors.rs +++ /dev/null @@ -1,15 +0,0 @@ -macro_rules! missing_trailing_comma { - ($span:expr) => { - proc_macro_error::emit_error!($span, "missing trailing comma") - }; -} - -macro_rules! attr_after_element { - ($span:expr) => { - proc_macro_error::emit_error!( - $span, - "expected element"; - help = "move the attribute above all the children and text elements" - ) - }; -} diff --git a/packages/core-macro/src/rsx/node.rs b/packages/core-macro/src/rsx/node.rs deleted file mode 100644 index d5d701767..000000000 --- a/packages/core-macro/src/rsx/node.rs +++ /dev/null @@ -1,84 +0,0 @@ -use super::*; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - token, Expr, LitStr, Result, Token, -}; - -/* -Parse --> div {} --> Component {} --> component() --> "text {with_args}" --> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr -*/ -pub enum BodyNode { - Element(Element), - Component(Component), - Text(LitStr), - RawExpr(Expr), -} - -impl Parse for BodyNode { - fn parse(stream: ParseStream) -> Result { - if stream.peek(LitStr) { - return Ok(BodyNode::Text(stream.parse()?)); - } - - // div {} -> el - // Div {} -> comp - if stream.peek(syn::Ident) && stream.peek2(token::Brace) { - if stream - .fork() - .parse::()? - .to_string() - .chars() - .next() - .unwrap() - .is_ascii_uppercase() - { - return Ok(BodyNode::Component(stream.parse()?)); - } else { - return Ok(BodyNode::Element(stream.parse::()?)); - } - } - - // component() -> comp - // ::component {} -> comp - // ::component () -> comp - if (stream.peek(syn::Ident) && stream.peek2(token::Paren)) - || (stream.peek(Token![::])) - || (stream.peek(Token![:]) && stream.peek2(Token![:])) - { - return Ok(BodyNode::Component(stream.parse::()?)); - } - - // crate::component{} -> comp - // crate::component() -> comp - if let Ok(pat) = stream.fork().parse::() { - if pat.segments.len() > 1 { - return Ok(BodyNode::Component(stream.parse::()?)); - } - } - - Ok(BodyNode::RawExpr(stream.parse::()?)) - } -} - -impl ToTokens for BodyNode { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match &self { - BodyNode::Element(el) => el.to_tokens(tokens), - BodyNode::Component(comp) => comp.to_tokens(tokens), - BodyNode::Text(txt) => tokens.append_all(quote! { - __cx.text(format_args_f!(#txt)) - }), - BodyNode::RawExpr(exp) => tokens.append_all(quote! { - __cx.fragment_from_iter(#exp) - }), - } - } -} diff --git a/packages/core-macro/src/rsx/pretty.rs b/packages/core-macro/src/rsx/pretty.rs deleted file mode 100644 index 26ad54006..000000000 --- a/packages/core-macro/src/rsx/pretty.rs +++ /dev/null @@ -1 +0,0 @@ -//! pretty printer for rsx! diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index fc697df12..b3a8007f0 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -48,5 +48,4 @@ backtrace = "0.3" [features] default = [] serialize = ["serde"] -debug_vdom = [] - +debug_vdom = [] \ No newline at end of file diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 3d8b9082d..71c9f1289 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,26 +1,26 @@ #![warn(clippy::pedantic)] #![allow(clippy::cast_possible_truncation)] -//! This module contains the stateful [`DiffState`] and all methods to diff [`VNodes`], their properties, and their children. +//! This module contains the stateful [`DiffState`] and all methods to diff [`VNode`]s, their properties, and their children. //! //! The [`DiffState`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set -//! of mutations for the [`RealDom`] to apply. +//! of mutations for the renderer to apply. //! //! ## Notice: //! //! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support -//! Components, Fragments, Suspense, [`SubTree`] memoization, incremental diffing, cancellation, [`NodeRefs`], pausing, priority +//! Components, Fragments, Suspense, `SubTree` memoization, incremental diffing, cancellation, pausing, priority //! scheduling, and additional batching operations. //! //! ## Implementation Details: //! //! ### IDs for elements //! -------------------- -//! All nodes are addressed by their IDs. The [`RealDom`] provides an imperative interface for making changes to these nodes. +//! All nodes are addressed by their IDs. //! We don't necessarily require that DOM changes happen instantly during the diffing process, so the implementor may choose //! to batch nodes if it is more performant for their application. The element IDs are indices into the internal element //! array. The expectation is that implementors will use the ID as an index into a Vec of real nodes, allowing for passive -//! garbage collection as the [`VirtualDOM`] replaces old nodes. +//! garbage collection as the [`crate::VirtualDom`] replaces old nodes. //! //! When new vnodes are created through `cx.render`, they won't know which real node they correspond to. During diffing, //! we always make sure to copy over the ID. If we don't do this properly, the [`ElementId`] will be populated incorrectly @@ -30,7 +30,7 @@ //! -------------------- //! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments //! can be particularly challenging when they are empty, so the anchor node lets us "reserve" a spot for the empty -//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the [`NodeFactory`] - it is +//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the [`crate::innerlude::NodeFactory`] - it is //! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. Adding //! "dummy" nodes _is_ inefficient, but it makes our diffing algorithm faster and the implementation is completely up to //! the platform. @@ -44,13 +44,13 @@ //! into a promise-like value. React will then work on the next "ready" fiber, checking back on the previous fiber once //! it has finished its new work. In Dioxus, we use a similar approach, but try to completely render the tree before //! switching sub-fibers. Instead, each future is submitted into a futures-queue and the node is manually loaded later on. -//! Due to the frequent calls to [`yield_now`] we can get the pure "fetch-as-you-render" behavior of React Fiber. +//! Due to the frequent calls to [`crate::virtual_dom::VirtualDom::work_with_deadline`] we can get the pure "fetch-as-you-render" behavior of React Fiber. //! //! We're able to use this approach because we use placeholder nodes - futures that aren't ready still get submitted to //! DOM, but as a placeholder. //! //! Right now, the "suspense" queue is intertwined with hooks. In the future, we should allow any future to drive attributes -//! and contents, without the need for the [`use_suspense`] hook. In the interim, this is the quickest way to get Suspense working. +//! and contents, without the need for a `use_suspense` hook. In the interim, this is the quickest way to get Suspense working. //! //! ## Subtree Memoization //! ----------------------- diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 22889a96b..92c86dec8 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -63,6 +63,9 @@ pub struct UserEvent { /// The event type IE "onclick" or "onmouseover" pub name: &'static str, + /// If the event is bubbles up through the vdom + pub bubbles: bool, + /// The event data to be passed onto the event handler pub data: Arc, } @@ -150,7 +153,7 @@ impl AnyEvent { /// You should prefer to use the name of the event directly, rather than /// the UiEvent generic type. /// -/// For the HTML crate, this would include [`MouseEvent`], [`FormEvent`] etc. +/// For the HTML crate, this would include MouseEvent, FormEvent etc. pub struct UiEvent { /// The internal data of the event /// This is wrapped in an Arc so that it can be sent across threads diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 66c25f1db..1b19aa878 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -550,7 +550,7 @@ impl<'a> NodeFactory<'a> { })) } - /// Create a new [`VNode::VElement`] + /// Create a new [`VNode::Element`] pub fn element( &self, el: impl DioxusElement, @@ -569,7 +569,7 @@ impl<'a> NodeFactory<'a> { ) } - /// Create a new [`VNode::VElement`] without the trait bound + /// Create a new [`VNode::Element`] without the trait bound /// /// IE pass in "div" instead of `div` pub fn raw_element( @@ -637,7 +637,7 @@ impl<'a> NodeFactory<'a> { } } - /// Create a new [`VNode::VComponent`] + /// Create a new [`VNode::Component`] pub fn component

( &self, component: fn(Scope<'a, P>) -> Element, @@ -684,7 +684,7 @@ impl<'a> NodeFactory<'a> { } } - /// Create a new [`VNode::VFragment`] from a root of the rsx! call + /// Create a new [`VNode::Fragment`] from a root of the rsx! call pub fn fragment_root<'b, 'c>( self, node_iter: impl IntoIterator + 'c> + 'b, @@ -705,7 +705,7 @@ impl<'a> NodeFactory<'a> { } } - /// Create a new [`VNode::VFragment`] from any iterator + /// Create a new [`VNode::Fragment`] from any iterator pub fn fragment_from_iter<'b, 'c>( self, node_iter: impl IntoIterator + 'c> + 'b, diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index e81fc52a2..e138acaa6 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -33,7 +33,7 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> { /// fn App(cx: Scope) -> Element { /// cx.render(rsx!{ /// CustomCard { -/// h1 {}2 +/// h1 {} /// p {} /// } /// }) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 010c63b03..ba6afcfe2 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -335,7 +335,7 @@ impl ScopeArena { log::trace!("calling listener {:?}", listener.event); if state.canceled.get() { // stop bubbling if canceled - break; + return; } let mut cb = listener.callback.borrow_mut(); @@ -349,6 +349,10 @@ impl ScopeArena { data: event.data.clone(), }); } + + if !event.bubbles { + return; + } } } @@ -609,7 +613,7 @@ impl ScopeState { /// Create a subscription that schedules a future render for the reference component /// - /// ## Notice: you should prefer using prepare_update and get_scope_id + /// ## Notice: you should prefer using schedule_update_any and scope_id pub fn schedule_update(&self) -> Arc { let (chan, id) = (self.tasks.sender.clone(), self.scope_id()); Arc::new(move || { @@ -1000,12 +1004,11 @@ impl TaskQueue { fn remove(&self, id: TaskId) { if let Ok(mut tasks) = self.tasks.try_borrow_mut() { let _ = tasks.remove(&id); + if let Some(task_map) = self.task_map.borrow_mut().get_mut(&id.scope) { + task_map.remove(&id); + } } - // the task map is still around, but it'll be removed when the scope is unmounted - if let Some(task_map) = self.task_map.borrow_mut().get_mut(&id.scope) { - task_map.remove(&id); - } } pub(crate) fn has_tasks(&self) -> bool { diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index a6a524e27..5d311f177 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -1,7 +1,7 @@ use crate::innerlude::*; /// An iterator that only yields "real" [`Element`]s. IE only Elements that are -/// not [`VNode::VComponent`] or [`VNode::VFragment`], . +/// not [`VNode::Component`] or [`VNode::Fragment`], . pub struct ElementIdIterator<'a> { vdom: &'a VirtualDom, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 8d3fd1789..37c30742a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -125,6 +125,9 @@ pub enum SchedulerMsg { /// Immediate updates from Components that mark them as dirty Immediate(ScopeId), + /// Mark all components as dirty and update them + DirtyAll, + /// New tasks from components that should be polled when the next poll is ready NewTask(ScopeId), } @@ -337,29 +340,12 @@ impl VirtualDom { let scopes = &mut self.scopes; let task_poll = poll_fn(|cx| { - // - let mut any_pending = false; - let mut tasks = scopes.tasks.tasks.borrow_mut(); - let mut to_remove = vec![]; + tasks.retain(|_, task| task.as_mut().poll(cx).is_pending()); - // this would be better served by retain - for (id, task) in tasks.iter_mut() { - if task.as_mut().poll(cx).is_ready() { - to_remove.push(*id); - } else { - any_pending = true; - } - } - - for id in to_remove { - tasks.remove(&id); - } - - // Resolve the future if any singular task is ready - match any_pending { - true => Poll::Pending, - false => Poll::Ready(()), + match tasks.is_empty() { + true => Poll::Ready(()), + false => Poll::Pending, } }); @@ -407,6 +393,11 @@ impl VirtualDom { SchedulerMsg::Immediate(s) => { self.dirty_scopes.insert(s); } + SchedulerMsg::DirtyAll => { + for id in self.scopes.scopes.borrow().keys() { + self.dirty_scopes.insert(*id); + } + } } } @@ -583,7 +574,7 @@ impl VirtualDom { /// /// *value.borrow_mut() = "goodbye"; /// - /// let edits = dom.diff(); + /// let edits = dom.hard_diff(ScopeId(0)); /// ``` pub fn hard_diff(&mut self, scope_id: ScopeId) -> Mutations { let mut diff_machine = DiffState::new(&self.scopes); diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index e61120d64..c5beefbdc 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" } dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1" } +dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true } serde = "1.0.136" serde_json = "1.0.79" @@ -32,6 +33,8 @@ webbrowser = "0.7.1" mime_guess = "2.0.3" dunce = "1.0.2" +interprocess = { version = "1.1.1", optional = true } + [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9.3" @@ -41,7 +44,7 @@ tokio_runtime = ["tokio"] fullscreen = ["wry/fullscreen"] transparent = ["wry/transparent"] tray = ["wry/tray"] - +hot-reload = ["dioxus-rsx-interpreter", "interprocess"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 2fa1355d2..6c0c7feef 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -1,4 +1,5 @@ use crate::desktop_context::{DesktopContext, UserWindowEvent}; + use dioxus_core::*; use std::{ collections::HashMap, @@ -49,6 +50,93 @@ impl DesktopController { dom.base_scope().provide_context(window_context); + // allow other proccesses to send the new rsx text to the @dioxusin ipc channel and recieve erros on the @dioxusout channel + #[cfg(feature = "hot-reload")] + { + use dioxus_rsx_interpreter::{ + error::Error, ErrorHandler, SetManyRsxMessage, RSX_CONTEXT, + }; + use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; + use std::io::{BufRead, BufReader, Write}; + use std::time::Duration; + + fn handle_error( + connection: std::io::Result, + ) -> Option { + connection + .map_err(|error| eprintln!("Incoming connection failed: {}", error)) + .ok() + } + + let latest_in_connection: Arc>>> = + Arc::new(Mutex::new(None)); + let latest_in_connection_handle = latest_in_connection.clone(); + let latest_out_connection: Arc>>> = + Arc::new(Mutex::new(None)); + let latest_out_connection_handle = latest_out_connection.clone(); + + struct DesktopErrorHandler { + latest_connection: Arc>>>, + } + impl ErrorHandler for DesktopErrorHandler { + fn handle_error(&self, err: Error) { + if let Some(conn) = &mut *self.latest_connection.lock().unwrap() { + conn.get_mut() + .write_all( + (serde_json::to_string(&err).unwrap() + "\n").as_bytes(), + ) + .unwrap(); + } + } + } + + RSX_CONTEXT.set_error_handler(DesktopErrorHandler { + latest_connection: latest_out_connection_handle, + }); + RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel()); + + // connect to processes for incoming data + std::thread::spawn(move || { + if let Ok(listener) = LocalSocketListener::bind("@dioxusin") { + for conn in listener.incoming().filter_map(handle_error) { + *latest_in_connection_handle.lock().unwrap() = + Some(BufReader::new(conn)); + } + } + }); + + // connect to processes for outgoing errors + std::thread::spawn(move || { + if let Ok(listener) = LocalSocketListener::bind("@dioxusout") { + for conn in listener.incoming().filter_map(handle_error) { + *latest_out_connection.lock().unwrap() = Some(BufReader::new(conn)); + } + } + }); + + std::thread::spawn(move || { + loop { + if let Some(conn) = &mut *latest_in_connection.lock().unwrap() { + let mut buf = String::new(); + match conn.read_line(&mut buf) { + Ok(_) => { + let msgs: SetManyRsxMessage = + serde_json::from_str(&buf).unwrap(); + RSX_CONTEXT.extend(msgs); + } + Err(err) => { + if err.kind() != std::io::ErrorKind::WouldBlock { + break; + } + } + } + } + // give the error handler time to take the mutex + std::thread::sleep(Duration::from_millis(100)); + } + }); + } + let edits = dom.rebuild(); edit_queue diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 3d4bf0b26..d6d436a09 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -212,7 +212,12 @@ pub(super) fn handler( log::warn!("Open print modal failed: {e}"); } } - DevTool => webview.open_devtools(), + DevTool => { + #[cfg(debug_assertions)] + webview.open_devtools(); + #[cfg(not(debug_assertions))] + log::warn!("Devtools are disabled in release builds"); + } Eval(code) => { if let Err(e) = webview.evaluate_script(code.as_str()) { diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index fd0675f10..803767a09 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -4,6 +4,7 @@ use std::any::Any; use std::sync::Arc; use dioxus_core::{ElementId, EventPriority, UserEvent}; +use dioxus_html::event_bubbles; use dioxus_html::on::*; #[derive(serde::Serialize, serde::Deserialize)] @@ -47,8 +48,8 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { } = serde_json::from_value(val).unwrap(); let mounted_dom_id = Some(ElementId(mounted_dom_id as usize)); - let name = event_name_from_type(&event); + let event = make_synthetic_event(&event, contents); UserEvent { @@ -56,6 +57,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { priority: EventPriority::Low, scope_id: None, element: mounted_dom_id, + bubbles: event_bubbles(name), data: event, } } diff --git a/packages/fermi/README.md b/packages/fermi/README.md index 0923aa6f6..ae89ea73c 100644 --- a/packages/fermi/README.md +++ b/packages/fermi/README.md @@ -69,7 +69,7 @@ It's that simple! Fermi is currently under construction, so you have to use the `master` branch to get started. ```rust -[depdencies] +[dependencies] fermi = { git = "https://github.com/dioxuslabs/fermi" } ``` diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs index c7c234ddb..f77930c75 100644 --- a/packages/fermi/src/root.rs +++ b/packages/fermi/src/root.rs @@ -74,6 +74,13 @@ impl AtomRoot { } } else { log::trace!("no atoms found for {:?}", ptr); + atoms.insert( + ptr, + Slot { + value: Rc::new(value), + subscribers: HashSet::new(), + }, + ); } } diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 114317428..3bd84ac0e 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -1443,3 +1443,91 @@ pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) { _ => (true, Low), } } + +pub fn event_bubbles(evt: &str) -> bool { + match evt { + "copy" => true, + "cut" => true, + "paste" => true, + "compositionend" => true, + "compositionstart" => true, + "compositionupdate" => true, + "keydown" => true, + "keypress" => true, + "keyup" => true, + "focus" => false, + "focusout" => true, + "focusin" => true, + "blur" => false, + "change" => true, + "input" => true, + "invalid" => true, + "reset" => true, + "submit" => true, + "click" => true, + "contextmenu" => true, + "doubleclick" => true, + "dblclick" => true, + "drag" => true, + "dragend" => true, + "dragenter" => false, + "dragexit" => false, + "dragleave" => true, + "dragover" => true, + "dragstart" => true, + "drop" => true, + "mousedown" => true, + "mouseenter" => false, + "mouseleave" => false, + "mousemove" => true, + "mouseout" => true, + "scroll" => false, + "mouseover" => true, + "mouseup" => true, + "pointerdown" => true, + "pointermove" => true, + "pointerup" => true, + "pointercancel" => true, + "gotpointercapture" => true, + "lostpointercapture" => true, + "pointerenter" => false, + "pointerleave" => false, + "pointerover" => true, + "pointerout" => true, + "select" => true, + "touchcancel" => true, + "touchend" => true, + "touchmove" => true, + "touchstart" => true, + "wheel" => true, + "abort" => false, + "canplay" => true, + "canplaythrough" => true, + "durationchange" => true, + "emptied" => true, + "encrypted" => true, + "ended" => true, + "error" => false, + "loadeddata" => true, + "loadedmetadata" => true, + "loadstart" => false, + "pause" => true, + "play" => true, + "playing" => true, + "progress" => false, + "ratechange" => true, + "seeked" => true, + "seeking" => true, + "stalled" => true, + "suspend" => true, + "timeupdate" => true, + "volumechange" => true, + "waiting" => true, + "animationstart" => true, + "animationend" => true, + "animationiteration" => true, + "transitionend" => true, + "toggle" => true, + _ => panic!("unsupported event type {:?}", evt), + } +} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index e7bdcf5d7..197eed737 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -110,7 +110,7 @@ impl From<&MouseEvent> for MouseData { ElementPoint::new(e.offset_x().into(), e.offset_y().into()), PagePoint::new(e.page_x().into(), e.page_y().into()), ), - Some(MouseButton::from_web_code(e.button().into())), + Some(MouseButton::from_web_code(e.button())), decode_mouse_button_set(e.buttons()), modifiers, ) diff --git a/packages/interpreter/src/bindings.rs b/packages/interpreter/src/bindings.rs index a08ef9c13..80df47c2c 100644 --- a/packages/interpreter/src/bindings.rs +++ b/packages/interpreter/src/bindings.rs @@ -48,10 +48,16 @@ extern "C" { pub fn CreatePlaceholder(this: &Interpreter, root: u64); #[wasm_bindgen(method)] - pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function); + pub fn NewEventListener( + this: &Interpreter, + name: &str, + root: u64, + handler: &Function, + bubbles: bool, + ); #[wasm_bindgen(method)] - pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); + pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str, bubbles: bool); #[wasm_bindgen(method)] pub fn SetText(this: &Interpreter, root: u64, text: JsValue); diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index f1060266c..ba38f06e1 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -5,11 +5,64 @@ export function main() { window.ipc.postMessage(serializeIpcMessage("initialize")); } } + +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; + } + + createBubbling(event_name, handler) { + 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++; + } + } + + createNonBubbling(event_name, element, handler) { + 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); + } + + removeBubbling(event_name) { + 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]; + } + } + + removeNonBubbling(element, event_name) { + 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); + } + + removeAllNonBubbling(element) { + const id = element.getAttribute("data-dioxus-id"); + delete this.local[id]; + } +} + export class Interpreter { constructor(root) { this.root = root; this.stack = [root]; - this.listeners = {}; + this.listeners = new ListenerMap(root); this.handlers = {}; this.lastNodeWasText = false; this.nodes = [root]; @@ -40,6 +93,7 @@ export class Interpreter { ReplaceWith(root_id, m) { let root = this.nodes[root_id]; let els = this.stack.splice(this.stack.length - m); + this.listeners.removeAllNonBubbling(root); root.replaceWith(...els); } InsertAfter(root, n) { @@ -54,6 +108,7 @@ export class Interpreter { } Remove(root) { let node = this.nodes[root]; + this.listeners.removeAllNonBubbling(node); if (node !== undefined) { node.remove(); } @@ -79,25 +134,24 @@ export class Interpreter { this.stack.push(el); this.nodes[root] = el; } - NewEventListener(event_name, root, handler) { + NewEventListener(event_name, root, handler, bubbles) { const element = this.nodes[root]; element.setAttribute("data-dioxus-id", `${root}`); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 1; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } else { - this.listeners[event_name]++; + if (bubbles) { + this.listeners.createBubbling(event_name, handler); + } + else { + this.listeners.createNonBubbling(event_name, element, handler); } } - RemoveEventListener(root, event_name) { + RemoveEventListener(root, event_name, bubbles) { const element = this.nodes[root]; element.removeAttribute(`data-dioxus-id`); - this.listeners[event_name]--; - if (this.listeners[event_name] === 0) { - this.root.removeEventListener(event_name, this.handlers[event_name]); - delete this.listeners[event_name]; - delete this.handlers[event_name]; + if (bubbles) { + this.listeners.removeBubbling(event_name) + } + else { + this.listeners.removeNonBubbling(element, event_name); } } SetText(root, text) { @@ -140,7 +194,9 @@ export class Interpreter { RemoveAttribute(root, field, ns) { const name = field; const node = this.nodes[root]; - if (ns !== null || ns !== undefined) { + if (ns == "style") { + node.style.removeProperty(name); + } else if (ns !== null || ns !== undefined) { node.removeAttributeNS(ns, name); } else if (name === "value") { node.value = ""; @@ -196,12 +252,9 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": - console.log(this.listeners); - // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event) => { - console.log(event); let target = event.target; if (target != null) { @@ -290,7 +343,8 @@ export class Interpreter { ); } }; - this.NewEventListener(edit.event_name, edit.root, handler); + this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name)); + break; case "SetText": this.SetText(edit.root, edit.text); @@ -606,3 +660,172 @@ const bool_attrs = { selected: true, truespeed: true, }; + +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 true; + case "canplaythrough": + return true; + case "durationchange": + return true; + case "emptied": + return true; + case "encrypted": + return true; + case "ended": + return true; + case "error": + return false; + case "loadeddata": + return true; + case "loadedmetadata": + return true; + case "loadstart": + return false; + case "pause": + return true; + case "play": + return true; + case "playing": + return true; + case "progress": + return false; + case "ratechange": + return true; + case "seeked": + return true; + case "seeking": + return true; + case "stalled": + return true; + case "suspend": + return true; + case "timeupdate": + return true; + case "volumechange": + return true; + case "waiting": + return true; + case "animationstart": + return true; + case "animationend": + return true; + case "animationiteration": + return true; + case "transitionend": + return true; + case "toggle": + return true; + } +} \ No newline at end of file diff --git a/packages/liveview/examples/axum.rs b/packages/liveview/examples/axum.rs index 3bcdee8c2..93bf9cae1 100644 --- a/packages/liveview/examples/axum.rs +++ b/packages/liveview/examples/axum.rs @@ -1,36 +1,31 @@ -use axum::{ - extract::ws::WebSocketUpgrade, response::Html, response::IntoResponse, routing::get, Extension, - Router, -}; +#![cfg(feature = "axum")] + +use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; use dioxus_core::{Element, LazyNodes, Scope}; -use dioxus_liveview::Liveview; #[tokio::main] async fn main() { - #[cfg(feature = "axum")] - { - pretty_env_logger::init(); + pretty_env_logger::init(); - 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 body = view.body("Dioxus Liveview"); + let view = dioxus_liveview::new(addr); + let body = view.body("Dioxus Liveview"); - let app = Router::new() - .route("/", get(move || async { Html(body) })) - .route( - "/app", - get(move |ws: WebSocketUpgrade| async move { - ws.on_upgrade(move |socket| async move { - view.upgrade(socket, app).await; - }) - }), - ); - axum::Server::bind(&addr.to_string().parse().unwrap()) - .serve(app.into_make_service()) - .await - .unwrap(); - } + let app = Router::new() + .route("/", get(move || async { Html(body) })) + .route( + "/app", + get(move |ws: WebSocketUpgrade| async move { + ws.on_upgrade(move |socket| async move { + view.upgrade_axum(socket, app).await; + }) + }), + ); + axum::Server::bind(&addr.to_string().parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); } fn app(cx: Scope) -> Element { diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index 2c14d318b..73c220881 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -1,3 +1,5 @@ +#![cfg(feature = "warp")] + use dioxus_core::{Element, LazyNodes, Scope}; use dioxus_liveview as liveview; use warp::ws::Ws; @@ -5,28 +7,25 @@ use warp::Filter; #[tokio::main] async fn main() { - #[cfg(feature = "warp")] - { - pretty_env_logger::init(); + pretty_env_logger::init(); - let addr = ([127, 0, 0, 1], 3030); + let addr = ([127, 0, 0, 1], 3030); - // todo: compactify this routing under one liveview::app method - let view = liveview::new(addr); - let body = view.body("Dioxus LiveView"); + // todo: compactify this routing under one liveview::app method + let view = liveview::new(addr); + let body = view.body("Dioxus LiveView"); - let routes = warp::path::end() - .map(move || warp::reply::html(body.clone())) - .or(warp::path("app") - .and(warp::ws()) - .and(warp::any().map(move || view.clone())) - .map(|ws: Ws, view: liveview::Liveview| { - ws.on_upgrade(|socket| async move { - view.upgrade(socket, app).await; - }) - })); - warp::serve(routes).run(addr).await; - } + let routes = warp::path::end() + .map(move || warp::reply::html(body.clone())) + .or(warp::path("app") + .and(warp::ws()) + .and(warp::any().map(move || view.clone())) + .map(|ws: Ws, view: liveview::Liveview| { + ws.on_upgrade(|socket| async move { + view.upgrade_warp(socket, app).await; + }) + })); + warp::serve(routes).run(addr).await; } fn app(cx: Scope) -> Element { diff --git a/packages/liveview/src/adapters/actix_adapter.rs b/packages/liveview/src/adapters/actix_adapter.rs deleted file mode 100644 index 8b1378917..000000000 --- a/packages/liveview/src/adapters/actix_adapter.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index 4122950bc..c60b267ea 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -1,4 +1,4 @@ -use crate::{events, Liveview}; +use crate::events; use axum::extract::ws::{Message, WebSocket}; use dioxus_core::prelude::*; use futures_util::{ @@ -10,11 +10,16 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_util::task::LocalPoolHandle; impl crate::Liveview { - pub async fn upgrade(&self, ws: WebSocket, app: fn(Scope) -> Element) { + pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) { connect(ws, self.pool.clone(), app, ()).await; } - pub async fn upgrade_with_props(&self, ws: WebSocket, app: fn(Scope) -> Element, props: T) - where + + pub async fn upgrade_axum_with_props( + &self, + ws: WebSocket, + app: fn(Scope) -> Element, + props: T, + ) where T: Send + Sync + 'static, { connect(ws, self.pool.clone(), app, props).await; diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index f3cf3914b..398c7cdd9 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -7,10 +7,10 @@ use tokio_util::task::LocalPoolHandle; use warp::ws::{Message, WebSocket}; impl crate::Liveview { - pub async fn upgrade(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) { + pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) { connect(ws, self.pool.clone(), app, ()).await; } - pub async fn upgrade_with_props( + pub async fn upgrade_warp_with_props( &self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element, diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs index 10d1ffd38..2c10925b5 100644 --- a/packages/liveview/src/events.rs +++ b/packages/liveview/src/events.rs @@ -6,6 +6,7 @@ use std::any::Any; use std::sync::Arc; use dioxus_core::{ElementId, EventPriority, UserEvent}; +use dioxus_html::event_bubbles; use dioxus_html::on::*; #[derive(serde::Serialize, serde::Deserialize)] @@ -46,6 +47,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent { scope_id: None, element: mounted_dom_id, data: event, + bubbles: event_bubbles(name), } } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index f3ad90511..2f1cd3b9b 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -7,21 +7,10 @@ pub mod adapters { #[cfg(feature = "axum")] pub mod axum_adapter; - - #[cfg(feature = "actix")] - pub mod actix_adapter; } use std::net::SocketAddr; -#[cfg(feature = "warp")] -pub use adapters::warp_adapter::connect; - -#[cfg(feature = "axum")] -pub use adapters::axum_adapter::connect; - -#[cfg(feature = "actix")] -pub use adapters::actix_adapter::connect; use tokio_util::task::LocalPoolHandle; #[derive(Clone)] diff --git a/packages/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index 508754bf7..58fa3e3a9 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/packages/native-core-macro/src/lib.rs @@ -211,7 +211,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream { ty: dioxus_native_core::state::MemberId, node: &'a dioxus_core::VNode<'a>, vdom: &'a dioxus_core::VirtualDom, - children: &Vec<&Self>, + children: &[&Self], ctx: &anymap::AnyMap, ) -> Option{ use dioxus_native_core::state::ChildDepState as _; @@ -225,7 +225,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream { ty - #sum_idents, node, vdom, - &children.iter().map(|p| &p.#child_state_idents).collect(), + &children.iter().map(|p| &p.#child_state_idents).collect::>(), ctx, ).map(|mut changed|{ for id in &mut changed.node_dep{ diff --git a/packages/native-core-macro/tests/peristant_iterator.rs b/packages/native-core-macro/tests/peristant_iterator.rs new file mode 100644 index 000000000..29df3afe5 --- /dev/null +++ b/packages/native-core-macro/tests/peristant_iterator.rs @@ -0,0 +1,342 @@ +use dioxus_native_core::{ + real_dom::{NodeType, RealDom}, + state::State, + utils::PersistantElementIter, +}; +use dioxus_native_core_macro::State; + +#[derive(State, Default, Clone)] +struct Empty {} + +#[test] +#[allow(unused_variables)] +fn traverse() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let mutations = vdom.create_vnodes(rsx! { + div{ + div{ + "hello" + p{ + "world" + } + "hello world" + } + } + }); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![mutations]); + + let mut iter = PersistantElementIter::new(); + let div_tag = "div".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + let text1 = "hello".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text1, .. } + )); + let p_tag = "p".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text2 = "world".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text2, .. } + )); + let text3 = "hello world".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text2, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: p_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text1, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_removes() { + use dioxus_core::VNode; + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + // this will end on the node that is removed + let mut iter1 = PersistantElementIter::new(); + // this will end on the after node that is removed + let mut iter2 = PersistantElementIter::new(); + // div + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // "hello" + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // "world" + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter2.next(&rdom).id(); + // "hello world" + iter2.next(&rdom).id(); + + iter1.prune(&update, &rdom); + iter2.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "p".to_string(); + let idx = iter1.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text = "hello world".to_string(); + let idx = iter1.next(&rdom).id(); + assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. })); + let div_tag = "div".to_string(); + let idx = iter2.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: div_tag, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_instertions_before() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "3", + "hello world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + let mut iter = PersistantElementIter::new(); + // div + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello" + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello world" + iter.next(&rdom).id(); + + iter.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "div".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_instertions_after() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + let mut iter = PersistantElementIter::new(); + // div + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello" + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "world" + iter.next(&rdom).id(); + + iter.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "p".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text = "hello world".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. })); +} diff --git a/packages/native-core-macro/tests/update_state.rs b/packages/native-core-macro/tests/update_state.rs index 5cd23732e..84ec5d530 100644 --- a/packages/native-core-macro/tests/update_state.rs +++ b/packages/native-core-macro/tests/update_state.rs @@ -92,6 +92,7 @@ impl NodeDepState for NodeDepCallCounter { } } +#[allow(clippy::vec_box)] #[derive(Debug, Clone, PartialEq, Default)] struct BubbledUpStateTester(Option, Vec>); impl ChildDepState for BubbledUpStateTester { diff --git a/packages/native-core/Cargo.toml b/packages/native-core/Cargo.toml index 284a9394a..1c54e0711 100644 --- a/packages/native-core/Cargo.toml +++ b/packages/native-core/Cargo.toml @@ -15,7 +15,7 @@ dioxus-core = { path = "../core", version = "^0.2.1" } dioxus-html = { path = "../html", version = "^0.2.1" } dioxus-core-macro = { path = "../core-macro", version = "^0.2.1" } -stretch2 = "0.4.2" +taffy = "0.1.0" smallvec = "1.6" fxhash = "0.2" anymap = "0.12.1" diff --git a/packages/native-core/src/layout_attributes.rs b/packages/native-core/src/layout_attributes.rs index 45461a3f9..9ae9e3c83 100644 --- a/packages/native-core/src/layout_attributes.rs +++ b/packages/native-core/src/layout_attributes.rs @@ -1,6 +1,6 @@ /* - [ ] pub display: Display, -- [x] pub position_type: PositionType, --> kinda, stretch doesnt support everything +- [x] pub position_type: PositionType, --> kinda, taffy doesnt support everything - [ ] pub direction: Direction, - [x] pub flex_direction: FlexDirection, @@ -9,7 +9,7 @@ - [x] pub flex_shrink: f32, - [x] pub flex_basis: Dimension, -- [x] pub overflow: Overflow, ---> kinda implemented... stretch doesnt have support for directional overflow +- [x] pub overflow: Overflow, ---> kinda implemented... taffy doesnt have support for directional overflow - [x] pub align_items: AlignItems, - [x] pub align_self: AlignSelf, @@ -29,7 +29,10 @@ - [ ] pub aspect_ratio: Number, */ -use stretch2::{prelude::*, style::PositionType}; +use taffy::{ + prelude::*, + style::{FlexDirection, PositionType}, +}; /// applies the entire html namespace defined in dioxus-html pub fn apply_layout_attributes(name: &str, value: &str, style: &mut Style) { @@ -109,13 +112,7 @@ pub fn apply_layout_attributes(name: &str, value: &str, style: &mut Style) { "counter-reset" => {} "cursor" => {} - "direction" => { - match value { - "ltr" => style.direction = Direction::LTR, - "rtl" => style.direction = Direction::RTL, - _ => {} - } - } + "direction" => {} "display" => apply_display(name, value, style), @@ -283,20 +280,8 @@ pub fn parse_value(value: &str) -> Option { } } -fn apply_overflow(name: &str, value: &str, style: &mut Style) { - match name { - // todo: add more overflow support to stretch2 - "overflow" | "overflow-x" | "overflow-y" => { - style.overflow = match value { - "auto" => Overflow::Visible, - "hidden" => Overflow::Hidden, - "scroll" => Overflow::Scroll, - "visible" => Overflow::Visible, - _ => Overflow::Visible, - }; - } - _ => {} - } +fn apply_overflow(_name: &str, _value: &str, _style: &mut Style) { + // todo: add overflow support to taffy } fn apply_display(_name: &str, value: &str, style: &mut Style) { @@ -307,7 +292,7 @@ fn apply_display(_name: &str, value: &str, style: &mut Style) { } // TODO: there are way more variants - // stretch needs to be updated to handle them + // taffy needs to be updated to handle them // // "block" => Display::Block, // "inline" => Display::Inline, diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index a6e011fa9..2319ca4ac 100644 --- a/packages/native-core/src/lib.rs +++ b/packages/native-core/src/lib.rs @@ -2,3 +2,4 @@ pub mod layout_attributes; pub mod node_ref; pub mod real_dom; pub mod state; +pub mod utils; diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 4d6964572..ca8da730a 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -15,7 +15,7 @@ use crate::{ /// A Dom that can sync with the VirtualDom mutations intended for use in lazy renderers. /// The render state passes from parent to children and or accumulates state from children to parents. -/// To get started implement [PushedDownState] and or [BubbledUpState] and call [RealDom::apply_mutations] to update the dom and [RealDom::update_state] to update the state of the nodes. +/// To get started implement [crate::state::ParentDepState], [crate::state::NodeDepState], or [crate::state::ChildDepState] and call [RealDom::apply_mutations] to update the dom and [RealDom::update_state] to update the state of the nodes. #[derive(Debug)] pub struct RealDom { root: usize, @@ -712,7 +712,7 @@ impl IndexMut for RealDom { } } -/// The node is stored client side and stores only basic data about the node. For more complete information about the node see [`domNode::element`]. +/// The node is stored client side and stores only basic data about the node. #[derive(Debug, Clone)] pub struct Node { /// The id of the node this node was created from. diff --git a/packages/native-core/src/state.rs b/packages/native-core/src/state.rs index b55f217d0..89c47e6e9 100644 --- a/packages/native-core/src/state.rs +++ b/packages/native-core/src/state.rs @@ -44,10 +44,10 @@ pub(crate) fn union_ordered_iter( } /// This state is derived from children. For example a node's size could be derived from the size of children. -/// Called when the current node's node properties are modified, a child's [BubbledUpState] is modified or a child is removed. +/// Called when the current node's node properties are modified, a child's [ChildDepState] is modified or a child is removed. /// Called at most once per update. pub trait ChildDepState { - /// The context is passed to the [PushedDownState::reduce] when it is pushed down. + /// The context is passed to the [ChildDepState::reduce] when it is pushed down. /// This is sometimes nessisary for lifetime purposes. type Ctx; /// This must be either a [ChildDepState] or [NodeDepState] @@ -64,10 +64,10 @@ pub trait ChildDepState { } /// This state that is passed down to children. For example text properties (`` `` ``) would be passed to children. -/// Called when the current node's node properties are modified or a parrent's [PushedDownState] is modified. +/// Called when the current node's node properties are modified or a parrent's [ParentDepState] is modified. /// Called at most once per update. pub trait ParentDepState { - /// The context is passed to the [PushedDownState::reduce] when it is pushed down. + /// The context is passed to the [ParentDepState::reduce] when it is pushed down. /// This is sometimes nessisary for lifetime purposes. type Ctx; /// This must be either a [ParentDepState] or [NodeDepState] @@ -77,7 +77,7 @@ pub trait ParentDepState { } /// This state that is upadated lazily. For example any propertys that do not effect other parts of the dom like bg-color. -/// Called when the current node's node properties are modified or a parrent's [PushedDownState] is modified. +/// Called when the current node's node properties are modified or a sibling's [NodeDepState] is modified. /// Called at most once per update. pub trait NodeDepState { type Ctx; @@ -132,7 +132,7 @@ pub trait State: Default + Clone { ty: MemberId, node: &'a VNode<'a>, vdom: &'a dioxus_core::VirtualDom, - children: &Vec<&Self>, + children: &[&Self], ctx: &AnyMap, ) -> Option; /// This must be a valid resolution order. (no nodes updated before a state they rely on) diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils.rs new file mode 100644 index 000000000..143149521 --- /dev/null +++ b/packages/native-core/src/utils.rs @@ -0,0 +1,230 @@ +use crate::{ + real_dom::{NodeType, RealDom}, + state::State, +}; +use dioxus_core::{DomEdit, ElementId, Mutations}; + +pub enum ElementProduced { + Progressed(ElementId), + Looped(ElementId), +} +impl ElementProduced { + pub fn id(&self) -> ElementId { + match self { + ElementProduced::Progressed(id) => *id, + ElementProduced::Looped(id) => *id, + } + } +} + +#[derive(Debug)] +enum NodePosition { + AtNode, + InChild(usize), +} + +impl NodePosition { + fn map(&self, mut f: impl FnMut(usize) -> usize) -> Self { + match self { + Self::AtNode => Self::AtNode, + Self::InChild(i) => Self::InChild(f(*i)), + } + } + + fn get_or_insert(&mut self, child_idx: usize) -> usize { + match self { + Self::AtNode => { + *self = Self::InChild(child_idx); + child_idx + } + Self::InChild(i) => *i, + } + } +} + +/// The focus system needs a iterator that can persist through changes in the [VirtualDom]. +/// Iterate through it with [ElementIter::next] [ElementIter::prev], and update it with [ElementIter::update] (with data from [`VirtualDom::work_with_deadline`]). +/// The iterator loops around when it reaches the end or the beginning. +pub struct PersistantElementIter { + // stack of elements and fragments + stack: smallvec::SmallVec<[(ElementId, NodePosition); 5]>, +} + +impl Default for PersistantElementIter { + fn default() -> Self { + PersistantElementIter { + stack: smallvec::smallvec![(ElementId(0), NodePosition::AtNode)], + } + } +} + +impl PersistantElementIter { + pub fn new() -> Self { + Self::default() + } + + /// remove stale element refreneces + /// returns true if the focused element is removed + pub fn prune(&mut self, mutations: &Mutations, rdom: &RealDom) -> bool { + let mut changed = false; + let ids_removed: Vec<_> = mutations + .edits + .iter() + .filter_map(|e| { + if let DomEdit::Remove { root } = e { + Some(*root) + } else { + None + } + }) + .collect(); + // if any element is removed in the chain, remove it and its children from the stack + if let Some(r) = self + .stack + .iter() + .position(|(el_id, _)| ids_removed.iter().any(|id| el_id.as_u64() == *id)) + { + self.stack.truncate(r); + changed = true; + } + // if a child is removed or inserted before or at the current element, update the child index + for (el_id, child_idx) in self.stack.iter_mut() { + if let NodePosition::InChild(child_idx) = child_idx { + if let NodeType::Element { children, .. } = &rdom[*el_id].node_type { + for m in &mutations.edits { + match m { + DomEdit::Remove { root } => { + if children + .iter() + .take(*child_idx + 1) + .any(|c| c.as_u64() == *root) + { + *child_idx -= 1; + } + } + DomEdit::InsertBefore { root, n } => { + if children + .iter() + .take(*child_idx + 1) + .any(|c| c.as_u64() == *root) + { + *child_idx += *n as usize; + } + } + DomEdit::InsertAfter { root, n } => { + if children + .iter() + .take(*child_idx) + .any(|c| c.as_u64() == *root) + { + *child_idx += *n as usize; + } + } + _ => (), + } + } + } + } + } + changed + } + + /// get the next element + pub fn next(&mut self, rdom: &RealDom) -> ElementProduced { + if self.stack.is_empty() { + let id = ElementId(0); + let new = (id, NodePosition::AtNode); + self.stack.push(new); + ElementProduced::Looped(id) + } else { + let (last, o_child_idx) = self.stack.last_mut().unwrap(); + let node = &rdom[*last]; + match &node.node_type { + NodeType::Element { children, .. } => { + *o_child_idx = o_child_idx.map(|i| i + 1); + // if we have children, go to the next child + let child_idx = o_child_idx.get_or_insert(0); + if child_idx >= children.len() { + self.pop(); + self.next(rdom) + } else { + let id = children[child_idx]; + if let NodeType::Element { .. } = &rdom[id].node_type { + self.stack.push((id, NodePosition::AtNode)); + } + ElementProduced::Progressed(id) + } + } + + NodeType::Text { .. } | NodeType::Placeholder { .. } => { + // we are at a leaf, so we are done + ElementProduced::Progressed(self.pop()) + } + } + } + } + + /// get the previous element + pub fn prev(&mut self, rdom: &RealDom) -> ElementProduced { + // recursively add the last child element to the stack + fn push_back( + stack: &mut smallvec::SmallVec<[(ElementId, NodePosition); 5]>, + new_node: ElementId, + rdom: &RealDom, + ) -> ElementId { + match &rdom[new_node].node_type { + NodeType::Element { children, .. } => { + if children.is_empty() { + new_node + } else { + stack.push((new_node, NodePosition::InChild(children.len() - 1))); + push_back(stack, *children.last().unwrap(), rdom) + } + } + _ => new_node, + } + } + if self.stack.is_empty() { + let new_node = ElementId(0); + ElementProduced::Looped(push_back(&mut self.stack, new_node, rdom)) + } else { + let (last, o_child_idx) = self.stack.last_mut().unwrap(); + let node = &rdom[*last]; + match &node.node_type { + NodeType::Element { children, .. } => { + // if we have children, go to the next child + if let NodePosition::InChild(0) = o_child_idx { + ElementProduced::Progressed(self.pop()) + } else { + *o_child_idx = o_child_idx.map(|i| i - 1); + if let NodePosition::InChild(child_idx) = o_child_idx { + if *child_idx >= children.len() || children.is_empty() { + self.pop(); + self.prev(rdom) + } else { + let new_node = children[*child_idx]; + ElementProduced::Progressed(push_back( + &mut self.stack, + new_node, + rdom, + )) + } + } else { + self.pop(); + self.prev(rdom) + } + } + } + + NodeType::Text { .. } | NodeType::Placeholder { .. } => { + // we are at a leaf, so we are done + ElementProduced::Progressed(self.pop()) + } + } + } + } + + fn pop(&mut self) -> ElementId { + self.stack.pop().unwrap().0 + } +} diff --git a/packages/router/README.md b/packages/router/README.md index d0c036bc9..ffd04ef0e 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -43,9 +43,9 @@ | Examples | - Guide (0.1.8) + Guide (Latest) | - Guide (Master) + Guide (Master) diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml new file mode 100644 index 000000000..24ef50c1d --- /dev/null +++ b/packages/rsx/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dioxus-rsx" +version = "0.0.0" +edition = "2018" +license = "MIT/Apache-2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = { version = "1.0" } +syn = { version = "1.0", features = ["full", "extra-traits"] } +quote = { version = "1.0" } diff --git a/packages/core-macro/src/rsx/component.rs b/packages/rsx/src/component.rs similarity index 76% rename from packages/core-macro/src/rsx/component.rs rename to packages/rsx/src/component.rs index dfdda74b8..90248ad1f 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/rsx/src/component.rs @@ -1,7 +1,6 @@ -//! Parse components into the VComponent VNode +//! Parse components into the VNode::Component variant //! ========================================== //! -//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the vcomponent format. //! We can be reasonably sure that whatever enters this parsing path is in the right format. //! This feature must support //! - [x] Namespaced components @@ -19,19 +18,59 @@ use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, - token, Expr, Ident, LitStr, Result, Token, + spanned::Spanned, + token, AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Result, + Token, }; +#[derive(PartialEq, Eq)] pub struct Component { pub name: syn::Path, + pub prop_gen_args: Option, pub body: Vec, pub children: Vec, pub manual_props: Option, } +impl Component { + pub fn validate_component_path(path: &syn::Path) -> Result<()> { + // ensure path segments doesn't have PathArguments, only the last + // segment is allowed to have one. + if path + .segments + .iter() + .take(path.segments.len() - 1) + .any(|seg| seg.arguments != PathArguments::None) + { + component_path_cannot_have_arguments!(path.span()); + } + + // ensure last segment only have value of None or AngleBracketed + if !matches!( + path.segments.last().unwrap().arguments, + PathArguments::None | PathArguments::AngleBracketed(_) + ) { + invalid_component_path!(path.span()); + } + + Ok(()) + } +} + impl Parse for Component { fn parse(stream: ParseStream) -> Result { - let name = syn::Path::parse_mod_style(stream)?; + let mut name = stream.parse::()?; + Component::validate_component_path(&name)?; + + // extract the path arguments from the path into prop_gen_args + let prop_gen_args = name.segments.last_mut().and_then(|seg| { + if let PathArguments::AngleBracketed(args) = seg.arguments.clone() { + seg.arguments = PathArguments::None; + Some(args) + } else { + None + } + }); let content: ParseBuffer; @@ -65,6 +104,7 @@ impl Parse for Component { Ok(Self { name, + prop_gen_args, body, children, manual_props, @@ -75,6 +115,7 @@ impl Parse for Component { impl ToTokens for Component { fn to_tokens(&self, tokens: &mut TokenStream2) { let name = &self.name; + let prop_gen_args = &self.prop_gen_args; let mut has_key = None; @@ -102,7 +143,10 @@ impl ToTokens for Component { }} } None => { - let mut toks = quote! { fc_to_builder(#name) }; + let mut toks = match prop_gen_args { + Some(gen_args) => quote! { fc_to_builder #gen_args(#name) }, + None => quote! { fc_to_builder(#name) }, + }; for field in &self.body { match field.name.to_string().as_str() { "key" => { @@ -149,12 +193,14 @@ impl ToTokens for Component { } // the struct's fields info +#[derive(PartialEq, Eq)] pub struct ComponentField { - name: Ident, - content: ContentField, + pub name: Ident, + pub content: ContentField, } -enum ContentField { +#[derive(PartialEq, Eq)] +pub enum ContentField { ManExpr(Expr), Formatted(LitStr), OnHandlerRaw(Expr), diff --git a/packages/core-macro/src/rsx/element.rs b/packages/rsx/src/element.rs similarity index 97% rename from packages/core-macro/src/rsx/element.rs rename to packages/rsx/src/element.rs index 96c7a2efc..38ab75575 100644 --- a/packages/core-macro/src/rsx/element.rs +++ b/packages/rsx/src/element.rs @@ -4,12 +4,13 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseBuffer, ParseStream}, - Expr, Ident, LitStr, Result, Token, + Error, Expr, Ident, LitStr, Result, Token, }; // ======================================= // Parse the VNode::Element type // ======================================= +#[derive(PartialEq, Eq)] pub struct Element { pub name: Ident, pub key: Option, @@ -64,7 +65,7 @@ impl Parse for Element { } if content.parse::().is_err() { - missing_trailing_comma!(ident); + missing_trailing_comma!(ident.span()); } continue; } @@ -122,7 +123,7 @@ impl Parse for Element { // todo: add a message saying you need to include commas between fields if content.parse::().is_err() { - missing_trailing_comma!(ident); + missing_trailing_comma!(ident.span()); } continue; } @@ -189,6 +190,7 @@ impl ToTokens for Element { } } +#[derive(PartialEq, Eq)] pub enum ElementAttr { /// attribute: "valuee {}" AttrText { name: Ident, value: LitStr }, @@ -208,6 +210,7 @@ pub enum ElementAttr { EventTokens { name: Ident, tokens: Expr }, } +#[derive(PartialEq, Eq)] pub struct ElementAttrNamed { pub el_name: Ident, pub attr: ElementAttr, diff --git a/packages/rsx/src/errors.rs b/packages/rsx/src/errors.rs new file mode 100644 index 000000000..f89b28500 --- /dev/null +++ b/packages/rsx/src/errors.rs @@ -0,0 +1,26 @@ +macro_rules! missing_trailing_comma { + ($span:expr) => { + return Err(Error::new($span, "missing trailing comma")); + }; +} + +macro_rules! attr_after_element { + ($span:expr) => { + return Err(Error::new($span, "expected element\n = help move the attribute above all the children and text elements")); + }; +} + +macro_rules! component_path_cannot_have_arguments { + ($span:expr) => { + return Err(Error::new( + $span, + "expected a path without arguments\n = try remove the path arguments", + )); + }; +} + +macro_rules! invalid_component_path { + ($span:expr) => { + return Err(Error::new($span, "Invalid component path syntax")); + }; +} diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs new file mode 100644 index 000000000..2d77a243f --- /dev/null +++ b/packages/rsx/src/ifmt.rs @@ -0,0 +1,176 @@ +use std::str::FromStr; + +use proc_macro2::{Span, TokenStream}; + +use quote::{quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + *, +}; + +pub fn format_args_f_impl(input: IfmtInput) -> Result { + // build format_literal + let mut format_literal = String::new(); + let mut expr_counter = 0; + for segment in input.segments.iter() { + match segment { + Segment::Literal(s) => format_literal += s, + Segment::Formatted { + format_args, + segment, + } => { + format_literal += "{"; + match segment { + FormattedSegment::Expr(_) => { + format_literal += &expr_counter.to_string(); + expr_counter += 1; + } + FormattedSegment::Ident(ident) => { + format_literal += &ident.to_string(); + } + } + format_literal += ":"; + format_literal += format_args; + format_literal += "}"; + } + } + } + + let positional_args = input.segments.iter().filter_map(|seg| { + if let Segment::Formatted { + segment: FormattedSegment::Expr(expr), + .. + } = seg + { + Some(expr) + } else { + None + } + }); + + let named_args = input.segments.iter().filter_map(|seg| { + if let Segment::Formatted { + segment: FormattedSegment::Ident(ident), + .. + } = seg + { + Some(quote! {#ident = #ident}) + } else { + None + } + }); + + Ok(quote! { + format_args!( + #format_literal + #(, #positional_args)* + #(, #named_args)* + ) + }) +} + +#[allow(dead_code)] // dumb compiler does not see the struct being used... +#[derive(Debug)] +pub struct IfmtInput { + pub segments: Vec, +} + +impl FromStr for IfmtInput { + type Err = syn::Error; + + fn from_str(input: &str) -> Result { + let mut chars = input.chars().peekable(); + let mut segments = Vec::new(); + let mut current_literal = String::new(); + while let Some(c) = chars.next() { + if c == '{' { + if let Some(c) = chars.next_if(|c| *c == '{') { + current_literal.push(c); + continue; + } + segments.push(Segment::Literal(current_literal)); + current_literal = String::new(); + let mut current_captured = String::new(); + while let Some(c) = chars.next() { + if c == ':' { + let mut current_format_args = String::new(); + for c in chars.by_ref() { + if c == '}' { + segments.push(Segment::Formatted { + format_args: current_format_args, + segment: FormattedSegment::parse(¤t_captured)?, + }); + break; + } + current_format_args.push(c); + } + break; + } + if c == '}' { + segments.push(Segment::Formatted { + format_args: String::new(), + segment: FormattedSegment::parse(¤t_captured)?, + }); + break; + } + current_captured.push(c); + } + } else { + current_literal.push(c); + } + } + segments.push(Segment::Literal(current_literal)); + Ok(Self { segments }) + } +} + +#[derive(Debug)] +pub enum Segment { + Literal(String), + Formatted { + format_args: String, + segment: FormattedSegment, + }, +} + +#[derive(Debug)] +pub enum FormattedSegment { + Expr(Box), + Ident(Ident), +} + +impl FormattedSegment { + fn parse(input: &str) -> Result { + if let Ok(ident) = parse_str::(input) { + if ident == input { + return Ok(Self::Ident(ident)); + } + } + // if let Ok(expr) = parse_str(&("{".to_string() + input + "}")) { + if let Ok(expr) = parse_str(input) { + Ok(Self::Expr(Box::new(expr))) + } else { + Err(Error::new( + Span::call_site(), + "Expected Ident or Expression", + )) + } + } +} + +impl ToTokens for FormattedSegment { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Expr(expr) => expr.to_tokens(tokens), + Self::Ident(ident) => ident.to_tokens(tokens), + } + } +} + +impl Parse for IfmtInput { + fn parse(input: ParseStream) -> Result { + let input: LitStr = input.parse()?; + let input_str = input.value(); + IfmtInput::from_str(&input_str) + } +} diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/rsx/src/lib.rs similarity index 99% rename from packages/core-macro/src/rsx/mod.rs rename to packages/rsx/src/lib.rs index 6b9dfc3dc..a5b8143f2 100644 --- a/packages/core-macro/src/rsx/mod.rs +++ b/packages/rsx/src/lib.rs @@ -16,13 +16,13 @@ mod errors; mod component; mod element; +mod ifmt; mod node; -pub mod pretty; - // Re-export the namespaces into each other pub use component::*; pub use element::*; +pub use ifmt::*; pub use node::*; // imports diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs new file mode 100644 index 000000000..e8b754f13 --- /dev/null +++ b/packages/rsx/src/node.rs @@ -0,0 +1,101 @@ +use super::*; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + token, Expr, LitStr, Result, +}; + +/* +Parse +-> div {} +-> Component {} +-> component() +-> "text {with_args}" +-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr +*/ +#[derive(PartialEq, Eq)] +pub enum BodyNode { + Element(Element), + Component(Component), + Text(LitStr), + RawExpr(Expr), +} + +impl BodyNode { + pub fn is_litstr(&self) -> bool { + matches!(self, BodyNode::Text(_)) + } +} + +impl Parse for BodyNode { + fn parse(stream: ParseStream) -> Result { + if stream.peek(LitStr) { + return Ok(BodyNode::Text(stream.parse()?)); + } + + let body_stream = stream.fork(); + if let Ok(path) = body_stream.parse::() { + // this is an Element if path match of: + // - one ident + // - followed by `{` + // - 1st char is lowercase + // + // example: + // div {} + if let Some(ident) = path.get_ident() { + if body_stream.peek(token::Brace) + && ident + .to_string() + .chars() + .next() + .unwrap() + .is_ascii_lowercase() + { + return Ok(BodyNode::Element(stream.parse::()?)); + } + } + + // Otherwise this should be Component, allowed syntax: + // - syn::Path + // - PathArguments can only apper in last segment + // - followed by `{` or `(`, note `(` cannot be used with one ident + // + // example + // Div {} + // Div () + // ::Div {} + // crate::Div {} + // component() + // ::component {} + // ::component () + // crate::component{} + // crate::component() + // Input:: {} + // crate::Input:: {} + if body_stream.peek(token::Brace) || body_stream.peek(token::Paren) { + Component::validate_component_path(&path)?; + + return Ok(BodyNode::Component(stream.parse()?)); + } + } + + Ok(BodyNode::RawExpr(stream.parse::()?)) + } +} + +impl ToTokens for BodyNode { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match &self { + BodyNode::Element(el) => el.to_tokens(tokens), + BodyNode::Component(comp) => comp.to_tokens(tokens), + BodyNode::Text(txt) => tokens.append_all(quote! { + __cx.text(format_args_f!(#txt)) + }), + BodyNode::RawExpr(exp) => tokens.append_all(quote! { + __cx.fragment_from_iter(#exp) + }), + } + } +} diff --git a/packages/rsx_interpreter/Cargo.toml b/packages/rsx_interpreter/Cargo.toml new file mode 100644 index 000000000..0b1f79a5d --- /dev/null +++ b/packages/rsx_interpreter/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "dioxus-rsx-interpreter" +version = "0.1.0" +edition = "2021" +license = "MIT/Apache-2.0" + +[dependencies] +syn = { version = "1.0", features = ["extra-traits"] } +proc-macro2 = { version = "1.0.39", features = ["span-locations"] } +quote = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +lazy_static = "1.4.0" + +dioxus-rsx = { path = "../rsx", default-features = false } +dioxus-ssr = { path = "../ssr" } +dioxus-core = { path = "../core" } +dioxus-html = { path = "../html" } +dioxus-hooks = { path = "../hooks"} + +[dev-dependencies] +dioxus-core-macro = { path = "../core-macro" } +bumpalo = { version = "3.6", features = ["collections", "boxed"] } diff --git a/packages/rsx_interpreter/src/attributes.rs b/packages/rsx_interpreter/src/attributes.rs new file mode 100644 index 000000000..f8d19fddb --- /dev/null +++ b/packages/rsx_interpreter/src/attributes.rs @@ -0,0 +1,869 @@ +use crate::elements::*; + +// map the rsx name of the attribute to the html name of the attribute and the namespace that contains it +pub fn attrbute_to_static_str(attr: &str) -> Option<(&'static str, Option<&'static str>)> { + NO_NAMESPACE_ATTRIBUTES + .iter() + .find(|&a| *a == attr) + .map(|a| (*a, None)) + .or_else(|| { + STYLE_ATTRIBUTES + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, Some("style"))) + }) + .or_else(|| { + MAPPED_ATTRIBUTES + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, None)) + }) + .or_else(|| { + svg::MAPPED_ATTRIBUTES + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, None)) + }) + .or_else(|| { + ELEMENTS_WITH_MAPPED_ATTRIBUTES + .iter() + .find_map(|(_, attrs)| { + attrs + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, None)) + }) + }) + .or_else(|| { + ELEMENTS_WITH_NAMESPACE + .iter() + .find_map(|(_, _, attrs)| attrs.iter().find(|a| **a == attr).map(|a| (*a, None))) + }) + .or_else(|| { + ELEMENTS_WITHOUT_NAMESPACE + .iter() + .find_map(|(_, attrs)| attrs.iter().find(|a| **a == attr).map(|a| (*a, None))) + }) +} + +macro_rules! no_namespace_trait_methods { + ( + $( + $(#[$attr:meta])* + $name:ident; + )* + ) => { + pub const NO_NAMESPACE_ATTRIBUTES: &'static [&'static str] = &[ + $( + stringify!($name), + )* + ]; + }; +} + +macro_rules! style_trait_methods { + ( + $( + $(#[$attr:meta])* + $name:ident: $lit:literal, + )* + ) => { + pub const STYLE_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[ + $( + (stringify!($name), $lit), + )* + ]; + }; +} + +macro_rules! mapped_trait_methods { + ( + $( + $(#[$attr:meta])* + $name:ident: $lit:literal, + )* + ) => { + pub const MAPPED_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[ + $( + (stringify!($name), $lit), + )* + ("prevent_default", "dioxus-prevent-default"), + ]; + }; +} + +no_namespace_trait_methods! { + accesskey; + + /// The HTML class attribute is used to specify a class for an HTML element. + /// + /// ## Details + /// Multiple HTML elements can share the same class. + /// + /// The class global attribute is a space-separated list of the case-sensitive classes of the element. + /// Classes allow CSS and Javascript to select and access specific elements via the class selectors or + /// functions like the DOM method document.getElementsByClassName. + /// + /// ## Example + /// + /// ### HTML: + /// ```html + ///

Above point sounds a bit obvious. Remove/rewrite?

+ /// ``` + /// + /// ### CSS: + /// ```css + /// .note { + /// font-style: italic; + /// font-weight: bold; + /// } + /// + /// .editorial { + /// background: rgb(255, 0, 0, .25); + /// padding: 10px; + /// } + /// ``` + class; + contenteditable; + data; + dir; + draggable; + hidden; + id; + lang; + spellcheck; + style; + tabindex; + title; + translate; + + role; + + /// dangerous_inner_html is Dioxus's replacement for using innerHTML in the browser DOM. In general, setting + /// HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) + /// attack. So, you can set HTML directly from Dioxus, but you have to type out dangerous_inner_html to remind + /// yourself that it’s dangerous + dangerous_inner_html; +} + +// This macro creates an explicit method call for each of the style attributes. +// +// The left token specifies the name of the attribute in the rsx! macro, and the right string literal specifies the +// actual name of the attribute generated. +// +// This roughly follows the html spec +style_trait_methods! { + align_content: "align-content", + align_items: "align-items", + align_self: "align-self", + alignment_adjust: "alignment-adjust", + alignment_baseline: "alignment-baseline", + all: "all", + alt: "alt", + animation: "animation", + animation_delay: "animation-delay", + animation_direction: "animation-direction", + animation_duration: "animation-duration", + animation_fill_mode: "animation-fill-mode", + animation_iteration_count: "animation-iteration-count", + animation_name: "animation-name", + animation_play_state: "animation-play-state", + animation_timing_function: "animation-timing-function", + azimuth: "azimuth", + backface_visibility: "backface-visibility", + background: "background", + background_attachment: "background-attachment", + background_clip: "background-clip", + background_color: "background-color", + background_image: "background-image", + background_origin: "background-origin", + background_position: "background-position", + background_repeat: "background-repeat", + background_size: "background-size", + background_blend_mode: "background-blend-mode", + baseline_shift: "baseline-shift", + bleed: "bleed", + bookmark_label: "bookmark-label", + bookmark_level: "bookmark-level", + bookmark_state: "bookmark-state", + border: "border", + border_color: "border-color", + border_style: "border-style", + border_width: "border-width", + border_bottom: "border-bottom", + border_bottom_color: "border-bottom-color", + border_bottom_style: "border-bottom-style", + border_bottom_width: "border-bottom-width", + border_left: "border-left", + border_left_color: "border-left-color", + border_left_style: "border-left-style", + border_left_width: "border-left-width", + border_right: "border-right", + border_right_color: "border-right-color", + border_right_style: "border-right-style", + border_right_width: "border-right-width", + border_top: "border-top", + border_top_color: "border-top-color", + border_top_style: "border-top-style", + border_top_width: "border-top-width", + border_collapse: "border-collapse", + border_image: "border-image", + border_image_outset: "border-image-outset", + border_image_repeat: "border-image-repeat", + border_image_slice: "border-image-slice", + border_image_source: "border-image-source", + border_image_width: "border-image-width", + border_radius: "border-radius", + border_bottom_left_radius: "border-bottom-left-radius", + border_bottom_right_radius: "border-bottom-right-radius", + border_top_left_radius: "border-top-left-radius", + border_top_right_radius: "border-top-right-radius", + border_spacing: "border-spacing", + bottom: "bottom", + box_decoration_break: "box-decoration-break", + box_shadow: "box-shadow", + box_sizing: "box-sizing", + box_snap: "box-snap", + break_after: "break-after", + break_before: "break-before", + break_inside: "break-inside", + buffered_rendering: "buffered-rendering", + caption_side: "caption-side", + clear: "clear", + clear_side: "clear-side", + clip: "clip", + clip_path: "clip-path", + clip_rule: "clip-rule", + color: "color", + color_adjust: "color-adjust", + color_correction: "color-correction", + color_interpolation: "color-interpolation", + color_interpolation_filters: "color-interpolation-filters", + color_profile: "color-profile", + color_rendering: "color-rendering", + column_fill: "column-fill", + column_gap: "column-gap", + column_rule: "column-rule", + column_rule_color: "column-rule-color", + column_rule_style: "column-rule-style", + column_rule_width: "column-rule-width", + column_span: "column-span", + columns: "columns", + column_count: "column-count", + column_width: "column-width", + contain: "contain", + content: "content", + counter_increment: "counter-increment", + counter_reset: "counter-reset", + counter_set: "counter-set", + cue: "cue", + cue_after: "cue-after", + cue_before: "cue-before", + cursor: "cursor", + direction: "direction", + display: "display", + display_inside: "display-inside", + display_outside: "display-outside", + display_extras: "display-extras", + display_box: "display-box", + dominant_baseline: "dominant-baseline", + elevation: "elevation", + empty_cells: "empty-cells", + enable_background: "enable-background", + fill: "fill", + fill_opacity: "fill-opacity", + fill_rule: "fill-rule", + filter: "filter", + float: "float", + float_defer_column: "float-defer-column", + float_defer_page: "float-defer-page", + float_offset: "float-offset", + float_wrap: "float-wrap", + flow_into: "flow-into", + flow_from: "flow-from", + flex: "flex", + flex_basis: "flex-basis", + flex_grow: "flex-grow", + flex_shrink: "flex-shrink", + flex_flow: "flex-flow", + flex_direction: "flex-direction", + flex_wrap: "flex-wrap", + flood_color: "flood-color", + flood_opacity: "flood-opacity", + font: "font", + font_family: "font-family", + font_size: "font-size", + font_stretch: "font-stretch", + font_style: "font-style", + font_weight: "font-weight", + font_feature_settings: "font-feature-settings", + font_kerning: "font-kerning", + font_language_override: "font-language-override", + font_size_adjust: "font-size-adjust", + font_synthesis: "font-synthesis", + font_variant: "font-variant", + font_variant_alternates: "font-variant-alternates", + font_variant_caps: "font-variant-caps", + font_variant_east_asian: "font-variant-east-asian", + font_variant_ligatures: "font-variant-ligatures", + font_variant_numeric: "font-variant-numeric", + font_variant_position: "font-variant-position", + footnote_policy: "footnote-policy", + glyph_orientation_horizontal: "glyph-orientation-horizontal", + glyph_orientation_vertical: "glyph-orientation-vertical", + grid: "grid", + grid_auto_flow: "grid-auto-flow", + grid_auto_columns: "grid-auto-columns", + grid_auto_rows: "grid-auto-rows", + grid_template: "grid-template", + grid_template_areas: "grid-template-areas", + grid_template_columns: "grid-template-columns", + grid_template_rows: "grid-template-rows", + grid_area: "grid-area", + grid_column: "grid-column", + grid_column_start: "grid-column-start", + grid_column_end: "grid-column-end", + grid_row: "grid-row", + grid_row_start: "grid-row-start", + grid_row_end: "grid-row-end", + hanging_punctuation: "hanging-punctuation", + height: "height", + hyphenate_character: "hyphenate-character", + hyphenate_limit_chars: "hyphenate-limit-chars", + hyphenate_limit_last: "hyphenate-limit-last", + hyphenate_limit_lines: "hyphenate-limit-lines", + hyphenate_limit_zone: "hyphenate-limit-zone", + hyphens: "hyphens", + icon: "icon", + image_orientation: "image-orientation", + image_resolution: "image-resolution", + image_rendering: "image-rendering", + ime: "ime", + ime_align: "ime-align", + ime_mode: "ime-mode", + ime_offset: "ime-offset", + ime_width: "ime-width", + initial_letters: "initial-letters", + inline_box_align: "inline-box-align", + isolation: "isolation", + justify_content: "justify-content", + justify_items: "justify-items", + justify_self: "justify-self", + kerning: "kerning", + left: "left", + letter_spacing: "letter-spacing", + lighting_color: "lighting-color", + line_box_contain: "line-box-contain", + line_break: "line-break", + line_grid: "line-grid", + line_height: "line-height", + line_slack: "line-slack", + line_snap: "line-snap", + list_style: "list-style", + list_style_image: "list-style-image", + list_style_position: "list-style-position", + list_style_type: "list-style-type", + margin: "margin", + margin_bottom: "margin-bottom", + margin_left: "margin-left", + margin_right: "margin-right", + margin_top: "margin-top", + marker: "marker", + marker_end: "marker-end", + marker_mid: "marker-mid", + marker_pattern: "marker-pattern", + marker_segment: "marker-segment", + marker_start: "marker-start", + marker_knockout_left: "marker-knockout-left", + marker_knockout_right: "marker-knockout-right", + marker_side: "marker-side", + marks: "marks", + marquee_direction: "marquee-direction", + marquee_play_count: "marquee-play-count", + marquee_speed: "marquee-speed", + marquee_style: "marquee-style", + mask: "mask", + mask_image: "mask-image", + mask_repeat: "mask-repeat", + mask_position: "mask-position", + mask_clip: "mask-clip", + mask_origin: "mask-origin", + mask_size: "mask-size", + mask_box: "mask-box", + mask_box_outset: "mask-box-outset", + mask_box_repeat: "mask-box-repeat", + mask_box_slice: "mask-box-slice", + mask_box_source: "mask-box-source", + mask_box_width: "mask-box-width", + mask_type: "mask-type", + max_height: "max-height", + max_lines: "max-lines", + max_width: "max-width", + min_height: "min-height", + min_width: "min-width", + mix_blend_mode: "mix-blend-mode", + nav_down: "nav-down", + nav_index: "nav-index", + nav_left: "nav-left", + nav_right: "nav-right", + nav_up: "nav-up", + object_fit: "object-fit", + object_position: "object-position", + offset_after: "offset-after", + offset_before: "offset-before", + offset_end: "offset-end", + offset_start: "offset-start", + opacity: "opacity", + order: "order", + orphans: "orphans", + outline: "outline", + outline_color: "outline-color", + outline_style: "outline-style", + outline_width: "outline-width", + outline_offset: "outline-offset", + overflow: "overflow", + overflow_x: "overflow-x", + overflow_y: "overflow-y", + overflow_style: "overflow-style", + overflow_wrap: "overflow-wrap", + padding: "padding", + padding_bottom: "padding-bottom", + padding_left: "padding-left", + padding_right: "padding-right", + padding_top: "padding-top", + page: "page", + page_break_after: "page-break-after", + page_break_before: "page-break-before", + page_break_inside: "page-break-inside", + paint_order: "paint-order", + pause: "pause", + pause_after: "pause-after", + pause_before: "pause-before", + perspective: "perspective", + perspective_origin: "perspective-origin", + pitch: "pitch", + pitch_range: "pitch-range", + play_during: "play-during", + pointer_events: "pointer-events", + position: "position", + quotes: "quotes", + region_fragment: "region-fragment", + resize: "resize", + rest: "rest", + rest_after: "rest-after", + rest_before: "rest-before", + richness: "richness", + right: "right", + ruby_align: "ruby-align", + ruby_merge: "ruby-merge", + ruby_position: "ruby-position", + scroll_behavior: "scroll-behavior", + scroll_snap_coordinate: "scroll-snap-coordinate", + scroll_snap_destination: "scroll-snap-destination", + scroll_snap_points_x: "scroll-snap-points-x", + scroll_snap_points_y: "scroll-snap-points-y", + scroll_snap_type: "scroll-snap-type", + shape_image_threshold: "shape-image-threshold", + shape_inside: "shape-inside", + shape_margin: "shape-margin", + shape_outside: "shape-outside", + shape_padding: "shape-padding", + shape_rendering: "shape-rendering", + size: "size", + speak: "speak", + speak_as: "speak-as", + speak_header: "speak-header", + speak_numeral: "speak-numeral", + speak_punctuation: "speak-punctuation", + speech_rate: "speech-rate", + stop_color: "stop-color", + stop_opacity: "stop-opacity", + stress: "stress", + string_set: "string-set", + stroke: "stroke", + stroke_dasharray: "stroke-dasharray", + stroke_dashoffset: "stroke-dashoffset", + stroke_linecap: "stroke-linecap", + stroke_linejoin: "stroke-linejoin", + stroke_miterlimit: "stroke-miterlimit", + stroke_opacity: "stroke-opacity", + stroke_width: "stroke-width", + tab_size: "tab-size", + table_layout: "table-layout", + text_align: "text-align", + text_align_all: "text-align-all", + text_align_last: "text-align-last", + text_anchor: "text-anchor", + text_combine_upright: "text-combine-upright", + text_decoration: "text-decoration", + text_decoration_color: "text-decoration-color", + text_decoration_line: "text-decoration-line", + text_decoration_style: "text-decoration-style", + text_decoration_skip: "text-decoration-skip", + text_emphasis: "text-emphasis", + text_emphasis_color: "text-emphasis-color", + text_emphasis_style: "text-emphasis-style", + text_emphasis_position: "text-emphasis-position", + text_emphasis_skip: "text-emphasis-skip", + text_height: "text-height", + text_indent: "text-indent", + text_justify: "text-justify", + text_orientation: "text-orientation", + text_overflow: "text-overflow", + text_rendering: "text-rendering", + text_shadow: "text-shadow", + text_size_adjust: "text-size-adjust", + text_space_collapse: "text-space-collapse", + text_spacing: "text-spacing", + text_transform: "text-transform", + text_underline_position: "text-underline-position", + text_wrap: "text-wrap", + top: "top", + touch_action: "touch-action", + transform: "transform", + transform_box: "transform-box", + transform_origin: "transform-origin", + transform_style: "transform-style", + transition: "transition", + transition_delay: "transition-delay", + transition_duration: "transition-duration", + transition_property: "transition-property", + unicode_bidi: "unicode-bidi", + vector_effect: "vector-effect", + vertical_align: "vertical-align", + visibility: "visibility", + voice_balance: "voice-balance", + voice_duration: "voice-duration", + voice_family: "voice-family", + voice_pitch: "voice-pitch", + voice_range: "voice-range", + voice_rate: "voice-rate", + voice_stress: "voice-stress", + voice_volumn: "voice-volumn", + volume: "volume", + white_space: "white-space", + widows: "widows", + width: "width", + will_change: "will-change", + word_break: "word-break", + word_spacing: "word-spacing", + word_wrap: "word-wrap", + wrap_flow: "wrap-flow", + wrap_through: "wrap-through", + writing_mode: "writing-mode", + gap: "gap", + list_styler_type: "list-style-type", + row_gap: "row-gap", + transition_timing_function: "transition-timing-function", + user_select: "user-select", + webkit_user_select: "-webkit-user-select", + z_index : "z-index", +} +mapped_trait_methods! { + aria_current: "aria-current", + aria_details: "aria-details", + aria_disabled: "aria-disabled", + aria_hidden: "aria-hidden", + aria_invalid: "aria-invalid", + aria_keyshortcuts: "aria-keyshortcuts", + aria_label: "aria-label", + aria_roledescription: "aria-roledescription", + // Widget Attributes + aria_autocomplete: "aria-autocomplete", + aria_checked: "aria-checked", + aria_expanded: "aria-expanded", + aria_haspopup: "aria-haspopup", + aria_level: "aria-level", + aria_modal: "aria-modal", + aria_multiline: "aria-multiline", + aria_multiselectable: "aria-multiselectable", + aria_orientation: "aria-orientation", + aria_placeholder: "aria-placeholder", + aria_pressed: "aria-pressed", + aria_readonly: "aria-readonly", + aria_required: "aria-required", + aria_selected: "aria-selected", + aria_sort: "aria-sort", + aria_valuemax: "aria-valuemax", + aria_valuemin: "aria-valuemin", + aria_valuenow: "aria-valuenow", + aria_valuetext: "aria-valuetext", + // Live Region Attributes + aria_atomic: "aria-atomic", + aria_busy: "aria-busy", + aria_live: "aria-live", + aria_relevant: "aria-relevant", + + aria_dropeffect: "aria-dropeffect", + aria_grabbed: "aria-grabbed", + // Relationship Attributes + aria_activedescendant: "aria-activedescendant", + aria_colcount: "aria-colcount", + aria_colindex: "aria-colindex", + aria_colspan: "aria-colspan", + aria_controls: "aria-controls", + aria_describedby: "aria-describedby", + aria_errormessage: "aria-errormessage", + aria_flowto: "aria-flowto", + aria_labelledby: "aria-labelledby", + aria_owns: "aria-owns", + aria_posinset: "aria-posinset", + aria_rowcount: "aria-rowcount", + aria_rowindex: "aria-rowindex", + aria_rowspan: "aria-rowspan", + aria_setsize: "aria-setsize", +} + +pub mod svg { + mapped_trait_methods! { + accent_height: "accent-height", + accumulate: "accumulate", + additive: "additive", + alignment_baseline: "alignment-baseline", + alphabetic: "alphabetic", + amplitude: "amplitude", + arabic_form: "arabic-form", + ascent: "ascent", + attributeName: "attributeName", + attributeType: "attributeType", + azimuth: "azimuth", + baseFrequency: "baseFrequency", + baseline_shift: "baseline-shift", + baseProfile: "baseProfile", + bbox: "bbox", + begin: "begin", + bias: "bias", + by: "by", + calcMode: "calcMode", + cap_height: "cap-height", + class: "class", + clip: "clip", + clipPathUnits: "clipPathUnits", + clip_path: "clip-path", + clip_rule: "clip-rule", + color: "color", + color_interpolation: "color-interpolation", + color_interpolation_filters: "color-interpolation-filters", + color_profile: "color-profile", + color_rendering: "color-rendering", + contentScriptType: "contentScriptType", + contentStyleType: "contentStyleType", + crossorigin: "crossorigin", + cursor: "cursor", + cx: "cx", + cy: "cy", + d: "d", + decelerate: "decelerate", + descent: "descent", + diffuseConstant: "diffuseConstant", + direction: "direction", + display: "display", + divisor: "divisor", + dominant_baseline: "dominant-baseline", + dur: "dur", + dx: "dx", + dy: "dy", + edgeMode: "edgeMode", + elevation: "elevation", + enable_background: "enable-background", + end: "end", + exponent: "exponent", + fill: "fill", + fill_opacity: "fill-opacity", + fill_rule: "fill-rule", + filter: "filter", + filterRes: "filterRes", + filterUnits: "filterUnits", + flood_color: "flood-color", + flood_opacity: "flood-opacity", + font_family: "font-family", + font_size: "font-size", + font_size_adjust: "font-size-adjust", + font_stretch: "font-stretch", + font_style: "font-style", + font_variant: "font-variant", + font_weight: "font-weight", + format: "format", + from: "from", + fr: "fr", + fx: "fx", + fy: "fy", + g1: "g1", + g2: "g2", + glyph_name: "glyph-name", + glyph_orientation_horizontal: "glyph-orientation-horizontal", + glyph_orientation_vertical: "glyph-orientation-vertical", + glyphRef: "glyphRef", + gradientTransform: "gradientTransform", + gradientUnits: "gradientUnits", + hanging: "hanging", + height: "height", + href: "href", + hreflang: "hreflang", + horiz_adv_x: "horiz-adv-x", + horiz_origin_x: "horiz-origin-x", + id: "id", + ideographic: "ideographic", + image_rendering: "image-rendering", + _in: "_in", + in2: "in2", + intercept: "intercept", + k: "k", + k1: "k1", + k2: "k2", + k3: "k3", + k4: "k4", + kernelMatrix: "kernelMatrix", + kernelUnitLength: "kernelUnitLength", + kerning: "kerning", + keyPoints: "keyPoints", + keySplines: "keySplines", + keyTimes: "keyTimes", + lang: "lang", + lengthAdjust: "lengthAdjust", + letter_spacing: "letter-spacing", + lighting_color: "lighting-color", + limitingConeAngle: "limitingConeAngle", + local: "local", + marker_end: "marker-end", + marker_mid: "marker-mid", + marker_start: "marker_start", + markerHeight: "markerHeight", + markerUnits: "markerUnits", + markerWidth: "markerWidth", + mask: "mask", + maskContentUnits: "maskContentUnits", + maskUnits: "maskUnits", + mathematical: "mathematical", + max: "max", + media: "media", + method: "method", + min: "min", + mode: "mode", + name: "name", + numOctaves: "numOctaves", + offset: "offset", + opacity: "opacity", + operator: "operator", + order: "order", + orient: "orient", + orientation: "orientation", + origin: "origin", + overflow: "overflow", + overline_position: "overline-position", + overline_thickness: "overline-thickness", + panose_1: "panose-1", + paint_order: "paint-order", + path: "path", + pathLength: "pathLength", + patternContentUnits: "patternContentUnits", + patternTransform: "patternTransform", + patternUnits: "patternUnits", + ping: "ping", + pointer_events: "pointer-events", + points: "points", + pointsAtX: "pointsAtX", + pointsAtY: "pointsAtY", + pointsAtZ: "pointsAtZ", + preserveAlpha: "preserveAlpha", + preserveAspectRatio: "preserveAspectRatio", + primitiveUnits: "primitiveUnits", + r: "r", + radius: "radius", + referrerPolicy: "referrerPolicy", + refX: "refX", + refY: "refY", + rel: "rel", + rendering_intent: "rendering-intent", + repeatCount: "repeatCount", + repeatDur: "repeatDur", + requiredExtensions: "requiredExtensions", + requiredFeatures: "requiredFeatures", + restart: "restart", + result: "result", + role: "role", + rotate: "rotate", + rx: "rx", + ry: "ry", + scale: "scale", + seed: "seed", + shape_rendering: "shape-rendering", + slope: "slope", + spacing: "spacing", + specularConstant: "specularConstant", + specularExponent: "specularExponent", + speed: "speed", + spreadMethod: "spreadMethod", + startOffset: "startOffset", + stdDeviation: "stdDeviation", + stemh: "stemh", + stemv: "stemv", + stitchTiles: "stitchTiles", + stop_color: "stop_color", + stop_opacity: "stop_opacity", + strikethrough_position: "strikethrough-position", + strikethrough_thickness: "strikethrough-thickness", + string: "string", + stroke: "stroke", + stroke_dasharray: "stroke-dasharray", + stroke_dashoffset: "stroke-dashoffset", + stroke_linecap: "stroke-linecap", + stroke_linejoin: "stroke-linejoin", + stroke_miterlimit: "stroke-miterlimit", + stroke_opacity: "stroke-opacity", + stroke_width: "stroke-width", + style: "style", + surfaceScale: "surfaceScale", + systemLanguage: "systemLanguage", + tabindex: "tabindex", + tableValues: "tableValues", + target: "target", + targetX: "targetX", + targetY: "targetY", + text_anchor: "text-anchor", + text_decoration: "text-decoration", + text_rendering: "text-rendering", + textLength: "textLength", + to: "to", + transform: "transform", + transform_origin: "transform-origin", + r#type: "_type", + u1: "u1", + u2: "u2", + underline_position: "underline-position", + underline_thickness: "underline-thickness", + unicode: "unicode", + unicode_bidi: "unicode-bidi", + unicode_range: "unicode-range", + units_per_em: "units-per-em", + v_alphabetic: "v-alphabetic", + v_hanging: "v-hanging", + v_ideographic: "v-ideographic", + v_mathematical: "v-mathematical", + values: "values", + vector_effect: "vector-effect", + version: "version", + vert_adv_y: "vert-adv-y", + vert_origin_x: "vert-origin-x", + vert_origin_y: "vert-origin-y", + view_box: "viewBox", + view_target: "viewTarget", + visibility: "visibility", + width: "width", + widths: "widths", + word_spacing: "word-spacing", + writing_mode: "writing-mode", + x: "x", + x_height: "x-height", + x1: "x1", + x2: "x2", + xmlns: "xmlns", + x_channel_selector: "xChannelSelector", + y: "y", + y1: "y1", + y2: "y2", + y_channel_selector: "yChannelSelector", + z: "z", + zoomAndPan: "zoomAndPan", + } +} diff --git a/packages/rsx_interpreter/src/captuered_context.rs b/packages/rsx_interpreter/src/captuered_context.rs new file mode 100644 index 000000000..8473771e9 --- /dev/null +++ b/packages/rsx_interpreter/src/captuered_context.rs @@ -0,0 +1,176 @@ +use dioxus_core::{Listener, VNode}; +use dioxus_rsx::{ + BodyNode, CallBody, Component, ElementAttr, ElementAttrNamed, IfmtInput, Segment, +}; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{Expr, Ident, Result}; + +use crate::CodeLocation; +#[derive(Default)] +pub struct CapturedContextBuilder { + pub ifmted: Vec, + pub components: Vec, + pub iterators: Vec, + pub captured_expressions: Vec, + pub listeners: Vec, + pub custom_context: Option, +} + +impl CapturedContextBuilder { + pub fn extend(&mut self, other: CapturedContextBuilder) { + self.ifmted.extend(other.ifmted); + self.components.extend(other.components); + self.iterators.extend(other.iterators); + self.listeners.extend(other.listeners); + self.captured_expressions.extend(other.captured_expressions); + } + + pub fn from_call_body(body: CallBody) -> Result { + let mut new = Self { + custom_context: body.custom_context, + ..Default::default() + }; + for node in body.roots { + new.extend(Self::find_captured(node)?); + } + Ok(new) + } + + fn find_captured(node: BodyNode) -> Result { + let mut captured = CapturedContextBuilder::default(); + match node { + BodyNode::Element(el) => { + for attr in el.attributes { + match attr.attr { + ElementAttr::AttrText { value, .. } + | ElementAttr::CustomAttrText { value, .. } => { + let value_tokens = value.to_token_stream(); + let formated: IfmtInput = syn::parse2(value_tokens)?; + captured.ifmted.push(formated); + } + ElementAttr::AttrExpression { name: _, value } => { + captured.captured_expressions.push(value); + } + ElementAttr::CustomAttrExpression { name: _, value } => { + captured.captured_expressions.push(value); + } + ElementAttr::EventTokens { .. } => captured.listeners.push(attr), + } + } + + if let Some(key) = el.key { + let value_tokens = key.to_token_stream(); + let formated: IfmtInput = syn::parse2(value_tokens)?; + captured.ifmted.push(formated); + } + + for child in el.children { + captured.extend(Self::find_captured(child)?); + } + } + BodyNode::Component(comp) => { + captured.components.push(comp); + } + BodyNode::Text(t) => { + let tokens = t.to_token_stream(); + let formated: IfmtInput = syn::parse2(tokens).unwrap(); + captured.ifmted.push(formated); + } + BodyNode::RawExpr(_) => captured.iterators.push(node), + } + Ok(captured) + } +} + +impl ToTokens for CapturedContextBuilder { + fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) { + let CapturedContextBuilder { + ifmted, + components, + iterators, + captured_expressions, + listeners, + custom_context: _, + } = self; + let listeners_str = listeners + .iter() + .map(|comp| comp.to_token_stream().to_string()); + let compontents_str = components + .iter() + .map(|comp| comp.to_token_stream().to_string()); + let iterators_str = iterators.iter().map(|node| match node { + BodyNode::RawExpr(expr) => expr.to_token_stream().to_string(), + _ => unreachable!(), + }); + let captured: Vec<_> = ifmted + .iter() + .flat_map(|input| input.segments.iter()) + .filter_map(|seg| match seg { + Segment::Formatted { + format_args, + segment, + } => { + let expr = segment.to_token_stream(); + let as_string = expr.to_string(); + let format_expr = if format_args.is_empty() { + "{".to_string() + format_args + "}" + } else { + "{".to_string() + ":" + format_args + "}" + }; + Some(quote! { + FormattedArg{ + expr: #as_string, + format_args: #format_args, + result: format!(#format_expr, #expr) + } + }) + } + _ => None, + }) + .collect(); + let captured_attr_expressions_text = captured_expressions + .iter() + .map(|e| format!("{}", e.to_token_stream())); + tokens.append_all(quote! { + CapturedContext { + captured: IfmtArgs{ + named_args: vec![#(#captured),*] + }, + components: vec![#((#compontents_str, #components)),*], + iterators: vec![#((#iterators_str, #iterators)),*], + expressions: vec![#((#captured_attr_expressions_text, #captured_expressions.to_string())),*], + listeners: vec![#((#listeners_str, #listeners)),*], + location: code_location.clone() + } + }) + } +} + +pub struct CapturedContext<'a> { + // map of the variable name to the formated value + pub captured: IfmtArgs, + // map of the attribute name and element path to the formated value + // pub captured_attribute_values: IfmtArgs, + // the only thing we can update in component is the children + pub components: Vec<(&'static str, VNode<'a>)>, + // we can't reasonably interpert iterators, so they are staticly inserted + pub iterators: Vec<(&'static str, VNode<'a>)>, + // map expression to the value resulting from the expression + pub expressions: Vec<(&'static str, String)>, + // map listener code to the resulting listener + pub listeners: Vec<(&'static str, Listener<'a>)>, + // used to provide better error messages + pub location: CodeLocation, +} + +pub struct IfmtArgs { + // All expressions that have been resolved + pub named_args: Vec, +} + +/// A formated segment that has been resolved +pub struct FormattedArg { + pub expr: &'static str, + pub format_args: &'static str, + pub result: String, +} diff --git a/packages/rsx_interpreter/src/elements.rs b/packages/rsx_interpreter/src/elements.rs new file mode 100644 index 000000000..0668f57ff --- /dev/null +++ b/packages/rsx_interpreter/src/elements.rs @@ -0,0 +1,1377 @@ +// map the rsx name of the element to the html name of the element and the namespace that contains it +pub fn element_to_static_str(element: &str) -> Option<(&'static str, Option<&'static str>)> { + ELEMENTS_WITH_MAPPED_ATTRIBUTES + .iter() + .find(|(el, _)| *el == element) + .map(|(el, _)| (*el, None)) + .or_else(|| { + ELEMENTS_WITH_NAMESPACE + .iter() + .find(|(el, _, _)| *el == element) + .map(|(el, ns, _)| (*el, Some(*ns))) + }) + .or_else(|| { + ELEMENTS_WITHOUT_NAMESPACE + .iter() + .find(|(el, _)| *el == element) + .map(|(el, _)| (*el, None)) + }) +} + +macro_rules! builder_constructors { + ( + $( + $(#[$attr:meta])* + $name:ident { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident, + )* + }; + )* + ) => { + pub const ELEMENTS_WITHOUT_NAMESPACE: &'static [(&'static str, &'static [&'static str])] = &[ + $( + ( + stringify!($name), + &[ + $( + stringify!($fil), + )* + ] + ), + )* + ]; + }; + + ( $( + $(#[$attr:meta])* + $name:ident <> $namespace:tt { + $($fil:ident: $vil:ident,)* + }; + )* ) => { + pub const ELEMENTS_WITH_NAMESPACE: &'static [(&'static str, &'static str, &'static [&'static str])] = &[ + $( + ( + stringify!($name), + stringify!($namespace), + &[ + $( + stringify!($fil), + )* + ] + ), + )* + ]; + }; +} +pub const ELEMENTS_WITH_MAPPED_ATTRIBUTES: &[(&str, &[(&str, &str)])] = &[ + ("script", &[("r#type", "type"), ("r#script", "script")]), + ("button", &[("r#type", "type")]), + ("select", &[("value", "value")]), + ("option", &[("selected", "selected")]), + ("textarea", &[("value", "value")]), + ("label", &[("r#for", "for")]), + ("input", &[("r#type", "type"), ("value", "value")]), +]; + +// Organized in the same order as +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element +// +// Does not include obsolete elements. +// +// This namespace represents a collection of modern HTML-5 compatiable elements. +// +// This list does not include obsolete, deprecated, experimental, or poorly supported elements. +builder_constructors! { + // Document metadata + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) + /// element. + /// + base { + href: Uri, + target: Target, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) + /// element. + head {}; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) + /// element. + link { + // as: Mime, + crossorigin: CrossOrigin, + href: Uri, + hreflang: LanguageTag, + media: String, // FIXME media query + rel: LinkType, + sizes: String, // FIXME + title: String, // FIXME + r#type: Mime, + integrity: String, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) + /// element. + meta { + charset: String, // FIXME IANA standard names + content: String, + http_equiv: HTTPEquiv, + name: Metadata, + }; + + /// Build a + /// [`