Merge branch 'master' into make-widgets-cross-framework

This commit is contained in:
Evan Almloff committed 2023-06-30 12:44:15 -07:00
commit d99a5f3338
207 files changed
+9886 -1194

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ keywords = ["dom", "ui", "gui", "react"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
dioxus-rsx = { workspace = true }
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
quote = "1.0"
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
+3 -1
View File
@@ -8,6 +8,8 @@ use std::fmt::{Result, Write};
use dioxus_rsx::IfmtInput;
use crate::write_ifmt;
/// The output buffer that tracks indent and string
#[derive(Debug, Default)]
pub struct Buffer {
@@ -48,7 +50,7 @@ impl Buffer {
}
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value())
write_ifmt(text, &mut self.buf)
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
use crate::{writer::Location, Writer};
use crate::{ifmt_to_string, writer::Location, Writer};
use dioxus_rsx::*;
use quote::ToTokens;
use std::fmt::{Result, Write};
@@ -170,9 +170,9 @@ impl Writer<'_> {
ContentField::Formatted(s) => {
write!(
self.out,
"{}: \"{}\"",
"{}: {}",
name,
s.source.as_ref().unwrap().value()
s.source.as_ref().unwrap().to_token_stream()
)?;
}
ContentField::OnHandlerRaw(exp) => {
@@ -215,7 +215,7 @@ impl Writer<'_> {
let attr_len = fields
.iter()
.map(|field| match &field.content {
ContentField::Formatted(s) => s.source.as_ref().unwrap().value().len() ,
ContentField::Formatted(s) => ifmt_to_string(s).len() ,
ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => {
let formatted = prettyplease::unparse_expr(exp);
let len = if formatted.contains('\n') {
+17 -25
View File
@@ -1,6 +1,7 @@
use crate::Writer;
use crate::{ifmt_to_string, Writer};
use dioxus_rsx::*;
use proc_macro2::Span;
use quote::ToTokens;
use std::{
fmt::Result,
fmt::{self, Write},
@@ -175,11 +176,7 @@ impl Writer<'_> {
if !sameline {
self.out.indented_tabbed_line()?;
}
write!(
self.out,
"key: \"{}\"",
key.source.as_ref().unwrap().value()
)?;
write!(self.out, "key: {}", ifmt_to_string(key))?;
if !attributes.is_empty() {
write!(self.out, ",")?;
if sameline {
@@ -216,11 +213,7 @@ impl Writer<'_> {
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
match &attr.attr {
ElementAttr::AttrText { name, value } => {
write!(
self.out,
"{name}: \"{value}\"",
value = value.source.as_ref().unwrap().value()
)?;
write!(self.out, "{name}: {value}", value = ifmt_to_string(value))?;
}
ElementAttr::AttrExpression { name, value } => {
let out = prettyplease::unparse_expr(value);
@@ -230,15 +223,15 @@ impl Writer<'_> {
ElementAttr::CustomAttrText { name, value } => {
write!(
self.out,
"\"{name}\": \"{value}\"",
name = name.value(),
value = value.source.as_ref().unwrap().value()
"{name}: {value}",
name = name.to_token_stream(),
value = ifmt_to_string(value)
)?;
}
ElementAttr::CustomAttrExpression { name, value } => {
let out = prettyplease::unparse_expr(value);
write!(self.out, "\"{}\": {}", name.value(), out)?;
write!(self.out, "{}: {}", name.to_token_stream(), out)?;
}
ElementAttr::EventTokens { name, tokens } => {
@@ -276,13 +269,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()
}
@@ -316,7 +308,7 @@ impl Writer<'_> {
}
match children {
[BodyNode::Text(ref text)] => Some(text.source.as_ref().unwrap().value().len()),
[BodyNode::Text(ref text)] => Some(ifmt_to_string(text).len()),
[BodyNode::Component(ref comp)] => {
let attr_len = self.field_len(&comp.fields, &comp.manual_props);
@@ -339,7 +331,7 @@ impl Writer<'_> {
if el.children.len() == 1 {
if let BodyNode::Text(ref text) = el.children[0] {
let value = text.source.as_ref().unwrap().value();
let value = ifmt_to_string(text);
if value.len() + el.name.to_string().len() + attr_len < 80 {
return Some(value.len() + el.name.to_string().len() + attr_len);
@@ -357,7 +349,7 @@ impl Writer<'_> {
match item {
BodyNode::Component(_) | BodyNode::Element(_) => return None,
BodyNode::Text(text) => {
total_count += text.source.as_ref().unwrap().value().len()
total_count += ifmt_to_string(text).len();
}
BodyNode::RawExpr(expr) => match get_expr_length(expr) {
Some(len) => total_count += len,
+25 -3
View File
@@ -1,7 +1,10 @@
use std::fmt::{Display, Write};
use crate::writer::*;
use collect_macros::byte_offset;
use dioxus_rsx::{BodyNode, CallBody};
use dioxus_rsx::{BodyNode, CallBody, IfmtInput};
use proc_macro2::LineColumn;
use quote::ToTokens;
use syn::{ExprMacro, MacroDelimiter};
mod buffer;
@@ -130,8 +133,6 @@ pub fn write_block_out(body: CallBody) -> Option<String> {
}
fn write_body(buf: &mut Writer, body: &CallBody) {
use std::fmt::Write;
if buf.is_short_children(&body.roots).is_some() {
// write all the indents with spaces and commas between
for idx in 0..body.roots.len() - 1 {
@@ -208,3 +209,24 @@ pub fn apply_formats(input: &str, blocks: Vec<FormattedBlock>) -> String {
out
}
struct DisplayIfmt<'a>(&'a IfmtInput);
impl Display for DisplayIfmt<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let inner_tokens = self.0.source.as_ref().unwrap().to_token_stream();
inner_tokens.fmt(f)
}
}
pub(crate) fn ifmt_to_string(input: &IfmtInput) -> String {
let mut buf = String::new();
let display = DisplayIfmt(input);
write!(&mut buf, "{}", display).unwrap();
buf
}
pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::fmt::Result {
let display = DisplayIfmt(input);
write!(writable, "{}", display)
}
+4 -3
View File
@@ -8,6 +8,7 @@ use std::{
use syn::{spanned::Spanned, Expr, ExprIf};
use crate::buffer::Buffer;
use crate::ifmt_to_string;
#[derive(Debug)]
pub struct Writer<'a> {
@@ -147,16 +148,16 @@ impl<'a> Writer<'a> {
total += match &attr.attr {
ElementAttr::AttrText { value, name } => {
value.source.as_ref().unwrap().value().len() + name.span().line_length() + 6
ifmt_to_string(value).len() + name.span().line_length() + 6
}
ElementAttr::AttrExpression { name, value } => {
value.span().line_length() + name.span().line_length() + 6
}
ElementAttr::CustomAttrText { value, name } => {
value.source.as_ref().unwrap().value().len() + name.value().len() + 6
ifmt_to_string(value).len() + name.to_token_stream().to_string().len() + 6
}
ElementAttr::CustomAttrExpression { name, value } => {
name.value().len() + value.span().line_length() + 6
name.to_token_stream().to_string().len() + value.span().line_length() + 6
}
ElementAttr::EventTokens { tokens, name } => {
let location = Location::new(tokens.span().start());
+22 -18
View File
@@ -4,8 +4,8 @@ macro_rules! twoway {
// doc attrs
$( #[doc = $doc:expr] )*
$name:ident
),*
$name:ident,
)*
) => {
$(
$( #[doc = $doc] )*
@@ -14,6 +14,9 @@ macro_rules! twoway {
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
let formatted = dioxus_autofmt::fmt_file(src);
let out = dioxus_autofmt::apply_formats(src, formatted);
// normalize line endings
let out = out.replace("\r", "");
let src = src.replace("\r", "");
pretty_assertions::assert_eq!(&src, &out);
}
)*
@@ -21,24 +24,25 @@ macro_rules! twoway {
}
twoway![
simple,
comments,
attributes,
manual_props,
collapse_expr,
comments,
commentshard,
complex,
emoji,
ifchain_forloop,
immediate_expr,
key,
long_exprs,
long,
manual_props,
messy_indent,
multirsx,
raw_strings,
reallylong,
simple,
t2,
tiny,
tinynoopt,
long,
key,
multirsx,
commentshard,
emoji,
messy_indent,
long_exprs,
ifchain_forloop,
t2,
reallylong,
immediate_expr,
collapse_expr,
trailing_expr
trailing_expr,
];
+3 -1
View File
@@ -13,7 +13,9 @@ rsx! {
// Complex nesting with handlers
li {
Link { class: "flex items-center pl-3 py-3 pr-4 {active_class} rounded", to: "{to}",
Link {
class: "flex items-center pl-3 py-3 pr-4 {active_class} rounded",
to: "{to}",
span { class: "inline-block mr-3", icons::icon_0 {} }
span { "{name}" }
children.is_some().then(|| rsx! {
@@ -0,0 +1,12 @@
rsx! {
// Raw text strings
button { r#"Click me"#, r##"Click me"##, r######"Click me"######, r#"dynamic {1}"# }
// Raw attribute strings
div {
width: r#"10px"#,
height: r##"{10}px"##,
"raw-attr": r###"raw-attr"###,
"raw-attr2": r###"{100}"###
}
}
+5
View File
@@ -6,6 +6,11 @@ macro_rules! twoway {
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
let formatted = dioxus_autofmt::fmt_file(src_wrong);
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
// normalize line endings
let out = out.replace("\r", "");
let src_right = src_right.replace("\r", "");
pretty_assertions::assert_eq!(&src_right, &out);
}
};
+1 -1
View File
@@ -18,7 +18,7 @@ proc-macro = true
proc-macro2 = { version = "1.0" }
quote = "1.0"
syn = { version = "1.0", features = ["full", "extra-traits"] }
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
dioxus-rsx = { workspace = true }
# testing
[dev-dependencies]
+8 -8
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"
@@ -18,23 +18,23 @@ keywords = ["dom", "ui", "gui", "react"]
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
# faster hashmaps
rustc-hash = "1.1.0"
rustc-hash = { workspace = true }
# Used in diffing
longest-increasing-subsequence = "0.1.0"
futures-util = { version = "0.3", default-features = false, features = [
futures-util = { workspace = true, default-features = false, features = [
"alloc",
] }
slab = "0.4"
slab = { workspace = true }
futures-channel = "0.3.21"
futures-channel = { workspace = true }
indexmap = "1.7"
smallbox = "0.8.1"
log = "0.4.17"
log = { workspace = true }
# Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
@@ -42,8 +42,8 @@ serde = { version = "1", features = ["derive"], optional = true }
bumpslab = { version = "0.2.0" }
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
dioxus = { path = "../dioxus" }
tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true }
pretty_assertions = "1.3.0"
rand = "0.8.5"
+3 -5
View File
@@ -7,7 +7,7 @@ use crate::{
nodes::{DynamicNode, VNode},
scopes::ScopeId,
virtual_dom::VirtualDom,
Attribute, AttributeValue, TemplateNode,
Attribute, TemplateNode,
};
use rustc_hash::{FxHashMap, FxHashSet};
@@ -125,10 +125,8 @@ impl<'b> VirtualDom {
.mounted_element
.set(left_attr.mounted_element.get());
// We want to make sure anything listener that gets pulled is valid
if let AttributeValue::Listener(_) = right_attr.value {
self.update_template(left_attr.mounted_element.get(), right_template);
}
// We want to make sure anything that gets pulled is valid
self.update_template(left_attr.mounted_element.get(), right_template);
// If the attributes are different (or volatile), we need to update them
if left_attr.value != right_attr.value || left_attr.volatile {
+15
View File
@@ -284,6 +284,17 @@ where
Ok(&*Box::leak(deserialized))
}
#[cfg(feature = "serialize")]
fn deserialize_option_leaky<'a, 'de, D>(deserializer: D) -> Result<Option<&'static str>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let deserialized = Option::<String>::deserialize(deserializer)?;
Ok(deserialized.map(|deserialized| &*Box::leak(deserialized.into_boxed_str())))
}
impl<'a> Template<'a> {
/// Is this template worth caching at all, since it's completely runtime?
///
@@ -319,6 +330,10 @@ pub enum TemplateNode<'a> {
///
/// In HTML, this would be a valid URI that defines a namespace for all elements below it
/// SVG is an example of this namespace
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "deserialize_option_leaky")
)]
namespace: Option<&'a str>,
/// A list of possibly dynamic attribues for this element
+65 -37
View File
@@ -384,51 +384,79 @@ 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
if let Some(template) = el_ref.template {
let template = unsafe { template.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 {
break;
}
}
} 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
if let Some(template) = el_ref.template {
let template = unsafe { template.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));
}
}
+29 -13
View File
@@ -12,30 +12,33 @@ keywords = ["dom", "ui", "gui", "react"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true, features = ["serialize", "native-bind"] }
dioxus-interpreter-js = { workspace = true }
dioxus-hot-reload = { workspace = true, optional = true }
serde = "1.0.136"
serde_json = "1.0.79"
thiserror = "1.0.30"
log = "0.4.14"
log = { workspace = true }
wry = { version = "0.27.2" }
futures-channel = "0.3.21"
tokio = { version = "1.16.1", features = [
futures-channel = { workspace = true }
tokio = { workspace = true, features = [
"sync",
"rt-multi-thread",
"rt",
"time",
"macros",
], optional = true, default-features = false }
"fs",
], optional = true }
webbrowser = "0.8.0"
infer = "0.11.0"
dunce = "1.0.2"
slab = "0.4"
slab = { workspace = true }
futures-util = "0.3.25"
futures-util = { workspace = true }
rfd = "0.11.3"
urlencoding = "2.1.2"
[target.'cfg(target_os = "ios")'.dependencies]
objc = "0.2.7"
@@ -53,6 +56,19 @@ tray = ["wry/tray"]
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-core-macro = { workspace = true }
dioxus-hooks = { workspace = true }
dioxus = { workspace = true }
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

+10
View File
@@ -23,6 +23,7 @@ pub struct Config {
pub(crate) custom_head: Option<String>,
pub(crate) custom_index: Option<String>,
pub(crate) root_name: String,
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
}
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
@@ -50,6 +51,7 @@ impl Config {
custom_head: None,
custom_index: None,
root_name: "main".to_string(),
background_color: None,
}
}
@@ -137,6 +139,14 @@ impl Config {
self.root_name = name.into();
self
}
/// Sets the background color of the WebView.
/// This will be set before the HTML is rendered and can be used to prevent flashing when the page loads.
/// Accepts a color in RGBA format
pub fn with_background_color(mut self, color: (u8, u8, u8, u8)) -> Self {
self.background_color = Some(color);
self
}
}
impl Default for Config {
+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),
+38 -5
View File
@@ -1,4 +1,4 @@
use dioxus_interpreter_js::INTERPRETER_JS;
use dioxus_interpreter_js::{COMMON_JS, INTERPRETER_JS};
use std::{
borrow::Cow,
path::{Path, PathBuf},
@@ -9,15 +9,41 @@ 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}
<script type="module">
{js}
let rootname = "{root_name}";
let root = window.document.getElementById(rootname);
if (root != null) {{
window.interpreter = new Interpreter(root);
window.interpreter = new Interpreter(root, new InterpreterConfig(true));
window.ipc.postMessage(serializeIpcMessage("initialize"));
}}
</script>
@@ -58,10 +84,17 @@ pub(super) fn desktop_handler(
.header("Content-Type", "text/html")
.body(Cow::from(body))
.map_err(From::from);
} else if request.uri().path() == "/common.js" {
return Response::builder()
.header("Content-Type", "text/javascript")
.body(Cow::from(COMMON_JS.as_bytes()))
.map_err(From::from);
}
// Else, try to serve a file from the filesystem.
let path = PathBuf::from(request.uri().path().trim_start_matches('/'));
let decoded = urlencoding::decode(request.uri().path().trim_start_matches('/'))
.expect("expected URL to be UTF-8 encoded");
let path = PathBuf::from(&*decoded);
// If the path is relative, we'll try to serve it from the assets directory.
let mut asset = get_asset_root()
+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(1000);
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,
}
+2 -9
View File
@@ -41,17 +41,10 @@ impl Shortcut {
impl ShortcutRegistry {
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
let myself = Self {
Self {
manager: Rc::new(RefCell::new(ShortcutManager::new(target))),
shortcuts: Rc::new(RefCell::new(HashMap::new())),
};
// prevent CTRL+R from reloading the page which breaks apps
let _ = myself.add_shortcut(
Some(ModifiersState::CONTROL),
KeyCode::KeyR,
Box::new(|| {}),
);
myself
}
}
pub(crate) fn call_handlers(&self, id: AcceleratorId) {
+15
View File
@@ -57,6 +57,21 @@ 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);
}
if let Some(color) = cfg.background_color {
webview = webview.with_background_color(color);
}
// 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)
}
+10 -10
View File
@@ -13,21 +13,21 @@ license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus = { path = "../dioxus", version = "^0.3.0" }
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
dioxus-html = { path = "../html", version = "^0.3.0" }
dioxus-native-core = { path = "../native-core", version = "^0.2.0", features = ["dioxus"] }
dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
rink = { path = "../rink" }
dioxus = { workspace = true }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true }
dioxus-native-core = { workspace = true, features = ["dioxus"] }
dioxus-native-core-macro = { workspace = true }
dioxus-hot-reload = { workspace = true, optional = true }
rink = { workspace = true }
crossterm = "0.23.0"
tokio = { version = "1.15.0", features = ["full"] }
tokio = { workspace = true, features = ["full"] }
futures = "0.3.19"
taffy = "0.2.1"
taffy = "0.3.12"
[dev-dependencies]
dioxus = { path = "../dioxus" }
dioxus = { workspace = true }
tokio = { version = "1" }
criterion = "0.3.5"
@@ -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 {}
+51 -9
View File
@@ -1,4 +1,7 @@
mod element;
use std::{
any::Any,
ops::Deref,
rc::Rc,
sync::{Arc, RwLock},
@@ -8,6 +11,7 @@ use dioxus_core::{Component, ElementId, VirtualDom};
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};
@@ -33,14 +37,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) =
@@ -58,6 +80,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>,
}
@@ -67,10 +91,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))),
);
}
}
}
@@ -91,6 +128,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();
+11 -11
View File
@@ -12,14 +12,14 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
rust-version = "1.65.0"
[dependencies]
dioxus-core = { path = "../core", version = "^0.3.0" }
dioxus-html = { path = "../html", version = "^0.3.0", optional = true }
dioxus-core-macro = { path = "../core-macro", version = "^0.3.0", optional = true }
dioxus-hooks = { path = "../hooks", version = "^0.3.0", optional = true }
dioxus-rsx = { path = "../rsx", version = "^0.0.3", optional = true }
dioxus-core = { workspace = true }
dioxus-html = { workspace = true, optional = true }
dioxus-core-macro = { workspace = true, optional = true }
dioxus-hooks = { workspace = true, optional = true }
dioxus-rsx = { workspace = true, optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dioxus-hot-reload = { path = "../hot-reload", version = "0.1.0", optional = true }
dioxus-hot-reload = { workspace = true, optional = true }
[features]
default = ["macro", "hooks", "html", "hot-reload"]
@@ -30,14 +30,14 @@ hot-reload = ["dioxus-hot-reload"]
[dev-dependencies]
futures-util = "0.3.21"
log = "0.4.14"
futures-util = { workspace = true }
log = { workspace = true }
rand = { version = "0.8.4", features = ["small_rng"] }
criterion = "0.3.5"
thiserror = "1.0.30"
env_logger = "0.9.0"
tokio = { version = "1.21.2", features = ["full"] }
# dioxus-edit-stream = { path = "../edit-stream" }
env_logger = "0.10.0"
tokio = { workspace = true, features = ["full"] }
# dioxus-edit-stream = { workspace = true }
[[bench]]
+2 -2
View File
@@ -13,9 +13,9 @@ keywords = ["dom", "ui", "gui", "react", "state-management"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-core = { path = "../core", version = "^0.3.0" }
dioxus-core = { workspace = true }
im-rc = { version = "15.0.0", features = ["serde"] }
log = "0.4.14"
log = { workspace = true }
[dev-dependencies]
closure = "0.3.0"
+6 -6
View File
@@ -9,22 +9,22 @@
<div align="center">
<!-- Crates version -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/v/dioxus.svg?style=flat-square"
<a href="https://crates.io/crates/fermi">
<img src="https://img.shields.io/crates/v/fermi.svg?style=flat-square"
alt="Crates.io version" />
</a>
<!-- Downloads -->
<a href="https://crates.io/crates/dioxus">
<img src="https://img.shields.io/crates/d/dioxus.svg?style=flat-square"
<a href="https://crates.io/crates/fermi">
<img src="https://img.shields.io/crates/d/fermi.svg?style=flat-square"
alt="Download" />
</a>
<!-- docs -->
<a href="https://docs.rs/dioxus">
<a href="https://docs.rs/fermi">
<img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
alt="docs.rs docs" />
</a>
<!-- CI -->
<a href="https://github.com/jkelleyrtp/dioxus/actions">
<a href="https://github.com/DioxusLabs/dioxus/actions">
<img src="https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg"
alt="CI status" />
</a>
+29
View File
@@ -70,17 +70,46 @@ impl<T: 'static> UseAtomRef<T> {
self.value.borrow()
}
/// This is silent operation
/// call `.force_update()` manually if required
pub fn with_mut_silent(&self, cb: impl FnOnce(&mut T)) {
cb(&mut *self.write_silent())
}
pub fn write(&self) -> RefMut<T> {
self.root.force_update(self.ptr);
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
+66
View File
@@ -0,0 +1,66 @@
[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 = "15a4e54435eb5a539afb75891292bcccd2cc8e85", default-features = false, 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 = { workspace = true }
dioxus-ssr = { workspace = true, optional = true }
hyper = { version = "0.14.25", optional = true }
http = { version = "0.2.9", optional = true }
log = { workspace = true }
once_cell = "1.17.1"
thiserror = "1.0.40"
tokio = { workspace = true, 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 = { workspace = true, 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 = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3.61", features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] }
[features]
default = ["hot-reload", "default-tls"]
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"]
default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls"]
+107
View File
@@ -0,0 +1,107 @@
# 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")]
{
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 = { workspace = true, optional = true }
dioxus = { workspace = true }
dioxus-router = { workspace = true }
dioxus-fullstack = { workspace = true }
axum = { version = "0.6.12", optional = true }
tokio = { workspace = true, 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,20 @@
// Run with:
// ```bash
// cargo run --bin server --features="ssr"
// ```
use axum_desktop::*;
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("")
.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 = { workspace = true, features=["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
axum = { version = "0.6.12", optional = true }
tokio = { workspace = true, 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,99 @@
//! 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
}));
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 = { workspace = true, features=["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-router = { workspace = true }
dioxus-fullstack = { workspace = true }
axum = { version = "0.6.12", optional = true }
tokio = { workspace = true, 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,151 @@
//! 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;
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 = { workspace = true, features=["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
tokio = { workspace = true, 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,95 @@
//! 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::*;
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 = { workspace = true, features=["hydrate"], optional = true }
dioxus = { workspace = true }
dioxus-fullstack = { workspace = true }
tokio = { workspace = true, 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,92 @@
//! 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
}));
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 = "15a4e54435eb5a539afb75891292bcccd2cc8e85", features = ["stable"] }
syn = { version = "2", 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:
/// - **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(),
syn::parse_quote!(::dioxus_fullstack::prelude::ServerFnTraitObj),
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,491 @@
//! 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")]
//! {
//! 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 FnMut(server_fn::ServerFnTraitObj<DioxusServerContext>) -> 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(server_fn::ServerFnTraitObj<DioxusServerContext>) -> 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: server_fn::ServerFnTraitObj<DioxusServerContext>,
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.call(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,556 @@
//! 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::*;
//! 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: server_fn::ServerFnTraitObj<DioxusServerContext>,
/// }
///
/// #[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(server_fn::ServerFnTraitObj<DioxusServerContext>) -> 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(server_fn::ServerFnTraitObj<DioxusServerContext>) -> 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));
#[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: server_fn::ServerFnTraitObj<DioxusServerContext>,
}
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: server_fn::ServerFnTraitObj<DioxusServerContext>,
) -> 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.call(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,443 @@
//! 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")]
//! {
//! 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, server_fn::ServerFnTraitObj<DioxusServerContext>) -> 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).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)
.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: server_fn::ServerFnTraitObj<DioxusServerContext>,
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.call(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) {
std::mem::drop(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::DioxusServerFn;
#[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,
}
}
}
}
+166
View File
@@ -0,0 +1,166 @@
use crate::server_context::DioxusServerContext;
#[cfg(any(feature = "ssr", doc))]
#[derive(Clone)]
/// A trait object for a function that be called on serializable arguments and returns a serializable result.
pub struct ServerFnTraitObj(server_fn::ServerFnTraitObj<DioxusServerContext>);
#[cfg(any(feature = "ssr", doc))]
impl std::ops::Deref for ServerFnTraitObj {
type Target = server_fn::ServerFnTraitObj<DioxusServerContext>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(any(feature = "ssr", doc))]
impl std::ops::DerefMut for ServerFnTraitObj {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg(any(feature = "ssr", doc))]
impl ServerFnTraitObj {
fn new(
prefix: &'static str,
url: &'static str,
encoding: server_fn::Encoding,
run: ServerFunction,
) -> Self {
Self(server_fn::ServerFnTraitObj::new(prefix, url, encoding, run))
}
/// Create a new `ServerFnTraitObj` from a `server_fn::ServerFnTraitObj`.
pub const fn from_generic_server_fn(
server_fn: server_fn::ServerFnTraitObj<DioxusServerContext>,
) -> Self {
Self(server_fn)
}
}
#[cfg(any(feature = "ssr", doc))]
server_fn::inventory::collect!(ServerFnTraitObj);
#[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::SerializedFnTraitObj<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, ServerFnTraitObj>>>,
> = once_cell::sync::Lazy::new(|| {
let mut map = std::collections::HashMap::new();
for server_fn in server_fn::inventory::iter::<ServerFnTraitObj> {
map.insert(server_fn.0.url(), server_fn.clone());
}
std::sync::Arc::new(std::sync::RwLock::new(map))
});
#[cfg(any(feature = "ssr", doc))]
/// The registry of all Dioxus server functions.
pub struct DioxusServerFnRegistry;
#[cfg(feature = "ssr")]
impl server_fn::ServerFunctionRegistry<DioxusServerContext> for DioxusServerFnRegistry {
type Error = ServerRegistrationFnError;
fn register_explicit(
prefix: &'static str,
url: &'static str,
server_function: ServerFunction,
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,
ServerFnTraitObj::new(prefix, url, encoding, server_function),
);
// 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(()),
}
}
fn register(
url: &'static str,
server_function: ServerFunction,
encoding: server_fn::Encoding,
) -> Result<(), Self::Error> {
Self::register_explicit("", url, server_function, encoding)
}
/// Returns the server function registered at the given URL, or `None` if no function is registered at that URL.
fn get(url: &str) -> Option<server_fn::ServerFnTraitObj<DioxusServerContext>> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).map(|inner| inner.0.clone()))
}
/// 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<server_fn::ServerFnTraitObj<DioxusServerContext>> {
Self::get(url)
}
fn get_encoding(url: &str) -> Option<server_fn::Encoding> {
REGISTERED_SERVER_FUNCTIONS
.read()
.ok()
.and_then(|fns| fns.get(url).map(|f| f.encoding()))
}
/// 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 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 DioxusServerFn: server_fn::ServerFn<DioxusServerContext> {
/// Registers the server function, allowing the client to query it by URL.
#[cfg(any(feature = "ssr", doc))]
fn register_explicit() -> Result<(), server_fn::ServerFnError> {
Self::register_in_explicit::<DioxusServerFnRegistry>()
}
}
impl<T> DioxusServerFn for T where T: server_fn::ServerFn<DioxusServerContext> {}
+7 -7
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"
@@ -12,12 +12,12 @@ keywords = ["dom", "ui", "gui", "react"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-core = { path = "../../packages/core", version = "^0.3.0" }
futures-channel = "0.3.21"
log = "0.4"
dioxus-core = { workspace = true }
futures-channel = { workspace = true }
log = { workspace = true }
[dev-dependencies]
futures-util = { version = "0.3", default-features = false }
dioxus-core = { path = "../../packages/core", version = "^0.3.0" }
dioxus = { path = "../../packages/dioxus", version = "^0.3.0" }
futures-util = { workspace = true, default-features = false }
dioxus-core = { workspace = true }
dioxus = { workspace = true }
+1 -1
View File
@@ -71,7 +71,7 @@ impl<T> ProvidedStateInner<T> {
/// let current_theme = *theme.read();
///
/// render! {
/// match &*theme.read() {
/// match current_theme {
/// Theme::Dark => {
/// "Dark mode"
/// }
+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
+4 -4
View File
@@ -12,13 +12,13 @@ keywords = ["dom", "ui", "gui", "react", "hot-reloading"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-rsx = { path = "../rsx" , version = "^0.0.3" }
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.2"}
dioxus-html = { path = "../html", features = ["hot-reload-context"], version = "^0.3.0" }
dioxus-rsx = { workspace = true }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true, features = ["hot-reload-context"] }
interprocess = { version = "1.2.1" }
notify = "5.0.0"
chrono = "0.4.23"
chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
serde_json = "1.0.91"
serde = { version = "1", features = ["derive"] }
execute = "0.2.11"
+168 -151
View File
@@ -159,173 +159,190 @@ pub fn init<Ctx: HotReloadingContext + Send + 'static>(cfg: Config<Ctx>) {
}
let file_map = Arc::new(Mutex::new(file_map));
if let Ok(local_socket_stream) = LocalSocketListener::bind("@dioxusin") {
let aborted = Arc::new(Mutex::new(false));
// listen for connections
std::thread::spawn({
let file_map = file_map.clone();
let channels = channels.clone();
let aborted = aborted.clone();
let _ = local_socket_stream.set_nonblocking(true);
move || {
loop {
if let Ok(mut connection) = local_socket_stream.accept() {
// send any templates than have changed before the socket connected
let templates: Vec<_> = {
file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if !send_msg(
HotReloadMsg::UpdateTemplate(template),
&mut connection,
) {
continue;
}
}
channels.lock().unwrap().push(connection);
if log {
println!("Connected to hot reloading 🚀");
}
}
std::thread::sleep(std::time::Duration::from_millis(10));
if *aborted.lock().unwrap() {
break;
}
}
#[cfg(target_os = "macos")]
{
// On unix, if you force quit the application, it can leave the file socket open
// This will cause the local socket listener to fail to open
// We check if the file socket is already open from an old session and then delete it
let paths = ["./dioxusin", "./@dioxusin"];
for path in paths {
let path = PathBuf::from(path);
if path.exists() {
let _ = std::fs::remove_file(path);
}
});
}
}
// watch for changes
std::thread::spawn(move || {
let mut last_update_time = chrono::Local::now().timestamp();
match LocalSocketListener::bind("@dioxusin") {
Ok(local_socket_stream) => {
let aborted = Arc::new(Mutex::new(false));
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
for path in listening_paths {
let full_path = crate_dir.join(path);
if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
if log {
println!(
"hot reloading failed to start watching {full_path:?}:\n{err:?}",
);
}
}
}
let mut rebuild = {
let aborted = aborted.clone();
// listen for connections
std::thread::spawn({
let file_map = file_map.clone();
let channels = channels.clone();
let aborted = aborted.clone();
let _ = local_socket_stream.set_nonblocking(true);
move || {
if let Some(rebuild_callback) = &mut rebuild_with {
if log {
println!("Rebuilding the application...");
}
let shutdown = rebuild_callback();
if shutdown {
*aborted.lock().unwrap() = true;
}
for channel in &mut *channels.lock().unwrap() {
send_msg(HotReloadMsg::Shutdown, channel);
}
return shutdown;
} else if log {
println!(
"Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes."
);
}
true
}
};
for evt in rx {
if chrono::Local::now().timestamp() > last_update_time {
if let Ok(evt) = evt {
let real_paths = evt
.paths
.iter()
.filter(|path| {
// skip non rust files
matches!(
path.extension().and_then(|p| p.to_str()),
Some("rs" | "toml" | "css" | "html" | "js")
) &&
// skip excluded paths
!excluded_paths.iter().any(|p| path.starts_with(p)) &&
// respect .gitignore
!gitignore
.matched_path_or_any_parents(path, false)
.is_ignore()
})
.collect::<Vec<_>>();
// Give time for the change to take effect before reading the file
if !real_paths.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(10));
}
let mut channels = channels.lock().unwrap();
for path in real_paths {
// if this file type cannot be hot reloaded, rebuild the application
if path.extension().and_then(|p| p.to_str()) != Some("rs")
&& rebuild()
{
return;
loop {
if let Ok(mut connection) = local_socket_stream.accept() {
// send any templates than have changed before the socket connected
let templates: Vec<_> = {
file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if !send_msg(
HotReloadMsg::UpdateTemplate(template),
&mut connection,
) {
continue;
}
}
// find changes to the rsx in the file
match file_map
.lock()
.unwrap()
.update_rsx(path, crate_dir.as_path())
{
Ok(UpdateResult::UpdatedRsx(msgs)) => {
for msg in msgs {
let mut i = 0;
while i < channels.len() {
let channel = &mut channels[i];
if send_msg(
HotReloadMsg::UpdateTemplate(msg),
channel,
) {
i += 1;
} else {
channels.remove(i);
channels.lock().unwrap().push(connection);
if log {
println!("Connected to hot reloading 🚀");
}
}
if *aborted.lock().unwrap() {
break;
}
}
}
});
// watch for changes
std::thread::spawn(move || {
let mut last_update_time = chrono::Local::now().timestamp();
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher =
RecommendedWatcher::new(tx, notify::Config::default()).unwrap();
for path in listening_paths {
let full_path = crate_dir.join(path);
if let Err(err) = watcher.watch(&full_path, RecursiveMode::Recursive) {
if log {
println!(
"hot reloading failed to start watching {full_path:?}:\n{err:?}",
);
}
}
}
let mut rebuild = {
let aborted = aborted.clone();
let channels = channels.clone();
move || {
if let Some(rebuild_callback) = &mut rebuild_with {
if log {
println!("Rebuilding the application...");
}
let shutdown = rebuild_callback();
if shutdown {
*aborted.lock().unwrap() = true;
}
for channel in &mut *channels.lock().unwrap() {
send_msg(HotReloadMsg::Shutdown, channel);
}
return shutdown;
} else if log {
println!(
"Rebuild needed... shutting down hot reloading.\nManually rebuild the application to view futher changes."
);
}
true
}
};
for evt in rx {
if chrono::Local::now().timestamp_millis() >= last_update_time {
if let Ok(evt) = evt {
let real_paths = evt
.paths
.iter()
.filter(|path| {
// skip non rust files
matches!(
path.extension().and_then(|p| p.to_str()),
Some("rs" | "toml" | "css" | "html" | "js")
) &&
// skip excluded paths
!excluded_paths.iter().any(|p| path.starts_with(p)) &&
// respect .gitignore
!gitignore
.matched_path_or_any_parents(path, false)
.is_ignore()
})
.collect::<Vec<_>>();
// Give time for the change to take effect before reading the file
if !real_paths.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(10));
}
let mut channels = channels.lock().unwrap();
for path in real_paths {
// if this file type cannot be hot reloaded, rebuild the application
if path.extension().and_then(|p| p.to_str()) != Some("rs")
&& rebuild()
{
return;
}
// find changes to the rsx in the file
match file_map
.lock()
.unwrap()
.update_rsx(path, crate_dir.as_path())
{
Ok(UpdateResult::UpdatedRsx(msgs)) => {
for msg in msgs {
let mut i = 0;
while i < channels.len() {
let channel = &mut channels[i];
if send_msg(
HotReloadMsg::UpdateTemplate(msg),
channel,
) {
i += 1;
} else {
channels.remove(i);
}
}
}
}
}
Ok(UpdateResult::NeedsRebuild) => {
drop(channels);
if rebuild() {
return;
Ok(UpdateResult::NeedsRebuild) => {
drop(channels);
if rebuild() {
return;
}
break;
}
break;
}
Err(err) => {
if log {
println!(
"hot reloading failed to update rsx:\n{err:?}"
);
Err(err) => {
if log {
println!(
"hot reloading failed to update rsx:\n{err:?}"
);
}
}
}
}
}
last_update_time = chrono::Local::now().timestamp_millis();
}
last_update_time = chrono::Local::now().timestamp();
}
}
});
});
}
Err(error) => println!("failed to connect to hot reloading\n{error}"),
}
}
}
+11 -3
View File
@@ -11,16 +11,18 @@ documentation = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react"]
[dependencies]
dioxus-core = { path = "../core", version = "^0.3.0" }
dioxus-rsx = { path = "../rsx", version = "^0.0.3", optional = true }
dioxus-core = { workspace = true }
dioxus-rsx = { workspace = true, optional = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_repr = { version = "0.1", optional = true }
wasm-bindgen = { version = "0.2.79", optional = true }
wasm-bindgen = { workspace = true, optional = true }
euclid = "0.22.7"
enumset = "1.0.11"
keyboard-types = "0.6.2"
async-trait = "0.1.58"
serde-value = "0.7.0"
tokio = { workspace = true, 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"]
+46 -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.
@@ -1121,6 +1154,7 @@ builder_constructors! {
r#type: InputType "type",
// value: String,
value: String volatile,
initial_value: String DEFAULT,
};
/// Build a
@@ -1594,7 +1628,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 {}
+7 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-interpreter-js"
version = "0.3.1"
version = "0.3.3"
edition = "2018"
authors = ["Jonathan Kelley"]
description = "JS Intepreter for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
@@ -10,17 +10,19 @@ 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 }
wasm-bindgen = { workspace = true, 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"]
minimal_bindings = []
+5 -1
View File
@@ -6,10 +6,14 @@ use web_sys::Element;
#[wasm_bindgen(module = "/src/interpreter.js")]
extern "C" {
pub type InterpreterConfig;
#[wasm_bindgen(constructor)]
pub fn new(intercept_link_redirects: bool) -> InterpreterConfig;
pub type Interpreter;
#[wasm_bindgen(constructor)]
pub fn new(arg: Element) -> Interpreter;
pub fn new(arg: Element, config: InterpreterConfig) -> Interpreter;
#[wasm_bindgen(method)]
pub fn SaveTemplate(this: &Interpreter, template: JsValue);
+68
View File
@@ -0,0 +1,68 @@
const bool_attrs = {
allowfullscreen: true,
allowpaymentrequest: true,
async: true,
autofocus: true,
autoplay: true,
checked: true,
controls: true,
default: true,
defer: true,
disabled: true,
formnovalidate: true,
hidden: true,
ismap: true,
itemscope: true,
loop: true,
multiple: true,
muted: true,
nomodule: true,
novalidate: true,
open: true,
playsinline: true,
readonly: true,
required: true,
reversed: true,
selected: true,
truespeed: true,
};
export function setAttributeInner(node, field, value, ns) {
const name = field;
if (ns === "style") {
// ????? why do we need to do this
if (node.style === undefined) {
node.style = {};
}
node.style[name] = value;
} else if (ns != null && ns != undefined) {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
case "value":
if (value !== node.value) {
node.value = value;
}
break;
case "initial_value":
node.defaultValue = value;
break;
case "checked":
node.checked = value === "true" || value === true;
break;
case "selected":
node.selected = value === "true" || value === true;
break;
case "dangerous_inner_html":
node.innerHTML = value;
break;
default:
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
} else {
node.setAttribute(name, value);
}
}
}
}
+180 -168
View File
@@ -1,3 +1,5 @@
import { setAttributeInner } from "./common.js";
class ListenerMap {
constructor(root) {
// bubbling events can listen at the root element
@@ -17,8 +19,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 +33,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) {
@@ -52,8 +55,15 @@ class ListenerMap {
}
}
class InterpreterConfig {
constructor(intercept_link_redirects) {
this.intercept_link_redirects = intercept_link_redirects;
}
}
class Interpreter {
constructor(root) {
constructor(root, config) {
this.config = config;
this.root = root;
this.listeners = new ListenerMap(root);
this.nodes = [root];
@@ -143,46 +153,9 @@ class Interpreter {
SetAttribute(id, field, value, ns) {
if (value === null) {
this.RemoveAttribute(id, field, ns);
}
else {
const node = this.nodes[id];
this.SetAttributeInner(node, field, value, ns);
}
}
SetAttributeInner(node, field, value, ns) {
const name = field;
if (ns === "style") {
// ????? why do we need to do this
if (node.style === undefined) {
node.style = {};
}
node.style[name] = value;
} else if (ns != null && ns != undefined) {
node.setAttributeNS(ns, name, value);
} else {
switch (name) {
case "value":
if (value !== node.value) {
node.value = value;
}
break;
case "checked":
node.checked = value === "true";
break;
case "selected":
node.selected = value === "true";
break;
case "dangerous_inner_html":
node.innerHTML = value;
break;
default:
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if (value === "false" && bool_attrs.hasOwnProperty(name)) {
node.removeAttribute(name);
} else {
node.setAttribute(name, value);
}
}
const node = this.nodes[id];
setAttributeInner(node, field, value, ns);
}
}
RemoveAttribute(root, field, ns) {
@@ -204,6 +177,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 +224,8 @@ class Interpreter {
for (let edit of edits.edits) {
this.handleEdit(edit);
}
/*POST_HANDLE_EDITS*/
}
SaveTemplate(template) {
@@ -243,7 +257,7 @@ class Interpreter {
for (let attr of node.attrs) {
if (attr.type == "Static") {
this.SetAttributeInner(el, attr.name, attr.value, attr.namespace);
setAttributeInner(el, attr.name, attr.value, attr.namespace);
}
}
@@ -342,107 +356,131 @@ 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, this.config);
});
}
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, config) {
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
if (config.intercept_link_redirects) {
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,
@@ -722,34 +760,6 @@ function serialize_event(event) {
function serializeIpcMessage(method, params = {}) {
return JSON.stringify({ method, params });
}
const bool_attrs = {
allowfullscreen: true,
allowpaymentrequest: true,
async: true,
autofocus: true,
autoplay: true,
checked: true,
controls: true,
default: true,
defer: true,
disabled: true,
formnovalidate: true,
hidden: true,
ismap: true,
itemscope: true,
loop: true,
multiple: true,
muted: true,
nomodule: true,
novalidate: true,
open: true,
playsinline: true,
readonly: true,
required: true,
reversed: true,
selected: true,
truespeed: true,
};
function is_element_node(node) {
return node.nodeType == 1;
@@ -921,6 +931,8 @@ function event_bubbles(event) {
return true;
case "toggle":
return true;
case "mounted":
return false;
}
return true;
+11
View File
@@ -1,4 +1,5 @@
pub static INTERPRETER_JS: &str = include_str!("./interpreter.js");
pub static COMMON_JS: &str = include_str!("./common.js");
#[cfg(feature = "sledgehammer")]
mod sledgehammer_bindings;
@@ -10,3 +11,13 @@ mod bindings;
#[cfg(feature = "web")]
pub use bindings::Interpreter;
// Common bindings for minimal usage.
#[cfg(feature = "minimal_bindings")]
pub mod minimal_bindings {
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
#[wasm_bindgen(module = "/src/common.js")]
extern "C" {
pub fn setAttributeInner(node: JsValue, name: &str, value: JsValue, ns: Option<&str>);
}
}
@@ -74,6 +74,9 @@ mod js {
node.value = value;
}
break;
case "initial_value":
node.defaultValue = value;
break;
case "checked":
node.checked = value === "true";
break;
@@ -108,6 +111,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 +120,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 +173,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);
}
+14 -11
View File
@@ -1,6 +1,6 @@
[package]
name = "dioxus-liveview"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
@@ -14,19 +14,21 @@ license = "MIT/Apache-2.0"
[dependencies]
thiserror = "1.0.38"
futures-util = { version = "0.3.25", default-features = false, features = [
log = { workspace = true }
slab = { workspace = true }
futures-util = { workspace = true, default-features = false, features = [
"sink",
] }
futures-channel = { version = "0.3.25", features = ["sink"] }
tokio = { version = "1.22.0", features = ["time"] }
futures-channel = { workspace = true }
tokio = { workspace = true, 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"] }
serde_json = "1.0.91"
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.3.0" }
dioxus-core = { path = "../core", features = ["serialize"], version = "^0.3.0" }
dioxus-interpreter-js = { path = "../interpreter", version = "0.3.0" }
dioxus-hot-reload = { path = "../hot-reload", optional = true }
dioxus-html = { workspace = true, features = ["serialize"] }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-interpreter-js = { workspace = true }
dioxus-hot-reload = { workspace = true, optional = true }
# warp
warp = { version = "0.3.3", optional = true }
@@ -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 }
@@ -43,9 +46,9 @@ salvo = { version = "0.37.7", optional = true, features = ["ws"] }
# actix-ws = { version = "0.2.5", optional = true }
[dev-dependencies]
pretty_env_logger = { version = "0.4.0" }
tokio = { version = "1.22.0", features = ["full"] }
dioxus = { path = "../dioxus", version = "0.3.0" }
pretty_env_logger = { version = "0.5.0" }
tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true }
warp = "0.3.3"
axum = { version = "0.6.1", features = ["ws"] }
salvo = { version = "0.37.7", features = ["affix", "ws"] }
+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 {}
Loaded 100 of 207 files, more files were not shown because too many files have changed in this diff. Show more