From 920fcf728c5b6a22357ef5149ab6be8c09360ab7 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 16:34:57 -0500 Subject: [PATCH 01/50] create onmounted event --- packages/html/Cargo.toml | 5 ++ packages/html/src/events.rs | 3 + packages/html/src/events/mounted.rs | 85 ++++++++++++++++++++++++ packages/html/src/web_sys_bind/events.rs | 49 +++++++++++++- 4 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 packages/html/src/events/mounted.rs diff --git a/packages/html/Cargo.toml b/packages/html/Cargo.toml index e60fcd8e5..513cc253a 100644 --- a/packages/html/Cargo.toml +++ b/packages/html/Cargo.toml @@ -39,6 +39,11 @@ features = [ "FocusEvent", "CompositionEvent", "ClipboardEvent", + "Element", + "DomRect", + "ScrollIntoViewOptions", + "ScrollLogicalPosition", + "ScrollBehavior", ] [dev-dependencies] diff --git a/packages/html/src/events.rs b/packages/html/src/events.rs index 853400c28..85a408d0b 100644 --- a/packages/html/src/events.rs +++ b/packages/html/src/events.rs @@ -33,6 +33,7 @@ mod form; mod image; mod keyboard; mod media; +mod mounted; mod mouse; mod pointer; mod scroll; @@ -51,6 +52,7 @@ pub use form::*; pub use image::*; pub use keyboard::*; pub use media::*; +pub use mounted::*; pub use mouse::*; pub use pointer::*; pub use scroll::*; @@ -144,6 +146,7 @@ pub fn event_bubbles(evt: &str) -> bool { "animationiteration" => true, "transitionend" => true, "toggle" => true, + "mounted" => false, _ => true, } } diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs new file mode 100644 index 000000000..a5413bb5b --- /dev/null +++ b/packages/html/src/events/mounted.rs @@ -0,0 +1,85 @@ +//! Handles quering data from the renderer + +use euclid::Rect; + +use std::{any::Any, rc::Rc}; + +/// An Element that has been rendered and allows reading and modifying information about it. +/// +/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. +pub trait RenderedElementBacking { + /// Get the renderer specific element for the given id + fn get_raw_element(&self) -> Option<&dyn Any> { + None + } + + /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) + fn get_client_rect(&self) -> Option> { + None + } + + /// Scroll to make the element visible + fn scroll_to(&self, _behavior: ScrollBehavior) -> Option<()> { + None + } + + /// Set the focus on the element + fn set_focus(&self, _focus: bool) -> Option<()> { + None + } +} + +/// The way that scrolling should be performed +pub enum ScrollBehavior { + /// Scroll to the element immediately + Instant, + /// Scroll to the element smoothly + Smooth, +} + +/// An Element that has been rendered and allows reading and modifying information about it. +/// +/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. +pub struct MountedData { + inner: Rc, +} + +impl MountedData { + /// Create a new MountedData + pub fn new(registry: impl RenderedElementBacking + 'static) -> Self { + Self { + inner: Rc::new(registry), + } + } + + /// Get the renderer specific element for the given id + pub fn get_raw_element(&self) -> Option<&dyn Any> { + self.inner.get_raw_element() + } + + /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) + pub fn get_client_rect(&self) -> Option> { + self.inner.get_client_rect() + } + + /// Scroll to make the element visible + pub fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + self.inner.scroll_to(behavior) + } + + /// Set the focus on the element + pub fn set_focus(&self, focus: bool) -> Option<()> { + self.inner.set_focus(focus) + } +} + +use dioxus_core::Event; + +pub type MountedEvent = Event; + +impl_event! [ + MountedData; + + /// mounted + onmounted +]; diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 196099444..01b25dfc2 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -4,14 +4,14 @@ use crate::events::{ }; use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::DragData; +use crate::{DragData, MountedData, RenderedElementBacking, ScrollBehavior}; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; use std::str::FromStr; use wasm_bindgen::JsCast; use web_sys::{ - AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, - TransitionEvent, WheelEvent, + AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, + ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent, }; macro_rules! uncheck_convert { @@ -193,3 +193,46 @@ impl From<&TransitionEvent> for TransitionData { } } } + +impl From<&web_sys::Element> for MountedData { + fn from(e: &web_sys::Element) -> Self { + MountedData::new(e.clone()) + } +} + +impl RenderedElementBacking for web_sys::Element { + fn get_client_rect(&self) -> Option> { + let rect = self.get_bounding_client_rect(); + Some(euclid::Rect::new( + euclid::Point2D::new(rect.left(), rect.top()), + euclid::Size2D::new(rect.width(), rect.height()), + )) + } + + fn get_raw_element(&self) -> Option<&dyn std::any::Any> { + Some(self) + } + + fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + match behavior { + ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options( + ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant), + ), + ScrollBehavior::Smooth => self.scroll_into_view_with_scroll_into_view_options( + ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Smooth), + ), + } + + Some(()) + } + + fn set_focus(&self, focus: bool) -> Option<()> { + self.dyn_ref::().and_then(|e| { + if focus { + e.focus().ok() + } else { + e.blur().ok() + } + }) + } +} From 1aad285853a4bbe340d9c4775fe8d1b7248a8ea2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 17:02:12 -0500 Subject: [PATCH 02/50] provide nicer error types --- packages/html/src/events/mounted.rs | 57 ++++++++++++++++++------ packages/html/src/web_sys_bind/events.rs | 44 +++++++++++------- 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index a5413bb5b..dafbf46c8 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -2,30 +2,34 @@ use euclid::Rect; -use std::{any::Any, rc::Rc}; +use std::{ + any::Any, + fmt::{Display, Formatter}, + rc::Rc, +}; /// An Element that has been rendered and allows reading and modifying information about it. /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. pub trait RenderedElementBacking { /// Get the renderer specific element for the given id - fn get_raw_element(&self) -> Option<&dyn Any> { - None + fn get_raw_element(&self) -> MountedResult<&dyn Any> { + Err(MountedError::NotSupported) } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - fn get_client_rect(&self) -> Option> { - None + fn get_client_rect(&self) -> MountedResult> { + Err(MountedError::NotSupported) } /// Scroll to make the element visible - fn scroll_to(&self, _behavior: ScrollBehavior) -> Option<()> { - None + fn scroll_to(&self, _behavior: ScrollBehavior) -> MountedResult<()> { + Err(MountedError::NotSupported) } /// Set the focus on the element - fn set_focus(&self, _focus: bool) -> Option<()> { - None + fn set_focus(&self, _focus: bool) -> MountedResult<()> { + Err(MountedError::NotSupported) } } @@ -53,22 +57,22 @@ impl MountedData { } /// Get the renderer specific element for the given id - pub fn get_raw_element(&self) -> Option<&dyn Any> { + pub fn get_raw_element(&self) -> MountedResult<&dyn Any> { self.inner.get_raw_element() } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - pub fn get_client_rect(&self) -> Option> { + pub fn get_client_rect(&self) -> MountedResult> { self.inner.get_client_rect() } /// Scroll to make the element visible - pub fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + pub fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { self.inner.scroll_to(behavior) } /// Set the focus on the element - pub fn set_focus(&self, focus: bool) -> Option<()> { + pub fn set_focus(&self, focus: bool) -> MountedResult<()> { self.inner.set_focus(focus) } } @@ -83,3 +87,30 @@ impl_event! [ /// mounted onmounted ]; + +/// The MountedResult type for the MountedData +pub type MountedResult = Result; + +#[derive(Debug)] +/// The error type for the MountedData +pub enum MountedError { + /// The renderer does not support the requested operation + NotSupported, + /// The element was not found + OperationFailed(Box), +} + +impl Display for MountedError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + MountedError::NotSupported => { + write!(f, "The renderer does not support the requested operation") + } + MountedError::OperationFailed(e) => { + write!(f, "The operation failed: {}", e) + } + } + } +} + +impl std::error::Error for MountedError {} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 01b25dfc2..1983a31da 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -4,11 +4,13 @@ use crate::events::{ }; use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint}; use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton}; -use crate::{DragData, MountedData, RenderedElementBacking, ScrollBehavior}; +use crate::{ + DragData, MountedData, MountedError, MountedResult, RenderedElementBacking, ScrollBehavior, +}; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; use std::str::FromStr; -use wasm_bindgen::JsCast; +use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, ScrollIntoViewOptions, TouchEvent, TransitionEvent, WheelEvent, @@ -201,19 +203,19 @@ impl From<&web_sys::Element> for MountedData { } impl RenderedElementBacking for web_sys::Element { - fn get_client_rect(&self) -> Option> { + fn get_client_rect(&self) -> MountedResult> { let rect = self.get_bounding_client_rect(); - Some(euclid::Rect::new( + Ok(euclid::Rect::new( euclid::Point2D::new(rect.left(), rect.top()), euclid::Size2D::new(rect.width(), rect.height()), )) } - fn get_raw_element(&self) -> Option<&dyn std::any::Any> { - Some(self) + fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> { + Ok(self) } - fn scroll_to(&self, behavior: ScrollBehavior) -> Option<()> { + fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { match behavior { ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options( ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant), @@ -223,16 +225,26 @@ impl RenderedElementBacking for web_sys::Element { ), } - Some(()) + Ok(()) } - fn set_focus(&self, focus: bool) -> Option<()> { - self.dyn_ref::().and_then(|e| { - if focus { - e.focus().ok() - } else { - e.blur().ok() - } - }) + fn set_focus(&self, focus: bool) -> MountedResult<()> { + self.dyn_ref::() + .ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into())))) + .and_then(|e| { + (if focus { e.focus() } else { e.blur() }) + .map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err)))) + }) } } + +#[derive(Debug)] +struct FocusError(JsValue); + +impl std::fmt::Display for FocusError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "failed to focus element {:?}", self.0) + } +} + +impl std::error::Error for FocusError {} From cb5cb56ad352f7d4edc1fb53b96e24a9fcd5d795 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 19 Mar 2023 18:28:34 -0500 Subject: [PATCH 03/50] implement onmounted for the web renderer --- .../interpreter/src/sledgehammer_bindings.rs | 7 +++ packages/web/src/dom.rs | 52 +++++++++++++++---- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 33f6b4420..650e721ef 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -116,6 +116,10 @@ mod js { export function set_node(id, node) { nodes[id] = node; } + export function get_node(id) { + console.log(nodes, id); + return nodes[id]; + } export function initilize(root, handler) { listeners.handler = handler; nodes = [root]; @@ -166,6 +170,9 @@ mod js { #[wasm_bindgen] pub fn set_node(id: u32, node: Node); + #[wasm_bindgen] + pub fn get_node(id: u32) -> Node; + #[wasm_bindgen] pub fn initilize(root: Node, handler: &Function); } diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 4d17749d8..8aae595eb 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -10,8 +10,8 @@ use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, }; -use dioxus_html::{event_bubbles, CompositionData, FormData}; -use dioxus_interpreter_js::{save_template, Channel}; +use dioxus_html::{event_bubbles, CompositionData, FormData, MountedData}; +use dioxus_interpreter_js::{get_node, save_template, Channel}; use futures_channel::mpsc; use rustc_hash::FxHashMap; use std::{any::Any, rc::Rc}; @@ -27,6 +27,7 @@ pub struct WebsysDom { templates: FxHashMap, max_template_id: u32, pub(crate) interpreter: Channel, + event_channel: mpsc::UnboundedSender, } pub struct UiEvent { @@ -34,7 +35,6 @@ pub struct UiEvent { pub bubbles: bool, pub element: ElementId, pub data: Rc, - pub event: Event, } impl WebsysDom { @@ -48,8 +48,9 @@ impl WebsysDom { }; let interpreter = Channel::default(); - let handler: Closure = - Closure::wrap(Box::new(move |event: &web_sys::Event| { + let handler: Closure = Closure::wrap(Box::new({ + let event_channel = event_channel.clone(); + move |event: &web_sys::Event| { let name = event.type_(); let element = walk_event_for_id(event); let bubbles = dioxus_html::event_bubbles(name.as_str()); @@ -69,10 +70,10 @@ impl WebsysDom { bubbles, element, data, - event: event.clone(), }); } - })); + } + })); dioxus_interpreter_js::initilize( root.clone().unchecked_into(), @@ -85,6 +86,7 @@ impl WebsysDom { interpreter, templates: FxHashMap::default(), max_template_id: 0, + event_channel, } } @@ -156,6 +158,8 @@ impl WebsysDom { pub fn apply_edits(&mut self, mut edits: Vec) { use Mutation::*; let i = &mut self.interpreter; + // we need to apply the mount events last, so we collect them here + let mut to_mount = Vec::new(); for edit in &edits { match edit { AppendChildren { id, m } => i.append_children(id.0 as u32, *m as u32), @@ -206,17 +210,43 @@ impl WebsysDom { }, SetText { value, id } => i.set_text(id.0 as u32, value), NewEventListener { name, id, .. } => { - i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); - } - RemoveEventListener { name, id } => { - i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8) + match *name { + // mounted events are fired immediately after the element is mounted. + "mounted" => { + to_mount.push(*id); + } + _ => { + i.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8); + } + } } + RemoveEventListener { name, id } => match *name { + "mounted" => {} + _ => { + i.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8); + } + }, Remove { id } => i.remove(id.0 as u32), PushRoot { id } => i.push_root(id.0 as u32), } } edits.clear(); i.flush(); + + for id in to_mount { + let node = get_node(id.0 as u32); + if let Some(element) = node.dyn_ref::() { + log::info!("mounted event fired: {}", id.0); + let data: MountedData = element.into(); + let data = Rc::new(data); + let _ = self.event_channel.unbounded_send(UiEvent { + name: "mounted".to_string(), + bubbles: false, + element: id, + data, + }); + } + } } } From 7636c046faa7b94f6ddca727a2ecdfd5511888c3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 20 Mar 2023 16:10:34 -0500 Subject: [PATCH 04/50] implement on mounted for desktop --- packages/desktop/src/desktop_context.rs | 5 + packages/desktop/src/element.rs | 208 +++++++++++++++++++++++ packages/desktop/src/lib.rs | 60 ++++++- packages/html/src/events/mounted.rs | 36 ++-- packages/html/src/transit.rs | 52 ++++++ packages/html/src/web_sys_bind/events.rs | 26 ++- packages/interpreter/Cargo.toml | 2 + packages/interpreter/src/interpreter.js | 85 +++++++++ 8 files changed, 450 insertions(+), 24 deletions(-) create mode 100644 packages/desktop/src/element.rs diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 2ed4bbee9..ccb8c7588 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use std::rc::Weak; use crate::create_new_window; +use crate::element::QueryEngine; use crate::eval::EvalResult; use crate::events::IpcMessage; use crate::shortcut::IntoKeyCode; @@ -62,6 +63,9 @@ pub struct DesktopContext { /// The receiver for eval results since eval is async pub(super) eval: tokio::sync::broadcast::Sender, + /// The receiver for queries about elements + pub(super) query: QueryEngine, + pub(super) pending_windows: WebviewQueue, pub(crate) event_loop: EventLoopWindowTarget, @@ -97,6 +101,7 @@ impl DesktopContext { proxy, event_loop, eval: tokio::sync::broadcast::channel(8).0, + query: Default::default(), pending_windows: webviews, event_handlers, shortcut_manager, diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs new file mode 100644 index 000000000..60a995f1b --- /dev/null +++ b/packages/desktop/src/element.rs @@ -0,0 +1,208 @@ +use std::{cell::RefCell, rc::Rc}; + +use dioxus_core::ElementId; +use dioxus_html::{ + MountedResult, MountedReturn, MountedReturnData, NodeUpdate, NodeUpdateData, + RenderedElementBacking, +}; +use slab::Slab; +use wry::webview::WebView; + +/// A mounted element passed to onmounted events +pub struct DesktopElement { + id: ElementId, + webview: Rc, + query: QueryEngine, +} + +impl DesktopElement { + pub(crate) fn new(id: ElementId, webview: Rc, query: QueryEngine) -> Self { + Self { id, webview, query } + } + + /// Get the id of the element + pub fn id(&self) -> ElementId { + self.id + } + + /// Get the webview the element is mounted in + pub fn webview(&self) -> &Rc { + &self.webview + } +} + +impl RenderedElementBacking for DesktopElement { + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } + + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures_util::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let fut = self + .query + .new_query(self.id, NodeUpdateData::GetClientRect {}, &self.webview) + .resolve(); + Box::pin(async move { + match fut.await { + Some(MountedReturnData::GetClientRect(rect)) => Ok(rect), + Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::MismatchedReturn), + )), + None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( + DesktopQueryError::FailedToQuery, + ))), + } + }) + } + + fn scroll_to( + &self, + behavior: dioxus_html::ScrollBehavior, + ) -> std::pin::Pin>>> { + let fut = self + .query + .new_query( + self.id, + NodeUpdateData::ScrollTo { behavior }, + &self.webview, + ) + .resolve(); + Box::pin(async move { + match fut.await { + Some(MountedReturnData::ScrollTo(())) => Ok(()), + Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::MismatchedReturn), + )), + None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( + DesktopQueryError::FailedToQuery, + ))), + } + }) + } + + fn set_focus( + &self, + focus: bool, + ) -> std::pin::Pin>>> { + let fut = self + .query + .new_query(self.id, NodeUpdateData::SetFocus { focus }, &self.webview) + .resolve(); + Box::pin(async move { + match fut.await { + Some(MountedReturnData::SetFocus(())) => Ok(()), + Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::MismatchedReturn), + )), + None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( + DesktopQueryError::FailedToQuery, + ))), + } + }) + } +} + +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + fn new_query(&self, id: ElementId, update: NodeUpdateData, webview: &WebView) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + let update = NodeUpdate { + id: id.0 as u32, + request_id, + data: update, + }; + + // start the query + webview + .evaluate_script(&format!( + "window.interpreter.handleNodeUpdate({})", + serde_json::to_string(&update).unwrap() + )) + .unwrap(); + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + } + } + + pub fn send(&self, data: MountedReturn) { + self.sender.send(data).unwrap(); + } +} + +struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, +} + +impl Query { + async fn resolve(mut self) -> Option { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break result.data; + } + } + Err(_) => { + break None; + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Debug)] +enum DesktopQueryError { + FailedToQuery, + MismatchedReturn, +} + +impl std::fmt::Display for DesktopQueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), + DesktopQueryError::MismatchedReturn => { + write!(f, "The return type did not match the query") + } + } + } +} + +impl std::error::Error for DesktopQueryError {} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 75c67dd22..079331fdb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -5,6 +5,7 @@ mod cfg; mod desktop_context; +mod element; mod escape; mod eval; mod events; @@ -19,7 +20,8 @@ pub use desktop_context::{ }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use dioxus_html::HtmlEvent; +use dioxus_html::{HtmlEvent, MountedData, MountedReturn}; +use element::DesktopElement; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; use shortcut::ShortcutRegistry; @@ -220,19 +222,69 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) } EventData::Ipc(msg) if msg.method() == "user_event" => { - let evt = match serde_json::from_value::(msg.params()) { + let params = msg.params(); + + let evt = match serde_json::from_value::(params) { Ok(value) => value, Err(_) => return, }; + let HtmlEvent { + element, + name, + bubbles, + data, + } = evt; + let view = webviews.get_mut(&event.1).unwrap(); - view.dom - .handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles); + // check for a mounted event placeholder and replace it with a desktop specific element + let as_any = if let dioxus_html::EventData::Mounted = &data { + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query; + + let element = DesktopElement::new(element, view.webview.clone(), query); + + Rc::new(MountedData::new(element)) + } else { + data.into_any() + }; + + view.dom.handle_event(&name, as_any, element, bubbles); send_edits(view.dom.render_immediate(), &view.webview); } + EventData::Ipc(msg) if msg.method() == "node_update" => { + let params = msg.params(); + println!("node_update: {:?}", params); + + // check for a mounted event + let evt = match serde_json::from_value::(params) { + Ok(value) => value, + Err(err) => { + println!("node_update: {:?}", err); + return; + } + }; + + let view = webviews.get(&event.1).unwrap(); + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query; + + println!("node_update: {:?}", evt); + + query.send(evt); + } + EventData::Ipc(msg) if msg.method() == "initialize" => { let view = webviews.get_mut(&event.1).unwrap(); send_edits(view.dom.rebuild(), &view.webview); diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index dafbf46c8..548359b5c 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -5,12 +5,15 @@ use euclid::Rect; use std::{ any::Any, fmt::{Display, Formatter}, + future::Future, + pin::Pin, rc::Rc, }; /// An Element that has been rendered and allows reading and modifying information about it. /// /// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries. +// we can not use async_trait here because it does not create a trait that is object safe pub trait RenderedElementBacking { /// Get the renderer specific element for the given id fn get_raw_element(&self) -> MountedResult<&dyn Any> { @@ -18,26 +21,35 @@ pub trait RenderedElementBacking { } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - fn get_client_rect(&self) -> MountedResult> { - Err(MountedError::NotSupported) + #[allow(clippy::type_complexity)] + fn get_client_rect(&self) -> Pin>>>> { + Box::pin(async { Err(MountedError::NotSupported) }) } /// Scroll to make the element visible - fn scroll_to(&self, _behavior: ScrollBehavior) -> MountedResult<()> { - Err(MountedError::NotSupported) + fn scroll_to( + &self, + _behavior: ScrollBehavior, + ) -> Pin>>> { + Box::pin(async { Err(MountedError::NotSupported) }) } /// Set the focus on the element - fn set_focus(&self, _focus: bool) -> MountedResult<()> { - Err(MountedError::NotSupported) + fn set_focus(&self, _focus: bool) -> Pin>>> { + Box::pin(async { Err(MountedError::NotSupported) }) } } +impl RenderedElementBacking for () {} + /// The way that scrolling should be performed +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum ScrollBehavior { /// Scroll to the element immediately + #[cfg_attr(feature = "serialize", serde(rename = "instant"))] Instant, /// Scroll to the element smoothly + #[cfg_attr(feature = "serialize", serde(rename = "smooth"))] Smooth, } @@ -62,18 +74,18 @@ impl MountedData { } /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position) - pub fn get_client_rect(&self) -> MountedResult> { - self.inner.get_client_rect() + pub async fn get_client_rect(&self) -> MountedResult> { + self.inner.get_client_rect().await } /// Scroll to make the element visible - pub fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { - self.inner.scroll_to(behavior) + pub async fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { + self.inner.scroll_to(behavior).await } /// Set the focus on the element - pub fn set_focus(&self, focus: bool) -> MountedResult<()> { - self.inner.set_focus(focus) + pub async fn set_focus(&self, focus: bool) -> MountedResult<()> { + self.inner.set_focus(focus).await } } diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 651b7e4f6..12c35bd28 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -2,6 +2,7 @@ use std::{any::Any, rc::Rc}; use crate::events::*; use dioxus_core::ElementId; +use euclid::Rect; use serde::{Deserialize, Serialize}; #[derive(Serialize, Debug, Clone, PartialEq)] @@ -113,6 +114,9 @@ fn fun_name( // Toggle "toggle" => Toggle(de(data)?), + // Mounted + "mounted" => Mounted, + // ImageData => "load" | "error"; // OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange"; other => { @@ -151,6 +155,7 @@ pub enum EventData { Animation(AnimationData), Transition(TransitionData), Toggle(ToggleData), + Mounted, } impl EventData { @@ -172,6 +177,7 @@ impl EventData { EventData::Animation(data) => Rc::new(data) as Rc, EventData::Transition(data) => Rc::new(data) as Rc, EventData::Toggle(data) => Rc::new(data) as Rc, + EventData::Mounted => Rc::new(MountedData::new(())) as Rc, } } } @@ -215,3 +221,49 @@ fn test_back_and_forth() { assert_eq!(data, p); } + +/// Message to update a node to support MountedData +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct NodeUpdate { + /// The id of the node to update + pub id: u32, + /// The id of the request + pub request_id: usize, + /// The data to update the node with + pub data: NodeUpdateData, +} + +/// Message to update a node to support MountedData +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum NodeUpdateData { + SetFocus { focus: bool }, + GetClientRect {}, + ScrollTo { behavior: ScrollBehavior }, +} + +/// The result of a element query +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct MountedReturn { + /// A unique id for the query + pub id: usize, + /// The result of the query + pub data: Option, +} + +/// The data of a element query +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + serde(tag = "type") +)] +pub enum MountedReturnData { + SetFocus(()), + GetClientRect(Rect), + ScrollTo(()), +} diff --git a/packages/html/src/web_sys_bind/events.rs b/packages/html/src/web_sys_bind/events.rs index 1983a31da..bcbe80dda 100644 --- a/packages/html/src/web_sys_bind/events.rs +++ b/packages/html/src/web_sys_bind/events.rs @@ -9,6 +9,8 @@ use crate::{ }; use keyboard_types::{Code, Key, Modifiers}; use std::convert::TryInto; +use std::future::Future; +use std::pin::Pin; use std::str::FromStr; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ @@ -203,19 +205,25 @@ impl From<&web_sys::Element> for MountedData { } impl RenderedElementBacking for web_sys::Element { - fn get_client_rect(&self) -> MountedResult> { + fn get_client_rect( + &self, + ) -> Pin>>>> { let rect = self.get_bounding_client_rect(); - Ok(euclid::Rect::new( + let result = Ok(euclid::Rect::new( euclid::Point2D::new(rect.left(), rect.top()), euclid::Size2D::new(rect.width(), rect.height()), - )) + )); + Box::pin(async { result }) } fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> { Ok(self) } - fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { + fn scroll_to( + &self, + behavior: ScrollBehavior, + ) -> Pin>>> { match behavior { ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options( ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant), @@ -225,16 +233,18 @@ impl RenderedElementBacking for web_sys::Element { ), } - Ok(()) + Box::pin(async { Ok(()) }) } - fn set_focus(&self, focus: bool) -> MountedResult<()> { - self.dyn_ref::() + fn set_focus(&self, focus: bool) -> Pin>>> { + let result = self + .dyn_ref::() .ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into())))) .and_then(|e| { (if focus { e.focus() } else { e.blur() }) .map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err)))) - }) + }); + Box::pin(async { result }) } } diff --git a/packages/interpreter/Cargo.toml b/packages/interpreter/Cargo.toml index 7c7d79607..bb8f7ec60 100644 --- a/packages/interpreter/Cargo.toml +++ b/packages/interpreter/Cargo.toml @@ -19,8 +19,10 @@ js-sys = { version = "0.3.56", optional = true } web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] } sledgehammer_bindgen = { version = "0.1.3", optional = true } sledgehammer_utils = { version = "0.1.0", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } [features] default = [] +serialize = ["serde"] web = ["wasm-bindgen", "js-sys", "web-sys"] sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"] diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index b921c7c22..ce2a010c0 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -204,6 +204,76 @@ class Interpreter { node.removeAttribute(name); } } + + GetClientRect(id) { + const node= this.nodes[id]; + if (!node) { + return; + } + const rect = node.getBoundingClientRect(); + return { + type: "GetClientRect", + origin: [ + rect.x, + rect.y, + ], + size: [ + rect.width, + rect.height, + ] + }; + } + + ScrollTo(id, behavior) { + const node = this.nodes[id]; + if (!node) { + return; + } + node.scrollIntoView({ + behavior: behavior + }); + return { + type: "ScrollTo", + }; + } + + /// Set the focus on the element + SetFocus(id, focus) { + const node = this.nodes[id]; + if (!node) { + return; + } + if (focus) { + node.focus(); + } else { + node.blur(); + } + return { + type: "SetFocus", + }; + } + + handleNodeUpdate(edit) { + let data; + switch (edit.data.type) { + case "SetFocus": + data = this.SetFocus(edit.id, edit.data.focus); + break; + case "ScrollTo": + data = this.ScrollTo(edit.id, edit.data.behavior); + break; + case "GetClientRect": + data = this.GetClientRect(edit.id); + break; + } + window.ipc.postMessage( + serializeIpcMessage("node_update", { + id: edit.request_id, + data: data + }) + ); + } + handleEdits(edits) { for (let template of edits.templates) { this.SaveTemplate(template); @@ -345,6 +415,19 @@ class Interpreter { let bubbles = event_bubbles(edit.name); + // if this is a mounted listener, we send the event immediately + if (edit.name === "mounted") { + window.ipc.postMessage( + serializeIpcMessage("user_event", { + name: edit.name, + element: edit.id, + data: null, + bubbles, + }) + ); + } + + // this handler is only provided on desktop implementations since this // method is not used by the web implementation let handler = (event) => { @@ -921,6 +1004,8 @@ function event_bubbles(event) { return true; case "toggle": return true; + case "mounted": + return false; } return true; From fa9f0d0f6ca6037c687c99afcf6dfd8163d89d4b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 15:19:00 -0500 Subject: [PATCH 05/50] Generalize Query system for use in use_eval and node querys --- packages/desktop/src/desktop_context.rs | 33 +---- packages/desktop/src/element.rs | 165 ++++++------------------ packages/desktop/src/eval.rs | 22 ++-- packages/desktop/src/lib.rs | 52 +++----- packages/desktop/src/query.rs | 105 +++++++++++++++ packages/html/src/events/mounted.rs | 11 +- packages/html/src/transit.rs | 47 ------- packages/interpreter/src/interpreter.js | 33 +---- 8 files changed, 186 insertions(+), 282 deletions(-) create mode 100644 packages/desktop/src/query.rs diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index ccb8c7588..6af6401f3 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -3,9 +3,9 @@ use std::rc::Rc; use std::rc::Weak; use crate::create_new_window; -use crate::element::QueryEngine; use crate::eval::EvalResult; use crate::events::IpcMessage; +use crate::query::QueryEngine; use crate::shortcut::IntoKeyCode; use crate::shortcut::IntoModifersState; use crate::shortcut::ShortcutId; @@ -17,7 +17,6 @@ use dioxus_core::ScopeState; use dioxus_core::VirtualDom; #[cfg(all(feature = "hot-reload", debug_assertions))] use dioxus_hot_reload::HotReloadMsg; -use serde_json::Value; use slab::Slab; use wry::application::event::Event; use wry::application::event_loop::EventLoopProxy; @@ -60,10 +59,7 @@ pub struct DesktopContext { /// The proxy to the event loop pub proxy: ProxyType, - /// The receiver for eval results since eval is async - pub(super) eval: tokio::sync::broadcast::Sender, - - /// The receiver for queries about elements + /// The receiver for queries about the current window pub(super) query: QueryEngine, pub(super) pending_windows: WebviewQueue, @@ -100,7 +96,6 @@ impl DesktopContext { webview, proxy, event_loop, - eval: tokio::sync::broadcast::channel(8).0, query: Default::default(), pending_windows: webviews, event_handlers, @@ -215,28 +210,10 @@ impl DesktopContext { /// Evaluate a javascript expression pub fn eval(&self, code: &str) -> EvalResult { - // Embed the return of the eval in a function so we can send it back to the main thread - let script = format!( - r#" - window.ipc.postMessage( - JSON.stringify({{ - "method":"eval_result", - "params": ( - function(){{ - {code} - }} - )() - }}) - ); - "# - ); + // the query id lets us keep track of the eval result and send it back to the main thread + let query = self.query.new_query(code, &self.webview); - if let Err(e) = self.webview.evaluate_script(&script) { - // send an error to the eval receiver - log::warn!("Eval script error: {e}"); - } - - EvalResult::new(self.eval.clone()) + EvalResult::new(query) } /// Create a wry event handler that listens for wry events. diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs index 60a995f1b..e94a481c7 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -1,13 +1,11 @@ -use std::{cell::RefCell, rc::Rc}; +use std::rc::Rc; use dioxus_core::ElementId; -use dioxus_html::{ - MountedResult, MountedReturn, MountedReturnData, NodeUpdate, NodeUpdateData, - RenderedElementBacking, -}; -use slab::Slab; +use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking}; use wry::webview::WebView; +use crate::query::QueryEngine; + /// A mounted element passed to onmounted events pub struct DesktopElement { id: ElementId, @@ -19,16 +17,6 @@ impl DesktopElement { pub(crate) fn new(id: ElementId, webview: Rc, query: QueryEngine) -> Self { Self { id, webview, query } } - - /// Get the id of the element - pub fn id(&self) -> ElementId { - self.id - } - - /// Get the webview the element is mounted in - pub fn webview(&self) -> &Rc { - &self.webview - } } impl RenderedElementBacking for DesktopElement { @@ -45,19 +33,21 @@ impl RenderedElementBacking for DesktopElement { >, >, > { + let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + let fut = self .query - .new_query(self.id, NodeUpdateData::GetClientRect {}, &self.webview) + .new_query::>>(&script, &self.webview) .resolve(); Box::pin(async move { match fut.await { - Some(MountedReturnData::GetClientRect(rect)) => Ok(rect), - Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( - Box::new(DesktopQueryError::MismatchedReturn), + Ok(Some(rect)) => Ok(rect), + Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), )), - None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( - DesktopQueryError::FailedToQuery, - ))), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } } }) } @@ -66,23 +56,25 @@ impl RenderedElementBacking for DesktopElement { &self, behavior: dioxus_html::ScrollBehavior, ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.ScrollTo({}, {});", + self.id.0, + serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") + ); + let fut = self .query - .new_query( - self.id, - NodeUpdateData::ScrollTo { behavior }, - &self.webview, - ) + .new_query::(&script, &self.webview) .resolve(); Box::pin(async move { match fut.await { - Some(MountedReturnData::ScrollTo(())) => Ok(()), - Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( - Box::new(DesktopQueryError::MismatchedReturn), + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), )), - None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( - DesktopQueryError::FailedToQuery, - ))), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } } }) } @@ -91,116 +83,41 @@ impl RenderedElementBacking for DesktopElement { &self, focus: bool, ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.SetFocus({}, {});", + self.id.0, focus + ); + + println!("script: {}", script); let fut = self .query - .new_query(self.id, NodeUpdateData::SetFocus { focus }, &self.webview) + .new_query::(&script, &self.webview) .resolve(); + println!("fut"); + Box::pin(async move { match fut.await { - Some(MountedReturnData::SetFocus(())) => Ok(()), - Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( - Box::new(DesktopQueryError::MismatchedReturn), + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), )), - None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new( - DesktopQueryError::FailedToQuery, - ))), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } } }) } } -#[derive(Default, Clone)] -struct SharedSlab { - slab: Rc>>, -} - -#[derive(Clone)] -pub(crate) struct QueryEngine { - sender: Rc>, - active_requests: SharedSlab, -} - -impl Default for QueryEngine { - fn default() -> Self { - let (sender, _) = tokio::sync::broadcast::channel(8); - Self { - sender: Rc::new(sender), - active_requests: SharedSlab::default(), - } - } -} - -impl QueryEngine { - fn new_query(&self, id: ElementId, update: NodeUpdateData, webview: &WebView) -> Query { - let request_id = self.active_requests.slab.borrow_mut().insert(()); - - let update = NodeUpdate { - id: id.0 as u32, - request_id, - data: update, - }; - - // start the query - webview - .evaluate_script(&format!( - "window.interpreter.handleNodeUpdate({})", - serde_json::to_string(&update).unwrap() - )) - .unwrap(); - - Query { - slab: self.active_requests.clone(), - id: request_id, - reciever: self.sender.subscribe(), - } - } - - pub fn send(&self, data: MountedReturn) { - self.sender.send(data).unwrap(); - } -} - -struct Query { - slab: SharedSlab, - id: usize, - reciever: tokio::sync::broadcast::Receiver, -} - -impl Query { - async fn resolve(mut self) -> Option { - let result = loop { - match self.reciever.recv().await { - Ok(result) => { - if result.id == self.id { - break result.data; - } - } - Err(_) => { - break None; - } - } - }; - - // Remove the query from the slab - self.slab.slab.borrow_mut().remove(self.id); - - result - } -} - #[derive(Debug)] enum DesktopQueryError { FailedToQuery, - MismatchedReturn, } impl std::fmt::Display for DesktopQueryError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), - DesktopQueryError::MismatchedReturn => { - write!(f, "The return type did not match the query") - } } } } diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index 7a92bedf5..1e11cad18 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -1,36 +1,32 @@ use std::rc::Rc; +use crate::query::Query; +use crate::query::QueryError; use crate::use_window; use dioxus_core::ScopeState; -use serde::de::Error; use std::future::Future; use std::future::IntoFuture; use std::pin::Pin; /// A future that resolves to the result of a JavaScript evaluation. pub struct EvalResult { - pub(crate) broadcast: tokio::sync::broadcast::Sender, + pub(crate) query: Query, } impl EvalResult { - pub(crate) fn new(sender: tokio::sync::broadcast::Sender) -> Self { - Self { broadcast: sender } + pub(crate) fn new(query: Query) -> Self { + Self { query } } } impl IntoFuture for EvalResult { - type Output = Result; + type Output = Result; - type IntoFuture = Pin>>>; + type IntoFuture = Pin>>>; fn into_future(self) -> Self::IntoFuture { - Box::pin(async move { - let mut reciever = self.broadcast.subscribe(); - match reciever.recv().await { - Ok(result) => Ok(result), - Err(_) => Err(serde_json::Error::custom("No result returned")), - } - }) as Pin>>> + Box::pin(self.query.resolve()) + as Pin>>> } } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 079331fdb..8e92d90aa 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -10,17 +10,19 @@ mod escape; mod eval; mod events; mod protocol; +mod query; mod shortcut; mod waker; mod webview; +use crate::query::QueryResult; pub use cfg::Config; pub use desktop_context::{ use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId, }; use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers}; use dioxus_core::*; -use dioxus_html::{HtmlEvent, MountedData, MountedReturn}; +use dioxus_html::{HtmlEvent, MountedData}; use element::DesktopElement; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; @@ -259,30 +261,21 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) send_edits(view.dom.render_immediate(), &view.webview); } - EventData::Ipc(msg) if msg.method() == "node_update" => { + // When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query + EventData::Ipc(msg) if msg.method() == "query" => { let params = msg.params(); - println!("node_update: {:?}", params); - // check for a mounted event - let evt = match serde_json::from_value::(params) { - Ok(value) => value, - Err(err) => { - println!("node_update: {:?}", err); - return; - } - }; + if let Ok(result) = dbg!(serde_json::from_value::(params)) { + let view = webviews.get(&event.1).unwrap(); + let query = view + .dom + .base_scope() + .consume_context::() + .unwrap() + .query; - let view = webviews.get(&event.1).unwrap(); - let query = view - .dom - .base_scope() - .consume_context::() - .unwrap() - .query; - - println!("node_update: {:?}", evt); - - query.send(evt); + query.send(result); + } } EventData::Ipc(msg) if msg.method() == "initialize" => { @@ -290,21 +283,6 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) send_edits(view.dom.rebuild(), &view.webview); } - // When the webview chirps back with the result of the eval, we send it to the active receiver - // - // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once, - // you might the wrong result. This should be fixed - EventData::Ipc(msg) if msg.method() == "eval_result" => { - webviews[&event.1] - .dom - .base_scope() - .consume_context::() - .unwrap() - .eval - .send(msg.params()) - .unwrap(); - } - EventData::Ipc(msg) if msg.method() == "browser_open" => { if let Some(temp) = msg.params().as_object() { if temp.contains_key("href") { diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs new file mode 100644 index 000000000..8b0cf3fc3 --- /dev/null +++ b/packages/desktop/src/query.rs @@ -0,0 +1,105 @@ +use std::{cell::RefCell, rc::Rc}; + +use serde::{de::DeserializeOwned, Deserialize}; +use serde_json::Value; +use slab::Slab; +use thiserror::Error; +use tokio::sync::broadcast::error::RecvError; +use wry::webview::WebView; + +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + pub fn new_query(&self, script: &str, webview: &WebView) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + // start the query + // We embed the return of the eval in a function so we can send it back to the main thread + if let Err(err) = webview.evaluate_script(&format!( + r#"window.ipc.postMessage( + JSON.stringify({{ + "method":"query", + "params": {{ + "id": {request_id}, + "data": (function(){{{script}}})() + }} + }}) + );"# + )) { + log::warn!("Query error: {err}"); + } + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + phantom: std::marker::PhantomData, + } + } + + pub fn send(&self, data: QueryResult) { + let _ = self.sender.send(data); + } +} + +pub(crate) struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, + phantom: std::marker::PhantomData, +} + +impl Query { + pub async fn resolve(mut self) -> Result { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break V::deserialize(result.data).map_err(QueryError::DeserializeError); + } + } + Err(err) => { + break Err(QueryError::RecvError(err)); + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Error, Debug)] +pub enum QueryError { + #[error("Error receiving query result: {0}")] + RecvError(RecvError), + #[error("Error deserializing query result: {0}")] + DeserializeError(serde_json::Error), +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct QueryResult { + id: usize, + data: Value, +} diff --git a/packages/html/src/events/mounted.rs b/packages/html/src/events/mounted.rs index 548359b5c..293b51b9e 100644 --- a/packages/html/src/events/mounted.rs +++ b/packages/html/src/events/mounted.rs @@ -79,13 +79,16 @@ impl MountedData { } /// Scroll to make the element visible - pub async fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> { - self.inner.scroll_to(behavior).await + pub fn scroll_to( + &self, + behavior: ScrollBehavior, + ) -> Pin>>> { + self.inner.scroll_to(behavior) } /// Set the focus on the element - pub async fn set_focus(&self, focus: bool) -> MountedResult<()> { - self.inner.set_focus(focus).await + pub fn set_focus(&self, focus: bool) -> Pin>>> { + self.inner.set_focus(focus) } } diff --git a/packages/html/src/transit.rs b/packages/html/src/transit.rs index 12c35bd28..ef0e7c70d 100644 --- a/packages/html/src/transit.rs +++ b/packages/html/src/transit.rs @@ -2,7 +2,6 @@ use std::{any::Any, rc::Rc}; use crate::events::*; use dioxus_core::ElementId; -use euclid::Rect; use serde::{Deserialize, Serialize}; #[derive(Serialize, Debug, Clone, PartialEq)] @@ -221,49 +220,3 @@ fn test_back_and_forth() { assert_eq!(data, p); } - -/// Message to update a node to support MountedData -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct NodeUpdate { - /// The id of the node to update - pub id: u32, - /// The id of the request - pub request_id: usize, - /// The data to update the node with - pub data: NodeUpdateData, -} - -/// Message to update a node to support MountedData -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] -pub enum NodeUpdateData { - SetFocus { focus: bool }, - GetClientRect {}, - ScrollTo { behavior: ScrollBehavior }, -} - -/// The result of a element query -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub struct MountedReturn { - /// A unique id for the query - pub id: usize, - /// The result of the query - pub data: Option, -} - -/// The data of a element query -#[derive(Debug, Clone, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] -pub enum MountedReturnData { - SetFocus(()), - GetClientRect(Rect), - ScrollTo(()), -} diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index ce2a010c0..2c919dcde 100644 --- a/packages/interpreter/src/interpreter.js +++ b/packages/interpreter/src/interpreter.js @@ -227,51 +227,26 @@ class Interpreter { ScrollTo(id, behavior) { const node = this.nodes[id]; if (!node) { - return; + return false; } node.scrollIntoView({ behavior: behavior }); - return { - type: "ScrollTo", - }; + return true; } /// Set the focus on the element SetFocus(id, focus) { const node = this.nodes[id]; if (!node) { - return; + return false; } if (focus) { node.focus(); } else { node.blur(); } - return { - type: "SetFocus", - }; - } - - handleNodeUpdate(edit) { - let data; - switch (edit.data.type) { - case "SetFocus": - data = this.SetFocus(edit.id, edit.data.focus); - break; - case "ScrollTo": - data = this.ScrollTo(edit.id, edit.data.behavior); - break; - case "GetClientRect": - data = this.GetClientRect(edit.id); - break; - } - window.ipc.postMessage( - serializeIpcMessage("node_update", { - id: edit.request_id, - data: data - }) - ); + return true; } handleEdits(edits) { From 8243dfe00dceff7c0ee49d5312d1cbf5a15a40b6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 16:52:29 -0500 Subject: [PATCH 06/50] implement a query engine for liveview --- packages/desktop/src/query.rs | 5 ++ packages/liveview/Cargo.toml | 2 + packages/liveview/src/element.rs | 125 +++++++++++++++++++++++++++++++ packages/liveview/src/lib.rs | 2 + packages/liveview/src/main.js | 16 +++- packages/liveview/src/pool.rs | 76 ++++++++++++++++--- packages/liveview/src/query.rs | 113 ++++++++++++++++++++++++++++ 7 files changed, 323 insertions(+), 16 deletions(-) create mode 100644 packages/liveview/src/element.rs create mode 100644 packages/liveview/src/query.rs diff --git a/packages/desktop/src/query.rs b/packages/desktop/src/query.rs index 8b0cf3fc3..bd8b2ee2a 100644 --- a/packages/desktop/src/query.rs +++ b/packages/desktop/src/query.rs @@ -7,11 +7,13 @@ use thiserror::Error; use tokio::sync::broadcast::error::RecvError; use wry::webview::WebView; +/// Tracks what query ids are currently active #[derive(Default, Clone)] struct SharedSlab { slab: Rc>>, } +/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. #[derive(Clone)] pub(crate) struct QueryEngine { sender: Rc>, @@ -29,6 +31,7 @@ impl Default for QueryEngine { } impl QueryEngine { + /// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id. pub fn new_query(&self, script: &str, webview: &WebView) -> Query { let request_id = self.active_requests.slab.borrow_mut().insert(()); @@ -56,6 +59,7 @@ impl QueryEngine { } } + /// Send a query result pub fn send(&self, data: QueryResult) { let _ = self.sender.send(data); } @@ -69,6 +73,7 @@ pub(crate) struct Query { } impl Query { + /// Resolve the query pub async fn resolve(mut self) -> Result { let result = loop { match self.reciever.recv().await { diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 5269927b0..8b80fedb8 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -14,6 +14,8 @@ license = "MIT/Apache-2.0" [dependencies] thiserror = "1.0.38" +log = "0.4.14" +slab = "0.4" futures-util = { version = "0.3.25", default-features = false, features = [ "sink", ] } diff --git a/packages/liveview/src/element.rs b/packages/liveview/src/element.rs new file mode 100644 index 000000000..21be23bee --- /dev/null +++ b/packages/liveview/src/element.rs @@ -0,0 +1,125 @@ +use dioxus_core::ElementId; +use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::query::QueryEngine; + +/// A mounted element passed to onmounted events +pub struct LiveviewElement { + id: ElementId, + query_tx: UnboundedSender, + query: QueryEngine, +} + +impl LiveviewElement { + pub(crate) fn new(id: ElementId, tx: UnboundedSender, query: QueryEngine) -> Self { + Self { + id, + query_tx: tx, + query, + } + } +} + +impl RenderedElementBacking for LiveviewElement { + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } + + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures_util::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let script = format!("return window.interpreter.GetClientRect({});", self.id.0); + + let fut = self + .query + .new_query::>>(&script, &self.query_tx) + .resolve(); + Box::pin(async move { + match fut.await { + Ok(Some(rect)) => Ok(rect), + Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } + + fn scroll_to( + &self, + behavior: dioxus_html::ScrollBehavior, + ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.ScrollTo({}, {});", + self.id.0, + serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior") + ); + + let fut = self + .query + .new_query::(&script, &self.query_tx) + .resolve(); + Box::pin(async move { + match fut.await { + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } + + fn set_focus( + &self, + focus: bool, + ) -> std::pin::Pin>>> { + let script = format!( + "return window.interpreter.SetFocus({}, {});", + self.id.0, focus + ); + + let fut = self + .query + .new_query::(&script, &self.query_tx) + .resolve(); + + Box::pin(async move { + match fut.await { + Ok(true) => Ok(()), + Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed( + Box::new(DesktopQueryError::FailedToQuery), + )), + Err(err) => { + MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err))) + } + } + }) + } +} + +#[derive(Debug)] +enum DesktopQueryError { + FailedToQuery, +} + +impl std::fmt::Display for DesktopQueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"), + } + } +} + +impl std::error::Error for DesktopQueryError {} diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index ffddf0272..0c6fc9b2a 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -18,7 +18,9 @@ pub mod adapters { pub use adapters::*; +mod element; pub mod pool; +mod query; use futures_util::{SinkExt, StreamExt}; pub use pool::*; diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js index 7dd9758d6..594c764c2 100644 --- a/packages/liveview/src/main.js +++ b/packages/liveview/src/main.js @@ -26,11 +26,19 @@ class IPC { // todo: retry the connection }; - ws.onmessage = (event) => { + ws.onmessage = (message) => { // Ignore pongs - if (event.data != "__pong__") { - let edits = JSON.parse(event.data); - window.interpreter.handleEdits(edits); + if (message.data != "__pong__") { + const event = JSON.parse(message.data); + switch (event.type) { + case "edits": + let edits = event.data; + window.interpreter.handleEdits(edits); + break; + case "query": + Function("Eval", `"use strict";${event.data};`)(); + break; + } } }; diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index e02c8195b..089d0bad4 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -1,8 +1,13 @@ -use crate::LiveViewError; -use dioxus_core::prelude::*; -use dioxus_html::HtmlEvent; +use crate::{ + element::LiveviewElement, + query::{QueryEngine, QueryResult}, + LiveViewError, +}; +use dioxus_core::{prelude::*, Mutations}; +use dioxus_html::{EventData, HtmlEvent, MountedData}; use futures_util::{pin_mut, SinkExt, StreamExt}; -use std::time::Duration; +use serde::Serialize; +use std::{rc::Rc, time::Duration}; use tokio_util::task::LocalPoolHandle; #[derive(Clone)] @@ -115,7 +120,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li }; // todo: use an efficient binary packed format for this - let edits = serde_json::to_string(&vdom.rebuild()).unwrap(); + let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap(); // pin the futures so we can use select! pin_mut!(ws); @@ -123,11 +128,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // send the initial render to the client ws.send(edits).await?; + // Create the a proxy for query engine + let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel(); + let query_engine = QueryEngine::default(); + // desktop uses this wrapper struct thing around the actual event itself // this is sorta driven by tao/wry - #[derive(serde::Deserialize)] - struct IpcMessage { - params: HtmlEvent, + #[derive(serde::Deserialize, Debug)] + #[serde(tag = "method", content = "params")] + enum IpcMessage { + #[serde(rename = "user_event")] + Event(HtmlEvent), + #[serde(rename = "query")] + Query(QueryResult), } loop { @@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li ws.send("__pong__".to_string()).await?; } Some(Ok(evt)) => { - if let Ok(IpcMessage { params }) = serde_json::from_str::(evt) { - vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles); + if let Ok(message) = serde_json::from_str::(evt) { + match message { + IpcMessage::Event(evt) => { + // Intercept the mounted event and insert a custom element type + if let EventData::Mounted = &evt.data { + let element = LiveviewElement::new(evt.element, query_tx.clone(), query_engine.clone()); + vdom.handle_event( + &evt.name, + Rc::new(MountedData::new(element)), + evt.element, + evt.bubbles, + ); + } + else{ + vdom.handle_event( + &evt.name, + evt.data.into_any(), + evt.element, + evt.bubbles, + ); + } + } + IpcMessage::Query(result) => { + query_engine.send(result); + }, + } } } // log this I guess? when would we get an error here? - Some(Err(_e)) => {}, + Some(Err(_e)) => {} None => return Ok(()), } } + // handle any new queries + Some(query) = query_rx.recv() => { + ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?; + } + Some(msg) = hot_reload_wait => { #[cfg(all(feature = "hot-reload", debug_assertions))] match msg{ @@ -176,6 +218,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) .await; - ws.send(serde_json::to_string(&edits).unwrap()).await?; + ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap()) + .await?; } } + +#[derive(Serialize)] +#[serde(tag = "type", content = "data")] +enum ClientUpdate<'a> { + #[serde(rename = "edits")] + Edits(Mutations<'a>), + #[serde(rename = "query")] + Query(String), +} diff --git a/packages/liveview/src/query.rs b/packages/liveview/src/query.rs new file mode 100644 index 000000000..04d531a63 --- /dev/null +++ b/packages/liveview/src/query.rs @@ -0,0 +1,113 @@ +use std::{cell::RefCell, rc::Rc}; + +use serde::{de::DeserializeOwned, Deserialize}; +use serde_json::Value; +use slab::Slab; +use thiserror::Error; +use tokio::sync::{broadcast::error::RecvError, mpsc::UnboundedSender}; + +/// Tracks what query ids are currently active +#[derive(Default, Clone)] +struct SharedSlab { + slab: Rc>>, +} + +/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. +#[derive(Clone)] +pub(crate) struct QueryEngine { + sender: Rc>, + active_requests: SharedSlab, +} + +impl Default for QueryEngine { + fn default() -> Self { + let (sender, _) = tokio::sync::broadcast::channel(8); + Self { + sender: Rc::new(sender), + active_requests: SharedSlab::default(), + } + } +} + +impl QueryEngine { + /// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id. + pub fn new_query( + &self, + script: &str, + tx: &UnboundedSender, + ) -> Query { + let request_id = self.active_requests.slab.borrow_mut().insert(()); + + // start the query + // We embed the return of the eval in a function so we can send it back to the main thread + if let Err(err) = tx.send(format!( + r#"window.ipc.postMessage( + JSON.stringify({{ + "method":"query", + "params": {{ + "id": {request_id}, + "data": (function(){{{script}}})() + }} + }}) + );"# + )) { + log::warn!("Query error: {err}"); + } + + Query { + slab: self.active_requests.clone(), + id: request_id, + reciever: self.sender.subscribe(), + phantom: std::marker::PhantomData, + } + } + + /// Send a query result + pub fn send(&self, data: QueryResult) { + let _ = self.sender.send(data); + } +} + +pub(crate) struct Query { + slab: SharedSlab, + id: usize, + reciever: tokio::sync::broadcast::Receiver, + phantom: std::marker::PhantomData, +} + +impl Query { + /// Resolve the query + pub async fn resolve(mut self) -> Result { + let result = loop { + match self.reciever.recv().await { + Ok(result) => { + if result.id == self.id { + break V::deserialize(result.data).map_err(QueryError::DeserializeError); + } + } + Err(err) => { + break Err(QueryError::RecvError(err)); + } + } + }; + + // Remove the query from the slab + self.slab.slab.borrow_mut().remove(self.id); + + result + } +} + +#[derive(Error, Debug)] +pub enum QueryError { + #[error("Error receiving query result: {0}")] + RecvError(RecvError), + #[error("Error deserializing query result: {0}")] + DeserializeError(serde_json::Error), +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct QueryResult { + id: usize, + data: Value, +} From e0d46d982092074304693917e73cbb3ea0202131 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 23 Mar 2023 17:33:27 -0500 Subject: [PATCH 07/50] implement mounted event for TUI --- packages/tui/src/element.rs | 92 +++++++++++++++++++++++++++++++++++++ packages/tui/src/lib.rs | 10 ++++ 2 files changed, 102 insertions(+) create mode 100644 packages/tui/src/element.rs diff --git a/packages/tui/src/element.rs b/packages/tui/src/element.rs new file mode 100644 index 000000000..5ea1e56af --- /dev/null +++ b/packages/tui/src/element.rs @@ -0,0 +1,92 @@ +use std::{ + fmt::{Display, Formatter}, + rc::Rc, +}; + +use dioxus_core::{ElementId, Mutations, VirtualDom}; +use dioxus_html::{ + geometry::euclid::{Point2D, Rect, Size2D}, + MountedData, MountedError, RenderedElementBacking, +}; + +use crate::query::{ElementRef, Query}; + +pub(crate) fn find_mount_events(mutations: &Mutations) -> Vec { + let mut mount_events = Vec::new(); + for mutation in &mutations.edits { + if let dioxus_core::Mutation::NewEventListener { + name: "mounted", + id, + } = mutation + { + mount_events.push(*id); + } + } + mount_events +} + +pub(crate) fn send_mounted_events(vdom: &mut VirtualDom, mount_events: Vec) { + let query: Query = vdom + .base_scope() + .consume_context() + .expect("Query should be in context"); + for id in mount_events { + let element = TuiElement { + query: query.clone(), + id, + }; + vdom.handle_event("mounted", Rc::new(MountedData::new(element)), id, false); + } +} + +struct TuiElement { + query: Query, + id: ElementId, +} + +impl TuiElement { + pub(crate) fn element(&self) -> ElementRef { + self.query.get(self.id) + } +} + +impl RenderedElementBacking for TuiElement { + fn get_client_rect( + &self, + ) -> std::pin::Pin< + Box< + dyn futures::Future< + Output = dioxus_html::MountedResult>, + >, + >, + > { + let layout = self.element().layout(); + Box::pin(async move { + match layout { + Some(layout) => { + let x = layout.location.x as f64; + let y = layout.location.y as f64; + let width = layout.size.width as f64; + let height = layout.size.height as f64; + Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height))) + } + None => Err(MountedError::OperationFailed(Box::new(TuiElementNotFound))), + } + }) + } + + fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> { + Ok(self) + } +} + +#[derive(Debug)] +struct TuiElementNotFound; + +impl Display for TuiElementNotFound { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "TUI element not found") + } +} + +impl std::error::Error for TuiElementNotFound {} diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 75c865de5..911c7c4db 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -7,6 +7,7 @@ use crossterm::{ }; use dioxus_core::*; use dioxus_native_core::{real_dom::RealDom, FxDashSet, NodeId, NodeMask, SendAnyMap}; +use element::{find_mount_events, send_mounted_events}; use focus::FocusState; use futures::{ channel::mpsc::{UnboundedReceiver, UnboundedSender}, @@ -26,6 +27,7 @@ use tokio::select; use tui::{backend::CrosstermBackend, layout::Rect, Terminal}; mod config; +mod element; mod focus; mod hooks; mod layout; @@ -114,7 +116,11 @@ pub fn launch_cfg_with_props(app: Component, props: Props { let mut rdom = rdom.borrow_mut(); let mutations = dom.rebuild(); + // Search for mount events that happened during the rebuild + let mounted = find_mount_events(&mutations); let (to_update, _) = rdom.apply_mutations(mutations); + // Send the mount events + send_mounted_events(&mut dom, mounted); let mut any_map = SendAnyMap::new(); any_map.insert(taffy.clone()); let _to_rerender = rdom.update_state(to_update, any_map); @@ -307,8 +313,12 @@ fn render_vdom( let mut rdom = rdom.borrow_mut(); let mutations = vdom.render_immediate(); handler.prune(&mutations, &rdom); + // Search for mount events that happened during the rebuild + let mounted = find_mount_events(&mutations); // updates the dom's nodes let (to_update, dirty) = rdom.apply_mutations(mutations); + // Send the mount events + send_mounted_events(vdom, mounted); // update the style and layout let mut any_map = SendAnyMap::new(); any_map.insert(taffy.clone()); From a551c0fcb8e4f5481c49d6d2b4a0ab2ab18081be Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Mar 2023 11:32:42 -0500 Subject: [PATCH 08/50] add examples for onmounted --- examples/controll_focus.rs | 44 +++++++++++++++++++++++++ examples/read_size.rs | 57 +++++++++++++++++++++++++++++++++ examples/scroll_to_top.rs | 33 +++++++++++++++++++ packages/desktop/src/element.rs | 2 -- packages/desktop/src/lib.rs | 2 +- 5 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 examples/controll_focus.rs create mode 100644 examples/read_size.rs create mode 100644 examples/scroll_to_top.rs diff --git a/examples/controll_focus.rs b/examples/controll_focus.rs new file mode 100644 index 000000000..0f14a940b --- /dev/null +++ b/examples/controll_focus.rs @@ -0,0 +1,44 @@ +use std::rc::Rc; + +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let elements: &UseRef>> = use_ref(cx, || Vec::new()); + let running = use_state(cx, || true); + + use_future!(cx, |(elements, running)| async move { + let mut focused = 0; + if *running.current() { + loop { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + if let Some(element) = elements.read().get(focused) { + element.set_focus(true); + } else { + focused = 0; + } + focused += 1; + } + } + }); + + cx.render(rsx!( + div { + h1 { "Input Roulette" } + for i in 0..100 { + input { + value: "{i}", + onmounted: move |cx| { + elements.write().push(cx.inner().clone()); + }, + oninput: move |_| { + running.set(false); + } + } + } + } + )) +} diff --git a/examples/read_size.rs b/examples/read_size.rs new file mode 100644 index 000000000..be996e2dd --- /dev/null +++ b/examples/read_size.rs @@ -0,0 +1,57 @@ +use std::rc::Rc; + +use dioxus::{html::geometry::euclid::Rect, prelude::*}; + +fn main() { + dioxus_desktop::launch_cfg( + app, + dioxus_desktop::Config::default().with_custom_head( + r#" + +"# + .to_owned(), + ), + ); +} + +fn app(cx: Scope) -> Element { + let div_element: &UseRef>> = use_ref(cx, || None); + + let dimentions = use_ref(cx, || Rect::zero()); + + cx.render(rsx!( + div { + width: "50%", + height: "50%", + background_color: "red", + onmounted: move |cx| { + div_element.set(Some(cx.inner().clone())); + }, + "This element is {dimentions.read():?}" + } + + button { + onclick: move |_| { + to_owned![div_element, dimentions]; + async move { + if let Some(div) = div_element.read().as_ref() { + if let Ok(rect)=div.get_client_rect().await{ + dimentions.set(rect); + } + } + } + }, + "Read dimentions" + } + )) +} diff --git a/examples/scroll_to_top.rs b/examples/scroll_to_top.rs new file mode 100644 index 000000000..4eb38a451 --- /dev/null +++ b/examples/scroll_to_top.rs @@ -0,0 +1,33 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let header_element = use_ref(cx, || None); + + cx.render(rsx!( + div { + h1 { + onmounted: move |cx| { + header_element.set(Some(cx.inner().clone())); + }, + "Scroll to top example" + } + + for i in 0..100 { + div { "Item {i}" } + } + + button { + onclick: move |_| { + if let Some(header) = header_element.read().as_ref() { + header.scroll_to(ScrollBehavior::Smooth); + } + }, + "Scroll to top" + } + } + )) +} diff --git a/packages/desktop/src/element.rs b/packages/desktop/src/element.rs index e94a481c7..94db28f0b 100644 --- a/packages/desktop/src/element.rs +++ b/packages/desktop/src/element.rs @@ -88,12 +88,10 @@ impl RenderedElementBacking for DesktopElement { self.id.0, focus ); - println!("script: {}", script); let fut = self .query .new_query::(&script, &self.webview) .resolve(); - println!("fut"); Box::pin(async move { match fut.await { diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 8e92d90aa..701c0e0bb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -265,7 +265,7 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) EventData::Ipc(msg) if msg.method() == "query" => { let params = msg.params(); - if let Ok(result) = dbg!(serde_json::from_value::(params)) { + if let Ok(result) = serde_json::from_value::(params) { let view = webviews.get(&event.1).unwrap(); let query = view .dom From c4027c2618228b14dc00a1bbf8c9f690c11c24a3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 26 Mar 2023 07:23:32 -0500 Subject: [PATCH 09/50] fix clippy --- examples/controll_focus.rs | 2 +- examples/read_size.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/controll_focus.rs b/examples/controll_focus.rs index 0f14a940b..4f4c2ddcb 100644 --- a/examples/controll_focus.rs +++ b/examples/controll_focus.rs @@ -7,7 +7,7 @@ fn main() { } fn app(cx: Scope) -> Element { - let elements: &UseRef>> = use_ref(cx, || Vec::new()); + let elements: &UseRef>> = use_ref(cx, Vec::new); let running = use_state(cx, || true); use_future!(cx, |(elements, running)| async move { diff --git a/examples/read_size.rs b/examples/read_size.rs index be996e2dd..806f71f80 100644 --- a/examples/read_size.rs +++ b/examples/read_size.rs @@ -1,3 +1,4 @@ +#![allow(clippy::await_holding_refcell_ref)] use std::rc::Rc; use dioxus::{html::geometry::euclid::Rect, prelude::*}; @@ -27,7 +28,7 @@ fn main() { fn app(cx: Scope) -> Element { let div_element: &UseRef>> = use_ref(cx, || None); - let dimentions = use_ref(cx, || Rect::zero()); + let dimentions = use_ref(cx, Rect::zero); cx.render(rsx!( div { @@ -44,8 +45,10 @@ fn app(cx: Scope) -> Element { onclick: move |_| { to_owned![div_element, dimentions]; async move { - if let Some(div) = div_element.read().as_ref() { - if let Ok(rect)=div.get_client_rect().await{ + let read = div_element.read(); + let client_rect = read.as_ref().map(|el| el.get_client_rect()); + if let Some(client_rect) = client_rect { + if let Ok(rect) = client_rect.await { dimentions.set(rect); } } From 0029fed24a4d40716deaff767a0e17cd7c6baac0 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 26 Mar 2023 11:29:56 -0500 Subject: [PATCH 10/50] create complete tailwind example project with steps to setup tailwind --- Cargo.toml | 2 + examples/tailwind/Cargo.toml | 21 + examples/tailwind/Dioxus.toml | 46 + examples/tailwind/README.md | 136 +++ examples/tailwind/input.css | 3 + examples/tailwind/public/tailwind.css | 833 ++++++++++++++++++ .../{tailwind.rs => tailwind/src/main.rs} | 16 +- examples/tailwind/tailwind.config.js | 9 + 8 files changed, 1055 insertions(+), 11 deletions(-) create mode 100644 examples/tailwind/Cargo.toml create mode 100644 examples/tailwind/Dioxus.toml create mode 100644 examples/tailwind/README.md create mode 100644 examples/tailwind/input.css create mode 100644 examples/tailwind/public/tailwind.css rename examples/{tailwind.rs => tailwind/src/main.rs} (90%) create mode 100644 examples/tailwind/tailwind.config.js diff --git a/Cargo.toml b/Cargo.toml index e32940e18..7b3fc0944 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ members = [ "packages/signals", "packages/hot-reload", "docs/guide", + # Full project examples + "examples/tailwind", ] # This is a "virtual package" diff --git a/examples/tailwind/Cargo.toml b/examples/tailwind/Cargo.toml new file mode 100644 index 000000000..5c2fbe666 --- /dev/null +++ b/examples/tailwind/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "dioxus-tailwind" +version = "0.0.0" +authors = [] +edition = "2021" +description = "A tailwindcss example using Dioxus" +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +documentation = "https://dioxuslabs.com" +rust-version = "1.60.0" +publish = false + +[dependencies] +dioxus = { path = "../../packages/dioxus" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +dioxus-desktop = { path = "../../packages/desktop" } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +dioxus-web = { path = "../../packages/web" } \ No newline at end of file diff --git a/examples/tailwind/Dioxus.toml b/examples/tailwind/Dioxus.toml new file mode 100644 index 000000000..67a63e686 --- /dev/null +++ b/examples/tailwind/Dioxus.toml @@ -0,0 +1,46 @@ +[application] + +# App (Project) Name +name = "Tailwind CSS + Dioxus" + +# Dioxus App Default Platform +# desktop, web, mobile, ssr +default_platform = "web" + +# `build` & `serve` dist path +out_dir = "dist" + +# resource (public) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "dioxus | ⛺" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "public"] + +# include `assets` in web platform +[web.resource] + +# CSS style file +style = ["tailwind.css"] + +# Javascript code file +script = [] + +[web.resource.dev] + +# serve: [dev-server] only + +# CSS style file +style = [] + +# Javascript code file +script = [] diff --git a/examples/tailwind/README.md b/examples/tailwind/README.md new file mode 100644 index 000000000..0498ecbd3 --- /dev/null +++ b/examples/tailwind/README.md @@ -0,0 +1,136 @@ +Example: Basic Tailwind usage + +This example shows how an app might be styled with TailwindCSS. + +# Setup + +1. Install the Dioxus CLI: + +```bash +cargo install --git https://github.com/DioxusLabs/cli +``` + +2. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm +3. Install the tailwind css cli: https://tailwindcss.com/docs/installation +4. Initialize the tailwind css project: + +```bash +npx tailwindcss init +``` + +This should create a `tailwind.config.js` file in the root of the project. + +5. Edit the `tailwind.config.js` file to include rust files: + +```json +module.exports = { + mode: "all", + content: [ + // include all rust, html and css files in the src directory + "./src/**/*.{rs,html,css}", + // include all html files in the output (dist) directory + "./dist/**/*.html", + ], + theme: { + extend: {}, + }, + plugins: [], +} +``` + +6. Create a `input.css` file with the following content: + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +7. Create a `Dioxus.toml` file with the following content that links to the `tailwind.css` file: + +```toml +[application] + +# App (Project) Name +name = "Tailwind CSS + Dioxus" + +# Dioxus App Default Platform +# desktop, web, mobile, ssr +default_platform = "web" + +# `build` & `serve` dist path +out_dir = "dist" + +# resource (public) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "dioxus | ⛺" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "public"] + +# include `assets` in web platform +[web.resource] + +# CSS style file +style = ["tailwind.css"] + +# Javascript code file +script = [] + +[web.resource.dev] + +# serve: [dev-server] only + +# CSS style file +style = [] + +# Javascript code file +script = [] +``` + +## Bonus Steps + +8. Install the tailwind css vs code extension +9. Go to the settings for the extension and find the experimental regex support section. Edit the setting.json file to look like this: + +```json +"tailwindCSS.experimental.classRegex": ["class: \"(.*)\""], +"tailwindCSS.includeLanguages": { + "rust": "html" +}, +``` + +# Development + +1. Run the following command in the root of the project to start the tailwind css compiler: + +```bash +npx tailwindcss -i ./input.css -o ./public/tailwind.css --watch +``` + +## Web + +- Run the following command in the root of the project to start the dioxus dev server: + +```bash +dioxus serve --hot-reload +``` + +- Open the browser to http://localhost:8080 + +## Desktop + +- Launch the dioxus desktop app + +```bash +cargo run +``` diff --git a/examples/tailwind/input.css b/examples/tailwind/input.css new file mode 100644 index 000000000..bd6213e1d --- /dev/null +++ b/examples/tailwind/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/examples/tailwind/public/tailwind.css b/examples/tailwind/public/tailwind.css new file mode 100644 index 000000000..65b95316e --- /dev/null +++ b/examples/tailwind/public/tailwind.css @@ -0,0 +1,833 @@ +/* +! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.ml-3 { + margin-left: 0.75rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.mr-5 { + margin-right: 1.25rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.hidden { + display: none; +} + +.h-10 { + height: 2.5rem; +} + +.h-4 { + height: 1rem; +} + +.w-10 { + width: 2.5rem; +} + +.w-4 { + width: 1rem; +} + +.w-5\/6 { + width: 83.333333%; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-full { + border-radius: 9999px; +} + +.border-0 { + border-width: 0px; +} + +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + +.bg-gray-900 { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); +} + +.bg-indigo-500 { + --tw-bg-opacity: 1; + background-color: rgb(99 102 241 / var(--tw-bg-opacity)); +} + +.object-cover { + -o-object-fit: cover; + object-fit: cover; +} + +.object-center { + -o-object-position: center; + object-position: center; +} + +.p-2 { + padding: 0.5rem; +} + +.p-5 { + padding: 1.25rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-24 { + padding-top: 6rem; + padding-bottom: 6rem; +} + +.text-center { + text-align: center; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-medium { + font-weight: 500; +} + +.leading-relaxed { + line-height: 1.625; +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.hover\:bg-gray-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); +} + +.hover\:bg-indigo-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(79 70 229 / var(--tw-bg-opacity)); +} + +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +@media (min-width: 640px) { + .sm\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } +} + +@media (min-width: 768px) { + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:ml-auto { + margin-left: auto; + } + + .md\:mt-0 { + margin-top: 0px; + } + + .md\:w-1\/2 { + width: 50%; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:items-start { + align-items: flex-start; + } + + .md\:pr-16 { + padding-right: 4rem; + } + + .md\:text-left { + text-align: left; + } +} + +@media (min-width: 1024px) { + .lg\:inline-block { + display: inline-block; + } + + .lg\:w-full { + width: 100%; + } + + .lg\:max-w-lg { + max-width: 32rem; + } + + .lg\:flex-grow { + flex-grow: 1; + } + + .lg\:pr-24 { + padding-right: 6rem; + } +} \ No newline at end of file diff --git a/examples/tailwind.rs b/examples/tailwind/src/main.rs similarity index 90% rename from examples/tailwind.rs rename to examples/tailwind/src/main.rs index af6bc4c4f..2dbb183c9 100644 --- a/examples/tailwind.rs +++ b/examples/tailwind/src/main.rs @@ -1,22 +1,16 @@ #![allow(non_snake_case)] -//! Example: Basic Tailwind usage -//! -//! This example shows how an app might be styled with TailwindCSS. -//! -//! To minify your tailwind bundle, currently you need to use npm. Follow these instructions: -//! -//! https://dev.to/arctic_hen7/how-to-set-up-tailwind-css-with-yew-and-trunk-il9 - use dioxus::prelude::*; -use dioxus_desktop::Config; fn main() { + #[cfg(not(target_arch = "wasm32"))] dioxus_desktop::launch_cfg( app, - Config::new() - .with_custom_head("".to_string()), + dioxus_desktop::Config::new() + .with_custom_head(r#""#.to_string()), ); + #[cfg(target_arch = "wasm32")] + dioxus_web::launch(app); } pub fn app(cx: Scope) -> Element { diff --git a/examples/tailwind/tailwind.config.js b/examples/tailwind/tailwind.config.js new file mode 100644 index 000000000..2a69d5803 --- /dev/null +++ b/examples/tailwind/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + mode: "all", + content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"], + theme: { + extend: {}, + }, + plugins: [], +}; From ca0e1fb9ab61466d61dbff1671612852e6a06497 Mon Sep 17 00:00:00 2001 From: bluekey Date: Sun, 23 Apr 2023 17:15:06 +0200 Subject: [PATCH 11/50] added select element example --- examples/inputs.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/examples/inputs.rs b/examples/inputs.rs index cf848d99e..311afc1f0 100644 --- a/examples/inputs.rs +++ b/examples/inputs.rs @@ -37,6 +37,7 @@ const FIELDS: &[(&str, &str)] = &[ fn app(cx: Scope) -> Element { cx.render(rsx! { div { margin_left: "30px", + select_example(cx), div { // handling inputs on divs will catch all input events below // so the value of our input event will be either huey, dewey, louie, or true/false (because of the checkboxe) @@ -134,3 +135,34 @@ fn app(cx: Scope) -> Element { } }) } + +fn select_example(cx: Scope) -> Element { + cx.render(rsx! { + div { + select { + id: "selection", + name: "selection", + multiple: true, + oninput: move |evt| { + println!("{evt:?}"); + }, + option { + value : "Option 1", + label : "Option 1", + } + option { + value : "Option 2", + label : "Option 2", + selected : true, + }, + option { + value : "Option 3", + label : "Option 3", + } + } + label { + r#for: "selection", + "select element" + } + }}) +} From 61dba2825cf9430c8ebd0e6bdaeaa6fe2c2a4d7d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 24 Apr 2023 11:16:53 -0500 Subject: [PATCH 12/50] Improve the custom hooks chapter --- docs/guide/Cargo.toml | 6 +- docs/guide/examples/hooks_anti_patterns.rs | 38 +++++++++++ docs/guide/examples/hooks_composed.rs | 54 +++++++++++++++ docs/guide/examples/hooks_custom_logic.rs | 57 ++++++++++++++++ .../src/en/interactivity/custom_hooks.md | 65 +++++++++++++++++++ 5 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 docs/guide/examples/hooks_anti_patterns.rs create mode 100644 docs/guide/examples/hooks_custom_logic.rs diff --git a/docs/guide/Cargo.toml b/docs/guide/Cargo.toml index 227bdfa35..3919dc11c 100644 --- a/docs/guide/Cargo.toml +++ b/docs/guide/Cargo.toml @@ -16,12 +16,12 @@ dioxus-native-core-macro = { path = "../../packages/native-core-macro" } dioxus-router = { path = "../../packages/router" } dioxus-liveview = { path = "../../packages/liveview", features = ["axum"] } dioxus-tui = { path = "../../packages/dioxus-tui" } -fermi = { path = "../../packages/fermi" } -shipyard = "0.6.2" - # dioxus = { path = "../../packages/dioxus", features = ["desktop", "web", "ssr", "router", "fermi", "tui"] } +fermi = { path = "../../packages/fermi" } +shipyard = "0.6.2" serde = { version = "1.0.138", features=["derive"] } reqwest = { version = "0.11.11", features = ["json"] } tokio = { version = "1.19.2" , features=[]} axum = { version = "0.6.1", features = ["ws"] } +gloo-storage = "0.2.2" diff --git a/docs/guide/examples/hooks_anti_patterns.rs b/docs/guide/examples/hooks_anti_patterns.rs new file mode 100644 index 000000000..2119b8601 --- /dev/null +++ b/docs/guide/examples/hooks_anti_patterns.rs @@ -0,0 +1,38 @@ +#![allow(unused)] + +use dioxus::prelude::*; + +fn main() {} + +// ANCHOR: non_clone_state +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +struct UseState<'a, T> { + value: &'a RefCell, + update: Arc, +} + +fn my_use_state(cx: &ScopeState, init: impl FnOnce() -> T) -> UseState { + // The update function will trigger a re-render in the component cx is attached to + let update = cx.schedule_update(); + // Create the initial state + let value = cx.use_hook(|| RefCell::new(init())); + + UseState { value, update } +} + +impl UseState<'_, T> { + fn get(&self) -> T { + self.value.borrow().clone() + } + + fn set(&self, value: T) { + // Update the state + *self.value.borrow_mut() = value; + // Trigger a re-render on the component the state is from + (self.update)(); + } +} +// ANCHOR_END: non_clone_state diff --git a/docs/guide/examples/hooks_composed.rs b/docs/guide/examples/hooks_composed.rs index 801474aa3..782a19d7d 100644 --- a/docs/guide/examples/hooks_composed.rs +++ b/docs/guide/examples/hooks_composed.rs @@ -11,3 +11,57 @@ fn use_settings(cx: &ScopeState) -> &UseSharedState { use_shared_state::(cx).expect("App settings not provided") } // ANCHOR_END: wrap_context + +// ANCHOR: use_storage +use gloo_storage::{LocalStorage, Storage}; +use serde::{de::DeserializeOwned, Serialize}; + +/// A persistent storage hook that can be used to store data across application reloads. +#[allow(clippy::needless_return)] +pub fn use_persistent( + cx: &ScopeState, + // A unique key for the storage entry + key: impl ToString, + // A function that returns the initial value if the storage entry is empty + init: impl FnOnce() -> T, +) -> &UsePersistent { + // Use the use_ref hook to create a mutable state for the storage entry + let state = use_ref(cx, move || { + // This closure will run when the hook is created + let key = key.to_string(); + let value = LocalStorage::get(key.as_str()).ok().unwrap_or_else(init); + StorageEntry { key, value } + }); + + // Wrap the state in a new struct with a custom API + // Note: We use use_hook here so that this hook is easier to use in closures in the rsx. Any values with the same lifetime as the ScopeState can be used in the closure without cloning. + cx.use_hook(|| UsePersistent { + inner: state.clone(), + }) +} + +struct StorageEntry { + key: String, + value: T, +} + +/// Storage that persists across application reloads +pub struct UsePersistent { + inner: UseRef>, +} + +impl UsePersistent { + /// Returns a reference to the value + pub fn get(&self) -> T { + self.inner.read().value.clone() + } + + /// Sets the value + pub fn set(&self, value: T) { + let mut inner = self.inner.write(); + // Write the new value to local storage + LocalStorage::set(inner.key.as_str(), &value); + inner.value = value; + } +} +// ANCHOR_END: use_storage diff --git a/docs/guide/examples/hooks_custom_logic.rs b/docs/guide/examples/hooks_custom_logic.rs new file mode 100644 index 000000000..2323ddb11 --- /dev/null +++ b/docs/guide/examples/hooks_custom_logic.rs @@ -0,0 +1,57 @@ +#![allow(unused)] + +use dioxus::prelude::*; + +fn main() {} + +// ANCHOR: use_state +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +#[derive(Clone)] +struct UseState { + value: Rc>, + update: Arc, +} + +fn my_use_state(cx: &ScopeState, init: impl FnOnce() -> T) -> &UseState { + cx.use_hook(|| { + // The update function will trigger a re-render in the component cx is attached to + let update = cx.schedule_update(); + // Create the initial state + let value = Rc::new(RefCell::new(init())); + + UseState { value, update } + }) +} + +impl UseState { + fn get(&self) -> T { + self.value.borrow().clone() + } + + fn set(&self, value: T) { + // Update the state + *self.value.borrow_mut() = value; + // Trigger a re-render on the component the state is from + (self.update)(); + } +} +// ANCHOR_END: use_state + +// ANCHOR: use_context +pub fn use_context(cx: &ScopeState) -> Option<&T> { + cx.use_hook(|| cx.consume_context::()).as_ref() +} + +pub fn use_context_provider(cx: &ScopeState, f: impl FnOnce() -> T) -> &T { + cx.use_hook(|| { + let val = f(); + // Provide the context state to the scope + cx.provide_context(val.clone()); + val + }) +} + +// ANCHOR_END: use_context diff --git a/docs/guide/src/en/interactivity/custom_hooks.md b/docs/guide/src/en/interactivity/custom_hooks.md index 07472bb95..f3acec269 100644 --- a/docs/guide/src/en/interactivity/custom_hooks.md +++ b/docs/guide/src/en/interactivity/custom_hooks.md @@ -2,6 +2,8 @@ Hooks are a great way to encapsulate business logic. If none of the existing hooks work for your problem, you can write your own. +When writing your hook, you can make a function that accepts `cx: &ScopeState` as a parameter to accept a scope with any Props. + ## Composing Hooks To avoid repetition, you can encapsulate business logic based on existing hooks to create a new hook. @@ -12,6 +14,12 @@ For example, if many components need to access an `AppSettings` struct, you can {{#include ../../../examples/hooks_composed.rs:wrap_context}} ``` +Or if you want to wrap a hook that persists reloads with the storage API, you can build on top of the use_ref hook to work with mutable state: + +```rust +{{#include ../../../examples/hooks_composed.rs:use_storage}} +``` + ## Custom Hook Logic You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.use_hook) to build your own hooks. In fact, this is what all the standard hooks are built on! @@ -23,4 +31,61 @@ You can use [`cx.use_hook`](https://docs.rs/dioxus/latest/dioxus/prelude/struct. Inside the initialization closure, you will typically make calls to other `cx` methods. For example: - The `use_state` hook tracks state in the hook value, and uses [`cx.schedule_update`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.schedule_update) to make Dioxus re-render the component whenever it changes. + +Here is a simplified implementation of the `use_state` hook: + +```rust +{{#include ../../../examples/hooks_custom_logic.rs:use_state}} +``` + - The `use_context` hook calls [`cx.consume_context`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html#method.consume_context) (which would be expensive to call on every render) to get some context from the scope + +Here is an implementation of the `use_context` and `use_context_provider` hooks: + +```rust +{{#include ../../../examples/hooks_custom_logic.rs:use_context}} +``` + +## Hook Anti-Patterns + +When writing a custom hook, you should avoid the following anti-patterns: + +- !Clone Hooks: To allow hooks to be used within async blocks, the hooks must be Clone. To make a hook clone, you can wrap data in Rc or Arc and avoid lifetimes in hooks. + +This version of use_state may seem more efficient, but it is not cloneable: + +```rust +{{#include ../../../examples/hooks_anti_patterns.rs:non_clone_state}} +``` + +If we try to use this hook in an async block, we will get a compile error: + +```rust +fn FutureComponent(cx: &ScopeState) -> Element { + let my_state = my_use_state(cx, || 0); + cx.spawn({ + to_owned![my_state]; + async move { + my_state.set(1); + } + }); + + todo!() +} +``` + +But with the original version, we can use it in an async block: + +```rust +fn FutureComponent(cx: &ScopeState) -> Element { + let my_state = use_state(cx, || 0); + cx.spawn({ + to_owned![my_state]; + async move { + my_state.set(1); + } + }); + + todo!() +} +``` From f381e56ab396542bb4955c9afe3875dc67707b6b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 27 Apr 2023 16:38:09 -0500 Subject: [PATCH 13/50] test scafolding --- packages/desktop/Cargo.toml | 6 ++++++ packages/desktop/headless_tests/main.rs | 28 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 packages/desktop/headless_tests/main.rs diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 8b41019e1..af5f3e68f 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -57,4 +57,10 @@ hot-reload = ["dioxus-hot-reload"] [dev-dependencies] dioxus-core-macro = { path = "../core-macro" } dioxus-hooks = { path = "../hooks" } +dioxus = { path = "../dioxus" } # image = "0.24.0" # enable this when generating a new desktop image + +[[test]] +name = "headless_tests" +path = "headless_tests/main.rs" +harness = false \ No newline at end of file diff --git a/packages/desktop/headless_tests/main.rs b/packages/desktop/headless_tests/main.rs new file mode 100644 index 000000000..e07cb08c3 --- /dev/null +++ b/packages/desktop/headless_tests/main.rs @@ -0,0 +1,28 @@ +// Check that all events are being forwarded to the mock. +//! This example roughly shows how events are serialized into Rust from JavaScript. +//! +//! There is some conversion happening when input types are checkbox/radio/select/textarea etc. + +use dioxus::prelude::*; + +mod events; + +fn main() { + events::test_events(); +} + +pub(crate) fn check_app_exits(app: Component) { + // This is a deadman's switch to ensure that the app exits + let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let should_panic_clone = should_panic.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(100)); + if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { + panic!("App did not exit successfully") + } + }); + + dioxus_desktop::launch(app); + + should_panic.store(false, std::sync::atomic::Ordering::SeqCst); +} From 01d673e6544c26395d8d719c19a507af2c95f611 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 27 Apr 2023 18:00:43 -0500 Subject: [PATCH 14/50] create event tests --- packages/desktop/Cargo.toml | 2 +- packages/desktop/headless_tests/events.rs | 290 ++++++++++++++++++++++ packages/desktop/headless_tests/main.rs | 4 +- 3 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 packages/desktop/headless_tests/events.rs diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index af5f3e68f..e8812df5a 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -58,7 +58,7 @@ hot-reload = ["dioxus-hot-reload"] dioxus-core-macro = { path = "../core-macro" } dioxus-hooks = { path = "../hooks" } dioxus = { path = "../dioxus" } -# image = "0.24.0" # enable this when generating a new desktop image +exitcode = "1.1.2" [[test]] name = "headless_tests" diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs new file mode 100644 index 000000000..34e29f024 --- /dev/null +++ b/packages/desktop/headless_tests/events.rs @@ -0,0 +1,290 @@ +use crate::check_app_exits; +use dioxus::prelude::*; +use dioxus_desktop::DesktopContext; +use dioxus::html::geometry::euclid::Vector3D; + +pub fn test_events() { + check_app_exits(app); +} + +fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) { + use_effect(cx, (), |_| { + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + async move { + desktop_context.eval(&format!( + r#"let element = document.getElementById('{}'); + // Dispatch a synthetic event + const event = {}; + element.dispatchEvent(event); + "#, + id, value + )); + } + }); +} + +#[allow(deprecated)] +fn app(cx: Scope) -> Element { + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + let recieved_events = use_state(cx, || 0); + + // button + mock_event( + &cx, + "button", + r#"new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + button: 0, + })"#, + ); + // mouse_move_div + mock_event( + &cx, + "mouse_move_div", + r#"new MouseEvent("mousemove", { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + })"#, + ); + // mouse_click_div + mock_event( + &cx, + "mouse_click_div", + r#"new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + button: 2, + })"# + ); + // mouse_dblclick_div + mock_event( + &cx, + "mouse_dblclick_div", + r#"new MouseEvent("dblclick", { + view: window, + bubbles: true, + cancelable: true, + buttons: 1|2, + button: 2, + })"#, + ); + // mouse_down_div + mock_event( + &cx, + "mouse_down_div", + r#"new MouseEvent("mousedown", { + view: window, + bubbles: true, + cancelable: true, + buttons: 2, + button: 2, + })"#, + ); + // mouse_up_div + mock_event( + &cx, + "mouse_up_div", + r#"new MouseEvent("mouseup", { + view: window, + bubbles: true, + cancelable: true, + buttons: 0, + button: 0, + })"#, + ); + // wheel_div + mock_event( + &cx, + "wheel_div", + r#"new WheelEvent("wheel", { + deltaX: 1.0, + deltaY: 2.0, + deltaZ: 3.0, + })"#, + ); + // key_down_div + mock_event( + &cx, + "key_down_div", + r#"new KeyboardEvent("keydown", { + key: "a", + code: "KeyA", + location: 0, + repeat: true, + })"#, + ); + // key_up_div + mock_event( + &cx, + "key_up_div", + r#"new KeyboardEvent("keyup", { + key: "a", + code: "KeyA", + location: 0, + repeat: false, + })"#, + ); + // key_press_div + mock_event( + &cx, + "key_press_div", + r#"new KeyboardEvent("keypress", { + key: "a", + code: "KeyA", + location: 0, + repeat: false, + })"#, + ); + // focus_in_div + mock_event( + &cx, + "focus_in_div", + r#"new FocusEvent("focusin")"#, + ); + // focus_out_div + mock_event( + &cx, + "focus_out_div", + r#"new FocusEvent("focusout")"#, + ); + + + if **recieved_events == 12 { + desktop_context.close(); + } + + cx.render(rsx! { + div { + button { + id: "button", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + recieved_events.modify(|x| *x + 1) + }, + } + div { + id: "mouse_move_div", + onmousemove: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + }, + } + div { + id: "mouse_click_div", + onclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + }, + } + div{ + id: "mouse_dblclick_div", + ondblclick: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Primary)); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "mouse_down_div", + onmousedown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().contains(dioxus_html::input_data::MouseButton::Secondary)); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Secondary)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "mouse_up_div", + onmouseup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert!(event.data.held_buttons().is_empty()); + assert_eq!(event.data.trigger_button(), Some(dioxus_html::input_data::MouseButton::Primary)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "wheel_div", + onwheel: move |event| { + println!("{:?}", event.data); + let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{ + panic!("Expected delta to be in pixels") + }; + assert_eq!(delta, Vector3D::new(1.0, 2.0, 3.0)); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "key_down_div", + onkeydown: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location, 0); + assert!(event.data.is_auto_repeating()); + + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "key_up_div", + onkeyup: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location, 0); + assert!(!event.data.is_auto_repeating()); + + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "key_press_div", + onkeypress: move |event| { + println!("{:?}", event.data); + assert!(event.data.modifiers().is_empty()); + assert_eq!(event.data.key().to_string(), "a"); + assert_eq!(event.data.code().to_string(), "KeyA"); + assert_eq!(event.data.location, 0); + assert!(!event.data.is_auto_repeating()); + + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "focus_in_div", + onfocusin: move |event| { + println!("{:?}", event.data); + recieved_events.modify(|x| *x + 1) + } + } + div{ + id: "focus_out_div", + onfocusout: move |event| { + println!("{:?}", event.data); + recieved_events.modify(|x| *x + 1) + } + } + } + }) +} diff --git a/packages/desktop/headless_tests/main.rs b/packages/desktop/headless_tests/main.rs index e07cb08c3..65b6c80a3 100644 --- a/packages/desktop/headless_tests/main.rs +++ b/packages/desktop/headless_tests/main.rs @@ -16,9 +16,9 @@ pub(crate) fn check_app_exits(app: Component) { let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let should_panic_clone = should_panic.clone(); std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(100)); + std::thread::sleep(std::time::Duration::from_secs(10)); if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { - panic!("App did not exit successfully") + std::process::exit(exitcode::SOFTWARE); } }); From 70687748d3577449d101e41a13a033c6cc76aa24 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 27 Apr 2023 18:36:28 -0500 Subject: [PATCH 15/50] fix events test --- packages/desktop/headless_tests/events.rs | 55 ++++++++++++++++++++--- packages/desktop/headless_tests/main.rs | 2 +- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index 34e29f024..60962c868 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -11,10 +11,12 @@ fn mock_event(cx: &ScopeState, id: &'static str, value: &'static str) { use_effect(cx, (), |_| { let desktop_context: DesktopContext = cx.consume_context().unwrap(); async move { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; desktop_context.eval(&format!( r#"let element = document.getElementById('{}'); // Dispatch a synthetic event const event = {}; + console.log(element, event); element.dispatchEvent(event); "#, id, value @@ -103,9 +105,12 @@ fn app(cx: Scope) -> Element { &cx, "wheel_div", r#"new WheelEvent("wheel", { + view: window, deltaX: 1.0, deltaY: 2.0, deltaZ: 3.0, + deltaMode: 0x00, + bubbles: true, })"#, ); // key_down_div @@ -117,6 +122,17 @@ fn app(cx: Scope) -> Element { code: "KeyA", location: 0, repeat: true, + keyCode: 65, + charCode: 97, + char: "a", + charCode: 0, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + isComposing: false, + which: 65, + bubbles: true, })"#, ); // key_up_div @@ -128,6 +144,17 @@ fn app(cx: Scope) -> Element { code: "KeyA", location: 0, repeat: false, + keyCode: 65, + charCode: 97, + char: "a", + charCode: 0, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + isComposing: false, + which: 65, + bubbles: true, })"#, ); // key_press_div @@ -139,19 +166,30 @@ fn app(cx: Scope) -> Element { code: "KeyA", location: 0, repeat: false, + keyCode: 65, + charCode: 97, + char: "a", + charCode: 0, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + isComposing: false, + which: 65, + bubbles: true, })"#, ); // focus_in_div mock_event( &cx, "focus_in_div", - r#"new FocusEvent("focusin")"#, + r#"new FocusEvent("focusin", {bubbles: true})"#, ); // focus_out_div mock_event( &cx, "focus_out_div", - r#"new FocusEvent("focusout")"#, + r#"new FocusEvent("focusout",{bubbles: true})"#, ); @@ -223,6 +261,9 @@ fn app(cx: Scope) -> Element { } div{ id: "wheel_div", + width: "100px", + height: "100px", + background_color: "red", onwheel: move |event| { println!("{:?}", event.data); let dioxus_html::geometry::WheelDelta::Pixels(delta)= event.data.delta()else{ @@ -232,7 +273,7 @@ fn app(cx: Scope) -> Element { recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "key_down_div", onkeydown: move |event| { println!("{:?}", event.data); @@ -245,7 +286,7 @@ fn app(cx: Scope) -> Element { recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "key_up_div", onkeyup: move |event| { println!("{:?}", event.data); @@ -258,7 +299,7 @@ fn app(cx: Scope) -> Element { recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "key_press_div", onkeypress: move |event| { println!("{:?}", event.data); @@ -271,14 +312,14 @@ fn app(cx: Scope) -> Element { recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "focus_in_div", onfocusin: move |event| { println!("{:?}", event.data); recieved_events.modify(|x| *x + 1) } } - div{ + input{ id: "focus_out_div", onfocusout: move |event| { println!("{:?}", event.data); diff --git a/packages/desktop/headless_tests/main.rs b/packages/desktop/headless_tests/main.rs index 65b6c80a3..a9d9ec61c 100644 --- a/packages/desktop/headless_tests/main.rs +++ b/packages/desktop/headless_tests/main.rs @@ -16,7 +16,7 @@ pub(crate) fn check_app_exits(app: Component) { let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let should_panic_clone = should_panic.clone(); std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(10)); + std::thread::sleep(std::time::Duration::from_secs(100)); if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { std::process::exit(exitcode::SOFTWARE); } From 3317dc3e177e80ff62ba1b3a5f66e942d222f051 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 29 Apr 2023 07:58:43 -0500 Subject: [PATCH 16/50] fix the link to tui widgets in the guide --- docs/guide/src/en/custom_renderer/index.md | 2 +- docs/guide/src/en/getting_started/tui.md | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/guide/src/en/custom_renderer/index.md b/docs/guide/src/en/custom_renderer/index.md index 2d4ddb759..b531593dc 100644 --- a/docs/guide/src/en/custom_renderer/index.md +++ b/docs/guide/src/en/custom_renderer/index.md @@ -15,7 +15,7 @@ Essentially, your renderer needs to process edits and generate events to update Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves. -For reference, check out the [javascript interpreter](https://github.com/DioxusLabs/dioxus/tree/master/packages/interpreter) or [tui renderer](https://github.com/DioxusLabs/dioxus/tree/master/packages/tui) as a starting point for your custom renderer. +For reference, check out the [javascript interpreter](https://github.com/DioxusLabs/dioxus/tree/master/packages/interpreter) or [tui renderer](https://github.com/DioxusLabs/dioxus/tree/master/packages/dioxus-tui) as a starting point for your custom renderer. ## Templates diff --git a/docs/guide/src/en/getting_started/tui.md b/docs/guide/src/en/getting_started/tui.md index e49cb84a8..4e8d36cf9 100644 --- a/docs/guide/src/en/getting_started/tui.md +++ b/docs/guide/src/en/getting_started/tui.md @@ -12,14 +12,12 @@ TUI support is currently quite experimental. But, if you're willing to venture i - It uses flexbox for the layout - It only supports a subset of the attributes and elements -- Regular widgets will not work in the tui render, but the tui renderer has its own widget components that start with a capital letter. See the [widgets example](https://github.com/DioxusLabs/dioxus/blob/master/packages/tui/examples/widgets.rs) +- Regular widgets will not work in the tui render, but the tui renderer has its own widget components that start with a capital letter. See the [widgets example](https://github.com/DioxusLabs/dioxus/blob/master/packages/dioxus-tui/examples/widgets.rs) - 1px is one character line height. Your regular CSS px does not translate - If your app panics, your terminal is wrecked. This will be fixed eventually - ## Getting Set up - Start by making a new package and adding Dioxus and the TUI renderer as dependancies. ```shell From 74cb70590c1b50d8f9bbaf82411e6c3c8a8223e0 Mon Sep 17 00:00:00 2001 From: stephenandary Date: Sat, 29 Apr 2023 11:44:28 -0400 Subject: [PATCH 17/50] WIP - Initial commit of devcontainer --- .devcontainer/devcontainer.json | 28 +++++++++++++++++++ .../linux/devcontainer-feature.json | 4 +++ .devcontainer/local-features/linux/install.sh | 6 ++++ 3 files changed, 38 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/local-features/linux/devcontainer-feature.json create mode 100644 .devcontainer/local-features/linux/install.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..1c5278332 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "dioxus", + "image": "mcr.microsoft.com/devcontainers/rust:latest", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "./local-features/linux": {} + }, + "containerEnv": { + "RUST_LOG": "INFO" + }, + "customizations": { + "vscode": { + "settings": { + "files.watcherExclude": { + "**/target/**": true + }, + "[rust]": { + "editor.formatOnSave": true + } + }, + "extensions": [ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates" + ] + } + } +} \ No newline at end of file diff --git a/.devcontainer/local-features/linux/devcontainer-feature.json b/.devcontainer/local-features/linux/devcontainer-feature.json new file mode 100644 index 000000000..daba5bc1f --- /dev/null +++ b/.devcontainer/local-features/linux/devcontainer-feature.json @@ -0,0 +1,4 @@ +{ + "id": "linux", + "version": "0.0.1" +} \ No newline at end of file diff --git a/.devcontainer/local-features/linux/install.sh b/.devcontainer/local-features/linux/install.sh new file mode 100644 index 000000000..8f9ad5e1a --- /dev/null +++ b/.devcontainer/local-features/linux/install.sh @@ -0,0 +1,6 @@ +#!/bin/bash +echo "Update Apt Database" +sudo apt-get update + +echo "Install Webkit Dependencies" +sudo apt-get -qq install build-essential libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev From 4d90bdee473bae9601ebe56a524de99e7e2a7be4 Mon Sep 17 00:00:00 2001 From: ealmloff Date: Sat, 29 Apr 2023 20:42:47 -0500 Subject: [PATCH 18/50] Remove console.log --- packages/interpreter/src/sledgehammer_bindings.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index a15d7bc81..2cb148e89 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -118,7 +118,6 @@ mod js { nodes[id] = node; } export function get_node(id) { - console.log(nodes, id); return nodes[id]; } export function initilize(root, handler) { From a78ee4ffe77ad68a2b6e677446853bef3b086584 Mon Sep 17 00:00:00 2001 From: stephenandary Date: Mon, 1 May 2023 14:12:08 -0400 Subject: [PATCH 19/50] WIP - Update devcontainer to use debian bookworm --- .devcontainer/Dockerfile | 9 +++++++++ .devcontainer/devcontainer.json | 15 ++++++++++++--- .../linux/devcontainer-feature.json | 4 ---- .devcontainer/local-features/linux/install.sh | 6 ------ 4 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/local-features/linux/devcontainer-feature.json delete mode 100644 .devcontainer/local-features/linux/install.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..b20e962b9 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +# [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye +ARG VARIANT="slim-bookworm" +FROM rust:${VARIANT} +ENV DEBIAN_FRONTEND noninteractive +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive + +RUN apt-get -qq install build-essential libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1c5278332..aa536ab53 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,9 +1,18 @@ { "name": "dioxus", - "image": "mcr.microsoft.com/devcontainers/rust:latest", + "remoteUser": "vscode", + "build": { + "dockerfile": "./Dockerfile", + "context": "." + }, "features": { - "ghcr.io/devcontainers/features/common-utils:2": {}, - "./local-features/linux": {} + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "vscode", + "uid": "1000", + "gid": "1000", + "upgradePackages": "true" + } }, "containerEnv": { "RUST_LOG": "INFO" diff --git a/.devcontainer/local-features/linux/devcontainer-feature.json b/.devcontainer/local-features/linux/devcontainer-feature.json deleted file mode 100644 index daba5bc1f..000000000 --- a/.devcontainer/local-features/linux/devcontainer-feature.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "id": "linux", - "version": "0.0.1" -} \ No newline at end of file diff --git a/.devcontainer/local-features/linux/install.sh b/.devcontainer/local-features/linux/install.sh deleted file mode 100644 index 8f9ad5e1a..000000000 --- a/.devcontainer/local-features/linux/install.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -echo "Update Apt Database" -sudo apt-get update - -echo "Install Webkit Dependencies" -sudo apt-get -qq install build-essential libwebkit2gtk-4.0-dev libgtk-3-dev libayatana-appindicator3-dev From 856650eb4d4c898af323fa4750e0a1bf2db9ac6a Mon Sep 17 00:00:00 2001 From: stephenandary Date: Mon, 1 May 2023 14:25:54 -0400 Subject: [PATCH 20/50] WIP - Add README --- .devcontainer/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .devcontainer/README.md diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 000000000..74dbbe8c5 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,26 @@ +# Dev Container + +A dev container in the most simple context allows one to create a consistent development environment within a docker container that can easily be opened locally or remotely via codespaces such that contributors don't need to install anything to contribute. + +## Useful Links + +- +- +- +- + +## Using A Dev Container + +### Locally + +To use this dev container locally, make sure Docker is installed and in VSCode install the `ms-vscode-remote.remote-containers` extension. Then from the root of Dioxus you can type `Ctrl + Shift + P`, then choose `Dev Containers: Rebuild and Reopen in Devcontainer`. + +### Codespaces + +TBD + +## Troubleshooting + +If having difficulty commiting with github, and you use ssh or gpg keys, you may need to ensure that the keys are being shared properly between your host and VSCode. + +Though VSCode does a pretty good job sharing credentials between host and devcontainer, to save some time you can always just reopen the container locally to commit with `Ctrl + Shift + P`, then choose `Dev Containers: Reopen Folder Locally` From e68c806f8ce294cc855c116db3a2ba0d26cc6bd3 Mon Sep 17 00:00:00 2001 From: stephenandary Date: Mon, 1 May 2023 14:27:47 -0400 Subject: [PATCH 21/50] WIP - Update Readme Codespaces section with setup link --- .devcontainer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 74dbbe8c5..570435663 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -17,7 +17,7 @@ To use this dev container locally, make sure Docker is installed and in VSCode i ### Codespaces -TBD +[Codespaces Setup](https://docs.github.com/en/codespaces/developing-in-codespaces/creating-a-codespace-for-a-repository#creating-a-codespace-for-a-repository) ## Troubleshooting From 5f3c3d3f04217fcd1d58b5a182ee19dfba369037 Mon Sep 17 00:00:00 2001 From: stephenandary Date: Mon, 1 May 2023 14:49:38 -0400 Subject: [PATCH 22/50] WIP - Update base image to nightly so format on save works --- .devcontainer/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b20e962b9..c0e58fab8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye -ARG VARIANT="slim-bookworm" -FROM rust:${VARIANT} +ARG VARIANT="nightly-bookworm-slim" +FROM rustlang/rust:${VARIANT} ENV DEBIAN_FRONTEND noninteractive RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections From d3cf9f728c6d501944c143baa1e39d77ac9b0f45 Mon Sep 17 00:00:00 2001 From: stephenandary Date: Mon, 1 May 2023 14:54:21 -0400 Subject: [PATCH 23/50] WIP - remove comment in Dockerfile --- .devcontainer/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c0e58fab8..e2fd03ad8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,3 @@ -# [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye ARG VARIANT="nightly-bookworm-slim" FROM rustlang/rust:${VARIANT} ENV DEBIAN_FRONTEND noninteractive From ed4709101c681b15de7d6ef5f290277f04daf059 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 2 May 2023 09:38:58 -0500 Subject: [PATCH 24/50] fix duplicate example names --- .../{all_events.rs => all_terminal_events.rs} | 0 packages/dioxus-tui/examples/components.rs | 67 ----------------- .../{stress.rs => many_small_edit_stress.rs} | 0 packages/dioxus-tui/examples/quadrants.rs | 71 ++++++++++--------- .../{readme.rs => readme_hello_world.rs} | 0 packages/native-core/src/tree.rs | 2 +- .../{counter.rs => counter_button.rs} | 0 packages/rink/src/widget.rs | 2 +- .../examples/{simple.rs => simple_routes.rs} | 0 9 files changed, 41 insertions(+), 101 deletions(-) rename packages/dioxus-tui/examples/{all_events.rs => all_terminal_events.rs} (100%) delete mode 100644 packages/dioxus-tui/examples/components.rs rename packages/dioxus-tui/examples/{stress.rs => many_small_edit_stress.rs} (100%) rename packages/dioxus-tui/examples/{readme.rs => readme_hello_world.rs} (100%) rename packages/rink/examples/{counter.rs => counter_button.rs} (100%) rename packages/router/examples/{simple.rs => simple_routes.rs} (100%) diff --git a/packages/dioxus-tui/examples/all_events.rs b/packages/dioxus-tui/examples/all_terminal_events.rs similarity index 100% rename from packages/dioxus-tui/examples/all_events.rs rename to packages/dioxus-tui/examples/all_terminal_events.rs diff --git a/packages/dioxus-tui/examples/components.rs b/packages/dioxus-tui/examples/components.rs deleted file mode 100644 index 45ed56338..000000000 --- a/packages/dioxus-tui/examples/components.rs +++ /dev/null @@ -1,67 +0,0 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; -use dioxus_tui::Config; - -fn main() { - dioxus_tui::launch_cfg(app, Config::default()); -} - -#[derive(Props, PartialEq)] -struct QuadrentProps { - color: String, - text: String, -} - -fn Quadrant(cx: Scope) -> Element { - cx.render(rsx! { - div { - border_width: "1px", - width: "50%", - height: "100%", - background_color: "{cx.props.color}", - justify_content: "center", - align_items: "center", - - "{cx.props.text}" - } - }) -} - -fn app(cx: Scope) -> Element { - cx.render(rsx! { - div { - width: "100%", - height: "100%", - flex_direction: "column", - - div { - width: "100%", - height: "50%", - flex_direction: "row", - Quadrant{ - color: "red".to_string(), - text: "[A]".to_string() - }, - Quadrant{ - color: "black".to_string(), - text: "[B]".to_string() - } - } - - div { - width: "100%", - height: "50%", - flex_direction: "row", - Quadrant{ - color: "green".to_string(), - text: "[C]".to_string() - }, - Quadrant{ - color: "blue".to_string(), - text: "[D]".to_string() - } - } - } - }) -} diff --git a/packages/dioxus-tui/examples/stress.rs b/packages/dioxus-tui/examples/many_small_edit_stress.rs similarity index 100% rename from packages/dioxus-tui/examples/stress.rs rename to packages/dioxus-tui/examples/many_small_edit_stress.rs diff --git a/packages/dioxus-tui/examples/quadrants.rs b/packages/dioxus-tui/examples/quadrants.rs index d9da5dcb5..45ed56338 100644 --- a/packages/dioxus-tui/examples/quadrants.rs +++ b/packages/dioxus-tui/examples/quadrants.rs @@ -1,7 +1,31 @@ +#![allow(non_snake_case)] + use dioxus::prelude::*; +use dioxus_tui::Config; fn main() { - dioxus_tui::launch(app); + dioxus_tui::launch_cfg(app, Config::default()); +} + +#[derive(Props, PartialEq)] +struct QuadrentProps { + color: String, + text: String, +} + +fn Quadrant(cx: Scope) -> Element { + cx.render(rsx! { + div { + border_width: "1px", + width: "50%", + height: "100%", + background_color: "{cx.props.color}", + justify_content: "center", + align_items: "center", + + "{cx.props.text}" + } + }) } fn app(cx: Scope) -> Element { @@ -15,22 +39,13 @@ fn app(cx: Scope) -> Element { width: "100%", height: "50%", flex_direction: "row", - div { - border_width: "1px", - width: "50%", - height: "100%", - background_color: "red", - justify_content: "center", - align_items: "center", - "[A]" - } - div { - width: "50%", - height: "100%", - background_color: "black", - justify_content: "center", - align_items: "center", - "[B]" + Quadrant{ + color: "red".to_string(), + text: "[A]".to_string() + }, + Quadrant{ + color: "black".to_string(), + text: "[B]".to_string() } } @@ -38,21 +53,13 @@ fn app(cx: Scope) -> Element { width: "100%", height: "50%", flex_direction: "row", - div { - width: "50%", - height: "100%", - background_color: "green", - justify_content: "center", - align_items: "center", - "[C]" - } - div { - width: "50%", - height: "100%", - background_color: "blue", - justify_content: "center", - align_items: "center", - "[D]" + Quadrant{ + color: "green".to_string(), + text: "[C]".to_string() + }, + Quadrant{ + color: "blue".to_string(), + text: "[D]".to_string() } } } diff --git a/packages/dioxus-tui/examples/readme.rs b/packages/dioxus-tui/examples/readme_hello_world.rs similarity index 100% rename from packages/dioxus-tui/examples/readme.rs rename to packages/dioxus-tui/examples/readme_hello_world.rs diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index df25deac3..e0c55eece 100644 --- a/packages/native-core/src/tree.rs +++ b/packages/native-core/src/tree.rs @@ -178,7 +178,7 @@ impl<'a> TreeMut for TreeMutView<'a> { fn set_height(tree: &mut TreeMutView<'_>, node: NodeId, height: u16) { let children = { let mut node_data_mut = &mut tree.1; - let mut node = (&mut node_data_mut).get(node).unwrap(); + let node = (&mut node_data_mut).get(node).unwrap(); node.height = height; node.children.clone() }; diff --git a/packages/rink/examples/counter.rs b/packages/rink/examples/counter_button.rs similarity index 100% rename from packages/rink/examples/counter.rs rename to packages/rink/examples/counter_button.rs diff --git a/packages/rink/src/widget.rs b/packages/rink/src/widget.rs index 7c6645692..ab9044998 100644 --- a/packages/rink/src/widget.rs +++ b/packages/rink/src/widget.rs @@ -26,7 +26,7 @@ impl<'a> RinkBuffer<'a> { // panic!("({x}, {y}) is not in {area:?}"); return; } - let mut cell = self.buf.get_mut(x, y); + let cell = self.buf.get_mut(x, y); cell.bg = convert(self.cfg.rendering_mode, new.bg.blend(cell.bg)); if new.symbol.is_empty() { if !cell.symbol.is_empty() { diff --git a/packages/router/examples/simple.rs b/packages/router/examples/simple_routes.rs similarity index 100% rename from packages/router/examples/simple.rs rename to packages/router/examples/simple_routes.rs From 230afadf3ded3b0fe56e1529f411318c0e693acb Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 3 May 2023 09:00:17 -0500 Subject: [PATCH 25/50] Fix web events starting on a text node --- packages/web/src/dom.rs | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index 20119dd25..b25b7826c 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -380,22 +380,38 @@ extern "C" { } fn walk_event_for_id(event: &web_sys::Event) -> Option<(ElementId, web_sys::Element)> { - let mut target = event + let target = event .target() .expect("missing target") - .dyn_into::() - .expect("not a valid element"); + .dyn_into::() + .expect("not a valid node"); + let mut current_target_element = target.dyn_ref::().cloned(); loop { - match target.get_attribute("data-dioxus-id").map(|f| f.parse()) { - Some(Ok(id)) => return Some((ElementId(id), target)), - Some(Err(_)) => return None, + match ( + current_target_element + .as_ref() + .and_then(|el| el.get_attribute("data-dioxus-id").map(|f| f.parse())), + current_target_element, + ) { + // This node is an element, and has a dioxus id, so we can stop walking + (Some(Ok(id)), Some(target)) => return Some((ElementId(id), target)), - // walk the tree upwards until we actually find an event target - None => match target.parent_element() { - Some(parent) => target = parent, - None => return None, - }, + // Walk the tree upwards until we actually find an event target + (None, target_element) => { + let parent = match target_element.as_ref() { + Some(el) => el.parent_element(), + // if this is the first node and not an element, we need to get the parent from the target node + None => target.parent_element(), + }; + match parent { + Some(parent) => current_target_element = Some(parent), + _ => return None, + } + } + + // This node is an element with an invalid dioxus id, give up + _ => return None, } } } From 1b199977d150176ba9020ef3f7225282c2619de6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 4 May 2023 11:04:06 -0500 Subject: [PATCH 26/50] add rendering tests --- packages/desktop/Cargo.toml | 13 ++- packages/desktop/headless_tests/events.rs | 21 ++++- packages/desktop/headless_tests/main.rs | 28 ------ packages/desktop/headless_tests/rendering.rs | 89 ++++++++++++++++++++ 4 files changed, 118 insertions(+), 33 deletions(-) delete mode 100644 packages/desktop/headless_tests/main.rs create mode 100644 packages/desktop/headless_tests/rendering.rs diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index e8812df5a..092676322 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -59,8 +59,15 @@ dioxus-core-macro = { path = "../core-macro" } dioxus-hooks = { path = "../hooks" } dioxus = { path = "../dioxus" } exitcode = "1.1.2" +scraper = "0.16.0" + +# These tests need to be run on the main thread, so they cannot use rust's test harness. +[[test]] +name = "check_events" +path = "headless_tests/events.rs" +harness = false [[test]] -name = "headless_tests" -path = "headless_tests/main.rs" -harness = false \ No newline at end of file +name = "check_rendering" +path = "headless_tests/rendering.rs" +harness = false diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index 60962c868..ec77850bd 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -1,9 +1,25 @@ -use crate::check_app_exits; + use dioxus::prelude::*; use dioxus_desktop::DesktopContext; use dioxus::html::geometry::euclid::Vector3D; -pub fn test_events() { +pub(crate) fn check_app_exits(app: Component) { + // This is a deadman's switch to ensure that the app exits + let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let should_panic_clone = should_panic.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(100)); + if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { + std::process::exit(exitcode::SOFTWARE); + } + }); + + dioxus_desktop::launch(app); + + should_panic.store(false, std::sync::atomic::Ordering::SeqCst); +} + +pub fn main() { check_app_exits(app); } @@ -194,6 +210,7 @@ fn app(cx: Scope) -> Element { if **recieved_events == 12 { + println!("all events recieved"); desktop_context.close(); } diff --git a/packages/desktop/headless_tests/main.rs b/packages/desktop/headless_tests/main.rs deleted file mode 100644 index a9d9ec61c..000000000 --- a/packages/desktop/headless_tests/main.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Check that all events are being forwarded to the mock. -//! This example roughly shows how events are serialized into Rust from JavaScript. -//! -//! There is some conversion happening when input types are checkbox/radio/select/textarea etc. - -use dioxus::prelude::*; - -mod events; - -fn main() { - events::test_events(); -} - -pub(crate) fn check_app_exits(app: Component) { - // This is a deadman's switch to ensure that the app exits - let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); - let should_panic_clone = should_panic.clone(); - std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_secs(100)); - if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { - std::process::exit(exitcode::SOFTWARE); - } - }); - - dioxus_desktop::launch(app); - - should_panic.store(false, std::sync::atomic::Ordering::SeqCst); -} diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs new file mode 100644 index 000000000..9a2c8bf57 --- /dev/null +++ b/packages/desktop/headless_tests/rendering.rs @@ -0,0 +1,89 @@ +use dioxus::prelude::*; +use dioxus_desktop::DesktopContext; + +pub(crate) fn check_app_exits(app: Component) { + // This is a deadman's switch to ensure that the app exits + let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let should_panic_clone = should_panic.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_secs(100)); + if should_panic_clone.load(std::sync::atomic::Ordering::SeqCst) { + std::process::exit(exitcode::SOFTWARE); + } + }); + + dioxus_desktop::launch(app); + + should_panic.store(false, std::sync::atomic::Ordering::SeqCst); +} + +fn main() { + check_app_exits(check_html_renders); +} + +fn use_inner_html(cx: &ScopeState, id: &'static str) -> Option { + let value: &UseRef> = use_ref(cx, || None); + use_effect(cx, (), |_| { + to_owned![value]; + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + async move { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + let html = desktop_context + .eval(&format!( + r#"let element = document.getElementById('{}'); + return element.innerHTML;"#, + id + )) + .await; + if let Ok(serde_json::Value::String(html)) = html { + println!("html: {}", html); + value.set(Some(html)); + } + } + }); + value.read().clone() +} + +const EXPECTED_HTML: &str = r#"

text

hello world

"#; + +fn check_html_renders(cx: Scope) -> Element { + let inner_html = use_inner_html(cx, "main_div"); + let desktop_context: DesktopContext = cx.consume_context().unwrap(); + + if let Some(raw_html) = inner_html.as_deref() { + let fragment = scraper::Html::parse_fragment(raw_html); + println!("fragment: {:?}", fragment.html()); + let expected = scraper::Html::parse_fragment(EXPECTED_HTML); + println!("fragment: {:?}", expected.html()); + if fragment == expected { + println!("html matches"); + desktop_context.close(); + } + } + + let dyn_value = 0; + let dyn_element = rsx! { + div { + dangerous_inner_html: "

hello world

", + } + }; + + render! { + div { + id: "main_div", + div { + width: "100px", + height: "100px", + color: "rgb({dyn_value}, {dyn_value}, {dyn_value})", + id: 5, + input { + "type": "checkbox", + }, + h1 { + "text" + } + dyn_element + } + } + } +} From ea5e664364b9a1e8ab62a41198dc4afb053977ce Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 4 May 2023 11:39:46 -0500 Subject: [PATCH 27/50] fix formatting --- packages/desktop/headless_tests/events.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index ec77850bd..c0933921a 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -1,7 +1,6 @@ - +use dioxus::html::geometry::euclid::Vector3D; use dioxus::prelude::*; use dioxus_desktop::DesktopContext; -use dioxus::html::geometry::euclid::Vector3D; pub(crate) fn check_app_exits(app: Component) { // This is a deadman's switch to ensure that the app exits @@ -67,7 +66,7 @@ fn app(cx: Scope) -> Element { cancelable: true, buttons: 2, })"#, - ); + ); // mouse_click_div mock_event( &cx, @@ -78,7 +77,7 @@ fn app(cx: Scope) -> Element { cancelable: true, buttons: 2, button: 2, - })"# + })"#, ); // mouse_dblclick_div mock_event( @@ -207,7 +206,6 @@ fn app(cx: Scope) -> Element { "focus_out_div", r#"new FocusEvent("focusout",{bubbles: true})"#, ); - if **recieved_events == 12 { println!("all events recieved"); @@ -299,7 +297,7 @@ fn app(cx: Scope) -> Element { assert_eq!(event.data.code().to_string(), "KeyA"); assert_eq!(event.data.location, 0); assert!(event.data.is_auto_repeating()); - + recieved_events.modify(|x| *x + 1) } } @@ -325,7 +323,7 @@ fn app(cx: Scope) -> Element { assert_eq!(event.data.code().to_string(), "KeyA"); assert_eq!(event.data.location, 0); assert!(!event.data.is_auto_repeating()); - + recieved_events.modify(|x| *x + 1) } } From b6463e4b27ff1a178c5269535fae4f015ab76ed4 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 4 May 2023 12:04:21 -0700 Subject: [PATCH 28/50] bump core --- packages/core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index b86466692..af0feba9c 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-core" -version = "0.3.2" +version = "0.3.3" authors = ["Jonathan Kelley"] edition = "2018" description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences" From ae71e4ce339d729ae3e930ece4e63688b7e8a924 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 5 May 2023 11:11:50 -0500 Subject: [PATCH 29/50] don't render headless tests --- packages/desktop/headless_tests/events.rs | 7 ++++++- packages/desktop/headless_tests/rendering.rs | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index c0933921a..5a3fc44b6 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -3,6 +3,8 @@ use dioxus::prelude::*; use dioxus_desktop::DesktopContext; pub(crate) fn check_app_exits(app: Component) { + use dioxus_desktop::tao::window::WindowBuilder; + use dioxus_desktop::Config; // This is a deadman's switch to ensure that the app exits let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let should_panic_clone = should_panic.clone(); @@ -13,7 +15,10 @@ pub(crate) fn check_app_exits(app: Component) { } }); - dioxus_desktop::launch(app); + dioxus_desktop::launch_cfg( + app, + Config::new().with_window(WindowBuilder::new().with_visible(false)), + ); should_panic.store(false, std::sync::atomic::Ordering::SeqCst); } diff --git a/packages/desktop/headless_tests/rendering.rs b/packages/desktop/headless_tests/rendering.rs index 9a2c8bf57..635da9c23 100644 --- a/packages/desktop/headless_tests/rendering.rs +++ b/packages/desktop/headless_tests/rendering.rs @@ -2,6 +2,8 @@ use dioxus::prelude::*; use dioxus_desktop::DesktopContext; pub(crate) fn check_app_exits(app: Component) { + use dioxus_desktop::tao::window::WindowBuilder; + use dioxus_desktop::Config; // This is a deadman's switch to ensure that the app exits let should_panic = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let should_panic_clone = should_panic.clone(); @@ -12,7 +14,10 @@ pub(crate) fn check_app_exits(app: Component) { } }); - dioxus_desktop::launch(app); + dioxus_desktop::launch_cfg( + app, + Config::new().with_window(WindowBuilder::new().with_visible(false)), + ); should_panic.store(false, std::sync::atomic::Ordering::SeqCst); } From ec5999b3b4546a21aed4ac2b27f268d06e095cd4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 5 May 2023 15:45:38 -0500 Subject: [PATCH 30/50] fix clippy --- packages/desktop/headless_tests/events.rs | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index 5a3fc44b6..0281969ec 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -52,7 +52,7 @@ fn app(cx: Scope) -> Element { // button mock_event( - &cx, + cx, "button", r#"new MouseEvent("click", { view: window, @@ -63,7 +63,7 @@ fn app(cx: Scope) -> Element { ); // mouse_move_div mock_event( - &cx, + cx, "mouse_move_div", r#"new MouseEvent("mousemove", { view: window, @@ -74,7 +74,7 @@ fn app(cx: Scope) -> Element { ); // mouse_click_div mock_event( - &cx, + cx, "mouse_click_div", r#"new MouseEvent("click", { view: window, @@ -86,7 +86,7 @@ fn app(cx: Scope) -> Element { ); // mouse_dblclick_div mock_event( - &cx, + cx, "mouse_dblclick_div", r#"new MouseEvent("dblclick", { view: window, @@ -98,7 +98,7 @@ fn app(cx: Scope) -> Element { ); // mouse_down_div mock_event( - &cx, + cx, "mouse_down_div", r#"new MouseEvent("mousedown", { view: window, @@ -110,7 +110,7 @@ fn app(cx: Scope) -> Element { ); // mouse_up_div mock_event( - &cx, + cx, "mouse_up_div", r#"new MouseEvent("mouseup", { view: window, @@ -122,7 +122,7 @@ fn app(cx: Scope) -> Element { ); // wheel_div mock_event( - &cx, + cx, "wheel_div", r#"new WheelEvent("wheel", { view: window, @@ -135,7 +135,7 @@ fn app(cx: Scope) -> Element { ); // key_down_div mock_event( - &cx, + cx, "key_down_div", r#"new KeyboardEvent("keydown", { key: "a", @@ -157,7 +157,7 @@ fn app(cx: Scope) -> Element { ); // key_up_div mock_event( - &cx, + cx, "key_up_div", r#"new KeyboardEvent("keyup", { key: "a", @@ -179,7 +179,7 @@ fn app(cx: Scope) -> Element { ); // key_press_div mock_event( - &cx, + cx, "key_press_div", r#"new KeyboardEvent("keypress", { key: "a", @@ -201,13 +201,13 @@ fn app(cx: Scope) -> Element { ); // focus_in_div mock_event( - &cx, + cx, "focus_in_div", r#"new FocusEvent("focusin", {bubbles: true})"#, ); // focus_out_div mock_event( - &cx, + cx, "focus_out_div", r#"new FocusEvent("focusout",{bubbles: true})"#, ); From 1959da1ec682844c0979f399b4b21512c0f1995c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 6 May 2023 09:15:09 -0500 Subject: [PATCH 31/50] include desktop tests in test with browser --- Makefile.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index 8ecbe6b2e..6f331b98f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -42,10 +42,10 @@ private = true [tasks.test] dependencies = ["build"] command = "cargo" -args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router"] +args = ["test", "--lib", "--bins", "--tests", "--examples", "--workspace", "--exclude", "dioxus-router", "--exclude", "dioxus-desktop"] private = true [tasks.test-with-browser] -env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router"] } +env = { CARGO_MAKE_WORKSPACE_INCLUDE_MEMBERS = ["**/packages/router", "**/packages/desktop"] } private = true workspace = true From 5f1f3e1b5893ca51dab637e63cb14e0171f0ac55 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 6 May 2023 15:15:49 -0500 Subject: [PATCH 32/50] start a contributing guide --- docs/README.md | 21 ++++++ docs/guide/src/en/SUMMARY.md | 7 +- .../src/en/contributing/guiding_principles.md | 39 ++++++++++ .../index.md} | 33 ++++++++- .../src/en/contributing/project_structure.md | 50 +++++++++++++ .../src/en/{ => contributing}/roadmap.md | 72 ++++++++++--------- 6 files changed, 185 insertions(+), 37 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/guide/src/en/contributing/guiding_principles.md rename docs/guide/src/en/{contributing.md => contributing/index.md} (54%) create mode 100644 docs/guide/src/en/contributing/project_structure.md rename docs/guide/src/en/{ => contributing}/roadmap.md (53%) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..a505a173e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,21 @@ +# Building the Documentation + +Dioxus uses a fork of MdBook with multilanguage support. To build the documentation, you will need to install the forked version of MdBook. + +```sh +cargo install mdbook --git https://github.com/Demonthos/mdBook.git --branch master +``` + +Then, you can build the documentation by running: + +Linux and MacOS: + +```sh +cd docs && cd guide && mdbook build -d ../nightly/guide && cd .. && cd router && mdbook build -d ../nightly/router && cd .. && cd .. +``` + +Windows: + +```cmd +cd docs; cd guide; mdbook build -d ../nightly/guide; cd ..; cd router; mdbook build -d ../nightly/router; cd ..; cd .. +``` diff --git a/docs/guide/src/en/SUMMARY.md b/docs/guide/src/en/SUMMARY.md index f9f31ed52..5889d4164 100644 --- a/docs/guide/src/en/SUMMARY.md +++ b/docs/guide/src/en/SUMMARY.md @@ -31,6 +31,7 @@ - [Error Handling](best_practices/error_handling.md) - [Antipatterns](best_practices/antipatterns.md) - [Publishing](publishing/index.md) + - [Desktop](publishing/desktop.md) - [Web](publishing/web.md) @@ -40,5 +41,7 @@ --- -[Roadmap](roadmap.md) -[Contributing](contributing.md) +- [Contributing](contributing/index.md) + - [Project Structure](contributing/project_structure.md) + - [Guiding Principles](contributing/guiding_principles.md) + - [Roadmap](contributing/roadmap.md) diff --git a/docs/guide/src/en/contributing/guiding_principles.md b/docs/guide/src/en/contributing/guiding_principles.md new file mode 100644 index 000000000..6bbb319ec --- /dev/null +++ b/docs/guide/src/en/contributing/guiding_principles.md @@ -0,0 +1,39 @@ +# Overall Goals + +This document outlines some of the overall goals for Dioxus. These goals are not set in stone, but they represent general guidelines for the project. + +The goal of Dioxus is to make it easy to build **cross-platform applications that scale**. + +## Cross-Platform + +Dioxus is designed to be cross-platform by default. This means that it should be easy to build applications that run on the web, desktop, and mobile. However, Dioxus should also be flexible enough to allow users to opt into platform-specific features when needed. The `use_eval` is one example of this. By default, Dioxus does not assume that the platform supports JavaScript, but it does provide a hook that allows users to opt into JavaScript when needed. + +## Performance + +As Dioxus applications grow, they should remain relatively performant without the need for manual optimizations. There will be cases where manual optimizations are needed, but Dioxus should try to make these cases as rare as possible. + +The is one of the benefits of the core architecture of Dioxus. It is based on a Virtual Dom which performs diffing which should prevent unnecessary re-renders even when large parts of the component tree are rerun. On top of this, Dioxus groups static parts of the RSX tree together to skip diffing them entirely. + +## Type Safety + +As teams grow, the Type safety of Rust is a huge advantage. Dioxus should leverage this advantage to make it easy to build applications with large teams. + +To take full advantage of Rust's type system, Dioxus should try to avoid exposing public `Any` types and stringly typed APIs where possible. + +## Developer Experience + +Dioxus should be easy to learn and ergonomic to use. + +- The API of Dioxus should try to remain close to React's API where possible + + - This makes it easier for people to learn Dioxus if they already know React + +- We can avoid the tradeoff between simplicity and flexibility by providing multiple layers of API: One for the very common use case, one for low-level control + + - Hooks: the hooks crate has the most common use cases, but `cx.hook` provides a way to access the underlying persistent reference if needed. + - The builder pattern in platform Configs: The builder pattern is used to default to the most common use case, but users can change the defaults if needed. + +- Documentation: + - All public APIs should have rust documentation + - Examples should be provided for all public features. These examples both serve as documentation and are checked by CI to ensure that they continue to compile + - The most common workflows should be documented in the guide diff --git a/docs/guide/src/en/contributing.md b/docs/guide/src/en/contributing/index.md similarity index 54% rename from docs/guide/src/en/contributing.md rename to docs/guide/src/en/contributing/index.md index 35efa11ac..68a8ae5ec 100644 --- a/docs/guide/src/en/contributing.md +++ b/docs/guide/src/en/contributing/index.md @@ -10,7 +10,7 @@ If you'd like to improve the docs, PRs are welcome! Both Rust docs ([source](htt ## Working on the Ecosystem -Part of what makes React great is the rich ecosystem. We'd like the same for Dioxus! So if you have a library in mind that you'd like to write and many people would benefit from, it will be appreciated. You can [browse npm.js](https://www.npmjs.com/search?q=keywords:react-component) for inspiration. +Part of what makes React great is the rich ecosystem. We'd like the same for Dioxus! So if you have a library in mind that you'd like to write and many people would benefit from, it will be appreciated. You can [browse npm.js](https://www.npmjs.com/search?q=keywords:react-component) for inspiration. Once you are done, add your library to the [awesome dioxus](https://github.com/DioxusLabs/awesome-dioxus) list or share it in the `#I-made-a-thing` channel on [Discord](https://discord.gg/XgGxMSkvUM). ## Bugs & Features @@ -18,3 +18,34 @@ If you've fixed [an open issue](https://github.com/DioxusLabs/dioxus/issues), fe All pull requests (including those made by a team member) must be approved by at least one other team member. Larger, more nuanced decisions about design, architecture, breaking changes, trade-offs, etc. are made by team consensus. + +## Tools + +The following tools can be helpful when developing Dioxus. Many of these tools are used in the CI pipeline. Running them locally before submitting a PR instead of waiting for CI can save time. + +- All code is tested with [cargo test](https://doc.rust-lang.org/cargo/commands/cargo-test.html) + +```sh +cargo fmt --all +``` + +- All code is formatted with [rustfmt](https://github.com/rust-lang/rustfmt) + +```sh +cargo check --workspace --examples --tests +``` + +- All code is linted with [Clippy](https://doc.rust-lang.org/clippy/) + +```sh +cargo clippy --workspace --examples --tests -- -D warnings +``` + +- Crates that use unsafe are checked for undefined behavior with [MIRI](https://github.com/rust-lang/miri). MIRI can be helpful to debug what unsafe code is causing issues. Only code that does not interact with system calls can be checked with MIRI. Currently this is used for the two MIRI tests in `dioxus-core` and `dioxus-native-core`. + +```sh +cargo miri test --package dioxus-core --test miri_stress +cargo miri test --package dioxus-native-core --test miri_native +``` + +- [Rust analyzer](https://rust-analyzer.github.io/) can be very helpful for quick feedback in your IDE. diff --git a/docs/guide/src/en/contributing/project_structure.md b/docs/guide/src/en/contributing/project_structure.md new file mode 100644 index 000000000..0f4fa64c8 --- /dev/null +++ b/docs/guide/src/en/contributing/project_structure.md @@ -0,0 +1,50 @@ +# Project Struture + +There are many packages in the Dioxus organization. This document will help you understand the purpose of each package and how they fit together. + +## Renderers + +- [Desktop](https://github.com/DioxusLabs/dioxus/tree/master/packages/desktop): A Render that Runs Dioxus applications natively, but renders them with the system webview +- [Mobile](https://github.com/DioxusLabs/dioxus/tree/master/packages/mobile): A Render that Runs Dioxus applications natively, but renders them with the system webview. This is currently a copy of the desktop render +- [Web](https://github.com/DioxusLabs/dioxus/tree/master/packages/Web): Renders Dioxus applications in the browser by compiling to WASM and manipulating the DOM +- [Liveview](https://github.com/DioxusLabs/dioxus/tree/master/packages/liveview): A Render that Runs on the server, and renders using a websocket proxy in the browser +- [Rink](https://github.com/DioxusLabs/dioxus/tree/master/packages/rink): A Renderer that renders a HTML-like tree into a terminal +- [TUI](https://github.com/DioxusLabs/dioxus/tree/master/packages/dioxus-tui): A Renderer that uses Rink to render a Dioxus application in a terminal +- [Blitz-Core](https://github.com/DioxusLabs/blitz/tree/master/blitz-core): An experimental native renderer that renders a HTML-like tree using WGPU. +- [Blitz](https://github.com/DioxusLabs/blitz): An experimental native renderer that uses Blitz-Core to render a Dioxus application using WGPU. +- [SSR](https://github.com/DioxusLabs/dioxus/tree/master/packages/ssr): A Render that Runs Dioxus applications on the server, and renders them to HTML + +## State Management/Hooks + +- [Hooks](https://github.com/DioxusLabs/dioxus/tree/master/packages/hooks): A collection of common hooks for Dioxus applications +- [Signals](https://github.com/DioxusLabs/dioxus/tree/master/packages/signals): A experimental state management library for Dioxus applications. This currently contains a `Copy` version of UseRef +- [Dioxus STD](https://github.com/DioxusLabs/dioxus-std): A collection of platform agnostic hooks to interact with system interfaces (The clipboard, camera, etc.). +- [Fermi](https://github.com/DioxusLabs/dioxus/tree/master/packages/fermi): A global state management library for Dioxus applications. + [Router](https://github.com/DioxusLabs/dioxus/tree/master/packages/router): A client-side router for Dioxus applications + +## Core utilities + +- [core](https://github.com/DioxusLabs/dioxus/tree/master/packages/core): The core virtual dom implementation every Dioxus application uses + - You can read more about the archetecture of the core [in this blog post](https://dioxuslabs.com/blog/templates-diffing/) and the [custom renderer section of the guide](../custom_renderer/index.md) +- [RSX](https://github.com/DioxusLabs/dioxus/tree/master/packages/RSX): The core parsing for RSX used for hot reloading, autoformatting, and the macro +- [core-macro](https://github.com/DioxusLabs/dioxus/tree/master/packages/core-macro): The rsx! macro used to write Dioxus applications. (This is a wrapper over the RSX crate) +- [HTML macro](https://github.com/DioxusLabs/dioxus-html-macro): A html-like alternative to the RSX macro + +## Native Renderer Utilities + +- [native-core](https://github.com/DioxusLabs/dioxus/tree/master/packages/native-core): Incrementally computed tree of states (mostly styles) + - You can read more about how native-core can help you build native renderers in the [custom renderer section of the guide](../custom_renderer/index.html#native-core) +- [native-core-macro](https://github.com/DioxusLabs/dioxus/tree/master/packages/native-core-macro): A helper macro for native core +- [Taffy](https://github.com/DioxusLabs/taffy): Layout engine powering Blitz-Core, Rink, and Bevy UI + +## Web renderer tooling + +- [HTML](https://github.com/DioxusLabs/dioxus/tree/master/packages/html): defines html specific elements, events, and attributes +- [Interpreter](https://github.com/DioxusLabs/dioxus/tree/master/packages/interpreter): defines browser bindings used by the web and desktop renderers + +## Developer tooling + +- [hot-reload](https://github.com/DioxusLabs/dioxus/tree/master/packages/hot-reload): Macro that uses the RSX crate to hot reload static parts of any rsx! macro. This macro works with any non-web renderer with an [integration](https://crates.io/crates/dioxus-hot-reload) +- [autofmt](https://github.com/DioxusLabs/dioxus/tree/master/packages/autofmt): Formats RSX code +- [rsx-rosetta](https://github.com/DioxusLabs/dioxus/tree/master/packages/RSX-rosetta): Handles conversion between HTML and RSX +- [CLI](https://github.com/DioxusLabs/cli): A Command Line Interface and VSCode extension to assist with Dioxus usage diff --git a/docs/guide/src/en/roadmap.md b/docs/guide/src/en/contributing/roadmap.md similarity index 53% rename from docs/guide/src/en/roadmap.md rename to docs/guide/src/en/contributing/roadmap.md index 5e7318de9..fba5913db 100644 --- a/docs/guide/src/en/roadmap.md +++ b/docs/guide/src/en/contributing/roadmap.md @@ -17,53 +17,55 @@ Generally, here's the status of each platform: - **LiveView**: LiveView support is very young. You'll be figuring things out as you go. Thankfully, none of it is too hard and any work can be upstreamed into Dioxus. ## Features + --- | Feature | Status | Description | | ------------------------- | ------ | -------------------------------------------------------------------- | -| Conditional Rendering | ✅ | if/then to hide/show component | -| Map, Iterator | ✅ | map/filter/reduce to produce rsx! | -| Keyed Components | ✅ | advanced diffing with keys | -| Web | ✅ | renderer for web browser | -| Desktop (webview) | ✅ | renderer for desktop | -| Shared State (Context) | ✅ | share state through the tree | -| Hooks | ✅ | memory cells in components | -| SSR | ✅ | render directly to string | -| Component Children | ✅ | cx.children() as a list of nodes | -| Headless components | ✅ | components that don't return real elements | -| Fragments | ✅ | multiple elements without a real root | -| Manual Props | ✅ | Manually pass in props with spread syntax | -| Controlled Inputs | ✅ | stateful wrappers around inputs | -| CSS/Inline Styles | ✅ | syntax for inline styles/attribute groups | -| Custom elements | ✅ | Define new element primitives | -| Suspense | ✅ | schedule future render from future/promise | -| Integrated error handling | ✅ | Gracefully handle errors with ? syntax | -| NodeRef | ✅ | gain direct access to nodes | -| Re-hydration | ✅ | Pre-render to HTML to speed up first contentful paint | -| Jank-Free Rendering | ✅ | Large diffs are segmented across frames for silky-smooth transitions | -| Effects | ✅ | Run effects after a component has been committed to render | +| Conditional Rendering | ✅ | if/then to hide/show component | +| Map, Iterator | ✅ | map/filter/reduce to produce rsx! | +| Keyed Components | ✅ | advanced diffing with keys | +| Web | ✅ | renderer for web browser | +| Desktop (webview) | ✅ | renderer for desktop | +| Shared State (Context) | ✅ | share state through the tree | +| Hooks | ✅ | memory cells in components | +| SSR | ✅ | render directly to string | +| Component Children | ✅ | cx.children() as a list of nodes | +| Headless components | ✅ | components that don't return real elements | +| Fragments | ✅ | multiple elements without a real root | +| Manual Props | ✅ | Manually pass in props with spread syntax | +| Controlled Inputs | ✅ | stateful wrappers around inputs | +| CSS/Inline Styles | ✅ | syntax for inline styles/attribute groups | +| Custom elements | ✅ | Define new element primitives | +| Suspense | ✅ | schedule future render from future/promise | +| Integrated error handling | ✅ | Gracefully handle errors with ? syntax | +| NodeRef | ✅ | gain direct access to nodes | +| Re-hydration | ✅ | Pre-render to HTML to speed up first contentful paint | +| Jank-Free Rendering | ✅ | Large diffs are segmented across frames for silky-smooth transitions | +| Effects | ✅ | Run effects after a component has been committed to render | | Portals | 🛠 | Render nodes outside of the traditional tree structure | | Cooperative Scheduling | 🛠 | Prioritize important events over non-important events | | Server Components | 🛠 | Hybrid components for SPA and Server | -| Bundle Splitting | 👀 | Efficiently and asynchronously load the app | -| Lazy Components | 👀 | Dynamically load the new components as the page is loaded | -| 1st class global state | ✅ | redux/recoil/mobx on top of context | -| Runs natively | ✅ | runs as a portable binary w/o a runtime (Node) | -| Subtree Memoization | ✅ | skip diffing static element subtrees | -| High-efficiency templates | ✅ | rsx! calls are translated to templates on the DOM's side | -| Compile-time correct | ✅ | Throw errors on invalid template layouts | -| Heuristic Engine | ✅ | track component memory usage to minimize future allocations | -| Fine-grained reactivity | 👀 | Skip diffing for fine-grain updates | +| Bundle Splitting | 👀 | Efficiently and asynchronously load the app | +| Lazy Components | 👀 | Dynamically load the new components as the page is loaded | +| 1st class global state | ✅ | redux/recoil/mobx on top of context | +| Runs natively | ✅ | runs as a portable binary w/o a runtime (Node) | +| Subtree Memoization | ✅ | skip diffing static element subtrees | +| High-efficiency templates | ✅ | rsx! calls are translated to templates on the DOM's side | +| Compile-time correct | ✅ | Throw errors on invalid template layouts | +| Heuristic Engine | ✅ | track component memory usage to minimize future allocations | +| Fine-grained reactivity | 👀 | Skip diffing for fine-grain updates | - ✅ = implemented and working - 🛠 = actively being worked on - 👀 = not yet implemented or being worked on - ## Roadmap + These Features are planned for the future of Dioxus: ### Core + - [x] Release of Dioxus Core - [x] Upgrade documentation to include more theory and be more comprehensive - [x] Support for HTML-side templates for lightning-fast dom manipulation @@ -72,16 +74,18 @@ These Features are planned for the future of Dioxus: - [ ] Support for Portals ### SSR + - [x] SSR Support + Hydration - [ ] Integrated suspense support for SSR ### Desktop + - [ ] Declarative window management - [ ] Templates for building/bundling -- [ ] Fully native renderer - [ ] Access to Canvas/WebGL context natively ### Mobile + - [ ] Mobile standard library - [ ] GPS - [ ] Camera @@ -92,9 +96,9 @@ These Features are planned for the future of Dioxus: - [ ] Notifications - [ ] Clipboard - [ ] Animations -- [ ] Native Renderer ### Bundling (CLI) + - [x] Translation from HTML into RSX - [x] Dev server - [x] Live reload @@ -106,11 +110,11 @@ These Features are planned for the future of Dioxus: - [ ] Image pipeline ### Essential hooks + - [x] Router - [x] Global state management - [ ] Resize observer - ## Work in Progress ### Build Tool From f593b655e27962d2ecdb0fb680e01607953401de Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 7 May 2023 18:26:30 -0500 Subject: [PATCH 33/50] start README example internals walkthrough --- docs/guide/examples/readme_expanded.rs | 107 ++++++++++++++++++ docs/guide/src/en/SUMMARY.md | 1 + .../src/en/contributing/walkthrough_readme.md | 31 +++++ 3 files changed, 139 insertions(+) create mode 100644 docs/guide/examples/readme_expanded.rs create mode 100644 docs/guide/src/en/contributing/walkthrough_readme.md diff --git a/docs/guide/examples/readme_expanded.rs b/docs/guide/examples/readme_expanded.rs new file mode 100644 index 000000000..896d365e5 --- /dev/null +++ b/docs/guide/examples/readme_expanded.rs @@ -0,0 +1,107 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let mut count = use_state(cx, || 0); + + cx.render( + // rsx expands to LazyNodes::new + ::dioxus::core::LazyNodes::new( + move |__cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode { + // The template is every static part of the rsx + static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template { + // This is the source location of the rsx that generated this template. This is used to make hot rsx reloading work. Hot rsx reloading just replaces the template with a new one generated from the rsx by the CLI. + name: "examples\\readme.rs:14:15:250", + // The root nodes are the top level nodes of the rsx + roots: &[ + // The h1 node + ::dioxus::core::TemplateNode::Element { + // Find the built in h1 tag in the dioxus_elements crate exported by the dioxus html crate + tag: dioxus_elements::h1::TAG_NAME, + namespace: dioxus_elements::h1::NAME_SPACE, + attrs: &[], + // The children of the h1 node + children: &[ + // The dynamic count text node + // Any nodes that are dynamic have a dynamic placeholder with a unique index + ::dioxus::core::TemplateNode::DynamicText { + // This index is used to find what element in `dynamic_nodes` to use instead of the placeholder + id: 0usize, + }, + ], + }, + // The up high button node + ::dioxus::core::TemplateNode::Element { + tag: dioxus_elements::button::TAG_NAME, + namespace: dioxus_elements::button::NAME_SPACE, + attrs: &[ + // The dynamic onclick listener attribute + // Any attributes that are dynamic have a dynamic placeholder with a unique index. + ::dioxus::core::TemplateAttribute::Dynamic { + // Similar to dynamic nodes, dynamic attributes have a unique index used to find the attribute in `dynamic_attrs` to use instead of the placeholder + id: 0usize, + }, + ], + children: &[::dioxus::core::TemplateNode::Text { text: "Up high!" }], + }, + // The down low button node + ::dioxus::core::TemplateNode::Element { + tag: dioxus_elements::button::TAG_NAME, + namespace: dioxus_elements::button::NAME_SPACE, + attrs: &[ + // The dynamic onclick listener attribute + ::dioxus::core::TemplateAttribute::Dynamic { id: 1usize }, + ], + children: &[::dioxus::core::TemplateNode::Text { text: "Down low!" }], + }, + ], + // Node paths is a list of paths to every dynamic node in the rsx + node_paths: &[ + // The first node path is the path to the dynamic node with an id of 0 (the count text node) + &[ + // Go to the index 0 root node + 0u8, + // + // Go to the first child of the root node + 0u8, + ], + ], + // Attr paths is a list of paths to every dynamic attribute in the rsx + attr_paths: &[ + // The first attr path is the path to the dynamic attribute with an id of 0 (the up high button onclick listener) + &[ + // Go to the index 1 root node + 1u8, + ], + // The second attr path is the path to the dynamic attribute with an id of 1 (the down low button onclick listener) + &[ + // Go to the index 2 root node + 2u8, + ], + ], + }; + // The VNode is a reference to the template with the dynamic parts of the rsx + ::dioxus::core::VNode { + parent: None, + key: None, + // The static template this node will use. The template is stored in a Cell so it can be replaced with a new template when hot rsx reloading is enabled + template: std::cell::Cell::new(TEMPLATE), + root_ids: Default::default(), + dynamic_nodes: __cx.bump().alloc([ + // The dynamic count text node (dynamic node id 0) + __cx.text_node(format_args!("High-Five counter: {0}", count)), + ]), + dynamic_attrs: __cx.bump().alloc([ + // The dynamic up high button onclick listener (dynamic attribute id 0) + dioxus_elements::events::onclick(__cx, move |_| count += 1), + // The dynamic down low button onclick listener (dynamic attribute id 1) + dioxus_elements::events::onclick(__cx, move |_| count -= 1), + ]), + } + }, + ), + ) +} diff --git a/docs/guide/src/en/SUMMARY.md b/docs/guide/src/en/SUMMARY.md index 5889d4164..08d23a25e 100644 --- a/docs/guide/src/en/SUMMARY.md +++ b/docs/guide/src/en/SUMMARY.md @@ -43,5 +43,6 @@ - [Contributing](contributing/index.md) - [Project Structure](contributing/project_structure.md) + - [Walkthrough of the Hello World Example Internals](contributing/walkthrough_readme.md) - [Guiding Principles](contributing/guiding_principles.md) - [Roadmap](contributing/roadmap.md) diff --git a/docs/guide/src/en/contributing/walkthrough_readme.md b/docs/guide/src/en/contributing/walkthrough_readme.md new file mode 100644 index 000000000..c948a7d6e --- /dev/null +++ b/docs/guide/src/en/contributing/walkthrough_readme.md @@ -0,0 +1,31 @@ +# Walkthrough of the Hello World Example Internals + +This walkthrough will take you through the internals of the Hello World example program. It will explain how major parts of Dioxus interals interact with each other to go from a source file to a running program. + +## The Source File + +We start will a hello world program. This program renders a desktop app with the text "Hello World" in a webview. + +```rust +{{#include ../../../../../examples/readme.rs}} +``` + +[![](https://mermaid.ink/img/pako:eNqNkT1vwyAQhv8KvSlR48HphtQtqjK0S6tuSBGBS0CxwcJHk8rxfy_YVqxKVdR3ug_u4YXrQHmNwOFQ-bMyMhB7fReOJbVxfwyyMSy0l7GSpW1ARda727ksUy5MuSyKgvBC5ULA1h5N8WK_kCkfHWHgrBuiXsBynrvdsY9E3u1iM_eyvFOVVadMnELOap-o1911JLPHZ1b-YqLTc3LjTt7WifTZMJPsPdx1ov3Z_ellfcdL8R8vmTy5eUqsTUpZ-vzZzjAEK6gx1NLqtJwuNwSQwRoF8BRqGU4ChOvTORnJf3w7BZxCxBXERkvCjZXpQTXwg6zaVEVtyYe3cdvD0vsf4bucgw?type=png)](https://mermaid.live/edit#pako:eNqNkT1vwyAQhv8KvSlR48HphtQtqjK0S6tuSBGBS0CxwcJHk8rxfy_YVqxKVdR3ug_u4YXrQHmNwOFQ-bMyMhB7fReOJbVxfwyyMSy0l7GSpW1ARda727ksUy5MuSyKgvBC5ULA1h5N8WK_kCkfHWHgrBuiXsBynrvdsY9E3u1iM_eyvFOVVadMnELOap-o1911JLPHZ1b-YqLTc3LjTt7WifTZMJPsPdx1ov3Z_ellfcdL8R8vmTy5eUqsTUpZ-vzZzjAEK6gx1NLqtJwuNwSQwRoF8BRqGU4ChOvTORnJf3w7BZxCxBXERkvCjZXpQTXwg6zaVEVtyYe3cdvD0vsf4bucgw) + +## The rsx! Macro + +Before rust runs the program, it will expand all macros. Here is what the hello world example looks like expanded: + +```rust +{{#include ../../../examples/readme_expanded.rs}} +``` + +The rsx macro separates the static parts of the rsx (the template) and the dynamic parts (the dynamic_nodes and dynamic_attributes). + +The static template only contains the parts of the rsx that cannot change at runtime with holes for the dynamic parts: + +[![](https://mermaid.ink/img/pako:eNqdksFuwjAMhl8l8wkkKtFx65njdtm0E0GVSQKJoEmVOgKEeHecUrXStO0wn5Lf9u8vcm6ggjZQwf4UzspiJPH2Ib3g6NLuELG1oiMkp0TsLs9EDu2iUeSCH8tz2HJmy3lRFPrqsXGq9mxeLzcbCU6LZSUGXWRdwnY7tY7Tdoko-Dq1U64fODgiUfzJMeuOe7_ZGq-ny2jNhGQu9DqT8NUK6w72RcL8dxgdzv4PnHLAKf-Fk80HoBUDrfkqeBkTUd8EC2hMbNBpXtYtJySQNQ0PqPioMR4lSH_nOkwUPq9eQUUxmQWkViOZtUN-UwPVHk8dq0Y7CvH9uf3-E9wfrmuk1A?type=png)](https://mermaid.live/edit#pako:eNqdksFuwjAMhl8l8wkkKtFx65njdtm0E0GVSQKJoEmVOgKEeHecUrXStO0wn5Lf9u8vcm6ggjZQwf4UzspiJPH2Ib3g6NLuELG1oiMkp0TsLs9EDu2iUeSCH8tz2HJmy3lRFPrqsXGq9mxeLzcbCU6LZSUGXWRdwnY7tY7Tdoko-Dq1U64fODgiUfzJMeuOe7_ZGq-ny2jNhGQu9DqT8NUK6w72RcL8dxgdzv4PnHLAKf-Fk80HoBUDrfkqeBkTUd8EC2hMbNBpXtYtJySQNQ0PqPioMR4lSH_nOkwUPq9eQUUxmQWkViOZtUN-UwPVHk8dq0Y7CvH9uf3-E9wfrmuk1A) + +The dynamic_nodes and dynamic_attributes are the parts of the rsx that can change at runtime: + +[![](https://mermaid.ink/img/pako:eNp1UcFOwzAM_RXLVzZpvUbighDiABfgtkxTlnirtSaZUgc0df130hZEEcwny35-79nu0EZHqHDfxA9bmyTw9KIDlGjz7pDMqQZ3DsazhVCQ7dQbwnEiKxwDvN3NqhN4O4C3q_VaIztYKXjkQ7184HcCG3MQSgq6Mes1bjbTPAV3RdqIJN5l-V__2_Fcf5iY68dgG7ZHBT4WD5ftZfIBN7dQ_Tj4w1B9MVTXGZa_GMYdcIGekjfsymW7oaFRavKkUZXUmXTUqENfcCZLfD0Hi0pSpgXmkzNC92zKATyqvWnaUiXHEtPz9KrxY_0nzYOPmA?type=png)](https://mermaid.live/edit#pako:eNp1UcFOwzAM_RXLVzZpvUbighDiABfgtkxTlnirtSaZUgc0df130hZEEcwny35-79nu0EZHqHDfxA9bmyTw9KIDlGjz7pDMqQZ3DsazhVCQ7dQbwnEiKxwDvN3NqhN4O4C3q_VaIztYKXjkQ7184HcCG3MQSgq6Mes1bjbTPAV3RdqIJN5l-V__2_Fcf5iY68dgG7ZHBT4WD5ftZfIBN7dQ_Tj4w1B9MVTXGZa_GMYdcIGekjfsymW7oaFRavKkUZXUmXTUqENfcCZLfD0Hi0pSpgXmkzNC92zKATyqvWnaUiXHEtPz9KrxY_0nzYOPmA) From fe695bbc00340132d31391687fb48a98078633fe Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 8 May 2023 15:17:40 -0500 Subject: [PATCH 34/50] finish the readme walkthrough --- docs/guide/src/en/SUMMARY.md | 2 +- .../src/en/contributing/walkthrough_readme.md | 99 ++++++++++++++++++- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/docs/guide/src/en/SUMMARY.md b/docs/guide/src/en/SUMMARY.md index 08d23a25e..8c2fbbf55 100644 --- a/docs/guide/src/en/SUMMARY.md +++ b/docs/guide/src/en/SUMMARY.md @@ -43,6 +43,6 @@ - [Contributing](contributing/index.md) - [Project Structure](contributing/project_structure.md) - - [Walkthrough of the Hello World Example Internals](contributing/walkthrough_readme.md) + - [Walkthrough of Internals](contributing/walkthrough_readme.md) - [Guiding Principles](contributing/guiding_principles.md) - [Roadmap](contributing/roadmap.md) diff --git a/docs/guide/src/en/contributing/walkthrough_readme.md b/docs/guide/src/en/contributing/walkthrough_readme.md index c948a7d6e..0160bf636 100644 --- a/docs/guide/src/en/contributing/walkthrough_readme.md +++ b/docs/guide/src/en/contributing/walkthrough_readme.md @@ -1,6 +1,6 @@ # Walkthrough of the Hello World Example Internals -This walkthrough will take you through the internals of the Hello World example program. It will explain how major parts of Dioxus interals interact with each other to go from a source file to a running program. +This walkthrough will take you through the internals of the Hello World example program. It will explain how major parts of Dioxus internals interact with each other to take the readme example from a source file to a running application. This guide should serve as a high-level overview of the internals of Dioxus. It is not meant to be a comprehensive guide. ## The Source File @@ -14,7 +14,7 @@ We start will a hello world program. This program renders a desktop app with the ## The rsx! Macro -Before rust runs the program, it will expand all macros. Here is what the hello world example looks like expanded: +Before the Rust compiler runs the program, it will expand all macros. Here is what the hello world example looks like expanded: ```rust {{#include ../../../examples/readme_expanded.rs}} @@ -29,3 +29,98 @@ The static template only contains the parts of the rsx that cannot change at run The dynamic_nodes and dynamic_attributes are the parts of the rsx that can change at runtime: [![](https://mermaid.ink/img/pako:eNp1UcFOwzAM_RXLVzZpvUbighDiABfgtkxTlnirtSaZUgc0df130hZEEcwny35-79nu0EZHqHDfxA9bmyTw9KIDlGjz7pDMqQZ3DsazhVCQ7dQbwnEiKxwDvN3NqhN4O4C3q_VaIztYKXjkQ7184HcCG3MQSgq6Mes1bjbTPAV3RdqIJN5l-V__2_Fcf5iY68dgG7ZHBT4WD5ftZfIBN7dQ_Tj4w1B9MVTXGZa_GMYdcIGekjfsymW7oaFRavKkUZXUmXTUqENfcCZLfD0Hi0pSpgXmkzNC92zKATyqvWnaUiXHEtPz9KrxY_0nzYOPmA?type=png)](https://mermaid.live/edit#pako:eNp1UcFOwzAM_RXLVzZpvUbighDiABfgtkxTlnirtSaZUgc0df130hZEEcwny35-79nu0EZHqHDfxA9bmyTw9KIDlGjz7pDMqQZ3DsazhVCQ7dQbwnEiKxwDvN3NqhN4O4C3q_VaIztYKXjkQ7184HcCG3MQSgq6Mes1bjbTPAV3RdqIJN5l-V__2_Fcf5iY68dgG7ZHBT4WD5ftZfIBN7dQ_Tj4w1B9MVTXGZa_GMYdcIGekjfsymW7oaFRavKkUZXUmXTUqENfcCZLfD0Hi0pSpgXmkzNC92zKATyqvWnaUiXHEtPz9KrxY_0nzYOPmA) + +## Launching the App + +The app is launched by calling the `launch` function with the root component. Internally, this function will create a new web view using [wry](https://docs.rs/wry/latest/wry/) and create a virtual dom with the root component. This guide will not explain the renderer in-depth, but you can read more about it in the [custom renderer](/guide/custom-renderer) section. + +## The Virtual DOM + +Before we dive into the initial render in the virtual dom, we need to discuss what the virtual dom is. The virtual dom is a representation of the dom that is used to diff the current dom from the new dom. This diff is then used to create a list of mutations that need to be applied to the dom. + +The Virtual Dom roughly looks like this: + +```rust +pub struct VirtualDom { + // All the templates that have been created or set durring hot reloading + pub(crate) templates: FxHashMap>>, + + // A slab of all the scopes that have been created + pub(crate) scopes: ScopeSlab, + + // All scopes that have been marked as dirty + pub(crate) dirty_scopes: BTreeSet, + + // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template + pub(crate) elements: Slab, + + // This receiver is used to receive messages from hooks about what scopes need to be marked as dirty + pub(crate) rx: futures_channel::mpsc::UnboundedReceiver, + + // The changes queued up to be sent to the renderer + pub(crate) mutations: Mutations<'static>, +} +``` + +> What is a [slab](https://docs.rs/slab/latest/slab/)? +> A slab acts like a hashmap with integer keys if you don't care about the value of the keys. It is internally backed by a dense vector which makes it more efficient than a hashmap. When you insert a value into a slab, it returns an integer key that you can use to retrieve the value later. + +> How does Dioxus use slabs? +> Dioxus uses "synchronized slabs" to communicate between the renderer and the VDOM. When an node is created in the Virtual Dom, a ElementId is passed along with the mutation to the renderer to identify the node. These ids are used by the Virtual Dom to reference that nodes in future mutations like setting an attribute on a node or removing a node. +> When the renderer sends an event to the Virtual Dom, it sends the ElementId of the node that the event was triggered on. The Virtual Dom uses this id to find the node in the slab and then run the necessary event handlers. + +The virtual dom is a tree of scopes. A new scope is created for every component when it is first rendered and recycled when the component is unmounted. + +Scopes serve three main purposes: + +1. They store the state of hooks used by the component +2. They store the state for the context API +3. They store the current and previous VNode that was rendered for diffing + +### The Initial Render + +The root scope is created and rebuilt: + +1. The root component is run +2. The root component returns a VNode +3. Mutations for the VNode are created and added to the mutation list (this may involve creating new child components) +4. The VNode is stored in the root scope + +After the root scope is built, the mutations are sent to the renderer to be applied to the dom. + +After the initial render, the root scope looks like this: + +[![](https://mermaid.ink/img/pako:eNqtVE1P4zAQ_SuzPrWikRpWXCLtBRDisItWsOxhCaqM7RKricdyJrQV8N93QtvQNCkfEnOynydv3nxkHoVCbUQipjnOVSYDwc_L1AFbWd3dB-kzuEQkuFLoDUwDFkCZAek9nGDh0RlHK__atA1GkUUHf45f0YbppAqB_aOzIAvz-t7-chN_Y-1bw1WSJKsglIu2w9tktWXxIIuHURT5XCqTYa5NmDguw2R8c5MKq2GcgF46WTB_jafi9rZL0yi5q4jQTSrf9altO4okCn1Ratwyz55Qxuku2ITlTMgs6HCQimsPmb3PvqVi-L5gjXP3QcnxWnL8JZLrwGvR31n0KV-Bx6-r-oVkT_-3G1S-NQLbk9i8rj7udP2cixed2QcDCitHJiQw7ub3EVlNecrPjudG2-6soFO5VbMECmR9T5OnlUY4-AFxfw9aTFst3McU9TK1Otm6NEn_DubBYlX2_dglLXOz48FgwJmJ5lZTlhz6xWgNaFnyDgpymcARHO0W2a9J_l5w2wYXvHuGPcqaQ-rESBQmFNJq3nCPNZoK3l4sUSR81DLMUpG6Z_aTFeHV0imRUKjMSFReSzKnVnKGhUimMi8ZNdoShl-rlfmyOUfCS_cPcePz_B_Wl4pc?type=png)](https://mermaid.live/edit#pako:eNqtVE1P4zAQ_SuzPrWikRpWXCLtBRDisItWsOxhCaqM7RKricdyJrQV8N93QtvQNCkfEnOynydv3nxkHoVCbUQipjnOVSYDwc_L1AFbWd3dB-kzuEQkuFLoDUwDFkCZAek9nGDh0RlHK__atA1GkUUHf45f0YbppAqB_aOzIAvz-t7-chN_Y-1bw1WSJKsglIu2w9tktWXxIIuHURT5XCqTYa5NmDguw2R8c5MKq2GcgF46WTB_jafi9rZL0yi5q4jQTSrf9altO4okCn1Ratwyz55Qxuku2ITlTMgs6HCQimsPmb3PvqVi-L5gjXP3QcnxWnL8JZLrwGvR31n0KV-Bx6-r-oVkT_-3G1S-NQLbk9i8rj7udP2cixed2QcDCitHJiQw7ub3EVlNecrPjudG2-6soFO5VbMECmR9T5OnlUY4-AFxfw9aTFst3McU9TK1Otm6NEn_DubBYlX2_dglLXOz48FgwJmJ5lZTlhz6xWgNaFnyDgpymcARHO0W2a9J_l5w2wYXvHuGPcqaQ-rESBQmFNJq3nCPNZoK3l4sUSR81DLMUpG6Z_aTFeHV0imRUKjMSFReSzKnVnKGhUimMi8ZNdoShl-rlfmyOUfCS_cPcePz_B_Wl4pc) + +### Waiting for Events + +The Virtual Dom will only ever rerender a scope if it is marked as dirty. Each hook is responsible for marking the scope as dirty if the state has changed. Hooks can mark a scope as dirty by sending a message to the Virtual Dom's channel. + +There are generally two ways a scope is marked as dirty: + +1. The renderer triggers an event: This causes an event listener to be called if needed which may mark a component as dirty +2. The renderer calls wait for work: This polls futures which may mark a component as dirty + +Once at least one scope is marked as dirty, the renderer can call `render_with_deadline` to diff the dirty scopes. + +### Diffing Scopes + +If the user clicked the "up high" button, the root scope would be marked as dirty by the use_state hook. Once the desktop renderer calls `render_with_deadline`, the root scope would be diffed. + +To start the diffing process, the component is run. After the root component is run it will look like this: + +[![](https://mermaid.ink/img/pako:eNrFVlFP2zAQ_iuen0BrpCaIl0i8AEJ72KQJtpcRFBnbJVYTn-U4tBXw33dpG5M2CetoBfdkny_ffb67fPIT5SAkjekkhxnPmHXk-3WiCVpZ3T9YZjJyDeDIDQcjycRCQVwmCTOGXEBhQEvtVvG1CWUldwo0-XX-6vVIF5W1GB9cWVbI1_PNL5v8jW3uPFbpmFOc2HK-GfA2WG1ZeJSFx0EQmJxxmUEupE01liEd394mVAkyjolYaFYgfu1P6N1dF8Yzua-cA51WphtTWzsLc872Zan9CnEGUkktuk6fFm_i5NxFRwn9bUimHrIvCT3-N2EBM70j5XBNOTwI5TrxmvQJkr7ELcHx67Jeggz0v92g8q0RaE-iP1193On6NyxecKUeJeFQaSdtTMLu_Xah5ctT_u94Nty2ZwU0zxWfxqQA5PecPq84kq9nfRw7SK0WDiEFZ4O37d34S_-08lFBVfb92KVb5HIrAp0WpjKYKeGyODLz0dohWIkaZNkiJqfkdLvIH6oRaTSoEmm0n06k0a5K0ZdpL61Io0Yt0nfpxc7UQ0_9cJrhyZ8syX-6brS706Mc489Vjja7fbWj3cxDqIdfJJqOaCFtwZTAV8hT7U0ovjBQRmiMS8HsNKGJfsE4Vjm4WWhOY2crOaKVEczJS8WwgAWNJywv0SuFcmB_rJ41y9fNiBqm_wA0MS9_AUuAiy0?type=png)](https://mermaid.live/edit#pako:eNrFVlFP2zAQ_iuen0BrpCaIl0i8AEJ72KQJtpcRFBnbJVYTn-U4tBXw33dpG5M2CetoBfdkny_ffb67fPIT5SAkjekkhxnPmHXk-3WiCVpZ3T9YZjJyDeDIDQcjycRCQVwmCTOGXEBhQEvtVvG1CWUldwo0-XX-6vVIF5W1GB9cWVbI1_PNL5v8jW3uPFbpmFOc2HK-GfA2WG1ZeJSFx0EQmJxxmUEupE01liEd394mVAkyjolYaFYgfu1P6N1dF8Yzua-cA51WphtTWzsLc872Zan9CnEGUkktuk6fFm_i5NxFRwn9bUimHrIvCT3-N2EBM70j5XBNOTwI5TrxmvQJkr7ELcHx67Jeggz0v92g8q0RaE-iP1193On6NyxecKUeJeFQaSdtTMLu_Xah5ctT_u94Nty2ZwU0zxWfxqQA5PecPq84kq9nfRw7SK0WDiEFZ4O37d34S_-08lFBVfb92KVb5HIrAp0WpjKYKeGyODLz0dohWIkaZNkiJqfkdLvIH6oRaTSoEmm0n06k0a5K0ZdpL61Io0Yt0nfpxc7UQ0_9cJrhyZ8syX-6brS706Mc489Vjja7fbWj3cxDqIdfJJqOaCFtwZTAV8hT7U0ovjBQRmiMS8HsNKGJfsE4Vjm4WWhOY2crOaKVEczJS8WwgAWNJywv0SuFcmB_rJ41y9fNiBqm_wA0MS9_AUuAiy0) + +Next, the Virtual Dom will compare the new VNode with the previous VNode and only update the parts of the tree that have changed. + +When a component is re-rendered, the Virtual Dom will compare the new VNode with the previous VNode and only update the parts of the tree that have changed. + +The diffing algorithm goes through the list of dynamic attributes and nodes and compares them to the previous VNode. If the attribute or node has changed, a mutation that describes the change is added to the mutation list. + +Here is what the diffing algorithm looks like for the root scope (red lines indicate that a mutation was generated, and green lines indicate that no mutation was generated) + +[![](https://mermaid.ink/img/pako:eNrFlFFPwjAQx7_KpT7Kko2Elya8qCE-aGLAJ5khpe1Yw9Zbug4k4He3OJjbGPig0T5t17tf_nf777aEo5CEkijBNY-ZsfAwDjW4kxfzhWFZDGNECxOOmYTIYAo2lsCyDG4xzVBLbcv8_RHKSG4V6orSIN0Wxrh8b2RYKr_uTyubd1W92GiWKg7aac6bOU3G803HbVk82xfP_Ok0JEqAT-FeLWJvpFYSOBbaSkMhCMnra5MgtfhWFrPWqHlhL2urT6atbU-oa0PNE8WXFFJ0-nazXakRroddGk9IwYEUnCd5w7Pddr5UTT8ZuVJY5F0fM7ebRLYyXNDgUnprJWxM-9lb7xAQLHe-M2xDYQCD9pD_2hez_kVn-P_rjLq6n3qjYv2iO5qz9DyvPdyv1ETp5eTTJ_7BGvQq8v1TVtl5jXUcRRcrqFh-dI4VtFlBN6t_ynLNkh5JpUmZEm5rbvfhkLiN6H4BQt2jYGYZklC_uzxWWJxsNCfUmkL2SJEJZuWdYs4cKaERS3IXlUJZNI_lGv7cxj2SMf2CeMx5_wBcbK19?type=png)](https://mermaid.live/edit#pako:eNrFlFFPwjAQx7_KpT7Kko2Elya8qCE-aGLAJ5khpe1Yw9Zbug4k4He3OJjbGPig0T5t17tf_nf777aEo5CEkijBNY-ZsfAwDjW4kxfzhWFZDGNECxOOmYTIYAo2lsCyDG4xzVBLbcv8_RHKSG4V6orSIN0Wxrh8b2RYKr_uTyubd1W92GiWKg7aac6bOU3G803HbVk82xfP_Ok0JEqAT-FeLWJvpFYSOBbaSkMhCMnra5MgtfhWFrPWqHlhL2urT6atbU-oa0PNE8WXFFJ0-nazXakRroddGk9IwYEUnCd5w7Pddr5UTT8ZuVJY5F0fM7ebRLYyXNDgUnprJWxM-9lb7xAQLHe-M2xDYQCD9pD_2hez_kVn-P_rjLq6n3qjYv2iO5qz9DyvPdyv1ETp5eTTJ_7BGvQq8v1TVtl5jXUcRRcrqFh-dI4VtFlBN6t_ynLNkh5JpUmZEm5rbvfhkLiN6H4BQt2jYGYZklC_uzxWWJxsNCfUmkL2SJEJZuWdYs4cKaERS3IXlUJZNI_lGv7cxj2SMf2CeMx5_wBcbK19) + +## Conclusion + +This is only a brief overview of how the Virtual Dom works. There are several aspects not yet covered in this guide including how the Virtual Dom handles async-components, keyed diffing, and how it uses bump allocation to efficiently allocate VNodes. If need more information about the Virtual Dom, you can read the code of the [core](https://github.com/DioxusLabs/dioxus/tree/master/packages/core) crate or reach out to us on [Discord](https://discord.gg/XgGxMSkvUM). From 6fc65e95659a0175e6a88e7fc1a219754328b49d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 8 May 2023 15:42:43 -0500 Subject: [PATCH 35/50] finish guiding principles --- docs/guide/src/en/contributing/guiding_principles.md | 10 ++++------ docs/guide/src/en/contributing/index.md | 2 +- docs/guide/src/en/contributing/walkthrough_readme.md | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/guide/src/en/contributing/guiding_principles.md b/docs/guide/src/en/contributing/guiding_principles.md index 6bbb319ec..5c730d622 100644 --- a/docs/guide/src/en/contributing/guiding_principles.md +++ b/docs/guide/src/en/contributing/guiding_principles.md @@ -12,21 +12,19 @@ Dioxus is designed to be cross-platform by default. This means that it should be As Dioxus applications grow, they should remain relatively performant without the need for manual optimizations. There will be cases where manual optimizations are needed, but Dioxus should try to make these cases as rare as possible. -The is one of the benefits of the core architecture of Dioxus. It is based on a Virtual Dom which performs diffing which should prevent unnecessary re-renders even when large parts of the component tree are rerun. On top of this, Dioxus groups static parts of the RSX tree together to skip diffing them entirely. +One of the benefits of the core architecture of Dioxus is that it delivers reasonable performance even when components are rerendered often. It is based on a Virtual Dom which performs diffing which should prevent unnecessary re-renders even when large parts of the component tree are rerun. On top of this, Dioxus groups static parts of the RSX tree together to skip diffing them entirely. ## Type Safety As teams grow, the Type safety of Rust is a huge advantage. Dioxus should leverage this advantage to make it easy to build applications with large teams. -To take full advantage of Rust's type system, Dioxus should try to avoid exposing public `Any` types and stringly typed APIs where possible. +To take full advantage of Rust's type system, Dioxus should try to avoid exposing public `Any` types and string-ly typed APIs where possible. ## Developer Experience Dioxus should be easy to learn and ergonomic to use. -- The API of Dioxus should try to remain close to React's API where possible - - - This makes it easier for people to learn Dioxus if they already know React +- The API of Dioxus attempts to remain close to React's API where possible. This makes it easier for people to learn Dioxus if they already know React - We can avoid the tradeoff between simplicity and flexibility by providing multiple layers of API: One for the very common use case, one for low-level control @@ -35,5 +33,5 @@ Dioxus should be easy to learn and ergonomic to use. - Documentation: - All public APIs should have rust documentation - - Examples should be provided for all public features. These examples both serve as documentation and are checked by CI to ensure that they continue to compile + - Examples should be provided for all public features. These examples both serve as documentation and testing. They are checked by CI to ensure that they continue to compile - The most common workflows should be documented in the guide diff --git a/docs/guide/src/en/contributing/index.md b/docs/guide/src/en/contributing/index.md index 68a8ae5ec..0e37db2ad 100644 --- a/docs/guide/src/en/contributing/index.md +++ b/docs/guide/src/en/contributing/index.md @@ -41,7 +41,7 @@ cargo check --workspace --examples --tests cargo clippy --workspace --examples --tests -- -D warnings ``` -- Crates that use unsafe are checked for undefined behavior with [MIRI](https://github.com/rust-lang/miri). MIRI can be helpful to debug what unsafe code is causing issues. Only code that does not interact with system calls can be checked with MIRI. Currently this is used for the two MIRI tests in `dioxus-core` and `dioxus-native-core`. +- Crates that use unsafe are checked for undefined behavior with [MIRI](https://github.com/rust-lang/miri). MIRI can be helpful to debug what unsafe code is causing issues. Only code that does not interact with system calls can be checked with MIRI. Currently, this is used for the two MIRI tests in `dioxus-core` and `dioxus-native-core`. ```sh cargo miri test --package dioxus-core --test miri_stress diff --git a/docs/guide/src/en/contributing/walkthrough_readme.md b/docs/guide/src/en/contributing/walkthrough_readme.md index 0160bf636..9165d65a8 100644 --- a/docs/guide/src/en/contributing/walkthrough_readme.md +++ b/docs/guide/src/en/contributing/walkthrough_readme.md @@ -123,4 +123,4 @@ Here is what the diffing algorithm looks like for the root scope (red lines indi ## Conclusion -This is only a brief overview of how the Virtual Dom works. There are several aspects not yet covered in this guide including how the Virtual Dom handles async-components, keyed diffing, and how it uses bump allocation to efficiently allocate VNodes. If need more information about the Virtual Dom, you can read the code of the [core](https://github.com/DioxusLabs/dioxus/tree/master/packages/core) crate or reach out to us on [Discord](https://discord.gg/XgGxMSkvUM). +This is only a brief overview of how the Virtual Dom works. There are several aspects not yet covered in this guide including how the Virtual Dom handles async-components, keyed diffing, and how it uses [bump allocation](https://github.com/fitzgen/bumpalo) to efficiently allocate VNodes. If need more information about the Virtual Dom, you can read the code of the [core](https://github.com/DioxusLabs/dioxus/tree/master/packages/core) crate or reach out to us on [Discord](https://discord.gg/XgGxMSkvUM). From ae8a7ccbd26fd0e999465142c00cfbc71d5d05a4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 9 May 2023 07:24:17 -0500 Subject: [PATCH 36/50] fix docs build command --- docs/README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/README.md b/docs/README.md index a505a173e..766752ecf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,14 +8,12 @@ cargo install mdbook --git https://github.com/Demonthos/mdBook.git --branch mast Then, you can build the documentation by running: -Linux and MacOS: - ```sh -cd docs && cd guide && mdbook build -d ../nightly/guide && cd .. && cd router && mdbook build -d ../nightly/router && cd .. && cd .. -``` - -Windows: - -```cmd -cd docs; cd guide; mdbook build -d ../nightly/guide; cd ..; cd router; mdbook build -d ../nightly/router; cd ..; cd .. +cd docs +cd guide +mdbook build -d ../nightly/guide +cd .. +cd router +mdbook build -d ../nightly/router +cd ../../ ``` From 40feb00f491c84d9e8952148bacb5b60e8da2ad3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 10 May 2023 14:45:12 +0200 Subject: [PATCH 37/50] Add mailmap to unify name of Jonathan Kelley --- .mailmap | 1 + 1 file changed, 1 insertion(+) create mode 100644 .mailmap diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000..8562f720b --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +Jonathan Kelley From 35cb6616af3dd85d2370583d2a2e8d575df23d73 Mon Sep 17 00:00:00 2001 From: Alexander Keliris Date: Wed, 10 May 2023 14:21:05 +0100 Subject: [PATCH 38/50] Add `hgroup` html tag (#1016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The [
](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup) HTML element represents a heading and related content. It groups a single

–

element with one or more

. --- 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 3fc400ea9..1f5ece4ee 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -359,6 +359,11 @@ builder_constructors! { /// element. header None {}; + /// Build a + /// [`

`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup) + /// element. + hgroup None {}; + /// Build a /// [`

`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1) /// element. From d059b2bc052c58d623215a6f3ddc199bafb08856 Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Sat, 13 May 2023 11:50:24 -0400 Subject: [PATCH 39/50] doc: mention futures-util in use_coroutine hook --- packages/hooks/src/usecoroutine.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index bc97735b9..a6ffd3e4e 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -33,6 +33,10 @@ use std::future::Future; /// However, you must plan out your own concurrency and synchronization. If you /// don't care about actions in your app being synchronized, you can use [`use_callback`] /// hook to spawn multiple tasks and run them concurrently. +/// +/// ### Notice +/// In order to use ``rx.next().await``, you will need to extend the ``Stream`` trait (used by ``UnboundedReceiver``) +/// by adding the ``futures-util`` crate as a dependency and adding ``StreamExt`` into scope via ``use futures_util::stream::StreamExt;`` /// /// ## Example /// From 06233ac550a851917b42fae38db4470185fa79ab Mon Sep 17 00:00:00 2001 From: Miles Murgaw Date: Sat, 13 May 2023 12:41:00 -0400 Subject: [PATCH 40/50] fix: rustfmt --- packages/hooks/src/usecoroutine.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hooks/src/usecoroutine.rs b/packages/hooks/src/usecoroutine.rs index a6ffd3e4e..b19a355d0 100644 --- a/packages/hooks/src/usecoroutine.rs +++ b/packages/hooks/src/usecoroutine.rs @@ -33,9 +33,9 @@ use std::future::Future; /// However, you must plan out your own concurrency and synchronization. If you /// don't care about actions in your app being synchronized, you can use [`use_callback`] /// hook to spawn multiple tasks and run them concurrently. -/// +/// /// ### Notice -/// In order to use ``rx.next().await``, you will need to extend the ``Stream`` trait (used by ``UnboundedReceiver``) +/// In order to use ``rx.next().await``, you will need to extend the ``Stream`` trait (used by ``UnboundedReceiver``) /// by adding the ``futures-util`` crate as a dependency and adding ``StreamExt`` into scope via ``use futures_util::stream::StreamExt;`` /// /// ## Example From 6acec6f020c6d45c86dd6597a6f4507e731189a1 Mon Sep 17 00:00:00 2001 From: Weru Date: Sun, 14 May 2023 16:16:05 +0300 Subject: [PATCH 41/50] fix router book link (#1024) Signed-off-by: weru --- docs/guide/src/en/interactivity/router.md | 2 +- docs/guide/src/pt-br/interactivity/router.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/src/en/interactivity/router.md b/docs/guide/src/en/interactivity/router.md index 90a90e8a9..4133e5e53 100644 --- a/docs/guide/src/en/interactivity/router.md +++ b/docs/guide/src/en/interactivity/router.md @@ -84,4 +84,4 @@ rsx!{ ## More reading -This page is just a very brief overview of the router. For more information, check out [the router book](https://dioxuslabs.com/router/guide/) or some of [the router examples](https://github.com/DioxusLabs/dioxus/blob/master/examples/router.rs). +This page is just a very brief overview of the router. For more information, check out [the router book](https://dioxuslabs.com/docs/0.3/router/) or some of [the router examples](https://github.com/DioxusLabs/dioxus/blob/master/examples/router.rs). diff --git a/docs/guide/src/pt-br/interactivity/router.md b/docs/guide/src/pt-br/interactivity/router.md index d3ac7abb9..382befddb 100644 --- a/docs/guide/src/pt-br/interactivity/router.md +++ b/docs/guide/src/pt-br/interactivity/router.md @@ -80,4 +80,4 @@ rsx!{ Esta página é apenas uma breve visão geral do roteador para mostrar que existe uma solução poderosa já construída para um problema muito comum. Para obter mais informações sobre o roteador, confira seu livro ou confira alguns dos exemplos. -O roteador tem sua própria documentação! [Disponível aqui](https://dioxuslabs.com/router/guide/). +O roteador tem sua própria documentação! [Disponível aqui](https://dioxuslabs.com/docs/0.3/router/). From 633eae2d5c2b8fbcd9dc235f0bfe2a02a461c80c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 15 May 2023 14:39:32 -0500 Subject: [PATCH 42/50] add use element to html --- packages/html/src/elements.rs | 52 +++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index 1f5ece4ee..85de2f837 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -31,16 +31,16 @@ macro_rules! impl_attribute { ( $(#[$attr_method:meta])* - $fil:ident: $vil:ident (in $ns:ident), + $fil:ident: $vil:ident (in $ns:literal), ) => { - pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), false) + pub const $fil: AttributeDiscription = (stringify!($fil), Some($ns), false) }; ( $(#[$attr_method:meta])* - $fil:ident: $vil:ident (in $ns:ident : volatile), + $fil:ident: $vil:ident (in $ns:literal : volatile), ) => { - pub const $fil: AttributeDiscription = (stringify!($fil), Some(stringify!($ns)), true) + pub const $fil: AttributeDiscription = (stringify!($fil), Some($ns), true) }; } @@ -71,10 +71,10 @@ macro_rules! impl_attribute_match { }; ( - $attr:ident $fil:ident: $vil:ident (in $ns:ident), + $attr:ident $fil:ident: $vil:ident (in $ns:literal), ) => { if $attr == stringify!($fil) { - return Some((stringify!(fil), Some(stringify!(ns)))); + return Some((stringify!(fil), Some(ns))); } }; } @@ -110,7 +110,7 @@ macro_rules! impl_element { ( $(#[$attr:meta])* - $name:ident $namespace:tt { + $name:ident $namespace:literal { $( $(#[$attr_method:meta])* $fil:ident: $vil:ident $extra:tt, @@ -130,7 +130,35 @@ macro_rules! impl_element { $( impl_attribute!( $(#[$attr_method])* - $fil: $vil in $namespace $extra + $fil: $vil ($extra), + ); + )* + } + }; + + ( + $(#[$attr:meta])* + $element:ident [$name:literal, $namespace:tt] { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident $extra:tt, + )* + } + ) => { + #[allow(non_camel_case_types)] + $(#[$attr])* + pub struct $element; + + impl SvgAttributes for $element {} + + impl $element { + pub const TAG_NAME: &'static str = $name; + pub const NAME_SPACE: Option<&'static str> = Some($namespace); + + $( + impl_attribute!( + $(#[$attr_method])* + $fil: $vil ($extra), ); )* } @@ -192,7 +220,7 @@ macro_rules! impl_element_match_attributes { if $el == stringify!($name) { $( impl_attribute_match!( - $attr $fil: $vil in $namespace $extra + $attr $fil: $vil ($extra), ); )* } @@ -1599,7 +1627,7 @@ builder_constructors! { // /// Build a // /// [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/use) // /// element. - // use "http://www.w3.org/2000/svg" {}; - - + r#use ["use", "http://www.w3.org/2000/svg"] { + href: String DEFAULT, + }; } From 2b0a86701ed50bd0b376cdad178ec2916e90fd3b Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 16 May 2023 10:46:13 +0100 Subject: [PATCH 43/50] Upgrade Taffy to v0.3.12] --- packages/native-core/Cargo.toml | 2 +- packages/native-core/src/layout_attributes.rs | 263 +++++++++++------- packages/rink/Cargo.toml | 2 +- 3 files changed, 158 insertions(+), 109 deletions(-) diff --git a/packages/native-core/Cargo.toml b/packages/native-core/Cargo.toml index c9be2f27d..7832a33ae 100644 --- a/packages/native-core/Cargo.toml +++ b/packages/native-core/Cargo.toml @@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react"] dioxus-core = { path = "../core", version = "^0.3.0", optional = true } keyboard-types = "0.6.2" -taffy = "0.2.1" +taffy = "0.3.12" smallvec = "1.6" rustc-hash = "1.1.0" anymap = "1.0.0-beta.2" diff --git a/packages/native-core/src/layout_attributes.rs b/packages/native-core/src/layout_attributes.rs index 84fd61ac4..83e2ae906 100644 --- a/packages/native-core/src/layout_attributes.rs +++ b/packages/native-core/src/layout_attributes.rs @@ -2,7 +2,7 @@ /* - [ ] pub display: Display, ----> taffy doesnt support all display types -- [x] pub position_type: PositionType, --> taffy doesnt support everything +- [x] pub position: Position, --> taffy doesnt support everything - [x] pub direction: Direction, - [x] pub flex_direction: FlexDirection, @@ -21,7 +21,7 @@ - [x] pub padding: Rect, - [x] pub justify_content: JustifyContent, -- [x] pub position: Rect, +- [x] pub inset: Rect, - [x] pub border: Rect, - [ ] pub size: Size, ----> seems to only be relevant for input? @@ -32,9 +32,9 @@ */ use lightningcss::properties::border::LineStyle; -use lightningcss::properties::{align, display, flex, position, size}; +use lightningcss::properties::{align, border, display, flex, position, size}; use lightningcss::{ - properties::{align::GapValue, border::BorderSideWidth, Property, PropertyId}, + properties::{Property, PropertyId}, stylesheet::ParserOptions, traits::Parse, values::{ @@ -45,7 +45,7 @@ use lightningcss::{ }; use taffy::{ prelude::*, - style::{FlexDirection, PositionType}, + style::{FlexDirection, Position}, }; /// Default values for layout attributes @@ -103,25 +103,23 @@ pub fn apply_layout_attributes_cfg( _ => (), }, Property::Position(position) => { - style.position_type = match position { - position::Position::Relative => PositionType::Relative, - position::Position::Absolute => PositionType::Absolute, + style.position = match position { + position::Position::Relative => Position::Relative, + position::Position::Absolute => Position::Absolute, _ => return, } } - Property::Top(top) => style.position.top = convert_length_percentage_or_auto(top), + Property::Top(top) => style.inset.top = convert_length_percentage_or_auto(top), Property::Bottom(bottom) => { - style.position.bottom = convert_length_percentage_or_auto(bottom) - } - Property::Left(left) => style.position.left = convert_length_percentage_or_auto(left), - Property::Right(right) => { - style.position.right = convert_length_percentage_or_auto(right) + style.inset.bottom = convert_length_percentage_or_auto(bottom) } + Property::Left(left) => style.inset.left = convert_length_percentage_or_auto(left), + Property::Right(right) => style.inset.right = convert_length_percentage_or_auto(right), Property::Inset(inset) => { - style.position.top = convert_length_percentage_or_auto(inset.top); - style.position.bottom = convert_length_percentage_or_auto(inset.bottom); - style.position.left = convert_length_percentage_or_auto(inset.left); - style.position.right = convert_length_percentage_or_auto(inset.right); + style.inset.top = convert_length_percentage_or_auto(inset.top); + style.inset.bottom = convert_length_percentage_or_auto(inset.bottom); + style.inset.left = convert_length_percentage_or_auto(inset.left); + style.inset.right = convert_length_percentage_or_auto(inset.right); } Property::BorderTopWidth(width) => { style.border.top = convert_border_side_width(width, &config.border_widths); @@ -164,44 +162,60 @@ pub fn apply_layout_attributes_cfg( } Property::BorderTopStyle(line_style) => { if line_style != LineStyle::None { - style.border.top = - convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + style.border.top = convert_border_side_width( + border::BorderSideWidth::Medium, + &config.border_widths, + ); } } Property::BorderBottomStyle(line_style) => { if line_style != LineStyle::None { - style.border.bottom = - convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + style.border.bottom = convert_border_side_width( + border::BorderSideWidth::Medium, + &config.border_widths, + ); } } Property::BorderLeftStyle(line_style) => { if line_style != LineStyle::None { - style.border.left = - convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + style.border.left = convert_border_side_width( + border::BorderSideWidth::Medium, + &config.border_widths, + ); } } Property::BorderRightStyle(line_style) => { if line_style != LineStyle::None { - style.border.right = - convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + style.border.right = convert_border_side_width( + border::BorderSideWidth::Medium, + &config.border_widths, + ); } } Property::BorderStyle(styles) => { if styles.top != LineStyle::None { - style.border.top = - convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + style.border.top = convert_border_side_width( + border::BorderSideWidth::Medium, + &config.border_widths, + ); } if styles.bottom != LineStyle::None { - style.border.bottom = - convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + style.border.bottom = convert_border_side_width( + border::BorderSideWidth::Medium, + &config.border_widths, + ); } if styles.left != LineStyle::None { - style.border.left = - convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + style.border.left = convert_border_side_width( + border::BorderSideWidth::Medium, + &config.border_widths, + ); } if styles.right != LineStyle::None { - style.border.right = - convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + style.border.right = convert_border_side_width( + border::BorderSideWidth::Medium, + &config.border_widths, + ); } } Property::FlexDirection(flex_direction, _) => { @@ -228,75 +242,73 @@ pub fn apply_layout_attributes_cfg( style.flex_shrink = shrink; } Property::FlexBasis(basis, _) => { - style.flex_basis = convert_length_percentage_or_auto(basis); + style.flex_basis = convert_length_percentage_or_auto(basis).into(); } Property::Flex(flex, _) => { style.flex_grow = flex.grow; style.flex_shrink = flex.shrink; - style.flex_basis = convert_length_percentage_or_auto(flex.basis); + style.flex_basis = convert_length_percentage_or_auto(flex.basis).into(); } + + // Alignment properties Property::AlignContent(align, _) => { use AlignContent::*; style.align_content = match align { align::AlignContent::ContentDistribution(distribution) => match distribution { - align::ContentDistribution::SpaceBetween => SpaceBetween, - align::ContentDistribution::SpaceAround => SpaceAround, - align::ContentDistribution::SpaceEvenly => SpaceEvenly, - align::ContentDistribution::Stretch => Stretch, + align::ContentDistribution::SpaceBetween => Some(SpaceBetween), + align::ContentDistribution::SpaceAround => Some(SpaceAround), + align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly), + align::ContentDistribution::Stretch => Some(Stretch), }, align::AlignContent::ContentPosition { value: position, .. } => match position { - align::ContentPosition::Center => Center, - align::ContentPosition::Start | align::ContentPosition::FlexStart => { - FlexStart - } - align::ContentPosition::End | align::ContentPosition::FlexEnd => FlexEnd, + align::ContentPosition::Center => Some(Center), + align::ContentPosition::Start => Some(Start), + align::ContentPosition::FlexStart => Some(FlexStart), + align::ContentPosition::End => Some(End), + align::ContentPosition::FlexEnd => Some(FlexEnd), }, _ => return, }; } Property::JustifyContent(justify, _) => { - use JustifyContent::*; + use AlignContent::*; style.justify_content = match justify { align::JustifyContent::ContentDistribution(distribution) => { match distribution { - align::ContentDistribution::SpaceBetween => SpaceBetween, - align::ContentDistribution::SpaceAround => SpaceAround, - align::ContentDistribution::SpaceEvenly => SpaceEvenly, + align::ContentDistribution::SpaceBetween => Some(SpaceBetween), + align::ContentDistribution::SpaceAround => Some(SpaceAround), + align::ContentDistribution::SpaceEvenly => Some(SpaceEvenly), _ => return, } } align::JustifyContent::ContentPosition { value: position, .. } => match position { - align::ContentPosition::Center => Center, - // start ignores -reverse flex-direction but there is no way to specify that in Taffy - align::ContentPosition::Start | align::ContentPosition::FlexStart => { - FlexStart - } - // end ignores -reverse flex-direction but there is no way to specify that in Taffy - align::ContentPosition::End | align::ContentPosition::FlexEnd => FlexEnd, + align::ContentPosition::Center => Some(Center), + align::ContentPosition::Start => Some(Start), + align::ContentPosition::FlexStart => Some(FlexStart), + align::ContentPosition::End => Some(End), + align::ContentPosition::FlexEnd => Some(FlexEnd), }, _ => return, }; } Property::AlignSelf(align, _) => { - use AlignSelf::*; + use AlignItems::*; style.align_self = match align { - align::AlignSelf::Auto => Auto, - align::AlignSelf::Stretch => Stretch, - align::AlignSelf::BaselinePosition(_) => Baseline, + align::AlignSelf::Auto => None, + align::AlignSelf::Stretch => Some(Stretch), + align::AlignSelf::BaselinePosition(_) => Some(Baseline), align::AlignSelf::SelfPosition { value: position, .. } => match position { - align::SelfPosition::Center => Center, - align::SelfPosition::Start - | align::SelfPosition::SelfStart - | align::SelfPosition::FlexStart => FlexStart, - align::SelfPosition::End - | align::SelfPosition::SelfEnd - | align::SelfPosition::FlexEnd => FlexEnd, + align::SelfPosition::Center => Some(Center), + align::SelfPosition::Start | align::SelfPosition::SelfStart => Some(Start), + align::SelfPosition::FlexStart => Some(FlexStart), + align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(End), + align::SelfPosition::FlexEnd => Some(FlexEnd), }, _ => return, }; @@ -304,15 +316,18 @@ pub fn apply_layout_attributes_cfg( Property::AlignItems(align, _) => { use AlignItems::*; style.align_items = match align { - align::AlignItems::BaselinePosition(_) => Baseline, - align::AlignItems::Stretch => Stretch, + align::AlignItems::BaselinePosition(_) => Some(Baseline), + align::AlignItems::Stretch => Some(Stretch), align::AlignItems::SelfPosition { value: position, .. } => match position { - align::SelfPosition::Center => Center, - align::SelfPosition::FlexStart => FlexStart, - align::SelfPosition::FlexEnd => FlexEnd, - _ => return, + align::SelfPosition::Center => Some(Center), + align::SelfPosition::FlexStart => Some(FlexStart), + align::SelfPosition::FlexEnd => Some(FlexEnd), + align::SelfPosition::Start | align::SelfPosition::SelfStart => { + Some(FlexEnd) + } + align::SelfPosition::End | align::SelfPosition::SelfEnd => Some(FlexEnd), }, _ => return, }; @@ -350,23 +365,23 @@ pub fn apply_layout_attributes_cfg( }; } Property::PaddingTop(padding) => { - style.padding.top = convert_length_percentage_or_auto(padding); + style.padding.top = convert_padding(padding); } Property::PaddingBottom(padding) => { - style.padding.bottom = convert_length_percentage_or_auto(padding); + style.padding.bottom = convert_padding(padding); } Property::PaddingLeft(padding) => { - style.padding.left = convert_length_percentage_or_auto(padding); + style.padding.left = convert_padding(padding); } Property::PaddingRight(padding) => { - style.padding.right = convert_length_percentage_or_auto(padding); + style.padding.right = convert_padding(padding); } Property::Padding(padding) => { style.padding = Rect { - top: convert_length_percentage_or_auto(padding.top), - bottom: convert_length_percentage_or_auto(padding.bottom), - left: convert_length_percentage_or_auto(padding.left), - right: convert_length_percentage_or_auto(padding.right), + top: convert_padding(padding.top), + bottom: convert_padding(padding.bottom), + left: convert_padding(padding.left), + right: convert_padding(padding.right), }; } Property::Width(width) => { @@ -386,59 +401,93 @@ pub fn apply_layout_attributes_cfg( } } -fn convert_length_value(length_value: LengthValue) -> Dimension { +fn extract_px_value(length_value: LengthValue) -> f32 { match length_value { - LengthValue::Px(value) => Dimension::Points(value), + LengthValue::Px(value) => value, _ => todo!(), } } -fn convert_dimension_percentage( +fn convert_length_percentage( dimension_percentage: DimensionPercentage, -) -> Dimension { +) -> LengthPercentage { match dimension_percentage { - DimensionPercentage::Dimension(value) => convert_length_value(value), - DimensionPercentage::Percentage(percentage) => Dimension::Percent(percentage.0), - _ => todo!(), + DimensionPercentage::Dimension(value) => LengthPercentage::Points(extract_px_value(value)), + DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0), + DimensionPercentage::Calc(_) => todo!(), + } +} + +fn convert_padding(dimension_percentage: LengthPercentageOrAuto) -> LengthPercentage { + match dimension_percentage { + LengthPercentageOrAuto::Auto => unimplemented!(), + LengthPercentageOrAuto::LengthPercentage(lp) => match lp { + DimensionPercentage::Dimension(value) => { + LengthPercentage::Points(extract_px_value(value)) + } + DimensionPercentage::Percentage(percentage) => LengthPercentage::Percent(percentage.0), + DimensionPercentage::Calc(_) => unimplemented!(), + }, } } fn convert_length_percentage_or_auto( - length_percentage_or_auto: LengthPercentageOrAuto, -) -> Dimension { - match length_percentage_or_auto { - LengthPercentageOrAuto::Auto => Dimension::Auto, - LengthPercentageOrAuto::LengthPercentage(percentage) => { - convert_dimension_percentage(percentage) - } + dimension_percentage: LengthPercentageOrAuto, +) -> LengthPercentageAuto { + match dimension_percentage { + LengthPercentageOrAuto::Auto => LengthPercentageAuto::Auto, + LengthPercentageOrAuto::LengthPercentage(lp) => match lp { + DimensionPercentage::Dimension(value) => { + LengthPercentageAuto::Points(extract_px_value(value)) + } + DimensionPercentage::Percentage(percentage) => { + LengthPercentageAuto::Percent(percentage.0) + } + DimensionPercentage::Calc(_) => todo!(), + }, + } +} + +fn convert_dimension(dimension_percentage: DimensionPercentage) -> Dimension { + match dimension_percentage { + DimensionPercentage::Dimension(value) => Dimension::Points(extract_px_value(value)), + DimensionPercentage::Percentage(percentage) => Dimension::Percent(percentage.0), + DimensionPercentage::Calc(_) => todo!(), } } fn convert_border_side_width( - border_side_width: BorderSideWidth, + border_side_width: border::BorderSideWidth, border_width_config: &BorderWidths, -) -> Dimension { +) -> LengthPercentage { match border_side_width { - BorderSideWidth::Length(Length::Value(value)) => convert_length_value(value), - BorderSideWidth::Thick => Dimension::Points(border_width_config.thick), - BorderSideWidth::Medium => Dimension::Points(border_width_config.medium), - BorderSideWidth::Thin => Dimension::Points(border_width_config.thin), - _ => todo!(), + border::BorderSideWidth::Length(Length::Value(value)) => { + LengthPercentage::Points(extract_px_value(value)) + } + border::BorderSideWidth::Thick => LengthPercentage::Points(border_width_config.thick), + border::BorderSideWidth::Medium => LengthPercentage::Points(border_width_config.medium), + border::BorderSideWidth::Thin => LengthPercentage::Points(border_width_config.thin), + border::BorderSideWidth::Length(_) => unimplemented!(), } } -fn convert_gap_value(gap_value: GapValue) -> Dimension { +fn convert_gap_value(gap_value: align::GapValue) -> LengthPercentage { match gap_value { - GapValue::LengthPercentage(dim) => convert_dimension_percentage(dim), - GapValue::Normal => Dimension::Auto, + align::GapValue::LengthPercentage(dim) => convert_length_percentage(dim), + align::GapValue::Normal => LengthPercentage::Points(0.0), } } fn convert_size(size: size::Size) -> Dimension { match size { size::Size::Auto => Dimension::Auto, - size::Size::LengthPercentage(length) => convert_dimension_percentage(length), - _ => todo!(), + size::Size::LengthPercentage(length) => convert_dimension(length), + size::Size::MinContent(_) => Dimension::Auto, // Unimplemented, so default auto + size::Size::MaxContent(_) => Dimension::Auto, // Unimplemented, so default auto + size::Size::FitContent(_) => Dimension::Auto, // Unimplemented, so default auto + size::Size::FitContentFunction(_) => Dimension::Auto, // Unimplemented, so default auto + size::Size::Stretch(_) => Dimension::Auto, // Unimplemented, so default auto + size::Size::Contain => Dimension::Auto, // Unimplemented, so default auto } } diff --git a/packages/rink/Cargo.toml b/packages/rink/Cargo.toml index 2f08eb720..3e19048dd 100644 --- a/packages/rink/Cargo.toml +++ b/packages/rink/Cargo.toml @@ -22,7 +22,7 @@ crossterm = "0.23.0" anyhow = "1.0.42" tokio = { version = "1.15.0", features = ["full"] } futures = "0.3.19" -taffy = "0.2.1" +taffy = "0.3.12" smallvec = "1.6" rustc-hash = "1.1.0" anymap = "1.0.0-beta.2" From c1f7600085a10e9b29a57ad694a370d882e053b3 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 16 May 2023 19:10:11 +0100 Subject: [PATCH 44/50] native-core: add support for CSS Grid properties --- packages/native-core/src/layout_attributes.rs | 130 +++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/packages/native-core/src/layout_attributes.rs b/packages/native-core/src/layout_attributes.rs index 83e2ae906..4997bc708 100644 --- a/packages/native-core/src/layout_attributes.rs +++ b/packages/native-core/src/layout_attributes.rs @@ -11,6 +11,14 @@ - [x] pub flex_shrink: f32, - [x] pub flex_basis: Dimension, +- [ ]pub grid_template_rows: GridTrackVec, +- [ ]pub grid_template_columns: GridTrackVec, +- [ ]pub grid_auto_rows: GridTrackVec, +- [ ]pub grid_auto_columns: GridTrackVec, +- [ ]pub grid_auto_flow: GridAutoFlow, +- [ ]pub grid_row: Line, +- [ ]pub grid_column: Line, + - [x] pub overflow: Overflow, ---> taffy doesnt have support for directional overflow - [x] pub align_items: AlignItems, @@ -28,11 +36,13 @@ - [ ] pub min_size: Size, - [ ] pub max_size: Size, -- [ ] pub aspect_ratio: Number, ----> parsing is done, but taffy doesnt support it +- [x] pub aspect_ratio: Number, */ use lightningcss::properties::border::LineStyle; -use lightningcss::properties::{align, border, display, flex, position, size}; +use lightningcss::properties::grid::{TrackBreadth, TrackList, TrackSizing}; +use lightningcss::properties::{align, border, display, flex, grid, position, size}; +use lightningcss::values::percentage::Percentage; use lightningcss::{ properties::{Property, PropertyId}, stylesheet::ParserOptions, @@ -218,6 +228,8 @@ pub fn apply_layout_attributes_cfg( ); } } + + // Flexbox properties Property::FlexDirection(flex_direction, _) => { use FlexDirection::*; style.flex_direction = match flex_direction { @@ -250,6 +262,56 @@ pub fn apply_layout_attributes_cfg( style.flex_basis = convert_length_percentage_or_auto(flex.basis).into(); } + // Grid properties + Property::GridAutoFlow(grid_auto_flow) => { + let is_row = grid_auto_flow.contains(grid::GridAutoFlow::Row); + let is_dense = grid_auto_flow.contains(grid::GridAutoFlow::Dense); + style.grid_auto_flow = match (is_row, is_dense) { + (true, false) => GridAutoFlow::Row, + (false, false) => GridAutoFlow::Column, + (true, true) => GridAutoFlow::RowDense, + (false, true) => GridAutoFlow::ColumnDense, + }; + } + Property::GridTemplateColumns(TrackSizing::TrackList(track_list)) => { + style.grid_template_columns = track_list + .items + .into_iter() + .map(|item| convert_grid_track_item(item)) + .collect(); + } + Property::GridTemplateRows(TrackSizing::TrackList(track_list)) => { + style.grid_template_rows = track_list + .items + .into_iter() + .map(|item| convert_grid_track_item(item)) + .collect(); + } + Property::GridAutoColumns(grid::TrackSizeList(track_size_list)) => { + style.grid_auto_columns = track_size_list + .into_iter() + .map(|item| convert_grid_track_size(item)) + .collect(); + } + Property::GridAutoRows(grid::TrackSizeList(track_size_list)) => { + style.grid_auto_rows = track_size_list + .into_iter() + .map(|item| convert_grid_track_size(item)) + .collect(); + } + Property::GridRow(grid_row) => { + style.grid_row = Line { + start: convert_grid_placement(grid_row.start), + end: convert_grid_placement(grid_row.end), + }; + } + Property::GridColumn(grid_column) => { + style.grid_column = Line { + start: convert_grid_placement(grid_column.start), + end: convert_grid_placement(grid_column.end), + }; + } + // Alignment properties Property::AlignContent(align, _) => { use AlignContent::*; @@ -491,6 +553,70 @@ fn convert_size(size: size::Size) -> Dimension { } } +fn convert_grid_placement(input: grid::GridLine) -> GridPlacement { + match input { + grid::GridLine::Auto => GridPlacement::Auto, + grid::GridLine::Line { index, .. } => line(index as i16), + grid::GridLine::Span { index, .. } => span(index as u16), + grid::GridLine::Area { .. } => unimplemented!(), + } +} + +fn convert_grid_track_item(input: grid::TrackListItem) -> TrackSizingFunction { + match input { + grid::TrackListItem::TrackSize(size) => { + TrackSizingFunction::Single(convert_grid_track_size(size)) + } + grid::TrackListItem::TrackRepeat(_) => todo!(), // TODO: requires TrackRepeat fields to be public! + } +} + +fn convert_grid_track_size(input: grid::TrackSize) -> NonRepeatedTrackSizingFunction { + match input { + grid::TrackSize::TrackBreadth(breadth) => minmax( + convert_track_breadth_min(&breadth), + convert_track_breadth_max(&breadth), + ), + grid::TrackSize::MinMax { min, max } => minmax( + convert_track_breadth_min(&min), + convert_track_breadth_max(&max), + ), + grid::TrackSize::FitContent(limit) => match limit { + DimensionPercentage::Dimension(LengthValue::Px(len)) => minmax(auto(), points(len)), + DimensionPercentage::Percentage(Percentage(pct)) => minmax(auto(), percent(pct)), + _ => unimplemented!(), + }, + } +} + +fn convert_track_breadth_max(breadth: &TrackBreadth) -> MaxTrackSizingFunction { + match breadth { + grid::TrackBreadth::Length(length_percentage) => match length_percentage { + DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len), + DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct), + _ => unimplemented!(), + }, + grid::TrackBreadth::Flex(fraction) => fr(*fraction), + grid::TrackBreadth::MinContent => MaxTrackSizingFunction::MinContent, + grid::TrackBreadth::MaxContent => MaxTrackSizingFunction::MaxContent, + grid::TrackBreadth::Auto => MaxTrackSizingFunction::Auto, + } +} + +fn convert_track_breadth_min(breadth: &TrackBreadth) -> MinTrackSizingFunction { + match breadth { + grid::TrackBreadth::Length(length_percentage) => match length_percentage { + DimensionPercentage::Dimension(LengthValue::Px(len)) => points(*len), + DimensionPercentage::Percentage(Percentage(pct)) => percent(*pct), + _ => unimplemented!(), + }, + grid::TrackBreadth::MinContent => MinTrackSizingFunction::MinContent, + grid::TrackBreadth::MaxContent => MinTrackSizingFunction::MaxContent, + grid::TrackBreadth::Auto => MinTrackSizingFunction::Auto, + grid::TrackBreadth::Flex(_) => MinTrackSizingFunction::Auto, + } +} + /// parse relative or absolute value pub fn parse_value(value: &str) -> Option { if value.ends_with("px") { From 8010d9b4e3afce740bcc40a111197400886f8474 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 16 May 2023 19:15:29 +0100 Subject: [PATCH 45/50] Fix unused import --- packages/native-core/src/layout_attributes.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/native-core/src/layout_attributes.rs b/packages/native-core/src/layout_attributes.rs index 4997bc708..0f56f59c7 100644 --- a/packages/native-core/src/layout_attributes.rs +++ b/packages/native-core/src/layout_attributes.rs @@ -11,13 +11,13 @@ - [x] pub flex_shrink: f32, - [x] pub flex_basis: Dimension, -- [ ]pub grid_template_rows: GridTrackVec, -- [ ]pub grid_template_columns: GridTrackVec, -- [ ]pub grid_auto_rows: GridTrackVec, -- [ ]pub grid_auto_columns: GridTrackVec, -- [ ]pub grid_auto_flow: GridAutoFlow, -- [ ]pub grid_row: Line, -- [ ]pub grid_column: Line, +- [x]pub grid_auto_flow: GridAutoFlow, +- [x]pub grid_template_rows: GridTrackVec, +- [x]pub grid_template_columns: GridTrackVec, +- [x]pub grid_auto_rows: GridTrackVec, +- [x]pub grid_auto_columns: GridTrackVec, +- [x]pub grid_row: Line, +- [x]pub grid_column: Line, - [x] pub overflow: Overflow, ---> taffy doesnt have support for directional overflow @@ -40,7 +40,7 @@ */ use lightningcss::properties::border::LineStyle; -use lightningcss::properties::grid::{TrackBreadth, TrackList, TrackSizing}; +use lightningcss::properties::grid::{TrackBreadth, TrackSizing}; use lightningcss::properties::{align, border, display, flex, grid, position, size}; use lightningcss::values::percentage::Percentage; use lightningcss::{ From 67d99b3a16d20207ffac905406340638b07b5be9 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Tue, 16 May 2023 20:01:31 +0100 Subject: [PATCH 46/50] Add support for parsing grid value of Display property --- packages/native-core/src/layout_attributes.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/native-core/src/layout_attributes.rs b/packages/native-core/src/layout_attributes.rs index 0f56f59c7..ed2d93d4d 100644 --- a/packages/native-core/src/layout_attributes.rs +++ b/packages/native-core/src/layout_attributes.rs @@ -105,12 +105,16 @@ pub fn apply_layout_attributes_cfg( display::Display::Keyword(display::DisplayKeyword::None) => { style.display = Display::None } - display::Display::Pair(pair) => { - if let display::DisplayInside::Flex(_) = pair.inside { - style.display = Display::Flex + display::Display::Pair(pair) => match pair.inside { + display::DisplayInside::Flex(_) => { + style.display = Display::Flex; } - } - _ => (), + display::DisplayInside::Grid => { + style.display = Display::Grid; + } + _ => {} + }, + _ => {} }, Property::Position(position) => { style.position = match position { From 2d40e0a26136735fe74964854b12ce119ac88368 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 17 May 2023 10:47:31 +0100 Subject: [PATCH 47/50] Fix rink compilation with Taffy 0.3 --- packages/rink/src/layout.rs | 90 +++++++++++++++++++++++-------------- packages/rink/src/lib.rs | 2 +- 2 files changed, 57 insertions(+), 35 deletions(-) diff --git a/packages/rink/src/layout.rs b/packages/rink/src/layout.rs index b7eff57a6..944d39400 100644 --- a/packages/rink/src/layout.rs +++ b/packages/rink/src/layout.rs @@ -81,10 +81,11 @@ impl State for TaffyLayout { }; if let PossiblyUninitalized::Initialized(n) = self.node { if self.style != style { - taffy.set_style(n, style).unwrap(); + taffy.set_style(n, style.clone()).unwrap(); } } else { - self.node = PossiblyUninitalized::Initialized(taffy.new_leaf(style).unwrap()); + self.node = + PossiblyUninitalized::Initialized(taffy.new_leaf(style.clone()).unwrap()); changed = true; } } else { @@ -117,69 +118,90 @@ impl State for TaffyLayout { child_layout.push(l.node.unwrap()); } - fn scale_dimention(d: Dimension) -> Dimension { + fn scale_dimension(d: Dimension) -> Dimension { match d { Dimension::Points(p) => Dimension::Points(unit_to_layout_space(p)), Dimension::Percent(p) => Dimension::Percent(p), Dimension::Auto => Dimension::Auto, - Dimension::Undefined => Dimension::Undefined, } } - let style = Style { - position: Rect { - left: scale_dimention(style.position.left), - right: scale_dimention(style.position.right), - top: scale_dimention(style.position.top), - bottom: scale_dimention(style.position.bottom), + + fn scale_length_percentage_auto(d: LengthPercentageAuto) -> LengthPercentageAuto { + match d { + LengthPercentageAuto::Points(p) => { + LengthPercentageAuto::Points(unit_to_layout_space(p)) + } + LengthPercentageAuto::Percent(p) => LengthPercentageAuto::Percent(p), + LengthPercentageAuto::Auto => LengthPercentageAuto::Auto, + } + } + + fn scale_length_percentage(d: LengthPercentage) -> LengthPercentage { + match d { + LengthPercentage::Points(p) => { + LengthPercentage::Points(unit_to_layout_space(p)) + } + LengthPercentage::Percent(p) => LengthPercentage::Percent(p), + } + } + + let scaled_style = Style { + inset: Rect { + left: scale_length_percentage_auto(style.inset.left), + right: scale_length_percentage_auto(style.inset.right), + top: scale_length_percentage_auto(style.inset.top), + bottom: scale_length_percentage_auto(style.inset.bottom), }, margin: Rect { - left: scale_dimention(style.margin.left), - right: scale_dimention(style.margin.right), - top: scale_dimention(style.margin.top), - bottom: scale_dimention(style.margin.bottom), + left: scale_length_percentage_auto(style.margin.left), + right: scale_length_percentage_auto(style.margin.right), + top: scale_length_percentage_auto(style.margin.top), + bottom: scale_length_percentage_auto(style.margin.bottom), }, padding: Rect { - left: scale_dimention(style.padding.left), - right: scale_dimention(style.padding.right), - top: scale_dimention(style.padding.top), - bottom: scale_dimention(style.padding.bottom), + left: scale_length_percentage(style.padding.left), + right: scale_length_percentage(style.padding.right), + top: scale_length_percentage(style.padding.top), + bottom: scale_length_percentage(style.padding.bottom), }, border: Rect { - left: scale_dimention(style.border.left), - right: scale_dimention(style.border.right), - top: scale_dimention(style.border.top), - bottom: scale_dimention(style.border.bottom), + left: scale_length_percentage(style.border.left), + right: scale_length_percentage(style.border.right), + top: scale_length_percentage(style.border.top), + bottom: scale_length_percentage(style.border.bottom), }, gap: Size { - width: scale_dimention(style.gap.width), - height: scale_dimention(style.gap.height), + width: scale_length_percentage(style.gap.width), + height: scale_length_percentage(style.gap.height), }, - flex_basis: scale_dimention(style.flex_basis), + flex_basis: scale_dimension(style.flex_basis), size: Size { - width: scale_dimention(style.size.width), - height: scale_dimention(style.size.height), + width: scale_dimension(style.size.width), + height: scale_dimension(style.size.height), }, min_size: Size { - width: scale_dimention(style.min_size.width), - height: scale_dimention(style.min_size.height), + width: scale_dimension(style.min_size.width), + height: scale_dimension(style.min_size.height), }, max_size: Size { - width: scale_dimention(style.max_size.width), - height: scale_dimention(style.max_size.height), + width: scale_dimension(style.max_size.width), + height: scale_dimension(style.max_size.height), }, - ..style + ..style.clone() }; if let PossiblyUninitalized::Initialized(n) = self.node { if self.style != style { - taffy.set_style(n, style).unwrap(); + taffy.set_style(n, scaled_style).unwrap(); } if taffy.children(n).unwrap() != child_layout { taffy.set_children(n, &child_layout).unwrap(); } } else { self.node = PossiblyUninitalized::Initialized( - taffy.new_with_children(style, &child_layout).unwrap(), + taffy + .new_with_children(scaled_style, &child_layout) + .unwrap(), ); changed = true; } diff --git a/packages/rink/src/lib.rs b/packages/rink/src/lib.rs index 7f15aa751..8ab53f833 100644 --- a/packages/rink/src/lib.rs +++ b/packages/rink/src/lib.rs @@ -172,7 +172,7 @@ pub fn render( .unwrap(); // the root node fills the entire area - let mut style = *taffy.style(root_node).unwrap(); + let mut style = taffy.style(root_node).unwrap().clone(); let new_size = Size { width: Dimension::Points(width), height: Dimension::Points(height), From f4b828b94976314434a884aaa4a7cf7300c3fdab Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 17 May 2023 13:12:00 +0100 Subject: [PATCH 48/50] Bump Taffy version in dioxus-tui --- packages/dioxus-tui/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dioxus-tui/Cargo.toml b/packages/dioxus-tui/Cargo.toml index 43e568b0f..7058036d1 100644 --- a/packages/dioxus-tui/Cargo.toml +++ b/packages/dioxus-tui/Cargo.toml @@ -24,7 +24,7 @@ rink = { path = "../rink" } crossterm = "0.23.0" tokio = { version = "1.15.0", features = ["full"] } futures = "0.3.19" -taffy = "0.2.1" +taffy = "0.3.12" [dev-dependencies] dioxus = { path = "../dioxus" } From 287227bf48e275233326f6e5f332d36c694502d9 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 17 May 2023 13:13:57 +0100 Subject: [PATCH 49/50] Fix clippy lints --- packages/native-core/src/layout_attributes.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/native-core/src/layout_attributes.rs b/packages/native-core/src/layout_attributes.rs index ed2d93d4d..4eb39292b 100644 --- a/packages/native-core/src/layout_attributes.rs +++ b/packages/native-core/src/layout_attributes.rs @@ -281,26 +281,26 @@ pub fn apply_layout_attributes_cfg( style.grid_template_columns = track_list .items .into_iter() - .map(|item| convert_grid_track_item(item)) + .map(convert_grid_track_item) .collect(); } Property::GridTemplateRows(TrackSizing::TrackList(track_list)) => { style.grid_template_rows = track_list .items .into_iter() - .map(|item| convert_grid_track_item(item)) + .map(convert_grid_track_item) .collect(); } Property::GridAutoColumns(grid::TrackSizeList(track_size_list)) => { style.grid_auto_columns = track_size_list .into_iter() - .map(|item| convert_grid_track_size(item)) + .map(convert_grid_track_size) .collect(); } Property::GridAutoRows(grid::TrackSizeList(track_size_list)) => { style.grid_auto_rows = track_size_list .into_iter() - .map(|item| convert_grid_track_size(item)) + .map(convert_grid_track_size) .collect(); } Property::GridRow(grid_row) => { From d82ae5cb1604eaa07681b3ac6fbff3c9c301145a Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 17 May 2023 13:19:18 +0100 Subject: [PATCH 50/50] dummy commit