From 2772b86629fef49b39db8ae0ab154a62155844a1 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 10 Jan 2023 21:35:11 -0600 Subject: [PATCH 01/16] create with event handler --- packages/desktop/src/cfg.rs | 24 ++++++++++++++++-------- packages/desktop/src/lib.rs | 10 ++++++++-- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index 4456eb2d7..dcf032f10 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,5 +1,7 @@ use std::path::PathBuf; +use wry::application::event::Event; +use wry::application::event_loop::EventLoopWindowTarget; use wry::application::window::Icon; use wry::{ application::window::{Window, WindowBuilder}, @@ -8,12 +10,15 @@ use wry::{ Result as WryResult, }; +use crate::desktop_context::UserWindowEvent; + // pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); /// The configuration for the desktop application. pub struct Config { pub(crate) window: WindowBuilder, pub(crate) file_drop_handler: Option, + pub(crate) event_handler: Option, pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, // pub(crate) event_handler: Option>, @@ -25,6 +30,7 @@ pub struct Config { } type DropHandler = Box bool>; +type EventHandler = Box, &EventLoopWindowTarget)>; pub(crate) type WryProtocol = ( String, @@ -42,6 +48,7 @@ impl Config { window, protocols: Vec::new(), file_drop_handler: None, + event_handler: None, pre_rendered: None, disable_context_menu: !cfg!(debug_assertions), resource_dir: None, @@ -77,14 +84,15 @@ impl Config { self } - // /// Set a custom event handler - // pub fn with_event_handler( - // mut self, - // handler: impl Fn(&mut EventLoop<()>, &mut WebView) + 'static, - // ) -> Self { - // self.event_handler = Some(Box::new(handler)); - // self - // } + /// Set a custom wry event handler. This can be used to listen to window and webview events + /// This only has an effect on the main window + pub fn with_event_handler( + mut self, + handler: impl Fn(&Event, &EventLoopWindowTarget) + 'static, + ) -> Self { + self.event_handler = Some(Box::new(handler)); + self + } /// Set a file drop handler pub fn with_file_drop_handler( diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index a75e6bbbb..0fa27d99f 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -104,7 +104,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub fn launch_with_props(root: Component

, props: P, cfg: Config) { +pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { let event_loop = EventLoop::::with_user_event(); let proxy = event_loop.create_proxy(); @@ -125,6 +125,8 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) let queue = WebviewQueue::default(); + let event_handler = cfg.event_handler.take(); + // By default, we'll create a new window when the app starts queue.borrow_mut().push(create_new_window( cfg, @@ -134,9 +136,13 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) &queue, )); - event_loop.run(move |window_event, _event_loop, control_flow| { + event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; + if let Some(handler) = &event_handler { + handler(&window_event, event_loop); + } + match window_event { Event::WindowEvent { event, window_id, .. From cd17f515d4df37ca4087880f848ec0ee25b73279 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 12 Jan 2023 18:48:23 -0600 Subject: [PATCH 02/16] change to hook api --- examples/window_focus.rs | 42 ++++++++ packages/desktop/Cargo.toml | 1 + packages/desktop/src/cfg.rs | 18 ---- packages/desktop/src/desktop_context.rs | 132 ++++++++++++++++++++++++ packages/desktop/src/lib.rs | 20 ++-- 5 files changed, 187 insertions(+), 26 deletions(-) create mode 100644 examples/window_focus.rs diff --git a/examples/window_focus.rs b/examples/window_focus.rs new file mode 100644 index 000000000..59b4d5166 --- /dev/null +++ b/examples/window_focus.rs @@ -0,0 +1,42 @@ +use dioxus::prelude::*; +use dioxus_desktop::tao::event::WindowEvent; +use dioxus_desktop::use_wry_event_handler; +use dioxus_desktop::wry::application::event::Event as WryEvent; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let focused = use_state(cx, || false); + + use_wry_event_handler(cx, { + to_owned![focused]; + move |event, _| { + if let WryEvent::WindowEvent { + event: WindowEvent::Focused(new_focused), + .. + } = event + { + focused.set(*new_focused); + } + } + }); + + cx.render(rsx! { + div{ + width: "100%", + height: "100%", + display: "flex", + flex_direction: "column", + align_items: "center", + { + if *focused.get() { + "This window is focused!" + } else { + "This window is not focused!" + } + } + } + }) +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 93fd0b29a..9b008ef55 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -32,6 +32,7 @@ tokio = { version = "1.16.1", features = [ webbrowser = "0.8.0" infer = "0.11.0" dunce = "1.0.2" +slab = "0.4" interprocess = { version = "1.1.1", optional = true } futures-util = "0.3.25" diff --git a/packages/desktop/src/cfg.rs b/packages/desktop/src/cfg.rs index dcf032f10..3127a7639 100644 --- a/packages/desktop/src/cfg.rs +++ b/packages/desktop/src/cfg.rs @@ -1,7 +1,5 @@ use std::path::PathBuf; -use wry::application::event::Event; -use wry::application::event_loop::EventLoopWindowTarget; use wry::application::window::Icon; use wry::{ application::window::{Window, WindowBuilder}, @@ -10,18 +8,14 @@ use wry::{ Result as WryResult, }; -use crate::desktop_context::UserWindowEvent; - // pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView); /// The configuration for the desktop application. pub struct Config { pub(crate) window: WindowBuilder, pub(crate) file_drop_handler: Option, - pub(crate) event_handler: Option, pub(crate) protocols: Vec, pub(crate) pre_rendered: Option, - // pub(crate) event_handler: Option>, pub(crate) disable_context_menu: bool, pub(crate) resource_dir: Option, pub(crate) custom_head: Option, @@ -30,7 +24,6 @@ pub struct Config { } type DropHandler = Box bool>; -type EventHandler = Box, &EventLoopWindowTarget)>; pub(crate) type WryProtocol = ( String, @@ -48,7 +41,6 @@ impl Config { window, protocols: Vec::new(), file_drop_handler: None, - event_handler: None, pre_rendered: None, disable_context_menu: !cfg!(debug_assertions), resource_dir: None, @@ -84,16 +76,6 @@ impl Config { self } - /// Set a custom wry event handler. This can be used to listen to window and webview events - /// This only has an effect on the main window - pub fn with_event_handler( - mut self, - handler: impl Fn(&Event, &EventLoopWindowTarget) + 'static, - ) -> Self { - self.event_handler = Some(Box::new(handler)); - self - } - /// Set a file drop handler pub fn with_file_drop_handler( mut self, diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index d3b9216b0..bea8c4ecf 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -10,6 +10,8 @@ use crate::WebviewHandler; use dioxus_core::ScopeState; use dioxus_core::VirtualDom; use serde_json::Value; +use slab::Slab; +use wry::application::event::Event; use wry::application::event_loop::EventLoopProxy; use wry::application::event_loop::EventLoopWindowTarget; #[cfg(target_os = "ios")] @@ -57,6 +59,8 @@ pub struct DesktopContext { pub(crate) event_loop: EventLoopWindowTarget, + pub(crate) event_handlers: WindowEventHandlers, + #[cfg(target_os = "ios")] pub(crate) views: Rc>>, } @@ -76,6 +80,7 @@ impl DesktopContext { proxy: ProxyType, event_loop: EventLoopWindowTarget, webviews: WebviewQueue, + event_handlers: WindowEventHandlers, ) -> Self { Self { webview, @@ -83,6 +88,7 @@ impl DesktopContext { event_loop, eval: tokio::sync::broadcast::channel(8).0, pending_windows: webviews, + event_handlers, #[cfg(target_os = "ios")] views: Default::default(), } @@ -102,6 +108,7 @@ impl DesktopContext { &self.proxy, dom, &self.pending_windows, + &self.event_handlers, ); let id = window.webview.window().id(); @@ -216,6 +223,22 @@ impl DesktopContext { EvalResult::new(self.eval.clone()) } + /// Create a wry event handler that listens for wry events. + /// This event handler is scoped to the currently active window and will only recieve events that are either global or related to the current window. + /// + /// The id this function returns can be used to remove the event handler with [`DesktopContext::remove_wry_event_handler`] + pub fn create_wry_event_handler( + &self, + handler: impl FnMut(&Event, &EventLoopWindowTarget) + 'static, + ) -> WryEventHandlerId { + self.event_handlers.add(self.id(), handler) + } + + /// Remove a wry event handler created with [`DesktopContext::create_wry_event_handler`] + pub fn remove_wry_event_handler(&self, id: WryEventHandlerId) { + self.event_handlers.remove(id) + } + /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { @@ -276,3 +299,112 @@ fn is_main_thread() -> bool { let result: BOOL = unsafe { msg_send![cls, isMainThread] }; result != NO } + +/// The unique identifier of a window event handler. This can be used to later remove the handler. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WryEventHandlerId(usize); + +#[derive(Clone, Default)] +pub(crate) struct WindowEventHandlers { + handlers: Rc>>, +} + +impl WindowEventHandlers { + pub(crate) fn add( + &self, + window_id: WindowId, + handler: impl FnMut(&Event, &EventLoopWindowTarget) + 'static, + ) -> WryEventHandlerId { + WryEventHandlerId( + self.handlers + .borrow_mut() + .insert(WryWindowEventHandlerInner { + window_id, + handler: Box::new(handler), + }), + ) + } + + pub(crate) fn remove(&self, id: WryEventHandlerId) { + self.handlers.borrow_mut().try_remove(id.0); + } + + pub(crate) fn apply_event( + &self, + event: &Event, + target: &EventLoopWindowTarget, + ) { + for (_, handler) in self.handlers.borrow_mut().iter_mut() { + handler.apply_event(event, target); + } + } +} + +struct WryWindowEventHandlerInner { + window_id: WindowId, + handler: + Box, &EventLoopWindowTarget) + 'static>, +} + +impl WryWindowEventHandlerInner { + fn apply_event( + &mut self, + event: &Event, + target: &EventLoopWindowTarget, + ) { + // if this event does not apply to the window this listener cares about, return + match event { + Event::WindowEvent { window_id, .. } + | Event::MenuEvent { + window_id: Some(window_id), + .. + } => { + if *window_id != self.window_id { + return; + } + } + _ => (), + } + (self.handler)(event, target) + } +} + +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_wry_event_handler( + cx: &ScopeState, + handler: impl FnMut(&Event, &EventLoopWindowTarget) + 'static, +) -> &WryEventHandler { + let desktop = use_window(cx); + cx.use_hook(move || { + let desktop = desktop.clone(); + + let id = desktop.create_wry_event_handler(handler); + + WryEventHandler { + handlers: desktop.event_handlers.clone(), + id, + } + }) +} + +/// A wry event handler that is scoped to the current component and window. The event handler will only receive events for the window it was created for and global events. +/// +/// This will automatically be removed when the component is unmounted. +pub struct WryEventHandler { + handlers: WindowEventHandlers, + /// The unique identifier of the event handler. + pub id: WryEventHandlerId, +} + +impl WryEventHandler { + /// Remove the event handler. + pub fn remove(&self) { + self.handlers.remove(self.id); + } +} + +impl Drop for WryEventHandler { + fn drop(&mut self) { + self.handlers.remove(self.id); + } +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 0fa27d99f..9d3130fac 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -16,8 +16,10 @@ mod webview; mod hot_reload; pub use cfg::Config; -pub use desktop_context::{use_window, DesktopContext}; -use desktop_context::{EventData, UserWindowEvent, WebviewQueue}; +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; pub use eval::{use_eval, EvalResult}; @@ -104,7 +106,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { +pub fn launch_with_props(root: Component

, props: P, cfg: Config) { let event_loop = EventLoop::::with_user_event(); let proxy = event_loop.create_proxy(); @@ -123,9 +125,10 @@ pub fn launch_with_props(root: Component

, props: P, mut cfg: Conf // Store them in a hashmap so we can remove them when they're closed let mut webviews = HashMap::::new(); - let queue = WebviewQueue::default(); + // We use this to allow dynamically adding and removing window event handlers + let event_handlers = WindowEventHandlers::default(); - let event_handler = cfg.event_handler.take(); + let queue = WebviewQueue::default(); // By default, we'll create a new window when the app starts queue.borrow_mut().push(create_new_window( @@ -134,14 +137,13 @@ pub fn launch_with_props(root: Component

, props: P, mut cfg: Conf &proxy, VirtualDom::new_with_props(root, props), &queue, + &event_handlers, )); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; - if let Some(handler) = &event_handler { - handler(&window_event, event_loop); - } + event_handlers.apply_event(&window_event, event_loop); match window_event { Event::WindowEvent { @@ -246,6 +248,7 @@ fn create_new_window( proxy: &EventLoopProxy, dom: VirtualDom, queue: &WebviewQueue, + event_handlers: &WindowEventHandlers, ) -> WebviewHandler { let webview = webview::build(&mut cfg, event_loop, proxy.clone()); @@ -254,6 +257,7 @@ fn create_new_window( proxy.clone(), event_loop.clone(), queue.clone(), + event_handlers.clone(), )); let id = webview.window().id(); From 70590c6af04149e718322cbb4b593bfd7ec5c5d3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 14 Jan 2023 16:07:13 -0600 Subject: [PATCH 03/16] pass clippy --- packages/desktop/src/desktop_context.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index bea8c4ecf..c90e97354 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -342,10 +342,12 @@ impl WindowEventHandlers { struct WryWindowEventHandlerInner { window_id: WindowId, - handler: - Box, &EventLoopWindowTarget) + 'static>, + handler: WryEventHandlerCallback, } +type WryEventHandlerCallback = + Box, &EventLoopWindowTarget) + 'static>; + impl WryWindowEventHandlerInner { fn apply_event( &mut self, @@ -381,7 +383,7 @@ pub fn use_wry_event_handler( let id = desktop.create_wry_event_handler(handler); WryEventHandler { - handlers: desktop.event_handlers.clone(), + handlers: desktop.event_handlers, id, } }) From 8548b324f17bb8f83bfe770a9394c1929928c191 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sun, 15 Jan 2023 21:23:49 -0600 Subject: [PATCH 04/16] fix dropping nested scopes --- packages/core/src/arena.rs | 42 ++++++++++---------------------- packages/core/src/diff.rs | 2 +- packages/core/src/nodes.rs | 8 ------ packages/core/src/virtual_dom.rs | 2 +- 4 files changed, 15 insertions(+), 39 deletions(-) diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 39beaa490..75ef20ee1 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -88,7 +88,9 @@ impl VirtualDom { } // Drop a scope and all its children - pub(crate) fn drop_scope(&mut self, id: ScopeId) { + // + // Note: This will not remove any ids from the arena + pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) { self.dirty_scopes.remove(&DirtyScope { height: self.scopes[id.0].height, id, @@ -96,18 +98,13 @@ impl VirtualDom { self.ensure_drop_safety(id); - if let Some(root) = self.scopes[id.0].as_ref().try_root_node() { - if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { - self.drop_scope_inner(node) + if recursive { + if let Some(root) = self.scopes[id.0].as_ref().try_root_node() { + if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { + self.drop_scope_inner(node) + } } } - if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } { - if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { - self.drop_scope_inner(node) - } - } - - self.scopes[id.0].props.take(); let scope = &mut self.scopes[id.0]; @@ -121,37 +118,24 @@ impl VirtualDom { for task_id in scope.spawned_tasks.borrow_mut().drain() { scope.tasks.remove(task_id); } + + self.scopes.remove(id.0); } fn drop_scope_inner(&mut self, node: &VNode) { - node.clear_listeners(); node.dynamic_nodes.iter().for_each(|node| match node { DynamicNode::Component(c) => { if let Some(f) = c.scope.get() { - self.drop_scope(f); + self.drop_scope(f, true); } c.props.take(); } DynamicNode::Fragment(nodes) => { nodes.iter().for_each(|node| self.drop_scope_inner(node)) } - DynamicNode::Placeholder(t) => { - if let Some(id) = t.id.get() { - self.try_reclaim(id); - } - } - DynamicNode::Text(t) => { - if let Some(id) = t.id.get() { - self.try_reclaim(id); - } - } + DynamicNode::Placeholder(_) => {} + DynamicNode::Text(_) => {} }); - - for id in &node.root_ids { - if id.0 != 0 { - self.try_reclaim(id); - } - } } /// Descend through the tree, removing any borrowed props and listeners diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 42cb9c3ff..dbbec6664 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -934,7 +934,7 @@ impl<'b> VirtualDom { *comp.props.borrow_mut() = unsafe { std::mem::transmute(props) }; // Now drop all the resouces - self.drop_scope(scope); + self.drop_scope(scope, false); } fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index dd7e245ba..1ca3d532f 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -195,14 +195,6 @@ impl<'a> VNode<'a> { } } } - - pub(crate) fn clear_listeners(&self) { - for attr in self.dynamic_attrs { - if let AttributeValue::Listener(l) = &attr.value { - l.borrow_mut().take(); - } - } - } } /// A static layout of a UI tree that describes a set of dynamic and static nodes. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index d051e4de2..9380bcaaf 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -682,6 +682,6 @@ impl VirtualDom { impl Drop for VirtualDom { fn drop(&mut self) { // Simply drop this scope which drops all of its children - self.drop_scope(ScopeId(0)); + self.drop_scope(ScopeId(0), true); } } From b6e4db1f2f2274aed8c1d0eef0e4ecea8402ceb9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 16 Jan 2023 16:36:50 -0800 Subject: [PATCH 05/16] fix: immediate exprs --- packages/autofmt/src/expr.rs | 7 ++----- packages/autofmt/src/lib.rs | 8 ++++++-- packages/autofmt/tests/samples.rs | 3 ++- packages/autofmt/tests/samples/immediate_expr.rsx | 4 ++++ 4 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 packages/autofmt/tests/samples/immediate_expr.rsx diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs index 9e09cdeb5..3ff133e7f 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -16,11 +16,8 @@ impl Writer<'_> { // if the expr is on one line, just write it directly if start.line == end.line { - write!( - self.out, - "{}", - &self.src[start.line - 1][start.column - 1..end.column].trim() - )?; + let row = &self.src[start.line - 1][start.column..end.column].trim(); + write!(self.out, "{}", row)?; return Ok(()); } diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 18a43c1b2..5b98fb1b6 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -1,6 +1,6 @@ use crate::writer::*; use collect_macros::byte_offset; -use dioxus_rsx::CallBody; +use dioxus_rsx::{BodyNode, CallBody}; use proc_macro2::LineColumn; use syn::{ExprMacro, MacroDelimiter}; @@ -105,7 +105,11 @@ pub fn fmt_file(contents: &str) -> Vec { let start = byte_offset(contents, span.start()) + 1; let end = byte_offset(contents, span.end()) - 1; - if formatted.len() <= 80 && !formatted.contains('\n') { + // Rustfmt will remove the space between the macro and the opening paren if the macro is a single expression + let body_is_solo_expr = + body.roots.len() == 1 && matches!(body.roots[0], BodyNode::RawExpr(_)); + + if formatted.len() <= 80 && !formatted.contains('\n') && !body_is_solo_expr { formatted = format!(" {} ", formatted); } diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs index 0c031c0d2..b3198f9a7 100644 --- a/packages/autofmt/tests/samples.rs +++ b/packages/autofmt/tests/samples.rs @@ -37,5 +37,6 @@ twoway![ long_exprs, ifchain_forloop, t2, - reallylong + reallylong, + immediate_expr ]; diff --git a/packages/autofmt/tests/samples/immediate_expr.rsx b/packages/autofmt/tests/samples/immediate_expr.rsx new file mode 100644 index 000000000..9e298499d --- /dev/null +++ b/packages/autofmt/tests/samples/immediate_expr.rsx @@ -0,0 +1,4 @@ +fn it_works() { + cx.render(rsx!(())) +} + From b81eca0433a6f50c23d348f82c420ff5ca7d6dbd Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 16 Jan 2023 17:25:55 -0800 Subject: [PATCH 06/16] fix: short expr lists should not eat the final exprs --- packages/autofmt/src/element.rs | 6 +-- packages/autofmt/src/lib.rs | 49 +++++++++---------- packages/autofmt/tests/samples.rs | 3 +- .../autofmt/tests/samples/collapse_expr.rsx | 4 ++ 4 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 packages/autofmt/tests/samples/collapse_expr.rsx diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 43e556bcb..a949e69f4 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -328,10 +328,8 @@ impl Writer<'_> { None } } - [BodyNode::RawExpr(ref expr)] => { - // TODO: let rawexprs to be inlined - get_expr_length(expr) - } + // TODO: let rawexprs to be inlined + [BodyNode::RawExpr(ref expr)] => get_expr_length(expr), [BodyNode::Element(ref el)] => { let attr_len = self.is_short_attrs(&el.attributes); diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 5b98fb1b6..7961a256c 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -79,12 +79,7 @@ pub fn fmt_file(contents: &str) -> Vec { .count() / 4; - // Oneliner optimization - if writer.is_short_children(&body.roots).is_some() { - writer.write_ident(&body.roots[0]).unwrap(); - } else { - writer.write_body_indented(&body.roots).unwrap(); - } + write_body(&mut writer, &body); // writing idents leaves the final line ended at the end of the last ident if writer.out.buf.contains('\n') { @@ -106,8 +101,8 @@ pub fn fmt_file(contents: &str) -> Vec { let end = byte_offset(contents, span.end()) - 1; // Rustfmt will remove the space between the macro and the opening paren if the macro is a single expression - let body_is_solo_expr = - body.roots.len() == 1 && matches!(body.roots[0], BodyNode::RawExpr(_)); + let body_is_solo_expr = body.roots.len() == 1 + && matches!(body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_)); if formatted.len() <= 80 && !formatted.contains('\n') && !body_is_solo_expr { formatted = format!(" {} ", formatted); @@ -135,14 +130,28 @@ pub fn write_block_out(body: CallBody) -> Option { ..Writer::default() }; - // Oneliner optimization + write_body(&mut buf, &body); + + buf.consume() +} + +fn write_body(buf: &mut Writer, body: &CallBody) { + use std::fmt::Write; + if buf.is_short_children(&body.roots).is_some() { - buf.write_ident(&body.roots[0]).unwrap(); + // write all the indents with spaces and commas between + for idx in 0..body.roots.len() - 1 { + let ident = &body.roots[idx]; + buf.write_ident(&ident).unwrap(); + write!(&mut buf.out.buf, ", ").unwrap(); + } + + // write the last ident without a comma + let ident = &body.roots[body.roots.len() - 1]; + buf.write_ident(&ident).unwrap(); } else { buf.write_body_indented(&body.roots).unwrap(); } - - buf.consume() } pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option { @@ -153,12 +162,7 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option { ..Writer::default() }; - // Oneliner optimization - if buf.is_short_children(&body.roots).is_some() { - buf.write_ident(&body.roots[0]).unwrap(); - } else { - buf.write_body_indented(&body.roots).unwrap(); - } + write_body(&mut buf, &body); buf.consume() } @@ -173,12 +177,7 @@ pub fn fmt_block(block: &str, indent_level: usize) -> Option { buf.out.indent = indent_level; - // Oneliner optimization - if buf.is_short_children(&body.roots).is_some() { - buf.write_ident(&body.roots[0]).unwrap(); - } else { - buf.write_body_indented(&body.roots).unwrap(); - } + write_body(&mut buf, &body); // writing idents leaves the final line ended at the end of the last ident if buf.out.buf.contains('\n') { @@ -195,8 +194,6 @@ pub fn apply_format(input: &str, block: FormattedBlock) -> String { let (left, _) = input.split_at(start); let (_, right) = input.split_at(end); - // dbg!(&block.formatted); - format!("{}{}{}", left, block.formatted, right) } diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs index b3198f9a7..84c48fde2 100644 --- a/packages/autofmt/tests/samples.rs +++ b/packages/autofmt/tests/samples.rs @@ -38,5 +38,6 @@ twoway![ ifchain_forloop, t2, reallylong, - immediate_expr + immediate_expr, + collapse_expr ]; diff --git a/packages/autofmt/tests/samples/collapse_expr.rsx b/packages/autofmt/tests/samples/collapse_expr.rsx new file mode 100644 index 000000000..189717a0a --- /dev/null +++ b/packages/autofmt/tests/samples/collapse_expr.rsx @@ -0,0 +1,4 @@ +fn itworks() { + rsx!( "{name}", "{name}", "{name}" ) +} + From 61b8a25ad84bbfdc8171cd42a4ca6a019e2742ae Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 17 Jan 2023 10:51:23 -0800 Subject: [PATCH 07/16] make clippy happy --- packages/autofmt/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 7961a256c..82cebffed 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -142,13 +142,13 @@ fn write_body(buf: &mut Writer, body: &CallBody) { // write all the indents with spaces and commas between for idx in 0..body.roots.len() - 1 { let ident = &body.roots[idx]; - buf.write_ident(&ident).unwrap(); + buf.write_ident(ident).unwrap(); write!(&mut buf.out.buf, ", ").unwrap(); } // write the last ident without a comma let ident = &body.roots[body.roots.len() - 1]; - buf.write_ident(&ident).unwrap(); + buf.write_ident(ident).unwrap(); } else { buf.write_body_indented(&body.roots).unwrap(); } From 07447fd756f60e4fcf68e677812450e1dc8fe277 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 17 Jan 2023 15:50:04 -0800 Subject: [PATCH 08/16] Fix 782 - inline props should use cx token --- packages/core-macro/src/inlineprops.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-macro/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs index dc9c4711b..e814e32fd 100644 --- a/packages/core-macro/src/inlineprops.rs +++ b/packages/core-macro/src/inlineprops.rs @@ -158,7 +158,7 @@ impl ToTokens for InlinePropsBody { #maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output #where_clause { - let #struct_name { #(#field_names),* } = &cx.props; + let #struct_name { #(#field_names),* } = &#cx_token.props; #block } }); From 4e4e101eee88905b77f3d7bae9d4d5cc43fb05b6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 17 Jan 2023 16:24:29 -0800 Subject: [PATCH 09/16] fix: dont choke on emojji n exprs --- packages/autofmt/src/expr.rs | 7 +++++-- packages/autofmt/src/lib.rs | 20 ++++--------------- packages/autofmt/src/writer.rs | 16 +++++++++++++-- packages/autofmt/tests/samples.rs | 3 ++- .../autofmt/tests/samples/trailing_expr.rsx | 7 +++++++ 5 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 packages/autofmt/tests/samples/trailing_expr.rsx diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs index 3ff133e7f..daf0c36ec 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -3,7 +3,7 @@ use std::fmt::{Result, Write}; use proc_macro2::Span; -use crate::Writer; +use crate::{collect_macros::byte_offset, Writer}; impl Writer<'_> { pub fn write_raw_expr(&mut self, placement: Span) -> Result { @@ -16,7 +16,10 @@ impl Writer<'_> { // if the expr is on one line, just write it directly if start.line == end.line { - let row = &self.src[start.line - 1][start.column..end.column].trim(); + // split counting utf8 chars + let start = byte_offset(self.raw_src, start); + let end = byte_offset(self.raw_src, end); + let row = self.raw_src[start..end].trim(); write!(self.out, "{}", row)?; return Ok(()); } diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 82cebffed..f1c888ff4 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -53,10 +53,7 @@ pub fn fmt_file(contents: &str) -> Vec { return formatted_blocks; } - let mut writer = Writer { - src: contents.lines().collect::>(), - ..Writer::default() - }; + let mut writer = Writer::new(contents); // Dont parse nested macros let mut end_span = LineColumn { column: 0, line: 0 }; @@ -125,10 +122,7 @@ pub fn fmt_file(contents: &str) -> Vec { } pub fn write_block_out(body: CallBody) -> Option { - let mut buf = Writer { - src: vec![""], - ..Writer::default() - }; + let mut buf = Writer::new(""); write_body(&mut buf, &body); @@ -157,10 +151,7 @@ fn write_body(buf: &mut Writer, body: &CallBody) { pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option { let body = syn::parse2::(expr.mac.tokens).unwrap(); - let mut buf = Writer { - src: raw.lines().collect(), - ..Writer::default() - }; + let mut buf = Writer::new(raw); write_body(&mut buf, &body); @@ -170,10 +161,7 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option { pub fn fmt_block(block: &str, indent_level: usize) -> Option { let body = syn::parse_str::(block).unwrap(); - let mut buf = Writer { - src: block.lines().collect(), - ..Writer::default() - }; + let mut buf = Writer::new(block); buf.out.indent = indent_level; diff --git a/packages/autofmt/src/writer.rs b/packages/autofmt/src/writer.rs index cdbdbed45..6a0e38d82 100644 --- a/packages/autofmt/src/writer.rs +++ b/packages/autofmt/src/writer.rs @@ -9,8 +9,9 @@ use syn::{spanned::Spanned, Expr, ExprIf}; use crate::buffer::Buffer; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Writer<'a> { + pub raw_src: &'a str, pub src: Vec<&'a str>, pub cached_formats: HashMap, pub comments: VecDeque, @@ -31,7 +32,18 @@ impl Location { } } -impl Writer<'_> { +impl<'a> Writer<'a> { + pub fn new(raw_src: &'a str) -> Self { + let src = raw_src.lines().collect(); + Self { + raw_src, + src, + cached_formats: HashMap::new(), + comments: VecDeque::new(), + out: Buffer::default(), + } + } + // Expects to be written directly into place pub fn write_ident(&mut self, node: &BodyNode) -> Result { match node { diff --git a/packages/autofmt/tests/samples.rs b/packages/autofmt/tests/samples.rs index 84c48fde2..19e636926 100644 --- a/packages/autofmt/tests/samples.rs +++ b/packages/autofmt/tests/samples.rs @@ -39,5 +39,6 @@ twoway![ t2, reallylong, immediate_expr, - collapse_expr + collapse_expr, + trailing_expr ]; diff --git a/packages/autofmt/tests/samples/trailing_expr.rsx b/packages/autofmt/tests/samples/trailing_expr.rsx new file mode 100644 index 000000000..667b8bb32 --- /dev/null +++ b/packages/autofmt/tests/samples/trailing_expr.rsx @@ -0,0 +1,7 @@ +fn it_works() { + cx.render(rsx! { + div { + span { "Description: ", package.description.as_deref().unwrap_or("❌❌❌❌ missing") } + } + }) +} From d6a9f6e2469bbdc814bb74ab4d607216958bda40 Mon Sep 17 00:00:00 2001 From: Igor Loskutov Date: Fri, 20 Jan 2023 16:24:43 +0700 Subject: [PATCH 10/16] Clean up a removed example from examples/README.md code removed in https://github.com/DioxusLabs/dioxus/commit/c4a18bc24d8722dda6c7c432a18f11671046718a --- examples/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/README.md b/examples/README.md index 2533722ed..b8ad09cab 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,8 +18,6 @@ cargo run --example hello_world [custom_assets](./custom_assets.rs) - Include images -[custom_element](./custom_element.rs) - Render webcomponents - [custom_html](./custom_html.rs) - Customize wrapper HTML [eval](./eval.rs) - Evaluate JS expressions @@ -175,4 +173,4 @@ Missing examples - Custom elements - Component Children: Pass children into child components - Render To string: Render a mounted virtualdom to a string -- Testing and Debugging \ No newline at end of file +- Testing and Debugging From 2113827ed93fa4e3dced8092f46510a10d1ab534 Mon Sep 17 00:00:00 2001 From: Igor Loskutov Date: Fri, 20 Jan 2023 16:31:26 +0700 Subject: [PATCH 11/16] Remove tui_* examples from README --- examples/README.md | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/examples/README.md b/examples/README.md index b8ad09cab..3963d186d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -130,36 +130,6 @@ cargo run --example hello_world [todomvc](./todomvc.rs) - Todo task list example -## Terminal UI - -[tui_all_events](../packages/tui/examples/tui_all_events.rs) - All of the terminal events - -[tui_border](../packages/tui/examples/tui_border.rs) - Different styles of borders and corners - -[tui_buttons](../packages/tui/examples/tui_buttons.rs) - A grid of buttons that work demonstrate the focus system - -[tui_color_test](../packages/tui/examples/tui_color_test.rs) - Grid of colors to demonstrate compatablility with different terminal color rendering modes - -[tui_colorpicker](../packages/tui/examples/tui_colorpicker.rs) - A colorpicker - -[tui_components](../packages/tui/examples/tui_components.rs) - Simple component example - -[tui_flex](../packages/tui/examples/tui_flex.rs) - Flexbox support - -[tui_hover](../packages/tui/examples/tui_hover.rs) - Detect hover and mouse events - -[tui_list](../packages/tui/examples/tui_list.rs) - Renders a list of items - -[tui_margin](../packages/tui/examples/tui_margin.rs) - Margins around flexboxes - -[tui_quadrants](../packages/tui/examples/tui_quadrants.rs) - Four quadrants - -[tui_readme](../packages/tui/examples/tui_readme.rs) - The readme example - -[tui_task](../packages/tui/examples/tui_task.rs) - Background tasks - -[tui_text](../packages/tui/examples/tui_text.rs) - Simple text example - # TODO Missing Features - Fine-grained reactivity From 8e3f2bcbe532c38d3abc9fb96c803c9c6288f8ab Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Jan 2023 07:46:29 -0600 Subject: [PATCH 12/16] create stable docs workflow --- .github/workflows/docs stable.yml | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/docs stable.yml diff --git a/.github/workflows/docs stable.yml b/.github/workflows/docs stable.yml new file mode 100644 index 000000000..895e7e8d0 --- /dev/null +++ b/.github/workflows/docs stable.yml @@ -0,0 +1,37 @@ +name: docs stable + +on: + workflow_dispatch: + +jobs: + build-deploy: + runs-on: ubuntu-latest + environment: docs + steps: + + # NOTE: Comment out when https://github.com/rust-lang/mdBook/pull/1306 is merged and released + # - name: Setup mdBook + # uses: peaceiris/actions-mdbook@v1 + # with: + # mdbook-version: "0.4.10" + + # NOTE: Delete when the previous one is enabled + - name: Setup mdBook + run: | + cargo install mdbook --git https://github.com/Demonthos/mdBook.git --branch master + - uses: actions/checkout@v3 + + - name: Build + run: cd docs && + cd guide && mdbook build -d ../nightly/guide && cd .. && + cd router && mdbook build -d ../nightly/router && cd .. + + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@v4.4.1 + with: + branch: gh-pages # The branch the action should deploy to. + folder: docs/nightly # The folder the action should deploy. + target-folder: docs + repository-name: dioxuslabs/docsite + clean: false + token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now From dc768fee2f98b91c4ca173c3e4ef263ad4cfc2ff Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Jan 2023 13:31:21 -0600 Subject: [PATCH 13/16] fix replacing a dynamic text node in native core --- packages/native-core/src/real_dom.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 1aa75c2d8..735421673 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -195,8 +195,7 @@ impl, V: FromAnyValue> RealDom { text: value.to_string(), }); let node_id = self.create_node(node); - let node = self.tree.get_mut(node_id).unwrap(); - node.node_data.element_id = Some(id); + self.set_element_id(node_id, id); self.stack.push(node_id); mark_dirty(node_id, NodeMask::new().with_text(), &mut nodes_updated); } From 8030ae5581119994138cf9937b815c4cb62b82ac Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Jan 2023 17:09:19 -0600 Subject: [PATCH 14/16] make text editing utilites agnostic over the text storage --- packages/native-core/src/utils/cursor.rs | 239 ++++++++++++++++------- packages/tui/src/widgets/number.rs | 8 +- packages/tui/src/widgets/password.rs | 8 +- packages/tui/src/widgets/textbox.rs | 8 +- 4 files changed, 177 insertions(+), 86 deletions(-) diff --git a/packages/native-core/src/utils/cursor.rs b/packages/native-core/src/utils/cursor.rs index c042d4cbf..69aab539d 100644 --- a/packages/native-core/src/utils/cursor.rs +++ b/packages/native-core/src/utils/cursor.rs @@ -1,87 +1,160 @@ -use std::cmp::Ordering; +use std::{cmp::Ordering, ops::Range}; use dioxus_html::input_data::keyboard_types::{Code, Key, Modifiers}; +/// This contains the information about the text that is used by the cursor to handle navigation. +pub trait Text { + /// Returns the line at the given index. + fn line(&self, number: usize) -> Option<&Self>; + /// Returns the length of the text in characters. + fn length(&self) -> usize; + /// Returns the number of lines in the text. + fn line_count(&self) -> usize; + /// Returns the character at the given character index. + fn character(&self, idx: usize) -> Option; + /// Returns the length of the text before the given line in characters. + fn len_before_line(&self, line: usize) -> usize; +} + +impl Text for str { + fn line(&self, number: usize) -> Option<&str> { + self.lines().nth(number) + } + + fn length(&self) -> usize { + self.chars().count() + } + + fn line_count(&self) -> usize { + self.lines().count() + } + + fn character(&self, idx: usize) -> Option { + self.chars().nth(idx) + } + + fn len_before_line(&self, line: usize) -> usize { + self.lines() + .take(line) + .map(|l| l.chars().count()) + .sum::() + } +} + +/// This contains the information about the text that is used by the cursor to handle editing text. +pub trait TextEditable: AsRef { + /// Inserts a character at the given character index. + fn insert_character(&mut self, idx: usize, text: char); + /// Deletes the given character range. + fn delete_range(&mut self, range: Range); +} + +impl TextEditable for String { + fn insert_character(&mut self, idx: usize, text: char) { + self.insert(idx, text); + } + + fn delete_range(&mut self, range: Range) { + self.replace_range(range, ""); + } +} + +/// A cursor position #[derive(Debug, Clone, PartialEq, Eq)] pub struct Pos { + /// The virtual column of the cursor. This can be more than the line length. To get the realized column, use [`Pos::col()`]. pub col: usize, + /// The row of the cursor. pub row: usize, } impl Pos { + /// Creates a new cursor position. pub fn new(col: usize, row: usize) -> Self { Self { row, col } } - pub fn up(&mut self, rope: &str) { - self.move_row(-1, rope); + /// Moves the position up by one line. + pub fn up(&mut self, text: &(impl Text + ?Sized)) { + self.move_row(-1, text); } - pub fn down(&mut self, rope: &str) { - self.move_row(1, rope); + /// Moves the position down by one line. + pub fn down(&mut self, text: &(impl Text + ?Sized)) { + self.move_row(1, text); } - pub fn right(&mut self, rope: &str) { - self.move_col(1, rope); + /// Moves the position right by one character. + pub fn right(&mut self, text: &(impl Text + ?Sized)) { + self.move_col(1, text); } - pub fn left(&mut self, rope: &str) { - self.move_col(-1, rope); + /// Moves the position left by one character. + pub fn left(&mut self, text: &(impl Text + ?Sized)) { + self.move_col(-1, text); } - pub fn move_row(&mut self, change: i32, rope: &str) { + /// Move the position's row by the given amount. (positive is down, negative is up) + pub fn move_row(&mut self, change: i32, text: &(impl Text + ?Sized)) { let new = self.row as i32 + change; - if new >= 0 && new < rope.lines().count() as i32 { + if new >= 0 && new < text.line_count() as i32 { self.row = new as usize; } } - pub fn move_col(&mut self, change: i32, rope: &str) { - self.realize_col(rope); - let idx = self.idx(rope) as i32; - if idx + change >= 0 && idx + change <= rope.len() as i32 { - let len_line = self.len_line(rope) as i32; + /// Move the position's column by the given amount. (positive is right, negative is left) + pub fn move_col(&mut self, change: i32, text: &(impl Text + ?Sized)) { + self.realize_col(text); + let idx = self.idx(text) as i32; + if idx + change >= 0 && idx + change <= text.length() as i32 { + let len_line = self.len_line(text) as i32; let new_col = self.col as i32 + change; let diff = new_col - len_line; if diff > 0 { - self.down(rope); + self.down(text); self.col = 0; - self.move_col(diff - 1, rope); + self.move_col(diff - 1, text); } else if new_col < 0 { - self.up(rope); - self.col = self.len_line(rope); - self.move_col(new_col + 1, rope); + self.up(text); + self.col = self.len_line(text); + self.move_col(new_col + 1, text); } else { self.col = new_col as usize; } } } - pub fn col(&self, rope: &str) -> usize { - self.col.min(self.len_line(rope)) + /// Get the realized column of the position. This is the column, but capped at the line length. + pub fn col(&self, text: &(impl Text + ?Sized)) -> usize { + self.col.min(self.len_line(text)) } + /// Get the row of the position. pub fn row(&self) -> usize { self.row } - fn len_line(&self, rope: &str) -> usize { - let line = rope.lines().nth(self.row).unwrap_or_default(); - let len = line.len(); - if len > 0 && line.chars().nth(len - 1) == Some('\n') { - len - 1 + fn len_line(&self, text: &(impl Text + ?Sized)) -> usize { + if let Some(line) = text.line(self.row) { + let len = line.length(); + if len > 0 && line.character(len - 1) == Some('\n') { + len - 1 + } else { + len + } } else { - len + 0 } } - pub fn idx(&self, rope: &str) -> usize { - rope.lines().take(self.row).map(|l| l.len()).sum::() + self.col(rope) + /// Get the character index of the position. + pub fn idx(&self, text: &(impl Text + ?Sized)) -> usize { + text.len_before_line(self.row) + self.col(text) } - // the column can be more than the line length, cap it - pub fn realize_col(&mut self, rope: &str) { - self.col = self.col(rope); + /// If the column is more than the line length, cap it to the line length. + pub fn realize_col(&mut self, text: &(impl Text + ?Sized)) { + self.col = self.col(text); } } @@ -97,13 +170,17 @@ impl PartialOrd for Pos { } } +/// A cursor is a selection of text. It has a start and end position of the selection. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cursor { + /// The start position of the selection. The start position is the origin of the selection, not necessarily the first position. pub start: Pos, + /// The end position of the selection. If the end position is None, the cursor is a caret. pub end: Option, } impl Cursor { + /// Create a new cursor with the given start position. pub fn from_start(pos: Pos) -> Self { Self { start: pos, @@ -111,6 +188,7 @@ impl Cursor { } } + /// Create a new cursor with the given start and end position. pub fn new(start: Pos, end: Pos) -> Self { Self { start, @@ -118,7 +196,8 @@ impl Cursor { } } - fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) { + /// Move the cursor position. If shift is true, the end position will be moved instead of the start position. + pub fn move_cursor(&mut self, f: impl FnOnce(&mut Pos), shift: bool) { if shift { self.with_end(f); } else { @@ -127,7 +206,7 @@ impl Cursor { } } - fn delete_selection(&mut self, text: &mut String) -> [i32; 2] { + fn delete_selection(&mut self, text: &mut impl TextEditable) -> [i32; 2] { let first = self.first(); let last = self.last(); let dr = first.row as i32 - last.row as i32; @@ -136,7 +215,7 @@ impl Cursor { } else { first.col as i32 - last.col as i32 }; - text.replace_range(first.idx(text)..last.idx(text), ""); + text.delete_range(first.idx(text.as_ref())..last.idx(text.as_ref())); if let Some(end) = self.end.take() { if self.start > end { self.start = end; @@ -145,20 +224,24 @@ impl Cursor { [dc, dr] } - pub fn handle_input( + /// Handle moving the cursor with the given key. + pub fn handle_input( &mut self, data: &dioxus_html::KeyboardData, - text: &mut String, - max_width: usize, + text: &mut impl TextEditable, + max_text_length: usize, ) { use Code::*; match data.code() { ArrowUp => { - self.move_cursor(|c| c.up(text), data.modifiers().contains(Modifiers::SHIFT)); + self.move_cursor( + |c| c.up(text.as_ref()), + data.modifiers().contains(Modifiers::SHIFT), + ); } ArrowDown => { self.move_cursor( - |c| c.down(text), + |c| c.down(text.as_ref()), data.modifiers().contains(Modifiers::SHIFT), ); } @@ -167,22 +250,22 @@ impl Cursor { self.move_cursor( |c| { let mut change = 1; - let idx = c.idx(text); - let length = text.len(); + let idx = c.idx(text.as_ref()); + let length = text.as_ref().length(); while idx + change < length { - let chr = text.chars().nth(idx + change).unwrap(); + let chr = text.as_ref().character(idx + change).unwrap(); if chr.is_whitespace() { break; } change += 1; } - c.move_col(change as i32, text); + c.move_col(change as i32, text.as_ref()); }, data.modifiers().contains(Modifiers::SHIFT), ); } else { self.move_cursor( - |c| c.right(text), + |c| c.right(text.as_ref()), data.modifiers().contains(Modifiers::SHIFT), ); } @@ -192,28 +275,28 @@ impl Cursor { self.move_cursor( |c| { let mut change = -1; - let idx = c.idx(text) as i32; + let idx = c.idx(text.as_ref()) as i32; while idx + change > 0 { - let chr = text.chars().nth((idx + change) as usize).unwrap(); + let chr = text.as_ref().character((idx + change) as usize).unwrap(); if chr == ' ' { break; } change -= 1; } - c.move_col(change, text); + c.move_col(change, text.as_ref()); }, data.modifiers().contains(Modifiers::SHIFT), ); } else { self.move_cursor( - |c| c.left(text), + |c| c.left(text.as_ref()), data.modifiers().contains(Modifiers::SHIFT), ); } } End => { self.move_cursor( - |c| c.col = c.len_line(text), + |c| c.col = c.len_line(text.as_ref()), data.modifiers().contains(Modifiers::SHIFT), ); } @@ -221,64 +304,70 @@ impl Cursor { self.move_cursor(|c| c.col = 0, data.modifiers().contains(Modifiers::SHIFT)); } Backspace => { - self.start.realize_col(text); - let mut start_idx = self.start.idx(text); + self.start.realize_col(text.as_ref()); + let mut start_idx = self.start.idx(text.as_ref()); if self.end.is_some() { self.delete_selection(text); } else if start_idx > 0 { - self.start.left(text); - text.replace_range(start_idx - 1..start_idx, ""); + self.start.left(text.as_ref()); + text.delete_range(start_idx - 1..start_idx); if data.modifiers().contains(Modifiers::CONTROL) { - start_idx = self.start.idx(text); + start_idx = self.start.idx(text.as_ref()); while start_idx > 0 && text - .chars() - .nth(start_idx - 1) + .as_ref() + .character(start_idx - 1) .filter(|c| *c != ' ') .is_some() { - self.start.left(text); - text.replace_range(start_idx - 1..start_idx, ""); - start_idx = self.start.idx(text); + self.start.left(text.as_ref()); + text.delete_range(start_idx - 1..start_idx); + start_idx = self.start.idx(text.as_ref()); } } } } Enter => { - if text.len() + 1 - self.selection_len(text) <= max_width { - text.insert(self.start.idx(text), '\n'); + if text.as_ref().length() + 1 - self.selection_len(text.as_ref()) <= max_text_length + { + text.insert_character(self.start.idx(text.as_ref()), '\n'); self.start.col = 0; - self.start.down(text); + self.start.down(text.as_ref()); } } Tab => { - if text.len() + 1 - self.selection_len(text) <= max_width { - self.start.realize_col(text); + if text.as_ref().length() + 1 - self.selection_len(text.as_ref()) <= max_text_length + { + self.start.realize_col(text.as_ref()); self.delete_selection(text); - text.insert(self.start.idx(text), '\t'); - self.start.right(text); + text.insert_character(self.start.idx(text.as_ref()), '\t'); + self.start.right(text.as_ref()); } } _ => { - self.start.realize_col(text); + self.start.realize_col(text.as_ref()); if let Key::Character(character) = data.key() { - if text.len() + 1 - self.selection_len(text) <= max_width { + if text.as_ref().length() + 1 - self.selection_len(text.as_ref()) + <= max_text_length + { self.delete_selection(text); let character = character.chars().next().unwrap(); - text.insert(self.start.idx(text), character); - self.start.right(text); + text.insert_character(self.start.idx(text.as_ref()), character); + self.start.right(text.as_ref()); } } } } } + /// Modify the end selection position pub fn with_end(&mut self, f: impl FnOnce(&mut Pos)) { let mut new = self.end.take().unwrap_or_else(|| self.start.clone()); f(&mut new); self.end.replace(new); } + /// Returns first position of the selection (this could be the start or the end depending on the position) pub fn first(&self) -> &Pos { if let Some(e) = &self.end { e.min(&self.start) @@ -287,6 +376,7 @@ impl Cursor { } } + /// Returns last position of the selection (this could be the start or the end depending on the position) pub fn last(&self) -> &Pos { if let Some(e) = &self.end { e.max(&self.start) @@ -295,7 +385,8 @@ impl Cursor { } } - pub fn selection_len(&self, text: &str) -> usize { + /// Returns the length of the selection + pub fn selection_len(&self, text: &(impl Text + ?Sized)) -> usize { self.last().idx(text) - self.first().idx(text) } } diff --git a/packages/tui/src/widgets/number.rs b/packages/tui/src/widgets/number.rs index 2df1f512b..f34618f60 100644 --- a/packages/tui/src/widgets/number.rs +++ b/packages/tui/src/widgets/number.rs @@ -40,8 +40,8 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a let dragging = use_state(cx, || false); let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&text); - let end_highlight = cursor.read().last().idx(&text); + let start_highlight = cursor.read().first().idx(&*text); + let end_highlight = cursor.read().last().idx(&*text); let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); let (text_highlighted, text_after_second_cursor) = text_after_first_cursor.split_at(end_highlight - start_highlight); @@ -113,7 +113,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a }; if is_text{ let mut text = text_ref.write(); - cursor.write().handle_input(&k, &mut text, max_len); + cursor.write().handle_input(&k, &mut *text, max_len); update(text.clone()); let node = tui_query.get(get_root_id(cx).unwrap()); @@ -165,7 +165,7 @@ pub(crate) fn NumbericInput<'a>(cx: Scope<'a, NumbericInputProps>) -> Element<'a } new.row = 0; - new.realize_col(&text_ref.read()); + new.realize_col(text_ref.read().as_str()); cursor.set(Cursor::from_start(new)); dragging.set(true); let node = tui_query_clone.get(get_root_id(cx).unwrap()); diff --git a/packages/tui/src/widgets/password.rs b/packages/tui/src/widgets/password.rs index f82d0a346..6574ab4f9 100644 --- a/packages/tui/src/widgets/password.rs +++ b/packages/tui/src/widgets/password.rs @@ -40,8 +40,8 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { let dragging = use_state(cx, || false); let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&text); - let end_highlight = cursor.read().last().idx(&text); + let start_highlight = cursor.read().first().idx(&*text); + let end_highlight = cursor.read().last().idx(&*text); let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); let (text_highlighted, text_after_second_cursor) = text_after_first_cursor.split_at(end_highlight - start_highlight); @@ -88,7 +88,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { return; } let mut text = text_ref.write(); - cursor.write().handle_input(&k, &mut text, max_len); + cursor.write().handle_input(&k, &mut *text, max_len); if let Some(input_handler) = &cx.props.raw_oninput { input_handler.call(FormData { value: text.clone(), @@ -147,7 +147,7 @@ pub(crate) fn Password<'a>(cx: Scope<'a, PasswordProps>) -> Element<'a> { // textboxs are only one line tall new.row = 0; - new.realize_col(&text_ref.read()); + new.realize_col(text_ref.read().as_str()); cursor.set(Cursor::from_start(new)); dragging.set(true); let node = tui_query_clone.get(get_root_id(cx).unwrap()); diff --git a/packages/tui/src/widgets/textbox.rs b/packages/tui/src/widgets/textbox.rs index 4a58725bc..5e3cc693a 100644 --- a/packages/tui/src/widgets/textbox.rs +++ b/packages/tui/src/widgets/textbox.rs @@ -40,8 +40,8 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { let dragging = use_state(cx, || false); let text = text_ref.read().clone(); - let start_highlight = cursor.read().first().idx(&text); - let end_highlight = cursor.read().last().idx(&text); + let start_highlight = cursor.read().first().idx(&*text); + let end_highlight = cursor.read().last().idx(&*text); let (text_before_first_cursor, text_after_first_cursor) = text.split_at(start_highlight); let (text_highlighted, text_after_second_cursor) = text_after_first_cursor.split_at(end_highlight - start_highlight); @@ -90,7 +90,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { return; } let mut text = text_ref.write(); - cursor.write().handle_input(&k, &mut text, max_len); + cursor.write().handle_input(&k, &mut *text, max_len); if let Some(input_handler) = &cx.props.raw_oninput{ input_handler.call(FormData{ value: text.clone(), @@ -138,7 +138,7 @@ pub(crate) fn TextBox<'a>(cx: Scope<'a, TextBoxProps>) -> Element<'a> { // textboxs are only one line tall new.row = 0; - new.realize_col(&text_ref.read()); + new.realize_col(text_ref.read().as_str()); cursor.set(Cursor::from_start(new)); dragging.set(true); let node = tui_query_clone.get(get_root_id(cx).unwrap()); From d019ada3dae5fe95653f99ce13bd5b8466c433fa Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Jan 2023 17:16:47 -0600 Subject: [PATCH 15/16] make delete selection public --- packages/native-core/src/utils/cursor.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/native-core/src/utils/cursor.rs b/packages/native-core/src/utils/cursor.rs index 69aab539d..eaded383e 100644 --- a/packages/native-core/src/utils/cursor.rs +++ b/packages/native-core/src/utils/cursor.rs @@ -206,22 +206,16 @@ impl Cursor { } } - fn delete_selection(&mut self, text: &mut impl TextEditable) -> [i32; 2] { + /// Delete the currently selected text and update the cursor position. + pub fn delete_selection(&mut self, text: &mut impl TextEditable) { let first = self.first(); let last = self.last(); - let dr = first.row as i32 - last.row as i32; - let dc = if dr != 0 { - -(last.col as i32) - } else { - first.col as i32 - last.col as i32 - }; text.delete_range(first.idx(text.as_ref())..last.idx(text.as_ref())); if let Some(end) = self.end.take() { if self.start > end { self.start = end; } } - [dc, dr] } /// Handle moving the cursor with the given key. From 20751327f35e52931d5be3e1c9359bb7b55a2efc Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 20 Jan 2023 17:36:41 -0600 Subject: [PATCH 16/16] apply medium width when the border style is not none to comply with browser behavior --- packages/native-core/src/layout_attributes.rs | 117 +++++++++++++++--- packages/tui/src/layout.rs | 17 ++- 2 files changed, 115 insertions(+), 19 deletions(-) diff --git a/packages/native-core/src/layout_attributes.rs b/packages/native-core/src/layout_attributes.rs index d149a11ac..77926eb96 100644 --- a/packages/native-core/src/layout_attributes.rs +++ b/packages/native-core/src/layout_attributes.rs @@ -29,6 +29,7 @@ - [ ] pub aspect_ratio: Number, ----> parsing is done, but taffy doesnt support it */ +use lightningcss::properties::border::LineStyle; use lightningcss::properties::{align, display, flex, position, size}; use lightningcss::{ properties::{align::GapValue, border::BorderSideWidth, Property, PropertyId}, @@ -45,8 +46,43 @@ use taffy::{ style::{FlexDirection, PositionType}, }; +#[derive(Default)] +pub struct LayoutConfigeration { + /// the default border widths to use + pub border_widths: BorderWidths, +} + +pub struct BorderWidths { + /// the default border width to use for thin borders + pub thin: f32, + /// the default border width to use for medium borders + pub medium: f32, + /// the default border width to use for thick borders + pub thick: f32, +} + +impl Default for BorderWidths { + fn default() -> Self { + Self { + thin: 1.0, + medium: 3.0, + thick: 5.0, + } + } +} + /// applies the entire html namespace defined in dioxus-html pub fn apply_layout_attributes(name: &str, value: &str, style: &mut Style) { + apply_layout_attributes_cfg(name, value, style, &LayoutConfigeration::default()) +} + +/// applies the entire html namespace defined in dioxus-html with the specified configeration +pub fn apply_layout_attributes_cfg( + name: &str, + value: &str, + style: &mut Style, + config: &LayoutConfigeration, +) { if let Ok(property) = Property::parse_string(PropertyId::from(name), value, ParserOptions::default()) { @@ -84,41 +120,85 @@ pub fn apply_layout_attributes(name: &str, value: &str, style: &mut Style) { style.position.right = convert_length_percentage_or_auto(inset.right); } Property::BorderTopWidth(width) => { - style.border.top = convert_border_side_width(width); + style.border.top = convert_border_side_width(width, &config.border_widths); } Property::BorderBottomWidth(width) => { - style.border.bottom = convert_border_side_width(width); + style.border.bottom = convert_border_side_width(width, &config.border_widths); } Property::BorderLeftWidth(width) => { - style.border.left = convert_border_side_width(width); + style.border.left = convert_border_side_width(width, &config.border_widths); } Property::BorderRightWidth(width) => { - style.border.right = convert_border_side_width(width); + style.border.right = convert_border_side_width(width, &config.border_widths); } Property::BorderWidth(width) => { - style.border.top = convert_border_side_width(width.top); - style.border.bottom = convert_border_side_width(width.bottom); - style.border.left = convert_border_side_width(width.left); - style.border.right = convert_border_side_width(width.right); + style.border.top = convert_border_side_width(width.top, &config.border_widths); + style.border.bottom = + convert_border_side_width(width.bottom, &config.border_widths); + style.border.left = convert_border_side_width(width.left, &config.border_widths); + style.border.right = convert_border_side_width(width.right, &config.border_widths); } Property::Border(border) => { - let width = convert_border_side_width(border.width); + let width = convert_border_side_width(border.width, &config.border_widths); style.border.top = width; style.border.bottom = width; style.border.left = width; style.border.right = width; } Property::BorderTop(top) => { - style.border.top = convert_border_side_width(top.width); + style.border.top = convert_border_side_width(top.width, &config.border_widths); } Property::BorderBottom(bottom) => { - style.border.bottom = convert_border_side_width(bottom.width); + style.border.bottom = + convert_border_side_width(bottom.width, &config.border_widths); } Property::BorderLeft(left) => { - style.border.left = convert_border_side_width(left.width); + style.border.left = convert_border_side_width(left.width, &config.border_widths); } Property::BorderRight(right) => { - style.border.right = convert_border_side_width(right.width); + style.border.right = convert_border_side_width(right.width, &config.border_widths); + } + Property::BorderTopStyle(line_style) => { + if line_style != LineStyle::None { + style.border.top = + convert_border_side_width(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); + } + } + Property::BorderLeftStyle(line_style) => { + if line_style != LineStyle::None { + style.border.left = + convert_border_side_width(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); + } + } + Property::BorderStyle(styles) => { + if styles.top != LineStyle::None { + style.border.top = + convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + } + if styles.bottom != LineStyle::None { + style.border.bottom = + convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + } + if styles.left != LineStyle::None { + style.border.left = + convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + } + if styles.right != LineStyle::None { + style.border.right = + convert_border_side_width(BorderSideWidth::Medium, &config.border_widths); + } } Property::FlexDirection(flex_direction, _) => { use FlexDirection::*; @@ -330,12 +410,15 @@ fn convert_length_percentage_or_auto( } } -fn convert_border_side_width(border_side_width: BorderSideWidth) -> Dimension { +fn convert_border_side_width( + border_side_width: BorderSideWidth, + border_width_config: &BorderWidths, +) -> Dimension { match border_side_width { BorderSideWidth::Length(Length::Value(value)) => convert_length_value(value), - BorderSideWidth::Thick => Dimension::Points(5.0), - BorderSideWidth::Medium => Dimension::Points(3.0), - BorderSideWidth::Thin => Dimension::Points(1.0), + 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!(), } } diff --git a/packages/tui/src/layout.rs b/packages/tui/src/layout.rs index ba50b59a6..0dafbfa3b 100644 --- a/packages/tui/src/layout.rs +++ b/packages/tui/src/layout.rs @@ -1,6 +1,8 @@ use std::sync::{Arc, Mutex}; -use dioxus_native_core::layout_attributes::apply_layout_attributes; +use dioxus_native_core::layout_attributes::{ + apply_layout_attributes_cfg, BorderWidths, LayoutConfigeration, +}; use dioxus_native_core::node::OwnedAttributeView; use dioxus_native_core::node_ref::{AttributeMask, NodeMask, NodeView}; use dioxus_native_core::state::ChildDepState; @@ -94,7 +96,18 @@ impl ChildDepState for TaffyLayout { .binary_search(&attribute.name.as_ref()) .is_ok()); if let Some(text) = value.as_text() { - apply_layout_attributes(&attribute.name, text, &mut style); + apply_layout_attributes_cfg( + &attribute.name, + text, + &mut style, + &LayoutConfigeration { + border_widths: BorderWidths { + thin: 1.0, + medium: 1.0, + thick: 1.0, + }, + }, + ); } } }