diff --git a/benches/tui_update.rs b/benches/tui_update.rs index 5a3a70b66..d46921a8d 100644 --- a/benches/tui_update.rs +++ b/benches/tui_update.rs @@ -17,62 +17,14 @@ fn tui_update(c: &mut Criterion) { &size, |b, size| { b.iter(|| match size { - 1 => dioxus::tui::launch_cfg( - app3, - Config { - headless: true, - ..Default::default() - }, - ), - 2 => dioxus::tui::launch_cfg( - app6, - Config { - headless: true, - ..Default::default() - }, - ), - 3 => dioxus::tui::launch_cfg( - app9, - Config { - headless: true, - ..Default::default() - }, - ), - 4 => dioxus::tui::launch_cfg( - app12, - Config { - headless: true, - ..Default::default() - }, - ), - 5 => dioxus::tui::launch_cfg( - app15, - Config { - headless: true, - ..Default::default() - }, - ), - 6 => dioxus::tui::launch_cfg( - app18, - Config { - headless: true, - ..Default::default() - }, - ), - 7 => dioxus::tui::launch_cfg( - app21, - Config { - headless: true, - ..Default::default() - }, - ), - 8 => dioxus::tui::launch_cfg( - app24, - Config { - headless: true, - ..Default::default() - }, - ), + 1 => dioxus::tui::launch_cfg(app3, Config::default().with_headless()), + 2 => dioxus::tui::launch_cfg(app6, Config::default().with_headless()), + 3 => dioxus::tui::launch_cfg(app9, Config::default().with_headless()), + 4 => dioxus::tui::launch_cfg(app12, Config::default().with_headless()), + 5 => dioxus::tui::launch_cfg(app15, Config::default().with_headless()), + 6 => dioxus::tui::launch_cfg(app18, Config::default().with_headless()), + 7 => dioxus::tui::launch_cfg(app21, Config::default().with_headless()), + 8 => dioxus::tui::launch_cfg(app24, Config::default().with_headless()), _ => (), }) }, diff --git a/examples/custom_html.rs b/examples/custom_html.rs new file mode 100644 index 000000000..02f603710 --- /dev/null +++ b/examples/custom_html.rs @@ -0,0 +1,37 @@ +//! This example shows how to use a custom index.html and custom extensions +//! to add things like stylesheets, scripts, and third-party JS libraries. + +use dioxus::prelude::*; + +fn main() { + dioxus::desktop::launch_cfg(app, |c| { + c.with_custom_head("".into()) + }); + + dioxus::desktop::launch_cfg(app, |c| { + c.with_custom_index( + r#" + + + + Dioxus app + + + + +
+ + + "# + .into(), + ) + }); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 {"hello world!"} + } + }) +} diff --git a/examples/tailwind.rs b/examples/tailwind.rs index 6611fc47b..e0cb2339a 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind.rs @@ -11,12 +11,13 @@ use dioxus::prelude::*; fn main() { - dioxus::desktop::launch(app); + dioxus::desktop::launch_cfg(app, |c| { + c.with_custom_head("".to_string()) + }); } pub fn app(cx: Scope) -> Element { cx.render(rsx!( - link { href:"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", rel:"stylesheet" } div { header { class: "text-gray-400 bg-gray-900 body-font", div { class: "container mx-auto flex flex-wrap p-5 flex-col md:flex-row items-center", diff --git a/examples/tui_color_test.rs b/examples/tui_color_test.rs index d9ebad275..1a5a67457 100644 --- a/examples/tui_color_test.rs +++ b/examples/tui_color_test.rs @@ -3,10 +3,7 @@ use dioxus::prelude::*; fn main() { dioxus::tui::launch_cfg( app, - dioxus::tui::Config { - rendering_mode: dioxus::tui::RenderingMode::Ansi, - ..Default::default() - }, + dioxus::tui::Config::default().with_rendering_mode(dioxus::tui::RenderingMode::Ansi), ); } diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 6028706bc..84ac3a72a 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -21,6 +21,8 @@ pub struct DesktopConfig { pub(crate) event_handler: Option>, pub(crate) disable_context_menu: bool, pub(crate) resource_dir: Option, + pub(crate) custom_head: Option, + pub(crate) custom_index: Option, } pub(crate) type WryProtocol = ( @@ -42,6 +44,8 @@ impl DesktopConfig { pre_rendered: None, disable_context_menu: !cfg!(debug_assertions), resource_dir: None, + custom_head: None, + custom_index: None, } } @@ -100,10 +104,30 @@ impl DesktopConfig { self } + /// Add a custom icon for this application pub fn with_icon(&mut self, icon: Icon) -> &mut Self { self.window.window.window_icon = Some(icon); self } + + /// Inject additional content into the document's HEAD. + /// + /// This is useful for loading CSS libraries, JS libraries, etc. + pub fn with_custom_head(&mut self, head: String) -> &mut Self { + self.custom_head = Some(head); + self + } + + /// Use a custom index.html instead of the default Dioxus one. + /// + /// Make sure your index.html is valid HTML. + /// + /// Dioxus injects some loader code into the closing body tag. Your document + /// must include a body element! + pub fn with_custom_index(&mut self, index: String) -> &mut Self { + self.custom_index = Some(index); + self + } } impl DesktopConfig { diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 4ad2ff3a7..155f7af1b 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -201,7 +201,7 @@ pub(super) fn handler( /// Get a closure that executes any JavaScript in the WebView context. pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) { - let desktop = use_window(&cx).clone(); + let desktop = use_window(cx).clone(); cx.use_hook(|_| move |script| desktop.eval(script)) } diff --git a/packages/desktop/src/index.html b/packages/desktop/src/index.html index 292a1ae6a..0704fd0f0 100644 --- a/packages/desktop/src/index.html +++ b/packages/desktop/src/index.html @@ -3,13 +3,10 @@ Dioxus app +
- + diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 596956c53..1e3898f05 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -125,8 +125,9 @@ pub fn launch_with_props( let proxy = proxy.clone(); let file_handler = cfg.file_drop_handler.take(); - + let custom_head = cfg.custom_head.clone(); let resource_dir = cfg.resource_dir.clone(); + let index_file = cfg.custom_index.clone(); let mut webview = WebViewBuilder::new(window) .unwrap() @@ -164,7 +165,12 @@ pub fn launch_with_props( }); }) .with_custom_protocol(String::from("dioxus"), move |r| { - protocol::desktop_handler(r, resource_dir.clone()) + protocol::desktop_handler( + r, + resource_dir.clone(), + custom_head.clone(), + index_file.clone(), + ) }) .with_file_drop_handler(move |window, evet| { file_handler @@ -183,12 +189,10 @@ pub fn launch_with_props( r#" if (document.addEventListener) { document.addEventListener('contextmenu', function(e) { - alert("You've tried to open context menu"); e.preventDefault(); }, false); } else { document.attachEvent('oncontextmenu', function() { - alert("You've tried to open context menu"); window.event.returnValue = false; }); } diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index bd8bb3d84..085c3c32d 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -4,7 +4,20 @@ use wry::{ Result, }; -pub(super) fn desktop_handler(request: &Request, asset_root: Option) -> Result { +const MODULE_LOADER: &str = r#" + +"#; + +pub(super) fn desktop_handler( + request: &Request, + asset_root: Option, + custom_head: Option, + custom_index: Option, +) -> Result { // Any content that uses the `dioxus://` scheme will be shuttled through this handler as a "special case". // For now, we only serve two pieces of content which get included as bytes into the final binary. let path = request.uri().replace("dioxus://", ""); @@ -13,9 +26,25 @@ pub(super) fn desktop_handler(request: &Request, asset_root: Option) -> let trimmed = path.trim_start_matches("index.html/"); if trimmed.is_empty() { - ResponseBuilder::new() - .mimetype("text/html") - .body(include_bytes!("./index.html").to_vec()) + // If a custom index is provided, just defer to that, expecting the user to know what they're doing. + // we'll look for the closing tag and insert our little module loader there. + if let Some(custom_index) = custom_index { + let rendered = custom_index + .replace("", &format!("{}", MODULE_LOADER)) + .into_bytes(); + ResponseBuilder::new().mimetype("text/html").body(rendered) + } else { + // Otherwise, we'll serve the default index.html and apply a custom head if that's specified. + let mut template = include_str!("./index.html").to_string(); + if let Some(custom_head) = custom_head { + template = template.replace("", &custom_head); + } + template = template.replace("", MODULE_LOADER); + + ResponseBuilder::new() + .mimetype("text/html") + .body(template.into_bytes()) + } } else if trimmed == "index.js" { ResponseBuilder::new() .mimetype("text/javascript") diff --git a/packages/fermi/src/hooks/state.rs b/packages/fermi/src/hooks/state.rs new file mode 100644 index 000000000..9db9a9fdf --- /dev/null +++ b/packages/fermi/src/hooks/state.rs @@ -0,0 +1,420 @@ +use crate::{AtomId, AtomRoot, Writable}; +use dioxus_core::{ScopeId, ScopeState}; +use std::{ + cell::RefMut, + fmt::{Debug, Display}, + ops::{Add, Div, Mul, Not, Sub}, + rc::Rc, +}; + +/// Store state between component renders. +/// +/// ## Dioxus equivalent of AtomState, designed for Rust +/// +/// The Dioxus version of `AtomState` for state management inside components. It allows you to ergonomically store and +/// modify state between component renders. When the state is updated, the component will re-render. +/// +/// +/// ```ignore +/// static COUNT: Atom = |_| 0; +/// +/// fn Example(cx: Scope) -> Element { +/// let mut count = use_atom_state(&cx, COUNT); +/// +/// cx.render(rsx! { +/// div { +/// h1 { "Count: {count}" } +/// button { onclick: move |_| count += 1, "Increment" } +/// button { onclick: move |_| count -= 1, "Decrement" } +/// } +/// )) +/// } +/// ``` +pub fn use_atom_state<'a, T: 'static>(cx: &'a ScopeState, f: impl Writable) -> &'a AtomState { + let root = crate::use_atom_root(cx); + + let inner = cx.use_hook(|_| AtomState { + value: None, + root: root.clone(), + scope_id: cx.scope_id(), + id: f.unique_id(), + }); + + inner.value = Some(inner.root.register(f, cx.scope_id())); + + inner +} + +pub struct AtomState { + root: Rc, + id: AtomId, + scope_id: ScopeId, + value: Option>, +} + +impl Drop for AtomState { + fn drop(&mut self) { + self.root.unsubscribe(self.id, self.scope_id) + } +} + +impl AtomState { + /// Set the state to a new value. + pub fn set(&self, new: T) { + self.root.set(self.id, new) + } + + /// Get the current value of the state by cloning its container Rc. + /// + /// This is useful when you are dealing with state in async contexts but need + /// to know the current value. You are not given a reference to the state. + /// + /// # Examples + /// An async context might need to know the current value: + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let count = use_state(&cx, || 0); + /// cx.spawn({ + /// let set_count = count.to_owned(); + /// async move { + /// let current = set_count.current(); + /// } + /// }) + /// } + /// ``` + #[must_use] + pub fn current(&self) -> Rc { + self.value.as_ref().unwrap().clone() + } + + /// Get the `setter` function directly without the `AtomState` wrapper. + /// + /// This is useful for passing the setter function to other components. + /// + /// However, for most cases, calling `to_owned` o`AtomState`te is the + /// preferred way to get "anoth`set_state`tate handle. + /// + /// + /// # Examples + /// A component might require an `Rc` as an input to set a value. + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let value = use_state(&cx, || 0); + /// + /// rsx!{ + /// Component { + /// handler: value.setter() + /// } + /// } + /// } + /// ``` + #[must_use] + pub fn setter(&self) -> Rc { + let root = self.root.clone(); + let id = self.id; + Rc::new(move |new_val| root.set(id, new_val)) + } + + /// Set the state to a new value, using the current state value as a reference. + /// + /// This is similar to passing a closure to React's `set_value` function. + /// + /// # Examples + /// + /// Basic usage: + /// ```rust + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let value = use_state(&cx, || 0); + /// + /// // to increment the value + /// value.modify(|v| v + 1); + /// + /// // usage in async + /// cx.spawn({ + /// let value = value.to_owned(); + /// async move { + /// value.modify(|v| v + 1); + /// } + /// }); + /// + /// # todo!() + /// } + /// ``` + pub fn modify(&self, f: impl FnOnce(&T) -> T) { + self.root.clone().set(self.id, { + let current = self.value.as_ref().unwrap(); + f(current.as_ref()) + }); + } + + /// Get the value of the state when this handle was created. + /// + /// This method is useful when you want an `Rc` around the data to cheaply + /// pass it around your app. + /// + /// ## Warning + /// + /// This will return a stale value if used within async contexts. + /// + /// Try `current` to get the real current value of the state. + /// + /// ## Example + /// + /// ```rust, ignore + /// # use dioxus_core::prelude::*; + /// # use dioxus_hooks::*; + /// fn component(cx: Scope) -> Element { + /// let value = use_state(&cx, || 0); + /// + /// let as_rc = value.get(); + /// assert_eq!(as_rc.as_ref(), &0); + /// + /// # todo!() + /// } + /// ``` + #[must_use] + pub fn get(&self) -> &T { + self.value.as_ref().unwrap() + } + + #[must_use] + pub fn get_rc(&self) -> &Rc { + self.value.as_ref().unwrap() + } + + /// Mark all consumers of this atom to re-render + /// + /// ```rust, ignore + /// fn component(cx: Scope) -> Element { + /// let count = use_state(&cx, || 0); + /// cx.spawn({ + /// let count = count.to_owned(); + /// async move { + /// // for the component to re-render + /// count.needs_update(); + /// } + /// }) + /// } + /// ``` + pub fn needs_update(&self) { + self.root.force_update(self.id) + } +} + +impl AtomState { + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// If you are comfortable dealing with `RefMut`, then you can use `make_mut` to get + /// the underlying slot. However, be careful with `RefMut` since you might panic + /// if the `RefCell` is left open. + /// + /// # Examples + /// + /// ``` + /// let val = use_state(&cx, || 0); + /// + /// val.with_mut(|v| *v = 1); + /// ``` + pub fn with_mut(&self, apply: impl FnOnce(&mut T)) { + let mut new_val = self.value.as_ref().unwrap().as_ref().to_owned(); + apply(&mut new_val); + self.set(new_val); + } + + /// Get a mutable handle to the value by calling `ToOwned::to_owned` on the + /// current value. + /// + /// This is essentially cloning the underlying value and then setting it, + /// giving you a mutable handle in the process. This method is intended for + /// types that are cheaply cloneable. + /// + /// # Warning + /// Be careful with `RefMut` since you might panic if the `RefCell` is left open! + /// + /// # Examples + /// + /// ``` + /// let val = use_state(&cx, || 0); + /// + /// *val.make_mut() += 1; + /// ``` + #[must_use] + pub fn make_mut(&self) -> RefMut { + todo!("make mut not support for atom values yet") + // let mut slot = self.value.as_ref().unwrap(); + + // self.needs_update(); + + // if Rc::strong_count(&*slot) > 0 { + // *slot = Rc::new(slot.as_ref().to_owned()); + // } + + // RefMut::map(slot, |rc| Rc::get_mut(rc).expect("the hard count to be 0")) + } + + /// Convert this handle to a tuple of the value and the handle itself. + #[must_use] + pub fn split(&self) -> (&T, &Self) { + (self.value.as_ref().unwrap(), self) + } +} + +impl Clone for AtomState { + fn clone(&self) -> Self { + AtomState { + root: self.root.clone(), + id: self.id, + scope_id: self.scope_id, + value: self.value.clone(), + } + } +} + +impl<'a, T: 'static + Display> std::fmt::Display for AtomState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.value.as_ref().unwrap()) + } +} + +impl<'a, T: std::fmt::Binary> std::fmt::Binary for AtomState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:b}", self.value.as_ref().unwrap().as_ref()) + } +} + +impl PartialEq for AtomState { + fn eq(&self, other: &T) -> bool { + self.value.as_ref().unwrap().as_ref() == other + } +} + +// todo: this but for more interesting conrete types +impl PartialEq for &AtomState { + fn eq(&self, other: &bool) -> bool { + self.value.as_ref().unwrap().as_ref() == other + } +} + +impl PartialEq> for AtomState { + fn eq(&self, other: &AtomState) -> bool { + Rc::ptr_eq(self.value.as_ref().unwrap(), other.value.as_ref().unwrap()) + } +} + +impl Debug for AtomState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.value.as_ref().unwrap()) + } +} + +impl<'a, T> std::ops::Deref for AtomState { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.value.as_ref().unwrap().as_ref() + } +} + +impl std::ops::Not for &AtomState { + type Output = ::Output; + + fn not(self) -> Self::Output { + self.value.as_ref().unwrap().not() + } +} + +impl std::ops::Not for AtomState { + type Output = ::Output; + + fn not(self) -> Self::Output { + self.value.as_ref().unwrap().not() + } +} + +impl std::ops::Add for &AtomState { + type Output = ::Output; + + fn add(self, other: T) -> Self::Output { + *self.value.as_ref().unwrap().as_ref() + other + } +} +impl std::ops::Sub for &AtomState { + type Output = ::Output; + + fn sub(self, other: T) -> Self::Output { + *self.value.as_ref().unwrap().as_ref() - other + } +} + +impl std::ops::Div for &AtomState { + type Output = ::Output; + + fn div(self, other: T) -> Self::Output { + *self.value.as_ref().unwrap().as_ref() / other + } +} + +impl std::ops::Mul for &AtomState { + type Output = ::Output; + + fn mul(self, other: T) -> Self::Output { + *self.value.as_ref().unwrap().as_ref() * other + } +} + +impl + Copy> std::ops::AddAssign for &AtomState { + fn add_assign(&mut self, rhs: T) { + self.set((*self.current()) + rhs); + } +} + +impl + Copy> std::ops::SubAssign for &AtomState { + fn sub_assign(&mut self, rhs: T) { + self.set((*self.current()) - rhs); + } +} + +impl + Copy> std::ops::MulAssign for &AtomState { + fn mul_assign(&mut self, rhs: T) { + self.set((*self.current()) * rhs); + } +} + +impl + Copy> std::ops::DivAssign for &AtomState { + fn div_assign(&mut self, rhs: T) { + self.set((*self.current()) / rhs); + } +} + +impl + Copy> std::ops::AddAssign for AtomState { + fn add_assign(&mut self, rhs: T) { + self.set((*self.current()) + rhs); + } +} + +impl + Copy> std::ops::SubAssign for AtomState { + fn sub_assign(&mut self, rhs: T) { + self.set((*self.current()) - rhs); + } +} + +impl + Copy> std::ops::MulAssign for AtomState { + fn mul_assign(&mut self, rhs: T) { + self.set((*self.current()) * rhs); + } +} + +impl + Copy> std::ops::DivAssign for AtomState { + fn div_assign(&mut self, rhs: T) { + self.set((*self.current()) / rhs); + } +} diff --git a/packages/fermi/src/lib.rs b/packages/fermi/src/lib.rs index f8a835db2..4943e38a3 100644 --- a/packages/fermi/src/lib.rs +++ b/packages/fermi/src/lib.rs @@ -30,11 +30,13 @@ pub mod hooks { mod init_atom_root; mod read; mod set; + mod state; pub use atom_ref::*; pub use atom_root::*; pub use init_atom_root::*; pub use read::*; pub use set::*; + pub use state::*; } /// All Atoms are `Readable` - they support reading their value. diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs index d80b176dc..c7c234ddb 100644 --- a/packages/fermi/src/root.rs +++ b/packages/fermi/src/root.rs @@ -39,8 +39,6 @@ impl AtomRoot { } pub fn register(&self, f: impl Readable, scope: ScopeId) -> Rc { - log::trace!("registering atom {:?}", f.unique_id()); - let mut atoms = self.atoms.borrow_mut(); // initialize the value if it's not already initialized @@ -97,7 +95,22 @@ impl AtomRoot { } } - pub fn read(&self, _f: impl Readable) -> &V { - todo!() + pub fn read(&self, f: impl Readable) -> Rc { + let mut atoms = self.atoms.borrow_mut(); + + // initialize the value if it's not already initialized + if let Some(slot) = atoms.get_mut(&f.unique_id()) { + slot.value.clone().downcast().unwrap() + } else { + let value = Rc::new(f.init()); + atoms.insert( + f.unique_id(), + Slot { + value: value.clone(), + subscribers: HashSet::new(), + }, + ); + value + } } } diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index c6b1cb7e0..ad4f66b7b 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -428,6 +428,11 @@ builder_constructors! { /// element. mark {}; + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu) + /// element. + menu {}; + /// Build a /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q) /// element. diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index d564fbbec..b43766dc1 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -611,7 +611,7 @@ pub mod on { feature = "serialize", derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr) )] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] #[repr(u8)] pub enum KeyCode { // That key has no keycode, = 0 diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 176e78d51..313892f45 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -30,5 +30,16 @@ dioxus-core = { path = "../core", features = ["serialize"] } # warp warp = { version = "0.3", optional = true } +# axum +axum = { version = "0.5.1", optional = true, features = ["ws"] } +tower = { version = "0.4.12", optional = true } + +[dev-dependencies] +tokio = { version = "1", features = ["full"] } +dioxus = { path = "../../" } +warp = "0.3" +axum = { version = "0.5.1", features = ["ws"] } +tower = "0.4.12" + [features] -default = [] +default = [] \ No newline at end of file diff --git a/packages/liveview/examples/axum.rs b/packages/liveview/examples/axum.rs new file mode 100644 index 000000000..3bcdee8c2 --- /dev/null +++ b/packages/liveview/examples/axum.rs @@ -0,0 +1,38 @@ +use axum::{ + extract::ws::WebSocketUpgrade, response::Html, response::IntoResponse, routing::get, Extension, + Router, +}; +use dioxus_core::{Element, LazyNodes, Scope}; +use dioxus_liveview::Liveview; + +#[tokio::main] +async fn main() { + #[cfg(feature = "axum")] + { + pretty_env_logger::init(); + + let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); + + let view = dioxus_liveview::new(addr); + let body = view.body("Dioxus Liveview"); + + let app = Router::new() + .route("/", get(move || async { Html(body) })) + .route( + "/app", + get(move |ws: WebSocketUpgrade| async move { + ws.on_upgrade(move |socket| async move { + view.upgrade(socket, app).await; + }) + }), + ); + axum::Server::bind(&addr.to_string().parse().unwrap()) + .serve(app.into_make_service()) + .await + .unwrap(); + } +} + +fn app(cx: Scope) -> Element { + cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) +} diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index a02a5231a..2c14d318b 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "warp")] - use dioxus_core::{Element, LazyNodes, Scope}; use dioxus_liveview as liveview; use warp::ws::Ws; @@ -7,26 +5,28 @@ use warp::Filter; #[tokio::main] async fn main() { - pretty_env_logger::init(); + #[cfg(feature = "warp")] + { + pretty_env_logger::init(); - let addr = ([127, 0, 0, 1], 3030); + let addr = ([127, 0, 0, 1], 3030); - // todo: compactify this routing under one liveview::app method - let view = liveview::new(addr); - let body = view.body(); + // todo: compactify this routing under one liveview::app method + let view = liveview::new(addr); + let body = view.body("Dioxus LiveView"); - let routes = warp::path::end() - .map(move || warp::reply::html(body.clone())) - .or(warp::path("app") - .and(warp::ws()) - .and(warp::any().map(move || view.clone())) - .map(|ws: Ws, view: liveview::Liveview| { - ws.on_upgrade(|socket| async move { - view.upgrade(socket, app).await; - }) - })); - - warp::serve(routes).run(addr).await; + let routes = warp::path::end() + .map(move || warp::reply::html(body.clone())) + .or(warp::path("app") + .and(warp::ws()) + .and(warp::any().map(move || view.clone())) + .map(|ws: Ws, view: liveview::Liveview| { + ws.on_upgrade(|socket| async move { + view.upgrade(socket, app).await; + }) + })); + warp::serve(routes).run(addr).await; + } } fn app(cx: Scope) -> Element { diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index 8b1378917..d85b6da68 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -1 +1,77 @@ +use crate::{events, Liveview}; +use axum::extract::ws::{Message, WebSocket}; +use dioxus_core::prelude::*; +use futures_util::{ + future::{select, Either}, + pin_mut, SinkExt, StreamExt, +}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; +use tokio_util::task::LocalPoolHandle; +#[cfg(feature = "axum")] +impl crate::Liveview { + pub async fn upgrade(&self, ws: WebSocket, app: fn(Scope) -> Element) { + connect(ws, self.pool.clone(), app).await; + } +} + +pub async fn connect(socket: WebSocket, pool: LocalPoolHandle, app: fn(Scope) -> Element) { + let (mut user_ws_tx, mut user_ws_rx) = socket.split(); + let (event_tx, event_rx) = mpsc::unbounded_channel(); + let (edits_tx, edits_rx) = mpsc::unbounded_channel(); + let mut edits_rx = UnboundedReceiverStream::new(edits_rx); + let mut event_rx = UnboundedReceiverStream::new(event_rx); + let vdom_fut = pool.clone().spawn_pinned(move || async move { + let mut vdom = VirtualDom::new(app); + let edits = vdom.rebuild(); + let serialized = serde_json::to_string(&edits.edits).unwrap(); + edits_tx.send(serialized).unwrap(); + loop { + let new_event = { + let vdom_fut = vdom.wait_for_work(); + pin_mut!(vdom_fut); + match select(event_rx.next(), vdom_fut).await { + Either::Left((l, _)) => l, + Either::Right((_, _)) => None, + } + }; + if let Some(new_event) = new_event { + vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); + } else { + let mutations = vdom.work_with_deadline(|| false); + for mutation in mutations { + let edits = serde_json::to_string(&mutation.edits).unwrap(); + edits_tx.send(edits).unwrap(); + } + } + } + }); + loop { + match select(user_ws_rx.next(), edits_rx.next()).await { + Either::Left((l, _)) => { + if let Some(Ok(msg)) = l { + if let Ok(Some(msg)) = msg.to_text().map(events::parse_ipc_message) { + let user_event = events::trigger_from_serialized(msg.params); + event_tx.send(user_event).unwrap(); + } else { + break; + } + } else { + break; + } + } + Either::Right((edits, _)) => { + if let Some(edits) = edits { + // send the edits to the client + if user_ws_tx.send(Message::Text(edits)).await.is_err() { + break; + } + } else { + break; + } + } + } + } + vdom_fut.abort(); +} diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index 948cffe5b..5bdcf671f 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -6,6 +6,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_util::task::LocalPoolHandle; use warp::ws::{Message, WebSocket}; +#[cfg(feature = "warp")] impl crate::Liveview { pub async fn upgrade(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) { connect(ws, self.pool.clone(), app).await; @@ -65,8 +66,10 @@ pub async fn connect(ws: WebSocket, pool: LocalPoolHandle, app: fn(Scope) -> Ele Either::Left((l, _)) => { if let Some(Ok(msg)) = l { if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); + if msg.method == "user_event" { + let user_event = events::trigger_from_serialized(msg.params); + event_tx.send(user_event).unwrap(); + } } else { break; } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 340a3e4ea..f3ad90511 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -31,14 +31,13 @@ pub struct Liveview { } impl Liveview { - pub fn body(&self) -> String { + pub fn body(&self, header: &str) -> String { format!( r#" - Dioxus app - + {header}
diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index cfb1b0447..a37c6f102 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -51,7 +51,7 @@ impl UseRoute { #[cfg(feature = "query")] pub fn query(&self) -> Option { let query = self.url().query()?; - serde_urlencoded::from_str(query.strip_prefix('?').unwrap_or("")).ok() + serde_urlencoded::from_str(query).ok() } /// Get the first query parameter given the parameter name. diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs index 6e839d8a7..fc2fb3da5 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -1,11 +1,39 @@ #[derive(Clone, Copy)] +#[non_exhaustive] pub struct Config { - pub rendering_mode: RenderingMode, + pub(crate) rendering_mode: RenderingMode, /// Controls if the terminal quit when the user presses `ctrl+c`? /// To handle quiting on your own, use the [crate::TuiContext] root context. - pub ctrl_c_quit: bool, + pub(crate) ctrl_c_quit: bool, /// Controls if the terminal should dislay anything, usefull for testing. - pub headless: bool, + pub(crate) headless: bool, +} + +impl Config { + pub fn new() -> Self { + Self::default() + } + + pub fn with_rendering_mode(self, rendering_mode: RenderingMode) -> Self { + Self { + rendering_mode, + ..self + } + } + + pub fn with_ctrl_c_quit(self) -> Self { + Self { + ctrl_c_quit: true, + ..self + } + } + + pub fn with_headless(self) -> Self { + Self { + headless: true, + ..self + } + } } impl Default for Config {