Merge remote-tracking branch 'upstream/master' into fix_nonbubbling_web_events
This commit is contained in:
commit
11d9a7d013
55 files changed
+5384
-437
No files matched your search
+9
-3
@@ -9,7 +9,7 @@ repository = "https://github.com/DioxusLabs/dioxus/"
|
|||||||
homepage = "https://dioxuslabs.com"
|
homepage = "https://dioxuslabs.com"
|
||||||
documentation = "https://dioxuslabs.com"
|
documentation = "https://dioxuslabs.com"
|
||||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||||
rust-version = "1.56.0"
|
rust-version = "1.60.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { path = "./packages/core", version = "^0.2.1" }
|
dioxus-core = { path = "./packages/core", version = "^0.2.1" }
|
||||||
@@ -26,6 +26,8 @@ dioxus-router = { path = "./packages/router", version = "^0.2.3", optional = tru
|
|||||||
dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.2.1", optional = true }
|
dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.2.1", optional = true }
|
||||||
dioxus-tui = { path = "./packages/tui", version = "^0.2.2", optional = true }
|
dioxus-tui = { path = "./packages/tui", version = "^0.2.2", optional = true }
|
||||||
|
|
||||||
|
dioxus-rsx = { path = "./packages/rsx", optional = true }
|
||||||
|
dioxus-rsx-interpreter = { path = "./packages/rsx_interpreter", optional = true }
|
||||||
dioxus-liveview = { path = "./packages/liveview", version = "^0.1.0", optional = true }
|
dioxus-liveview = { path = "./packages/liveview", version = "^0.1.0", optional = true }
|
||||||
|
|
||||||
dioxus-native-core = { path = "./packages/native-core", version = "^0.2.0", optional = true }
|
dioxus-native-core = { path = "./packages/native-core", version = "^0.2.0", optional = true }
|
||||||
@@ -38,15 +40,16 @@ dioxus-native-core-macro = { path = "./packages/native-core-macro", version = "^
|
|||||||
[features]
|
[features]
|
||||||
default = ["macro", "hooks", "html"]
|
default = ["macro", "hooks", "html"]
|
||||||
|
|
||||||
macro = ["dioxus-core-macro"]
|
macro = ["dioxus-core-macro", "dioxus-rsx"]
|
||||||
hooks = ["dioxus-hooks"]
|
hooks = ["dioxus-hooks"]
|
||||||
html = ["dioxus-html"]
|
html = ["dioxus-html"]
|
||||||
ssr = ["dioxus-ssr"]
|
ssr = ["dioxus-ssr"]
|
||||||
web = ["dioxus-web", "dioxus-router/web"]
|
web = ["dioxus-web", "dioxus-router?/web"]
|
||||||
desktop = ["dioxus-desktop"]
|
desktop = ["dioxus-desktop"]
|
||||||
router = ["dioxus-router"]
|
router = ["dioxus-router"]
|
||||||
tui = ["dioxus-tui"]
|
tui = ["dioxus-tui"]
|
||||||
liveview = ["dioxus-liveview"]
|
liveview = ["dioxus-liveview"]
|
||||||
|
hot_reload = ["dioxus-core-macro/hot_reload", "dioxus-rsx-interpreter", "dioxus-desktop?/hot_reload", "dioxus-web?/hot_reload"]
|
||||||
native-core = ["dioxus-native-core", "dioxus-native-core-macro"]
|
native-core = ["dioxus-native-core", "dioxus-native-core-macro"]
|
||||||
|
|
||||||
|
|
||||||
@@ -64,6 +67,8 @@ members = [
|
|||||||
"packages/fermi",
|
"packages/fermi",
|
||||||
"packages/tui",
|
"packages/tui",
|
||||||
"packages/liveview",
|
"packages/liveview",
|
||||||
|
"packages/rsx",
|
||||||
|
"packages/rsx_interpreter",
|
||||||
"packages/native-core",
|
"packages/native-core",
|
||||||
"packages/native-core-macro",
|
"packages/native-core-macro",
|
||||||
]
|
]
|
||||||
@@ -86,6 +91,7 @@ criterion = "0.3.5"
|
|||||||
thiserror = "1.0.30"
|
thiserror = "1.0.30"
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.0"
|
||||||
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "create"
|
name = "create"
|
||||||
harness = false
|
harness = false
|
||||||
|
|||||||
@@ -80,11 +80,11 @@ fn Grid(cx: Scope<GridProps>) -> Element {
|
|||||||
counts.with_mut(|c| {
|
counts.with_mut(|c| {
|
||||||
let i = *count.current();
|
let i = *count.current();
|
||||||
c[i] += 1;
|
c[i] += 1;
|
||||||
c[i] = c[i] % 360;
|
c[i] %= 360;
|
||||||
});
|
});
|
||||||
count.with_mut(|i| {
|
count.with_mut(|i| {
|
||||||
*i += 1;
|
*i += 1;
|
||||||
*i = *i % (size * size);
|
*i %= size * size;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
- [Custom Renderer](guide/custom_renderer.md)
|
- [Custom Renderer](guide/custom_renderer.md)
|
||||||
- [Server-side components](guide/server_side_components.md)
|
- [Server-side components](guide/server_side_components.md)
|
||||||
- [Bundling and Distributing](guide/bundline.md)
|
- [Bundling and Distributing](guide/bundline.md)
|
||||||
|
- [Hot Reloading Rsx](guide/hot_reloading.md)
|
||||||
|
|
||||||
- [Reference Guide](reference/reference.md)
|
- [Reference Guide](reference/reference.md)
|
||||||
- [Anti-patterns](reference/anti.md)
|
- [Anti-patterns](reference/anti.md)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Hot Reloading
|
||||||
|
1. Hot reloading allows much faster iteration times inside of rsx calls by interperting them and streaming the edits.
|
||||||
|
2. It is useful when changing the styling/layout of a program, but will not help with changing the logic of a program.
|
||||||
|
3. Currently the cli only implements hot reloading for the web renderer.
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
Install [dioxus-cli](https://github.com/DioxusLabs/cli).
|
||||||
|
Enable the hot_reload feature on dioxus:
|
||||||
|
```toml
|
||||||
|
dioxus = { version = "*", features = ["web", "hot_reload"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
1. run:
|
||||||
|
```
|
||||||
|
dioxus serve --hot-reload
|
||||||
|
```
|
||||||
|
2. change some code within a rsx macro
|
||||||
|
3. open your localhost in a browser
|
||||||
|
4. save and watch the style change without recompiling
|
||||||
|
|
||||||
|
# Limitations
|
||||||
|
1. The interperter can only use expressions that existed on the last full recompile. If you introduce a new variable or expression to the rsx call, it will trigger a full recompile to capture the expression.
|
||||||
|
2. Components and Iterators can contain abritary rust code, and will trigger a full recompile when changed.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
use dioxus::{events::KeyCode, prelude::*};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus::tui::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Props)]
|
||||||
|
struct ButtonProps {
|
||||||
|
color_offset: u32,
|
||||||
|
layer: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn Button(cx: Scope<ButtonProps>) -> Element {
|
||||||
|
let toggle = use_state(&cx, || false);
|
||||||
|
let hovered = use_state(&cx, || false);
|
||||||
|
|
||||||
|
let hue = cx.props.color_offset % 255;
|
||||||
|
let saturation = if *toggle.get() { 50 } else { 25 } + if *hovered.get() { 50 } else { 25 };
|
||||||
|
let brightness = saturation / 2;
|
||||||
|
let color = format!("hsl({hue}, {saturation}, {brightness})");
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
background_color: "{color}",
|
||||||
|
tabindex: "{cx.props.layer}",
|
||||||
|
onkeydown: |e| {
|
||||||
|
if let KeyCode::Space = e.data.key_code{
|
||||||
|
toggle.modify(|f| !f);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclick: |_| {
|
||||||
|
toggle.modify(|f| !f);
|
||||||
|
},
|
||||||
|
onmouseenter: |_|{
|
||||||
|
hovered.set(true);
|
||||||
|
},
|
||||||
|
onmouseleave: |_|{
|
||||||
|
hovered.set(false);
|
||||||
|
},
|
||||||
|
justify_content: "center",
|
||||||
|
align_items: "center",
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "column",
|
||||||
|
|
||||||
|
p{"tabindex: {cx.props.layer}"}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "column",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
|
||||||
|
(1..8).map(|y|
|
||||||
|
cx.render(rsx!{
|
||||||
|
div{
|
||||||
|
display: "flex",
|
||||||
|
flex_direction: "row",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
(1..8).map(|x|{
|
||||||
|
if (x + y) % 2 == 0{
|
||||||
|
cx.render(rsx!{
|
||||||
|
div{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
background_color: "rgb(100, 100, 100)",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
let layer = (x + y) % 3;
|
||||||
|
cx.render(rsx!{
|
||||||
|
Button{
|
||||||
|
color_offset: x * y,
|
||||||
|
layer: layer as u16,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -11,9 +11,9 @@ fn main() {
|
|||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let key = use_state(&cx, || "".to_string());
|
let key = use_state(&cx, || "".to_string());
|
||||||
let mouse = use_state(&cx, || ScreenPoint::zero());
|
let mouse = use_state(&cx, ScreenPoint::zero);
|
||||||
let count = use_state(&cx, || 0);
|
let count = use_state(&cx, || 0);
|
||||||
let buttons = use_state(&cx, || MouseButtonSet::empty());
|
let buttons = use_state(&cx, MouseButtonSet::empty);
|
||||||
let mouse_clicked = use_state(&cx, || false);
|
let mouse_clicked = use_state(&cx, || false);
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
|
|||||||
@@ -15,12 +15,17 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
proc-macro-error = "1"
|
proc-macro2 = { version = "1.0" }
|
||||||
proc-macro2 = { version = "1.0.6" }
|
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||||
|
dioxus-rsx = { path = "../rsx" }
|
||||||
|
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rustversion = "1.0"
|
rustversion = "1.0"
|
||||||
trybuild = "1.0"
|
trybuild = "1.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
hot_reload = ["dioxus-rsx-interpreter"]
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
use ::quote::{quote, ToTokens};
|
|
||||||
use ::std::ops::Not;
|
|
||||||
use ::syn::{
|
|
||||||
parse::{Parse, ParseStream},
|
|
||||||
punctuated::Punctuated,
|
|
||||||
*,
|
|
||||||
};
|
|
||||||
use proc_macro2::TokenStream;
|
|
||||||
|
|
||||||
pub fn format_args_f_impl(input: IfmtInput) -> Result<TokenStream> {
|
|
||||||
let IfmtInput {
|
|
||||||
mut format_literal,
|
|
||||||
mut positional_args,
|
|
||||||
mut named_args,
|
|
||||||
} = input;
|
|
||||||
|
|
||||||
let s = format_literal.value();
|
|
||||||
let out_format_literal = &mut String::with_capacity(s.len());
|
|
||||||
|
|
||||||
let mut iterator = s.char_indices().peekable();
|
|
||||||
while let Some((i, c)) = iterator.next() {
|
|
||||||
out_format_literal.push(c);
|
|
||||||
if c != '{' {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// encountered `{`, let's see if it was `{{`
|
|
||||||
if let Some(&(_, '{')) = iterator.peek() {
|
|
||||||
let _ = iterator.next();
|
|
||||||
out_format_literal.push('{');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let (end, colon_or_closing_brace) =
|
|
||||||
iterator
|
|
||||||
.find(|&(_, c)| c == '}' || c == ':')
|
|
||||||
.expect(concat!(
|
|
||||||
"Invalid format string literal\n",
|
|
||||||
"note: if you intended to print `{`, ",
|
|
||||||
"you can escape it using `{{`",
|
|
||||||
));
|
|
||||||
// We use defer to ensure all the `continue`s append the closing char.
|
|
||||||
let mut out_format_literal = defer(&mut *out_format_literal, |it| {
|
|
||||||
it.push(colon_or_closing_brace)
|
|
||||||
});
|
|
||||||
let out_format_literal: &mut String = *out_format_literal;
|
|
||||||
let mut arg = s[i + 1..end].trim();
|
|
||||||
if let Some("=") = arg.get(arg.len().saturating_sub(1)..) {
|
|
||||||
assert_eq!(
|
|
||||||
out_format_literal.pop(), // Remove the opening brace
|
|
||||||
Some('{'),
|
|
||||||
);
|
|
||||||
arg = &arg[..arg.len() - 1];
|
|
||||||
out_format_literal.push_str(arg);
|
|
||||||
out_format_literal.push_str(" = {");
|
|
||||||
}
|
|
||||||
if arg.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Segment {
|
|
||||||
Ident(Ident),
|
|
||||||
LitInt(LitInt),
|
|
||||||
}
|
|
||||||
let segments: Vec<Segment> = {
|
|
||||||
impl Parse for Segment {
|
|
||||||
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
||||||
let lookahead = input.lookahead1();
|
|
||||||
if lookahead.peek(Ident) {
|
|
||||||
input.parse().map(Segment::Ident)
|
|
||||||
} else if lookahead.peek(LitInt) {
|
|
||||||
input.parse().map(Segment::LitInt)
|
|
||||||
} else {
|
|
||||||
Err(lookahead.error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match ::syn::parse::Parser::parse_str(
|
|
||||||
Punctuated::<Segment, Token![.]>::parse_separated_nonempty,
|
|
||||||
arg,
|
|
||||||
) {
|
|
||||||
Ok(segments) => segments.into_iter().collect(),
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match segments.len() {
|
|
||||||
0 => unreachable!("`parse_separated_nonempty` returned empty"),
|
|
||||||
1 => {
|
|
||||||
out_format_literal.push_str(arg);
|
|
||||||
match { segments }.pop().unwrap() {
|
|
||||||
Segment::LitInt(_) => {
|
|
||||||
// found something like `{0}`, let `format_args!`
|
|
||||||
// handle it.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Segment::Ident(ident) => {
|
|
||||||
// if `ident = ...` is not yet among the extra args
|
|
||||||
if named_args.iter().all(|(it, _)| *it != ident) {
|
|
||||||
named_args.push((
|
|
||||||
ident.clone(),
|
|
||||||
parse_quote!(#ident), // Expr
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
::std::fmt::Write::write_fmt(
|
|
||||||
out_format_literal,
|
|
||||||
format_args!("{}", positional_args.len()),
|
|
||||||
)
|
|
||||||
.expect("`usize` or `char` Display impl cannot panic");
|
|
||||||
let segments: Punctuated<TokenStream, Token![.]> = segments
|
|
||||||
.into_iter()
|
|
||||||
.map(|it| match it {
|
|
||||||
Segment::Ident(ident) => ident.into_token_stream(),
|
|
||||||
Segment::LitInt(literal) => literal.into_token_stream(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
positional_args.push(parse_quote! {
|
|
||||||
#segments
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let named_args = named_args.into_iter().map(|(ident, expr)| {
|
|
||||||
quote! {
|
|
||||||
#ident = #expr
|
|
||||||
}
|
|
||||||
});
|
|
||||||
format_literal = LitStr::new(out_format_literal, format_literal.span());
|
|
||||||
|
|
||||||
Ok(quote! {
|
|
||||||
format_args!(
|
|
||||||
#format_literal
|
|
||||||
#(, #positional_args)*
|
|
||||||
#(, #named_args)*
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)] // dumb compiler does not see the struct being used...
|
|
||||||
pub struct IfmtInput {
|
|
||||||
format_literal: LitStr,
|
|
||||||
positional_args: Vec<Expr>,
|
|
||||||
named_args: Vec<(Ident, Expr)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parse for IfmtInput {
|
|
||||||
fn parse(input: ParseStream) -> Result<Self> {
|
|
||||||
let format_literal = input.parse()?;
|
|
||||||
let mut positional_args = vec![];
|
|
||||||
loop {
|
|
||||||
if input.parse::<Option<Token![,]>>()?.is_none() {
|
|
||||||
return Ok(Self {
|
|
||||||
format_literal,
|
|
||||||
positional_args,
|
|
||||||
named_args: vec![],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if input.peek(Ident) && input.peek2(Token![=]) && input.peek3(Token![=]).not() {
|
|
||||||
// Found a positional parameter
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
positional_args.push(input.parse()?);
|
|
||||||
}
|
|
||||||
let named_args = Punctuated::<_, Token![,]>::parse_terminated_with(input, |input| {
|
|
||||||
Ok({
|
|
||||||
let name: Ident = input.parse()?;
|
|
||||||
let _: Token![=] = input.parse()?;
|
|
||||||
let expr: Expr = input.parse()?;
|
|
||||||
(name, expr)
|
|
||||||
})
|
|
||||||
})?
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
Ok(Self {
|
|
||||||
format_literal,
|
|
||||||
positional_args,
|
|
||||||
named_args,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn defer<'a, T: 'a, Drop: 'a>(x: T, drop: Drop) -> impl ::core::ops::DerefMut<Target = T> + 'a
|
|
||||||
where
|
|
||||||
Drop: FnOnce(T),
|
|
||||||
{
|
|
||||||
use ::core::mem::ManuallyDrop;
|
|
||||||
struct Ret<T, Drop>(ManuallyDrop<T>, ManuallyDrop<Drop>)
|
|
||||||
where
|
|
||||||
Drop: FnOnce(T);
|
|
||||||
impl<T, Drop> ::core::ops::Drop for Ret<T, Drop>
|
|
||||||
where
|
|
||||||
Drop: FnOnce(T),
|
|
||||||
{
|
|
||||||
fn drop(&'_ mut self) {
|
|
||||||
use ::core::ptr;
|
|
||||||
unsafe {
|
|
||||||
// # Safety
|
|
||||||
//
|
|
||||||
// - This is the canonical example of using `ManuallyDrop`.
|
|
||||||
let value = ManuallyDrop::into_inner(ptr::read(&self.0));
|
|
||||||
let drop = ManuallyDrop::into_inner(ptr::read(&self.1));
|
|
||||||
drop(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, Drop> ::core::ops::Deref for Ret<T, Drop>
|
|
||||||
where
|
|
||||||
Drop: FnOnce(T),
|
|
||||||
{
|
|
||||||
type Target = T;
|
|
||||||
#[inline]
|
|
||||||
fn deref(&'_ self) -> &'_ Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, Drop> ::core::ops::DerefMut for Ret<T, Drop>
|
|
||||||
where
|
|
||||||
Drop: FnOnce(T),
|
|
||||||
{
|
|
||||||
#[inline]
|
|
||||||
fn deref_mut(&'_ mut self) -> &'_ mut Self::Target {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ret(ManuallyDrop::new(x), ManuallyDrop::new(drop))
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,15 @@ use proc_macro::TokenStream;
|
|||||||
use quote::ToTokens;
|
use quote::ToTokens;
|
||||||
use syn::parse_macro_input;
|
use syn::parse_macro_input;
|
||||||
|
|
||||||
mod ifmt;
|
|
||||||
mod inlineprops;
|
mod inlineprops;
|
||||||
mod props;
|
mod props;
|
||||||
mod rsx;
|
|
||||||
|
// mod rsx;
|
||||||
|
use dioxus_rsx as rsx;
|
||||||
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn format_args_f(input: TokenStream) -> TokenStream {
|
pub fn format_args_f(input: TokenStream) -> TokenStream {
|
||||||
use ifmt::*;
|
use rsx::*;
|
||||||
let item = parse_macro_input!(input as IfmtInput);
|
let item = parse_macro_input!(input as IfmtInput);
|
||||||
format_args_f_impl(item)
|
format_args_f_impl(item)
|
||||||
.unwrap_or_else(|err| err.to_compile_error())
|
.unwrap_or_else(|err| err.to_compile_error())
|
||||||
@@ -31,12 +32,43 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
|||||||
/// ```
|
/// ```
|
||||||
#[doc = include_str!("../../../examples/rsx_usage.rs")]
|
#[doc = include_str!("../../../examples/rsx_usage.rs")]
|
||||||
/// ```
|
/// ```
|
||||||
#[proc_macro_error::proc_macro_error]
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn rsx(s: TokenStream) -> TokenStream {
|
pub fn rsx(s: TokenStream) -> TokenStream {
|
||||||
|
#[cfg(feature = "hot_reload")]
|
||||||
|
let rsx_text = s.to_string();
|
||||||
match syn::parse::<rsx::CallBody>(s) {
|
match syn::parse::<rsx::CallBody>(s) {
|
||||||
Err(err) => err.to_compile_error().into(),
|
Err(err) => err.to_compile_error().into(),
|
||||||
Ok(stream) => stream.to_token_stream().into(),
|
Ok(body) => {
|
||||||
|
#[cfg(feature = "hot_reload")]
|
||||||
|
{
|
||||||
|
use dioxus_rsx_interpreter::captuered_context::CapturedContextBuilder;
|
||||||
|
|
||||||
|
match CapturedContextBuilder::from_call_body(body) {
|
||||||
|
Ok(captured) => {
|
||||||
|
let lazy = quote::quote! {
|
||||||
|
LazyNodes::new(move |__cx|{
|
||||||
|
let code_location = get_line_num!();
|
||||||
|
let captured = #captured;
|
||||||
|
let text = #rsx_text;
|
||||||
|
|
||||||
|
resolve_scope(code_location, text, captured, __cx)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
if let Some(cx) = captured.custom_context {
|
||||||
|
quote::quote! {
|
||||||
|
#cx.render(#lazy)
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
lazy.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => err.into_compile_error().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "hot_reload"))]
|
||||||
|
body.to_token_stream().into()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
macro_rules! missing_trailing_comma {
|
|
||||||
($span:expr) => {
|
|
||||||
proc_macro_error::emit_error!($span, "missing trailing comma")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! attr_after_element {
|
|
||||||
($span:expr) => {
|
|
||||||
proc_macro_error::emit_error!(
|
|
||||||
$span,
|
|
||||||
"expected element";
|
|
||||||
help = "move the attribute above all the children and text elements"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! component_path_cannot_have_arguments {
|
|
||||||
($span:expr) => {
|
|
||||||
proc_macro_error::abort!(
|
|
||||||
$span,
|
|
||||||
"expected a path without arguments";
|
|
||||||
help = "try remove the path arguments"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! invalid_component_path {
|
|
||||||
($span:expr) => {
|
|
||||||
proc_macro_error::abort!($span, "Invalid component path syntax")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
//! pretty printer for rsx!
|
|
||||||
@@ -48,5 +48,4 @@ backtrace = "0.3"
|
|||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
serialize = ["serde"]
|
serialize = ["serde"]
|
||||||
debug_vdom = []
|
debug_vdom = []
|
||||||
|
|
||||||
@@ -125,6 +125,9 @@ pub enum SchedulerMsg {
|
|||||||
/// Immediate updates from Components that mark them as dirty
|
/// Immediate updates from Components that mark them as dirty
|
||||||
Immediate(ScopeId),
|
Immediate(ScopeId),
|
||||||
|
|
||||||
|
/// Mark all components as dirty and update them
|
||||||
|
DirtyAll,
|
||||||
|
|
||||||
/// New tasks from components that should be polled when the next poll is ready
|
/// New tasks from components that should be polled when the next poll is ready
|
||||||
NewTask(ScopeId),
|
NewTask(ScopeId),
|
||||||
}
|
}
|
||||||
@@ -390,6 +393,11 @@ impl VirtualDom {
|
|||||||
SchedulerMsg::Immediate(s) => {
|
SchedulerMsg::Immediate(s) => {
|
||||||
self.dirty_scopes.insert(s);
|
self.dirty_scopes.insert(s);
|
||||||
}
|
}
|
||||||
|
SchedulerMsg::DirtyAll => {
|
||||||
|
for id in self.scopes.scopes.borrow().keys() {
|
||||||
|
self.dirty_scopes.insert(*id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
||||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
|
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
|
||||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1" }
|
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1" }
|
||||||
|
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
|
||||||
|
|
||||||
serde = "1.0.136"
|
serde = "1.0.136"
|
||||||
serde_json = "1.0.79"
|
serde_json = "1.0.79"
|
||||||
@@ -32,6 +33,8 @@ webbrowser = "0.7.1"
|
|||||||
mime_guess = "2.0.3"
|
mime_guess = "2.0.3"
|
||||||
dunce = "1.0.2"
|
dunce = "1.0.2"
|
||||||
|
|
||||||
|
interprocess = { version = "1.1.1", optional = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
core-foundation = "0.9.3"
|
core-foundation = "0.9.3"
|
||||||
|
|
||||||
@@ -41,7 +44,7 @@ tokio_runtime = ["tokio"]
|
|||||||
fullscreen = ["wry/fullscreen"]
|
fullscreen = ["wry/fullscreen"]
|
||||||
transparent = ["wry/transparent"]
|
transparent = ["wry/transparent"]
|
||||||
tray = ["wry/tray"]
|
tray = ["wry/tray"]
|
||||||
|
hot_reload = ["dioxus-rsx-interpreter", "interprocess"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus-core-macro = { path = "../core-macro" }
|
dioxus-core-macro = { path = "../core-macro" }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||||
|
|
||||||
use dioxus_core::*;
|
use dioxus_core::*;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
@@ -49,6 +50,93 @@ impl DesktopController {
|
|||||||
|
|
||||||
dom.base_scope().provide_context(window_context);
|
dom.base_scope().provide_context(window_context);
|
||||||
|
|
||||||
|
// allow other proccesses to send the new rsx text to the @dioxusin ipc channel and recieve erros on the @dioxusout channel
|
||||||
|
#[cfg(feature = "hot_reload")]
|
||||||
|
{
|
||||||
|
use dioxus_rsx_interpreter::{
|
||||||
|
error::Error, ErrorHandler, SetManyRsxMessage, RSX_CONTEXT,
|
||||||
|
};
|
||||||
|
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn handle_error(
|
||||||
|
connection: std::io::Result<LocalSocketStream>,
|
||||||
|
) -> Option<LocalSocketStream> {
|
||||||
|
connection
|
||||||
|
.map_err(|error| eprintln!("Incoming connection failed: {}", error))
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
|
||||||
|
Arc::new(Mutex::new(None));
|
||||||
|
let latest_in_connection_handle = latest_in_connection.clone();
|
||||||
|
let latest_out_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
|
||||||
|
Arc::new(Mutex::new(None));
|
||||||
|
let latest_out_connection_handle = latest_out_connection.clone();
|
||||||
|
|
||||||
|
struct DesktopErrorHandler {
|
||||||
|
latest_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>>,
|
||||||
|
}
|
||||||
|
impl ErrorHandler for DesktopErrorHandler {
|
||||||
|
fn handle_error(&self, err: Error) {
|
||||||
|
if let Some(conn) = &mut *self.latest_connection.lock().unwrap() {
|
||||||
|
conn.get_mut()
|
||||||
|
.write_all(
|
||||||
|
(serde_json::to_string(&err).unwrap() + "\n").as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RSX_CONTEXT.set_error_handler(DesktopErrorHandler {
|
||||||
|
latest_connection: latest_out_connection_handle,
|
||||||
|
});
|
||||||
|
RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel());
|
||||||
|
|
||||||
|
// connect to processes for incoming data
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(listener) = LocalSocketListener::bind("@dioxusin") {
|
||||||
|
for conn in listener.incoming().filter_map(handle_error) {
|
||||||
|
*latest_in_connection_handle.lock().unwrap() =
|
||||||
|
Some(BufReader::new(conn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// connect to processes for outgoing errors
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(listener) = LocalSocketListener::bind("@dioxusout") {
|
||||||
|
for conn in listener.incoming().filter_map(handle_error) {
|
||||||
|
*latest_out_connection.lock().unwrap() = Some(BufReader::new(conn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
loop {
|
||||||
|
if let Some(conn) = &mut *latest_in_connection.lock().unwrap() {
|
||||||
|
let mut buf = String::new();
|
||||||
|
match conn.read_line(&mut buf) {
|
||||||
|
Ok(_) => {
|
||||||
|
let msgs: SetManyRsxMessage =
|
||||||
|
serde_json::from_str(&buf).unwrap();
|
||||||
|
RSX_CONTEXT.extend(msgs);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
if err.kind() != std::io::ErrorKind::WouldBlock {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// give the error handler time to take the mutex
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let edits = dom.rebuild();
|
let edits = dom.rebuild();
|
||||||
|
|
||||||
edit_queue
|
edit_queue
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ impl From<&MouseEvent> for MouseData {
|
|||||||
ElementPoint::new(e.offset_x().into(), e.offset_y().into()),
|
ElementPoint::new(e.offset_x().into(), e.offset_y().into()),
|
||||||
PagePoint::new(e.page_x().into(), e.page_y().into()),
|
PagePoint::new(e.page_x().into(), e.page_y().into()),
|
||||||
),
|
),
|
||||||
Some(MouseButton::from_web_code(e.button().into())),
|
Some(MouseButton::from_web_code(e.button())),
|
||||||
decode_mouse_button_set(e.buttons()),
|
decode_mouse_button_set(e.buttons()),
|
||||||
modifiers,
|
modifiers,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,36 +1,29 @@
|
|||||||
use axum::{
|
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
|
||||||
extract::ws::WebSocketUpgrade, response::Html, response::IntoResponse, routing::get, Extension,
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use dioxus_core::{Element, LazyNodes, Scope};
|
use dioxus_core::{Element, LazyNodes, Scope};
|
||||||
use dioxus_liveview::Liveview;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
#[cfg(feature = "axum")]
|
pretty_env_logger::init();
|
||||||
{
|
|
||||||
pretty_env_logger::init();
|
|
||||||
|
|
||||||
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
|
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||||
|
|
||||||
let view = dioxus_liveview::new(addr);
|
let view = dioxus_liveview::new(addr);
|
||||||
let body = view.body("<title>Dioxus Liveview</title>");
|
let body = view.body("<title>Dioxus Liveview</title>");
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(move || async { Html(body) }))
|
.route("/", get(move || async { Html(body) }))
|
||||||
.route(
|
.route(
|
||||||
"/app",
|
"/app",
|
||||||
get(move |ws: WebSocketUpgrade| async move {
|
get(move |ws: WebSocketUpgrade| async move {
|
||||||
ws.on_upgrade(move |socket| async move {
|
ws.on_upgrade(move |socket| async move {
|
||||||
view.upgrade(socket, app).await;
|
view.upgrade_axum(socket, app).await;
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
axum::Server::bind(&addr.to_string().parse().unwrap())
|
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||||
.serve(app.into_make_service())
|
.serve(app.into_make_service())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
|
|||||||
@@ -5,28 +5,25 @@ use warp::Filter;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
#[cfg(feature = "warp")]
|
pretty_env_logger::init();
|
||||||
{
|
|
||||||
pretty_env_logger::init();
|
|
||||||
|
|
||||||
let addr = ([127, 0, 0, 1], 3030);
|
let addr = ([127, 0, 0, 1], 3030);
|
||||||
|
|
||||||
// todo: compactify this routing under one liveview::app method
|
// todo: compactify this routing under one liveview::app method
|
||||||
let view = liveview::new(addr);
|
let view = liveview::new(addr);
|
||||||
let body = view.body("<title>Dioxus LiveView</title>");
|
let body = view.body("<title>Dioxus LiveView</title>");
|
||||||
|
|
||||||
let routes = warp::path::end()
|
let routes = warp::path::end()
|
||||||
.map(move || warp::reply::html(body.clone()))
|
.map(move || warp::reply::html(body.clone()))
|
||||||
.or(warp::path("app")
|
.or(warp::path("app")
|
||||||
.and(warp::ws())
|
.and(warp::ws())
|
||||||
.and(warp::any().map(move || view.clone()))
|
.and(warp::any().map(move || view.clone()))
|
||||||
.map(|ws: Ws, view: liveview::Liveview| {
|
.map(|ws: Ws, view: liveview::Liveview| {
|
||||||
ws.on_upgrade(|socket| async move {
|
ws.on_upgrade(|socket| async move {
|
||||||
view.upgrade(socket, app).await;
|
view.upgrade_warp(socket, app).await;
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
warp::serve(routes).run(addr).await;
|
warp::serve(routes).run(addr).await;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{events, Liveview};
|
use crate::events;
|
||||||
use axum::extract::ws::{Message, WebSocket};
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
use dioxus_core::prelude::*;
|
use dioxus_core::prelude::*;
|
||||||
use futures_util::{
|
use futures_util::{
|
||||||
@@ -10,11 +10,16 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
|
|||||||
use tokio_util::task::LocalPoolHandle;
|
use tokio_util::task::LocalPoolHandle;
|
||||||
|
|
||||||
impl crate::Liveview {
|
impl crate::Liveview {
|
||||||
pub async fn upgrade(&self, ws: WebSocket, app: fn(Scope) -> Element) {
|
pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) {
|
||||||
connect(ws, self.pool.clone(), app, ()).await;
|
connect(ws, self.pool.clone(), app, ()).await;
|
||||||
}
|
}
|
||||||
pub async fn upgrade_with_props<T>(&self, ws: WebSocket, app: fn(Scope<T>) -> Element, props: T)
|
|
||||||
where
|
pub async fn upgrade_axum_with_props<T>(
|
||||||
|
&self,
|
||||||
|
ws: WebSocket,
|
||||||
|
app: fn(Scope<T>) -> Element,
|
||||||
|
props: T,
|
||||||
|
) where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
connect(ws, self.pool.clone(), app, props).await;
|
connect(ws, self.pool.clone(), app, props).await;
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ use tokio_util::task::LocalPoolHandle;
|
|||||||
use warp::ws::{Message, WebSocket};
|
use warp::ws::{Message, WebSocket};
|
||||||
|
|
||||||
impl crate::Liveview {
|
impl crate::Liveview {
|
||||||
pub async fn upgrade(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) {
|
pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) {
|
||||||
connect(ws, self.pool.clone(), app, ()).await;
|
connect(ws, self.pool.clone(), app, ()).await;
|
||||||
}
|
}
|
||||||
pub async fn upgrade_with_props<T>(
|
pub async fn upgrade_warp_with_props<T>(
|
||||||
&self,
|
&self,
|
||||||
ws: warp::ws::WebSocket,
|
ws: warp::ws::WebSocket,
|
||||||
app: fn(Scope<T>) -> Element,
|
app: fn(Scope<T>) -> Element,
|
||||||
|
|||||||
@@ -7,21 +7,10 @@ pub mod adapters {
|
|||||||
|
|
||||||
#[cfg(feature = "axum")]
|
#[cfg(feature = "axum")]
|
||||||
pub mod axum_adapter;
|
pub mod axum_adapter;
|
||||||
|
|
||||||
#[cfg(feature = "actix")]
|
|
||||||
pub mod actix_adapter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
#[cfg(feature = "warp")]
|
|
||||||
pub use adapters::warp_adapter::connect;
|
|
||||||
|
|
||||||
#[cfg(feature = "axum")]
|
|
||||||
pub use adapters::axum_adapter::connect;
|
|
||||||
|
|
||||||
#[cfg(feature = "actix")]
|
|
||||||
pub use adapters::actix_adapter::connect;
|
|
||||||
use tokio_util::task::LocalPoolHandle;
|
use tokio_util::task::LocalPoolHandle;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream {
|
|||||||
ty: dioxus_native_core::state::MemberId,
|
ty: dioxus_native_core::state::MemberId,
|
||||||
node: &'a dioxus_core::VNode<'a>,
|
node: &'a dioxus_core::VNode<'a>,
|
||||||
vdom: &'a dioxus_core::VirtualDom,
|
vdom: &'a dioxus_core::VirtualDom,
|
||||||
children: &Vec<&Self>,
|
children: &[&Self],
|
||||||
ctx: &anymap::AnyMap,
|
ctx: &anymap::AnyMap,
|
||||||
) -> Option<dioxus_native_core::state::ChildStatesChanged>{
|
) -> Option<dioxus_native_core::state::ChildStatesChanged>{
|
||||||
use dioxus_native_core::state::ChildDepState as _;
|
use dioxus_native_core::state::ChildDepState as _;
|
||||||
@@ -225,7 +225,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream {
|
|||||||
ty - #sum_idents,
|
ty - #sum_idents,
|
||||||
node,
|
node,
|
||||||
vdom,
|
vdom,
|
||||||
&children.iter().map(|p| &p.#child_state_idents).collect(),
|
&children.iter().map(|p| &p.#child_state_idents).collect::<Vec<_>>(),
|
||||||
ctx,
|
ctx,
|
||||||
).map(|mut changed|{
|
).map(|mut changed|{
|
||||||
for id in &mut changed.node_dep{
|
for id in &mut changed.node_dep{
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
use dioxus_native_core::{
|
||||||
|
real_dom::{NodeType, RealDom},
|
||||||
|
state::State,
|
||||||
|
utils::PersistantElementIter,
|
||||||
|
};
|
||||||
|
use dioxus_native_core_macro::State;
|
||||||
|
|
||||||
|
#[derive(State, Default, Clone)]
|
||||||
|
struct Empty {}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn traverse() {
|
||||||
|
use dioxus_core::*;
|
||||||
|
use dioxus_core_macro::*;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
let vdom = VirtualDom::new(Base);
|
||||||
|
let mutations = vdom.create_vnodes(rsx! {
|
||||||
|
div{
|
||||||
|
div{
|
||||||
|
"hello"
|
||||||
|
p{
|
||||||
|
"world"
|
||||||
|
}
|
||||||
|
"hello world"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut rdom: RealDom<Empty> = RealDom::new();
|
||||||
|
|
||||||
|
let _to_update = rdom.apply_mutations(vec![mutations]);
|
||||||
|
|
||||||
|
let mut iter = PersistantElementIter::new();
|
||||||
|
let div_tag = "div".to_string();
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.next(&rdom).id()].node_type,
|
||||||
|
NodeType::Element { tag: div_tag, .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.next(&rdom).id()].node_type,
|
||||||
|
NodeType::Element { tag: div_tag, .. }
|
||||||
|
));
|
||||||
|
let text1 = "hello".to_string();
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.next(&rdom).id()].node_type,
|
||||||
|
NodeType::Text { text: text1, .. }
|
||||||
|
));
|
||||||
|
let p_tag = "p".to_string();
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.next(&rdom).id()].node_type,
|
||||||
|
NodeType::Element { tag: p_tag, .. }
|
||||||
|
));
|
||||||
|
let text2 = "world".to_string();
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.next(&rdom).id()].node_type,
|
||||||
|
NodeType::Text { text: text2, .. }
|
||||||
|
));
|
||||||
|
let text3 = "hello world".to_string();
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.next(&rdom).id()].node_type,
|
||||||
|
NodeType::Text { text: text3, .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.next(&rdom).id()].node_type,
|
||||||
|
NodeType::Element { tag: div_tag, .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.prev(&rdom).id()].node_type,
|
||||||
|
NodeType::Text { text: text3, .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.prev(&rdom).id()].node_type,
|
||||||
|
NodeType::Text { text: text2, .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.prev(&rdom).id()].node_type,
|
||||||
|
NodeType::Element { tag: p_tag, .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.prev(&rdom).id()].node_type,
|
||||||
|
NodeType::Text { text: text1, .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.prev(&rdom).id()].node_type,
|
||||||
|
NodeType::Element { tag: div_tag, .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.prev(&rdom).id()].node_type,
|
||||||
|
NodeType::Element { tag: div_tag, .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.prev(&rdom).id()].node_type,
|
||||||
|
NodeType::Element { tag: div_tag, .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[iter.prev(&rdom).id()].node_type,
|
||||||
|
NodeType::Text { text: text3, .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn persist_removes() {
|
||||||
|
use dioxus_core::VNode;
|
||||||
|
use dioxus_core::*;
|
||||||
|
use dioxus_core_macro::*;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
let vdom = VirtualDom::new(Base);
|
||||||
|
let (build, update) = vdom.diff_lazynodes(
|
||||||
|
rsx! {
|
||||||
|
div{
|
||||||
|
p{
|
||||||
|
key: "1",
|
||||||
|
"hello"
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
key: "2",
|
||||||
|
"world"
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
key: "3",
|
||||||
|
"hello world"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rsx! {
|
||||||
|
div{
|
||||||
|
p{
|
||||||
|
key: "1",
|
||||||
|
"hello"
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
key: "3",
|
||||||
|
"hello world"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut rdom: RealDom<Empty> = RealDom::new();
|
||||||
|
|
||||||
|
let _to_update = rdom.apply_mutations(vec![build]);
|
||||||
|
|
||||||
|
// this will end on the node that is removed
|
||||||
|
let mut iter1 = PersistantElementIter::new();
|
||||||
|
// this will end on the after node that is removed
|
||||||
|
let mut iter2 = PersistantElementIter::new();
|
||||||
|
// div
|
||||||
|
iter1.next(&rdom).id();
|
||||||
|
iter2.next(&rdom).id();
|
||||||
|
// p
|
||||||
|
iter1.next(&rdom).id();
|
||||||
|
iter2.next(&rdom).id();
|
||||||
|
// "hello"
|
||||||
|
iter1.next(&rdom).id();
|
||||||
|
iter2.next(&rdom).id();
|
||||||
|
// p
|
||||||
|
iter1.next(&rdom).id();
|
||||||
|
iter2.next(&rdom).id();
|
||||||
|
// "world"
|
||||||
|
iter1.next(&rdom).id();
|
||||||
|
iter2.next(&rdom).id();
|
||||||
|
// p
|
||||||
|
iter2.next(&rdom).id();
|
||||||
|
// "hello world"
|
||||||
|
iter2.next(&rdom).id();
|
||||||
|
|
||||||
|
iter1.prune(&update, &rdom);
|
||||||
|
iter2.prune(&update, &rdom);
|
||||||
|
let _to_update = rdom.apply_mutations(vec![update]);
|
||||||
|
|
||||||
|
let p_tag = "p".to_string();
|
||||||
|
let idx = iter1.next(&rdom).id();
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[idx].node_type,
|
||||||
|
NodeType::Element { tag: p_tag, .. }
|
||||||
|
));
|
||||||
|
let text = "hello world".to_string();
|
||||||
|
let idx = iter1.next(&rdom).id();
|
||||||
|
assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. }));
|
||||||
|
let div_tag = "div".to_string();
|
||||||
|
let idx = iter2.next(&rdom).id();
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[idx].node_type,
|
||||||
|
NodeType::Element { tag: div_tag, .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn persist_instertions_before() {
|
||||||
|
use dioxus_core::*;
|
||||||
|
use dioxus_core_macro::*;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
let vdom = VirtualDom::new(Base);
|
||||||
|
let (build, update) = vdom.diff_lazynodes(
|
||||||
|
rsx! {
|
||||||
|
div{
|
||||||
|
p{
|
||||||
|
key: "1",
|
||||||
|
"hello"
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
key: "3",
|
||||||
|
"hello world"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rsx! {
|
||||||
|
div{
|
||||||
|
p{
|
||||||
|
key: "1",
|
||||||
|
"hello"
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
key: "2",
|
||||||
|
"world"
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
key: "3",
|
||||||
|
"hello world"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut rdom: RealDom<Empty> = RealDom::new();
|
||||||
|
|
||||||
|
let _to_update = rdom.apply_mutations(vec![build]);
|
||||||
|
|
||||||
|
let mut iter = PersistantElementIter::new();
|
||||||
|
// div
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
// p
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
// "hello"
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
// p
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
// "hello world"
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
|
||||||
|
iter.prune(&update, &rdom);
|
||||||
|
let _to_update = rdom.apply_mutations(vec![update]);
|
||||||
|
|
||||||
|
let p_tag = "div".to_string();
|
||||||
|
let idx = iter.next(&rdom).id();
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[idx].node_type,
|
||||||
|
NodeType::Element { tag: p_tag, .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn persist_instertions_after() {
|
||||||
|
use dioxus_core::*;
|
||||||
|
use dioxus_core_macro::*;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
let vdom = VirtualDom::new(Base);
|
||||||
|
let (build, update) = vdom.diff_lazynodes(
|
||||||
|
rsx! {
|
||||||
|
div{
|
||||||
|
p{
|
||||||
|
key: "1",
|
||||||
|
"hello"
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
key: "2",
|
||||||
|
"world"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rsx! {
|
||||||
|
div{
|
||||||
|
p{
|
||||||
|
key: "1",
|
||||||
|
"hello"
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
key: "2",
|
||||||
|
"world"
|
||||||
|
}
|
||||||
|
p{
|
||||||
|
key: "3",
|
||||||
|
"hello world"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut rdom: RealDom<Empty> = RealDom::new();
|
||||||
|
|
||||||
|
let _to_update = rdom.apply_mutations(vec![build]);
|
||||||
|
|
||||||
|
let mut iter = PersistantElementIter::new();
|
||||||
|
// div
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
// p
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
// "hello"
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
// p
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
// "world"
|
||||||
|
iter.next(&rdom).id();
|
||||||
|
|
||||||
|
iter.prune(&update, &rdom);
|
||||||
|
let _to_update = rdom.apply_mutations(vec![update]);
|
||||||
|
|
||||||
|
let p_tag = "p".to_string();
|
||||||
|
let idx = iter.next(&rdom).id();
|
||||||
|
assert!(matches!(
|
||||||
|
&rdom[idx].node_type,
|
||||||
|
NodeType::Element { tag: p_tag, .. }
|
||||||
|
));
|
||||||
|
let text = "hello world".to_string();
|
||||||
|
let idx = iter.next(&rdom).id();
|
||||||
|
assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. }));
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@ impl NodeDepState for NodeDepCallCounter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::vec_box)]
|
||||||
#[derive(Debug, Clone, PartialEq, Default)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
struct BubbledUpStateTester(Option<String>, Vec<Box<BubbledUpStateTester>>);
|
struct BubbledUpStateTester(Option<String>, Vec<Box<BubbledUpStateTester>>);
|
||||||
impl ChildDepState for BubbledUpStateTester {
|
impl ChildDepState for BubbledUpStateTester {
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ pub mod layout_attributes;
|
|||||||
pub mod node_ref;
|
pub mod node_ref;
|
||||||
pub mod real_dom;
|
pub mod real_dom;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
pub mod utils;
|
||||||
@@ -132,7 +132,7 @@ pub trait State: Default + Clone {
|
|||||||
ty: MemberId,
|
ty: MemberId,
|
||||||
node: &'a VNode<'a>,
|
node: &'a VNode<'a>,
|
||||||
vdom: &'a dioxus_core::VirtualDom,
|
vdom: &'a dioxus_core::VirtualDom,
|
||||||
children: &Vec<&Self>,
|
children: &[&Self],
|
||||||
ctx: &AnyMap,
|
ctx: &AnyMap,
|
||||||
) -> Option<ChildStatesChanged>;
|
) -> Option<ChildStatesChanged>;
|
||||||
/// This must be a valid resolution order. (no nodes updated before a state they rely on)
|
/// This must be a valid resolution order. (no nodes updated before a state they rely on)
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
use crate::{
|
||||||
|
real_dom::{NodeType, RealDom},
|
||||||
|
state::State,
|
||||||
|
};
|
||||||
|
use dioxus_core::{DomEdit, ElementId, Mutations};
|
||||||
|
|
||||||
|
pub enum ElementProduced {
|
||||||
|
Progressed(ElementId),
|
||||||
|
Looped(ElementId),
|
||||||
|
}
|
||||||
|
impl ElementProduced {
|
||||||
|
pub fn id(&self) -> ElementId {
|
||||||
|
match self {
|
||||||
|
ElementProduced::Progressed(id) => *id,
|
||||||
|
ElementProduced::Looped(id) => *id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum NodePosition {
|
||||||
|
AtNode,
|
||||||
|
InChild(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodePosition {
|
||||||
|
fn map(&self, mut f: impl FnMut(usize) -> usize) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::AtNode => Self::AtNode,
|
||||||
|
Self::InChild(i) => Self::InChild(f(*i)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_or_insert(&mut self, child_idx: usize) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::AtNode => {
|
||||||
|
*self = Self::InChild(child_idx);
|
||||||
|
child_idx
|
||||||
|
}
|
||||||
|
Self::InChild(i) => *i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The focus system needs a iterator that can persist through changes in the [VirtualDom].
|
||||||
|
/// Iterate through it with [ElementIter::next] [ElementIter::prev], and update it with [ElementIter::update] (with data from [`VirtualDom::work_with_deadline`]).
|
||||||
|
/// The iterator loops around when it reaches the end or the beginning.
|
||||||
|
pub struct PersistantElementIter {
|
||||||
|
// stack of elements and fragments
|
||||||
|
stack: smallvec::SmallVec<[(ElementId, NodePosition); 5]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PersistantElementIter {
|
||||||
|
fn default() -> Self {
|
||||||
|
PersistantElementIter {
|
||||||
|
stack: smallvec::smallvec![(ElementId(0), NodePosition::AtNode)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistantElementIter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// remove stale element refreneces
|
||||||
|
/// returns true if the focused element is removed
|
||||||
|
pub fn prune<S: State>(&mut self, mutations: &Mutations, rdom: &RealDom<S>) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
let ids_removed: Vec<_> = mutations
|
||||||
|
.edits
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| {
|
||||||
|
if let DomEdit::Remove { root } = e {
|
||||||
|
Some(*root)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
// if any element is removed in the chain, remove it and its children from the stack
|
||||||
|
if let Some(r) = self
|
||||||
|
.stack
|
||||||
|
.iter()
|
||||||
|
.position(|(el_id, _)| ids_removed.iter().any(|id| el_id.as_u64() == *id))
|
||||||
|
{
|
||||||
|
self.stack.truncate(r);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
// if a child is removed or inserted before or at the current element, update the child index
|
||||||
|
for (el_id, child_idx) in self.stack.iter_mut() {
|
||||||
|
if let NodePosition::InChild(child_idx) = child_idx {
|
||||||
|
if let NodeType::Element { children, .. } = &rdom[*el_id].node_type {
|
||||||
|
for m in &mutations.edits {
|
||||||
|
match m {
|
||||||
|
DomEdit::Remove { root } => {
|
||||||
|
if children
|
||||||
|
.iter()
|
||||||
|
.take(*child_idx + 1)
|
||||||
|
.any(|c| c.as_u64() == *root)
|
||||||
|
{
|
||||||
|
*child_idx -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DomEdit::InsertBefore { root, n } => {
|
||||||
|
if children
|
||||||
|
.iter()
|
||||||
|
.take(*child_idx + 1)
|
||||||
|
.any(|c| c.as_u64() == *root)
|
||||||
|
{
|
||||||
|
*child_idx += *n as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DomEdit::InsertAfter { root, n } => {
|
||||||
|
if children
|
||||||
|
.iter()
|
||||||
|
.take(*child_idx)
|
||||||
|
.any(|c| c.as_u64() == *root)
|
||||||
|
{
|
||||||
|
*child_idx += *n as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the next element
|
||||||
|
pub fn next<S: State>(&mut self, rdom: &RealDom<S>) -> ElementProduced {
|
||||||
|
if self.stack.is_empty() {
|
||||||
|
let id = ElementId(0);
|
||||||
|
let new = (id, NodePosition::AtNode);
|
||||||
|
self.stack.push(new);
|
||||||
|
ElementProduced::Looped(id)
|
||||||
|
} else {
|
||||||
|
let (last, o_child_idx) = self.stack.last_mut().unwrap();
|
||||||
|
let node = &rdom[*last];
|
||||||
|
match &node.node_type {
|
||||||
|
NodeType::Element { children, .. } => {
|
||||||
|
*o_child_idx = o_child_idx.map(|i| i + 1);
|
||||||
|
// if we have children, go to the next child
|
||||||
|
let child_idx = o_child_idx.get_or_insert(0);
|
||||||
|
if child_idx >= children.len() {
|
||||||
|
self.pop();
|
||||||
|
self.next(rdom)
|
||||||
|
} else {
|
||||||
|
let id = children[child_idx];
|
||||||
|
if let NodeType::Element { .. } = &rdom[id].node_type {
|
||||||
|
self.stack.push((id, NodePosition::AtNode));
|
||||||
|
}
|
||||||
|
ElementProduced::Progressed(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeType::Text { .. } | NodeType::Placeholder { .. } => {
|
||||||
|
// we are at a leaf, so we are done
|
||||||
|
ElementProduced::Progressed(self.pop())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the previous element
|
||||||
|
pub fn prev<S: State>(&mut self, rdom: &RealDom<S>) -> ElementProduced {
|
||||||
|
// recursively add the last child element to the stack
|
||||||
|
fn push_back<S: State>(
|
||||||
|
stack: &mut smallvec::SmallVec<[(ElementId, NodePosition); 5]>,
|
||||||
|
new_node: ElementId,
|
||||||
|
rdom: &RealDom<S>,
|
||||||
|
) -> ElementId {
|
||||||
|
match &rdom[new_node].node_type {
|
||||||
|
NodeType::Element { children, .. } => {
|
||||||
|
if children.is_empty() {
|
||||||
|
new_node
|
||||||
|
} else {
|
||||||
|
stack.push((new_node, NodePosition::InChild(children.len() - 1)));
|
||||||
|
push_back(stack, *children.last().unwrap(), rdom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => new_node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.stack.is_empty() {
|
||||||
|
let new_node = ElementId(0);
|
||||||
|
ElementProduced::Looped(push_back(&mut self.stack, new_node, rdom))
|
||||||
|
} else {
|
||||||
|
let (last, o_child_idx) = self.stack.last_mut().unwrap();
|
||||||
|
let node = &rdom[*last];
|
||||||
|
match &node.node_type {
|
||||||
|
NodeType::Element { children, .. } => {
|
||||||
|
// if we have children, go to the next child
|
||||||
|
if let NodePosition::InChild(0) = o_child_idx {
|
||||||
|
ElementProduced::Progressed(self.pop())
|
||||||
|
} else {
|
||||||
|
*o_child_idx = o_child_idx.map(|i| i - 1);
|
||||||
|
if let NodePosition::InChild(child_idx) = o_child_idx {
|
||||||
|
if *child_idx >= children.len() || children.is_empty() {
|
||||||
|
self.pop();
|
||||||
|
self.prev(rdom)
|
||||||
|
} else {
|
||||||
|
let new_node = children[*child_idx];
|
||||||
|
ElementProduced::Progressed(push_back(
|
||||||
|
&mut self.stack,
|
||||||
|
new_node,
|
||||||
|
rdom,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.pop();
|
||||||
|
self.prev(rdom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeType::Text { .. } | NodeType::Placeholder { .. } => {
|
||||||
|
// we are at a leaf, so we are done
|
||||||
|
ElementProduced::Progressed(self.pop())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop(&mut self) -> ElementId {
|
||||||
|
self.stack.pop().unwrap().0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "dioxus-rsx"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2018"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = { version = "1.0" }
|
||||||
|
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||||
|
quote = { version = "1.0" }
|
||||||
@@ -18,9 +18,12 @@ use quote::{quote, ToTokens, TokenStreamExt};
|
|||||||
use syn::{
|
use syn::{
|
||||||
ext::IdentExt,
|
ext::IdentExt,
|
||||||
parse::{Parse, ParseBuffer, ParseStream},
|
parse::{Parse, ParseBuffer, ParseStream},
|
||||||
token, AngleBracketedGenericArguments, Expr, Ident, LitStr, PathArguments, Result, Token,
|
spanned::Spanned,
|
||||||
|
token, AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Result,
|
||||||
|
Token,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
pub struct Component {
|
pub struct Component {
|
||||||
pub name: syn::Path,
|
pub name: syn::Path,
|
||||||
pub prop_gen_args: Option<AngleBracketedGenericArguments>,
|
pub prop_gen_args: Option<AngleBracketedGenericArguments>,
|
||||||
@@ -39,7 +42,7 @@ impl Component {
|
|||||||
.take(path.segments.len() - 1)
|
.take(path.segments.len() - 1)
|
||||||
.any(|seg| seg.arguments != PathArguments::None)
|
.any(|seg| seg.arguments != PathArguments::None)
|
||||||
{
|
{
|
||||||
component_path_cannot_have_arguments!(path);
|
component_path_cannot_have_arguments!(path.span());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure last segment only have value of None or AngleBracketed
|
// ensure last segment only have value of None or AngleBracketed
|
||||||
@@ -47,7 +50,7 @@ impl Component {
|
|||||||
path.segments.last().unwrap().arguments,
|
path.segments.last().unwrap().arguments,
|
||||||
PathArguments::None | PathArguments::AngleBracketed(_)
|
PathArguments::None | PathArguments::AngleBracketed(_)
|
||||||
) {
|
) {
|
||||||
invalid_component_path!(path);
|
invalid_component_path!(path.span());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -190,12 +193,14 @@ impl ToTokens for Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the struct's fields info
|
// the struct's fields info
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
pub struct ComponentField {
|
pub struct ComponentField {
|
||||||
name: Ident,
|
pub name: Ident,
|
||||||
content: ContentField,
|
pub content: ContentField,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ContentField {
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub enum ContentField {
|
||||||
ManExpr(Expr),
|
ManExpr(Expr),
|
||||||
Formatted(LitStr),
|
Formatted(LitStr),
|
||||||
OnHandlerRaw(Expr),
|
OnHandlerRaw(Expr),
|
||||||
@@ -4,12 +4,13 @@ use proc_macro2::TokenStream as TokenStream2;
|
|||||||
use quote::{quote, ToTokens, TokenStreamExt};
|
use quote::{quote, ToTokens, TokenStreamExt};
|
||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseBuffer, ParseStream},
|
parse::{Parse, ParseBuffer, ParseStream},
|
||||||
Expr, Ident, LitStr, Result, Token,
|
Error, Expr, Ident, LitStr, Result, Token,
|
||||||
};
|
};
|
||||||
|
|
||||||
// =======================================
|
// =======================================
|
||||||
// Parse the VNode::Element type
|
// Parse the VNode::Element type
|
||||||
// =======================================
|
// =======================================
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
pub struct Element {
|
pub struct Element {
|
||||||
pub name: Ident,
|
pub name: Ident,
|
||||||
pub key: Option<LitStr>,
|
pub key: Option<LitStr>,
|
||||||
@@ -64,7 +65,7 @@ impl Parse for Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if content.parse::<Token![,]>().is_err() {
|
if content.parse::<Token![,]>().is_err() {
|
||||||
missing_trailing_comma!(ident);
|
missing_trailing_comma!(ident.span());
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,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() {
|
||||||
missing_trailing_comma!(ident);
|
missing_trailing_comma!(ident.span());
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -189,6 +190,7 @@ impl ToTokens for Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum ElementAttr {
|
pub enum ElementAttr {
|
||||||
/// attribute: "valuee {}"
|
/// attribute: "valuee {}"
|
||||||
AttrText { name: Ident, value: LitStr },
|
AttrText { name: Ident, value: LitStr },
|
||||||
@@ -208,6 +210,7 @@ pub enum ElementAttr {
|
|||||||
EventTokens { name: Ident, tokens: Expr },
|
EventTokens { name: Ident, tokens: Expr },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
pub struct ElementAttrNamed {
|
pub struct ElementAttrNamed {
|
||||||
pub el_name: Ident,
|
pub el_name: Ident,
|
||||||
pub attr: ElementAttr,
|
pub attr: ElementAttr,
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
macro_rules! missing_trailing_comma {
|
||||||
|
($span:expr) => {
|
||||||
|
return Err(Error::new($span, "missing trailing comma"));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! attr_after_element {
|
||||||
|
($span:expr) => {
|
||||||
|
return Err(Error::new($span, "expected element\n = help move the attribute above all the children and text elements"));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! component_path_cannot_have_arguments {
|
||||||
|
($span:expr) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
$span,
|
||||||
|
"expected a path without arguments\n = try remove the path arguments",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! invalid_component_path {
|
||||||
|
($span:expr) => {
|
||||||
|
return Err(Error::new($span, "Invalid component path syntax"));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use proc_macro2::{Span, TokenStream};
|
||||||
|
|
||||||
|
use quote::{quote, ToTokens};
|
||||||
|
use syn::{
|
||||||
|
parse::{Parse, ParseStream},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn format_args_f_impl(input: IfmtInput) -> Result<TokenStream> {
|
||||||
|
// build format_literal
|
||||||
|
let mut format_literal = String::new();
|
||||||
|
let mut expr_counter = 0;
|
||||||
|
for segment in input.segments.iter() {
|
||||||
|
match segment {
|
||||||
|
Segment::Literal(s) => format_literal += s,
|
||||||
|
Segment::Formatted {
|
||||||
|
format_args,
|
||||||
|
segment,
|
||||||
|
} => {
|
||||||
|
format_literal += "{";
|
||||||
|
match segment {
|
||||||
|
FormattedSegment::Expr(_) => {
|
||||||
|
format_literal += &expr_counter.to_string();
|
||||||
|
expr_counter += 1;
|
||||||
|
}
|
||||||
|
FormattedSegment::Ident(ident) => {
|
||||||
|
format_literal += &ident.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
format_literal += ":";
|
||||||
|
format_literal += format_args;
|
||||||
|
format_literal += "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let positional_args = input.segments.iter().filter_map(|seg| {
|
||||||
|
if let Segment::Formatted {
|
||||||
|
segment: FormattedSegment::Expr(expr),
|
||||||
|
..
|
||||||
|
} = seg
|
||||||
|
{
|
||||||
|
Some(expr)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let named_args = input.segments.iter().filter_map(|seg| {
|
||||||
|
if let Segment::Formatted {
|
||||||
|
segment: FormattedSegment::Ident(ident),
|
||||||
|
..
|
||||||
|
} = seg
|
||||||
|
{
|
||||||
|
Some(quote! {#ident = #ident})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
format_args!(
|
||||||
|
#format_literal
|
||||||
|
#(, #positional_args)*
|
||||||
|
#(, #named_args)*
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)] // dumb compiler does not see the struct being used...
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct IfmtInput {
|
||||||
|
pub segments: Vec<Segment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for IfmtInput {
|
||||||
|
type Err = syn::Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self> {
|
||||||
|
let mut chars = input.chars().peekable();
|
||||||
|
let mut segments = Vec::new();
|
||||||
|
let mut current_literal = String::new();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '{' {
|
||||||
|
if let Some(c) = chars.next_if(|c| *c == '{') {
|
||||||
|
current_literal.push(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
segments.push(Segment::Literal(current_literal));
|
||||||
|
current_literal = String::new();
|
||||||
|
let mut current_captured = String::new();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == ':' {
|
||||||
|
let mut current_format_args = String::new();
|
||||||
|
for c in chars.by_ref() {
|
||||||
|
if c == '}' {
|
||||||
|
segments.push(Segment::Formatted {
|
||||||
|
format_args: current_format_args,
|
||||||
|
segment: FormattedSegment::parse(¤t_captured)?,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current_format_args.push(c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if c == '}' {
|
||||||
|
segments.push(Segment::Formatted {
|
||||||
|
format_args: String::new(),
|
||||||
|
segment: FormattedSegment::parse(¤t_captured)?,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current_captured.push(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current_literal.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segments.push(Segment::Literal(current_literal));
|
||||||
|
Ok(Self { segments })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Segment {
|
||||||
|
Literal(String),
|
||||||
|
Formatted {
|
||||||
|
format_args: String,
|
||||||
|
segment: FormattedSegment,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FormattedSegment {
|
||||||
|
Expr(Box<Expr>),
|
||||||
|
Ident(Ident),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormattedSegment {
|
||||||
|
fn parse(input: &str) -> Result<Self> {
|
||||||
|
if let Ok(ident) = parse_str::<Ident>(input) {
|
||||||
|
if ident == input {
|
||||||
|
return Ok(Self::Ident(ident));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if let Ok(expr) = parse_str(&("{".to_string() + input + "}")) {
|
||||||
|
if let Ok(expr) = parse_str(input) {
|
||||||
|
Ok(Self::Expr(Box::new(expr)))
|
||||||
|
} else {
|
||||||
|
Err(Error::new(
|
||||||
|
Span::call_site(),
|
||||||
|
"Expected Ident or Expression",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for FormattedSegment {
|
||||||
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
|
match self {
|
||||||
|
Self::Expr(expr) => expr.to_tokens(tokens),
|
||||||
|
Self::Ident(ident) => ident.to_tokens(tokens),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for IfmtInput {
|
||||||
|
fn parse(input: ParseStream) -> Result<Self> {
|
||||||
|
let input: LitStr = input.parse()?;
|
||||||
|
let input_str = input.value();
|
||||||
|
IfmtInput::from_str(&input_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,13 +16,13 @@ mod errors;
|
|||||||
|
|
||||||
mod component;
|
mod component;
|
||||||
mod element;
|
mod element;
|
||||||
|
mod ifmt;
|
||||||
mod node;
|
mod node;
|
||||||
|
|
||||||
pub mod pretty;
|
|
||||||
|
|
||||||
// Re-export the namespaces into each other
|
// Re-export the namespaces into each other
|
||||||
pub use component::*;
|
pub use component::*;
|
||||||
pub use element::*;
|
pub use element::*;
|
||||||
|
pub use ifmt::*;
|
||||||
pub use node::*;
|
pub use node::*;
|
||||||
|
|
||||||
// imports
|
// imports
|
||||||
@@ -15,6 +15,7 @@ Parse
|
|||||||
-> "text {with_args}"
|
-> "text {with_args}"
|
||||||
-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr
|
-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr
|
||||||
*/
|
*/
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
pub enum BodyNode {
|
pub enum BodyNode {
|
||||||
Element(Element),
|
Element(Element),
|
||||||
Component(Component),
|
Component(Component),
|
||||||
@@ -22,6 +23,12 @@ pub enum BodyNode {
|
|||||||
RawExpr(Expr),
|
RawExpr(Expr),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl BodyNode {
|
||||||
|
pub fn is_litstr(&self) -> bool {
|
||||||
|
matches!(self, BodyNode::Text(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Parse for BodyNode {
|
impl Parse for BodyNode {
|
||||||
fn parse(stream: ParseStream) -> Result<Self> {
|
fn parse(stream: ParseStream) -> Result<Self> {
|
||||||
if stream.peek(LitStr) {
|
if stream.peek(LitStr) {
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "dioxus-rsx-interpreter"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = { version = "1.0", features = ["extra-traits"] }
|
||||||
|
proc-macro2 = { version = "1.0.39", features = ["span-locations"] }
|
||||||
|
quote = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
|
||||||
|
dioxus-rsx = { path = "../rsx", default-features = false }
|
||||||
|
dioxus-ssr = { path = "../ssr" }
|
||||||
|
dioxus-core = { path = "../core" }
|
||||||
|
dioxus-html = { path = "../html" }
|
||||||
|
dioxus-hooks = { path = "../hooks"}
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
dioxus-core-macro = { path = "../core-macro" }
|
||||||
|
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
|
||||||
@@ -0,0 +1,869 @@
|
|||||||
|
use crate::elements::*;
|
||||||
|
|
||||||
|
// map the rsx name of the attribute to the html name of the attribute and the namespace that contains it
|
||||||
|
pub fn attrbute_to_static_str(attr: &str) -> Option<(&'static str, Option<&'static str>)> {
|
||||||
|
NO_NAMESPACE_ATTRIBUTES
|
||||||
|
.iter()
|
||||||
|
.find(|&a| *a == attr)
|
||||||
|
.map(|a| (*a, None))
|
||||||
|
.or_else(|| {
|
||||||
|
STYLE_ATTRIBUTES
|
||||||
|
.iter()
|
||||||
|
.find(|(a, _)| *a == attr)
|
||||||
|
.map(|(_, b)| (*b, Some("style")))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
MAPPED_ATTRIBUTES
|
||||||
|
.iter()
|
||||||
|
.find(|(a, _)| *a == attr)
|
||||||
|
.map(|(_, b)| (*b, None))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
svg::MAPPED_ATTRIBUTES
|
||||||
|
.iter()
|
||||||
|
.find(|(a, _)| *a == attr)
|
||||||
|
.map(|(_, b)| (*b, None))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
ELEMENTS_WITH_MAPPED_ATTRIBUTES
|
||||||
|
.iter()
|
||||||
|
.find_map(|(_, attrs)| {
|
||||||
|
attrs
|
||||||
|
.iter()
|
||||||
|
.find(|(a, _)| *a == attr)
|
||||||
|
.map(|(_, b)| (*b, None))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
ELEMENTS_WITH_NAMESPACE
|
||||||
|
.iter()
|
||||||
|
.find_map(|(_, _, attrs)| attrs.iter().find(|a| **a == attr).map(|a| (*a, None)))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
ELEMENTS_WITHOUT_NAMESPACE
|
||||||
|
.iter()
|
||||||
|
.find_map(|(_, attrs)| attrs.iter().find(|a| **a == attr).map(|a| (*a, None)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! no_namespace_trait_methods {
|
||||||
|
(
|
||||||
|
$(
|
||||||
|
$(#[$attr:meta])*
|
||||||
|
$name:ident;
|
||||||
|
)*
|
||||||
|
) => {
|
||||||
|
pub const NO_NAMESPACE_ATTRIBUTES: &'static [&'static str] = &[
|
||||||
|
$(
|
||||||
|
stringify!($name),
|
||||||
|
)*
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! style_trait_methods {
|
||||||
|
(
|
||||||
|
$(
|
||||||
|
$(#[$attr:meta])*
|
||||||
|
$name:ident: $lit:literal,
|
||||||
|
)*
|
||||||
|
) => {
|
||||||
|
pub const STYLE_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[
|
||||||
|
$(
|
||||||
|
(stringify!($name), $lit),
|
||||||
|
)*
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! mapped_trait_methods {
|
||||||
|
(
|
||||||
|
$(
|
||||||
|
$(#[$attr:meta])*
|
||||||
|
$name:ident: $lit:literal,
|
||||||
|
)*
|
||||||
|
) => {
|
||||||
|
pub const MAPPED_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[
|
||||||
|
$(
|
||||||
|
(stringify!($name), $lit),
|
||||||
|
)*
|
||||||
|
("prevent_default", "dioxus-prevent-default"),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
no_namespace_trait_methods! {
|
||||||
|
accesskey;
|
||||||
|
|
||||||
|
/// The HTML class attribute is used to specify a class for an HTML element.
|
||||||
|
///
|
||||||
|
/// ## Details
|
||||||
|
/// Multiple HTML elements can share the same class.
|
||||||
|
///
|
||||||
|
/// The class global attribute is a space-separated list of the case-sensitive classes of the element.
|
||||||
|
/// Classes allow CSS and Javascript to select and access specific elements via the class selectors or
|
||||||
|
/// functions like the DOM method document.getElementsByClassName.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ### HTML:
|
||||||
|
/// ```html
|
||||||
|
/// <p class="note editorial">Above point sounds a bit obvious. Remove/rewrite?</p>
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ### CSS:
|
||||||
|
/// ```css
|
||||||
|
/// .note {
|
||||||
|
/// font-style: italic;
|
||||||
|
/// font-weight: bold;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// .editorial {
|
||||||
|
/// background: rgb(255, 0, 0, .25);
|
||||||
|
/// padding: 10px;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
class;
|
||||||
|
contenteditable;
|
||||||
|
data;
|
||||||
|
dir;
|
||||||
|
draggable;
|
||||||
|
hidden;
|
||||||
|
id;
|
||||||
|
lang;
|
||||||
|
spellcheck;
|
||||||
|
style;
|
||||||
|
tabindex;
|
||||||
|
title;
|
||||||
|
translate;
|
||||||
|
|
||||||
|
role;
|
||||||
|
|
||||||
|
/// dangerous_inner_html is Dioxus's replacement for using innerHTML in the browser DOM. In general, setting
|
||||||
|
/// HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS)
|
||||||
|
/// attack. So, you can set HTML directly from Dioxus, but you have to type out dangerous_inner_html to remind
|
||||||
|
/// yourself that it’s dangerous
|
||||||
|
dangerous_inner_html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This macro creates an explicit method call for each of the style attributes.
|
||||||
|
//
|
||||||
|
// The left token specifies the name of the attribute in the rsx! macro, and the right string literal specifies the
|
||||||
|
// actual name of the attribute generated.
|
||||||
|
//
|
||||||
|
// This roughly follows the html spec
|
||||||
|
style_trait_methods! {
|
||||||
|
align_content: "align-content",
|
||||||
|
align_items: "align-items",
|
||||||
|
align_self: "align-self",
|
||||||
|
alignment_adjust: "alignment-adjust",
|
||||||
|
alignment_baseline: "alignment-baseline",
|
||||||
|
all: "all",
|
||||||
|
alt: "alt",
|
||||||
|
animation: "animation",
|
||||||
|
animation_delay: "animation-delay",
|
||||||
|
animation_direction: "animation-direction",
|
||||||
|
animation_duration: "animation-duration",
|
||||||
|
animation_fill_mode: "animation-fill-mode",
|
||||||
|
animation_iteration_count: "animation-iteration-count",
|
||||||
|
animation_name: "animation-name",
|
||||||
|
animation_play_state: "animation-play-state",
|
||||||
|
animation_timing_function: "animation-timing-function",
|
||||||
|
azimuth: "azimuth",
|
||||||
|
backface_visibility: "backface-visibility",
|
||||||
|
background: "background",
|
||||||
|
background_attachment: "background-attachment",
|
||||||
|
background_clip: "background-clip",
|
||||||
|
background_color: "background-color",
|
||||||
|
background_image: "background-image",
|
||||||
|
background_origin: "background-origin",
|
||||||
|
background_position: "background-position",
|
||||||
|
background_repeat: "background-repeat",
|
||||||
|
background_size: "background-size",
|
||||||
|
background_blend_mode: "background-blend-mode",
|
||||||
|
baseline_shift: "baseline-shift",
|
||||||
|
bleed: "bleed",
|
||||||
|
bookmark_label: "bookmark-label",
|
||||||
|
bookmark_level: "bookmark-level",
|
||||||
|
bookmark_state: "bookmark-state",
|
||||||
|
border: "border",
|
||||||
|
border_color: "border-color",
|
||||||
|
border_style: "border-style",
|
||||||
|
border_width: "border-width",
|
||||||
|
border_bottom: "border-bottom",
|
||||||
|
border_bottom_color: "border-bottom-color",
|
||||||
|
border_bottom_style: "border-bottom-style",
|
||||||
|
border_bottom_width: "border-bottom-width",
|
||||||
|
border_left: "border-left",
|
||||||
|
border_left_color: "border-left-color",
|
||||||
|
border_left_style: "border-left-style",
|
||||||
|
border_left_width: "border-left-width",
|
||||||
|
border_right: "border-right",
|
||||||
|
border_right_color: "border-right-color",
|
||||||
|
border_right_style: "border-right-style",
|
||||||
|
border_right_width: "border-right-width",
|
||||||
|
border_top: "border-top",
|
||||||
|
border_top_color: "border-top-color",
|
||||||
|
border_top_style: "border-top-style",
|
||||||
|
border_top_width: "border-top-width",
|
||||||
|
border_collapse: "border-collapse",
|
||||||
|
border_image: "border-image",
|
||||||
|
border_image_outset: "border-image-outset",
|
||||||
|
border_image_repeat: "border-image-repeat",
|
||||||
|
border_image_slice: "border-image-slice",
|
||||||
|
border_image_source: "border-image-source",
|
||||||
|
border_image_width: "border-image-width",
|
||||||
|
border_radius: "border-radius",
|
||||||
|
border_bottom_left_radius: "border-bottom-left-radius",
|
||||||
|
border_bottom_right_radius: "border-bottom-right-radius",
|
||||||
|
border_top_left_radius: "border-top-left-radius",
|
||||||
|
border_top_right_radius: "border-top-right-radius",
|
||||||
|
border_spacing: "border-spacing",
|
||||||
|
bottom: "bottom",
|
||||||
|
box_decoration_break: "box-decoration-break",
|
||||||
|
box_shadow: "box-shadow",
|
||||||
|
box_sizing: "box-sizing",
|
||||||
|
box_snap: "box-snap",
|
||||||
|
break_after: "break-after",
|
||||||
|
break_before: "break-before",
|
||||||
|
break_inside: "break-inside",
|
||||||
|
buffered_rendering: "buffered-rendering",
|
||||||
|
caption_side: "caption-side",
|
||||||
|
clear: "clear",
|
||||||
|
clear_side: "clear-side",
|
||||||
|
clip: "clip",
|
||||||
|
clip_path: "clip-path",
|
||||||
|
clip_rule: "clip-rule",
|
||||||
|
color: "color",
|
||||||
|
color_adjust: "color-adjust",
|
||||||
|
color_correction: "color-correction",
|
||||||
|
color_interpolation: "color-interpolation",
|
||||||
|
color_interpolation_filters: "color-interpolation-filters",
|
||||||
|
color_profile: "color-profile",
|
||||||
|
color_rendering: "color-rendering",
|
||||||
|
column_fill: "column-fill",
|
||||||
|
column_gap: "column-gap",
|
||||||
|
column_rule: "column-rule",
|
||||||
|
column_rule_color: "column-rule-color",
|
||||||
|
column_rule_style: "column-rule-style",
|
||||||
|
column_rule_width: "column-rule-width",
|
||||||
|
column_span: "column-span",
|
||||||
|
columns: "columns",
|
||||||
|
column_count: "column-count",
|
||||||
|
column_width: "column-width",
|
||||||
|
contain: "contain",
|
||||||
|
content: "content",
|
||||||
|
counter_increment: "counter-increment",
|
||||||
|
counter_reset: "counter-reset",
|
||||||
|
counter_set: "counter-set",
|
||||||
|
cue: "cue",
|
||||||
|
cue_after: "cue-after",
|
||||||
|
cue_before: "cue-before",
|
||||||
|
cursor: "cursor",
|
||||||
|
direction: "direction",
|
||||||
|
display: "display",
|
||||||
|
display_inside: "display-inside",
|
||||||
|
display_outside: "display-outside",
|
||||||
|
display_extras: "display-extras",
|
||||||
|
display_box: "display-box",
|
||||||
|
dominant_baseline: "dominant-baseline",
|
||||||
|
elevation: "elevation",
|
||||||
|
empty_cells: "empty-cells",
|
||||||
|
enable_background: "enable-background",
|
||||||
|
fill: "fill",
|
||||||
|
fill_opacity: "fill-opacity",
|
||||||
|
fill_rule: "fill-rule",
|
||||||
|
filter: "filter",
|
||||||
|
float: "float",
|
||||||
|
float_defer_column: "float-defer-column",
|
||||||
|
float_defer_page: "float-defer-page",
|
||||||
|
float_offset: "float-offset",
|
||||||
|
float_wrap: "float-wrap",
|
||||||
|
flow_into: "flow-into",
|
||||||
|
flow_from: "flow-from",
|
||||||
|
flex: "flex",
|
||||||
|
flex_basis: "flex-basis",
|
||||||
|
flex_grow: "flex-grow",
|
||||||
|
flex_shrink: "flex-shrink",
|
||||||
|
flex_flow: "flex-flow",
|
||||||
|
flex_direction: "flex-direction",
|
||||||
|
flex_wrap: "flex-wrap",
|
||||||
|
flood_color: "flood-color",
|
||||||
|
flood_opacity: "flood-opacity",
|
||||||
|
font: "font",
|
||||||
|
font_family: "font-family",
|
||||||
|
font_size: "font-size",
|
||||||
|
font_stretch: "font-stretch",
|
||||||
|
font_style: "font-style",
|
||||||
|
font_weight: "font-weight",
|
||||||
|
font_feature_settings: "font-feature-settings",
|
||||||
|
font_kerning: "font-kerning",
|
||||||
|
font_language_override: "font-language-override",
|
||||||
|
font_size_adjust: "font-size-adjust",
|
||||||
|
font_synthesis: "font-synthesis",
|
||||||
|
font_variant: "font-variant",
|
||||||
|
font_variant_alternates: "font-variant-alternates",
|
||||||
|
font_variant_caps: "font-variant-caps",
|
||||||
|
font_variant_east_asian: "font-variant-east-asian",
|
||||||
|
font_variant_ligatures: "font-variant-ligatures",
|
||||||
|
font_variant_numeric: "font-variant-numeric",
|
||||||
|
font_variant_position: "font-variant-position",
|
||||||
|
footnote_policy: "footnote-policy",
|
||||||
|
glyph_orientation_horizontal: "glyph-orientation-horizontal",
|
||||||
|
glyph_orientation_vertical: "glyph-orientation-vertical",
|
||||||
|
grid: "grid",
|
||||||
|
grid_auto_flow: "grid-auto-flow",
|
||||||
|
grid_auto_columns: "grid-auto-columns",
|
||||||
|
grid_auto_rows: "grid-auto-rows",
|
||||||
|
grid_template: "grid-template",
|
||||||
|
grid_template_areas: "grid-template-areas",
|
||||||
|
grid_template_columns: "grid-template-columns",
|
||||||
|
grid_template_rows: "grid-template-rows",
|
||||||
|
grid_area: "grid-area",
|
||||||
|
grid_column: "grid-column",
|
||||||
|
grid_column_start: "grid-column-start",
|
||||||
|
grid_column_end: "grid-column-end",
|
||||||
|
grid_row: "grid-row",
|
||||||
|
grid_row_start: "grid-row-start",
|
||||||
|
grid_row_end: "grid-row-end",
|
||||||
|
hanging_punctuation: "hanging-punctuation",
|
||||||
|
height: "height",
|
||||||
|
hyphenate_character: "hyphenate-character",
|
||||||
|
hyphenate_limit_chars: "hyphenate-limit-chars",
|
||||||
|
hyphenate_limit_last: "hyphenate-limit-last",
|
||||||
|
hyphenate_limit_lines: "hyphenate-limit-lines",
|
||||||
|
hyphenate_limit_zone: "hyphenate-limit-zone",
|
||||||
|
hyphens: "hyphens",
|
||||||
|
icon: "icon",
|
||||||
|
image_orientation: "image-orientation",
|
||||||
|
image_resolution: "image-resolution",
|
||||||
|
image_rendering: "image-rendering",
|
||||||
|
ime: "ime",
|
||||||
|
ime_align: "ime-align",
|
||||||
|
ime_mode: "ime-mode",
|
||||||
|
ime_offset: "ime-offset",
|
||||||
|
ime_width: "ime-width",
|
||||||
|
initial_letters: "initial-letters",
|
||||||
|
inline_box_align: "inline-box-align",
|
||||||
|
isolation: "isolation",
|
||||||
|
justify_content: "justify-content",
|
||||||
|
justify_items: "justify-items",
|
||||||
|
justify_self: "justify-self",
|
||||||
|
kerning: "kerning",
|
||||||
|
left: "left",
|
||||||
|
letter_spacing: "letter-spacing",
|
||||||
|
lighting_color: "lighting-color",
|
||||||
|
line_box_contain: "line-box-contain",
|
||||||
|
line_break: "line-break",
|
||||||
|
line_grid: "line-grid",
|
||||||
|
line_height: "line-height",
|
||||||
|
line_slack: "line-slack",
|
||||||
|
line_snap: "line-snap",
|
||||||
|
list_style: "list-style",
|
||||||
|
list_style_image: "list-style-image",
|
||||||
|
list_style_position: "list-style-position",
|
||||||
|
list_style_type: "list-style-type",
|
||||||
|
margin: "margin",
|
||||||
|
margin_bottom: "margin-bottom",
|
||||||
|
margin_left: "margin-left",
|
||||||
|
margin_right: "margin-right",
|
||||||
|
margin_top: "margin-top",
|
||||||
|
marker: "marker",
|
||||||
|
marker_end: "marker-end",
|
||||||
|
marker_mid: "marker-mid",
|
||||||
|
marker_pattern: "marker-pattern",
|
||||||
|
marker_segment: "marker-segment",
|
||||||
|
marker_start: "marker-start",
|
||||||
|
marker_knockout_left: "marker-knockout-left",
|
||||||
|
marker_knockout_right: "marker-knockout-right",
|
||||||
|
marker_side: "marker-side",
|
||||||
|
marks: "marks",
|
||||||
|
marquee_direction: "marquee-direction",
|
||||||
|
marquee_play_count: "marquee-play-count",
|
||||||
|
marquee_speed: "marquee-speed",
|
||||||
|
marquee_style: "marquee-style",
|
||||||
|
mask: "mask",
|
||||||
|
mask_image: "mask-image",
|
||||||
|
mask_repeat: "mask-repeat",
|
||||||
|
mask_position: "mask-position",
|
||||||
|
mask_clip: "mask-clip",
|
||||||
|
mask_origin: "mask-origin",
|
||||||
|
mask_size: "mask-size",
|
||||||
|
mask_box: "mask-box",
|
||||||
|
mask_box_outset: "mask-box-outset",
|
||||||
|
mask_box_repeat: "mask-box-repeat",
|
||||||
|
mask_box_slice: "mask-box-slice",
|
||||||
|
mask_box_source: "mask-box-source",
|
||||||
|
mask_box_width: "mask-box-width",
|
||||||
|
mask_type: "mask-type",
|
||||||
|
max_height: "max-height",
|
||||||
|
max_lines: "max-lines",
|
||||||
|
max_width: "max-width",
|
||||||
|
min_height: "min-height",
|
||||||
|
min_width: "min-width",
|
||||||
|
mix_blend_mode: "mix-blend-mode",
|
||||||
|
nav_down: "nav-down",
|
||||||
|
nav_index: "nav-index",
|
||||||
|
nav_left: "nav-left",
|
||||||
|
nav_right: "nav-right",
|
||||||
|
nav_up: "nav-up",
|
||||||
|
object_fit: "object-fit",
|
||||||
|
object_position: "object-position",
|
||||||
|
offset_after: "offset-after",
|
||||||
|
offset_before: "offset-before",
|
||||||
|
offset_end: "offset-end",
|
||||||
|
offset_start: "offset-start",
|
||||||
|
opacity: "opacity",
|
||||||
|
order: "order",
|
||||||
|
orphans: "orphans",
|
||||||
|
outline: "outline",
|
||||||
|
outline_color: "outline-color",
|
||||||
|
outline_style: "outline-style",
|
||||||
|
outline_width: "outline-width",
|
||||||
|
outline_offset: "outline-offset",
|
||||||
|
overflow: "overflow",
|
||||||
|
overflow_x: "overflow-x",
|
||||||
|
overflow_y: "overflow-y",
|
||||||
|
overflow_style: "overflow-style",
|
||||||
|
overflow_wrap: "overflow-wrap",
|
||||||
|
padding: "padding",
|
||||||
|
padding_bottom: "padding-bottom",
|
||||||
|
padding_left: "padding-left",
|
||||||
|
padding_right: "padding-right",
|
||||||
|
padding_top: "padding-top",
|
||||||
|
page: "page",
|
||||||
|
page_break_after: "page-break-after",
|
||||||
|
page_break_before: "page-break-before",
|
||||||
|
page_break_inside: "page-break-inside",
|
||||||
|
paint_order: "paint-order",
|
||||||
|
pause: "pause",
|
||||||
|
pause_after: "pause-after",
|
||||||
|
pause_before: "pause-before",
|
||||||
|
perspective: "perspective",
|
||||||
|
perspective_origin: "perspective-origin",
|
||||||
|
pitch: "pitch",
|
||||||
|
pitch_range: "pitch-range",
|
||||||
|
play_during: "play-during",
|
||||||
|
pointer_events: "pointer-events",
|
||||||
|
position: "position",
|
||||||
|
quotes: "quotes",
|
||||||
|
region_fragment: "region-fragment",
|
||||||
|
resize: "resize",
|
||||||
|
rest: "rest",
|
||||||
|
rest_after: "rest-after",
|
||||||
|
rest_before: "rest-before",
|
||||||
|
richness: "richness",
|
||||||
|
right: "right",
|
||||||
|
ruby_align: "ruby-align",
|
||||||
|
ruby_merge: "ruby-merge",
|
||||||
|
ruby_position: "ruby-position",
|
||||||
|
scroll_behavior: "scroll-behavior",
|
||||||
|
scroll_snap_coordinate: "scroll-snap-coordinate",
|
||||||
|
scroll_snap_destination: "scroll-snap-destination",
|
||||||
|
scroll_snap_points_x: "scroll-snap-points-x",
|
||||||
|
scroll_snap_points_y: "scroll-snap-points-y",
|
||||||
|
scroll_snap_type: "scroll-snap-type",
|
||||||
|
shape_image_threshold: "shape-image-threshold",
|
||||||
|
shape_inside: "shape-inside",
|
||||||
|
shape_margin: "shape-margin",
|
||||||
|
shape_outside: "shape-outside",
|
||||||
|
shape_padding: "shape-padding",
|
||||||
|
shape_rendering: "shape-rendering",
|
||||||
|
size: "size",
|
||||||
|
speak: "speak",
|
||||||
|
speak_as: "speak-as",
|
||||||
|
speak_header: "speak-header",
|
||||||
|
speak_numeral: "speak-numeral",
|
||||||
|
speak_punctuation: "speak-punctuation",
|
||||||
|
speech_rate: "speech-rate",
|
||||||
|
stop_color: "stop-color",
|
||||||
|
stop_opacity: "stop-opacity",
|
||||||
|
stress: "stress",
|
||||||
|
string_set: "string-set",
|
||||||
|
stroke: "stroke",
|
||||||
|
stroke_dasharray: "stroke-dasharray",
|
||||||
|
stroke_dashoffset: "stroke-dashoffset",
|
||||||
|
stroke_linecap: "stroke-linecap",
|
||||||
|
stroke_linejoin: "stroke-linejoin",
|
||||||
|
stroke_miterlimit: "stroke-miterlimit",
|
||||||
|
stroke_opacity: "stroke-opacity",
|
||||||
|
stroke_width: "stroke-width",
|
||||||
|
tab_size: "tab-size",
|
||||||
|
table_layout: "table-layout",
|
||||||
|
text_align: "text-align",
|
||||||
|
text_align_all: "text-align-all",
|
||||||
|
text_align_last: "text-align-last",
|
||||||
|
text_anchor: "text-anchor",
|
||||||
|
text_combine_upright: "text-combine-upright",
|
||||||
|
text_decoration: "text-decoration",
|
||||||
|
text_decoration_color: "text-decoration-color",
|
||||||
|
text_decoration_line: "text-decoration-line",
|
||||||
|
text_decoration_style: "text-decoration-style",
|
||||||
|
text_decoration_skip: "text-decoration-skip",
|
||||||
|
text_emphasis: "text-emphasis",
|
||||||
|
text_emphasis_color: "text-emphasis-color",
|
||||||
|
text_emphasis_style: "text-emphasis-style",
|
||||||
|
text_emphasis_position: "text-emphasis-position",
|
||||||
|
text_emphasis_skip: "text-emphasis-skip",
|
||||||
|
text_height: "text-height",
|
||||||
|
text_indent: "text-indent",
|
||||||
|
text_justify: "text-justify",
|
||||||
|
text_orientation: "text-orientation",
|
||||||
|
text_overflow: "text-overflow",
|
||||||
|
text_rendering: "text-rendering",
|
||||||
|
text_shadow: "text-shadow",
|
||||||
|
text_size_adjust: "text-size-adjust",
|
||||||
|
text_space_collapse: "text-space-collapse",
|
||||||
|
text_spacing: "text-spacing",
|
||||||
|
text_transform: "text-transform",
|
||||||
|
text_underline_position: "text-underline-position",
|
||||||
|
text_wrap: "text-wrap",
|
||||||
|
top: "top",
|
||||||
|
touch_action: "touch-action",
|
||||||
|
transform: "transform",
|
||||||
|
transform_box: "transform-box",
|
||||||
|
transform_origin: "transform-origin",
|
||||||
|
transform_style: "transform-style",
|
||||||
|
transition: "transition",
|
||||||
|
transition_delay: "transition-delay",
|
||||||
|
transition_duration: "transition-duration",
|
||||||
|
transition_property: "transition-property",
|
||||||
|
unicode_bidi: "unicode-bidi",
|
||||||
|
vector_effect: "vector-effect",
|
||||||
|
vertical_align: "vertical-align",
|
||||||
|
visibility: "visibility",
|
||||||
|
voice_balance: "voice-balance",
|
||||||
|
voice_duration: "voice-duration",
|
||||||
|
voice_family: "voice-family",
|
||||||
|
voice_pitch: "voice-pitch",
|
||||||
|
voice_range: "voice-range",
|
||||||
|
voice_rate: "voice-rate",
|
||||||
|
voice_stress: "voice-stress",
|
||||||
|
voice_volumn: "voice-volumn",
|
||||||
|
volume: "volume",
|
||||||
|
white_space: "white-space",
|
||||||
|
widows: "widows",
|
||||||
|
width: "width",
|
||||||
|
will_change: "will-change",
|
||||||
|
word_break: "word-break",
|
||||||
|
word_spacing: "word-spacing",
|
||||||
|
word_wrap: "word-wrap",
|
||||||
|
wrap_flow: "wrap-flow",
|
||||||
|
wrap_through: "wrap-through",
|
||||||
|
writing_mode: "writing-mode",
|
||||||
|
gap: "gap",
|
||||||
|
list_styler_type: "list-style-type",
|
||||||
|
row_gap: "row-gap",
|
||||||
|
transition_timing_function: "transition-timing-function",
|
||||||
|
user_select: "user-select",
|
||||||
|
webkit_user_select: "-webkit-user-select",
|
||||||
|
z_index : "z-index",
|
||||||
|
}
|
||||||
|
mapped_trait_methods! {
|
||||||
|
aria_current: "aria-current",
|
||||||
|
aria_details: "aria-details",
|
||||||
|
aria_disabled: "aria-disabled",
|
||||||
|
aria_hidden: "aria-hidden",
|
||||||
|
aria_invalid: "aria-invalid",
|
||||||
|
aria_keyshortcuts: "aria-keyshortcuts",
|
||||||
|
aria_label: "aria-label",
|
||||||
|
aria_roledescription: "aria-roledescription",
|
||||||
|
// Widget Attributes
|
||||||
|
aria_autocomplete: "aria-autocomplete",
|
||||||
|
aria_checked: "aria-checked",
|
||||||
|
aria_expanded: "aria-expanded",
|
||||||
|
aria_haspopup: "aria-haspopup",
|
||||||
|
aria_level: "aria-level",
|
||||||
|
aria_modal: "aria-modal",
|
||||||
|
aria_multiline: "aria-multiline",
|
||||||
|
aria_multiselectable: "aria-multiselectable",
|
||||||
|
aria_orientation: "aria-orientation",
|
||||||
|
aria_placeholder: "aria-placeholder",
|
||||||
|
aria_pressed: "aria-pressed",
|
||||||
|
aria_readonly: "aria-readonly",
|
||||||
|
aria_required: "aria-required",
|
||||||
|
aria_selected: "aria-selected",
|
||||||
|
aria_sort: "aria-sort",
|
||||||
|
aria_valuemax: "aria-valuemax",
|
||||||
|
aria_valuemin: "aria-valuemin",
|
||||||
|
aria_valuenow: "aria-valuenow",
|
||||||
|
aria_valuetext: "aria-valuetext",
|
||||||
|
// Live Region Attributes
|
||||||
|
aria_atomic: "aria-atomic",
|
||||||
|
aria_busy: "aria-busy",
|
||||||
|
aria_live: "aria-live",
|
||||||
|
aria_relevant: "aria-relevant",
|
||||||
|
|
||||||
|
aria_dropeffect: "aria-dropeffect",
|
||||||
|
aria_grabbed: "aria-grabbed",
|
||||||
|
// Relationship Attributes
|
||||||
|
aria_activedescendant: "aria-activedescendant",
|
||||||
|
aria_colcount: "aria-colcount",
|
||||||
|
aria_colindex: "aria-colindex",
|
||||||
|
aria_colspan: "aria-colspan",
|
||||||
|
aria_controls: "aria-controls",
|
||||||
|
aria_describedby: "aria-describedby",
|
||||||
|
aria_errormessage: "aria-errormessage",
|
||||||
|
aria_flowto: "aria-flowto",
|
||||||
|
aria_labelledby: "aria-labelledby",
|
||||||
|
aria_owns: "aria-owns",
|
||||||
|
aria_posinset: "aria-posinset",
|
||||||
|
aria_rowcount: "aria-rowcount",
|
||||||
|
aria_rowindex: "aria-rowindex",
|
||||||
|
aria_rowspan: "aria-rowspan",
|
||||||
|
aria_setsize: "aria-setsize",
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod svg {
|
||||||
|
mapped_trait_methods! {
|
||||||
|
accent_height: "accent-height",
|
||||||
|
accumulate: "accumulate",
|
||||||
|
additive: "additive",
|
||||||
|
alignment_baseline: "alignment-baseline",
|
||||||
|
alphabetic: "alphabetic",
|
||||||
|
amplitude: "amplitude",
|
||||||
|
arabic_form: "arabic-form",
|
||||||
|
ascent: "ascent",
|
||||||
|
attributeName: "attributeName",
|
||||||
|
attributeType: "attributeType",
|
||||||
|
azimuth: "azimuth",
|
||||||
|
baseFrequency: "baseFrequency",
|
||||||
|
baseline_shift: "baseline-shift",
|
||||||
|
baseProfile: "baseProfile",
|
||||||
|
bbox: "bbox",
|
||||||
|
begin: "begin",
|
||||||
|
bias: "bias",
|
||||||
|
by: "by",
|
||||||
|
calcMode: "calcMode",
|
||||||
|
cap_height: "cap-height",
|
||||||
|
class: "class",
|
||||||
|
clip: "clip",
|
||||||
|
clipPathUnits: "clipPathUnits",
|
||||||
|
clip_path: "clip-path",
|
||||||
|
clip_rule: "clip-rule",
|
||||||
|
color: "color",
|
||||||
|
color_interpolation: "color-interpolation",
|
||||||
|
color_interpolation_filters: "color-interpolation-filters",
|
||||||
|
color_profile: "color-profile",
|
||||||
|
color_rendering: "color-rendering",
|
||||||
|
contentScriptType: "contentScriptType",
|
||||||
|
contentStyleType: "contentStyleType",
|
||||||
|
crossorigin: "crossorigin",
|
||||||
|
cursor: "cursor",
|
||||||
|
cx: "cx",
|
||||||
|
cy: "cy",
|
||||||
|
d: "d",
|
||||||
|
decelerate: "decelerate",
|
||||||
|
descent: "descent",
|
||||||
|
diffuseConstant: "diffuseConstant",
|
||||||
|
direction: "direction",
|
||||||
|
display: "display",
|
||||||
|
divisor: "divisor",
|
||||||
|
dominant_baseline: "dominant-baseline",
|
||||||
|
dur: "dur",
|
||||||
|
dx: "dx",
|
||||||
|
dy: "dy",
|
||||||
|
edgeMode: "edgeMode",
|
||||||
|
elevation: "elevation",
|
||||||
|
enable_background: "enable-background",
|
||||||
|
end: "end",
|
||||||
|
exponent: "exponent",
|
||||||
|
fill: "fill",
|
||||||
|
fill_opacity: "fill-opacity",
|
||||||
|
fill_rule: "fill-rule",
|
||||||
|
filter: "filter",
|
||||||
|
filterRes: "filterRes",
|
||||||
|
filterUnits: "filterUnits",
|
||||||
|
flood_color: "flood-color",
|
||||||
|
flood_opacity: "flood-opacity",
|
||||||
|
font_family: "font-family",
|
||||||
|
font_size: "font-size",
|
||||||
|
font_size_adjust: "font-size-adjust",
|
||||||
|
font_stretch: "font-stretch",
|
||||||
|
font_style: "font-style",
|
||||||
|
font_variant: "font-variant",
|
||||||
|
font_weight: "font-weight",
|
||||||
|
format: "format",
|
||||||
|
from: "from",
|
||||||
|
fr: "fr",
|
||||||
|
fx: "fx",
|
||||||
|
fy: "fy",
|
||||||
|
g1: "g1",
|
||||||
|
g2: "g2",
|
||||||
|
glyph_name: "glyph-name",
|
||||||
|
glyph_orientation_horizontal: "glyph-orientation-horizontal",
|
||||||
|
glyph_orientation_vertical: "glyph-orientation-vertical",
|
||||||
|
glyphRef: "glyphRef",
|
||||||
|
gradientTransform: "gradientTransform",
|
||||||
|
gradientUnits: "gradientUnits",
|
||||||
|
hanging: "hanging",
|
||||||
|
height: "height",
|
||||||
|
href: "href",
|
||||||
|
hreflang: "hreflang",
|
||||||
|
horiz_adv_x: "horiz-adv-x",
|
||||||
|
horiz_origin_x: "horiz-origin-x",
|
||||||
|
id: "id",
|
||||||
|
ideographic: "ideographic",
|
||||||
|
image_rendering: "image-rendering",
|
||||||
|
_in: "_in",
|
||||||
|
in2: "in2",
|
||||||
|
intercept: "intercept",
|
||||||
|
k: "k",
|
||||||
|
k1: "k1",
|
||||||
|
k2: "k2",
|
||||||
|
k3: "k3",
|
||||||
|
k4: "k4",
|
||||||
|
kernelMatrix: "kernelMatrix",
|
||||||
|
kernelUnitLength: "kernelUnitLength",
|
||||||
|
kerning: "kerning",
|
||||||
|
keyPoints: "keyPoints",
|
||||||
|
keySplines: "keySplines",
|
||||||
|
keyTimes: "keyTimes",
|
||||||
|
lang: "lang",
|
||||||
|
lengthAdjust: "lengthAdjust",
|
||||||
|
letter_spacing: "letter-spacing",
|
||||||
|
lighting_color: "lighting-color",
|
||||||
|
limitingConeAngle: "limitingConeAngle",
|
||||||
|
local: "local",
|
||||||
|
marker_end: "marker-end",
|
||||||
|
marker_mid: "marker-mid",
|
||||||
|
marker_start: "marker_start",
|
||||||
|
markerHeight: "markerHeight",
|
||||||
|
markerUnits: "markerUnits",
|
||||||
|
markerWidth: "markerWidth",
|
||||||
|
mask: "mask",
|
||||||
|
maskContentUnits: "maskContentUnits",
|
||||||
|
maskUnits: "maskUnits",
|
||||||
|
mathematical: "mathematical",
|
||||||
|
max: "max",
|
||||||
|
media: "media",
|
||||||
|
method: "method",
|
||||||
|
min: "min",
|
||||||
|
mode: "mode",
|
||||||
|
name: "name",
|
||||||
|
numOctaves: "numOctaves",
|
||||||
|
offset: "offset",
|
||||||
|
opacity: "opacity",
|
||||||
|
operator: "operator",
|
||||||
|
order: "order",
|
||||||
|
orient: "orient",
|
||||||
|
orientation: "orientation",
|
||||||
|
origin: "origin",
|
||||||
|
overflow: "overflow",
|
||||||
|
overline_position: "overline-position",
|
||||||
|
overline_thickness: "overline-thickness",
|
||||||
|
panose_1: "panose-1",
|
||||||
|
paint_order: "paint-order",
|
||||||
|
path: "path",
|
||||||
|
pathLength: "pathLength",
|
||||||
|
patternContentUnits: "patternContentUnits",
|
||||||
|
patternTransform: "patternTransform",
|
||||||
|
patternUnits: "patternUnits",
|
||||||
|
ping: "ping",
|
||||||
|
pointer_events: "pointer-events",
|
||||||
|
points: "points",
|
||||||
|
pointsAtX: "pointsAtX",
|
||||||
|
pointsAtY: "pointsAtY",
|
||||||
|
pointsAtZ: "pointsAtZ",
|
||||||
|
preserveAlpha: "preserveAlpha",
|
||||||
|
preserveAspectRatio: "preserveAspectRatio",
|
||||||
|
primitiveUnits: "primitiveUnits",
|
||||||
|
r: "r",
|
||||||
|
radius: "radius",
|
||||||
|
referrerPolicy: "referrerPolicy",
|
||||||
|
refX: "refX",
|
||||||
|
refY: "refY",
|
||||||
|
rel: "rel",
|
||||||
|
rendering_intent: "rendering-intent",
|
||||||
|
repeatCount: "repeatCount",
|
||||||
|
repeatDur: "repeatDur",
|
||||||
|
requiredExtensions: "requiredExtensions",
|
||||||
|
requiredFeatures: "requiredFeatures",
|
||||||
|
restart: "restart",
|
||||||
|
result: "result",
|
||||||
|
role: "role",
|
||||||
|
rotate: "rotate",
|
||||||
|
rx: "rx",
|
||||||
|
ry: "ry",
|
||||||
|
scale: "scale",
|
||||||
|
seed: "seed",
|
||||||
|
shape_rendering: "shape-rendering",
|
||||||
|
slope: "slope",
|
||||||
|
spacing: "spacing",
|
||||||
|
specularConstant: "specularConstant",
|
||||||
|
specularExponent: "specularExponent",
|
||||||
|
speed: "speed",
|
||||||
|
spreadMethod: "spreadMethod",
|
||||||
|
startOffset: "startOffset",
|
||||||
|
stdDeviation: "stdDeviation",
|
||||||
|
stemh: "stemh",
|
||||||
|
stemv: "stemv",
|
||||||
|
stitchTiles: "stitchTiles",
|
||||||
|
stop_color: "stop_color",
|
||||||
|
stop_opacity: "stop_opacity",
|
||||||
|
strikethrough_position: "strikethrough-position",
|
||||||
|
strikethrough_thickness: "strikethrough-thickness",
|
||||||
|
string: "string",
|
||||||
|
stroke: "stroke",
|
||||||
|
stroke_dasharray: "stroke-dasharray",
|
||||||
|
stroke_dashoffset: "stroke-dashoffset",
|
||||||
|
stroke_linecap: "stroke-linecap",
|
||||||
|
stroke_linejoin: "stroke-linejoin",
|
||||||
|
stroke_miterlimit: "stroke-miterlimit",
|
||||||
|
stroke_opacity: "stroke-opacity",
|
||||||
|
stroke_width: "stroke-width",
|
||||||
|
style: "style",
|
||||||
|
surfaceScale: "surfaceScale",
|
||||||
|
systemLanguage: "systemLanguage",
|
||||||
|
tabindex: "tabindex",
|
||||||
|
tableValues: "tableValues",
|
||||||
|
target: "target",
|
||||||
|
targetX: "targetX",
|
||||||
|
targetY: "targetY",
|
||||||
|
text_anchor: "text-anchor",
|
||||||
|
text_decoration: "text-decoration",
|
||||||
|
text_rendering: "text-rendering",
|
||||||
|
textLength: "textLength",
|
||||||
|
to: "to",
|
||||||
|
transform: "transform",
|
||||||
|
transform_origin: "transform-origin",
|
||||||
|
r#type: "_type",
|
||||||
|
u1: "u1",
|
||||||
|
u2: "u2",
|
||||||
|
underline_position: "underline-position",
|
||||||
|
underline_thickness: "underline-thickness",
|
||||||
|
unicode: "unicode",
|
||||||
|
unicode_bidi: "unicode-bidi",
|
||||||
|
unicode_range: "unicode-range",
|
||||||
|
units_per_em: "units-per-em",
|
||||||
|
v_alphabetic: "v-alphabetic",
|
||||||
|
v_hanging: "v-hanging",
|
||||||
|
v_ideographic: "v-ideographic",
|
||||||
|
v_mathematical: "v-mathematical",
|
||||||
|
values: "values",
|
||||||
|
vector_effect: "vector-effect",
|
||||||
|
version: "version",
|
||||||
|
vert_adv_y: "vert-adv-y",
|
||||||
|
vert_origin_x: "vert-origin-x",
|
||||||
|
vert_origin_y: "vert-origin-y",
|
||||||
|
view_box: "viewBox",
|
||||||
|
view_target: "viewTarget",
|
||||||
|
visibility: "visibility",
|
||||||
|
width: "width",
|
||||||
|
widths: "widths",
|
||||||
|
word_spacing: "word-spacing",
|
||||||
|
writing_mode: "writing-mode",
|
||||||
|
x: "x",
|
||||||
|
x_height: "x-height",
|
||||||
|
x1: "x1",
|
||||||
|
x2: "x2",
|
||||||
|
xmlns: "xmlns",
|
||||||
|
x_channel_selector: "xChannelSelector",
|
||||||
|
y: "y",
|
||||||
|
y1: "y1",
|
||||||
|
y2: "y2",
|
||||||
|
y_channel_selector: "yChannelSelector",
|
||||||
|
z: "z",
|
||||||
|
zoomAndPan: "zoomAndPan",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
use dioxus_core::{Listener, VNode};
|
||||||
|
use dioxus_rsx::{
|
||||||
|
BodyNode, CallBody, Component, ElementAttr, ElementAttrNamed, IfmtInput, Segment,
|
||||||
|
};
|
||||||
|
use quote::{quote, ToTokens, TokenStreamExt};
|
||||||
|
use syn::{Expr, Ident, Result};
|
||||||
|
|
||||||
|
use crate::CodeLocation;
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CapturedContextBuilder {
|
||||||
|
pub ifmted: Vec<IfmtInput>,
|
||||||
|
pub components: Vec<Component>,
|
||||||
|
pub iterators: Vec<BodyNode>,
|
||||||
|
pub captured_expressions: Vec<Expr>,
|
||||||
|
pub listeners: Vec<ElementAttrNamed>,
|
||||||
|
pub custom_context: Option<Ident>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapturedContextBuilder {
|
||||||
|
pub fn extend(&mut self, other: CapturedContextBuilder) {
|
||||||
|
self.ifmted.extend(other.ifmted);
|
||||||
|
self.components.extend(other.components);
|
||||||
|
self.iterators.extend(other.iterators);
|
||||||
|
self.listeners.extend(other.listeners);
|
||||||
|
self.captured_expressions.extend(other.captured_expressions);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_call_body(body: CallBody) -> Result<Self> {
|
||||||
|
let mut new = Self {
|
||||||
|
custom_context: body.custom_context,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
for node in body.roots {
|
||||||
|
new.extend(Self::find_captured(node)?);
|
||||||
|
}
|
||||||
|
Ok(new)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_captured(node: BodyNode) -> Result<Self> {
|
||||||
|
let mut captured = CapturedContextBuilder::default();
|
||||||
|
match node {
|
||||||
|
BodyNode::Element(el) => {
|
||||||
|
for attr in el.attributes {
|
||||||
|
match attr.attr {
|
||||||
|
ElementAttr::AttrText { value, .. }
|
||||||
|
| ElementAttr::CustomAttrText { value, .. } => {
|
||||||
|
let value_tokens = value.to_token_stream();
|
||||||
|
let formated: IfmtInput = syn::parse2(value_tokens)?;
|
||||||
|
captured.ifmted.push(formated);
|
||||||
|
}
|
||||||
|
ElementAttr::AttrExpression { name: _, value } => {
|
||||||
|
captured.captured_expressions.push(value);
|
||||||
|
}
|
||||||
|
ElementAttr::CustomAttrExpression { name: _, value } => {
|
||||||
|
captured.captured_expressions.push(value);
|
||||||
|
}
|
||||||
|
ElementAttr::EventTokens { .. } => captured.listeners.push(attr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(key) = el.key {
|
||||||
|
let value_tokens = key.to_token_stream();
|
||||||
|
let formated: IfmtInput = syn::parse2(value_tokens)?;
|
||||||
|
captured.ifmted.push(formated);
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in el.children {
|
||||||
|
captured.extend(Self::find_captured(child)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BodyNode::Component(comp) => {
|
||||||
|
captured.components.push(comp);
|
||||||
|
}
|
||||||
|
BodyNode::Text(t) => {
|
||||||
|
let tokens = t.to_token_stream();
|
||||||
|
let formated: IfmtInput = syn::parse2(tokens).unwrap();
|
||||||
|
captured.ifmted.push(formated);
|
||||||
|
}
|
||||||
|
BodyNode::RawExpr(_) => captured.iterators.push(node),
|
||||||
|
}
|
||||||
|
Ok(captured)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for CapturedContextBuilder {
|
||||||
|
fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) {
|
||||||
|
let CapturedContextBuilder {
|
||||||
|
ifmted,
|
||||||
|
components,
|
||||||
|
iterators,
|
||||||
|
captured_expressions,
|
||||||
|
listeners,
|
||||||
|
custom_context: _,
|
||||||
|
} = self;
|
||||||
|
let listeners_str = listeners
|
||||||
|
.iter()
|
||||||
|
.map(|comp| comp.to_token_stream().to_string());
|
||||||
|
let compontents_str = components
|
||||||
|
.iter()
|
||||||
|
.map(|comp| comp.to_token_stream().to_string());
|
||||||
|
let iterators_str = iterators.iter().map(|node| match node {
|
||||||
|
BodyNode::RawExpr(expr) => expr.to_token_stream().to_string(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
});
|
||||||
|
let captured: Vec<_> = ifmted
|
||||||
|
.iter()
|
||||||
|
.flat_map(|input| input.segments.iter())
|
||||||
|
.filter_map(|seg| match seg {
|
||||||
|
Segment::Formatted {
|
||||||
|
format_args,
|
||||||
|
segment,
|
||||||
|
} => {
|
||||||
|
let expr = segment.to_token_stream();
|
||||||
|
let as_string = expr.to_string();
|
||||||
|
let format_expr = if format_args.is_empty() {
|
||||||
|
"{".to_string() + format_args + "}"
|
||||||
|
} else {
|
||||||
|
"{".to_string() + ":" + format_args + "}"
|
||||||
|
};
|
||||||
|
Some(quote! {
|
||||||
|
FormattedArg{
|
||||||
|
expr: #as_string,
|
||||||
|
format_args: #format_args,
|
||||||
|
result: format!(#format_expr, #expr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let captured_attr_expressions_text = captured_expressions
|
||||||
|
.iter()
|
||||||
|
.map(|e| format!("{}", e.to_token_stream()));
|
||||||
|
tokens.append_all(quote! {
|
||||||
|
CapturedContext {
|
||||||
|
captured: IfmtArgs{
|
||||||
|
named_args: vec![#(#captured),*]
|
||||||
|
},
|
||||||
|
components: vec![#((#compontents_str, #components)),*],
|
||||||
|
iterators: vec![#((#iterators_str, #iterators)),*],
|
||||||
|
expressions: vec![#((#captured_attr_expressions_text, #captured_expressions.to_string())),*],
|
||||||
|
listeners: vec![#((#listeners_str, #listeners)),*],
|
||||||
|
location: code_location.clone()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CapturedContext<'a> {
|
||||||
|
// map of the variable name to the formated value
|
||||||
|
pub captured: IfmtArgs,
|
||||||
|
// map of the attribute name and element path to the formated value
|
||||||
|
// pub captured_attribute_values: IfmtArgs,
|
||||||
|
// the only thing we can update in component is the children
|
||||||
|
pub components: Vec<(&'static str, VNode<'a>)>,
|
||||||
|
// we can't reasonably interpert iterators, so they are staticly inserted
|
||||||
|
pub iterators: Vec<(&'static str, VNode<'a>)>,
|
||||||
|
// map expression to the value resulting from the expression
|
||||||
|
pub expressions: Vec<(&'static str, String)>,
|
||||||
|
// map listener code to the resulting listener
|
||||||
|
pub listeners: Vec<(&'static str, Listener<'a>)>,
|
||||||
|
// used to provide better error messages
|
||||||
|
pub location: CodeLocation,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IfmtArgs {
|
||||||
|
// All expressions that have been resolved
|
||||||
|
pub named_args: Vec<FormattedArg>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A formated segment that has been resolved
|
||||||
|
pub struct FormattedArg {
|
||||||
|
pub expr: &'static str,
|
||||||
|
pub format_args: &'static str,
|
||||||
|
pub result: String,
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,39 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::CodeLocation;
|
||||||
|
|
||||||
|
/// An error produced when interperting the rsx
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Error {
|
||||||
|
ParseError(ParseError),
|
||||||
|
RecompileRequiredError(RecompileReason),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum RecompileReason {
|
||||||
|
CapturedVariable(String),
|
||||||
|
CapturedExpression(String),
|
||||||
|
CapturedComponent(String),
|
||||||
|
CapturedListener(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ParseError {
|
||||||
|
pub message: String,
|
||||||
|
pub location: CodeLocation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseError {
|
||||||
|
pub fn new(error: syn::Error, mut location: CodeLocation) -> Self {
|
||||||
|
let message = error.to_string();
|
||||||
|
let syn_call_site = error.span().start();
|
||||||
|
location.line += syn_call_site.line as u32;
|
||||||
|
if syn_call_site.line == 0 {
|
||||||
|
location.column += syn_call_site.column as u32;
|
||||||
|
} else {
|
||||||
|
location.column = syn_call_site.column as u32;
|
||||||
|
}
|
||||||
|
location.column += 1;
|
||||||
|
ParseError { message, location }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
use dioxus_core::{Attribute, AttributeValue, NodeFactory, VNode};
|
||||||
|
use dioxus_rsx::{BodyNode, CallBody, ElementAttr, IfmtInput, Segment};
|
||||||
|
use quote::ToTokens;
|
||||||
|
use quote::__private::Span;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use syn::{parse2, parse_str, Expr};
|
||||||
|
|
||||||
|
use crate::attributes::attrbute_to_static_str;
|
||||||
|
use crate::captuered_context::{CapturedContext, IfmtArgs};
|
||||||
|
use crate::elements::element_to_static_str;
|
||||||
|
use crate::error::{Error, ParseError, RecompileReason};
|
||||||
|
|
||||||
|
fn resolve_ifmt(ifmt: &IfmtInput, captured: &IfmtArgs) -> Result<String, Error> {
|
||||||
|
let mut result = String::new();
|
||||||
|
for seg in &ifmt.segments {
|
||||||
|
match seg {
|
||||||
|
Segment::Formatted {
|
||||||
|
segment,
|
||||||
|
format_args,
|
||||||
|
} => {
|
||||||
|
let expr = segment.to_token_stream();
|
||||||
|
let expr: Expr = parse2(expr).unwrap();
|
||||||
|
let search = captured.named_args.iter().find(|fmted| {
|
||||||
|
parse_str::<Expr>(fmted.expr).unwrap() == expr
|
||||||
|
&& fmted.format_args == format_args
|
||||||
|
});
|
||||||
|
match search {
|
||||||
|
Some(formatted) => {
|
||||||
|
result.push_str(&formatted.result);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let expr_str = segment.to_token_stream().to_string();
|
||||||
|
return Err(Error::RecompileRequiredError(
|
||||||
|
RecompileReason::CapturedExpression(format!(
|
||||||
|
"could not resolve {{{}:{}}}",
|
||||||
|
expr_str, format_args
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Segment::Literal(lit) => result.push_str(lit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build<'a>(
|
||||||
|
rsx: CallBody,
|
||||||
|
mut ctx: CapturedContext<'a>,
|
||||||
|
factory: &NodeFactory<'a>,
|
||||||
|
) -> Result<VNode<'a>, Error> {
|
||||||
|
let children_built = factory.bump().alloc(Vec::new());
|
||||||
|
for child in rsx.roots {
|
||||||
|
children_built.push(build_node(child, &mut ctx, factory)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if children_built.len() == 1 {
|
||||||
|
Ok(children_built.pop().unwrap())
|
||||||
|
} else {
|
||||||
|
Ok(factory.fragment_from_iter(children_built.iter()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_node<'a>(
|
||||||
|
node: BodyNode,
|
||||||
|
ctx: &mut CapturedContext<'a>,
|
||||||
|
factory: &NodeFactory<'a>,
|
||||||
|
) -> Result<VNode<'a>, Error> {
|
||||||
|
let bump = factory.bump();
|
||||||
|
match node {
|
||||||
|
BodyNode::Text(text) => {
|
||||||
|
let ifmt = IfmtInput::from_str(&text.value())
|
||||||
|
.map_err(|err| Error::ParseError(ParseError::new(err, ctx.location.clone())))?;
|
||||||
|
let text = bump.alloc(resolve_ifmt(&ifmt, &ctx.captured)?);
|
||||||
|
Ok(factory.text(format_args!("{}", text)))
|
||||||
|
}
|
||||||
|
BodyNode::Element(el) => {
|
||||||
|
let attributes: &mut Vec<Attribute> = bump.alloc(Vec::new());
|
||||||
|
for attr in &el.attributes {
|
||||||
|
match &attr.attr {
|
||||||
|
ElementAttr::AttrText { .. } | ElementAttr::CustomAttrText { .. } => {
|
||||||
|
let (name, value, span): (String, IfmtInput, Span) = match &attr.attr {
|
||||||
|
ElementAttr::AttrText { name, value } => (
|
||||||
|
name.to_string(),
|
||||||
|
IfmtInput::from_str(&value.value()).map_err(|err| {
|
||||||
|
Error::ParseError(ParseError::new(err, ctx.location.clone()))
|
||||||
|
})?,
|
||||||
|
name.span(),
|
||||||
|
),
|
||||||
|
ElementAttr::CustomAttrText { name, value } => (
|
||||||
|
name.value(),
|
||||||
|
IfmtInput::from_str(&value.value()).map_err(|err| {
|
||||||
|
Error::ParseError(ParseError::new(err, ctx.location.clone()))
|
||||||
|
})?,
|
||||||
|
name.span(),
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((name, namespace)) = attrbute_to_static_str(&name) {
|
||||||
|
let value = bump.alloc(resolve_ifmt(&value, &ctx.captured)?);
|
||||||
|
attributes.push(Attribute {
|
||||||
|
name,
|
||||||
|
value: AttributeValue::Text(value),
|
||||||
|
is_static: true,
|
||||||
|
is_volatile: false,
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError(ParseError::new(
|
||||||
|
syn::Error::new(span, format!("unknown attribute: {}", name)),
|
||||||
|
ctx.location.clone(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElementAttr::AttrExpression { .. }
|
||||||
|
| ElementAttr::CustomAttrExpression { .. } => {
|
||||||
|
let (name, value) = match &attr.attr {
|
||||||
|
ElementAttr::AttrExpression { name, value } => {
|
||||||
|
(name.to_string(), value)
|
||||||
|
}
|
||||||
|
ElementAttr::CustomAttrExpression { name, value } => {
|
||||||
|
(name.value(), value)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
if let Some((_, resulting_value)) = ctx
|
||||||
|
.expressions
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _)| parse_str::<Expr>(*n).unwrap() == *value)
|
||||||
|
{
|
||||||
|
if let Some((name, namespace)) = attrbute_to_static_str(&name) {
|
||||||
|
let value = bump.alloc(resulting_value.clone());
|
||||||
|
attributes.push(Attribute {
|
||||||
|
name,
|
||||||
|
value: AttributeValue::Text(value),
|
||||||
|
is_static: true,
|
||||||
|
is_volatile: false,
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::RecompileRequiredError(
|
||||||
|
RecompileReason::CapturedExpression(
|
||||||
|
value.into_token_stream().to_string(),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let children = bump.alloc(Vec::new());
|
||||||
|
for child in el.children {
|
||||||
|
let node = build_node(child, ctx, factory)?;
|
||||||
|
children.push(node);
|
||||||
|
}
|
||||||
|
let listeners = bump.alloc(Vec::new());
|
||||||
|
for attr in el.attributes {
|
||||||
|
if let ElementAttr::EventTokens { .. } = attr.attr {
|
||||||
|
let expr: Expr = parse2(attr.to_token_stream()).map_err(|err| {
|
||||||
|
Error::ParseError(ParseError::new(err, ctx.location.clone()))
|
||||||
|
})?;
|
||||||
|
if let Some(idx) = ctx.listeners.iter().position(|(code, _)| {
|
||||||
|
if let Ok(parsed) = parse_str::<Expr>(*code) {
|
||||||
|
parsed == expr
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
let (_, listener) = ctx.listeners.remove(idx);
|
||||||
|
listeners.push(listener)
|
||||||
|
} else {
|
||||||
|
return Err(Error::RecompileRequiredError(
|
||||||
|
RecompileReason::CapturedListener(expr.to_token_stream().to_string()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tag = bump.alloc(el.name.to_string());
|
||||||
|
if let Some((tag, ns)) = element_to_static_str(tag) {
|
||||||
|
match el.key {
|
||||||
|
None => Ok(factory.raw_element(
|
||||||
|
tag,
|
||||||
|
ns,
|
||||||
|
listeners,
|
||||||
|
attributes.as_slice(),
|
||||||
|
children.as_slice(),
|
||||||
|
None,
|
||||||
|
)),
|
||||||
|
Some(lit) => {
|
||||||
|
let ifmt: IfmtInput = parse_str(&lit.value()).map_err(|err| {
|
||||||
|
Error::ParseError(ParseError::new(err, ctx.location.clone()))
|
||||||
|
})?;
|
||||||
|
let key = bump.alloc(resolve_ifmt(&ifmt, &ctx.captured)?);
|
||||||
|
|
||||||
|
Ok(factory.raw_element(
|
||||||
|
tag,
|
||||||
|
ns,
|
||||||
|
listeners,
|
||||||
|
attributes.as_slice(),
|
||||||
|
children.as_slice(),
|
||||||
|
Some(format_args!("{}", key)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(Error::ParseError(ParseError::new(
|
||||||
|
syn::Error::new(el.name.span(), format!("unknown element: {}", tag)),
|
||||||
|
ctx.location.clone(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BodyNode::Component(comp) => {
|
||||||
|
let expr: Expr = parse2(comp.to_token_stream())
|
||||||
|
.map_err(|err| Error::ParseError(ParseError::new(err, ctx.location.clone())))?;
|
||||||
|
if let Some(idx) = ctx.components.iter().position(|(code, _)| {
|
||||||
|
if let Ok(parsed) = parse_str::<Expr>(*code) {
|
||||||
|
parsed == expr
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
let (_, vnode) = ctx.components.remove(idx);
|
||||||
|
Ok(vnode)
|
||||||
|
} else {
|
||||||
|
Err(Error::RecompileRequiredError(
|
||||||
|
RecompileReason::CapturedComponent(comp.name.to_token_stream().to_string()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BodyNode::RawExpr(iterator) => {
|
||||||
|
if let Some(idx) = ctx.iterators.iter().position(|(code, _)| {
|
||||||
|
if let Ok(parsed) = parse_str::<Expr>(*code) {
|
||||||
|
parsed == iterator
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
let (_, vnode) = ctx.iterators.remove(idx);
|
||||||
|
Ok(vnode)
|
||||||
|
} else {
|
||||||
|
Err(Error::RecompileRequiredError(
|
||||||
|
RecompileReason::CapturedExpression(iterator.to_token_stream().to_string()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
use captuered_context::CapturedContext;
|
||||||
|
use dioxus_core::{NodeFactory, SchedulerMsg, VNode};
|
||||||
|
use dioxus_hooks::UnboundedSender;
|
||||||
|
use error::{Error, ParseError};
|
||||||
|
use interperter::build;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{RwLock, RwLockReadGuard};
|
||||||
|
use syn::parse_str;
|
||||||
|
|
||||||
|
mod attributes;
|
||||||
|
pub mod captuered_context;
|
||||||
|
mod elements;
|
||||||
|
pub mod error;
|
||||||
|
mod interperter;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
/// This a a global store of the current rsx text for each call to rsx
|
||||||
|
// Global mutable data is genrally not great, but it allows users to not worry about passing down the text RsxContex every time they switch to hot reloading.
|
||||||
|
pub static ref RSX_CONTEXT: RsxContext = RsxContext::new(RsxData::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
// the location of the code relative to the current crate based on [std::panic::Location]
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CodeLocation {
|
||||||
|
pub file: String,
|
||||||
|
pub line: u32,
|
||||||
|
pub column: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the resolved rsx given the origional rsx, a captured context of dynamic components, and a factory to build the resulting node
|
||||||
|
#[cfg_attr(
|
||||||
|
not(debug_assertions),
|
||||||
|
deprecated(
|
||||||
|
note = "The hot reload feature is enabled in release mode. This feature should be disabled for production builds."
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub fn resolve_scope<'a>(
|
||||||
|
location: CodeLocation,
|
||||||
|
rsx: &'static str,
|
||||||
|
captured: CapturedContext<'a>,
|
||||||
|
factory: NodeFactory<'a>,
|
||||||
|
) -> VNode<'a> {
|
||||||
|
let rsx_text_index = &*RSX_CONTEXT;
|
||||||
|
// only the insert the rsx text once
|
||||||
|
if !rsx_text_index.read().hm.contains_key(&location) {
|
||||||
|
rsx_text_index.insert(location.clone(), rsx.to_string());
|
||||||
|
}
|
||||||
|
if let Some(text) = {
|
||||||
|
let read = rsx_text_index.read();
|
||||||
|
// clone prevents deadlock on nested rsx calls
|
||||||
|
read.hm.get(&location).cloned()
|
||||||
|
} {
|
||||||
|
match interpert_rsx(factory, &text, captured) {
|
||||||
|
Ok(vnode) => vnode,
|
||||||
|
Err(err) => {
|
||||||
|
rsx_text_index.report_error(err);
|
||||||
|
factory.text(format_args!(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("rsx: line number {:?} not found in rsx index", location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpert_rsx<'a, 'b>(
|
||||||
|
factory: dioxus_core::NodeFactory<'a>,
|
||||||
|
text: &str,
|
||||||
|
context: captuered_context::CapturedContext<'a>,
|
||||||
|
) -> Result<VNode<'a>, Error> {
|
||||||
|
build(
|
||||||
|
parse_str(text)
|
||||||
|
.map_err(|err| Error::ParseError(ParseError::new(err, context.location.clone())))?,
|
||||||
|
context,
|
||||||
|
&factory,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the code location of the code that called this function
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! get_line_num {
|
||||||
|
() => {{
|
||||||
|
let line = line!();
|
||||||
|
let column = column!();
|
||||||
|
let file = file!();
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
let file = env!("CARGO_MANIFEST_DIR").to_string() + "\\" + file!();
|
||||||
|
#[cfg(unix)]
|
||||||
|
let file = env!("CARGO_MANIFEST_DIR").to_string() + "/" + file!();
|
||||||
|
CodeLocation {
|
||||||
|
file: file.to_string(),
|
||||||
|
line: line,
|
||||||
|
column: column,
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle to the rsx context with interior mutability
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RsxContext {
|
||||||
|
data: RwLock<RsxData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A store of the text for the rsx macro for each call to rsx
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct RsxData {
|
||||||
|
pub hm: HashMap<CodeLocation, String>,
|
||||||
|
pub error_handler: Option<Box<dyn ErrorHandler>>,
|
||||||
|
pub scheduler_channel: Option<UnboundedSender<SchedulerMsg>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for RsxData {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("RsxData").field("hm", &self.hm).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RsxContext {
|
||||||
|
pub fn new(data: RsxData) -> Self {
|
||||||
|
Self {
|
||||||
|
data: RwLock::new(data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the text for an rsx call at some location
|
||||||
|
pub fn insert(&self, loc: CodeLocation, text: String) {
|
||||||
|
let mut write = self.data.write().unwrap();
|
||||||
|
write.hm.insert(loc, text);
|
||||||
|
if let Some(channel) = &mut write.scheduler_channel {
|
||||||
|
channel.unbounded_send(SchedulerMsg::DirtyAll).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the text for many rsx calls
|
||||||
|
pub fn extend(&self, msg: SetManyRsxMessage) {
|
||||||
|
let mut write = self.data.write().unwrap();
|
||||||
|
for rsx in msg.0 {
|
||||||
|
write.hm.insert(rsx.location, rsx.new_text);
|
||||||
|
}
|
||||||
|
if let Some(channel) = &mut write.scheduler_channel {
|
||||||
|
channel.unbounded_send(SchedulerMsg::DirtyAll).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(&self) -> RwLockReadGuard<RsxData> {
|
||||||
|
self.data.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_error(&self, error: Error) {
|
||||||
|
if let Some(handler) = &self.data.write().unwrap().error_handler {
|
||||||
|
handler.handle_error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the handler for errors interperting the rsx
|
||||||
|
pub fn set_error_handler(&self, handler: impl ErrorHandler + 'static) {
|
||||||
|
self.data.write().unwrap().error_handler = Some(Box::new(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provide the scduler channel from [dioxus_code::VirtualDom::get_scheduler_channel].
|
||||||
|
/// The channel allows the interpreter to force re-rendering of the dom when the rsx is changed.
|
||||||
|
pub fn provide_scheduler_channel(&self, channel: UnboundedSender<SchedulerMsg>) {
|
||||||
|
self.data.write().unwrap().scheduler_channel = Some(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A error handler for errors reported by the rsx interperter
|
||||||
|
pub trait ErrorHandler: Send + Sync {
|
||||||
|
fn handle_error(&self, err: Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct SetRsxMessage {
|
||||||
|
pub location: CodeLocation,
|
||||||
|
pub new_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set many rsx texts at once to avoid duplicate errors
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct SetManyRsxMessage(pub Vec<SetRsxMessage>);
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
use dioxus_core::prelude::*;
|
||||||
|
use dioxus_core_macro::*;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use dioxus_rsx_interpreter::{
|
||||||
|
captuered_context::{CapturedContext, FormattedArg, IfmtArgs},
|
||||||
|
CodeLocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn render_basic() {
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
|
||||||
|
let dom = VirtualDom::new(Base);
|
||||||
|
let static_vnodes = rsx!(div{"hello world"});
|
||||||
|
let location = CodeLocation {
|
||||||
|
file: String::new(),
|
||||||
|
line: 0,
|
||||||
|
column: 0,
|
||||||
|
};
|
||||||
|
let empty_context = CapturedContext {
|
||||||
|
captured: IfmtArgs {
|
||||||
|
named_args: Vec::new(),
|
||||||
|
},
|
||||||
|
components: Vec::new(),
|
||||||
|
iterators: Vec::new(),
|
||||||
|
expressions: Vec::new(),
|
||||||
|
listeners: Vec::new(),
|
||||||
|
location: location.clone(),
|
||||||
|
};
|
||||||
|
let interperted_vnodes = LazyNodes::new(|factory| {
|
||||||
|
dioxus_rsx_interpreter::resolve_scope(
|
||||||
|
location,
|
||||||
|
"div{\"hello world\"}",
|
||||||
|
empty_context,
|
||||||
|
factory,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
|
||||||
|
let static_vnodes = dom.render_vnodes(static_vnodes);
|
||||||
|
assert!(check_eq(interperted_vnodes, static_vnodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn render_nested() {
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
|
||||||
|
let dom = VirtualDom::new(Base);
|
||||||
|
let static_vnodes = rsx! {
|
||||||
|
div {
|
||||||
|
p { "hello world" }
|
||||||
|
div {
|
||||||
|
p { "hello world" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let location = CodeLocation {
|
||||||
|
file: String::new(),
|
||||||
|
line: 1,
|
||||||
|
column: 0,
|
||||||
|
};
|
||||||
|
let empty_context = CapturedContext {
|
||||||
|
captured: IfmtArgs {
|
||||||
|
named_args: Vec::new(),
|
||||||
|
},
|
||||||
|
components: Vec::new(),
|
||||||
|
iterators: Vec::new(),
|
||||||
|
expressions: Vec::new(),
|
||||||
|
listeners: Vec::new(),
|
||||||
|
location: location.clone(),
|
||||||
|
};
|
||||||
|
let interperted_vnodes = LazyNodes::new(|factory| {
|
||||||
|
dioxus_rsx_interpreter::resolve_scope(
|
||||||
|
location,
|
||||||
|
r#"div {
|
||||||
|
p { "hello world" }
|
||||||
|
div {
|
||||||
|
p { "hello world" }
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
empty_context,
|
||||||
|
factory,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
|
||||||
|
let static_vnodes = dom.render_vnodes(static_vnodes);
|
||||||
|
assert!(check_eq(interperted_vnodes, static_vnodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn render_component() {
|
||||||
|
fn Comp(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
|
||||||
|
let dom = VirtualDom::new(Base);
|
||||||
|
let static_vnodes = rsx! {
|
||||||
|
div {
|
||||||
|
Comp {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let location = CodeLocation {
|
||||||
|
file: String::new(),
|
||||||
|
line: 2,
|
||||||
|
column: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let interperted_vnodes = LazyNodes::new(|factory| {
|
||||||
|
let context = CapturedContext {
|
||||||
|
captured: IfmtArgs {
|
||||||
|
named_args: Vec::new(),
|
||||||
|
},
|
||||||
|
components: vec![(
|
||||||
|
r#"__cx.component(Comp, fc_to_builder(Comp).build(), None, "Comp")"#,
|
||||||
|
factory.component(Comp, (), None, "Comp"),
|
||||||
|
)],
|
||||||
|
iterators: Vec::new(),
|
||||||
|
expressions: Vec::new(),
|
||||||
|
listeners: Vec::new(),
|
||||||
|
location: location.clone(),
|
||||||
|
};
|
||||||
|
dioxus_rsx_interpreter::resolve_scope(
|
||||||
|
location,
|
||||||
|
r#"div {
|
||||||
|
Comp {}
|
||||||
|
}"#,
|
||||||
|
context,
|
||||||
|
factory,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
|
||||||
|
let static_vnodes = dom.render_vnodes(static_vnodes);
|
||||||
|
println!("{:#?}", interperted_vnodes);
|
||||||
|
println!("{:#?}", static_vnodes);
|
||||||
|
assert!(check_eq(interperted_vnodes, static_vnodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn render_iterator() {
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
|
||||||
|
let dom = VirtualDom::new(Base);
|
||||||
|
let iter = (0..10).map(|i| dom.render_vnodes(rsx! {"{i}"}));
|
||||||
|
let static_vnodes = rsx! {
|
||||||
|
div {
|
||||||
|
iter
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let location = CodeLocation {
|
||||||
|
file: String::new(),
|
||||||
|
line: 3,
|
||||||
|
column: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let interperted_vnodes = LazyNodes::new(|factory| {
|
||||||
|
let context = CapturedContext {
|
||||||
|
captured: IfmtArgs {
|
||||||
|
named_args: Vec::new(),
|
||||||
|
},
|
||||||
|
components: Vec::new(),
|
||||||
|
iterators: vec![(
|
||||||
|
r#"
|
||||||
|
(0..10).map(|i| dom.render_vnodes(rsx!{"{i}"}))"#,
|
||||||
|
factory.fragment_from_iter((0..10).map(|i| factory.text(format_args!("{i}")))),
|
||||||
|
)],
|
||||||
|
expressions: Vec::new(),
|
||||||
|
listeners: Vec::new(),
|
||||||
|
location: location.clone(),
|
||||||
|
};
|
||||||
|
dioxus_rsx_interpreter::resolve_scope(
|
||||||
|
location,
|
||||||
|
r#"div {
|
||||||
|
(0..10).map(|i| dom.render_vnodes(rsx!{"{i}"}))
|
||||||
|
}"#,
|
||||||
|
context,
|
||||||
|
factory,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
|
||||||
|
let static_vnodes = dom.render_vnodes(static_vnodes);
|
||||||
|
println!("{:#?}", interperted_vnodes);
|
||||||
|
println!("{:#?}", static_vnodes);
|
||||||
|
assert!(check_eq(interperted_vnodes, static_vnodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn render_captured_variable() {
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
|
||||||
|
let dom = VirtualDom::new(Base);
|
||||||
|
|
||||||
|
let x = 10;
|
||||||
|
let static_vnodes = rsx! {
|
||||||
|
div {
|
||||||
|
"{x}"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let location = CodeLocation {
|
||||||
|
file: String::new(),
|
||||||
|
line: 4,
|
||||||
|
column: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let interperted_vnodes = LazyNodes::new(|factory| {
|
||||||
|
let context = CapturedContext {
|
||||||
|
captured: IfmtArgs {
|
||||||
|
named_args: vec![FormattedArg {
|
||||||
|
expr: "x",
|
||||||
|
format_args: "",
|
||||||
|
result: x.to_string(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
components: Vec::new(),
|
||||||
|
iterators: Vec::new(),
|
||||||
|
expressions: Vec::new(),
|
||||||
|
listeners: Vec::new(),
|
||||||
|
location: location.clone(),
|
||||||
|
};
|
||||||
|
dioxus_rsx_interpreter::resolve_scope(
|
||||||
|
location,
|
||||||
|
r#"div {
|
||||||
|
"{x}"
|
||||||
|
}"#,
|
||||||
|
context,
|
||||||
|
factory,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
|
||||||
|
let static_vnodes = dom.render_vnodes(static_vnodes);
|
||||||
|
println!("{:#?}", interperted_vnodes);
|
||||||
|
println!("{:#?}", static_vnodes);
|
||||||
|
assert!(check_eq(interperted_vnodes, static_vnodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn render_listener() {
|
||||||
|
fn Base(cx: Scope) -> Element {
|
||||||
|
rsx!(cx, div {})
|
||||||
|
}
|
||||||
|
|
||||||
|
let dom = VirtualDom::new(Base);
|
||||||
|
let static_vnodes = rsx! {
|
||||||
|
div {
|
||||||
|
onclick: |_| println!("clicked")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let location = CodeLocation {
|
||||||
|
file: String::new(),
|
||||||
|
line: 5,
|
||||||
|
column: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let interperted_vnodes = LazyNodes::new(|factory| {
|
||||||
|
let f = |_| println!("clicked");
|
||||||
|
let f = factory.bump().alloc(f);
|
||||||
|
let context = CapturedContext {
|
||||||
|
captured: IfmtArgs {
|
||||||
|
named_args: Vec::new(),
|
||||||
|
},
|
||||||
|
components: Vec::new(),
|
||||||
|
iterators: Vec::new(),
|
||||||
|
expressions: Vec::new(),
|
||||||
|
listeners: vec![(
|
||||||
|
r#"dioxus_elements::on::onclick(__cx, |_| println!("clicked"))"#,
|
||||||
|
dioxus_elements::on::onclick(factory, f),
|
||||||
|
)],
|
||||||
|
location: location.clone(),
|
||||||
|
};
|
||||||
|
dioxus_rsx_interpreter::resolve_scope(
|
||||||
|
location,
|
||||||
|
r#"div {
|
||||||
|
onclick: |_| println!("clicked")
|
||||||
|
}"#,
|
||||||
|
context,
|
||||||
|
factory,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
|
||||||
|
let static_vnodes = dom.render_vnodes(static_vnodes);
|
||||||
|
println!("{:#?}", interperted_vnodes);
|
||||||
|
println!("{:#?}", static_vnodes);
|
||||||
|
assert!(check_eq(interperted_vnodes, static_vnodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_eq<'a>(a: &'a VNode<'a>, b: &'a VNode<'a>) -> bool {
|
||||||
|
match (a, b) {
|
||||||
|
(VNode::Text(t_a), VNode::Text(t_b)) => t_a.text == t_b.text,
|
||||||
|
(VNode::Element(e_a), VNode::Element(e_b)) => {
|
||||||
|
e_a.attributes
|
||||||
|
.iter()
|
||||||
|
.zip(e_b.attributes.iter())
|
||||||
|
.all(|(a, b)| {
|
||||||
|
a.is_static == b.is_static
|
||||||
|
&& a.is_volatile == b.is_volatile
|
||||||
|
&& a.name == b.name
|
||||||
|
&& a.value == b.value
|
||||||
|
&& a.namespace == b.namespace
|
||||||
|
})
|
||||||
|
&& e_a
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.zip(e_b.children.iter())
|
||||||
|
.all(|(a, b)| check_eq(a, b))
|
||||||
|
&& e_a.key == e_b.key
|
||||||
|
&& e_a.tag == e_b.tag
|
||||||
|
&& e_a.namespace == e_b.namespace
|
||||||
|
&& e_a
|
||||||
|
.listeners
|
||||||
|
.iter()
|
||||||
|
.zip(e_b.listeners.iter())
|
||||||
|
.all(|(a, b)| a.event == b.event)
|
||||||
|
}
|
||||||
|
(VNode::Fragment(f_a), VNode::Fragment(f_b)) => {
|
||||||
|
f_a.key == f_b.key
|
||||||
|
&& f_a
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.zip(f_b.children.iter())
|
||||||
|
.all(|(a, b)| check_eq(a, b))
|
||||||
|
}
|
||||||
|
(VNode::Component(c_a), VNode::Component(c_b)) => {
|
||||||
|
c_a.can_memoize == c_b.can_memoize
|
||||||
|
&& c_a.key == c_b.key
|
||||||
|
&& c_a.fn_name == c_b.fn_name
|
||||||
|
&& c_a.user_fc == c_b.user_fc
|
||||||
|
}
|
||||||
|
(VNode::Placeholder(_), VNode::Placeholder(_)) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,3 +27,8 @@ taffy = "0.1.0"
|
|||||||
smallvec = "1.6"
|
smallvec = "1.6"
|
||||||
fxhash = "0.2"
|
fxhash = "0.2"
|
||||||
anymap = "0.12.1"
|
anymap = "0.12.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
dioxus-core-macro = { path = "../core-macro", version = "^0.2.1" }
|
||||||
|
dioxus-hooks = { path = "../hooks", version = "^0.2.1" }
|
||||||
|
tokio = { version = "1" }
|
||||||
@@ -85,11 +85,10 @@ Rink features:
|
|||||||
- [x] Flexbox based layout system
|
- [x] Flexbox based layout system
|
||||||
- [ ] CSS selectors
|
- [ ] CSS selectors
|
||||||
- [x] inline CSS support
|
- [x] inline CSS support
|
||||||
- [ ] Built-in focusing system
|
- [x] Built-in focusing system
|
||||||
- [ ] high-quality keyboard support
|
- [ ] high-quality keyboard support
|
||||||
- [ ] Support for events, hooks, and callbacks
|
* [ ] Support for events, hooks, and callbacks<sup>1</sup>
|
||||||
* [ ] Html tags<sup>1</sup>
|
* [ ] Html tags<sup>2</sup>
|
||||||
|
|
||||||
<sup>1</sup> Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality.
|
|
||||||
|
|
||||||
|
|
||||||
|
<sup>1</sup> Basic keyboard and mouse events are implemented.
|
||||||
|
<sup>2</sup> Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality.
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
use crate::{node::PreventDefault, Dom};
|
||||||
|
|
||||||
|
use dioxus_core::ElementId;
|
||||||
|
use dioxus_native_core::utils::{ElementProduced, PersistantElementIter};
|
||||||
|
use dioxus_native_core_macro::sorted_str_slice;
|
||||||
|
|
||||||
|
use std::{cmp::Ordering, num::NonZeroU16};
|
||||||
|
|
||||||
|
use dioxus_native_core::{
|
||||||
|
node_ref::{AttributeMask, NodeMask, NodeView},
|
||||||
|
real_dom::NodeType,
|
||||||
|
state::NodeDepState,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub(crate) enum FocusLevel {
|
||||||
|
Unfocusable,
|
||||||
|
Focusable,
|
||||||
|
Ordered(std::num::NonZeroU16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusLevel {
|
||||||
|
pub fn focusable(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
FocusLevel::Unfocusable => false,
|
||||||
|
FocusLevel::Focusable => true,
|
||||||
|
FocusLevel::Ordered(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for FocusLevel {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
match (self, other) {
|
||||||
|
(FocusLevel::Unfocusable, FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Equal),
|
||||||
|
(FocusLevel::Unfocusable, FocusLevel::Focusable) => Some(std::cmp::Ordering::Less),
|
||||||
|
(FocusLevel::Unfocusable, FocusLevel::Ordered(_)) => Some(std::cmp::Ordering::Less),
|
||||||
|
(FocusLevel::Focusable, FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Greater),
|
||||||
|
(FocusLevel::Focusable, FocusLevel::Focusable) => Some(std::cmp::Ordering::Equal),
|
||||||
|
(FocusLevel::Focusable, FocusLevel::Ordered(_)) => Some(std::cmp::Ordering::Greater),
|
||||||
|
(FocusLevel::Ordered(_), FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Greater),
|
||||||
|
(FocusLevel::Ordered(_), FocusLevel::Focusable) => Some(std::cmp::Ordering::Less),
|
||||||
|
(FocusLevel::Ordered(a), FocusLevel::Ordered(b)) => a.partial_cmp(b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for FocusLevel {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.partial_cmp(other).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FocusLevel {
|
||||||
|
fn default() -> Self {
|
||||||
|
FocusLevel::Unfocusable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Default)]
|
||||||
|
pub(crate) struct Focus {
|
||||||
|
pub level: FocusLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeDepState for Focus {
|
||||||
|
type Ctx = ();
|
||||||
|
type DepState = ();
|
||||||
|
const NODE_MASK: NodeMask =
|
||||||
|
NodeMask::new_with_attrs(AttributeMask::Static(FOCUS_ATTRIBUTES)).with_listeners();
|
||||||
|
|
||||||
|
fn reduce(&mut self, node: NodeView<'_>, _sibling: &Self::DepState, _: &Self::Ctx) -> bool {
|
||||||
|
let new = Focus {
|
||||||
|
level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") {
|
||||||
|
if let Some(index) = a
|
||||||
|
.value
|
||||||
|
.as_int32()
|
||||||
|
.or_else(|| a.value.as_text().and_then(|v| v.parse::<i32>().ok()))
|
||||||
|
{
|
||||||
|
match index.cmp(&0) {
|
||||||
|
Ordering::Less => FocusLevel::Unfocusable,
|
||||||
|
Ordering::Equal => FocusLevel::Focusable,
|
||||||
|
Ordering::Greater => {
|
||||||
|
FocusLevel::Ordered(NonZeroU16::new(index as u16).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FocusLevel::Unfocusable
|
||||||
|
}
|
||||||
|
} else if node
|
||||||
|
.listeners()
|
||||||
|
.iter()
|
||||||
|
.any(|l| FOCUS_EVENTS.binary_search(&l.event).is_ok())
|
||||||
|
{
|
||||||
|
FocusLevel::Focusable
|
||||||
|
} else {
|
||||||
|
FocusLevel::Unfocusable
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if *self != new {
|
||||||
|
*self = new;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FOCUS_EVENTS: &[&str] = &sorted_str_slice!(["keydown", "keypress", "keyup"]);
|
||||||
|
const FOCUS_ATTRIBUTES: &[&str] = &sorted_str_slice!(["tabindex"]);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct FocusState {
|
||||||
|
pub(crate) focus_iter: PersistantElementIter,
|
||||||
|
pub(crate) last_focused_id: Option<ElementId>,
|
||||||
|
pub(crate) focus_level: FocusLevel,
|
||||||
|
pub(crate) dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusState {
|
||||||
|
/// Returns true if the focus has changed.
|
||||||
|
pub fn progress(&mut self, rdom: &mut Dom, forward: bool) -> bool {
|
||||||
|
if let Some(last) = self.last_focused_id {
|
||||||
|
if rdom[last].state.prevent_default == PreventDefault::KeyDown {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the id that started focused to track when a loop has happened
|
||||||
|
let mut loop_marker_id = self.last_focused_id;
|
||||||
|
let focus_level = &mut self.focus_level;
|
||||||
|
let mut next_focus = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let new = if forward {
|
||||||
|
self.focus_iter.next(rdom)
|
||||||
|
} else {
|
||||||
|
self.focus_iter.prev(rdom)
|
||||||
|
};
|
||||||
|
let new_id = new.id();
|
||||||
|
if let ElementProduced::Looped(_) = new {
|
||||||
|
let mut closest_level = None;
|
||||||
|
|
||||||
|
if forward {
|
||||||
|
// find the closest focusable element after the current level
|
||||||
|
rdom.traverse_depth_first(|n| {
|
||||||
|
let node_level = n.state.focus.level;
|
||||||
|
if node_level != *focus_level
|
||||||
|
&& node_level.focusable()
|
||||||
|
&& node_level > *focus_level
|
||||||
|
{
|
||||||
|
if let Some(level) = &mut closest_level {
|
||||||
|
if node_level < *level {
|
||||||
|
*level = node_level;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
closest_level = Some(node_level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// find the closest focusable element before the current level
|
||||||
|
rdom.traverse_depth_first(|n| {
|
||||||
|
let node_level = n.state.focus.level;
|
||||||
|
if node_level != *focus_level
|
||||||
|
&& node_level.focusable()
|
||||||
|
&& node_level < *focus_level
|
||||||
|
{
|
||||||
|
if let Some(level) = &mut closest_level {
|
||||||
|
if node_level > *level {
|
||||||
|
*level = node_level;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
closest_level = Some(node_level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// extend the loop_marker_id to allow for another pass
|
||||||
|
loop_marker_id = None;
|
||||||
|
|
||||||
|
if let Some(level) = closest_level {
|
||||||
|
*focus_level = level;
|
||||||
|
} else if forward {
|
||||||
|
*focus_level = FocusLevel::Unfocusable;
|
||||||
|
} else {
|
||||||
|
*focus_level = FocusLevel::Focusable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// once we have looked at all the elements exit the loop
|
||||||
|
if let Some(last) = loop_marker_id {
|
||||||
|
if new_id == last {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loop_marker_id = Some(new_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_level = rdom[new_id].state.focus.level;
|
||||||
|
let after_previous_focused = if forward {
|
||||||
|
current_level >= *focus_level
|
||||||
|
} else {
|
||||||
|
current_level <= *focus_level
|
||||||
|
};
|
||||||
|
if after_previous_focused && current_level.focusable() && current_level == *focus_level
|
||||||
|
{
|
||||||
|
next_focus = Some(new_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(id) = next_focus {
|
||||||
|
rdom[id].state.focused = true;
|
||||||
|
if let Some(old) = self.last_focused_id.replace(id) {
|
||||||
|
rdom[old].state.focused = false;
|
||||||
|
}
|
||||||
|
// reset the position to the currently focused element
|
||||||
|
while self.focus_iter.next(rdom).id() != id {}
|
||||||
|
self.dirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prune(&mut self, mutations: &dioxus_core::Mutations, rdom: &Dom) {
|
||||||
|
fn remove_children(
|
||||||
|
to_prune: &mut [&mut Option<ElementId>],
|
||||||
|
rdom: &Dom,
|
||||||
|
removed: ElementId,
|
||||||
|
) {
|
||||||
|
for opt in to_prune.iter_mut() {
|
||||||
|
if let Some(id) = opt {
|
||||||
|
if *id == removed {
|
||||||
|
**opt = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let NodeType::Element { children, .. } = &rdom[removed].node_type {
|
||||||
|
for child in children {
|
||||||
|
remove_children(to_prune, rdom, *child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.focus_iter.prune(mutations, rdom) {
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
for m in &mutations.edits {
|
||||||
|
match m {
|
||||||
|
dioxus_core::DomEdit::ReplaceWith { root, .. } => remove_children(
|
||||||
|
&mut [&mut self.last_focused_id],
|
||||||
|
rdom,
|
||||||
|
ElementId(*root as usize),
|
||||||
|
),
|
||||||
|
dioxus_core::DomEdit::Remove { root } => remove_children(
|
||||||
|
&mut [&mut self.last_focused_id],
|
||||||
|
rdom,
|
||||||
|
ElementId(*root as usize),
|
||||||
|
),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_focus(&mut self, rdom: &mut Dom, id: ElementId) {
|
||||||
|
if let Some(old) = self.last_focused_id.replace(id) {
|
||||||
|
rdom[old].state.focused = false;
|
||||||
|
}
|
||||||
|
let state = &mut rdom[id].state;
|
||||||
|
state.focused = true;
|
||||||
|
self.focus_level = state.focus.level;
|
||||||
|
// reset the position to the currently focused element
|
||||||
|
while self.focus_iter.next(rdom).id() != id {}
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clean(&mut self) -> bool {
|
||||||
|
let old = self.dirty;
|
||||||
|
self.dirty = false;
|
||||||
|
old
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
-50
@@ -12,7 +12,7 @@ use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet};
|
|||||||
use dioxus_html::{event_bubbles, on::*, KeyCode};
|
use dioxus_html::{event_bubbles, on::*, KeyCode};
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
cell::RefCell,
|
cell::{RefCell, RefMut},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
@@ -20,6 +20,7 @@ use std::{
|
|||||||
use taffy::geometry::{Point, Size};
|
use taffy::geometry::{Point, Size};
|
||||||
use taffy::{prelude::Layout, Taffy};
|
use taffy::{prelude::Layout, Taffy};
|
||||||
|
|
||||||
|
use crate::FocusState;
|
||||||
use crate::{Dom, Node};
|
use crate::{Dom, Node};
|
||||||
|
|
||||||
// a wrapper around the input state for easier access
|
// a wrapper around the input state for easier access
|
||||||
@@ -36,22 +37,22 @@ use crate::{Dom, Node};
|
|||||||
|
|
||||||
// pub fn mouse(&self) -> Option<MouseData> {
|
// pub fn mouse(&self) -> Option<MouseData> {
|
||||||
// let data = (**self.0).borrow();
|
// let data = (**self.0).borrow();
|
||||||
// data.mouse.as_ref().map(|m| m.clone())
|
// mouse.as_ref().map(|m| m.clone())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// pub fn wheel(&self) -> Option<WheelData> {
|
// pub fn wheel(&self) -> Option<WheelData> {
|
||||||
// let data = (**self.0).borrow();
|
// let data = (**self.0).borrow();
|
||||||
// data.wheel.as_ref().map(|w| w.clone())
|
// wheel.as_ref().map(|w| w.clone())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// pub fn screen(&self) -> Option<(u16, u16)> {
|
// pub fn screen(&self) -> Option<(u16, u16)> {
|
||||||
// let data = (**self.0).borrow();
|
// let data = (**self.0).borrow();
|
||||||
// data.screen.as_ref().map(|m| m.clone())
|
// screen.as_ref().map(|m| m.clone())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// pub fn last_key_pressed(&self) -> Option<KeyboardData> {
|
// pub fn last_key_pressed(&self) -> Option<KeyboardData> {
|
||||||
// let data = (**self.0).borrow();
|
// let data = (**self.0).borrow();
|
||||||
// data.last_key_pressed
|
// last_key_pressed
|
||||||
// .as_ref()
|
// .as_ref()
|
||||||
// .map(|k| &k.0.clone())
|
// .map(|k| &k.0.clone())
|
||||||
// }
|
// }
|
||||||
@@ -84,6 +85,7 @@ pub struct InnerInputState {
|
|||||||
wheel: Option<WheelData>,
|
wheel: Option<WheelData>,
|
||||||
last_key_pressed: Option<(KeyboardData, Instant)>,
|
last_key_pressed: Option<(KeyboardData, Instant)>,
|
||||||
screen: Option<(u16, u16)>,
|
screen: Option<(u16, u16)>,
|
||||||
|
pub(crate) focus_state: FocusState,
|
||||||
// subscribers: Vec<Rc<dyn Fn() + 'static>>,
|
// subscribers: Vec<Rc<dyn Fn() + 'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ impl InnerInputState {
|
|||||||
last_key_pressed: None,
|
last_key_pressed: None,
|
||||||
screen: None,
|
screen: None,
|
||||||
// subscribers: Vec::new(),
|
// subscribers: Vec::new(),
|
||||||
|
focus_state: FocusState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +154,7 @@ impl InnerInputState {
|
|||||||
|
|
||||||
fn update(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
evts: &mut [EventCore],
|
evts: &mut Vec<EventCore>,
|
||||||
resolved_events: &mut Vec<UserEvent>,
|
resolved_events: &mut Vec<UserEvent>,
|
||||||
layout: &Taffy,
|
layout: &Taffy,
|
||||||
dom: &mut Dom,
|
dom: &mut Dom,
|
||||||
@@ -160,19 +163,57 @@ impl InnerInputState {
|
|||||||
|
|
||||||
self.wheel = None;
|
self.wheel = None;
|
||||||
|
|
||||||
|
let old_focus = self.focus_state.last_focused_id;
|
||||||
|
|
||||||
|
evts.retain(|e| match &e.1 {
|
||||||
|
EventData::Keyboard(k) => match k.key_code {
|
||||||
|
KeyCode::Tab => !self.focus_state.progress(dom, !k.shift_key),
|
||||||
|
_ => true,
|
||||||
|
},
|
||||||
|
_ => true,
|
||||||
|
});
|
||||||
|
|
||||||
for e in evts.iter_mut() {
|
for e in evts.iter_mut() {
|
||||||
self.apply_event(e);
|
self.apply_event(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.resolve_mouse_events(previous_mouse, resolved_events, layout, dom);
|
self.resolve_mouse_events(previous_mouse, resolved_events, layout, dom);
|
||||||
|
|
||||||
|
if old_focus != self.focus_state.last_focused_id {
|
||||||
|
if let Some(id) = self.focus_state.last_focused_id {
|
||||||
|
resolved_events.push(UserEvent {
|
||||||
|
scope_id: None,
|
||||||
|
priority: EventPriority::Medium,
|
||||||
|
name: "focus",
|
||||||
|
element: Some(id),
|
||||||
|
data: Arc::new(FocusData {}),
|
||||||
|
});
|
||||||
|
resolved_events.push(UserEvent {
|
||||||
|
scope_id: None,
|
||||||
|
priority: EventPriority::Medium,
|
||||||
|
name: "focusin",
|
||||||
|
element: Some(id),
|
||||||
|
data: Arc::new(FocusData {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(id) = old_focus {
|
||||||
|
resolved_events.push(UserEvent {
|
||||||
|
scope_id: None,
|
||||||
|
priority: EventPriority::Medium,
|
||||||
|
name: "focusout",
|
||||||
|
element: Some(id),
|
||||||
|
data: Arc::new(FocusData {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// for s in &self.subscribers {
|
// for s in &self.subscribers {
|
||||||
// s();
|
// s();
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_mouse_events(
|
fn resolve_mouse_events(
|
||||||
&self,
|
&mut self,
|
||||||
previous_mouse: Option<MouseData>,
|
previous_mouse: Option<MouseData>,
|
||||||
resolved_events: &mut Vec<UserEvent>,
|
resolved_events: &mut Vec<UserEvent>,
|
||||||
layout: &Taffy,
|
layout: &Taffy,
|
||||||
@@ -257,16 +298,16 @@ impl InnerInputState {
|
|||||||
if old_pos != Some(new_pos) {
|
if old_pos != Some(new_pos) {
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("mousemove") {
|
for node in dom.get_listening_sorted("mousemove") {
|
||||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
let previously_contained = old_pos
|
let previously_contained = old_pos
|
||||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||||
.is_some();
|
.is_some();
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if currently_contains && previously_contained {
|
if currently_contains && previously_contained {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
"mousemove",
|
"mousemove",
|
||||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||||
&mut will_bubble,
|
&mut will_bubble,
|
||||||
resolved_events,
|
resolved_events,
|
||||||
node,
|
node,
|
||||||
@@ -281,11 +322,11 @@ impl InnerInputState {
|
|||||||
// mouseenter
|
// mouseenter
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("mouseenter") {
|
for node in dom.get_listening_sorted("mouseenter") {
|
||||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
let previously_contained = old_pos
|
let previously_contained = old_pos
|
||||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||||
.is_some();
|
.is_some();
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if currently_contains && !previously_contained {
|
if currently_contains && !previously_contained {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
@@ -304,16 +345,16 @@ impl InnerInputState {
|
|||||||
// mouseover
|
// mouseover
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("mouseover") {
|
for node in dom.get_listening_sorted("mouseover") {
|
||||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
let previously_contained = old_pos
|
let previously_contained = old_pos
|
||||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||||
.is_some();
|
.is_some();
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if currently_contains && !previously_contained {
|
if currently_contains && !previously_contained {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
"mouseover",
|
"mouseover",
|
||||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||||
&mut will_bubble,
|
&mut will_bubble,
|
||||||
resolved_events,
|
resolved_events,
|
||||||
node,
|
node,
|
||||||
@@ -327,13 +368,13 @@ impl InnerInputState {
|
|||||||
if was_pressed {
|
if was_pressed {
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("mousedown") {
|
for node in dom.get_listening_sorted("mousedown") {
|
||||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if currently_contains {
|
if currently_contains {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
"mousedown",
|
"mousedown",
|
||||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||||
&mut will_bubble,
|
&mut will_bubble,
|
||||||
resolved_events,
|
resolved_events,
|
||||||
node,
|
node,
|
||||||
@@ -348,13 +389,13 @@ impl InnerInputState {
|
|||||||
if was_released {
|
if was_released {
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("mouseup") {
|
for node in dom.get_listening_sorted("mouseup") {
|
||||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if currently_contains {
|
if currently_contains {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
"mouseup",
|
"mouseup",
|
||||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||||
&mut will_bubble,
|
&mut will_bubble,
|
||||||
resolved_events,
|
resolved_events,
|
||||||
node,
|
node,
|
||||||
@@ -370,13 +411,13 @@ impl InnerInputState {
|
|||||||
if mouse_data.trigger_button() == Some(DioxusMouseButton::Primary) && was_released {
|
if mouse_data.trigger_button() == Some(DioxusMouseButton::Primary) && was_released {
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("click") {
|
for node in dom.get_listening_sorted("click") {
|
||||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if currently_contains {
|
if currently_contains {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
"click",
|
"click",
|
||||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||||
&mut will_bubble,
|
&mut will_bubble,
|
||||||
resolved_events,
|
resolved_events,
|
||||||
node,
|
node,
|
||||||
@@ -393,13 +434,13 @@ impl InnerInputState {
|
|||||||
{
|
{
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("contextmenu") {
|
for node in dom.get_listening_sorted("contextmenu") {
|
||||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if currently_contains {
|
if currently_contains {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
"contextmenu",
|
"contextmenu",
|
||||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||||
&mut will_bubble,
|
&mut will_bubble,
|
||||||
resolved_events,
|
resolved_events,
|
||||||
node,
|
node,
|
||||||
@@ -416,9 +457,9 @@ impl InnerInputState {
|
|||||||
if wheel_delta != 0.0 {
|
if wheel_delta != 0.0 {
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("wheel") {
|
for node in dom.get_listening_sorted("wheel") {
|
||||||
let node_layout =
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
layout.layout(node.state.layout.node.unwrap()).unwrap();
|
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if currently_contains {
|
if currently_contains {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
@@ -439,16 +480,16 @@ impl InnerInputState {
|
|||||||
// mouseleave
|
// mouseleave
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("mouseleave") {
|
for node in dom.get_listening_sorted("mouseleave") {
|
||||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
let previously_contained = old_pos
|
let previously_contained = old_pos
|
||||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||||
.is_some();
|
.is_some();
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if !currently_contains && previously_contained {
|
if !currently_contains && previously_contained {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
"mouseleave",
|
"mouseleave",
|
||||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||||
&mut will_bubble,
|
&mut will_bubble,
|
||||||
resolved_events,
|
resolved_events,
|
||||||
node,
|
node,
|
||||||
@@ -462,16 +503,16 @@ impl InnerInputState {
|
|||||||
// mouseout
|
// mouseout
|
||||||
let mut will_bubble = FxHashSet::default();
|
let mut will_bubble = FxHashSet::default();
|
||||||
for node in dom.get_listening_sorted("mouseout") {
|
for node in dom.get_listening_sorted("mouseout") {
|
||||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
let node_layout = get_abs_layout(node, dom, layout);
|
||||||
let previously_contained = old_pos
|
let previously_contained = old_pos
|
||||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||||
.is_some();
|
.is_some();
|
||||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||||
|
|
||||||
if !currently_contains && previously_contained {
|
if !currently_contains && previously_contained {
|
||||||
try_create_event(
|
try_create_event(
|
||||||
"mouseout",
|
"mouseout",
|
||||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||||
&mut will_bubble,
|
&mut will_bubble,
|
||||||
resolved_events,
|
resolved_events,
|
||||||
node,
|
node,
|
||||||
@@ -480,6 +521,22 @@ impl InnerInputState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update focus
|
||||||
|
if was_released {
|
||||||
|
let mut focus_id = None;
|
||||||
|
dom.traverse_depth_first(|node| {
|
||||||
|
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
||||||
|
let currently_contains = layout_contains_point(node_layout, new_pos);
|
||||||
|
|
||||||
|
if currently_contains && node.state.focus.level.focusable() {
|
||||||
|
focus_id = Some(node.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(id) = focus_id {
|
||||||
|
self.focus_state.set_focus(dom, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,6 +545,20 @@ impl InnerInputState {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_abs_layout(node: &Node, dom: &Dom, taffy: &Taffy) -> Layout {
|
||||||
|
let mut node_layout = *taffy.layout(node.state.layout.node.unwrap()).unwrap();
|
||||||
|
let mut current = node;
|
||||||
|
|
||||||
|
while let Some(parent_id) = current.parent {
|
||||||
|
let parent = &dom[parent_id];
|
||||||
|
current = parent;
|
||||||
|
let parent_layout = taffy.layout(parent.state.layout.node.unwrap()).unwrap();
|
||||||
|
node_layout.location.x += parent_layout.location.x;
|
||||||
|
node_layout.location.y += parent_layout.location.y;
|
||||||
|
}
|
||||||
|
node_layout
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RinkInputHandler {
|
pub struct RinkInputHandler {
|
||||||
state: Rc<RefCell<InnerInputState>>,
|
state: Rc<RefCell<InnerInputState>>,
|
||||||
queued_events: Rc<RefCell<Vec<EventCore>>>,
|
queued_events: Rc<RefCell<Vec<EventCore>>>,
|
||||||
@@ -524,6 +595,10 @@ impl RinkInputHandler {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prune(&self, mutations: &dioxus_core::Mutations, rdom: &Dom) {
|
||||||
|
self.state.borrow_mut().focus_state.prune(mutations, rdom);
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_events(&self, layout: &Taffy, dom: &mut Dom) -> Vec<UserEvent> {
|
pub(crate) fn get_events(&self, layout: &Taffy, dom: &mut Dom) -> Vec<UserEvent> {
|
||||||
let mut resolved_events = Vec::new();
|
let mut resolved_events = Vec::new();
|
||||||
|
|
||||||
@@ -557,7 +632,6 @@ impl RinkInputHandler {
|
|||||||
})
|
})
|
||||||
.map(|evt| (evt.0, evt.1.into_any()));
|
.map(|evt| (evt.0, evt.1.into_any()));
|
||||||
|
|
||||||
// todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus
|
|
||||||
let mut hm: FxHashMap<&'static str, Vec<Arc<dyn Any + Send + Sync>>> = FxHashMap::default();
|
let mut hm: FxHashMap<&'static str, Vec<Arc<dyn Any + Send + Sync>>> = FxHashMap::default();
|
||||||
for (event, data) in events {
|
for (event, data) in events {
|
||||||
if let Some(v) = hm.get_mut(event) {
|
if let Some(v) = hm.get_mut(event) {
|
||||||
@@ -569,20 +643,26 @@ impl RinkInputHandler {
|
|||||||
for (event, datas) in hm {
|
for (event, datas) in hm {
|
||||||
for node in dom.get_listening_sorted(event) {
|
for node in dom.get_listening_sorted(event) {
|
||||||
for data in &datas {
|
for data in &datas {
|
||||||
resolved_events.push(UserEvent {
|
if node.state.focused {
|
||||||
scope_id: None,
|
resolved_events.push(UserEvent {
|
||||||
priority: EventPriority::Medium,
|
scope_id: None,
|
||||||
name: event,
|
priority: EventPriority::Medium,
|
||||||
element: Some(node.id),
|
name: event,
|
||||||
data: data.clone(),
|
element: Some(node.id),
|
||||||
bubbles: event_bubbles(event),
|
data: data.clone(),
|
||||||
});
|
bubbles: event_bubbles(event),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved_events
|
resolved_events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn state(&self) -> RefMut<InnerInputState> {
|
||||||
|
self.state.borrow_mut()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// translate crossterm events into dioxus events
|
// translate crossterm events into dioxus events
|
||||||
|
|||||||
+24
-23
@@ -7,23 +7,23 @@ use crossterm::{
|
|||||||
};
|
};
|
||||||
use dioxus_core::exports::futures_channel::mpsc::unbounded;
|
use dioxus_core::exports::futures_channel::mpsc::unbounded;
|
||||||
use dioxus_core::*;
|
use dioxus_core::*;
|
||||||
use dioxus_native_core::{real_dom::RealDom, state::*};
|
use dioxus_native_core::real_dom::RealDom;
|
||||||
use dioxus_native_core_macro::State;
|
use focus::FocusState;
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::mpsc::{UnboundedReceiver, UnboundedSender},
|
channel::mpsc::{UnboundedReceiver, UnboundedSender},
|
||||||
pin_mut, StreamExt,
|
pin_mut, StreamExt,
|
||||||
};
|
};
|
||||||
use layout::TaffyLayout;
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::{io, time::Duration};
|
use std::{io, time::Duration};
|
||||||
use style_attributes::StyleModifier;
|
|
||||||
use taffy::{geometry::Point, prelude::Size, Taffy};
|
use taffy::{geometry::Point, prelude::Size, Taffy};
|
||||||
use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
|
use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod focus;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
mod layout;
|
mod layout;
|
||||||
|
mod node;
|
||||||
mod render;
|
mod render;
|
||||||
mod style;
|
mod style;
|
||||||
mod style_attributes;
|
mod style_attributes;
|
||||||
@@ -31,18 +31,7 @@ mod widget;
|
|||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use hooks::*;
|
pub use hooks::*;
|
||||||
|
pub(crate) use node::*;
|
||||||
type Dom = RealDom<NodeState>;
|
|
||||||
type Node = dioxus_native_core::real_dom::Node<NodeState>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, State, Default)]
|
|
||||||
struct NodeState {
|
|
||||||
#[child_dep_state(layout, RefCell<Taffy>)]
|
|
||||||
layout: TaffyLayout,
|
|
||||||
// depends on attributes, the C component of it's parent and a u8 context
|
|
||||||
#[parent_dep_state(style)]
|
|
||||||
style: StyleModifier,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TuiContext {
|
pub struct TuiContext {
|
||||||
@@ -52,6 +41,12 @@ impl TuiContext {
|
|||||||
pub fn quit(&self) {
|
pub fn quit(&self) {
|
||||||
self.tx.unbounded_send(InputEvent::Close).unwrap();
|
self.tx.unbounded_send(InputEvent::Close).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn inject_event(&self, event: crossterm::event::Event) {
|
||||||
|
self.tx
|
||||||
|
.unbounded_send(InputEvent::UserInput(event))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn launch(app: Component<()>) {
|
pub fn launch(app: Component<()>) {
|
||||||
@@ -71,7 +66,6 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) {
|
|||||||
let tick_rate = Duration::from_millis(1000);
|
let tick_rate = Duration::from_millis(1000);
|
||||||
loop {
|
loop {
|
||||||
if crossterm::event::poll(tick_rate).unwrap() {
|
if crossterm::event::poll(tick_rate).unwrap() {
|
||||||
// if crossterm::event::poll(timeout).unwrap() {
|
|
||||||
let evt = crossterm::event::read().unwrap();
|
let evt = crossterm::event::read().unwrap();
|
||||||
if event_tx.unbounded_send(InputEvent::UserInput(evt)).is_err() {
|
if event_tx.unbounded_send(InputEvent::UserInput(evt)).is_err() {
|
||||||
break;
|
break;
|
||||||
@@ -130,7 +124,7 @@ fn render_vdom(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut to_rerender: fxhash::FxHashSet<usize> = vec![0].into_iter().collect();
|
let mut to_rerender: fxhash::FxHashSet<usize> = vec![0].into_iter().collect();
|
||||||
let mut resized = true;
|
let mut updated = true;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
/*
|
/*
|
||||||
@@ -144,8 +138,8 @@ fn render_vdom(
|
|||||||
todo: lazy re-rendering
|
todo: lazy re-rendering
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if !to_rerender.is_empty() || resized {
|
if !to_rerender.is_empty() || updated {
|
||||||
resized = false;
|
updated = false;
|
||||||
fn resize(dims: Rect, taffy: &mut Taffy, rdom: &Dom) {
|
fn resize(dims: Rect, taffy: &mut Taffy, rdom: &Dom) {
|
||||||
let width = dims.width;
|
let width = dims.width;
|
||||||
let height = dims.height;
|
let height = dims.height;
|
||||||
@@ -180,8 +174,8 @@ fn render_vdom(
|
|||||||
Rect {
|
Rect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 300,
|
width: 100,
|
||||||
height: 300,
|
height: 100,
|
||||||
},
|
},
|
||||||
&mut taffy.borrow_mut(),
|
&mut taffy.borrow_mut(),
|
||||||
&rdom,
|
&rdom,
|
||||||
@@ -209,7 +203,7 @@ fn render_vdom(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TermEvent::Resize(_, _) => resized = true,
|
TermEvent::Resize(_, _) => updated = true,
|
||||||
TermEvent::Mouse(_) => {}
|
TermEvent::Mouse(_) => {}
|
||||||
},
|
},
|
||||||
InputEvent::Close => break,
|
InputEvent::Close => break,
|
||||||
@@ -224,10 +218,16 @@ fn render_vdom(
|
|||||||
|
|
||||||
{
|
{
|
||||||
let evts = handler.get_events(&taffy.borrow(), &mut rdom);
|
let evts = handler.get_events(&taffy.borrow(), &mut rdom);
|
||||||
|
{
|
||||||
|
updated |= handler.state().focus_state.clean();
|
||||||
|
}
|
||||||
for e in evts {
|
for e in evts {
|
||||||
vdom.handle_message(SchedulerMsg::Event(e));
|
vdom.handle_message(SchedulerMsg::Event(e));
|
||||||
}
|
}
|
||||||
let mutations = vdom.work_with_deadline(|| false);
|
let mutations = vdom.work_with_deadline(|| false);
|
||||||
|
for m in &mutations {
|
||||||
|
handler.prune(m, &rdom);
|
||||||
|
}
|
||||||
// updates the dom's nodes
|
// updates the dom's nodes
|
||||||
let to_update = rdom.apply_mutations(mutations);
|
let to_update = rdom.apply_mutations(mutations);
|
||||||
// update the style and layout
|
// update the style and layout
|
||||||
@@ -251,6 +251,7 @@ fn render_vdom(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
enum InputEvent {
|
enum InputEvent {
|
||||||
UserInput(TermEvent),
|
UserInput(TermEvent),
|
||||||
Close,
|
Close,
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
use crate::focus::Focus;
|
||||||
|
use crate::layout::TaffyLayout;
|
||||||
|
use crate::style_attributes::StyleModifier;
|
||||||
|
use dioxus_native_core::{real_dom::RealDom, state::*};
|
||||||
|
use dioxus_native_core_macro::{sorted_str_slice, State};
|
||||||
|
|
||||||
|
pub(crate) type Dom = RealDom<NodeState>;
|
||||||
|
pub(crate) type Node = dioxus_native_core::real_dom::Node<NodeState>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, State, Default)]
|
||||||
|
pub(crate) struct NodeState {
|
||||||
|
#[child_dep_state(layout, RefCell<Stretch>)]
|
||||||
|
pub layout: TaffyLayout,
|
||||||
|
#[parent_dep_state(style)]
|
||||||
|
pub style: StyleModifier,
|
||||||
|
#[node_dep_state()]
|
||||||
|
pub prevent_default: PreventDefault,
|
||||||
|
#[node_dep_state()]
|
||||||
|
pub focus: Focus,
|
||||||
|
pub focused: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub(crate) enum PreventDefault {
|
||||||
|
Focus,
|
||||||
|
KeyPress,
|
||||||
|
KeyRelease,
|
||||||
|
KeyDown,
|
||||||
|
KeyUp,
|
||||||
|
MouseDown,
|
||||||
|
Click,
|
||||||
|
MouseEnter,
|
||||||
|
MouseLeave,
|
||||||
|
MouseOut,
|
||||||
|
Unknown,
|
||||||
|
MouseOver,
|
||||||
|
ContextMenu,
|
||||||
|
Wheel,
|
||||||
|
MouseUp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PreventDefault {
|
||||||
|
fn default() -> Self {
|
||||||
|
PreventDefault::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeDepState for PreventDefault {
|
||||||
|
type Ctx = ();
|
||||||
|
|
||||||
|
type DepState = ();
|
||||||
|
|
||||||
|
const NODE_MASK: dioxus_native_core::node_ref::NodeMask =
|
||||||
|
dioxus_native_core::node_ref::NodeMask::new_with_attrs(
|
||||||
|
dioxus_native_core::node_ref::AttributeMask::Static(&sorted_str_slice!([
|
||||||
|
"dioxus-prevent-default"
|
||||||
|
])),
|
||||||
|
);
|
||||||
|
|
||||||
|
fn reduce(
|
||||||
|
&mut self,
|
||||||
|
node: dioxus_native_core::node_ref::NodeView,
|
||||||
|
_sibling: &Self::DepState,
|
||||||
|
_ctx: &Self::Ctx,
|
||||||
|
) -> bool {
|
||||||
|
let new = match node
|
||||||
|
.attributes()
|
||||||
|
.find(|a| a.name == "dioxus-prevent-default")
|
||||||
|
.and_then(|a| a.value.as_text())
|
||||||
|
{
|
||||||
|
Some("onfocus") => PreventDefault::Focus,
|
||||||
|
Some("onkeypress") => PreventDefault::KeyPress,
|
||||||
|
Some("onkeyrelease") => PreventDefault::KeyRelease,
|
||||||
|
Some("onkeydown") => PreventDefault::KeyDown,
|
||||||
|
Some("onkeyup") => PreventDefault::KeyUp,
|
||||||
|
Some("onclick") => PreventDefault::Click,
|
||||||
|
Some("onmousedown") => PreventDefault::MouseDown,
|
||||||
|
Some("onmouseup") => PreventDefault::MouseUp,
|
||||||
|
Some("onmouseenter") => PreventDefault::MouseEnter,
|
||||||
|
Some("onmouseover") => PreventDefault::MouseOver,
|
||||||
|
Some("onmouseleave") => PreventDefault::MouseLeave,
|
||||||
|
Some("onmouseout") => PreventDefault::MouseOut,
|
||||||
|
Some("onwheel") => PreventDefault::Wheel,
|
||||||
|
Some("oncontextmenu") => PreventDefault::ContextMenu,
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
if new == *self {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
*self = new;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ use taffy::{
|
|||||||
prelude::{Layout, Size},
|
prelude::{Layout, Size},
|
||||||
Taffy,
|
Taffy,
|
||||||
};
|
};
|
||||||
use tui::{backend::CrosstermBackend, layout::Rect};
|
use tui::{backend::CrosstermBackend, layout::Rect, style::Color};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
style::{RinkColor, RinkStyle},
|
style::{RinkColor, RinkStyle},
|
||||||
@@ -272,6 +272,10 @@ impl RinkWidget for &Node {
|
|||||||
if let Some(c) = self.state.style.core.bg {
|
if let Some(c) = self.state.style.core.bg {
|
||||||
new_cell.bg = c;
|
new_cell.bg = c;
|
||||||
}
|
}
|
||||||
|
if self.state.focused {
|
||||||
|
new_cell.bg.alpha = 100;
|
||||||
|
new_cell.bg.color = new_cell.bg.blend(Color::White);
|
||||||
|
}
|
||||||
buf.set(x, y, new_cell);
|
buf.set(x, y, new_cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ impl<T: RinkWidget> WidgetWithContext<T> {
|
|||||||
|
|
||||||
impl<T: RinkWidget> Widget for WidgetWithContext<T> {
|
impl<T: RinkWidget> Widget for WidgetWithContext<T> {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
self.widget.render(area, RinkBuffer::new(buf, self.config))
|
self.widget.render(area, RinkBuffer::new(buf, self.config));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
|
||||||
|
use dioxus_core::VNode;
|
||||||
|
use dioxus_core::*;
|
||||||
|
use dioxus_core_macro::*;
|
||||||
|
use dioxus_hooks::*;
|
||||||
|
use dioxus_html as dioxus_elements;
|
||||||
|
use dioxus_tui::TuiContext;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_down() {
|
||||||
|
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_count = use_state(&cx, || 0);
|
||||||
|
let tui_ctx: TuiContext = cx.consume_context().unwrap();
|
||||||
|
let render_count_handle = render_count.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
render_count_handle.modify(|x| *x + 1);
|
||||||
|
});
|
||||||
|
if *render_count.get() > 2 {
|
||||||
|
panic!("Event was not received");
|
||||||
|
}
|
||||||
|
tui_ctx.inject_event(Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('a'),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
onkeydown: move |evt| {
|
||||||
|
assert_eq!(evt.data.key_code, dioxus_html::KeyCode::A);
|
||||||
|
tui_ctx.quit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_down() {
|
||||||
|
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_count = use_state(&cx, || 0);
|
||||||
|
let tui_ctx: TuiContext = cx.consume_context().unwrap();
|
||||||
|
let render_count_handle = render_count.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
render_count_handle.modify(|x| *x + 1);
|
||||||
|
});
|
||||||
|
if *render_count.get() > 2 {
|
||||||
|
panic!("Event was not received");
|
||||||
|
}
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 0,
|
||||||
|
row: 0,
|
||||||
|
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
onmousedown: move |evt| {
|
||||||
|
assert!(evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
|
||||||
|
tui_ctx.quit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_up() {
|
||||||
|
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_count = use_state(&cx, || 0);
|
||||||
|
let tui_ctx: TuiContext = cx.consume_context().unwrap();
|
||||||
|
let render_count_handle = render_count.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
render_count_handle.modify(|x| *x + 1);
|
||||||
|
});
|
||||||
|
if *render_count.get() > 2 {
|
||||||
|
panic!("Event was not received");
|
||||||
|
}
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 0,
|
||||||
|
row: 0,
|
||||||
|
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 0,
|
||||||
|
row: 0,
|
||||||
|
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
onmouseup: move |_| {
|
||||||
|
tui_ctx.quit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_enter() {
|
||||||
|
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_count = use_state(&cx, || 0);
|
||||||
|
let tui_ctx: TuiContext = cx.consume_context().unwrap();
|
||||||
|
let render_count_handle = render_count.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
render_count_handle.modify(|x| *x + 1);
|
||||||
|
});
|
||||||
|
if *render_count.get() > 2 {
|
||||||
|
panic!("Event was not received");
|
||||||
|
}
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 100,
|
||||||
|
row: 100,
|
||||||
|
kind: crossterm::event::MouseEventKind::Moved,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 0,
|
||||||
|
row: 0,
|
||||||
|
kind: crossterm::event::MouseEventKind::Moved,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "50%",
|
||||||
|
height: "50%",
|
||||||
|
onmouseenter: move |_| {
|
||||||
|
tui_ctx.quit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_exit() {
|
||||||
|
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_count = use_state(&cx, || 0);
|
||||||
|
let tui_ctx: TuiContext = cx.consume_context().unwrap();
|
||||||
|
let render_count_handle = render_count.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
render_count_handle.modify(|x| *x + 1);
|
||||||
|
});
|
||||||
|
if *render_count.get() > 2 {
|
||||||
|
panic!("Event was not received");
|
||||||
|
}
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 0,
|
||||||
|
row: 0,
|
||||||
|
kind: crossterm::event::MouseEventKind::Moved,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 100,
|
||||||
|
row: 100,
|
||||||
|
kind: crossterm::event::MouseEventKind::Moved,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "50%",
|
||||||
|
height: "50%",
|
||||||
|
onmouseenter: move |_| {
|
||||||
|
tui_ctx.quit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_move() {
|
||||||
|
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_count = use_state(&cx, || 0);
|
||||||
|
let tui_ctx: TuiContext = cx.consume_context().unwrap();
|
||||||
|
let render_count_handle = render_count.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
render_count_handle.modify(|x| *x + 1);
|
||||||
|
});
|
||||||
|
if *render_count.get() > 2 {
|
||||||
|
panic!("Event was not received");
|
||||||
|
}
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 40,
|
||||||
|
row: 40,
|
||||||
|
kind: crossterm::event::MouseEventKind::Moved,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 60,
|
||||||
|
row: 60,
|
||||||
|
kind: crossterm::event::MouseEventKind::Moved,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
onmousemove: move |_|{
|
||||||
|
tui_ctx.quit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wheel() {
|
||||||
|
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_count = use_state(&cx, || 0);
|
||||||
|
let tui_ctx: TuiContext = cx.consume_context().unwrap();
|
||||||
|
let render_count_handle = render_count.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
render_count_handle.modify(|x| *x + 1);
|
||||||
|
});
|
||||||
|
if *render_count.get() > 2 {
|
||||||
|
panic!("Event was not received");
|
||||||
|
}
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 50,
|
||||||
|
row: 50,
|
||||||
|
kind: crossterm::event::MouseEventKind::Moved,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 50,
|
||||||
|
row: 50,
|
||||||
|
kind: crossterm::event::MouseEventKind::ScrollDown,
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
onwheel: move |evt| {
|
||||||
|
assert!(evt.data.delta_y > 0.0);
|
||||||
|
tui_ctx.quit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn click() {
|
||||||
|
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_count = use_state(&cx, || 0);
|
||||||
|
let tui_ctx: TuiContext = cx.consume_context().unwrap();
|
||||||
|
let render_count_handle = render_count.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
render_count_handle.modify(|x| *x + 1);
|
||||||
|
});
|
||||||
|
if *render_count.get() > 2 {
|
||||||
|
panic!("Event was not received");
|
||||||
|
}
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 50,
|
||||||
|
row: 50,
|
||||||
|
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 50,
|
||||||
|
row: 50,
|
||||||
|
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
onclick: move |_|{
|
||||||
|
tui_ctx.quit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_menu() {
|
||||||
|
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let render_count = use_state(&cx, || 0);
|
||||||
|
let tui_ctx: TuiContext = cx.consume_context().unwrap();
|
||||||
|
let render_count_handle = render_count.clone();
|
||||||
|
cx.spawn(async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
render_count_handle.modify(|x| *x + 1);
|
||||||
|
});
|
||||||
|
if *render_count.get() > 2 {
|
||||||
|
panic!("Event was not received");
|
||||||
|
}
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 50,
|
||||||
|
row: 50,
|
||||||
|
kind: crossterm::event::MouseEventKind::Down(MouseButton::Right),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
tui_ctx.inject_event(Event::Mouse(MouseEvent {
|
||||||
|
column: 50,
|
||||||
|
row: 50,
|
||||||
|
kind: crossterm::event::MouseEventKind::Up(MouseButton::Right),
|
||||||
|
modifiers: KeyModifiers::NONE,
|
||||||
|
}));
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
oncontextmenu: move |_|{
|
||||||
|
tui_ctx.quit();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
|
|||||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
|
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
|
||||||
"web"
|
"web"
|
||||||
] }
|
] }
|
||||||
|
dioxus-rsx-interpreter = { path = "../rsx_interpreter", version = "*", optional = true }
|
||||||
|
|
||||||
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"] }
|
||||||
@@ -30,6 +31,7 @@ futures-util = "0.3.19"
|
|||||||
smallstr = "0.2.0"
|
smallstr = "0.2.0"
|
||||||
serde-wasm-bindgen = "0.4.2"
|
serde-wasm-bindgen = "0.4.2"
|
||||||
futures-channel = "0.3.21"
|
futures-channel = "0.3.21"
|
||||||
|
serde_json = { version = "1.0", optional = true }
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3.56"
|
version = "0.3.56"
|
||||||
@@ -75,6 +77,7 @@ features = [
|
|||||||
[features]
|
[features]
|
||||||
default = ["panic_hook"]
|
default = ["panic_hook"]
|
||||||
panic_hook = ["console_error_panic_hook"]
|
panic_hook = ["console_error_panic_hook"]
|
||||||
|
hot_reload = ["dioxus-rsx-interpreter", "web-sys/WebSocket", "web-sys/Location", "web-sys/MessageEvent", "web-sys/console", "serde_json"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus-core-macro = { path = "../core-macro" }
|
dioxus-core-macro = { path = "../core-macro" }
|
||||||
|
|||||||
@@ -217,6 +217,70 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
|
|||||||
|
|
||||||
let mut work_loop = ric_raf::RafLoop::new();
|
let mut work_loop = ric_raf::RafLoop::new();
|
||||||
|
|
||||||
|
#[cfg(feature = "hot_reload")]
|
||||||
|
{
|
||||||
|
use dioxus_rsx_interpreter::error::Error;
|
||||||
|
use dioxus_rsx_interpreter::{ErrorHandler, SetManyRsxMessage, RSX_CONTEXT};
|
||||||
|
use futures_channel::mpsc::unbounded;
|
||||||
|
use futures_channel::mpsc::UnboundedSender;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use wasm_bindgen::closure::Closure;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::{MessageEvent, WebSocket};
|
||||||
|
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
|
||||||
|
let protocol = if window.location().protocol().unwrap() == "https:" {
|
||||||
|
"wss:"
|
||||||
|
} else {
|
||||||
|
"ws:"
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!(
|
||||||
|
"{protocol}//{}/_dioxus/hot_reload",
|
||||||
|
window.location().host().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let ws = WebSocket::new(&url).unwrap();
|
||||||
|
|
||||||
|
// change the rsx when new data is received
|
||||||
|
let cl = Closure::wrap(Box::new(|e: MessageEvent| {
|
||||||
|
if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
|
||||||
|
let msgs: SetManyRsxMessage = serde_json::from_str(&format!("{text}")).unwrap();
|
||||||
|
RSX_CONTEXT.extend(msgs);
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut(MessageEvent)>);
|
||||||
|
|
||||||
|
ws.set_onmessage(Some(cl.as_ref().unchecked_ref()));
|
||||||
|
cl.forget();
|
||||||
|
|
||||||
|
let (error_channel_sender, mut error_channel_receiver) = unbounded();
|
||||||
|
|
||||||
|
struct WebErrorHandler {
|
||||||
|
sender: UnboundedSender<Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorHandler for WebErrorHandler {
|
||||||
|
fn handle_error(&self, err: dioxus_rsx_interpreter::error::Error) {
|
||||||
|
self.sender.unbounded_send(err).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RSX_CONTEXT.set_error_handler(WebErrorHandler {
|
||||||
|
sender: error_channel_sender,
|
||||||
|
});
|
||||||
|
|
||||||
|
RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel());
|
||||||
|
|
||||||
|
// forward stream to the websocket
|
||||||
|
dom.base_scope().spawn_forever(async move {
|
||||||
|
while let Some(err) = error_channel_receiver.next().await {
|
||||||
|
ws.send_with_str(serde_json::to_string(&err).unwrap().as_str())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
log::trace!("waiting for work");
|
log::trace!("waiting for work");
|
||||||
// if virtualdom has nothing, wait for it to have something before requesting idle time
|
// if virtualdom has nothing, wait for it to have something before requesting idle time
|
||||||
|
|||||||
+11
@@ -44,6 +44,11 @@ pub mod events {
|
|||||||
pub use dioxus_html::{on::*, KeyCode};
|
pub use dioxus_html::{on::*, KeyCode};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub use dioxus_rsx as rsx;
|
||||||
|
|
||||||
|
#[cfg(feature = "hot_reload")]
|
||||||
|
pub use dioxus_rsx_interpreter as rsx_interpreter;
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::hooks::*;
|
pub use crate::hooks::*;
|
||||||
pub use dioxus_core::prelude::*;
|
pub use dioxus_core::prelude::*;
|
||||||
@@ -56,4 +61,10 @@ pub mod prelude {
|
|||||||
|
|
||||||
#[cfg(feature = "fermi")]
|
#[cfg(feature = "fermi")]
|
||||||
pub use fermi::{use_atom_ref, use_init_atom_root, use_read, use_set, Atom, AtomRef};
|
pub use fermi::{use_atom_ref, use_init_atom_root, use_read, use_set, Atom, AtomRef};
|
||||||
|
|
||||||
|
#[cfg(feature = "hot_reload")]
|
||||||
|
pub use dioxus_rsx_interpreter::{
|
||||||
|
captuered_context::{CapturedContext, FormattedArg, IfmtArgs},
|
||||||
|
get_line_num, resolve_scope, CodeLocation, RsxContext,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user