From 4518b6bc8cfbe246d0844b2a32f11f437115d00d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 5 Apr 2022 21:36:48 -0400 Subject: [PATCH 01/21] feat: add better use_state for fermi --- packages/fermi/src/hooks/state.rs | 422 ++++++++++++++++++++++++++++++ packages/fermi/src/lib.rs | 2 + packages/fermi/src/root.rs | 2 - 3 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 packages/fermi/src/hooks/state.rs diff --git a/packages/fermi/src/hooks/state.rs b/packages/fermi/src/hooks/state.rs new file mode 100644 index 000000000..ab035f152 --- /dev/null +++ b/packages/fermi/src/hooks/state.rs @@ -0,0 +1,422 @@ +use crate::{use_atom_root, AtomId, AtomRoot, Readable, Writable}; +use dioxus_core::{ScopeId, ScopeState}; +use std::{ + cell::{RefCell, RefMut}, + fmt::{Debug, Display}, + marker::PhantomData, + ops::{Add, Div, Mul, Not, Sub}, + rc::Rc, + sync::Arc, +}; + +/// 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..d3f10afa9 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 From 049976d23ab5404bb53e52db6545651ed31cb8c1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 16 Apr 2022 16:53:47 -0400 Subject: [PATCH 02/21] feat: allow customizing the index and head --- examples/custom_html.rs | 37 +++++++++++++++++++++++++ packages/desktop/src/cfg.rs | 26 +++++++++++++++++ packages/desktop/src/desktop_context.rs | 2 +- packages/desktop/src/index.html | 7 ++--- packages/desktop/src/lib.rs | 10 +++++-- packages/desktop/src/protocol.rs | 37 ++++++++++++++++++++++--- 6 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 examples/custom_html.rs 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/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 6028706bc..cc154942b 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,32 @@ 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..89222f271 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 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") From 29bda8aa487e9f49515ec498a23b83aa20ee2418 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 16 Apr 2022 16:59:41 -0400 Subject: [PATCH 03/21] docs: use tailwindv3 in examples --- examples/tailwind.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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", From fd2a5f5a6be10659ee49d64c656b3c529a7e1fe4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 16 Apr 2022 17:02:57 -0400 Subject: [PATCH 04/21] fmt --- packages/desktop/src/cfg.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index cc154942b..84ac3a72a 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -104,14 +104,12 @@ 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. From 078287301d675e911531063bc1a6fc4e6baa7e8b Mon Sep 17 00:00:00 2001 From: Michael Mroz Date: Sun, 17 Apr 2022 01:51:02 -0700 Subject: [PATCH 05/21] Fix query parsing: the router was silently throwing away queries not prefixed with an extra '?' --- packages/router/src/hooks/use_route.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 39b77cd66332f6c48d00e02a7e1726f33f14c91a Mon Sep 17 00:00:00 2001 From: Ian Date: Sun, 17 Apr 2022 11:59:43 -0400 Subject: [PATCH 06/21] fixed warp example --- packages/liveview/Cargo.toml | 5 +++++ packages/liveview/examples/warp.rs | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 176e78d51..40ea894fc 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -30,5 +30,10 @@ dioxus-core = { path = "../core", features = ["serialize"] } # warp warp = { version = "0.3", optional = true } +[dev-dependencies] +dioxus-liveview = { path = "./", features = ["warp"] } +warp = "0.3" + + [features] default = [] diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index a02a5231a..4adc357c4 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; From 149d484ffb1f2326b781425fc119a888c58ed5c5 Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 18 Apr 2022 22:24:17 -0400 Subject: [PATCH 07/21] added menu element --- packages/html/src/elements.rs | 5 +++++ 1 file changed, 5 insertions(+) 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. From a632d9b12b2d0734f5001627ee85a88a5e72bda5 Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 19 Apr 2022 22:51:56 -0400 Subject: [PATCH 08/21] implemented liveview axum adapter --- packages/liveview/Cargo.toml | 9 ++- .../liveview/src/adapters/axum_adapter.rs | 68 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 40ea894fc..51b1a89bb 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -30,10 +30,15 @@ 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] dioxus-liveview = { path = "./", features = ["warp"] } 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/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index 8b1378917..1b779f32f 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -1 +1,69 @@ +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; +pub async fn connect(socket: WebSocket, liveview: Liveview, 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 = liveview.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(); +} From a1c644d80bd1d8ea69eb5d071ac73458fb575f5c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 19 Apr 2022 23:48:17 -0400 Subject: [PATCH 09/21] feat: allow header injection for liveview --- packages/liveview/examples/warp.rs | 2 +- packages/liveview/src/lib.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index a02a5231a..e20f07964 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -13,7 +13,7 @@ async fn main() { // todo: compactify this routing under one liveview::app method let view = liveview::new(addr); - let body = view.body(); + let body = view.body("Dioxus LiveView"); let routes = warp::path::end() .map(move || warp::reply::html(body.clone())) 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}
From 5ff87f637521200e4b11164a62d17f2db6ac229c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 20 Apr 2022 00:52:10 -0400 Subject: [PATCH 10/21] fix: liveview handler should filter only for events --- packages/liveview/src/adapters/warp_adapter.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index 948cffe5b..0c6f3e892 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -65,8 +65,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; } From 366a0a8026f98e5fc61a52a26db05d278bb651a4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 20 Apr 2022 09:45:34 -0400 Subject: [PATCH 11/21] feat: read works on fermi root --- packages/fermi/src/root.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/fermi/src/root.rs b/packages/fermi/src/root.rs index d3f10afa9..c7c234ddb 100644 --- a/packages/fermi/src/root.rs +++ b/packages/fermi/src/root.rs @@ -95,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 + } } } From c8919ad77b57c0db27a3313976ea847ea7f73df5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Apr 2022 21:44:37 -0500 Subject: [PATCH 12/21] add benchmark --- Cargo.toml | 4 + benches/tui_update.rs | 268 ++++++++++++++++++++++++++++++++++++ examples/tui_stress_test.rs | 260 ++++++++++++++++++++++++++++++++++ packages/tui/src/config.rs | 17 ++- packages/tui/src/lib.rs | 161 ++++++++++++++-------- 5 files changed, 650 insertions(+), 60 deletions(-) create mode 100644 benches/tui_update.rs create mode 100644 examples/tui_stress_test.rs diff --git a/Cargo.toml b/Cargo.toml index e5ec20306..8291a290c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,3 +88,7 @@ harness = false [[bench]] name = "jsframework" harness = false + +[[bench]] +name = "tui_update" +harness = false \ No newline at end of file diff --git a/benches/tui_update.rs b/benches/tui_update.rs new file mode 100644 index 000000000..5a3a70b66 --- /dev/null +++ b/benches/tui_update.rs @@ -0,0 +1,268 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use dioxus::prelude::*; +use dioxus_tui::{Config, TuiContext}; + +criterion_group!(mbenches, tui_update); +criterion_main!(mbenches); + +/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. +fn tui_update(c: &mut Criterion) { + let mut group = c.benchmark_group("Update boxes"); + + // We can also use loops to define multiple benchmarks, even over multiple dimensions. + for size in 1..=8u32 { + let parameter_string = format!("{}", (3 * size).pow(2)); + group.bench_with_input( + BenchmarkId::new("size", parameter_string), + &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() + }, + ), + _ => (), + }) + }, + ); + } +} + +#[derive(Props, PartialEq)] +struct BoxProps { + x: usize, + y: usize, + hue: f32, + alpha: f32, +} +#[allow(non_snake_case)] +fn Box(cx: Scope) -> Element { + let count = use_state(&cx, || 0); + + let x = cx.props.x * 2; + let y = cx.props.y * 2; + let hue = cx.props.hue; + let display_hue = cx.props.hue as u32 / 10; + let count = count.get(); + let alpha = cx.props.alpha + (count % 100) as f32; + + cx.render(rsx! { + div { + left: "{x}%", + top: "{y}%", + width: "100%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + align_items: "center", + p{"{display_hue:03}"} + } + }) +} + +#[derive(Props, PartialEq)] +struct GridProps { + size: usize, +} +#[allow(non_snake_case)] +fn Grid(cx: Scope) -> Element { + let size = cx.props.size; + let count = use_state(&cx, || 0); + let counts = use_ref(&cx, || vec![0; size * size]); + + let ctx: TuiContext = cx.consume_context().unwrap(); + if *count.get() + 1 >= (size * size) { + ctx.quit(); + } else { + counts.with_mut(|c| { + let i = *count.current(); + c[i] += 1; + c[i] = c[i] % 360; + }); + count.with_mut(|i| { + *i += 1; + *i = *i % (size * size); + }); + } + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..size).map(|x| + { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..size).map(|y| + { + let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; + let key = format!("{}-{}", x, y); + cx.render(rsx! { + Box{ + x: x, + y: y, + alpha: 100.0, + hue: alpha, + key: "{key}", + } + }) + } + ) + } + }) + } + ) + } + }) +} + +fn app3(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 3, + } + } + }) +} + +fn app6(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 6, + } + } + }) +} + +fn app9(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 9, + } + } + }) +} + +fn app12(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 12, + } + } + }) +} + +fn app15(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 15, + } + } + }) +} + +fn app18(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 18, + } + } + }) +} + +fn app21(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 21, + } + } + }) +} + +fn app24(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 24, + } + } + }) +} diff --git a/examples/tui_stress_test.rs b/examples/tui_stress_test.rs new file mode 100644 index 000000000..1674ac055 --- /dev/null +++ b/examples/tui_stress_test.rs @@ -0,0 +1,260 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use dioxus::prelude::*; +use dioxus_tui::{Config, TuiContext}; + +criterion_group!(mbenches, tui_update); +criterion_main!(mbenches); + +/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. +fn tui_update(c: &mut Criterion) { + let mut group = c.benchmark_group("Update boxes"); + + // We can also use loops to define multiple benchmarks, even over multiple dimensions. + for size in 1..=8u32 { + let parameter_string = format!("{}", (3 * size).pow(2)); + group.bench_with_input( + BenchmarkId::new("size", parameter_string), + &size, + |b, size| { + b.iter(|| match size { + 1 => dioxus::tui::launch_cfg( + app3, + Config { + ..Default::default() + }, + ), + 2 => dioxus::tui::launch_cfg( + app6, + Config { + ..Default::default() + }, + ), + 3 => dioxus::tui::launch_cfg( + app9, + Config { + ..Default::default() + }, + ), + 4 => dioxus::tui::launch_cfg( + app12, + Config { + ..Default::default() + }, + ), + 5 => dioxus::tui::launch_cfg( + app15, + Config { + ..Default::default() + }, + ), + 6 => dioxus::tui::launch_cfg( + app18, + Config { + ..Default::default() + }, + ), + 7 => dioxus::tui::launch_cfg( + app21, + Config { + ..Default::default() + }, + ), + 8 => dioxus::tui::launch_cfg( + app24, + Config { + ..Default::default() + }, + ), + _ => (), + }) + }, + ); + } +} + +#[derive(Props, PartialEq)] +struct BoxProps { + x: usize, + y: usize, + hue: f32, + alpha: f32, +} +#[allow(non_snake_case)] +fn Box(cx: Scope) -> Element { + let count = use_state(&cx, || 0); + + let x = cx.props.x * 2; + let y = cx.props.y * 2; + let hue = cx.props.hue; + let display_hue = cx.props.hue as u32 / 10; + let count = count.get(); + let alpha = cx.props.alpha + (count % 100) as f32; + + cx.render(rsx! { + div { + left: "{x}%", + top: "{y}%", + width: "100%", + height: "100%", + background_color: "hsl({hue}, 100%, 50%, {alpha}%)", + align_items: "center", + p{"{display_hue:03}"} + } + }) +} + +#[derive(Props, PartialEq)] +struct GridProps { + size: usize, +} +#[allow(non_snake_case)] +fn Grid(cx: Scope) -> Element { + let size = cx.props.size; + let count = use_state(&cx, || 0); + let counts = use_ref(&cx, || vec![0; size * size]); + + let ctx: TuiContext = cx.consume_context().unwrap(); + if *count.get() + 1 >= (size * size) { + ctx.quit(); + } else { + counts.with_mut(|c| { + let i = *count.current(); + c[i] += 1; + c[i] = c[i] % 360; + }); + count.with_mut(|i| { + *i += 1; + *i = *i % (size * size); + }); + } + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "column", + (0..size).map(|x| + { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + flex_direction: "row", + (0..size).map(|y| + { + let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; + let key = format!("{}-{}", x, y); + cx.render(rsx! { + Box{ + x: x, + y: y, + alpha: 100.0, + hue: alpha, + key: "{key}", + } + }) + } + ) + } + }) + } + ) + } + }) +} + +fn app3(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 3, + } + } + }) +} + +fn app6(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 6, + } + } + }) +} + +fn app9(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 9, + } + } + }) +} + +fn app12(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 12, + } + } + }) +} + +fn app15(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 15, + } + } + }) +} + +fn app18(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 18, + } + } + }) +} + +fn app21(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 21, + } + } + }) +} + +fn app24(cx: Scope) -> Element { + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + Grid{ + size: 24, + } + } + }) +} diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs index 46a0926b8..6e839d8a7 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -1,6 +1,21 @@ -#[derive(Default, Clone, Copy)] +#[derive(Clone, Copy)] pub struct Config { pub 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, + /// Controls if the terminal should dislay anything, usefull for testing. + pub headless: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + rendering_mode: Default::default(), + ctrl_c_quit: true, + headless: false, + } + } } #[derive(Clone, Copy)] diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index f90967802..71f5026fd 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -6,13 +6,19 @@ use crossterm::{ }; use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::*; -use futures::{channel::mpsc::UnboundedSender, pin_mut, StreamExt}; +use futures::{ + channel::mpsc::{UnboundedReceiver, UnboundedSender}, + pin_mut, StreamExt, +}; use std::{ collections::HashMap, io, time::{Duration, Instant}, }; -use stretch2::{prelude::Size, Stretch}; +use stretch2::{ + prelude::{Node, Size}, + Stretch, +}; use style::RinkStyle; use tui::{backend::CrosstermBackend, Terminal}; @@ -30,6 +36,16 @@ pub use hooks::*; pub use layout::*; pub use render::*; +#[derive(Clone)] +pub struct TuiContext { + tx: UnboundedSender, +} +impl TuiContext { + pub fn quit(&self) { + self.tx.unbounded_send(InputEvent::Close).unwrap(); + } +} + pub fn launch(app: Component<()>) { launch_cfg(app, Config::default()) } @@ -37,8 +53,34 @@ pub fn launch(app: Component<()>) { pub fn launch_cfg(app: Component<()>, cfg: Config) { let mut dom = VirtualDom::new(app); let (tx, rx) = unbounded(); + // Setup input handling + let (event_tx, event_rx) = unbounded(); + let event_tx_clone = event_tx.clone(); + if !cfg.headless { + std::thread::spawn(move || { + let tick_rate = Duration::from_millis(100); + let mut last_tick = Instant::now(); + loop { + // poll for tick rate duration, if no events, sent tick event. + let timeout = tick_rate + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + + if crossterm::event::poll(timeout).unwrap() { + let evt = crossterm::event::read().unwrap(); + event_tx.unbounded_send(InputEvent::UserInput(evt)).unwrap(); + } + + if last_tick.elapsed() >= tick_rate { + event_tx.unbounded_send(InputEvent::Tick).unwrap(); + last_tick = Instant::now(); + } + } + }); + } let cx = dom.base_scope(); + cx.provide_root_context(TuiContext { tx: event_tx_clone }); let (handler, state) = RinkInputHandler::new(rx, cx); @@ -46,7 +88,7 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) { dom.rebuild(); - render_vdom(&mut dom, tx, handler, cfg).unwrap(); + render_vdom(&mut dom, event_rx, tx, handler, cfg).unwrap(); } pub struct TuiNode<'a> { @@ -56,35 +98,13 @@ pub struct TuiNode<'a> { pub node: &'a VNode<'a>, } -pub fn render_vdom( +fn render_vdom( vdom: &mut VirtualDom, + mut event_reciever: UnboundedReceiver, ctx: UnboundedSender, handler: RinkInputHandler, cfg: Config, ) -> Result<()> { - // Setup input handling - let (tx, mut rx) = unbounded(); - std::thread::spawn(move || { - let tick_rate = Duration::from_millis(100); - let mut last_tick = Instant::now(); - loop { - // poll for tick rate duration, if no events, sent tick event. - let timeout = tick_rate - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - - if crossterm::event::poll(timeout).unwrap() { - let evt = crossterm::event::read().unwrap(); - tx.unbounded_send(InputEvent::UserInput(evt)).unwrap(); - } - - if last_tick.elapsed() >= tick_rate { - tx.unbounded_send(InputEvent::Tick).unwrap(); - last_tick = Instant::now(); - } - } - }); - tokio::runtime::Builder::new_current_thread() .enable_all() .build()? @@ -92,13 +112,17 @@ pub fn render_vdom( /* Get the terminal to calcualte the layout from */ - enable_raw_mode().unwrap(); - let mut stdout = std::io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); - let backend = CrosstermBackend::new(io::stdout()); - let mut terminal = Terminal::new(backend).unwrap(); + let mut terminal = (!cfg.headless).then(|| { + enable_raw_mode().unwrap(); + let mut stdout = std::io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).unwrap(); + let backend = CrosstermBackend::new(io::stdout()); + Terminal::new(backend).unwrap() + }); - terminal.clear().unwrap(); + if let Some(terminal) = &mut terminal { + terminal.clear().unwrap(); + } loop { /* @@ -126,34 +150,51 @@ pub fn render_vdom( let root_layout = nodes[&node_id].layout; let mut events = Vec::new(); - terminal.draw(|frame| { - // size is guaranteed to not change when rendering - let dims = frame.size(); + fn resize(dims: tui::layout::Rect, stretch: &mut Stretch, root_layout: Node) { let width = dims.width; let height = dims.height; - layout + + stretch .compute_layout( root_layout, Size { - width: stretch2::prelude::Number::Defined(width as f32), - height: stretch2::prelude::Number::Defined(height as f32), + width: stretch2::prelude::Number::Defined((width - 1) as f32), + height: stretch2::prelude::Number::Defined((height - 1) as f32), }, ) .unwrap(); + } - // resolve events before rendering - events = handler.get_events(vdom, &layout, &mut nodes, root_node); - render::render_vnode( - frame, - &layout, - &mut nodes, - vdom, - root_node, - &RinkStyle::default(), - cfg, + if let Some(terminal) = &mut terminal { + terminal.draw(|frame| { + // size is guaranteed to not change when rendering + resize(frame.size(), &mut layout, root_layout); + + // resolve events before rendering + events = handler.get_events(vdom, &layout, &mut nodes, root_node); + render::render_vnode( + frame, + &layout, + &mut nodes, + vdom, + root_node, + &RinkStyle::default(), + cfg, + ); + assert!(nodes.is_empty()); + })?; + } else { + resize( + tui::layout::Rect { + x: 0, + y: 0, + width: 100, + height: 100, + }, + &mut layout, + root_layout, ); - assert!(nodes.is_empty()); - })?; + } for e in events { vdom.handle_message(SchedulerMsg::Event(e)); @@ -164,7 +205,7 @@ pub fn render_vdom( let wait = vdom.wait_for_work(); pin_mut!(wait); - match select(wait, rx.next()).await { + match select(wait, event_reciever.next()).await { Either::Left((_a, _b)) => { // } @@ -194,13 +235,15 @@ pub fn render_vdom( vdom.work_with_deadline(|| false); } - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; + if let Some(terminal) = &mut terminal { + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + } Ok(()) }) From 931aceca43e337568451477ecd78351292851491 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 20 Apr 2022 21:50:49 -0500 Subject: [PATCH 13/21] remove stress test --- examples/tui_stress_test.rs | 260 ------------------------------------ 1 file changed, 260 deletions(-) delete mode 100644 examples/tui_stress_test.rs diff --git a/examples/tui_stress_test.rs b/examples/tui_stress_test.rs deleted file mode 100644 index 1674ac055..000000000 --- a/examples/tui_stress_test.rs +++ /dev/null @@ -1,260 +0,0 @@ -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use dioxus::prelude::*; -use dioxus_tui::{Config, TuiContext}; - -criterion_group!(mbenches, tui_update); -criterion_main!(mbenches); - -/// This benchmarks the cache performance of the TUI for small edits by changing one box at a time. -fn tui_update(c: &mut Criterion) { - let mut group = c.benchmark_group("Update boxes"); - - // We can also use loops to define multiple benchmarks, even over multiple dimensions. - for size in 1..=8u32 { - let parameter_string = format!("{}", (3 * size).pow(2)); - group.bench_with_input( - BenchmarkId::new("size", parameter_string), - &size, - |b, size| { - b.iter(|| match size { - 1 => dioxus::tui::launch_cfg( - app3, - Config { - ..Default::default() - }, - ), - 2 => dioxus::tui::launch_cfg( - app6, - Config { - ..Default::default() - }, - ), - 3 => dioxus::tui::launch_cfg( - app9, - Config { - ..Default::default() - }, - ), - 4 => dioxus::tui::launch_cfg( - app12, - Config { - ..Default::default() - }, - ), - 5 => dioxus::tui::launch_cfg( - app15, - Config { - ..Default::default() - }, - ), - 6 => dioxus::tui::launch_cfg( - app18, - Config { - ..Default::default() - }, - ), - 7 => dioxus::tui::launch_cfg( - app21, - Config { - ..Default::default() - }, - ), - 8 => dioxus::tui::launch_cfg( - app24, - Config { - ..Default::default() - }, - ), - _ => (), - }) - }, - ); - } -} - -#[derive(Props, PartialEq)] -struct BoxProps { - x: usize, - y: usize, - hue: f32, - alpha: f32, -} -#[allow(non_snake_case)] -fn Box(cx: Scope) -> Element { - let count = use_state(&cx, || 0); - - let x = cx.props.x * 2; - let y = cx.props.y * 2; - let hue = cx.props.hue; - let display_hue = cx.props.hue as u32 / 10; - let count = count.get(); - let alpha = cx.props.alpha + (count % 100) as f32; - - cx.render(rsx! { - div { - left: "{x}%", - top: "{y}%", - width: "100%", - height: "100%", - background_color: "hsl({hue}, 100%, 50%, {alpha}%)", - align_items: "center", - p{"{display_hue:03}"} - } - }) -} - -#[derive(Props, PartialEq)] -struct GridProps { - size: usize, -} -#[allow(non_snake_case)] -fn Grid(cx: Scope) -> Element { - let size = cx.props.size; - let count = use_state(&cx, || 0); - let counts = use_ref(&cx, || vec![0; size * size]); - - let ctx: TuiContext = cx.consume_context().unwrap(); - if *count.get() + 1 >= (size * size) { - ctx.quit(); - } else { - counts.with_mut(|c| { - let i = *count.current(); - c[i] += 1; - c[i] = c[i] % 360; - }); - count.with_mut(|i| { - *i += 1; - *i = *i % (size * size); - }); - } - - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - flex_direction: "column", - (0..size).map(|x| - { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - flex_direction: "row", - (0..size).map(|y| - { - let alpha = y as f32*100.0/size as f32 + counts.read()[x*size + y] as f32; - let key = format!("{}-{}", x, y); - cx.render(rsx! { - Box{ - x: x, - y: y, - alpha: 100.0, - hue: alpha, - key: "{key}", - } - }) - } - ) - } - }) - } - ) - } - }) -} - -fn app3(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 3, - } - } - }) -} - -fn app6(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 6, - } - } - }) -} - -fn app9(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 9, - } - } - }) -} - -fn app12(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 12, - } - } - }) -} - -fn app15(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 15, - } - } - }) -} - -fn app18(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 18, - } - } - }) -} - -fn app21(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 21, - } - } - }) -} - -fn app24(cx: Scope) -> Element { - cx.render(rsx! { - div{ - width: "100%", - height: "100%", - Grid{ - size: 24, - } - } - }) -} From 9d9aa33e259693580442c4d384822fb21cb299eb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 06:29:22 -0500 Subject: [PATCH 14/21] add builder pattern and update examples --- examples/tui_color_test.rs | 4 +--- packages/tui/src/config.rs | 31 ++++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/examples/tui_color_test.rs b/examples/tui_color_test.rs index 358fae1f6..1a5a67457 100644 --- a/examples/tui_color_test.rs +++ b/examples/tui_color_test.rs @@ -3,9 +3,7 @@ use dioxus::prelude::*; fn main() { dioxus::tui::launch_cfg( app, - dioxus::tui::Config { - rendering_mode: dioxus::tui::RenderingMode::Ansi, - }, + dioxus::tui::Config::default().with_rendering_mode(dioxus::tui::RenderingMode::Ansi), ); } diff --git a/packages/tui/src/config.rs b/packages/tui/src/config.rs index 6e839d8a7..47ec817ee 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -1,11 +1,36 @@ #[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, ctrl_c_quit: bool) -> Self { + Self { + ctrl_c_quit, + ..self + } + } + + pub fn with_headless(self, headless: bool) -> Self { + Self { headless, ..self } + } } impl Default for Config { From efa682735e4244bb80dcf731a1181ba629c9b5e0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 06:33:03 -0500 Subject: [PATCH 15/21] ignore ctrl-c if ctrl_c_quit is false --- packages/tui/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 71f5026fd..1c4499e18 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -215,6 +215,7 @@ fn render_vdom( TermEvent::Key(key) => { if matches!(key.code, KeyCode::Char('C' | 'c')) && key.modifiers.contains(KeyModifiers::CONTROL) + && cfg.ctrl_c_quit { break; } From 91b21475522745eb9276172f60780b8f224f38bb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 06:44:48 -0500 Subject: [PATCH 16/21] update benchmark --- benches/tui_update.rs | 64 +++++--------------------------------- packages/tui/src/config.rs | 11 ++++--- 2 files changed, 15 insertions(+), 60 deletions(-) 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/packages/tui/src/config.rs b/packages/tui/src/config.rs index 47ec817ee..fc2fb3da5 100644 --- a/packages/tui/src/config.rs +++ b/packages/tui/src/config.rs @@ -21,15 +21,18 @@ impl Config { } } - pub fn with_ctrl_c_quit(self, ctrl_c_quit: bool) -> Self { + pub fn with_ctrl_c_quit(self) -> Self { Self { - ctrl_c_quit, + ctrl_c_quit: true, ..self } } - pub fn with_headless(self, headless: bool) -> Self { - Self { headless, ..self } + pub fn with_headless(self) -> Self { + Self { + headless: true, + ..self + } } } From 159be32fd4f16f194a9d48ddaa206ef5a9555da4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 21 Apr 2022 13:48:11 -0500 Subject: [PATCH 17/21] update documentation to refect tui changes --- docs/reference/src/platforms/tui.md | 49 ++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/reference/src/platforms/tui.md b/docs/reference/src/platforms/tui.md index e646bd4ba..2b5a3ef96 100644 --- a/docs/reference/src/platforms/tui.md +++ b/docs/reference/src/platforms/tui.md @@ -9,13 +9,12 @@ TUI support is currently quite experimental. Even the project name will change. ## Getting Set up -To tinker with TUI support, start by making a new package and adding our TUI package from git. +To tinker with TUI support, start by making a new package and adding our TUI feature. ```shell $ cargo new --bin demo $ cd demo -$ cargo add dioxus -$ cargo add rink --git https://github.com/DioxusLabs/rink.git +$ cargo add dioxus --features tui ``` @@ -27,10 +26,7 @@ Then, edit your `main.rs` with the basic template. use dioxus::prelude::*; fn main() { - let mut dom = VirtualDom::new(app); - dom.rebuild(); - - rink::render_vdom(&mut dom).unwrap(); + dioxus::tui::launch(app); } fn app(cx: Scope) -> Element { @@ -54,7 +50,44 @@ To run our app: $ cargo run ``` -Press "q" to close the app (yes, this is hardcoded, we are working on handlers to expose this in your code.) +Press "ctrl-c" to close the app. To switch from "ctrl-c" to just "q" to quit you can launch the app with a configuration to disable the default quit and use the root TuiContext to quit on your own. + +```rust +// main +use dioxus::events::{KeyCode, KeyboardEvent}; +use dioxus::prelude::*; +use dioxus::tui::TuiContext; + +fn main() { + dioxus::tui::launch_cfg( + app, + dioxus::tui::Config { + ctrl_c_quit: false, + // Some older terminals only support 16 colors or ANSI colors if your terminal is one of these change this to BaseColors or ANSI + rendering_mode: dioxus::tui::RenderingMode::Rgb, + }, + ); +} + +fn app(cx: Scope) -> Element { + let tui_ctx: TuiContext = cx.consume_context().unwrap(); + + cx.render(rsx! { + div { + width: "100%", + height: "10px", + background_color: "red", + justify_content: "center", + align_items: "center", + onkeydown: move |k: KeyboardEvent| if let KeyCode::Q = k.data.key_code { + tui_ctx.quit(); + }, + + "Hello world!" + } + }) +} +``` ## Notes From 0105218db9f9949c825c7c7cb837d58e2d0f6c8f Mon Sep 17 00:00:00 2001 From: Benjamin Lemelin Date: Thu, 21 Apr 2022 15:44:43 -0400 Subject: [PATCH 18/21] Removed alerts when right click on release. --- packages/desktop/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 89222f271..1e3898f05 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -189,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; }); } From 5e162695322b40a4470c61309bf9eea1ad6dd0c3 Mon Sep 17 00:00:00 2001 From: Aleksei Zolotov Date: Sat, 23 Apr 2022 16:21:07 +0200 Subject: [PATCH 19/21] Add PartialEq to events::KeyCode --- packages/html/src/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 7e95375cf..79c90e8ab 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 From dfaa6c186bc1f2fc06146a62cc57864857dcf0e9 Mon Sep 17 00:00:00 2001 From: Ian Date: Sat, 23 Apr 2022 22:13:43 -0400 Subject: [PATCH 20/21] Fixed example squiglies. --- packages/liveview/Cargo.toml | 3 +- packages/liveview/examples/axum.rs | 38 +++++++++++++++++++ packages/liveview/examples/warp.rs | 36 +++++++++--------- .../liveview/src/adapters/axum_adapter.rs | 12 +++++- .../liveview/src/adapters/warp_adapter.rs | 1 + 5 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 packages/liveview/examples/axum.rs diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 51b1a89bb..313892f45 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -35,7 +35,8 @@ axum = { version = "0.5.1", optional = true, features = ["ws"] } tower = { version = "0.4.12", optional = true } [dev-dependencies] -dioxus-liveview = { path = "./", features = ["warp"] } +tokio = { version = "1", features = ["full"] } +dioxus = { path = "../../" } warp = "0.3" axum = { version = "0.5.1", features = ["ws"] } tower = "0.4.12" 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 5540f7b08..2c14d318b 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -5,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("Dioxus LiveView"); + // 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 1b779f32f..d85b6da68 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -7,14 +7,22 @@ use futures_util::{ }; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; +use tokio_util::task::LocalPoolHandle; -pub async fn connect(socket: WebSocket, liveview: Liveview, app: fn(Scope) -> Element) { +#[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 = liveview.pool.clone().spawn_pinned(move || async move { + 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(); diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index 0c6f3e892..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; From bcefeae10f8abd6c72ac5dbc69547c9ed742f49e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 23 Apr 2022 23:21:39 -0400 Subject: [PATCH 21/21] clippy: remove some unused imports --- packages/fermi/src/hooks/state.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/fermi/src/hooks/state.rs b/packages/fermi/src/hooks/state.rs index ab035f152..9db9a9fdf 100644 --- a/packages/fermi/src/hooks/state.rs +++ b/packages/fermi/src/hooks/state.rs @@ -1,12 +1,10 @@ -use crate::{use_atom_root, AtomId, AtomRoot, Readable, Writable}; +use crate::{AtomId, AtomRoot, Writable}; use dioxus_core::{ScopeId, ScopeState}; use std::{ - cell::{RefCell, RefMut}, + cell::RefMut, fmt::{Debug, Display}, - marker::PhantomData, ops::{Add, Div, Mul, Not, Sub}, rc::Rc, - sync::Arc, }; /// Store state between component renders.