Merge remote-tracking branch 'upstream/master' into fix_nonbubbling_web_events

This commit is contained in:
Evan Almloff committed 2022-06-22 13:59:10 -05:00
commit 11d9a7d013
55 files changed
+5384 -437

No files matched your search

+9 -3
View File
@@ -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
+2 -2
View File
@@ -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;
});
}
+1
View File
@@ -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
View 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.
+93
View 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,
}
})
}
})
}
})
)
}
})
}
+2 -2
View File
@@ -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);
cx.render(rsx! {
+8 -3
View File
@@ -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"]
-228
View File
@@ -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))
}
+37 -5
View File
@@ -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())
@@ -31,12 +32,43 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
/// ```
#[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()
}
}
}
-31
View File
@@ -1,31 +0,0 @@
macro_rules! missing_trailing_comma {
($span:expr) => {
proc_macro_error::emit_error!($span, "missing trailing comma")
};
}
macro_rules! attr_after_element {
($span:expr) => {
proc_macro_error::emit_error!(
$span,
"expected element";
help = "move the attribute above all the children and text elements"
)
};
}
macro_rules! component_path_cannot_have_arguments {
($span:expr) => {
proc_macro_error::abort!(
$span,
"expected a path without arguments";
help = "try remove the path arguments"
)
};
}
macro_rules! invalid_component_path {
($span:expr) => {
proc_macro_error::abort!($span, "Invalid component path syntax")
};
}
-1
View File
@@ -1 +0,0 @@
//! pretty printer for rsx!
+1 -2
View File
@@ -48,5 +48,4 @@ backtrace = "0.3"
[features]
default = []
serialize = ["serde"]
debug_vdom = []
debug_vdom = []
+8
View File
@@ -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),
}
@@ -390,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);
}
}
}
}
+4 -1
View File
@@ -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" }
+88
View File
@@ -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
+1 -1
View File
@@ -96,7 +96,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,
)
+19 -26
View File
@@ -1,36 +1,29 @@
use axum::{
extract::ws::WebSocketUpgrade, response::Html, response::IntoResponse, routing::get, Extension,
Router,
};
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 {
+16 -19
View File
@@ -5,28 +5,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,
-11
View File
@@ -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)]
+2 -2
View File
@@ -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{
@@ -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 {
+1
View File
@@ -2,3 +2,4 @@ pub mod layout_attributes;
pub mod node_ref;
pub mod real_dom;
pub mod state;
pub mod utils;
+1 -1
View File
@@ -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
View 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
}
}
+12
View 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" }
@@ -18,9 +18,12 @@ use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
ext::IdentExt,
parse::{Parse, ParseBuffer, ParseStream},
token, AngleBracketedGenericArguments, Expr, Ident, LitStr, PathArguments, Result, Token,
spanned::Spanned,
token, AngleBracketedGenericArguments, Error, Expr, Ident, LitStr, PathArguments, Result,
Token,
};
#[derive(PartialEq, Eq)]
pub struct Component {
pub name: syn::Path,
pub prop_gen_args: Option<AngleBracketedGenericArguments>,
@@ -39,7 +42,7 @@ impl Component {
.take(path.segments.len() - 1)
.any(|seg| seg.arguments != PathArguments::None)
{
component_path_cannot_have_arguments!(path);
component_path_cannot_have_arguments!(path.span());
}
// ensure last segment only have value of None or AngleBracketed
@@ -47,7 +50,7 @@ impl Component {
path.segments.last().unwrap().arguments,
PathArguments::None | PathArguments::AngleBracketed(_)
) {
invalid_component_path!(path);
invalid_component_path!(path.span());
}
Ok(())
@@ -190,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
View 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
View 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(&current_captured)?,
});
break;
}
current_format_args.push(c);
}
break;
}
if c == '}' {
segments.push(Segment::Formatted {
format_args: String::new(),
segment: FormattedSegment::parse(&current_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
@@ -15,6 +15,7 @@ Parse
-> "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),
@@ -22,6 +23,12 @@ pub enum BodyNode {
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) {
+23
View 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
View 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",
}
}
@@ -0,0 +1,176 @@
use dioxus_core::{Listener, VNode};
use dioxus_rsx::{
BodyNode, CallBody, Component, ElementAttr, ElementAttrNamed, IfmtInput, Segment,
};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{Expr, Ident, Result};
use crate::CodeLocation;
#[derive(Default)]
pub struct CapturedContextBuilder {
pub ifmted: Vec<IfmtInput>,
pub components: Vec<Component>,
pub iterators: Vec<BodyNode>,
pub captured_expressions: Vec<Expr>,
pub listeners: Vec<ElementAttrNamed>,
pub custom_context: Option<Ident>,
}
impl CapturedContextBuilder {
pub fn extend(&mut self, other: CapturedContextBuilder) {
self.ifmted.extend(other.ifmted);
self.components.extend(other.components);
self.iterators.extend(other.iterators);
self.listeners.extend(other.listeners);
self.captured_expressions.extend(other.captured_expressions);
}
pub fn from_call_body(body: CallBody) -> Result<Self> {
let mut new = Self {
custom_context: body.custom_context,
..Default::default()
};
for node in body.roots {
new.extend(Self::find_captured(node)?);
}
Ok(new)
}
fn find_captured(node: BodyNode) -> Result<Self> {
let mut captured = CapturedContextBuilder::default();
match node {
BodyNode::Element(el) => {
for attr in el.attributes {
match attr.attr {
ElementAttr::AttrText { value, .. }
| ElementAttr::CustomAttrText { value, .. } => {
let value_tokens = value.to_token_stream();
let formated: IfmtInput = syn::parse2(value_tokens)?;
captured.ifmted.push(formated);
}
ElementAttr::AttrExpression { name: _, value } => {
captured.captured_expressions.push(value);
}
ElementAttr::CustomAttrExpression { name: _, value } => {
captured.captured_expressions.push(value);
}
ElementAttr::EventTokens { .. } => captured.listeners.push(attr),
}
}
if let Some(key) = el.key {
let value_tokens = key.to_token_stream();
let formated: IfmtInput = syn::parse2(value_tokens)?;
captured.ifmted.push(formated);
}
for child in el.children {
captured.extend(Self::find_captured(child)?);
}
}
BodyNode::Component(comp) => {
captured.components.push(comp);
}
BodyNode::Text(t) => {
let tokens = t.to_token_stream();
let formated: IfmtInput = syn::parse2(tokens).unwrap();
captured.ifmted.push(formated);
}
BodyNode::RawExpr(_) => captured.iterators.push(node),
}
Ok(captured)
}
}
impl ToTokens for CapturedContextBuilder {
fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) {
let CapturedContextBuilder {
ifmted,
components,
iterators,
captured_expressions,
listeners,
custom_context: _,
} = self;
let listeners_str = listeners
.iter()
.map(|comp| comp.to_token_stream().to_string());
let compontents_str = components
.iter()
.map(|comp| comp.to_token_stream().to_string());
let iterators_str = iterators.iter().map(|node| match node {
BodyNode::RawExpr(expr) => expr.to_token_stream().to_string(),
_ => unreachable!(),
});
let captured: Vec<_> = ifmted
.iter()
.flat_map(|input| input.segments.iter())
.filter_map(|seg| match seg {
Segment::Formatted {
format_args,
segment,
} => {
let expr = segment.to_token_stream();
let as_string = expr.to_string();
let format_expr = if format_args.is_empty() {
"{".to_string() + format_args + "}"
} else {
"{".to_string() + ":" + format_args + "}"
};
Some(quote! {
FormattedArg{
expr: #as_string,
format_args: #format_args,
result: format!(#format_expr, #expr)
}
})
}
_ => None,
})
.collect();
let captured_attr_expressions_text = captured_expressions
.iter()
.map(|e| format!("{}", e.to_token_stream()));
tokens.append_all(quote! {
CapturedContext {
captured: IfmtArgs{
named_args: vec![#(#captured),*]
},
components: vec![#((#compontents_str, #components)),*],
iterators: vec![#((#iterators_str, #iterators)),*],
expressions: vec![#((#captured_attr_expressions_text, #captured_expressions.to_string())),*],
listeners: vec![#((#listeners_str, #listeners)),*],
location: code_location.clone()
}
})
}
}
pub struct CapturedContext<'a> {
// map of the variable name to the formated value
pub captured: IfmtArgs,
// map of the attribute name and element path to the formated value
// pub captured_attribute_values: IfmtArgs,
// the only thing we can update in component is the children
pub components: Vec<(&'static str, VNode<'a>)>,
// we can't reasonably interpert iterators, so they are staticly inserted
pub iterators: Vec<(&'static str, VNode<'a>)>,
// map expression to the value resulting from the expression
pub expressions: Vec<(&'static str, String)>,
// map listener code to the resulting listener
pub listeners: Vec<(&'static str, Listener<'a>)>,
// used to provide better error messages
pub location: CodeLocation,
}
pub struct IfmtArgs {
// All expressions that have been resolved
pub named_args: Vec<FormattedArg>,
}
/// A formated segment that has been resolved
pub struct FormattedArg {
pub expr: &'static str,
pub format_args: &'static str,
pub result: String,
}
File diff suppressed because it is too large. Load diff
+39
View 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
View 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
View 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
View 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,
}
}
+5
View File
@@ -27,3 +27,8 @@ 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" }
+5 -6
View File
@@ -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
View 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
}
}
+130 -50
View File
@@ -12,7 +12,7 @@ use dioxus_html::input_data::{MouseButton as DioxusMouseButton, MouseButtonSet};
use dioxus_html::{event_bubbles, on::*, KeyCode};
use std::{
any::Any,
cell::RefCell,
cell::{RefCell, RefMut},
rc::Rc,
sync::Arc,
time::{Duration, Instant},
@@ -20,6 +20,7 @@ use std::{
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
@@ -36,22 +37,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())
// }
@@ -84,6 +85,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>>,
}
@@ -95,6 +97,7 @@ impl InnerInputState {
last_key_pressed: None,
screen: None,
// subscribers: Vec::new(),
focus_state: FocusState::default(),
}
}
@@ -151,7 +154,7 @@ impl InnerInputState {
fn update(
&mut self,
evts: &mut [EventCore],
evts: &mut Vec<EventCore>,
resolved_events: &mut Vec<UserEvent>,
layout: &Taffy,
dom: &mut Dom,
@@ -160,19 +163,57 @@ impl InnerInputState {
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 {}),
});
resolved_events.push(UserEvent {
scope_id: None,
priority: EventPriority::Medium,
name: "focusin",
element: Some(id),
data: Arc::new(FocusData {}),
});
}
if let Some(id) = old_focus {
resolved_events.push(UserEvent {
scope_id: None,
priority: EventPriority::Medium,
name: "focusout",
element: Some(id),
data: Arc::new(FocusData {}),
});
}
}
// for s in &self.subscribers {
// s();
// }
}
fn resolve_mouse_events(
&self,
&mut self,
previous_mouse: Option<MouseData>,
resolved_events: &mut Vec<UserEvent>,
layout: &Taffy,
@@ -257,16 +298,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,
@@ -281,11 +322,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(
@@ -304,16 +345,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,
@@ -327,13 +368,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,
@@ -348,13 +389,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,
@@ -370,13 +411,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,
@@ -393,13 +434,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,
@@ -416,9 +457,9 @@ impl InnerInputState {
if wheel_delta != 0.0 {
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(
@@ -439,16 +480,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,
@@ -462,16 +503,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,
@@ -480,6 +521,22 @@ impl InnerInputState {
}
}
}
// update focus
if was_released {
let mut focus_id = None;
dom.traverse_depth_first(|node| {
let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap();
let currently_contains = layout_contains_point(node_layout, new_pos);
if currently_contains && node.state.focus.level.focusable() {
focus_id = Some(node.id);
}
});
if let Some(id) = focus_id {
self.focus_state.set_focus(dom, id);
}
}
}
}
@@ -488,6 +545,20 @@ impl InnerInputState {
// }
}
fn get_abs_layout(node: &Node, dom: &Dom, taffy: &Taffy) -> Layout {
let mut node_layout = *taffy.layout(node.state.layout.node.unwrap()).unwrap();
let mut current = node;
while let Some(parent_id) = current.parent {
let parent = &dom[parent_id];
current = parent;
let parent_layout = taffy.layout(parent.state.layout.node.unwrap()).unwrap();
node_layout.location.x += parent_layout.location.x;
node_layout.location.y += parent_layout.location.y;
}
node_layout
}
pub struct RinkInputHandler {
state: Rc<RefCell<InnerInputState>>,
queued_events: Rc<RefCell<Vec<EventCore>>>,
@@ -524,6 +595,10 @@ impl RinkInputHandler {
)
}
pub(crate) fn prune(&self, mutations: &dioxus_core::Mutations, rdom: &Dom) {
self.state.borrow_mut().focus_state.prune(mutations, rdom);
}
pub(crate) fn get_events(&self, layout: &Taffy, dom: &mut Dom) -> Vec<UserEvent> {
let mut resolved_events = Vec::new();
@@ -557,7 +632,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) {
@@ -569,20 +643,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(),
bubbles: event_bubbles(event),
});
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
+24 -23
View File
@@ -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::TaffyLayout;
use std::cell::RefCell;
use std::rc::Rc;
use std::{io, time::Duration};
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<Taffy>)]
layout: TaffyLayout,
// 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;
@@ -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,8 +138,8 @@ fn render_vdom(
todo: lazy re-rendering
*/
if !to_rerender.is_empty() || resized {
resized = false;
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;
@@ -180,8 +174,8 @@ fn render_vdom(
Rect {
x: 0,
y: 0,
width: 300,
height: 300,
width: 100,
height: 100,
},
&mut taffy.borrow_mut(),
&rdom,
@@ -209,7 +203,7 @@ fn render_vdom(
break;
}
}
TermEvent::Resize(_, _) => resized = true,
TermEvent::Resize(_, _) => updated = true,
TermEvent::Mouse(_) => {}
},
InputEvent::Close => break,
@@ -224,10 +218,16 @@ fn render_vdom(
{
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
@@ -251,6 +251,7 @@ fn render_vdom(
})
}
#[derive(Debug)]
enum InputEvent {
UserInput(TermEvent),
Close,
+94
View 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
}
}
}
+5 -1
View File
@@ -5,7 +5,7 @@ use taffy::{
prelude::{Layout, Size},
Taffy,
};
use tui::{backend::CrosstermBackend, layout::Rect};
use tui::{backend::CrosstermBackend, layout::Rect, style::Color};
use crate::{
style::{RinkColor, RinkStyle},
@@ -272,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 -1
View File
@@ -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));
}
}
+349
View File
@@ -0,0 +1,349 @@
use std::time::Duration;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
use dioxus_core::VNode;
use dioxus_core::*;
use dioxus_core_macro::*;
use dioxus_hooks::*;
use dioxus_html as dioxus_elements;
use dioxus_tui::TuiContext;
#[test]
fn key_down() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(&cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onkeydown: move |evt| {
assert_eq!(evt.data.key_code, dioxus_html::KeyCode::A);
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_down() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(&cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onmousedown: move |evt| {
assert!(evt.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_up() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(&cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onmouseup: move |_| {
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_enter() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(&cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 100,
row: 100,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "50%",
height: "50%",
onmouseenter: move |_| {
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_exit() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(&cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 0,
row: 0,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 100,
row: 100,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "50%",
height: "50%",
onmouseenter: move |_| {
tui_ctx.quit();
},
}
})
}
}
#[test]
fn mouse_move() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(&cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 40,
row: 40,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 60,
row: 60,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onmousemove: move |_|{
tui_ctx.quit();
},
}
})
}
}
#[test]
fn wheel() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(&cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Moved,
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::ScrollDown,
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onwheel: move |evt| {
assert!(evt.data.delta_y > 0.0);
tui_ctx.quit();
},
}
})
}
}
#[test]
fn click() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(&cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Left),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
onclick: move |_|{
tui_ctx.quit();
},
}
})
}
}
#[test]
fn context_menu() {
dioxus_tui::launch_cfg(app, dioxus_tui::Config::new().with_headless());
fn app(cx: Scope) -> Element {
let render_count = use_state(&cx, || 0);
let tui_ctx: TuiContext = cx.consume_context().unwrap();
let render_count_handle = render_count.clone();
cx.spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
render_count_handle.modify(|x| *x + 1);
});
if *render_count.get() > 2 {
panic!("Event was not received");
}
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Down(MouseButton::Right),
modifiers: KeyModifiers::NONE,
}));
tui_ctx.inject_event(Event::Mouse(MouseEvent {
column: 50,
row: 50,
kind: crossterm::event::MouseEventKind::Up(MouseButton::Right),
modifiers: KeyModifiers::NONE,
}));
cx.render(rsx! {
div {
width: "100%",
height: "100%",
oncontextmenu: move |_|{
tui_ctx.quit();
},
}
})
}
}
+3
View File
@@ -16,6 +16,7 @@ dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
"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" }
+64
View File
@@ -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
+11
View File
@@ -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,
};
}