瀏覽代碼

Merge branch 'master' into eval-stuff

Ilya Maximov 3 年之前
父節點
當前提交
ee22881e08

+ 0 - 1
Cargo.toml

@@ -51,7 +51,6 @@ liveview = ["dioxus-liveview"]
 members = [
     "packages/core",
     "packages/core-macro",
-    "packages/rsx",
     "packages/html",
     "packages/hooks",
     "packages/web",

+ 38 - 8
docs/guide/src/components/propsmacro.md

@@ -131,23 +131,29 @@ Borrowed Props cannot be safely memoized. However, this is not a problem – Dio
 
 ## Optional Props
 
-You can easily create optional fields by attaching the `optional` modifier to a field:
+You can easily create optional fields by using the `Option<…>` type for a field:
 
 ```rust
 #[derive(Props, PartialEq)]
 struct MyProps {
     name: String,
 
-    #[props(optional)]
     description: Option<String>
 }
 
 fn Demo(cx: MyProps) -> Element {
-    todo!()
+    let text = match cx.props.description {
+        Some(d) => d,             // if a value is provided
+        None => "No description"  // if the prop is omitted
+    };
+    
+    cx.render(rsx! {
+        "{name}": "{text}"
+    })
 }
 ```
-
-Then, we can completely omit the description field when calling the component:
+In this example ˋnameˋ is a required prop and ˋdescriptionˋ is optional.
+This means we can completely omit the description field when calling the component:
 
 ```rust
 rsx!{
@@ -157,15 +163,39 @@ rsx!{
     }
 }
 ```
+Additionally if we provide a value we don't have to wrap it with ˋSome(…)ˋ. This is done automatically for us:
+
+ˋˋˋrust
+rsx!{
+    Demo {
+        name: "Thing".to_string(),
+        description: "This is explains it".to_string(),
+    }
+}
+ˋˋˋ
+
+If you want to make a prop required even though it is of type ˋOptionˋ you can provide the ˋ!optionalˋ modifier:
+
+ˋˋˋrust
+#[derive(Props, PartialEq)]
+struct MyProps {
+    name: String,
+
+    #[props(!optional)]
+    description: Option<String>
+}
+ˋˋˋ
+
+This can be especially useful if you have a type alias named ˋOptionˋ in the current scope.
+
+For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new. The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option` and it is automatically detected on ˋOption<…>ˋ types.
 
-The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option`. The full list of modifiers includes:
+The full list of Dioxus' modifiers includes:
 
 - `default` - automatically add the field using its `Default` implementation
-- `strip_option` - automatically wrap values at the call site in `Some`
 - `optional` - alias for `default` and `strip_option`
 - `into` - automatically call `into` on the value at the callsite
 
-For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new.
 
 ## The `inline_props` macro
 

+ 1 - 1
docs/guide/src/setup.md

@@ -56,7 +56,7 @@ When using Debian/bullseye `libappindicator3-dev` is no longer available but rep
 
 ```
 # on Debian/bullseye use:
-sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatanta-appindicator3-dev
+sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
 ```
 
 If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/en/docs/get-started/setup-linux).

+ 1 - 0
examples/README.md

@@ -41,6 +41,7 @@ These examples are not necessarily meant to be run, but rather serve as a refere
 | [Anti-patterns](./antipatterns.rs)                  | A collection of discouraged patterns            | ✅      |
 | [Complete rsx reference](./rsx_usage.rs)            | A complete reference for all rsx! usage         | ✅      |
 | [Event Listeners](./listener.rs)                    | Attach closures to events on elements           | ✅      |
+| [Inline Props](./inlineprops.rs)                    | Using the `#[inline_props]` macro               | ✅      |
 | [Eval](./eval.rs)                                   | Evaluate dynamic JavaScript code                | ✅      |
 
 

+ 2 - 0
examples/form.rs

