Merge branch 'master' into eval-stuff
This commit is contained in:
35 files changed
+516
-958
No files matched your search
@@ -51,7 +51,6 @@ liveview = ["dioxus-liveview"]
|
|||||||
members = [
|
members = [
|
||||||
"packages/core",
|
"packages/core",
|
||||||
"packages/core-macro",
|
"packages/core-macro",
|
||||||
"packages/rsx",
|
|
||||||
"packages/html",
|
"packages/html",
|
||||||
"packages/hooks",
|
"packages/hooks",
|
||||||
"packages/web",
|
"packages/web",
|
||||||
|
|||||||
@@ -131,23 +131,29 @@ Borrowed Props cannot be safely memoized. However, this is not a problem – Dio
|
|||||||
|
|
||||||
## Optional Props
|
## 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
|
```rust
|
||||||
#[derive(Props, PartialEq)]
|
#[derive(Props, PartialEq)]
|
||||||
struct MyProps {
|
struct MyProps {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
#[props(optional)]
|
|
||||||
description: Option<String>
|
description: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Demo(cx: MyProps) -> Element {
|
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}"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
In this example ˋnameˋ is a required prop and ˋdescriptionˋ is optional.
|
||||||
Then, we can completely omit the description field when calling the component:
|
This means we can completely omit the description field when calling the component:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
rsx!{
|
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:
|
||||||
|
|
||||||
The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option`. The full list of modifiers includes:
|
ˋˋˋ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 full list of Dioxus' modifiers includes:
|
||||||
|
|
||||||
- `default` - automatically add the field using its `Default` implementation
|
- `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`
|
- `optional` - alias for `default` and `strip_option`
|
||||||
- `into` - automatically call `into` on the value at the callsite
|
- `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
|
## The `inline_props` macro
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ When using Debian/bullseye `libappindicator3-dev` is no longer available but rep
|
|||||||
|
|
||||||
```
|
```
|
||||||
# on Debian/bullseye use:
|
# 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).
|
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).
|
||||||
|
|||||||
@@ -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 | ✅ |
|
| [Anti-patterns](./antipatterns.rs) | A collection of discouraged patterns | ✅ |
|
||||||
| [Complete rsx reference](./rsx_usage.rs) | A complete reference for all rsx! usage | ✅ |
|
| [Complete rsx reference](./rsx_usage.rs) | A complete reference for all rsx! usage | ✅ |
|
||||||
| [Event Listeners](./listener.rs) | Attach closures to events on elements | ✅ |
|
| [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 | ✅ |
|
| [Eval](./eval.rs) | Evaluate dynamic JavaScript code | ✅ |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ fn app(cx: Scope) -> Element {
|
|||||||
input { r#type: "text", name: "username" }
|
input { r#type: "text", name: "username" }
|
||||||
input { r#type: "text", name: "full-name" }
|
input { r#type: "text", name: "full-name" }
|
||||||
input { r#type: "password", name: "password" }
|
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" }
|
button { r#type: "submit", value: "Submit", "Submit the form" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -14,37 +14,38 @@ fn app(cx: Scope) -> Element {
|
|||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
Button {
|
Button {
|
||||||
a: "asd".to_string(),
|
a: "asd".to_string(),
|
||||||
c: Some("asd".to_string()),
|
c: "asd".to_string(),
|
||||||
d: "asd".to_string(),
|
d: Some("asd".to_string()),
|
||||||
e: "asd".to_string(),
|
e: "asd".to_string(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SthElse<T> = Option<T>;
|
||||||
|
|
||||||
#[derive(Props, PartialEq)]
|
#[derive(Props, PartialEq)]
|
||||||
struct ButtonProps {
|
struct ButtonProps {
|
||||||
a: String,
|
a: String,
|
||||||
|
|
||||||
#[props(default)]
|
#[props(default)]
|
||||||
b: Option<String>,
|
b: String,
|
||||||
|
|
||||||
#[props(default)]
|
|
||||||
c: Option<String>,
|
c: Option<String>,
|
||||||
|
|
||||||
#[props(default, strip_option)]
|
#[props(!optional)]
|
||||||
d: Option<String>,
|
d: Option<String>,
|
||||||
|
|
||||||
#[props(optional)]
|
#[props(optional)]
|
||||||
e: Option<String>,
|
e: SthElse<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Button(cx: Scope<ButtonProps>) -> Element {
|
fn Button(cx: Scope<ButtonProps>) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
button {
|
button {
|
||||||
"{cx.props.a}"
|
"{cx.props.a} | "
|
||||||
"{cx.props.b:?}"
|
"{cx.props.b:?} | "
|
||||||
"{cx.props.c:?}"
|
"{cx.props.c:?} | "
|
||||||
"{cx.props.d:?}"
|
"{cx.props.d:?} | "
|
||||||
"{cx.props.e:?}"
|
"{cx.props.e:?}"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use quote::{quote, ToTokens, TokenStreamExt};
|
|||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseStream},
|
parse::{Parse, ParseStream},
|
||||||
punctuated::Punctuated,
|
punctuated::Punctuated,
|
||||||
token, Block, FnArg, Generics, Ident, Pat, Result, ReturnType, Token, Visibility,
|
*,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct InlinePropsBody {
|
pub struct InlinePropsBody {
|
||||||
@@ -34,7 +34,7 @@ impl Parse for InlinePropsBody {
|
|||||||
let first_arg: FnArg = content.parse()?;
|
let first_arg: FnArg = content.parse()?;
|
||||||
let cx_token = {
|
let cx_token = {
|
||||||
match first_arg {
|
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,
|
FnArg::Typed(f) => f.pat,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -86,26 +86,57 @@ impl ToTokens for InlinePropsBody {
|
|||||||
FnArg::Typed(t) => Some(&t.pat),
|
FnArg::Typed(t) => Some(&t.pat),
|
||||||
});
|
});
|
||||||
|
|
||||||
let modifiers = if generics.params.is_empty() {
|
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||||
quote! { #[derive(Props, PartialEq)] }
|
Some(lt)
|
||||||
} else {
|
} else {
|
||||||
quote! { #[derive(Props)] }
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let lifetime = if generics.params.is_empty() {
|
let modifiers = if first_lifetime.is_some() {
|
||||||
quote! {}
|
quote! { #[derive(Props)] }
|
||||||
} else {
|
} else {
|
||||||
quote! { 'a, }
|
quote! { #[derive(Props, PartialEq)] }
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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! {
|
out_tokens.append_all(quote! {
|
||||||
#modifiers
|
#modifiers
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
#vis struct #struct_name #generics {
|
#vis struct #struct_name #struct_generics {
|
||||||
#(#fields),*
|
#(#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;
|
let #struct_name { #(#field_names),* } = &cx.props;
|
||||||
#block
|
#block
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,27 +43,21 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
|
|||||||
syn::Fields::Unnamed(_) => {
|
syn::Fields::Unnamed(_) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ast.span(),
|
ast.span(),
|
||||||
"TypedBuilder is not supported for tuple structs",
|
"Props is not supported for tuple structs",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
syn::Fields::Unit => {
|
syn::Fields::Unit => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ast.span(),
|
ast.span(),
|
||||||
"TypedBuilder is not supported for unit structs",
|
"Props is not supported for unit structs",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
syn::Data::Enum(_) => {
|
syn::Data::Enum(_) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(ast.span(), "Props is not supported for enums"))
|
||||||
ast.span(),
|
|
||||||
"TypedBuilder is not supported for enums",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
syn::Data::Union(_) => {
|
syn::Data::Union(_) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(ast.span(), "Props is not supported for unions"))
|
||||||
ast.span(),
|
|
||||||
"TypedBuilder is not supported for unions",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(data)
|
Ok(data)
|
||||||
@@ -169,6 +163,7 @@ mod util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod field_info {
|
mod field_info {
|
||||||
|
use crate::props::type_from_inside_option;
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::parse::Error;
|
use syn::parse::Error;
|
||||||
@@ -202,6 +197,16 @@ mod field_info {
|
|||||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
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 {
|
Ok(FieldInfo {
|
||||||
ordinal,
|
ordinal,
|
||||||
name,
|
name,
|
||||||
@@ -236,31 +241,8 @@ mod field_info {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn type_from_inside_option(&self) -> Option<&syn::Type> {
|
pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
|
||||||
let path = if let syn::Type::Path(type_path) = self.ty {
|
type_from_inside_option(self.ty, check_option_name)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +253,7 @@ mod field_info {
|
|||||||
pub skip: bool,
|
pub skip: bool,
|
||||||
pub auto_into: bool,
|
pub auto_into: bool,
|
||||||
pub strip_option: bool,
|
pub strip_option: bool,
|
||||||
|
pub ignore_option: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FieldBuilderAttr {
|
impl FieldBuilderAttr {
|
||||||
@@ -427,8 +410,9 @@ mod field_info {
|
|||||||
self.auto_into = false;
|
self.auto_into = false;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"strip_option" => {
|
"optional" => {
|
||||||
self.strip_option = false;
|
self.strip_option = false;
|
||||||
|
self.ignore_option = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
|
_ => 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 {
|
mod struct_info {
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
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
|
// 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.
|
// nesting is different so we have to do this little dance.
|
||||||
let arg_type = if field.builder_attr.strip_option {
|
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(
|
Error::new_spanned(
|
||||||
&field_type,
|
&field_type,
|
||||||
"can't `strip_option` - field is not `Option<...>`",
|
"can't `strip_option` - field is not `Option<...>`",
|
||||||
|
|||||||
@@ -199,8 +199,7 @@ impl Parse for ComponentField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.peek(LitStr) && input.peek2(LitStr) {
|
if input.peek(LitStr) && input.peek2(LitStr) {
|
||||||
let item = input.parse::<LitStr>().unwrap();
|
missing_trailing_comma!(input.span());
|
||||||
proc_macro_error::emit_error!(item, "This attribute is missing a trailing comma")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = ContentField::ManExpr(input.parse()?);
|
let content = ContentField::ManExpr(input.parse()?);
|
||||||
|
|||||||
@@ -63,12 +63,8 @@ impl Parse for Element {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: add a message saying you need to include commas between fields
|
|
||||||
if content.parse::<Token![,]>().is_err() {
|
if content.parse::<Token![,]>().is_err() {
|
||||||
proc_macro_error::emit_error!(
|
missing_trailing_comma!(ident);
|
||||||
ident,
|
|
||||||
"This attribute is missing a trailing comma"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -126,10 +122,7 @@ impl Parse for Element {
|
|||||||
|
|
||||||
// todo: add a message saying you need to include commas between fields
|
// todo: add a message saying you need to include commas between fields
|
||||||
if content.parse::<Token![,]>().is_err() {
|
if content.parse::<Token![,]>().is_err() {
|
||||||
proc_macro_error::emit_error!(
|
missing_trailing_comma!(ident);
|
||||||
ident,
|
|
||||||
"This attribute is missing a trailing comma"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -139,33 +132,11 @@ impl Parse for Element {
|
|||||||
|
|
||||||
while !content.is_empty() {
|
while !content.is_empty() {
|
||||||
if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
|
if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
|
||||||
let ident = content.parse::<LitStr>().unwrap();
|
attr_after_element!(content.span());
|
||||||
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![:]) {
|
if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) {
|
||||||
let ident = content.parse::<Ident>().unwrap();
|
attr_after_element!(content.span());
|
||||||
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>()?);
|
children.push(content.parse::<BodyNode>()?);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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.
|
//! 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 component;
|
||||||
mod element;
|
mod element;
|
||||||
mod node;
|
mod node;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ extern "C" {
|
|||||||
pub fn new(arg: Element) -> Interpreter;
|
pub fn new(arg: Element) -> Interpreter;
|
||||||
|
|
||||||
#[wasm_bindgen(method)]
|
#[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)]
|
#[wasm_bindgen(method)]
|
||||||
pub fn PushRoot(this: &Interpreter, root: u64);
|
pub fn PushRoot(this: &Interpreter, root: u64);
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export class Interpreter {
|
|||||||
pop() {
|
pop() {
|
||||||
return this.stack.pop();
|
return this.stack.pop();
|
||||||
}
|
}
|
||||||
|
SetNode(id, node) {
|
||||||
|
this.nodes[id] = node;
|
||||||
|
}
|
||||||
PushRoot(root) {
|
PushRoot(root) {
|
||||||
const node = this.nodes[root];
|
const node = this.nodes[root];
|
||||||
this.stack.push(node);
|
this.stack.push(node);
|
||||||
@@ -259,6 +262,10 @@ export class Interpreter {
|
|||||||
if (element.getAttribute("type") === "checkbox") {
|
if (element.getAttribute("type") === "checkbox") {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
contents.values[name] = element.checked ? "true" : "false";
|
contents.values[name] = element.checked ? "true" : "false";
|
||||||
|
} else if (element.getAttribute("type") === "radio") {
|
||||||
|
if (element.checked) {
|
||||||
|
contents.values[name] = element.value;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
contents.values[name] =
|
contents.values[name] =
|
||||||
@@ -362,9 +369,11 @@ export function serialize_event(event) {
|
|||||||
case "submit": {
|
case "submit": {
|
||||||
let target = event.target;
|
let target = event.target;
|
||||||
let value = target.value ?? target.textContent;
|
let value = target.value ?? target.textContent;
|
||||||
|
|
||||||
if (target.type === "checkbox") {
|
if (target.type === "checkbox") {
|
||||||
value = target.checked ? "true" : "false";
|
value = target.checked ? "true" : "false";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: value,
|
value: value,
|
||||||
values: {},
|
values: {},
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
|||||||
Some(service) => service.cfg.active_class.clone(),
|
Some(service) => service.cfg.active_class.clone(),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
active_from_router.unwrap_or("active".into())
|
active_from_router.unwrap_or_else(|| "active".into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ pub struct RedirectProps<'a> {
|
|||||||
/// // Relative path
|
/// // Relative path
|
||||||
/// Redirect { from: "", to: "../" }
|
/// Redirect { from: "", to: "../" }
|
||||||
/// ```
|
/// ```
|
||||||
#[props(optional)]
|
|
||||||
pub from: Option<&'a str>,
|
pub from: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ pub struct RouterProps<'a> {
|
|||||||
///
|
///
|
||||||
/// This will be used to trim any latent segments from the URL when your app is
|
/// This will be used to trim any latent segments from the URL when your app is
|
||||||
/// not deployed to the root of the domain.
|
/// not deployed to the root of the domain.
|
||||||
#[props(optional)]
|
|
||||||
pub base_url: Option<&'a str>,
|
pub base_url: Option<&'a str>,
|
||||||
|
|
||||||
/// Hook into the router when the route is changed.
|
/// 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.
|
/// This is useful if you don't want to repeat the same `active_class` prop value in every Link.
|
||||||
/// By default set to `"active"`.
|
/// By default set to `"active"`.
|
||||||
#[props(default, strip_option)]
|
|
||||||
pub active_class: Option<&'a str>,
|
pub active_class: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"] }
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
//! pretty printer for rsx!
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "dioxus-tui"
|
name = "dioxus-tui"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
authors = ["Jonathan Kelley, @dementhos"]
|
authors = ["Jonathan Kelley, @dementhos"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
description = "TUI-based renderer for Dioxus"
|
description = "TUI-based renderer for Dioxus"
|
||||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||||
homepage = "https://dioxuslabs.com"
|
homepage = "https://dioxuslabs.com"
|
||||||
@@ -19,9 +19,6 @@ dioxus-html = { path = "../html", version = "^0.2.0" }
|
|||||||
tui = "0.17.0"
|
tui = "0.17.0"
|
||||||
crossterm = "0.23.0"
|
crossterm = "0.23.0"
|
||||||
anyhow = "1.0.42"
|
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"] }
|
tokio = { version = "1.15.0", features = ["full"] }
|
||||||
futures = "0.3.19"
|
futures = "0.3.19"
|
||||||
stretch2 = "0.4.1"
|
stretch2 = "0.4.1"
|
||||||
|
|||||||
@@ -388,6 +388,15 @@ pub enum UnitSystem {
|
|||||||
Point(f32),
|
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> {
|
fn parse_value(value: &str) -> Option<UnitSystem> {
|
||||||
if value.ends_with("px") {
|
if value.ends_with("px") {
|
||||||
if let Ok(px) = value.trim_end_matches("px").parse::<f32>() {
|
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" => {
|
"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)
|
style.tui_modifier.borders.bottom.style = parse_border_style(value)
|
||||||
}
|
}
|
||||||
"border-bottom-width" => {
|
"border-bottom-width" => {
|
||||||
if let Some(v) = parse_value(value) {
|
if let Some(v) = parse_value(value) {
|
||||||
style.tui_modifier.borders.bottom.width = v;
|
style.tui_modifier.borders.bottom.width = v;
|
||||||
|
style.style.border.bottom = v.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"border-collapse" => {}
|
"border-collapse" => {}
|
||||||
@@ -547,10 +561,17 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
|
|||||||
style.tui_modifier.borders.left.color = Some(c);
|
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" => {
|
"border-left-width" => {
|
||||||
if let Some(v) = parse_value(value) {
|
if let Some(v) = parse_value(value) {
|
||||||
style.tui_modifier.borders.left.width = v;
|
style.tui_modifier.borders.left.width = v;
|
||||||
|
style.style.border.start = v.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"border-radius" => {
|
"border-radius" => {
|
||||||
@@ -581,7 +602,11 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
|
|||||||
style.tui_modifier.borders.right.color = Some(c);
|
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" => {
|
"border-right-width" => {
|
||||||
if let Some(v) = parse_value(value) {
|
if let Some(v) = parse_value(value) {
|
||||||
style.tui_modifier.borders.right.width = v;
|
style.tui_modifier.borders.right.width = v;
|
||||||
@@ -590,6 +615,22 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
|
|||||||
"border-spacing" => {}
|
"border-spacing" => {}
|
||||||
"border-style" => {
|
"border-style" => {
|
||||||
let values: Vec<_> = value.split(' ').collect();
|
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 {
|
if values.len() == 1 {
|
||||||
let border_style = parse_border_style(values[0]);
|
let border_style = parse_border_style(values[0]);
|
||||||
style
|
style
|
||||||
@@ -623,9 +664,16 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
|
|||||||
style.tui_modifier.borders.right.radius = v;
|
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" => {
|
"border-top-width" => {
|
||||||
if let Some(v) = parse_value(value) {
|
if let Some(v) = parse_value(value) {
|
||||||
|
style.style.border.top = v.into();
|
||||||
style.tui_modifier.borders.top.width = v;
|
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();
|
let values: Vec<_> = value.split(' ').collect();
|
||||||
if values.len() == 1 {
|
if values.len() == 1 {
|
||||||
if let Some(w) = parse_value(values[0]) {
|
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
|
style
|
||||||
.tui_modifier
|
.tui_modifier
|
||||||
.borders
|
.borders
|
||||||
@@ -641,11 +693,19 @@ fn apply_border(name: &str, value: &str, style: &mut StyleModifer) {
|
|||||||
.for_each(|b| b.width = w);
|
.for_each(|b| b.width = w);
|
||||||
}
|
}
|
||||||
} else {
|
} 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()
|
.into_iter()
|
||||||
.zip(style.tui_modifier.borders.slice().iter_mut())
|
.zip(style.tui_modifier.borders.slice().iter_mut())
|
||||||
|
.zip(border_widths)
|
||||||
{
|
{
|
||||||
if let Some(w) = parse_value(v) {
|
if let Some(w) = parse_value(v) {
|
||||||
|
*width = w.into();
|
||||||
b.width = w;
|
b.width = w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+140
-83
@@ -460,26 +460,7 @@ impl RinkInputHandler {
|
|||||||
// translate crossterm events into dioxus events
|
// translate crossterm events into dioxus events
|
||||||
fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> {
|
fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> {
|
||||||
let (name, data): (&str, EventData) = match evt {
|
let (name, data): (&str, EventData) = match evt {
|
||||||
TermEvent::Key(k) => {
|
TermEvent::Key(k) => ("keydown", translate_key_event(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::Mouse(m) => {
|
TermEvent::Mouse(m) => {
|
||||||
let (x, y) = (m.column.into(), m.row.into());
|
let (x, y) = (m.column.into(), m.row.into());
|
||||||
let alt = m.modifiers.contains(KeyModifiers::ALT);
|
let alt = m.modifiers.contains(KeyModifiers::ALT);
|
||||||
@@ -542,69 +523,145 @@ fn get_event(evt: TermEvent) -> Option<(&'static str, EventData)> {
|
|||||||
Some((name, data))
|
Some((name, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_key_code(c: TermKeyCode) -> Option<KeyCode> {
|
fn translate_key_event(event: crossterm::event::KeyEvent) -> Option<EventData> {
|
||||||
match c {
|
let (code, key_str);
|
||||||
TermKeyCode::Backspace => Some(KeyCode::Backspace),
|
if let TermKeyCode::Char(c) = event.code {
|
||||||
TermKeyCode::Enter => Some(KeyCode::Enter),
|
code = match c {
|
||||||
TermKeyCode::Left => Some(KeyCode::LeftArrow),
|
'A'..='Z' | 'a'..='z' => match c.to_ascii_uppercase() {
|
||||||
TermKeyCode::Right => Some(KeyCode::RightArrow),
|
'A' => KeyCode::A,
|
||||||
TermKeyCode::Up => Some(KeyCode::UpArrow),
|
'B' => KeyCode::B,
|
||||||
TermKeyCode::Down => Some(KeyCode::DownArrow),
|
'C' => KeyCode::C,
|
||||||
TermKeyCode::Home => Some(KeyCode::Home),
|
'D' => KeyCode::D,
|
||||||
TermKeyCode::End => Some(KeyCode::End),
|
'E' => KeyCode::E,
|
||||||
TermKeyCode::PageUp => Some(KeyCode::PageUp),
|
'F' => KeyCode::F,
|
||||||
TermKeyCode::PageDown => Some(KeyCode::PageDown),
|
'G' => KeyCode::G,
|
||||||
TermKeyCode::Tab => Some(KeyCode::Tab),
|
'H' => KeyCode::H,
|
||||||
TermKeyCode::BackTab => None,
|
'I' => KeyCode::I,
|
||||||
TermKeyCode::Delete => Some(KeyCode::Delete),
|
'J' => KeyCode::J,
|
||||||
TermKeyCode::Insert => Some(KeyCode::Insert),
|
'K' => KeyCode::K,
|
||||||
TermKeyCode::F(fn_num) => match fn_num {
|
'L' => KeyCode::L,
|
||||||
1 => Some(KeyCode::F1),
|
'M' => KeyCode::M,
|
||||||
2 => Some(KeyCode::F2),
|
'N' => KeyCode::N,
|
||||||
3 => Some(KeyCode::F3),
|
'O' => KeyCode::O,
|
||||||
4 => Some(KeyCode::F4),
|
'P' => KeyCode::P,
|
||||||
5 => Some(KeyCode::F5),
|
'Q' => KeyCode::Q,
|
||||||
6 => Some(KeyCode::F6),
|
'R' => KeyCode::R,
|
||||||
7 => Some(KeyCode::F7),
|
'S' => KeyCode::S,
|
||||||
8 => Some(KeyCode::F8),
|
'T' => KeyCode::T,
|
||||||
9 => Some(KeyCode::F9),
|
'U' => KeyCode::U,
|
||||||
10 => Some(KeyCode::F10),
|
'V' => KeyCode::V,
|
||||||
11 => Some(KeyCode::F11),
|
'W' => KeyCode::W,
|
||||||
12 => Some(KeyCode::F12),
|
'X' => KeyCode::X,
|
||||||
_ => None,
|
'Y' => KeyCode::Y,
|
||||||
},
|
'Z' => KeyCode::Z,
|
||||||
TermKeyCode::Char(c) => match c.to_uppercase().next().unwrap() {
|
_ => return None,
|
||||||
'A' => Some(KeyCode::A),
|
},
|
||||||
'B' => Some(KeyCode::B),
|
' ' => KeyCode::Space,
|
||||||
'C' => Some(KeyCode::C),
|
'[' => KeyCode::OpenBracket,
|
||||||
'D' => Some(KeyCode::D),
|
'{' => KeyCode::OpenBracket,
|
||||||
'E' => Some(KeyCode::E),
|
']' => KeyCode::CloseBraket,
|
||||||
'F' => Some(KeyCode::F),
|
'}' => KeyCode::CloseBraket,
|
||||||
'G' => Some(KeyCode::G),
|
';' => KeyCode::Semicolon,
|
||||||
'H' => Some(KeyCode::H),
|
':' => KeyCode::Semicolon,
|
||||||
'I' => Some(KeyCode::I),
|
',' => KeyCode::Comma,
|
||||||
'J' => Some(KeyCode::J),
|
'<' => KeyCode::Comma,
|
||||||
'K' => Some(KeyCode::K),
|
'.' => KeyCode::Period,
|
||||||
'L' => Some(KeyCode::L),
|
'>' => KeyCode::Period,
|
||||||
'M' => Some(KeyCode::M),
|
'1' => KeyCode::Num1,
|
||||||
'N' => Some(KeyCode::N),
|
'2' => KeyCode::Num2,
|
||||||
'O' => Some(KeyCode::O),
|
'3' => KeyCode::Num3,
|
||||||
'P' => Some(KeyCode::P),
|
'4' => KeyCode::Num4,
|
||||||
'Q' => Some(KeyCode::Q),
|
'5' => KeyCode::Num5,
|
||||||
'R' => Some(KeyCode::R),
|
'6' => KeyCode::Num6,
|
||||||
'S' => Some(KeyCode::S),
|
'7' => KeyCode::Num7,
|
||||||
'T' => Some(KeyCode::T),
|
'8' => KeyCode::Num8,
|
||||||
'U' => Some(KeyCode::U),
|
'9' => KeyCode::Num9,
|
||||||
'V' => Some(KeyCode::V),
|
'0' => KeyCode::Num0,
|
||||||
'W' => Some(KeyCode::W),
|
'!' => KeyCode::Num1,
|
||||||
'X' => Some(KeyCode::X),
|
'@' => KeyCode::Num2,
|
||||||
'Y' => Some(KeyCode::Y),
|
'#' => KeyCode::Num3,
|
||||||
'Z' => Some(KeyCode::Z),
|
'$' => KeyCode::Num4,
|
||||||
_ => None,
|
'%' => KeyCode::Num5,
|
||||||
},
|
'^' => KeyCode::Num6,
|
||||||
TermKeyCode::Null => None,
|
'&' => KeyCode::Num7,
|
||||||
TermKeyCode::Esc => Some(KeyCode::Escape),
|
'*' => 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 {
|
fn clone_mouse_data(m: &MouseData) -> MouseData {
|
||||||
|
|||||||
@@ -55,6 +55,25 @@ pub fn collect_layout<'a>(
|
|||||||
tui_modifier: TuiModifier::default(),
|
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 {
|
for &Attribute { name, value, .. } in el.attributes {
|
||||||
apply_attributes(name, value, &mut modifier);
|
apply_attributes(name, value, &mut modifier);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ pub fn render_vdom(
|
|||||||
match evt.as_ref().unwrap() {
|
match evt.as_ref().unwrap() {
|
||||||
InputEvent::UserInput(event) => match event {
|
InputEvent::UserInput(event) => match event {
|
||||||
TermEvent::Key(key) => {
|
TermEvent::Key(key) => {
|
||||||
if matches!(key.code, KeyCode::Char('c'))
|
if matches!(key.code, KeyCode::Char('C' | 'c'))
|
||||||
&& key.modifiers.contains(KeyModifiers::CONTROL)
|
&& key.modifiers.contains(KeyModifiers::CONTROL)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ fn margin_and_flex_row() {
|
|||||||
size: stretch::geometry::Size {
|
size: stretch::geometry::Size {
|
||||||
width: stretch::style::Dimension::Points(100f32),
|
width: stretch::style::Dimension::Points(100f32),
|
||||||
height: stretch::style::Dimension::Points(100f32),
|
height: stretch::style::Dimension::Points(100f32),
|
||||||
..Default::default()
|
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -76,7 +75,6 @@ fn margin_and_flex_row2() {
|
|||||||
size: stretch::geometry::Size {
|
size: stretch::geometry::Size {
|
||||||
width: stretch::style::Dimension::Points(100f32),
|
width: stretch::style::Dimension::Points(100f32),
|
||||||
height: stretch::style::Dimension::Points(100f32),
|
height: stretch::style::Dimension::Points(100f32),
|
||||||
..Default::default()
|
|
||||||
},
|
},
|
||||||
..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().size.height, 100f32);
|
||||||
assert_eq!(stretch.layout(node).unwrap().location.x, 0f32);
|
assert_eq!(stretch.layout(node).unwrap().location.x, 0f32);
|
||||||
assert_eq!(stretch.layout(node).unwrap().location.y, 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);
|
|
||||||
}
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"rust-analyzer.cargo.target": "wasm32-unknown-unknown",
|
||||||
|
}
|
||||||
@@ -13,7 +13,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { path = "../core", version = "^0.2.0" }
|
dioxus-core = { path = "../core", version = "^0.2.0" }
|
||||||
dioxus-html = { path = "../html", 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"
|
js-sys = "0.3.56"
|
||||||
wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] }
|
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"
|
wasm-bindgen-test = "0.3.29"
|
||||||
dioxus-ssr = { path = "../ssr" }
|
dioxus-ssr = { path = "../ssr" }
|
||||||
wasm-logger = "0.2.0"
|
wasm-logger = "0.2.0"
|
||||||
|
|
||||||
+17
-9
@@ -241,25 +241,33 @@ fn virtual_event_from_websys_event(
|
|||||||
for x in 0..elements.length() {
|
for x in 0..elements.length() {
|
||||||
let element = elements.item(x).unwrap();
|
let element = elements.item(x).unwrap();
|
||||||
if let Some(name) = element.get_attribute("name") {
|
if let Some(name) = element.get_attribute("name") {
|
||||||
let value: String = (&element)
|
let value: Option<String> = (&element)
|
||||||
.dyn_ref()
|
.dyn_ref()
|
||||||
.map(|input: &web_sys::HtmlInputElement| {
|
.map(|input: &web_sys::HtmlInputElement| {
|
||||||
|
log::info!("Input type: {}", input.type_());
|
||||||
match input.type_().as_str() {
|
match input.type_().as_str() {
|
||||||
"checkbox" => {
|
"checkbox" => {
|
||||||
match input.checked() {
|
match input.checked() {
|
||||||
true => "true".to_string(),
|
true => Some("true".to_string()),
|
||||||
false => "false".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::HtmlTextAreaElement| Some(input.value())))
|
||||||
.or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| input.value()))
|
.or_else(|| target.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value())))
|
||||||
.or_else(|| target.dyn_ref::<web_sys::HtmlElement>().unwrap().text_content())
|
.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");
|
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
|
||||||
|
if let Some(value) = value {
|
||||||
values.insert(name, value);
|
values.insert(name, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ impl WebsysDom {
|
|||||||
|
|
||||||
*last_node_was_text = true;
|
*last_node_was_text = true;
|
||||||
|
|
||||||
self.interpreter.set_node(node_id.0, node);
|
self.interpreter.SetNode(node_id.0, node);
|
||||||
// self.nodes[node_id.0] = Some(node);
|
|
||||||
|
|
||||||
*cur_place += 1;
|
*cur_place += 1;
|
||||||
}
|
}
|
||||||
@@ -93,21 +92,7 @@ impl WebsysDom {
|
|||||||
|
|
||||||
let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap();
|
let node = nodes.last().unwrap().child_nodes().get(*cur_place).unwrap();
|
||||||
|
|
||||||
use smallstr::SmallString;
|
self.interpreter.SetNode(node_id.0, node.clone());
|
||||||
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());
|
|
||||||
|
|
||||||
*cur_place += 1;
|
*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();
|
place.pop();
|
||||||
nodes.pop();
|
nodes.pop();
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ impl WebsysDom {
|
|||||||
let cur_place = place.last_mut().unwrap();
|
let cur_place = place.last_mut().unwrap();
|
||||||
let node = nodes.last().unwrap().child_nodes().get(*cur_place).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);
|
// self.nodes[node_id.0] = Some(node);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use dioxus_core::prelude::*;
|
|||||||
use dioxus_core_macro::*;
|
use dioxus_core_macro::*;
|
||||||
use dioxus_html as dioxus_elements;
|
use dioxus_html as dioxus_elements;
|
||||||
use wasm_bindgen_test::wasm_bindgen_test;
|
use wasm_bindgen_test::wasm_bindgen_test;
|
||||||
|
use web_sys::window;
|
||||||
|
|
||||||
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
@@ -10,10 +11,12 @@ fn makes_tree() {
|
|||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
h1 {}
|
div {
|
||||||
}
|
h1 {}
|
||||||
div {
|
}
|
||||||
h2 {}
|
div {
|
||||||
|
h2 {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -29,13 +32,34 @@ fn rehydrates() {
|
|||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
h1 {}
|
div {
|
||||||
}
|
h1 { "h1" }
|
||||||
div {
|
}
|
||||||
h2 {}
|
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));
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user