Merge branch 'upstream' into router-typesafe

This commit is contained in:
Evan Almloff committed 2023-05-23 11:24:31 -05:00
commit b91fb39142
164 files changed
+9007 -681

No files matched your search

+6 -7
View File
@@ -276,13 +276,12 @@ impl Writer<'_> {
let start = location.start();
let line_start = start.line - 1;
let this_line = self.src[line_start];
let beginning = if this_line.len() > start.column {
this_line[..start.column].trim()
} else {
""
};
let beginning = self
.src
.get(line_start)
.filter(|this_line| this_line.len() > start.column)
.map(|this_line| this_line[..start.column].trim())
.unwrap_or_default();
beginning.is_empty()
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-core"
version = "0.3.2"
version = "0.3.3"
authors = ["Jonathan Kelley"]
edition = "2018"
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
+59 -37
View File
@@ -384,51 +384,73 @@ impl VirtualDom {
data,
};
// Loop through each dynamic attribute in this template before moving up to the template's parent.
while let Some(el_ref) = parent_path {
// safety: we maintain references of all vnodes in the element slab
let template = unsafe { el_ref.template.unwrap().as_ref() };
let node_template = template.template.get();
let target_path = el_ref.path;
// If the event bubbles, we traverse through the tree until we find the target element.
if bubbles {
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
while let Some(el_ref) = parent_path {
// safety: we maintain references of all vnodes in the element slab
let template = unsafe { el_ref.template.unwrap().as_ref() };
let node_template = template.template.get();
let target_path = el_ref.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if attr.name.trim_start_matches("on") == name
&& target_path.is_decendant(&this_path)
{
listeners.push(&attr.value);
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
if attr.name.trim_start_matches("on") == name
&& target_path.is_decendant(&this_path)
{
listeners.push(&attr.value);
// Break if the event doesn't bubble anyways
if !bubbles {
break;
// Break if this is the exact target element.
// This means we won't call two listeners with the same name on the same element. This should be
// documented, or be rejected from the rsx! macro outright
if target_path == this_path {
break;
}
}
}
// Break if this is the exact target element.
// This means we won't call two listeners with the same name on the same element. This should be
// documented, or be rejected from the rsx! macro outright
if target_path == this_path {
break;
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
// We check the bubble state between each call to see if the event has been stopped from bubbling
for listener in listeners.drain(..).rev() {
if let AttributeValue::Listener(listener) = listener {
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
if !uievent.propagates.get() {
return;
}
}
}
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
}
} else {
// Otherwise, we just call the listener on the target element
if let Some(el_ref) = parent_path {
// safety: we maintain references of all vnodes in the element slab
let template = unsafe { el_ref.template.unwrap().as_ref() };
let node_template = template.template.get();
let target_path = el_ref.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
let this_path = node_template.attr_paths[idx];
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
// Only call the listener if this is the exact target element.
if attr.name.trim_start_matches("on") == name && target_path == this_path {
if let AttributeValue::Listener(listener) = &attr.value {
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
break;
}
}
}
}
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
// We check the bubble state between each call to see if the event has been stopped from bubbling
for listener in listeners.drain(..).rev() {
if let AttributeValue::Listener(listener) = listener {
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
cb(uievent.clone());
}
if !uievent.propagates.get() {
return;
}
}
}
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
}
}
+18 -3
View File
@@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
dioxus-html = { path = "../html", features = ["serialize", "native-bind"], version = "^0.3.0" }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
@@ -23,12 +23,13 @@ thiserror = "1.0.30"
log = "0.4.14"
wry = { version = "0.27.2" }
futures-channel = "0.3.21"
tokio = { version = "1.16.1", features = [
tokio = { version = "1.27", features = [
"sync",
"rt-multi-thread",
"rt",
"time",
"macros",
"fs",
], optional = true, default-features = false }
webbrowser = "0.8.0"
infer = "0.11.0"
@@ -36,6 +37,7 @@ dunce = "1.0.2"
slab = "0.4"
futures-util = "0.3.25"
rfd = "0.11.3"
[target.'cfg(target_os = "ios")'.dependencies]
objc = "0.2.7"
@@ -55,4 +57,17 @@ hot-reload = ["dioxus-hot-reload"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }
dioxus-hooks = { path = "../hooks" }
# image = "0.24.0" # enable this when generating a new desktop image
dioxus = { path = "../dioxus" }
exitcode = "1.1.2"
scraper = "0.16.0"
# These tests need to be run on the main thread, so they cannot use rust's test harness.
[[test]]
name = "check_events"
path = "headless_tests/events.rs"
harness = false
[[test]]
name = "check_rendering"
path = "headless_tests/rendering.rs"
harness = false
+351
View File
@@ -0,0 +1,351 @@
use dioxus::html::geometry::euclid::Vector3D;
use dioxus::prelude::*;
use dioxus_desktop::DesktopContext;
pub(crate) fn check_app_exits(app: Component) {
use dioxus_desktop::tao::window::WindowBuilder;
use dioxus_desktop::Config;
// This is a deadman's switch to ensure that the app exits
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let should_panic_clone = should_panic.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(100));
if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
std::process::exit(exitcode::SOFTWARE);
}
});
dioxus_desktop::launch_cfg(
app,
Config::new().with_window(WindowBuilder::new().with_visible(false)),
);
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
}
pub fn main() {
check_app_exits(app);
}
fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) {
use_effect(cx, (), |_| {
let desktop_context: DesktopContext = cx.consume_context().unwrap();
async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
desktop_context.eval(&format!(
r#"let element = document.getElementById('{}');
// Dispatch a synthetic event
const event = {};
console.log(element, event);
element.dispatchEvent(event);
"#,
id, value
));
}
});
}
#[allow(deprecated)]
fn app(cx: Scope) -> Element {
let desktop_context: DesktopContext = cx.consume_context().unwrap();
let recieved_events = use_state(cx, || 0);
// button
mock_event(
cx,
"button",
r#"new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
button: 0,
})"#,
);
// mouse_move_div
mock_event(
cx,
"mouse_move_div",
r#"new MouseEvent("mousemove", {
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
})"#,
);
// mouse_click_div
mock_event(
cx,
"mouse_click_div",
r#"new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
button: 2,
})"#,
);
// mouse_dblclick_div
mock_event(
cx,
"mouse_dblclick_div",
r#"new MouseEvent("dblclick", {
view: window,
bubbles: true,
cancelable: true,
buttons: 1|2,
button: 2,
})"#,
);
// mouse_down_div
mock_event(
cx,
"mouse_down_div",
r#"new MouseEvent("mousedown", {
view: window,
bubbles: true,
cancelable: true,
buttons: 2,
button: 2,
})"#,
);
// mouse_up_div
mock_event(
cx,
"mouse_up_div",
r#"new MouseEvent("mouseup", {
view: window,
bubbles: true,
cancelable: true,
buttons: 0,
button: 0,
})"#,
);
// wheel_div
mock_event(
cx,
"wheel_div",
r#"new WheelEvent("wheel", {
view: window,
deltaX: 1.0,
deltaY: 2.0,
deltaZ: 3.0,
deltaMode: 0x00,
bubbles: true,
})"#,
);
// key_down_div
mock_event(
cx,
"key_down_div",
r#"new KeyboardEvent("keydown", {
key: "a",
code: "KeyA",
location: 0,
repeat: true,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
);
// key_up_div
mock_event(
cx,
"key_up_div",
r#"new KeyboardEvent("keyup", {
key: "a",
code: "KeyA",
location: 0,
repeat: false,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
);
// key_press_div
mock_event(
cx,
"key_press_div",
r#"new KeyboardEvent("keypress", {
key: "a",
code: "KeyA",
location: 0,
repeat: false,
keyCode: 65,
charCode: 97,
char: "a",
charCode: 0,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
isComposing: false,
which: 65,
bubbles: true,
})"#,
);
// focus_in_div
mock_event(
cx,
"focus_in_div",
r#"new FocusEvent("focusin", {bubbles: true})"#,
);
// focus_out_div
mock_event(
cx,
"focus_out_div",
r#"new FocusEvent("focusout",{bubbles: true})"#,
);
if **recieved_events == 12 {
println!("all events recieved");
desktop_context.close();
}
cx.render(rsx! {
div {
button {
id: "button",
onclick: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().is_empty());
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
recieved_events.modify(|x| *x + 1)
},
}
div {
id: "mouse_move_div",
onmousemove: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
recieved_events.modify(|x| *x + 1)
},
}
div {
id: "mouse_click_div",
onclick: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
recieved_events.modify(|x| *x + 1)
},
}
div{
id: "mouse_dblclick_div",
ondblclick: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary));
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
recieved_events.modify(|x| *x + 1)
}
}
div{
id: "mouse_down_div",
onmousedown: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary));
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary));
recieved_events.modify(|x| *x + 1)
}
}
div{
id: "mouse_up_div",
onmouseup: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert!(event.data.held_buttons().is_empty());
assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary));
recieved_events.modify(|x| *x + 1)
}
}
div{
id: "wheel_div",
width: "100px",
height: "100px",
background_color: "red",
onwheel: move |event| {
println!("{:?}", event.data);
let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{
panic!("Expected delta to be in pixels")
};
assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0));
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "key_down_div",
onkeydown: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert_eq!(event.data.key().to_string(), "a");
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(event.data.is_auto_repeating());
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "key_up_div",
onkeyup: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert_eq!(event.data.key().to_string(), "a");
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(!event.data.is_auto_repeating());
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "key_press_div",
onkeypress: move |event| {
println!("{:?}", event.data);
assert!(event.data.modifiers().is_empty());
assert_eq!(event.data.key().to_string(), "a");
assert_eq!(event.data.code().to_string(), "KeyA");
assert_eq!(event.data.location, 0);
assert!(!event.data.is_auto_repeating());
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "focus_in_div",
onfocusin: move |event| {
println!("{:?}", event.data);
recieved_events.modify(|x| *x + 1)
}
}
input{
id: "focus_out_div",
onfocusout: move |event| {
println!("{:?}", event.data);
recieved_events.modify(|x| *x + 1)
}
}
}
})
}
@@ -0,0 +1,94 @@
use dioxus::prelude::*;
use dioxus_desktop::DesktopContext;
pub(crate) fn check_app_exits(app: Component) {
use dioxus_desktop::tao::window::WindowBuilder;
use dioxus_desktop::Config;
// This is a deadman's switch to ensure that the app exits
let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let should_panic_clone = should_panic.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(100));
if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) {
std::process::exit(exitcode::SOFTWARE);
}
});
dioxus_desktop::launch_cfg(
app,
Config::new().with_window(WindowBuilder::new().with_visible(false)),
);
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
}
fn main() {
check_app_exits(check_html_renders);
}
fn use_inner_html(cx: &ScopeState, id: &'static str) -> Option<String> {
let value: &UseRef<Option<String>> = use_ref(cx, || None);
use_effect(cx, (), |_| {
to_owned![value];
let desktop_context: DesktopContext = cx.consume_context().unwrap();
async move {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let html = desktop_context
.eval(&format!(
r#"let element = document.getElementById('{}');
return element.innerHTML;"#,
id
))
.await;
if let Ok(serde_json::Value::String(html)) = html {
println!("html: {}", html);
value.set(Some(html));
}
}
});
value.read().clone()
}
const EXPECTED_HTML: &str = r#"<div id="5" style="width: 100px; height: 100px; color: rgb(0, 0, 0);"><input type="checkbox"><h1>text</h1><div><p>hello world</p></div></div>"#;
fn check_html_renders(cx: Scope) -> Element {
let inner_html = use_inner_html(cx, "main_div");
let desktop_context: DesktopContext = cx.consume_context().unwrap();
if let Some(raw_html) = inner_html.as_deref() {
let fragment = scraper::Html::parse_fragment(raw_html);
println!("fragment: {:?}", fragment.html());
let expected = scraper::Html::parse_fragment(EXPECTED_HTML);
println!("fragment: {:?}", expected.html());
if fragment == expected {
println!("html matches");
desktop_context.close();
}
}
let dyn_value = 0;
let dyn_element = rsx! {
div {
dangerous_inner_html: "<p>hello world</p>",
}
};
render! {
div {
id: "main_div",
div {
width: "100px",
height: "100px",
color: "rgb({dyn_value}, {dyn_value}, {dyn_value})",
id: 5,
input {
"type": "checkbox",
},
h1 {
"text"
}
dyn_element
}
}
}
}
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+7 -25
View File
@@ -5,6 +5,7 @@ use std::rc::Weak;
use crate::create_new_window;
use crate::eval::EvalResult;
use crate::events::IpcMessage;
use crate::query::QueryEngine;
use crate::shortcut::IntoKeyCode;
use crate::shortcut::IntoModifersState;
use crate::shortcut::ShortcutId;
@@ -16,7 +17,6 @@ use dioxus_core::ScopeState;
use dioxus_core::VirtualDom;
#[cfg(all(feature = "hot-reload", debug_assertions))]
use dioxus_hot_reload::HotReloadMsg;
use serde_json::Value;
use slab::Slab;
use wry::application::event::Event;
use wry::application::event_loop::EventLoopProxy;
@@ -59,8 +59,8 @@ pub struct DesktopContext {
/// The proxy to the event loop
pub proxy: ProxyType,
/// The receiver for eval results since eval is async
pub(super) eval: tokio::sync::broadcast::Sender<Value>,
/// The receiver for queries about the current window
pub(super) query: QueryEngine,
pub(super) pending_windows: WebviewQueue,
@@ -96,7 +96,7 @@ impl DesktopContext {
webview,
proxy,
event_loop,
eval: tokio::sync::broadcast::channel(8).0,
query: Default::default(),
pending_windows: webviews,
event_handlers,
shortcut_manager,
@@ -210,28 +210,10 @@ impl DesktopContext {
/// Evaluate a javascript expression
pub fn eval(&self, code: &str) -> EvalResult {
// Embed the return of the eval in a function so we can send it back to the main thread
let script = format!(
r#"
window.ipc.postMessage(
JSON.stringify({{
"method":"eval_result",
"params": (
function(){{
{code}
}}
)()
}})
);
"#
);
// the query id lets us keep track of the eval result and send it back to the main thread
let query = self.query.new_query(code, &self.webview);
if let Err(e) = self.webview.evaluate_script(&script) {
// send an error to the eval receiver
log::warn!("Eval script error: {e}");
}
EvalResult::new(self.eval.clone())
EvalResult::new(query)
}
/// Create a wry event handler that listens for wry events.
+123
View File
@@ -0,0 +1,123 @@
use std::rc::Rc;
use dioxus_core::ElementId;
use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
use wry::webview::WebView;
use crate::query::QueryEngine;
/// A mounted element passed to onmounted events
pub struct DesktopElement {
id: ElementId,
webview: Rc<WebView>,
query: QueryEngine,
}
impl DesktopElement {
pub(crate) fn new(id: ElementId, webview: Rc<WebView>, query: QueryEngine) -> Self {
Self { id, webview, query }
}
}
impl RenderedElementBacking for DesktopElement {
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
}
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures_util::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
let fut = self
.query
.new_query::<Option<Rect<f64, f64>>>(&script, &self.webview)
.resolve();
Box::pin(async move {
match fut.await {
Ok(Some(rect)) => Ok(rect),
Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn scroll_to(
&self,
behavior: dioxus_html::ScrollBehavior,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.ScrollTo({}, {});",
self.id.0,
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
);
let fut = self
.query
.new_query::<bool>(&script, &self.webview)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn set_focus(
&self,
focus: bool,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.SetFocus({}, {});",
self.id.0, focus
);
let fut = self
.query
.new_query::<bool>(&script, &self.webview)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
}
#[derive(Debug)]
enum DesktopQueryError {
FailedToQuery,
}
impl std::fmt::Display for DesktopQueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"),
}
}
}
impl std::error::Error for DesktopQueryError {}
+9 -13
View File
@@ -1,36 +1,32 @@
use std::rc::Rc;
use crate::query::Query;
use crate::query::QueryError;
use crate::use_window;
use dioxus_core::ScopeState;
use serde::de::Error;
use std::future::Future;
use std::future::IntoFuture;
use std::pin::Pin;
/// A future that resolves to the result of a JavaScript evaluation.
pub struct EvalResult {
pub(crate) broadcast: tokio::sync::broadcast::Sender<serde_json::Value>,
pub(crate) query: Query<serde_json::Value>,
}
impl EvalResult {
pub(crate) fn new(sender: tokio::sync::broadcast::Sender<serde_json::Value>) -> Self {
Self { broadcast: sender }
pub(crate) fn new(query: Query<serde_json::Value>) -> Self {
Self { query }
}
}
impl IntoFuture for EvalResult {
type Output = Result<serde_json::Value, serde_json::Error>;
type Output = Result<serde_json::Value, QueryError>;
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let mut reciever = self.broadcast.subscribe();
match reciever.recv().await {
Ok(result) => Ok(result),
Err(_) => Err(serde_json::Error::custom("No result returned")),
}
}) as Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
Box::pin(self.query.resolve())
as Pin<Box<dyn Future<Output = Result<serde_json::Value, QueryError>>>>
}
}
+77
View File
@@ -0,0 +1,77 @@
use std::{path::PathBuf, str::FromStr};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub(crate) struct FileDiologRequest {
#[serde(default)]
accept: Option<String>,
multiple: bool,
pub event: String,
pub target: usize,
pub bubbles: bool,
}
pub(crate) fn get_file_event(request: &FileDiologRequest) -> Vec<PathBuf> {
let mut dialog = rfd::FileDialog::new();
let filters: Vec<_> = request
.accept
.as_deref()
.unwrap_or_default()
.split(',')
.filter_map(|s| Filters::from_str(s).ok())
.collect();
let file_extensions: Vec<_> = filters
.iter()
.flat_map(|f| f.as_extensions().into_iter())
.collect();
dialog = dialog.add_filter("name", file_extensions.as_slice());
let files: Vec<_> = if request.multiple {
dialog.pick_files().into_iter().flatten().collect()
} else {
dialog.pick_file().into_iter().collect()
};
files
}
enum Filters {
Extension(String),
Mime(String),
Audio,
Video,
Image,
}
impl Filters {
fn as_extensions(&self) -> Vec<&str> {
match self {
Filters::Extension(extension) => vec![extension.as_str()],
Filters::Mime(_) => vec![],
Filters::Audio => vec!["mp3", "wav", "ogg"],
Filters::Video => vec!["mp4", "webm"],
Filters::Image => vec!["png", "jpg", "jpeg", "gif", "webp"],
}
}
}
impl FromStr for Filters {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(extension) = s.strip_prefix('.') {
Ok(Filters::Extension(extension.to_string()))
} else {
match s {
"audio/*" => Ok(Filters::Audio),
"video/*" => Ok(Filters::Video),
"image/*" => Ok(Filters::Image),
_ => Ok(Filters::Mime(s.to_string())),
}
}
}
}
+80 -20
View File
@@ -5,28 +5,34 @@
mod cfg;
mod desktop_context;
mod element;
mod escape;
mod eval;
mod events;
mod file_upload;
mod protocol;
mod query;
mod shortcut;
mod waker;
mod webview;
use crate::query::QueryResult;
pub use cfg::Config;
pub use desktop_context::{
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
};
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
use dioxus_core::*;
use dioxus_html::HtmlEvent;
use dioxus_html::MountedData;
use dioxus_html::{native_bind::NativeFileEngine, FormData, HtmlEvent};
use element::DesktopElement;
pub use eval::{use_eval, EvalResult};
use futures_util::{pin_mut, FutureExt};
use shortcut::ShortcutRegistry;
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
use std::collections::HashMap;
use std::rc::Rc;
use std::task::Waker;
use std::{collections::HashMap, sync::Arc};
pub use tao::dpi::{LogicalSize, PhysicalSize};
use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget};
pub use tao::window::WindowBuilder;
@@ -220,39 +226,65 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
}
EventData::Ipc(msg) if msg.method() == "user_event" => {
let evt = match serde_json::from_value::<HtmlEvent>(msg.params()) {
let params = msg.params();
let evt = match serde_json::from_value::<HtmlEvent>(params) {
Ok(value) => value,
Err(_) => return,
};
let HtmlEvent {
element,
name,
bubbles,
data,
} = evt;
let view = webviews.get_mut(&event.1).unwrap();
view.dom
.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles);
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = if let dioxus_html::EventData::Mounted = &data {
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query;
let element = DesktopElement::new(element, view.webview.clone(), query);
Rc::new(MountedData::new(element))
} else {
data.into_any()
};
view.dom.handle_event(&name, as_any, element, bubbles);
send_edits(view.dom.render_immediate(), &view.webview);
}
// When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
EventData::Ipc(msg) if msg.method() == "query" => {
let params = msg.params();
if let Ok(result) = serde_json::from_value::<QueryResult>(params) {
let view = webviews.get(&event.1).unwrap();
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query;
query.send(result);
}
}
EventData::Ipc(msg) if msg.method() == "initialize" => {
let view = webviews.get_mut(&event.1).unwrap();
send_edits(view.dom.rebuild(), &view.webview);
}
// When the webview chirps back with the result of the eval, we send it to the active receiver
//
// This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once,
// you might the wrong result. This should be fixed
EventData::Ipc(msg) if msg.method() == "eval_result" => {
webviews[&event.1]
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.eval
.send(msg.params())
.unwrap();
}
EventData::Ipc(msg) if msg.method() == "browser_open" => {
if let Some(temp) = msg.params().as_object() {
if temp.contains_key("href") {
@@ -264,6 +296,34 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
}
}
EventData::Ipc(msg) if msg.method() == "file_diolog" => {
if let Ok(file_diolog) =
serde_json::from_value::<file_upload::FileDiologRequest>(msg.params())
{
let id = ElementId(file_diolog.target);
let event_name = &file_diolog.event;
let event_bubbles = file_diolog.bubbles;
let files = file_upload::get_file_event(&file_diolog);
let data = Rc::new(FormData {
value: Default::default(),
values: Default::default(),
files: Some(Arc::new(NativeFileEngine::new(files))),
});
let view = webviews.get_mut(&event.1).unwrap();
if event_name == "change&input" {
view.dom
.handle_event("input", data.clone(), id, event_bubbles);
view.dom.handle_event("change", data, id, event_bubbles);
} else {
view.dom.handle_event(event_name, data, id, event_bubbles);
}
send_edits(view.dom.render_immediate(), &view.webview);
}
}
_ => {}
},
Event::GlobalShortcutEvent(id) => shortcut_manager.call_handlers(id),
+27 -1
View File
@@ -9,10 +9,36 @@ use wry::{
};
fn module_loader(root_name: &str) -> String {
let js = INTERPRETER_JS.replace(
"/*POST_HANDLE_EDITS*/",
r#"// Prevent file inputs from opening the file dialog on click
let inputs = document.querySelectorAll("input");
for (let input of inputs) {
if (!input.getAttribute("data-dioxus-file-listener")) {
// prevent file inputs from opening the file dialog on click
const type = input.getAttribute("type");
if (type === "file") {
input.setAttribute("data-dioxus-file-listener", true);
input.addEventListener("click", (event) => {
let target = event.target;
let target_id = find_real_id(target);
if (target_id !== null) {
const send = (event_name) => {
const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name });
window.ipc.postMessage(message);
};
send("change&input");
}
event.preventDefault();
});
}
}
}"#,
);
format!(
r#"
<script>
{INTERPRETER_JS}
{js}
let rootname = "{root_name}";
let root = window.document.getElementById(rootname);
+110
View File
@@ -0,0 +1,110 @@
use std::{cell::RefCell, rc::Rc};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use slab::Slab;
use thiserror::Error;
use tokio::sync::broadcast::error::RecvError;
use wry::webview::WebView;
/// Tracks what query ids are currently active
#[derive(Default, Clone)]
struct SharedSlab {
slab: Rc<RefCell<Slab<()>>>,
}
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
#[derive(Clone)]
pub(crate) struct QueryEngine {
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
active_requests: SharedSlab,
}
impl Default for QueryEngine {
fn default() -> Self {
let (sender, _) = tokio::sync::broadcast::channel(8);
Self {
sender: Rc::new(sender),
active_requests: SharedSlab::default(),
}
}
}
impl QueryEngine {
/// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id.
pub fn new_query<V: DeserializeOwned>(&self, script: &str, webview: &WebView) -> Query<V> {
let request_id = self.active_requests.slab.borrow_mut().insert(());
// start the query
// We embed the return of the eval in a function so we can send it back to the main thread
if let Err(err) = webview.evaluate_script(&format!(
r#"window.ipc.postMessage(
JSON.stringify({{
"method":"query",
"params": {{
"id": {request_id},
"data": (function(){{{script}}})()
}}
}})
);"#
)) {
log::warn!("Query error: {err}");
}
Query {
slab: self.active_requests.clone(),
id: request_id,
reciever: self.sender.subscribe(),
phantom: std::marker::PhantomData,
}
}
/// Send a query result
pub fn send(&self, data: QueryResult) {
let _ = self.sender.send(data);
}
}
pub(crate) struct Query<V: DeserializeOwned> {
slab: SharedSlab,
id: usize,
reciever: tokio::sync::broadcast::Receiver<QueryResult>,
phantom: std::marker::PhantomData<V>,
}
impl<V: DeserializeOwned> Query<V> {
/// Resolve the query
pub async fn resolve(mut self) -> Result<V, QueryError> {
let result = loop {
match self.reciever.recv().await {
Ok(result) => {
if result.id == self.id {
break V::deserialize(result.data).map_err(QueryError::DeserializeError);
}
}
Err(err) => {
break Err(QueryError::RecvError(err));
}
}
};
// Remove the query from the slab
self.slab.slab.borrow_mut().remove(self.id);
result
}
}
#[derive(Error, Debug)]
pub enum QueryError {
#[error("Error receiving query result: {0}")]
RecvError(RecvError),
#[error("Error deserializing query result: {0}")]
DeserializeError(serde_json::Error),
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct QueryResult {
id: usize,
data: Value,
}
+11
View File
@@ -57,6 +57,17 @@ pub fn build(
})
.with_web_context(&mut web_context);
#[cfg(windows)]
{
// Windows has a platform specific settings to disable the browser shortcut keys
use wry::webview::WebViewBuilderExtWindows;
webview = webview.with_browser_accelerator_keys(false);
}
// These are commented out because wry is currently broken in wry
// let mut web_context = WebContext::new(cfg.data_dir.clone());
// .with_web_context(&mut web_context);
for (name, handler) in cfg.protocols.drain(..) {
webview = webview.with_custom_protocol(name, handler)
}
+1 -1
View File
@@ -24,7 +24,7 @@ rink = { path = "../rink" }
crossterm = "0.23.0"
tokio = { version = "1.15.0", features = ["full"] }
futures = "0.3.19"
taffy = "0.2.1"
taffy = "0.3.12"
[dev-dependencies]
dioxus = { path = "../dioxus" }
@@ -1,67 +0,0 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_tui::Config;
fn main() {
dioxus_tui::launch_cfg(app, Config::default());
}
#[derive(Props, PartialEq)]
struct QuadrentProps {
color: String,
text: String,
}
fn Quadrant(cx: Scope<QuadrentProps>) -> Element {
cx.render(rsx! {
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "{cx.props.color}",
justify_content: "center",
align_items: "center",
"{cx.props.text}"
}
})
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
width: "100%",
height: "100%",
flex_direction: "column",
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant{
color: "red".to_string(),
text: "[A]".to_string()
},
Quadrant{
color: "black".to_string(),
text: "[B]".to_string()
}
}
div {
width: "100%",
height: "50%",
flex_direction: "row",
Quadrant{
color: "green".to_string(),
text: "[C]".to_string()
},
Quadrant{
color: "blue".to_string(),
text: "[D]".to_string()
}
}
}
})
}
+39 -32
View File
@@ -1,7 +1,31 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_tui::Config;
fn main() {
dioxus_tui::launch(app);
dioxus_tui::launch_cfg(app, Config::default());
}
#[derive(Props, PartialEq)]
struct QuadrentProps {
color: String,
text: String,
}
fn Quadrant(cx: Scope<QuadrentProps>) -> Element {
cx.render(rsx! {
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "{cx.props.color}",
justify_content: "center",
align_items: "center",
"{cx.props.text}"
}
})
}
fn app(cx: Scope) -> Element {
@@ -15,22 +39,13 @@ fn app(cx: Scope) -> Element {
width: "100%",
height: "50%",
flex_direction: "row",
div {
border_width: "1px",
width: "50%",
height: "100%",
background_color: "red",
justify_content: "center",
align_items: "center",
"[A]"
}
div {
width: "50%",
height: "100%",
background_color: "black",
justify_content: "center",
align_items: "center",
"[B]"
Quadrant{
color: "red".to_string(),
text: "[A]".to_string()
},
Quadrant{
color: "black".to_string(),
text: "[B]".to_string()
}
}
@@ -38,21 +53,13 @@ fn app(cx: Scope) -> Element {
width: "100%",
height: "50%",
flex_direction: "row",
div {
width: "50%",
height: "100%",
background_color: "green",
justify_content: "center",
align_items: "center",
"[C]"
}
div {
width: "50%",
height: "100%",
background_color: "blue",
justify_content: "center",
align_items: "center",
"[D]"
Quadrant{
color: "green".to_string(),
text: "[C]".to_string()
},
Quadrant{
color: "blue".to_string(),
text: "[D]".to_string()
}
}
}
+99
View File
@@ -0,0 +1,99 @@
use std::{
any::Any,
fmt::{Display, Formatter},
rc::Rc,
};
use dioxus_core::{ElementId, Mutations, VirtualDom};
use dioxus_html::{
geometry::euclid::{Point2D, Rect, Size2D},
MountedData, MountedError, RenderedElementBacking,
};
use dioxus_native_core::NodeId;
use rink::query::{ElementRef, Query};
pub(crate) fn find_mount_events(mutations: &Mutations) -> Vec<ElementId> {
let mut mount_events = Vec::new();
for mutation in &mutations.edits {
if let dioxus_core::Mutation::NewEventListener {
name: "mounted",
id,
} = mutation
{
mount_events.push(*id);
}
}
mount_events
}
// We need to queue the mounted events to give rink time to rendere and resolve the layout of elements after they are created
pub(crate) fn create_mounted_events(
vdom: &VirtualDom,
events: &mut Vec<(ElementId, &'static str, Rc<dyn Any>, bool)>,
mount_events: impl Iterator<Item = (ElementId, NodeId)>,
) {
let query: Query = vdom
.base_scope()
.consume_context()
.expect("Query should be in context");
for (id, node_id) in mount_events {
let element = TuiElement {
query: query.clone(),
id: node_id,
};
events.push((id, "mounted", Rc::new(MountedData::new(element)), false));
}
}
struct TuiElement {
query: Query,
id: NodeId,
}
impl TuiElement {
pub(crate) fn element(&self) -> ElementRef {
self.query.get(self.id)
}
}
impl RenderedElementBacking for TuiElement {
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let layout = self.element().layout();
Box::pin(async move {
match layout {
Some(layout) => {
let x = layout.location.x as f64;
let y = layout.location.y as f64;
let width = layout.size.width as f64;
let height = layout.size.height as f64;
Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height)))
}
None => Err(MountedError::OperationFailed(Box::new(TuiElementNotFound))),
}
})
}
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
}
}
#[derive(Debug)]
struct TuiElementNotFound;
impl Display for TuiElementNotFound {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "TUI element not found")
}
}
impl std::error::Error for TuiElementNotFound {}
+50 -9
View File
@@ -1,7 +1,9 @@
mod element;
pub mod prelude;
pub mod widgets;
use std::{
any::Any,
ops::Deref,
rc::Rc,
sync::{Arc, RwLock},
@@ -12,6 +14,7 @@ use dioxus_html::EventData;
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
use dioxus_native_core::prelude::*;
use element::{create_mounted_events, find_mount_events};
pub use rink::{query::Query, Config, RenderingMode, Size, TuiContext};
use rink::{render, Driver};
@@ -37,14 +40,32 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
mapping: dioxus_state.clone(),
});
let muts = vdom.rebuild();
let mut rdom = rdom.write().unwrap();
dioxus_state
.write()
.unwrap()
.apply_mutations(&mut rdom, muts);
let mut queued_events = Vec::new();
{
let mut rdom = rdom.write().unwrap();
let mut dioxus_state = dioxus_state.write().unwrap();
// Find any mount events
let mounted = dbg!(find_mount_events(&muts));
dioxus_state.apply_mutations(&mut rdom, muts);
// Send the mount events
create_mounted_events(
&vdom,
&mut queued_events,
mounted
.iter()
.map(|id| (*dbg!(id), dioxus_state.element_to_node_id(*id))),
);
}
DioxusRenderer {
vdom,
dioxus_state,
queued_events,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: {
let (hot_reload_tx, hot_reload_rx) =
@@ -62,6 +83,8 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
struct DioxusRenderer {
vdom: VirtualDom,
dioxus_state: Rc<RwLock<DioxusState>>,
// Events that are queued up to be sent to the vdom next time the vdom is polled
queued_events: Vec<(ElementId, &'static str, Rc<dyn Any>, bool)>,
#[cfg(all(feature = "hot-reload", debug_assertions))]
hot_reload_rx: tokio::sync::mpsc::UnboundedReceiver<dioxus_hot_reload::HotReloadMsg>,
}
@@ -71,10 +94,23 @@ impl Driver for DioxusRenderer {
let muts = self.vdom.render_immediate();
{
let mut rdom = rdom.write().unwrap();
self.dioxus_state
.write()
.unwrap()
.apply_mutations(&mut rdom, muts);
{
// Find any mount events
let mounted = find_mount_events(&muts);
let mut dioxus_state = self.dioxus_state.write().unwrap();
dioxus_state.apply_mutations(&mut rdom, muts);
// Send the mount events
create_mounted_events(
&self.vdom,
&mut self.queued_events,
mounted
.iter()
.map(|id| (*id, dioxus_state.element_to_node_id(*id))),
);
}
}
}
@@ -94,6 +130,11 @@ impl Driver for DioxusRenderer {
}
fn poll_async(&mut self) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + '_>> {
// Add any queued events
for (id, event, value, bubbles) in self.queued_events.drain(..) {
self.vdom.handle_event(event, value, id, bubbles);
}
#[cfg(all(feature = "hot-reload", debug_assertions))]
return Box::pin(async {
let hot_reload_wait = self.hot_reload_rx.recv();
+23
View File
@@ -75,12 +75,35 @@ impl<T: 'static> UseAtomRef<T> {
self.value.borrow_mut()
}
/// Silent write to AtomRef
/// does not update Subscribed scopes
pub fn write_silent(&self) -> RefMut<T> {
self.value.borrow_mut()
}
/// Replace old value with new one
pub fn set(&self, new: T) {
self.root.force_update(self.ptr);
self.root.set(self.ptr, new);
}
/// Do not update provided context on Write ops
/// Example:
/// ```ignore
/// static ATOM_DATA: AtomRef<Collection> = |_| Default::default();
/// fn App(cx: Scope) {
/// use_init_atom_root(cx);
/// let atom_data = use_atom_ref(cx, ATOM_DATA);
/// atom_data.unsubscribe(cx);
/// atom_data.write().update();
/// }
/// ```
pub fn unsubscribe(&self, cx: &ScopeState) {
self.root.unsubscribe(self.ptr, cx.scope_id());
}
/// Force update of subscribed Scopes
pub fn force_update(&self) {
self.root.force_update(self.ptr);
}
}
+1
View File
@@ -0,0 +1 @@
target
+64
View File
@@ -0,0 +1,64 @@
[package]
name = "dioxus-fullstack"
version = "0.1.0"
edition = "2021"
description = "Fullstack Dioxus Utilities"
license = "MIT/Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "ssr", "fullstack"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# server functions
server_fn = { git = "https://github.com/leptos-rs/leptos", rev = "671b1e4a8fff7a2e05bb621ef08e87be2b18ccae", features = ["stable"] }
dioxus_server_macro = { path = "server-macro" }
# warp
warp = { version = "0.3.3", optional = true }
http-body = { version = "0.4.5", optional = true }
# axum
axum = { version = "0.6.1", features = ["ws"], optional = true }
tower-http = { version = "0.4.0", optional = true, features = ["fs"] }
axum-macros = "0.3.7"
# salvo
salvo = { version = "0.37.7", optional = true, features = ["serve-static", "ws"] }
serde = "1.0.159"
# Dioxus + SSR
dioxus-core = { path = "../core", version = "^0.3.0" }
dioxus-ssr = { path = "../ssr", version = "^0.3.0", optional = true }
hyper = { version = "0.14.25", optional = true }
http = { version = "0.2.9", optional = true }
log = "0.4.17"
once_cell = "1.17.1"
thiserror = "1.0.40"
tokio = { version = "1.27.0", features = ["full"], optional = true }
object-pool = "0.5.4"
anymap = "0.12.1"
serde_json = { version = "1.0.95", optional = true }
tokio-stream = { version = "0.1.12", features = ["sync"], optional = true }
futures-util = { version = "0.3.28", optional = true }
postcard = { version = "1.0.4", features = ["use-std"] }
yazi = "0.1.5"
base64 = "0.21.0"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dioxus-hot-reload = { path = "../hot-reload" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] }
[features]
default = ["hot-reload"]
hot-reload = ["serde_json", "tokio-stream", "futures-util"]
warp = ["dep:warp", "http-body", "ssr"]
axum = ["dep:axum", "tower-http", "ssr"]
salvo = ["dep:salvo", "ssr"]
ssr = ["server_fn/ssr", "tokio", "dioxus-ssr", "hyper", "http"]
+108
View File
@@ -0,0 +1,108 @@
# Dioxus Fullstack
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
[![Discord chat][discord-badge]][discord-url]
[crates-badge]: https://img.shields.io/crates/v/dioxus-fullstack.svg
[crates-url]: https://crates.io/crates/dioxus-fullstack
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[API Docs](https://docs.rs/dioxus-fullstack/latest/dioxus_sever) |
[Chat](https://discord.gg/XgGxMSkvUM)
Fullstack utilities for the [`Dioxus`](https://dioxuslabs.com) framework.
# Features
- Intigrations with the [Axum](https::/docs.rs/dioxus-fullstack/latest/dixous_server/axum_adapter/index.html), [Salvo](https::/docs.rs/dioxus-fullstack/latest/dixous_server/salvo_adapter/index.html), and [Warp](https::/docs.rs/dioxus-fullstack/latest/dixous_server/warp_adapter/index.html) server frameworks with utilities for serving and rendering Dioxus applications.
- [Server functions](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/attr.server.html) allow you to call code on the server from the client as if it were a normal function.
- Instant RSX Hot reloading with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload).
- Passing root props from the server to the client.
# Example
Full stack Dioxus in under 50 lines of code
```rust
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
GetMeaning::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
warp::serve(
// Automatically handles server side rendering, hot reloading intigration, and hosting server functions
serve_dioxus_application(
"",
ServeConfigBuilder::new(app, ()),
)
)
.run(([127, 0, 0, 1], 8080))
.await;
});
}
}
fn app(cx: Scope) -> Element {
let meaning = use_state(cx, || None);
cx.render(rsx! {
button {
onclick: move |_| {
to_owned![meaning];
async move {
if let Ok(data) = get_meaning("life the universe and everything".into()).await {
meaning.set(data);
}
}
},
"Run a server function"
}
"Server said: {meaning:?}"
})
}
// This code will only run on the server
#[server(GetMeaning)]
async fn get_meaning(of: String) -> Result<Option<u32>, ServerFnError> {
Ok(of.contains("life").then(|| 42))
}
```
## Getting Started
To get started with full stack Dioxus, check out our [getting started guide](https://dioxuslabs.com/docs/nightly/guide/en/getting_started/ssr.html), or the [full stack examples](https://github.com/DioxusLabs/dioxus/tree/master/packages/fullstack/examples).
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.
@@ -0,0 +1,2 @@
dist
target
@@ -0,0 +1,31 @@
[package]
name = "axum-desktop"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
[dependencies]
dioxus-desktop = { path = "../../../desktop", optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-router = { path = "../../../router" }
dioxus-fullstack = { path = "../../" }
axum = { version = "0.6.12", optional = true }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
[features]
default = []
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
desktop = ["dioxus-desktop"]
[[bin]]
name = "client"
path = "src/client.rs"
required-features = ["desktop"]
[[bin]]
name = "server"
path = "src/server.rs"
required-features = ["ssr"]
@@ -0,0 +1,13 @@
// Run with:
// ```bash
// cargo run --bin client --features="desktop"
// ```
use axum_desktop::*;
use dioxus_fullstack::prelude::server_fn::set_server_url;
fn main() {
// Set the url of the server where server functions are hosted.
set_server_url("http://localhost:8080");
dioxus_desktop::launch(app)
}
@@ -0,0 +1,40 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
pub fn app(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string());
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(data).await.unwrap();
}
}
},
"Run a server function"
}
"Server said: {text}"
})
}
#[server(PostServerData)]
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
println!("Server received: {}", data);
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
@@ -0,0 +1,23 @@
// Run with:
// ```bash
// cargo run --bin server --features="ssr"
// ```
use axum_desktop::*;
use dioxus_fullstack::prelude::*;
#[tokio::main]
async fn main() {
PostServerData::register().unwrap();
GetServerData::register().unwrap();
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
.register_server_fns("")
.into_make_service(),
)
.await
.unwrap();
}
@@ -0,0 +1,2 @@
dist
target
@@ -0,0 +1,21 @@
[package]
name = "axum-hello-world"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { path = "../../../web", features=["hydrate"], optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-fullstack = { path = "../../" }
axum = { version = "0.6.12", optional = true }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
execute = "0.2.12"
[features]
default = ["web"]
ssr = ["axum", "tokio", "dioxus-fullstack/axum"]
web = ["dioxus-web"]
@@ -0,0 +1,101 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
PostServerData::register().unwrap();
GetServerData::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
.serve_dioxus_application(
"",
ServeConfigBuilder::new(app, AppProps { count: 12345 }).build(),
)
.into_make_service(),
)
.await
.unwrap();
});
}
}
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
struct AppProps {
count: i32,
}
fn app(cx: Scope<AppProps>) -> Element {
let mut count = use_state(cx, || cx.props.count);
let text = use_state(cx, || "...".to_string());
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text];
let sc = cx.sc();
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(sc, data).await.unwrap();
}
}
},
"Run a server function! testing1234"
}
"Server said: {text}"
})
}
#[server(PostServerData)]
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
// The server context contains information about the current request and allows you to modify the response.
cx.response_headers_mut()
.insert("Set-Cookie", "foo=bar".parse().unwrap());
println!("Server received: {}", data);
println!("Request parts are {:?}", cx.request_parts());
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
@@ -0,0 +1,2 @@
dist
target
@@ -0,0 +1,24 @@
[package]
name = "axum-router"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { path = "../../../web", features=["hydrate"], optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-router = { path = "../../../router" }
dioxus-fullstack = { path = "../../" }
axum = { version = "0.6.12", optional = true }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
tower-http = { version = "0.4.0", features = ["fs"], optional = true }
http = { version = "0.2.9", optional = true }
execute = "0.2.12"
[features]
default = ["web"]
ssr = ["axum", "tokio", "dioxus-fullstack/axum", "tower-http", "http"]
web = ["dioxus-web", "dioxus-router/web"]
@@ -0,0 +1,153 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use dioxus_router::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
App,
AppProps { route: None },
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
use axum::extract::State;
PostServerData::register().unwrap();
GetServerData::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
axum::Server::bind(&addr)
.serve(
axum::Router::new()
// Serve the dist/assets folder with the javascript and WASM files created by the CLI
.serve_static_assets("./dist")
// Register server functions
.register_server_fns("")
// Connect to the hot reload server
.connect_hot_reload()
// If the path is unknown, render the application
.fallback(
move |uri: http::uri::Uri, State(ssr_state): State<SSRState>| {
let rendered = ssr_state.render(
&ServeConfigBuilder::new(
App,
AppProps {
route: Some(format!("http://{addr}{uri}")),
},
)
.build(),
);
async move { axum::body::Full::from(rendered) }
},
)
.with_state(SSRState::default())
.into_make_service(),
)
.await
.unwrap();
});
}
}
#[derive(Clone, Debug, Props, PartialEq, Serialize, Deserialize)]
struct AppProps {
route: Option<String>,
}
fn App(cx: Scope<AppProps>) -> Element {
cx.render(rsx! {
Router {
initial_url: cx.props.route.clone(),
Route { to: "/blog",
Link {
to: "/",
"Go to counter"
}
table {
tbody {
for _ in 0..100 {
tr {
for _ in 0..100 {
td { "hello world!" }
}
}
}
}
}
},
// Fallback
Route { to: "",
Counter {}
},
}
})
}
fn Counter(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string());
cx.render(rsx! {
Link {
to: "/blog",
"Go to blog"
}
div{
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(data).await.unwrap();
}
}
},
"Run a server function"
}
"Server said: {text}"
}
})
}
#[server(PostServerData)]
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
println!("Server received: {}", data);
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
@@ -0,0 +1,2 @@
dist
target
@@ -0,0 +1,21 @@
[package]
name = "salvo-hello-world"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { path = "../../../web", features=["hydrate"], optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-fullstack = { path = "../../" }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
salvo = { version = "0.37.9", optional = true }
execute = "0.2.12"
[features]
default = ["web"]
ssr = ["salvo", "tokio", "dioxus-fullstack/salvo"]
web = ["dioxus-web"]
@@ -0,0 +1,97 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
use salvo::prelude::*;
PostServerData::register().unwrap();
GetServerData::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let router = Router::new().serve_dioxus_application(
"",
ServeConfigBuilder::new(app, AppProps { count: 12345 }),
);
Server::new(TcpListener::bind("127.0.0.1:8080"))
.serve(router)
.await;
});
}
}
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
struct AppProps {
count: i32,
}
fn app(cx: Scope<AppProps>) -> Element {
let mut count = use_state(cx, || cx.props.count);
let text = use_state(cx, || "...".to_string());
let server_context = cx.sc();
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text, server_context];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(server_context, data).await.unwrap();
}
}
},
"Run a server function"
}
"Server said: {text}"
})
}
#[server(PostServerData)]
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
// The server context contains information about the current request and allows you to modify the response.
cx.response_headers_mut()
.insert("Set-Cookie", "foo=bar".parse().unwrap());
println!("Server received: {}", data);
println!("Request parts are {:?}", cx.request_parts());
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
@@ -0,0 +1,2 @@
dist
target
@@ -0,0 +1,21 @@
[package]
name = "warp-hello-world"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-web = { path = "../../../web", features=["hydrate"], optional = true }
dioxus = { path = "../../../dioxus" }
dioxus-fullstack = { path = "../../" }
tokio = { version = "1.27.0", features = ["full"], optional = true }
serde = "1.0.159"
warp = { version = "0.3.3", optional = true }
execute = "0.2.12"
[features]
default = ["web"]
ssr = ["warp", "tokio", "dioxus-fullstack/warp"]
web = ["dioxus-web"]
@@ -0,0 +1,94 @@
//! Run with:
//!
//! ```sh
//! dioxus build --features web
//! cargo run --features ssr --no-default-features
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use serde::{Deserialize, Serialize};
fn main() {
#[cfg(feature = "web")]
dioxus_web::launch_with_props(
app,
get_root_props_from_document().unwrap_or_default(),
dioxus_web::Config::new().hydrate(true),
);
#[cfg(feature = "ssr")]
{
// Start hot reloading
hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| {
execute::shell("dioxus build --features web")
.spawn()
.unwrap()
.wait()
.unwrap();
execute::shell("cargo run --features ssr --no-default-features")
.spawn()
.unwrap();
true
}));
PostServerData::register().unwrap();
GetServerData::register().unwrap();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async move {
let routes = serve_dioxus_application(
"",
ServeConfigBuilder::new(app, AppProps { count: 12345 }),
);
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
});
}
}
#[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)]
struct AppProps {
count: i32,
}
fn app(cx: Scope<AppProps>) -> Element {
let mut count = use_state(cx, || cx.props.count);
let text = use_state(cx, || "...".to_string());
let server_context = cx.sc();
cx.render(rsx! {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 10, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| {
to_owned![text, server_context];
async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(server_context, data).await.unwrap();
}
}
},
"Run a server function"
}
"Server said: {text}"
})
}
#[server(PostServerData)]
async fn post_server_data(cx: DioxusServerContext, data: String) -> Result<(), ServerFnError> {
// The server context contains information about the current request and allows you to modify the response.
cx.response_headers_mut()
.insert("Set-Cookie", "foo=bar".parse().unwrap());
println!("Server received: {}", data);
println!("Request parts are {:?}", cx.request_parts());
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
@@ -0,0 +1,14 @@
[package]
name = "dioxus_server_macro"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
quote = "1.0.26"
server_fn_macro = { git = "https://github.com/leptos-rs/leptos", rev = "671b1e4a8fff7a2e05bb621ef08e87be2b18ccae", features = ["stable"] }
syn = { version = "1", features = ["full"] }
[lib]
proc-macro = true
@@ -0,0 +1,71 @@
use proc_macro::TokenStream;
use quote::ToTokens;
use server_fn_macro::*;
/// Declares that a function is a [server function](dioxus_fullstack). This means that
/// its body will only run on the server, i.e., when the `ssr` feature is enabled.
///
/// If you call a server function from the client (i.e., when the `csr` or `hydrate` features
/// are enabled), it will instead make a network request to the server.
///
/// You can specify one, two, or three arguments to the server function:
/// 1. **Required**: A type name that will be used to identify and register the server function
/// (e.g., `MyServerFn`).
/// 2. *Optional*: A URL prefix at which the function will be mounted when it’s registered
/// (e.g., `"/api"`). Defaults to `"/"`.
/// 3. *Optional*: either `"Cbor"` (specifying that it should use the binary `cbor` format for
/// serialization), `"Url"` (specifying that it should be use a URL-encoded form-data string).
/// Defaults to `"Url"`. If you want to use this server function
/// using Get instead of Post methods, the encoding must be `"GetCbor"` or `"GetJson"`.
///
/// The server function itself can take any number of arguments, each of which should be serializable
/// and deserializable with `serde`. Optionally, its first argument can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html),
/// which will be injected *on the server side.* This can be used to inject the raw HTTP request or other
/// server-side context into the server function.
///
/// ```ignore
/// # use dioxus_fullstack::prelude::*; use serde::{Serialize, Deserialize};
/// # #[derive(Serialize, Deserialize)]
/// # pub struct Post { }
/// #[server(ReadPosts, "/api")]
/// pub async fn read_posts(how_many: u8, query: String) -> Result<Vec<Post>, ServerFnError> {
/// // do some work on the server to access the database
/// todo!()
/// }
/// ```
///
/// Note the following:
/// - You must **register** the server function by calling `T::register()` somewhere in your main function.
/// - **Server functions must be `async`.** Even if the work being done inside the function body
/// can run synchronously on the server, from the client’s perspective it involves an asynchronous
/// function call.
/// - **Server functions must return `Result<T, ServerFnError>`.** Even if the work being done
/// inside the function body can’t fail, the processes of serialization/deserialization and the
/// network call are fallible.
/// - **Return types must implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html).**
/// This should be fairly obvious: we have to serialize arguments to send them to the server, and we
/// need to deserialize the result to return it to the client.
/// - **Arguments must be implement [`Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html)
/// and [`DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html).**
/// They are serialized as an `application/x-www-form-urlencoded`
/// form data using [`serde_urlencoded`](https://docs.rs/serde_urlencoded/latest/serde_urlencoded/) or as `application/cbor`
/// using [`cbor`](https://docs.rs/cbor/latest/cbor/).
/// - **The [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html) comes from the server.** Optionally, the first argument of a server function
/// can be a [DioxusServerContext](https::/docs.rs/dioxus-fullstack/latest/dixous_server/prelude/struct.DioxusServerContext.html). This scope can be used to inject dependencies like the HTTP request
/// or response or other server-only dependencies, but it does *not* have access to reactive state that exists in the client.
#[proc_macro_attribute]
pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
let context = ServerContext {
ty: syn::parse_quote!(DioxusServerContext),
path: syn::parse_quote!(::dioxus_fullstack::prelude::DioxusServerContext),
};
match server_macro_impl(
args.into(),
s.into(),
Some(context),
Some(syn::parse_quote!(::dioxus_fullstack::prelude::server_fn)),
) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
}
}
@@ -0,0 +1,492 @@
//! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework.
//!
//! # Example
//! ```rust
//! #![allow(non_snake_case)]
//! use dioxus::prelude::*;
//! use dioxus_fullstack::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! // Hydrate the application on the client
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
//! #[cfg(feature = "ssr")]
//! {
//! GetServerData::register().unwrap();
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
//! axum::Server::bind(&addr)
//! .serve(
//! axum::Router::new()
//! // Server side render the application, serve static assets, and register server functions
//! .serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
//! .into_make_service(),
//! )
//! .await
//! .unwrap();
//! });
//! }
//! }
//!
//! fn app(cx: Scope) -> Element {
//! let text = use_state(cx, || "...".to_string());
//!
//! cx.render(rsx! {
//! button {
//! onclick: move |_| {
//! to_owned![text];
//! async move {
//! if let Ok(data) = get_server_data().await {
//! text.set(data);
//! }
//! }
//! },
//! "Run a server function"
//! }
//! "Server said: {text}"
//! })
//! }
//!
//! #[server(GetServerData)]
//! async fn get_server_data() -> Result<String, ServerFnError> {
//! Ok("Hello from the server!".to_string())
//! }
//! ```
use axum::{
body::{self, Body, BoxBody, Full},
extract::{State, WebSocketUpgrade},
handler::Handler,
http::{Request, Response, StatusCode},
response::IntoResponse,
routing::{get, post},
Router,
};
use dioxus_core::VirtualDom;
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_context::DioxusServerContext,
server_fn::DioxusServerFnRegistry,
};
/// A extension trait with utilities for integrating Dioxus with your Axum router.
pub trait DioxusRouterExt<S> {
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
///
/// # Example
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// .register_server_fns_with_handler("", |func| {
/// move |req: Request<Body>| async move {
/// let (parts, body) = req.into_parts();
/// let parts: Arc<RequestParts> = Arc::new(parts.into());
/// let server_context = DioxusServerContext::new(parts.clone());
/// server_fn_handler(server_context, func.clone(), parts, body).await
/// }
/// })
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
handler: impl Fn(ServerFunction) -> H,
) -> Self
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static;
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Register server functions routes with the default handler
/// .register_server_fns("")
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn register_server_fns(self, server_fn_route: &'static str) -> Self;
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// hot_reload_init!();
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Connect to hot reloading in debug mode
/// .connect_hot_reload()
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
/// ```
fn connect_hot_reload(self) -> Self;
/// Serves the static WASM for your Dioxus application (except the generated index.html).
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_static_assets(ServeConfigBuilder::new(app, ()))
/// // Server render the application
/// // ...
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
///
/// fn app(cx: Scope) -> Element {
/// todo!()
/// }
/// ```
fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> Self;
/// Serves the Dioxus application. This will serve a complete server side rendered application.
/// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
/// axum::Server::bind(&addr)
/// .serve(
/// axum::Router::new()
/// // Server side render the application, serve static assets, and register server functions
/// .serve_dioxus_application("", ServeConfigBuilder::new(app, ()))
/// .into_make_service(),
/// )
/// .await
/// .unwrap();
/// }
///
/// fn app(cx: Scope) -> Element {
/// todo!()
/// }
/// ```
fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
self,
server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self;
}
impl<S> DioxusRouterExt<S> for Router<S>
where
S: Send + Sync + Clone + 'static,
{
fn register_server_fns_with_handler<H, T>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(ServerFunction) -> H,
) -> Self
where
H: Handler<T, S, Body>,
T: 'static,
S: Clone + Send + Sync + 'static,
{
let mut router = self;
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}");
match func.encoding {
Encoding::Url | Encoding::Cbor => {
router = router.route(&full_route, post(handler(func)));
}
Encoding::GetJSON | Encoding::GetCBOR => {
router = router.route(&full_route, get(handler(func)));
}
}
}
router
}
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(server_fn_route, |func| {
move |req: Request<Body>| async move {
let (parts, body) = req.into_parts();
let parts: Arc<RequestParts> = Arc::new(parts.into());
let server_context = DioxusServerContext::new(parts.clone());
server_fn_handler(server_context, func.clone(), parts, body).await
}
})
}
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
use tower_http::services::{ServeDir, ServeFile};
let assets_path = assets_path.into();
// Serve all files in dist folder except index.html
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
panic!(
"Couldn't read assets directory at {:?}: {}",
&assets_path, e
)
});
for entry in dir.flatten() {
let path = entry.path();
if path.ends_with("index.html") {
continue;
}
let route = path
.strip_prefix(&assets_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
})
.collect::<Vec<_>>()
.join("/");
let route = format!("/{}", route);
if path.is_dir() {
self = self.nest_service(&route, ServeDir::new(path));
} else {
self = self.nest_service(&route, ServeFile::new(path));
}
}
self
}
fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
self,
server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self {
let cfg = cfg.into();
// Add server functions and render index.html
self.serve_static_assets(&cfg.assets_path)
.route(
"/",
get(render_handler).with_state((cfg, SSRState::default())),
)
.connect_hot_reload()
.register_server_fns(server_fn_route)
}
fn connect_hot_reload(self) -> Self {
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
{
self.nest(
"/_dioxus",
Router::new()
.route(
"/disconnect",
get(|ws: WebSocketUpgrade| async {
ws.on_upgrade(|mut ws| async move {
use axum::extract::ws::Message;
let _ = ws.send(Message::Text("connected".into())).await;
loop {
if ws.recv().await.is_none() {
break;
}
}
})
}),
)
.route("/hot_reload", get(hot_reload_handler)),
)
}
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
{
self
}
}
}
async fn render_handler<P: Clone + serde::Serialize + Send + Sync + 'static>(
State((cfg, ssr_state)): State<(ServeConfig<P>, SSRState)>,
request: Request<Body>,
) -> impl IntoResponse {
let (parts, _) = request.into_parts();
let parts: Arc<RequestParts> = Arc::new(parts.into());
let server_context = DioxusServerContext::new(parts);
let mut vdom =
VirtualDom::new_with_props(cfg.app, cfg.props.clone()).with_root_context(server_context);
let _ = vdom.rebuild();
let rendered = ssr_state.render_vdom(&vdom, &cfg);
Full::from(rendered)
}
/// A default handler for server functions. It will deserialize the request, call the server function, and serialize the response.
pub async fn server_fn_handler(
server_context: DioxusServerContext,
function: ServerFunction,
parts: Arc<RequestParts>,
body: Body,
) -> impl IntoResponse {
let body = hyper::body::to_bytes(body).await;
let Ok(body) = body else {
return report_err(body.err().unwrap());
};
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
let query_string = parts.uri.query().unwrap_or_default().to_string();
spawn_blocking({
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async {
let query = &query_string.into();
let data = match &function.encoding {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => query,
};
let resp = match (function.trait_obj)(server_context.clone(), &data).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
let accept_header = parts
.headers
.get("Accept")
.and_then(|value| value.to_str().ok());
let mut res = Response::builder();
*res.headers_mut().expect("empty response should be valid") =
server_context.take_response_headers();
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
let resp = match serialized {
Payload::Binary(data) => res
.header("Content-Type", "application/cbor")
.body(body::boxed(Full::from(data))),
Payload::Url(data) => res
.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
)
.body(body::boxed(data)),
Payload::Json(data) => res
.header("Content-Type", "application/json")
.body(body::boxed(data)),
};
resp.unwrap()
}
Err(e) => report_err(e),
};
resp_tx.send(resp).unwrap();
})
}
});
resp_rx.await.unwrap()
}
fn report_err<E: Error>(e: E) -> Response<BoxBody> {
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(body::boxed(format!("Error: {}", e)))
.unwrap()
}
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
pub async fn hot_reload_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
use axum::extract::ws::Message;
use futures_util::StreamExt;
let state = crate::hot_reload::spawn_hot_reload().await;
ws.on_upgrade(move |mut socket| async move {
println!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
println!("🔮 Finding updates since last compile...");
let templates_read = state.templates.read().await;
for template in &*templates_read {
if socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
println!("finished");
}
let mut rx =
tokio_stream::wrappers::WatchStream::from_changes(state.message_receiver.clone());
while let Some(change) = rx.next().await {
if let Some(template) = change {
let template = { serde_json::to_string(&template).unwrap() };
if socket.send(Message::Text(template)).await.is_err() {
break;
};
}
}
})
}
+18
View File
@@ -0,0 +1,18 @@
//! # Adapters
//! Adapters for different web frameworks.
//!
//! Each adapter provides a set of utilities that is ergonomic to use with the framework.
//!
//! Each framework has utilies for some or all of the following:
//! - Server functions
//! - A generic way to register server functions
//! - A way to register server functions with a custom handler that allows users to pass in a custom [`crate::server_context::DioxusServerContext`] based on the state of the server framework.
//! - A way to register static WASM files that is accepts [`crate::serve_config::ServeConfig`]
//! - A hot reloading web socket that intigrates with [`dioxus-hot-reload`](https://crates.io/crates/dioxus-hot-reload)
#[cfg(feature = "axum")]
pub mod axum_adapter;
#[cfg(feature = "salvo")]
pub mod salvo_adapter;
#[cfg(feature = "warp")]
pub mod warp_adapter;
@@ -0,0 +1,554 @@
//! Dioxus utilities for the [Salvo](https://salvo.rs) server framework.
//!
//! # Example
//! ```rust
//! #![allow(non_snake_case)]
//! use dioxus::prelude::*;
//! use dioxus_fullstack::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
//! #[cfg(feature = "ssr")]
//! {
//! use salvo::prelude::*;
//! GetServerData::register().unwrap();
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! let router =
//! Router::new().serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
//! Server::new(TcpListener::bind("127.0.0.1:8080"))
//! .serve(router)
//! .await;
//! });
//! }
//! }
//!
//! fn app(cx: Scope) -> Element {
//! let text = use_state(cx, || "...".to_string());
//!
//! cx.render(rsx! {
//! button {
//! onclick: move |_| {
//! to_owned![text];
//! async move {
//! if let Ok(data) = get_server_data().await {
//! text.set(data);
//! }
//! }
//! },
//! "Run a server function"
//! }
//! "Server said: {text}"
//! })
//! }
//!
//! #[server(GetServerData)]
//! async fn get_server_data() -> Result<String, ServerFnError> {
//! Ok("Hello from the server!".to_string())
//! }
//! ```
use dioxus_core::VirtualDom;
use hyper::{http::HeaderValue, StatusCode};
use salvo::{
async_trait, handler,
serve_static::{StaticDir, StaticFile},
Depot, FlowCtrl, Handler, Request, Response, Router,
};
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
};
/// A extension trait with utilities for integrating Dioxus with your Salvo router.
pub trait DioxusRouterExt {
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// struct ServerFunctionHandler {
/// server_fn: ServerFunction,
/// }
///
/// #[handler]
/// impl ServerFunctionHandler {
/// async fn handle(
/// &self,
/// req: &mut Request,
/// depot: &mut Depot,
/// res: &mut Response,
/// flow: &mut FlowCtrl,
/// ) {
/// // Add the headers to server context
/// ServerFnHandler::new((req.headers().clone(),), self.server_fn.clone())
/// .handle(req, depot, res, flow)
/// .await
/// }
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .register_server_fns_with_handler("", |func| {
/// ServerFnHandler::new(DioxusServerContext::default(), func)
/// });
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
/// ```
fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
handler: impl Fn(ServerFunction) -> H,
) -> Self
where
H: Handler + 'static;
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .register_server_fns("");
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
///
/// ```
fn register_server_fns(self, server_fn_route: &'static str) -> Self;
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .connect_hot_reload();
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
fn connect_hot_reload(self) -> Self;
/// Serves the static WASM for your Dioxus application (except the generated index.html).
///
/// # Example
/// ```rust
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new()
/// .server_static_assets("/dist");
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
/// ```
fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> Self;
/// Serves the Dioxus application. This will serve a complete server side rendered application.
/// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
/// use salvo::prelude::*;
/// use std::{net::TcpListener, sync::Arc};
///
/// #[tokio::main]
/// async fn main() {
/// let router = Router::new().serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
/// Server::new(TcpListener::bind("127.0.0.1:8080"))
/// .serve(router)
/// .await;
/// }
///
/// fn app(cx: Scope) -> Element {todo!()}
/// ```
fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
self,
server_fn_path: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self;
}
impl DioxusRouterExt for Router {
fn register_server_fns_with_handler<H>(
self,
server_fn_route: &'static str,
mut handler: impl FnMut(ServerFunction) -> H,
) -> Self
where
H: Handler + 'static,
{
let mut router = self;
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}");
match func.encoding {
Encoding::Url | Encoding::Cbor => {
router = router.push(Router::with_path(&full_route).post(handler(func)));
}
Encoding::GetJSON | Encoding::GetCBOR => {
router = router.push(Router::with_path(&full_route).get(handler(func)));
}
}
}
router
}
fn register_server_fns(self, server_fn_route: &'static str) -> Self {
self.register_server_fns_with_handler(server_fn_route, |func| ServerFnHandler {
server_context: DioxusServerContext::default(),
function: func,
})
}
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
let assets_path = assets_path.into();
// Serve all files in dist folder except index.html
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
panic!(
"Couldn't read assets directory at {:?}: {}",
&assets_path, e
)
});
for entry in dir.flatten() {
let path = entry.path();
if path.ends_with("index.html") {
continue;
}
let route = path
.strip_prefix(&assets_path)
.unwrap()
.iter()
.map(|segment| {
segment.to_str().unwrap_or_else(|| {
panic!("Failed to convert path segment {:?} to string", segment)
})
})
.collect::<Vec<_>>()
.join("/");
if path.is_file() {
let route = format!("/{}", route);
let serve_dir = StaticFile::new(path.clone());
self = self.push(Router::with_path(route).get(serve_dir))
} else {
let route = format!("/{}/<**path>", route);
let serve_dir = StaticDir::new([path.clone()]);
self = self.push(Router::with_path(route).get(serve_dir))
}
}
self
}
fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
self,
server_fn_path: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> Self {
let cfg = cfg.into();
self.serve_static_assets(&cfg.assets_path)
.connect_hot_reload()
.register_server_fns(server_fn_path)
.push(Router::with_path("/").get(SSRHandler { cfg }))
}
fn connect_hot_reload(self) -> Self {
let mut _dioxus_router = Router::with_path("_dioxus");
_dioxus_router = _dioxus_router
.push(Router::with_path("hot_reload").handle(HotReloadHandler::default()));
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
{
_dioxus_router = _dioxus_router.push(Router::with_path("disconnect").handle(ignore_ws));
}
self.push(_dioxus_router)
}
}
/// Extracts the parts of a request that are needed for server functions. This will take parts of the request and replace them with empty values.
pub fn extract_parts(req: &mut Request) -> RequestParts {
RequestParts {
method: std::mem::take(req.method_mut()),
uri: std::mem::take(req.uri_mut()),
version: req.version(),
headers: std::mem::take(req.headers_mut()),
extensions: std::mem::take(req.extensions_mut()),
}
}
struct SSRHandler<P: Clone> {
cfg: ServeConfig<P>,
}
#[async_trait]
impl<P: Clone + serde::Serialize + Send + Sync + 'static> Handler for SSRHandler<P> {
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
_flow: &mut FlowCtrl,
) {
// Get the SSR renderer from the depot or create a new one if it doesn't exist
let renderer_pool = if let Some(renderer) = depot.obtain::<SSRState>() {
renderer.clone()
} else {
let renderer = SSRState::default();
depot.inject(renderer.clone());
renderer
};
let parts: Arc<RequestParts> = Arc::new(extract_parts(req));
let server_context = DioxusServerContext::new(parts);
let mut vdom = VirtualDom::new_with_props(self.cfg.app, self.cfg.props.clone())
.with_root_context(server_context.clone());
let _ = vdom.rebuild();
res.write_body(renderer_pool.render_vdom(&vdom, &self.cfg))
.unwrap();
*res.headers_mut() = server_context.take_response_headers();
}
}
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
pub struct ServerFnHandler {
server_context: DioxusServerContext,
function: ServerFunction,
}
impl ServerFnHandler {
/// Create a new server function handler with the given server context and server function.
pub fn new(server_context: impl Into<DioxusServerContext>, function: ServerFunction) -> Self {
let server_context = server_context.into();
Self {
server_context,
function,
}
}
}
#[handler]
impl ServerFnHandler {
async fn handle(&self, req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let Self {
server_context,
function,
} = self;
let query = req
.uri()
.query()
.unwrap_or_default()
.as_bytes()
.to_vec()
.into();
let body = hyper::body::to_bytes(req.body_mut().unwrap()).await;
let Ok(body)=body else {
handle_error(body.err().unwrap(), res);
return;
};
let headers = req.headers();
let accept_header = headers.get("Accept").cloned();
let parts = Arc::new(extract_parts(req));
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
spawn_blocking({
let function = function.clone();
let mut server_context = server_context.clone();
server_context.parts = parts;
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async move {
let data = match &function.encoding {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
let resp = (function.trait_obj)(server_context, data).await;
resp_tx.send(resp).unwrap();
})
}
});
let result = resp_rx.await.unwrap();
// Set the headers from the server context
*res.headers_mut() = server_context.take_response_headers();
match result {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
let accept_header = accept_header.as_ref().and_then(|value| value.to_str().ok());
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res.set_status_code(StatusCode::OK);
}
match serialized {
Payload::Binary(data) => {
res.headers_mut()
.insert("Content-Type", HeaderValue::from_static("application/cbor"));
res.write_body(data).unwrap();
}
Payload::Url(data) => {
res.headers_mut().insert(
"Content-Type",
HeaderValue::from_static(
"application/\
x-www-form-urlencoded",
),
);
res.write_body(data).unwrap();
}
Payload::Json(data) => {
res.headers_mut()
.insert("Content-Type", HeaderValue::from_static("application/json"));
res.write_body(data).unwrap();
}
}
}
Err(err) => handle_error(err, res),
}
}
}
fn handle_error(error: impl Error + Send + Sync, res: &mut Response) {
let mut resp_err = Response::new();
resp_err.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
resp_err.render(format!("Internal Server Error: {}", error));
*res = resp_err;
}
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
#[derive(Default)]
pub struct HotReloadHandler;
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
#[handler]
impl HotReloadHandler {
async fn handle(
&self,
_req: &mut Request,
_depot: &mut Depot,
_res: &mut Response,
) -> Result<(), salvo::http::StatusError> {
Err(salvo::http::StatusError::not_found())
}
}
/// A handler for Dioxus web hot reload websocket. This will send the updated static parts of the RSX to the client when they change.
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
#[derive(Default)]
pub struct HotReloadHandler;
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
#[handler]
impl HotReloadHandler {
async fn handle(
&self,
req: &mut Request,
_depot: &mut Depot,
res: &mut Response,
) -> Result<(), salvo::http::StatusError> {
use salvo::ws::Message;
use salvo::ws::WebSocketUpgrade;
let state = crate::hot_reload::spawn_hot_reload().await;
WebSocketUpgrade::new()
.upgrade(req, res, move |mut websocket| async move {
use futures_util::StreamExt;
println!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
println!("🔮 Finding updates since last compile...");
let templates_read = state.templates.read().await;
for template in &*templates_read {
if websocket
.send(Message::text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
println!("finished");
}
let mut rx = tokio_stream::wrappers::WatchStream::from_changes(
state.message_receiver.clone(),
);
while let Some(change) = rx.next().await {
if let Some(template) = change {
let template = { serde_json::to_string(&template).unwrap() };
if websocket.send(Message::text(template)).await.is_err() {
break;
};
}
}
})
.await
}
}
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
#[handler]
async fn ignore_ws(req: &mut Request, res: &mut Response) -> Result<(), salvo::http::StatusError> {
use salvo::ws::WebSocketUpgrade;
WebSocketUpgrade::new()
.upgrade(req, res, |mut ws| async move {
let _ = ws.send(salvo::ws::Message::text("connected")).await;
while let Some(msg) = ws.recv().await {
if msg.is_err() {
return;
};
}
})
.await
}
@@ -0,0 +1,444 @@
//! Dioxus utilities for the [Warp](https://docs.rs/warp/latest/warp/index.html) server framework.
//!
//! # Example
//! ```rust
//! #![allow(non_snake_case)]
//! use dioxus::prelude::*;
//! use dioxus_fullstack::prelude::*;
//!
//! fn main() {
//! #[cfg(feature = "web")]
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
//! #[cfg(feature = "ssr")]
//! {
//! GetServerData::register().unwrap();
//! tokio::runtime::Runtime::new()
//! .unwrap()
//! .block_on(async move {
//! let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
//! warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
//! });
//! }
//! }
//!
//! fn app(cx: Scope) -> Element {
//! let text = use_state(cx, || "...".to_string());
//!
//! cx.render(rsx! {
//! button {
//! onclick: move |_| {
//! to_owned![text];
//! async move {
//! if let Ok(data) = get_server_data().await {
//! text.set(data);
//! }
//! }
//! },
//! "Run a server function"
//! }
//! "Server said: {text}"
//! })
//! }
//!
//! #[server(GetServerData)]
//! async fn get_server_data() -> Result<String, ServerFnError> {
//! Ok("Hello from the server!".to_string())
//! }
//!
//! ```
use crate::{
prelude::*, render::SSRState, serve_config::ServeConfig, server_fn::DioxusServerFnRegistry,
};
use dioxus_core::VirtualDom;
use server_fn::{Encoding, Payload, ServerFunctionRegistry};
use std::error::Error;
use std::sync::Arc;
use tokio::task::spawn_blocking;
use warp::path::FullPath;
use warp::{
filters::BoxedFilter,
http::{Response, StatusCode},
hyper::body::Bytes,
path, Filter, Reply,
};
/// Registers server functions with a custom handler function. This allows you to pass custom context to your server functions by generating a [`DioxusServerContext`] from the request.
///
/// # Example
/// ```rust
/// use warp::{body, header, hyper::HeaderMap, path, post, Filter};
///
/// #[tokio::main]
/// async fn main() {
/// let routes = register_server_fns_with_handler("", |full_route, func| {
/// path(full_route)
/// .and(post())
/// .and(header::headers_cloned())
/// .and(body::bytes())
/// .and_then(move |headers: HeaderMap, body| {
/// let func = func.clone();
/// async move {
/// // Add the headers to the server function context
/// server_fn_handler((headers.clone(),), func, headers, body).await
/// }
/// })
/// });
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
pub fn register_server_fns_with_handler<H, F, R>(
server_fn_route: &'static str,
mut handler: H,
) -> BoxedFilter<(R,)>
where
H: FnMut(String, ServerFunction) -> F,
F: Filter<Extract = (R,), Error = warp::Rejection> + Send + Sync + 'static,
F::Extract: Send,
R: Reply + 'static,
{
let mut filter: Option<BoxedFilter<F::Extract>> = None;
for server_fn_path in DioxusServerFnRegistry::paths_registered() {
let func = DioxusServerFnRegistry::get(server_fn_path).unwrap();
let full_route = format!("{server_fn_route}/{server_fn_path}")
.trim_start_matches('/')
.to_string();
let route = handler(full_route, func.clone()).boxed();
if let Some(boxed_filter) = filter.take() {
filter = Some(boxed_filter.or(route).unify().boxed());
} else {
filter = Some(route);
}
}
filter.expect("No server functions found")
}
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
///
/// # Example
/// ```rust
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let routes = register_server_fns("");
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl Reply,)> {
register_server_fns_with_handler(server_fn_route, |full_route, func| {
path(full_route.clone())
.and(warp::post().or(warp::get()).unify())
.and(request_parts())
.and(warp::body::bytes())
.and_then(move |parts, bytes| {
let func = func.clone();
async move {
server_fn_handler(DioxusServerContext::default(), func, parts, bytes).await
}
})
})
}
/// Serves the Dioxus application. This will serve a complete server side rendered application.
/// This will serve static assets, server render the application, register server functions, and intigrate with hot reloading.
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus::prelude::*;
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let routes = serve_dioxus_application("", ServeConfigBuilder::new(app, ()));
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
///
/// fn app(cx: Scope) -> Element {
/// todo!()
/// }
/// ```
pub fn serve_dioxus_application<P: Clone + serde::Serialize + Send + Sync + 'static>(
server_fn_route: &'static str,
cfg: impl Into<ServeConfig<P>>,
) -> BoxedFilter<(impl Reply,)> {
let cfg = cfg.into();
// Serve the dist folder and the index.html file
let serve_dir = warp::fs::dir(cfg.assets_path);
connect_hot_reload()
.or(register_server_fns(server_fn_route))
.or(warp::path::end().and(render_ssr(cfg)))
.or(serve_dir)
.boxed()
}
/// Server render the application.
pub fn render_ssr<P: Clone + serde::Serialize + Send + Sync + 'static>(
cfg: ServeConfig<P>,
) -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone {
warp::get()
.and(request_parts())
.and(with_ssr_state())
.map(move |parts, renderer: SSRState| {
let parts = Arc::new(parts);
let server_context = DioxusServerContext::new(parts);
let mut vdom = VirtualDom::new_with_props(cfg.app, cfg.props.clone())
.with_root_context(server_context.clone());
let _ = vdom.rebuild();
let html = renderer.render_vdom(&vdom, &cfg);
let mut res = Response::builder();
*res.headers_mut().expect("empty request should be valid") =
server_context.take_response_headers();
res.header("Content-Type", "text/html")
.body(Bytes::from(html))
.unwrap()
})
}
/// An extractor for the request parts (used in [DioxusServerContext]). This will extract the method, uri, query, and headers from the request.
pub fn request_parts(
) -> impl Filter<Extract = (RequestParts,), Error = warp::reject::Rejection> + Clone {
warp::method()
.and(warp::filters::path::full())
.and(
warp::filters::query::raw()
.or(warp::any().map(String::new))
.unify(),
)
.and(warp::header::headers_cloned())
.and_then(move |method, path: FullPath, query, headers| async move {
http::uri::Builder::new()
.path_and_query(format!("{}?{}", path.as_str(), query))
.build()
.map_err(|err| {
warp::reject::custom(FailedToReadBody(format!("Failed to build uri: {}", err)))
})
.map(|uri| RequestParts {
method,
uri,
headers,
..Default::default()
})
})
}
fn with_ssr_state() -> impl Filter<Extract = (SSRState,), Error = std::convert::Infallible> + Clone
{
let state = SSRState::default();
warp::any().map(move || state.clone())
}
#[derive(Debug)]
struct FailedToReadBody(String);
impl warp::reject::Reject for FailedToReadBody {}
#[derive(Debug)]
struct RecieveFailed(String);
impl warp::reject::Reject for RecieveFailed {}
/// A default handler for server functions. It will deserialize the request body, call the server function, and serialize the response.
pub async fn server_fn_handler(
server_context: impl Into<DioxusServerContext>,
function: ServerFunction,
parts: RequestParts,
body: Bytes,
) -> Result<Box<dyn warp::Reply>, warp::Rejection> {
let mut server_context = server_context.into();
let parts = Arc::new(parts);
server_context.parts = parts.clone();
// Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
spawn_blocking({
move || {
tokio::runtime::Runtime::new()
.expect("couldn't spawn runtime")
.block_on(async move {
let query = parts
.uri
.query()
.unwrap_or_default()
.as_bytes()
.to_vec()
.into();
let data = match &function.encoding {
Encoding::Url | Encoding::Cbor => &body,
Encoding::GetJSON | Encoding::GetCBOR => &query,
};
let resp = match (function.trait_obj)(server_context.clone(), data).await {
Ok(serialized) => {
// if this is Accept: application/json then send a serialized JSON response
let accept_header = parts
.headers
.get("Accept")
.as_ref()
.and_then(|value| value.to_str().ok());
let mut res = Response::builder();
*res.headers_mut().expect("empty request should be valid") =
server_context.take_response_headers();
if accept_header == Some("application/json")
|| accept_header
== Some(
"application/\
x-www-form-urlencoded",
)
|| accept_header == Some("application/cbor")
{
res = res.status(StatusCode::OK);
}
let resp = match serialized {
Payload::Binary(data) => res
.header("Content-Type", "application/cbor")
.body(Bytes::from(data)),
Payload::Url(data) => res
.header(
"Content-Type",
"application/\
x-www-form-urlencoded",
)
.body(Bytes::from(data)),
Payload::Json(data) => res
.header("Content-Type", "application/json")
.body(Bytes::from(data)),
};
Box::new(resp.unwrap())
}
Err(e) => report_err(e),
};
if resp_tx.send(resp).is_err() {
eprintln!("Error sending response");
}
})
}
});
resp_rx.await.map_err(|err| {
warp::reject::custom(RecieveFailed(format!("Failed to recieve response {err}")))
})
}
fn report_err<E: Error>(e: E) -> Box<dyn warp::Reply> {
Box::new(
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(format!("Error: {}", e))
.unwrap(),
) as Box<dyn warp::Reply>
}
/// Register the web RSX hot reloading endpoint. This will enable hot reloading for your application in debug mode when you call [`dioxus_hot_reload::hot_reload_init`].
///
/// # Example
/// ```rust
/// #![allow(non_snake_case)]
/// use dioxus_fullstack::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let routes = connect_hot_reload();
/// warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
/// }
/// ```
pub fn connect_hot_reload() -> impl Filter<Extract = (impl Reply,), Error = warp::Rejection> + Clone
{
#[cfg(not(all(debug_assertions, feature = "hot-reload", feature = "ssr")))]
{
warp::path!("_dioxus" / "hot_reload")
.and(warp::ws())
.map(warp::reply)
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
}
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
{
use crate::hot_reload::HotReloadState;
use futures_util::sink::SinkExt;
use futures_util::StreamExt;
use warp::ws::Message;
let hot_reload = warp::path!("_dioxus" / "hot_reload")
.and(warp::any().then(|| crate::hot_reload::spawn_hot_reload()))
.and(warp::ws())
.map(move |state: &'static HotReloadState, ws: warp::ws::Ws| {
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
ws.on_upgrade(move |mut websocket| {
async move {
println!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
println!("🔮 Finding updates since last compile...");
let templates_read = state.templates.read().await;
for template in &*templates_read {
if websocket
.send(Message::text(
serde_json::to_string(&template).unwrap(),
))
.await
.is_err()
{
return;
}
}
}
println!("finished");
}
let mut rx = tokio_stream::wrappers::WatchStream::from_changes(
state.message_receiver.clone(),
);
while let Some(change) = rx.next().await {
if let Some(template) = change {
let template = { serde_json::to_string(&template).unwrap() };
if websocket.send(Message::text(template)).await.is_err() {
break;
};
}
}
}
})
});
let disconnect =
warp::path!("_dioxus" / "disconnect")
.and(warp::ws())
.map(move |ws: warp::ws::Ws| {
println!("disconnect");
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
ws.on_upgrade(move |mut websocket| async move {
struct DisconnectOnDrop(Option<warp::ws::WebSocket>);
impl Drop for DisconnectOnDrop {
fn drop(&mut self) {
let _ = self.0.take().unwrap().close();
}
}
let _ = websocket.send(Message::text("connected")).await;
let mut ws = DisconnectOnDrop(Some(websocket));
loop {
if ws.0.as_mut().unwrap().next().await.is_none() {
break;
}
}
})
});
disconnect.or(hot_reload)
}
}
+61
View File
@@ -0,0 +1,61 @@
use std::sync::Arc;
use dioxus_core::Template;
use tokio::sync::{
watch::{channel, Receiver},
RwLock,
};
#[derive(Clone)]
pub struct HotReloadState {
// The cache of all templates that have been modified since the last time we checked
pub(crate) templates: Arc<RwLock<std::collections::HashSet<dioxus_core::Template<'static>>>>,
// The channel to send messages to the hot reload thread
pub(crate) message_receiver: Receiver<Option<Template<'static>>>,
}
impl Default for HotReloadState {
fn default() -> Self {
let templates = Arc::new(RwLock::new(std::collections::HashSet::new()));
let (tx, rx) = channel(None);
dioxus_hot_reload::connect({
let templates = templates.clone();
move |msg| match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
{
let mut templates = templates.blocking_write();
templates.insert(template);
}
if let Err(err) = tx.send(Some(template)) {
log::error!("Failed to send hot reload message: {}", err);
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => {
std::process::exit(0);
}
}
});
Self {
templates,
message_receiver: rx,
}
}
}
// Hot reloading can be expensive to start so we spawn a new thread
static HOT_RELOAD_STATE: tokio::sync::OnceCell<HotReloadState> = tokio::sync::OnceCell::const_new();
pub(crate) async fn spawn_hot_reload() -> &'static HotReloadState {
HOT_RELOAD_STATE
.get_or_init(|| async {
println!("spinning up hot reloading");
let r = tokio::task::spawn_blocking(HotReloadState::default)
.await
.unwrap();
println!("hot reloading ready");
r
})
.await
}
+42
View File
@@ -0,0 +1,42 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![deny(missing_docs)]
pub use adapters::*;
mod props_html;
mod adapters;
#[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))]
mod hot_reload;
#[cfg(feature = "ssr")]
mod render;
#[cfg(feature = "ssr")]
mod serve_config;
mod server_context;
mod server_fn;
/// A prelude of commonly used items in dioxus-fullstack.
pub mod prelude {
#[cfg(feature = "axum")]
pub use crate::adapters::axum_adapter::*;
#[cfg(feature = "salvo")]
pub use crate::adapters::salvo_adapter::*;
#[cfg(feature = "warp")]
pub use crate::adapters::warp_adapter::*;
#[cfg(not(feature = "ssr"))]
pub use crate::props_html::deserialize_props::get_root_props_from_document;
#[cfg(feature = "ssr")]
pub use crate::render::SSRState;
#[cfg(feature = "ssr")]
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
#[cfg(feature = "ssr")]
pub use crate::server_context::RequestParts;
pub use crate::server_context::{DioxusServerContext, HasServerContext};
pub use crate::server_fn::ServerFn;
#[cfg(feature = "ssr")]
pub use crate::server_fn::{ServerFnTraitObj, ServerFunction};
pub use dioxus_server_macro::*;
pub use server_fn::{self, ServerFn as _, ServerFnError};
}
@@ -0,0 +1,32 @@
use serde::de::DeserializeOwned;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
#[allow(unused)]
pub(crate) fn serde_from_string<T: DeserializeOwned>(string: &str) -> Option<T> {
let decompressed = STANDARD.decode(string.as_bytes()).ok()?;
let (decompressed, _) = yazi::decompress(&decompressed, yazi::Format::Zlib).unwrap();
postcard::from_bytes(&decompressed).ok()
}
#[cfg(not(feature = "ssr"))]
/// Get the props from the document. This is only available in the browser.
///
/// When dioxus-fullstack renders the page, it will serialize the root props and put them in the document. This function gets them from the document.
pub fn get_root_props_from_document<T: DeserializeOwned>() -> Option<T> {
#[cfg(not(target_arch = "wasm32"))]
{
None
}
#[cfg(target_arch = "wasm32")]
{
let attribute = web_sys::window()?
.document()?
.get_element_by_id("dioxus-storage")?
.get_attribute("data-serialized")?;
serde_from_string(&attribute)
}
}
+54
View File
@@ -0,0 +1,54 @@
pub(crate) mod deserialize_props;
pub(crate) mod serialize_props;
#[test]
fn serialized_and_deserializes() {
use postcard::to_allocvec;
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
struct Data {
a: u32,
b: String,
bytes: Vec<u8>,
nested: Nested,
}
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
struct Nested {
a: u32,
b: u16,
c: u8,
}
for x in 0..10usize {
for y in 0..10 {
let mut as_string = String::new();
let data = vec![
Data {
a: x as u32,
b: "hello".to_string(),
bytes: vec![0; x],
nested: Nested {
a: 1,
b: x as u16,
c: 3
},
};
y
];
serialize_props::serde_to_writable(&data, &mut as_string).unwrap();
println!("{}", as_string);
println!(
"original size: {}",
std::mem::size_of::<Data>() * data.len()
);
println!("serialized size: {}", to_allocvec(&data).unwrap().len());
println!("compressed size: {}", as_string.len());
let decoded: Vec<Data> = deserialize_props::serde_from_string(&as_string).unwrap();
assert_eq!(data, decoded);
}
}
}
@@ -0,0 +1,31 @@
use serde::Serialize;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
#[allow(unused)]
pub(crate) fn serde_to_writable<T: Serialize>(
value: &T,
mut write_to: impl std::fmt::Write,
) -> std::fmt::Result {
let serialized = postcard::to_allocvec(value).unwrap();
let compressed = yazi::compress(
&serialized,
yazi::Format::Zlib,
yazi::CompressionLevel::BestSize,
)
.unwrap();
write_to.write_str(&STANDARD.encode(compressed));
Ok(())
}
#[cfg(feature = "ssr")]
/// Encode data into a element. This is inteded to be used in the server to send data to the client.
pub(crate) fn encode_in_element<T: Serialize>(
data: T,
mut write_to: impl std::fmt::Write,
) -> std::fmt::Result {
write_to.write_str(r#"<meta hidden="true" id="dioxus-storage" data-serialized=""#)?;
serde_to_writable(&data, &mut write_to)?;
write_to.write_str(r#"" />"#)
}
+100
View File
@@ -0,0 +1,100 @@
//! A shared pool of renderers for efficient server side rendering.
use std::sync::Arc;
use dioxus_core::VirtualDom;
use dioxus_ssr::Renderer;
use crate::prelude::ServeConfig;
/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
#[derive(Clone)]
pub struct SSRState {
// We keep a pool of renderers to avoid re-creating them on every request. They are boxed to make them very cheap to move
renderers: Arc<object_pool::Pool<Renderer>>,
}
impl Default for SSRState {
fn default() -> Self {
Self {
renderers: Arc::new(object_pool::Pool::new(10, pre_renderer)),
}
}
}
impl SSRState {
/// Render the application to HTML.
pub fn render<P: 'static + Clone + serde::Serialize>(&self, cfg: &ServeConfig<P>) -> String {
let ServeConfig { app, props, .. } = cfg;
let mut vdom = VirtualDom::new_with_props(*app, props.clone());
let _ = vdom.rebuild();
self.render_vdom(&vdom, cfg)
}
/// Render a VirtualDom to HTML.
pub fn render_vdom<P: 'static + Clone + serde::Serialize>(
&self,
vdom: &VirtualDom,
cfg: &ServeConfig<P>,
) -> String {
let ServeConfig { index, .. } = cfg;
let mut renderer = self.renderers.pull(pre_renderer);
let mut html = String::new();
html += &index.pre_main;
let _ = renderer.render_to(&mut html, vdom);
// serialize the props
let _ = crate::props_html::serialize_props::encode_in_element(&cfg.props, &mut html);
#[cfg(all(debug_assertions, feature = "hot-reload"))]
{
// In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work
let disconnect_js = r#"(function () {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = protocol + '//' + window.location.host + '/_dioxus/disconnect';
const poll_interval = 1000;
const reload_upon_connect = () => {
console.log('Disconnected from server. Attempting to reconnect...');
window.setTimeout(
() => {
// Try to reconnect to the websocket
const ws = new WebSocket(url);
ws.onopen = () => {
// If we reconnect, reload the page
window.location.reload();
}
// Otherwise, try again in a second
reload_upon_connect();
},
poll_interval);
};
// on initial page load connect to the disconnect ws
const ws = new WebSocket(url);
// if we disconnect, start polling
ws.onclose = reload_upon_connect;
})()"#;
html += r#"<script>"#;
html += disconnect_js;
html += r#"</script>"#;
}
html += &index.post_main;
html
}
}
fn pre_renderer() -> Renderer {
let mut renderer = Renderer::default();
renderer.pre_render = true;
renderer
}
+115
View File
@@ -0,0 +1,115 @@
//! Configeration for how to serve a Dioxus application
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use dioxus_core::Component;
/// A ServeConfig is used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
#[derive(Clone)]
pub struct ServeConfigBuilder<P: Clone> {
pub(crate) app: Component<P>,
pub(crate) props: P,
pub(crate) root_id: Option<&'static str>,
pub(crate) index_path: Option<&'static str>,
pub(crate) assets_path: Option<&'static str>,
}
impl<P: Clone> ServeConfigBuilder<P> {
/// Create a new ServeConfigBuilder with the root component and props to render on the server.
pub fn new(app: Component<P>, props: P) -> Self {
Self {
app,
props,
root_id: None,
index_path: None,
assets_path: None,
}
}
/// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html)
pub fn index_path(mut self, index_path: &'static str) -> Self {
self.index_path = Some(index_path);
self
}
/// Set the id of the root element in the index.html file to place the prerendered content into. (defaults to main)
pub fn root_id(mut self, root_id: &'static str) -> Self {
self.root_id = Some(root_id);
self
}
/// Set the path of the assets folder generated by the Dioxus CLI. (defaults to dist)
pub fn assets_path(mut self, assets_path: &'static str) -> Self {
self.assets_path = Some(assets_path);
self
}
/// Build the ServeConfig
pub fn build(self) -> ServeConfig<P> {
let assets_path = self.assets_path.unwrap_or("dist");
let index_path = self
.index_path
.map(PathBuf::from)
.unwrap_or_else(|| format!("{assets_path}/index.html").into());
let root_id = self.root_id.unwrap_or("main");
let index = load_index_html(index_path, root_id);
ServeConfig {
app: self.app,
props: self.props,
index,
assets_path,
}
}
}
fn load_index_html(path: PathBuf, root_id: &'static str) -> IndexHtml {
let mut file = File::open(path).expect("Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built.");
let mut contents = String::new();
file.read_to_string(&mut contents)
.expect("Failed to read index.html");
let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")).unwrap_or_else(|| panic!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page."));
let post_main = post_main.split_once('>').unwrap_or_else(|| {
panic!("Failed to find closing > after id=\"{root_id}\" in index.html.")
});
let (pre_main, post_main) = (
pre_main.to_string() + &format!("id=\"{root_id}\"") + post_main.0 + ">",
post_main.1.to_string(),
);
IndexHtml {
pre_main,
post_main,
}
}
#[derive(Clone)]
pub(crate) struct IndexHtml {
pub(crate) pre_main: String,
pub(crate) post_main: String,
}
/// Used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
/// See [`ServeConfigBuilder`] to create a ServeConfig
#[derive(Clone)]
pub struct ServeConfig<P: Clone> {
pub(crate) app: Component<P>,
pub(crate) props: P,
pub(crate) index: IndexHtml,
pub(crate) assets_path: &'static str,
}
impl<P: Clone> From<ServeConfigBuilder<P>> for ServeConfig<P> {
fn from(builder: ServeConfigBuilder<P>) -> Self {
builder.build()
}
}
+160
View File
@@ -0,0 +1,160 @@
use dioxus_core::ScopeState;
/// A trait for an object that contains a server context
pub trait HasServerContext {
/// Get the server context from the state
fn server_context(&self) -> DioxusServerContext;
/// A shortcut for `self.server_context()`
fn sc(&self) -> DioxusServerContext {
self.server_context()
}
}
impl HasServerContext for &ScopeState {
fn server_context(&self) -> DioxusServerContext {
#[cfg(feature = "ssr")]
{
self.consume_context().expect("No server context found")
}
#[cfg(not(feature = "ssr"))]
{
DioxusServerContext {}
}
}
}
/// A shared context for server functions that contains infomation about the request and middleware state.
/// This allows you to pass data between your server framework and the server functions. This can be used to pass request information or information about the state of the server. For example, you could pass authentication data though this context to your server functions.
///
/// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope.
#[derive(Clone)]
pub struct DioxusServerContext {
#[cfg(feature = "ssr")]
shared_context: std::sync::Arc<
std::sync::RwLock<anymap::Map<dyn anymap::any::Any + Send + Sync + 'static>>,
>,
#[cfg(feature = "ssr")]
headers: std::sync::Arc<std::sync::RwLock<hyper::header::HeaderMap>>,
#[cfg(feature = "ssr")]
pub(crate) parts: std::sync::Arc<RequestParts>,
}
#[allow(clippy::derivable_impls)]
impl Default for DioxusServerContext {
fn default() -> Self {
Self {
#[cfg(feature = "ssr")]
shared_context: std::sync::Arc::new(std::sync::RwLock::new(anymap::Map::new())),
#[cfg(feature = "ssr")]
headers: Default::default(),
#[cfg(feature = "ssr")]
parts: Default::default(),
}
}
}
#[cfg(feature = "ssr")]
pub use server_fn_impl::*;
#[cfg(feature = "ssr")]
mod server_fn_impl {
use super::*;
use std::sync::LockResult;
use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
use anymap::{any::Any, Map};
type SendSyncAnyMap = Map<dyn Any + Send + Sync + 'static>;
impl DioxusServerContext {
/// Create a new server context from a request
pub fn new(parts: impl Into<Arc<RequestParts>>) -> Self {
Self {
parts: parts.into(),
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
headers: Default::default(),
}
}
/// Clone a value from the shared server context
pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<T> {
self.shared_context.read().ok()?.get::<T>().cloned()
}
/// Insert a value into the shared server context
pub fn insert<T: Any + Send + Sync + 'static>(
&mut self,
value: T,
) -> Result<(), PoisonError<RwLockWriteGuard<'_, SendSyncAnyMap>>> {
self.shared_context
.write()
.map(|mut map| map.insert(value))
.map(|_| ())
}
/// Get the headers from the server context
pub fn response_headers(&self) -> RwLockReadGuard<'_, hyper::header::HeaderMap> {
self.try_response_headers()
.expect("Failed to get headers from server context")
}
/// Try to get the headers from the server context
pub fn try_response_headers(
&self,
) -> LockResult<RwLockReadGuard<'_, hyper::header::HeaderMap>> {
self.headers.read()
}
/// Get the headers mutably from the server context
pub fn response_headers_mut(&self) -> RwLockWriteGuard<'_, hyper::header::HeaderMap> {
self.try_response_headers_mut()
.expect("Failed to get headers mutably from server context")
}
/// Try to get the headers mut from the server context
pub fn try_response_headers_mut(
&self,
) -> LockResult<RwLockWriteGuard<'_, hyper::header::HeaderMap>> {
self.headers.write()
}
pub(crate) fn take_response_headers(&self) -> hyper::header::HeaderMap {
let mut headers = self.headers.write().unwrap();
std::mem::take(&mut *headers)
}
/// Get the request that triggered:
/// - The initial SSR render if called from a ScopeState or ServerFn
/// - The server function to be called if called from a server function after the initial render
pub fn request_parts(&self) -> &RequestParts {
&self.parts
}
}
/// Associated parts of an HTTP Request
#[derive(Debug, Default)]
pub struct RequestParts {
/// The request's method
pub method: http::Method,
/// The request's URI
pub uri: http::Uri,
/// The request's version
pub version: http::Version,
/// The request's headers
pub headers: http::HeaderMap<http::HeaderValue>,
/// The request's extensions
pub extensions: http::Extensions,
}
impl From<http::request::Parts> for RequestParts {
fn from(parts: http::request::Parts) -> Self {
Self {
method: parts.method,
uri: parts.uri,
version: parts.version,
headers: parts.headers,
extensions: parts.extensions,
}
}
}
}
+118
View File
@@ -0,0 +1,118 @@
use crate::server_context::DioxusServerContext;
#[cfg(any(feature = "ssr", doc))]
/// A trait object for a function that be called on serializable arguments and returns a serializable result.
pub type ServerFnTraitObj = server_fn::ServerFnTraitObj<DioxusServerContext>;
#[cfg(any(feature = "ssr", doc))]
/// A server function that can be called on serializable arguments and returns a serializable result.
pub type ServerFunction = server_fn::ServerFunction<DioxusServerContext>;
#[cfg(any(feature = "ssr", doc))]
#[allow(clippy::type_complexity)]
static REGISTERED_SERVER_FUNCTIONS: once_cell::sync::Lazy<
std::sync::Arc<std::sync::RwLock<std::collections::HashMap<&'static str, ServerFunction>>>,
> = once_cell::sync::Lazy::new(Default::default);
#[cfg(any(feature = "ssr", doc))]
/// The registry of all Dioxus server functions.
pub struct DioxusServerFnRegistry;
#[cfg(any(feature = "ssr"))]
impl server_fn::ServerFunctionRegistry<DioxusServerContext> for DioxusServerFnRegistry {
type Error = ServerRegistrationFnError;
fn register(
url: &'static str,
server_function: std::sync::Arc<ServerFnTraitObj>,
encoding: server_fn::Encoding,
) -> Result<(), Self::Error> {
// store it in the hashmap
let mut write = REGISTERED_SERVER_FUNCTIONS
.write()
.map_err(|e| ServerRegistrationFnError::Poisoned(e.to_string()))?;
let prev = write.insert(
url,
ServerFunction {
trait_obj: server_function,
encoding,
},
);
// if there was already a server function with this key,
// return Err
match prev {
Some(_) => Err(ServerRegistrationFnError::AlreadyRegistered(format!(
"There was already a server function registered at {:?}. \
This can happen if you use the same server function name \
in two different modules
on `stable` or in `release` mode.",
url
))),
None => Ok(()),
}
}
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get(url: &str) -> Option<ServerFunction> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).cloned())
}
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get_trait_obj(url: &str) -> Option<std::sync::Arc<ServerFnTraitObj>> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).map(|f| f.trait_obj.clone()))
}
fn get_encoding(url: &str) -> Option<server_fn::Encoding> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).map(|f| f.encoding.clone()))
}
/// Returns a list of all registered server functions.
fn paths_registered() -> Vec<&'static str> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.map(|fns| fns.keys().cloned().collect())
.unwrap_or_default()
}
}
#[cfg(any(feature = "ssr", doc))]
/// Errors that can occur when registering a server function.
#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ServerRegistrationFnError {
/// The server function is already registered.
#[error("The server function {0} is already registered")]
AlreadyRegistered(String),
/// The server function registry is poisoned.
#[error("The server function registry is poisoned: {0}")]
Poisoned(String),
}
/// Defines a "server function." A server function can be called from the server or the client,
/// but the body of its code will only be run on the server, i.e., if a crate feature `ssr` is enabled.
///
/// Server functions are created using the `server` macro.
///
/// The function should be registered by calling `ServerFn::register()`. The set of server functions
/// can be queried on the server for routing purposes by calling [server_fn::ServerFunctionRegistry::get].
///
/// Technically, the trait is implemented on a type that describes the server function's arguments, not the function itself.
pub trait ServerFn: server_fn::ServerFn<DioxusServerContext> {
/// Registers the server function, allowing the client to query it by URL.
#[cfg(any(feature = "ssr", doc))]
fn register() -> Result<(), server_fn::ServerFnError> {
Self::register_in::<DioxusServerFnRegistry>()
}
}
impl<T> ServerFn for T where T: server_fn::ServerFn<DioxusServerContext> {}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-hooks"
version = "0.3.0"
version = "0.3.1"
authors = ["Jonathan Kelley"]
edition = "2018"
description = "Dioxus VirtualDOM renderer for a remote webview instance"
+4
View File
@@ -34,6 +34,10 @@ use std::future::Future;
/// don't care about actions in your app being synchronized, you can use [`use_callback`]
/// hook to spawn multiple tasks and run them concurrently.
///
/// ### Notice
/// In order to use ``rx.next().await``, you will need to extend the ``Stream`` trait (used by ``UnboundedReceiver``)
/// by adding the ``futures-util`` crate as a dependency and adding ``StreamExt`` into scope via ``use futures_util::stream::StreamExt;``
///
/// ## Example
///
/// ```rust, ignore
+2 -3
View File
@@ -194,7 +194,6 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
println!("Connected to hot reloading 🚀");
}
}
std::thread::sleep(std::time::Duration::from_millis(10));
if *aborted.lock().unwrap() {
break;
}
@@ -250,7 +249,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
};
for evt in rx {
if chrono::Local::now().timestamp() > last_update_time {
if chrono::Local::now().timestamp_millis() >= last_update_time {
if let Ok(evt) = evt {
let real_paths = evt
.paths
@@ -322,7 +321,7 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
}
}
}
last_update_time = chrono::Local::now().timestamp();
last_update_time = chrono::Local::now().timestamp_millis();
}
}
});
+8
View File
@@ -21,6 +21,8 @@ enumset = "1.0.11"
keyboard-types = "0.6.2"
async-trait = "0.1.58"
serde-value = "0.7.0"
tokio = { version = "1.27", features = ["fs", "io-util"], optional = true }
rfd = { version = "0.11.3", optional = true }
[dependencies.web-sys]
optional = true
@@ -39,6 +41,11 @@ features = [
"FocusEvent",
"CompositionEvent",
"ClipboardEvent",
"Element",
"DomRect",
"ScrollIntoViewOptions",
"ScrollLogicalPosition",
"ScrollBehavior",
]
[dev-dependencies]
@@ -48,4 +55,5 @@ serde_json = "1"
default = ["serialize"]
serialize = ["serde", "serde_repr", "euclid/serde", "keyboard-types/serde", "dioxus-core/serialize"]
wasm-bind = ["web-sys", "wasm-bindgen"]
native-bind = ["tokio", "rfd"]
hot-reload-context = ["dioxus-rsx"]
+45 -12
View File
@@ -31,16 +31,16 @@ macro_rules! impl_attribute {
(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident (in $ns:ident),
$fil:ident: $vil:ident (in $ns:literal),
) => {
pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), false)
pub const $fil: AttributeDiscription = (stringify!($fil), Some($ns), false)
};
(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident (in $ns:ident : volatile),
$fil:ident: $vil:ident (in $ns:literal : volatile),
) => {
pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), true)
pub const $fil: AttributeDiscription = (stringify!($fil), Some($ns), true)
};
}
@@ -71,10 +71,10 @@ macro_rules! impl_attribute_match {
};
(
$attr:ident $fil:ident: $vil:ident (in $ns:ident),
$attr:ident $fil:ident: $vil:ident (in $ns:literal),
) => {
if $attr == stringify!($fil) {
return Some((stringify!(fil), Some(stringify!(ns))));
return Some((stringify!(fil), Some(ns)));
}
};
}
@@ -110,7 +110,7 @@ macro_rules! impl_element {
(
$(#[$attr:meta])*
$name:ident $namespace:tt {
$name:ident $namespace:literal {
$(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident $extra:tt,
@@ -130,7 +130,35 @@ macro_rules! impl_element {
$(
impl_attribute!(
$(#[$attr_method])*
$fil: $vil in $namespace $extra
$fil: $vil ($extra),
);
)*
}
};
(
$(#[$attr:meta])*
$element:ident [$name:literal, $namespace:tt] {
$(
$(#[$attr_method:meta])*
$fil:ident: $vil:ident $extra:tt,
)*
}
) => {
#[allow(non_camel_case_types)]
$(#[$attr])*
pub struct $element;
impl SvgAttributes for $element {}
impl $element {
pub const TAG_NAME: &'static str = $name;
pub const NAME_SPACE: Option<&'static str> = Some($namespace);
$(
impl_attribute!(
$(#[$attr_method])*
$fil: $vil ($extra),
);
)*
}
@@ -192,7 +220,7 @@ macro_rules! impl_element_match_attributes {
if $el == stringify!($name) {
$(
impl_attribute_match!(
$attr $fil: $vil in $namespace $extra
$attr $fil: $vil ($extra),
);
)*
}
@@ -359,6 +387,11 @@ builder_constructors! {
/// element.
header None {};
/// Build a
/// [`<hgroup>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup)
/// element.
hgroup None {};
/// Build a
/// [`<h1>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1)
/// element.
@@ -1594,7 +1627,7 @@ builder_constructors! {
// /// Build a
// /// [`<use>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use)
// /// element.
// use "http://www.w3.org/2000/svg" {};
r#use ["use", "http://www.w3.org/2000/svg"] {
href: String DEFAULT,
};
}
+3
View File
@@ -33,6 +33,7 @@ mod form;
mod image;
mod keyboard;
mod media;
mod mounted;
mod mouse;
mod pointer;
mod scroll;
@@ -51,6 +52,7 @@ pub use form::*;
pub use image::*;
pub use keyboard::*;
pub use media::*;
pub use mounted::*;
pub use mouse::*;
pub use pointer::*;
pub use scroll::*;
@@ -144,6 +146,7 @@ pub fn event_bubbles(evt: &str) -> bool {
"animationiteration" => true,
"transitionend" => true,
"toggle" => true,
"mounted" => false,
_ => true,
}
}
+51 -2
View File
@@ -10,12 +10,61 @@ pub type FormEvent = Event<FormData>;
pub struct FormData {
pub value: String,
pub values: HashMap<String, String>,
pub values: HashMap<String, Vec<String>>,
#[cfg_attr(feature = "serialize", serde(skip))]
#[cfg_attr(
feature = "serialize",
serde(
default,
skip_serializing,
deserialize_with = "deserialize_file_engine"
)
)]
pub files: Option<std::sync::Arc<dyn FileEngine>>,
}
#[cfg(feature = "serialize")]
#[derive(serde::Serialize, serde::Deserialize)]
struct SerializedFileEngine {
files: HashMap<String, Vec<u8>>,
}
#[cfg(feature = "serialize")]
#[async_trait::async_trait(?Send)]
impl FileEngine for SerializedFileEngine {
fn files(&self) -> Vec<String> {
self.files.keys().cloned().collect()
}
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
self.files.get(file).cloned()
}
async fn read_file_to_string(&self, file: &str) -> Option<String> {
self.read_file(file)
.await
.map(|bytes| String::from_utf8_lossy(&bytes).to_string())
}
}
#[cfg(feature = "serialize")]
fn deserialize_file_engine<'de, D>(
deserializer: D,
) -> Result<Option<std::sync::Arc<dyn FileEngine>>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let Ok(file_engine) =
SerializedFileEngine::deserialize(deserializer) else{
return Ok(None);
};
let file_engine = std::sync::Arc::new(file_engine);
Ok(Some(file_engine))
}
impl PartialEq for FormData {
fn eq(&self, other: &Self) -> bool {
self.value == other.value && self.values == other.values
+131
View File
@@ -0,0 +1,131 @@
//! Handles quering data from the renderer
use euclid::Rect;
use std::{
any::Any,
fmt::{Display, Formatter},
future::Future,
pin::Pin,
rc::Rc,
};
/// An Element that has been rendered and allows reading and modifying information about it.
///
/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries.
// we can not use async_trait here because it does not create a trait that is object safe
pub trait RenderedElementBacking {
/// Get the renderer specific element for the given id
fn get_raw_element(&self) -> MountedResult<&dyn Any> {
Err(MountedError::NotSupported)
}
/// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
#[allow(clippy::type_complexity)]
fn get_client_rect(&self) -> Pin<Box<dyn Future<Output = MountedResult<Rect<f64, f64>>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
/// Scroll to make the element visible
fn scroll_to(
&self,
_behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
/// Set the focus on the element
fn set_focus(&self, _focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
}
impl RenderedElementBacking for () {}
/// The way that scrolling should be performed
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub enum ScrollBehavior {
/// Scroll to the element immediately
#[cfg_attr(feature = "serialize", serde(rename = "instant"))]
Instant,
/// Scroll to the element smoothly
#[cfg_attr(feature = "serialize", serde(rename = "smooth"))]
Smooth,
}
/// An Element that has been rendered and allows reading and modifying information about it.
///
/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries.
pub struct MountedData {
inner: Rc<dyn RenderedElementBacking>,
}
impl MountedData {
/// Create a new MountedData
pub fn new(registry: impl RenderedElementBacking + 'static) -> Self {
Self {
inner: Rc::new(registry),
}
}
/// Get the renderer specific element for the given id
pub fn get_raw_element(&self) -> MountedResult<&dyn Any> {
self.inner.get_raw_element()
}
/// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
pub async fn get_client_rect(&self) -> MountedResult<Rect<f64, f64>> {
self.inner.get_client_rect().await
}
/// Scroll to make the element visible
pub fn scroll_to(
&self,
behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
self.inner.scroll_to(behavior)
}
/// Set the focus on the element
pub fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
self.inner.set_focus(focus)
}
}
use dioxus_core::Event;
pub type MountedEvent = Event<MountedData>;
impl_event! [
MountedData;
/// mounted
onmounted
];
/// The MountedResult type for the MountedData
pub type MountedResult<T> = Result<T, MountedError>;
#[derive(Debug)]
/// The error type for the MountedData
pub enum MountedError {
/// The renderer does not support the requested operation
NotSupported,
/// The element was not found
OperationFailed(Box<dyn std::error::Error>),
}
impl Display for MountedError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
MountedError::NotSupported => {
write!(f, "The renderer does not support the requested operation")
}
MountedError::OperationFailed(e) => {
write!(f, "The operation failed: {}", e)
}
}
}
}
impl std::error::Error for MountedError {}
+2
View File
@@ -20,6 +20,8 @@ pub mod events;
pub mod geometry;
mod global_attributes;
pub mod input_data;
#[cfg(feature = "native-bind")]
pub mod native_bind;
mod render_template;
#[cfg(feature = "wasm-bind")]
mod web_sys_bind;
+3
View File
@@ -0,0 +1,3 @@
mod native_file_engine;
pub use native_file_engine::*;
@@ -0,0 +1,43 @@
use std::path::PathBuf;
use crate::FileEngine;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
pub struct NativeFileEngine {
files: Vec<PathBuf>,
}
impl NativeFileEngine {
pub fn new(files: Vec<PathBuf>) -> Self {
Self { files }
}
}
#[async_trait::async_trait(?Send)]
impl FileEngine for NativeFileEngine {
fn files(&self) -> Vec<String> {
self.files
.iter()
.filter_map(|f| Some(f.to_str()?.to_string()))
.collect()
}
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
let mut file = File::open(file).await.ok()?;
let mut contents = Vec::new();
file.read_to_end(&mut contents).await.ok()?;
Some(contents)
}
async fn read_file_to_string(&self, file: &str) -> Option<String> {
let mut file = File::open(file).await.ok()?;
let mut contents = String::new();
file.read_to_string(&mut contents).await.ok()?;
Some(contents)
}
}
+5
View File
@@ -113,6 +113,9 @@ fn fun_name(
// Toggle
"toggle" => Toggle(de(data)?),
// Mounted
"mounted" => Mounted,
// ImageData => "load" | "error";
// OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
other => {
@@ -151,6 +154,7 @@ pub enum EventData {
Animation(AnimationData),
Transition(TransitionData),
Toggle(ToggleData),
Mounted,
}
impl EventData {
@@ -172,6 +176,7 @@ impl EventData {
EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Mounted => Rc::new(MountedData::new(())) as Rc<dyn Any>,
}
}
}
+69 -4
View File
@@ -4,14 +4,18 @@ use crate::events::{
};
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
use crate::DragData;
use crate::{
DragData, MountedData, MountedError, MountedResult, RenderedElementBacking, ScrollBehavior,
};
use keyboard_types::{Code, Key, Modifiers};
use std::convert::TryInto;
use std::future::Future;
use std::pin::Pin;
use std::str::FromStr;
use wasm_bindgen::JsCast;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent,
TransitionEvent, WheelEvent,
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent,
ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent,
};
macro_rules! uncheck_convert {
@@ -193,3 +197,64 @@ impl From<&TransitionEvent> for TransitionData {
}
}
}
impl From<&web_sys::Element> for MountedData {
fn from(e: &web_sys::Element) -> Self {
MountedData::new(e.clone())
}
}
impl RenderedElementBacking for web_sys::Element {
fn get_client_rect(
&self,
) -> Pin<Box<dyn Future<Output = MountedResult<euclid::Rect<f64, f64>>>>> {
let rect = self.get_bounding_client_rect();
let result = Ok(euclid::Rect::new(
euclid::Point2D::new(rect.left(), rect.top()),
euclid::Size2D::new(rect.width(), rect.height()),
));
Box::pin(async { result })
}
fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> {
Ok(self)
}
fn scroll_to(
&self,
behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
match behavior {
ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options(
ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant),
),
ScrollBehavior::Smooth => self.scroll_into_view_with_scroll_into_view_options(
ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Smooth),
),
}
Box::pin(async { Ok(()) })
}
fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
let result = self
.dyn_ref::<web_sys::HtmlElement>()
.ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into()))))
.and_then(|e| {
(if focus { e.focus() } else { e.blur() })
.map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err))))
});
Box::pin(async { result })
}
}
#[derive(Debug)]
struct FocusError(JsValue);
impl std::fmt::Display for FocusError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "failed to focus element {:?}", self.0)
}
}
impl std::error::Error for FocusError {}
+4 -3
View File
@@ -10,17 +10,18 @@ homepage = "https://dioxuslabs.com"
documentation = "https://docs.rs/dioxus"
keywords = ["dom", "ui", "gui", "react", "wasm"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wasm-bindgen = { version = "0.2.79", optional = true }
js-sys = { version = "0.3.56", optional = true }
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
sledgehammer_bindgen = { version = "0.1.3", optional = true }
sledgehammer_utils = { version = "0.1.0", optional = true }
sledgehammer_bindgen = { version = "0.2.1", optional = true }
sledgehammer_utils = { version = "0.1.1", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
[features]
default = []
serialize = ["serde"]
web = ["wasm-bindgen", "js-sys", "web-sys"]
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]
+168 -103
View File
@@ -17,8 +17,7 @@ class ListenerMap {
} else {
this.global[event_name].active++;
}
}
else {
} else {
const id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
@@ -32,11 +31,13 @@ class ListenerMap {
if (bubbles) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
this.root.removeEventListener(
event_name,
this.global[event_name].callback
);
delete this.global[event_name];
}
}
else {
} else {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (this.local[id].length === 0) {
@@ -143,8 +144,7 @@ class Interpreter {
SetAttribute(id, field, value, ns) {
if (value === null) {
this.RemoveAttribute(id, field, ns);
}
else {
} else {
const node = this.nodes[id];
this.SetAttributeInner(node, field, value, ns);
}
@@ -167,10 +167,10 @@ class Interpreter {
}
break;
case "checked":
node.checked = value === "true";
node.checked = value === "true" || value === true;
break;
case "selected":
node.selected = value === "true";
node.selected = value === "true" || value === true;
break;
case "dangerous_inner_html":
node.innerHTML = value;
@@ -204,6 +204,45 @@ class Interpreter {
node.removeAttribute(name);
}
}
GetClientRect(id) {
const node = this.nodes[id];
if (!node) {
return;
}
const rect = node.getBoundingClientRect();
return {
type: "GetClientRect",
origin: [rect.x, rect.y],
size: [rect.width, rect.height],
};
}
ScrollTo(id, behavior) {
const node = this.nodes[id];
if (!node) {
return false;
}
node.scrollIntoView({
behavior: behavior,
});
return true;
}
/// Set the focus on the element
SetFocus(id, focus) {
const node = this.nodes[id];
if (!node) {
return false;
}
if (focus) {
node.focus();
} else {
node.blur();
}
return true;
}
handleEdits(edits) {
for (let template of edits.templates) {
this.SaveTemplate(template);
@@ -212,6 +251,8 @@ class Interpreter {
for (let edit of edits.edits) {
this.handleEdit(edit);
}
/*POST_HANDLE_EDITS*/
}
SaveTemplate(template) {
@@ -342,107 +383,129 @@ class Interpreter {
this.RemoveEventListener(edit.id, edit.name);
break;
case "NewEventListener":
let bubbles = event_bubbles(edit.name);
// this handler is only provided on desktop implementations since this
// method is not used by the web implementation
let handler = (event) => {
let target = event.target;
if (target != null) {
let realId = target.getAttribute(`data-dioxus-id`);
let shouldPreventDefault = target.getAttribute(
`dioxus-prevent-default`
);
if (event.type === "click") {
// todo call prevent default if it's the right type of event
let a_element = target.closest("a");
if (a_element != null) {
event.preventDefault();
if (shouldPreventDefault !== `onclick` && a_element.getAttribute(`dioxus-prevent-default`) !== `onclick`) {
const href = a_element.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.ipc.postMessage(
serializeIpcMessage("browser_open", { href })
);
}
}
}
// also prevent buttons from submitting
if (target.tagName === "BUTTON" && event.type == "submit") {
event.preventDefault();
}
}
// walk the tree to find the real element
while (realId == null) {
// we've reached the root we don't want to send an event
if (target.parentElement === null) {
return;
}
target = target.parentElement;
realId = target.getAttribute(`data-dioxus-id`);
}
shouldPreventDefault = target.getAttribute(
`dioxus-prevent-default`
);
let contents = serialize_event(event);
if (shouldPreventDefault === `on${event.type}`) {
event.preventDefault();
}
if (event.type === "submit") {
event.preventDefault();
}
if (
target.tagName === "FORM" &&
(event.type === "submit" || event.type === "input")
) {
for (let x = 0; x < target.elements.length; x++) {
let element = target.elements[x];
let name = element.getAttribute("name");
if (name != null) {
if (element.getAttribute("type") === "checkbox") {
// @ts-ignore
contents.values[name] = element.checked ? "true" : "false";
} else if (element.getAttribute("type") === "radio") {
if (element.checked) {
contents.values[name] = element.value;
}
} else {
// @ts-ignore
contents.values[name] =
element.value ?? element.textContent;
}
}
}
}
if (realId === null) {
return;
}
window.ipc.postMessage(
serializeIpcMessage("user_event", {
name: edit.name,
element: parseInt(realId),
data: contents,
bubbles,
})
);
}
};
this.NewEventListener(edit.name, edit.id, bubbles, handler);
// if this is a mounted listener, we send the event immediately
if (edit.name === "mounted") {
window.ipc.postMessage(
serializeIpcMessage("user_event", {
name: edit.name,
element: edit.id,
data: null,
bubbles,
})
);
} else {
this.NewEventListener(edit.name, edit.id, bubbles, (event) => {
handler(event, edit.name, bubbles);
});
}
break;
}
}
}
// this handler is only provided on the desktop and liveview implementations since this
// method is not used by the web implementation
function handler(event, name, bubbles) {
let target = event.target;
if (target != null) {
let preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`);
if (event.type === "click") {
// todo call prevent default if it's the right type of event
let a_element = target.closest("a");
if (a_element != null) {
event.preventDefault();
let elementShouldPreventDefault =
preventDefaultRequests && preventDefaultRequests.includes(`onclick`);
let aElementShouldPreventDefault = a_element.getAttribute(
`dioxus-prevent-default`
);
let linkShouldPreventDefault =
aElementShouldPreventDefault &&
aElementShouldPreventDefault.includes(`onclick`);
if (!elementShouldPreventDefault && !linkShouldPreventDefault) {
const href = a_element.getAttribute("href");
if (href !== "" && href !== null && href !== undefined) {
window.ipc.postMessage(
serializeIpcMessage("browser_open", { href })
);
}
}
}
// also prevent buttons from submitting
if (target.tagName === "BUTTON" && event.type == "submit") {
event.preventDefault();
}
}
const realId = find_real_id(target);
if (
preventDefaultRequests &&
preventDefaultRequests.includes(`on${event.type}`)
) {
event.preventDefault();
}
if (event.type === "submit") {
event.preventDefault();
}
let contents = serialize_event(event);
/*POST_EVENT_SERIALIZATION*/
if (
target.tagName === "FORM" &&
(event.type === "submit" || event.type === "input")
) {
if (
target.tagName === "FORM" &&
(event.type === "submit" || event.type === "input")
) {
const formData = new FormData(target);
for (let name of formData.keys()) {
let value = formData.getAll(name);
contents.values[name] = value;
}
}
}
if (realId === null) {
return;
}
window.ipc.postMessage(
serializeIpcMessage("user_event", {
name: name,
element: parseInt(realId),
data: contents,
bubbles,
})
);
}
}
function find_real_id(target) {
let realId = target.getAttribute(`data-dioxus-id`);
// walk the tree to find the real element
while (realId == null) {
// we've reached the root we don't want to send an event
if (target.parentElement === null) {
return;
}
target = target.parentElement;
realId = target.getAttribute(`data-dioxus-id`);
}
return realId;
}
function get_mouse_data(event) {
const {
altKey,
@@ -921,6 +984,8 @@ function event_bubbles(event) {
return true;
case "toggle":
return true;
case "mounted":
return false;
}
return true;
@@ -108,6 +108,7 @@ mod js {
const listeners = new ListenerMap();
let nodes = [];
let stack = [];
let root;
const templates = {};
let node, els, end, ptr_end, k;
export function save_template(nodes, tmpl_id) {
@@ -116,6 +117,9 @@ mod js {
export function set_node(id, node) {
nodes[id] = node;
}
export function get_node(id) {
return nodes[id];
}
export function initilize(root, handler) {
listeners.handler = handler;
nodes = [root];
@@ -166,6 +170,9 @@ mod js {
#[wasm_bindgen]
pub fn set_node(id: u32, node: Node);
#[wasm_bindgen]
pub fn get_node(id: u32) -> Node;
#[wasm_bindgen]
pub fn initilize(root: Node, handler: &Function);
}
+4 -1
View File
@@ -14,11 +14,13 @@ license = "MIT/Apache-2.0"
[dependencies]
thiserror = "1.0.38"
log = "0.4.14"
slab = "0.4"
futures-util = { version = "0.3.25", default-features = false, features = [
"sink",
] }
futures-channel = { version = "0.3.25", features = ["sink"] }
tokio = { version = "1.22.0", features = ["time"] }
tokio = { version = "1.22.0", features = ["time", "macros"] }
tokio-stream = { version = "0.1.11", features = ["net"] }
tokio-util = { version = "0.7.4", features = ["rt"] }
serde = { version = "1.0.151", features = ["derive"] }
@@ -36,6 +38,7 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] }
# salvo
salvo = { version = "0.37.7", optional = true, features = ["ws"] }
once_cell = "1.17.1"
# actix is ... complicated?
# actix-files = { version = "0.6.2", optional = true }
+125
View File
@@ -0,0 +1,125 @@
use dioxus_core::ElementId;
use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
use tokio::sync::mpsc::UnboundedSender;
use crate::query::QueryEngine;
/// A mounted element passed to onmounted events
pub struct LiveviewElement {
id: ElementId,
query_tx: UnboundedSender<String>,
query: QueryEngine,
}
impl LiveviewElement {
pub(crate) fn new(id: ElementId, tx: UnboundedSender<String>, query: QueryEngine) -> Self {
Self {
id,
query_tx: tx,
query,
}
}
}
impl RenderedElementBacking for LiveviewElement {
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
}
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures_util::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
let fut = self
.query
.new_query::<Option<Rect<f64, f64>>>(&script, &self.query_tx)
.resolve();
Box::pin(async move {
match fut.await {
Ok(Some(rect)) => Ok(rect),
Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn scroll_to(
&self,
behavior: dioxus_html::ScrollBehavior,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.ScrollTo({}, {});",
self.id.0,
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
);
let fut = self
.query
.new_query::<bool>(&script, &self.query_tx)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn set_focus(
&self,
focus: bool,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.SetFocus({}, {});",
self.id.0, focus
);
let fut = self
.query
.new_query::<bool>(&script, &self.query_tx)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
}
#[derive(Debug)]
enum DesktopQueryError {
FailedToQuery,
}
impl std::fmt::Display for DesktopQueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"),
}
}
}
impl std::error::Error for DesktopQueryError {}
+49 -2
View File
@@ -18,7 +18,9 @@ pub mod adapters {
pub use adapters::*;
mod element;
pub mod pool;
mod query;
use futures_util::{SinkExt, StreamExt};
pub use pool::*;
@@ -34,7 +36,51 @@ pub enum LiveViewError {
SendingFailed,
}
use dioxus_interpreter_js::INTERPRETER_JS;
use once_cell::sync::Lazy;
static INTERPRETER_JS: Lazy<String> = Lazy::new(|| {
let interpreter = dioxus_interpreter_js::INTERPRETER_JS;
let serialize_file_uploads = r#"if (
target.tagName === "INPUT" &&
(event.type === "change" || event.type === "input")
) {
const type = target.getAttribute("type");
if (type === "file") {
async function read_files() {
const files = target.files;
const file_contents = {};
for (let i = 0; i < files.length; i++) {
const file = files[i];
file_contents[file.name] = Array.from(
new Uint8Array(await file.arrayBuffer())
);
}
let file_engine = {
files: file_contents,
};
contents.files = file_engine;
if (realId === null) {
return;
}
const message = serializeIpcMessage("user_event", {
name: name,
element: parseInt(realId),
data: contents,
bubbles,
});
window.ipc.postMessage(message);
}
read_files();
return;
}
}"#;
interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads)
});
static MAIN_JS: &str = include_str!("./main.js");
/// This script that gets injected into your app connects this page to the websocket endpoint
@@ -42,11 +88,12 @@ static MAIN_JS: &str = include_str!("./main.js");
/// Once the endpoint is connected, it will send the initial state of the app, and then start
/// processing user events and returning edits to the liveview instance
pub fn interpreter_glue(url: &str) -> String {
let js = &*INTERPRETER_JS;
format!(
r#"
<script>
var WS_ADDR = "{url}";
{INTERPRETER_JS}
{js}
{MAIN_JS}
main();
</script>
+12 -4
View File
@@ -26,11 +26,19 @@ class IPC {
// todo: retry the connection
};
ws.onmessage = (event) => {
ws.onmessage = (message) => {
// Ignore pongs
if (event.data != "__pong__") {
let edits = JSON.parse(event.data);
window.interpreter.handleEdits(edits);
if (message.data != "__pong__") {
const event = JSON.parse(message.data);
switch (event.type) {
case "edits":
let edits = event.data;
window.interpreter.handleEdits(edits);
break;
case "query":
Function("Eval", `"use strict";${event.data};`)();
break;
}
}
};
+64 -12
View File
@@ -1,8 +1,13 @@
use crate::LiveViewError;
use dioxus_core::prelude::*;
use dioxus_html::HtmlEvent;
use crate::{
element::LiveviewElement,
query::{QueryEngine, QueryResult},
LiveViewError,
};
use dioxus_core::{prelude::*, Mutations};
use dioxus_html::{EventData, HtmlEvent, MountedData};
use futures_util::{pin_mut, SinkExt, StreamExt};
use std::time::Duration;
use serde::Serialize;
use std::{rc::Rc, time::Duration};
use tokio_util::task::LocalPoolHandle;
#[derive(Clone)]
@@ -115,7 +120,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
};
// todo: use an efficient binary packed format for this
let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap();
// pin the futures so we can use select!
pin_mut!(ws);
@@ -123,11 +128,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
// send the initial render to the client
ws.send(edits).await?;
// Create the a proxy for query engine
let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
let query_engine = QueryEngine::default();
// desktop uses this wrapper struct thing around the actual event itself
// this is sorta driven by tao/wry
#[derive(serde::Deserialize)]
struct IpcMessage {
params: HtmlEvent,
#[derive(serde::Deserialize, Debug)]
#[serde(tag = "method", content = "params")]
enum IpcMessage {
#[serde(rename = "user_event")]
Event(HtmlEvent),
#[serde(rename = "query")]
Query(QueryResult),
}
loop {
@@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
ws.send("__pong__".to_string()).await?;
}
Some(Ok(evt)) => {
if let Ok(IpcMessage { params }) = serde_json::from_str::<IpcMessage>(evt) {
vdom.handle_event(&params.name, params.data.into_any(), params.element, params.bubbles);
if let Ok(message) = serde_json::from_str::<IpcMessage>(evt) {
match message {
IpcMessage::Event(evt) => {
// Intercept the mounted event and insert a custom element type
if let EventData::Mounted = &evt.data {
let element = LiveviewElement::new(evt.element, query_tx.clone(), query_engine.clone());
vdom.handle_event(
&evt.name,
Rc::new(MountedData::new(element)),
evt.element,
evt.bubbles,
);
}
else{
vdom.handle_event(
&evt.name,
evt.data.into_any(),
evt.element,
evt.bubbles,
);
}
}
IpcMessage::Query(result) => {
query_engine.send(result);
},
}
}
}
// log this I guess? when would we get an error here?
Some(Err(_e)) => {},
Some(Err(_e)) => {}
None => return Ok(()),
}
}
// handle any new queries
Some(query) = query_rx.recv() => {
ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?;
}
Some(msg) = hot_reload_wait => {
#[cfg(all(feature = "hot-reload", debug_assertions))]
match msg{
@@ -176,6 +218,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
.await;
ws.send(serde_json::to_string(&edits).unwrap()).await?;
ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap())
.await?;
}
}
#[derive(Serialize)]
#[serde(tag = "type", content = "data")]
enum ClientUpdate<'a> {
#[serde(rename = "edits")]
Edits(Mutations<'a>),
#[serde(rename = "query")]
Query(String),
}
+113
View File
@@ -0,0 +1,113 @@
use std::{cell::RefCell, rc::Rc};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use slab::Slab;
use thiserror::Error;
use tokio::sync::{broadcast::error::RecvError, mpsc::UnboundedSender};
/// Tracks what query ids are currently active
#[derive(Default, Clone)]
struct SharedSlab {
slab: Rc<RefCell<Slab<()>>>,
}
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
#[derive(Clone)]
pub(crate) struct QueryEngine {
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
active_requests: SharedSlab,
}
impl Default for QueryEngine {
fn default() -> Self {
let (sender, _) = tokio::sync::broadcast::channel(8);
Self {
sender: Rc::new(sender),
active_requests: SharedSlab::default(),
}
}
}
impl QueryEngine {
/// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id.
pub fn new_query<V: DeserializeOwned>(
&self,
script: &str,
tx: &UnboundedSender<String>,
) -> Query<V> {
let request_id = self.active_requests.slab.borrow_mut().insert(());
// start the query
// We embed the return of the eval in a function so we can send it back to the main thread
if let Err(err) = tx.send(format!(
r#"window.ipc.postMessage(
JSON.stringify({{
"method":"query",
"params": {{
"id": {request_id},
"data": (function(){{{script}}})()
}}
}})
);"#
)) {
log::warn!("Query error: {err}");
}
Query {
slab: self.active_requests.clone(),
id: request_id,
reciever: self.sender.subscribe(),
phantom: std::marker::PhantomData,
}
}
/// Send a query result
pub fn send(&self, data: QueryResult) {
let _ = self.sender.send(data);
}
}
pub(crate) struct Query<V: DeserializeOwned> {
slab: SharedSlab,
id: usize,
reciever: tokio::sync::broadcast::Receiver<QueryResult>,
phantom: std::marker::PhantomData<V>,
}
impl<V: DeserializeOwned> Query<V> {
/// Resolve the query
pub async fn resolve(mut self) -> Result<V, QueryError> {
let result = loop {
match self.reciever.recv().await {
Ok(result) => {
if result.id == self.id {
break V::deserialize(result.data).map_err(QueryError::DeserializeError);
}
}
Err(err) => {
break Err(QueryError::RecvError(err));
}
}
};
// Remove the query from the slab
self.slab.slab.borrow_mut().remove(self.id);
result
}
}
#[derive(Error, Debug)]
pub enum QueryError {
#[error("Error receiving query result: {0}")]
RecvError(RecvError),
#[error("Error deserializing query result: {0}")]
DeserializeError(serde_json::Error),
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct QueryResult {
id: usize,
data: Value,
}
+1 -2
View File
@@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react"]
dioxus-core = { path = "../core", version = "^0.3.0", optional = true }
keyboard-types = "0.6.2"
taffy = "0.2.1"
taffy = "0.3.12"
smallvec = "1.6"
rustc-hash = "1.1.0"
anymap = "1.0.0-beta.2"
@@ -29,7 +29,6 @@ lightningcss = "1.0.0-alpha.39"
rayon = "1.6.1"
shipyard = { version = "0.6.2", features = ["proc", "std"], default-features = false }
shipyard_hierarchy = "0.6.0"
[dev-dependencies]
rand = "0.8.5"
+292 -113
View File
@@ -2,7 +2,7 @@
/*
- [ ] pub display: Display, ----> taffy doesnt support all display types
- [x] pub position_type: PositionType, --> taffy doesnt support everything
- [x] pub position: Position, --> taffy doesnt support everything
- [x] pub direction: Direction,
- [x] pub flex_direction: FlexDirection,
@@ -11,6 +11,14 @@
- [x] pub flex_shrink: f32,
- [x] pub flex_basis: Dimension,
- [x]pub grid_auto_flow: GridAutoFlow,
- [x]pub grid_template_rows: GridTrackVec<TrackSizingFunction>,
- [x]pub grid_template_columns: GridTrackVec<TrackSizingFunction>,
- [x]pub grid_auto_rows: GridTrackVec<NonRepeatedTrackSizingFunction>,
- [x]pub grid_auto_columns: GridTrackVec<NonRepeatedTrackSizingFunction>,
- [x]pub grid_row: Line<GridPlacement>,
- [x]pub grid_column: Line<GridPlacement>,
- [x] pub overflow: Overflow, ---> taffy doesnt have support for directional overflow
- [x] pub align_items: AlignItems,
@@ -21,20 +29,22 @@
- [x] pub padding: Rect<Dimension>,
- [x] pub justify_content: JustifyContent,
- [x] pub position: Rect<Dimension>,
- [x] pub inset: Rect<Dimension>,
- [x] pub border: Rect<Dimension>,
- [ ] pub size: Size<Dimension>, ----> seems to only be relevant for input?
- [ ] pub min_size: Size<Dimension>,
- [ ] pub max_size: Size<Dimension>,
- [ ] pub aspect_ratio: Number, ----> parsing is done, but taffy doesnt support it
- [x] pub aspect_ratio: Number,
*/
use lightningcss::properties::border::LineStyle;
use lightningcss::properties::{align, display, flex, position, size};
use lightningcss::properties::grid::{TrackBreadth, TrackSizing};
use lightningcss::properties::{align, border, display, flex, grid, position, size};
use lightningcss::values::percentage::Percentage;
use lightningcss::{
properties::{align::GapValue, border::BorderSideWidth, Property, PropertyId},
properties::{Property, PropertyId},
stylesheet::ParserOptions,
traits::Parse,
values::{
@@ -45,7 +55,7 @@ use lightningcss::{
};
use taffy::{
prelude::*,
style::{FlexDirection, PositionType},
style::{FlexDirection, Position},
};
/// Default values for layout attributes
@@ -95,33 +105,35 @@ pub fn apply_layout_attributes_cfg(
display::Display::Keyword(display::DisplayKeyword::None) => {
style.display = Display::None
}
display::Display::Pair(pair) => {
if let display::DisplayInside::Flex(_) = pair.inside {
style.display = Display::Flex
display::Display::Pair(pair) => match pair.inside {
display::DisplayInside::Flex(_) => {
style.display = Display::Flex;
}
}
_ => (),
display::DisplayInside::Grid => {
style.display = Display::Grid;
}
_ => {}
},
_ => {}
},
Property::Position(position) => {
style.position_type = match position {
position::Position::Relative => PositionType::Relative,
position::Position::Absolute => PositionType::Absolute,
style.position = match position {
position::Position::Relative => Position::Relative,
position::Position::Absolute => Position::Absolute,
_ => return,
}
}
Property::Top(top) => style.position.top = convert_length_percentage_or_auto(top),
Property::Top(top) => style.inset.top = convert_length_percentage_or_auto(top),
Property::Bottom(bottom) => {
style.position.bottom = convert_length_percentage_or_auto(bottom)
}
Property::Left(left) => style.position.left = convert_length_percentage_or_auto(left),
Property::Right(right) => {
style.position.right = convert_length_percentage_or_auto(right)
style.inset.bottom = convert_length_percentage_or_auto(bottom)
}
Property::Left(left) => style.inset.left = convert_length_percentage_or_auto(left),
Property::Right(right) => style.inset.right = convert_length_percentage_or_auto(right),
Property::Inset(inset) => {
style.position.top = convert_length_percentage_or_auto(inset.top);
style.position.bottom = convert_length_percentage_or_auto(inset.bottom);
style.position.left = convert_length_percentage_or_auto(inset.left);
style.position.right = convert_length_percentage_or_auto(inset.right);
style.inset.top = convert_length_percentage_or_auto(inset.top);
style.inset.bottom = convert_length_percentage_or_auto(inset.bottom);
style.inset.left = convert_length_percentage_or_auto(inset.left);
style.inset.right = convert_length_percentage_or_auto(inset.right);
}
Property::BorderTopWidth(width) => {
style.border.top = convert_border_side_width(width, &config.border_widths);
@@ -164,46 +176,64 @@ pub fn apply_layout_attributes_cfg(
}
Property::BorderTopStyle(line_style) => {
if line_style != LineStyle::None {
style.border.top =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.top = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderBottomStyle(line_style) => {
if line_style != LineStyle::None {
style.border.bottom =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.bottom = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderLeftStyle(line_style) => {
if line_style != LineStyle::None {
style.border.left =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.left = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderRightStyle(line_style) => {
if line_style != LineStyle::None {
style.border.right =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.right = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
Property::BorderStyle(styles) => {
if styles.top != LineStyle::None {
style.border.top =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.top = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.bottom != LineStyle::None {
style.border.bottom =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.bottom = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.left != LineStyle::None {
style.border.left =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.left = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
if styles.right != LineStyle::None {
style.border.right =
convert_border_side_width(BorderSideWidth::Medium, &config.border_widths);
style.border.right = convert_border_side_width(
border::BorderSideWidth::Medium,
&config.border_widths,
);
}
}
// Flexbox properties
Property::FlexDirection(flex_direction, _) => {
use FlexDirection::*;
style.flex_direction = match flex_direction {
@@ -228,75 +258,123 @@ pub fn apply_layout_attributes_cfg(
style.flex_shrink = shrink;
}
Property::FlexBasis(basis, _) => {
style.flex_basis = convert_length_percentage_or_auto(basis);
style.flex_basis = convert_length_percentage_or_auto(basis).into();
}
Property::Flex(flex, _) => {
style.flex_grow = flex.grow;
style.flex_shrink = flex.shrink;
style.flex_basis = convert_length_percentage_or_auto(flex.basis);
style.flex_basis = convert_length_percentage_or_auto(flex.basis).into();
}
// Grid properties
Property::GridAutoFlow(grid_auto_flow) => {
let is_row = grid_auto_flow.contains(grid::GridAutoFlow::Row);
let is_dense = grid_auto_flow.contains(grid::GridAutoFlow::Dense);
style.grid_auto_flow = match (is_row, is_dense) {
(true, false) => GridAutoFlow::Row,
(false, false) => GridAutoFlow::Column,
(true, true) => GridAutoFlow::RowDense,
(false, true) => GridAutoFlow::ColumnDense,
};
}
Property::GridTemplateColumns(TrackSizing::TrackList(track_list)) => {
style.grid_template_columns = track_list
.items
.into_iter()
.map(convert_grid_track_item)
.collect();
}
Property::GridTemplateRows(TrackSizing::TrackList(track_list)) => {
style.grid_template_rows = track_list
.items
.into_iter()
.map(convert_grid_track_item)
.collect();
}
Property::GridAutoColumns(grid::TrackSizeList(track_size_list)) => {
style.grid_auto_columns = track_size_list
.into_iter()
.map(convert_grid_track_size)
.collect();
}
Property::GridAutoRows(grid::TrackSizeList(track_size_list)) => {
style.grid_auto_rows = track_size_list
.into_iter()
.map(convert_grid_track_size)
.collect();
}
Property::GridRow(grid_row) => {
style.grid_row = Line {
start: convert_grid_placement(grid_row.start),
end: convert_grid_placement(grid_row.end),
};
}
Property::GridColumn(grid_column) => {
style.grid_column = Line {
start: convert_grid_placement(grid_column.start),
end: convert_grid_placement(grid_column.end),
};
}
// Alignment properties
Property::AlignContent(align, _) => {
use AlignContent::*;
style.align_content = match align {
align::AlignContent::ContentDistribution(distribution) => match distribution {
align::ContentDistribution::SpaceBetween => SpaceBetween,
align::ContentDistribution::SpaceAround => SpaceAround,
align::ContentDistribution::SpaceEvenly => SpaceEvenly,
align::ContentDistribution::Stretch => Stretch,
align::ContentDistribution::SpaceBetween => Some(SpaceBetween),
align::ContentDistribution::SpaceAround => Some(SpaceAround),
align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly),
align::ContentDistribution::Stretch => Some(Stretch),
},
align::AlignContent::ContentPosition {
value: position, ..
} => match position {
align::ContentPosition::Center => Center,
align::ContentPosition::Start | align::ContentPosition::FlexStart => {
FlexStart
}
align::ContentPosition::End | align::ContentPosition::FlexEnd => FlexEnd,
align::ContentPosition::Center => Some(Center),
align::ContentPosition::Start => Some(Start),
align::ContentPosition::FlexStart => Some(FlexStart),
align::ContentPosition::End => Some(End),
align::ContentPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::JustifyContent(justify, _) => {
use JustifyContent::*;
use AlignContent::*;
style.justify_content = match justify {
align::JustifyContent::ContentDistribution(distribution) => {
match distribution {
align::ContentDistribution::SpaceBetween => SpaceBetween,
align::ContentDistribution::SpaceAround => SpaceAround,
align::ContentDistribution::SpaceEvenly => SpaceEvenly,
align::ContentDistribution::SpaceBetween => Some(SpaceBetween),
align::ContentDistribution::SpaceAround => Some(SpaceAround),
align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly),
_ => return,
}
}
align::JustifyContent::ContentPosition {
value: position, ..
} => match position {
align::ContentPosition::Center => Center,
// start ignores -reverse flex-direction but there is no way to specify that in Taffy
align::ContentPosition::Start | align::ContentPosition::FlexStart => {
FlexStart
}
// end ignores -reverse flex-direction but there is no way to specify that in Taffy
align::ContentPosition::End | align::ContentPosition::FlexEnd => FlexEnd,
align::ContentPosition::Center => Some(Center),
align::ContentPosition::Start => Some(Start),
align::ContentPosition::FlexStart => Some(FlexStart),
align::ContentPosition::End => Some(End),
align::ContentPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
}
Property::AlignSelf(align, _) => {
use AlignSelf::*;
use AlignItems::*;
style.align_self = match align {
align::AlignSelf::Auto => Auto,
align::AlignSelf::Stretch => Stretch,
align::AlignSelf::BaselinePosition(_) => Baseline,
align::AlignSelf::Auto => None,
align::AlignSelf::Stretch => Some(Stretch),
align::AlignSelf::BaselinePosition(_) => Some(Baseline),
align::AlignSelf::SelfPosition {
value: position, ..
} => match position {
align::SelfPosition::Center => Center,
align::SelfPosition::Start
| align::SelfPosition::SelfStart
| align::SelfPosition::FlexStart => FlexStart,
align::SelfPosition::End
| align::SelfPosition::SelfEnd
| align::SelfPosition::FlexEnd => FlexEnd,
align::SelfPosition::Center => Some(Center),
align::SelfPosition::Start | align::SelfPosition::SelfStart => Some(Start),
align::SelfPosition::FlexStart => Some(FlexStart),
align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(End),
align::SelfPosition::FlexEnd => Some(FlexEnd),
},
_ => return,
};
@@ -304,15 +382,18 @@ pub fn apply_layout_attributes_cfg(
Property::AlignItems(align, _) => {
use AlignItems::*;
style.align_items = match align {
align::AlignItems::BaselinePosition(_) => Baseline,
align::AlignItems::Stretch => Stretch,
align::AlignItems::BaselinePosition(_) => Some(Baseline),
align::AlignItems::Stretch => Some(Stretch),
align::AlignItems::SelfPosition {
value: position, ..
} => match position {
align::SelfPosition::Center => Center,
align::SelfPosition::FlexStart => FlexStart,
align::SelfPosition::FlexEnd => FlexEnd,
_ => return,
align::SelfPosition::Center => Some(Center),
align::SelfPosition::FlexStart => Some(FlexStart),
align::SelfPosition::FlexEnd => Some(FlexEnd),
align::SelfPosition::Start | align::SelfPosition::SelfStart => {
Some(FlexEnd)
}
align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(FlexEnd),
},
_ => return,
};
@@ -350,23 +431,23 @@ pub fn apply_layout_attributes_cfg(
};
}
Property::PaddingTop(padding) => {
style.padding.top = convert_length_percentage_or_auto(padding);
style.padding.top = convert_padding(padding);
}
Property::PaddingBottom(padding) => {
style.padding.bottom = convert_length_percentage_or_auto(padding);
style.padding.bottom = convert_padding(padding);
}
Property::PaddingLeft(padding) => {
style.padding.left = convert_length_percentage_or_auto(padding);
style.padding.left = convert_padding(padding);
}
Property::PaddingRight(padding) => {
style.padding.right = convert_length_percentage_or_auto(padding);
style.padding.right = convert_padding(padding);
}
Property::Padding(padding) => {
style.padding = Rect {
top: convert_length_percentage_or_auto(padding.top),
bottom: convert_length_percentage_or_auto(padding.bottom),
left: convert_length_percentage_or_auto(padding.left),
right: convert_length_percentage_or_auto(padding.right),
top: convert_padding(padding.top),
bottom: convert_padding(padding.bottom),
left: convert_padding(padding.left),
right: convert_padding(padding.right),
};
}
Property::Width(width) => {
@@ -386,59 +467,157 @@ pub fn apply_layout_attributes_cfg(
}
}
fn convert_length_value(length_value: LengthValue) -> Dimension {
fn extract_px_value(length_value: LengthValue) -> f32 {
match length_value {
LengthValue::Px(value) => Dimension::Points(value),
LengthValue::Px(value) => value,
_ => todo!(),
}
}
fn convert_dimension_percentage(
fn convert_length_percentage(
dimension_percentage: DimensionPercentage<LengthValue>,
) -> Dimension {
) -> LengthPercentage {
match dimension_percentage {
DimensionPercentage::Dimension(value) => convert_length_value(value),
DimensionPercentage::Percentage(percentage) => Dimension::Percent(percentage.0),
_ => todo!(),
DimensionPercentage::Dimension(value) => LengthPercentage::Points(extract_px_value(value)),
DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0),
DimensionPercentage::Calc(_) => todo!(),
}
}
fn convert_padding(dimension_percentage: LengthPercentageOrAuto) -> LengthPercentage {
match dimension_percentage {
LengthPercentageOrAuto::Auto => unimplemented!(),
LengthPercentageOrAuto::LengthPercentage(lp) => match lp {
DimensionPercentage::Dimension(value) => {
LengthPercentage::Points(extract_px_value(value))
}
DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0),
DimensionPercentage::Calc(_) => unimplemented!(),
},
}
}
fn convert_length_percentage_or_auto(
length_percentage_or_auto: LengthPercentageOrAuto,
) -> Dimension {
match length_percentage_or_auto {
LengthPercentageOrAuto::Auto => Dimension::Auto,
LengthPercentageOrAuto::LengthPercentage(percentage) => {
convert_dimension_percentage(percentage)
}
dimension_percentage: LengthPercentageOrAuto,
) -> LengthPercentageAuto {
match dimension_percentage {
LengthPercentageOrAuto::Auto => LengthPercentageAuto::Auto,
LengthPercentageOrAuto::LengthPercentage(lp) => match lp {
DimensionPercentage::Dimension(value) => {
LengthPercentageAuto::Points(extract_px_value(value))
}
DimensionPercentage::Percentage(percentage) => {
LengthPercentageAuto::Percent(percentage.0)
}
DimensionPercentage::Calc(_) => todo!(),
},
}
}
fn convert_dimension(dimension_percentage: DimensionPercentage<LengthValue>) -> Dimension {
match dimension_percentage {
DimensionPercentage::Dimension(value) => Dimension::Points(extract_px_value(value)),
DimensionPercentage::Percentage(percentage) => Dimension::Percent(percentage.0),
DimensionPercentage::Calc(_) => todo!(),
}
}
fn convert_border_side_width(
border_side_width: BorderSideWidth,
border_side_width: border::BorderSideWidth,
border_width_config: &BorderWidths,
) -> Dimension {
) -> LengthPercentage {
match border_side_width {
BorderSideWidth::Length(Length::Value(value)) => convert_length_value(value),
BorderSideWidth::Thick => Dimension::Points(border_width_config.thick),
BorderSideWidth::Medium => Dimension::Points(border_width_config.medium),
BorderSideWidth::Thin => Dimension::Points(border_width_config.thin),
_ => todo!(),
border::BorderSideWidth::Length(Length::Value(value)) => {
LengthPercentage::Points(extract_px_value(value))
}
border::BorderSideWidth::Thick => LengthPercentage::Points(border_width_config.thick),
border::BorderSideWidth::Medium => LengthPercentage::Points(border_width_config.medium),
border::BorderSideWidth::Thin => LengthPercentage::Points(border_width_config.thin),
border::BorderSideWidth::Length(_) => unimplemented!(),
}
}
fn convert_gap_value(gap_value: GapValue) -> Dimension {
fn convert_gap_value(gap_value: align::GapValue) -> LengthPercentage {
match gap_value {
GapValue::LengthPercentage(dim) => convert_dimension_percentage(dim),
GapValue::Normal => Dimension::Auto,
align::GapValue::LengthPercentage(dim) => convert_length_percentage(dim),
align::GapValue::Normal => LengthPercentage::Points(0.0),
}
}
fn convert_size(size: size::Size) -> Dimension {
match size {
size::Size::Auto => Dimension::Auto,
size::Size::LengthPercentage(length) => convert_dimension_percentage(length),
_ => todo!(),
size::Size::LengthPercentage(length) => convert_dimension(length),
size::Size::MinContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::MaxContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::FitContent(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::FitContentFunction(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::Stretch(_) => Dimension::Auto, // Unimplemented, so default auto
size::Size::Contain => Dimension::Auto, // Unimplemented, so default auto
}
}
fn convert_grid_placement(input: grid::GridLine) -> GridPlacement {
match input {
grid::GridLine::Auto => GridPlacement::Auto,
grid::GridLine::Line { index, .. } => line(index as i16),
grid::GridLine::Span { index, .. } => span(index as u16),
grid::GridLine::Area { .. } => unimplemented!(),
}
}
fn convert_grid_track_item(input: grid::TrackListItem) -> TrackSizingFunction {
match input {
grid::TrackListItem::TrackSize(size) => {
TrackSizingFunction::Single(convert_grid_track_size(size))
}
grid::TrackListItem::TrackRepeat(_) => todo!(), // TODO: requires TrackRepeat fields to be public!
}
}
fn convert_grid_track_size(input: grid::TrackSize) -> NonRepeatedTrackSizingFunction {
match input {
grid::TrackSize::TrackBreadth(breadth) => minmax(
convert_track_breadth_min(&breadth),
convert_track_breadth_max(&breadth),
),
grid::TrackSize::MinMax { min, max } => minmax(
convert_track_breadth_min(&min),
convert_track_breadth_max(&max),
),
grid::TrackSize::FitContent(limit) => match limit {
DimensionPercentage::Dimension(LengthValue::Px(len)) => minmax(auto(), points(len)),
DimensionPercentage::Percentage(Percentage(pct)) => minmax(auto(), percent(pct)),
_ => unimplemented!(),
},
}
}
fn convert_track_breadth_max(breadth: &TrackBreadth) -> MaxTrackSizingFunction {
match breadth {
grid::TrackBreadth::Length(length_percentage) => match length_percentage {
DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len),
DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct),
_ => unimplemented!(),
},
grid::TrackBreadth::Flex(fraction) => fr(*fraction),
grid::TrackBreadth::MinContent => MaxTrackSizingFunction::MinContent,
grid::TrackBreadth::MaxContent => MaxTrackSizingFunction::MaxContent,
grid::TrackBreadth::Auto => MaxTrackSizingFunction::Auto,
}
}
fn convert_track_breadth_min(breadth: &TrackBreadth) -> MinTrackSizingFunction {
match breadth {
grid::TrackBreadth::Length(length_percentage) => match length_percentage {
DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len),
DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct),
_ => unimplemented!(),
},
grid::TrackBreadth::MinContent => MinTrackSizingFunction::MinContent,
grid::TrackBreadth::MaxContent => MinTrackSizingFunction::MaxContent,
grid::TrackBreadth::Auto => MinTrackSizingFunction::Auto,
grid::TrackBreadth::Flex(_) => MinTrackSizingFunction::Auto,
}
}
+1 -1
View File
@@ -178,7 +178,7 @@ impl<'a> TreeMut for TreeMutView<'a> {
fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) {
let children = {
let mut node_data_mut = &mut tree.1;
let mut node = (&mut node_data_mut).get(node).unwrap();
let node = (&mut node_data_mut).get(node).unwrap();
node.height = height;
node.children.clone()
};
+1 -1
View File
@@ -22,7 +22,7 @@ crossterm = "0.23.0"
anyhow = "1.0.42"
tokio = { version = "1.15.0", features = ["full"] }
futures = "0.3.19"
taffy = "0.2.1"
taffy = "0.3.12"
smallvec = "1.6"
rustc-hash = "1.1.0"
anymap = "1.0.0-beta.2"
File renamed without changes.
+56 -34
View File
@@ -81,10 +81,11 @@ impl State for TaffyLayout {
};
if let PossiblyUninitalized::Initialized(n) = self.node {
if self.style != style {
taffy.set_style(n, style).unwrap();
taffy.set_style(n, style.clone()).unwrap();
}
} else {
self.node = PossiblyUninitalized::Initialized(taffy.new_leaf(style).unwrap());
self.node =
PossiblyUninitalized::Initialized(taffy.new_leaf(style.clone()).unwrap());
changed = true;
}
} else {
@@ -117,69 +118,90 @@ impl State for TaffyLayout {
child_layout.push(l.node.unwrap());
}
fn scale_dimention(d: Dimension) -> Dimension {
fn scale_dimension(d: Dimension) -> Dimension {
match d {
Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)),
Dimension::Percent(p) => Dimension::Percent(p),
Dimension::Auto => Dimension::Auto,
Dimension::Undefined => Dimension::Undefined,
}
}
let style = Style {
position: Rect {
left: scale_dimention(style.position.left),
right: scale_dimention(style.position.right),
top: scale_dimention(style.position.top),
bottom: scale_dimention(style.position.bottom),
fn scale_length_percentage_auto(d: LengthPercentageAuto) -> LengthPercentageAuto {
match d {
LengthPercentageAuto::Points(p) => {
LengthPercentageAuto::Points(unit_to_layout_space(p))
}
LengthPercentageAuto::Percent(p) => LengthPercentageAuto::Percent(p),
LengthPercentageAuto::Auto => LengthPercentageAuto::Auto,
}
}
fn scale_length_percentage(d: LengthPercentage) -> LengthPercentage {
match d {
LengthPercentage::Points(p) => {
LengthPercentage::Points(unit_to_layout_space(p))
}
LengthPercentage::Percent(p) => LengthPercentage::Percent(p),
}
}
let scaled_style = Style {
inset: Rect {
left: scale_length_percentage_auto(style.inset.left),
right: scale_length_percentage_auto(style.inset.right),
top: scale_length_percentage_auto(style.inset.top),
bottom: scale_length_percentage_auto(style.inset.bottom),
},
margin: Rect {
left: scale_dimention(style.margin.left),
right: scale_dimention(style.margin.right),
top: scale_dimention(style.margin.top),
bottom: scale_dimention(style.margin.bottom),
left: scale_length_percentage_auto(style.margin.left),
right: scale_length_percentage_auto(style.margin.right),
top: scale_length_percentage_auto(style.margin.top),
bottom: scale_length_percentage_auto(style.margin.bottom),
},
padding: Rect {
left: scale_dimention(style.padding.left),
right: scale_dimention(style.padding.right),
top: scale_dimention(style.padding.top),
bottom: scale_dimention(style.padding.bottom),
left: scale_length_percentage(style.padding.left),
right: scale_length_percentage(style.padding.right),
top: scale_length_percentage(style.padding.top),
bottom: scale_length_percentage(style.padding.bottom),
},
border: Rect {
left: scale_dimention(style.border.left),
right: scale_dimention(style.border.right),
top: scale_dimention(style.border.top),
bottom: scale_dimention(style.border.bottom),
left: scale_length_percentage(style.border.left),
right: scale_length_percentage(style.border.right),
top: scale_length_percentage(style.border.top),
bottom: scale_length_percentage(style.border.bottom),
},
gap: Size {
width: scale_dimention(style.gap.width),
height: scale_dimention(style.gap.height),
width: scale_length_percentage(style.gap.width),
height: scale_length_percentage(style.gap.height),
},
flex_basis: scale_dimention(style.flex_basis),
flex_basis: scale_dimension(style.flex_basis),
size: Size {
width: scale_dimention(style.size.width),
height: scale_dimention(style.size.height),
width: scale_dimension(style.size.width),
height: scale_dimension(style.size.height),
},
min_size: Size {
width: scale_dimention(style.min_size.width),
height: scale_dimention(style.min_size.height),
width: scale_dimension(style.min_size.width),
height: scale_dimension(style.min_size.height),
},
max_size: Size {
width: scale_dimention(style.max_size.width),
height: scale_dimention(style.max_size.height),
width: scale_dimension(style.max_size.width),
height: scale_dimension(style.max_size.height),
},
..style
..style.clone()
};
if let PossiblyUninitalized::Initialized(n) = self.node {
if self.style != style {
taffy.set_style(n, style).unwrap();
taffy.set_style(n, scaled_style).unwrap();
}
if taffy.children(n).unwrap() != child_layout {
taffy.set_children(n, &child_layout).unwrap();
}
} else {
self.node = PossiblyUninitalized::Initialized(
taffy.new_with_children(style, &child_layout).unwrap(),
taffy
.new_with_children(scaled_style, &child_layout)
.unwrap(),
);
changed = true;
}
+3 -2
View File
@@ -98,7 +98,8 @@ pub fn render<R: Driver>(
let event_tx_clone = event_tx.clone();
if !cfg.headless {
std::thread::spawn(move || {
let tick_rate = Duration::from_millis(1000);
// Timeout after 10ms when waiting for events
let tick_rate = Duration::from_millis(10);
loop {
if crossterm::event::poll(tick_rate).unwrap() {
let evt = crossterm::event::read().unwrap();
@@ -171,7 +172,7 @@ pub fn render<R: Driver>(
.unwrap();
// the root node fills the entire area
let mut style = *taffy.style(root_node).unwrap();
let mut style = taffy.style(root_node).unwrap().clone();
let new_size = Size {
width: Dimension::Points(width),
height: Dimension::Points(height),
+1 -9
View File
@@ -91,15 +91,7 @@ impl<'a> ElementRef<'a> {
pub fn layout(&self) -> Option<Layout> {
let layout = self
.stretch
.layout(
self.inner
.get(self.id)
.unwrap()
.get::<TaffyLayout>()
.unwrap()
.node
.ok()?,
)
.layout(self.inner.get(self.id)?.get::<TaffyLayout>()?.node.ok()?)
.ok();
layout.map(|layout| Layout {
order: layout.order,
+1 -1
View File
@@ -26,7 +26,7 @@ impl<'a> RinkBuffer<'a> {
// panic!("({x}, {y}) is not in {area:?}");
return;
}
let mut cell = self.buf.get_mut(x, y);
let cell = self.buf.get_mut(x, y);
cell.bg = convert(self.cfg.rendering_mode, new.bg.blend(cell.bg));
if new.symbol.is_empty() {
if !cell.symbol.is_empty() {
File renamed without changes.
+3
View File
@@ -25,3 +25,6 @@ convert_case = "0.5.0"
# default = ["html"]
# eventually more output options
[dev-dependencies]
pretty_assertions = "1.2.1"
+68
View File
@@ -0,0 +1,68 @@
use html_parser::Dom;
#[test]
fn simple_elements() {
let html = r#"
<div>
<div class="asd">hello world!</div>
<div id="asd">hello world!</div>
<div id="asd">hello world!</div>
<div for="asd">hello world!</div>
<div async="asd">hello world!</div>
<div LargeThing="asd">hello world!</div>
<ai-is-awesome>hello world!</ai-is-awesome>
</div>
"#
.trim();
let dom = Dom::parse(html).unwrap();
let body = rsx_rosetta::rsx_from_html(&dom);
let out = dioxus_autofmt::write_block_out(body).unwrap();
let expected = r#"
div {
div { class: "asd", "hello world!" }
div { id: "asd", "hello world!" }
div { id: "asd", "hello world!" }
div { r#for: "asd", "hello world!" }
div { r#async: "asd", "hello world!" }
div { large_thing: "asd", "hello world!" }
ai_is_awesome { "hello world!" }
}"#;
pretty_assertions::assert_eq!(&out, &expected);
}
#[test]
fn deeply_nested() {
let html = r#"
<div>
<div class="asd">
<div class="asd">
<div class="asd">
<div class="asd">
</div>
</div>
</div>
</div>
</div>
"#
.trim();
let dom = Dom::parse(html).unwrap();
let body = rsx_rosetta::rsx_from_html(&dom);
let out = dioxus_autofmt::write_block_out(body).unwrap();
let expected = r#"
div {
div { class: "asd",
div { class: "asd",
div { class: "asd", div { class: "asd" } }
}
}
}"#;
pretty_assertions::assert_eq!(&out, &expected);
}
+7 -3
View File
@@ -3,7 +3,7 @@ use crate::cache::StringCache;
use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn};
use std::collections::HashMap;
use std::fmt::Write;
use std::rc::Rc;
use std::sync::Arc;
/// A virtualdom renderer that caches the templates it has seen for faster rendering
#[derive(Default)]
@@ -25,7 +25,7 @@ pub struct Renderer {
pub skip_components: bool,
/// A cache of templates that have been rendered
template_cache: HashMap<&'static str, Rc<StringCache>>,
template_cache: HashMap<&'static str, Arc<StringCache>>,
}
impl Renderer {
@@ -67,7 +67,7 @@ impl Renderer {
let entry = self
.template_cache
.entry(template.template.get().name)
.or_insert_with(|| Rc::new(StringCache::from_template(template).unwrap()))
.or_insert_with(|| Arc::new(StringCache::from_template(template).unwrap()))
.clone();
let mut inner_html = None;
@@ -89,6 +89,10 @@ impl Renderer {
write!(buf, " {}=\"{}\"", attr.name, value)?
}
AttributeValue::Bool(value) => write!(buf, " {}={}", attr.name, value)?,
AttributeValue::Int(value) => write!(buf, " {}={}", attr.name, value)?,
AttributeValue::Float(value) => {
write!(buf, " {}={}", attr.name, value)?
}
_ => {}
};
}
+5 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-web"
version = "0.3.1"
version = "0.3.2"
authors = ["Jonathan Kelley"]
edition = "2018"
description = "Dioxus VirtualDOM renderer for the web browser using websys"
@@ -32,6 +32,7 @@ futures-channel = "0.3.21"
serde_json = { version = "1.0" }
serde = { version = "1.0" }
serde-wasm-bindgen = "0.4.5"
async-trait = "0.1.58"
[dependencies.web-sys]
version = "0.3.56"
@@ -76,6 +77,9 @@ features = [
"Location",
"MessageEvent",
"console",
"FileList",
"File",
"FileReader"
]
[features]
+118 -60
View File
@@ -10,15 +10,16 @@
use dioxus_core::{
BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode,
};
use dioxus_html::{event_bubbles, CompositionData, FormData};
use dioxus_interpreter_js::{save_template, Channel};
use dioxus_html::{event_bubbles, CompositionData, FileEngine, FormData, MountedData};
use dioxus_interpreter_js::{get_node, save_template, Channel};
use futures_channel::mpsc;
use js_sys::Array;
use rustc_hash::FxHashMap;
use std::{any::Any, rc::Rc};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{Document, Element, Event, HtmlElement};
use std::{any::Any, rc::Rc, sync::Arc};
use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast};
use web_sys::{console, Document, Element, Event, HtmlElement};
use crate::Config;
use crate::{file_engine::WebFileEngine, Config};
pub struct WebsysDom {
document: Document,
@@ -27,6 +28,7 @@ pub struct WebsysDom {
templates: FxHashMap<String, u32>,
max_template_id: u32,
pub(crate) interpreter: Channel,
event_channel: mpsc::UnboundedSender<UiEvent>,
}
pub struct UiEvent {
@@ -34,7 +36,6 @@ pub struct UiEvent {
pub bubbles: bool,
pub element: ElementId,
pub data: Rc<dyn Any>,
pub event: Event,
}
impl WebsysDom {
@@ -48,19 +49,24 @@ impl WebsysDom {
};
let interpreter = Channel::default();
let handler: Closure<dyn FnMut(&Event)> =
Closure::wrap(Box::new(move |event: &web_sys::Event| {
let handler: Closure<dyn FnMut(&Event)> = Closure::wrap(Box::new({
let event_channel = event_channel.clone();
move |event: &web_sys::Event| {
let name = event.type_();
let element = walk_event_for_id(event);
let bubbles = dioxus_html::event_bubbles(name.as_str());
if let Some((element, target)) = element {
if target
if let Some(prevent_requests) = target
.get_attribute("dioxus-prevent-default")
.as_deref()
.map(|f| f.trim_start_matches("on"))
== Some(&name)
.map(|f| f.split_whitespace())
{
event.prevent_default();
if prevent_requests
.map(|f| f.trim_start_matches("on"))
.any(|f| f == name)
{
event.prevent_default();
}
}
let data = virtual_event_from_websys_event(event.clone(), target);
@@ -69,10 +75,10 @@ impl WebsysDom {
bubbles,
element,
data,
event: event.clone(),
});
}
}));
}
}));
dioxus_interpreter_js::initilize(
root.clone().unchecked_into(),
@@ -85,6 +91,7 @@ impl WebsysDom {
interpreter,
templates: FxHashMap::default(),
max_template_id: 0,
event_channel,
}
}
@@ -156,6 +163,8 @@ impl WebsysDom {
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
use Mutation::*;
let i = &mut self.interpreter;
// we need to apply the mount events last, so we collect them here
let mut to_mount = Vec::new();
for edit in &edits {
match edit {
AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32),
@@ -206,17 +215,43 @@ impl WebsysDom {
},
SetText { value, id } => i.set_text(id.0 as u32, value),
NewEventListener { name, id, .. } => {
i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
RemoveEventListener { name, id } => {
i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
match *name {
// mounted events are fired immediately after the element is mounted.
"mounted" => {
to_mount.push(*id);
}
_ => {
i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
}
}
RemoveEventListener { name, id } => match *name {
"mounted" => {}
_ => {
i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8);
}
},
Remove { id } => i.remove(id.0 as u32),
PushRoot { id } => i.push_root(id.0 as u32),
}
}
edits.clear();
i.flush();
for id in to_mount {
let node = get_node(id.0 as u32);
if let Some(element) = node.dyn_ref::<Element>() {
log::info!("mounted event fired: {}", id.0);
let data: MountedData = element.into();
let data = Rc::new(data);
let _ = self.event_channel.unbounded_send(UiEvent {
name: "mounted".to_string(),
bubbles: false,
element: id,
data,
});
}
}
}
}
@@ -224,6 +259,7 @@ impl WebsysDom {
// We need tests that simulate clicks/etc and make sure every event type works.
pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc<dyn Any> {
use dioxus_html::events::*;
console::log_1(&event.clone().into());
match event.type_().as_str() {
"copy" | "cut" | "paste" => Rc::new(ClipboardData {}),
@@ -325,64 +361,86 @@ fn read_input_to_data(target: Element) -> Rc<FormData> {
// try to fill in form values
if let Some(form) = target.dyn_ref::<web_sys::HtmlFormElement>() {
let elements = form.elements();
for x in 0..elements.length() {
let element = elements.item(x).unwrap();
if let Some(name) = element.get_attribute("name") {
let value: Option<String> = element
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| {
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => Some("true".to_string()),
false => Some("false".to_string()),
}
},
"radio" => {
match input.checked() {
true => Some(input.value()),
false => None,
}
}
_ => Some(input.value())
}
})
.or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value())))
.or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value())))
.or_else(|| Some(element.dyn_ref::<web_sys::HtmlElement>().unwrap().text_content()))
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
if let Some(value) = value {
values.insert(name, value);
let form_data = get_form_data(form);
for value in form_data.entries().into_iter().flatten() {
if let Ok(array) = value.dyn_into::<Array>() {
if let Some(name) = array.get(0).as_string() {
if let Ok(item_values) = array.get(1).dyn_into::<Array>() {
let item_values =
item_values.iter().filter_map(|v| v.as_string()).collect();
values.insert(name, item_values);
}
}
}
}
}
let files = target
.dyn_ref()
.and_then(|input: &web_sys::HtmlInputElement| {
input.files().and_then(|files| {
WebFileEngine::new(files).map(|f| Arc::new(f) as Arc<dyn FileEngine>)
})
});
Rc::new(FormData {
value,
values,
files: None,
files,
})
}
// web-sys does not expose the keys api for form data, so we need to manually bind to it
#[wasm_bindgen(inline_js = r#"
export function get_form_data(form) {
let values = new Map();
const formData = new FormData(form);
for (let name of formData.keys()) {
values.set(name, formData.getAll(name));
}
return values;
}
"#)]
extern "C" {
fn get_form_data(form: &web_sys::HtmlFormElement) -> js_sys::Map;
}
fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> {
let mut target = event
let target = event
.target()
.expect("missing target")
.dyn_into::<web_sys::Element>()
.expect("not a valid element");
.dyn_into::<web_sys::Node>()
.expect("not a valid node");
let mut current_target_element = target.dyn_ref::<web_sys::Element>().cloned();
loop {
match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
Some(Ok(id)) => return Some((ElementId(id), target)),
Some(Err(_)) => return None,
match (
current_target_element
.as_ref()
.and_then(|el| el.get_attribute("data-dioxus-id").map(|f| f.parse())),
current_target_element,
) {
// This node is an element, and has a dioxus id, so we can stop walking
(Some(Ok(id)), Some(target)) => return Some((ElementId(id), target)),
// walk the tree upwards until we actually find an event target
None => match target.parent_element() {
Some(parent) => target = parent,
None => return None,
},
// Walk the tree upwards until we actually find an event target
(None, target_element) => {
let parent = match target_element.as_ref() {
Some(el) => el.parent_element(),
// if this is the first node and not an element, we need to get the parent from the target node
None => target.parent_element(),
};
match parent {
Some(parent) => current_target_element = Some(parent),
_ => return None,
}
}
// This node is an element with an invalid dioxus id, give up
_ => return None,
}
}
}
+103
View File
@@ -0,0 +1,103 @@
use dioxus_html::FileEngine;
use futures_channel::oneshot;
use js_sys::Uint8Array;
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::{File, FileList, FileReader};
pub(crate) struct WebFileEngine {
file_reader: FileReader,
file_list: FileList,
}
impl WebFileEngine {
pub fn new(file_list: FileList) -> Option<Self> {
Some(Self {
file_list,
file_reader: FileReader::new().ok()?,
})
}
fn len(&self) -> usize {
self.file_list.length() as usize
}
fn get(&self, index: usize) -> Option<File> {
self.file_list.item(index as u32)
}
fn find(&self, name: &str) -> Option<File> {
(0..self.len())
.filter_map(|i| self.get(i))
.find(|f| f.name() == name)
}
}
#[async_trait::async_trait(?Send)]
impl FileEngine for WebFileEngine {
fn files(&self) -> Vec<String> {
(0..self.len())
.filter_map(|i| self.get(i).map(|f| f.name()))
.collect()
}
// read a file to bytes
async fn read_file(&self, file: &str) -> Option<Vec<u8>> {
let file = self.find(file)?;
let file_reader = self.file_reader.clone();
let (rx, tx) = oneshot::channel();
let on_load: Closure<dyn FnMut()> = Closure::new({
let mut rx = Some(rx);
move || {
let result = file_reader.result();
let _ = rx
.take()
.expect("multiple files read without refreshing the channel")
.send(result);
}
});
self.file_reader
.set_onload(Some(on_load.as_ref().unchecked_ref()));
on_load.forget();
self.file_reader.read_as_array_buffer(&file).ok()?;
if let Ok(Ok(js_val)) = tx.await {
let as_u8_arr = Uint8Array::new(&js_val);
let as_u8_vec = as_u8_arr.to_vec();
Some(as_u8_vec)
} else {
None
}
}
// read a file to string
async fn read_file_to_string(&self, file: &str) -> Option<String> {
let file = self.find(file)?;
let file_reader = self.file_reader.clone();
let (rx, tx) = oneshot::channel();
let on_load: Closure<dyn FnMut()> = Closure::new({
let mut rx = Some(rx);
move || {
let result = file_reader.result();
let _ = rx
.take()
.expect("multiple files read without refreshing the channel")
.send(result);
}
});
self.file_reader
.set_onload(Some(on_load.as_ref().unchecked_ref()));
on_load.forget();
self.file_reader.read_as_text(&file).ok()?;
if let Ok(Ok(js_val)) = tx.await {
js_val.as_string()
} else {
None
}
}
}
+3 -2
View File
@@ -61,6 +61,7 @@ use futures_util::{pin_mut, FutureExt, StreamExt};
mod cache;
mod cfg;
mod dom;
mod file_engine;
mod hot_reload;
#[cfg(feature = "hydrate")]
mod rehydrate;
@@ -225,7 +226,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
websys_dom.mount();
loop {
log::debug!("waiting for work");
log::trace!("waiting for work");
// if virtualdom has nothing, wait for it to have something before requesting idle time
// if there is work then this future resolves immediately.
@@ -253,7 +254,7 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
res = rx.try_next().transpose().unwrap().ok();
}
// Todo: This is currently disabled because it has a negative impact on responce times for events but it could be re-enabled for tasks
// Todo: This is currently disabled because it has a negative impact on response times for events but it could be re-enabled for tasks
// Jank free rendering
//
// 1. wait for the browser to give us "idle" time