mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 14:44:12 +00:00
Merge branch 'master' of https://github.com/DioxusLabs/dioxus into rusty-events
Conflicts: packages/html/src/events.rs packages/tui/src/hooks.rs
This commit is contained in:
commit
f3fcec2bdf
87 changed files with 6230 additions and 868 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -9,7 +9,7 @@ repository = "https://github.com/DioxusLabs/dioxus/"
|
|||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
rust-version = "1.56.0"
|
||||
rust-version = "1.60.0"
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "./packages/core", version = "^0.2.1" }
|
||||
|
@ -26,6 +26,8 @@ dioxus-router = { path = "./packages/router", version = "^0.2.3", optional = tru
|
|||
dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.2.1", optional = true }
|
||||
dioxus-tui = { path = "./packages/tui", version = "^0.2.2", optional = true }
|
||||
|
||||
dioxus-rsx = { path = "./packages/rsx", optional = true }
|
||||
dioxus-rsx-interpreter = { path = "./packages/rsx_interpreter", optional = true }
|
||||
dioxus-liveview = { path = "./packages/liveview", version = "^0.1.0", optional = true }
|
||||
|
||||
dioxus-native-core = { path = "./packages/native-core", version = "^0.2.0", optional = true }
|
||||
|
@ -38,15 +40,16 @@ dioxus-native-core-macro = { path = "./packages/native-core-macro", version = "^
|
|||
[features]
|
||||
default = ["macro", "hooks", "html"]
|
||||
|
||||
macro = ["dioxus-core-macro"]
|
||||
macro = ["dioxus-core-macro", "dioxus-rsx"]
|
||||
hooks = ["dioxus-hooks"]
|
||||
html = ["dioxus-html"]
|
||||
ssr = ["dioxus-ssr"]
|
||||
web = ["dioxus-web", "dioxus-router/web"]
|
||||
web = ["dioxus-web", "dioxus-router?/web"]
|
||||
desktop = ["dioxus-desktop"]
|
||||
router = ["dioxus-router"]
|
||||
tui = ["dioxus-tui"]
|
||||
liveview = ["dioxus-liveview"]
|
||||
hot-reload = ["dioxus-core-macro/hot-reload", "dioxus-rsx-interpreter", "dioxus-desktop?/hot-reload", "dioxus-web?/hot-reload"]
|
||||
native-core = ["dioxus-native-core", "dioxus-native-core-macro"]
|
||||
|
||||
|
||||
|
@ -64,6 +67,8 @@ members = [
|
|||
"packages/fermi",
|
||||
"packages/tui",
|
||||
"packages/liveview",
|
||||
"packages/rsx",
|
||||
"packages/rsx_interpreter",
|
||||
"packages/native-core",
|
||||
"packages/native-core-macro",
|
||||
]
|
||||
|
@ -86,6 +91,7 @@ criterion = "0.3.5"
|
|||
thiserror = "1.0.30"
|
||||
env_logger = "0.9.0"
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "create"
|
||||
harness = false
|
||||
|
|
|
@ -80,11 +80,11 @@ fn Grid(cx: Scope<GridProps>) -> Element {
|
|||
counts.with_mut(|c| {
|
||||
let i = *count.current();
|
||||
c[i] += 1;
|
||||
c[i] = c[i] % 360;
|
||||
c[i] %= 360;
|
||||
});
|
||||
count.with_mut(|i| {
|
||||
*i += 1;
|
||||
*i = *i % (size * size);
|
||||
*i %= size * size;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ Desktop APIs will likely be in flux as we figure out better patterns than our El
|
|||
[Jump to the getting started guide for Desktop.](/reference/platforms/desktop)
|
||||
|
||||
Examples:
|
||||
- [File explorer](https://github.com/dioxusLabs/file-explorer/)
|
||||
- [File explorer](https://github.com/DioxusLabs/example-projects/blob/master/file-explorer)
|
||||
- [WiFi scanner](https://github.com/DioxusLabs/example-projects/blob/master/wifi-scanner)
|
||||
|
||||
[![File ExplorerExample](https://raw.githubusercontent.com/DioxusLabs/example-projects/master/file-explorer/image.png)](https://github.com/DioxusLabs/example-projects/tree/master/file-explorer)
|
||||
|
|
|
@ -64,7 +64,7 @@ The most common hook you'll use for storing state is `use_state`. `use_state` pr
|
|||
|
||||
```rust
|
||||
fn App(cx: Scope)-> Element {
|
||||
let (post, set_post) = use_state(&cx, || {
|
||||
let post = use_state(&cx, || {
|
||||
PostData {
|
||||
id: Uuid::new_v4(),
|
||||
score: 10,
|
||||
|
@ -112,11 +112,11 @@ For example, let's say we provide a button to generate a new post. Whenever the
|
|||
|
||||
```rust
|
||||
fn App(cx: Scope)-> Element {
|
||||
let (post, set_post) = use_state(&cx, || PostData::new());
|
||||
let post = use_state(&cx, || PostData::new());
|
||||
|
||||
cx.render(rsx!{
|
||||
button {
|
||||
onclick: move |_| set_post(PostData::random())
|
||||
onclick: move |_| post.set(PostData::random())
|
||||
"Generate a random post"
|
||||
}
|
||||
Post { props: &post }
|
||||
|
@ -141,19 +141,19 @@ We can use tasks in our components to build a tiny stopwatch that ticks every se
|
|||
|
||||
```rust
|
||||
fn App(cx: Scope)-> Element {
|
||||
let (elapsed, set_elapsed) = use_state(&cx, || 0);
|
||||
|
||||
use_future(&cx, || {
|
||||
to_owned![set_elapsed]; // explicitly capture this hook for use in async
|
||||
let elapsed = use_state(&cx, || 0);
|
||||
|
||||
use_future(&cx, (), |()| {
|
||||
to_owned![elapsed]; // explicitly capture this hook for use in async
|
||||
async move {
|
||||
loop {
|
||||
TimeoutFuture::from_ms(1000).await;
|
||||
set_elapsed.modify(|i| i + 1)
|
||||
gloo_timers::future::TimeoutFuture::new(1_000).await;
|
||||
elapsed.modify(|i| i + 1)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rsx!(cx, div { "Current stopwatch time: {sec_elapsed}" })
|
||||
rsx!(cx, div { "Current stopwatch time: {elapsed}" })
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ When using Debian/bullseye `libappindicator3-dev` is no longer available but rep
|
|||
sudo apt install libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev
|
||||
```
|
||||
|
||||
If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/en/docs/get-started/setup-linux).
|
||||
If you run into issues, make sure you have all the basics installed, as outlined in the [Tauri docs](https://tauri.studio/v1/guides/getting-started/prerequisites#setting-up-linux).
|
||||
|
||||
|
||||
### macOS
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
- [Custom Renderer](guide/custom_renderer.md)
|
||||
- [Server-side components](guide/server_side_components.md)
|
||||
- [Bundling and Distributing](guide/bundline.md)
|
||||
- [Hot Reloading Rsx](guide/hot_reloading.md)
|
||||
|
||||
- [Reference Guide](reference/reference.md)
|
||||
- [Anti-patterns](reference/anti.md)
|
||||
|
|
24
docs/reference/src/guide/hot_reloading.md
Normal file
24
docs/reference/src/guide/hot_reloading.md
Normal file
|
@ -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.
|
|
@ -11,7 +11,7 @@ Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Ca
|
|||
|
||||
```shell
|
||||
$ cargo new --bin demo
|
||||
$ cd app
|
||||
$ cd demo
|
||||
```
|
||||
|
||||
Add Dioxus with the `desktop` feature:
|
||||
|
|
|
@ -46,6 +46,9 @@ fn main() {
|
|||
/// This type alias specifies the type for you so you don't need to write "None as Option<()>"
|
||||
const NONE_ELEMENT: Option<()> = None;
|
||||
|
||||
use core::{fmt, str::FromStr};
|
||||
use std::fmt::Display;
|
||||
|
||||
use baller::Baller;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
|
@ -187,6 +190,15 @@ fn app(cx: Scope) -> Element {
|
|||
text: "using functionc all syntax"
|
||||
)
|
||||
|
||||
// Components can be geneirc too
|
||||
// This component takes i32 type to give you typed input
|
||||
TypedInput::<TypedInputProps<i32>> {}
|
||||
// Type inference can be used too
|
||||
TypedInput { initial: 10.0 }
|
||||
// geneircs with the `inline_props` macro
|
||||
label(text: "hello geneirc world!")
|
||||
label(text: 99.9)
|
||||
|
||||
// helper functions
|
||||
// Single values must be wrapped in braces or `Some` to satisfy `IntoIterator`
|
||||
[helper(&cx, "hello world!")]
|
||||
|
@ -227,9 +239,35 @@ pub fn Taller<'a>(cx: Scope<'a, TallerProps<'a>>) -> Element {
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Props, PartialEq)]
|
||||
pub struct TypedInputProps<T> {
|
||||
#[props(optional, default)]
|
||||
initial: Option<T>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn TypedInput<T>(_: Scope<TypedInputProps<T>>) -> Element
|
||||
where
|
||||
T: FromStr + fmt::Display,
|
||||
<T as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn with_inline<'a>(cx: Scope<'a>, text: &'a str) -> Element {
|
||||
cx.render(rsx! {
|
||||
p { "{text}" }
|
||||
})
|
||||
}
|
||||
|
||||
// generic component with inline_props too
|
||||
#[inline_props]
|
||||
fn label<T>(cx: Scope, text: T) -> Element
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
cx.render(rsx! {
|
||||
p { "{text}" }
|
||||
})
|
||||
}
|
||||
|
|
75
examples/svg_basic.rs
Normal file
75
examples/svg_basic.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use dioxus::prelude::*;
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx!( svg {
|
||||
width: "200",
|
||||
height: "250",
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
version: "1.1",
|
||||
rect {
|
||||
x: "10",
|
||||
y: "10",
|
||||
width: "30",
|
||||
height: "30",
|
||||
stroke: "black",
|
||||
fill: "transparent",
|
||||
stroke_width: "5",
|
||||
}
|
||||
rect {
|
||||
x: "60",
|
||||
y: "10",
|
||||
width: "30",
|
||||
height: "30",
|
||||
stroke: "black",
|
||||
fill: "transparent",
|
||||
stroke_width: "5",
|
||||
}
|
||||
circle {
|
||||
cx: "25",
|
||||
cy: "75",
|
||||
r: "20",
|
||||
stroke: "red",
|
||||
fill: "transparent",
|
||||
stroke_width: "5",
|
||||
}
|
||||
ellipse {
|
||||
cx: "75",
|
||||
cy: "75",
|
||||
rx: "20",
|
||||
ry: "5",
|
||||
stroke: "red",
|
||||
fill: "transparent",
|
||||
stroke_width: "5",
|
||||
}
|
||||
line {
|
||||
x1: "10",
|
||||
x2: "50",
|
||||
y1: "110",
|
||||
y2: "150",
|
||||
stroke: "orange",
|
||||
stroke_width: "5",
|
||||
}
|
||||
polyline {
|
||||
points: "60 110 65 120 70 115 75 130 80 125 85 140 90 135 95 150 100 145",
|
||||
stroke: "orange",
|
||||
fill: "transparent",
|
||||
stroke_width: "5",
|
||||
}
|
||||
polygon {
|
||||
points: "50 160 55 180 70 180 60 190 65 205 50 195 35 205 40 190 30 180 45 180",
|
||||
stroke: "green",
|
||||
fill: "transparent",
|
||||
stroke_width: "5",
|
||||
}
|
||||
path {
|
||||
d: "M20,230 Q40,205 50,230 T90,230",
|
||||
fill: "none",
|
||||
stroke: "blue",
|
||||
stroke_width: "5",
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dioxus::desktop::launch(app);
|
||||
}
|
93
examples/tui_buttons.rs
Normal file
93
examples/tui_buttons.rs
Normal file
|
@ -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 {
|
||||
let key = use_state(&cx, || "".to_string());
|
||||
let mouse = use_state(&cx, || ScreenPoint::zero());
|
||||
let mouse = use_state(&cx, ScreenPoint::zero);
|
||||
let count = use_state(&cx, || 0);
|
||||
let buttons = use_state(&cx, || MouseButtonSet::empty());
|
||||
let buttons = use_state(&cx, MouseButtonSet::empty);
|
||||
let mouse_clicked = use_state(&cx, || false);
|
||||
|
||||
let key_down_handler = move |evt: KeyboardEvent| {
|
||||
|
|
|
@ -15,12 +15,17 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro-error = "1"
|
||||
proc-macro2 = { version = "1.0.6" }
|
||||
proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
dioxus-rsx = { path = "../rsx" }
|
||||
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
|
||||
|
||||
# testing
|
||||
[dev-dependencies]
|
||||
rustversion = "1.0"
|
||||
trybuild = "1.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
hot-reload = ["dioxus-rsx-interpreter"]
|
|
@ -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))
|
||||
}
|
|
@ -17,6 +17,7 @@ pub struct InlinePropsBody {
|
|||
pub inputs: Punctuated<FnArg, Token![,]>,
|
||||
// pub fields: FieldsNamed,
|
||||
pub output: ReturnType,
|
||||
pub where_clause: Option<WhereClause>,
|
||||
pub block: Box<Block>,
|
||||
}
|
||||
|
||||
|
@ -28,7 +29,7 @@ impl Parse for InlinePropsBody {
|
|||
|
||||
let fn_token = input.parse()?;
|
||||
let ident = input.parse()?;
|
||||
let generics = input.parse()?;
|
||||
let generics: Generics = input.parse()?;
|
||||
|
||||
let content;
|
||||
let paren_token = syn::parenthesized!(content in input);
|
||||
|
@ -47,6 +48,11 @@ impl Parse for InlinePropsBody {
|
|||
|
||||
let output = input.parse()?;
|
||||
|
||||
let where_clause = input
|
||||
.peek(syn::token::Where)
|
||||
.then(|| input.parse())
|
||||
.transpose()?;
|
||||
|
||||
let block = input.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
|
@ -57,6 +63,7 @@ impl Parse for InlinePropsBody {
|
|||
paren_token,
|
||||
inputs,
|
||||
output,
|
||||
where_clause,
|
||||
block,
|
||||
cx_token,
|
||||
attrs,
|
||||
|
@ -73,6 +80,7 @@ impl ToTokens for InlinePropsBody {
|
|||
generics,
|
||||
inputs,
|
||||
output,
|
||||
where_clause,
|
||||
block,
|
||||
cx_token,
|
||||
attrs,
|
||||
|
@ -136,12 +144,16 @@ impl ToTokens for InlinePropsBody {
|
|||
out_tokens.append_all(quote! {
|
||||
#modifiers
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #struct_name #struct_generics {
|
||||
#vis struct #struct_name #struct_generics
|
||||
#where_clause
|
||||
{
|
||||
#(#fields),*
|
||||
}
|
||||
|
||||
#(#attrs)*
|
||||
#vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output {
|
||||
#vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output
|
||||
#where_clause
|
||||
{
|
||||
let #struct_name { #(#field_names),* } = &cx.props;
|
||||
#block
|
||||
}
|
||||
|
|
|
@ -2,14 +2,15 @@ use proc_macro::TokenStream;
|
|||
use quote::ToTokens;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
mod ifmt;
|
||||
mod inlineprops;
|
||||
mod props;
|
||||
mod rsx;
|
||||
|
||||
// mod rsx;
|
||||
use dioxus_rsx as rsx;
|
||||
|
||||
#[proc_macro]
|
||||
pub fn format_args_f(input: TokenStream) -> TokenStream {
|
||||
use ifmt::*;
|
||||
use rsx::*;
|
||||
let item = parse_macro_input!(input as IfmtInput);
|
||||
format_args_f_impl(item)
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
|
@ -29,159 +30,45 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
|
|||
///
|
||||
/// ## Complete Reference Guide:
|
||||
/// ```
|
||||
/// const Example: Component = |cx| {
|
||||
/// let formatting = "formatting!";
|
||||
/// let formatting_tuple = ("a", "b");
|
||||
/// let lazy_fmt = format_args!("lazily formatted text");
|
||||
/// cx.render(rsx! {
|
||||
/// div {
|
||||
/// // Elements
|
||||
/// div {}
|
||||
/// h1 {"Some text"}
|
||||
/// h1 {"Some text with {formatting}"}
|
||||
/// h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"}
|
||||
/// h2 {
|
||||
/// "Multiple"
|
||||
/// "Text"
|
||||
/// "Blocks"
|
||||
/// "Use comments as separators in html"
|
||||
/// }
|
||||
/// div {
|
||||
/// h1 {"multiple"}
|
||||
/// h2 {"nested"}
|
||||
/// h3 {"elements"}
|
||||
/// }
|
||||
/// div {
|
||||
/// class: "my special div"
|
||||
/// h1 {"Headers and attributes!"}
|
||||
/// }
|
||||
/// div {
|
||||
/// // pass simple rust expressions in
|
||||
/// class: lazy_fmt,
|
||||
/// id: format_args!("attributes can be passed lazily with std::fmt::Arguments"),
|
||||
/// div {
|
||||
/// class: {
|
||||
/// const WORD: &str = "expressions";
|
||||
/// format_args!("Arguments can be passed in through curly braces for complex {}", WORD)
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Expressions can be used in element position too:
|
||||
/// {rsx!(p { "More templating!" })}
|
||||
/// {html!(<p>"Even HTML templating!!"</p>)}
|
||||
///
|
||||
/// // Iterators
|
||||
/// {(0..10).map(|i| rsx!(li { "{i}" }))}
|
||||
/// {{
|
||||
/// let data = std::collections::HashMap::<&'static str, &'static str>::new();
|
||||
/// // Iterators *should* have keys when you can provide them.
|
||||
/// // Keys make your app run faster. Make sure your keys are stable, unique, and predictable.
|
||||
/// // Using an "ID" associated with your data is a good idea.
|
||||
/// data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" }))
|
||||
/// }}
|
||||
///
|
||||
/// // Matching
|
||||
/// {match true {
|
||||
/// true => rsx!(h1 {"Top text"}),
|
||||
/// false => rsx!(h1 {"Bottom text"})
|
||||
/// }}
|
||||
///
|
||||
/// // Conditional rendering
|
||||
/// // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals.
|
||||
/// // You can convert a bool condition to rsx! with .then and .or
|
||||
/// {true.then(|| rsx!(div {}))}
|
||||
///
|
||||
/// // True conditions
|
||||
/// {if true {
|
||||
/// rsx!(h1 {"Top text"})
|
||||
/// } else {
|
||||
/// rsx!(h1 {"Bottom text"})
|
||||
/// }}
|
||||
///
|
||||
/// // returning "None" is a bit noisy... but rare in practice
|
||||
/// {None as Option<()>}
|
||||
///
|
||||
/// // Use the Dioxus type-alias for less noise
|
||||
/// {NONE_ELEMENT}
|
||||
///
|
||||
/// // can also just use empty fragments
|
||||
/// Fragment {}
|
||||
///
|
||||
/// // Fragments let you insert groups of nodes without a parent.
|
||||
/// // This lets you make components that insert elements as siblings without a container.
|
||||
/// div {"A"}
|
||||
/// Fragment {
|
||||
/// div {"B"}
|
||||
/// div {"C"}
|
||||
/// Fragment {
|
||||
/// "D"
|
||||
/// Fragment {
|
||||
/// "heavily nested fragments is an antipattern"
|
||||
/// "they cause Dioxus to do unnecessary work"
|
||||
/// "don't use them carelessly if you can help it"
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Components
|
||||
/// // Can accept any paths
|
||||
/// // Notice how you still get syntax highlighting and IDE support :)
|
||||
/// Baller {}
|
||||
/// baller::Baller { }
|
||||
/// crate::baller::Baller {}
|
||||
///
|
||||
/// // Can take properties
|
||||
/// Taller { a: "asd" }
|
||||
///
|
||||
/// // Can take optional properties
|
||||
/// Taller { a: "asd" }
|
||||
///
|
||||
/// // Can pass in props directly as an expression
|
||||
/// {{
|
||||
/// let props = TallerProps {a: "hello"};
|
||||
/// rsx!(Taller { ..props })
|
||||
/// }}
|
||||
///
|
||||
/// // Spreading can also be overridden manually
|
||||
/// Taller {
|
||||
/// ..TallerProps { a: "ballin!" }
|
||||
/// a: "not ballin!"
|
||||
/// }
|
||||
///
|
||||
/// // Can take children too!
|
||||
/// Taller { a: "asd", div {"hello world!"} }
|
||||
/// }
|
||||
/// })
|
||||
/// };
|
||||
///
|
||||
/// mod baller {
|
||||
/// use super::*;
|
||||
/// pub struct BallerProps {}
|
||||
///
|
||||
/// /// This component totally balls
|
||||
/// pub fn Baller(cx: Scope) -> DomTree {
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, PartialEq, Props)]
|
||||
/// pub struct TallerProps {
|
||||
/// a: &'static str,
|
||||
/// }
|
||||
///
|
||||
/// /// This component is taller than most :)
|
||||
/// pub fn Taller(cx: Scope<TallerProps>) -> DomTree {
|
||||
/// let b = true;
|
||||
/// todo!()
|
||||
/// }
|
||||
#[doc = include_str!("../../../examples/rsx_usage.rs")]
|
||||
/// ```
|
||||
#[proc_macro_error::proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn rsx(s: TokenStream) -> TokenStream {
|
||||
#[cfg(feature = "hot-reload")]
|
||||
let rsx_text = s.to_string();
|
||||
match syn::parse::<rsx::CallBody>(s) {
|
||||
Err(err) => err.to_compile_error().into(),
|
||||
Ok(stream) => stream.to_token_stream().into(),
|
||||
Ok(body) => {
|
||||
#[cfg(feature = "hot-reload")]
|
||||
{
|
||||
use dioxus_rsx_interpreter::captuered_context::CapturedContextBuilder;
|
||||
|
||||
match CapturedContextBuilder::from_call_body(body) {
|
||||
Ok(captured) => {
|
||||
let lazy = quote::quote! {
|
||||
LazyNodes::new(move |__cx|{
|
||||
let code_location = get_line_num!();
|
||||
let captured = #captured;
|
||||
let text = #rsx_text;
|
||||
|
||||
resolve_scope(code_location, text, captured, __cx)
|
||||
})
|
||||
};
|
||||
if let Some(cx) = captured.custom_context {
|
||||
quote::quote! {
|
||||
#cx.render(#lazy)
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
lazy.into()
|
||||
}
|
||||
}
|
||||
Err(err) => err.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "hot-reload"))]
|
||||
body.to_token_stream().into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -653,7 +653,9 @@ Finally, call `.build()` to create the instance of `{name}`.
|
|||
}
|
||||
}
|
||||
|
||||
impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{
|
||||
impl #impl_generics dioxus::prelude::Properties for #name #ty_generics
|
||||
#b_generics_where_extras_predicates
|
||||
{
|
||||
type Builder = #builder_name #generics_with_empty;
|
||||
const IS_STATIC: bool = #is_static;
|
||||
fn builder() -> Self::Builder {
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
macro_rules! missing_trailing_comma {
|
||||
($span:expr) => {
|
||||
proc_macro_error::emit_error!($span, "missing trailing comma")
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! attr_after_element {
|
||||
($span:expr) => {
|
||||
proc_macro_error::emit_error!(
|
||||
$span,
|
||||
"expected element";
|
||||
help = "move the attribute above all the children and text elements"
|
||||
)
|
||||
};
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
use super::*;
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
token, Expr, LitStr, Result, Token,
|
||||
};
|
||||
|
||||
/*
|
||||
Parse
|
||||
-> div {}
|
||||
-> Component {}
|
||||
-> component()
|
||||
-> "text {with_args}"
|
||||
-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr
|
||||
*/
|
||||
pub enum BodyNode {
|
||||
Element(Element),
|
||||
Component(Component),
|
||||
Text(LitStr),
|
||||
RawExpr(Expr),
|
||||
}
|
||||
|
||||
impl Parse for BodyNode {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
if stream.peek(LitStr) {
|
||||
return Ok(BodyNode::Text(stream.parse()?));
|
||||
}
|
||||
|
||||
// div {} -> el
|
||||
// Div {} -> comp
|
||||
if stream.peek(syn::Ident) && stream.peek2(token::Brace) {
|
||||
if stream
|
||||
.fork()
|
||||
.parse::<Ident>()?
|
||||
.to_string()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.is_ascii_uppercase()
|
||||
{
|
||||
return Ok(BodyNode::Component(stream.parse()?));
|
||||
} else {
|
||||
return Ok(BodyNode::Element(stream.parse::<Element>()?));
|
||||
}
|
||||
}
|
||||
|
||||
// component() -> comp
|
||||
// ::component {} -> comp
|
||||
// ::component () -> comp
|
||||
if (stream.peek(syn::Ident) && stream.peek2(token::Paren))
|
||||
|| (stream.peek(Token![::]))
|
||||
|| (stream.peek(Token![:]) && stream.peek2(Token![:]))
|
||||
{
|
||||
return Ok(BodyNode::Component(stream.parse::<Component>()?));
|
||||
}
|
||||
|
||||
// crate::component{} -> comp
|
||||
// crate::component() -> comp
|
||||
if let Ok(pat) = stream.fork().parse::<syn::Path>() {
|
||||
if pat.segments.len() > 1 {
|
||||
return Ok(BodyNode::Component(stream.parse::<Component>()?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BodyNode::RawExpr(stream.parse::<Expr>()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for BodyNode {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match &self {
|
||||
BodyNode::Element(el) => el.to_tokens(tokens),
|
||||
BodyNode::Component(comp) => comp.to_tokens(tokens),
|
||||
BodyNode::Text(txt) => tokens.append_all(quote! {
|
||||
__cx.text(format_args_f!(#txt))
|
||||
}),
|
||||
BodyNode::RawExpr(exp) => tokens.append_all(quote! {
|
||||
__cx.fragment_from_iter(#exp)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
//! pretty printer for rsx!
|
|
@ -48,5 +48,4 @@ backtrace = "0.3"
|
|||
[features]
|
||||
default = []
|
||||
serialize = ["serde"]
|
||||
debug_vdom = []
|
||||
|
||||
debug_vdom = []
|
|
@ -1,26 +1,26 @@
|
|||
#![warn(clippy::pedantic)]
|
||||
#![allow(clippy::cast_possible_truncation)]
|
||||
|
||||
//! This module contains the stateful [`DiffState`] and all methods to diff [`VNodes`], their properties, and their children.
|
||||
//! This module contains the stateful [`DiffState`] and all methods to diff [`VNode`]s, their properties, and their children.
|
||||
//!
|
||||
//! The [`DiffState`] calculates the diffs between the old and new frames, updates the new nodes, and generates a set
|
||||
//! of mutations for the [`RealDom`] to apply.
|
||||
//! of mutations for the renderer to apply.
|
||||
//!
|
||||
//! ## Notice:
|
||||
//!
|
||||
//! The inspiration and code for this module was originally taken from Dodrio (@fitzgen) and then modified to support
|
||||
//! Components, Fragments, Suspense, [`SubTree`] memoization, incremental diffing, cancellation, [`NodeRefs`], pausing, priority
|
||||
//! Components, Fragments, Suspense, `SubTree` memoization, incremental diffing, cancellation, pausing, priority
|
||||
//! scheduling, and additional batching operations.
|
||||
//!
|
||||
//! ## Implementation Details:
|
||||
//!
|
||||
//! ### IDs for elements
|
||||
//! --------------------
|
||||
//! All nodes are addressed by their IDs. The [`RealDom`] provides an imperative interface for making changes to these nodes.
|
||||
//! All nodes are addressed by their IDs.
|
||||
//! We don't necessarily require that DOM changes happen instantly during the diffing process, so the implementor may choose
|
||||
//! to batch nodes if it is more performant for their application. The element IDs are indices into the internal element
|
||||
//! array. The expectation is that implementors will use the ID as an index into a Vec of real nodes, allowing for passive
|
||||
//! garbage collection as the [`VirtualDOM`] replaces old nodes.
|
||||
//! garbage collection as the [`crate::VirtualDom`] replaces old nodes.
|
||||
//!
|
||||
//! When new vnodes are created through `cx.render`, they won't know which real node they correspond to. During diffing,
|
||||
//! we always make sure to copy over the ID. If we don't do this properly, the [`ElementId`] will be populated incorrectly
|
||||
|
@ -30,7 +30,7 @@
|
|||
//! --------------------
|
||||
//! Fragments (nodes without a parent) are supported through a combination of "replace with" and anchor vnodes. Fragments
|
||||
//! can be particularly challenging when they are empty, so the anchor node lets us "reserve" a spot for the empty
|
||||
//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the [`NodeFactory`] - it is
|
||||
//! fragment to be replaced with when it is no longer empty. This is guaranteed by logic in the [`crate::innerlude::NodeFactory`] - it is
|
||||
//! impossible to craft a fragment with 0 elements - they must always have at least a single placeholder element. Adding
|
||||
//! "dummy" nodes _is_ inefficient, but it makes our diffing algorithm faster and the implementation is completely up to
|
||||
//! the platform.
|
||||
|
@ -44,13 +44,13 @@
|
|||
//! into a promise-like value. React will then work on the next "ready" fiber, checking back on the previous fiber once
|
||||
//! it has finished its new work. In Dioxus, we use a similar approach, but try to completely render the tree before
|
||||
//! switching sub-fibers. Instead, each future is submitted into a futures-queue and the node is manually loaded later on.
|
||||
//! Due to the frequent calls to [`yield_now`] we can get the pure "fetch-as-you-render" behavior of React Fiber.
|
||||
//! Due to the frequent calls to [`crate::virtual_dom::VirtualDom::work_with_deadline`] we can get the pure "fetch-as-you-render" behavior of React Fiber.
|
||||
//!
|
||||
//! We're able to use this approach because we use placeholder nodes - futures that aren't ready still get submitted to
|
||||
//! DOM, but as a placeholder.
|
||||
//!
|
||||
//! Right now, the "suspense" queue is intertwined with hooks. In the future, we should allow any future to drive attributes
|
||||
//! and contents, without the need for the [`use_suspense`] hook. In the interim, this is the quickest way to get Suspense working.
|
||||
//! and contents, without the need for a `use_suspense` hook. In the interim, this is the quickest way to get Suspense working.
|
||||
//!
|
||||
//! ## Subtree Memoization
|
||||
//! -----------------------
|
||||
|
|
|
@ -63,6 +63,9 @@ pub struct UserEvent {
|
|||
/// The event type IE "onclick" or "onmouseover"
|
||||
pub name: &'static str,
|
||||
|
||||
/// If the event is bubbles up through the vdom
|
||||
pub bubbles: bool,
|
||||
|
||||
/// The event data to be passed onto the event handler
|
||||
pub data: Arc<dyn Any + Send + Sync>,
|
||||
}
|
||||
|
@ -150,7 +153,7 @@ impl AnyEvent {
|
|||
/// You should prefer to use the name of the event directly, rather than
|
||||
/// the UiEvent<T> generic type.
|
||||
///
|
||||
/// For the HTML crate, this would include [`MouseEvent`], [`FormEvent`] etc.
|
||||
/// For the HTML crate, this would include MouseEvent, FormEvent etc.
|
||||
pub struct UiEvent<T> {
|
||||
/// The internal data of the event
|
||||
/// This is wrapped in an Arc so that it can be sent across threads
|
||||
|
|
|
@ -550,7 +550,7 @@ impl<'a> NodeFactory<'a> {
|
|||
}))
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VElement`]
|
||||
/// Create a new [`VNode::Element`]
|
||||
pub fn element(
|
||||
&self,
|
||||
el: impl DioxusElement,
|
||||
|
@ -569,7 +569,7 @@ impl<'a> NodeFactory<'a> {
|
|||
)
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VElement`] without the trait bound
|
||||
/// Create a new [`VNode::Element`] without the trait bound
|
||||
///
|
||||
/// IE pass in "div" instead of `div`
|
||||
pub fn raw_element(
|
||||
|
@ -637,7 +637,7 @@ impl<'a> NodeFactory<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VComponent`]
|
||||
/// Create a new [`VNode::Component`]
|
||||
pub fn component<P>(
|
||||
&self,
|
||||
component: fn(Scope<'a, P>) -> Element,
|
||||
|
@ -684,7 +684,7 @@ impl<'a> NodeFactory<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VFragment`] from a root of the rsx! call
|
||||
/// Create a new [`VNode::Fragment`] from a root of the rsx! call
|
||||
pub fn fragment_root<'b, 'c>(
|
||||
self,
|
||||
node_iter: impl IntoIterator<Item = impl IntoVNode<'a> + 'c> + 'b,
|
||||
|
@ -705,7 +705,7 @@ impl<'a> NodeFactory<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new [`VNode::VFragment`] from any iterator
|
||||
/// Create a new [`VNode::Fragment`] from any iterator
|
||||
pub fn fragment_from_iter<'b, 'c>(
|
||||
self,
|
||||
node_iter: impl IntoIterator<Item = impl IntoVNode<'a> + 'c> + 'b,
|
||||
|
|
|
@ -33,7 +33,7 @@ impl<'a, const A: bool> FragmentBuilder<'a, A> {
|
|||
/// fn App(cx: Scope) -> Element {
|
||||
/// cx.render(rsx!{
|
||||
/// CustomCard {
|
||||
/// h1 {}2
|
||||
/// h1 {}
|
||||
/// p {}
|
||||
/// }
|
||||
/// })
|
||||
|
|
|
@ -335,7 +335,7 @@ impl ScopeArena {
|
|||
log::trace!("calling listener {:?}", listener.event);
|
||||
if state.canceled.get() {
|
||||
// stop bubbling if canceled
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
let mut cb = listener.callback.borrow_mut();
|
||||
|
@ -349,6 +349,10 @@ impl ScopeArena {
|
|||
data: event.data.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if !event.bubbles {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -609,7 +613,7 @@ impl ScopeState {
|
|||
|
||||
/// Create a subscription that schedules a future render for the reference component
|
||||
///
|
||||
/// ## Notice: you should prefer using prepare_update and get_scope_id
|
||||
/// ## Notice: you should prefer using schedule_update_any and scope_id
|
||||
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||
Arc::new(move || {
|
||||
|
@ -1000,12 +1004,11 @@ impl TaskQueue {
|
|||
fn remove(&self, id: TaskId) {
|
||||
if let Ok(mut tasks) = self.tasks.try_borrow_mut() {
|
||||
let _ = tasks.remove(&id);
|
||||
if let Some(task_map) = self.task_map.borrow_mut().get_mut(&id.scope) {
|
||||
task_map.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
// the task map is still around, but it'll be removed when the scope is unmounted
|
||||
if let Some(task_map) = self.task_map.borrow_mut().get_mut(&id.scope) {
|
||||
task_map.remove(&id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_tasks(&self) -> bool {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::innerlude::*;
|
||||
|
||||
/// An iterator that only yields "real" [`Element`]s. IE only Elements that are
|
||||
/// not [`VNode::VComponent`] or [`VNode::VFragment`], .
|
||||
/// not [`VNode::Component`] or [`VNode::Fragment`], .
|
||||
pub struct ElementIdIterator<'a> {
|
||||
vdom: &'a VirtualDom,
|
||||
|
||||
|
|
|
@ -125,6 +125,9 @@ pub enum SchedulerMsg {
|
|||
/// Immediate updates from Components that mark them as dirty
|
||||
Immediate(ScopeId),
|
||||
|
||||
/// Mark all components as dirty and update them
|
||||
DirtyAll,
|
||||
|
||||
/// New tasks from components that should be polled when the next poll is ready
|
||||
NewTask(ScopeId),
|
||||
}
|
||||
|
@ -337,29 +340,12 @@ impl VirtualDom {
|
|||
|
||||
let scopes = &mut self.scopes;
|
||||
let task_poll = poll_fn(|cx| {
|
||||
//
|
||||
let mut any_pending = false;
|
||||
|
||||
let mut tasks = scopes.tasks.tasks.borrow_mut();
|
||||
let mut to_remove = vec![];
|
||||
tasks.retain(|_, task| task.as_mut().poll(cx).is_pending());
|
||||
|
||||
// this would be better served by retain
|
||||
for (id, task) in tasks.iter_mut() {
|
||||
if task.as_mut().poll(cx).is_ready() {
|
||||
to_remove.push(*id);
|
||||
} else {
|
||||
any_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
for id in to_remove {
|
||||
tasks.remove(&id);
|
||||
}
|
||||
|
||||
// Resolve the future if any singular task is ready
|
||||
match any_pending {
|
||||
true => Poll::Pending,
|
||||
false => Poll::Ready(()),
|
||||
match tasks.is_empty() {
|
||||
true => Poll::Ready(()),
|
||||
false => Poll::Pending,
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -407,6 +393,11 @@ impl VirtualDom {
|
|||
SchedulerMsg::Immediate(s) => {
|
||||
self.dirty_scopes.insert(s);
|
||||
}
|
||||
SchedulerMsg::DirtyAll => {
|
||||
for id in self.scopes.scopes.borrow().keys() {
|
||||
self.dirty_scopes.insert(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -583,7 +574,7 @@ impl VirtualDom {
|
|||
///
|
||||
/// *value.borrow_mut() = "goodbye";
|
||||
///
|
||||
/// let edits = dom.diff();
|
||||
/// let edits = dom.hard_diff(ScopeId(0));
|
||||
/// ```
|
||||
pub fn hard_diff(&mut self, scope_id: ScopeId) -> Mutations {
|
||||
let mut diff_machine = DiffState::new(&self.scopes);
|
||||
|
|
|
@ -15,6 +15,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1" }
|
||||
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
|
||||
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
|
@ -32,6 +33,8 @@ webbrowser = "0.7.1"
|
|||
mime_guess = "2.0.3"
|
||||
dunce = "1.0.2"
|
||||
|
||||
interprocess = { version = "1.1.1", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9.3"
|
||||
|
||||
|
@ -41,7 +44,7 @@ tokio_runtime = ["tokio"]
|
|||
fullscreen = ["wry/fullscreen"]
|
||||
transparent = ["wry/transparent"]
|
||||
tray = ["wry/tray"]
|
||||
|
||||
hot-reload = ["dioxus-rsx-interpreter", "interprocess"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::desktop_context::{DesktopContext, UserWindowEvent};
|
||||
|
||||
use dioxus_core::*;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
@ -49,6 +50,93 @@ impl DesktopController {
|
|||
|
||||
dom.base_scope().provide_context(window_context);
|
||||
|
||||
// allow other proccesses to send the new rsx text to the @dioxusin ipc channel and recieve erros on the @dioxusout channel
|
||||
#[cfg(feature = "hot-reload")]
|
||||
{
|
||||
use dioxus_rsx_interpreter::{
|
||||
error::Error, ErrorHandler, SetManyRsxMessage, RSX_CONTEXT,
|
||||
};
|
||||
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
fn handle_error(
|
||||
connection: std::io::Result<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();
|
||||
|
||||
edit_queue
|
||||
|
|
|
@ -212,7 +212,12 @@ pub(super) fn handler(
|
|||
log::warn!("Open print modal failed: {e}");
|
||||
}
|
||||
}
|
||||
DevTool => webview.open_devtools(),
|
||||
DevTool => {
|
||||
#[cfg(debug_assertions)]
|
||||
webview.open_devtools();
|
||||
#[cfg(not(debug_assertions))]
|
||||
log::warn!("Devtools are disabled in release builds");
|
||||
}
|
||||
|
||||
Eval(code) => {
|
||||
if let Err(e) = webview.evaluate_script(code.as_str()) {
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::any::Any;
|
|||
use std::sync::Arc;
|
||||
|
||||
use dioxus_core::{ElementId, EventPriority, UserEvent};
|
||||
use dioxus_html::event_bubbles;
|
||||
use dioxus_html::on::*;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
|
@ -47,8 +48,8 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
|||
} = serde_json::from_value(val).unwrap();
|
||||
|
||||
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
|
||||
|
||||
let name = event_name_from_type(&event);
|
||||
|
||||
let event = make_synthetic_event(&event, contents);
|
||||
|
||||
UserEvent {
|
||||
|
@ -56,6 +57,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
|||
priority: EventPriority::Low,
|
||||
scope_id: None,
|
||||
element: mounted_dom_id,
|
||||
bubbles: event_bubbles(name),
|
||||
data: event,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ It's that simple!
|
|||
Fermi is currently under construction, so you have to use the `master` branch to get started.
|
||||
|
||||
```rust
|
||||
[depdencies]
|
||||
[dependencies]
|
||||
fermi = { git = "https://github.com/dioxuslabs/fermi" }
|
||||
```
|
||||
|
||||
|
|
|
@ -74,6 +74,13 @@ impl AtomRoot {
|
|||
}
|
||||
} else {
|
||||
log::trace!("no atoms found for {:?}", ptr);
|
||||
atoms.insert(
|
||||
ptr,
|
||||
Slot {
|
||||
value: Rc::new(value),
|
||||
subscribers: HashSet::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1443,3 +1443,91 @@ pub(crate) fn _event_meta(event: &UserEvent) -> (bool, EventPriority) {
|
|||
_ => (true, Low),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn event_bubbles(evt: &str) -> bool {
|
||||
match evt {
|
||||
"copy" => true,
|
||||
"cut" => true,
|
||||
"paste" => true,
|
||||
"compositionend" => true,
|
||||
"compositionstart" => true,
|
||||
"compositionupdate" => true,
|
||||
"keydown" => true,
|
||||
"keypress" => true,
|
||||
"keyup" => true,
|
||||
"focus" => false,
|
||||
"focusout" => true,
|
||||
"focusin" => true,
|
||||
"blur" => false,
|
||||
"change" => true,
|
||||
"input" => true,
|
||||
"invalid" => true,
|
||||
"reset" => true,
|
||||
"submit" => true,
|
||||
"click" => true,
|
||||
"contextmenu" => true,
|
||||
"doubleclick" => true,
|
||||
"dblclick" => true,
|
||||
"drag" => true,
|
||||
"dragend" => true,
|
||||
"dragenter" => false,
|
||||
"dragexit" => false,
|
||||
"dragleave" => true,
|
||||
"dragover" => true,
|
||||
"dragstart" => true,
|
||||
"drop" => true,
|
||||
"mousedown" => true,
|
||||
"mouseenter" => false,
|
||||
"mouseleave" => false,
|
||||
"mousemove" => true,
|
||||
"mouseout" => true,
|
||||
"scroll" => false,
|
||||
"mouseover" => true,
|
||||
"mouseup" => true,
|
||||
"pointerdown" => true,
|
||||
"pointermove" => true,
|
||||
"pointerup" => true,
|
||||
"pointercancel" => true,
|
||||
"gotpointercapture" => true,
|
||||
"lostpointercapture" => true,
|
||||
"pointerenter" => false,
|
||||
"pointerleave" => false,
|
||||
"pointerover" => true,
|
||||
"pointerout" => true,
|
||||
"select" => true,
|
||||
"touchcancel" => true,
|
||||
"touchend" => true,
|
||||
"touchmove" => true,
|
||||
"touchstart" => true,
|
||||
"wheel" => true,
|
||||
"abort" => false,
|
||||
"canplay" => true,
|
||||
"canplaythrough" => true,
|
||||
"durationchange" => true,
|
||||
"emptied" => true,
|
||||
"encrypted" => true,
|
||||
"ended" => true,
|
||||
"error" => false,
|
||||
"loadeddata" => true,
|
||||
"loadedmetadata" => true,
|
||||
"loadstart" => false,
|
||||
"pause" => true,
|
||||
"play" => true,
|
||||
"playing" => true,
|
||||
"progress" => false,
|
||||
"ratechange" => true,
|
||||
"seeked" => true,
|
||||
"seeking" => true,
|
||||
"stalled" => true,
|
||||
"suspend" => true,
|
||||
"timeupdate" => true,
|
||||
"volumechange" => true,
|
||||
"waiting" => true,
|
||||
"animationstart" => true,
|
||||
"animationend" => true,
|
||||
"animationiteration" => true,
|
||||
"transitionend" => true,
|
||||
"toggle" => true,
|
||||
_ => panic!("unsupported event type {:?}", evt),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ impl From<&MouseEvent> for MouseData {
|
|||
ElementPoint::new(e.offset_x().into(), e.offset_y().into()),
|
||||
PagePoint::new(e.page_x().into(), e.page_y().into()),
|
||||
),
|
||||
Some(MouseButton::from_web_code(e.button().into())),
|
||||
Some(MouseButton::from_web_code(e.button())),
|
||||
decode_mouse_button_set(e.buttons()),
|
||||
modifiers,
|
||||
)
|
||||
|
|
|
@ -48,10 +48,16 @@ extern "C" {
|
|||
pub fn CreatePlaceholder(this: &Interpreter, root: u64);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn NewEventListener(this: &Interpreter, name: &str, root: u64, handler: &Function);
|
||||
pub fn NewEventListener(
|
||||
this: &Interpreter,
|
||||
name: &str,
|
||||
root: u64,
|
||||
handler: &Function,
|
||||
bubbles: bool,
|
||||
);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str);
|
||||
pub fn RemoveEventListener(this: &Interpreter, root: u64, name: &str, bubbles: bool);
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn SetText(this: &Interpreter, root: u64, text: JsValue);
|
||||
|
|
|
@ -5,11 +5,64 @@ export function main() {
|
|||
window.ipc.postMessage(serializeIpcMessage("initialize"));
|
||||
}
|
||||
}
|
||||
|
||||
class ListenerMap {
|
||||
constructor(root) {
|
||||
// bubbling events can listen at the root element
|
||||
this.global = {};
|
||||
// non bubbling events listen at the element the listener was created at
|
||||
this.local = {};
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
createBubbling(event_name, handler) {
|
||||
if (this.global[event_name] === undefined) {
|
||||
this.global[event_name] = {};
|
||||
this.global[event_name].active = 1;
|
||||
this.global[event_name].callback = handler;
|
||||
this.root.addEventListener(event_name, handler);
|
||||
} else {
|
||||
this.global[event_name].active++;
|
||||
}
|
||||
}
|
||||
|
||||
createNonBubbling(event_name, element, handler) {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
if (!this.local[id]) {
|
||||
this.local[id] = {};
|
||||
}
|
||||
this.local[id][event_name] = handler;
|
||||
element.addEventListener(event_name, handler);
|
||||
}
|
||||
|
||||
removeBubbling(event_name) {
|
||||
this.global[event_name].active--;
|
||||
if (this.global[event_name].active === 0) {
|
||||
this.root.removeEventListener(event_name, this.global[event_name].callback);
|
||||
delete this.global[event_name];
|
||||
}
|
||||
}
|
||||
|
||||
removeNonBubbling(element, event_name) {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
delete this.local[id][event_name];
|
||||
if (this.local[id].length === 0) {
|
||||
delete this.local[id];
|
||||
}
|
||||
element.removeEventListener(event_name, handler);
|
||||
}
|
||||
|
||||
removeAllNonBubbling(element) {
|
||||
const id = element.getAttribute("data-dioxus-id");
|
||||
delete this.local[id];
|
||||
}
|
||||
}
|
||||
|
||||
export class Interpreter {
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
this.stack = [root];
|
||||
this.listeners = {};
|
||||
this.listeners = new ListenerMap(root);
|
||||
this.handlers = {};
|
||||
this.lastNodeWasText = false;
|
||||
this.nodes = [root];
|
||||
|
@ -40,6 +93,7 @@ export class Interpreter {
|
|||
ReplaceWith(root_id, m) {
|
||||
let root = this.nodes[root_id];
|
||||
let els = this.stack.splice(this.stack.length - m);
|
||||
this.listeners.removeAllNonBubbling(root);
|
||||
root.replaceWith(...els);
|
||||
}
|
||||
InsertAfter(root, n) {
|
||||
|
@ -54,6 +108,7 @@ export class Interpreter {
|
|||
}
|
||||
Remove(root) {
|
||||
let node = this.nodes[root];
|
||||
this.listeners.removeAllNonBubbling(node);
|
||||
if (node !== undefined) {
|
||||
node.remove();
|
||||
}
|
||||
|
@ -79,25 +134,24 @@ export class Interpreter {
|
|||
this.stack.push(el);
|
||||
this.nodes[root] = el;
|
||||
}
|
||||
NewEventListener(event_name, root, handler) {
|
||||
NewEventListener(event_name, root, handler, bubbles) {
|
||||
const element = this.nodes[root];
|
||||
element.setAttribute("data-dioxus-id", `${root}`);
|
||||
if (this.listeners[event_name] === undefined) {
|
||||
this.listeners[event_name] = 1;
|
||||
this.handlers[event_name] = handler;
|
||||
this.root.addEventListener(event_name, handler);
|
||||
} else {
|
||||
this.listeners[event_name]++;
|
||||
if (bubbles) {
|
||||
this.listeners.createBubbling(event_name, handler);
|
||||
}
|
||||
else {
|
||||
this.listeners.createNonBubbling(event_name, element, handler);
|
||||
}
|
||||
}
|
||||
RemoveEventListener(root, event_name) {
|
||||
RemoveEventListener(root, event_name, bubbles) {
|
||||
const element = this.nodes[root];
|
||||
element.removeAttribute(`data-dioxus-id`);
|
||||
this.listeners[event_name]--;
|
||||
if (this.listeners[event_name] === 0) {
|
||||
this.root.removeEventListener(event_name, this.handlers[event_name]);
|
||||
delete this.listeners[event_name];
|
||||
delete this.handlers[event_name];
|
||||
if (bubbles) {
|
||||
this.listeners.removeBubbling(event_name)
|
||||
}
|
||||
else {
|
||||
this.listeners.removeNonBubbling(element, event_name);
|
||||
}
|
||||
}
|
||||
SetText(root, text) {
|
||||
|
@ -140,7 +194,9 @@ export class Interpreter {
|
|||
RemoveAttribute(root, field, ns) {
|
||||
const name = field;
|
||||
const node = this.nodes[root];
|
||||
if (ns !== null || ns !== undefined) {
|
||||
if (ns == "style") {
|
||||
node.style.removeProperty(name);
|
||||
} else if (ns !== null || ns !== undefined) {
|
||||
node.removeAttributeNS(ns, name);
|
||||
} else if (name === "value") {
|
||||
node.value = "";
|
||||
|
@ -196,12 +252,9 @@ export class Interpreter {
|
|||
this.RemoveEventListener(edit.root, edit.event_name);
|
||||
break;
|
||||
case "NewEventListener":
|
||||
console.log(this.listeners);
|
||||
|
||||
// this handler is only provided on desktop implementations since this
|
||||
// method is not used by the web implementation
|
||||
let handler = (event) => {
|
||||
console.log(event);
|
||||
|
||||
let target = event.target;
|
||||
if (target != null) {
|
||||
|
@ -290,7 +343,8 @@ export class Interpreter {
|
|||
);
|
||||
}
|
||||
};
|
||||
this.NewEventListener(edit.event_name, edit.root, handler);
|
||||
this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name));
|
||||
|
||||
break;
|
||||
case "SetText":
|
||||
this.SetText(edit.root, edit.text);
|
||||
|
@ -606,3 +660,172 @@ const bool_attrs = {
|
|||
selected: true,
|
||||
truespeed: true,
|
||||
};
|
||||
|
||||
function event_bubbles(event) {
|
||||
switch (event) {
|
||||
case "copy":
|
||||
return true;
|
||||
case "cut":
|
||||
return true;
|
||||
case "paste":
|
||||
return true;
|
||||
case "compositionend":
|
||||
return true;
|
||||
case "compositionstart":
|
||||
return true;
|
||||
case "compositionupdate":
|
||||
return true;
|
||||
case "keydown":
|
||||
return true;
|
||||
case "keypress":
|
||||
return true;
|
||||
case "keyup":
|
||||
return true;
|
||||
case "focus":
|
||||
return false;
|
||||
case "focusout":
|
||||
return true;
|
||||
case "focusin":
|
||||
return true;
|
||||
case "blur":
|
||||
return false;
|
||||
case "change":
|
||||
return true;
|
||||
case "input":
|
||||
return true;
|
||||
case "invalid":
|
||||
return true;
|
||||
case "reset":
|
||||
return true;
|
||||
case "submit":
|
||||
return true;
|
||||
case "click":
|
||||
return true;
|
||||
case "contextmenu":
|
||||
return true;
|
||||
case "doubleclick":
|
||||
return true;
|
||||
case "dblclick":
|
||||
return true;
|
||||
case "drag":
|
||||
return true;
|
||||
case "dragend":
|
||||
return true;
|
||||
case "dragenter":
|
||||
return false;
|
||||
case "dragexit":
|
||||
return false;
|
||||
case "dragleave":
|
||||
return true;
|
||||
case "dragover":
|
||||
return true;
|
||||
case "dragstart":
|
||||
return true;
|
||||
case "drop":
|
||||
return true;
|
||||
case "mousedown":
|
||||
return true;
|
||||
case "mouseenter":
|
||||
return false;
|
||||
case "mouseleave":
|
||||
return false;
|
||||
case "mousemove":
|
||||
return true;
|
||||
case "mouseout":
|
||||
return true;
|
||||
case "scroll":
|
||||
return false;
|
||||
case "mouseover":
|
||||
return true;
|
||||
case "mouseup":
|
||||
return true;
|
||||
case "pointerdown":
|
||||
return true;
|
||||
case "pointermove":
|
||||
return true;
|
||||
case "pointerup":
|
||||
return true;
|
||||
case "pointercancel":
|
||||
return true;
|
||||
case "gotpointercapture":
|
||||
return true;
|
||||
case "lostpointercapture":
|
||||
return true;
|
||||
case "pointerenter":
|
||||
return false;
|
||||
case "pointerleave":
|
||||
return false;
|
||||
case "pointerover":
|
||||
return true;
|
||||
case "pointerout":
|
||||
return true;
|
||||
case "select":
|
||||
return true;
|
||||
case "touchcancel":
|
||||
return true;
|
||||
case "touchend":
|
||||
return true;
|
||||
case "touchmove":
|
||||
return true;
|
||||
case "touchstart":
|
||||
return true;
|
||||
case "wheel":
|
||||
return true;
|
||||
case "abort":
|
||||
return false;
|
||||
case "canplay":
|
||||
return true;
|
||||
case "canplaythrough":
|
||||
return true;
|
||||
case "durationchange":
|
||||
return true;
|
||||
case "emptied":
|
||||
return true;
|
||||
case "encrypted":
|
||||
return true;
|
||||
case "ended":
|
||||
return true;
|
||||
case "error":
|
||||
return false;
|
||||
case "loadeddata":
|
||||
return true;
|
||||
case "loadedmetadata":
|
||||
return true;
|
||||
case "loadstart":
|
||||
return false;
|
||||
case "pause":
|
||||
return true;
|
||||
case "play":
|
||||
return true;
|
||||
case "playing":
|
||||
return true;
|
||||
case "progress":
|
||||
return false;
|
||||
case "ratechange":
|
||||
return true;
|
||||
case "seeked":
|
||||
return true;
|
||||
case "seeking":
|
||||
return true;
|
||||
case "stalled":
|
||||
return true;
|
||||
case "suspend":
|
||||
return true;
|
||||
case "timeupdate":
|
||||
return true;
|
||||
case "volumechange":
|
||||
return true;
|
||||
case "waiting":
|
||||
return true;
|
||||
case "animationstart":
|
||||
return true;
|
||||
case "animationend":
|
||||
return true;
|
||||
case "animationiteration":
|
||||
return true;
|
||||
case "transitionend":
|
||||
return true;
|
||||
case "toggle":
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,36 +1,31 @@
|
|||
use axum::{
|
||||
extract::ws::WebSocketUpgrade, response::Html, response::IntoResponse, routing::get, Extension,
|
||||
Router,
|
||||
};
|
||||
#![cfg(feature = "axum")]
|
||||
|
||||
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
|
||||
use dioxus_core::{Element, LazyNodes, Scope};
|
||||
use dioxus_liveview::Liveview;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
#[cfg(feature = "axum")]
|
||||
{
|
||||
pretty_env_logger::init();
|
||||
pretty_env_logger::init();
|
||||
|
||||
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||
let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
|
||||
|
||||
let view = dioxus_liveview::new(addr);
|
||||
let body = view.body("<title>Dioxus Liveview</title>");
|
||||
let view = dioxus_liveview::new(addr);
|
||||
let body = view.body("<title>Dioxus Liveview</title>");
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(move || async { Html(body) }))
|
||||
.route(
|
||||
"/app",
|
||||
get(move |ws: WebSocketUpgrade| async move {
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
view.upgrade(socket, app).await;
|
||||
})
|
||||
}),
|
||||
);
|
||||
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let app = Router::new()
|
||||
.route("/", get(move || async { Html(body) }))
|
||||
.route(
|
||||
"/app",
|
||||
get(move |ws: WebSocketUpgrade| async move {
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
view.upgrade_axum(socket, app).await;
|
||||
})
|
||||
}),
|
||||
);
|
||||
axum::Server::bind(&addr.to_string().parse().unwrap())
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg(feature = "warp")]
|
||||
|
||||
use dioxus_core::{Element, LazyNodes, Scope};
|
||||
use dioxus_liveview as liveview;
|
||||
use warp::ws::Ws;
|
||||
|
@ -5,28 +7,25 @@ use warp::Filter;
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
#[cfg(feature = "warp")]
|
||||
{
|
||||
pretty_env_logger::init();
|
||||
pretty_env_logger::init();
|
||||
|
||||
let addr = ([127, 0, 0, 1], 3030);
|
||||
let addr = ([127, 0, 0, 1], 3030);
|
||||
|
||||
// todo: compactify this routing under one liveview::app method
|
||||
let view = liveview::new(addr);
|
||||
let body = view.body("<title>Dioxus LiveView</title>");
|
||||
// todo: compactify this routing under one liveview::app method
|
||||
let view = liveview::new(addr);
|
||||
let body = view.body("<title>Dioxus LiveView</title>");
|
||||
|
||||
let routes = warp::path::end()
|
||||
.map(move || warp::reply::html(body.clone()))
|
||||
.or(warp::path("app")
|
||||
.and(warp::ws())
|
||||
.and(warp::any().map(move || view.clone()))
|
||||
.map(|ws: Ws, view: liveview::Liveview| {
|
||||
ws.on_upgrade(|socket| async move {
|
||||
view.upgrade(socket, app).await;
|
||||
})
|
||||
}));
|
||||
warp::serve(routes).run(addr).await;
|
||||
}
|
||||
let routes = warp::path::end()
|
||||
.map(move || warp::reply::html(body.clone()))
|
||||
.or(warp::path("app")
|
||||
.and(warp::ws())
|
||||
.and(warp::any().map(move || view.clone()))
|
||||
.map(|ws: Ws, view: liveview::Liveview| {
|
||||
ws.on_upgrade(|socket| async move {
|
||||
view.upgrade_warp(socket, app).await;
|
||||
})
|
||||
}));
|
||||
warp::serve(routes).run(addr).await;
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{events, Liveview};
|
||||
use crate::events;
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use dioxus_core::prelude::*;
|
||||
use futures_util::{
|
||||
|
@ -10,11 +10,16 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
|
|||
use tokio_util::task::LocalPoolHandle;
|
||||
|
||||
impl crate::Liveview {
|
||||
pub async fn upgrade(&self, ws: WebSocket, app: fn(Scope) -> Element) {
|
||||
pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) {
|
||||
connect(ws, self.pool.clone(), app, ()).await;
|
||||
}
|
||||
pub async fn upgrade_with_props<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,
|
||||
{
|
||||
connect(ws, self.pool.clone(), app, props).await;
|
||||
|
|
|
@ -7,10 +7,10 @@ use tokio_util::task::LocalPoolHandle;
|
|||
use warp::ws::{Message, WebSocket};
|
||||
|
||||
impl crate::Liveview {
|
||||
pub async fn upgrade(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) {
|
||||
pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) {
|
||||
connect(ws, self.pool.clone(), app, ()).await;
|
||||
}
|
||||
pub async fn upgrade_with_props<T>(
|
||||
pub async fn upgrade_warp_with_props<T>(
|
||||
&self,
|
||||
ws: warp::ws::WebSocket,
|
||||
app: fn(Scope<T>) -> Element,
|
||||
|
|
|
@ -6,6 +6,7 @@ use std::any::Any;
|
|||
use std::sync::Arc;
|
||||
|
||||
use dioxus_core::{ElementId, EventPriority, UserEvent};
|
||||
use dioxus_html::event_bubbles;
|
||||
use dioxus_html::on::*;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
|
@ -46,6 +47,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
|||
scope_id: None,
|
||||
element: mounted_dom_id,
|
||||
data: event,
|
||||
bubbles: event_bubbles(name),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,21 +7,10 @@ pub mod adapters {
|
|||
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum_adapter;
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
pub mod actix_adapter;
|
||||
}
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg(feature = "warp")]
|
||||
pub use adapters::warp_adapter::connect;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub use adapters::axum_adapter::connect;
|
||||
|
||||
#[cfg(feature = "actix")]
|
||||
pub use adapters::actix_adapter::connect;
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -211,7 +211,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream {
|
|||
ty: dioxus_native_core::state::MemberId,
|
||||
node: &'a dioxus_core::VNode<'a>,
|
||||
vdom: &'a dioxus_core::VirtualDom,
|
||||
children: &Vec<&Self>,
|
||||
children: &[&Self],
|
||||
ctx: &anymap::AnyMap,
|
||||
) -> Option<dioxus_native_core::state::ChildStatesChanged>{
|
||||
use dioxus_native_core::state::ChildDepState as _;
|
||||
|
@ -225,7 +225,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream {
|
|||
ty - #sum_idents,
|
||||
node,
|
||||
vdom,
|
||||
&children.iter().map(|p| &p.#child_state_idents).collect(),
|
||||
&children.iter().map(|p| &p.#child_state_idents).collect::<Vec<_>>(),
|
||||
ctx,
|
||||
).map(|mut changed|{
|
||||
for id in &mut changed.node_dep{
|
||||
|
|
342
packages/native-core-macro/tests/peristant_iterator.rs
Normal file
342
packages/native-core-macro/tests/peristant_iterator.rs
Normal file
|
@ -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)]
|
||||
struct BubbledUpStateTester(Option<String>, Vec<Box<BubbledUpStateTester>>);
|
||||
impl ChildDepState for BubbledUpStateTester {
|
||||
|
|
|
@ -15,7 +15,7 @@ dioxus-core = { path = "../core", version = "^0.2.1" }
|
|||
dioxus-html = { path = "../html", version = "^0.2.1" }
|
||||
dioxus-core-macro = { path = "../core-macro", version = "^0.2.1" }
|
||||
|
||||
stretch2 = "0.4.2"
|
||||
taffy = "0.1.0"
|
||||
smallvec = "1.6"
|
||||
fxhash = "0.2"
|
||||
anymap = "0.12.1"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
- [ ] pub display: Display,
|
||||
- [x] pub position_type: PositionType, --> kinda, stretch doesnt support everything
|
||||
- [x] pub position_type: PositionType, --> kinda, taffy doesnt support everything
|
||||
- [ ] pub direction: Direction,
|
||||
|
||||
- [x] pub flex_direction: FlexDirection,
|
||||
|
@ -9,7 +9,7 @@
|
|||
- [x] pub flex_shrink: f32,
|
||||
- [x] pub flex_basis: Dimension,
|
||||
|
||||
- [x] pub overflow: Overflow, ---> kinda implemented... stretch doesnt have support for directional overflow
|
||||
- [x] pub overflow: Overflow, ---> kinda implemented... taffy doesnt have support for directional overflow
|
||||
|
||||
- [x] pub align_items: AlignItems,
|
||||
- [x] pub align_self: AlignSelf,
|
||||
|
@ -29,7 +29,10 @@
|
|||
- [ ] pub aspect_ratio: Number,
|
||||
*/
|
||||
|
||||
use stretch2::{prelude::*, style::PositionType};
|
||||
use taffy::{
|
||||
prelude::*,
|
||||
style::{FlexDirection, PositionType},
|
||||
};
|
||||
|
||||
/// applies the entire html namespace defined in dioxus-html
|
||||
pub fn apply_layout_attributes(name: &str, value: &str, style: &mut Style) {
|
||||
|
@ -109,13 +112,7 @@ pub fn apply_layout_attributes(name: &str, value: &str, style: &mut Style) {
|
|||
"counter-reset" => {}
|
||||
|
||||
"cursor" => {}
|
||||
"direction" => {
|
||||
match value {
|
||||
"ltr" => style.direction = Direction::LTR,
|
||||
"rtl" => style.direction = Direction::RTL,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
"direction" => {}
|
||||
|
||||
"display" => apply_display(name, value, style),
|
||||
|
||||
|
@ -283,20 +280,8 @@ pub fn parse_value(value: &str) -> Option<UnitSystem> {
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_overflow(name: &str, value: &str, style: &mut Style) {
|
||||
match name {
|
||||
// todo: add more overflow support to stretch2
|
||||
"overflow" | "overflow-x" | "overflow-y" => {
|
||||
style.overflow = match value {
|
||||
"auto" => Overflow::Visible,
|
||||
"hidden" => Overflow::Hidden,
|
||||
"scroll" => Overflow::Scroll,
|
||||
"visible" => Overflow::Visible,
|
||||
_ => Overflow::Visible,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
fn apply_overflow(_name: &str, _value: &str, _style: &mut Style) {
|
||||
// todo: add overflow support to taffy
|
||||
}
|
||||
|
||||
fn apply_display(_name: &str, value: &str, style: &mut Style) {
|
||||
|
@ -307,7 +292,7 @@ fn apply_display(_name: &str, value: &str, style: &mut Style) {
|
|||
}
|
||||
|
||||
// TODO: there are way more variants
|
||||
// stretch needs to be updated to handle them
|
||||
// taffy needs to be updated to handle them
|
||||
//
|
||||
// "block" => Display::Block,
|
||||
// "inline" => Display::Inline,
|
||||
|
|
|
@ -2,3 +2,4 @@ pub mod layout_attributes;
|
|||
pub mod node_ref;
|
||||
pub mod real_dom;
|
||||
pub mod state;
|
||||
pub mod utils;
|
||||
|
|
|
@ -15,7 +15,7 @@ use crate::{
|
|||
|
||||
/// A Dom that can sync with the VirtualDom mutations intended for use in lazy renderers.
|
||||
/// The render state passes from parent to children and or accumulates state from children to parents.
|
||||
/// To get started implement [PushedDownState] and or [BubbledUpState] and call [RealDom::apply_mutations] to update the dom and [RealDom::update_state] to update the state of the nodes.
|
||||
/// To get started implement [crate::state::ParentDepState], [crate::state::NodeDepState], or [crate::state::ChildDepState] and call [RealDom::apply_mutations] to update the dom and [RealDom::update_state] to update the state of the nodes.
|
||||
#[derive(Debug)]
|
||||
pub struct RealDom<S: State> {
|
||||
root: usize,
|
||||
|
@ -712,7 +712,7 @@ impl<S: State> IndexMut<ElementId> for RealDom<S> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The node is stored client side and stores only basic data about the node. For more complete information about the node see [`domNode::element`].
|
||||
/// The node is stored client side and stores only basic data about the node.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Node<S: State> {
|
||||
/// The id of the node this node was created from.
|
||||
|
|
|
@ -44,10 +44,10 @@ pub(crate) fn union_ordered_iter<T: Ord + Debug>(
|
|||
}
|
||||
|
||||
/// This state is derived from children. For example a node's size could be derived from the size of children.
|
||||
/// Called when the current node's node properties are modified, a child's [BubbledUpState] is modified or a child is removed.
|
||||
/// Called when the current node's node properties are modified, a child's [ChildDepState] is modified or a child is removed.
|
||||
/// Called at most once per update.
|
||||
pub trait ChildDepState {
|
||||
/// The context is passed to the [PushedDownState::reduce] when it is pushed down.
|
||||
/// The context is passed to the [ChildDepState::reduce] when it is pushed down.
|
||||
/// This is sometimes nessisary for lifetime purposes.
|
||||
type Ctx;
|
||||
/// This must be either a [ChildDepState] or [NodeDepState]
|
||||
|
@ -64,10 +64,10 @@ pub trait ChildDepState {
|
|||
}
|
||||
|
||||
/// This state that is passed down to children. For example text properties (`<b>` `<i>` `<u>`) would be passed to children.
|
||||
/// Called when the current node's node properties are modified or a parrent's [PushedDownState] is modified.
|
||||
/// Called when the current node's node properties are modified or a parrent's [ParentDepState] is modified.
|
||||
/// Called at most once per update.
|
||||
pub trait ParentDepState {
|
||||
/// The context is passed to the [PushedDownState::reduce] when it is pushed down.
|
||||
/// The context is passed to the [ParentDepState::reduce] when it is pushed down.
|
||||
/// This is sometimes nessisary for lifetime purposes.
|
||||
type Ctx;
|
||||
/// This must be either a [ParentDepState] or [NodeDepState]
|
||||
|
@ -77,7 +77,7 @@ pub trait ParentDepState {
|
|||
}
|
||||
|
||||
/// This state that is upadated lazily. For example any propertys that do not effect other parts of the dom like bg-color.
|
||||
/// Called when the current node's node properties are modified or a parrent's [PushedDownState] is modified.
|
||||
/// Called when the current node's node properties are modified or a sibling's [NodeDepState] is modified.
|
||||
/// Called at most once per update.
|
||||
pub trait NodeDepState {
|
||||
type Ctx;
|
||||
|
@ -132,7 +132,7 @@ pub trait State: Default + Clone {
|
|||
ty: MemberId,
|
||||
node: &'a VNode<'a>,
|
||||
vdom: &'a dioxus_core::VirtualDom,
|
||||
children: &Vec<&Self>,
|
||||
children: &[&Self],
|
||||
ctx: &AnyMap,
|
||||
) -> Option<ChildStatesChanged>;
|
||||
/// This must be a valid resolution order. (no nodes updated before a state they rely on)
|
||||
|
|
230
packages/native-core/src/utils.rs
Normal file
230
packages/native-core/src/utils.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -43,9 +43,9 @@
|
|||
<span> | </span>
|
||||
<a href="https://github.com/DioxusLabs/example-projects"> Examples </a>
|
||||
<span> | </span>
|
||||
<a href="https://dioxuslabs.com/guide"> Guide (0.1.8) </a>
|
||||
<a href="https://dioxuslabs.com/router"> Guide (Latest) </a>
|
||||
<span> | </span>
|
||||
<a href="https://dioxuslabs.com/nightly/guide"> Guide (Master) </a>
|
||||
<a href="https://dioxuslabs.com/nightly/router"> Guide (Master) </a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
|
12
packages/rsx/Cargo.toml
Normal file
12
packages/rsx/Cargo.toml
Normal file
|
@ -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" }
|
|
@ -1,7 +1,6 @@
|
|||
//! Parse components into the VComponent VNode
|
||||
//! Parse components into the VNode::Component variant
|
||||
//! ==========================================
|
||||
//!
|
||||
//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the vcomponent format.
|
||||
//! We can be reasonably sure that whatever enters this parsing path is in the right format.
|
||||
//! This feature must support
|
||||
//! - [x] Namespaced components
|
||||
|
@ -19,19 +18,59 @@ use quote::{quote, ToTokens, TokenStreamExt};
|
|||
use syn::{
|
||||
ext::IdentExt,
|
||||
parse::{Parse, ParseBuffer, ParseStream},
|
||||
token, Expr, Ident, LitStr, Result, Token,
|
||||
spanned::Spanned,
|
||||
token, AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Result,
|
||||
Token,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct Component {
|
||||
pub name: syn::Path,
|
||||
pub prop_gen_args: Option<AngleBracketedGenericArguments>,
|
||||
pub body: Vec<ComponentField>,
|
||||
pub children: Vec<BodyNode>,
|
||||
pub manual_props: Option<Expr>,
|
||||
}
|
||||
|
||||
impl Component {
|
||||
pub fn validate_component_path(path: &syn::Path) -> Result<()> {
|
||||
// ensure path segments doesn't have PathArguments, only the last
|
||||
// segment is allowed to have one.
|
||||
if path
|
||||
.segments
|
||||
.iter()
|
||||
.take(path.segments.len() - 1)
|
||||
.any(|seg| seg.arguments != PathArguments::None)
|
||||
{
|
||||
component_path_cannot_have_arguments!(path.span());
|
||||
}
|
||||
|
||||
// ensure last segment only have value of None or AngleBracketed
|
||||
if !matches!(
|
||||
path.segments.last().unwrap().arguments,
|
||||
PathArguments::None | PathArguments::AngleBracketed(_)
|
||||
) {
|
||||
invalid_component_path!(path.span());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for Component {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
let name = syn::Path::parse_mod_style(stream)?;
|
||||
let mut name = stream.parse::<syn::Path>()?;
|
||||
Component::validate_component_path(&name)?;
|
||||
|
||||
// extract the path arguments from the path into prop_gen_args
|
||||
let prop_gen_args = name.segments.last_mut().and_then(|seg| {
|
||||
if let PathArguments::AngleBracketed(args) = seg.arguments.clone() {
|
||||
seg.arguments = PathArguments::None;
|
||||
Some(args)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let content: ParseBuffer;
|
||||
|
||||
|
@ -65,6 +104,7 @@ impl Parse for Component {
|
|||
|
||||
Ok(Self {
|
||||
name,
|
||||
prop_gen_args,
|
||||
body,
|
||||
children,
|
||||
manual_props,
|
||||
|
@ -75,6 +115,7 @@ impl Parse for Component {
|
|||
impl ToTokens for Component {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let name = &self.name;
|
||||
let prop_gen_args = &self.prop_gen_args;
|
||||
|
||||
let mut has_key = None;
|
||||
|
||||
|
@ -102,7 +143,10 @@ impl ToTokens for Component {
|
|||
}}
|
||||
}
|
||||
None => {
|
||||
let mut toks = quote! { fc_to_builder(#name) };
|
||||
let mut toks = match prop_gen_args {
|
||||
Some(gen_args) => quote! { fc_to_builder #gen_args(#name) },
|
||||
None => quote! { fc_to_builder(#name) },
|
||||
};
|
||||
for field in &self.body {
|
||||
match field.name.to_string().as_str() {
|
||||
"key" => {
|
||||
|
@ -149,12 +193,14 @@ impl ToTokens for Component {
|
|||
}
|
||||
|
||||
// the struct's fields info
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct ComponentField {
|
||||
name: Ident,
|
||||
content: ContentField,
|
||||
pub name: Ident,
|
||||
pub content: ContentField,
|
||||
}
|
||||
|
||||
enum ContentField {
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum ContentField {
|
||||
ManExpr(Expr),
|
||||
Formatted(LitStr),
|
||||
OnHandlerRaw(Expr),
|
|
@ -4,12 +4,13 @@ use proc_macro2::TokenStream as TokenStream2;
|
|||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseBuffer, ParseStream},
|
||||
Expr, Ident, LitStr, Result, Token,
|
||||
Error, Expr, Ident, LitStr, Result, Token,
|
||||
};
|
||||
|
||||
// =======================================
|
||||
// Parse the VNode::Element type
|
||||
// =======================================
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct Element {
|
||||
pub name: Ident,
|
||||
pub key: Option<LitStr>,
|
||||
|
@ -64,7 +65,7 @@ impl Parse for Element {
|
|||
}
|
||||
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
missing_trailing_comma!(ident);
|
||||
missing_trailing_comma!(ident.span());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
@ -122,7 +123,7 @@ impl Parse for Element {
|
|||
|
||||
// todo: add a message saying you need to include commas between fields
|
||||
if content.parse::<Token![,]>().is_err() {
|
||||
missing_trailing_comma!(ident);
|
||||
missing_trailing_comma!(ident.span());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
@ -189,6 +190,7 @@ impl ToTokens for Element {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum ElementAttr {
|
||||
/// attribute: "valuee {}"
|
||||
AttrText { name: Ident, value: LitStr },
|
||||
|
@ -208,6 +210,7 @@ pub enum ElementAttr {
|
|||
EventTokens { name: Ident, tokens: Expr },
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct ElementAttrNamed {
|
||||
pub el_name: Ident,
|
||||
pub attr: ElementAttr,
|
26
packages/rsx/src/errors.rs
Normal file
26
packages/rsx/src/errors.rs
Normal file
|
@ -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"));
|
||||
};
|
||||
}
|
176
packages/rsx/src/ifmt.rs
Normal file
176
packages/rsx/src/ifmt.rs
Normal file
|
@ -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 element;
|
||||
mod ifmt;
|
||||
mod node;
|
||||
|
||||
pub mod pretty;
|
||||
|
||||
// Re-export the namespaces into each other
|
||||
pub use component::*;
|
||||
pub use element::*;
|
||||
pub use ifmt::*;
|
||||
pub use node::*;
|
||||
|
||||
// imports
|
101
packages/rsx/src/node.rs
Normal file
101
packages/rsx/src/node.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
use super::*;
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
token, Expr, LitStr, Result,
|
||||
};
|
||||
|
||||
/*
|
||||
Parse
|
||||
-> div {}
|
||||
-> Component {}
|
||||
-> component()
|
||||
-> "text {with_args}"
|
||||
-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr
|
||||
*/
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum BodyNode {
|
||||
Element(Element),
|
||||
Component(Component),
|
||||
Text(LitStr),
|
||||
RawExpr(Expr),
|
||||
}
|
||||
|
||||
impl BodyNode {
|
||||
pub fn is_litstr(&self) -> bool {
|
||||
matches!(self, BodyNode::Text(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for BodyNode {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
if stream.peek(LitStr) {
|
||||
return Ok(BodyNode::Text(stream.parse()?));
|
||||
}
|
||||
|
||||
let body_stream = stream.fork();
|
||||
if let Ok(path) = body_stream.parse::<syn::Path>() {
|
||||
// this is an Element if path match of:
|
||||
// - one ident
|
||||
// - followed by `{`
|
||||
// - 1st char is lowercase
|
||||
//
|
||||
// example:
|
||||
// div {}
|
||||
if let Some(ident) = path.get_ident() {
|
||||
if body_stream.peek(token::Brace)
|
||||
&& ident
|
||||
.to_string()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap()
|
||||
.is_ascii_lowercase()
|
||||
{
|
||||
return Ok(BodyNode::Element(stream.parse::<Element>()?));
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise this should be Component, allowed syntax:
|
||||
// - syn::Path
|
||||
// - PathArguments can only apper in last segment
|
||||
// - followed by `{` or `(`, note `(` cannot be used with one ident
|
||||
//
|
||||
// example
|
||||
// Div {}
|
||||
// Div ()
|
||||
// ::Div {}
|
||||
// crate::Div {}
|
||||
// component()
|
||||
// ::component {}
|
||||
// ::component ()
|
||||
// crate::component{}
|
||||
// crate::component()
|
||||
// Input::<InputProps<'_, i32> {}
|
||||
// crate::Input::<InputProps<'_, i32> {}
|
||||
if body_stream.peek(token::Brace) || body_stream.peek(token::Paren) {
|
||||
Component::validate_component_path(&path)?;
|
||||
|
||||
return Ok(BodyNode::Component(stream.parse()?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BodyNode::RawExpr(stream.parse::<Expr>()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for BodyNode {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match &self {
|
||||
BodyNode::Element(el) => el.to_tokens(tokens),
|
||||
BodyNode::Component(comp) => comp.to_tokens(tokens),
|
||||
BodyNode::Text(txt) => tokens.append_all(quote! {
|
||||
__cx.text(format_args_f!(#txt))
|
||||
}),
|
||||
BodyNode::RawExpr(exp) => tokens.append_all(quote! {
|
||||
__cx.fragment_from_iter(#exp)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
23
packages/rsx_interpreter/Cargo.toml
Normal file
23
packages/rsx_interpreter/Cargo.toml
Normal file
|
@ -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"] }
|
869
packages/rsx_interpreter/src/attributes.rs
Normal file
869
packages/rsx_interpreter/src/attributes.rs
Normal file
|
@ -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",
|
||||
}
|
||||
}
|
176
packages/rsx_interpreter/src/captuered_context.rs
Normal file
176
packages/rsx_interpreter/src/captuered_context.rs
Normal file
|
@ -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,
|
||||
}
|
1377
packages/rsx_interpreter/src/elements.rs
Normal file
1377
packages/rsx_interpreter/src/elements.rs
Normal file
File diff suppressed because it is too large
Load diff
39
packages/rsx_interpreter/src/error.rs
Normal file
39
packages/rsx_interpreter/src/error.rs
Normal file
|
@ -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 }
|
||||
}
|
||||
}
|
251
packages/rsx_interpreter/src/interperter.rs
Normal file
251
packages/rsx_interpreter/src/interperter.rs
Normal file
|
@ -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()),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
182
packages/rsx_interpreter/src/lib.rs
Normal file
182
packages/rsx_interpreter/src/lib.rs
Normal file
|
@ -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>);
|
353
packages/rsx_interpreter/tests/render.rs
Normal file
353
packages/rsx_interpreter/tests/render.rs
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -23,7 +23,12 @@ crossterm = "0.23.0"
|
|||
anyhow = "1.0.42"
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
futures = "0.3.19"
|
||||
stretch2 = "0.4.2"
|
||||
taffy = "0.1.0"
|
||||
smallvec = "1.6"
|
||||
fxhash = "0.2"
|
||||
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
|
||||
- [ ] CSS selectors
|
||||
- [x] inline CSS support
|
||||
- [ ] Built-in focusing system
|
||||
- [x] Built-in focusing system
|
||||
- [ ] high-quality keyboard support
|
||||
- [ ] Support for events, hooks, and callbacks
|
||||
* [ ] Html tags<sup>1</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.
|
||||
|
||||
* [ ] Support for events, hooks, and callbacks<sup>1</sup>
|
||||
* [ ] Html tags<sup>2</sup>
|
||||
|
||||
<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.
|
||||
|
|
282
packages/tui/src/focus.rs
Normal file
282
packages/tui/src/focus.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -11,17 +11,18 @@ use dioxus_html::geometry::{
|
|||
use dioxus_html::input_data::keyboard_types::{Code, Key, Location, Modifiers};
|
||||
use dioxus_html::input_data::MouseButtonSet as DioxusMouseButtons;
|
||||
use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet};
|
||||
use dioxus_html::on::*;
|
||||
use dioxus_html::{event_bubbles, on::*, KeyCode};
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
cell::{RefCell, RefMut},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use stretch2::geometry::{Point, Size};
|
||||
use stretch2::{prelude::Layout, Stretch};
|
||||
use taffy::geometry::{Point, Size};
|
||||
use taffy::{prelude::Layout, Taffy};
|
||||
|
||||
use crate::FocusState;
|
||||
use crate::{Dom, Node};
|
||||
|
||||
// a wrapper around the input state for easier access
|
||||
|
@ -38,22 +39,22 @@ use crate::{Dom, Node};
|
|||
|
||||
// pub fn mouse(&self) -> Option<MouseData> {
|
||||
// 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> {
|
||||
// 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)> {
|
||||
// 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> {
|
||||
// let data = (**self.0).borrow();
|
||||
// data.last_key_pressed
|
||||
// last_key_pressed
|
||||
// .as_ref()
|
||||
// .map(|k| &k.0.clone())
|
||||
// }
|
||||
|
@ -86,6 +87,7 @@ pub struct InnerInputState {
|
|||
wheel: Option<WheelData>,
|
||||
last_key_pressed: Option<(KeyboardData, Instant)>,
|
||||
screen: Option<(u16, u16)>,
|
||||
pub(crate) focus_state: FocusState,
|
||||
// subscribers: Vec<Rc<dyn Fn() + 'static>>,
|
||||
}
|
||||
|
||||
|
@ -97,6 +99,7 @@ impl InnerInputState {
|
|||
last_key_pressed: None,
|
||||
screen: None,
|
||||
// subscribers: Vec::new(),
|
||||
focus_state: FocusState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,31 +160,72 @@ impl InnerInputState {
|
|||
|
||||
fn update(
|
||||
&mut self,
|
||||
evts: &mut [EventCore],
|
||||
evts: &mut Vec<EventCore>,
|
||||
resolved_events: &mut Vec<UserEvent>,
|
||||
layout: &Stretch,
|
||||
layout: &Taffy,
|
||||
dom: &mut Dom,
|
||||
) {
|
||||
let previous_mouse = self.mouse.clone();
|
||||
|
||||
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() {
|
||||
self.apply_event(e);
|
||||
}
|
||||
|
||||
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 {}),
|
||||
bubbles: event_bubbles("focus"),
|
||||
});
|
||||
resolved_events.push(UserEvent {
|
||||
scope_id: None,
|
||||
priority: EventPriority::Medium,
|
||||
name: "focusin",
|
||||
element: Some(id),
|
||||
data: Arc::new(FocusData {}),
|
||||
bubbles: event_bubbles("focusin"),
|
||||
});
|
||||
}
|
||||
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 {}),
|
||||
bubbles: event_bubbles("focusout"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// for s in &self.subscribers {
|
||||
// s();
|
||||
// }
|
||||
}
|
||||
|
||||
fn resolve_mouse_events(
|
||||
&self,
|
||||
&mut self,
|
||||
previous_mouse: Option<MouseData>,
|
||||
resolved_events: &mut Vec<UserEvent>,
|
||||
layout: &Stretch,
|
||||
layout: &Taffy,
|
||||
dom: &mut Dom,
|
||||
) {
|
||||
fn layout_contains_point(layout: &Layout, point: ScreenPoint) -> bool {
|
||||
|
@ -213,6 +257,7 @@ impl InnerInputState {
|
|||
name,
|
||||
element: Some(node.id),
|
||||
data,
|
||||
bubbles: event_bubbles(name),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -265,16 +310,16 @@ impl InnerInputState {
|
|||
if old_pos != Some(new_pos) {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
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
|
||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.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 {
|
||||
try_create_event(
|
||||
"mousemove",
|
||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
||||
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
|
@ -289,11 +334,11 @@ impl InnerInputState {
|
|||
// mouseenter
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
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
|
||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.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 {
|
||||
try_create_event(
|
||||
|
@ -312,16 +357,16 @@ impl InnerInputState {
|
|||
// mouseover
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
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
|
||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.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 {
|
||||
try_create_event(
|
||||
"mouseover",
|
||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
||||
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
|
@ -335,13 +380,13 @@ impl InnerInputState {
|
|||
if was_pressed {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("mousedown") {
|
||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
"mousedown",
|
||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
||||
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
|
@ -356,13 +401,13 @@ impl InnerInputState {
|
|||
if was_released {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("mouseup") {
|
||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
"mouseup",
|
||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
||||
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
|
@ -378,13 +423,13 @@ impl InnerInputState {
|
|||
if mouse_data.trigger_button() == Some(DioxusMouseButton::Primary) && was_released {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("click") {
|
||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
"click",
|
||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
||||
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
|
@ -401,13 +446,13 @@ impl InnerInputState {
|
|||
{
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("contextmenu") {
|
||||
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
"contextmenu",
|
||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
||||
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
|
@ -424,9 +469,9 @@ impl InnerInputState {
|
|||
if was_scrolled {
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
for node in dom.get_listening_sorted("wheel") {
|
||||
let node_layout =
|
||||
layout.layout(node.state.layout.node.unwrap()).unwrap();
|
||||
let currently_contains = layout_contains_point(node_layout, new_pos);
|
||||
let node_layout = get_abs_layout(node, dom, layout);
|
||||
|
||||
let currently_contains = layout_contains_point(&node_layout, new_pos);
|
||||
|
||||
if currently_contains {
|
||||
try_create_event(
|
||||
|
@ -447,16 +492,16 @@ impl InnerInputState {
|
|||
// mouseleave
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
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
|
||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.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 {
|
||||
try_create_event(
|
||||
"mouseleave",
|
||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
||||
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
|
@ -470,16 +515,16 @@ impl InnerInputState {
|
|||
// mouseout
|
||||
let mut will_bubble = FxHashSet::default();
|
||||
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
|
||||
.filter(|pos| layout_contains_point(node_layout, *pos))
|
||||
.filter(|pos| layout_contains_point(&node_layout, *pos))
|
||||
.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 {
|
||||
try_create_event(
|
||||
"mouseout",
|
||||
Arc::new(prepare_mouse_data(mouse_data, node_layout)),
|
||||
Arc::new(prepare_mouse_data(mouse_data, &node_layout)),
|
||||
&mut will_bubble,
|
||||
resolved_events,
|
||||
node,
|
||||
|
@ -488,6 +533,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -496,6 +557,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 {
|
||||
state: Rc<RefCell<InnerInputState>>,
|
||||
queued_events: Rc<RefCell<Vec<EventCore>>>,
|
||||
|
@ -532,7 +607,11 @@ impl RinkInputHandler {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get_events(&self, layout: &Stretch, dom: &mut Dom) -> Vec<UserEvent> {
|
||||
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> {
|
||||
let mut resolved_events = Vec::new();
|
||||
|
||||
(*self.state).borrow_mut().update(
|
||||
|
@ -565,7 +644,6 @@ impl RinkInputHandler {
|
|||
})
|
||||
.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();
|
||||
for (event, data) in events {
|
||||
if let Some(v) = hm.get_mut(event) {
|
||||
|
@ -577,19 +655,26 @@ impl RinkInputHandler {
|
|||
for (event, datas) in hm {
|
||||
for node in dom.get_listening_sorted(event) {
|
||||
for data in &datas {
|
||||
resolved_events.push(UserEvent {
|
||||
scope_id: None,
|
||||
priority: EventPriority::Medium,
|
||||
name: event,
|
||||
element: Some(node.id),
|
||||
data: data.clone(),
|
||||
});
|
||||
if node.state.focused {
|
||||
resolved_events.push(UserEvent {
|
||||
scope_id: None,
|
||||
priority: EventPriority::Medium,
|
||||
name: event,
|
||||
element: Some(node.id),
|
||||
data: data.clone(),
|
||||
bubbles: event_bubbles(event),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolved_events
|
||||
}
|
||||
|
||||
pub(crate) fn state(&self) -> RefMut<InnerInputState> {
|
||||
self.state.borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
// translate crossterm events into dioxus events
|
||||
|
|
|
@ -6,7 +6,7 @@ use dioxus_native_core::layout_attributes::apply_layout_attributes;
|
|||
use dioxus_native_core::node_ref::{AttributeMask, NodeMask, NodeView};
|
||||
use dioxus_native_core::state::ChildDepState;
|
||||
use dioxus_native_core_macro::sorted_str_slice;
|
||||
use stretch2::prelude::*;
|
||||
use taffy::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum PossiblyUninitalized<T> {
|
||||
|
@ -28,13 +28,13 @@ impl<T> Default for PossiblyUninitalized<T> {
|
|||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Default, Debug)]
|
||||
pub(crate) struct StretchLayout {
|
||||
pub(crate) struct TaffyLayout {
|
||||
pub style: Style,
|
||||
pub node: PossiblyUninitalized<Node>,
|
||||
}
|
||||
|
||||
impl ChildDepState for StretchLayout {
|
||||
type Ctx = Rc<RefCell<Stretch>>;
|
||||
impl ChildDepState for TaffyLayout {
|
||||
type Ctx = Rc<RefCell<Taffy>>;
|
||||
type DepState = Self;
|
||||
// use tag to force this to be called when a node is built
|
||||
const NODE_MASK: NodeMask =
|
||||
|
@ -53,7 +53,7 @@ impl ChildDepState for StretchLayout {
|
|||
Self::DepState: 'a,
|
||||
{
|
||||
let mut changed = false;
|
||||
let mut stretch = ctx.borrow_mut();
|
||||
let mut taffy = ctx.borrow_mut();
|
||||
let mut style = Style::default();
|
||||
if let Some(text) = node.text() {
|
||||
let char_len = text.chars().count();
|
||||
|
@ -70,11 +70,10 @@ impl ChildDepState for StretchLayout {
|
|||
};
|
||||
if let PossiblyUninitalized::Initialized(n) = self.node {
|
||||
if self.style != style {
|
||||
stretch.set_style(n, style).unwrap();
|
||||
taffy.set_style(n, style).unwrap();
|
||||
}
|
||||
} else {
|
||||
self.node =
|
||||
PossiblyUninitalized::Initialized(stretch.new_node(style, &[]).unwrap());
|
||||
self.node = PossiblyUninitalized::Initialized(taffy.new_node(style, &[]).unwrap());
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
|
@ -100,14 +99,14 @@ impl ChildDepState for StretchLayout {
|
|||
|
||||
if let PossiblyUninitalized::Initialized(n) = self.node {
|
||||
if self.style != style {
|
||||
stretch.set_style(n, style).unwrap();
|
||||
taffy.set_style(n, style).unwrap();
|
||||
}
|
||||
if stretch.children(n).unwrap() != child_layout {
|
||||
stretch.set_children(n, &child_layout).unwrap();
|
||||
if taffy.children(n).unwrap() != child_layout {
|
||||
taffy.set_children(n, &child_layout).unwrap();
|
||||
}
|
||||
} else {
|
||||
self.node = PossiblyUninitalized::Initialized(
|
||||
stretch.new_node(style, &child_layout).unwrap(),
|
||||
taffy.new_node(style, &child_layout).unwrap(),
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
|
|
|
@ -7,23 +7,23 @@ use crossterm::{
|
|||
};
|
||||
use dioxus_core::exports::futures_channel::mpsc::unbounded;
|
||||
use dioxus_core::*;
|
||||
use dioxus_native_core::{real_dom::RealDom, state::*};
|
||||
use dioxus_native_core_macro::State;
|
||||
use dioxus_native_core::real_dom::RealDom;
|
||||
use focus::FocusState;
|
||||
use futures::{
|
||||
channel::mpsc::{UnboundedReceiver, UnboundedSender},
|
||||
pin_mut, StreamExt,
|
||||
};
|
||||
use layout::StretchLayout;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::{io, time::Duration};
|
||||
use stretch2::{prelude::Size, Stretch};
|
||||
use style_attributes::StyleModifier;
|
||||
use taffy::{geometry::Point, prelude::Size, Taffy};
|
||||
use tui::{backend::CrosstermBackend, layout::Rect, Terminal};
|
||||
|
||||
mod config;
|
||||
mod focus;
|
||||
mod hooks;
|
||||
mod layout;
|
||||
mod node;
|
||||
mod render;
|
||||
mod style;
|
||||
mod style_attributes;
|
||||
|
@ -31,18 +31,7 @@ mod widget;
|
|||
|
||||
pub use config::*;
|
||||
pub use hooks::*;
|
||||
|
||||
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<Stretch>)]
|
||||
layout: StretchLayout,
|
||||
// depends on attributes, the C component of it's parent and a u8 context
|
||||
#[parent_dep_state(style)]
|
||||
style: StyleModifier,
|
||||
}
|
||||
pub(crate) use node::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TuiContext {
|
||||
|
@ -52,6 +41,12 @@ impl TuiContext {
|
|||
pub fn quit(&self) {
|
||||
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<()>) {
|
||||
|
@ -71,7 +66,6 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) {
|
|||
let tick_rate = Duration::from_millis(1000);
|
||||
loop {
|
||||
if crossterm::event::poll(tick_rate).unwrap() {
|
||||
// if crossterm::event::poll(timeout).unwrap() {
|
||||
let evt = crossterm::event::read().unwrap();
|
||||
if event_tx.unbounded_send(InputEvent::UserInput(evt)).is_err() {
|
||||
break;
|
||||
|
@ -88,9 +82,9 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) {
|
|||
let mut rdom: Dom = RealDom::new();
|
||||
let mutations = dom.rebuild();
|
||||
let to_update = rdom.apply_mutations(vec![mutations]);
|
||||
let stretch = Rc::new(RefCell::new(Stretch::new()));
|
||||
let taffy = Rc::new(RefCell::new(Taffy::new()));
|
||||
let mut any_map = AnyMap::new();
|
||||
any_map.insert(stretch.clone());
|
||||
any_map.insert(taffy.clone());
|
||||
let _to_rerender = rdom.update_state(&dom, to_update, any_map).unwrap();
|
||||
|
||||
render_vdom(
|
||||
|
@ -99,7 +93,7 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) {
|
|||
handler,
|
||||
cfg,
|
||||
rdom,
|
||||
stretch,
|
||||
taffy,
|
||||
register_event,
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -111,7 +105,7 @@ fn render_vdom(
|
|||
handler: RinkInputHandler,
|
||||
cfg: Config,
|
||||
mut rdom: Dom,
|
||||
stretch: Rc<RefCell<Stretch>>,
|
||||
taffy: Rc<RefCell<Taffy>>,
|
||||
mut register_event: impl FnMut(crossterm::event::Event),
|
||||
) -> Result<()> {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
|
@ -130,7 +124,7 @@ fn render_vdom(
|
|||
}
|
||||
|
||||
let mut to_rerender: fxhash::FxHashSet<usize> = vec![0].into_iter().collect();
|
||||
let mut resized = true;
|
||||
let mut updated = true;
|
||||
|
||||
loop {
|
||||
/*
|
||||
|
@ -144,19 +138,19 @@ fn render_vdom(
|
|||
todo: lazy re-rendering
|
||||
*/
|
||||
|
||||
if !to_rerender.is_empty() || resized {
|
||||
resized = false;
|
||||
fn resize(dims: Rect, stretch: &mut Stretch, rdom: &Dom) {
|
||||
if !to_rerender.is_empty() || updated {
|
||||
updated = false;
|
||||
fn resize(dims: Rect, taffy: &mut Taffy, rdom: &Dom) {
|
||||
let width = dims.width;
|
||||
let height = dims.height;
|
||||
let root_node = rdom[0].state.layout.node.unwrap();
|
||||
|
||||
stretch
|
||||
taffy
|
||||
.compute_layout(
|
||||
root_node,
|
||||
Size {
|
||||
width: stretch2::prelude::Number::Defined((width - 1) as f32),
|
||||
height: stretch2::prelude::Number::Defined((height - 1) as f32),
|
||||
width: taffy::prelude::Number::Defined((width - 1) as f32),
|
||||
height: taffy::prelude::Number::Defined((height - 1) as f32),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -164,19 +158,26 @@ fn render_vdom(
|
|||
if let Some(terminal) = &mut terminal {
|
||||
terminal.draw(|frame| {
|
||||
// size is guaranteed to not change when rendering
|
||||
resize(frame.size(), &mut stretch.borrow_mut(), &rdom);
|
||||
resize(frame.size(), &mut taffy.borrow_mut(), &rdom);
|
||||
let root = &rdom[0];
|
||||
render::render_vnode(frame, &stretch.borrow(), &rdom, root, cfg);
|
||||
render::render_vnode(
|
||||
frame,
|
||||
&taffy.borrow(),
|
||||
&rdom,
|
||||
root,
|
||||
cfg,
|
||||
Point::zero(),
|
||||
);
|
||||
})?;
|
||||
} else {
|
||||
resize(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 300,
|
||||
height: 300,
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
&mut stretch.borrow_mut(),
|
||||
&mut taffy.borrow_mut(),
|
||||
&rdom,
|
||||
);
|
||||
}
|
||||
|
@ -202,7 +203,7 @@ fn render_vdom(
|
|||
break;
|
||||
}
|
||||
}
|
||||
TermEvent::Resize(_, _) => resized = true,
|
||||
TermEvent::Resize(_, _) => updated = true,
|
||||
TermEvent::Mouse(_) => {}
|
||||
},
|
||||
InputEvent::Close => break,
|
||||
|
@ -216,16 +217,22 @@ fn render_vdom(
|
|||
}
|
||||
|
||||
{
|
||||
let evts = handler.get_events(&stretch.borrow(), &mut rdom);
|
||||
let evts = handler.get_events(&taffy.borrow(), &mut rdom);
|
||||
{
|
||||
updated |= handler.state().focus_state.clean();
|
||||
}
|
||||
for e in evts {
|
||||
vdom.handle_message(SchedulerMsg::Event(e));
|
||||
}
|
||||
let mutations = vdom.work_with_deadline(|| false);
|
||||
for m in &mutations {
|
||||
handler.prune(m, &rdom);
|
||||
}
|
||||
// updates the dom's nodes
|
||||
let to_update = rdom.apply_mutations(mutations);
|
||||
// update the style and layout
|
||||
let mut any_map = AnyMap::new();
|
||||
any_map.insert(stretch.clone());
|
||||
any_map.insert(taffy.clone());
|
||||
to_rerender = rdom.update_state(vdom, to_update, any_map).unwrap();
|
||||
}
|
||||
}
|
||||
|
@ -244,6 +251,7 @@ fn render_vdom(
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InputEvent {
|
||||
UserInput(TermEvent),
|
||||
Close,
|
||||
|
|
94
packages/tui/src/node.rs
Normal file
94
packages/tui/src/node.rs
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
use dioxus_native_core::layout_attributes::UnitSystem;
|
||||
use std::io::Stdout;
|
||||
use stretch2::{
|
||||
use taffy::{
|
||||
geometry::Point,
|
||||
prelude::{Layout, Size},
|
||||
Stretch,
|
||||
Taffy,
|
||||
};
|
||||
use tui::{backend::CrosstermBackend, layout::Rect};
|
||||
use tui::{backend::CrosstermBackend, layout::Rect, style::Color};
|
||||
|
||||
use crate::{
|
||||
style::{RinkColor, RinkStyle},
|
||||
|
@ -18,10 +18,11 @@ const RADIUS_MULTIPLIER: [f32; 2] = [1.0, 0.5];
|
|||
|
||||
pub(crate) fn render_vnode(
|
||||
frame: &mut tui::Frame<CrosstermBackend<Stdout>>,
|
||||
layout: &Stretch,
|
||||
layout: &Taffy,
|
||||
rdom: &Dom,
|
||||
node: &Node,
|
||||
cfg: Config,
|
||||
parent_location: Point<f32>,
|
||||
) {
|
||||
use dioxus_native_core::real_dom::NodeType;
|
||||
|
||||
|
@ -29,7 +30,11 @@ pub(crate) fn render_vnode(
|
|||
return;
|
||||
}
|
||||
|
||||
let Layout { location, size, .. } = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
||||
let Layout {
|
||||
mut location, size, ..
|
||||
} = layout.layout(node.state.layout.node.unwrap()).unwrap();
|
||||
location.x += parent_location.x;
|
||||
location.y += parent_location.y;
|
||||
|
||||
let Point { x, y } = location;
|
||||
let Size { width, height } = size;
|
||||
|
@ -57,7 +62,7 @@ pub(crate) fn render_vnode(
|
|||
text,
|
||||
style: node.state.style.core,
|
||||
};
|
||||
let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16);
|
||||
let area = Rect::new(x as u16, y as u16, *width as u16, *height as u16);
|
||||
|
||||
// the renderer will panic if a node is rendered out of range even if the size is zero
|
||||
if area.width > 0 && area.height > 0 {
|
||||
|
@ -65,7 +70,7 @@ pub(crate) fn render_vnode(
|
|||
}
|
||||
}
|
||||
NodeType::Element { children, .. } => {
|
||||
let area = Rect::new(*x as u16, *y as u16, *width as u16, *height as u16);
|
||||
let area = Rect::new(x as u16, y as u16, *width as u16, *height as u16);
|
||||
|
||||
// the renderer will panic if a node is rendered out of range even if the size is zero
|
||||
if area.width > 0 && area.height > 0 {
|
||||
|
@ -73,7 +78,7 @@ pub(crate) fn render_vnode(
|
|||
}
|
||||
|
||||
for c in children {
|
||||
render_vnode(frame, layout, rdom, &rdom[c.0], cfg);
|
||||
render_vnode(frame, layout, rdom, &rdom[c.0], cfg, location);
|
||||
}
|
||||
}
|
||||
NodeType::Placeholder => unreachable!(),
|
||||
|
@ -267,6 +272,10 @@ impl RinkWidget for &Node {
|
|||
if let Some(c) = self.state.style.core.bg {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
- [ ] pub display: Display,
|
||||
- [x] pub position_type: PositionType, --> kinda, stretch doesnt support everything
|
||||
- [x] pub position_type: PositionType, --> kinda, taffy doesnt support everything
|
||||
- [ ] pub direction: Direction,
|
||||
|
||||
- [x] pub flex_direction: FlexDirection,
|
||||
|
@ -9,7 +9,7 @@
|
|||
- [x] pub flex_shrink: f32,
|
||||
- [x] pub flex_basis: Dimension,
|
||||
|
||||
- [x] pub overflow: Overflow, ---> kinda implemented... stretch doesnt have support for directional overflow
|
||||
- [x] pub overflow: Overflow, ---> kinda implemented... taffy doesnt have support for directional overflow
|
||||
|
||||
- [x] pub align_items: AlignItems,
|
||||
- [x] pub align_self: AlignSelf,
|
||||
|
|
|
@ -57,7 +57,7 @@ impl<T: RinkWidget> WidgetWithContext<T> {
|
|||
|
||||
impl<T: RinkWidget> Widget for WidgetWithContext<T> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
377
packages/tui/tests/events.rs
Normal file
377
packages/tui/tests/events.rs
Normal file
|
@ -0,0 +1,377 @@
|
|||
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;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
/// The tui renderer will look for any event that has occured or any future that has resolved in a loop.
|
||||
/// It will resolve at most one event per loop.
|
||||
/// This future will resolve after a certain number of polls. If the number of polls is greater than the number of events triggered, and the event has not been recieved there is an issue with the event system.
|
||||
struct PollN(usize);
|
||||
impl PollN {
|
||||
fn new(n: usize) -> Self {
|
||||
PollN(n)
|
||||
}
|
||||
}
|
||||
impl Future for PollN {
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if self.0 == 0 {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
self.0 -= 1;
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
PollN::new(3).await;
|
||||
render_count_handle.modify(|x| *x + 1);
|
||||
});
|
||||
if *render_count.get() > 2 {
|
||||
panic!("Event was not received");
|
||||
}
|
||||
// focus the element
|
||||
tui_ctx.inject_event(Event::Key(KeyEvent {
|
||||
code: KeyCode::Tab,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}));
|
||||
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 {
|
||||
PollN::new(2).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 {
|
||||
PollN::new(3).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 {
|
||||
PollN::new(3).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 {
|
||||
PollN::new(3).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 {
|
||||
PollN::new(3).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 {
|
||||
PollN::new(3).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 {
|
||||
PollN::new(3).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 {
|
||||
PollN::new(3).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();
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,15 +1,13 @@
|
|||
use stretch2 as stretch;
|
||||
|
||||
#[test]
|
||||
fn margin_and_flex_row() {
|
||||
let mut stretch = stretch::Stretch::new();
|
||||
let node0 = stretch
|
||||
let mut taffy = taffy::Taffy::new();
|
||||
let node0 = taffy
|
||||
.new_node(
|
||||
stretch::style::Style {
|
||||
taffy::style::Style {
|
||||
flex_grow: 1f32,
|
||||
margin: stretch::geometry::Rect {
|
||||
start: stretch::style::Dimension::Points(10f32),
|
||||
end: stretch::style::Dimension::Points(10f32),
|
||||
margin: taffy::geometry::Rect {
|
||||
start: taffy::style::Dimension::Points(10f32),
|
||||
end: taffy::style::Dimension::Points(10f32),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
|
@ -17,50 +15,50 @@ fn margin_and_flex_row() {
|
|||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
let node = stretch
|
||||
let node = taffy
|
||||
.new_node(
|
||||
stretch::style::Style {
|
||||
size: stretch::geometry::Size {
|
||||
width: stretch::style::Dimension::Points(100f32),
|
||||
height: stretch::style::Dimension::Points(100f32),
|
||||
taffy::style::Style {
|
||||
size: taffy::geometry::Size {
|
||||
width: taffy::style::Dimension::Points(100f32),
|
||||
height: taffy::style::Dimension::Points(100f32),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&[node0],
|
||||
)
|
||||
.unwrap();
|
||||
stretch
|
||||
.compute_layout(node, stretch::geometry::Size::undefined())
|
||||
taffy
|
||||
.compute_layout(node, taffy::geometry::Size::undefined())
|
||||
.unwrap();
|
||||
assert_eq!(stretch.layout(node).unwrap().size.width, 100f32);
|
||||
assert_eq!(stretch.layout(node).unwrap().size.height, 100f32);
|
||||
assert_eq!(stretch.layout(node).unwrap().location.x, 0f32);
|
||||
assert_eq!(stretch.layout(node).unwrap().location.y, 0f32);
|
||||
assert_eq!(stretch.layout(node0).unwrap().size.width, 80f32);
|
||||
assert_eq!(stretch.layout(node0).unwrap().size.height, 100f32);
|
||||
assert_eq!(stretch.layout(node0).unwrap().location.x, 10f32);
|
||||
assert_eq!(stretch.layout(node0).unwrap().location.y, 0f32);
|
||||
assert_eq!(taffy.layout(node).unwrap().size.width, 100f32);
|
||||
assert_eq!(taffy.layout(node).unwrap().size.height, 100f32);
|
||||
assert_eq!(taffy.layout(node).unwrap().location.x, 0f32);
|
||||
assert_eq!(taffy.layout(node).unwrap().location.y, 0f32);
|
||||
assert_eq!(taffy.layout(node0).unwrap().size.width, 80f32);
|
||||
assert_eq!(taffy.layout(node0).unwrap().size.height, 100f32);
|
||||
assert_eq!(taffy.layout(node0).unwrap().location.x, 10f32);
|
||||
assert_eq!(taffy.layout(node0).unwrap().location.y, 0f32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn margin_and_flex_row2() {
|
||||
let mut stretch = stretch::Stretch::new();
|
||||
let node0 = stretch
|
||||
let mut taffy = taffy::Taffy::new();
|
||||
let node0 = taffy
|
||||
.new_node(
|
||||
stretch::style::Style {
|
||||
taffy::style::Style {
|
||||
flex_grow: 1f32,
|
||||
margin: stretch::geometry::Rect {
|
||||
margin: taffy::geometry::Rect {
|
||||
// left
|
||||
start: stretch::style::Dimension::Points(10f32),
|
||||
start: taffy::style::Dimension::Points(10f32),
|
||||
|
||||
// right?
|
||||
end: stretch::style::Dimension::Points(10f32),
|
||||
end: taffy::style::Dimension::Points(10f32),
|
||||
|
||||
// top?
|
||||
// top: stretch::style::Dimension::Points(10f32),
|
||||
// top: taffy::style::Dimension::Points(10f32),
|
||||
|
||||
// bottom?
|
||||
// bottom: stretch::style::Dimension::Points(10f32),
|
||||
// bottom: taffy::style::Dimension::Points(10f32),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
|
@ -69,12 +67,12 @@ fn margin_and_flex_row2() {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let node = stretch
|
||||
let node = taffy
|
||||
.new_node(
|
||||
stretch::style::Style {
|
||||
size: stretch::geometry::Size {
|
||||
width: stretch::style::Dimension::Points(100f32),
|
||||
height: stretch::style::Dimension::Points(100f32),
|
||||
taffy::style::Style {
|
||||
size: taffy::geometry::Size {
|
||||
width: taffy::style::Dimension::Points(100f32),
|
||||
height: taffy::style::Dimension::Points(100f32),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -82,12 +80,12 @@ fn margin_and_flex_row2() {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
stretch
|
||||
.compute_layout(node, stretch::geometry::Size::undefined())
|
||||
taffy
|
||||
.compute_layout(node, taffy::geometry::Size::undefined())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(stretch.layout(node).unwrap().size.width, 100f32);
|
||||
assert_eq!(stretch.layout(node).unwrap().size.height, 100f32);
|
||||
assert_eq!(stretch.layout(node).unwrap().location.x, 0f32);
|
||||
assert_eq!(stretch.layout(node).unwrap().location.y, 0f32);
|
||||
assert_eq!(taffy.layout(node).unwrap().size.width, 100f32);
|
||||
assert_eq!(taffy.layout(node).unwrap().size.height, 100f32);
|
||||
assert_eq!(taffy.layout(node).unwrap().location.x, 0f32);
|
||||
assert_eq!(taffy.layout(node).unwrap().location.y, 0f32);
|
||||
}
|
||||
|
|
|
@ -11,11 +11,12 @@ documentation = "https://dioxuslabs.com"
|
|||
keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.2.1" }
|
||||
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
|
||||
"web"
|
||||
] }
|
||||
dioxus-rsx-interpreter = { path = "../rsx_interpreter", version = "*", optional = true }
|
||||
|
||||
js-sys = "0.3.56"
|
||||
wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] }
|
||||
|
@ -30,6 +31,7 @@ futures-util = "0.3.19"
|
|||
smallstr = "0.2.0"
|
||||
serde-wasm-bindgen = "0.4.2"
|
||||
futures-channel = "0.3.21"
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.56"
|
||||
|
@ -75,6 +77,7 @@ features = [
|
|||
[features]
|
||||
default = ["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]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
//! - Partial delegation?>
|
||||
|
||||
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
|
||||
use dioxus_html::event_bubbles;
|
||||
use dioxus_interpreter_js::Interpreter;
|
||||
use js_sys::Function;
|
||||
use std::{any::Any, rc::Rc, sync::Arc};
|
||||
|
@ -45,6 +46,7 @@ impl WebsysDom {
|
|||
element: Some(ElementId(id)),
|
||||
scope_id: None,
|
||||
priority: dioxus_core::EventPriority::Medium,
|
||||
bubbles: event.bubbles(),
|
||||
});
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
|
@ -64,6 +66,7 @@ impl WebsysDom {
|
|||
element: None,
|
||||
scope_id: None,
|
||||
priority: dioxus_core::EventPriority::Low,
|
||||
bubbles: event.bubbles(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -121,12 +124,17 @@ impl WebsysDom {
|
|||
event_name, root, ..
|
||||
} => {
|
||||
let handler: &Function = self.handler.as_ref().unchecked_ref();
|
||||
self.interpreter.NewEventListener(event_name, root, handler);
|
||||
self.interpreter.NewEventListener(
|
||||
event_name,
|
||||
root,
|
||||
handler,
|
||||
event_bubbles(event_name),
|
||||
);
|
||||
}
|
||||
|
||||
DomEdit::RemoveEventListener { root, event } => {
|
||||
self.interpreter.RemoveEventListener(root, event)
|
||||
}
|
||||
DomEdit::RemoveEventListener { root, event } => self
|
||||
.interpreter
|
||||
.RemoveEventListener(root, event, event_bubbles(event)),
|
||||
|
||||
DomEdit::RemoveAttribute { root, name, ns } => {
|
||||
self.interpreter.RemoveAttribute(root, name, ns)
|
||||
|
@ -178,7 +186,7 @@ fn virtual_event_from_websys_event(
|
|||
})
|
||||
}
|
||||
"keydown" | "keypress" | "keyup" => Arc::new(KeyboardData::from(event)),
|
||||
"focus" | "blur" => Arc::new(FocusData {}),
|
||||
"focus" | "blur" | "focusout" | "focusin" => Arc::new(FocusData {}),
|
||||
|
||||
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
|
||||
// don't have a good solution with the serialized event problem
|
||||
|
@ -258,9 +266,9 @@ fn virtual_event_from_websys_event(
|
|||
|
||||
Arc::new(FormData { value, values })
|
||||
}
|
||||
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
|
||||
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
|
||||
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
|
||||
"click" | "contextmenu" | "dblclick" | "doubleclick" | "drag" | "dragend" | "dragenter"
|
||||
| "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown"
|
||||
| "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
|
||||
Arc::new(MouseData::from(event))
|
||||
}
|
||||
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
|
||||
|
@ -305,6 +313,8 @@ fn event_name_from_typ(typ: &str) -> &'static str {
|
|||
"keypress" => "keypress",
|
||||
"keyup" => "keyup",
|
||||
"focus" => "focus",
|
||||
"focusout" => "focusout",
|
||||
"focusin" => "focusin",
|
||||
"blur" => "blur",
|
||||
"change" => "change",
|
||||
"input" => "input",
|
||||
|
@ -314,6 +324,7 @@ fn event_name_from_typ(typ: &str) -> &'static str {
|
|||
"click" => "click",
|
||||
"contextmenu" => "contextmenu",
|
||||
"doubleclick" => "doubleclick",
|
||||
"dblclick" => "dblclick",
|
||||
"drag" => "drag",
|
||||
"dragend" => "dragend",
|
||||
"dragenter" => "dragenter",
|
||||
|
@ -374,8 +385,8 @@ fn event_name_from_typ(typ: &str) -> &'static str {
|
|||
"volumechange" => "volumechange",
|
||||
"waiting" => "waiting",
|
||||
"toggle" => "toggle",
|
||||
_ => {
|
||||
panic!("unsupported event type")
|
||||
a => {
|
||||
panic!("unsupported event type {:?}", a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
// main thread.
|
||||
//
|
||||
// React solves this problem by breaking up the rendering process into a "diff" phase and a "render" phase. In Dioxus,
|
||||
// the diff phase is non-blocking, using "yield_now" to allow the browser to process other events. When the diff phase
|
||||
// the diff phase is non-blocking, using "work_with_deadline" to allow the browser to process other events. When the diff phase
|
||||
// is finally complete, the VirtualDOM will return a set of "Mutations" for this crate to apply.
|
||||
//
|
||||
// Here, we schedule the "diff" phase during the browser's idle period, achieved by calling RequestIdleCallback and then
|
||||
|
@ -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();
|
||||
|
||||
#[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 {
|
||||
log::trace!("waiting for work");
|
||||
// if virtualdom has nothing, wait for it to have something before requesting idle time
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::dom::WebsysDom;
|
||||
use dioxus_core::{VNode, VirtualDom};
|
||||
use dioxus_html::event_bubbles;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Comment, Element, Node, Text};
|
||||
|
||||
|
@ -111,6 +112,7 @@ impl WebsysDom {
|
|||
listener.event,
|
||||
listener.mounted_node.get().unwrap().as_u64(),
|
||||
self.handler.as_ref().unchecked_ref(),
|
||||
event_bubbles(listener.event),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
11
src/lib.rs
11
src/lib.rs
|
@ -44,6 +44,11 @@ pub mod events {
|
|||
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 use crate::hooks::*;
|
||||
pub use dioxus_core::prelude::*;
|
||||
|
@ -56,4 +61,10 @@ pub mod prelude {
|
|||
|
||||
#[cfg(feature = "fermi")]
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue