From b6903bf558bc7a3d0fe6794a137c44fca0957d11 Mon Sep 17 00:00:00 2001 From: Alexandre Kirszenberg Date: Sat, 8 Jan 2022 15:32:53 +0100 Subject: [PATCH 01/79] Enable clippy --- .github/workflows/main.yml | 36 ++++++++++++------------ packages/core-macro/src/ifmt.rs | 2 +- packages/core-macro/src/props/mod.rs | 13 +-------- packages/core-macro/src/rsx/component.rs | 2 +- packages/core/src/scopes.rs | 1 + packages/desktop/src/cfg.rs | 10 ++++++- packages/hooks/src/use_shared_state.rs | 2 +- packages/hooks/src/usecoroutine.rs | 6 ++-- packages/hooks/src/useref.rs | 6 ++-- packages/hooks/src/usesuspense.rs | 2 +- packages/router/src/components/link.rs | 2 +- packages/router/src/components/route.rs | 4 +-- packages/router/src/lib.rs | 1 + packages/ssr/src/lib.rs | 9 +++--- 14 files changed, 46 insertions(+), 50 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 750dfb93b..765102dea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,21 +55,21 @@ jobs: command: fmt args: --all -- --check - # clippy: - # name: Clippy - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v2 - # - uses: actions-rs/toolchain@v1 - # with: - # profile: minimal - # toolchain: stable - # override: true - # - uses: Swatinem/rust-cache@v1 - # - run: rustup component add clippy - # - uses: actions-rs/cargo@v1 - # with: - # command: clippy - # args: -- -D warnings -# -# sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev libappindicator3-dev + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - uses: Swatinem/rust-cache@v1 + - run: sudo apt-get update + - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev libappindicator3-dev + - run: rustup component add clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings diff --git a/packages/core-macro/src/ifmt.rs b/packages/core-macro/src/ifmt.rs index 71b8bdee6..912d78f7e 100644 --- a/packages/core-macro/src/ifmt.rs +++ b/packages/core-macro/src/ifmt.rs @@ -41,7 +41,7 @@ pub fn format_args_f_impl(input: IfmtInput) -> Result { let mut out_format_literal = defer(&mut *out_format_literal, |it| { it.push(colon_or_closing_brace) }); - let out_format_literal: &mut String = &mut *out_format_literal; + 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!( diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index c2b667524..8e922ea8a 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -270,7 +270,7 @@ mod field_info { pub setter: SetterSettings, } - #[derive(Debug, Clone)] + #[derive(Debug, Clone, Default)] pub struct SetterSettings { pub doc: Option, pub skip: bool, @@ -278,17 +278,6 @@ mod field_info { pub strip_option: bool, } - impl Default for SetterSettings { - fn default() -> Self { - Self { - doc: Default::default(), - skip: false, - auto_into: false, - strip_option: false, - } - } - } - impl FieldBuilderAttr { pub fn with(mut self, attrs: &[syn::Attribute]) -> Result { let mut skip_tokens = None; diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 13bcd30d3..e8696504b 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -181,7 +181,7 @@ impl Parse for ComponentField { return Ok(Self { name, content }); } - if name.to_string() == "key" { + if name == "key" { let content = ContentField::ManExpr(input.parse()?); return Ok(Self { name, content }); } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index bd4dc34ff..65511470e 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -745,6 +745,7 @@ impl ScopeState { /// use_hook(|| Rc::new(RefCell::new(initial_value()))) /// } /// ``` + #[allow(clippy::mut_from_ref)] pub fn use_hook<'src, State: 'static>( &'src self, initializer: impl FnOnce(usize) -> State, diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 49d781465..60e3ff887 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -4,11 +4,13 @@ use wry::{ webview::WebView, }; +pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); + pub struct DesktopConfig<'a> { pub window: WindowBuilder, pub(crate) manual_edits: Option>>, pub(crate) pre_rendered: Option, - pub(crate) event_handler: Option, &mut WebView)>>, + pub(crate) event_handler: Option>, } impl<'a> DesktopConfig<'a> { @@ -55,3 +57,9 @@ impl<'a> DesktopConfig<'a> { self } } + +impl<'a> Default for DesktopConfig<'a> { + fn default() -> Self { + Self::new() + } +} diff --git a/packages/hooks/src/use_shared_state.rs b/packages/hooks/src/use_shared_state.rs index 3d25a5ac3..9b59601b5 100644 --- a/packages/hooks/src/use_shared_state.rs +++ b/packages/hooks/src/use_shared_state.rs @@ -171,7 +171,7 @@ where /// /// /// -pub fn use_context_provider<'a, T: 'static>(cx: &'a ScopeState, f: impl FnOnce() -> T) { +pub fn use_context_provider(cx: &ScopeState, f: impl FnOnce() -> T) { cx.use_hook(|_| { let state: ProvidedState = RefCell::new(ProvidedStateInner { value: Rc::new(RefCell::new(f())), diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index 737f3c2a5..7faf507ed 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -15,10 +15,7 @@ let g = use_coroutine(&cx, || { */ -pub fn use_coroutine<'a, F>( - cx: &'a ScopeState, - create_future: impl FnOnce() -> F, -) -> CoroutineHandle<'a> +pub fn use_coroutine(cx: &ScopeState, create_future: impl FnOnce() -> F) -> CoroutineHandle<'_> where F: Future + 'static, { @@ -98,6 +95,7 @@ impl Clone for CoroutineHandle<'_> { impl Copy for CoroutineHandle<'_> {} impl<'a> CoroutineHandle<'a> { + #[allow(clippy::needless_return)] pub fn start(&self) { if self.is_running() { return; diff --git a/packages/hooks/src/useref.rs b/packages/hooks/src/useref.rs index 9a5d9ffb9..3ddf86e8c 100644 --- a/packages/hooks/src/useref.rs +++ b/packages/hooks/src/useref.rs @@ -6,12 +6,10 @@ use std::{ use dioxus_core::ScopeState; pub fn use_ref<'a, T: 'static>(cx: &'a ScopeState, f: impl FnOnce() -> T) -> &'a UseRef { - let inner = cx.use_hook(|_| UseRef { + cx.use_hook(|_| UseRef { update_callback: cx.schedule_update(), value: Rc::new(RefCell::new(f())), - }); - - inner + }) } pub struct UseRef { diff --git a/packages/hooks/src/usesuspense.rs b/packages/hooks/src/usesuspense.rs index dfd37ac7a..834871304 100644 --- a/packages/hooks/src/usesuspense.rs +++ b/packages/hooks/src/usesuspense.rs @@ -27,7 +27,7 @@ pub fn use_suspense + 'static>( }); if let Some(value) = sus.value.as_ref() { - render(&value) + render(value) } else { // generate a placeholder node if the future isnt ready None diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 5a97c657c..417b77525 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -46,7 +46,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { id: format_args!("{}", cx.props.id.unwrap_or("")), prevent_default: "onclick", - onclick: move |_| service.push_route(cx.props.to.clone()), + onclick: move |_| service.push_route(cx.props.to), &cx.props.children } diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 2b687a868..2d9a270b8 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -27,8 +27,8 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { cx.use_hook(|_| { // create a bigger, better, longer route if one above us exists let total_route = match cx.consume_context::() { - Some(ctx) => format!("{}", ctx.total_route.clone()), - None => format!("{}", cx.props.to.clone()), + Some(ctx) => ctx.total_route.to_string(), + None => cx.props.to.to_string(), }; // provide our route context diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index 23d1c4a07..b6ffb5971 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(warnings)] //! Dioxus-Router //! //! A simple match-based router and router service for most routing needs. diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index 67537195c..bda95cd40 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -24,7 +24,7 @@ impl SsrRenderer { pub fn render_lazy<'a>(&'a mut self, f: LazyNodes<'a, '_>) -> String { let scope = self.vdom.base_scope(); - let factory = NodeFactory::new(&scope); + let factory = NodeFactory::new(scope); let root = f.into_vnode(factory); format!( @@ -38,6 +38,7 @@ impl SsrRenderer { } } +#[allow(clippy::needless_lifetimes)] pub fn render_lazy<'a>(f: LazyNodes<'a, '_>) -> String { let vdom = VirtualDom::new(app); let scope: *const ScopeState = vdom.base_scope(); @@ -50,11 +51,11 @@ pub fn render_lazy<'a>(f: LazyNodes<'a, '_>) -> String { // When LazyNodes are provided, they are FnOnce, but do not come with a allocator selected to borrow from. The <'a> // lifetime is therefore longer than the lifetime of the allocator which doesn't exist... yet. // - // Therefore, we cast our local bump alloactor into right lifetime. This is okay because our usage of the bump arena - // is *definitely* shorter than the <'a> lifetime, and we return *owned* data - not borrowed data. + // Therefore, we cast our local bump allocator to the right lifetime. This is okay because our usage of the bump + // arena is *definitely* shorter than the <'a> lifetime, and we return *owned* data - not borrowed data. let scope = unsafe { &*scope }; - let root = f.into_vnode(NodeFactory::new(&scope)); + let root = f.into_vnode(NodeFactory::new(scope)); format!( "{:}", From bc0d9ccab48f26e797b6a73645ae9afded37cff1 Mon Sep 17 00:00:00 2001 From: Alexandre Kirszenberg Date: Sat, 8 Jan 2022 19:05:11 +0100 Subject: [PATCH 02/79] Remove duplicate libappindicator3 --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 765102dea..b941960b7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: override: true - uses: Swatinem/rust-cache@v1 - run: sudo apt-get update - - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev libappindicator3-dev + - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev - uses: actions-rs/cargo@v1 with: command: check @@ -32,7 +32,7 @@ jobs: override: true - uses: Swatinem/rust-cache@v1 - run: sudo apt-get update - - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev libappindicator3-dev + - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev - uses: actions-rs/cargo@v1 with: command: test @@ -67,7 +67,7 @@ jobs: override: true - uses: Swatinem/rust-cache@v1 - run: sudo apt-get update - - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev libappindicator3-dev + - run: sudo apt install libwebkit2gtk-4.0-dev libappindicator3-dev libgtk-3-dev - run: rustup component add clippy - uses: actions-rs/cargo@v1 with: From 90abd9c9a08c165384fae9c1f7c3fd098d512c48 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 10 Jan 2022 01:20:03 -0500 Subject: [PATCH 03/79] fix: detection of f-string formatting in components --- packages/core-macro/src/rsx/component.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 13bcd30d3..5257022a4 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -181,7 +181,7 @@ impl Parse for ComponentField { return Ok(Self { name, content }); } - if name.to_string() == "key" { + if name == "key" { let content = ContentField::ManExpr(input.parse()?); return Ok(Self { name, content }); } @@ -221,7 +221,7 @@ fn is_literal_foramtted(lit: &LitStr) -> bool { while let Some(next) = chars.next() { if next == '{' { let nen = chars.next(); - if nen == Some('{') { + if nen != Some('{') { return true; } } From 255f58af63e002339b02bf91c5b85cc5ec917428 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 10 Jan 2022 01:32:32 -0500 Subject: [PATCH 04/79] fix: ssr respects bool attrs --- packages/ssr/src/lib.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index bda95cd40..b473e7fa8 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -187,6 +187,36 @@ impl<'a> TextRenderer<'a, '_> { match attr.namespace { None => match attr.name { "dangerous_inner_html" => inner_html = Some(attr.value), + "allowfullscreen" + | "allowpaymentrequest" + | "async" + | "autofocus" + | "autoplay" + | "checked" + | "controls" + | "default" + | "defer" + | "disabled" + | "formnovalidate" + | "hidden" + | "ismap" + | "itemscope" + | "loop" + | "multiple" + | "muted" + | "nomodule" + | "novalidate" + | "open" + | "playsinline" + | "readonly" + | "required" + | "reversed" + | "selected" + | "truespeed" => { + if attr.value != "false" { + write!(f, " {}=\"{}\"", attr.name, attr.value)? + } + } _ => write!(f, " {}=\"{}\"", attr.name, attr.value)?, }, From d2372717bd01fcff50af0572360e3f763d4c869d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 10 Jan 2022 02:57:03 -0500 Subject: [PATCH 05/79] feat: flatten props attrs --- examples/optional_props.rs | 37 ++++++ packages/core-macro/src/props/mod.rs | 152 ++++++----------------- packages/router/src/components/link.rs | 6 +- packages/router/src/components/router.rs | 2 +- 4 files changed, 79 insertions(+), 118 deletions(-) create mode 100644 examples/optional_props.rs diff --git a/examples/optional_props.rs b/examples/optional_props.rs new file mode 100644 index 000000000..d2b9eda10 --- /dev/null +++ b/examples/optional_props.rs @@ -0,0 +1,37 @@ +//! Example: README.md showcase +//! +//! The example from the README.md. + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + Button { + a: "asd".to_string(), + c: Some("asd".to_string()), + d: "asd".to_string(), + } + }) +} + +#[derive(Props, PartialEq)] +struct ButtonProps { + a: String, + + #[props(default)] + b: Option, + + #[props(default)] + c: Option, + + #[props(default)] + d: Option, +} + +fn Button(cx: Scope) -> Element { + todo!() +} diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 8e922ea8a..f5443272b 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -267,11 +267,6 @@ mod field_info { #[derive(Debug, Default, Clone)] pub struct FieldBuilderAttr { pub default: Option, - pub setter: SetterSettings, - } - - #[derive(Debug, Clone, Default)] - pub struct SetterSettings { pub doc: Option, pub skip: bool, pub auto_into: bool, @@ -305,12 +300,12 @@ mod field_info { } } // Stash its span for later (we don’t yet know if it’ll be an error) - if self.setter.skip && skip_tokens.is_none() { + if self.skip && skip_tokens.is_none() { skip_tokens = Some(attr.tokens.clone()); } } - if self.setter.skip && self.default.is_none() { + if self.skip && self.default.is_none() { return Err(Error::new_spanned( skip_tokens.unwrap(), "#[props(skip)] must be accompanied by default or default_code", @@ -331,6 +326,10 @@ mod field_info { self.default = Some(*assign.right); Ok(()) } + "doc" => { + self.doc = Some(*assign.right); + Ok(()) + } "default_code" => { if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(code), @@ -355,7 +354,7 @@ mod field_info { } } - // uh not sure + // #[props(default)] syn::Expr::Path(path) => { let name = path_to_single_string(&path.path) .ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?; @@ -365,39 +364,33 @@ mod field_info { Some(syn::parse(quote!(Default::default()).into()).unwrap()); Ok(()) } - _ => Err(Error::new_spanned( - &path, - format!("Unknown parameter {:?}", name), - )), - } - } - - // - syn::Expr::Call(call) => { - let subsetting_name = if let syn::Expr::Path(path) = &*call.func { - path_to_single_string(&path.path) - } else { - None - } - .ok_or_else(|| { - let call_func = &call.func; - let call_func = quote!(#call_func); - Error::new_spanned( - &call.func, - format!("Illegal builder setting group {}", call_func), - ) - })?; - match subsetting_name.as_ref() { - "setter" => { - for arg in call.args { - self.setter.apply_meta(arg)?; + _ => { + macro_rules! handle_fields { + ( $( $flag:expr, $field:ident, $already:expr; )* ) => { + match name.as_str() { + $( + $flag => { + if self.$field { + Err(Error::new(path.span(), concat!("Illegal setting - field is already ", $already))) + } else { + self.$field = true; + Ok(()) + } + } + )* + _ => Err(Error::new_spanned( + &path, + format!("Unknown setter parameter {:?}", name), + )) + } + } } - Ok(()) + handle_fields!( + "skip", skip, "skipped"; + "into", auto_into, "calling into() on the argument"; + "strip_option", strip_option, "putting the argument in Some(...)"; + ) } - _ => Err(Error::new_spanned( - &call.func, - format!("Illegal builder setting group name {}", subsetting_name), - )), } } @@ -414,75 +407,6 @@ mod field_info { self.default = None; Ok(()) } - _ => Err(Error::new_spanned(path, "Unknown setting".to_owned())), - } - } else { - Err(Error::new_spanned( - expr, - "Expected simple identifier".to_owned(), - )) - } - } - _ => Err(Error::new_spanned(expr, "Expected (<...>=<...>)")), - } - } - } - - impl SetterSettings { - fn apply_meta(&mut self, expr: syn::Expr) -> Result<(), Error> { - match expr { - syn::Expr::Assign(assign) => { - let name = expr_to_single_string(&assign.left) - .ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?; - match name.as_str() { - "doc" => { - self.doc = Some(*assign.right); - Ok(()) - } - _ => Err(Error::new_spanned( - &assign, - format!("Unknown parameter {:?}", name), - )), - } - } - syn::Expr::Path(path) => { - let name = path_to_single_string(&path.path) - .ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?; - macro_rules! handle_fields { - ( $( $flag:expr, $field:ident, $already:expr; )* ) => { - match name.as_str() { - $( - $flag => { - if self.$field { - Err(Error::new(path.span(), concat!("Illegal setting - field is already ", $already))) - } else { - self.$field = true; - Ok(()) - } - } - )* - _ => Err(Error::new_spanned( - &path, - format!("Unknown setter parameter {:?}", name), - )) - } - } - } - handle_fields!( - "skip", skip, "skipped"; - "into", auto_into, "calling into() on the argument"; - "strip_option", strip_option, "putting the argument in Some(...)"; - ) - } - syn::Expr::Unary(syn::ExprUnary { - op: syn::UnOp::Not(_), - expr, - .. - }) => { - if let syn::Expr::Path(path) = *expr { - let name = path_to_single_string(&path.path) - .ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?; - match name.as_str() { "doc" => { self.doc = None; Ok(()) @@ -540,7 +464,7 @@ mod struct_info { impl<'a> StructInfo<'a> { pub fn included_fields(&self) -> impl Iterator> { - self.fields.iter().filter(|f| !f.builder_attr.setter.skip) + self.fields.iter().filter(|f| !f.builder_attr.skip) } pub fn new( @@ -826,14 +750,14 @@ Finally, call `.build()` to create the instance of `{name}`. syn::GenericArgument::Type(ty_generics_tuple.into()), ); let (impl_generics, _, where_clause) = generics.split_for_impl(); - let doc = match field.builder_attr.setter.doc { + let doc = match field.builder_attr.doc { Some(ref doc) => quote!(#[doc = #doc]), None => quote!(), }; // NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of // nesting is different so we have to do this little dance. - let arg_type = if field.builder_attr.setter.strip_option { + let arg_type = if field.builder_attr.strip_option { let internal_type = field.type_from_inside_option().ok_or_else(|| { Error::new_spanned( &field_type, @@ -844,7 +768,7 @@ Finally, call `.build()` to create the instance of `{name}`. } else { field_type }; - let (arg_type, arg_expr) = if field.builder_attr.setter.auto_into { + let (arg_type, arg_expr) = if field.builder_attr.auto_into { ( quote!(impl core::convert::Into<#arg_type>), quote!(#field_name.into()), @@ -852,7 +776,7 @@ Finally, call `.build()` to create the instance of `{name}`. } else { (quote!(#arg_type), quote!(#field_name)) }; - let arg_expr = if field.builder_attr.setter.strip_option { + let arg_expr = if field.builder_attr.strip_option { quote!(Some(#arg_expr)) } else { arg_expr @@ -1076,7 +1000,7 @@ Finally, call `.build()` to create the instance of `{name}`. let assignments = self.fields.iter().map(|field| { let name = &field.name; if let Some(ref default) = field.builder_attr.default { - if field.builder_attr.setter.skip { + if field.builder_attr.skip { quote!(let #name = #default;) } else { quote!(let #name = #helper_trait_name::into_value(#name, || #default);) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 417b77525..634696bf4 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -22,13 +22,13 @@ pub struct LinkProps<'a> { /// Link { to: Route::Home, href: Route::as_url } /// /// ``` - #[props(default, setter(strip_option))] + #[props(default, strip_option)] href: Option<&'a str>, - #[props(default, setter(strip_option))] + #[props(default, strip_option)] class: Option<&'a str>, - #[props(default, setter(strip_option))] + #[props(default, strip_option)] id: Option<&'a str>, children: Element<'a>, diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 90c101fa8..85f0a62a3 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -11,7 +11,7 @@ use crate::RouterService; pub struct RouterProps<'a> { children: Element<'a>, - #[props(default, setter(strip_option))] + #[props(default, strip_option)] onchange: Option<&'a Fn(&'a str)>, } From c8535fb18451dcc3a39f5284dea68c5453114f29 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 10 Jan 2022 03:05:49 -0500 Subject: [PATCH 06/79] fix: example bugged out --- examples/optional_props.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/optional_props.rs b/examples/optional_props.rs index d2b9eda10..47ccbeac0 100644 --- a/examples/optional_props.rs +++ b/examples/optional_props.rs @@ -28,7 +28,7 @@ struct ButtonProps { #[props(default)] c: Option, - #[props(default)] + #[props(default, strip_option)] d: Option, } From 9849f68f257200fac511c048bfb1a076243b86d3 Mon Sep 17 00:00:00 2001 From: Sven Assmann Date: Mon, 10 Jan 2022 14:46:23 +0100 Subject: [PATCH 07/79] feat(example:todomvc): add editing support - implement "onfocusout" handler - set autofocus - handle enter, escape and tab on a todo item - make the `
` element between todo list items of margin 0 to be unnoticable
---
 examples/assets/todomvc.css    |  2 +-
 examples/todomvc.rs            | 26 +++++++++++++++++++-------
 packages/desktop/src/events.rs |  3 ++-
 packages/html/src/events.rs    |  5 ++++-
 packages/web/src/cache.rs      |  1 +
 5 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/examples/assets/todomvc.css b/examples/assets/todomvc.css
index f9076525c..c928424af 100644
--- a/examples/assets/todomvc.css
+++ b/examples/assets/todomvc.css
@@ -1,5 +1,5 @@
 html,
-body {
+body, pre {
     margin: 0;
     padding: 0;
 }
diff --git a/examples/todomvc.rs b/examples/todomvc.rs
index b41adc5f0..8adb01d34 100644
--- a/examples/todomvc.rs
+++ b/examples/todomvc.rs
@@ -116,8 +116,12 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
     let todo = &cx.props.todos[&cx.props.id];
     let is_editing = use_state(&cx, || false);
     let completed = if todo.checked { "completed" } else { "" };
+    let editing = if *is_editing.get() { "editing" } else { "" };
 
-    rsx!(cx, li { class: "{completed}",
+    rsx!(cx, li {
+        class: "{completed} {editing}",
+        onclick: move |_| is_editing.set(true),
+        onfocusout: move |_| is_editing.set(false),
         div { class: "view",
             input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}",
                 onchange: move |evt| {
@@ -125,12 +129,20 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
                 }
             }
             label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" }
-            is_editing.then(|| rsx!{
-                input {
-                    value: "{todo.contents}",
-                    oninput: move |evt| cx.props.todos.modify()[&cx.props.id].contents = evt.value.clone(),
-                }
-            })
         }
+        is_editing.then(|| rsx!{
+            input {
+                class: "edit",
+                value: "{todo.contents}",
+                oninput: move |evt| cx.props.todos.modify()[&cx.props.id].contents = evt.value.clone(),
+                autofocus: "true",
+                onkeydown: move |evt| {
+                    match evt.key.as_str() {
+                        "Enter" | "Escape" | "Tab" => is_editing.set(false),
+                        _ => {}
+                    }
+                },
+            }
+        })
     })
 }
diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs
index 026b543c8..cd860fa8a 100644
--- a/packages/desktop/src/events.rs
+++ b/packages/desktop/src/events.rs
@@ -52,7 +52,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc(val).unwrap();
             Arc::new(evt)
         }
-        "focus" | "blur" => {
+        "focus" | "blur" | "focusout" => {
             //
             Arc::new(FocusData {})
         }
@@ -117,6 +117,7 @@ fn event_name_from_typ(typ: &str) -> &'static str {
         "keypress" => "keypress",
         "keyup" => "keyup",
         "focus" => "focus",
+        "focusout" => "focusout",
         "blur" => "blur",
         "change" => "change",
         "input" => "input",
diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs
index 7e5e95372..b2ed3fcb3 100644
--- a/packages/html/src/events.rs
+++ b/packages/html/src/events.rs
@@ -91,6 +91,9 @@ pub mod on {
             /// onfocus
             onfocus
 
+            // onfocusout
+            onfocusout
+
             /// onblur
             onblur
         ];
@@ -1092,7 +1095,7 @@ pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) {
         "keydown" | "keypress" | "keyup" => (true, High),
 
         // Focus
-        "focus" | "blur" => (true, Low),
+        "focus" | "blur" | "focusout" => (true, Low),
 
         // Form
         "change" | "input" | "invalid" | "reset" | "submit" => (true, Medium),
diff --git a/packages/web/src/cache.rs b/packages/web/src/cache.rs
index 8beb58f18..d01243644 100644
--- a/packages/web/src/cache.rs
+++ b/packages/web/src/cache.rs
@@ -203,6 +203,7 @@ pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[
     "onended",
     "onerror",
     "onfocus",
+    "onfocusout",
     "onhashchange",
     "oninput",
     "oninvalid",

From be614e6535e6e13e6ff93e9c6a171c1c002e6b01 Mon Sep 17 00:00:00 2001
From: Jonathan Kelley 
Date: Mon, 10 Jan 2022 12:04:54 -0500
Subject: [PATCH 08/79] fix: cursor jumping  and use set instead of lsit

---
 packages/desktop/src/index.js | 62 ++++++++++++++++++-----------------
 1 file changed, 32 insertions(+), 30 deletions(-)

diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js
index 75b838b02..dfa11bf67 100644
--- a/packages/desktop/src/index.js
+++ b/packages/desktop/src/index.js
@@ -1,31 +1,31 @@
-const bool_attrs = [
-  "allowfullscreen",
-  "allowpaymentrequest",
-  "async",
-  "autofocus",
-  "autoplay",
-  "checked",
-  "controls",
-  "default",
-  "defer",
-  "disabled",
-  "formnovalidate",
-  "hidden",
-  "ismap",
-  "itemscope",
-  "loop",
-  "multiple",
-  "muted",
-  "nomodule",
-  "novalidate",
-  "open",
-  "playsinline",
-  "readonly",
-  "required",
-  "reversed",
-  "selected",
-  "truespeed",
-]
+const bool_attrs = {
+  allowfullscreen: true,
+  allowpaymentrequest: true,
+  async: true,
+  autofocus: true,
+  autoplay: true,
+  checked: true,
+  controls: true,
+  default: true,
+  defer: true,
+  disabled: true,
+  formnovalidate: true,
+  hidden: true,
+  ismap: true,
+  itemscope: true,
+  loop: true,
+  multiple: true,
+  muted: true,
+  nomodule: true,
+  novalidate: true,
+  open: true,
+  playsinline: true,
+  readonly: true,
+  required: true,
+  reversed: true,
+  selected: true,
+  truespeed: true,
+};
 
 function serialize_event(event) {
   switch (event.type) {
@@ -386,7 +386,9 @@ class Interpreter {
     } else {
       switch (name) {
         case "value":
-          node.value = value;
+          if (value != node.value) {
+            node.value = value;
+          }
           break;
         case "checked":
           node.checked = value === "true";
@@ -399,7 +401,7 @@ class Interpreter {
           break;
         default:
           // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
-          if (value == "false" && bool_attrs.indexOf(name)) {
+          if (value == "false" && bool_attrs.hasOwnProperty(name)) {
             node.removeAttribute(name);
           } else {
             node.setAttribute(name, value);

From eb138848ec7a8978f0ed7c717374684d2315dc03 Mon Sep 17 00:00:00 2001
From: Jonathan Kelley 
Date: Mon, 10 Jan 2022 12:06:55 -0500
Subject: [PATCH 09/79] feat: also hide placeholder node

---
 packages/desktop/src/index.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js
index dfa11bf67..88cf9fcdc 100644
--- a/packages/desktop/src/index.js
+++ b/packages/desktop/src/index.js
@@ -321,6 +321,7 @@ class Interpreter {
 
   CreatePlaceholder(edit) {
     let el = document.createElement("pre");
+    el.hidden = true;
     this.stack.push(el);
     this.nodes[edit.root] = el;
   }

From 007d06d602f1adfaa51c87ec89b2afe90d8cdef9 Mon Sep 17 00:00:00 2001
From: Sven Assmann 
Date: Mon, 10 Jan 2022 18:08:29 +0100
Subject: [PATCH 10/79] feat(events:focus): add missing `onfocusin` event

---
 packages/desktop/src/events.rs | 3 ++-
 packages/html/src/events.rs    | 5 ++++-
 packages/web/src/cache.rs      | 1 +
 3 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs
index cd860fa8a..760bf7662 100644
--- a/packages/desktop/src/events.rs
+++ b/packages/desktop/src/events.rs
@@ -52,7 +52,7 @@ fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc(val).unwrap();
             Arc::new(evt)
         }
-        "focus" | "blur" | "focusout" => {
+        "focus" | "blur" | "focusout" | "focusin" => {
             //
             Arc::new(FocusData {})
         }
@@ -118,6 +118,7 @@ fn event_name_from_typ(typ: &str) -> &'static str {
         "keyup" => "keyup",
         "focus" => "focus",
         "focusout" => "focusout",
+        "focusin" => "focusin",
         "blur" => "blur",
         "change" => "change",
         "input" => "input",
diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs
index b2ed3fcb3..b9bcecd4f 100644
--- a/packages/html/src/events.rs
+++ b/packages/html/src/events.rs
@@ -94,6 +94,9 @@ pub mod on {
             // onfocusout
             onfocusout
 
+            // onfocusin
+            onfocusin
+
             /// onblur
             onblur
         ];
@@ -1095,7 +1098,7 @@ pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) {
         "keydown" | "keypress" | "keyup" => (true, High),
 
         // Focus
-        "focus" | "blur" | "focusout" => (true, Low),
+        "focus" | "blur" | "focusout" | "focusin" => (true, Low),
 
         // Form
         "change" | "input" | "invalid" | "reset" | "submit" => (true, Medium),
diff --git a/packages/web/src/cache.rs b/packages/web/src/cache.rs
index d01243644..190b505c1 100644
--- a/packages/web/src/cache.rs
+++ b/packages/web/src/cache.rs
@@ -204,6 +204,7 @@ pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[
     "onerror",
     "onfocus",
     "onfocusout",
+    "onfocusin",
     "onhashchange",
     "oninput",
     "oninvalid",

From 9ae981a1af4b5474ce16e27e070794d59128c12a Mon Sep 17 00:00:00 2001
From: Jonathan Kelley 
Date: Mon, 10 Jan 2022 12:57:39 -0500
Subject: [PATCH 11/79] wip: drag and drop support

---
 examples/filedragdrop.rs    | 18 +++++++++++++
 examples/manual_edits.rs    | 39 ----------------------------
 packages/desktop/src/cfg.rs | 48 ++++++++++++++++++++++++----------
 packages/desktop/src/lib.rs | 51 +++++++++++++++++++++++--------------
 4 files changed, 85 insertions(+), 71 deletions(-)
 create mode 100644 examples/filedragdrop.rs
 delete mode 100644 examples/manual_edits.rs

diff --git a/examples/filedragdrop.rs b/examples/filedragdrop.rs
new file mode 100644
index 000000000..a36b64ddb
--- /dev/null
+++ b/examples/filedragdrop.rs
@@ -0,0 +1,18 @@
+use dioxus::prelude::*;
+
+fn main() {
+    dioxus::desktop::launch_with_props(app, (), |c| {
+        c.with_file_drop_handler(|w, e| {
+            println!("{:?}", e);
+            false
+        })
+    });
+}
+
+fn app(cx: Scope) -> Element {
+    cx.render(rsx!(
+        div {
+            h1 { "drag an file here" }
+        }
+    ))
+}
diff --git a/examples/manual_edits.rs b/examples/manual_edits.rs
deleted file mode 100644
index c6486919a..000000000
--- a/examples/manual_edits.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
-Example: Manual Edits
-
-It's possible to manually provide a stream of DomEdits to a Dioxus Renderer. All renderers are designed to accept a stream
-of DomEdits that abstract over a stack machine. This allows the VirtualDOM to exist entirely separately from the RealDOM,
-though features like NodeRefs and NativeEvents might not work properly everywhere.
-*/
-
-use dioxus::core::*;
-use dioxus::prelude::*;
-
-fn main() {
-    use DomEdit::*;
-
-    let edits = vec![
-        // create a container and push it onto the stack
-        CreateElement {
-            tag: "div",
-            root: 0,
-        },
-        // create an element and push it onto the stack
-        CreateElement { tag: "h1", root: 2 },
-        // create a text node and push it onto the stack
-        CreateTextNode {
-            text: "hello world",
-            root: 3,
-        },
-        // append the text node to the h1 element
-        AppendChildren { many: 1 },
-        // append the h1 element to the container
-        AppendChildren { many: 1 },
-        // append the container to the default render element ("dioxusroot" if used with default config)
-        AppendChildren { many: 1 },
-    ];
-
-    let app: Component = |cx| cx.render(rsx!(div { "some app" }));
-
-    dioxus_desktop::launch_with_props(app, (), |c| c.with_edits(edits));
-}
diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs
index 60e3ff887..ad85cfc0f 100644
--- a/packages/desktop/src/cfg.rs
+++ b/packages/desktop/src/cfg.rs
@@ -1,19 +1,29 @@
-use dioxus_core::DomEdit;
 use wry::{
-    application::{event_loop::EventLoop, window::WindowBuilder},
-    webview::WebView,
+    application::{
+        event_loop::EventLoop,
+        window::{Window, WindowBuilder},
+    },
+    http::{Request as HttpRequest, Response as HttpResponse},
+    webview::{FileDropEvent, WebView},
+    Result as WryResult,
 };
 
 pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
 
-pub struct DesktopConfig<'a> {
+pub struct DesktopConfig {
     pub window: WindowBuilder,
-    pub(crate) manual_edits: Option>>,
+    pub file_drop_handler: Option bool>>,
+    pub protocos: Vec,
     pub(crate) pre_rendered: Option,
     pub(crate) event_handler: Option>,
 }
 
-impl<'a> DesktopConfig<'a> {
+pub type WryProtocl = (
+    String,
+    Box WryResult + 'static>,
+);
+
+impl DesktopConfig {
     /// Initializes a new `WindowBuilder` with default values.
     #[inline]
     pub fn new() -> Self {
@@ -21,16 +31,12 @@ impl<'a> DesktopConfig<'a> {
         Self {
             event_handler: None,
             window,
+            protocos: Vec::new(),
+            file_drop_handler: None,
             pre_rendered: None,
-            manual_edits: None,
         }
     }
 
-    pub fn with_edits(&mut self, edits: Vec>) -> &mut Self {
-        self.manual_edits = Some(edits);
-        self
-    }
-
     pub fn with_prerendered(&mut self, content: String) -> &mut Self {
         self.pre_rendered = Some(content);
         self
@@ -56,9 +62,25 @@ impl<'a> DesktopConfig<'a> {
         self.event_handler = Some(Box::new(handler));
         self
     }
+
+    pub fn with_file_drop_handler(
+        &mut self,
+        handler: impl Fn(&Window, FileDropEvent) -> bool + 'static,
+    ) -> &mut Self {
+        self.file_drop_handler = Some(Box::new(handler));
+        self
+    }
+
+    pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self
+    where
+        F: Fn(&HttpRequest) -> WryResult + 'static,
+    {
+        self.protocos.push((name, Box::new(handler)));
+        self
+    }
 }
 
-impl<'a> Default for DesktopConfig<'a> {
+impl Default for DesktopConfig {
     fn default() -> Self {
         Self::new()
     }
diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs
index 62270290e..57b859140 100644
--- a/packages/desktop/src/lib.rs
+++ b/packages/desktop/src/lib.rs
@@ -116,7 +116,7 @@ pub fn launch(root: Component) {
 /// ```
 pub fn launch_cfg(
     root: Component,
-    config_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
+    config_builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig,
 ) {
     launch_with_props(root, (), config_builder)
 }
@@ -147,7 +147,7 @@ pub fn launch_cfg(
 pub fn launch_with_props(
     root: Component

, props: P, - builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>, + builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig, ) { let mut cfg = DesktopConfig::new(); builder(&mut cfg); @@ -170,7 +170,9 @@ pub fn launch_with_props( let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone()); let proxy = proxy.clone(); - let webview = WebViewBuilder::new(window) + let file_handler = cfg.file_drop_handler.take(); + + let mut webview = WebViewBuilder::new(window) .unwrap() .with_url("wry://index.html/") .unwrap() @@ -189,26 +191,37 @@ pub fn launch_with_props( } None }) - .with_custom_protocol("wry".into(), move |request| { - // Any content that that uses the `wry://` scheme will be shuttled through this handler as a "special case" + .with_custom_protocol("dioxus".into(), move |request| { + // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" // For now, we only serve two pieces of content which get included as bytes into the final binary. - let path = request.uri().replace("wry://", ""); - let (data, meta) = match path.as_str() { - "index.html" | "index.html/" | "/index.html" => { - (include_bytes!("./index.html").to_vec(), "text/html") - } - "index.html/index.js" => { - (include_bytes!("./index.js").to_vec(), "text/javascript") - } - _ => (include_bytes!("./index.html").to_vec(), "text/html"), - }; + let path = request.uri().replace("dioxus://", ""); - wry::http::ResponseBuilder::new().mimetype(meta).body(data) + if path.trim_end_matches("/") == "index.html" { + wry::http::ResponseBuilder::new() + .mimetype("text/html") + .body(include_bytes!("./index.html").to_vec()) + } else if path == "index.js" { + wry::http::ResponseBuilder::new() + .mimetype("text/javascript") + .body(include_bytes!("./index.js").to_vec()) + } else { + wry::http::ResponseBuilder::new() + .status(wry::http::status::StatusCode::NOT_FOUND) + .body(format!("Not found: {}", path).as_bytes().to_vec()) + } }) - .build() - .unwrap(); + .with_file_drop_handler(move |window, evet| { + if let Some(handler) = file_handler.as_ref() { + return handler(window, evet); + } + false + }); - desktop.webviews.insert(window_id, webview); + for (name, handler) in cfg.protocos.drain(..) { + webview = webview.with_custom_protocol(name, handler) + } + + desktop.webviews.insert(window_id, webview.build().unwrap()); } Event::WindowEvent { From bad36162af764291f5a031b6233d151f61d745a4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 8 Jan 2022 08:23:26 -0500 Subject: [PATCH 12/79] fix: wry pathing --- packages/desktop/src/index.html | 2 +- packages/desktop/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 5228818bd..a67432e98 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -15,4 +15,4 @@ - \ No newline at end of file + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 57b859140..e700dfc78 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -174,7 +174,7 @@ pub fn launch_with_props( let mut webview = WebViewBuilder::new(window) .unwrap() - .with_url("wry://index.html/") + .with_url("dioxus://index.html/") .unwrap() .with_rpc_handler(move |_window: &Window, req: RpcRequest| { match req.method.as_str() { @@ -200,7 +200,7 @@ pub fn launch_with_props( wry::http::ResponseBuilder::new() .mimetype("text/html") .body(include_bytes!("./index.html").to_vec()) - } else if path == "index.js" { + } else if path.trim_end_matches("/") == "index.html/index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") .body(include_bytes!("./index.js").to_vec()) From 6bc45b1c5064a4e2b04d452c52a8167ad179691e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 10 Jan 2022 13:00:23 -0500 Subject: [PATCH 13/79] fix: clippy --- packages/desktop/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index e700dfc78..0b6044761 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -196,11 +196,11 @@ pub fn launch_with_props( // For now, we only serve two pieces of content which get included as bytes into the final binary. let path = request.uri().replace("dioxus://", ""); - if path.trim_end_matches("/") == "index.html" { + if path.trim_end_matches('/') == "index.html" { wry::http::ResponseBuilder::new() .mimetype("text/html") .body(include_bytes!("./index.html").to_vec()) - } else if path.trim_end_matches("/") == "index.html/index.js" { + } else if path.trim_end_matches('/') == "index.html/index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") .body(include_bytes!("./index.js").to_vec()) From 79c39c164d61ffb734b05dba56eaf8c61322fda3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20B=C3=A4dorf?= Date: Tue, 11 Jan 2022 17:43:23 +0000 Subject: [PATCH 14/79] Fix typo in docs --- docs/guide/src/setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 9b49f6cb5..223150521 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -38,7 +38,7 @@ For windows users: download the [bootstrapper for Webview2 from Microsoft](https For linux users, we need the development libraries for libgtk. -```` +``` sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev ``` From 388acf3277c25301da78411c09845f993f9ea9db Mon Sep 17 00:00:00 2001 From: yuniruyuni Date: Wed, 12 Jan 2022 03:09:25 +0900 Subject: [PATCH 15/79] docs: allow build core_reference/children example It needs lifetime parameters. --- examples/core_reference/children.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/core_reference/children.rs b/examples/core_reference/children.rs index 1b922b322..1a1bee316 100644 --- a/examples/core_reference/children.rs +++ b/examples/core_reference/children.rs @@ -32,11 +32,11 @@ fn Example(cx: Scope) -> Element { } #[derive(Props)] -struct BannerProps { +struct BannerProps<'a> { children: Element<'a>, } -fn Banner(cx: Scope) -> Element { +fn Banner<'a>(cx: Scope<'a, BannerProps<'a>>) -> Element<'a> { cx.render(rsx! { div { h1 { "This is a great banner!" } From 5bf6c96f9fed04de949403202bafcbeadb5d2030 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 12 Jan 2022 07:34:37 -0500 Subject: [PATCH 16/79] feat: setup a typescript build --- packages/desktop/src/index.html | 5 +- packages/desktop/src/index.js | 446 ------------------ packages/desktop/src/lib.rs | 2 +- packages/jsinterpreter/interpreter.js | 461 +++++++++++++++++++ packages/jsinterpreter/interpreter.ts | 636 ++++++++++++++++++++++++++ packages/jsinterpreter/tsconfig.json | 10 + 6 files changed, 1111 insertions(+), 449 deletions(-) delete mode 100644 packages/desktop/src/index.js create mode 100644 packages/jsinterpreter/interpreter.js create mode 100644 packages/jsinterpreter/interpreter.ts create mode 100644 packages/jsinterpreter/tsconfig.json diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index a67432e98..888ab8437 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -12,7 +12,8 @@

- + + + diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js deleted file mode 100644 index 88cf9fcdc..000000000 --- a/packages/desktop/src/index.js +++ /dev/null @@ -1,446 +0,0 @@ -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; - -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": - return {}; - - case "compositionend": - case "compositionstart": - case "compositionupdate": - return { - data: event.data, - }; - - case "keydown": - case "keypress": - case "keyup": - return { - char_code: event.charCode, - key: event.key, - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - key_code: event.keyCode, - shift_key: event.shiftKey, - locale: "locale", - location: event.location, - repeat: event.repeat, - which: event.which, - // locale: event.locale, - }; - - case "focus": - case "blur": - return {}; - - case "change": - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - - return { - value: value, - }; - - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - }; - } - - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - }; - - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - pointer_id: event.pointerId, - width: event.width, - height: event.height, - pressure: event.pressure, - tangential_pressure: event.tangentialPressure, - tilt_x: event.tiltX, - tilt_y: event.tiltY, - twist: event.twist, - pointer_type: event.pointerType, - is_primary: event.isPrimary, - }; - - case "select": - return {}; - - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": - return { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - }; - - case "scroll": - return {}; - - case "wheel": - return { - delta_x: event.deltaX, - delta_y: event.deltaY, - delta_z: event.deltaZ, - delta_mode: event.deltaMode, - }; - - case "animationstart": - case "animationend": - case "animationiteration": - return { - animation_name: event.animationName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - - case "transitionend": - return { - property_name: event.propertyName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - }; - - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": - return {}; - - case "toggle": - return {}; - - default: - return {}; - } -} - -class Interpreter { - constructor(root) { - this.root = root; - this.stack = [root]; - this.listeners = { - onclick: {}, - }; - this.lastNodeWasText = false; - this.nodes = [root]; - } - - top() { - return this.stack[this.stack.length - 1]; - } - - pop() { - return this.stack.pop(); - } - - PushRoot(edit) { - const id = edit.root; - const node = this.nodes[id]; - this.stack.push(node); - } - - AppendChildren(edit) { - let root = this.stack[this.stack.length - (1 + edit.many)]; - - let to_add = this.stack.splice(this.stack.length - edit.many); - - for (let i = 0; i < edit.many; i++) { - root.appendChild(to_add[i]); - } - } - - ReplaceWith(edit) { - let root = this.nodes[edit.root]; - let els = this.stack.splice(this.stack.length - edit.m); - - root.replaceWith(...els); - } - - InsertAfter(edit) { - let old = this.nodes[edit.root]; - let new_nodes = this.stack.splice(this.stack.length - edit.n); - old.after(...new_nodes); - } - - InsertBefore(edit) { - let old = this.nodes[edit.root]; - let new_nodes = this.stack.splice(this.stack.length - edit.n); - old.before(...new_nodes); - } - - Remove(edit) { - let node = this.nodes[edit.root]; - if (node !== undefined) { - node.remove(); - } - } - - CreateTextNode(edit) { - const node = document.createTextNode(edit.text); - this.nodes[edit.root] = node; - this.stack.push(node); - } - - CreateElement(edit) { - const tagName = edit.tag; - const el = document.createElement(tagName); - this.nodes[edit.root] = el; - el.setAttribute("dioxus-id", edit.root); - this.stack.push(el); - } - - CreateElementNs(edit) { - let el = document.createElementNS(edit.ns, edit.tag); - this.stack.push(el); - this.nodes[edit.root] = el; - } - - CreatePlaceholder(edit) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[edit.root] = el; - } - - RemoveEventListener(edit) {} - - NewEventListener(edit) { - const event_name = edit.event_name; - const mounted_node_id = edit.root; - const scope = edit.scope; - - const element = this.nodes[edit.root]; - element.setAttribute( - `dioxus-event-${event_name}`, - `${scope}.${mounted_node_id}` - ); - - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = true; - - this.root.addEventListener(event_name, (event) => { - const target = event.target; - const real_id = target.getAttribute(`dioxus-id`); - - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); - - let contents = serialize_event(event); - - if (should_prevent_default === `on${event.type}`) { - event.preventDefault(); - } - - if (real_id == null) { - return; - } - - rpc.call("user_event", { - event: event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); - }); - } - } - - SetText(edit) { - this.nodes[edit.root].textContent = edit.text; - } - - SetAttribute(edit) { - // console.log("setting attr", edit); - const name = edit.field; - const value = edit.value; - const ns = edit.ns; - const node = this.nodes[edit.root]; - - if (ns == "style") { - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(edit) { - const name = edit.field; - const node = this.nodes[edit.root]; - node.removeAttribute(name); - - if (name === "value") { - node.value = null; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - } - - handleEdits(edits) { - this.stack.push(this.root); - - for (let x = 0; x < edits.length; x++) { - let edit = edits[x]; - let f = this[edit.type]; - f.call(this, edit); - } - } -} - -function main() { - let root = window.document.getElementById("main"); - window.interpreter = new Interpreter(root); - rpc.call("initialize"); -} - -main(); diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0b6044761..02e1dca82 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -203,7 +203,7 @@ pub fn launch_with_props( } else if path.trim_end_matches('/') == "index.html/index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") - .body(include_bytes!("./index.js").to_vec()) + .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec()) } else { wry::http::ResponseBuilder::new() .status(wry::http::status::StatusCode::NOT_FOUND) diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js new file mode 100644 index 000000000..ac2e7adb5 --- /dev/null +++ b/packages/jsinterpreter/interpreter.js @@ -0,0 +1,461 @@ +"use strict"; +exports.__esModule = true; +function serialize_event(event) { + var _a, _b; + switch (event.type) { + case "copy": + case "cut": + case "past": + return {}; + case "compositionend": + case "compositionstart": + case "compositionupdate": + var data = event.data; + return { + data: data + }; + case "keydown": + case "keypress": + case "keyup": + var _c = event, charCode = _c.charCode, key = _c.key, altKey = _c.altKey, ctrlKey = _c.ctrlKey, metaKey = _c.metaKey, keyCode = _c.keyCode, shiftKey = _c.shiftKey, location_1 = _c.location, repeat = _c.repeat, which = _c.which; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location_1, + repeat: repeat, + which: which, + locale: "locale" + }; + case "focus": + case "blur": + return {}; + case "change": + var target = event.target; + var value = void 0; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } + else { + value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; + } + return { + value: value + }; + case "input": + case "invalid": + case "reset": + case "submit": { + var target_1 = event.target; + var value_1 = (_b = target_1.value) !== null && _b !== void 0 ? _b : target_1.textContent; + if (target_1.type == "checkbox") { + value_1 = target_1.checked ? "true" : "false"; + } + return { + value: value_1 + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + var _d = event, altKey_1 = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey_1 = _d.ctrlKey, metaKey_1 = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey_1 = _d.shiftKey; + return { + alt_key: altKey_1, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_1, + meta_key: metaKey_1, + page_x: pageX, + page_y: pageY, + screen_x: screenX_1, + screen_y: screenY_1, + shift_key: shiftKey_1 + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + var _e = event, altKey_2 = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey_2 = _e.ctrlKey, metaKey_2 = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey_2 = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary; + return { + alt_key: altKey_2, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_2, + meta_key: metaKey_2, + page_x: pageX, + page_y: pageY, + screen_x: screenX_2, + screen_y: screenY_2, + shift_key: shiftKey_2, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary + }; + } + case "select": + return {}; + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + var _f = event, altKey_3 = _f.altKey, ctrlKey_3 = _f.ctrlKey, metaKey_3 = _f.metaKey, shiftKey_3 = _f.shiftKey; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey_3, + ctrl_key: ctrlKey_3, + meta_key: metaKey_3, + shift_key: shiftKey_3 + }; + } + case "scroll": + return {}; + case "wheel": { + var _g = event, deltaX = _g.deltaX, deltaY = _g.deltaY, deltaZ = _g.deltaZ, deltaMode = _g.deltaMode; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + var _h = event, animationName = _h.animationName, elapsedTime = _h.elapsedTime, pseudoElement = _h.pseudoElement; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement + }; + } + case "transitionend": { + var _j = event, propertyName = _j.propertyName, elapsedTime = _j.elapsedTime, pseudoElement = _j.pseudoElement; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": + return {}; + case "toggle": + return {}; + default: + return {}; + } +} +var bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + "default": true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true +}; +var Interpreter = /** @class */ (function () { + function Interpreter(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + Interpreter.prototype.top = function () { + return this.stack[this.stack.length - 1]; + }; + Interpreter.prototype.pop = function () { + return this.stack.pop(); + }; + Interpreter.prototype.PushRoot = function (edit) { + var id = edit.root; + var node = this.nodes[id]; + this.stack.push(node); + }; + Interpreter.prototype.AppendChildren = function (edit) { + var root = this.stack[this.stack.length - (1 + edit.many)]; + var to_add = this.stack.splice(this.stack.length - edit.many); + for (var i = 0; i < edit.many; i++) { + root.appendChild(to_add[i]); + } + }; + Interpreter.prototype.ReplaceWith = function (edit) { + var root = this.nodes[edit.root]; + var els = this.stack.splice(this.stack.length - edit.m); + root.replaceWith.apply(root, els); + }; + Interpreter.prototype.InsertAfter = function (edit) { + var old = this.nodes[edit.root]; + var new_nodes = this.stack.splice(this.stack.length - edit.n); + old.after.apply(old, new_nodes); + }; + Interpreter.prototype.InsertBefore = function (edit) { + var old = this.nodes[edit.root]; + var new_nodes = this.stack.splice(this.stack.length - edit.n); + old.before.apply(old, new_nodes); + }; + Interpreter.prototype.Remove = function (edit) { + var node = this.nodes[edit.root]; + if (node !== undefined) { + node.remove(); + } + }; + Interpreter.prototype.CreateTextNode = function (edit) { + // todo: make it so the types are okay + var node = document.createTextNode(edit.text); + this.nodes[edit.root] = node; + this.stack.push(node); + }; + Interpreter.prototype.CreateElement = function (edit) { + var el = document.createElement(edit.tag); + el.setAttribute("dioxus-id", "".concat(edit.root)); + this.nodes[edit.root] = el; + this.stack.push(el); + }; + Interpreter.prototype.CreateElementNs = function (edit) { + var el = document.createElementNS(edit.ns, edit.tag); + this.stack.push(el); + this.nodes[edit.root] = el; + }; + Interpreter.prototype.CreatePlaceholder = function (edit) { + var el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[edit.root] = el; + }; + Interpreter.prototype.RemoveEventListener = function (edit) { }; + Interpreter.prototype.NewEventListener = function (edit, handler) { + var event_name = edit.event_name; + var mounted_node_id = edit.root; + var scope = edit.scope; + console.log('new event listener', event_name, mounted_node_id, scope); + var element = this.nodes[edit.root]; + element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(mounted_node_id)); + if (!this.listeners[event_name]) { + this.listeners[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + }; + Interpreter.prototype.SetText = function (edit) { + this.nodes[edit.root].textContent = edit.text; + }; + Interpreter.prototype.SetAttribute = function (edit) { + // console.log("setting attr", edit); + var name = edit.field; + var value = edit.value; + var ns = edit.ns; + var node = this.nodes[edit.root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } + else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } + else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } + else { + node.setAttribute(name, value); + } + } + } + }; + Interpreter.prototype.RemoveAttribute = function (edit) { + var name = edit.name; + var node = this.nodes[edit.root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + }; + Interpreter.prototype.handleEdits = function (edits) { + console.log("handling edits ", edits); + this.stack.push(this.root); + var _loop_1 = function (edit) { + switch (edit.type) { + case "AppendChildren": + this_1.AppendChildren(edit); + break; + case "ReplaceWith": + this_1.ReplaceWith(edit); + break; + case "InsertAfter": + this_1.InsertAfter(edit); + break; + case "InsertBefore": + this_1.InsertBefore(edit); + break; + case "Remove": + this_1.Remove(edit); + break; + case "CreateTextNode": + this_1.CreateTextNode(edit); + break; + case "CreateElement": + this_1.CreateElement(edit); + break; + case "CreateElementNs": + this_1.CreateElementNs(edit); + break; + case "CreatePlaceholder": + this_1.CreatePlaceholder(edit); + break; + case "RemoveEventListener": + this_1.RemoveEventListener(edit); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + var handler = function (event) { + var target = event.target; + console.log("event", event); + if (target != null) { + var real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute("dioxus-prevent-default"); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents + }); + } + }; + this_1.NewEventListener(edit, handler); + break; + case "SetText": + this_1.SetText(edit); + break; + case "SetAttribute": + this_1.SetAttribute(edit); + break; + case "RemoveAttribute": + this_1.RemoveAttribute(edit); + break; + } + }; + var this_1 = this; + for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { + var edit = edits_1[_i]; + _loop_1(edit); + } + }; + return Interpreter; +}()); +function main() { + var root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +main(); diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts new file mode 100644 index 000000000..faf9e1dab --- /dev/null +++ b/packages/jsinterpreter/interpreter.ts @@ -0,0 +1,636 @@ + + +function serialize_event(event: Event) { + switch (event.type) { + case "copy": + case "cut": + case "past": + return {}; + + case "compositionend": + case "compositionstart": + case "compositionupdate": + let { data } = (event as CompositionEvent); + return { + data, + }; + + case "keydown": + case "keypress": + case "keyup": + let { + charCode, + key, + altKey, + ctrlKey, + metaKey, + keyCode, + shiftKey, + location, + repeat, + which, + } = (event as KeyboardEvent); + + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location, + repeat: repeat, + which: which, + locale: "locale", + }; + + case "focus": + case "blur": + return {}; + + case "change": + let target = event.target as HTMLInputElement; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = target.value ?? target.textContent; + } + + return { + value: value, + }; + + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target as HTMLFormElement; + let value = target.value ?? target.textContent; + + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + + return { + value: value, + }; + } + + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { + altKey, + button, + buttons, + clientX, + clientY, + ctrlKey, + metaKey, + pageX, + pageY, + screenX, + screenY, + shiftKey, + } = event as MouseEvent; + + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + }; + } + + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + const { + altKey, + button, + buttons, + clientX, + clientY, + ctrlKey, + metaKey, + pageX, + pageY, + screenX, + screenY, + shiftKey, + pointerId, + width, + height, + pressure, + tangentialPressure, + tiltX, + tiltY, + twist, + pointerType, + isPrimary, + } = event as PointerEvent; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + + case "select": + return {}; + + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + const { + altKey, + ctrlKey, + metaKey, + shiftKey, + } = event as TouchEvent; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, + }; + } + + case "scroll": + return {}; + case "wheel": { + const { + deltaX, + deltaY, + deltaZ, + deltaMode, + } = event as WheelEvent; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + + case "animationstart": + case "animationend": + case "animationiteration": { + const { + animationName, + elapsedTime, + pseudoElement, + } = event as AnimationEvent; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + + case "transitionend": { + const { + propertyName, + elapsedTime, + pseudoElement, + } = event as TransitionEvent; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": + return {}; + + case "toggle": + return {}; + + default: + return {}; + } +} + +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, +}; + +class Interpreter { + root: Element; + stack: Element[]; + listeners: { [key: string]: (event: Event) => void }; + lastNodeWasText: boolean; + nodes: Element[]; + + + constructor(root: Element) { + this.root = root; + this.stack = [root]; + this.listeners = { + }; + this.lastNodeWasText = false; + this.nodes = [root]; + } + + top() { + return this.stack[this.stack.length - 1]; + } + + pop() { + return this.stack.pop(); + } + + PushRoot(edit: PushRoot) { + const id = edit.root; + const node = this.nodes[id]; + this.stack.push(node); + } + + AppendChildren(edit: AppendChildren) { + let root = this.stack[this.stack.length - (1 + edit.many)]; + + let to_add = this.stack.splice(this.stack.length - edit.many); + + for (let i = 0; i < edit.many; i++) { + root.appendChild(to_add[i]); + } + } + + ReplaceWith(edit: ReplaceWith) { + let root = this.nodes[edit.root] as Element; + let els = this.stack.splice(this.stack.length - edit.m); + + root.replaceWith(...els); + } + + InsertAfter(edit: InsertAfter) { + let old = this.nodes[edit.root] as Element; + let new_nodes = this.stack.splice(this.stack.length - edit.n); + old.after(...new_nodes); + } + + InsertBefore(edit: InsertBefore) { + let old = this.nodes[edit.root] as Element; + let new_nodes = this.stack.splice(this.stack.length - edit.n); + old.before(...new_nodes); + } + + Remove(edit: Remove) { + let node = this.nodes[edit.root] as Element; + if (node !== undefined) { + node.remove(); + } + } + + CreateTextNode(edit: CreateTextNode) { + // todo: make it so the types are okay + const node = document.createTextNode(edit.text) as any as Element; + this.nodes[edit.root] = node; + this.stack.push(node); + } + + CreateElement(edit: CreateElement) { + const el = document.createElement(edit.tag); + el.setAttribute("dioxus-id", `${edit.root}`); + + this.nodes[edit.root] = el; + this.stack.push(el); + } + + CreateElementNs(edit: CreateElementNs) { + let el = document.createElementNS(edit.ns, edit.tag); + this.stack.push(el); + this.nodes[edit.root] = el; + } + + CreatePlaceholder(edit: CreatePlaceholder) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[edit.root] = el; + } + + RemoveEventListener(edit: RemoveEventListener) { } + + NewEventListener(edit: NewEventListener, handler: (event: Event) => void) { + const event_name = edit.event_name; + const mounted_node_id = edit.root; + const scope = edit.scope; + console.log('new event listener', event_name, mounted_node_id, scope); + + const element = this.nodes[edit.root]; + element.setAttribute( + `dioxus-event-${event_name}`, + `${scope}.${mounted_node_id}` + ); + + if (!this.listeners[event_name]) { + this.listeners[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + } + + SetText(edit: SetText) { + this.nodes[edit.root].textContent = edit.text; + } + + SetAttribute(edit: SetAttribute) { + // console.log("setting attr", edit); + const name = edit.field; + const value = edit.value; + const ns = edit.ns; + const node = this.nodes[edit.root]; + + if (ns == "style") { + + // @ts-ignore + (node as HTMLElement).style[name] = value; + + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != (node as HTMLInputElement).value) { + (node as HTMLInputElement).value = value; + } + break; + case "checked": + (node as HTMLInputElement).checked = value === "true"; + break; + case "selected": + (node as HTMLOptionElement).selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(edit: RemoveAttribute) { + const name = edit.name; + + const node = this.nodes[edit.root]; + node.removeAttribute(name); + + if (name === "value") { + (node as HTMLInputElement).value = ""; + } + + if (name === "checked") { + (node as HTMLInputElement).checked = false; + } + + if (name === "selected") { + (node as HTMLOptionElement).selected = false; + } + } + + handleEdits(edits: DomEdit[]) { + console.log("handling edits ", edits); + this.stack.push(this.root); + + for (let edit of edits) { + switch (edit.type) { + case "AppendChildren": + this.AppendChildren(edit); + break; + case "ReplaceWith": + this.ReplaceWith(edit); + break; + case "InsertAfter": + this.InsertAfter(edit); + break; + case "InsertBefore": + this.InsertBefore(edit); + break; + case "Remove": + this.Remove(edit); + break; + case "CreateTextNode": + this.CreateTextNode(edit); + break; + case "CreateElement": + this.CreateElement(edit); + break; + case "CreateElementNs": + this.CreateElementNs(edit); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + let handler = (event: Event) => { + const target = event.target as Element | null; + console.log("event", event); + if (target != null) { + + const real_id = target.getAttribute(`dioxus-id`); + + const should_prevent_default = target.getAttribute( + `dioxus-prevent-default` + ); + + let contents = serialize_event(event); + + if (should_prevent_default === `on${event.type}`) { + event.preventDefault(); + } + + if (real_id == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit, handler); + break; + case "SetText": + this.SetText(edit); + break; + case "SetAttribute": + this.SetAttribute(edit); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit); + break; + } + } + } +} + +function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} + + +type PushRoot = { type: "PushRoot", root: number }; +type AppendChildren = { type: "AppendChildren", many: number }; +type ReplaceWith = { type: "ReplaceWith", root: number, m: number }; +type InsertAfter = { type: "InsertAfter", root: number, n: number }; +type InsertBefore = { type: "InsertBefore", root: number, n: number }; +type Remove = { type: "Remove", root: number }; +type CreateTextNode = { type: "CreateTextNode", text: string, root: number }; +type CreateElement = { type: "CreateElement", tag: string, root: number }; +type CreateElementNs = { type: "CreateElementNs", tag: string, root: number, ns: string }; +type CreatePlaceholder = { type: "CreatePlaceholder", root: number }; +type NewEventListener = { type: "NewEventListener", root: number, event_name: string, scope: number }; +type RemoveEventListener = { type: "RemoveEventListener", event_name: string, scope: number, root: number }; +type SetText = { type: "SetText", root: number, text: string }; +type SetAttribute = { type: "SetAttribute", root: number, field: string, value: string, ns: string | undefined }; +type RemoveAttribute = { type: "RemoveAttribute", root: number, name: string }; + + +type DomEdit = + PushRoot | + AppendChildren | + ReplaceWith | + InsertAfter | + InsertBefore | + Remove | + CreateTextNode | + CreateElement | + CreateElementNs | + CreatePlaceholder | + NewEventListener | + RemoveEventListener | + SetText | + SetAttribute | + RemoveAttribute; + + +export { }; +declare global { + interface Window { + interpreter: Interpreter; + rpc: { call: (method: string, args?: any) => void }; + } +} + + +type Edits = DomEdit[]; + +main(); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json new file mode 100644 index 000000000..698781908 --- /dev/null +++ b/packages/jsinterpreter/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es5", + // "module": "None", + "module": "commonjs", + "strict": true, + "outDir": "dist", + "sourceMap": true + } +} From 10db6ad65bd23dabf60d0afbe6ff82cda8220a8b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 12 Jan 2022 08:57:42 -0500 Subject: [PATCH 17/79] wip: web now links against the js interprter code --- packages/jsinterpreter/interpreter.js | 243 ++++---- packages/jsinterpreter/interpreter.ts | 251 ++++---- packages/jsinterpreter/tsconfig.json | 1 - packages/web/Cargo.toml | 4 +- packages/web/src/bindings.rs | 59 ++ packages/web/src/dom.rs | 419 +------------- packages/web/src/interpreter.js | 529 +++++++++++++++++ packages/web/src/lib.rs | 1 + packages/web/src/olddom.rs | 789 ++++++++++++++++++++++++++ packages/web/src/rehydrate.rs | 10 +- 10 files changed, 1661 insertions(+), 645 deletions(-) create mode 100644 packages/web/src/bindings.rs create mode 100644 packages/web/src/interpreter.js create mode 100644 packages/web/src/olddom.rs diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index ac2e7adb5..6139280f8 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,5 +1,6 @@ "use strict"; exports.__esModule = true; +exports.Interpreter = void 0; function serialize_event(event) { var _a, _b; switch (event.type) { @@ -247,84 +248,79 @@ var Interpreter = /** @class */ (function () { Interpreter.prototype.pop = function () { return this.stack.pop(); }; - Interpreter.prototype.PushRoot = function (edit) { - var id = edit.root; - var node = this.nodes[id]; + Interpreter.prototype.PushRoot = function (root) { + var node = this.nodes[root]; this.stack.push(node); }; - Interpreter.prototype.AppendChildren = function (edit) { - var root = this.stack[this.stack.length - (1 + edit.many)]; - var to_add = this.stack.splice(this.stack.length - edit.many); - for (var i = 0; i < edit.many; i++) { + Interpreter.prototype.AppendChildren = function (many) { + var root = this.stack[this.stack.length - (1 + many)]; + var to_add = this.stack.splice(this.stack.length - many); + for (var i = 0; i < many; i++) { root.appendChild(to_add[i]); } }; - Interpreter.prototype.ReplaceWith = function (edit) { - var root = this.nodes[edit.root]; - var els = this.stack.splice(this.stack.length - edit.m); + Interpreter.prototype.ReplaceWith = function (root_id, m) { + var root = this.nodes[root_id]; + var els = this.stack.splice(this.stack.length - m); root.replaceWith.apply(root, els); }; - Interpreter.prototype.InsertAfter = function (edit) { - var old = this.nodes[edit.root]; - var new_nodes = this.stack.splice(this.stack.length - edit.n); + Interpreter.prototype.InsertAfter = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); old.after.apply(old, new_nodes); }; - Interpreter.prototype.InsertBefore = function (edit) { - var old = this.nodes[edit.root]; - var new_nodes = this.stack.splice(this.stack.length - edit.n); + Interpreter.prototype.InsertBefore = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); old.before.apply(old, new_nodes); }; - Interpreter.prototype.Remove = function (edit) { - var node = this.nodes[edit.root]; + Interpreter.prototype.Remove = function (root) { + var node = this.nodes[root]; if (node !== undefined) { node.remove(); } }; - Interpreter.prototype.CreateTextNode = function (edit) { + Interpreter.prototype.CreateTextNode = function (text, root) { // todo: make it so the types are okay - var node = document.createTextNode(edit.text); - this.nodes[edit.root] = node; + var node = document.createTextNode(text); + this.nodes[root] = node; this.stack.push(node); }; - Interpreter.prototype.CreateElement = function (edit) { - var el = document.createElement(edit.tag); - el.setAttribute("dioxus-id", "".concat(edit.root)); - this.nodes[edit.root] = el; + Interpreter.prototype.CreateElement = function (tag, root) { + var el = document.createElement(tag); + el.setAttribute("dioxus-id", "".concat(root)); + this.nodes[root] = el; this.stack.push(el); }; - Interpreter.prototype.CreateElementNs = function (edit) { - var el = document.createElementNS(edit.ns, edit.tag); + Interpreter.prototype.CreateElementNs = function (tag, root, ns) { + var el = document.createElementNS(ns, tag); this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; }; - Interpreter.prototype.CreatePlaceholder = function (edit) { + Interpreter.prototype.CreatePlaceholder = function (root) { var el = document.createElement("pre"); el.hidden = true; this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; }; - Interpreter.prototype.RemoveEventListener = function (edit) { }; - Interpreter.prototype.NewEventListener = function (edit, handler) { - var event_name = edit.event_name; - var mounted_node_id = edit.root; - var scope = edit.scope; - console.log('new event listener', event_name, mounted_node_id, scope); - var element = this.nodes[edit.root]; - element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(mounted_node_id)); - if (!this.listeners[event_name]) { - this.listeners[event_name] = handler; - this.root.addEventListener(event_name, handler); - } + Interpreter.prototype.NewEventListener = function (event_name, scope, root) { + console.log('new event listener', event_name, root, scope); + var element = this.nodes[root]; + element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(root)); + // if (!this.listeners[event_name]) { + // this.listeners[event_name] = handler; + // this.root.addEventListener(event_name, handler); + // } }; - Interpreter.prototype.SetText = function (edit) { - this.nodes[edit.root].textContent = edit.text; + Interpreter.prototype.RemoveEventListener = function (root, event_name, scope) { + // }; - Interpreter.prototype.SetAttribute = function (edit) { - // console.log("setting attr", edit); - var name = edit.field; - var value = edit.value; - var ns = edit.ns; - var node = this.nodes[edit.root]; + Interpreter.prototype.SetText = function (root, text) { + this.nodes[root].textContent = text; + }; + Interpreter.prototype.SetAttribute = function (root, field, value, ns) { + var name = field; + var node = this.nodes[root]; if (ns == "style") { // @ts-ignore node.style[name] = value; @@ -359,9 +355,8 @@ var Interpreter = /** @class */ (function () { } } }; - Interpreter.prototype.RemoveAttribute = function (edit) { - var name = edit.name; - var node = this.nodes[edit.root]; + Interpreter.prototype.RemoveAttribute = function (root, name) { + var node = this.nodes[root]; node.removeAttribute(name); if (name === "value") { node.value = ""; @@ -376,81 +371,85 @@ var Interpreter = /** @class */ (function () { Interpreter.prototype.handleEdits = function (edits) { console.log("handling edits ", edits); this.stack.push(this.root); - var _loop_1 = function (edit) { - switch (edit.type) { - case "AppendChildren": - this_1.AppendChildren(edit); - break; - case "ReplaceWith": - this_1.ReplaceWith(edit); - break; - case "InsertAfter": - this_1.InsertAfter(edit); - break; - case "InsertBefore": - this_1.InsertBefore(edit); - break; - case "Remove": - this_1.Remove(edit); - break; - case "CreateTextNode": - this_1.CreateTextNode(edit); - break; - case "CreateElement": - this_1.CreateElement(edit); - break; - case "CreateElementNs": - this_1.CreateElementNs(edit); - break; - case "CreatePlaceholder": - this_1.CreatePlaceholder(edit); - break; - case "RemoveEventListener": - this_1.RemoveEventListener(edit); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - var handler = function (event) { - var target = event.target; - console.log("event", event); - if (target != null) { - var real_id = target.getAttribute("dioxus-id"); - var should_prevent_default = target.getAttribute("dioxus-prevent-default"); - var contents = serialize_event(event); - if (should_prevent_default === "on".concat(event.type)) { - event.preventDefault(); - } - if (real_id == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(real_id), - contents: contents - }); - } - }; - this_1.NewEventListener(edit, handler); - break; - case "SetText": - this_1.SetText(edit); - break; - case "SetAttribute": - this_1.SetAttribute(edit); - break; - case "RemoveAttribute": - this_1.RemoveAttribute(edit); - break; - } - }; - var this_1 = this; for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { var edit = edits_1[_i]; - _loop_1(edit); + this.handleEdit(edit); + } + }; + Interpreter.prototype.handleEdit = function (edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name, edit.scope); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + var handler = function (event) { + var target = event.target; + console.log("event", event); + if (target != null) { + var real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute("dioxus-prevent-default"); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root); + // this.NewEventListener(edit, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; } }; return Interpreter; }()); +exports.Interpreter = Interpreter; function main() { var root = window.document.getElementById("main"); if (root != null) { diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index faf9e1dab..e3547effb 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -318,7 +318,7 @@ const bool_attrs = { truespeed: true, }; -class Interpreter { +export class Interpreter { root: Element; stack: Element[]; listeners: { [key: string]: (event: Event) => void }; @@ -343,106 +343,102 @@ class Interpreter { return this.stack.pop(); } - PushRoot(edit: PushRoot) { - const id = edit.root; - const node = this.nodes[id]; + PushRoot(root: number) { + const node = this.nodes[root]; this.stack.push(node); } - AppendChildren(edit: AppendChildren) { - let root = this.stack[this.stack.length - (1 + edit.many)]; + AppendChildren(many: number) { + let root = this.stack[this.stack.length - (1 + many)]; - let to_add = this.stack.splice(this.stack.length - edit.many); + let to_add = this.stack.splice(this.stack.length - many); - for (let i = 0; i < edit.many; i++) { + for (let i = 0; i < many; i++) { root.appendChild(to_add[i]); } } - ReplaceWith(edit: ReplaceWith) { - let root = this.nodes[edit.root] as Element; - let els = this.stack.splice(this.stack.length - edit.m); + ReplaceWith(root_id: number, m: number) { + let root = this.nodes[root_id] as Element; + let els = this.stack.splice(this.stack.length - m); root.replaceWith(...els); } - InsertAfter(edit: InsertAfter) { - let old = this.nodes[edit.root] as Element; - let new_nodes = this.stack.splice(this.stack.length - edit.n); + InsertAfter(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); old.after(...new_nodes); } - InsertBefore(edit: InsertBefore) { - let old = this.nodes[edit.root] as Element; - let new_nodes = this.stack.splice(this.stack.length - edit.n); + InsertBefore(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); old.before(...new_nodes); } - Remove(edit: Remove) { - let node = this.nodes[edit.root] as Element; + Remove(root: number) { + let node = this.nodes[root] as Element; if (node !== undefined) { node.remove(); } } - CreateTextNode(edit: CreateTextNode) { + CreateTextNode(text: string, root: number) { // todo: make it so the types are okay - const node = document.createTextNode(edit.text) as any as Element; - this.nodes[edit.root] = node; + const node = document.createTextNode(text) as any as Element; + this.nodes[root] = node; this.stack.push(node); } - CreateElement(edit: CreateElement) { - const el = document.createElement(edit.tag); - el.setAttribute("dioxus-id", `${edit.root}`); + CreateElement(tag: string, root: number) { + const el = document.createElement(tag); + el.setAttribute("dioxus-id", `${root}`); - this.nodes[edit.root] = el; + this.nodes[root] = el; this.stack.push(el); } - CreateElementNs(edit: CreateElementNs) { - let el = document.createElementNS(edit.ns, edit.tag); + CreateElementNs(tag: string, root: number, ns: string) { + let el = document.createElementNS(ns, tag); this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; } - CreatePlaceholder(edit: CreatePlaceholder) { + CreatePlaceholder(root: number) { let el = document.createElement("pre"); el.hidden = true; this.stack.push(el); - this.nodes[edit.root] = el; + this.nodes[root] = el; } - RemoveEventListener(edit: RemoveEventListener) { } - - NewEventListener(edit: NewEventListener, handler: (event: Event) => void) { - const event_name = edit.event_name; - const mounted_node_id = edit.root; - const scope = edit.scope; - console.log('new event listener', event_name, mounted_node_id, scope); - - const element = this.nodes[edit.root]; + NewEventListener(event_name: string, scope: number, root: number) { + console.log('new event listener', event_name, root, scope); + const element = this.nodes[root]; element.setAttribute( `dioxus-event-${event_name}`, - `${scope}.${mounted_node_id}` + `${scope}.${root}` ); - if (!this.listeners[event_name]) { - this.listeners[event_name] = handler; - this.root.addEventListener(event_name, handler); - } + // if (!this.listeners[event_name]) { + // this.listeners[event_name] = handler; + // this.root.addEventListener(event_name, handler); + // } } - SetText(edit: SetText) { - this.nodes[edit.root].textContent = edit.text; + + RemoveEventListener(root: number, event_name: string, scope: number) { + // } - SetAttribute(edit: SetAttribute) { - // console.log("setting attr", edit); - const name = edit.field; - const value = edit.value; - const ns = edit.ns; - const node = this.nodes[edit.root]; + + SetText(root: number, text: string) { + this.nodes[root].textContent = text; + } + + SetAttribute(root: number, field: string, value: string, ns: string | undefined) { + const name = field; + const node = this.nodes[root]; if (ns == "style") { @@ -477,10 +473,9 @@ class Interpreter { } } } - RemoveAttribute(edit: RemoveAttribute) { - const name = edit.name; + RemoveAttribute(root: number, name: string) { - const node = this.nodes[edit.root]; + const node = this.nodes[root]; node.removeAttribute(name); if (name === "value") { @@ -501,79 +496,87 @@ class Interpreter { this.stack.push(this.root); for (let edit of edits) { - switch (edit.type) { - case "AppendChildren": - this.AppendChildren(edit); - break; - case "ReplaceWith": - this.ReplaceWith(edit); - break; - case "InsertAfter": - this.InsertAfter(edit); - break; - case "InsertBefore": - this.InsertBefore(edit); - break; - case "Remove": - this.Remove(edit); - break; - case "CreateTextNode": - this.CreateTextNode(edit); - break; - case "CreateElement": - this.CreateElement(edit); - break; - case "CreateElementNs": - this.CreateElementNs(edit); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - let handler = (event: Event) => { - const target = event.target as Element | null; - console.log("event", event); - if (target != null) { + this.handleEdit(edit); + } + } - const real_id = target.getAttribute(`dioxus-id`); + handleEdit(edit: DomEdit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name, edit.scope); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + let handler = (event: Event) => { + const target = event.target as Element | null; + console.log("event", event); + if (target != null) { - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); + const real_id = target.getAttribute(`dioxus-id`); - let contents = serialize_event(event); + const should_prevent_default = target.getAttribute( + `dioxus-prevent-default` + ); - if (should_prevent_default === `on${event.type}`) { - event.preventDefault(); - } + let contents = serialize_event(event); - if (real_id == null) { - return; - } - - window.rpc.call("user_event", { - event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); + if (should_prevent_default === `on${event.type}`) { + event.preventDefault(); } - }; - this.NewEventListener(edit, handler); - break; - case "SetText": - this.SetText(edit); - break; - case "SetAttribute": - this.SetAttribute(edit); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit); - break; - } + + if (real_id == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root); + // this.NewEventListener(edit, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; } } } @@ -632,5 +635,3 @@ declare global { type Edits = DomEdit[]; - -main(); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json index 698781908..8f39daddf 100644 --- a/packages/jsinterpreter/tsconfig.json +++ b/packages/jsinterpreter/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "target": "es5", - // "module": "None", "module": "commonjs", "strict": true, "outDir": "dist", diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 868c5d7b0..52a78e631 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -11,8 +11,8 @@ documentation = "https://dioxuslabs.com" keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7"} -dioxus-html = { path = "../html", version ="^0.1.4"} +dioxus-core = { path = "../core", version = "^0.1.7" } +dioxus-html = { path = "../html", version = "^0.1.4" } js-sys = "0.3" wasm-bindgen = { version = "0.2.78", features = ["enable-interning"] } lazy_static = "1.4.0" diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs new file mode 100644 index 000000000..d65fc1300 --- /dev/null +++ b/packages/web/src/bindings.rs @@ -0,0 +1,59 @@ +use dioxus_core::DomEdit; +use wasm_bindgen::{convert::IntoWasmAbi, describe::WasmDescribe, prelude::*}; +use web_sys::{Element, HtmlElement, Node}; + +#[wasm_bindgen(module = "/src/interpreter.js")] +extern "C" { + pub type Interpreter; + + #[wasm_bindgen(constructor)] + pub fn new(arg: Element) -> Interpreter; + + #[wasm_bindgen(method)] + pub fn set_node(this: &Interpreter, id: usize, node: Node); + + #[wasm_bindgen(method)] + pub fn PushRoot(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn AppendChildren(this: &Interpreter, many: u32); + + #[wasm_bindgen(method)] + pub fn ReplaceWith(this: &Interpreter, root: u64, m: u32); + + #[wasm_bindgen(method)] + pub fn InsertAfter(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn InsertBefore(this: &Interpreter, root: u64, n: u32); + + #[wasm_bindgen(method)] + pub fn Remove(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateTextNode(this: &Interpreter, text: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElement(this: &Interpreter, tag: &str, root: u64); + + #[wasm_bindgen(method)] + pub fn CreateElementNs(this: &Interpreter, tag: &str, root: u64, ns: &str); + + #[wasm_bindgen(method)] + pub fn CreatePlaceholder(this: &Interpreter, root: u64); + + #[wasm_bindgen(method)] + pub fn NewEventListener(this: &Interpreter, name: &str, scope: usize, root: u64); + + #[wasm_bindgen(method)] + pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); + + #[wasm_bindgen(method)] + pub fn SetText(this: &Interpreter, root: u64, text: &str); + + #[wasm_bindgen(method)] + pub fn SetAttribute(this: &Interpreter, root: u64, field: &str, value: &str, ns: Option<&str>); + + #[wasm_bindgen(method)] + pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str); +} diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 364f1a1d7..918af4c4a 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -7,10 +7,11 @@ //! - tests to ensure dyn_into works for various event types. //! - Partial delegation?> +use crate::bindings::Interpreter; use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; use fxhash::FxHashMap; use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; -use wasm_bindgen::{closure::Closure, JsCast}; +use wasm_bindgen::{closure::Closure, JsCast, JsValue}; use web_sys::{ CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, HtmlTextAreaElement, Node, @@ -19,13 +20,10 @@ use web_sys::{ use crate::{nodeslab::NodeSlab, WebConfig}; pub struct WebsysDom { - stack: Stack, - - /// A map from ElementID (index) to Node - pub(crate) nodes: NodeSlab, - document: Document, + pub interpreter: Interpreter, + pub(crate) root: Element, sender_callback: Rc, @@ -42,7 +40,6 @@ impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { let document = load_document(); - let nodes = NodeSlab::new(2000); let listeners = FxHashMap::default(); let mut stack = Stack::with_capacity(10); @@ -52,8 +49,7 @@ impl WebsysDom { stack.push(root_node); Self { - stack, - nodes, + interpreter: Interpreter::new(root.clone()), listeners, document, sender_callback, @@ -64,399 +60,38 @@ impl WebsysDom { pub fn apply_edits(&mut self, mut edits: Vec) { for edit in edits.drain(..) { match edit { - DomEdit::PushRoot { root } => self.push(root), - DomEdit::AppendChildren { many } => self.append_children(many), - DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), - DomEdit::Remove { root } => self.remove(root), - DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), - DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), - DomEdit::CreateElementNs { tag, root: id, ns } => { - self.create_element(tag, Some(ns), id) + DomEdit::PushRoot { root } => self.interpreter.PushRoot(root), + DomEdit::AppendChildren { many } => self.interpreter.AppendChildren(many), + DomEdit::ReplaceWith { root, m } => self.interpreter.ReplaceWith(root, m), + DomEdit::InsertAfter { root, n } => self.interpreter.InsertAfter(root, n), + DomEdit::InsertBefore { root, n } => self.interpreter.InsertBefore(root, n), + DomEdit::Remove { root } => self.interpreter.Remove(root), + DomEdit::CreateTextNode { text, root } => { + self.interpreter.CreateTextNode(text, root) } - DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), + DomEdit::CreateElement { tag, root } => self.interpreter.CreateElement(tag, root), + DomEdit::CreateElementNs { tag, root, ns } => { + self.interpreter.CreateElementNs(tag, root, ns) + } + DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root), DomEdit::NewEventListener { event_name, scope, - root: mounted_node_id, - } => self.new_event_listener(event_name, scope, mounted_node_id), - - DomEdit::RemoveEventListener { event, root } => { - self.remove_event_listener(event, root) + root, + } => self.interpreter.NewEventListener(event_name, scope.0, root), + DomEdit::RemoveEventListener { root, event } => { + self.interpreter.RemoveEventListener(root, event) } - - DomEdit::SetText { text, root } => self.set_text(text, root), + DomEdit::SetText { root, text } => self.interpreter.SetText(root, text), DomEdit::SetAttribute { + root, field, value, ns, - root, - } => self.set_attribute(field, value, ns, root), - DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), - - DomEdit::InsertAfter { n, root } => self.insert_after(n, root), - DomEdit::InsertBefore { n, root } => self.insert_before(n, root), - } - } - } - fn push(&mut self, root: u64) { - let key = root as usize; - let domnode = &self.nodes[key]; - - let real_node: Node = match domnode { - Some(n) => n.clone(), - None => todo!(), - }; - - self.stack.push(real_node); - } - - fn append_children(&mut self, many: u32) { - let root: Node = self - .stack - .list - .get(self.stack.list.len() - (1 + many as usize)) - .unwrap() - .clone(); - - // We need to make sure to add comments between text nodes - // We ensure that the text siblings are patched by preventing the browser from merging - // neighboring text nodes. Originally inspired by some of React's work from 2016. - // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes - // -> https://github.com/facebook/react/pull/5753 - /* - todo: we need to track this for replacing/insert after/etc - */ - let mut last_node_was_text = false; - - for child in self - .stack - .list - .drain((self.stack.list.len() - many as usize)..) - { - if child.dyn_ref::().is_some() { - if last_node_was_text { - let comment_node = self - .document - .create_comment("dioxus") - .dyn_into::() - .unwrap(); - root.append_child(&comment_node).unwrap(); + } => self.interpreter.SetAttribute(root, field, value, ns), + DomEdit::RemoveAttribute { root, name } => { + self.interpreter.RemoveAttribute(root, name) } - last_node_was_text = true; - } else { - last_node_was_text = false; - } - root.append_child(&child).unwrap(); - } - } - - fn replace_with(&mut self, m: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - m as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.replace_with_with_node(&arr).unwrap(); - } - } - - fn remove(&mut self, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(element) = node.dyn_ref::() { - element.remove(); - } else { - if let Some(parent) = node.parent_node() { - parent.remove_child(&node).unwrap(); - } - } - } - - fn create_placeholder(&mut self, id: u64) { - self.create_element("pre", None, id); - self.set_attribute("hidden", "", None, id); - } - - fn create_text_node(&mut self, text: &str, id: u64) { - let textnode = self - .document - .create_text_node(text) - .dyn_into::() - .unwrap(); - - self.stack.push(textnode.clone()); - - self.nodes[(id as usize)] = Some(textnode); - } - - fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { - let tag = wasm_bindgen::intern(tag); - - let el = match ns { - Some(ns) => self - .document - .create_element_ns(Some(ns), tag) - .unwrap() - .dyn_into::() - .unwrap(), - None => self - .document - .create_element(tag) - .unwrap() - .dyn_into::() - .unwrap(), - }; - - use smallstr::SmallString; - use std::fmt::Write; - - let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); - write!(s, "{}", id).unwrap(); - - let el2 = el.dyn_ref::().unwrap(); - el2.set_attribute("dioxus-id", s.as_str()).unwrap(); - - self.stack.push(el.clone()); - self.nodes[(id as usize)] = Some(el); - } - - fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { - let event = wasm_bindgen::intern(event); - - // attach the correct attributes to the element - // these will be used by accessing the event's target - // This ensures we only ever have one handler attached to the root, but decide - // dynamically when we want to call a listener. - - let el = self.stack.top(); - - let el = el.dyn_ref::().unwrap(); - - el.set_attribute("dioxus-event", event).unwrap(); - - // Register the callback to decode - - if let Some(entry) = self.listeners.get_mut(event) { - entry.0 += 1; - } else { - let trigger = self.sender_callback.clone(); - - let c: Box = Box::new(move |event: &web_sys::Event| { - // "Result" cannot be received from JS - // Instead, we just build and immediately execute a closure that returns result - match decode_trigger(event) { - Ok(synthetic_event) => { - let target = event.target().unwrap(); - if let Some(node) = target.dyn_ref::() { - if let Some(name) = node.get_attribute("dioxus-prevent-default") { - if name == synthetic_event.name - || name.trim_start_matches("on") == synthetic_event.name - { - log::trace!("Preventing default"); - event.prevent_default(); - } - } - } - - trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) - } - Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), - }; - }); - - let handler = Closure::wrap(c); - - self.root - .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) - .unwrap(); - - // Increment the listeners - self.listeners.insert(event.into(), (1, handler)); - } - } - - fn remove_event_listener(&mut self, _event: &str, _root: u64) { - todo!() - } - - fn set_text(&mut self, text: &str, root: u64) { - let el = self.nodes[root as usize].as_ref().unwrap(); - el.set_text_content(Some(text)) - } - - fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if ns == Some("style") { - if let Some(el) = node.dyn_ref::() { - let el = el.dyn_ref::().unwrap(); - let style_dc: CssStyleDeclaration = el.style(); - style_dc.set_property(name, value).unwrap(); - } - } else { - let fallback = || { - let el = node.dyn_ref::().unwrap(); - el.set_attribute(name, value).unwrap() - }; - match name { - "dangerous_inner_html" => { - if let Some(el) = node.dyn_ref::() { - el.set_inner_html(value); - } - } - "value" => { - if let Some(input) = node.dyn_ref::() { - /* - if the attribute being set is the same as the value of the input, then don't bother setting it. - This is used in controlled components to keep the cursor in the right spot. - - this logic should be moved into the virtualdom since we have the notion of "volatile" - */ - if input.value() != value { - input.set_value(value); - } - } else if let Some(node) = node.dyn_ref::() { - if name == "value" { - node.set_value(value); - } - } else { - fallback(); - } - } - "checked" => { - if let Some(input) = node.dyn_ref::() { - match value { - "true" => input.set_checked(true), - "false" => input.set_checked(false), - _ => fallback(), - } - } else { - fallback(); - } - } - "selected" => { - if let Some(node) = node.dyn_ref::() { - node.set_selected(true); - } else { - fallback(); - } - } - _ => { - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if value == "false" { - if let Some(el) = node.dyn_ref::() { - match name { - "allowfullscreen" - | "allowpaymentrequest" - | "async" - | "autofocus" - | "autoplay" - | "checked" - | "controls" - | "default" - | "defer" - | "disabled" - | "formnovalidate" - | "hidden" - | "ismap" - | "itemscope" - | "loop" - | "multiple" - | "muted" - | "nomodule" - | "novalidate" - | "open" - | "playsinline" - | "readonly" - | "required" - | "reversed" - | "selected" - | "truespeed" => { - let _ = el.remove_attribute(name); - } - _ => { - let _ = el.set_attribute(name, value); - } - }; - } - } else { - fallback(); - } - } - } - } - } - - fn remove_attribute(&mut self, name: &str, root: u64) { - let node = self.nodes[root as usize].as_ref().unwrap(); - if let Some(node) = node.dyn_ref::() { - node.remove_attribute(name).unwrap(); - } - if let Some(node) = node.dyn_ref::() { - // Some attributes are "volatile" and don't work through `removeAttribute`. - if name == "value" { - node.set_value(""); - } - if name == "checked" { - node.set_checked(false); - } - } - - if let Some(node) = node.dyn_ref::() { - if name == "selected" { - node.set_selected(true); - } - } - } - - fn insert_after(&mut self, n: u32, root: u64) { - let old = self.nodes[root as usize].as_ref().unwrap(); - - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } else if let Some(el) = old.dyn_ref::() { - el.after_with_node(&arr).unwrap(); - } - } - - fn insert_before(&mut self, n: u32, root: u64) { - let anchor = self.nodes[root as usize].as_ref().unwrap(); - - if n == 1 { - let before = self.stack.pop(); - - anchor - .parent_node() - .unwrap() - .insert_before(&before, Some(&anchor)) - .unwrap(); - } else { - let arr: js_sys::Array = self - .stack - .list - .drain((self.stack.list.len() - n as usize)..) - .collect(); - - if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); - } else if let Some(el) = anchor.dyn_ref::() { - el.before_with_node(&arr).unwrap(); } } } diff --git a/packages/web/src/interpreter.js b/packages/web/src/interpreter.js new file mode 100644 index 000000000..1d0f14c68 --- /dev/null +++ b/packages/web/src/interpreter.js @@ -0,0 +1,529 @@ +"use strict"; +var exports = {}; +exports.__esModule = true; +exports.Interpreter = void 0; +function serialize_event(event) { + var _a, _b; + switch (event.type) { + case "copy": + case "cut": + case "past": + return {}; + case "compositionend": + case "compositionstart": + case "compositionupdate": + var data = event.data; + return { + data: data, + }; + case "keydown": + case "keypress": + case "keyup": + var _c = event, + charCode = _c.charCode, + key = _c.key, + altKey = _c.altKey, + ctrlKey = _c.ctrlKey, + metaKey = _c.metaKey, + keyCode = _c.keyCode, + shiftKey = _c.shiftKey, + location_1 = _c.location, + repeat = _c.repeat, + which = _c.which; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location_1, + repeat: repeat, + which: which, + locale: "locale", + }; + case "focus": + case "blur": + return {}; + case "change": + var target = event.target; + var value = void 0; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = + (_a = target.value) !== null && _a !== void 0 + ? _a + : target.textContent; + } + return { + value: value, + }; + case "input": + case "invalid": + case "reset": + case "submit": { + var target_1 = event.target; + var value_1 = + (_b = target_1.value) !== null && _b !== void 0 + ? _b + : target_1.textContent; + if (target_1.type == "checkbox") { + value_1 = target_1.checked ? "true" : "false"; + } + return { + value: value_1, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + var _d = event, + altKey_1 = _d.altKey, + button = _d.button, + buttons = _d.buttons, + clientX = _d.clientX, + clientY = _d.clientY, + ctrlKey_1 = _d.ctrlKey, + metaKey_1 = _d.metaKey, + pageX = _d.pageX, + pageY = _d.pageY, + screenX_1 = _d.screenX, + screenY_1 = _d.screenY, + shiftKey_1 = _d.shiftKey; + return { + alt_key: altKey_1, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_1, + meta_key: metaKey_1, + page_x: pageX, + page_y: pageY, + screen_x: screenX_1, + screen_y: screenY_1, + shift_key: shiftKey_1, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + var _e = event, + altKey_2 = _e.altKey, + button = _e.button, + buttons = _e.buttons, + clientX = _e.clientX, + clientY = _e.clientY, + ctrlKey_2 = _e.ctrlKey, + metaKey_2 = _e.metaKey, + pageX = _e.pageX, + pageY = _e.pageY, + screenX_2 = _e.screenX, + screenY_2 = _e.screenY, + shiftKey_2 = _e.shiftKey, + pointerId = _e.pointerId, + width = _e.width, + height = _e.height, + pressure = _e.pressure, + tangentialPressure = _e.tangentialPressure, + tiltX = _e.tiltX, + tiltY = _e.tiltY, + twist = _e.twist, + pointerType = _e.pointerType, + isPrimary = _e.isPrimary; + return { + alt_key: altKey_2, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey_2, + meta_key: metaKey_2, + page_x: pageX, + page_y: pageY, + screen_x: screenX_2, + screen_y: screenY_2, + shift_key: shiftKey_2, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + case "select": + return {}; + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + var _f = event, + altKey_3 = _f.altKey, + ctrlKey_3 = _f.ctrlKey, + metaKey_3 = _f.metaKey, + shiftKey_3 = _f.shiftKey; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey_3, + ctrl_key: ctrlKey_3, + meta_key: metaKey_3, + shift_key: shiftKey_3, + }; + } + case "scroll": + return {}; + case "wheel": { + var _g = event, + deltaX = _g.deltaX, + deltaY = _g.deltaY, + deltaZ = _g.deltaZ, + deltaMode = _g.deltaMode; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + var _h = event, + animationName = _h.animationName, + elapsedTime = _h.elapsedTime, + pseudoElement = _h.pseudoElement; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + var _j = event, + propertyName = _j.propertyName, + elapsedTime = _j.elapsedTime, + pseudoElement = _j.pseudoElement; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": + return {}; + case "toggle": + return {}; + default: + return {}; + } +} +var bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, +}; +export var Interpreter = /** @class */ (function () { + function Interpreter(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + Interpreter.prototype.top = function () { + return this.stack[this.stack.length - 1]; + }; + Interpreter.prototype.pop = function () { + return this.stack.pop(); + }; + Interpreter.prototype.PushRoot = function (root) { + var node = this.nodes[root]; + this.stack.push(node); + }; + Interpreter.prototype.AppendChildren = function (many) { + var root = this.stack[this.stack.length - (1 + many)]; + var to_add = this.stack.splice(this.stack.length - many); + for (var i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + }; + Interpreter.prototype.ReplaceWith = function (root_id, m) { + var root = this.nodes[root_id]; + var els = this.stack.splice(this.stack.length - m); + root.replaceWith.apply(root, els); + }; + Interpreter.prototype.InsertAfter = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.after.apply(old, new_nodes); + }; + Interpreter.prototype.InsertBefore = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.before.apply(old, new_nodes); + }; + Interpreter.prototype.Remove = function (root) { + var node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + }; + Interpreter.prototype.CreateTextNode = function (text, root) { + // todo: make it so the types are okay + var node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + }; + Interpreter.prototype.CreateElement = function (tag, root) { + var el = document.createElement(tag); + el.setAttribute("dioxus-id", "".concat(root)); + this.nodes[root] = el; + this.stack.push(el); + }; + Interpreter.prototype.CreateElementNs = function (tag, root, ns) { + var el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.CreatePlaceholder = function (root) { + var el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.NewEventListener = function (event_name, scope, root) { + console.log("new event listener", event_name, root, scope); + var element = this.nodes[root]; + element.setAttribute( + "dioxus-event-".concat(event_name), + "".concat(scope, ".").concat(root) + ); + // if (!this.listeners[event_name]) { + // this.listeners[event_name] = handler; + // this.root.addEventListener(event_name, handler); + // } + }; + Interpreter.prototype.RemoveEventListener = function ( + root, + event_name, + scope + ) { + // + }; + Interpreter.prototype.SetText = function (root, text) { + this.nodes[root].textContent = text; + }; + Interpreter.prototype.SetAttribute = function (root, field, value, ns) { + var name = field; + var node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + }; + Interpreter.prototype.RemoveAttribute = function (root, name) { + var node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + }; + Interpreter.prototype.handleEdits = function (edits) { + console.log("handling edits ", edits); + this.stack.push(this.root); + for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { + var edit = edits_1[_i]; + this.handleEdit(edit); + } + }; + Interpreter.prototype.handleEdit = function (edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name, edit.scope); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + var handler = function (event) { + var target = event.target; + console.log("event", event); + if (target != null) { + var real_id = target.getAttribute("dioxus-id"); + var should_prevent_default = target.getAttribute( + "dioxus-prevent-default" + ); + var contents = serialize_event(event); + if (should_prevent_default === "on".concat(event.type)) { + event.preventDefault(); + } + if (real_id == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(real_id), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root); + // this.NewEventListener(edit, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + }; + return Interpreter; +})(); +exports.Interpreter = Interpreter; +function main() { + var root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 67d52f4fe..ba9398276 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -61,6 +61,7 @@ pub use dioxus_core as dioxus; use dioxus_core::prelude::Component; use futures_util::FutureExt; +pub(crate) mod bindings; mod cache; mod cfg; mod dom; diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs new file mode 100644 index 000000000..364f1a1d7 --- /dev/null +++ b/packages/web/src/olddom.rs @@ -0,0 +1,789 @@ +//! Implementation of a renderer for Dioxus on the web. +//! +//! Oustanding todos: +//! - Removing event listeners (delegation) +//! - Passive event listeners +//! - no-op event listener patch for safari +//! - tests to ensure dyn_into works for various event types. +//! - Partial delegation?> + +use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; +use fxhash::FxHashMap; +use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{ + CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, + HtmlOptionElement, HtmlTextAreaElement, Node, +}; + +use crate::{nodeslab::NodeSlab, WebConfig}; + +pub struct WebsysDom { + stack: Stack, + + /// A map from ElementID (index) to Node + pub(crate) nodes: NodeSlab, + + document: Document, + + pub(crate) root: Element, + + sender_callback: Rc, + + // map of listener types to number of those listeners + // This is roughly a delegater + // TODO: check how infero delegates its events - some are more performant + listeners: FxHashMap<&'static str, ListenerEntry>, +} + +type ListenerEntry = (usize, Closure); + +impl WebsysDom { + pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { + let document = load_document(); + + let nodes = NodeSlab::new(2000); + let listeners = FxHashMap::default(); + + let mut stack = Stack::with_capacity(10); + + let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); + let root_node = root.clone().dyn_into::().unwrap(); + stack.push(root_node); + + Self { + stack, + nodes, + listeners, + document, + sender_callback, + root, + } + } + + pub fn apply_edits(&mut self, mut edits: Vec) { + for edit in edits.drain(..) { + match edit { + DomEdit::PushRoot { root } => self.push(root), + DomEdit::AppendChildren { many } => self.append_children(many), + DomEdit::ReplaceWith { m, root } => self.replace_with(m, root), + DomEdit::Remove { root } => self.remove(root), + DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id), + DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id), + DomEdit::CreateElementNs { tag, root: id, ns } => { + self.create_element(tag, Some(ns), id) + } + DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), + DomEdit::NewEventListener { + event_name, + scope, + root: mounted_node_id, + } => self.new_event_listener(event_name, scope, mounted_node_id), + + DomEdit::RemoveEventListener { event, root } => { + self.remove_event_listener(event, root) + } + + DomEdit::SetText { text, root } => self.set_text(text, root), + DomEdit::SetAttribute { + field, + value, + ns, + root, + } => self.set_attribute(field, value, ns, root), + DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root), + + DomEdit::InsertAfter { n, root } => self.insert_after(n, root), + DomEdit::InsertBefore { n, root } => self.insert_before(n, root), + } + } + } + fn push(&mut self, root: u64) { + let key = root as usize; + let domnode = &self.nodes[key]; + + let real_node: Node = match domnode { + Some(n) => n.clone(), + None => todo!(), + }; + + self.stack.push(real_node); + } + + fn append_children(&mut self, many: u32) { + let root: Node = self + .stack + .list + .get(self.stack.list.len() - (1 + many as usize)) + .unwrap() + .clone(); + + // We need to make sure to add comments between text nodes + // We ensure that the text siblings are patched by preventing the browser from merging + // neighboring text nodes. Originally inspired by some of React's work from 2016. + // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes + // -> https://github.com/facebook/react/pull/5753 + /* + todo: we need to track this for replacing/insert after/etc + */ + let mut last_node_was_text = false; + + for child in self + .stack + .list + .drain((self.stack.list.len() - many as usize)..) + { + if child.dyn_ref::().is_some() { + if last_node_was_text { + let comment_node = self + .document + .create_comment("dioxus") + .dyn_into::() + .unwrap(); + root.append_child(&comment_node).unwrap(); + } + last_node_was_text = true; + } else { + last_node_was_text = false; + } + root.append_child(&child).unwrap(); + } + } + + fn replace_with(&mut self, m: u32, root: u64) { + let old = self.nodes[root as usize].as_ref().unwrap(); + + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - m as usize)..) + .collect(); + + if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.replace_with_with_node(&arr).unwrap(); + } + } + + fn remove(&mut self, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if let Some(element) = node.dyn_ref::() { + element.remove(); + } else { + if let Some(parent) = node.parent_node() { + parent.remove_child(&node).unwrap(); + } + } + } + + fn create_placeholder(&mut self, id: u64) { + self.create_element("pre", None, id); + self.set_attribute("hidden", "", None, id); + } + + fn create_text_node(&mut self, text: &str, id: u64) { + let textnode = self + .document + .create_text_node(text) + .dyn_into::() + .unwrap(); + + self.stack.push(textnode.clone()); + + self.nodes[(id as usize)] = Some(textnode); + } + + fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) { + let tag = wasm_bindgen::intern(tag); + + let el = match ns { + Some(ns) => self + .document + .create_element_ns(Some(ns), tag) + .unwrap() + .dyn_into::() + .unwrap(), + None => self + .document + .create_element(tag) + .unwrap() + .dyn_into::() + .unwrap(), + }; + + use smallstr::SmallString; + use std::fmt::Write; + + let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new(); + write!(s, "{}", id).unwrap(); + + let el2 = el.dyn_ref::().unwrap(); + el2.set_attribute("dioxus-id", s.as_str()).unwrap(); + + self.stack.push(el.clone()); + self.nodes[(id as usize)] = Some(el); + } + + fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { + let event = wasm_bindgen::intern(event); + + // attach the correct attributes to the element + // these will be used by accessing the event's target + // This ensures we only ever have one handler attached to the root, but decide + // dynamically when we want to call a listener. + + let el = self.stack.top(); + + let el = el.dyn_ref::().unwrap(); + + el.set_attribute("dioxus-event", event).unwrap(); + + // Register the callback to decode + + if let Some(entry) = self.listeners.get_mut(event) { + entry.0 += 1; + } else { + let trigger = self.sender_callback.clone(); + + let c: Box = Box::new(move |event: &web_sys::Event| { + // "Result" cannot be received from JS + // Instead, we just build and immediately execute a closure that returns result + match decode_trigger(event) { + Ok(synthetic_event) => { + let target = event.target().unwrap(); + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); + } + } + } + + trigger.as_ref()(SchedulerMsg::Event(synthetic_event)) + } + Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e), + }; + }); + + let handler = Closure::wrap(c); + + self.root + .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) + .unwrap(); + + // Increment the listeners + self.listeners.insert(event.into(), (1, handler)); + } + } + + fn remove_event_listener(&mut self, _event: &str, _root: u64) { + todo!() + } + + fn set_text(&mut self, text: &str, root: u64) { + let el = self.nodes[root as usize].as_ref().unwrap(); + el.set_text_content(Some(text)) + } + + fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if ns == Some("style") { + if let Some(el) = node.dyn_ref::() { + let el = el.dyn_ref::().unwrap(); + let style_dc: CssStyleDeclaration = el.style(); + style_dc.set_property(name, value).unwrap(); + } + } else { + let fallback = || { + let el = node.dyn_ref::().unwrap(); + el.set_attribute(name, value).unwrap() + }; + match name { + "dangerous_inner_html" => { + if let Some(el) = node.dyn_ref::() { + el.set_inner_html(value); + } + } + "value" => { + if let Some(input) = node.dyn_ref::() { + /* + if the attribute being set is the same as the value of the input, then don't bother setting it. + This is used in controlled components to keep the cursor in the right spot. + + this logic should be moved into the virtualdom since we have the notion of "volatile" + */ + if input.value() != value { + input.set_value(value); + } + } else if let Some(node) = node.dyn_ref::() { + if name == "value" { + node.set_value(value); + } + } else { + fallback(); + } + } + "checked" => { + if let Some(input) = node.dyn_ref::() { + match value { + "true" => input.set_checked(true), + "false" => input.set_checked(false), + _ => fallback(), + } + } else { + fallback(); + } + } + "selected" => { + if let Some(node) = node.dyn_ref::() { + node.set_selected(true); + } else { + fallback(); + } + } + _ => { + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if value == "false" { + if let Some(el) = node.dyn_ref::() { + match name { + "allowfullscreen" + | "allowpaymentrequest" + | "async" + | "autofocus" + | "autoplay" + | "checked" + | "controls" + | "default" + | "defer" + | "disabled" + | "formnovalidate" + | "hidden" + | "ismap" + | "itemscope" + | "loop" + | "multiple" + | "muted" + | "nomodule" + | "novalidate" + | "open" + | "playsinline" + | "readonly" + | "required" + | "reversed" + | "selected" + | "truespeed" => { + let _ = el.remove_attribute(name); + } + _ => { + let _ = el.set_attribute(name, value); + } + }; + } + } else { + fallback(); + } + } + } + } + } + + fn remove_attribute(&mut self, name: &str, root: u64) { + let node = self.nodes[root as usize].as_ref().unwrap(); + if let Some(node) = node.dyn_ref::() { + node.remove_attribute(name).unwrap(); + } + if let Some(node) = node.dyn_ref::() { + // Some attributes are "volatile" and don't work through `removeAttribute`. + if name == "value" { + node.set_value(""); + } + if name == "checked" { + node.set_checked(false); + } + } + + if let Some(node) = node.dyn_ref::() { + if name == "selected" { + node.set_selected(true); + } + } + } + + fn insert_after(&mut self, n: u32, root: u64) { + let old = self.nodes[root as usize].as_ref().unwrap(); + + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } else if let Some(el) = old.dyn_ref::() { + el.after_with_node(&arr).unwrap(); + } + } + + fn insert_before(&mut self, n: u32, root: u64) { + let anchor = self.nodes[root as usize].as_ref().unwrap(); + + if n == 1 { + let before = self.stack.pop(); + + anchor + .parent_node() + .unwrap() + .insert_before(&before, Some(&anchor)) + .unwrap(); + } else { + let arr: js_sys::Array = self + .stack + .list + .drain((self.stack.list.len() - n as usize)..) + .collect(); + + if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } else if let Some(el) = anchor.dyn_ref::() { + el.before_with_node(&arr).unwrap(); + } + } + } +} + +#[derive(Debug, Default)] +struct Stack { + list: Vec, +} + +impl Stack { + #[inline] + fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + #[inline] + fn push(&mut self, node: Node) { + self.list.push(node); + } + + #[inline] + fn pop(&mut self) -> Node { + self.list.pop().unwrap() + } + + fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} + +pub struct DioxusWebsysEvent(web_sys::Event); + +// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread +unsafe impl Send for DioxusWebsysEvent {} +unsafe impl Sync for DioxusWebsysEvent {} + +// todo: some of these events are being casted to the wrong event type. +// We need tests that simulate clicks/etc and make sure every event type works. +fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc { + use dioxus_html::on::*; + use dioxus_html::KeyCode; + + match event.type_().as_str() { + "copy" | "cut" | "paste" => Arc::new(ClipboardData {}), + "compositionend" | "compositionstart" | "compositionupdate" => { + let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap(); + Arc::new(CompositionData { + data: evt.data().unwrap_or_default(), + }) + } + "keydown" | "keypress" | "keyup" => { + let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap(); + Arc::new(KeyboardData { + alt_key: evt.alt_key(), + char_code: evt.char_code(), + key: evt.key(), + key_code: KeyCode::from_raw_code(evt.key_code() as u8), + ctrl_key: evt.ctrl_key(), + locale: "not implemented".to_string(), + location: evt.location() as usize, + meta_key: evt.meta_key(), + repeat: evt.repeat(), + shift_key: evt.shift_key(), + which: evt.which() as usize, + }) + } + "focus" | "blur" => Arc::new(FocusData {}), + + // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy + // don't have a good solution with the serialized event problem + "change" | "input" | "invalid" | "reset" | "submit" => { + let evt: &web_sys::Event = event.dyn_ref().unwrap(); + + let target: web_sys::EventTarget = evt.target().unwrap(); + let value: String = (&target) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| { + // todo: special case more input types + match input.type_().as_str() { + "checkbox" => { + match input.checked() { + true => "true".to_string(), + false => "false".to_string(), + } + }, + _ => { + input.value() + } + } + }) + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + // select elements are NOT input events - because - why woudn't they be?? + .or_else(|| { + target + .dyn_ref() + .map(|input: &web_sys::HtmlSelectElement| input.value()) + }) + .or_else(|| { + target + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + Arc::new(FormData { value }) + } + "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" + | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" + | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { + let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap(); + Arc::new(MouseData { + alt_key: evt.alt_key(), + button: evt.button(), + buttons: evt.buttons(), + client_x: evt.client_x(), + client_y: evt.client_y(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + screen_x: evt.screen_x(), + screen_y: evt.screen_y(), + shift_key: evt.shift_key(), + page_x: evt.page_x(), + page_y: evt.page_y(), + }) + } + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" + | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { + let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap(); + Arc::new(PointerData { + alt_key: evt.alt_key(), + button: evt.button(), + buttons: evt.buttons(), + client_x: evt.client_x(), + client_y: evt.client_y(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + page_x: evt.page_x(), + page_y: evt.page_y(), + screen_x: evt.screen_x(), + screen_y: evt.screen_y(), + shift_key: evt.shift_key(), + pointer_id: evt.pointer_id(), + width: evt.width(), + height: evt.height(), + pressure: evt.pressure(), + tangential_pressure: evt.tangential_pressure(), + tilt_x: evt.tilt_x(), + tilt_y: evt.tilt_y(), + twist: evt.twist(), + pointer_type: evt.pointer_type(), + is_primary: evt.is_primary(), + // get_modifier_state: evt.get_modifier_state(), + }) + } + "select" => Arc::new(SelectionData {}), + "touchcancel" | "touchend" | "touchmove" | "touchstart" => { + let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap(); + Arc::new(TouchData { + alt_key: evt.alt_key(), + ctrl_key: evt.ctrl_key(), + meta_key: evt.meta_key(), + shift_key: evt.shift_key(), + }) + } + "scroll" => Arc::new(()), + "wheel" => { + let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); + Arc::new(WheelData { + delta_x: evt.delta_x(), + delta_y: evt.delta_y(), + delta_z: evt.delta_z(), + delta_mode: evt.delta_mode(), + }) + } + "animationstart" | "animationend" | "animationiteration" => { + let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap(); + Arc::new(AnimationData { + elapsed_time: evt.elapsed_time(), + animation_name: evt.animation_name(), + pseudo_element: evt.pseudo_element(), + }) + } + "transitionend" => { + let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap(); + Arc::new(TransitionData { + elapsed_time: evt.elapsed_time(), + property_name: evt.property_name(), + pseudo_element: evt.pseudo_element(), + }) + } + "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" + | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" + | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" + | "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}), + "toggle" => Arc::new(ToggleData {}), + _ => Arc::new(()), + } +} + +/// This function decodes a websys event and produces an EventTrigger +/// With the websys implementation, we attach a unique key to the nodes +fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { + use anyhow::Context; + + let target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); + + let typ = event.type_(); + + let element_id = target + .get_attribute("dioxus-id") + .context("Could not find element id on event target")? + .parse()?; + + Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: Some(ElementId(element_id)), + scope_id: None, + priority: dioxus_core::EventPriority::Medium, + }) +} + +pub(crate) fn load_document() -> Document { + web_sys::window() + .expect("should have access to the Window") + .document() + .expect("should have access to the Document") +} + +fn event_name_from_typ(typ: &str) -> &'static str { + match typ { + "copy" => "copy", + "cut" => "cut", + "paste" => "paste", + "compositionend" => "compositionend", + "compositionstart" => "compositionstart", + "compositionupdate" => "compositionupdate", + "keydown" => "keydown", + "keypress" => "keypress", + "keyup" => "keyup", + "focus" => "focus", + "blur" => "blur", + "change" => "change", + "input" => "input", + "invalid" => "invalid", + "reset" => "reset", + "submit" => "submit", + "click" => "click", + "contextmenu" => "contextmenu", + "doubleclick" => "doubleclick", + "drag" => "drag", + "dragend" => "dragend", + "dragenter" => "dragenter", + "dragexit" => "dragexit", + "dragleave" => "dragleave", + "dragover" => "dragover", + "dragstart" => "dragstart", + "drop" => "drop", + "mousedown" => "mousedown", + "mouseenter" => "mouseenter", + "mouseleave" => "mouseleave", + "mousemove" => "mousemove", + "mouseout" => "mouseout", + "mouseover" => "mouseover", + "mouseup" => "mouseup", + "pointerdown" => "pointerdown", + "pointermove" => "pointermove", + "pointerup" => "pointerup", + "pointercancel" => "pointercancel", + "gotpointercapture" => "gotpointercapture", + "lostpointercapture" => "lostpointercapture", + "pointerenter" => "pointerenter", + "pointerleave" => "pointerleave", + "pointerover" => "pointerover", + "pointerout" => "pointerout", + "select" => "select", + "touchcancel" => "touchcancel", + "touchend" => "touchend", + "touchmove" => "touchmove", + "touchstart" => "touchstart", + "scroll" => "scroll", + "wheel" => "wheel", + "animationstart" => "animationstart", + "animationend" => "animationend", + "animationiteration" => "animationiteration", + "transitionend" => "transitionend", + "abort" => "abort", + "canplay" => "canplay", + "canplaythrough" => "canplaythrough", + "durationchange" => "durationchange", + "emptied" => "emptied", + "encrypted" => "encrypted", + "ended" => "ended", + "error" => "error", + "loadeddata" => "loadeddata", + "loadedmetadata" => "loadedmetadata", + "loadstart" => "loadstart", + "pause" => "pause", + "play" => "play", + "playing" => "playing", + "progress" => "progress", + "ratechange" => "ratechange", + "seeked" => "seeked", + "seeking" => "seeking", + "stalled" => "stalled", + "suspend" => "suspend", + "timeupdate" => "timeupdate", + "volumechange" => "volumechange", + "waiting" => "waiting", + "toggle" => "toggle", + _ => { + panic!("unsupported event type") + } + } +} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 8993dd58c..25b126036 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -80,7 +80,8 @@ impl WebsysDom { *last_node_was_text = true; - self.nodes[node_id.0] = Some(node); + self.interpreter.set_node(node_id.0, node); + // self.nodes[node_id.0] = Some(node); *cur_place += 1; } @@ -105,7 +106,8 @@ impl WebsysDom { .set_attribute("dioxus-id", s.as_str()) .unwrap(); - self.nodes[node_id.0] = Some(node.clone()); + self.interpreter.set_node(node_id.0, node.clone()); + // self.nodes[node_id.0] = Some(node.clone()); *cur_place += 1; @@ -135,7 +137,9 @@ impl WebsysDom { let cur_place = place.last_mut().unwrap(); let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap(); - self.nodes[node_id.0] = Some(node); + self.interpreter.set_node(node_id.0, node); + + // self.nodes[node_id.0] = Some(node); *cur_place += 1; } From aa15d00b1e18850ca17e38ae29add12d6a946d6b Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Thu, 13 Jan 2022 12:16:17 +0800 Subject: [PATCH 18/79] docs: add `zh-cn` readme --- README.md | 8 ++ notes/README/ZH_CN.md | 165 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 notes/README/ZH_CN.md diff --git a/README.md b/README.md index 04d01dead..bb0ebcdc9 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,14 @@ +
+

+ English + | + Chinese +

+
+
Dioxus is a portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust. diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md new file mode 100644 index 000000000..6e22092d5 --- /dev/null +++ b/notes/README/ZH_CN.md @@ -0,0 +1,165 @@ +
+

🌗🚀 Dioxus

+

+ Frontend that scales. +

+
+ +
+ + + Crates.io version + + + + Download + + + + docs.rs docs + + + + CI status + +
+ +
+ + + Awesome Page + + + + Awesome Page + +
+ + +
+

+ 官网 + | + 手册 + | + 示例 +

+
+ +
+

+ English + | + 中文 +

+
+ + +
+ +Dioxus 是一个可移植、高性能的框架,用于在 Rust 中建跨平台的用户界面。 + +```rust +fn app(cx: Scope) -> Element { + let mut count = use_state(&cx, || 0); + + cx.render(rsx!( + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + )) +} +``` + +Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应用。Dioxus 为不同的平台都提供了很好的开发文档。 + +如果你会使用 React ,那 Dioxus 对你来说会很简单。 + +### 项目特点: +- 桌面程序在本地运行。 +- 强大的状态管理工具。 +- 支持所有 HTML 标签,监听器和事件。 +- 超高的内存使用率,稳定的组件分配器。 +- 多通道异步调动器,超强的异步支持。 +- 更多信息请查阅: [版本发布文档](https://dioxuslabs.com/blog/introducing-dioxus/). + +### 示例 + +所有例子都是 桌面应用 程序,请使用 `cargo run --example XYZ` 运行这些例子。 + +``` +cargo run --example EXAMPLE +``` + +## 开始学习 + + + + + + + + + + +
教程网页端桌面端SSR移动端状态管理
+ + +## Dioxus 项目 + +| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | + + +查看 [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) 查看更多有趣(~~NiuBi~~)的项目! + +## 为什么使用 Rust 开发 + +TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScript。 + +TS 代码运行效率不高,而且有大量的配置项。 + +相比之下,Dioxus 使用用 Rust 编写将大大的提高效能。 + +使用 Rust 开发,我们能获得: + +- 所有库的静态类型支持。 +- 变量的默认不变性。 +- 一个简单且直观的模块管理系统。 +- 集成在内部的文档系统。 +- 先进的模式匹配系统。 +- 简洁、高效、可组合的迭代器。 +- 内联内置的 单元测试 / 集成测试。 +- 一流的异常处理系统。 +- 强大且健全的标准库。 +- 灵活的 `宏` 系统。 +- 使用 `crates.io` 管理包。 + +Dioxus 能为开发者提供的: + +- 正确且安全的使用不可变数据结构。 +- 安全的错误处理结果。 +- 拥有原生移动端的性能。 +- 直接访问系统层IO。 + +Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让你的前端团队更有信心在更短的时间内做出重大改变。 + +### 不建议使用 Dioxus 的情况? + +您不该在这些情况下使用 Dioxus : + +- 你不喜欢 React Hooks 的前端开发方案。 +- 你需要一个 `no-std` 的渲染器。 +- 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 + + +## 协议 + +这个项目使用 [MIT 协议]. + +[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE \ No newline at end of file From 55b84fd9bdce08119a39de909192035e8ae9d73e Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Thu, 13 Jan 2022 12:30:05 +0800 Subject: [PATCH 19/79] docs: commit docs --- README.md | 2 +- notes/README/ZH_CN.md | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bb0ebcdc9..588e1f873 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ cargo run --example EXAMPLE | [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | -See the awesome-dioxus page for a curated list of content in the Dioxus Ecosystem. +See the [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) page for a curated list of content in the Dioxus Ecosystem. ## Why Dioxus and why Rust? diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 6e22092d5..eaf364c80 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -75,7 +75,9 @@ fn app(cx: Scope) -> Element { } ``` -Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应用。Dioxus 为不同的平台都提供了很好的开发文档。 +Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应用。 + +Dioxus 为不同的平台都提供了很好的开发文档。 如果你会使用 React ,那 Dioxus 对你来说会很简单。 @@ -89,13 +91,13 @@ Dioxus 可用于制作 网页程序、桌面应用、静态站点、移动端应 ### 示例 -所有例子都是 桌面应用 程序,请使用 `cargo run --example XYZ` 运行这些例子。 +本项目中的所有例子都是 `桌面应用` 程序,请使用 `cargo run --example XYZ` 运行这些例子。 ``` cargo run --example EXAMPLE ``` -## 开始学习 +## 进入学习 @@ -118,7 +120,7 @@ cargo run --example EXAMPLE 查看 [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) 查看更多有趣(~~NiuBi~~)的项目! -## 为什么使用 Rust 开发 +## 为什么使用 Dioxus 和 Rust ? TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScript。 @@ -128,7 +130,7 @@ TS 代码运行效率不高,而且有大量的配置项。 使用 Rust 开发,我们能获得: -- 所有库的静态类型支持。 +- 库的静态类型支持。 - 变量的默认不变性。 - 一个简单且直观的模块管理系统。 - 集成在内部的文档系统。 @@ -145,7 +147,7 @@ Dioxus 能为开发者提供的: - 正确且安全的使用不可变数据结构。 - 安全的错误处理结果。 - 拥有原生移动端的性能。 -- 直接访问系统层IO。 +- 直接访问系统的IO层。 Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让你的前端团队更有信心在更短的时间内做出重大改变。 @@ -153,9 +155,10 @@ Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但 您不该在这些情况下使用 Dioxus : -- 你不喜欢 React Hooks 的前端开发方案。 -- 你需要一个 `no-std` 的渲染器。 +- 您不喜欢 React Hooks 的前端开发方案。 +- 您需要一个 `no-std` 的渲染器。 - 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 +- 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。 ## 协议 From 5d85eb643862e6ddd2b4cb7d3eb7e973750c3b89 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Thu, 13 Jan 2022 18:16:55 +0800 Subject: [PATCH 20/79] fix: edit `git-repository-url` --- docs/guide/book.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/book.toml b/docs/guide/book.toml index 48ead3936..eabbe6f2b 100644 --- a/docs/guide/book.toml +++ b/docs/guide/book.toml @@ -10,8 +10,8 @@ edition = "2018" [output.html] mathjax-support = true site-url = "/mdBook/" -git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide" -edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}" +git-repository-url = "https://github.com/DioxusLabs/dioxus/edit/master/docs/guide" +edit-url-template = "https://github.com/DioxusLabs/dioxus/edit/master/docs/guide/{path}" [output.html.playground] editable = true From 75032da885d3e8d568531783b0f3005072077aa6 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Thu, 13 Jan 2022 22:30:18 +0800 Subject: [PATCH 21/79] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 588e1f873..b7d167711 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@

English | - Chinese + 中文

From 6bd7e983c4eadd31501d0e30d9558f1f684721ae Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 13 Jan 2022 11:25:18 -0500 Subject: [PATCH 22/79] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b7d167711..641f5a8b8 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ You shouldn't use Dioxus if: ### Comparison with other Rust UI frameworks Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. -- [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, no SSR. +- [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, supports SSR (no hydration). - [Percy](https://github.com/chinedufn/percy): Supports SSR but less emphasis on state management and event handling. - [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but lacking in ergonomics. - [Dominator](https://github.com/Pauan/rust-dominator): Signal-based zero-cost alternative, less emphasis on community and docs. From e22ba5b1e52fe00e08e0b6ac0abae29b6068b8f0 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 8 Jan 2022 13:44:53 -0600 Subject: [PATCH 23/79] Add title prop to Link Every element can have a title, but it's particularly useful on links, so I think making it an explicit option is worthwhile. --- packages/router/src/components/link.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 634696bf4..dc807aa1a 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -31,6 +31,9 @@ pub struct LinkProps<'a> { #[props(default, strip_option)] id: Option<&'a str>, + #[props(default, strip_option)] + title: Option<&'a str>, + children: Element<'a>, #[props(default)] @@ -44,6 +47,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { href: "{cx.props.to}", class: format_args!("{}", cx.props.class.unwrap_or("")), id: format_args!("{}", cx.props.id.unwrap_or("")), + title: format_args!("{}", cx.props.title.unwrap_or("")), prevent_default: "onclick", onclick: move |_| service.push_route(cx.props.to), From d367e0f89f1b31484bde42efaf10c3531aed1d64 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 8 Jan 2022 13:53:45 -0600 Subject: [PATCH 24/79] Fix typo in RouterService struct's "registered_routes" field name This was "registerd_routes", missing an "e". --- packages/router/src/service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 7f069b6ba..ae7c1ff05 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -10,7 +10,7 @@ use dioxus_core::ScopeId; pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, - registerd_routes: RefCell, + registered_routes: RefCell, slots: Rc>>, root_found: Rc>, cur_root: RefCell, @@ -53,7 +53,7 @@ impl RouterService { }); Self { - registerd_routes: RefCell::new(RouteSlot::Routes { + registered_routes: RefCell::new(RouteSlot::Routes { partial: String::from("/"), total: String::from("/"), rest: Vec::new(), From 3a5b417ad1639a31e9aac9f41b05d8b3f074b128 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sat, 8 Jan 2022 13:55:13 -0600 Subject: [PATCH 25/79] Add more trace messages to the RouterService code I was trying to debug some issues with my routes and this additional tracing was quite helpful. --- packages/router/src/components/route.rs | 1 + packages/router/src/service.rs | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/router/src/components/route.rs b/packages/router/src/components/route.rs index 2d9a270b8..129953eea 100644 --- a/packages/router/src/components/route.rs +++ b/packages/router/src/components/route.rs @@ -30,6 +30,7 @@ pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element { Some(ctx) => ctx.total_route.to_string(), None => cx.props.to.to_string(), }; + log::trace!("total route for {} is {}", cx.props.to, total_route); // provide our route context let route_context = cx.provide_context(RouteContext { diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index ae7c1ff05..7eddeb69d 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -46,8 +46,8 @@ impl RouterService { let listener = history.listen(move || { _root_found.set(false); // checking if the route is valid is cheap, so we do it - for (slot, _) in _slots.borrow_mut().iter().rev() { - log::trace!("regenerating slot {:?}", slot); + for (slot, root) in _slots.borrow_mut().iter().rev() { + log::trace!("regenerating slot {:?} for root '{}'", slot, root); regen(*slot); } }); @@ -68,20 +68,25 @@ impl RouterService { } pub fn push_route(&self, route: &str) { + log::trace!("Pushing route: {}", route); self.history.borrow_mut().push(route); } pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { + log::trace!("Registered route '{}' with scope id {:?}", route, scope); self.slots.borrow_mut().push((scope, route)); } pub fn should_render(&self, scope: ScopeId) -> bool { + log::trace!("Should render scope id {:?}?", scope); if self.root_found.get() { + log::trace!(" no - because root_found is true"); return false; } let location = self.history.borrow().location(); let path = location.path(); + log::trace!(" current path is '{}'", path); let roots = self.slots.borrow(); @@ -90,14 +95,22 @@ impl RouterService { // fallback logic match root { Some((_id, route)) => { + log::trace!( + " matched given scope id {:?} with route root '{}'", + scope, + route, + ); if route == path { + log::trace!(" and it matches the current path '{}'", path); self.root_found.set(true); true } else { if route == "" { + log::trace!(" and the route is the root, so we will use that without a better match"); self.root_found.set(true); true } else { + log::trace!(" and the route '{}' is not the root nor does it match the current path", route); false } } From 3c6142fb9d8b5f715adf0fb30c2f3534b9ecd923 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Sun, 9 Jan 2022 23:44:37 -0600 Subject: [PATCH 26/79] Commit WIP on router There are a few different changes in here that probably need to be picked apart. I'm sure much of this is wrong. * Fix missing `dyn` that compiler complained about in router.rs * Make UseRoute store a `Rc` rather than a string so we can get information out of the router like current location. * Implement `UseRoute`'s nth_segment and last_segment methods. I changed the return type to a String because of the above. * Remove some unused imports in platform/mod.rs and service.rs * Implement the `use_route` fn. It panics if called outside a Router { } (I think). I think that makes sense. * Add a `current_location` method to `RouterService` that returns the current location. I needed this both for the `UseRoute` implementation and _also_ so I could get at this in my webapp code. I think having some way to get this will be useful for others, whether or not this exact API is used. In my case, I want to compare the current path to the `to` path of a `Link` so I can use a different class for that `Link` if it is the currently active page. --- packages/router/src/components/router.rs | 2 +- packages/router/src/hooks/use_route.rs | 40 ++++++++++++++++++++---- packages/router/src/platform/mod.rs | 2 -- packages/router/src/service.rs | 7 +++-- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 85f0a62a3..3d9f0c83a 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -12,7 +12,7 @@ pub struct RouterProps<'a> { children: Element<'a>, #[props(default, strip_option)] - onchange: Option<&'a Fn(&'a str)>, + onchange: Option<&'a dyn Fn(&'a str)>, } #[allow(non_snake_case)] diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 8307ed1f0..bf76d0975 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,7 +1,11 @@ use dioxus_core::ScopeState; +use gloo::history::Location; +use std::rc::Rc; + +use crate::RouterService; pub struct UseRoute<'a> { - cur_route: String, + router: Rc, cx: &'a ScopeState, } @@ -11,20 +15,44 @@ impl<'a> UseRoute<'a> { todo!() } - pub fn nth_segment(&self, n: usize) -> Option<&str> { - todo!() + pub fn nth_segment(&self, n: usize) -> Option { + let mut segments = self.path_segments(); + let len = segments.len(); + if len - 1 < n { + return None; + } + Some(segments.remove(n)) } - pub fn last_segment(&self) -> Option<&'a str> { - todo!() + pub fn last_segment(&self) -> Option { + let mut segments = self.path_segments(); + let len = segments.len(); + if len == 0 { + return None; + } + Some(segments.remove(len - 1)) } /// Parse the segments of the URL, using named parameters (defined in your router) pub fn segment(&self, name: &str) -> Option<&T> { todo!() } + + pub fn current_location(&self) -> Location { + self.router.current_location() + } + + fn path_segments(&self) -> Vec { + let location = self.router.current_location(); + let stripped = &location.path()[1..]; + stripped.split('/').map(str::to_string).collect::>() + } } pub fn use_route<'a>(cx: &'a ScopeState) -> UseRoute<'a> { - todo!() + let router = cx + .consume_context::() + .expect("Cannot call use_route outside the scope of a Router component") + .clone(); + UseRoute { router, cx } } diff --git a/packages/router/src/platform/mod.rs b/packages/router/src/platform/mod.rs index bbb0e7c18..7afc1316a 100644 --- a/packages/router/src/platform/mod.rs +++ b/packages/router/src/platform/mod.rs @@ -1,5 +1,3 @@ -use url::Url; - pub trait RouterProvider { fn get_current_route(&self) -> String; fn subscribe_to_route_changes(&self, callback: Box); diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 7eddeb69d..c0b383e81 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,7 +1,6 @@ -use gloo::history::{BrowserHistory, History, HistoryListener}; +use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ cell::{Cell, RefCell}, - collections::HashMap, rc::Rc, }; @@ -118,6 +117,10 @@ impl RouterService { None => false, } } + + pub fn current_location(&self) -> Location { + self.history.borrow().location().clone() + } } pub struct RouterCfg { From f8a7e1cd8255fd7d0116384247e0e305bb73bf3d Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Mon, 10 Jan 2022 11:30:26 -0600 Subject: [PATCH 27/79] Implement router matching for path parameters We don't want to have the router just always match paths as exact strings. If a path contains a parameter like "/thing/:id" then the ":id" portion of the route should match _any_ string, not a literal ":id". --- packages/router/src/service.rs | 60 ++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index c0b383e81..9525ac44c 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -72,8 +72,9 @@ impl RouterService { } pub fn register_total_route(&self, route: String, scope: ScopeId, fallback: bool) { - log::trace!("Registered route '{}' with scope id {:?}", route, scope); - self.slots.borrow_mut().push((scope, route)); + let clean = clean_route(route); + log::trace!("Registered route '{}' with scope id {:?}", clean, scope); + self.slots.borrow_mut().push((scope, clean)); } pub fn should_render(&self, scope: ScopeId) -> bool { @@ -99,7 +100,7 @@ impl RouterService { scope, route, ); - if route == path { + if route_matches_path(route, path) { log::trace!(" and it matches the current path '{}'", path); self.root_found.set(true); true @@ -123,6 +124,59 @@ impl RouterService { } } +fn clean_route(route: String) -> String { + if route.as_str() == "/" { + return route; + } + route.trim_end_matches('/').to_string() +} + +fn clean_path(path: &str) -> &str { + if path == "/" { + return path; + } + path.trim_end_matches('/') +} + +fn route_matches_path(route: &str, path: &str) -> bool { + let route_pieces = route.split('/').collect::>(); + let path_pieces = clean_path(path).split('/').collect::>(); + + log::trace!( + " checking route pieces {:?} vs path pieces {:?}", + route_pieces, + path_pieces, + ); + + if route_pieces.len() != path_pieces.len() { + log::trace!(" the routes are different lengths"); + return false; + } + + for (i, r) in route_pieces.iter().enumerate() { + log::trace!(" checking route piece '{}' vs path", r); + // If this is a parameter then it matches as long as there's + // _any_thing in that spot in the path. + if r.starts_with(':') { + log::trace!( + " route piece '{}' starts with a colon so it matches anything", + r, + ); + continue; + } + log::trace!( + " route piece '{}' must be an exact match for path piece '{}'", + r, + path_pieces[i], + ); + if path_pieces[i] != *r { + return false; + } + } + + return true; +} + pub struct RouterCfg { initial_route: String, } From c9408da7310423b1676fab6d41635f9a8000d89e Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 11 Jan 2022 10:47:17 -0600 Subject: [PATCH 28/79] Implement UseRoute segment method This relies on the RouterService to capture path params when it does path matching. --- packages/router/src/hooks/use_route.rs | 17 ++++++++++---- packages/router/src/service.rs | 31 ++++++++++++++------------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index bf76d0975..5ed056f65 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,6 +1,6 @@ use dioxus_core::ScopeState; use gloo::history::Location; -use std::rc::Rc; +use std::{rc::Rc, str::FromStr}; use crate::RouterService; @@ -33,9 +33,18 @@ impl<'a> UseRoute<'a> { Some(segments.remove(len - 1)) } - /// Parse the segments of the URL, using named parameters (defined in your router) - pub fn segment(&self, name: &str) -> Option<&T> { - todo!() + /// Get the named parameter from the path, as defined in your router. The + /// value will be parsed into the type specified by `T` by calling + /// `value.parse::()`. This method returns `None` if the named + /// parameter does not exist in the current path. + pub fn segment(&self, name: &str) -> Option> + where + T: FromStr, + { + self.router + .current_path_params() + .get(name) + .and_then(|v| Some(v.parse::())) } pub fn current_location(&self) -> Location { diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 9525ac44c..2fac8509b 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -1,6 +1,7 @@ use gloo::history::{BrowserHistory, History, HistoryListener, Location}; use std::{ - cell::{Cell, RefCell}, + cell::{Cell, Ref, RefCell}, + collections::HashMap, rc::Rc, }; @@ -9,10 +10,9 @@ use dioxus_core::ScopeId; pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, - registered_routes: RefCell, slots: Rc>>, root_found: Rc>, - cur_root: RefCell, + cur_path_params: Rc>>, listener: HistoryListener, } @@ -52,16 +52,11 @@ impl RouterService { }); Self { - registered_routes: RefCell::new(RouteSlot::Routes { - partial: String::from("/"), - total: String::from("/"), - rest: Vec::new(), - }), root_found, history: Rc::new(RefCell::new(history)), regen_route, slots, - cur_root: RefCell::new(path.to_string()), + cur_path_params: Rc::new(RefCell::new(HashMap::new())), listener, } } @@ -100,9 +95,10 @@ impl RouterService { scope, route, ); - if route_matches_path(route, path) { + if let Some(params) = route_matches_path(route, path) { log::trace!(" and it matches the current path '{}'", path); self.root_found.set(true); + *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { @@ -122,6 +118,10 @@ impl RouterService { pub fn current_location(&self) -> Location { self.history.borrow().location().clone() } + + pub fn current_path_params(&self) -> Ref> { + self.cur_path_params.borrow() + } } fn clean_route(route: String) -> String { @@ -138,7 +138,7 @@ fn clean_path(path: &str) -> &str { path.trim_end_matches('/') } -fn route_matches_path(route: &str, path: &str) -> bool { +fn route_matches_path(route: &str, path: &str) -> Option> { let route_pieces = route.split('/').collect::>(); let path_pieces = clean_path(path).split('/').collect::>(); @@ -150,9 +150,10 @@ fn route_matches_path(route: &str, path: &str) -> bool { if route_pieces.len() != path_pieces.len() { log::trace!(" the routes are different lengths"); - return false; + return None; } + let mut matches = HashMap::new(); for (i, r) in route_pieces.iter().enumerate() { log::trace!(" checking route piece '{}' vs path", r); // If this is a parameter then it matches as long as there's @@ -162,6 +163,8 @@ fn route_matches_path(route: &str, path: &str) -> bool { " route piece '{}' starts with a colon so it matches anything", r, ); + let param = &r[1..]; + matches.insert(param.to_string(), path_pieces[i].to_string()); continue; } log::trace!( @@ -170,11 +173,11 @@ fn route_matches_path(route: &str, path: &str) -> bool { path_pieces[i], ); if path_pieces[i] != *r { - return false; + return None; } } - return true; + Some(matches) } pub struct RouterCfg { From e06eac1ce5c6bb0a6680482574d82b16a141a626 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Tue, 11 Jan 2022 11:27:02 -0600 Subject: [PATCH 29/79] More WIP router implementation --- examples/router.rs | 12 +++++-- packages/router/src/hooks/use_route.rs | 46 +++++++++++++++++--------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/examples/router.rs b/examples/router.rs index bd6d51881..121514423 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -2,6 +2,7 @@ use dioxus::prelude::*; use dioxus::router::{Link, Route, Router}; +use serde::Deserialize; fn main() { dioxus::desktop::launch(app); @@ -30,7 +31,7 @@ fn app(cx: Scope) -> Element { } fn BlogPost(cx: Scope) -> Element { - let post = dioxus::router::use_route(&cx).last_segment()?; + let post = dioxus::router::use_route(&cx).last_segment(); cx.render(rsx! { div { @@ -40,9 +41,14 @@ fn BlogPost(cx: Scope) -> Element { }) } +#[derive(Deserialize)] +struct Query { + bold: bool, +} + fn User(cx: Scope) -> Element { - let post = dioxus::router::use_route(&cx).last_segment()?; - let bold = dioxus::router::use_route(&cx).param::("bold"); + let post = dioxus::router::use_route(&cx).last_segment(); + let query = dioxus::router::use_route(&cx).query::(); cx.render(rsx! { div { diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 5ed056f65..97dd6c048 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -1,20 +1,29 @@ use dioxus_core::ScopeState; -use gloo::history::Location; +use gloo::history::{HistoryResult, Location}; +use serde::de::DeserializeOwned; use std::{rc::Rc, str::FromStr}; use crate::RouterService; -pub struct UseRoute<'a> { +/// This struct provides is a wrapper around the internal router +/// implementation, with methods for getting information about the current +/// route. +pub struct UseRoute { router: Rc, - cx: &'a ScopeState, } -impl<'a> UseRoute<'a> { - /// Parse the query part of the URL - pub fn param(&self, param: &str) -> Option<&T> { - todo!() +impl UseRoute { + /// This method simply calls the [`Location::query`] method. + pub fn query(&self) -> HistoryResult + where + T: DeserializeOwned, + { + self.current_location().query::() } + /// Returns the nth segment in the path. Paths that end with a slash have + /// the slash removed before determining the segments. If the path has + /// fewer segments than `n` then this method returns `None`. pub fn nth_segment(&self, n: usize) -> Option { let mut segments = self.path_segments(); let len = segments.len(); @@ -24,13 +33,12 @@ impl<'a> UseRoute<'a> { Some(segments.remove(n)) } - pub fn last_segment(&self) -> Option { + /// Returns the last segment in the path. Paths that end with a slash have + /// the slash removed before determining the segments. The root path, `/`, + /// will return an empty string. + pub fn last_segment(&self) -> String { let mut segments = self.path_segments(); - let len = segments.len(); - if len == 0 { - return None; - } - Some(segments.remove(len - 1)) + segments.remove(segments.len() - 1) } /// Get the named parameter from the path, as defined in your router. The @@ -47,21 +55,29 @@ impl<'a> UseRoute<'a> { .and_then(|v| Some(v.parse::())) } + /// Returns the [Location] for the current route. pub fn current_location(&self) -> Location { self.router.current_location() } fn path_segments(&self) -> Vec { let location = self.router.current_location(); + let path = location.path(); + if path == "/" { + return vec![String::new()]; + } let stripped = &location.path()[1..]; stripped.split('/').map(str::to_string).collect::>() } } -pub fn use_route<'a>(cx: &'a ScopeState) -> UseRoute<'a> { +/// This hook provides access to information about the current location in the +/// context of a [`Router`]. If this function is called outside of a `Router` +/// component it will panic. +pub fn use_route(cx: &ScopeState) -> UseRoute { let router = cx .consume_context::() .expect("Cannot call use_route outside the scope of a Router component") .clone(); - UseRoute { router, cx } + UseRoute { router } } From 7dd7944487d369e3c002991753487e869bfdc4a1 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Fri, 14 Jan 2022 09:56:46 +0800 Subject: [PATCH 30/79] Update ZH_CN.md --- notes/README/ZH_CN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index eaf364c80..dfc5b3e89 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -61,7 +61,7 @@
-Dioxus 是一个可移植、高性能的框架,用于在 Rust 中建跨平台的用户界面。 +Dioxus 是一个可移植、高性能的框架,用于在 Rust 中构建跨平台的用户界面。 ```rust fn app(cx: Scope) -> Element { @@ -165,4 +165,4 @@ Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但 这个项目使用 [MIT 协议]. -[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE \ No newline at end of file +[MIT 协议]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE From 05331ddd8033f6997d4916179b62f4d62f832988 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 13 Jan 2022 22:51:01 -0500 Subject: [PATCH 31/79] feat: wire up both desktop and web --- packages/desktop/src/index.html | 1 + packages/jsinterpreter/README.md | 8 + packages/jsinterpreter/interpreter.js | 133 ++++--- packages/jsinterpreter/interpreter.ts | 77 ++-- packages/jsinterpreter/tsconfig.json | 14 +- packages/web/src/bindings.rs | 16 +- packages/web/src/cache.rs | 2 +- packages/web/src/cfg.rs | 5 +- packages/web/src/dom.rs | 94 ++--- packages/web/src/interpreter.js | 529 -------------------------- packages/web/src/lib.rs | 1 - packages/web/src/nodeslab.rs | 34 -- packages/web/src/olddom.rs | 68 ++++ packages/web/src/rehydrate.rs | 4 +- packages/web/src/ric_raf.rs | 3 +- 15 files changed, 261 insertions(+), 728 deletions(-) create mode 100644 packages/jsinterpreter/README.md delete mode 100644 packages/web/src/interpreter.js delete mode 100644 packages/web/src/nodeslab.rs diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 888ab8437..c17a63d2a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -15,5 +15,6 @@ + diff --git a/packages/jsinterpreter/README.md b/packages/jsinterpreter/README.md new file mode 100644 index 000000000..bab57ac16 --- /dev/null +++ b/packages/jsinterpreter/README.md @@ -0,0 +1,8 @@ +# JS Interpreter + +After diffing old and new trees, the Dioxus VirtualDom produces patches that are used to modify the existing Dom. We can send these patches anywhere - including targets without WASM support. + +In renderers with support for JavaScript, we use the interpreter from this repository - written in TypeScript - to patch the Dom. This lets us circumvent any overhead on the Rust <-> Dom boundary and keep consistency in our interpreter implementation in web/webview targets. + + +For now - both Dioxus Web and Dioxus Desktop (webview) use the same interpreter code with tweaks. diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index 6139280f8..453b34406 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,23 +1,22 @@ -"use strict"; -exports.__esModule = true; -exports.Interpreter = void 0; function serialize_event(event) { var _a, _b; switch (event.type) { case "copy": case "cut": - case "past": + case "past": { return {}; + } case "compositionend": case "compositionstart": - case "compositionupdate": + case "compositionupdate": { var data = event.data; return { - data: data + data: data, }; + } case "keydown": case "keypress": - case "keyup": + case "keyup": { var _c = event, charCode = _c.charCode, key = _c.key, altKey = _c.altKey, ctrlKey = _c.ctrlKey, metaKey = _c.metaKey, keyCode = _c.keyCode, shiftKey = _c.shiftKey, location_1 = _c.location, repeat = _c.repeat, which = _c.which; return { char_code: charCode, @@ -30,12 +29,14 @@ function serialize_event(event) { location: location_1, repeat: repeat, which: which, - locale: "locale" + locale: "locale", }; + } case "focus": - case "blur": + case "blur": { return {}; - case "change": + } + case "change": { var target = event.target; var value = void 0; if (target.type === "checkbox" || target.type === "radio") { @@ -45,19 +46,20 @@ function serialize_event(event) { value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; } return { - value: value + value: value, }; + } case "input": case "invalid": case "reset": case "submit": { - var target_1 = event.target; - var value_1 = (_b = target_1.value) !== null && _b !== void 0 ? _b : target_1.textContent; - if (target_1.type == "checkbox") { - value_1 = target_1.checked ? "true" : "false"; + var target = event.target; + var value = (_b = target.value) !== null && _b !== void 0 ? _b : target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; } return { - value: value_1 + value: value, }; } case "click": @@ -78,20 +80,20 @@ function serialize_event(event) { case "mouseout": case "mouseover": case "mouseup": { - var _d = event, altKey_1 = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey_1 = _d.ctrlKey, metaKey_1 = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey_1 = _d.shiftKey; + var _d = event, altKey = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey = _d.ctrlKey, metaKey = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey = _d.shiftKey; return { - alt_key: altKey_1, + alt_key: altKey, button: button, buttons: buttons, client_x: clientX, client_y: clientY, - ctrl_key: ctrlKey_1, - meta_key: metaKey_1, + ctrl_key: ctrlKey, + meta_key: metaKey, page_x: pageX, page_y: pageY, screen_x: screenX_1, screen_y: screenY_1, - shift_key: shiftKey_1 + shift_key: shiftKey, }; } case "pointerdown": @@ -104,20 +106,20 @@ function serialize_event(event) { case "pointerleave": case "pointerover": case "pointerout": { - var _e = event, altKey_2 = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey_2 = _e.ctrlKey, metaKey_2 = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey_2 = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary; + var _e = event, altKey = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey = _e.ctrlKey, metaKey = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary; return { - alt_key: altKey_2, + alt_key: altKey, button: button, buttons: buttons, client_x: clientX, client_y: clientY, - ctrl_key: ctrlKey_2, - meta_key: metaKey_2, + ctrl_key: ctrlKey, + meta_key: metaKey, page_x: pageX, page_y: pageY, screen_x: screenX_2, screen_y: screenY_2, - shift_key: shiftKey_2, + shift_key: shiftKey, pointer_id: pointerId, width: width, height: height, @@ -127,35 +129,37 @@ function serialize_event(event) { tilt_y: tiltY, twist: twist, pointer_type: pointerType, - is_primary: isPrimary + is_primary: isPrimary, }; } - case "select": + case "select": { return {}; + } case "touchcancel": case "touchend": case "touchmove": case "touchstart": { - var _f = event, altKey_3 = _f.altKey, ctrlKey_3 = _f.ctrlKey, metaKey_3 = _f.metaKey, shiftKey_3 = _f.shiftKey; + var _f = event, altKey = _f.altKey, ctrlKey = _f.ctrlKey, metaKey = _f.metaKey, shiftKey = _f.shiftKey; return { // changed_touches: event.changedTouches, // target_touches: event.targetTouches, // touches: event.touches, - alt_key: altKey_3, - ctrl_key: ctrlKey_3, - meta_key: metaKey_3, - shift_key: shiftKey_3 + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, }; } - case "scroll": + case "scroll": { return {}; + } case "wheel": { var _g = event, deltaX = _g.deltaX, deltaY = _g.deltaY, deltaZ = _g.deltaZ, deltaMode = _g.deltaMode; return { delta_x: deltaX, delta_y: deltaY, delta_z: deltaZ, - delta_mode: deltaMode + delta_mode: deltaMode, }; } case "animationstart": @@ -165,7 +169,7 @@ function serialize_event(event) { return { animation_name: animationName, elapsed_time: elapsedTime, - pseudo_element: pseudoElement + pseudo_element: pseudoElement, }; } case "transitionend": { @@ -173,7 +177,7 @@ function serialize_event(event) { return { property_name: propertyName, elapsed_time: elapsedTime, - pseudo_element: pseudoElement + pseudo_element: pseudoElement, }; } case "abort": @@ -198,12 +202,15 @@ function serialize_event(event) { case "suspend": case "timeupdate": case "volumechange": - case "waiting": + case "waiting": { return {}; - case "toggle": + } + case "toggle": { return {}; - default: + } + default: { return {}; + } } } var bool_attrs = { @@ -214,7 +221,7 @@ var bool_attrs = { autoplay: true, checked: true, controls: true, - "default": true, + default: true, defer: true, disabled: true, formnovalidate: true, @@ -232,13 +239,14 @@ var bool_attrs = { required: true, reversed: true, selected: true, - truespeed: true + truespeed: true, }; var Interpreter = /** @class */ (function () { function Interpreter(root) { this.root = root; this.stack = [root]; this.listeners = {}; + this.handlers = {}; this.lastNodeWasText = false; this.nodes = [root]; } @@ -303,17 +311,28 @@ var Interpreter = /** @class */ (function () { this.stack.push(el); this.nodes[root] = el; }; - Interpreter.prototype.NewEventListener = function (event_name, scope, root) { - console.log('new event listener', event_name, root, scope); + Interpreter.prototype.NewEventListener = function (event_name, scope, root, handler) { + // console.log('new event listener', event_name, root, scope); var element = this.nodes[root]; element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(root)); - // if (!this.listeners[event_name]) { - // this.listeners[event_name] = handler; - // this.root.addEventListener(event_name, handler); - // } + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + else { + this.listeners[event_name]++; + } }; - Interpreter.prototype.RemoveEventListener = function (root, event_name, scope) { - // + Interpreter.prototype.RemoveEventListener = function (root, event_name) { + var element = this.nodes[root]; + element.removeAttribute("dioxus-event-".concat(event_name)); + 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]; + } }; Interpreter.prototype.SetText = function (root, text) { this.nodes[root].textContent = text; @@ -369,7 +388,6 @@ var Interpreter = /** @class */ (function () { } }; Interpreter.prototype.handleEdits = function (edits) { - console.log("handling edits ", edits); this.stack.push(this.root); for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { var edit = edits_1[_i]; @@ -409,13 +427,13 @@ var Interpreter = /** @class */ (function () { this.CreatePlaceholder(edit.root); break; case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name, edit.scope); + this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": // todo: only on desktop should we make our own handler var handler = function (event) { var target = event.target; - console.log("event", event); + // console.log("event", event); if (target != null) { var real_id = target.getAttribute("dioxus-id"); var should_prevent_default = target.getAttribute("dioxus-prevent-default"); @@ -429,12 +447,11 @@ var Interpreter = /** @class */ (function () { window.rpc.call("user_event", { event: edit.event_name, mounted_dom_id: parseInt(real_id), - contents: contents + contents: contents, }); } }; - this.NewEventListener(edit.event_name, edit.scope, edit.root); - // this.NewEventListener(edit, handler); + this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); break; case "SetText": this.SetText(edit.root, edit.text); @@ -449,7 +466,7 @@ var Interpreter = /** @class */ (function () { }; return Interpreter; }()); -exports.Interpreter = Interpreter; +export { Interpreter }; function main() { var root = window.document.getElementById("main"); if (root != null) { @@ -457,4 +474,4 @@ function main() { window.rpc.call("initialize"); } } -main(); +//# sourceMappingURL=interpreter.js.map \ No newline at end of file diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index e3547effb..17617a209 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -1,23 +1,23 @@ - - function serialize_event(event: Event) { switch (event.type) { case "copy": case "cut": - case "past": + case "past": { return {}; + } case "compositionend": case "compositionstart": - case "compositionupdate": + case "compositionupdate": { let { data } = (event as CompositionEvent); return { data, }; + } case "keydown": case "keypress": - case "keyup": + case "keyup": { let { charCode, key, @@ -44,12 +44,14 @@ function serialize_event(event: Event) { which: which, locale: "locale", }; + } case "focus": - case "blur": + case "blur": { return {}; + } - case "change": + case "change": { let target = event.target as HTMLInputElement; let value; if (target.type === "checkbox" || target.type === "radio") { @@ -61,6 +63,7 @@ function serialize_event(event: Event) { return { value: value, }; + } case "input": case "invalid": @@ -187,8 +190,9 @@ function serialize_event(event: Event) { }; } - case "select": + case "select": { return {}; + } case "touchcancel": case "touchend": @@ -211,8 +215,10 @@ function serialize_event(event: Event) { }; } - case "scroll": + case "scroll": { return {}; + } + case "wheel": { const { deltaX, @@ -278,14 +284,17 @@ function serialize_event(event: Event) { case "suspend": case "timeupdate": case "volumechange": - case "waiting": + case "waiting": { return {}; + } - case "toggle": + case "toggle": { return {}; + } - default: + default: { return {}; + } } } @@ -321,7 +330,8 @@ const bool_attrs = { export class Interpreter { root: Element; stack: Element[]; - listeners: { [key: string]: (event: Event) => void }; + listeners: { [key: string]: number }; + handlers: { [key: string]: (evt: Event) => void }; lastNodeWasText: boolean; nodes: Element[]; @@ -329,8 +339,8 @@ export class Interpreter { constructor(root: Element) { this.root = root; this.stack = [root]; - this.listeners = { - }; + this.listeners = {}; + this.handlers = {}; this.lastNodeWasText = false; this.nodes = [root]; } @@ -412,23 +422,36 @@ export class Interpreter { this.nodes[root] = el; } - NewEventListener(event_name: string, scope: number, root: number) { - console.log('new event listener', event_name, root, scope); + NewEventListener(event_name: string, scope: number, root: number, handler: (evt: Event) => void) { + // console.log('new event listener', event_name, root, scope); const element = this.nodes[root]; element.setAttribute( `dioxus-event-${event_name}`, `${scope}.${root}` ); - // if (!this.listeners[event_name]) { - // this.listeners[event_name] = handler; - // this.root.addEventListener(event_name, handler); - // } + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } else { + this.listeners[event_name]++; + } } + RemoveEventListener(root: number, event_name: string) { + const element = this.nodes[root]; + element.removeAttribute( + `dioxus-event-${event_name}` + ); - RemoveEventListener(root: number, event_name: string, scope: number) { - // + 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]; + } } @@ -492,7 +515,6 @@ export class Interpreter { } handleEdits(edits: DomEdit[]) { - console.log("handling edits ", edits); this.stack.push(this.root); for (let edit of edits) { @@ -533,13 +555,13 @@ export class Interpreter { this.CreatePlaceholder(edit.root); break; case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name, edit.scope); + this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": // todo: only on desktop should we make our own handler let handler = (event: Event) => { const target = event.target as Element | null; - console.log("event", event); + // console.log("event", event); if (target != null) { const real_id = target.getAttribute(`dioxus-id`); @@ -565,8 +587,7 @@ export class Interpreter { }); } }; - this.NewEventListener(edit.event_name, edit.scope, edit.root); - // this.NewEventListener(edit, handler); + this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); break; case "SetText": this.SetText(edit.root, edit.text); diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json index 8f39daddf..883760881 100644 --- a/packages/jsinterpreter/tsconfig.json +++ b/packages/jsinterpreter/tsconfig.json @@ -1,9 +1,13 @@ { "compilerOptions": { - "target": "es5", - "module": "commonjs", - "strict": true, - "outDir": "dist", - "sourceMap": true + "target": "ES5", + "module": "ESNext", + "lib": [ + "es2015", + "es5", + "es6", + "dom" + ], + "strict": true } } diff --git a/packages/web/src/bindings.rs b/packages/web/src/bindings.rs index d65fc1300..cc4202886 100644 --- a/packages/web/src/bindings.rs +++ b/packages/web/src/bindings.rs @@ -1,8 +1,8 @@ -use dioxus_core::DomEdit; -use wasm_bindgen::{convert::IntoWasmAbi, describe::WasmDescribe, prelude::*}; -use web_sys::{Element, HtmlElement, Node}; +use js_sys::Function; +use wasm_bindgen::prelude::*; +use web_sys::{Element, Node}; -#[wasm_bindgen(module = "/src/interpreter.js")] +#[wasm_bindgen(module = "/../jsinterpreter/interpreter.js")] extern "C" { pub type Interpreter; @@ -43,7 +43,13 @@ extern "C" { pub fn CreatePlaceholder(this: &Interpreter, root: u64); #[wasm_bindgen(method)] - pub fn NewEventListener(this: &Interpreter, name: &str, scope: usize, root: u64); + pub fn NewEventListener( + this: &Interpreter, + name: &str, + scope: usize, + root: u64, + handler: &Function, + ); #[wasm_bindgen(method)] pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str); diff --git a/packages/web/src/cache.rs b/packages/web/src/cache.rs index 190b505c1..e17203a65 100644 --- a/packages/web/src/cache.rs +++ b/packages/web/src/cache.rs @@ -1,4 +1,4 @@ -pub static BUILTIN_INTERNED_STRINGS: &[&'static str] = &[ +pub static BUILTIN_INTERNED_STRINGS: &[&str] = &[ // Important tags to dioxus "dioxus-id", "dioxus", diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index 0fa0dc48c..cc5d18bf7 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -3,10 +3,9 @@ /// This struct helps configure the specifics of hydration and render destination for WebSys. /// /// # Example +/// /// ```rust, ignore -/// fn main() { -/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot")) -/// } +/// dioxus::web::launch(App, |cfg| cfg.hydrate(true).root_name("myroot")) /// ``` pub struct WebConfig { pub(crate) hydrate: bool, diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 918af4c4a..c4bd6a6e5 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -8,51 +8,50 @@ //! - Partial delegation?> use crate::bindings::Interpreter; -use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent}; -use fxhash::FxHashMap; -use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc}; -use wasm_bindgen::{closure::Closure, JsCast, JsValue}; -use web_sys::{ - CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement, - HtmlOptionElement, HtmlTextAreaElement, Node, -}; +use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent}; +use js_sys::Function; +use std::{any::Any, rc::Rc, sync::Arc}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{Document, Element, Event, HtmlElement}; -use crate::{nodeslab::NodeSlab, WebConfig}; +use crate::WebConfig; pub struct WebsysDom { - document: Document, - pub interpreter: Interpreter, pub(crate) root: Element, - sender_callback: Rc, - - // map of listener types to number of those listeners - // This is roughly a delegater - // TODO: check how infero delegates its events - some are more performant - listeners: FxHashMap<&'static str, ListenerEntry>, + handler: Closure, } -type ListenerEntry = (usize, Closure); - impl WebsysDom { pub fn new(cfg: WebConfig, sender_callback: Rc) -> Self { - let document = load_document(); + // eventually, we just want to let the interpreter do all the work of decoding events into our event type + let callback: Box = Box::new(move |event: &web_sys::Event| { + if let Ok(synthetic_event) = decode_trigger(event) { + // Try to prevent default if the attribute is set + if let Some(target) = event.target() { + if let Some(node) = target.dyn_ref::() { + if let Some(name) = node.get_attribute("dioxus-prevent-default") { + if name == synthetic_event.name + || name.trim_start_matches("on") == synthetic_event.name + { + log::trace!("Preventing default"); + event.prevent_default(); + } + } + } + } - let listeners = FxHashMap::default(); - - let mut stack = Stack::with_capacity(10); + sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event)) + } + }); let root = load_document().get_element_by_id(&cfg.rootname).unwrap(); - let root_node = root.clone().dyn_into::().unwrap(); - stack.push(root_node); Self { interpreter: Interpreter::new(root.clone()), - listeners, - document, - sender_callback, + handler: Closure::wrap(callback), root, } } @@ -78,7 +77,12 @@ impl WebsysDom { event_name, scope, root, - } => self.interpreter.NewEventListener(event_name, scope.0, root), + } => { + // + let handler: &Function = self.handler.as_ref().unchecked_ref(); + self.interpreter + .NewEventListener(event_name, scope.0, root, handler); + } DomEdit::RemoveEventListener { root, event } => { self.interpreter.RemoveEventListener(root, event) } @@ -97,37 +101,6 @@ impl WebsysDom { } } -#[derive(Debug, Default)] -struct Stack { - list: Vec, -} - -impl Stack { - #[inline] - fn with_capacity(cap: usize) -> Self { - Stack { - list: Vec::with_capacity(cap), - } - } - - #[inline] - fn push(&mut self, node: Node) { - self.list.push(node); - } - - #[inline] - fn pop(&mut self) -> Node { - self.list.pop().unwrap() - } - - fn top(&self) -> &Node { - match self.list.last() { - Some(a) => a, - None => panic!("Called 'top' of an empty stack, make sure to push the root first"), - } - } -} - pub struct DioxusWebsysEvent(web_sys::Event); // safety: currently the web is not multithreaded and our VirtualDom exists on the same thread @@ -267,6 +240,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc Arc::new(()), "wheel" => { let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap(); diff --git a/packages/web/src/interpreter.js b/packages/web/src/interpreter.js deleted file mode 100644 index 1d0f14c68..000000000 --- a/packages/web/src/interpreter.js +++ /dev/null @@ -1,529 +0,0 @@ -"use strict"; -var exports = {}; -exports.__esModule = true; -exports.Interpreter = void 0; -function serialize_event(event) { - var _a, _b; - switch (event.type) { - case "copy": - case "cut": - case "past": - return {}; - case "compositionend": - case "compositionstart": - case "compositionupdate": - var data = event.data; - return { - data: data, - }; - case "keydown": - case "keypress": - case "keyup": - var _c = event, - charCode = _c.charCode, - key = _c.key, - altKey = _c.altKey, - ctrlKey = _c.ctrlKey, - metaKey = _c.metaKey, - keyCode = _c.keyCode, - shiftKey = _c.shiftKey, - location_1 = _c.location, - repeat = _c.repeat, - which = _c.which; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location_1, - repeat: repeat, - which: which, - locale: "locale", - }; - case "focus": - case "blur": - return {}; - case "change": - var target = event.target; - var value = void 0; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = - (_a = target.value) !== null && _a !== void 0 - ? _a - : target.textContent; - } - return { - value: value, - }; - case "input": - case "invalid": - case "reset": - case "submit": { - var target_1 = event.target; - var value_1 = - (_b = target_1.value) !== null && _b !== void 0 - ? _b - : target_1.textContent; - if (target_1.type == "checkbox") { - value_1 = target_1.checked ? "true" : "false"; - } - return { - value: value_1, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - var _d = event, - altKey_1 = _d.altKey, - button = _d.button, - buttons = _d.buttons, - clientX = _d.clientX, - clientY = _d.clientY, - ctrlKey_1 = _d.ctrlKey, - metaKey_1 = _d.metaKey, - pageX = _d.pageX, - pageY = _d.pageY, - screenX_1 = _d.screenX, - screenY_1 = _d.screenY, - shiftKey_1 = _d.shiftKey; - return { - alt_key: altKey_1, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey_1, - meta_key: metaKey_1, - page_x: pageX, - page_y: pageY, - screen_x: screenX_1, - screen_y: screenY_1, - shift_key: shiftKey_1, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - var _e = event, - altKey_2 = _e.altKey, - button = _e.button, - buttons = _e.buttons, - clientX = _e.clientX, - clientY = _e.clientY, - ctrlKey_2 = _e.ctrlKey, - metaKey_2 = _e.metaKey, - pageX = _e.pageX, - pageY = _e.pageY, - screenX_2 = _e.screenX, - screenY_2 = _e.screenY, - shiftKey_2 = _e.shiftKey, - pointerId = _e.pointerId, - width = _e.width, - height = _e.height, - pressure = _e.pressure, - tangentialPressure = _e.tangentialPressure, - tiltX = _e.tiltX, - tiltY = _e.tiltY, - twist = _e.twist, - pointerType = _e.pointerType, - isPrimary = _e.isPrimary; - return { - alt_key: altKey_2, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey_2, - meta_key: metaKey_2, - page_x: pageX, - page_y: pageY, - screen_x: screenX_2, - screen_y: screenY_2, - shift_key: shiftKey_2, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": - return {}; - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - var _f = event, - altKey_3 = _f.altKey, - ctrlKey_3 = _f.ctrlKey, - metaKey_3 = _f.metaKey, - shiftKey_3 = _f.shiftKey; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey_3, - ctrl_key: ctrlKey_3, - meta_key: metaKey_3, - shift_key: shiftKey_3, - }; - } - case "scroll": - return {}; - case "wheel": { - var _g = event, - deltaX = _g.deltaX, - deltaY = _g.deltaY, - deltaZ = _g.deltaZ, - deltaMode = _g.deltaMode; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - var _h = event, - animationName = _h.animationName, - elapsedTime = _h.elapsedTime, - pseudoElement = _h.pseudoElement; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - var _j = event, - propertyName = _j.propertyName, - elapsedTime = _j.elapsedTime, - pseudoElement = _j.pseudoElement; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": - return {}; - case "toggle": - return {}; - default: - return {}; - } -} -var bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; -export var Interpreter = /** @class */ (function () { - function Interpreter(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - Interpreter.prototype.top = function () { - return this.stack[this.stack.length - 1]; - }; - Interpreter.prototype.pop = function () { - return this.stack.pop(); - }; - Interpreter.prototype.PushRoot = function (root) { - var node = this.nodes[root]; - this.stack.push(node); - }; - Interpreter.prototype.AppendChildren = function (many) { - var root = this.stack[this.stack.length - (1 + many)]; - var to_add = this.stack.splice(this.stack.length - many); - for (var i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - }; - Interpreter.prototype.ReplaceWith = function (root_id, m) { - var root = this.nodes[root_id]; - var els = this.stack.splice(this.stack.length - m); - root.replaceWith.apply(root, els); - }; - Interpreter.prototype.InsertAfter = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.after.apply(old, new_nodes); - }; - Interpreter.prototype.InsertBefore = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.before.apply(old, new_nodes); - }; - Interpreter.prototype.Remove = function (root) { - var node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - }; - Interpreter.prototype.CreateTextNode = function (text, root) { - // todo: make it so the types are okay - var node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - }; - Interpreter.prototype.CreateElement = function (tag, root) { - var el = document.createElement(tag); - el.setAttribute("dioxus-id", "".concat(root)); - this.nodes[root] = el; - this.stack.push(el); - }; - Interpreter.prototype.CreateElementNs = function (tag, root, ns) { - var el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.CreatePlaceholder = function (root) { - var el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.NewEventListener = function (event_name, scope, root) { - console.log("new event listener", event_name, root, scope); - var element = this.nodes[root]; - element.setAttribute( - "dioxus-event-".concat(event_name), - "".concat(scope, ".").concat(root) - ); - // if (!this.listeners[event_name]) { - // this.listeners[event_name] = handler; - // this.root.addEventListener(event_name, handler); - // } - }; - Interpreter.prototype.RemoveEventListener = function ( - root, - event_name, - scope - ) { - // - }; - Interpreter.prototype.SetText = function (root, text) { - this.nodes[root].textContent = text; - }; - Interpreter.prototype.SetAttribute = function (root, field, value, ns) { - var name = field; - var node = this.nodes[root]; - if (ns == "style") { - // @ts-ignore - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - }; - Interpreter.prototype.RemoveAttribute = function (root, name) { - var node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - }; - Interpreter.prototype.handleEdits = function (edits) { - console.log("handling edits ", edits); - this.stack.push(this.root); - for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { - var edit = edits_1[_i]; - this.handleEdit(edit); - } - }; - Interpreter.prototype.handleEdit = function (edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name, edit.scope); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - var handler = function (event) { - var target = event.target; - console.log("event", event); - if (target != null) { - var real_id = target.getAttribute("dioxus-id"); - var should_prevent_default = target.getAttribute( - "dioxus-prevent-default" - ); - var contents = serialize_event(event); - if (should_prevent_default === "on".concat(event.type)) { - event.preventDefault(); - } - if (real_id == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, edit.root); - // this.NewEventListener(edit, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - }; - return Interpreter; -})(); -exports.Interpreter = Interpreter; -function main() { - var root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index ba9398276..8c0c0c3b4 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -65,7 +65,6 @@ pub(crate) mod bindings; mod cache; mod cfg; mod dom; -mod nodeslab; mod rehydrate; mod ric_raf; diff --git a/packages/web/src/nodeslab.rs b/packages/web/src/nodeslab.rs deleted file mode 100644 index b986bd34e..000000000 --- a/packages/web/src/nodeslab.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. - -use std::ops::{Index, IndexMut}; -use web_sys::Node; - -pub(crate) struct NodeSlab { - nodes: Vec>, -} - -impl NodeSlab { - pub fn new(capacity: usize) -> NodeSlab { - let nodes = Vec::with_capacity(capacity); - NodeSlab { nodes } - } -} -impl Index for NodeSlab { - type Output = Option; - fn index(&self, index: usize) -> &Self::Output { - &self.nodes[index] - } -} - -impl IndexMut for NodeSlab { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - if index >= self.nodes.capacity() * 3 { - panic!("Trying to mutate an element way too far out of bounds"); - } - - if index + 1 > self.nodes.len() { - self.nodes.resize_with(index + 1, || None); - } - &mut self.nodes[index] - } -} diff --git a/packages/web/src/olddom.rs b/packages/web/src/olddom.rs index 364f1a1d7..8654ced90 100644 --- a/packages/web/src/olddom.rs +++ b/packages/web/src/olddom.rs @@ -787,3 +787,71 @@ fn event_name_from_typ(typ: &str) -> &'static str { } } } + + +//! This module provides a mirror of the VirtualDOM Element Slab using a Vector. + +use std::ops::{Index, IndexMut}; +use web_sys::Node; + +pub(crate) struct NodeSlab { + nodes: Vec>, +} + +impl NodeSlab { + pub fn new(capacity: usize) -> NodeSlab { + let nodes = Vec::with_capacity(capacity); + NodeSlab { nodes } + } +} +impl Index for NodeSlab { + type Output = Option; + fn index(&self, index: usize) -> &Self::Output { + &self.nodes[index] + } +} + +impl IndexMut for NodeSlab { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + if index >= self.nodes.capacity() * 3 { + panic!("Trying to mutate an element way too far out of bounds"); + } + + if index + 1 > self.nodes.len() { + self.nodes.resize_with(index + 1, || None); + } + &mut self.nodes[index] + } +} + + +#[derive(Debug, Default)] +struct Stack { + list: Vec, +} + +impl Stack { + #[inline] + fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + #[inline] + fn push(&mut self, node: Node) { + self.list.push(node); + } + + #[inline] + fn pop(&mut self) -> Node { + self.list.pop().unwrap() + } + + fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 25b126036..b62cba753 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -118,7 +118,7 @@ impl WebsysDom { // we cant have the last node be text let mut last_node_was_text = false; for child in vel.children { - self.rehydrate_single(nodes, place, dom, &child, &mut last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?; } place.pop(); @@ -146,7 +146,7 @@ impl WebsysDom { VNode::Fragment(el) => { for el in el.children { - self.rehydrate_single(nodes, place, dom, &el, last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?; } } diff --git a/packages/web/src/ric_raf.rs b/packages/web/src/ric_raf.rs index 29c97bcba..2ac0668b9 100644 --- a/packages/web/src/ric_raf.rs +++ b/packages/web/src/ric_raf.rs @@ -68,8 +68,7 @@ impl RafLoop { let ric_fn = self.ric_closure.as_ref().dyn_ref::().unwrap(); let _cb_id: u32 = self.window.request_idle_callback(ric_fn).unwrap(); let deadline = self.ric_receiver.recv().await.unwrap(); - let deadline = TimeoutFuture::new(deadline); - deadline + TimeoutFuture::new(deadline) } pub async fn wait_for_raf(&self) { From 81c094ed29fcdc5c6099492dd6ab09a59b79252c Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 14 Jan 2022 14:55:12 -0600 Subject: [PATCH 32/79] Fix handling of re-renders in the Router Previously the router just stored a `root_found` boolean after it picked a route. But on re-render it would just always return false from `should_render` if this was true. This just aborted routing after a future resolved (or anything else that triggered a re-render). Now we store the matching ScopeId and check that against our routes in a re-render so we actually do re-render the matching route. --- packages/router/src/service.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 2fac8509b..00642414f 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -11,7 +11,7 @@ pub struct RouterService { pub(crate) regen_route: Rc, history: Rc>, slots: Rc>>, - root_found: Rc>, + root_found: Rc>>, cur_path_params: Rc>>, listener: HistoryListener, } @@ -39,11 +39,11 @@ impl RouterService { let _slots = slots.clone(); - let root_found = Rc::new(Cell::new(false)); + let root_found = Rc::new(Cell::new(None)); let regen = regen_route.clone(); let _root_found = root_found.clone(); let listener = history.listen(move || { - _root_found.set(false); + _root_found.set(None); // checking if the route is valid is cheap, so we do it for (slot, root) in _slots.borrow_mut().iter().rev() { log::trace!("regenerating slot {:?} for root '{}'", slot, root); @@ -74,8 +74,13 @@ impl RouterService { pub fn should_render(&self, scope: ScopeId) -> bool { log::trace!("Should render scope id {:?}?", scope); - if self.root_found.get() { - log::trace!(" no - because root_found is true"); + if let Some(root_id) = self.root_found.get() { + log::trace!(" we already found a root with scope id {:?}", root_id); + if root_id == scope { + log::trace!(" yes - it's a match"); + return true; + } + log::trace!(" no - it's not a match"); return false; } @@ -89,7 +94,7 @@ impl RouterService { // fallback logic match root { - Some((_id, route)) => { + Some((id, route)) => { log::trace!( " matched given scope id {:?} with route root '{}'", scope, @@ -97,13 +102,13 @@ impl RouterService { ); if let Some(params) = route_matches_path(route, path) { log::trace!(" and it matches the current path '{}'", path); - self.root_found.set(true); + self.root_found.set(Some(*id)); *self.cur_path_params.borrow_mut() = params; true } else { if route == "" { log::trace!(" and the route is the root, so we will use that without a better match"); - self.root_found.set(true); + self.root_found.set(Some(*id)); true } else { log::trace!(" and the route '{}' is not the root nor does it match the current path", route); From 9b282d877ba0fe2463687cde7ea899b7f8510425 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 14 Jan 2022 15:40:01 -0600 Subject: [PATCH 33/79] Don't expect all components to have a scope in ScopeArena.ensure_drop_safety Not all components will be mounted when using a Router, so we cannot assume all components have a scope. --- packages/core/src/scopes.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 65511470e..5b8f527c5 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -198,12 +198,9 @@ impl ScopeArena { // run the hooks (which hold an &mut Reference) // recursively call ensure_drop_safety on all children items.borrowed_props.drain(..).for_each(|comp| { - let scope_id = comp - .scope - .get() - .expect("VComponents should be associated with a valid Scope"); - - self.ensure_drop_safety(scope_id); + if let Some(scope_id) = comp.scope.get() { + self.ensure_drop_safety(scope_id); + } drop(comp.props.take()); }); From e47ead5347ce778935f8f2127fcb14f520eda0ce Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 15 Jan 2022 20:17:48 -0500 Subject: [PATCH 34/79] fix: allow eventhandler to derive default --- packages/core/src/nodes.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index d662b4998..93d4c8b10 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -351,8 +351,9 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// } /// /// ``` +#[derive(Default)] pub struct EventHandler<'bump, T = ()> { - pub callback: &'bump RefCell>>, + pub callback: RefCell>>, } impl EventHandler<'_, T> { @@ -367,15 +368,6 @@ impl EventHandler<'_, T> { } } -impl Copy for EventHandler<'_, T> {} -impl Clone for EventHandler<'_, T> { - fn clone(&self) -> Self { - Self { - callback: self.callback, - } - } -} - /// Virtual Components for custom user-defined components /// Only supports the functional syntax pub struct VComponent<'src> { @@ -677,7 +669,7 @@ impl<'a> NodeFactory<'a> { pub fn event_handler(self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> { let handler: &mut dyn FnMut(T) = self.bump.alloc(f); let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; - let callback = self.bump.alloc(RefCell::new(Some(caller))); + let callback = RefCell::new(Some(caller)); EventHandler { callback } } } From 036a0ff49a7dade0e04c9c07071a1ff49133ee24 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 16 Jan 2022 15:13:31 -0500 Subject: [PATCH 35/79] docs: add comments for the Handler --- packages/core/src/nodes.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 93d4c8b10..80de92917 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -357,12 +357,14 @@ pub struct EventHandler<'bump, T = ()> { } impl EventHandler<'_, T> { + /// Call this event handler with the appropriate event type pub fn call(&self, event: T) { if let Some(callback) = self.callback.borrow_mut().as_mut() { callback(event); } } + /// Forcibly drop the internal handler callback, releasing memory pub fn release(&self) { self.callback.replace(None); } From 47bc4e4a44a7d08d4f42102d13f0766d9d6bf358 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 16 Jan 2022 15:27:41 -0500 Subject: [PATCH 36/79] feat: add "optional" flag for props --- examples/optional_props.rs | 16 +++++++++++++++- packages/core-macro/src/props/mod.rs | 8 ++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/examples/optional_props.rs b/examples/optional_props.rs index 47ccbeac0..9485a2aeb 100644 --- a/examples/optional_props.rs +++ b/examples/optional_props.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Example: README.md showcase //! //! The example from the README.md. @@ -14,6 +16,7 @@ fn app(cx: Scope) -> Element { a: "asd".to_string(), c: Some("asd".to_string()), d: "asd".to_string(), + e: "asd".to_string(), } }) } @@ -30,8 +33,19 @@ struct ButtonProps { #[props(default, strip_option)] d: Option, + + #[props(optional)] + e: Option, } fn Button(cx: Scope) -> Element { - todo!() + cx.render(rsx! { + button { + "{cx.props.a}" + "{cx.props.b:?}" + "{cx.props.c:?}" + "{cx.props.d:?}" + "{cx.props.e:?}" + } + }) } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index f5443272b..d569e711a 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -364,6 +364,14 @@ mod field_info { Some(syn::parse(quote!(Default::default()).into()).unwrap()); Ok(()) } + + "optional" => { + self.default = + Some(syn::parse(quote!(Default::default()).into()).unwrap()); + self.strip_option = true; + Ok(()) + } + _ => { macro_rules! handle_fields { ( $( $flag:expr, $field:ident, $already:expr; )* ) => { From 58839f47bae3c49cadd4b41da9a5debebb1def99 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 16 Jan 2022 15:56:48 -0500 Subject: [PATCH 37/79] update: modify usestate to be borrowed --- examples/calculator.rs | 6 +- examples/crm.rs | 2 +- examples/disabled.rs | 2 +- examples/dog_app.rs | 2 +- examples/readme.rs | 6 +- examples/todomvc.rs | 34 ++-- packages/hooks/src/usestate.rs | 212 +++++++++++++++++++++++++ packages/hooks/src/usestate/handle.rs | 215 -------------------------- packages/hooks/src/usestate/mod.rs | 78 ---------- packages/hooks/src/usestate/owned.rs | 99 ------------ 10 files changed, 237 insertions(+), 419 deletions(-) create mode 100644 packages/hooks/src/usestate.rs delete mode 100644 packages/hooks/src/usestate/handle.rs delete mode 100644 packages/hooks/src/usestate/mod.rs delete mode 100644 packages/hooks/src/usestate/owned.rs diff --git a/examples/calculator.rs b/examples/calculator.rs index a849833a3..f7668de88 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -18,7 +18,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let display_value: UseState = use_state(&cx, || String::from("0")); + let display_value = use_state(&cx, || String::from("0")); let input_digit = move |num: u8| { if display_value.get() == "0" { @@ -66,11 +66,11 @@ fn app(cx: Scope) -> Element { class: "calculator-key key-clear", onclick: move |_| { display_value.set(String::new()); - if display_value != "" { + if *display_value != "" { display_value.set("0".into()); } }, - [if display_value == "" { "C" } else { "AC" }] + [if *display_value == "" { "C" } else { "AC" }] } button { class: "calculator-key key-sign", diff --git a/examples/crm.rs b/examples/crm.rs index d4f3d1579..a206e0f43 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -38,7 +38,7 @@ fn app(cx: Scope) -> Element { h1 {"Dioxus CRM Example"} - match *scene { + match scene.get() { Scene::ClientsList => rsx!( div { class: "crm", h2 { margin_bottom: "10px", "List of clients" } diff --git a/examples/disabled.rs b/examples/disabled.rs index 390a5550f..5bfa5bbb8 100644 --- a/examples/disabled.rs +++ b/examples/disabled.rs @@ -11,7 +11,7 @@ fn app(cx: Scope) -> Element { div { button { onclick: move |_| disabled.set(!disabled.get()), - "click to " [if *disabled {"enable"} else {"disable"} ] " the lower button" + "click to " [if **disabled {"enable"} else {"disable"} ] " the lower button" } button { diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 3b35ae174..b4575a9de 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -42,7 +42,7 @@ fn app(cx: Scope) -> Element { )) } div { flex: "50%", - match &*selected_breed { + match selected_breed.get() { Some(breed) => rsx!( Breed { breed: breed.clone() } ), None => rsx!("No Breed selected"), } diff --git a/examples/readme.rs b/examples/readme.rs index a9ea60822..89282babd 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -9,13 +9,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let count = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| *count.modify() += 1, "Up high!" } + button { onclick: move |_| *count.modify() -= 1, "Down low!" } } }) } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 8adb01d34..30d1fbb10 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -19,15 +19,15 @@ pub struct TodoItem { } pub fn app(cx: Scope<()>) -> Element { - let todos = use_state(&cx, || im_rc::HashMap::::default()); + let todos = use_state(&cx, im_rc::HashMap::::default); let filter = use_state(&cx, || FilterState::All); let draft = use_state(&cx, || "".to_string()); - let mut todo_id = use_state(&cx, || 0); + let (todo_id, set_todo_id) = use_state(&cx, || 0).split(); // Filter the todos based on the filter state let mut filtered_todos = todos .iter() - .filter(|(_, item)| match *filter { + .filter(|(_, item)| match **filter { FilterState::All => true, FilterState::Active => !item.checked, FilterState::Completed => item.checked, @@ -56,19 +56,17 @@ pub fn app(cx: Scope<()>) -> Element { autofocus: "true", oninput: move |evt| draft.set(evt.value.clone()), onkeydown: move |evt| { - if evt.key == "Enter" { - if !draft.is_empty() { - todos.modify().insert( - *todo_id, - TodoItem { - id: *todo_id, - checked: false, - contents: draft.get().clone(), - }, - ); - todo_id += 1; - draft.set("".to_string()); - } + if evt.key == "Enter" && !draft.is_empty() { + todos.modify().insert( + *todo_id, + TodoItem { + id: *todo_id, + checked: false, + contents: draft.get().clone(), + }, + ); + set_todo_id(todo_id + 1); + draft.set("".to_string()); } } } @@ -90,7 +88,7 @@ pub fn app(cx: Scope<()>) -> Element { (show_clear_completed).then(|| rsx!( button { class: "clear-completed", - onclick: move |_| todos.modify().retain(|_, todo| todo.checked == false), + onclick: move |_| todos.modify().retain(|_, todo| !todo.checked), "Clear completed" } )) @@ -108,7 +106,7 @@ pub fn app(cx: Scope<()>) -> Element { #[derive(Props)] pub struct TodoEntryProps<'a> { - todos: UseState<'a, im_rc::HashMap>, + todos: &'a UseState>, id: u32, } diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs new file mode 100644 index 000000000..9cfcedf4f --- /dev/null +++ b/packages/hooks/src/usestate.rs @@ -0,0 +1,212 @@ +use dioxus_core::prelude::*; +use std::{ + cell::{Cell, Ref, RefCell, RefMut}, + fmt::{Debug, Display}, + rc::Rc, +}; + +/// Store state between component renders! +/// +/// ## Dioxus equivalent of useState, designed for Rust +/// +/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and +/// modify state between component renders. When the state is updated, the component will re-render. +/// +/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. +/// +/// [`use_state`] exposes a few helper methods to modify the underlying state: +/// - `.set(new)` allows you to override the "work in progress" value with a new value +/// - `.get_mut()` allows you to modify the WIP value +/// - `.get_wip()` allows you to access the WIP value +/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) +/// +/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations +/// will automatically be called on the WIP value. +/// +/// ## Combinators +/// +/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: +/// - `.classic()` and `.split()` convert the hook into the classic React-style hook +/// ```rust +/// let (state, set_state) = use_state(&cx, || 10).split() +/// ``` +/// Usage: +/// +/// ```ignore +/// const Example: Component = |cx| { +/// let counter = use_state(&cx, || 0); +/// +/// cx.render(rsx! { +/// div { +/// h1 { "Counter: {counter}" } +/// button { onclick: move |_| counter.set(**counter + 1), "Increment" } +/// button { onclick: move |_| counter.set(**counter - 1), "Decrement" } +/// } +/// )) +/// } +/// ``` +pub fn use_state<'a, T: 'static>( + cx: &'a ScopeState, + initial_state_fn: impl FnOnce() -> T, +) -> &'a UseState { + let hook = cx.use_hook(move |_| UseState { + current_val: Rc::new(initial_state_fn()), + update_callback: cx.schedule_update(), + wip: Rc::new(RefCell::new(None)), + update_scheuled: Cell::new(false), + }); + + hook.update_scheuled.set(false); + let mut new_val = hook.wip.borrow_mut(); + + if new_val.is_some() { + // if there's only one reference (weak or otherwise), we can just swap the values + if let Some(val) = Rc::get_mut(&mut hook.current_val) { + *val = new_val.take().unwrap(); + } else { + hook.current_val = Rc::new(new_val.take().unwrap()); + } + } + + hook +} + +pub struct UseState { + pub(crate) current_val: Rc, + pub(crate) wip: Rc>>, + pub(crate) update_callback: Rc, + pub(crate) update_scheuled: Cell, +} + +impl Debug for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.current_val) + } +} + +impl UseState { + /// Tell the Dioxus Scheduler that we need to be processed + pub fn needs_update(&self) { + if !self.update_scheuled.get() { + self.update_scheuled.set(true); + (self.update_callback)(); + } + } + + pub fn set(&self, new_val: T) { + *self.wip.borrow_mut() = Some(new_val); + self.needs_update(); + } + + pub fn get(&self) -> &T { + &self.current_val + } + + pub fn get_rc(&self) -> &Rc { + &self.current_val + } + + /// Get the current status of the work-in-progress data + pub fn get_wip(&self) -> Ref> { + self.wip.borrow() + } + + /// Get the current status of the work-in-progress data + pub fn get_wip_mut(&self) -> RefMut> { + self.wip.borrow_mut() + } + + pub fn split(&self) -> (&T, Rc) { + (&self.current_val, self.setter()) + } + + pub fn setter(&self) -> Rc { + let slot = self.wip.clone(); + let callback = self.update_callback.clone(); + Rc::new(move |new| { + callback(); + *slot.borrow_mut() = Some(new) + }) + } + + pub fn wtih(&self, f: impl FnOnce(&mut T)) { + let mut val = self.wip.borrow_mut(); + + if let Some(inner) = val.as_mut() { + f(inner); + } + } + + pub fn for_async(&self) -> UseState { + let UseState { + current_val, + wip, + update_callback, + update_scheuled, + } = self; + + UseState { + current_val: current_val.clone(), + wip: wip.clone(), + update_callback: update_callback.clone(), + update_scheuled: update_scheuled.clone(), + } + } +} + +impl> UseState { + /// Gain mutable access to the new value via [`RefMut`]. + /// + /// If `modify` is called, then the component will re-render. + /// + /// This method is only available when the value is a `ToOwned` type. + /// + /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. + /// + /// To get a reference to the current value, use `.get()` + pub fn modify(&self) -> RefMut { + // make sure we get processed + self.needs_update(); + + // Bring out the new value, cloning if it we need to + // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this + RefMut::map(self.wip.borrow_mut(), |slot| { + if slot.is_none() { + *slot = Some(self.current_val.as_ref().to_owned()); + } + slot.as_mut().unwrap() + }) + } + + pub fn inner(self) -> T { + self.current_val.as_ref().to_owned() + } +} + +impl<'a, T> std::ops::Deref for UseState { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + +// enable displaty for the handle +impl<'a, T: 'static + Display> std::fmt::Display for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.current_val) + } +} + +impl<'a, V, T: PartialEq> PartialEq for UseState { + fn eq(&self, other: &V) -> bool { + self.get() == other + } +} +impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState { + type Output = O; + + fn not(self) -> Self::Output { + !*self.get() + } +} diff --git a/packages/hooks/src/usestate/handle.rs b/packages/hooks/src/usestate/handle.rs deleted file mode 100644 index 2638feaa8..000000000 --- a/packages/hooks/src/usestate/handle.rs +++ /dev/null @@ -1,215 +0,0 @@ -use super::owned::UseStateOwned; -use std::{ - cell::{Ref, RefMut}, - fmt::{Debug, Display}, - rc::Rc, -}; - -pub struct UseState<'a, T: 'static>(pub(crate) &'a UseStateOwned); - -impl Copy for UseState<'_, T> {} - -impl<'a, T: 'static> Clone for UseState<'a, T> { - fn clone(&self) -> Self { - UseState(self.0) - } -} - -impl Debug for UseState<'_, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.0.current_val) - } -} - -impl<'a, T: 'static> UseState<'a, T> { - /// Tell the Dioxus Scheduler that we need to be processed - pub fn needs_update(&self) { - if !self.0.update_scheuled.get() { - self.0.update_scheuled.set(true); - (self.0.update_callback)(); - } - } - - pub fn set(&self, new_val: T) { - *self.0.wip.borrow_mut() = Some(new_val); - self.needs_update(); - } - - pub fn get(&self) -> &'a T { - &self.0.current_val - } - - pub fn get_rc(&self) -> &'a Rc { - &self.0.current_val - } - - /// Get the current status of the work-in-progress data - pub fn get_wip(&self) -> Ref> { - self.0.wip.borrow() - } - - /// Get the current status of the work-in-progress data - pub fn get_wip_mut(&self) -> RefMut> { - self.0.wip.borrow_mut() - } - - pub fn classic(self) -> (&'a T, Rc) { - (&self.0.current_val, self.setter()) - } - - pub fn setter(&self) -> Rc { - let slot = self.0.wip.clone(); - let callback = self.0.update_callback.clone(); - Rc::new(move |new| { - callback(); - *slot.borrow_mut() = Some(new) - }) - } - - pub fn wtih(&self, f: impl FnOnce(&mut T)) { - let mut val = self.0.wip.borrow_mut(); - - if let Some(inner) = val.as_mut() { - f(inner); - } - } - - pub fn for_async(&self) -> UseStateOwned { - let UseStateOwned { - current_val, - wip, - update_callback, - update_scheuled, - } = self.0; - - UseStateOwned { - current_val: current_val.clone(), - wip: wip.clone(), - update_callback: update_callback.clone(), - update_scheuled: update_scheuled.clone(), - } - } -} - -impl<'a, T: 'static + ToOwned> UseState<'a, T> { - /// Gain mutable access to the new value via [`RefMut`]. - /// - /// If `modify` is called, then the component will re-render. - /// - /// This method is only available when the value is a `ToOwned` type. - /// - /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. - /// - /// To get a reference to the current value, use `.get()` - pub fn modify(self) -> RefMut<'a, T> { - // make sure we get processed - self.needs_update(); - - // Bring out the new value, cloning if it we need to - // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this - RefMut::map(self.0.wip.borrow_mut(), |slot| { - if slot.is_none() { - *slot = Some(self.0.current_val.as_ref().to_owned()); - } - slot.as_mut().unwrap() - }) - } - - pub fn inner(self) -> T { - self.0.current_val.as_ref().to_owned() - } -} - -impl<'a, T> std::ops::Deref for UseState<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.get() - } -} - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.current_val) - } -} -impl<'a, V, T: PartialEq> PartialEq for UseState<'a, T> { - fn eq(&self, other: &V) -> bool { - self.get() == other - } -} -impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState<'a, T> { - type Output = O; - - fn not(self) -> Self::Output { - !*self.get() - } -} - -/* - -Convenience methods for UseState. - -Note! - -This is not comprehensive. -This is *just* meant to make common operations easier. -*/ - -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; - -impl<'a, T: Copy + Add> Add for UseState<'a, T> { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.0.current_val.add(rhs) - } -} -impl<'a, T: Copy + Add> AddAssign for UseState<'a, T> { - fn add_assign(&mut self, rhs: T) { - self.set(self.0.current_val.add(rhs)); - } -} - -/// Sub -impl<'a, T: Copy + Sub> Sub for UseState<'a, T> { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.0.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseState<'a, T> { - fn sub_assign(&mut self, rhs: T) { - self.set(self.0.current_val.sub(rhs)); - } -} - -/// MUL -impl<'a, T: Copy + Mul> Mul for UseState<'a, T> { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.0.current_val.mul(rhs) - } -} -impl<'a, T: Copy + Mul> MulAssign for UseState<'a, T> { - fn mul_assign(&mut self, rhs: T) { - self.set(self.0.current_val.mul(rhs)); - } -} - -/// DIV -impl<'a, T: Copy + Div> Div for UseState<'a, T> { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.0.current_val.div(rhs) - } -} -impl<'a, T: Copy + Div> DivAssign for UseState<'a, T> { - fn div_assign(&mut self, rhs: T) { - self.set(self.0.current_val.div(rhs)); - } -} diff --git a/packages/hooks/src/usestate/mod.rs b/packages/hooks/src/usestate/mod.rs deleted file mode 100644 index cf125c2a4..000000000 --- a/packages/hooks/src/usestate/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -mod handle; -mod owned; -pub use handle::*; -pub use owned::*; - -use dioxus_core::prelude::*; -use std::{ - cell::{Cell, RefCell}, - rc::Rc, -}; - -/// Store state between component renders! -/// -/// ## Dioxus equivalent of useState, designed for Rust -/// -/// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and -/// modify state between component renders. When the state is updated, the component will re-render. -/// -/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. -/// -/// [`use_state`] exposes a few helper methods to modify the underlying state: -/// - `.set(new)` allows you to override the "work in progress" value with a new value -/// - `.get_mut()` allows you to modify the WIP value -/// - `.get_wip()` allows you to access the WIP value -/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) -/// -/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations -/// will automatically be called on the WIP value. -/// -/// ## Combinators -/// -/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: -/// - `.classic()` and `.split()` convert the hook into the classic React-style hook -/// ```rust -/// let (state, set_state) = use_state(&cx, || 10).split() -/// ``` -/// -/// -/// Usage: -/// -/// ```ignore -/// const Example: Component = |cx| { -/// let counter = use_state(&cx, || 0); -/// -/// cx.render(rsx! { -/// div { -/// h1 { "Counter: {counter}" } -/// button { onclick: move |_| counter += 1, "Increment" } -/// button { onclick: move |_| counter -= 1, "Decrement" } -/// } -/// )) -/// } -/// ``` -pub fn use_state<'a, T: 'static>( - cx: &'a ScopeState, - initial_state_fn: impl FnOnce() -> T, -) -> UseState<'a, T> { - let hook = cx.use_hook(move |_| UseStateOwned { - current_val: Rc::new(initial_state_fn()), - update_callback: cx.schedule_update(), - wip: Rc::new(RefCell::new(None)), - update_scheuled: Cell::new(false), - }); - - hook.update_scheuled.set(false); - let mut new_val = hook.wip.borrow_mut(); - - if new_val.is_some() { - // if there's only one reference (weak or otherwise), we can just swap the values - if let Some(val) = Rc::get_mut(&mut hook.current_val) { - *val = new_val.take().unwrap(); - } else { - hook.current_val = Rc::new(new_val.take().unwrap()); - } - } - - UseState(hook) -} diff --git a/packages/hooks/src/usestate/owned.rs b/packages/hooks/src/usestate/owned.rs deleted file mode 100644 index a1a6bdb73..000000000 --- a/packages/hooks/src/usestate/owned.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::{ - cell::{Cell, Ref, RefCell, RefMut}, - fmt::{Debug, Display}, - rc::Rc, -}; -pub struct UseStateOwned { - // this will always be outdated - pub(crate) current_val: Rc, - pub(crate) wip: Rc>>, - pub(crate) update_callback: Rc, - pub(crate) update_scheuled: Cell, -} - -impl UseStateOwned { - pub fn get(&self) -> Ref> { - self.wip.borrow() - } - - pub fn set(&self, new_val: T) { - *self.wip.borrow_mut() = Some(new_val); - (self.update_callback)(); - } - - pub fn modify(&self) -> RefMut> { - (self.update_callback)(); - self.wip.borrow_mut() - } -} - -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}; - -impl Debug for UseStateOwned { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.current_val) - } -} - -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseStateOwned { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.current_val) - } -} - -impl<'a, T: Copy + Add> Add for UseStateOwned { - type Output = T; - - fn add(self, rhs: T) -> Self::Output { - self.current_val.add(rhs) - } -} - -impl<'a, T: Copy + Add> AddAssign for UseStateOwned { - fn add_assign(&mut self, rhs: T) { - self.set(self.current_val.add(rhs)); - } -} - -/// Sub -impl<'a, T: Copy + Sub> Sub for UseStateOwned { - type Output = T; - - fn sub(self, rhs: T) -> Self::Output { - self.current_val.sub(rhs) - } -} -impl<'a, T: Copy + Sub> SubAssign for UseStateOwned { - fn sub_assign(&mut self, rhs: T) { - self.set(self.current_val.sub(rhs)); - } -} - -/// MUL -impl<'a, T: Copy + Mul> Mul for UseStateOwned { - type Output = T; - - fn mul(self, rhs: T) -> Self::Output { - self.current_val.mul(rhs) - } -} -impl<'a, T: Copy + Mul> MulAssign for UseStateOwned { - fn mul_assign(&mut self, rhs: T) { - self.set(self.current_val.mul(rhs)); - } -} - -/// DIV -impl<'a, T: Copy + Div> Div for UseStateOwned { - type Output = T; - - fn div(self, rhs: T) -> Self::Output { - self.current_val.div(rhs) - } -} -impl<'a, T: Copy + Div> DivAssign for UseStateOwned { - fn div_assign(&mut self, rhs: T) { - self.set(self.current_val.div(rhs)); - } -} From 5badcd830cdc1d19faab8fb516baaebc09168c62 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Mon, 17 Jan 2022 23:44:16 +0800 Subject: [PATCH 38/79] Update ZH_CN.md --- notes/README/ZH_CN.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index dfc5b3e89..680409e88 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -82,7 +82,7 @@ Dioxus 为不同的平台都提供了很好的开发文档。 如果你会使用 React ,那 Dioxus 对你来说会很简单。 ### 项目特点: -- 桌面程序在本地运行。 +- 对桌面应用的原生支持。 - 强大的状态管理工具。 - 支持所有 HTML 标签,监听器和事件。 - 超高的内存使用率,稳定的组件分配器。 @@ -126,30 +126,30 @@ TypeScript 是一个不错的 JavaScript 拓展集,但它仍然算是 JavaScri TS 代码运行效率不高,而且有大量的配置项。 -相比之下,Dioxus 使用用 Rust 编写将大大的提高效能。 +相比之下,Dioxus 使用 Rust 编写将大大的提高效能。 使用 Rust 开发,我们能获得: -- 库的静态类型支持。 -- 变量的默认不变性。 -- 一个简单且直观的模块管理系统。 -- 集成在内部的文档系统。 +- 静态类型支持。 +- 变量默认不变性。 +- 简单直观的模块系统。 +- 内部集成的文档系统。 - 先进的模式匹配系统。 -- 简洁、高效、可组合的迭代器。 -- 内联内置的 单元测试 / 集成测试。 -- 一流的异常处理系统。 +- 简洁、高效、强大的迭代器。 +- 内置的 单元测试 / 集成测试。 +- 优秀的异常处理系统。 - 强大且健全的标准库。 - 灵活的 `宏` 系统。 - 使用 `crates.io` 管理包。 Dioxus 能为开发者提供的: -- 正确且安全的使用不可变数据结构。 +- 安全使用数据结构。 - 安全的错误处理结果。 - 拥有原生移动端的性能。 - 直接访问系统的IO层。 -Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让你的前端团队更有信心在更短的时间内做出重大改变。 +Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但提供了更多的健壮性,让团队能在更短的时间内做出强大功能。 ### 不建议使用 Dioxus 的情况? From 27bdef304083e8826b2dddbef5c0a0136f3898b1 Mon Sep 17 00:00:00 2001 From: YuKun Liu Date: Mon, 17 Jan 2022 23:47:38 +0800 Subject: [PATCH 39/79] Update ZH_CN.md --- notes/README/ZH_CN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 680409e88..614774595 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -86,7 +86,7 @@ Dioxus 为不同的平台都提供了很好的开发文档。 - 强大的状态管理工具。 - 支持所有 HTML 标签,监听器和事件。 - 超高的内存使用率,稳定的组件分配器。 -- 多通道异步调动器,超强的异步支持。 +- 多通道异步调度器,超强的异步支持。 - 更多信息请查阅: [版本发布文档](https://dioxuslabs.com/blog/introducing-dioxus/). ### 示例 @@ -155,7 +155,7 @@ Dioxus 使 Rust 应用程序的编写速度和 React 应用程序一样快,但 您不该在这些情况下使用 Dioxus : -- 您不喜欢 React Hooks 的前端开发方案。 +- 您不喜欢类似 React 的开发风格。 - 您需要一个 `no-std` 的渲染器。 - 您希望应用运行在 `不支持 Wasm 或 asm.js` 的浏览器。 - 您需要一个 `Send + Sync` UI 解决方案(目前不支持)。 From 64080588d02c54a1d380116cbecdd17de16d2392 Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 14 Jan 2022 15:44:18 -0600 Subject: [PATCH 40/79] Add a warning when Link it called outside of a Router context --- packages/router/src/components/link.rs | 31 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index dc807aa1a..6a1b671ab 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -41,18 +41,25 @@ pub struct LinkProps<'a> { } pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { - let service = cx.consume_context::()?; - cx.render(rsx! { - a { - href: "{cx.props.to}", - class: format_args!("{}", cx.props.class.unwrap_or("")), - id: format_args!("{}", cx.props.id.unwrap_or("")), - title: format_args!("{}", cx.props.title.unwrap_or("")), + log::debug!("render Link to {}", cx.props.to); + if let Some(service) = cx.consume_context::() { + return cx.render(rsx! { + a { + href: "{cx.props.to}", + class: format_args!("{}", cx.props.class.unwrap_or("")), + id: format_args!("{}", cx.props.id.unwrap_or("")), + title: format_args!("{}", cx.props.title.unwrap_or("")), - prevent_default: "onclick", - onclick: move |_| service.push_route(cx.props.to), + prevent_default: "onclick", + onclick: move |_| service.push_route(cx.props.to), - &cx.props.children - } - }) + &cx.props.children + } + }); + } + log::warn!( + "Attempted to create a Link to {} outside of a Router context", + cx.props.to, + ); + None } From a0f60152bc7e5866f114ed469809ce8be70d17d4 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:19:12 +0800 Subject: [PATCH 41/79] feat: link open in browser --- examples/link.rs | 27 +++++++++++++++++++++++++++ packages/desktop/Cargo.toml | 1 + packages/desktop/src/index.js | 25 ++++++++++++++++++++++++- packages/desktop/src/lib.rs | 21 +++++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 examples/link.rs diff --git a/examples/link.rs b/examples/link.rs new file mode 100644 index 000000000..bed14f8a2 --- /dev/null +++ b/examples/link.rs @@ -0,0 +1,27 @@ +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! ( + div { + p { + a { + href: "http://dioxuslabs.com/", + "default link" + } + } + p { + a { + href: "http://dioxuslabs.com/", + onclick: |_| { + println!("Hello Dioxus"); + }, + "custom event link", + } + } + } + )) +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 8a0e5e8f5..eb9425c1f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -29,6 +29,7 @@ tokio = { version = "1.12.0", features = [ ], optional = true, default-features = false } dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"} dioxus-html = { path = "../html", features = ["serialize"], version ="^0.1.4"} +webbrowser = "0.5.5" [features] default = ["tokio_runtime"] diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 88cf9fcdc..b7de5bf0d 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -374,7 +374,7 @@ class Interpreter { } SetAttribute(edit) { - // console.log("setting attr", edit); + console.log("setting attr", edit); const name = edit.field; const value = edit.value; const ns = edit.ns; @@ -399,6 +399,29 @@ class Interpreter { break; case "dangerous_inner_html": node.innerHTML = value; + break; + case "href": + + if (node.tagName == "A") { + // open the tag in browser + node.setAttribute("browser-href", value); + node.setAttribute("href", "#"); + + node.addEventListener("click", function(event) { + + const target = event.target; + const real_id = target.getAttribute(`dioxus-id`); + + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: target.getAttribute("browser-href"), + }) + }) + + } else { + node.setAttribute(name, value); + } + break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0b6044761..546db27c1 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -187,6 +187,27 @@ pub fn launch_with_props( is_ready.store(true, std::sync::atomic::Ordering::Relaxed); let _ = proxy.send_event(UserWindowEvent::Update); } + "browser_open" => { + let data = req.params.unwrap(); + log::trace!("Open browser: {:?}", data); + if data.is_array() { + let arr = data.as_array().unwrap(); + if arr[0].is_object() { + let temp = arr[0] + .as_object() + .unwrap(); + if temp.contains_key("href") { + let url = temp + .get("href").unwrap() + .as_str().unwrap(); + match webbrowser::open(url) { + Ok(_) => {}, + Err(e) => log::trace!("Open browser error: {:?}", e), + } + } + } + } + } _ => {} } None From c737c424b05ad8453e8770a14a0d210fb0c7c2fe Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:19:58 +0800 Subject: [PATCH 42/79] feat: link open in browser --- packages/desktop/src/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index b7de5bf0d..d15fc39b0 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -374,7 +374,9 @@ class Interpreter { } SetAttribute(edit) { - console.log("setting attr", edit); + + // console.log("setting attr", edit); + const name = edit.field; const value = edit.value; const ns = edit.ns; From 5233ee97d9314f7f0e0bdf05c56d2a9e4201a596 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:20:36 +0800 Subject: [PATCH 43/79] fix: format code --- examples/link.rs | 2 +- packages/desktop/src/lib.rs | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/examples/link.rs b/examples/link.rs index bed14f8a2..da4b5b459 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -6,7 +6,7 @@ fn main() { fn app(cx: Scope) -> Element { cx.render(rsx! ( - div { + div { p { a { href: "http://dioxuslabs.com/", diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 546db27c1..0f451bd0d 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -193,16 +193,14 @@ pub fn launch_with_props( if data.is_array() { let arr = data.as_array().unwrap(); if arr[0].is_object() { - let temp = arr[0] - .as_object() - .unwrap(); + let temp = arr[0].as_object().unwrap(); if temp.contains_key("href") { - let url = temp - .get("href").unwrap() - .as_str().unwrap(); + let url = temp.get("href").unwrap().as_str().unwrap(); match webbrowser::open(url) { - Ok(_) => {}, - Err(e) => log::trace!("Open browser error: {:?}", e), + Ok(_) => {} + Err(e) => { + log::trace!("Open browser error: {:?}", e) + } } } } From 9e04ce5342850d2e0a01dde169807d6f6eb16566 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 09:32:27 +0800 Subject: [PATCH 44/79] feat: `open_browser` bool attribute --- examples/link.rs | 1 + packages/desktop/src/index.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/link.rs b/examples/link.rs index da4b5b459..49921f98a 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -16,6 +16,7 @@ fn app(cx: Scope) -> Element { p { a { href: "http://dioxuslabs.com/", + "browser-open": "false", onclick: |_| { println!("Hello Dioxus"); }, diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index d15fc39b0..c141f56d4 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -414,10 +414,15 @@ class Interpreter { const target = event.target; const real_id = target.getAttribute(`dioxus-id`); - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: target.getAttribute("browser-href"), - }) + if (target.getAttribute("browser-open") != "false") { + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: target.getAttribute("browser-href"), + }); + } else { + window.location = target.getAttribute("browser-href"); + } + }) } else { From f006f50317f4b75fac353bc988db057a281ba7f8 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:52:14 +0800 Subject: [PATCH 45/79] feat: move `rpc` to handler --- examples/link.rs | 2 +- packages/desktop/src/index.js | 43 ++++++++++++----------------------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/examples/link.rs b/examples/link.rs index 49921f98a..41b010e8d 100644 --- a/examples/link.rs +++ b/examples/link.rs @@ -16,7 +16,7 @@ fn app(cx: Scope) -> Element { p { a { href: "http://dioxuslabs.com/", - "browser-open": "false", + prevent_default: "onclick", onclick: |_| { println!("Hello Dioxus"); }, diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index c141f56d4..0bd767581 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -343,6 +343,7 @@ class Interpreter { this.listeners[event_name] = true; this.root.addEventListener(event_name, (event) => { + const target = event.target; const real_id = target.getAttribute(`dioxus-id`); @@ -356,6 +357,20 @@ class Interpreter { event.preventDefault(); } + console.log(event); + if (event.type == "click") { + event.preventDefault(); + if (should_prevent_default !== `onclick`) { + console.log(event.target.getAttribute("href")); + if(element.tagName == "A") { + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: event.target.getAttribute("href") + }) + } + } + } + if (real_id == null) { return; } @@ -401,34 +416,6 @@ class Interpreter { break; case "dangerous_inner_html": node.innerHTML = value; - break; - case "href": - - if (node.tagName == "A") { - // open the tag in browser - node.setAttribute("browser-href", value); - node.setAttribute("href", "#"); - - node.addEventListener("click", function(event) { - - const target = event.target; - const real_id = target.getAttribute(`dioxus-id`); - - if (target.getAttribute("browser-open") != "false") { - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: target.getAttribute("browser-href"), - }); - } else { - window.location = target.getAttribute("browser-href"); - } - - }) - - } else { - node.setAttribute(name, value); - } - break; default: // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 From 62b637f8b0eaf616c49461fa23b9251a79abc147 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:54:48 +0800 Subject: [PATCH 46/79] fix: error pattern --- packages/desktop/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0f451bd0d..38c85052a 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -196,11 +196,8 @@ pub fn launch_with_props( let temp = arr[0].as_object().unwrap(); if temp.contains_key("href") { let url = temp.get("href").unwrap().as_str().unwrap(); - match webbrowser::open(url) { - Ok(_) => {} - Err(e) => { - log::trace!("Open browser error: {:?}", e) - } + if let Err(e) = webbrowser::open(url) { + log::error!("Open Browser error: {:?}", e); } } } From 46fd6ac3450ca5ebf9aecb2d59a5a92b2a68bdd0 Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 14:57:41 +0800 Subject: [PATCH 47/79] feat: link open in browser --- packages/desktop/src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 38c85052a..8526a4a93 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -190,10 +190,8 @@ pub fn launch_with_props( "browser_open" => { let data = req.params.unwrap(); log::trace!("Open browser: {:?}", data); - if data.is_array() { - let arr = data.as_array().unwrap(); - if arr[0].is_object() { - let temp = arr[0].as_object().unwrap(); + if let Some(arr) = data.as_array() { + if let Some(temp) = arr[0].as_object() { if temp.contains_key("href") { let url = temp.get("href").unwrap().as_str().unwrap(); if let Err(e) = webbrowser::open(url) { From 8089023a6c3a54957af9c9c05c9dee6088b059ef Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 15:00:49 +0800 Subject: [PATCH 48/79] fix: prevent `submit` default --- packages/desktop/src/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 0bd767581..3ab21d6ba 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -357,7 +357,10 @@ class Interpreter { event.preventDefault(); } - console.log(event); + if (event.type == "submit") { + event.preventDefault(); + } + if (event.type == "click") { event.preventDefault(); if (should_prevent_default !== `onclick`) { From 327f9015481809d8e5b9e69f26202e8d66dd198e Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 23:15:39 +0800 Subject: [PATCH 49/79] fix: check `href` null --- packages/desktop/src/index.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 3ab21d6ba..685d1c7be 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -364,12 +364,14 @@ class Interpreter { if (event.type == "click") { event.preventDefault(); if (should_prevent_default !== `onclick`) { - console.log(event.target.getAttribute("href")); if(element.tagName == "A") { - rpc.call("browser_open", { - mounted_dom_id: parseInt(real_id), - href: event.target.getAttribute("href") - }) + const href = event.target.getAttribute("href") + if (href !== "" || href !== null) { + rpc.call("browser_open", { + mounted_dom_id: parseInt(real_id), + href: event.target.getAttribute("href") + }); + } } } } From 2073b400df55f0c6d8bed7371b2313be6c064e6e Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Tue, 18 Jan 2022 23:25:45 +0800 Subject: [PATCH 50/79] fix: check `href` null --- packages/desktop/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index 685d1c7be..2aba572f4 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -366,7 +366,7 @@ class Interpreter { if (should_prevent_default !== `onclick`) { if(element.tagName == "A") { const href = event.target.getAttribute("href") - if (href !== "" || href !== null) { + if (href !== "" && href !== null && href !== undefined) { rpc.call("browser_open", { mounted_dom_id: parseInt(real_id), href: event.target.getAttribute("href") From 16d2074fcf9187e14b7676aa9672e8ede92224fd Mon Sep 17 00:00:00 2001 From: YuKun Liu <41265098+mrxiaozhuox@users.noreply.github.com> Date: Wed, 19 Jan 2022 20:14:41 +0800 Subject: [PATCH 51/79] fix: `dog-app` breed change --- examples/dog_app.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 3b35ae174..b2f8429ee 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -61,7 +61,7 @@ fn app(cx: Scope) -> Element { #[inline_props] fn Breed(cx: Scope, breed: String) -> Element { - #[derive(serde::Deserialize)] + #[derive(serde::Deserialize, Debug)] struct DogApi { message: String, } @@ -72,6 +72,12 @@ fn Breed(cx: Scope, breed: String) -> Element { reqwest::get(endpoint).await.unwrap().json::().await }); + let breed_name = use_state(&cx, || breed.clone()); + if breed_name.get() != breed { + breed_name.set(breed.clone()); + fut.restart(); + } + cx.render(match fut.value() { Some(Ok(resp)) => rsx! { button { From c1a5d4e11f5277e6ee644f670dc6fdd68f2202f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 20 Jan 2022 04:01:38 -0500 Subject: [PATCH 52/79] fix: ssr + hydration event listeners --- packages/web/src/dom.rs | 20 ++++++++------------ packages/web/src/rehydrate.rs | 11 +++++++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 364f1a1d7..219f5aa79 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -75,10 +75,8 @@ impl WebsysDom { } DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id), DomEdit::NewEventListener { - event_name, - scope, - root: mounted_node_id, - } => self.new_event_listener(event_name, scope, mounted_node_id), + event_name, root, .. + } => self.new_event_listener(event_name, root), DomEdit::RemoveEventListener { event, root } => { self.remove_event_listener(event, root) @@ -172,10 +170,8 @@ impl WebsysDom { let node = self.nodes[root as usize].as_ref().unwrap(); if let Some(element) = node.dyn_ref::() { element.remove(); - } else { - if let Some(parent) = node.parent_node() { - parent.remove_child(&node).unwrap(); - } + } else if let Some(parent) = node.parent_node() { + parent.remove_child(node).unwrap(); } } @@ -227,7 +223,7 @@ impl WebsysDom { self.nodes[(id as usize)] = Some(el); } - fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) { + pub fn new_event_listener(&mut self, event: &'static str, node: u64) { let event = wasm_bindgen::intern(event); // attach the correct attributes to the element @@ -235,7 +231,7 @@ impl WebsysDom { // This ensures we only ever have one handler attached to the root, but decide // dynamically when we want to call a listener. - let el = self.stack.top(); + let el = self.nodes[(node as usize)].as_ref().unwrap(); let el = el.dyn_ref::().unwrap(); @@ -278,7 +274,7 @@ impl WebsysDom { .unwrap(); // Increment the listeners - self.listeners.insert(event.into(), (1, handler)); + self.listeners.insert(event, (1, handler)); } } @@ -442,7 +438,7 @@ impl WebsysDom { anchor .parent_node() .unwrap() - .insert_before(&before, Some(&anchor)) + .insert_before(&before, Some(anchor)) .unwrap(); } else { let arr: js_sys::Array = self diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 8993dd58c..91abb4fa1 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -116,7 +116,14 @@ impl WebsysDom { // we cant have the last node be text let mut last_node_was_text = false; for child in vel.children { - self.rehydrate_single(nodes, place, dom, &child, &mut last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, child, &mut last_node_was_text)?; + } + + for listener in vel.listeners { + self.new_event_listener( + listener.event, + listener.mounted_node.get().unwrap().as_u64(), + ); } place.pop(); @@ -142,7 +149,7 @@ impl WebsysDom { VNode::Fragment(el) => { for el in el.children { - self.rehydrate_single(nodes, place, dom, &el, last_node_was_text)?; + self.rehydrate_single(nodes, place, dom, el, last_node_was_text)?; } } From 50f90d889affbd355f2f9caa4e5c039ab44500db Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 00:21:23 -0500 Subject: [PATCH 53/79] fix: bump to readd desktop click handling code --- packages/jsinterpreter/interpreter.ts | 40 +++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index 17617a209..533536277 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -558,31 +558,50 @@ export class Interpreter { this.RemoveEventListener(edit.root, edit.event_name); break; case "NewEventListener": - // todo: only on desktop should we make our own handler + + + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation let handler = (event: Event) => { const target = event.target as Element | null; - // console.log("event", event); + if (target != null) { + const realId = target.getAttribute(`dioxus-id`); - const real_id = target.getAttribute(`dioxus-id`); - - const should_prevent_default = target.getAttribute( - `dioxus-prevent-default` - ); + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); let contents = serialize_event(event); - if (should_prevent_default === `on${event.type}`) { + if (shouldPreventDefault === `on${event.type}`) { event.preventDefault(); } - if (real_id == null) { + if (event.type == "submit") { + event.preventDefault(); + } + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href") + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + + if (realId == null) { return; } window.rpc.call("user_event", { event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(real_id), + mounted_dom_id: parseInt(realId), contents: contents, }); } @@ -655,4 +674,3 @@ declare global { } -type Edits = DomEdit[]; From d2bd1751436ef4bec554bb361ba87aea1357036a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 00:37:52 -0500 Subject: [PATCH 54/79] feat: allow providing context to the root component --- packages/core/src/scopes.rs | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 5b8f527c5..a4b32e50d 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -667,6 +667,56 @@ impl ScopeState { value } + /// Provide a context for the root component from anywhere in your app. + /// + /// + /// # Example + /// + /// ```rust, ignore + /// struct SharedState(&'static str); + /// + /// static App: Component = |cx| { + /// cx.use_hook(|_| cx.provide_root_context(SharedState("world"))); + /// rsx!(cx, Child {}) + /// } + /// + /// static Child: Component = |cx| { + /// let state = cx.consume_state::(); + /// rsx!(cx, div { "hello {state.0}" }) + /// } + /// ``` + pub fn provide_root_context(&self, value: T) -> Rc { + let value = Rc::new(value); + + // if we *are* the root component, then we can just provide the context directly + if self.scope_id() == ScopeId(0) { + self.shared_contexts + .borrow_mut() + .insert(TypeId::of::(), value.clone()) + .map(|f| f.downcast::().ok()) + .flatten(); + } + + let mut search_parent = self.parent_scope; + + while let Some(parent) = search_parent.take() { + let parent = unsafe { &*parent }; + + if parent + .shared_contexts + .borrow_mut() + .insert(TypeId::of::(), value.clone()) + .is_some() + { + log::warn!("Context already provided to parent scope - replacing it"); + } + + search_parent = parent.parent_scope; + } + + value + } + /// Try to retrieve a SharedState with type T from the any parent Scope. pub fn consume_context(&self) -> Option> { if let Some(shared) = self.shared_contexts.borrow().get(&TypeId::of::()) { From c8d528b3b18e320ca0cee1542ae5fc659c1cca81 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 09:29:41 -0500 Subject: [PATCH 55/79] fix: proprogation of root context --- packages/core/src/scopes.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index a4b32e50d..cfc555543 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -702,19 +702,22 @@ impl ScopeState { while let Some(parent) = search_parent.take() { let parent = unsafe { &*parent }; - if parent - .shared_contexts - .borrow_mut() - .insert(TypeId::of::(), value.clone()) - .is_some() - { - log::warn!("Context already provided to parent scope - replacing it"); + if parent.scope_id() == ScopeId(0) { + let exists = parent + .shared_contexts + .borrow_mut() + .insert(TypeId::of::(), value.clone()); + + if exists.is_some() { + log::warn!("Context already provided to parent scope - replacing it"); + } + return value; } search_parent = parent.parent_scope; } - value + unreachable!("all apps have a root scope") } /// Try to retrieve a SharedState with type T from the any parent Scope. From 997684a39094ccd51bbab96210f400df8af036fa Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 09:53:25 -0500 Subject: [PATCH 56/79] fix: update desktop intrepreter with browser open --- packages/jsinterpreter/interpreter.js | 1024 ++++++++++++++----------- 1 file changed, 558 insertions(+), 466 deletions(-) diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index 453b34406..9da95fa7c 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,477 +1,569 @@ function serialize_event(event) { - var _a, _b; - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - var data = event.data; - return { - data: data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - var _c = event, charCode = _c.charCode, key = _c.key, altKey = _c.altKey, ctrlKey = _c.ctrlKey, metaKey = _c.metaKey, keyCode = _c.keyCode, shiftKey = _c.shiftKey, location_1 = _c.location, repeat = _c.repeat, which = _c.which; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location_1, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - var target = event.target; - var value = void 0; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } - else { - value = (_a = target.value) !== null && _a !== void 0 ? _a : target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - var target = event.target; - var value = (_b = target.value) !== null && _b !== void 0 ? _b : target.textContent; - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - var _d = event, altKey = _d.altKey, button = _d.button, buttons = _d.buttons, clientX = _d.clientX, clientY = _d.clientY, ctrlKey = _d.ctrlKey, metaKey = _d.metaKey, pageX = _d.pageX, pageY = _d.pageY, screenX_1 = _d.screenX, screenY_1 = _d.screenY, shiftKey = _d.shiftKey; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX_1, - screen_y: screenY_1, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - var _e = event, altKey = _e.altKey, button = _e.button, buttons = _e.buttons, clientX = _e.clientX, clientY = _e.clientY, ctrlKey = _e.ctrlKey, metaKey = _e.metaKey, pageX = _e.pageX, pageY = _e.pageY, screenX_2 = _e.screenX, screenY_2 = _e.screenY, shiftKey = _e.shiftKey, pointerId = _e.pointerId, width = _e.width, height = _e.height, pressure = _e.pressure, tangentialPressure = _e.tangentialPressure, tiltX = _e.tiltX, tiltY = _e.tiltY, twist = _e.twist, pointerType = _e.pointerType, isPrimary = _e.isPrimary; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX_2, - screen_y: screenY_2, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - var _f = event, altKey = _f.altKey, ctrlKey = _f.ctrlKey, metaKey = _f.metaKey, shiftKey = _f.shiftKey; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - var _g = event, deltaX = _g.deltaX, deltaY = _g.deltaY, deltaZ = _g.deltaZ, deltaMode = _g.deltaMode; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - var _h = event, animationName = _h.animationName, elapsedTime = _h.elapsedTime, pseudoElement = _h.pseudoElement; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - var _j = event, propertyName = _j.propertyName, elapsedTime = _j.elapsedTime, pseudoElement = _j.pseudoElement; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } + var _a, _b; + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + var data = event.data; + return { + data: data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + var _c = event, + charCode = _c.charCode, + key = _c.key, + altKey = _c.altKey, + ctrlKey = _c.ctrlKey, + metaKey = _c.metaKey, + keyCode = _c.keyCode, + shiftKey = _c.shiftKey, + location_1 = _c.location, + repeat = _c.repeat, + which = _c.which; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location_1, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + var target = event.target; + var value = void 0; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = + (_a = target.value) !== null && _a !== void 0 + ? _a + : target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + var target = event.target; + var value = + (_b = target.value) !== null && _b !== void 0 ? _b : target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + var _d = event, + altKey = _d.altKey, + button = _d.button, + buttons = _d.buttons, + clientX = _d.clientX, + clientY = _d.clientY, + ctrlKey = _d.ctrlKey, + metaKey = _d.metaKey, + pageX = _d.pageX, + pageY = _d.pageY, + screenX_1 = _d.screenX, + screenY_1 = _d.screenY, + shiftKey = _d.shiftKey; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX_1, + screen_y: screenY_1, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + var _e = event, + altKey = _e.altKey, + button = _e.button, + buttons = _e.buttons, + clientX = _e.clientX, + clientY = _e.clientY, + ctrlKey = _e.ctrlKey, + metaKey = _e.metaKey, + pageX = _e.pageX, + pageY = _e.pageY, + screenX_2 = _e.screenX, + screenY_2 = _e.screenY, + shiftKey = _e.shiftKey, + pointerId = _e.pointerId, + width = _e.width, + height = _e.height, + pressure = _e.pressure, + tangentialPressure = _e.tangentialPressure, + tiltX = _e.tiltX, + tiltY = _e.tiltY, + twist = _e.twist, + pointerType = _e.pointerType, + isPrimary = _e.isPrimary; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX_2, + screen_y: screenY_2, + shift_key: shiftKey, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + case "select": { + return {}; + } + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + var _f = event, + altKey = _f.altKey, + ctrlKey = _f.ctrlKey, + metaKey = _f.metaKey, + shiftKey = _f.shiftKey; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, + }; + } + case "scroll": { + return {}; + } + case "wheel": { + var _g = event, + deltaX = _g.deltaX, + deltaY = _g.deltaY, + deltaZ = _g.deltaZ, + deltaMode = _g.deltaMode; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + var _h = event, + animationName = _h.animationName, + elapsedTime = _h.elapsedTime, + pseudoElement = _h.pseudoElement; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + var _j = event, + propertyName = _j.propertyName, + elapsedTime = _j.elapsedTime, + pseudoElement = _j.pseudoElement; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": { + return {}; + } + case "toggle": { + return {}; + } + default: { + return {}; + } + } } var bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, }; var Interpreter = /** @class */ (function () { - function Interpreter(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; + function Interpreter(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + Interpreter.prototype.top = function () { + return this.stack[this.stack.length - 1]; + }; + Interpreter.prototype.pop = function () { + return this.stack.pop(); + }; + Interpreter.prototype.PushRoot = function (root) { + var node = this.nodes[root]; + this.stack.push(node); + }; + Interpreter.prototype.AppendChildren = function (many) { + var root = this.stack[this.stack.length - (1 + many)]; + var to_add = this.stack.splice(this.stack.length - many); + for (var i = 0; i < many; i++) { + root.appendChild(to_add[i]); } - Interpreter.prototype.top = function () { - return this.stack[this.stack.length - 1]; - }; - Interpreter.prototype.pop = function () { - return this.stack.pop(); - }; - Interpreter.prototype.PushRoot = function (root) { - var node = this.nodes[root]; - this.stack.push(node); - }; - Interpreter.prototype.AppendChildren = function (many) { - var root = this.stack[this.stack.length - (1 + many)]; - var to_add = this.stack.splice(this.stack.length - many); - for (var i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - }; - Interpreter.prototype.ReplaceWith = function (root_id, m) { - var root = this.nodes[root_id]; - var els = this.stack.splice(this.stack.length - m); - root.replaceWith.apply(root, els); - }; - Interpreter.prototype.InsertAfter = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.after.apply(old, new_nodes); - }; - Interpreter.prototype.InsertBefore = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.before.apply(old, new_nodes); - }; - Interpreter.prototype.Remove = function (root) { - var node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - }; - Interpreter.prototype.CreateTextNode = function (text, root) { - // todo: make it so the types are okay - var node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - }; - Interpreter.prototype.CreateElement = function (tag, root) { - var el = document.createElement(tag); - el.setAttribute("dioxus-id", "".concat(root)); - this.nodes[root] = el; - this.stack.push(el); - }; - Interpreter.prototype.CreateElementNs = function (tag, root, ns) { - var el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.CreatePlaceholder = function (root) { - var el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.NewEventListener = function (event_name, scope, root, handler) { - // console.log('new event listener', event_name, root, scope); - var element = this.nodes[root]; - element.setAttribute("dioxus-event-".concat(event_name), "".concat(scope, ".").concat(root)); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } - else { - this.listeners[event_name]++; - } - }; - Interpreter.prototype.RemoveEventListener = function (root, event_name) { - var element = this.nodes[root]; - element.removeAttribute("dioxus-event-".concat(event_name)); - 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]; - } - }; - Interpreter.prototype.SetText = function (root, text) { - this.nodes[root].textContent = text; - }; - Interpreter.prototype.SetAttribute = function (root, field, value, ns) { - var name = field; - var node = this.nodes[root]; - if (ns == "style") { - // @ts-ignore - node.style[name] = value; - } - else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } - else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } - else { - node.setAttribute(name, value); - } + }; + Interpreter.prototype.ReplaceWith = function (root_id, m) { + var root = this.nodes[root_id]; + var els = this.stack.splice(this.stack.length - m); + root.replaceWith.apply(root, els); + }; + Interpreter.prototype.InsertAfter = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.after.apply(old, new_nodes); + }; + Interpreter.prototype.InsertBefore = function (root, n) { + var old = this.nodes[root]; + var new_nodes = this.stack.splice(this.stack.length - n); + old.before.apply(old, new_nodes); + }; + Interpreter.prototype.Remove = function (root) { + var node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + }; + Interpreter.prototype.CreateTextNode = function (text, root) { + // todo: make it so the types are okay + var node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + }; + Interpreter.prototype.CreateElement = function (tag, root) { + var el = document.createElement(tag); + el.setAttribute("dioxus-id", "".concat(root)); + this.nodes[root] = el; + this.stack.push(el); + }; + Interpreter.prototype.CreateElementNs = function (tag, root, ns) { + var el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.CreatePlaceholder = function (root) { + var el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + }; + Interpreter.prototype.NewEventListener = function ( + event_name, + scope, + root, + handler + ) { + // console.log('new event listener', event_name, root, scope); + var element = this.nodes[root]; + element.setAttribute( + "dioxus-event-".concat(event_name), + "".concat(scope, ".").concat(root) + ); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } else { + this.listeners[event_name]++; + } + }; + Interpreter.prototype.RemoveEventListener = function (root, event_name) { + var element = this.nodes[root]; + element.removeAttribute("dioxus-event-".concat(event_name)); + 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]; + } + }; + Interpreter.prototype.SetText = function (root, text) { + this.nodes[root].textContent = text; + }; + Interpreter.prototype.SetAttribute = function (root, field, value, ns) { + var name = field; + var node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + }; + Interpreter.prototype.RemoveAttribute = function (root, name) { + var node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + }; + Interpreter.prototype.handleEdits = function (edits) { + this.stack.push(this.root); + for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { + var edit = edits_1[_i]; + this.handleEdit(edit); + } + }; + Interpreter.prototype.handleEdit = function (edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // todo: only on desktop should we make our own handler + var handler = function (event) { + var target = event.target; + // console.log("event", event); + if (target != null) { + var realId = target.getAttribute("dioxus-id"); + var shouldPreventDefault = target.getAttribute( + "dioxus-prevent-default" + ); + var contents = serialize_event(event); + if (shouldPreventDefault === "on".concat(event.type)) { + event.preventDefault(); } - } - }; - Interpreter.prototype.RemoveAttribute = function (root, name) { - var node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - }; - Interpreter.prototype.handleEdits = function (edits) { - this.stack.push(this.root); - for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { - var edit = edits_1[_i]; - this.handleEdit(edit); - } - }; - Interpreter.prototype.handleEdit = function (edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - var handler = function (event) { - var target = event.target; - // console.log("event", event); - if (target != null) { - var real_id = target.getAttribute("dioxus-id"); - var should_prevent_default = target.getAttribute("dioxus-prevent-default"); - var contents = serialize_event(event); - if (should_prevent_default === "on".concat(event.type)) { - event.preventDefault(); - } - if (real_id == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(real_id), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - }; - return Interpreter; -}()); + + if (event.type == "submit") { + event.preventDefault(); + } + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + var href = target.getAttribute("href"); + if ( + href !== "" && + href !== null && + href !== undefined && + realId != null + ) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href: href, + }); + } + } + } + } + + if (realId == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + }; + return Interpreter; +})(); export { Interpreter }; function main() { - var root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } + var root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } } -//# sourceMappingURL=interpreter.js.map \ No newline at end of file +//# sourceMappingURL=interpreter.js.map From 95e93ed0bcf6c69990f4cf3c6448b2bf5da96c36 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 21 Jan 2022 15:06:57 -0500 Subject: [PATCH 57/79] feat: remove dioxus id on non-event elements --- examples/nested_listeners.rs | 33 + packages/desktop/src/index.html | 8 +- packages/desktop/src/lib.rs | 10 +- packages/jsinterpreter/interpreter.js | 1060 ++++++++++++------------- packages/jsinterpreter/interpreter.ts | 628 +++++++-------- packages/jsinterpreter/tsconfig.json | 6 +- packages/web/src/dom.rs | 46 +- 7 files changed, 888 insertions(+), 903 deletions(-) create mode 100644 examples/nested_listeners.rs diff --git a/examples/nested_listeners.rs b/examples/nested_listeners.rs new file mode 100644 index 000000000..4b9fd14e0 --- /dev/null +++ b/examples/nested_listeners.rs @@ -0,0 +1,33 @@ +//! Nested Listeners +//! +//! This example showcases how to control event bubbling from child to parents. +//! +//! Both web and desktop support bubbling and bubble cancelation. + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + onclick: move |_| println!("clicked! top"), + button { + onclick: move |_| println!("clicked! bottom propoate"), + "Propogate" + } + button { + onclick: move |evt| { + println!("clicked! bottom no bubbling"); + evt.cancel_bubble(); + }, + "Dont propogate" + } + button { + "Does not handle clicks" + } + } + }) +} diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index c17a63d2a..88fcba12a 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -13,8 +13,10 @@ - - - + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 802e7441c..bc9a84e42 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -205,7 +205,7 @@ pub fn launch_with_props( } None }) - .with_custom_protocol("dioxus".into(), move |request| { + .with_custom_protocol(String::from("dioxus"), move |request| { // Any content that that uses the `dioxus://` scheme will be shuttled through this handler as a "special case" // For now, we only serve two pieces of content which get included as bytes into the final binary. let path = request.uri().replace("dioxus://", ""); @@ -225,10 +225,10 @@ pub fn launch_with_props( } }) .with_file_drop_handler(move |window, evet| { - if let Some(handler) = file_handler.as_ref() { - return handler(window, evet); - } - false + file_handler + .as_ref() + .map(|handler| handler(window, evet)) + .unwrap_or_default() }); for (name, handler) in cfg.protocos.drain(..) { diff --git a/packages/jsinterpreter/interpreter.js b/packages/jsinterpreter/interpreter.js index 9da95fa7c..ea50ec3d3 100644 --- a/packages/jsinterpreter/interpreter.js +++ b/packages/jsinterpreter/interpreter.js @@ -1,569 +1,499 @@ +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} +export class Interpreter { + root; + stack; + listeners; + handlers; + lastNodeWasText; + nodes; + constructor(root) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + top() { + return this.stack[this.stack.length - 1]; + } + pop() { + return this.stack.pop(); + } + PushRoot(root) { + const node = this.nodes[root]; + this.stack.push(node); + } + AppendChildren(many) { + let root = this.stack[this.stack.length - (1 + many)]; + let to_add = this.stack.splice(this.stack.length - many); + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + ReplaceWith(root_id, m) { + let root = this.nodes[root_id]; + let els = this.stack.splice(this.stack.length - m); + root.replaceWith(...els); + } + InsertAfter(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + InsertBefore(root, n) { + let old = this.nodes[root]; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + Remove(root) { + let node = this.nodes[root]; + if (node !== undefined) { + node.remove(); + } + } + CreateTextNode(text, root) { + // todo: make it so the types are okay + const node = document.createTextNode(text); + this.nodes[root] = node; + this.stack.push(node); + } + CreateElement(tag, root) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + this.nodes[root] = el; + this.stack.push(el); + } + CreateElementNs(tag, root, ns) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + CreatePlaceholder(root) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + NewEventListener(event_name, root, handler) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } + else { + this.listeners[event_name]++; + } + } + RemoveEventListener(root, event_name) { + 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]; + } + } + SetText(root, text) { + this.nodes[root].textContent = text; + } + SetAttribute(root, field, value, ns) { + const name = field; + const node = this.nodes[root]; + if (ns == "style") { + // @ts-ignore + node.style[name] = value; + } + else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } + else { + switch (name) { + case "value": + if (value != node.value) { + node.value = value; + } + break; + case "checked": + node.checked = value === "true"; + break; + case "selected": + node.selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } + else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root, name) { + const node = this.nodes[root]; + node.removeAttribute(name); + if (name === "value") { + node.value = ""; + } + if (name === "checked") { + node.checked = false; + } + if (name === "selected") { + node.selected = false; + } + } + handleEdits(edits) { + this.stack.push(this.root); + for (let edit of edits) { + this.handleEdit(edit); + } + } + handleEdit(edit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event) => { + let target = event.target; + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + let contents = serialize_event(event); + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + if (event.type == "submit") { + event.preventDefault(); + } + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href"); + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + if (realId == null) { + return; + } + window.rpc.call("user_event", { + event: edit.event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} function serialize_event(event) { - var _a, _b; - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; + switch (event.type) { + case "copy": + case "cut": + case "past": { + return {}; + } + case "compositionend": + case "compositionstart": + case "compositionupdate": { + let { data } = event; + return { + data, + }; + } + case "keydown": + case "keypress": + case "keyup": { + let { charCode, key, altKey, ctrlKey, metaKey, keyCode, shiftKey, location, repeat, which, } = event; + return { + char_code: charCode, + key: key, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + key_code: keyCode, + shift_key: shiftKey, + location: location, + repeat: repeat, + which: which, + locale: "locale", + }; + } + case "focus": + case "blur": { + return {}; + } + case "change": { + let target = event.target; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } + else { + value = target.value ?? target.textContent; + } + return { + value: value, + }; + } + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target; + let value = target.value ?? target.textContent; + if (target.type == "checkbox") { + value = target.checked ? "true" : "false"; + } + return { + value: value, + }; + } + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": { + const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + }; + } + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": { + const { altKey, button, buttons, clientX, clientY, ctrlKey, metaKey, pageX, pageY, screenX, screenY, shiftKey, pointerId, width, height, pressure, tangentialPressure, tiltX, tiltY, twist, pointerType, isPrimary, } = event; + return { + alt_key: altKey, + button: button, + buttons: buttons, + client_x: clientX, + client_y: clientY, + ctrl_key: ctrlKey, + meta_key: metaKey, + page_x: pageX, + page_y: pageY, + screen_x: screenX, + screen_y: screenY, + shift_key: shiftKey, + pointer_id: pointerId, + width: width, + height: height, + pressure: pressure, + tangential_pressure: tangentialPressure, + tilt_x: tiltX, + tilt_y: tiltY, + twist: twist, + pointer_type: pointerType, + is_primary: isPrimary, + }; + } + case "select": { + return {}; + } + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": { + const { altKey, ctrlKey, metaKey, shiftKey, } = event; + return { + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + alt_key: altKey, + ctrl_key: ctrlKey, + meta_key: metaKey, + shift_key: shiftKey, + }; + } + case "scroll": { + return {}; + } + case "wheel": { + const { deltaX, deltaY, deltaZ, deltaMode, } = event; + return { + delta_x: deltaX, + delta_y: deltaY, + delta_z: deltaZ, + delta_mode: deltaMode, + }; + } + case "animationstart": + case "animationend": + case "animationiteration": { + const { animationName, elapsedTime, pseudoElement, } = event; + return { + animation_name: animationName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "transitionend": { + const { propertyName, elapsedTime, pseudoElement, } = event; + return { + property_name: propertyName, + elapsed_time: elapsedTime, + pseudo_element: pseudoElement, + }; + } + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": { + return {}; + } + case "toggle": { + return {}; + } + default: { + return {}; + } } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - var data = event.data; - return { - data: data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - var _c = event, - charCode = _c.charCode, - key = _c.key, - altKey = _c.altKey, - ctrlKey = _c.ctrlKey, - metaKey = _c.metaKey, - keyCode = _c.keyCode, - shiftKey = _c.shiftKey, - location_1 = _c.location, - repeat = _c.repeat, - which = _c.which; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location_1, - repeat: repeat, - which: which, - locale: "locale", - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - var target = event.target; - var value = void 0; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = - (_a = target.value) !== null && _a !== void 0 - ? _a - : target.textContent; - } - return { - value: value, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - var target = event.target; - var value = - (_b = target.value) !== null && _b !== void 0 ? _b : target.textContent; - if (target.type == "checkbox") { - value = target.checked ? "true" : "false"; - } - return { - value: value, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - var _d = event, - altKey = _d.altKey, - button = _d.button, - buttons = _d.buttons, - clientX = _d.clientX, - clientY = _d.clientY, - ctrlKey = _d.ctrlKey, - metaKey = _d.metaKey, - pageX = _d.pageX, - pageY = _d.pageY, - screenX_1 = _d.screenX, - screenY_1 = _d.screenY, - shiftKey = _d.shiftKey; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX_1, - screen_y: screenY_1, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - var _e = event, - altKey = _e.altKey, - button = _e.button, - buttons = _e.buttons, - clientX = _e.clientX, - clientY = _e.clientY, - ctrlKey = _e.ctrlKey, - metaKey = _e.metaKey, - pageX = _e.pageX, - pageY = _e.pageY, - screenX_2 = _e.screenX, - screenY_2 = _e.screenY, - shiftKey = _e.shiftKey, - pointerId = _e.pointerId, - width = _e.width, - height = _e.height, - pressure = _e.pressure, - tangentialPressure = _e.tangentialPressure, - tiltX = _e.tiltX, - tiltY = _e.tiltY, - twist = _e.twist, - pointerType = _e.pointerType, - isPrimary = _e.isPrimary; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX_2, - screen_y: screenY_2, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - var _f = event, - altKey = _f.altKey, - ctrlKey = _f.ctrlKey, - metaKey = _f.metaKey, - shiftKey = _f.shiftKey; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - var _g = event, - deltaX = _g.deltaX, - deltaY = _g.deltaY, - deltaZ = _g.deltaZ, - deltaMode = _g.deltaMode; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - var _h = event, - animationName = _h.animationName, - elapsedTime = _h.elapsedTime, - pseudoElement = _h.pseudoElement; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - var _j = event, - propertyName = _j.propertyName, - elapsedTime = _j.elapsedTime, - pseudoElement = _j.pseudoElement; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } } -var bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, +const bool_attrs = { + allowfullscreen: true, + allowpaymentrequest: true, + async: true, + autofocus: true, + autoplay: true, + checked: true, + controls: true, + default: true, + defer: true, + disabled: true, + formnovalidate: true, + hidden: true, + ismap: true, + itemscope: true, + loop: true, + multiple: true, + muted: true, + nomodule: true, + novalidate: true, + open: true, + playsinline: true, + readonly: true, + required: true, + reversed: true, + selected: true, + truespeed: true, }; -var Interpreter = /** @class */ (function () { - function Interpreter(root) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - Interpreter.prototype.top = function () { - return this.stack[this.stack.length - 1]; - }; - Interpreter.prototype.pop = function () { - return this.stack.pop(); - }; - Interpreter.prototype.PushRoot = function (root) { - var node = this.nodes[root]; - this.stack.push(node); - }; - Interpreter.prototype.AppendChildren = function (many) { - var root = this.stack[this.stack.length - (1 + many)]; - var to_add = this.stack.splice(this.stack.length - many); - for (var i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - }; - Interpreter.prototype.ReplaceWith = function (root_id, m) { - var root = this.nodes[root_id]; - var els = this.stack.splice(this.stack.length - m); - root.replaceWith.apply(root, els); - }; - Interpreter.prototype.InsertAfter = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.after.apply(old, new_nodes); - }; - Interpreter.prototype.InsertBefore = function (root, n) { - var old = this.nodes[root]; - var new_nodes = this.stack.splice(this.stack.length - n); - old.before.apply(old, new_nodes); - }; - Interpreter.prototype.Remove = function (root) { - var node = this.nodes[root]; - if (node !== undefined) { - node.remove(); - } - }; - Interpreter.prototype.CreateTextNode = function (text, root) { - // todo: make it so the types are okay - var node = document.createTextNode(text); - this.nodes[root] = node; - this.stack.push(node); - }; - Interpreter.prototype.CreateElement = function (tag, root) { - var el = document.createElement(tag); - el.setAttribute("dioxus-id", "".concat(root)); - this.nodes[root] = el; - this.stack.push(el); - }; - Interpreter.prototype.CreateElementNs = function (tag, root, ns) { - var el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.CreatePlaceholder = function (root) { - var el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - }; - Interpreter.prototype.NewEventListener = function ( - event_name, - scope, - root, - handler - ) { - // console.log('new event listener', event_name, root, scope); - var element = this.nodes[root]; - element.setAttribute( - "dioxus-event-".concat(event_name), - "".concat(scope, ".").concat(root) - ); - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } else { - this.listeners[event_name]++; - } - }; - Interpreter.prototype.RemoveEventListener = function (root, event_name) { - var element = this.nodes[root]; - element.removeAttribute("dioxus-event-".concat(event_name)); - 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]; - } - }; - Interpreter.prototype.SetText = function (root, text) { - this.nodes[root].textContent = text; - }; - Interpreter.prototype.SetAttribute = function (root, field, value, ns) { - var name = field; - var node = this.nodes[root]; - if (ns == "style") { - // @ts-ignore - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value != node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - }; - Interpreter.prototype.RemoveAttribute = function (root, name) { - var node = this.nodes[root]; - node.removeAttribute(name); - if (name === "value") { - node.value = ""; - } - if (name === "checked") { - node.checked = false; - } - if (name === "selected") { - node.selected = false; - } - }; - Interpreter.prototype.handleEdits = function (edits) { - this.stack.push(this.root); - for (var _i = 0, edits_1 = edits; _i < edits_1.length; _i++) { - var edit = edits_1[_i]; - this.handleEdit(edit); - } - }; - Interpreter.prototype.handleEdit = function (edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // todo: only on desktop should we make our own handler - var handler = function (event) { - var target = event.target; - // console.log("event", event); - if (target != null) { - var realId = target.getAttribute("dioxus-id"); - var shouldPreventDefault = target.getAttribute( - "dioxus-prevent-default" - ); - var contents = serialize_event(event); - if (shouldPreventDefault === "on".concat(event.type)) { - event.preventDefault(); - } - - if (event.type == "submit") { - event.preventDefault(); - } - - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - var href = target.getAttribute("href"); - if ( - href !== "" && - href !== null && - href !== undefined && - realId != null - ) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href: href, - }); - } - } - } - } - - if (realId == null) { - return; - } - window.rpc.call("user_event", { - event: edit.event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - }; - return Interpreter; -})(); -export { Interpreter }; -function main() { - var root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} -//# sourceMappingURL=interpreter.js.map diff --git a/packages/jsinterpreter/interpreter.ts b/packages/jsinterpreter/interpreter.ts index 533536277..fdd8c671d 100644 --- a/packages/jsinterpreter/interpreter.ts +++ b/packages/jsinterpreter/interpreter.ts @@ -1,3 +1,319 @@ + + + + +export function main() { + let root = window.document.getElementById("main"); + if (root != null) { + window.interpreter = new Interpreter(root); + window.rpc.call("initialize"); + } +} + +declare global { + interface Window { + interpreter: Interpreter; + rpc: { call: (method: string, args?: any) => void }; + } +} + + +export class Interpreter { + root: Element; + stack: Element[]; + listeners: { [key: string]: number }; + handlers: { [key: string]: (evt: Event) => void }; + lastNodeWasText: boolean; + nodes: Element[]; + + + constructor(root: Element) { + this.root = root; + this.stack = [root]; + this.listeners = {}; + this.handlers = {}; + this.lastNodeWasText = false; + this.nodes = [root]; + } + + top() { + return this.stack[this.stack.length - 1]; + } + + pop() { + return this.stack.pop(); + } + + PushRoot(root: number) { + const node = this.nodes[root]; + this.stack.push(node); + } + + AppendChildren(many: number) { + let root = this.stack[this.stack.length - (1 + many)]; + + let to_add = this.stack.splice(this.stack.length - many); + + for (let i = 0; i < many; i++) { + root.appendChild(to_add[i]); + } + } + + ReplaceWith(root_id: number, m: number) { + let root = this.nodes[root_id] as Element; + let els = this.stack.splice(this.stack.length - m); + + root.replaceWith(...els); + } + + InsertAfter(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); + old.after(...new_nodes); + } + + InsertBefore(root: number, n: number) { + let old = this.nodes[root] as Element; + let new_nodes = this.stack.splice(this.stack.length - n); + old.before(...new_nodes); + } + + Remove(root: number) { + let node = this.nodes[root] as Element; + if (node !== undefined) { + node.remove(); + } + } + + CreateTextNode(text: string, root: number) { + // todo: make it so the types are okay + const node = document.createTextNode(text) as any as Element; + this.nodes[root] = node; + this.stack.push(node); + } + + CreateElement(tag: string, root: number) { + const el = document.createElement(tag); + // el.setAttribute("data-dioxus-id", `${root}`); + + this.nodes[root] = el; + this.stack.push(el); + } + + CreateElementNs(tag: string, root: number, ns: string) { + let el = document.createElementNS(ns, tag); + this.stack.push(el); + this.nodes[root] = el; + } + + CreatePlaceholder(root: number) { + let el = document.createElement("pre"); + el.hidden = true; + this.stack.push(el); + this.nodes[root] = el; + } + + NewEventListener(event_name: string, root: number, handler: (evt: Event) => void) { + const element = this.nodes[root]; + element.setAttribute("data-dioxus-id", `${root}`); + + if (this.listeners[event_name] === undefined) { + this.listeners[event_name] = 0; + this.handlers[event_name] = handler; + this.root.addEventListener(event_name, handler); + } else { + this.listeners[event_name]++; + } + } + + RemoveEventListener(root: number, event_name: string) { + 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]; + } + } + + + SetText(root: number, text: string) { + this.nodes[root].textContent = text; + } + + SetAttribute(root: number, field: string, value: string, ns: string | undefined) { + const name = field; + const node = this.nodes[root]; + + if (ns == "style") { + + // @ts-ignore + (node as HTMLElement).style[name] = value; + + } else if (ns != null || ns != undefined) { + node.setAttributeNS(ns, name, value); + } else { + switch (name) { + case "value": + if (value != (node as HTMLInputElement).value) { + (node as HTMLInputElement).value = value; + } + break; + case "checked": + (node as HTMLInputElement).checked = value === "true"; + break; + case "selected": + (node as HTMLOptionElement).selected = value === "true"; + break; + case "dangerous_inner_html": + node.innerHTML = value; + break; + default: + // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 + if (value == "false" && bool_attrs.hasOwnProperty(name)) { + node.removeAttribute(name); + } else { + node.setAttribute(name, value); + } + } + } + } + RemoveAttribute(root: number, name: string) { + + const node = this.nodes[root]; + node.removeAttribute(name); + + if (name === "value") { + (node as HTMLInputElement).value = ""; + } + + if (name === "checked") { + (node as HTMLInputElement).checked = false; + } + + if (name === "selected") { + (node as HTMLOptionElement).selected = false; + } + } + + handleEdits(edits: DomEdit[]) { + this.stack.push(this.root); + + for (let edit of edits) { + this.handleEdit(edit); + } + } + + handleEdit(edit: DomEdit) { + switch (edit.type) { + case "PushRoot": + this.PushRoot(edit.root); + break; + case "AppendChildren": + this.AppendChildren(edit.many); + break; + case "ReplaceWith": + this.ReplaceWith(edit.root, edit.m); + break; + case "InsertAfter": + this.InsertAfter(edit.root, edit.n); + break; + case "InsertBefore": + this.InsertBefore(edit.root, edit.n); + break; + case "Remove": + this.Remove(edit.root); + break; + case "CreateTextNode": + this.CreateTextNode(edit.text, edit.root); + break; + case "CreateElement": + this.CreateElement(edit.tag, edit.root); + break; + case "CreateElementNs": + this.CreateElementNs(edit.tag, edit.root, edit.ns); + break; + case "CreatePlaceholder": + this.CreatePlaceholder(edit.root); + break; + case "RemoveEventListener": + this.RemoveEventListener(edit.root, edit.event_name); + break; + case "NewEventListener": + + + // this handler is only provided on desktop implementations since this + // method is not used by the web implementation + let handler = (event: Event) => { + let target = event.target as Element | null; + + if (target != null) { + let realId = target.getAttribute(`data-dioxus-id`); + + // walk the tree to find the real element + while (realId == null && target.parentElement != null) { + target = target.parentElement; + realId = target.getAttribute(`data-dioxus-id`); + } + + const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); + + let contents = serialize_event(event); + + if (shouldPreventDefault === `on${event.type}`) { + event.preventDefault(); + } + + if (event.type == "submit") { + event.preventDefault(); + } + + if (event.type == "click") { + event.preventDefault(); + if (shouldPreventDefault !== `onclick`) { + if (target.tagName == "A") { + const href = target.getAttribute("href") + if (href !== "" && href !== null && href !== undefined && realId != null) { + window.rpc.call("browser_open", { + mounted_dom_id: parseInt(realId), + href + }); + } + } + } + } + + if (realId == null) { + return; + } + + window.rpc.call("user_event", { + event: (edit as NewEventListener).event_name, + mounted_dom_id: parseInt(realId), + contents: contents, + }); + } + }; + this.NewEventListener(edit.event_name, edit.root, handler); + break; + case "SetText": + this.SetText(edit.root, edit.text); + break; + case "SetAttribute": + this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); + break; + case "RemoveAttribute": + this.RemoveAttribute(edit.root, edit.name); + break; + } + } +} + + + function serialize_event(event: Event) { switch (event.type) { case "copy": @@ -327,307 +643,6 @@ const bool_attrs = { truespeed: true, }; -export class Interpreter { - root: Element; - stack: Element[]; - listeners: { [key: string]: number }; - handlers: { [key: string]: (evt: Event) => void }; - lastNodeWasText: boolean; - nodes: Element[]; - - - constructor(root: Element) { - this.root = root; - this.stack = [root]; - this.listeners = {}; - this.handlers = {}; - this.lastNodeWasText = false; - this.nodes = [root]; - } - - top() { - return this.stack[this.stack.length - 1]; - } - - pop() { - return this.stack.pop(); - } - - PushRoot(root: number) { - const node = this.nodes[root]; - this.stack.push(node); - } - - AppendChildren(many: number) { - let root = this.stack[this.stack.length - (1 + many)]; - - let to_add = this.stack.splice(this.stack.length - many); - - for (let i = 0; i < many; i++) { - root.appendChild(to_add[i]); - } - } - - ReplaceWith(root_id: number, m: number) { - let root = this.nodes[root_id] as Element; - let els = this.stack.splice(this.stack.length - m); - - root.replaceWith(...els); - } - - InsertAfter(root: number, n: number) { - let old = this.nodes[root] as Element; - let new_nodes = this.stack.splice(this.stack.length - n); - old.after(...new_nodes); - } - - InsertBefore(root: number, n: number) { - let old = this.nodes[root] as Element; - let new_nodes = this.stack.splice(this.stack.length - n); - old.before(...new_nodes); - } - - Remove(root: number) { - let node = this.nodes[root] as Element; - if (node !== undefined) { - node.remove(); - } - } - - CreateTextNode(text: string, root: number) { - // todo: make it so the types are okay - const node = document.createTextNode(text) as any as Element; - this.nodes[root] = node; - this.stack.push(node); - } - - CreateElement(tag: string, root: number) { - const el = document.createElement(tag); - el.setAttribute("dioxus-id", `${root}`); - - this.nodes[root] = el; - this.stack.push(el); - } - - CreateElementNs(tag: string, root: number, ns: string) { - let el = document.createElementNS(ns, tag); - this.stack.push(el); - this.nodes[root] = el; - } - - CreatePlaceholder(root: number) { - let el = document.createElement("pre"); - el.hidden = true; - this.stack.push(el); - this.nodes[root] = el; - } - - NewEventListener(event_name: string, scope: number, root: number, handler: (evt: Event) => void) { - // console.log('new event listener', event_name, root, scope); - const element = this.nodes[root]; - element.setAttribute( - `dioxus-event-${event_name}`, - `${scope}.${root}` - ); - - if (this.listeners[event_name] === undefined) { - this.listeners[event_name] = 0; - this.handlers[event_name] = handler; - this.root.addEventListener(event_name, handler); - } else { - this.listeners[event_name]++; - } - } - - RemoveEventListener(root: number, event_name: string) { - const element = this.nodes[root]; - element.removeAttribute( - `dioxus-event-${event_name}` - ); - - 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]; - } - } - - - SetText(root: number, text: string) { - this.nodes[root].textContent = text; - } - - SetAttribute(root: number, field: string, value: string, ns: string | undefined) { - const name = field; - const node = this.nodes[root]; - - if (ns == "style") { - - // @ts-ignore - (node as HTMLElement).style[name] = value; - - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value != (node as HTMLInputElement).value) { - (node as HTMLInputElement).value = value; - } - break; - case "checked": - (node as HTMLInputElement).checked = value === "true"; - break; - case "selected": - (node as HTMLOptionElement).selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value == "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root: number, name: string) { - - const node = this.nodes[root]; - node.removeAttribute(name); - - if (name === "value") { - (node as HTMLInputElement).value = ""; - } - - if (name === "checked") { - (node as HTMLInputElement).checked = false; - } - - if (name === "selected") { - (node as HTMLOptionElement).selected = false; - } - } - - handleEdits(edits: DomEdit[]) { - this.stack.push(this.root); - - for (let edit of edits) { - this.handleEdit(edit); - } - } - - handleEdit(edit: DomEdit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.many); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.m); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.n); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.n); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - - - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event: Event) => { - const target = event.target as Element | null; - - if (target != null) { - const realId = target.getAttribute(`dioxus-id`); - - const shouldPreventDefault = target.getAttribute(`dioxus-prevent-default`); - - let contents = serialize_event(event); - - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - - if (event.type == "submit") { - event.preventDefault(); - } - - if (event.type == "click") { - event.preventDefault(); - if (shouldPreventDefault !== `onclick`) { - if (target.tagName == "A") { - const href = target.getAttribute("href") - if (href !== "" && href !== null && href !== undefined && realId != null) { - window.rpc.call("browser_open", { - mounted_dom_id: parseInt(realId), - href - }); - } - } - } - } - - if (realId == null) { - return; - } - - window.rpc.call("user_event", { - event: (edit as NewEventListener).event_name, - mounted_dom_id: parseInt(realId), - contents: contents, - }); - } - }; - this.NewEventListener(edit.event_name, edit.scope, edit.root, handler); - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name); - break; - } - } -} - -function main() { - let root = window.document.getElementById("main"); - if (root != null) { - window.interpreter = new Interpreter(root); - window.rpc.call("initialize"); - } -} type PushRoot = { type: "PushRoot", root: number }; @@ -663,14 +678,3 @@ type DomEdit = SetText | SetAttribute | RemoveAttribute; - - -export { }; -declare global { - interface Window { - interpreter: Interpreter; - rpc: { call: (method: string, args?: any) => void }; - } -} - - diff --git a/packages/jsinterpreter/tsconfig.json b/packages/jsinterpreter/tsconfig.json index 883760881..f63034044 100644 --- a/packages/jsinterpreter/tsconfig.json +++ b/packages/jsinterpreter/tsconfig.json @@ -1,13 +1,13 @@ { "compilerOptions": { - "target": "ES5", - "module": "ESNext", + "target": "ESNext", + "module": "ES2015", "lib": [ "es2015", "es5", "es6", "dom" ], - "strict": true + "strict": true, } } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index b0b4dd035..ffbef9f4b 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -277,9 +277,7 @@ fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc anyhow::Result { - use anyhow::Context; - - let target = event + let mut target = event .target() .expect("missing target") .dyn_into::() @@ -287,18 +285,36 @@ fn decode_trigger(event: &web_sys::Event) -> anyhow::Result { let typ = event.type_(); - let element_id = target - .get_attribute("dioxus-id") - .context("Could not find element id on event target")? - .parse()?; - - Ok(UserEvent { - name: event_name_from_typ(&typ), - data: virtual_event_from_websys_event(event.clone()), - element: Some(ElementId(element_id)), - scope_id: None, - priority: dioxus_core::EventPriority::Medium, - }) + loop { + match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { + Some(Ok(id)) => { + return Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: Some(ElementId(id)), + scope_id: None, + priority: dioxus_core::EventPriority::Medium, + }); + } + Some(Err(e)) => { + return Err(e.into()); + } + None => { + // walk the tree upwards until we actually find an event target + if let Some(parent) = target.parent_element() { + target = parent; + } else { + return Ok(UserEvent { + name: event_name_from_typ(&typ), + data: virtual_event_from_websys_event(event.clone()), + element: None, + scope_id: None, + priority: dioxus_core::EventPriority::Low, + }); + } + } + } + } } pub(crate) fn load_document() -> Document { From 9e4ec43b1e78d355c56a38e4c092170b2b01b20d Mon Sep 17 00:00:00 2001 From: Dave Rolsky Date: Fri, 21 Jan 2022 21:43:43 -0600 Subject: [PATCH 58/79] Fix various typos and grammar nits I also removed all trailing whitespace from lines since I have Emacs configured to highlight this. --- README.md | 18 +++++------ docs/guide/src/README.md | 12 ++++---- docs/guide/src/SUMMARY.md | 6 ++-- docs/guide/src/advanced-guides/00-index.md | 2 +- .../src/advanced-guides/10-concurrent-mode.md | 4 +-- .../src/advanced-guides/11-arena-memo.md | 8 ++--- docs/guide/src/advanced-guides/12-signals.md | 8 ++--- docs/guide/src/advanced-guides/13-subtrees.md | 4 +-- .../src/advanced-guides/custom-renderer.md | 2 +- docs/guide/src/async/index.md | 4 +-- docs/guide/src/elements/component_children.md | 8 ++--- docs/guide/src/elements/components.md | 26 ++++++++-------- .../src/elements/conditional_rendering.md | 4 +-- .../src/elements/exporting_components.md | 30 +++++++++---------- docs/guide/src/elements/index.md | 2 +- docs/guide/src/elements/lists.md | 12 ++++---- docs/guide/src/elements/vnodes.md | 8 ++--- docs/guide/src/final.md | 4 +-- docs/guide/src/hello_world.md | 8 ++--- docs/guide/src/interactivity/hooks.md | 10 +++---- docs/guide/src/interactivity/index.md | 14 ++++----- docs/guide/src/setup.md | 13 ++++---- docs/guide/src/state/index.md | 4 +-- packages/core-macro/src/lib.rs | 8 ++--- packages/core/README.md | 2 +- packages/core/architecture.md | 16 +++++----- packages/core/src/events.rs | 2 +- packages/core/src/nodes.rs | 4 +-- packages/core/src/scopes.rs | 4 +-- packages/core/src/util.rs | 2 +- packages/core/src/virtual_dom.rs | 8 ++--- packages/hooks/src/usestate/handle.rs | 2 +- packages/html/README.md | 18 +++++------ packages/liveview/README.md | 4 +-- packages/mobile/README.md | 2 +- packages/router/README.md | 2 +- packages/router/src/components/link.rs | 2 +- packages/ssr/README.md | 4 +-- src/lib.rs | 30 +++++++++---------- 39 files changed, 160 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 641f5a8b8..7b277d16a 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ If you know React, then you already know Dioxus. - Comprehensive inline documentation - hover and guides for all HTML elements, listeners, and events. - Extremely memory efficient - 0 global allocations for steady-state components. - Multi-channel asynchronous scheduler for first-class async support. -- And more! Read the [full release post here](https://dioxuslabs.com/blog/introducing-dioxus/). +- And more! Read the [full release post](https://dioxuslabs.com/blog/introducing-dioxus/). ### Examples @@ -121,9 +121,9 @@ See the [awesome-dioxus](https://github.com/DioxusLabs/awesome-dioxus) page for ## Why Dioxus and why Rust? -TypeScript is a fantastic addition to JavaScript, but it's still fundamentally JavaScript. TS code runs slightly slower, has tons of configuration options, and not every package is properly typed. +TypeScript is a fantastic addition to JavaScript, but it's still fundamentally JavaScript. TS code runs slightly slower, has tons of configuration options, and not every package is properly typed. -In contrast, Dioxus is written in Rust - which is almost like "TypeScript on steroids". +In contrast, Dioxus is written in Rust - which is almost like "TypeScript on steroids". By using Rust, we gain: @@ -141,12 +141,12 @@ By using Rust, we gain: Specifically, Dioxus provides us many other assurances: -- Proper use of immutable datastructures -- Guaranteed error handling (so you can sleep easy at night not worrying about `cannot read property of undefined`) +- Proper use of immutable data structures +- Guaranteed error handling (so you can sleep easy at night not worrying about `cannot read property of undefined`) - Native performance on mobile - Direct access to system IO -And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. +And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. ### Why NOT Dioxus? You shouldn't use Dioxus if: @@ -154,13 +154,13 @@ You shouldn't use Dioxus if: - You don't like the React Hooks approach to frontend - You need a no-std renderer - You want to support browsers where Wasm or asm.js are not supported. -- You need a Send+Sync UI solution (Dioxus is not currently ThreadSafe) +- You need a Send+Sync UI solution (Dioxus is not currently thread-safe) ### Comparison with other Rust UI frameworks -Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. +Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. - [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, supports SSR (no hydration). -- [Percy](https://github.com/chinedufn/percy): Supports SSR but less emphasis on state management and event handling. +- [Percy](https://github.com/chinedufn/percy): Supports SSR but with less emphasis on state management and event handling. - [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but lacking in ergonomics. - [Dominator](https://github.com/Pauan/rust-dominator): Signal-based zero-cost alternative, less emphasis on community and docs. diff --git a/docs/guide/src/README.md b/docs/guide/src/README.md index c91f9f1bb..c00dcd274 100644 --- a/docs/guide/src/README.md +++ b/docs/guide/src/README.md @@ -22,7 +22,7 @@ In general, Dioxus and React share many functional similarities. If this guide i ## Multiplatform -Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to Web-Sys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `Html` feature enabled which can be disabled depending on your target renderer. +Dioxus is a *portable* toolkit, meaning the Core implementation can run anywhere with no platform-dependent linking. Unlike many other Rust frontend toolkits, Dioxus is not intrinsically linked to Web-Sys. In fact, every element and event listener can be swapped out at compile time. By default, Dioxus ships with the `html` feature enabled, but this can be disabled depending on your target renderer. Right now, we have several 1st-party renderers: - WebSys (for WASM) @@ -33,8 +33,7 @@ Right now, we have several 1st-party renderers: ### Web Support --- - -The Web is the most-supported target platform for Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` crate with the `web` feature enabled. Because of the Wasm limitation, not every crate will work with your web-apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc). +The Web is the best-supported target platform for Dioxus. To run on the Web, your app must be compiled to WebAssembly and depend on the `dioxus` crate with the `web` feature enabled. Because of the limitations of Wasm not every crate will work with your web-apps, so you'll need to make sure that your crates work without native system calls (timers, IO, etc). Because the web is a fairly mature platform, we expect there to be very little API churn for web-based features. @@ -45,9 +44,10 @@ Examples: - [ECommerce](https://github.com/DioxusLabs/example-projects/tree/master/ecommerce-site) [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) + ### SSR Support --- -Dioxus supports server-side rendering! +Dioxus supports server-side rendering! For rendering statically to an `.html` file or from a WebServer, then you'll want to make sure the `ssr` feature is enabled in the `dioxus` crate and use the `dioxus::ssr` API. We don't expect the SSR API to change drastically in the future. @@ -90,13 +90,13 @@ Examples: ### LiveView / Server Component Support --- -The internal architecture of Dioxus was designed from day one to support the `LiveView` use-case, where a web server hosts a running app for each connected user. As of today, there is no first-class LiveView support - you'll need to wire this up yourself. +The internal architecture of Dioxus was designed from day one to support the `LiveView` use-case, where a web server hosts a running app for each connected user. As of today, there is no first-class LiveView support - you'll need to wire this up yourself. While not currently fully implemented, the expectation is that LiveView apps can be a hybrid between Wasm and server-rendered where only portions of a page are "live" and the rest of the page is either server-rendered, statically generated, or handled by the host SPA. ### Multithreaded Support --- -The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. This means you can't easily use Dioxus with most web frameworks like Tide, Rocket, Axum, etc. +The Dioxus VirtualDom, sadly, is not currently `Send`. Internally, we use quite a bit of interior mutability which is not thread-safe. This means you can't easily use Dioxus with most web frameworks like Tide, Rocket, Axum, etc. To solve this, you'll want to spawn a VirtualDom on its own thread and communicate with it via channels. diff --git a/docs/guide/src/SUMMARY.md b/docs/guide/src/SUMMARY.md index 2e77d4ab8..2fa29f39e 100644 --- a/docs/guide/src/SUMMARY.md +++ b/docs/guide/src/SUMMARY.md @@ -1,7 +1,7 @@ # Summary - [Introduction](README.md) -- [Getting Setup](setup.md) +- [Getting Set Up](setup.md) - [Hello, World!](hello_world.md) - [Describing the UI](elements/index.md) - [Intro to Elements](elements/vnodes.md) @@ -17,8 +17,8 @@ - [User Input and Controlled Components](interactivity/user_input.md) - [Lifecycle, updates, and effects](interactivity/lifecycles.md) - [Managing State](state/index.md) - - [Local State](state/localstate.md) - - [Lifting State](state/liftingstate.md) + - [Local State](state/localstate.md) + - [Lifting State](state/liftingstate.md) - [Global State](state/sharedstate.md) - [Error handling](state/errorhandling.md) - [Working with Async](async/index.md) diff --git a/docs/guide/src/advanced-guides/00-index.md b/docs/guide/src/advanced-guides/00-index.md index 13a61a868..aedc39e34 100644 --- a/docs/guide/src/advanced-guides/00-index.md +++ b/docs/guide/src/advanced-guides/00-index.md @@ -1,6 +1,6 @@ # Core Topics -In this chapter, we'll cover some core topics on how Dioxus works and how to best leverage the features to build a beautiful, reactive app. +In this chapter, we'll cover some core topics about how Dioxus works and how to best leverage the features to build a beautiful, reactive app. At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes. diff --git a/docs/guide/src/advanced-guides/10-concurrent-mode.md b/docs/guide/src/advanced-guides/10-concurrent-mode.md index 5d7801cb6..6bbebbeed 100644 --- a/docs/guide/src/advanced-guides/10-concurrent-mode.md +++ b/docs/guide/src/advanced-guides/10-concurrent-mode.md @@ -33,9 +33,9 @@ async fetch_name() -> String { This component will only schedule its render once the fetch is complete. However, we _don't_ recommend using async/await directly in your components. -Async is a notoriously challenging yet rewarding tool for efficient tools. If not careful, locking and unlocking shared aspects of the component's context can lead to data races and panics. If a shared resource is locked while the component is awaiting, then other components can be locked or panic when trying to access the same resource. These rules are especially important when references to shared global state are accessed using the context object's lifetime. If mutable references to data captured immutably by the context are taken, then the component will panic, and cause confusion. +Async is a notoriously challenging yet rewarding tool for efficient tools. If not careful, locking and unlocking shared aspects of the component's context can lead to data races and panics. If a shared resource is locked while the component is awaiting, then other components can be locked or panic when trying to access the same resource. These rules are especially important when references to shared global state are accessed using the context object's lifetime. If mutable references to data captured immutably by the context are taken, then the component will panic, causing confusion. -Instead, we suggest using hooks and future combinators that can safely utilize the safe guards of the component's Context when interacting with async tasks. +Instead, we suggest using hooks and future combinators that can safely utilize the safeguards of the component's Context when interacting with async tasks. As part of our Dioxus hooks crate, we provide a data loader hook which pauses a component until its async dependencies are ready. This caches requests, reruns the fetch if dependencies have changed, and provides the option to render something else while the component is loading. diff --git a/docs/guide/src/advanced-guides/11-arena-memo.md b/docs/guide/src/advanced-guides/11-arena-memo.md index d95a67d19..d5eaaa1d0 100644 --- a/docs/guide/src/advanced-guides/11-arena-memo.md +++ b/docs/guide/src/advanced-guides/11-arena-memo.md @@ -2,13 +2,13 @@ Dioxus differs slightly from other UI virtual doms in some subtle ways due to its memory allocator. -One important aspect to understand is how props are passed down from parent components to children. All "components" (custom user-made UI elements) are tightly allocated together in an arena. However, because props and hooks are generically typed, they are casted to any and allocated on the heap - not in the arena with the components. +One important aspect to understand is how props are passed down from parent components to children. All "components" (custom user-made UI elements) are tightly allocated together in an arena. However, because props and hooks are generically typed, they are casted to `Any` and allocated on the heap - not in the arena with the components. With this system, we try to be more efficient when leaving the component arena and entering the heap. By default, props are memoized between renders using COW and context. This makes props comparisons fast - done via ptr comparisons on the cow pointer. Because memoization is done by default, parent re-renders will _not_ cascade to children if the child's props did not change. https://dmitripavlutin.com/use-react-memo-wisely/ -This behavior is defined as an implicit attribute to user components. When in React land you might wrap a component is `react.memo`, Dioxus components are automatically memoized via an implicit attribute. You can manually configure this behavior on any component with "nomemo" to disable memoization. +This behavior is defined as an attribute implicit to user components. When in React land you might wrap a component with `react.memo`, Dioxus components are automatically memoized via an implicit attribute. You can manually configure this behavior on any component with "nomemo" to disable memoization. ```rust fn test() -> DomTree { @@ -40,7 +40,7 @@ fn test_component(cx: Scope, name: String) -> Element { "This is different than React, why differ?". -Take a component likes this: +Take a component like this: ```rust fn test(cx: Scope) -> DomTree { @@ -55,4 +55,4 @@ fn test(cx: Scope) -> DomTree { } ``` -While the contents of the destructured bundle might change, not every child component will need to be re-rendered. +While the contents of the destructured bundle might change, not every child component will need to be re-rendered every time the context changes. diff --git a/docs/guide/src/advanced-guides/12-signals.md b/docs/guide/src/advanced-guides/12-signals.md index 051247475..cae2e1726 100644 --- a/docs/guide/src/advanced-guides/12-signals.md +++ b/docs/guide/src/advanced-guides/12-signals.md @@ -1,6 +1,6 @@ # Signals: Skipping the Diff -In most cases, the traditional VirtualDOM diffing pattern is plenty fast. Dioxus will compare trees of VNodes, find the differences, and then update the Renderer's DOM with the updates. However, this can generate a lot of overhead for certain types of components. In apps where reducing visual latency is a top priority, you can opt into the `Signals` api to entirely disable diffing of hot-path components. Dioxus will then automatically construct a state machine for your component, making updates nearly instant. +In most cases, the traditional VirtualDOM diffing pattern is plenty fast. Dioxus will compare trees of VNodes, find the differences, and then update the Renderer's DOM with the diffs. However, this can generate a lot of overhead for certain types of components. In apps where reducing visual latency is a top priority, you can opt into the `Signals` api to entirely disable diffing of hot-path components. Dioxus will then automatically construct a state machine for your component, making updates nearly instant. Signals build on the same infrastructure that powers asynchronous rendering where in-tree values can be updated outside of the render phase. In async rendering, a future is used as the signal source. With the raw signal API, any value can be used as a signal source. @@ -69,7 +69,7 @@ fn Calculator(cx: Scope) -> DomTree { } ``` -Do you notice how we can use built-in operations on signals? Under the hood, we actually create a new derived signal that depends on `a` and `b`. Whenever `a` or `b` update, then `c` will update. If we need to create a new derived signal that's more complex that a basic operation (`std::ops`) we can either chain signals together or combine them: +Do you notice how we can use built-in operations on signals? Under the hood, we actually create a new derived signal that depends on `a` and `b`. Whenever `a` or `b` update, then `c` will update. If we need to create a new derived signal that's more complex than a basic operation (`std::ops`) we can either chain signals together or combine them: ```rust let mut a = use_signal(&cx, || 0); @@ -88,7 +88,7 @@ let mut a = use_signal(&cx, || 0); let c = *a + *b; ``` -Calling `deref` or `derefmut` is actually more complex than it seems. When a value is derefed, you're essentially telling Dioxus that _this_ element _needs_ to be subscribed to the signal. If a signal is derefed outside of an element, the entire component will be subscribed and the advantage of skipping diffing will be lost. Dioxus will throw an error in the console when this happens to tell you that you're using signals wrong, but your component will continue to work. +Calling `deref` or `deref_mut` is actually more complex than it seems. When a value is derefed, you're essentially telling Dioxus that _this_ element _needs_ to be subscribed to the signal. If a signal is derefed outside of an element, the entire component will be subscribed and the advantage of skipping diffing will be lost. Dioxus will throw an error in the console when this happens to tell you that you're using signals wrong, but your component will continue to work. ## Global Signals @@ -128,7 +128,7 @@ Sometimes you want to use a collection of items. With Signals, you can bypass di By default, Dioxus is limited when you use iter/map. With the `For` component, you can provide an iterator and a function for the iterator to map to. -Dioxus automatically understands how to use your signals when mixed with iterators through Deref/DerefMut. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster. +Dioxus automatically understands how to use your signals when mixed with iterators through `Deref`/`DerefMut`. This lets you efficiently map collections while avoiding the re-rendering of lists. In essence, signals act as a hint to Dioxus on how to avoid un-necessary checks and renders, making your app faster. ```rust const DICT: AtomFamily = |_| {}; diff --git a/docs/guide/src/advanced-guides/13-subtrees.md b/docs/guide/src/advanced-guides/13-subtrees.md index 37a2ef786..7c99ecf4f 100644 --- a/docs/guide/src/advanced-guides/13-subtrees.md +++ b/docs/guide/src/advanced-guides/13-subtrees.md @@ -6,7 +6,7 @@ For a VirtualDom that has a root tree with two subtrees, the edits follow a patt Root -> Tree 1 --> Tree 2 +-> Tree 2 -> Original root tree - Root edits @@ -39,7 +39,7 @@ fn Window() -> DomTree { onassign: move |e| { // create window } - children() + children() } } diff --git a/docs/guide/src/advanced-guides/custom-renderer.md b/docs/guide/src/advanced-guides/custom-renderer.md index fbff277b0..d2aa40bc9 100644 --- a/docs/guide/src/advanced-guides/custom-renderer.md +++ b/docs/guide/src/advanced-guides/custom-renderer.md @@ -20,7 +20,7 @@ For reference, check out the WebSys renderer as a starting point for your custom ## Trait implementation and DomEdits -The current `RealDom` trait lives in `dioxus_core/diff`. A version of it is provided here (but might not be up-to-date): +The current `RealDom` trait lives in `dioxus-core/diff`. A version of it is provided here (but might not be up-to-date): ```rust pub trait RealDom<'a> { diff --git a/docs/guide/src/async/index.md b/docs/guide/src/async/index.md index 41a8b9231..47b84e81a 100644 --- a/docs/guide/src/async/index.md +++ b/docs/guide/src/async/index.md @@ -1,13 +1,13 @@ # Working with Async -Not all apps you'll build can be self-contained with synchronous code. You'll often need to interact with file systems, network interfaces, hardware, or timers. +Not all apps you'll build can be self-contained with synchronous code. You'll often need to interact with file systems, network interfaces, hardware, or timers. So far, we've only talked about building apps with synchronous code, so this chapter will focus integrating asynchronous code into your app. ## The Runtime -By default, Dioxus-Desktop ships with the `Tokio` runtime and automatically sets everything up for you. +By default, Dioxus-Desktop ships with the `Tokio` runtime and automatically sets everything up for you. diff --git a/docs/guide/src/elements/component_children.md b/docs/guide/src/elements/component_children.md index d8469241a..d99273394 100644 --- a/docs/guide/src/elements/component_children.md +++ b/docs/guide/src/elements/component_children.md @@ -9,7 +9,7 @@ In this chapter, you'll learn about: ## The use case -Let's say you're building a user interface and want to make some part of it clickable to another website. You would normally start with the HTML `` tag, like so: +Let's say you're building a user interface and want to make some part of it a clickable link to another website. You would normally start with the HTML `` tag, like so: ```rust rsx!( @@ -98,7 +98,7 @@ struct ClickableProps<'a> { children: Element<'a> } -fn clickable(cx: Scope) -> Element { +fn Clickable(cx: Scope) -> Element { cx.render(rsx!( a { href: "{cx.props.href}", @@ -162,7 +162,7 @@ struct ClickableProps<'a> { fn clickable(cx: Scope) -> Element { cx.render(rsx!( - a { + a { ..cx.props.attributes, "Any link, anywhere" } @@ -186,7 +186,7 @@ struct ClickableProps<'a> { fn clickable(cx: Scope) -> Element { cx.render(rsx!( - a { + a { onclick: move |evt| cx.props.onclick.call(evt) } )) diff --git a/docs/guide/src/elements/components.md b/docs/guide/src/elements/components.md index 8567e9270..ea43034d9 100644 --- a/docs/guide/src/elements/components.md +++ b/docs/guide/src/elements/components.md @@ -3,8 +3,8 @@ In the previous chapter, we learned about Elements and how they can be composed to create a basic User Interface. In this chapter, we'll learn how to group Elements together to form Components. In this chapter, we'll learn: -- What makes a Component -- How to model a component and its properties in Dioxus +- What makes a Component +- How to model a component and its properties in Dioxus - How to "think declaratively" ## What is a component? @@ -39,7 +39,7 @@ struct PostData { } ``` -If we look at the layout of the component, we notice quite a few buttons and functionality: +If we look at the layout of the component, we notice quite a few buttons and pieces of functionality: - Upvote/Downvote - View comments @@ -51,9 +51,9 @@ If we look at the layout of the component, we notice quite a few buttons and fun - Crosspost - Filter by site - View article -- Visit user +- Visit user -If we included all this functionality in one `rsx!` call, it would be huge! Instead, let's break the post down into some core pieces: +If we included all this functionality in one `rsx!` call it would be huge! Instead, let's break the post down into some core pieces: ![Post as Component](../images/reddit_post_components.png) @@ -112,7 +112,7 @@ Let's take a look at the `VoteButton` component. For now, we won't include any i Most of your Components will look exactly like this: a Props struct and a render function. Every component must take a `Scope` generic over some `Props` and return an `Element`. -As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`. +As covered before, we'll build our User Interface with the `rsx!` macro and HTML tags. However, with components, we must actually "render" our HTML markup. Calling `cx.render` converts our "lazy" `rsx!` structure into an `Element`. ```rust #[derive(PartialEq, Props)] @@ -133,9 +133,9 @@ fn VoteButton(cx: Scope) -> Element { ## Borrowing -You can avoid clones using borrowed component syntax. For example, let's say we passed the `TitleCard` title as an `&str` instead of `String`. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling `clone` on `Rc`. +You can avoid clones by using borrowed component syntax. For example, let's say we passed the `TitleCard` title as an `&str` instead of `String`. In JavaScript, the string would be copied by reference - none of the contents would be copied, but rather the reference to the string's contents are copied. In Rust, this would be similar to calling `clone` on `Rc`. -Because we're working in Rust, we can choose to either use `Rc`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned. +Because we're working in Rust, we can choose to either use `Rc`, clone `Title` on every re-render of `Post`, or simply borrow it. In most cases, you'll just want to let `Title` be cloned. To enable borrowed values for your component, we need to add a lifetime to let the Rust compiler know that the output `Element` borrows from the component's props. @@ -149,7 +149,7 @@ fn TitleCard<'a>(cx: Scope<'a, TitleCardProps<'a>>) -> Element { cx.render(rsx!{ h1 { "{cx.props.title}" } }) -} +} ``` For users of React: Dioxus knows *not* to memoize components that borrow property fields. By default, every component in Dioxus is memoized. This can be disabled by the presence of a non-`'static` borrow. @@ -174,7 +174,7 @@ fn TitleCard(cx: Scope) -> Element { cx.render(rsx!{ h1 { "{cx.props.title}" } }) -} +} ``` to: @@ -185,7 +185,7 @@ fn TitleCard(cx: Scope, title: String) -> Element { cx.render(rsx!{ h1 { "{title}" } }) -} +} ``` Again, this macro is optional and should not be used by library authors since you have less fine-grained control over documentation and optionality. @@ -194,9 +194,9 @@ However, it's great for quickly throwing together an app without dealing with *a ## The `Scope` object -Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration. +Though very similar to React, Dioxus is different in a few ways. Most notably, React components will not have a `Scope` parameter in the component declaration. -Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state? +Have you ever wondered how the `useState()` call works in React without a `this` object to actually store the state? React uses global variables to store this information. Global mutable variables must be carefully managed and are broadly discouraged in Rust programs. diff --git a/docs/guide/src/elements/conditional_rendering.md b/docs/guide/src/elements/conditional_rendering.md index 211da5d1a..c13263cf6 100644 --- a/docs/guide/src/elements/conditional_rendering.md +++ b/docs/guide/src/elements/conditional_rendering.md @@ -27,7 +27,7 @@ Now that we have a "logged_in" flag accessible in our props, we can render two d ```rust fn App(cx: Scope) -> Element { - if props.logged_in { + if cx.props.logged_in { cx.render(rsx!{ DashboardScreen {} }) @@ -39,7 +39,7 @@ fn App(cx: Scope) -> Element { } ``` -When the user is logged in, then this component will return the DashboardScreen. Else, the component will render the LoginScreen. +When the user is logged in, then this component will return the DashboardScreen. If they're not logged in, the component will render the LoginScreen. ## Using match statements diff --git a/docs/guide/src/elements/exporting_components.md b/docs/guide/src/elements/exporting_components.md index 16d6f1002..117249db8 100644 --- a/docs/guide/src/elements/exporting_components.md +++ b/docs/guide/src/elements/exporting_components.md @@ -25,27 +25,27 @@ fn main() { dioxus::desktop::launch(App); } -fn App(Scope) -> Element {} +fn App(Scope) -> Element {} #[derive(PartialEq, Props)] struct PostProps{} -fn Post(Scope) -> Element {} +fn Post(Scope) -> Element {} #[derive(PartialEq, Props)] struct VoteButtonsProps {} -fn VoteButtons(Scope) -> Element {} +fn VoteButtons(Scope) -> Element {} #[derive(PartialEq, Props)] struct TitleCardProps {} -fn TitleCard(Scope) -> Element {} +fn TitleCard(Scope) -> Element {} #[derive(PartialEq, Props)] struct MetaCardProps {} -fn MetaCard(Scope) -> Element {} +fn MetaCard(Scope) -> Element {} #[derive(PartialEq, Props)] struct ActionCardProps {} -fn ActionCard(Scope) -> Element {} +fn ActionCard(Scope) -> Element {} ``` That's a lot of components for one file! We've successfully refactored our app into components, but we should probably start breaking it up into a file for each component. @@ -61,7 +61,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] struct ActionCardProps {} -fn ActionCard(Scope) -> Element {} +fn ActionCard(Scope) -> Element {} ``` We should also create a `mod.rs` file in the `post` folder so we can use it from our `main.rs`. Our `Post` component and its props will go into this file. @@ -104,10 +104,10 @@ fn App(Scope) -> Element { original_poster: "me".to_string() } }) -} +} ``` -If you tried to build this app right now, you'll get an error message saying that `Post is private, trying changing it to public`. This is because we haven't properly exported our component! To fix this, we need to make sure both the Props and Component are declared as "public": +If you tried to build this app right now, you'll get an error message saying that `Post is private, try changing it to public`. This is because we haven't properly exported our component! To fix this, we need to make sure both the Props and Component are declared as "public": ```rust // src/post/mod.rs @@ -116,7 +116,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct PostProps {} -pub fn Post(Scope) -> Element {} +pub fn Post(Scope) -> Element {} ``` While we're here, we also need to make sure each of our subcomponents are included as modules and exported. @@ -203,7 +203,7 @@ fn App(Scope) -> Element { original_poster: "me".to_string() } }) -} +} ``` @@ -255,7 +255,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct VoteButtonsProps {} -pub fn VoteButtons(Scope) -> Element {} +pub fn VoteButtons(Scope) -> Element {} ``` ```rust @@ -264,7 +264,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct TitleCardProps {} -pub fn TitleCard(Scope) -> Element {} +pub fn TitleCard(Scope) -> Element {} ``` ```rust @@ -273,7 +273,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct MetaCardProps {} -pub fn MetaCard(Scope) -> Element {} +pub fn MetaCard(Scope) -> Element {} ``` ```rust @@ -282,7 +282,7 @@ use dioxus::prelude::*; #[derive(PartialEq, Props)] pub struct ActionCardProps {} -pub fn ActionCard(Scope) -> Element {} +pub fn ActionCard(Scope) -> Element {} ``` ## Moving forward diff --git a/docs/guide/src/elements/index.md b/docs/guide/src/elements/index.md index 13a61a868..aedc39e34 100644 --- a/docs/guide/src/elements/index.md +++ b/docs/guide/src/elements/index.md @@ -1,6 +1,6 @@ # Core Topics -In this chapter, we'll cover some core topics on how Dioxus works and how to best leverage the features to build a beautiful, reactive app. +In this chapter, we'll cover some core topics about how Dioxus works and how to best leverage the features to build a beautiful, reactive app. At a very high level, Dioxus is simply a Rust framework for _declaring_ user interfaces and _reacting_ to changes. diff --git a/docs/guide/src/elements/lists.md b/docs/guide/src/elements/lists.md index 32dbfd833..37e87b7da 100644 --- a/docs/guide/src/elements/lists.md +++ b/docs/guide/src/elements/lists.md @@ -1,6 +1,6 @@ # Conditional Lists and Keys -You will often want to display multiple similar components from a collection of data. +You will often want to display multiple similar components from a collection of data. In this chapter, you will learn: @@ -22,9 +22,9 @@ rsx!( ) ``` -Instead, we need to transform the list of data into a list of Elements. +Instead, we need to transform the list of data into a list of Elements. -For convenience, `rsx!` supports any type in curly braces that implements the `IntoVnodeList` trait. Conveniently, every iterator that returns something that can be rendered as an Element also implements `IntoVnodeList`. +For convenience, `rsx!` supports any type in curly braces that implements the `IntoVnodeList` trait. Conveniently, every iterator that returns something that can be rendered as an Element also implements `IntoVnodeList`. As a simple example, let's render a list of names. First, start with our input data: @@ -61,7 +61,7 @@ The HTML-rendered version of this list would follow what you would expect: ### Rendering our posts with a PostList component -Let's start by modeling this problem with a component and some properties. +Let's start by modeling this problem with a component and some properties. For this example, we're going to use the borrowed component syntax since we probably have a large list of posts that we don't want to clone every time we render the Post List. @@ -98,7 +98,7 @@ fn App(cx: Scope) -> Element { Rust's iterators are extremely powerful, especially when used for filtering tasks. When building user interfaces, you might want to display a list of items filtered by some arbitrary check. -As a very simple example, let's set up a filter where we only list names that begin with the letter "J". +As a very simple example, let's set up a filter where we only list names that begin with the letter "J". Let's make our list of names: @@ -117,7 +117,7 @@ let name_list = names Rust's iterators provide us tons of functionality and are significantly easier to work with than JavaScript's map/filter/reduce. -For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collected` our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new _lazy_ iterator which will then be consumed by Dioxus in the `render` call. +For keen Rustaceans: notice how we don't actually call `collect` on the name list. If we `collected` our filtered list into new Vec, then we would need to make an allocation to store these new elements. Instead, we create an entirely new _lazy_ iterator which will then be consumed by Dioxus in the `render` call. The `render` method is extraordinarily efficient, so it's best practice to let it do most of the allocations for us. diff --git a/docs/guide/src/elements/vnodes.md b/docs/guide/src/elements/vnodes.md index b407ed02f..d441693d7 100644 --- a/docs/guide/src/elements/vnodes.md +++ b/docs/guide/src/elements/vnodes.md @@ -93,7 +93,7 @@ Alternatively, `&str` can be included directly, though it must be inside of an a rsx!( "Hello ", [if enabled { "Jack" } else { "Bob" }] ) ``` -This is different from React's way of generating arbitrary markup but fits within idiomatic Rust. +This is different from React's way of generating arbitrary markup but fits within idiomatic Rust. Typically, with Dioxus, you'll just want to compute your substrings outside of the `rsx!` call and leverage the f-string formatting: @@ -138,11 +138,11 @@ rsx!( All element attributes must occur *before* child elements. The `rsx!` macro will throw an error if your child elements come before any of your attributes. If you don't see the error, try editing your Rust-Analyzer IDE setting to ignore macro-errors. This is a temporary workaround because Rust-Analyzer currently throws *two* errors instead of just the one we care about. ```rust -// settings.json +// settings.json { "rust-analyzer.diagnostics.disabled": [ "macro-error" - ], + ], } ``` @@ -166,7 +166,7 @@ This chapter just scratches the surface on how Elements can be defined. We learned: - Elements are the basic building blocks of User Interfaces -- Elements can contain other elements +- Elements can contain other elements - Elements can either be a named container or text - Some Elements have properties that the renderer can use to draw the UI to the screen diff --git a/docs/guide/src/final.md b/docs/guide/src/final.md index 68706f1aa..41449edc7 100644 --- a/docs/guide/src/final.md +++ b/docs/guide/src/final.md @@ -15,7 +15,7 @@ With any luck, you followed through the "Putting it All Together" mini guide and Continuing on your journey with Dioxus, you can try a number of things: -- Build a simple TUI app +- Build a simple TUI app - Publish your search engine app - Deploy a WASM app to GitHub - Design a custom hook @@ -37,7 +37,7 @@ The core team is actively working on: - Declarative window management (via Tauri) for Desktop apps - Portals for Dioxus Core -- Mobile support +- Mobile support - Integration with 3D renderers - Better async story (suspense, error handling) - Global state management diff --git a/docs/guide/src/hello_world.md b/docs/guide/src/hello_world.md index e9eb92081..e1d4777d9 100644 --- a/docs/guide/src/hello_world.md +++ b/docs/guide/src/hello_world.md @@ -1,6 +1,6 @@ # "Hello, World" desktop app -Let's put together a simple "hello world" desktop application to get acquainted with Dioxus. +Let's put together a simple "hello world" desktop application to get acquainted with Dioxus. In this chapter, we'll cover: @@ -31,7 +31,7 @@ $ tree We are greeted with a pre-initialized git repository, our code folder (`src`) and our project file (`Cargo.toml`). -Our `src` folder holds our code. Our `main.rs` file holds our `fn main` which will be executed when our app is ran. +Our `src` folder holds our code. Our `main.rs` file holds our `fn main` which will be executed when our app is run. ```shell $ more src/main.rs @@ -128,13 +128,13 @@ Finally, our app. Every component in Dioxus is a function that takes in `Context fn App(cx: Scope) -> Element { cx.render(rsx! { div { "Hello, world!" } - }) + }) } ``` ### What is this `Scope` object? -Coming from React, the `Scope` object might be confusing. In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering. +Coming from React, the `Scope` object might be confusing. In React, you'll want to store data between renders with hooks. However, hooks rely on global variables which make them difficult to integrate in multi-tenant systems like server-rendering. In Dioxus, you are given an explicit `Scope` object to control how the component renders and stores data. The `Scope` object provides a handful of useful APIs for features like suspense, rendering, and more. diff --git a/docs/guide/src/interactivity/hooks.md b/docs/guide/src/interactivity/hooks.md index 029e2db15..adf43c6ef 100644 --- a/docs/guide/src/interactivity/hooks.md +++ b/docs/guide/src/interactivity/hooks.md @@ -1,5 +1,5 @@ # Hooks and Internal State - + In the [Adding Interactivity](./interactivity.md) section, we briefly covered the concept of hooks and state stored internal to components. In this section, we'll dive a bit deeper into hooks, exploring both the theory and mechanics. @@ -10,7 +10,7 @@ In this section, we'll dive a bit deeper into hooks, exploring both the theory a Over the past several decades, computer scientists and engineers have long sought the "right way" of designing user interfaces. With each new programming language, novel features are unlocked that change the paradigm in which user interfaces are coded. -Generally, a number of patterns have emerged, each with their own strengths and tradeoffs. +Generally, a number of patterns have emerged, each with their own strengths and tradeoffs. Broadly, there are two types of GUI structures: @@ -21,7 +21,7 @@ Typically, immediate-mode GUIs are simpler to write but can slow down as more fe Many GUIs today are written in *Retained mode* - your code changes the data of the user interface but the renderer is responsible for actually drawing to the screen. In these cases, our GUI's state sticks around as the UI is rendered. To help accommodate retained mode GUIs, like the web browser, Dioxus provides a mechanism to keep state around. -> Note: Even though hooks are accessible, you should still prefer to one-way data flow and encapsulation. Your UI code should be as predictable as possible. Dioxus is plenty fast, even for the largest apps. +> Note: Even though hooks are accessible, you should still prefer one-way data flow and encapsulation. Your UI code should be as predictable as possible. Dioxus is plenty fast, even for the largest apps. ## Mechanics of Hooks In order to have state stick around between renders, Dioxus provides the `hook` through the `use_hook` API. This gives us a mutable reference to data returned from the initialization function. @@ -48,7 +48,7 @@ fn example(cx: Scope) -> Element { } ``` -Mechanically, each call to `use_hook` provides us with `&mut T` for a new value. +Mechanically, each call to `use_hook` provides us with `&mut T` for a new value. ```rust fn example(cx: Scope) -> Element { @@ -182,7 +182,7 @@ By default, we bundle a handful of hooks in the Dioxus-Hooks package. Feel free - [use_context](https://docs.rs/dioxus_hooks/use_context) - consume state provided by `use_provide_context` For a more in-depth guide to building new hooks, checkout out the advanced hook building guide in the reference. - + ## Wrapping up In this chapter, we learned about the mechanics and intricacies of storing state inside a component. diff --git a/docs/guide/src/interactivity/index.md b/docs/guide/src/interactivity/index.md index 9e23862f1..0e1a61252 100644 --- a/docs/guide/src/interactivity/index.md +++ b/docs/guide/src/interactivity/index.md @@ -10,7 +10,7 @@ Before we get too deep into the mechanics of interactivity, we should first unde Every app you'll ever build has some sort of information that needs to be rendered to the screen. Dioxus is responsible for translating your desired user interface to what is rendered to the screen. *You* are responsible for providing the content. -The dynamic data in your user interface is called `State`. +The dynamic data in your user interface is called `State`. When you first launch your app with `dioxus::web::launch_with_props` you'll be providing the initial state. You need to declare the initial state *before* starting the app. @@ -32,7 +32,7 @@ fn main() { } ``` -When Dioxus renders your app, it will pass an immutable reference of `PostProps` to your `Post` component. Here, you can pass the state down into children. +When Dioxus renders your app, it will pass an immutable reference to `PostProps` into your `Post` component. Here, you can pass the state down into children. ```rust fn App(cx: Scope) -> Element { @@ -73,14 +73,14 @@ fn App(cx: Scope)-> Element { url: String::from("dioxuslabs.com"), title: String::from("Hello, world"), original_poster: String::from("dioxus") - } + } }); cx.render(rsx!{ Title { title: &post.title } Score { score: &post.score } // etc - }) + }) } ``` @@ -102,7 +102,7 @@ We'll dive deeper into how exactly these hooks work later. ### When do I update my state? -There are a few different approaches to choosing when to update your state. You can update your state in response to user-triggered events or asynchronously in some background task. +There are a few different approaches to choosing when to update your state. You can update your state in response to user-triggered events or asynchronously in some background task. ### Updating state in listeners @@ -120,7 +120,7 @@ fn App(cx: Scope)-> Element { "Generate a random post" } Post { props: &post } - }) + }) } ``` @@ -162,7 +162,7 @@ Whenever you inform Dioxus that the component needs to be updated, it will "rend ![Diffing](../images/diffing.png) -In React, the specifics of when a component gets re-rendered is somewhat blurry. With Dioxus, any component can mark itself as "dirty" through a method on `Context`: `needs_update`. In addition, any component can mark any _other_ component as dirty provided it knows the other component's ID with `needs_update_any`. +In React, the specifics of when a component gets re-rendered is somewhat blurry. With Dioxus, any component can mark itself as "dirty" through a method on `Context`: `needs_update`. In addition, any component can mark any _other_ component as dirty provided it knows the other component's ID with `needs_update_any`. With these building blocks, we can craft new hooks similar to `use_state` that let us easily tell Dioxus that new information is ready to be sent to the screen. diff --git a/docs/guide/src/setup.md b/docs/guide/src/setup.md index 223150521..de634dc0a 100644 --- a/docs/guide/src/setup.md +++ b/docs/guide/src/setup.md @@ -1,6 +1,6 @@ # Overview -In this chapter, we're going to get "set up" with a small desktop application. +In this chapter, we're going to get set up with a small desktop application. We'll learn about: - Installing the Rust programming language @@ -15,16 +15,15 @@ For platform-specific guides, check out the [Platform Specific Guides](../platfo Dioxus requires a few main things to get up and running: - The [Rust compiler](https://www.rust-lang.org) and associated build tooling - - An editor of your choice, ideally configured with the [Rust-Analyzer LSP plugin](https://rust-analyzer.github.io) Dioxus integrates very well with the Rust-Analyzer IDE plugin which will provide appropriate syntax highlighting, code navigation, folding, and more. ### Installing Rust -Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler. +Head over to [https://rust-lang.org](http://rust-lang.org) and install the Rust compiler. -Once installed, make sure to install wasm32-unknown-unknown as a target if you're planning on deploying your app to the web. +Once installed, make sure to install wasm32-unknown-unknown as a target if you're planning on deploying your app to the web. ``` rustup target add wasm32-unknown-unknown @@ -32,7 +31,7 @@ rustup target add wasm32-unknown-unknown ### Platform-Specific Dependencies -If you are running a modern, mainstream operating system, you should need no additional setup to build WebView-based Desktop apps. However, if you are running an older version of Windows or a flavor of linux with no default web rendering engine, you might need to install some additional dependencies. +If you are running a modern, mainstream operating system, you should need no additional setup to build WebView-based Desktop apps. However, if you are running an older version of Windows or a flavor of Linux with no default web rendering engine, you might need to install some additional dependencies. For windows users: download the [bootstrapper for Webview2 from Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) @@ -49,13 +48,13 @@ When distributing onto older Windows platforms or less-mainstream ### Dioxus-CLI for dev server, bundling, etc. -We also recommend installing the Dioxus CLI. The Dioxus CLI automates building and packaging for various targets and integrates with simulators, development servers, and app deployment. To install the CLI, you'll need cargo (should be automatically installed with Rust): +We also recommend installing the Dioxus CLI. The Dioxus CLI automates building and packaging for various targets and integrates with simulators, development servers, and app deployment. To install the CLI, you'll need cargo (which should be automatically installed with Rust): ``` $ cargo install dioxus-cli ``` -You can update the dioxus-cli at any time with: +You can update dioxus-cli at any time with: ``` $ cargo install --force dioxus-cli diff --git a/docs/guide/src/state/index.md b/docs/guide/src/state/index.md index 9b3e054fc..1874dd73e 100644 --- a/docs/guide/src/state/index.md +++ b/docs/guide/src/state/index.md @@ -2,7 +2,7 @@ Every app you'll build with Dioxus will have some sort of state that needs to be maintained and updated as your users interact with it. However, managing state can be particular challenging at times, and is frequently the source of bugs in many GUI frameworks. -In this chapter, we'll cover the various ways to manage state, the appropriate terminology, various patterns, and then take an overview +In this chapter, we'll cover the various ways to manage state, the appropriate terminology, various patterns, and then take an overview -## Terminology +## Terminology diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 4ad7543a2..7099311ce 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -81,7 +81,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// // 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"}), @@ -229,18 +229,18 @@ pub fn routable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStrea /// #[inline_props] /// fn app(cx: Scope, bob: String) -> Element { /// cx.render(rsx!("hello, {bob}")) -/// } +/// } /// /// // is equivalent to /// /// #[derive(PartialEq, Props)] /// struct AppProps { /// bob: String, -/// } +/// } /// /// fn app(cx: Scope) -> Element { /// cx.render(rsx!("hello, {bob}")) -/// } +/// } /// ``` #[proc_macro_attribute] pub fn inline_props(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { diff --git a/packages/core/README.md b/packages/core/README.md index 6313b1415..0cb185f45 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -94,5 +94,5 @@ Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinn If you don't want to use a crate that uses unsafe, then this crate is not for you. -However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous variants required to execute certain features. +However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous invariants required to execute certain features. diff --git a/packages/core/architecture.md b/packages/core/architecture.md index f852fda3c..87c3cf4a6 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -5,7 +5,7 @@ This document is mostly a brain-dump on how things work. A lot of this informati Main topics covered here: - Fiber, Concurrency, and Cooperative Scheduling - Suspense -- Signals +- Signals - Patches - Diffing - Const/Static structures @@ -25,7 +25,7 @@ During diffing, the "caller" closure is updated if the props are not `static. Th Hooks are a form of state that's slightly more finicky than structs but more extensible overall. Hooks cannot be used in conditionals, but are portable enough to run on most targets. -The Dioxus hook model uses a Bump arena where user's data lives. +The Dioxus hook model uses a Bump arena where user's data lives. Initializing hooks: - The component is created @@ -38,7 +38,7 @@ Initializing hooks: Running hooks: - Each time use_hook is called, the internal hook state is fetched as &mut T -- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies +- We are guaranteed that our &mut T is not aliasing by re-generating any &mut T dependencies - The hook counter is incremented @@ -62,7 +62,7 @@ The diffing engine in Dioxus expects the RealDom Dioxus uses patches - not imperative methods - to modify the real dom. This speeds up the diffing operation and makes diffing cancelable which is useful for cooperative scheduling. In general, the RealDom trait exists so renderers can share "Node pointers" across runtime boundaries. -There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself. +There are no contractual obligations between the VirtualDOM and RealDOM. When the VirtualDOM finishes its work, it releases a Vec of Edits (patches) which the RealDOM can use to update itself. @@ -70,11 +70,11 @@ There are no contractual obligations between the VirtualDOM and RealDOM. When th When an EventTrigger enters the queue and "progress" is called (an async function), Dioxus will get to work running scopes and diffing nodes. Scopes are run and nodes are diffed together. Dioxus records which scopes get diffed to track the progress of its work. -While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending. +While descending through the stack frame, Dioxus will query the RealDom for "time remaining." When the time runs out, Dioxus will escape the stack frame by queuing whatever work it didn't get to, and then bubbling up out of "diff_node". Dioxus will also bubble out of "diff_node" if more important work gets queued while it was descending. -Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom. +Once bubbled out of diff_node, Dioxus will request the next idle callback and await for it to become available. The return of this callback is a "Deadline" object which Dioxus queries through the RealDom. -All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks. +All of this is orchestrated to keep high priority events moving through the VirtualDOM and scheduling lower-priority work around the RealDOM's animations and periodic tasks. ```js // returns a "deadline" object function idle() { @@ -83,7 +83,7 @@ function idle() { ``` ## Suspense -In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier. +In React, "suspense" is the ability render nodes outside of the traditional lifecycle. React will wait on a future to complete, and once the data is ready, will render those nodes. React's version of suspense is designed to make working with promises in components easier. In Dioxus, we have similar philosophy, but the use and details of suspense is slightly different. For starters, we don't currently allow using futures in the element structure. Technically, we can allow futures - and we do with "Signals" - but the "suspense" feature itself is meant to be self-contained within a single component. This forces you to handle all the loading states within your component, instead of outside the component, keeping things a bit more containerized. diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index c0dad0a47..8386cedd8 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -18,7 +18,7 @@ impl BubbleState { } } -/// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel. +/// User Events are events that are shuttled from the renderer into the VirtualDom through the scheduler channel. /// /// These events will be passed to the appropriate Element given by `mounted_dom_id` and then bubbled up through the tree /// where each listener is checked and fired if the event name matches. diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 80de92917..e30a07001 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -21,7 +21,7 @@ use std::{ /// - the `rsx!` macro /// - the [`NodeFactory`] API pub enum VNode<'src> { - /// Text VNodes simply bump-allocated (or static) string slices + /// Text VNodes are simply bump-allocated (or static) string slices /// /// # Example /// @@ -399,7 +399,7 @@ impl

AnyProps for VComponentProps

{ } // Safety: - // this will downcat the other ptr as our swallowed type! + // this will downcast the other ptr as our swallowed type! // you *must* make this check *before* calling this method // if your functions are not the same, then you will downcast a pointer into a different type (UB) unsafe fn memoize(&self, other: &dyn AnyProps) -> bool { diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index cfc555543..bfa721f69 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -70,7 +70,7 @@ impl ScopeArena { } /// Safety: - /// - Obtaining a mutable refernece to any Scope is unsafe + /// - Obtaining a mutable reference to any Scope is unsafe /// - Scopes use interior mutability when sharing data into components pub(crate) fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> { unsafe { self.scopes.borrow().get(&id).map(|f| &**f) } @@ -101,7 +101,7 @@ impl ScopeArena { let parent_scope = parent_scope.map(|f| self.get_scope_raw(f)).flatten(); /* - This scopearena aggressively reuse old scopes when possible. + This scopearena aggressively reuses old scopes when possible. We try to minimize the new allocations for props/arenas. However, this will probably lead to some sort of fragmentation. diff --git a/packages/core/src/util.rs b/packages/core/src/util.rs index f29a13e17..b9640792d 100644 --- a/packages/core/src/util.rs +++ b/packages/core/src/util.rs @@ -3,7 +3,7 @@ use crate::innerlude::*; pub struct ElementIdIterator<'a> { vdom: &'a VirtualDom, - // Heuristcally we should never bleed into 5 completely nested fragments/components + // Heuristically we should never bleed into 5 completely nested fragments/components // Smallvec lets us stack allocate our little stack machine so the vast majority of cases are sane stack: smallvec::SmallVec<[(u16, &'a VNode<'a>); 5]>, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 29e8355cc..4e426caad 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -9,12 +9,12 @@ use fxhash::FxHashSet; use indexmap::IndexSet; use std::{collections::VecDeque, iter::FromIterator, task::Poll}; -/// A virtual node s ystem that progresses user events and diffs UI trees. +/// A virtual node system that progresses user events and diffs UI trees. /// /// /// ## Guide /// -/// Components are defined as simple functions that take [`Scope`] and return an [`Element`]. +/// Components are defined as simple functions that take [`Scope`] and return an [`Element`]. /// /// ```rust, ignore /// #[derive(Props, PartialEq)] @@ -233,7 +233,7 @@ impl VirtualDom { /// Get the [`Scope`] for the root component. /// - /// This is useful for traversing the tree from the root for heuristics or alternsative renderers that use Dioxus + /// This is useful for traversing the tree from the root for heuristics or alternative renderers that use Dioxus /// directly. /// /// This method is equivalent to calling `get_scope(ScopeId(0))` @@ -618,7 +618,7 @@ impl VirtualDom { /// /// let dom = VirtualDom::new(Base); /// let nodes = dom.render_nodes(rsx!("div")); - /// ``` + /// ``` pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> { let mut machine = DiffState::new(&self.scopes); machine.stack.push(DiffInstruction::Diff { new, old }); diff --git a/packages/hooks/src/usestate/handle.rs b/packages/hooks/src/usestate/handle.rs index 2638feaa8..83acf781e 100644 --- a/packages/hooks/src/usestate/handle.rs +++ b/packages/hooks/src/usestate/handle.rs @@ -66,7 +66,7 @@ impl<'a, T: 'static> UseState<'a, T> { }) } - pub fn wtih(&self, f: impl FnOnce(&mut T)) { + pub fn with(&self, f: impl FnOnce(&mut T)) { let mut val = self.0.wip.borrow_mut(); if let Some(inner) = val.as_mut() { diff --git a/packages/html/README.md b/packages/html/README.md index 46f1b2078..cbc03b0f2 100644 --- a/packages/html/README.md +++ b/packages/html/README.md @@ -42,7 +42,7 @@ The HTML namespace is defined mostly with macros. However, the expanded form wou struct base; impl DioxusElement for base { const TAG_NAME: &'static str = "base"; - const NAME_SPACE: Option<&'static str> = None; + const NAME_SPACE: Option<&'static str> = None; } impl base { #[inline] @@ -60,7 +60,7 @@ Because attributes are defined as methods on the unit struct, they guard the att ## How to extend it: -Whenever the rsx! macro is called, it relies on a module `dioxus_elements` to be in scope. When you enable the `html` feature in dioxus, this module gets imported in the prelude. However, you can extend this with your own set of custom elements by making your own `dioxus_elements` module and re-exporting the html namespace. +Whenever the rsx! macro is called, it relies on a module `dioxus_elements` to be in scope. When you enable the `html` feature in dioxus, this module gets imported in the prelude. However, you can extend this with your own set of custom elements by making your own `dioxus_elements` module and re-exporting the html namespace. ```rust mod dioxus_elements { @@ -68,13 +68,13 @@ mod dioxus_elements { struct my_element; impl DioxusElement for my_element { const TAG_NAME: &'static str = "base"; - const NAME_SPACE: Option<&'static str> = None; + const NAME_SPACE: Option<&'static str> = None; } } ``` ## Limitations: -- +- ## How to work around it: If an attribute in Dioxus is invalid (defined incorrectly) - first, make an issue - but then, you can work around it. The raw builder API is actually somewhat ergonomic to work with, and the NodeFactory type exposes a bunch of methods to make any type of tree - even invalid ones! So obviously, be careful, but there's basically anything you can do. @@ -87,19 +87,19 @@ cx.render(rsx!{ {LazyNodes::new(move |f| { f.raw_element( // tag name - "custom_element", + "custom_element", // attributes - &[f.attr("billy", format_args!("goat"))], + &[f.attr("billy", format_args!("goat"))], // listeners - &[f.listener(onclick(move |_| {}))], + &[f.listener(onclick(move |_| {}))], // children - &[cx.render(rsx!(div {} ))], + &[cx.render(rsx!(div {} ))], // key - None + None ) })} } diff --git a/packages/liveview/README.md b/packages/liveview/README.md index 1d708eab2..00a8ce2d6 100644 --- a/packages/liveview/README.md +++ b/packages/liveview/README.md @@ -20,7 +20,7 @@ async fn main() -> tide::Result<()> { } ``` -Dioxus LiveView runs your Dioxus apps on the server +Dioxus LiveView runs your Dioxus apps on the server @@ -36,7 +36,7 @@ async fn main() { async fn order_shoes(mut req: WebsocketRequest) -> Response { let stream = req.upgrade(); - dioxus::liveview::launch(App, stream).await; + dioxus::liveview::launch(App, stream).await; } fn App(cx: Scope) -> Element { diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 5a41ffe60..aa3470761 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -21,7 +21,7 @@ $ cargo install --git https://github.com/BrainiumLLC/cargo-mobile And then initialize your app for the right platform. Use the `winit` template for now. Right now, there's no "Dioxus" template in cargo-mobile. ```shell -$ cargo mobile init +$ cargo mobile init ``` We're going to completely clear out the `dependencies` it generates for us, swapping out `winit` with `dioxus-mobile`. diff --git a/packages/router/README.md b/packages/router/README.md index c32441888..a6dc5027b 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -16,7 +16,7 @@ fn app() { Then, in your route, you can choose to parse the Route any way you want through `use_route`. ```rust -let id: usize = use_route(&cx).path("id")?; +let id: usize = use_route(&cx).segment("id")?; let state: CustomState = use_route(&cx).parse()?; ``` diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 6a1b671ab..4c0ecb996 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -11,7 +11,7 @@ pub struct LinkProps<'a> { /// The url that gets pushed to the history stack /// - /// You can either put it your own inline method or just autoderive the route using `derive(Routable)` + /// You can either put in your own inline method or just autoderive the route using `derive(Routable)` /// /// ```rust, ignore /// diff --git a/packages/ssr/README.md b/packages/ssr/README.md index d6c199fc8..7a7f1c4be 100644 --- a/packages/ssr/README.md +++ b/packages/ssr/README.md @@ -52,7 +52,7 @@ let content = dioxus::ssr::render_vdom(&dom); ``` ## Configuring output -It's possible to configure the output of the generated HTML. +It's possible to configure the output of the generated HTML. ```rust, ignore let content = dioxus::ssr::render_vdom(&dom, |config| config.pretty(true).prerender(true)); @@ -82,7 +82,7 @@ buf.write_fmt!(format_args!("{}", args)); -## Usage in pre-rendering +## Usage in pre-rendering This crate is particularly useful in pre-generating pages server-side and then selectively loading dioxus client-side to pick up the reactive elements. diff --git a/src/lib.rs b/src/lib.rs index c0a984abb..d7b479a07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ //! //! # Resources //! -//! This overview is provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out: +//! This overview provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out: //! - [Getting Started](https://dioxuslabs.com/getting-started) //! - [Book](https://dioxuslabs.com/book) //! - [Reference](https://dioxuslabs.com/reference) @@ -53,7 +53,7 @@ //! //! ## Elements & your first component //! -//! To assemble UI trees with Diouxs, you need to use the `render` function on +//! To assemble UI trees with Dioxus, you need to use the `render` function on //! something called `LazyNodes`. To produce `LazyNodes`, you can use the `rsx!` //! macro or the NodeFactory API. For the most part, you want to use the `rsx!` //! macro. @@ -74,7 +74,7 @@ //! ) //! ``` //! -//! The rsx macro accepts attributes in "struct form" and then will parse the rest +//! The `rsx!` macro accepts attributes in "struct form" and will parse the rest //! of the body as child elements and rust expressions. Any rust expression that //! implements `IntoIterator` will be parsed as a child. //! @@ -87,7 +87,7 @@ //! //! ``` //! -//! Used within components, the rsx! macro must be rendered into an `Element` with +//! Used within components, the `rsx!` macro must be rendered into an `Element` with //! the `render` function on Scope. //! //! If we want to omit the boilerplate of `cx.render`, we can simply pass in @@ -104,7 +104,7 @@ //! } //! ``` //! -//! Putting everything together, we can write a simple component that a list of +//! Putting everything together, we can write a simple component that renders a list of //! elements: //! //! ```rust, ignore @@ -146,8 +146,8 @@ //! } //! ``` //! -//! Our `Header` component takes in a `title` and a `color` property, which we -//! delcare on an explicit `HeaderProps` struct. +//! Our `Header` component takes a `title` and a `color` property, which we +//! declare on an explicit `HeaderProps` struct. //! //! ```rust, ignore //! // The `Props` derive macro lets us add additional functionality to how props are interpreted. @@ -203,8 +203,8 @@ //! } //! ``` //! -//! Components that beging with an uppercase letter may be called through -//! traditional curly-brace syntax like so: +//! Components that begin with an uppercase letter may be called with +//! the traditional (for React) curly-brace syntax like so: //! //! ```rust, ignore //! rsx!( @@ -224,7 +224,7 @@ //! ## Hooks //! //! While components are reusable forms of UI elements, hooks are reusable forms -//! of logic. Hooks provide us a way of retrieving state from `Scope` and using +//! of logic. Hooks provide us a way of retrieving state from the `Scope` and using //! it to render UI elements. //! //! By convention, all hooks are functions that should start with `use_`. We can @@ -245,7 +245,7 @@ //! - Functions with "use_" should not be called in loops or conditionals //! //! In a sense, hooks let us add a field of state to our component without declaring -//! an explicit struct. However, this means we need to "load" the struct in the right +//! an explicit state struct. However, this means we need to "load" the struct in the right //! order. If that order is wrong, then the hook will pick the wrong state and panic. //! //! Most hooks you'll write are simply composition of other hooks: @@ -256,8 +256,8 @@ //! users.get(&id).map(|user| user.logged_in).ok_or(false) //! } //! ``` -//! -//! To create entirely new foundational hooks, we can use the `use_hook` method on ScopeState. +//! +//! To create entirely new foundational hooks, we can use the `use_hook` method on `ScopeState`. //! //! ```rust, ignore //! fn use_mut_string(cx: &ScopeState) -> &mut String { @@ -316,9 +316,9 @@ //! //! Alternatives to Dioxus include: //! - Yew: supports function components and web, but no SSR, borrowed data, or bump allocation. Rather slow at times. -//! - Percy: supports function components, web, ssr, but lacks in state management +//! - Percy: supports function components, web, ssr, but lacks state management //! - Sycamore: supports function components, web, ssr, but closer to SolidJS than React -//! - MoonZoom/Seed: opionated in the Elm model (message, update) - no hooks +//! - MoonZoom/Seed: opinionated frameworks based on the Elm model (message, update) - no hooks //! //! We've put a lot of work into making Dioxus ergonomic and *familiar*. //! Our target audience is TypeSrcipt developers looking to switch to Rust for the web - so we need to be comparabale to React. From 5f4a72446e63441628e18bb19fd9b2d9f65f9d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rasmus=20L=C3=B6vegren?= Date: Sat, 22 Jan 2022 19:57:46 +0200 Subject: [PATCH 59/79] Add gap and row_gap to style_trait_methods --- packages/html/src/global_attributes.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index c40eabd31..f99d041b5 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -399,6 +399,9 @@ pub trait GlobalAttributes { /// Specify the font weight of the text. font_weight: "font-weight", + /// Sets gaps (gutters) between rows and columns. Shorthand for row_gap and column_gap. + gap: "gap", + /// Specify the height of an element. height: "height", @@ -530,6 +533,9 @@ pub trait GlobalAttributes { /// Specify the location of the right edge of the positioned element. right: "right", + + /// Specifies the gap between the rows in a multi_column element. + row_gap: "row-gap", /// Specifies the length of the tab character. tab_size: "tab-size", From 43e78d56f72e23ada939f5688a6bd2fdd9b8292d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 22 Jan 2022 14:55:19 -0500 Subject: [PATCH 60/79] fix: rustfmt --- packages/html/src/global_attributes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/src/global_attributes.rs b/packages/html/src/global_attributes.rs index f99d041b5..1368f87e3 100644 --- a/packages/html/src/global_attributes.rs +++ b/packages/html/src/global_attributes.rs @@ -533,7 +533,7 @@ pub trait GlobalAttributes { /// Specify the location of the right edge of the positioned element. right: "right", - + /// Specifies the gap between the rows in a multi_column element. row_gap: "row-gap", From 564bce8eeb32415583dce6d08216eba561b7997c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 22 Jan 2022 16:19:52 -0500 Subject: [PATCH 61/79] docs: add azul compairson --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7b277d16a..262270dd5 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ Dioxus primarily emphasizes **developer experience** and **familiarity with Reac - [Percy](https://github.com/chinedufn/percy): Supports SSR but with less emphasis on state management and event handling. - [Sycamore](https://github.com/sycamore-rs/sycamore): VDOM-less using fine-grained reactivity, but lacking in ergonomics. - [Dominator](https://github.com/Pauan/rust-dominator): Signal-based zero-cost alternative, less emphasis on community and docs. +- [Azul](https://azul.rs): Fully native HTML/CSS renderer for desktop applications, no support for web/ssr # Parity with React From d9a07ddddb45ba00e35645ac0e0ad1a4d2dd996d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 02:34:16 -0500 Subject: [PATCH 62/79] fix: provide_root_context on root scopes --- packages/core/src/scopes.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index bfa721f69..4eda40c50 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -695,6 +695,7 @@ impl ScopeState { .insert(TypeId::of::(), value.clone()) .map(|f| f.downcast::().ok()) .flatten(); + return value; } let mut search_parent = self.parent_scope; From 1406c9020bb3102635737dc2d13e5b3171499a18 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 02:57:56 -0500 Subject: [PATCH 63/79] feat: add panic hook by default --- packages/web/Cargo.toml | 19 ++++--------------- packages/web/src/lib.rs | 4 ++++ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 52a78e631..a5befe7b3 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -20,7 +20,7 @@ wasm-bindgen-futures = "0.4.20" log = { version = "0.4.14", features = ["release_max_level_off"] } fxhash = "0.2.1" wasm-logger = "0.2.0" -console_error_panic_hook = "0.1.6" +console_error_panic_hook = { version = "0.1.7", optional = true } wasm-bindgen-test = "0.3.21" once_cell = "1.8" async-channel = "1.6.1" @@ -69,22 +69,11 @@ features = [ "IdleDeadline", ] - -# [lib] -# crate-type = ["cdylib", "rlib"] +[features] +default = ["panic_hook"] +panic_hook = ["console_error_panic_hook"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } wasm-bindgen-test = "0.3.28" dioxus-ssr = { path = "../ssr" } -# im-rc = "15.0.0" -# separator = "0.4.1" -# uuid = { version = "0.8.2", features = ["v4", "wasm-bindgen"] } -# serde = { version = "1.0.126", features = ["derive"] } -# reqwest = { version = "0.11", features = ["json"] } -# dioxus-hooks = { path = "../hooks" } -# rand = { version = "0.8.4", features = ["small_rng"] } - -# [dev-dependencies.getrandom] -# version = "0.2" -# features = ["js"] diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 8c0c0c3b4..e1e2af491 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -120,6 +120,10 @@ pub fn launch_with_props( T: Send + 'static, F: FnOnce(WebConfig) -> WebConfig, { + if cfg!(feature = "panic_hook") { + console_error_panic_hook::set_once(); + } + let config = configuration_builder(WebConfig::default()); wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config)); } From 2e4f7659327b69b74e20af59b6d5e65c699b6adb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 12:21:24 -0500 Subject: [PATCH 64/79] tweak: drop hooks before resetting bump arena --- packages/core/src/scopes.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 4eda40c50..c54918b55 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -891,13 +891,15 @@ impl ScopeState { self.frames[0].reset(); self.frames[1].reset(); - // Finally, free up the hook values - self.hook_arena.reset(); + // Free up the hook values self.hook_vals.get_mut().drain(..).for_each(|state| { let as_mut = unsafe { &mut *state }; let boxed = unsafe { bumpalo::boxed::Box::from_raw(as_mut) }; drop(boxed); }); + + // Finally, clear the hook arena + self.hook_arena.reset(); } } From bad4b773b7bbab6a38d4f48d88d8c5a8787927ac Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 15:21:56 -0500 Subject: [PATCH 65/79] feat: add "spawn" method --- packages/core/src/scopes.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index c54918b55..77c648d18 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -751,6 +751,11 @@ impl ScopeState { self.tasks.push_fut(fut) } + /// Spawns the future but does not return the TaskId + pub fn spawn(&self, fut: impl Future + 'static) { + self.push_future(fut); + } + // todo: attach some state to the future to know if we should poll it pub fn remove_future(&self, id: TaskId) { self.tasks.remove_fut(id); From 26ada2c7c18097fe86579b71b8b1a20f4227e405 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 19:52:12 -0500 Subject: [PATCH 66/79] chore: make clippy happier --- examples/borrowed.rs | 2 ++ examples/calculator.rs | 6 +++--- examples/crm.rs | 2 +- examples/dog_app.rs | 5 +++-- examples/file_explorer.rs | 6 +++--- examples/filedragdrop.rs | 2 +- examples/framework_benchmark.rs | 4 +++- examples/inputs.rs | 2 -- examples/pattern_model.rs | 12 +++++++----- examples/router.rs | 12 ++++++++++-- examples/suspense.rs | 2 ++ examples/tailwind.rs | 2 ++ examples/tasks.rs | 11 +++++------ examples/todomvc.rs | 28 +++++++++++++--------------- 14 files changed, 55 insertions(+), 41 deletions(-) diff --git a/examples/borrowed.rs b/examples/borrowed.rs index 63191bc97..815d6ceed 100644 --- a/examples/borrowed.rs +++ b/examples/borrowed.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + /* Dioxus manages borrow lifetimes for you. This means any child may borrow from its parent. However, it is not possible to hand out an &mut T to children - all props are consumed by &P, so you'd only get an &&mut T. diff --git a/examples/calculator.rs b/examples/calculator.rs index a849833a3..8597a8897 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -98,7 +98,7 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" } - button { class: "calculator-key key-dot", onclick: move |_| display_value.modify().push_str("."), + button { class: "calculator-key key-dot", onclick: move |_| display_value.modify().push('.'), "●" } (1..10).map(|k| rsx!{ @@ -175,7 +175,7 @@ fn calc_val(val: String) -> f64 { for c in val[start_index..].chars() { if c == '+' || c == '-' || c == '*' || c == '/' { - if temp != "" { + if !temp.is_empty() { match &operation as &str { "+" => result += temp.parse::().unwrap(), "-" => result -= temp.parse::().unwrap(), @@ -191,7 +191,7 @@ fn calc_val(val: String) -> f64 { } } - if temp != "" { + if !temp.is_empty() { match &operation as &str { "+" => result += temp.parse::().unwrap(), "-" => result -= temp.parse::().unwrap(), diff --git a/examples/crm.rs b/examples/crm.rs index d4f3d1579..504459fc7 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -1,7 +1,7 @@ /* Tiny CRM: A port of the Yew CRM example to Dioxus. */ -use dioxus::{events::FormEvent, prelude::*}; +use dioxus::prelude::*; fn main() { dioxus::desktop::launch(app); diff --git a/examples/dog_app.rs b/examples/dog_app.rs index b2f8429ee..f2c51d6fb 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -1,5 +1,6 @@ +#![allow(non_snake_case)] + //! Render a bunch of doggos! -//! use std::collections::HashMap; @@ -50,7 +51,7 @@ fn app(cx: Scope) -> Element { } } }), - Some(Err(e)) => cx.render(rsx! { + Some(Err(_e)) => cx.render(rsx! { div { "Error fetching breeds" } }), None => cx.render(rsx! { diff --git a/examples/file_explorer.rs b/examples/file_explorer.rs index 5d4bf8be8..fe5ab23bb 100644 --- a/examples/file_explorer.rs +++ b/examples/file_explorer.rs @@ -15,7 +15,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let files = use_ref(&cx, || Files::new()); + let files = use_ref(&cx, Files::new); rsx!(cx, div { link { href:"https://fonts.googleapis.com/icon?family=Material+Icons", rel:"stylesheet", } @@ -28,8 +28,8 @@ fn app(cx: Scope) -> Element { } main { files.read().path_names.iter().enumerate().map(|(dir_id, path)| { - let path_end = path.split('/').last().unwrap_or(path.as_str()); - let icon_type = if path_end.contains(".") { + let path_end = path.split('/').last().unwrap_or_else(|| path.as_str()); + let icon_type = if path_end.contains('.') { "description" } else { "folder" diff --git a/examples/filedragdrop.rs b/examples/filedragdrop.rs index a36b64ddb..523405b39 100644 --- a/examples/filedragdrop.rs +++ b/examples/filedragdrop.rs @@ -2,7 +2,7 @@ use dioxus::prelude::*; fn main() { dioxus::desktop::launch_with_props(app, (), |c| { - c.with_file_drop_handler(|w, e| { + c.with_file_drop_handler(|_w, e| { println!("{:?}", e); false }) diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index 0a70bd25b..69e3b8faa 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + use dioxus::prelude::*; use rand::prelude::*; @@ -30,7 +32,7 @@ impl Label { } fn app(cx: Scope) -> Element { - let items = use_ref(&cx, || vec![]); + let items = use_ref(&cx, Vec::new); let selected = use_state(&cx, || None); cx.render(rsx! { diff --git a/examples/inputs.rs b/examples/inputs.rs index 11334b59a..5e0a52244 100644 --- a/examples/inputs.rs +++ b/examples/inputs.rs @@ -2,8 +2,6 @@ //! //! There is some conversion happening when input types are checkbox/radio/select/textarea etc. -use std::sync::Arc; - use dioxus::{events::FormEvent, prelude::*}; fn main() { diff --git a/examples/pattern_model.rs b/examples/pattern_model.rs index c679d1427..a7b821750 100644 --- a/examples/pattern_model.rs +++ b/examples/pattern_model.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Example: Calculator //! ------------------- //! @@ -30,7 +32,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let state = use_ref(&cx, || Calculator::new()); + let state = use_ref(&cx, Calculator::new); cx.render(rsx! { style { [include_str!("./assets/calculator.css")] } @@ -171,8 +173,8 @@ impl Calculator { } } fn input_dot(&mut self) { - if self.display_value.find(".").is_none() { - self.display_value.push_str("."); + if !self.display_value.contains('.') { + self.display_value.push('.'); } } fn perform_operation(&mut self) { @@ -190,8 +192,8 @@ impl Calculator { } } fn toggle_sign(&mut self) { - if self.display_value.starts_with("-") { - self.display_value = self.display_value.trim_start_matches("-").to_string(); + if self.display_value.starts_with('-') { + self.display_value = self.display_value.trim_start_matches('-').to_string(); } else { self.display_value = format!("-{}", self.display_value); } diff --git a/examples/router.rs b/examples/router.rs index 121514423..f34676ab7 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -19,7 +19,7 @@ fn app(cx: Scope) -> Element { Route { to: "/", "Home" } Route { to: "users", Route { to: "/", "User list" } - Route { to: ":name", BlogPost {} } + Route { to: ":name", User {} } } Route { to: "blog" Route { to: "/", "Blog list" } @@ -48,12 +48,20 @@ struct Query { fn User(cx: Scope) -> Element { let post = dioxus::router::use_route(&cx).last_segment(); - let query = dioxus::router::use_route(&cx).query::(); + let query = dioxus::router::use_route(&cx) + .query::() + .unwrap_or(Query { bold: false }); cx.render(rsx! { div { h1 { "Reading blog post: {post}" } p { "example blog post" } + + if query.bold { + rsx!{ b { "bold" } } + } else { + rsx!{ i { "italic" } } + } } }) } diff --git a/examples/suspense.rs b/examples/suspense.rs index 881d6cae7..5a9bf4fd8 100644 --- a/examples/suspense.rs +++ b/examples/suspense.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Suspense in Dioxus //! //! Currently, `rsx!` does not accept futures as values. To achieve the functionality diff --git a/examples/tailwind.rs b/examples/tailwind.rs index cbefd248f..6611fc47b 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + //! Example: Basic Tailwind usage //! //! This example shows how an app might be styled with TailwindCSS. diff --git a/examples/tasks.rs b/examples/tasks.rs index a0ab7675c..f71abe38d 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -13,13 +13,12 @@ fn app(cx: Scope) -> Element { let count = use_state(&cx, || 0); use_future(&cx, move || { - let count = UseState::for_async(&count); - // for_async![count]; + let mut count = count.for_async(); async move { - // loop { - // tokio::time::sleep(Duration::from_millis(1000)).await; - // count += 1; - // } + loop { + tokio::time::sleep(Duration::from_millis(1000)).await; + count += 1; + } } }); diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 8adb01d34..6daddbf20 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -19,7 +19,7 @@ pub struct TodoItem { } pub fn app(cx: Scope<()>) -> Element { - let todos = use_state(&cx, || im_rc::HashMap::::default()); + let todos = use_state(&cx, im_rc::HashMap::::default); let filter = use_state(&cx, || FilterState::All); let draft = use_state(&cx, || "".to_string()); let mut todo_id = use_state(&cx, || 0); @@ -56,19 +56,17 @@ pub fn app(cx: Scope<()>) -> Element { autofocus: "true", oninput: move |evt| draft.set(evt.value.clone()), onkeydown: move |evt| { - if evt.key == "Enter" { - if !draft.is_empty() { - todos.modify().insert( - *todo_id, - TodoItem { - id: *todo_id, - checked: false, - contents: draft.get().clone(), - }, - ); - todo_id += 1; - draft.set("".to_string()); - } + if evt.key == "Enter" && !draft.is_empty() { + todos.modify().insert( + *todo_id, + TodoItem { + id: *todo_id, + checked: false, + contents: draft.get().clone(), + }, + ); + todo_id += 1; + draft.set("".to_string()); } } } @@ -90,7 +88,7 @@ pub fn app(cx: Scope<()>) -> Element { (show_clear_completed).then(|| rsx!( button { class: "clear-completed", - onclick: move |_| todos.modify().retain(|_, todo| todo.checked == false), + onclick: move |_| todos.modify().retain(|_, todo| !todo.checked), "Clear completed" } )) From 1d964c54e5266e820d57019064c92558f35bc1cc Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 19:54:27 -0500 Subject: [PATCH 67/79] chore: fix cargo fmt --- examples/router.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/router.rs b/examples/router.rs index f34676ab7..216b69922 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -56,7 +56,7 @@ fn User(cx: Scope) -> Element { div { h1 { "Reading blog post: {post}" } p { "example blog post" } - + if query.bold { rsx!{ b { "bold" } } } else { From a239d2ba6ac7f1f3d09de16c022ce8ca52cf0f63 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 20:24:50 -0500 Subject: [PATCH 68/79] docs: fix web doc example and use &mut for builders everywhere --- packages/desktop/src/lib.rs | 2 +- packages/web/src/lib.rs | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index bc9a84e42..93c175cc6 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -149,7 +149,7 @@ pub fn launch_with_props( props: P, builder: impl FnOnce(&mut DesktopConfig) -> &mut DesktopConfig, ) { - let mut cfg = DesktopConfig::new(); + let mut cfg = DesktopConfig::default(); builder(&mut cfg); let event_loop = EventLoop::with_user_event(); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index e1e2af491..f4266a7c8 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -92,6 +92,29 @@ pub fn launch(root_component: Component) { launch_with_props(root_component, (), |c| c); } +/// Launch your app and run the event loop, with configuration. +/// +/// This function will start your web app on the main web thread. +/// +/// You can configure the WebView window with a configuration closure +/// +/// ```rust +/// use dioxus::prelude::*; +/// +/// fn main() { +/// dioxus_web::launch_with_props(App, |config| config.pre_render(true)); +/// } +/// +/// fn app(cx: Scope) -> Element { +/// cx.render(rsx!{ +/// h1 {"hello world!"} +/// }) +/// } +/// ``` +pub fn launch_cfg(root: Component, config_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig) { + launch_with_props(root, (), config_builder) +} + /// Launches the VirtualDOM from the specified component function and props. /// /// This method will block the thread with `spawn_local` @@ -100,7 +123,11 @@ pub fn launch(root_component: Component) { /// /// ```rust, ignore /// fn main() { -/// dioxus_web::launch_with_props(App, RootProps { name: String::from("joe") }); +/// dioxus_web::launch_with_props( +/// App, +/// RootProps { name: String::from("joe") }, +/// |config| config +/// ); /// } /// /// #[derive(ParitalEq, Props)] @@ -112,19 +139,19 @@ pub fn launch(root_component: Component) { /// rsx!(cx, div {"hello {cx.props.name}"}) /// } /// ``` -pub fn launch_with_props( +pub fn launch_with_props( root_component: Component, root_properties: T, - configuration_builder: F, + configuration_builder: impl FnOnce(&mut WebConfig) -> &mut WebConfig, ) where T: Send + 'static, - F: FnOnce(WebConfig) -> WebConfig, { if cfg!(feature = "panic_hook") { console_error_panic_hook::set_once(); } - let config = configuration_builder(WebConfig::default()); + let mut config = WebConfig::default(); + configuration_builder(&mut config); wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config)); } From ae676d9d816fcbf88d110157124443a5b3aef9eb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 24 Jan 2022 20:25:32 -0500 Subject: [PATCH 69/79] fix: webconfig should take &mut self --- packages/web/src/cfg.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/src/cfg.rs b/packages/web/src/cfg.rs index cc5d18bf7..591868c34 100644 --- a/packages/web/src/cfg.rs +++ b/packages/web/src/cfg.rs @@ -30,7 +30,7 @@ impl WebConfig { /// work and suspended nodes. /// /// Dioxus will load up all the elements with the `dio_el` data attribute into memory when the page is loaded. - pub fn hydrate(mut self, f: bool) -> Self { + pub fn hydrate(&mut self, f: bool) -> &mut Self { self.hydrate = f; self } @@ -38,7 +38,7 @@ impl WebConfig { /// Set the name of the element that Dioxus will use as the root. /// /// This is akint to calling React.render() on the element with the specified name. - pub fn rootname(mut self, name: impl Into) -> Self { + pub fn rootname(&mut self, name: impl Into) -> &mut Self { self.rootname = name.into(); self } @@ -46,7 +46,7 @@ impl WebConfig { /// Set the name of the element that Dioxus will use as the root. /// /// This is akint to calling React.render() on the element with the specified name. - pub fn with_string_cache(mut self, cache: Vec) -> Self { + pub fn with_string_cache(&mut self, cache: Vec) -> &mut Self { self.cached_strings = cache; self } From 71371d570a82f136593c73ccdb4511369514a708 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 25 Jan 2022 13:17:43 -0500 Subject: [PATCH 70/79] docs: change discord readme badge --- README.md | 3 ++- notes/README/ZH_CN.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 262270dd5..9895dfb76 100644 --- a/README.md +++ b/README.md @@ -35,11 +35,12 @@ - Awesome Page + Discord Link +

Website diff --git a/notes/README/ZH_CN.md b/notes/README/ZH_CN.md index 614774595..28bd07ed0 100644 --- a/notes/README/ZH_CN.md +++ b/notes/README/ZH_CN.md @@ -35,7 +35,7 @@ - Awesome Page + Discord Link

@@ -113,7 +113,7 @@ cargo run --example EXAMPLE ## Dioxus 项目 -| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) | +| 文件浏览器 (桌面应用) | WiFi 扫描器 (桌面应用) | Todo管理 (所有平台) | 商城系统 (SSR/liveview) | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [![File Explorer](https://github.com/DioxusLabs/example-projects/raw/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer) | [![Wifi Scanner Demo](https://github.com/DioxusLabs/example-projects/raw/master/wifi-scanner/demo_small.png)](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner) | [![TodoMVC example](https://github.com/DioxusLabs/example-projects/raw/master/todomvc/example.png)](https://github.com/DioxusLabs/example-projects/blob/master/todomvc) | [![E-commerce Example](https://github.com/DioxusLabs/example-projects/raw/master/ecommerce-site/demo.png)](https://github.com/DioxusLabs/example-projects/blob/master/ecommerce-site) | From 29ed7ebece26e9d53925af55f2f34a8fd8241405 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 25 Jan 2022 15:06:37 -0500 Subject: [PATCH 71/79] feat: connect an onchange listener --- packages/router/README.md | 2 +- packages/router/src/components/router.rs | 15 +++++++-- packages/router/src/lib.rs | 15 +-------- packages/router/src/service.rs | 40 +++++++++++++++++------- 4 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/router/README.md b/packages/router/README.md index a6dc5027b..6fbc652eb 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -5,7 +5,7 @@ DioxusRouter adds React-Router style routing to your Dioxus apps. Works in brows ```rust fn app() { cx.render(rsx! { - Routes { + Router { Route { to: "/", Component {} }, Route { to: "/blog", Blog {} }, Route { to: "/blog/:id", BlogPost {} }, diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 3d9f0c83a..240c3806a 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -11,17 +11,26 @@ use crate::RouterService; pub struct RouterProps<'a> { children: Element<'a>, - #[props(default, strip_option)] - onchange: Option<&'a dyn Fn(&'a str)>, + #[props(default)] + onchange: EventHandler<'a, String>, } #[allow(non_snake_case)] pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element { - cx.use_hook(|_| { + let svc = cx.use_hook(|_| { let update = cx.schedule_update_any(); cx.provide_context(RouterService::new(update, cx.scope_id())) }); + let any_pending = svc.pending_events.borrow().len() > 0; + svc.pending_events.borrow_mut().clear(); + + if any_pending { + let location = svc.current_location(); + let path = location.path(); + cx.props.onchange.call(path.to_string()); + } + cx.render(rsx!( div { &cx.props.children } )) diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index b6ffb5971..309cef905 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -8,22 +8,9 @@ //! //! ```rust //! fn app(cx: Scope) -> Element { +//! //! } -//! -//! -//! -//! -//! //! ``` -//! -//! -//! -//! -//! -//! -//! -//! -//! mod hooks { mod use_route; diff --git a/packages/router/src/service.rs b/packages/router/src/service.rs index 00642414f..9da2bef7e 100644 --- a/packages/router/src/service.rs +++ b/packages/router/src/service.rs @@ -9,6 +9,7 @@ use dioxus_core::ScopeId; pub struct RouterService { pub(crate) regen_route: Rc, + pub(crate) pending_events: Rc>>, history: Rc>, slots: Rc>>, root_found: Rc>>, @@ -16,6 +17,12 @@ pub struct RouterService { listener: HistoryListener, } +pub enum RouteEvent { + Change, + Pop, + Push, +} + enum RouteSlot { Routes { // the partial route @@ -36,28 +43,37 @@ impl RouterService { let path = location.path(); let slots: Rc>> = Default::default(); - - let _slots = slots.clone(); - + let pending_events: Rc>> = Default::default(); let root_found = Rc::new(Cell::new(None)); - let regen = regen_route.clone(); - let _root_found = root_found.clone(); - let listener = history.listen(move || { - _root_found.set(None); - // checking if the route is valid is cheap, so we do it - for (slot, root) in _slots.borrow_mut().iter().rev() { - log::trace!("regenerating slot {:?} for root '{}'", slot, root); - regen(*slot); + + let listener = history.listen({ + let pending_events = pending_events.clone(); + let regen_route = regen_route.clone(); + let root_found = root_found.clone(); + let slots = slots.clone(); + move || { + root_found.set(None); + // checking if the route is valid is cheap, so we do it + for (slot, root) in slots.borrow_mut().iter().rev() { + log::trace!("regenerating slot {:?} for root '{}'", slot, root); + regen_route(*slot); + } + + // also regenerate the root + regen_route(root_scope); + + pending_events.borrow_mut().push(RouteEvent::Change) } }); Self { + listener, root_found, history: Rc::new(RefCell::new(history)), regen_route, slots, + pending_events, cur_path_params: Rc::new(RefCell::new(HashMap::new())), - listener, } } From a8952a9ee8d8831fa911cef4e7035293c3a9f88b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 25 Jan 2022 21:41:40 -0500 Subject: [PATCH 72/79] fix: exampels --- examples/calculator.rs | 36 +-- examples/crm.rs | 30 +-- examples/disabled.rs | 6 +- examples/dog_app.rs | 12 +- examples/framework_benchmark.rs | 4 +- examples/hydration.rs | 4 +- examples/pattern_reducer.rs | 6 +- examples/readme.rs | 6 +- examples/rsx_compile_fail.rs | 2 +- examples/tasks.rs | 8 +- examples/todomvc.rs | 48 ++-- examples/xss_safety.rs | 4 +- packages/hooks/src/lib.rs | 64 ++--- packages/hooks/src/usestate.rs | 455 ++++++++++++++++++++------------ 14 files changed, 400 insertions(+), 285 deletions(-) diff --git a/examples/calculator.rs b/examples/calculator.rs index 7f051052b..5e23a3846 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -18,17 +18,19 @@ fn main() { } fn app(cx: Scope) -> Element { - let display_value = use_state(&cx, || String::from("0")); + let (display_value, set_display_value) = use_state(&cx, || String::from("0")); let input_digit = move |num: u8| { - if display_value.get() == "0" { - display_value.set(String::new()); + if display_value == "0" { + set_display_value(String::new()); } - display_value.modify().push_str(num.to_string().as_str()); + set_display_value + .make_mut() + .push_str(num.to_string().as_str()); }; let input_operator = move |key: &str| { - display_value.modify().push_str(key); + set_display_value.make_mut().push_str(key); }; cx.render(rsx!( @@ -53,7 +55,7 @@ fn app(cx: Scope) -> Element { KeyCode::Num9 => input_digit(9), KeyCode::Backspace => { if !display_value.len() != 0 { - display_value.modify().pop(); + set_display_value.make_mut().pop(); } } _ => {} @@ -65,21 +67,21 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-clear", onclick: move |_| { - display_value.set(String::new()); - if *display_value != "" { - display_value.set("0".into()); + set_display_value(String::new()); + if !display_value.is_empty(){ + set_display_value("0".into()); } }, - [if *display_value == "" { "C" } else { "AC" }] + [if display_value.is_empty() { "C" } else { "AC" }] } button { class: "calculator-key key-sign", onclick: move |_| { - let temp = calc_val(display_value.get().clone()); + let temp = calc_val(display_value.clone()); if temp > 0.0 { - display_value.set(format!("-{}", temp)); + set_display_value(format!("-{}", temp)); } else { - display_value.set(format!("{}", temp.abs())); + set_display_value(format!("{}", temp.abs())); } }, "±" @@ -87,8 +89,8 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-percent", onclick: move |_| { - display_value.set( - format!("{}", calc_val(display_value.get().clone()) / 100.0) + set_display_value( + format!("{}", calc_val(display_value.clone()) / 100.0) ); }, "%" @@ -98,7 +100,7 @@ fn app(cx: Scope) -> Element { button { class: "calculator-key key-0", onclick: move |_| input_digit(0), "0" } - button { class: "calculator-key key-dot", onclick: move |_| display_value.modify().push('.'), + button { class: "calculator-key key-dot", onclick: move |_| set_display_value.make_mut().push('.'), "●" } (1..10).map(|k| rsx!{ @@ -130,7 +132,7 @@ fn app(cx: Scope) -> Element { } button { class: "calculator-key key-equals", onclick: move |_| { - display_value.set(format!("{}", calc_val(display_value.get().clone()))); + set_display_value(format!("{}", calc_val(display_value.clone()))); }, "=" } diff --git a/examples/crm.rs b/examples/crm.rs index 241511385..f6e7e05af 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -20,11 +20,11 @@ pub struct Client { } fn app(cx: Scope) -> Element { - let scene = use_state(&cx, || Scene::ClientsList); let clients = use_ref(&cx, || vec![] as Vec); - let firstname = use_state(&cx, String::new); - let lastname = use_state(&cx, String::new); - let description = use_state(&cx, String::new); + let (scene, set_scene) = use_state(&cx, || Scene::ClientsList); + let (firstname, set_firstname) = use_state(&cx, String::new); + let (lastname, set_lastname) = use_state(&cx, String::new); + let (description, set_description) = use_state(&cx, String::new); cx.render(rsx!( body { @@ -38,7 +38,7 @@ fn app(cx: Scope) -> Element { h1 {"Dioxus CRM Example"} - match scene.get() { + match scene { Scene::ClientsList => rsx!( div { class: "crm", h2 { margin_bottom: "10px", "List of clients" } @@ -51,8 +51,8 @@ fn app(cx: Scope) -> Element { }) ) } - button { class: "pure-button pure-button-primary", onclick: move |_| scene.set(Scene::NewClientForm), "Add New" } - button { class: "pure-button", onclick: move |_| scene.set(Scene::Settings), "Settings" } + button { class: "pure-button pure-button-primary", onclick: move |_| set_scene(Scene::NewClientForm), "Add New" } + button { class: "pure-button", onclick: move |_| set_scene(Scene::Settings), "Settings" } } ), Scene::NewClientForm => rsx!( @@ -63,19 +63,19 @@ fn app(cx: Scope) -> Element { class: "new-client firstname", placeholder: "First name", value: "{firstname}", - oninput: move |e| firstname.set(e.value.clone()) + oninput: move |e| set_firstname(e.value.clone()) } input { class: "new-client lastname", placeholder: "Last name", value: "{lastname}", - oninput: move |e| lastname.set(e.value.clone()) + oninput: move |e| set_lastname(e.value.clone()) } textarea { class: "new-client description", placeholder: "Description", value: "{description}", - oninput: move |e| description.set(e.value.clone()) + oninput: move |e| set_description(e.value.clone()) } } button { @@ -86,13 +86,13 @@ fn app(cx: Scope) -> Element { first_name: (*firstname).clone(), last_name: (*lastname).clone(), }); - description.set(String::new()); - firstname.set(String::new()); - lastname.set(String::new()); + set_description(String::new()); + set_firstname(String::new()); + set_lastname(String::new()); }, "Add New" } - button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList), + button { class: "pure-button", onclick: move |_| set_scene(Scene::ClientsList), "Go Back" } } @@ -108,7 +108,7 @@ fn app(cx: Scope) -> Element { } button { class: "pure-button pure-button-primary", - onclick: move |_| scene.set(Scene::ClientsList), + onclick: move |_| set_scene(Scene::ClientsList), "Go Back" } } diff --git a/examples/disabled.rs b/examples/disabled.rs index 5bfa5bbb8..f72a8ff77 100644 --- a/examples/disabled.rs +++ b/examples/disabled.rs @@ -5,13 +5,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let disabled = use_state(&cx, || false); + let (disabled, set_disabled) = use_state(&cx, || false); cx.render(rsx! { div { button { - onclick: move |_| disabled.set(!disabled.get()), - "click to " [if **disabled {"enable"} else {"disable"} ] " the lower button" + onclick: move |_| set_disabled(!disabled), + "click to " [if *disabled {"enable"} else {"disable"} ] " the lower button" } button { diff --git a/examples/dog_app.rs b/examples/dog_app.rs index 54fb12baa..331574a75 100644 --- a/examples/dog_app.rs +++ b/examples/dog_app.rs @@ -24,7 +24,7 @@ fn app(cx: Scope) -> Element { .await }); - let selected_breed = use_state(&cx, || None); + let (breed, set_breed) = use_state(&cx, || None); match fut.value() { Some(Ok(breeds)) => cx.render(rsx! { @@ -36,14 +36,14 @@ fn app(cx: Scope) -> Element { breeds.message.keys().map(|breed| rsx!( li { button { - onclick: move |_| selected_breed.set(Some(breed.clone())), + onclick: move |_| set_breed(Some(breed.clone())), "{breed}" } } )) } div { flex: "50%", - match selected_breed.get() { + match breed { Some(breed) => rsx!( Breed { breed: breed.clone() } ), None => rsx!("No Breed selected"), } @@ -73,9 +73,9 @@ fn Breed(cx: Scope, breed: String) -> Element { reqwest::get(endpoint).await.unwrap().json::().await }); - let breed_name = use_state(&cx, || breed.clone()); - if breed_name.get() != breed { - breed_name.set(breed.clone()); + let (name, set_name) = use_state(&cx, || breed.clone()); + if name != breed { + set_name(breed.clone()); fut.restart(); } diff --git a/examples/framework_benchmark.rs b/examples/framework_benchmark.rs index 69e3b8faa..7cea4f5f7 100644 --- a/examples/framework_benchmark.rs +++ b/examples/framework_benchmark.rs @@ -33,7 +33,7 @@ impl Label { fn app(cx: Scope) -> Element { let items = use_ref(&cx, Vec::new); - let selected = use_state(&cx, || None); + let (selected, set_selected) = use_state(&cx, || None); cx.render(rsx! { div { class: "container", @@ -71,7 +71,7 @@ fn app(cx: Scope) -> Element { rsx!(tr { class: "{is_in_danger}", td { class:"col-md-1" } td { class:"col-md-1", "{item.key}" } - td { class:"col-md-1", onclick: move |_| selected.set(Some(id)), + td { class:"col-md-1", onclick: move |_| set_selected(Some(id)), a { class: "lbl", item.labels } } td { class: "col-md-1", diff --git a/examples/hydration.rs b/examples/hydration.rs index 6ec777352..83b7633df 100644 --- a/examples/hydration.rs +++ b/examples/hydration.rs @@ -20,13 +20,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let val = use_state(&cx, || 0); + let (val, set_val) = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "hello world. Count: {val}" } button { - onclick: move |_| *val.modify() += 1, + onclick: move |_| set_val(val + 1), "click to increment" } } diff --git a/examples/pattern_reducer.rs b/examples/pattern_reducer.rs index 82634d771..04c1811a1 100644 --- a/examples/pattern_reducer.rs +++ b/examples/pattern_reducer.rs @@ -15,16 +15,16 @@ fn main() { } fn app(cx: Scope) -> Element { - let state = use_state(&cx, PlayerState::new); + let (state, set_state) = use_state(&cx, PlayerState::new); cx.render(rsx!( div { h1 {"Select an option"} h3 { "The radio is... " [state.is_playing()] "!" } - button { onclick: move |_| state.modify().reduce(PlayerAction::Pause), + button { onclick: move |_| set_state.make_mut().reduce(PlayerAction::Pause), "Pause" } - button { onclick: move |_| state.modify().reduce(PlayerAction::Play), + button { onclick: move |_| set_state.make_mut().reduce(PlayerAction::Play), "Play" } } diff --git a/examples/readme.rs b/examples/readme.rs index 89282babd..b37a8e07a 100644 --- a/examples/readme.rs +++ b/examples/readme.rs @@ -9,13 +9,13 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx! { div { h1 { "High-Five counter: {count}" } - button { onclick: move |_| *count.modify() += 1, "Up high!" } - button { onclick: move |_| *count.modify() -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } } }) } diff --git a/examples/rsx_compile_fail.rs b/examples/rsx_compile_fail.rs index 20760ef5b..8f490bf26 100644 --- a/examples/rsx_compile_fail.rs +++ b/examples/rsx_compile_fail.rs @@ -12,7 +12,7 @@ fn main() { } fn example(cx: Scope) -> Element { - let items = use_state(&cx, || { + let (items, _set_items) = use_state(&cx, || { vec![Thing { a: "asd".to_string(), b: 10, diff --git a/examples/tasks.rs b/examples/tasks.rs index f71abe38d..3c4d4cba3 100644 --- a/examples/tasks.rs +++ b/examples/tasks.rs @@ -10,14 +10,14 @@ fn main() { } fn app(cx: Scope) -> Element { - let count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); use_future(&cx, move || { - let mut count = count.for_async(); + let set_count = set_count.to_owned(); async move { loop { tokio::time::sleep(Duration::from_millis(1000)).await; - count += 1; + set_count.modify(|f| f + 1); } } }); @@ -26,7 +26,7 @@ fn app(cx: Scope) -> Element { div { h1 { "Current count: {count}" } button { - onclick: move |_| count.set(0), + onclick: move |_| set_count(0), "Reset the count" } } diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 30d1fbb10..e5f7d1dfa 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -19,15 +19,15 @@ pub struct TodoItem { } pub fn app(cx: Scope<()>) -> Element { - let todos = use_state(&cx, im_rc::HashMap::::default); - let filter = use_state(&cx, || FilterState::All); - let draft = use_state(&cx, || "".to_string()); - let (todo_id, set_todo_id) = use_state(&cx, || 0).split(); + let (todos, set_todos) = use_state(&cx, im_rc::HashMap::::default); + let (filter, set_filter) = use_state(&cx, || FilterState::All); + let (draft, set_draft) = use_state(&cx, || "".to_string()); + let (todo_id, set_todo_id) = use_state(&cx, || 0); // Filter the todos based on the filter state let mut filtered_todos = todos .iter() - .filter(|(_, item)| match **filter { + .filter(|(_, item)| match filter { FilterState::All => true, FilterState::Active => !item.checked, FilterState::Completed => item.checked, @@ -54,25 +54,25 @@ pub fn app(cx: Scope<()>) -> Element { placeholder: "What needs to be done?", value: "{draft}", autofocus: "true", - oninput: move |evt| draft.set(evt.value.clone()), + oninput: move |evt| set_draft(evt.value.clone()), onkeydown: move |evt| { if evt.key == "Enter" && !draft.is_empty() { - todos.modify().insert( + set_todos.make_mut().insert( *todo_id, TodoItem { id: *todo_id, checked: false, - contents: draft.get().clone(), + contents: draft.clone(), }, ); set_todo_id(todo_id + 1); - draft.set("".to_string()); + set_draft("".to_string()); } } } } ul { class: "todo-list", - filtered_todos.iter().map(|id| rsx!(todo_entry( key: "{id}", id: *id, todos: todos ))) + filtered_todos.iter().map(|id| rsx!(todo_entry( key: "{id}", id: *id, set_todos: set_todos ))) } (!todos.is_empty()).then(|| rsx!( footer { class: "footer", @@ -81,14 +81,14 @@ pub fn app(cx: Scope<()>) -> Element { span {"{item_text} left"} } ul { class: "filters", - li { class: "All", a { onclick: move |_| filter.set(FilterState::All), "All" }} - li { class: "Active", a { onclick: move |_| filter.set(FilterState::Active), "Active" }} - li { class: "Completed", a { onclick: move |_| filter.set(FilterState::Completed), "Completed" }} + li { class: "All", a { onclick: move |_| set_filter(FilterState::All), "All" }} + li { class: "Active", a { onclick: move |_| set_filter(FilterState::Active), "Active" }} + li { class: "Completed", a { onclick: move |_| set_filter(FilterState::Completed), "Completed" }} } (show_clear_completed).then(|| rsx!( button { class: "clear-completed", - onclick: move |_| todos.modify().retain(|_, todo| !todo.checked), + onclick: move |_| set_todos.make_mut().retain(|_, todo| !todo.checked), "Clear completed" } )) @@ -106,24 +106,26 @@ pub fn app(cx: Scope<()>) -> Element { #[derive(Props)] pub struct TodoEntryProps<'a> { - todos: &'a UseState>, + set_todos: &'a UseState>, id: u32, } pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { - let todo = &cx.props.todos[&cx.props.id]; - let is_editing = use_state(&cx, || false); + let (is_editing, set_is_editing) = use_state(&cx, || false); + + let todos = cx.props.set_todos.get(); + let todo = &todos[&cx.props.id]; let completed = if todo.checked { "completed" } else { "" }; - let editing = if *is_editing.get() { "editing" } else { "" }; + let editing = if *is_editing { "editing" } else { "" }; rsx!(cx, li { class: "{completed} {editing}", - onclick: move |_| is_editing.set(true), - onfocusout: move |_| is_editing.set(false), + onclick: move |_| set_is_editing(true), + onfocusout: move |_| set_is_editing(false), div { class: "view", input { class: "toggle", r#type: "checkbox", id: "cbg-{todo.id}", checked: "{todo.checked}", onchange: move |evt| { - cx.props.todos.modify()[&cx.props.id].checked = evt.value.parse().unwrap(); + cx.props.set_todos.make_mut()[&cx.props.id].checked = evt.value.parse().unwrap(); } } label { r#for: "cbg-{todo.id}", pointer_events: "none", "{todo.contents}" } @@ -132,11 +134,11 @@ pub fn todo_entry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { input { class: "edit", value: "{todo.contents}", - oninput: move |evt| cx.props.todos.modify()[&cx.props.id].contents = evt.value.clone(), + oninput: move |evt| cx.props.set_todos.make_mut()[&cx.props.id].contents = evt.value.clone(), autofocus: "true", onkeydown: move |evt| { match evt.key.as_str() { - "Enter" | "Escape" | "Tab" => is_editing.set(false), + "Enter" | "Escape" | "Tab" => set_is_editing(false), _ => {} } }, diff --git a/examples/xss_safety.rs b/examples/xss_safety.rs index 36fc81a95..2790a8a89 100644 --- a/examples/xss_safety.rs +++ b/examples/xss_safety.rs @@ -9,7 +9,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let contents = use_state(&cx, || { + let (contents, set_contents) = use_state(&cx, || { String::from("") }); @@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element { input { value: "{contents}", r#type: "text", - oninput: move |e| contents.set(e.value.clone()), + oninput: move |e| set_contents(e.value.clone()), } } }) diff --git a/packages/hooks/src/lib.rs b/packages/hooks/src/lib.rs index 139e05245..cb4afd297 100644 --- a/packages/hooks/src/lib.rs +++ b/packages/hooks/src/lib.rs @@ -16,45 +16,25 @@ pub use usefuture::*; mod usesuspense; pub use usesuspense::*; -// #[macro_export] -// macro_rules! to_owned { -// ($($es:ident),+) => {$( -// #[allow(unused_mut)] -// let mut $es = $es.to_owned(); -// )*} -// } - -// /// Calls `for_async` on the series of paramters. -// /// -// /// If the type is Clone, then it will be cloned. However, if the type is not `clone` -// /// then it must have a `for_async` method for Rust to lower down into. -// /// -// /// See: how use_state implements `for_async` but *not* through the trait. -// #[macro_export] -// macro_rules! for_async { -// ($($es:ident),+) => {$( -// #[allow(unused_mut)] -// let mut $es = $es.for_async(); -// )*} -// } - -// /// This is a marker trait that uses decoherence. -// /// -// /// It is *not* meant for hooks to actually implement, but rather defer to their -// /// underlying implementation if they *don't* implement the trait. -// /// -// /// -// pub trait AsyncHook { -// type Output; -// fn for_async(self) -> Self::Output; -// } - -// impl AsyncHook for T -// where -// T: ToOwned, -// { -// type Output = T; -// fn for_async(self) -> Self::Output { -// self -// } -// } +#[macro_export] +/// A helper macro for using hooks in async environements. +/// +/// # Usage +/// +/// +/// ``` +/// let (data) = use_ref(&cx, || {}); +/// +/// let handle_thing = move |_| { +/// to_owned![data] +/// cx.spawn(async move { +/// // do stuff +/// }); +/// } +/// ``` +macro_rules! to_owned { + ($($es:ident),+) => {$( + #[allow(unused_mut)] + let mut $es = $es.to_owned(); + )*} +} diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 9cfcedf4f..eb3900981 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -1,46 +1,29 @@ +#![warn(clippy::pedantic)] + use dioxus_core::prelude::*; use std::{ - cell::{Cell, Ref, RefCell, RefMut}, + cell::{RefCell, RefMut}, fmt::{Debug, Display}, rc::Rc, }; -/// Store state between component renders! +/// Store state between component renders. /// /// ## Dioxus equivalent of useState, designed for Rust /// /// The Dioxus version of `useState` for state management inside components. It allows you to ergonomically store and /// modify state between component renders. When the state is updated, the component will re-render. /// -/// Dioxus' use_state basically wraps a RefCell with helper methods and integrates it with the VirtualDOM update system. -/// -/// [`use_state`] exposes a few helper methods to modify the underlying state: -/// - `.set(new)` allows you to override the "work in progress" value with a new value -/// - `.get_mut()` allows you to modify the WIP value -/// - `.get_wip()` allows you to access the WIP value -/// - `.deref()` provides the previous value (often done implicitly, though a manual dereference with `*` might be required) -/// -/// Additionally, a ton of std::ops traits are implemented for the `UseState` wrapper, meaning any mutative type operations -/// will automatically be called on the WIP value. -/// -/// ## Combinators -/// -/// On top of the methods to set/get state, `use_state` also supports fancy combinators to extend its functionality: -/// - `.classic()` and `.split()` convert the hook into the classic React-style hook -/// ```rust -/// let (state, set_state) = use_state(&cx, || 10).split() -/// ``` -/// Usage: /// /// ```ignore /// const Example: Component = |cx| { -/// let counter = use_state(&cx, || 0); +/// let (count, set_count) = use_state(&cx, || 0); /// /// cx.render(rsx! { /// div { -/// h1 { "Counter: {counter}" } -/// button { onclick: move |_| counter.set(**counter + 1), "Increment" } -/// button { onclick: move |_| counter.set(**counter - 1), "Decrement" } +/// h1 { "Count: {count}" } +/// button { onclick: move |_| set_count(a - 1), "Increment" } +/// button { onclick: move |_| set_count(a + 1), "Decrement" } /// } /// )) /// } @@ -48,34 +31,269 @@ use std::{ pub fn use_state<'a, T: 'static>( cx: &'a ScopeState, initial_state_fn: impl FnOnce() -> T, -) -> &'a UseState { - let hook = cx.use_hook(move |_| UseState { - current_val: Rc::new(initial_state_fn()), - update_callback: cx.schedule_update(), - wip: Rc::new(RefCell::new(None)), - update_scheuled: Cell::new(false), +) -> (&'a T, &'a UseState) { + let hook = cx.use_hook(move |_| { + let current_val = Rc::new(initial_state_fn()); + let update_callback = cx.schedule_update(); + let slot = Rc::new(RefCell::new(current_val.clone())); + let setter = Rc::new({ + crate::to_owned![update_callback, slot]; + move |new| { + let mut slot = slot.borrow_mut(); + + // if there's only one reference (weak or otherwise), we can just swap the values + // Typically happens when the state is set multiple times - we don't want to create a new Rc for each new value + if let Some(val) = Rc::get_mut(&mut slot) { + *val = new; + } else { + *slot = Rc::new(new); + } + + update_callback(); + } + }); + + UseState { + current_val, + update_callback, + setter, + slot, + } }); - hook.update_scheuled.set(false); - let mut new_val = hook.wip.borrow_mut(); - - if new_val.is_some() { - // if there's only one reference (weak or otherwise), we can just swap the values - if let Some(val) = Rc::get_mut(&mut hook.current_val) { - *val = new_val.take().unwrap(); - } else { - hook.current_val = Rc::new(new_val.take().unwrap()); - } - } - - hook + (hook.current_val.as_ref(), hook) } pub struct UseState { pub(crate) current_val: Rc, - pub(crate) wip: Rc>>, pub(crate) update_callback: Rc, - pub(crate) update_scheuled: Cell, + pub(crate) setter: Rc, + pub(crate) slot: Rc>>, +} + +impl UseState { + /// Get the current value of the state by cloning its container Rc. + /// + /// This is useful when you are dealing with state in async contexts but need + /// to know the current value. You are not given a reference to the state. + /// + /// # Examples + /// An async context might need to know the current value: + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (count, set_count) = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = set_count.to_owned(); + /// async move { + /// let current = set_count.current(); + /// } + /// }) + /// } + /// ``` + #[must_use] + pub fn current(&self) -> Rc { + self.slot.borrow().clone() + } + + /// Get the `setter` function directly without the `UseState` wrapper. + /// + /// This is useful for passing the setter function to other components. + /// + /// However, for most cases, calling `to_owned` o`UseState`te is the + /// preferred way to get "anoth`set_state`tate handle. + /// + /// + /// # Examples + /// A component might require an `Rc` as an input to set a value. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// rsx!{ + /// Component { + /// handler: set_val.setter() + /// } + /// } + /// } + /// ``` + #[must_use] + pub fn setter(&self) -> Rc { + self.setter.clone() + } + + /// Set the state to a new value, using the current state value as a reference. + /// + /// This is similar to passing a closure to React's `set_value` function. + /// + /// # Examples + /// + /// Basic usage: + /// ```rust + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// // to increment the value + /// set_value.modify(|v| v + 1); + /// + /// // usage in async + /// cx.spawn({ + /// let set_value = set_value.to_owned(); + /// async move { + /// set_value.modify(|v| v + 1); + /// } + /// }); + /// + /// # todo!() + /// } + /// ``` + pub fn modify(&self, f: impl FnOnce(&T) -> T) { + let curernt = self.slot.borrow(); + let new_val = f(curernt.as_ref()); + (self.setter)(new_val); + } + + /// Get the value of the state when this handle was created. + /// + /// This method is useful when you want an `Rc` around the data to cheaply + /// pass it around your app. + /// + /// ## Warning + /// + /// This will return a stale value if used within async contexts. + /// + /// Try `current` to get the real current value of the state. + /// + /// ## Example + /// + /// ```rust, ignore + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let (value, set_value) = use_state(&cx, || 0); + /// + /// let as_rc = set_value.get(); + /// assert_eq!(as_rc.as_ref(), &0); + /// + /// # todo!() + /// } + /// ``` + #[must_use] + pub fn get(&self) -> &Rc { + &self.current_val + } + + /// Mark the component that create this [`UseState`] as dirty, forcing it to re-render. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let (count, set_count) = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = set_count.to_owned(); + /// async move { + /// // for the component to re-render + /// set_count.needs_update(); + /// } + /// }) + /// } + /// ``` + pub fn needs_update(&self) { + (self.update_callback)(); + } +} + +impl UseState { + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// If you are comfortable dealing with `RefMut`, then you can use `make_mut` to get + /// the underlying slot. However, be careful with `RefMut` since you might panic + /// if the `RefCell` is left open. + /// + /// # Examples + /// + /// ``` + /// let (val, set_val) = use_state(&cx, || 0); + /// + /// set_val.with_mut(|v| *v = 1); + /// ``` + pub fn with_mut(&self, apply: impl FnOnce(&mut T)) { + let mut slot = self.slot.borrow_mut(); + let mut inner = slot.as_ref().to_owned(); + + apply(&mut inner); + + if let Some(new) = Rc::get_mut(&mut slot) { + *new = inner; + } else { + *slot = Rc::new(inner); + } + + self.needs_update(); + } + + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// # Warning + /// Be careful with `RefMut` since you might panic if the `RefCell` is left open! + /// + /// # Examples + /// + /// ``` + /// let (val, set_val) = use_state(&cx, || 0); + /// + /// set_val.with_mut(|v| *v = 1); + /// ``` + #[must_use] + pub fn make_mut(&self) -> RefMut { + let mut slot = self.slot.borrow_mut(); + + self.needs_update(); + + if Rc::strong_count(&*slot) > 0 { + *slot = Rc::new(slot.as_ref().to_owned()); + } + + RefMut::map(slot, |rc| Rc::get_mut(rc).expect("the hard count to be 0")) + } +} + +impl ToOwned for UseState { + type Owned = UseState; + + fn to_owned(&self) -> Self::Owned { + UseState { + current_val: self.current_val.clone(), + update_callback: self.update_callback.clone(), + setter: self.setter.clone(), + slot: self.slot.clone(), + } + } +} + +impl<'a, T: 'static + Display> std::fmt::Display for UseState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.current_val) + } +} + +impl PartialEq> for UseState { + fn eq(&self, other: &UseState) -> bool { + // some level of memoization for UseState + Rc::ptr_eq(&self.slot, &other.slot) + } } impl Debug for UseState { @@ -84,129 +302,42 @@ impl Debug for UseState { } } -impl UseState { - /// Tell the Dioxus Scheduler that we need to be processed - pub fn needs_update(&self) { - if !self.update_scheuled.get() { - self.update_scheuled.set(true); - (self.update_callback)(); - } - } - - pub fn set(&self, new_val: T) { - *self.wip.borrow_mut() = Some(new_val); - self.needs_update(); - } - - pub fn get(&self) -> &T { - &self.current_val - } - - pub fn get_rc(&self) -> &Rc { - &self.current_val - } - - /// Get the current status of the work-in-progress data - pub fn get_wip(&self) -> Ref> { - self.wip.borrow() - } - - /// Get the current status of the work-in-progress data - pub fn get_wip_mut(&self) -> RefMut> { - self.wip.borrow_mut() - } - - pub fn split(&self) -> (&T, Rc) { - (&self.current_val, self.setter()) - } - - pub fn setter(&self) -> Rc { - let slot = self.wip.clone(); - let callback = self.update_callback.clone(); - Rc::new(move |new| { - callback(); - *slot.borrow_mut() = Some(new) - }) - } - - pub fn wtih(&self, f: impl FnOnce(&mut T)) { - let mut val = self.wip.borrow_mut(); - - if let Some(inner) = val.as_mut() { - f(inner); - } - } - - pub fn for_async(&self) -> UseState { - let UseState { - current_val, - wip, - update_callback, - update_scheuled, - } = self; - - UseState { - current_val: current_val.clone(), - wip: wip.clone(), - update_callback: update_callback.clone(), - update_scheuled: update_scheuled.clone(), - } - } -} - -impl> UseState { - /// Gain mutable access to the new value via [`RefMut`]. - /// - /// If `modify` is called, then the component will re-render. - /// - /// This method is only available when the value is a `ToOwned` type. - /// - /// Mutable access is derived by calling "ToOwned" (IE cloning) on the current value. - /// - /// To get a reference to the current value, use `.get()` - pub fn modify(&self) -> RefMut { - // make sure we get processed - self.needs_update(); - - // Bring out the new value, cloning if it we need to - // "get_mut" is locked behind "ToOwned" to make it explicit that cloning occurs to use this - RefMut::map(self.wip.borrow_mut(), |slot| { - if slot.is_none() { - *slot = Some(self.current_val.as_ref().to_owned()); - } - slot.as_mut().unwrap() - }) - } - - pub fn inner(self) -> T { - self.current_val.as_ref().to_owned() - } -} - impl<'a, T> std::ops::Deref for UseState { - type Target = T; + type Target = Rc; fn deref(&self) -> &Self::Target { - self.get() + &self.setter } } -// enable displaty for the handle -impl<'a, T: 'static + Display> std::fmt::Display for UseState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.current_val) - } -} +#[test] +fn api_makes_sense() { + #[allow(unused)] + fn app(cx: Scope) -> Element { + let (val, set_val) = use_state(&cx, || 0); -impl<'a, V, T: PartialEq> PartialEq for UseState { - fn eq(&self, other: &V) -> bool { - self.get() == other - } -} -impl<'a, O, T: std::ops::Not + Copy> std::ops::Not for UseState { - type Output = O; + set_val(0); + set_val.modify(|v| v + 1); + let real_current = set_val.current(); - fn not(self) -> Self::Output { - !*self.get() + match val { + 10 => { + set_val(20); + set_val.modify(|v| v + 1); + } + 20 => {} + _ => { + println!("{real_current}"); + } + } + + cx.spawn({ + crate::to_owned![set_val]; + async move { + set_val.modify(|f| f + 1); + } + }); + + cx.render(LazyNodes::new(|f| f.static_text("asd"))) } } From 06d81475f279e88c150ac319b7f7a1e213d3f145 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 00:40:01 -0500 Subject: [PATCH 73/79] docs: improve readme by removing table --- README.md | 61 ++++++++++++++++++------------------------------------- 1 file changed, 20 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 9895dfb76..8ac24bc92 100644 --- a/README.md +++ b/README.md @@ -167,52 +167,31 @@ Dioxus primarily emphasizes **developer experience** and **familiarity with Reac - [Azul](https://azul.rs): Fully native HTML/CSS renderer for desktop applications, no support for web/ssr -# Parity with React +## Parity with React & Roadmap -Dioxus is heavily inspired by React, but we want your transition to feel like an upgrade. Dioxus is _most_ of the way there, but missing a few key features. This parity table does not necessarily include important ecosystem crates like code blocks, markdown, resizing hooks, etc. +Dioxus is heavily inspired by React, but we want your transition to feel like an upgrade. Dioxus is _most_ of the way there, but missing a few key features. These include: +- Portals +- Suspense integration with SSR +- Server Components / Bundle Splitting / Lazy -| Feature | Dioxus | React | Notes for Dioxus | -| ------------------------- | ------ | ----- | -------------------------------------------------------------------- | -| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | -| Map, Iterator | ✅ | ✅ | map/filter/reduce to produce rsx! | -| Keyed Components | ✅ | ✅ | advanced diffing with keys | -| Web | ✅ | ✅ | renderer for web browser | -| Desktop (webview) | ✅ | ✅ | renderer for desktop | -| Shared State (Context) | ✅ | ✅ | share state through the tree | -| Hooks | ✅ | ✅ | memory cells in components | -| SSR | ✅ | ✅ | render directly to string | -| Component Children | ✅ | ✅ | cx.children() as a list of nodes | -| Headless components | ✅ | ✅ | components that don't return real elements | -| Fragments | ✅ | ✅ | multiple elements without a real root | -| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | -| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | -| CSS/Inline Styles | ✅ | ✅ | syntax for inline styles/attribute groups | -| Custom elements | ✅ | ✅ | Define new element primitives | -| Suspense | ✅ | ✅ | schedule future render from future/promise | -| Integrated error handling | ✅ | ✅ | Gracefully handle errors with ? syntax | -| NodeRef | ✅ | ✅ | gain direct access to nodes | -| Re-hydration | ✅ | ✅ | Pre-render to HTML to speed up first contentful paint | -| Jank-Free Rendering | ✅ | ✅ | Large diffs are segmented across frames for silky-smooth transitions | -| Effects | ✅ | ✅ | Run effects after a component has been committed to render | -| Portals | 🛠 | ✅ | Render nodes outside of the traditional tree structure | -| Cooperative Scheduling | 🛠 | ✅ | Prioritize important events over non-important events | -| Server Components | 🛠 | ✅ | Hybrid components for SPA and Server | -| Bundle Splitting | 👀 | ✅ | Efficiently and asynchronously load the app | -| Lazy Components | 👀 | ✅ | Dynamically load the new components as the page is loaded | -| 1st class global state | ✅ | ✅ | redux/recoil/mobx on top of context | -| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | -| Subtree Memoization | ✅ | ❓ | skip diffing static element subtrees | -| High-efficiency templates | 🛠 | ❓ | rsx! calls are translated to templates on the DOM's side | -| Compile-time correct | ✅ | ❓ | Throw errors on invalid template layouts | -| Heuristic Engine | ✅ | ❓ | track component memory usage to minimize future allocations | -| Fine-grained reactivity | 👀 | ❓ | Skip diffing for fine-grain updates | +Dioxus is unique in the Rust ecosystem in that it supports: -- ✅ = implemented and working -- 🛠 = actively being worked on -- 👀 = not yet implemented or being worked on -- ❓ = not sure if will or can implement +- Components with props that borrow from their parent +- Server-side-rendering with client-side hydration +- Support for desktop applications +For more information on what features are currently available and the roadmap for the future, be sure to check out [the guide](https://dioxuslabs.com/guide/). + +## Projects in the ecosystem + +Want to jump in and help build the future of Rust frontend? There's plenty of places where your contributions can make a huge difference: + +- [TUI renderer](https://github.com/dioxusLabs/rink) +- [CLI Tooling](https://github.com/dioxusLabs/cli) +- [Documentation and Example Projects](https://github.com/dioxusLabs/docsite) +- LiveView and Web Server +- Asset System ## License From fbb610df329d83357c9e03faf6a11d24bbf20ef7 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 00:40:51 -0500 Subject: [PATCH 74/79] docs: change indentation on readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ac24bc92..658a6e48d 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ Specifically, Dioxus provides us many other assurances: And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. -### Why NOT Dioxus? +## Why NOT Dioxus? You shouldn't use Dioxus if: - You don't like the React Hooks approach to frontend @@ -157,7 +157,7 @@ You shouldn't use Dioxus if: - You want to support browsers where Wasm or asm.js are not supported. - You need a Send+Sync UI solution (Dioxus is not currently thread-safe) -### Comparison with other Rust UI frameworks +## Comparison with other Rust UI frameworks Dioxus primarily emphasizes **developer experience** and **familiarity with React principles**. - [Yew](https://github.com/yewstack/yew): prefers the elm pattern instead of React-hooks, no borrowed props, supports SSR (no hydration). From 25286a6711e2505601f34c39bb8166d793dc927e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 26 Jan 2022 14:42:07 -0500 Subject: [PATCH 75/79] Create FUNDING.yml --- .github/FUNDING.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..332e03eb2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: jkelleyrtp # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +open_collective: dioxus-labs # Replace with a single Open Collective username From 6073bddf308278661d3f22bc626e6ad676d9e15b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 27 Jan 2022 15:53:06 -0500 Subject: [PATCH 76/79] docs: fix readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 658a6e48d..52493b119 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,12 @@ Dioxus is a portable, performant, and ergonomic framework for building cross-pla ```rust fn app(cx: Scope) -> Element { - let mut count = use_state(&cx, || 0); + let (count, set_count) = use_state(&cx, || 0); cx.render(rsx!( h1 { "High-Five counter: {count}" } - button { onclick: move |_| count += 1, "Up high!" } - button { onclick: move |_| count -= 1, "Down low!" } + button { onclick: move |_| set_count(count + 1), "Up high!" } + button { onclick: move |_| set_count(count - 1), "Down low!" } )) } ``` From 430cde7068d308f2783e33d278fd0c0efa659c1b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 27 Jan 2022 16:36:17 -0500 Subject: [PATCH 77/79] feat: default asset server --- examples/assets/logo.png | Bin 0 -> 59292 bytes examples/custom_assets.rs | 18 ++++++++++++++++++ packages/desktop/Cargo.toml | 7 ++++--- packages/desktop/src/lib.rs | 35 ++++++++++++++++++++++++++++++----- 4 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 examples/assets/logo.png create mode 100644 examples/custom_assets.rs diff --git a/examples/assets/logo.png b/examples/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7d08f2af8f057780eddae4b0f8b8fa753b69e7f8 GIT binary patch literal 59292 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 Element { + let (disabled, set_disabled) = use_state(&cx, || false); + + cx.render(rsx! { + div { + "hi" + img { + src: "examples/../../../assets/logo.png", + } + } + }) +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index eb9425c1f..123013e5e 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dioxus-core = { path = "../core", version ="^0.1.7", features = ["serialize"] } +dioxus-core = { path = "../core", version = "^0.1.7", features = ["serialize"] } argh = "0.1.4" serde = "1.0.120" serde_json = "1.0.61" @@ -27,9 +27,10 @@ tokio = { version = "1.12.0", features = [ "rt", "time", ], optional = true, default-features = false } -dioxus-core-macro = { path = "../core-macro", version ="^0.1.6"} -dioxus-html = { path = "../html", features = ["serialize"], version ="^0.1.4"} +dioxus-core-macro = { path = "../core-macro", version = "^0.1.6" } +dioxus-html = { path = "../html", features = ["serialize"], version = "^0.1.4" } webbrowser = "0.5.5" +mime_guess = "2.0.3" [features] default = ["tokio_runtime"] diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 93c175cc6..36b017077 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -210,18 +210,43 @@ pub fn launch_with_props( // For now, we only serve two pieces of content which get included as bytes into the final binary. let path = request.uri().replace("dioxus://", ""); - if path.trim_end_matches('/') == "index.html" { + // all assets shouldbe called from index.html + let trimmed = path.trim_start_matches("index.html/"); + + if trimmed.is_empty() { wry::http::ResponseBuilder::new() .mimetype("text/html") .body(include_bytes!("./index.html").to_vec()) - } else if path.trim_end_matches('/') == "index.html/index.js" { + } else if trimmed == "index.js" { wry::http::ResponseBuilder::new() .mimetype("text/javascript") .body(include_bytes!("../../jsinterpreter/interpreter.js").to_vec()) } else { - wry::http::ResponseBuilder::new() - .status(wry::http::status::StatusCode::NOT_FOUND) - .body(format!("Not found: {}", path).as_bytes().to_vec()) + // Read the file content from file path + use std::fs::read; + + let path_buf = std::path::Path::new(trimmed).canonicalize()?; + let cur_path = std::path::Path::new(".").canonicalize()?; + + if !path_buf.starts_with(cur_path) { + return wry::http::ResponseBuilder::new() + .status(wry::http::status::StatusCode::FORBIDDEN) + .body(String::from("Forbidden").into_bytes()); + } + + if !path_buf.exists() { + return wry::http::ResponseBuilder::new() + .status(wry::http::status::StatusCode::NOT_FOUND) + .body(String::from("Not Found").into_bytes()); + } + + let mime = mime_guess::from_path(&path_buf).first_or_octet_stream(); + + // do not let path searching to go two layers beyond the caller level + let data = read(path_buf)?; + let meta = format!("{mime}"); + + wry::http::ResponseBuilder::new().mimetype(&meta).body(data) } }) .with_file_drop_handler(move |window, evet| { From 6e4ed9e3510aa7383652adbf786677a86409ca6a Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 27 Jan 2022 16:37:09 -0500 Subject: [PATCH 78/79] examples: fixup asset example --- examples/custom_assets.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/custom_assets.rs b/examples/custom_assets.rs index 931caa839..e623d5b50 100644 --- a/examples/custom_assets.rs +++ b/examples/custom_assets.rs @@ -5,14 +5,10 @@ fn main() { } fn app(cx: Scope) -> Element { - let (disabled, set_disabled) = use_state(&cx, || false); - cx.render(rsx! { div { - "hi" - img { - src: "examples/../../../assets/logo.png", - } + "This should show an image:" + img { src: "examples/assets/logo.png", } } }) } From 22308eb26a9ea48b14f5f5abb833aa90a4e3fc40 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 27 Jan 2022 17:00:40 -0500 Subject: [PATCH 79/79] fix: custom protocol receiver type --- packages/desktop/src/cfg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index ad85cfc0f..5baa46f5d 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -71,7 +71,7 @@ impl DesktopConfig { self } - pub fn with_custom_protocol(mut self, name: String, handler: F) -> Self + pub fn with_custom_protocol(&mut self, name: String, handler: F) -> &mut Self where F: Fn(&HttpRequest) -> WryResult + 'static, {