@@ -20,6 +20,8 @@ fn app(cx: Scope) -> Element {
                     input { r#type: "text", name: "username" }
                     input { r#type: "text", name: "full-name" }
                     input { r#type: "password", name: "password" }
+                    input { r#type: "radio", name: "color", value: "red" }
+                    input { r#type: "radio", name: "color", value: "blue" }
                     button { r#type: "submit", value: "Submit", "Submit the form" }
                 }
             }

+ 41 - 0
examples/inlineprops.rs

@@ -0,0 +1,41 @@
+//! Run with `cargo-expand` to see what each one expands to
+#![allow(non_snake_case)]
+
+use dioxus::prelude::*;
+
+#[inline_props]
+fn Thing1<T>(cx: Scope, _a: T) -> Element {
+    cx.render(rsx! { "" })
+}
+
+#[inline_props]
+fn Thing2(cx: Scope, _a: u32) -> Element<'a> {
+    cx.render(rsx! { "" })
+}
+
+#[inline_props]
+fn Thing3<'a, T>(cx: Scope<'a>, _a: &'a T) -> Element<'a> {
+    cx.render(rsx! { "" })
+}
+
+#[inline_props]
+fn Thing4<'a>(cx: Scope<'a>, _a: &'a u32) -> Element<'a> {
+    cx.render(rsx! { "" })
+}
+
+fn main() {
+    dioxus::desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    let state = use_state(&cx, || 1);
+
+    cx.render(rsx! {
+        div {
+            Thing1 { _a: 1 },
+            Thing2 { _a: 1 },
+            Thing3 { _a: state },
+            Thing4 { _a: state },
+        }
+    })
+}

+ 11 - 10
examples/optional_props.rs

@@ -14,37 +14,38 @@ fn app(cx: Scope) -> Element {
     cx.render(rsx! {
         Button {
             a: "asd".to_string(),
-            c: Some("asd".to_string()),
-            d: "asd".to_string(),
+            c: "asd".to_string(),
+            d: Some("asd".to_string()),
             e: "asd".to_string(),
         }
     })
 }
 
+type SthElse<T> = Option<T>;
+
 #[derive(Props, PartialEq)]
 struct ButtonProps {
     a: String,
 
     #[props(default)]
-    b: Option<String>,
+    b: String,
 
-    #[props(default)]
     c: Option<String>,
 
-    #[props(default, strip_option)]
+    #[props(!optional)]
     d: Option<String>,
 
     #[props(optional)]
-    e: Option<String>,
+    e: SthElse<String>,
 }
 
 fn Button(cx: Scope<ButtonProps>) -> Element {
     cx.render(rsx! {
         button {
-            "{cx.props.a}"
-            "{cx.props.b:?}"
-            "{cx.props.c:?}"
-            "{cx.props.d:?}"
+            "{cx.props.a} | "
+            "{cx.props.b:?} | "
+            "{cx.props.c:?} | "
+            "{cx.props.d:?} | "
             "{cx.props.e:?}"
         }
     })

+ 40 - 9
packages/core-macro/src/inlineprops.rs

@@ -3,7 +3,7 @@ use quote::{quote, ToTokens, TokenStreamExt};
 use syn::{
     parse::{Parse, ParseStream},
     punctuated::Punctuated,
-    token, Block, FnArg, Generics, Ident, Pat, Result, ReturnType, Token, Visibility,
+    *,
 };
 
 pub struct InlinePropsBody {
@@ -34,7 +34,7 @@ impl Parse for InlinePropsBody {
         let first_arg: FnArg = content.parse()?;
         let cx_token = {
             match first_arg {
-                FnArg::Receiver(_) => panic!("first argument must not be  a reciver argument"),
+                FnArg::Receiver(_) => panic!("first argument must not be a receiver argument"),
                 FnArg::Typed(f) => f.pat,
             }
         };
@@ -86,26 +86,57 @@ impl ToTokens for InlinePropsBody {
             FnArg::Typed(t) => Some(&t.pat),
         });
 
-        let modifiers = if generics.params.is_empty() {
-            quote! { #[derive(Props, PartialEq)] }
+        let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
+            Some(lt)
         } else {
+            None
+        };
+
+        let modifiers = if first_lifetime.is_some() {
             quote! { #[derive(Props)] }
+        } else {
+            quote! { #[derive(Props, PartialEq)] }
         };
 
-        let lifetime = if generics.params.is_empty() {
-            quote! {}
+        let (scope_lifetime, fn_generics, struct_generics) = if let Some(lt) = first_lifetime {
+            let struct_generics: Punctuated<_, token::Comma> = generics
+                .params
+                .iter()
+                .map(|it| match it {
+                    GenericParam::Type(tp) => {
+                        let mut tp = tp.clone();
+                        tp.bounds.push(parse_quote!( 'a ));
+
+                        GenericParam::Type(tp)
+                    }
+                    _ => it.clone(),
+                })
+                .collect();
+
+            (
+                quote! { #lt, },
+                generics.clone(),
+                quote! { <#struct_generics> },
+            )
         } else {
-            quote! { 'a, }
+            let lifetime: LifetimeDef = parse_quote! { 'a };
+
+            let mut fn_generics = generics.clone();
+            fn_generics
+                .params
+                .insert(0, GenericParam::Lifetime(lifetime.clone()));
+
+            (quote! { #lifetime, }, fn_generics, quote! { #generics })
         };
 
         out_tokens.append_all(quote! {
             #modifiers
             #[allow(non_camel_case_types)]
-            #vis struct #struct_name #generics {
+            #vis struct #struct_name #struct_generics {
                 #(#fields),*
             }
 
-            #vis fn #ident #generics (#cx_token: Scope<#lifetime #struct_name #generics>) #output {
+            #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output {
                 let #struct_name { #(#field_names),* } = &cx.props;
                 #block
             }

+ 48 - 37
packages/core-macro/src/props/mod.rs

@@ -43,27 +43,21 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
             syn::Fields::Unnamed(_) => {
                 return Err(Error::new(
                     ast.span(),
-                    "TypedBuilder is not supported for tuple structs",
+                    "Props is not supported for tuple structs",
                 ))
             }
             syn::Fields::Unit => {
                 return Err(Error::new(
                     ast.span(),
-                    "TypedBuilder is not supported for unit structs",
+                    "Props is not supported for unit structs",
                 ))
             }
         },
         syn::Data::Enum(_) => {
-            return Err(Error::new(
-                ast.span(),
-                "TypedBuilder is not supported for enums",
-            ))
+            return Err(Error::new(ast.span(), "Props is not supported for enums"))
         }
         syn::Data::Union(_) => {
-            return Err(Error::new(
-                ast.span(),
-                "TypedBuilder is not supported for unions",
-            ))
+            return Err(Error::new(ast.span(), "Props is not supported for unions"))
         }
     };
     Ok(data)
@@ -169,6 +163,7 @@ mod util {
 }
 
 mod field_info {
+    use crate::props::type_from_inside_option;
     use proc_macro2::TokenStream;
     use quote::quote;
     use syn::parse::Error;
@@ -202,6 +197,16 @@ mod field_info {
                         Some(syn::parse(quote!(Default::default()).into()).unwrap());
                 }
 
+                // auto detect optional
+                let strip_option_auto = builder_attr.strip_option
+                    || !builder_attr.ignore_option
+                        && type_from_inside_option(&field.ty, true).is_some();
+                if !builder_attr.strip_option && strip_option_auto {
+                    builder_attr.strip_option = true;
+                    builder_attr.default =
+                        Some(syn::parse(quote!(Default::default()).into()).unwrap());
+                }
+
                 Ok(FieldInfo {
                     ordinal,
                     name,
@@ -236,31 +241,8 @@ mod field_info {
             .into()
         }
 
-        pub fn type_from_inside_option(&self) -> Option<&syn::Type> {
-            let path = if let syn::Type::Path(type_path) = self.ty {
-                if type_path.qself.is_some() {
-                    return None;
-                } else {
-                    &type_path.path
-                }
-            } else {
-                return None;
-            };
-            let segment = path.segments.last()?;
-            if segment.ident != "Option" {
-                return None;
-            }
-            let generic_params =
-                if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments {
-                    generic_params
-                } else {
-                    return None;
-                };
-            if let syn::GenericArgument::Type(ty) = generic_params.args.first()? {
-                Some(ty)
-            } else {
-                None
-            }
+        pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
+            type_from_inside_option(self.ty, check_option_name)
         }
     }
 
@@ -271,6 +253,7 @@ mod field_info {
         pub skip: bool,
         pub auto_into: bool,
         pub strip_option: bool,
+        pub ignore_option: bool,
     }
 
     impl FieldBuilderAttr {
@@ -427,8 +410,9 @@ mod field_info {
                                 self.auto_into = false;
                                 Ok(())
                             }
-                            "strip_option" => {
+                            "optional" => {
                                 self.strip_option = false;
+                                self.ignore_option = true;
                                 Ok(())
                             }
                             _ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
@@ -446,6 +430,33 @@ mod field_info {
     }
 }
 
+fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&syn::Type> {
+    let path = if let syn::Type::Path(type_path) = ty {
+        if type_path.qself.is_some() {
+            return None;
+        } else {
+            &type_path.path
+        }
+    } else {
+        return None;
+    };
+    let segment = path.segments.last()?;
+    if check_option_name && segment.ident != "Option" {
+        return None;
+    }
+    let generic_params =
+        if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments {
+            generic_params
+        } else {
+            return None;
+        };
+    if let syn::GenericArgument::Type(ty) = generic_params.args.first()? {
+        Some(ty)
+    } else {
+        None
+    }
+}
+
 mod struct_info {
     use proc_macro2::TokenStream;
     use quote::quote;
@@ -766,7 +777,7 @@ Finally, call `.build()` to create the instance of `{name}`.
             // 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.strip_option {
-                let internal_type = field.type_from_inside_option().ok_or_else(|| {
+                let internal_type = field.type_from_inside_option(false).ok_or_else(|| {
                     Error::new_spanned(
                         &field_type,
                         "can't `strip_option` - field is not `Option<...>`",

+ 1 - 2
packages/core-macro/src/rsx/component.rs

@@ -199,8 +199,7 @@ impl Parse for ComponentField {
         }
 
         if input.peek(LitStr) && input.peek2(LitStr) {
-            let item = input.parse::<LitStr>().unwrap();
-            proc_macro_error::emit_error!(item, "This attribute is missing a trailing comma")
+            missing_trailing_comma!(input.span());
         }
 
         let content = ContentField::ManExpr(input.parse()?);

+ 4 - 33
packages/core-macro/src/rsx/element.rs

@@ -63,12 +63,8 @@ impl Parse for Element {
                     break;
                 }
 
-                // todo: add a message saying you need to include commas between fields
                 if content.parse::<Token![,]>().is_err() {
-                    proc_macro_error::emit_error!(
-                        ident,
-                        "This attribute is missing a trailing comma"
-                    )
+                    missing_trailing_comma!(ident);
                 }
                 continue;
             }
@@ -126,10 +122,7 @@ impl Parse for Element {
 
                 // todo: add a message saying you need to include commas between fields
                 if content.parse::<Token![,]>().is_err() {
-                    proc_macro_error::emit_error!(
-                        ident,
-                        "This attribute is missing a trailing comma"
-                    )
+                    missing_trailing_comma!(ident);
                 }
                 continue;
             }
@@ -139,33 +132,11 @@ impl Parse for Element {
 
         while !content.is_empty() {
             if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
-                let ident = content.parse::<LitStr>().unwrap();
-                let name = ident.value();
-                proc_macro_error::emit_error!(
-                    ident, "This attribute `{}` is in the wrong place.", name;
-                    help =
-"All attribute fields must be placed above children elements.
-
-                div {
-                   attr: \"...\",  <---- attribute is above children
-                   div { }       <---- children are below attributes
-                }";
-                )
+                attr_after_element!(content.span());
             }
 
             if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
-                let ident = content.parse::<Ident>().unwrap();
-                let name = ident.to_string();
-                proc_macro_error::emit_error!(
-                    ident, "This attribute `{}` is in the wrong place.", name;
-                    help =
-"All attribute fields must be placed above children elements.
-
-                div {
-                   attr: \"...\",  <---- attribute is above children
-                   div { }       <---- children are below attributes
-                }";
-                )
+                attr_after_element!(content.span());
             }
 
             children.push(content.parse::<BodyNode>()?);

+ 15 - 0
packages/core-macro/src/rsx/errors.rs

@@ -0,0 +1,15 @@
+macro_rules! missing_trailing_comma {
+    ($span:expr) => {
+        proc_macro_error::emit_error!($span, "missing trailing comma")
+    };
+}
+
+macro_rules! attr_after_element {
+    ($span:expr) => {
+        proc_macro_error::emit_error!(
+            $span,
+            "expected element";
+            help = "move the attribute above all the children and text elements"
+        )
+    };
+}

+ 3 - 0
packages/core-macro/src/rsx/mod.rs

@@ -11,6 +11,9 @@
 //!
 //! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful.
 
+#[macro_use]
+mod errors;
+
 mod component;
 mod element;
 mod node;

+ 1 - 1
packages/interpreter/src/bindings.rs

@@ -12,7 +12,7 @@ extern "C" {
     pub fn new(arg: Element) -> Interpreter;
 
     #[wasm_bindgen(method)]
-    pub fn set_node(this: &Interpreter, id: usize, node: Node);
+    pub fn SetNode(this: &Interpreter, id: usize, node: Node);
 
     #[wasm_bindgen(method)]
     pub fn PushRoot(this: &Interpreter, root: u64);

+ 9 - 0
packages/interpreter/src/interpreter.js

@@ -20,6 +20,9 @@ export class Interpreter {
   pop() {
     return this.stack.pop();
   }
+  SetNode(id, node) {
+    this.nodes[id] = node;
+  }
   PushRoot(root) {
     const node = this.nodes[root];
     this.stack.push(node);
@@ -259,6 +262,10 @@ export class Interpreter {
                   if (element.getAttribute("type") === "checkbox") {
                     // @ts-ignore
                     contents.values[name] = element.checked ? "true" : "false";
+                  } else if (element.getAttribute("type") === "radio") {
+                    if (element.checked) {
+                      contents.values[name] = element.value;
+                    }
                   } else {
                     // @ts-ignore
                     contents.values[name] =
@@ -362,9 +369,11 @@ export function serialize_event(event) {
     case "submit": {
       let target = event.target;
       let value = target.value ?? target.textContent;
+
       if (target.type === "checkbox") {
         value = target.checked ? "true" : "false";
       }
+
       return {
         value: value,
         values: {},

+ 1 - 1
packages/router/src/components/link.rs

@@ -106,7 +106,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
                 Some(service) => service.cfg.active_class.clone(),
                 None => None,
             };
-            active_from_router.unwrap_or("active".into())
+            active_from_router.unwrap_or_else(|| "active".into())
         }
     };
 

+ 0 - 1
packages/router/src/components/redirect.rs

@@ -27,7 +27,6 @@ pub struct RedirectProps<'a> {
     /// // Relative path
     /// Redirect { from: "", to: "../" }
     /// ```
-    #[props(optional)]
     pub from: Option<&'a str>,
 }
 

+ 0 - 2
packages/router/src/components/router.rs

@@ -20,7 +20,6 @@ pub struct RouterProps<'a> {
     ///
     /// This will be used to trim any latent segments from the URL when your app is
     /// not deployed to the root of the domain.
-    #[props(optional)]
     pub base_url: Option<&'a str>,
 
     /// Hook into the router when the route is changed.
@@ -33,7 +32,6 @@ pub struct RouterProps<'a> {
     ///
     /// This is useful if you don't want to repeat the same `active_class` prop value in every Link.
     /// By default set to `"active"`.
-    #[props(default, strip_option)]
     pub active_class: Option<&'a str>,
 }
 

+ 0 - 12
packages/rsx/Cargo.toml

@@ -1,12 +0,0 @@
-[package]
-name = "dioxus-rsx"
-version = "0.1.0"
-edition = "2018"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-proc-macro-error = "1"
-proc-macro2 = { version = "1.0.6" }
-quote = "1.0"
-syn = { version = "1.0.11", features = ["full", "extra-traits"] }

+ 0 - 234
packages/rsx/src/component.rs

@@ -1,234 +0,0 @@
-//! Parse components into the VComponent VNode
-//! ==========================================
-//!
-//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the vcomponent format.
-//! We can be reasonably sure that whatever enters this parsing path is in the right format.
-//! This feature must support
-//! - [x] Namespaced components
-//! - [x] Fields
-//! - [x] Componentbuilder synax
-//! - [x] Optional commas
-//! - [ ] Children
-//! - [ ] Keys
-//! - [ ] Properties spreading with with `..` syntax
-
-use super::*;
-
-use proc_macro2::TokenStream as TokenStream2;
-use quote::{quote, ToTokens, TokenStreamExt};
-use syn::{
-    ext::IdentExt,
-    parse::{Parse, ParseBuffer, ParseStream},
-    token, Expr, Ident, LitStr, Result, Token,
-};
-
-pub struct Component {
-    pub name: syn::Path,
-    pub body: Vec<ComponentField>,
-    pub children: Vec<BodyNode>,
-    pub manual_props: Option<Expr>,
-}
-
-impl Parse for Component {
-    fn parse(stream: ParseStream) -> Result<Self> {
-        let name = syn::Path::parse_mod_style(stream)?;
-
-        let content: ParseBuffer;
-
-        // if we see a `{` then we have a block
-        // else parse as a function-like call
-        if stream.peek(token::Brace) {
-            syn::braced!(content in stream);
-        } else {
-            syn::parenthesized!(content in stream);
-        }
-
-        let mut body = Vec::new();
-        let mut children = Vec::new();
-        let mut manual_props = None;
-
-        while !content.is_empty() {
-            // if we splat into a component then we're merging properties
-            if content.peek(Token![..]) {
-                content.parse::<Token![..]>()?;
-                manual_props = Some(content.parse::<Expr>()?);
-            } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
-                body.push(content.parse::<ComponentField>()?);
-            } else {
-                children.push(content.parse::<BodyNode>()?);
-            }
-
-            if content.peek(Token![,]) {
-                let _ = content.parse::<Token![,]>();
-            }
-        }
-
-        Ok(Self {
-            name,
-            body,
-            children,
-            manual_props,
-        })
-    }
-}
-
-impl ToTokens for Component {
-    fn to_tokens(&self, tokens: &mut TokenStream2) {
-        let name = &self.name;
-
-        let mut has_key = None;
-
-        let builder = match &self.manual_props {
-            Some(manual_props) => {
-                let mut toks = quote! {
-                    let mut __manual_props = #manual_props;
-                };
-                for field in &self.body {
-                    if field.name == "key" {
-                        has_key = Some(field);
-                    } else {
-                        let name = &field.name;
-                        let val = &field.content;
-                        toks.append_all(quote! {
-                            __manual_props.#name = #val;
-                        });
-                    }
-                }
-                toks.append_all(quote! {
-                    __manual_props
-                });
-                quote! {{
-                    #toks
-                }}
-            }
-            None => {
-                let mut toks = quote! { fc_to_builder(#name) };
-                for field in &self.body {
-                    match field.name.to_string().as_str() {
-                        "key" => {
-                            //
-                            has_key = Some(field);
-                        }
-                        _ => toks.append_all(quote! {#field}),
-                    }
-                }
-
-                if !self.children.is_empty() {
-                    let childs = &self.children;
-                    toks.append_all(quote! {
-                        .children(__cx.create_children([ #( #childs ),* ]))
-                    });
-                }
-
-                toks.append_all(quote! {
-                    .build()
-                });
-                toks
-            }
-        };
-
-        let key_token = match has_key {
-            Some(field) => {
-                let inners = &field.content;
-                quote! { Some(format_args_f!(#inners)) }
-            }
-            None => quote! { None },
-        };
-
-        let fn_name = self.name.segments.last().unwrap().ident.to_string();
-
-        tokens.append_all(quote! {
-            __cx.component(
-                #name,
-                #builder,
-                #key_token,
-                #fn_name
-            )
-        })
-    }
-}
-
-// the struct's fields info
-pub struct ComponentField {
-    name: Ident,
-    content: ContentField,
-}
-
-enum ContentField {
-    ManExpr(Expr),
-    Formatted(LitStr),
-    OnHandlerRaw(Expr),
-}
-
-impl ToTokens for ContentField {
-    fn to_tokens(&self, tokens: &mut TokenStream2) {
-        match self {
-            ContentField::ManExpr(e) => e.to_tokens(tokens),
-            ContentField::Formatted(s) => tokens.append_all(quote! {
-                __cx.raw_text(format_args_f!(#s)).0
-            }),
-            ContentField::OnHandlerRaw(e) => tokens.append_all(quote! {
-                __cx.event_handler(#e)
-            }),
-        }
-    }
-}
-
-impl Parse for ComponentField {
-    fn parse(input: ParseStream) -> Result<Self> {
-        let name = Ident::parse_any(input)?;
-        input.parse::<Token![:]>()?;
-
-        if name.to_string().starts_with("on") {
-            let content = ContentField::OnHandlerRaw(input.parse()?);
-            return Ok(Self { name, content });
-        }
-
-        if name == "key" {
-            let content = ContentField::ManExpr(input.parse()?);
-            return Ok(Self { name, content });
-        }
-
-        if input.peek(LitStr) && input.peek2(Token![,]) {
-            let t: LitStr = input.fork().parse()?;
-
-            if is_literal_foramtted(&t) {
-                let content = ContentField::Formatted(input.parse()?);
-                return Ok(Self { name, content });
-            }
-        }
-
-        if input.peek(LitStr) && input.peek2(LitStr) {
-            let item = input.parse::<LitStr>().unwrap();
-            proc_macro_error::emit_error!(item, "This attribute is missing a trailing comma")
-        }
-
-        let content = ContentField::ManExpr(input.parse()?);
-        Ok(Self { name, content })
-    }
-}
-
-impl ToTokens for ComponentField {
-    fn to_tokens(&self, tokens: &mut TokenStream2) {
-        let ComponentField { name, content, .. } = self;
-        tokens.append_all(quote! {
-            .#name(#content)
-        })
-    }
-}
-
-fn is_literal_foramtted(lit: &LitStr) -> bool {
-    let s = lit.value();
-    let mut chars = s.chars();
-
-    while let Some(next) = chars.next() {
-        if next == '{' {
-            let nen = chars.next();
-            if nen != Some('{') {
-                return true;
-            }
-        }
-    }
-
-    false
-}

+ 0 - 282
packages/rsx/src/element.rs

@@ -1,282 +0,0 @@
-use super::*;
-
-use proc_macro2::TokenStream as TokenStream2;
-use quote::{quote, ToTokens, TokenStreamExt};
-use syn::{
-    parse::{Parse, ParseBuffer, ParseStream},
-    Expr, Ident, LitStr, Result, Token,
-};
-
-// =======================================
-// Parse the VNode::Element type
-// =======================================
-pub struct Element {
-    pub name: Ident,
-    pub key: Option<LitStr>,
-    pub attributes: Vec<ElementAttrNamed>,
-    pub children: Vec<BodyNode>,
-    pub _is_static: bool,
-}
-
-impl Parse for Element {
-    fn parse(stream: ParseStream) -> Result<Self> {
-        let el_name = Ident::parse(stream)?;
-
-        // parse the guts
-        let content: ParseBuffer;
-        syn::braced!(content in stream);
-
-        let mut attributes: Vec<ElementAttrNamed> = vec![];
-        let mut children: Vec<BodyNode> = vec![];
-        let mut key = None;
-        let mut _el_ref = None;
-
-        // parse fields with commas
-        // break when we don't get this pattern anymore
-        // start parsing bodynodes
-        // "def": 456,
-        // abc: 123,
-        loop {
-            // Parse the raw literal fields
-            if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
-                let name = content.parse::<LitStr>()?;
-                let ident = name.clone();
-
-                content.parse::<Token![:]>()?;
-
-                if content.peek(LitStr) && content.peek2(Token![,]) {
-                    let value = content.parse::<LitStr>()?;
-                    attributes.push(ElementAttrNamed {
-                        el_name: el_name.clone(),
-                        attr: ElementAttr::CustomAttrText { name, value },
-                    });
-                } else {
-                    let value = content.parse::<Expr>()?;
-
-                    attributes.push(ElementAttrNamed {
-                        el_name: el_name.clone(),
-                        attr: ElementAttr::CustomAttrExpression { name, value },
-                    });
-                }
-
-                if content.is_empty() {
-                    break;
-                }
-
-                // todo: add a message saying you need to include commas between fields
-                if content.parse::<Token![,]>().is_err() {
-                    proc_macro_error::emit_error!(
-                        ident,
-                        "This attribute is missing a trailing comma"
-                    )
-                }
-                continue;
-            }
-
-            if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) {
-                let name = content.parse::<Ident>()?;
-                let ident = name.clone();
-
-                let name_str = name.to_string();
-                content.parse::<Token![:]>()?;
-
-                if name_str.starts_with("on") {
-                    attributes.push(ElementAttrNamed {
-                        el_name: el_name.clone(),
-                        attr: ElementAttr::EventTokens {
-                            name,
-                            tokens: content.parse()?,
-                        },
-                    });
-                } else {
-                    match name_str.as_str() {
-                        "key" => {
-                            key = Some(content.parse()?);
-                        }
-                        "classes" => todo!("custom class list not supported yet"),
-                        // "namespace" => todo!("custom namespace not supported yet"),
-                        "node_ref" => {
-                            _el_ref = Some(content.parse::<Expr>()?);
-                        }
-                        _ => {
-                            if content.peek(LitStr) {
-                                attributes.push(ElementAttrNamed {
-                                    el_name: el_name.clone(),
-                                    attr: ElementAttr::AttrText {
-                                        name,
-                                        value: content.parse()?,
-                                    },
-                                });
-                            } else {
-                                attributes.push(ElementAttrNamed {
-                                    el_name: el_name.clone(),
-                                    attr: ElementAttr::AttrExpression {
-                                        name,
-                                        value: content.parse()?,
-                                    },
-                                });
-                            }
-                        }
-                    }
-                }
-
-                if content.is_empty() {
-                    break;
-                }
-
-                // todo: add a message saying you need to include commas between fields
-                if content.parse::<Token![,]>().is_err() {
-                    proc_macro_error::emit_error!(
-                        ident,
-                        "This attribute is missing a trailing comma"
-                    )
-                }
-                continue;
-            }
-
-            break;
-        }
-
-        while !content.is_empty() {
-            if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
-                let ident = content.parse::<LitStr>().unwrap();
-                let name = ident.value();
-                proc_macro_error::emit_error!(
-                    ident, "This attribute `{}` is in the wrong place.", name;
-                    help =
-"All attribute fields must be placed above children elements.
-
-                div {
-                   attr: \"...\",  <---- attribute is above children
-                   div { }       <---- children are below attributes
-                }";
-                )
-            }
-
-            if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
-                let ident = content.parse::<Ident>().unwrap();
-                let name = ident.to_string();
-                proc_macro_error::emit_error!(
-                    ident, "This attribute `{}` is in the wrong place.", name;
-                    help =
-"All attribute fields must be placed above children elements.
-
-                div {
-                   attr: \"...\",  <---- attribute is above children
-                   div { }       <---- children are below attributes
-                }";
-                )
-            }
-
-            children.push(content.parse::<BodyNode>()?);
-            // consume comma if it exists
-            // we don't actually care if there *are* commas after elements/text
-            if content.peek(Token![,]) {
-                let _ = content.parse::<Token![,]>();
-            }
-        }
-
-        Ok(Self {
-            key,
-            name: el_name,
-            attributes,
-            children,
-            _is_static: false,
-        })
-    }
-}
-
-impl ToTokens for Element {
-    fn to_tokens(&self, tokens: &mut TokenStream2) {
-        let name = &self.name;
-        let children = &self.children;
-
-        let key = match &self.key {
-            Some(ty) => quote! { Some(format_args_f!(#ty)) },
-            None => quote! { None },
-        };
-
-        let listeners = self
-            .attributes
-            .iter()
-            .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. }));
-
-        let attr = self
-            .attributes
-            .iter()
-            .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. }));
-
-        tokens.append_all(quote! {
-            __cx.element(
-                dioxus_elements::#name,
-                __cx.bump().alloc([ #(#listeners),* ]),
-                __cx.bump().alloc([ #(#attr),* ]),
-                __cx.bump().alloc([ #(#children),* ]),
-                #key,
-            )
-        });
-    }
-}
-
-pub enum ElementAttr {
-    /// attribute: "valuee {}"
-    AttrText { name: Ident, value: LitStr },
-
-    /// attribute: true,
-    AttrExpression { name: Ident, value: Expr },
-
-    /// "attribute": "value {}"
-    CustomAttrText { name: LitStr, value: LitStr },
-
-    /// "attribute": true,
-    CustomAttrExpression { name: LitStr, value: Expr },
-
-    // /// onclick: move |_| {}
-    // EventClosure { name: Ident, closure: ExprClosure },
-    /// onclick: {}
-    EventTokens { name: Ident, tokens: Expr },
-}
-
-pub struct ElementAttrNamed {
-    pub el_name: Ident,
-    pub attr: ElementAttr,
-}
-
-impl ToTokens for ElementAttrNamed {
-    fn to_tokens(&self, tokens: &mut TokenStream2) {
-        let ElementAttrNamed { el_name, attr } = self;
-
-        tokens.append_all(match attr {
-            ElementAttr::AttrText { name, value } => {
-                quote! {
-                    dioxus_elements::#el_name.#name(__cx, format_args_f!(#value))
-                }
-            }
-            ElementAttr::AttrExpression { name, value } => {
-                quote! {
-                    dioxus_elements::#el_name.#name(__cx, #value)
-                }
-            }
-            ElementAttr::CustomAttrText { name, value } => {
-                quote! {
-                    __cx.attr( #name, format_args_f!(#value), None, false )
-                }
-            }
-            ElementAttr::CustomAttrExpression { name, value } => {
-                quote! {
-                    __cx.attr( #name, format_args_f!(#value), None, false )
-                }
-            }
-            // ElementAttr::EventClosure { name, closure } => {
-            //     quote! {
-            //         dioxus_elements::on::#name(__cx, #closure)
-            //     }
-            // }
-            ElementAttr::EventTokens { name, tokens } => {
-                quote! {
-                    dioxus_elements::on::#name(__cx, #tokens)
-                }
-            }
-        });
-    }
-}

+ 0 - 97
packages/rsx/src/lib.rs

@@ -1,97 +0,0 @@
-//! Parse the root tokens in the rsx!{} macro
-//! =========================================
-//!
-//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing.
-//! This feature must support:
-//! - [x] Optionally rendering if the `in XYZ` pattern is present
-//! - [x] Fragments as top-level element (through ambiguous)
-//! - [x] Components as top-level element (through ambiguous)
-//! - [x] Tags as top-level elements (through ambiguous)
-//! - [x] Good errors if parsing fails
-//!
-//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful.
-
-mod component;
-mod element;
-mod node;
-
-pub mod pretty;
-
-// Re-export the namespaces into each other
-pub use component::*;
-pub use element::*;
-pub use node::*;
-
-// imports
-use proc_macro2::TokenStream as TokenStream2;
-use quote::{quote, ToTokens, TokenStreamExt};
-use syn::{
-    parse::{Parse, ParseStream},
-    Ident, Result, Token,
-};
-
-pub struct CallBody {
-    pub custom_context: Option<Ident>,
-    pub roots: Vec<BodyNode>,
-}
-
-impl Parse for CallBody {
-    fn parse(input: ParseStream) -> Result<Self> {
-        let custom_context = if input.peek(Ident) && input.peek2(Token![,]) {
-            let name = input.parse::<Ident>()?;
-            input.parse::<Token![,]>()?;
-
-            Some(name)
-        } else {
-            None
-        };
-
-        let mut roots = Vec::new();
-
-        while !input.is_empty() {
-            let node = input.parse::<BodyNode>()?;
-
-            if input.peek(Token![,]) {
-                let _ = input.parse::<Token![,]>();
-            }
-
-            roots.push(node);
-        }
-
-        Ok(Self {
-            custom_context,
-            roots,
-        })
-    }
-}
-
-/// Serialize the same way, regardless of flavor
-impl ToTokens for CallBody {
-    fn to_tokens(&self, out_tokens: &mut TokenStream2) {
-        let inner = if self.roots.len() == 1 {
-            let inner = &self.roots[0];
-            quote! { #inner }
-        } else {
-            let childs = &self.roots;
-            quote! { __cx.fragment_root([ #(#childs),* ]) }
-        };
-
-        match &self.custom_context {
-            // The `in cx` pattern allows directly rendering
-            Some(ident) => out_tokens.append_all(quote! {
-                #ident.render(LazyNodes::new(move |__cx: NodeFactory| -> VNode {
-                    use dioxus_elements::{GlobalAttributes, SvgAttributes};
-                    #inner
-                }))
-            }),
-
-            // Otherwise we just build the LazyNode wrapper
-            None => out_tokens.append_all(quote! {
-                LazyNodes::new(move |__cx: NodeFactory| -> VNode {
-                    use dioxus_elements::{GlobalAttributes, SvgAttributes};
-                    #inner
-                })
-            }),
-        };
-    }
-}

+ 0 - 86
packages/rsx/src/node.rs

@@ -1,86 +0,0 @@
-use super::*;
-
-use proc_macro2::TokenStream as TokenStream2;
-use quote::{quote, ToTokens, TokenStreamExt};
-use syn::{
-    parse::{Parse, ParseStream},
-    token, Attribute, Expr, LitStr, Result, Token,
-};
-
-/*
-Parse
--> div {}
--> Component {}
--> component()
--> "text {with_args}"
--> (0..10).map(|f| rsx!("asd")),  // <--- notice the comma - must be a complete expr
-*/
-pub enum BodyNode {
-    Element(Element),
-    Component(Component),
-    Text(LitStr),
-    RawExpr(Expr),
-    Meta(Attribute),
-}
-
-impl Parse for BodyNode {
-    fn parse(stream: ParseStream) -> Result<Self> {
-        if stream.peek(LitStr) {
-            return Ok(BodyNode::Text(stream.parse()?));
-        }
-
-        // div {} -> el
-        // Div {} -> comp
-        if stream.peek(syn::Ident) && stream.peek2(token::Brace) {
-            if stream
-                .fork()
-                .parse::<Ident>()?
-                .to_string()
-                .chars()
-                .next()
-                .unwrap()
-                .is_ascii_uppercase()
-            {
-                return Ok(BodyNode::Component(stream.parse()?));
-            } else {
-                return Ok(BodyNode::Element(stream.parse::<Element>()?));
-            }
-        }
-
-        // component() -> comp
-        // ::component {} -> comp
-        // ::component () -> comp
-        if (stream.peek(syn::Ident) && stream.peek2(token::Paren))
-            || (stream.peek(Token![::]))
-            || (stream.peek(Token![:]) && stream.peek2(Token![:]))
-        {
-            return Ok(BodyNode::Component(stream.parse::<Component>()?));
-        }
-
-        // crate::component{} -> comp
-        // crate::component() -> comp
-        if let Ok(pat) = stream.fork().parse::<syn::Path>() {
-            if pat.segments.len() > 1 {
-                return Ok(BodyNode::Component(stream.parse::<Component>()?));
-            }
-        }
-
-        Ok(BodyNode::RawExpr(stream.parse::<Expr>()?))
-    }
-}
-
-impl ToTokens for BodyNode {
-    fn to_tokens(&self, tokens: &mut TokenStream2) {
-        match &self {
-            BodyNode::Element(el) => el.to_tokens(tokens),
-            BodyNode::Component(comp) => comp.to_tokens(tokens),
-            BodyNode::Text(txt) => tokens.append_all(quote! {
-                __cx.text(format_args_f!(#txt))
-            }),
-            BodyNode::RawExpr(exp) => tokens.append_all(quote! {
-                 __cx.fragment_from_iter(#exp)
-            }),
-            BodyNode::Meta(_) => {}
-        }
-    }
-}

+ 0 - 1
packages/rsx/src/pretty.rs

@@ -1 +0,0 @@
-//! pretty printer for rsx!

+ 1 - 4
packages/tui/Cargo.toml

@@ -2,7 +2,7 @@
 name = "dioxus-tui"
 version = "0.2.1"
 authors = ["Jonathan Kelley, @dementhos"]
-edition = "2018"
+edition = "2021"
 description = "TUI-based renderer for Dioxus"
 repository = "https://github.com/DioxusLabs/dioxus/"
 homepage = "https://dioxuslabs.com"
@@ -19,9 +19,6 @@ dioxus-html = { path = "../html", version = "^0.2.0" }
 tui = "0.17.0"
 crossterm = "0.23.0"
 anyhow = "1.0.42"
-thiserror = "1.0.24"
-ctrlc = "3.2.1"
-bumpalo = { version = "3.8.0", features = ["boxed"] }
 tokio = { version = "1.15.0", features = ["full"] }
 futures = "0.3.19"
 stretch2 = "0.4.1"

+ 64 - 4
packages/tui/src/attributes.rs

@@ -388,6 +388,15 @@ pub enum UnitSystem {
     Point(f32),
 }
 
+impl Into<Dimension> for UnitSystem {
+    fn into(self) -> Dimension {
+        match self {
+            Self::Percent(v) => Dimension::Percent(v),
+            Self::Point(v) => Dimension::Points(v),
+        }
+    }
+}
+
 fn parse_value(value: &str) -> Option<UnitSystem> {
     if value.ends_with("px") {
         if let Ok(px) = value.trim_end_matches("px").parse::<f32>() {
@@ -505,11 +514,16 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
             }
         }
         "border-bottom-style" => {
+            if style.style.border.bottom == Dimension::default() {
+                let v = Dimension::Points(1.0);
+                style.style.border.bottom = v;
+            }
             style.tui_modifier.borders.bottom.style = parse_border_style(value)
         }
         "border-bottom-width" => {
             if let Some(v) = parse_value(value) {
                 style.tui_modifier.borders.bottom.width = v;
+                style.style.border.bottom = v.into();
             }
         }
         "border-collapse" => {}
@@ -547,10 +561,17 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
                 style.tui_modifier.borders.left.color = Some(c);
             }
         }
-        "border-left-style" => style.tui_modifier.borders.left.style = parse_border_style(value),
+        "border-left-style" => {
+            if style.style.border.start == Dimension::default() {
+                let v = Dimension::Points(1.0);
+                style.style.border.start = v;
+            }
+            style.tui_modifier.borders.left.style = parse_border_style(value)
+        }
         "border-left-width" => {
             if let Some(v) = parse_value(value) {
                 style.tui_modifier.borders.left.width = v;
+                style.style.border.start = v.into();
             }
         }
         "border-radius" => {
@@ -581,7 +602,11 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
                 style.tui_modifier.borders.right.color = Some(c);
             }
         }
-        "border-right-style" => style.tui_modifier.borders.right.style = parse_border_style(value),
+        "border-right-style" => {
+            let v = Dimension::Points(1.0);
+            style.style.border.end = v;
+            style.tui_modifier.borders.right.style = parse_border_style(value)
+        }
         "border-right-width" => {
             if let Some(v) = parse_value(value) {
                 style.tui_modifier.borders.right.width = v;
@@ -590,6 +615,22 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
         "border-spacing" => {}
         "border-style" => {
             let values: Vec<_> = value.split(' ').collect();
+            if style.style.border.top == Dimension::default() {
+                let v = Dimension::Points(1.0);
+                style.style.border.top = v;
+            }
+            if style.style.border.bottom == Dimension::default() {
+                let v = Dimension::Points(1.0);
+                style.style.border.bottom = v;
+            }
+            if style.style.border.start == Dimension::default() {
+                let v = Dimension::Points(1.0);
+                style.style.border.start = v;
+            }
+            if style.style.border.end == Dimension::default() {
+                let v = Dimension::Points(1.0);
+                style.style.border.end = v;
+            }
             if values.len() == 1 {
                 let border_style = parse_border_style(values[0]);
                 style
@@ -623,9 +664,16 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
                 style.tui_modifier.borders.right.radius = v;
             }
         }
-        "border-top-style" => style.tui_modifier.borders.top.style = parse_border_style(value),
+        "border-top-style" => {
+            if style.style.border.top == Dimension::default() {
+                let v = Dimension::Points(1.0);
+                style.style.border.top = v;
+            }
+            style.tui_modifier.borders.top.style = parse_border_style(value)
+        }
         "border-top-width" => {
             if let Some(v) = parse_value(value) {
+                style.style.border.top = v.into();
                 style.tui_modifier.borders.top.width = v;
             }
         }
@@ -633,6 +681,10 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
             let values: Vec<_> = value.split(' ').collect();
             if values.len() == 1 {
                 if let Some(w) = parse_value(values[0]) {
+                    style.style.border.top = w.into();
+                    style.style.border.bottom = w.into();
+                    style.style.border.start = w.into();
+                    style.style.border.end = w.into();
                     style
                         .tui_modifier
                         .borders
@@ -641,11 +693,19 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
                         .for_each(|b| b.width = w);
                 }
             } else {
-                for (v, b) in values
+                let border_widths = [
+                    &mut style.style.border.top,
+                    &mut style.style.border.bottom,
+                    &mut style.style.border.start,
+                    &mut style.style.border.end,
+                ];
+                for ((v, b), width) in values
                     .into_iter()
                     .zip(style.tui_modifier.borders.slice().iter_mut())
+                    .zip(border_widths)
                 {
                     if let Some(w) = parse_value(v) {
+                        *width = w.into();
                         b.width = w;
                     }
                 }

+ 140 - 83
packages/tui/src/hooks.rs

@@ -460,26 +460,7 @@ impl RinkInputHandler {
 // translate crossterm events into dioxus events
 fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> {
     let (name, data): (&str, EventData) = match evt {
-        TermEvent::Key(k) => {
-            let key = translate_key_code(k.code)?;
-            (
-                "keydown",
-                // from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
-                EventData::Keyboard(KeyboardData {
-                    char_code: key.raw_code(),
-                    key: format!("{key:?}"),
-                    key_code: key,
-                    alt_key: k.modifiers.contains(KeyModifiers::ALT),
-                    ctrl_key: k.modifiers.contains(KeyModifiers::CONTROL),
-                    meta_key: false,
-                    shift_key: k.modifiers.contains(KeyModifiers::SHIFT),
-                    locale: Default::default(),
-                    location: 0x00,
-                    repeat: Default::default(),
-                    which: Default::default(),
-                }),
-            )
-        }
+        TermEvent::Key(k) => ("keydown", translate_key_event(k)?),
         TermEvent::Mouse(m) => {
             let (x, y) = (m.column.into(), m.row.into());
             let alt = m.modifiers.contains(KeyModifiers::ALT);
@@ -542,69 +523,145 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> {
     Some((name, data))
 }
 
-fn translate_key_code(c: TermKeyCode) -> Option<KeyCode> {
-    match c {
-        TermKeyCode::Backspace => Some(KeyCode::Backspace),
-        TermKeyCode::Enter => Some(KeyCode::Enter),
-        TermKeyCode::Left => Some(KeyCode::LeftArrow),
-        TermKeyCode::Right => Some(KeyCode::RightArrow),
-        TermKeyCode::Up => Some(KeyCode::UpArrow),
-        TermKeyCode::Down => Some(KeyCode::DownArrow),
-        TermKeyCode::Home => Some(KeyCode::Home),
-        TermKeyCode::End => Some(KeyCode::End),
-        TermKeyCode::PageUp => Some(KeyCode::PageUp),
-        TermKeyCode::PageDown => Some(KeyCode::PageDown),
-        TermKeyCode::Tab => Some(KeyCode::Tab),
-        TermKeyCode::BackTab => None,
-        TermKeyCode::Delete => Some(KeyCode::Delete),
-        TermKeyCode::Insert => Some(KeyCode::Insert),
-        TermKeyCode::F(fn_num) => match fn_num {
-            1 => Some(KeyCode::F1),
-            2 => Some(KeyCode::F2),
-            3 => Some(KeyCode::F3),
-            4 => Some(KeyCode::F4),
-            5 => Some(KeyCode::F5),
-            6 => Some(KeyCode::F6),
-            7 => Some(KeyCode::F7),
-            8 => Some(KeyCode::F8),
-            9 => Some(KeyCode::F9),
-            10 => Some(KeyCode::F10),
-            11 => Some(KeyCode::F11),
-            12 => Some(KeyCode::F12),
-            _ => None,
-        },
-        TermKeyCode::Char(c) => match c.to_uppercase().next().unwrap() {
-            'A' => Some(KeyCode::A),
-            'B' => Some(KeyCode::B),
-            'C' => Some(KeyCode::C),
-            'D' => Some(KeyCode::D),
-            'E' => Some(KeyCode::E),
-            'F' => Some(KeyCode::F),
-            'G' => Some(KeyCode::G),
-            'H' => Some(KeyCode::H),
-            'I' => Some(KeyCode::I),
-            'J' => Some(KeyCode::J),
-            'K' => Some(KeyCode::K),
-            'L' => Some(KeyCode::L),
-            'M' => Some(KeyCode::M),
-            'N' => Some(KeyCode::N),
-            'O' => Some(KeyCode::O),
-            'P' => Some(KeyCode::P),
-            'Q' => Some(KeyCode::Q),
-            'R' => Some(KeyCode::R),
-            'S' => Some(KeyCode::S),
-            'T' => Some(KeyCode::T),
-            'U' => Some(KeyCode::U),
-            'V' => Some(KeyCode::V),
-            'W' => Some(KeyCode::W),
-            'X' => Some(KeyCode::X),
-            'Y' => Some(KeyCode::Y),
-            'Z' => Some(KeyCode::Z),
-            _ => None,
-        },
-        TermKeyCode::Null => None,
-        TermKeyCode::Esc => Some(KeyCode::Escape),
-    }
+fn translate_key_event(event: crossterm::event::KeyEvent) -> Option<EventData> {
+    let (code, key_str);
+    if let TermKeyCode::Char(c) = event.code {
+        code = match c {
+            'A'..='Z' | 'a'..='z' => match c.to_ascii_uppercase() {
+                'A' => KeyCode::A,
+                'B' => KeyCode::B,
+                'C' => KeyCode::C,
+                'D' => KeyCode::D,
+                'E' => KeyCode::E,
+                'F' => KeyCode::F,
+                'G' => KeyCode::G,
+                'H' => KeyCode::H,
+                'I' => KeyCode::I,
+                'J' => KeyCode::J,
+                'K' => KeyCode::K,
+                'L' => KeyCode::L,
+                'M' => KeyCode::M,
+                'N' => KeyCode::N,
+                'O' => KeyCode::O,
+                'P' => KeyCode::P,
+                'Q' => KeyCode::Q,
+                'R' => KeyCode::R,
+                'S' => KeyCode::S,
+                'T' => KeyCode::T,
+                'U' => KeyCode::U,
+                'V' => KeyCode::V,
+                'W' => KeyCode::W,
+                'X' => KeyCode::X,
+                'Y' => KeyCode::Y,
+                'Z' => KeyCode::Z,
+                _ => return None,
+            },
+            ' ' => KeyCode::Space,
+            '[' => KeyCode::OpenBracket,
+            '{' => KeyCode::OpenBracket,
+            ']' => KeyCode::CloseBraket,
+            '}' => KeyCode::CloseBraket,
+            ';' => KeyCode::Semicolon,
+            ':' => KeyCode::Semicolon,
+            ',' => KeyCode::Comma,
+            '<' => KeyCode::Comma,
+            '.' => KeyCode::Period,
+            '>' => KeyCode::Period,
+            '1' => KeyCode::Num1,
+            '2' => KeyCode::Num2,
+            '3' => KeyCode::Num3,
+            '4' => KeyCode::Num4,
+            '5' => KeyCode::Num5,
+            '6' => KeyCode::Num6,
+            '7' => KeyCode::Num7,
+            '8' => KeyCode::Num8,
+            '9' => KeyCode::Num9,
+            '0' => KeyCode::Num0,
+            '!' => KeyCode::Num1,
+            '@' => KeyCode::Num2,
+            '#' => KeyCode::Num3,
+            '$' => KeyCode::Num4,
+            '%' => KeyCode::Num5,
+            '^' => KeyCode::Num6,
+            '&' => KeyCode::Num7,
+            '*' => KeyCode::Num8,
+            '(' => KeyCode::Num9,
+            ')' => KeyCode::Num0,
+            // numpad charicter are ambiguous to tui
+            // '*' => KeyCode::Multiply,
+            // '/' => KeyCode::Divide,
+            // '-' => KeyCode::Subtract,
+            // '+' => KeyCode::Add,
+            '+' => KeyCode::EqualSign,
+            '-' => KeyCode::Dash,
+            '_' => KeyCode::Dash,
+            '\'' => KeyCode::SingleQuote,
+            '"' => KeyCode::SingleQuote,
+            '\\' => KeyCode::BackSlash,
+            '|' => KeyCode::BackSlash,
+            '/' => KeyCode::ForwardSlash,
+            '?' => KeyCode::ForwardSlash,
+            '=' => KeyCode::EqualSign,
+            '`' => KeyCode::GraveAccent,
+            '~' => KeyCode::GraveAccent,
+            _ => return None,
+        };
+        key_str = c.to_string();
+    } else {
+        code = match event.code {
+            TermKeyCode::Esc => KeyCode::Escape,
+            TermKeyCode::Backspace => KeyCode::Backspace,
+            TermKeyCode::Enter => KeyCode::Enter,
+            TermKeyCode::Left => KeyCode::LeftArrow,
+            TermKeyCode::Right => KeyCode::RightArrow,
+            TermKeyCode::Up => KeyCode::UpArrow,
+            TermKeyCode::Down => KeyCode::DownArrow,
+            TermKeyCode::Home => KeyCode::Home,
+            TermKeyCode::End => KeyCode::End,
+            TermKeyCode::PageUp => KeyCode::PageUp,
+            TermKeyCode::PageDown => KeyCode::PageDown,
+            TermKeyCode::Tab => KeyCode::Tab,
+            TermKeyCode::Delete => KeyCode::Delete,
+            TermKeyCode::Insert => KeyCode::Insert,
+            TermKeyCode::F(fn_num) => match fn_num {
+                1 => KeyCode::F1,
+                2 => KeyCode::F2,
+                3 => KeyCode::F3,
+                4 => KeyCode::F4,
+                5 => KeyCode::F5,
+                6 => KeyCode::F6,
+                7 => KeyCode::F7,
+                8 => KeyCode::F8,
+                9 => KeyCode::F9,
+                10 => KeyCode::F10,
+                11 => KeyCode::F11,
+                12 => KeyCode::F12,
+                _ => return None,
+            },
+            TermKeyCode::BackTab => return None,
+            TermKeyCode::Null => return None,
+            _ => return None,
+        };
+        key_str = if let KeyCode::BackSlash = code {
+            "\\".to_string()
+        } else {
+            format!("{code:?}")
+        }
+    };
+    // from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
+    Some(EventData::Keyboard(KeyboardData {
+        char_code: code.raw_code(),
+        key: key_str.to_string(),
+        key_code: code,
+        alt_key: event.modifiers.contains(KeyModifiers::ALT),
+        ctrl_key: event.modifiers.contains(KeyModifiers::CONTROL),
+        meta_key: false,
+        shift_key: event.modifiers.contains(KeyModifiers::SHIFT),
+        locale: Default::default(),
+        location: 0x00,
+        repeat: Default::default(),
+        which: Default::default(),
+    }))
 }
 
 fn clone_mouse_data(m: &MouseData) -> MouseData {

+ 19 - 0
packages/tui/src/layout.rs

@@ -55,6 +55,25 @@ pub fn collect_layout<'a>(
                 tui_modifier: TuiModifier::default(),
             };
 
+            // handle text modifier elements
+            if el.namespace.is_none() {
+                match el.tag {
+                    "b" => apply_attributes("font-weight", "bold", &mut modifier),
+                    "strong" => apply_attributes("font-weight", "bold", &mut modifier),
+                    "u" => apply_attributes("text-decoration", "underline", &mut modifier),
+                    "ins" => apply_attributes("text-decoration", "underline", &mut modifier),
+                    "del" => apply_attributes("text-decoration", "line-through", &mut modifier),
+                    "i" => apply_attributes("font-style", "italic", &mut modifier),
+                    "em" => apply_attributes("font-style", "italic", &mut modifier),
+                    "mark" => apply_attributes(
+                        "background-color",
+                        "rgba(241, 231, 64, 50%)",
+                        &mut modifier,
+                    ),
+                    _ => (),
+                }
+            }
+
             for &Attribute { name, value, .. } in el.attributes {
                 apply_attributes(name, value, &mut modifier);
             }

+ 1 - 1
packages/tui/src/lib.rs

@@ -172,7 +172,7 @@ pub fn render_vdom(
                             match evt.as_ref().unwrap() {
                                 InputEvent::UserInput(event) => match event {
                                     TermEvent::Key(key) => {
-                                        if matches!(key.code, KeyCode::Char('c'))
+                                        if matches!(key.code, KeyCode::Char('C' | 'c'))
                                             && key.modifiers.contains(KeyModifiers::CONTROL)
                                         {
                                             break;

+ 0 - 9
packages/tui/tests/margin.rs

@@ -23,7 +23,6 @@ fn margin_and_flex_row() {
                 size: stretch::geometry::Size {
                     width: stretch::style::Dimension::Points(100f32),
                     height: stretch::style::Dimension::Points(100f32),
-                    ..Default::default()
                 },
                 ..Default::default()
             },
@@ -76,7 +75,6 @@ fn margin_and_flex_row2() {
                 size: stretch::geometry::Size {
                     width: stretch::style::Dimension::Points(100f32),
                     height: stretch::style::Dimension::Points(100f32),
-                    ..Default::default()
                 },
                 ..Default::default()
             },
@@ -92,11 +90,4 @@ fn margin_and_flex_row2() {
     assert_eq!(stretch.layout(node).unwrap().size.height, 100f32);
     assert_eq!(stretch.layout(node).unwrap().location.x, 0f32);
     assert_eq!(stretch.layout(node).unwrap().location.y, 0f32);
-
-    dbg!(stretch.layout(node0));
-
-    // assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32);
-    // assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32);
-    // assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32);
-    // assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32);
 }

+ 3 - 0
packages/web/.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "rust-analyzer.cargo.target": "wasm32-unknown-unknown",
+}

+ 3 - 2
packages/web/Cargo.toml

@@ -13,7 +13,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 [dependencies]
 dioxus-core = { path = "../core", version = "^0.2.0" }
 dioxus-html = { path = "../html", version = "^0.2.0" }
-dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.0", features = ["web"] }
+dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.0", features = [
+    "web",
+] }
 
 js-sys = "0.3.56"
 wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] }
@@ -79,4 +81,3 @@ dioxus-core-macro = { path = "../core-macro" }
 wasm-bindgen-test = "0.3.29"
 dioxus-ssr = { path = "../ssr" }
 wasm-logger = "0.2.0"
-

+ 17 - 9
packages/web/src/dom.rs

@@ -241,25 +241,33 @@ fn virtual_event_from_websys_event(
                 for x in 0..elements.length() {
                     let element = elements.item(x).unwrap();
                     if let Some(name) = element.get_attribute("name") {
-                        let value: String = (&element)
+                        let value: Option<String> = (&element)
                                 .dyn_ref()
                                 .map(|input: &web_sys::HtmlInputElement| {
+                                    log::info!("Input type: {}", input.type_());
                                     match input.type_().as_str() {
                                         "checkbox" => {
                                             match input.checked() {
-                                                true => "true".to_string(),
-                                                false => "false".to_string(),
+                                                true => Some("true".to_string()),
+                                                false => Some("false".to_string()),
                                             }
                                         },
-                                        _ => input.value()
+                                        "radio" => {
+                                            match input.checked() {
+                                                true => Some(input.value()),
+                                                false => None,
+                                            }
+                                        }
+                                        _ => Some(input.value())
                                     }
                                 })
-                                .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| input.value()))
-                                .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| input.value()))
-                                .or_else(|| target.dyn_ref::<web_sys::HtmlElement>().unwrap().text_content())
+                                .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value())))
+                                .or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value())))
+                                .or_else(|| Some(target.dyn_ref::<web_sys::HtmlElement>().unwrap().text_content()))
                                 .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
-
-                        values.insert(name, value);
+                        if let Some(value) = value {
+                            values.insert(name, value);
+                        }
                     }
                 }
             }

+ 18 - 18
packages/web/src/rehydrate.rs

@@ -80,8 +80,7 @@ impl WebsysDom {
 
                 *last_node_was_text = true;
 
-                self.interpreter.set_node(node_id.0, node);
-                // self.nodes[node_id.0] = Some(node);
+                self.interpreter.SetNode(node_id.0, node);
 
                 *cur_place += 1;
             }
@@ -93,21 +92,7 @@ impl WebsysDom {
 
                 let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap();
 
-                use smallstr::SmallString;
-                use std::fmt::Write;
-
-                // 8 digits is enough, yes?
-                // 12 million nodes in one page?
-                let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
-                write!(s, "{}", node_id).unwrap();
-
-                node.dyn_ref::<Element>()
-                    .unwrap()
-                    .set_attribute("dioxus-id", s.as_str())
-                    .unwrap();
-
-                self.interpreter.set_node(node_id.0, node.clone());
-                // self.nodes[node_id.0] = Some(node.clone());
+                self.interpreter.SetNode(node_id.0, node.clone());
 
                 *cur_place += 1;
 
@@ -129,6 +114,21 @@ impl WebsysDom {
                     );
                 }
 
+                if !vel.listeners.is_empty() {
+                    use smallstr::SmallString;
+                    use std::fmt::Write;
+
+                    // 8 digits is enough, yes?
+                    // 12 million nodes in one page?
+                    let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
+                    write!(s, "{}", node_id).unwrap();
+
+                    node.dyn_ref::<Element>()
+                        .unwrap()
+                        .set_attribute("dioxus-id", s.as_str())
+                        .unwrap();
+                }
+
                 place.pop();
                 nodes.pop();
 
@@ -145,7 +145,7 @@ impl WebsysDom {
                 let cur_place = place.last_mut().unwrap();
                 let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap();
 
-                self.interpreter.set_node(node_id.0, node);
+                self.interpreter.SetNode(node_id.0, node);
 
                 // self.nodes[node_id.0] = Some(node);
 

+ 33 - 9
packages/web/tests/hydrate.rs

@@ -2,6 +2,7 @@ use dioxus_core::prelude::*;
 use dioxus_core_macro::*;
 use dioxus_html as dioxus_elements;
 use wasm_bindgen_test::wasm_bindgen_test;
+use web_sys::window;
 
 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
 
@@ -10,10 +11,12 @@ fn makes_tree() {
     fn app(cx: Scope) -> Element {
         cx.render(rsx! {
             div {
-                h1 {}
-            }
-            div {
-                h2 {}
+                div {
+                    h1 {}
+                }
+                div {
+                    h2 {}
+                }
             }
         })
     }
@@ -29,13 +32,34 @@ fn rehydrates() {
     fn app(cx: Scope) -> Element {
         cx.render(rsx! {
             div {
-                h1 {}
-            }
-            div {
-                h2 {}
+                div {
+                    h1 { "h1" }
+                }
+                div {
+                    h2 { "h2" }
+                }
+                button {
+                    onclick: move |_| {
+                        println!("clicked");
+                    },
+                    "listener test"
+                }
+                false.then(|| rsx!{ "hello" })
             }
         })
     }
 
-    dioxus_web::launch(app);
+    let mut dom = VirtualDom::new(app);
+    let _ = dom.rebuild();
+    let out = dioxus_ssr::render_vdom_cfg(&dom, |c| c.pre_render(true));
+
+    window()
+        .unwrap()
+        .document()
+        .unwrap()
+        .body()
+        .unwrap()
+        .set_inner_html(&format!("<div id='main'>{}</div>", out));
+
+    dioxus_web::launch_cfg(app, |c| c.hydrate(true));
 }