From 25a8411485e85bb7e3c8f20701d484529efe9a80 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 16 Nov 2021 01:25:38 -0500 Subject: [PATCH] wip: better desktop support --- packages/core/Cargo.toml | 1 + packages/core/src/nodes.rs | 43 +-- packages/core/src/scope.rs | 16 +- packages/core/src/scopearena.rs | 6 +- packages/core/src/virtual_dom.rs | 193 +++++------ packages/desktop/Cargo.toml | 1 + packages/desktop/examples/async.rs | 34 ++ packages/desktop/src/index.js | 521 +++++++++++++---------------- packages/desktop/src/lib.rs | 285 +++++++--------- packages/desktop/src/serialize.js | 0 packages/hooks/src/usestate.rs | 5 +- 11 files changed, 519 insertions(+), 586 deletions(-) create mode 100644 packages/desktop/examples/async.rs create mode 100644 packages/desktop/src/serialize.js diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 94a23c6fd..8619c7e71 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -39,6 +39,7 @@ indexmap = "1.7.0" # Serialize the Edits for use in Webview/Liveview instances serde = { version = "1", features = ["derive"], optional = true } +backtrace = "0.3.63" [dev-dependencies] anyhow = "1.0.42" diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index cedebddab..d7dcd5309 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -643,9 +643,9 @@ impl<'a> NodeFactory<'a> { } } - pub fn fragment_from_iter( + pub fn fragment_from_iter<'b, 'c>( self, - node_iter: impl IntoIterator>, + node_iter: impl IntoIterator + 'c> + 'b, ) -> VNode<'a> { let bump = self.bump; let mut nodes = bumpalo::collections::Vec::new_in(bump); @@ -662,24 +662,25 @@ impl<'a> NodeFactory<'a> { let children = nodes.into_bump_slice(); - // TODO - // We need a dedicated path in the rsx! macro that will trigger the "you need keys" warning - // - // if cfg!(debug_assertions) { - // if children.len() > 1 { - // if children.last().unwrap().key().is_none() { - // log::error!( - // r#" - // Warning: Each child in an array or iterator should have a unique "key" prop. - // Not providing a key will lead to poor performance with lists. - // See docs.rs/dioxus for more information. - // --- - // To help you identify where this error is coming from, we've generated a backtrace. - // "#, - // ); - // } - // } - // } + // todo: add a backtrace + if cfg!(debug_assertions) && children.len() > 1 && children.last().unwrap().key().is_none() + { + // todo: rsx! calls get turned into fragments which means they always trip this error + // + // + // use backtrace::Backtrace; + // let bt = Backtrace::new(); + + log::error!( + r#" + Warning: Each child in an array or iterator should have a unique "key" prop. + Not providing a key will lead to poor performance with lists. + See docs.rs/dioxus for more information. + ------------- + "#, + // bt + ); + } VNode::Fragment(VFragment { children, @@ -820,7 +821,7 @@ impl<'a> IntoVNode<'a> for Option> { } } -impl<'a> IntoVNode<'a> for LazyNodes<'a, '_> { +impl<'a, 'b> IntoVNode<'a> for LazyNodes<'a, 'b> { fn into_vnode(self, cx: NodeFactory<'a>) -> VNode<'a> { self.call(cx) } diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index a2497ed80..9c79a82a0 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -196,6 +196,7 @@ impl Scope { let chan = self.sender.clone(); let id = self.scope_id(); Rc::new(move || { + log::debug!("set on channel an update for scope {:?}", id); let _ = chan.unbounded_send(SchedulerMsg::Immediate(id)); }) } @@ -303,10 +304,18 @@ impl Scope { /// Pushes the future onto the poll queue to be polled /// The future is forcibly dropped if the component is not ready by the next render - pub fn push_task<'src, F: Future + 'src>( + pub fn push_task<'src, F: Future>( &'src self, - mut fut: impl FnOnce() -> F + 'src, - ) -> usize { + fut: impl FnOnce() -> F + 'src, + ) -> usize + where + F::Output: 'src, + F: 'src, + { + self.sender + .unbounded_send(SchedulerMsg::TaskPushed(self.our_arena_idx)) + .unwrap(); + // allocate the future let fut = fut(); let fut: &mut dyn Future = self.bump().alloc(fut); @@ -318,6 +327,7 @@ impl Scope { let self_ref_fut = unsafe { std::mem::transmute(boxed_fut) }; let mut items = self.items.borrow_mut(); + items.tasks.push(self_ref_fut); items.tasks.len() - 1 } diff --git a/packages/core/src/scopearena.rs b/packages/core/src/scopearena.rs index c83d1820a..dd1f555cd 100644 --- a/packages/core/src/scopearena.rs +++ b/packages/core/src/scopearena.rs @@ -133,8 +133,6 @@ impl ScopeArena { let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope); debug_assert!(any_item.is_none()); - - new_scope_id } else { let (node_capacity, hook_capacity) = { let heuristics = self.heuristics.borrow(); @@ -196,9 +194,9 @@ impl ScopeArena { let any_item = self.scopes.borrow_mut().insert(new_scope_id, scope); debug_assert!(any_item.is_none()); - - new_scope_id } + + new_scope_id } pub fn try_remove(&self, id: &ScopeId) -> Option<()> { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 6dda54d18..fe1fcac23 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -260,87 +260,45 @@ impl VirtualDom { /// Waits for the scheduler to have work /// This lets us poll async tasks during idle periods without blocking the main thread. pub async fn wait_for_work(&mut self) { - // todo: poll the events once even if there is work to do to prevent starvation - - // if there's no futures in the virtualdom, just wait for a scheduler message and put it into the queue to be processed - if self.scopes.pending_futures.borrow().is_empty() { - self.pending_messages - .push_front(self.receiver.next().await.unwrap()); - } else { - struct PollTasks<'a> { - scopes: &'a mut ScopeArena, + loop { + if !self.dirty_scopes.is_empty() && self.pending_messages.is_empty() { + break; } - impl<'a> Future for PollTasks<'a> { - type Output = (); + if self.pending_messages.is_empty() { + if self.scopes.pending_futures.borrow().is_empty() { + self.pending_messages + .push_front(self.receiver.next().await.unwrap()); + } else { + use futures_util::future::{select, Either}; - fn poll( - self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll { - let mut all_pending = true; - - let mut unfinished_tasks: SmallVec<[_; 10]> = smallvec::smallvec![]; - let mut scopes_to_clear: SmallVec<[_; 10]> = smallvec::smallvec![]; - - // Poll every scope manually - for fut in self.scopes.pending_futures.borrow().iter() { - let scope = self - .scopes - .get_scope(fut) - .expect("Scope should never be moved"); - - let mut items = scope.items.borrow_mut(); - - // really this should just be retain_mut but that doesn't exist yet - while let Some(mut task) = items.tasks.pop() { - // todo: does this make sense? - // I don't usually write futures by hand - // I think the futures neeed to be pinned using bumpbox or something - // right now, they're bump allocated so this shouldn't matter anyway - they're not going to move - let task_mut = task.as_mut(); - let unpinned = unsafe { Pin::new_unchecked(task_mut) }; - - if unpinned.poll(cx).is_ready() { - all_pending = false - } else { - unfinished_tasks.push(task); - } - } - - if unfinished_tasks.is_empty() { - scopes_to_clear.push(*fut); - } - - items.tasks.extend(unfinished_tasks.drain(..)); - } - - for scope in scopes_to_clear { - self.scopes.pending_futures.borrow_mut().remove(&scope); - } - - // Resolve the future if any singular task is ready - match all_pending { - true => Poll::Pending, - false => Poll::Ready(()), + match select(PollTasks(&mut self.scopes), self.receiver.next()).await { + Either::Left((_, _)) => {} + Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()), } } } - // Poll both the futures and the scheduler message queue simulataneously - use futures_util::future::{select, Either}; + while let Ok(Some(msg)) = self.receiver.try_next() { + self.pending_messages.push_front(msg); + } - let scheduler_fut = self.receiver.next(); - let tasks_fut = PollTasks { - scopes: &mut self.scopes, - }; - - match select(tasks_fut, scheduler_fut).await { - // Futures don't generate work - Either::Left((_, _)) => {} - - // Save these messages in FIFO to be processed later - Either::Right((msg, _)) => self.pending_messages.push_front(msg.unwrap()), + if let Some(msg) = self.pending_messages.pop_back() { + match msg { + // just keep looping, the task is now saved but we should actually poll it + SchedulerMsg::TaskPushed(id) => { + self.scopes.pending_futures.borrow_mut().insert(id); + } + SchedulerMsg::UiEvent(event) => { + if let Some(element) = event.element { + self.scopes.call_listener_with_bubbling(event, element); + } + } + SchedulerMsg::Immediate(s) => { + self.dirty_scopes.insert(s); + } + SchedulerMsg::Suspended { scope } => todo!(), + } } } } @@ -393,36 +351,8 @@ impl VirtualDom { pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec { let mut committed_mutations = vec![]; - while self.has_any_work() { - while let Ok(Some(msg)) = self.receiver.try_next() { - self.pending_messages.push_front(msg); - } - - while let Some(msg) = self.pending_messages.pop_back() { - match msg { - // TODO: Suspsense - SchedulerMsg::Immediate(id) => { - self.dirty_scopes.insert(id); - } - SchedulerMsg::Suspended { node, scope } => { - todo!("should wire up suspense") - // self.suspense_scopes.insert(id); - } - SchedulerMsg::UiEvent(event) => { - if let Some(element) = event.element { - log::info!("Calling listener {:?}, {:?}", event.scope_id, element); - - self.scopes.call_listener_with_bubbling(event, element); - while let Ok(Some(dirty_scope)) = self.receiver.try_next() { - self.pending_messages.push_front(dirty_scope); - } - } else { - log::debug!("User event without a targetted ElementId. Not currently supported.\nUnsure how to proceed. {:?}", event); - } - } - } - } - + while !self.dirty_scopes.is_empty() { + log::debug!("working with deadline"); let scopes = &self.scopes; let mut diff_state = DiffState::new(scopes); @@ -616,7 +546,10 @@ pub enum SchedulerMsg { // setstate Immediate(ScopeId), - Suspended { scope: ScopeId, node: NodeLink }, + // an async task pushed from an event handler (or just spawned) + TaskPushed(ScopeId), + + Suspended { scope: ScopeId }, } /// User Events are events that are shuttled from the renderer into the VirtualDom trhough the scheduler channel. @@ -720,3 +653,55 @@ pub enum EventPriority { /// This is considered "idle" work or "background" work. Low = 0, } + +struct PollTasks<'a>(&'a mut ScopeArena); + +impl<'a> Future for PollTasks<'a> { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let mut all_pending = true; + + let mut unfinished_tasks: SmallVec<[_; 10]> = smallvec::smallvec![]; + let mut scopes_to_clear: SmallVec<[_; 10]> = smallvec::smallvec![]; + + // Poll every scope manually + for fut in self.0.pending_futures.borrow().iter() { + let scope = self.0.get_scope(fut).expect("Scope should never be moved"); + + let mut items = scope.items.borrow_mut(); + + // really this should just be retain_mut but that doesn't exist yet + while let Some(mut task) = items.tasks.pop() { + // todo: does this make sense? + // I don't usually write futures by hand + // I think the futures neeed to be pinned using bumpbox or something + // right now, they're bump allocated so this shouldn't matter anyway - they're not going to move + let task_mut = task.as_mut(); + let unpinned = unsafe { Pin::new_unchecked(task_mut) }; + + if unpinned.poll(cx).is_ready() { + all_pending = false + } else { + unfinished_tasks.push(task); + } + } + + if unfinished_tasks.is_empty() { + scopes_to_clear.push(*fut); + } + + items.tasks.extend(unfinished_tasks.drain(..)); + } + + for scope in scopes_to_clear { + self.0.pending_futures.borrow_mut().remove(&scope); + } + + // Resolve the future if any singular task is ready + match all_pending { + true => Poll::Pending, + false => Poll::Ready(()), + } + } +} diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 142b76367..a2bffd8de 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -22,6 +22,7 @@ tokio = { version = "1.12.0", features = [ "sync", "rt-multi-thread", "rt", + "time", ], optional = true, default-features = false } dioxus-core-macro = { path = "../core-macro" } dioxus-html = { path = "../html", features = ["serialize"] } diff --git a/packages/desktop/examples/async.rs b/packages/desktop/examples/async.rs new file mode 100644 index 000000000..3e8b73d6f --- /dev/null +++ b/packages/desktop/examples/async.rs @@ -0,0 +1,34 @@ +//! Example: README.md showcase +//! +//! The example from the README.md. + +use std::time::Duration; + +use dioxus::prelude::*; +use dioxus_core as dioxus; +use dioxus_core_macro::*; +use dioxus_hooks::*; +use dioxus_html as dioxus_elements; + +fn main() { + dioxus_desktop::launch(App, |c| c); +} + +static App: FC<()> = |cx, props| { + let mut count = use_state(cx, || 0); + + cx.push_task(|| async move { + tokio::time::sleep(Duration::from_millis(100)).await; + count += 1; + }); + + cx.render(rsx! { + div { + h1 { "High-Five counter: {count}" } + button { + onclick: move |_| count.set(0), + "Click me!" + } + } + }) +}; diff --git a/packages/desktop/src/index.js b/packages/desktop/src/index.js index ced36b378..aca3d3139 100644 --- a/packages/desktop/src/index.js +++ b/packages/desktop/src/index.js @@ -1,4 +1,213 @@ +function serialize_event(event) { + switch (event.type) { + case "copy": + case "cut": + case "past": + return {}; + + case "compositionend": + case "compositionstart": + case "compositionupdate": + return { + data: event.data + } + + case "keydown": + case "keypress": + case "keyup": + return { + char_code: event.charCode, + key: event.key, + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + key_code: event.keyCode, + shift_key: event.shiftKey, + locale: "locale", + location: event.location, + repeat: event.repeat, + which: event.which, + // locale: event.locale, + } + + case "focus": + case "blur": + return {}; + + case "change": + let target = event.target; + let value; + if (target.type === "checkbox" || target.type === "radio") { + value = target.checked ? "true" : "false"; + } else { + value = target.value ?? target.textContent; + } + + return { + value: value + }; + + case "input": + case "invalid": + case "reset": + case "submit": { + let target = event.target; + let value = target.value ?? target.textContent; + return { + value: value + }; + } + + case "click": + case "contextmenu": + case "doubleclick": + case "drag": + case "dragend": + case "dragenter": + case "dragexit": + case "dragleave": + case "dragover": + case "dragstart": + case "drop": + case "mousedown": + case "mouseenter": + case "mouseleave": + case "mousemove": + case "mouseout": + case "mouseover": + case "mouseup": + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + } + + case "pointerdown": + case "pointermove": + case "pointerup": + case "pointercancel": + case "gotpointercapture": + case "lostpointercapture": + case "pointerenter": + case "pointerleave": + case "pointerover": + case "pointerout": + return { + alt_key: event.altKey, + button: event.button, + buttons: event.buttons, + client_x: event.clientX, + client_y: event.clientY, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + page_x: event.pageX, + page_y: event.pageY, + screen_x: event.screenX, + screen_y: event.screenY, + shift_key: event.shiftKey, + pointer_id: event.pointerId, + width: event.width, + height: event.height, + pressure: event.pressure, + tangential_pressure: event.tangentialPressure, + tilt_x: event.tiltX, + tilt_y: event.tiltY, + twist: event.twist, + pointer_type: event.pointerType, + is_primary: event.isPrimary, + } + + case "select": + return {}; + + case "touchcancel": + case "touchend": + case "touchmove": + case "touchstart": + return { + alt_key: event.altKey, + ctrl_key: event.ctrlKey, + meta_key: event.metaKey, + shift_key: event.shiftKey, + + // changed_touches: event.changedTouches, + // target_touches: event.targetTouches, + // touches: event.touches, + } + + case "scroll": + return {}; + + case "wheel": + return { + delta_x: event.deltaX, + delta_y: event.deltaY, + delta_z: event.deltaZ, + delta_mode: event.deltaMode, + }; + + case "animationstart": + case "animationend": + case "animationiteration": + return { + animation_name: event.animationName, + elapsed_time: event.elapsedTime, + pseudo_element: event.pseudoElement, + }; + + case "transitionend": + return { + property_name: event.propertyName, + elapsed_time: event.elapsedTime, + pseudo_element: event.pseudoElement, + }; + + case "abort": + case "canplay": + case "canplaythrough": + case "durationchange": + case "emptied": + case "encrypted": + case "ended": + case "error": + case "loadeddata": + case "loadedmetadata": + case "loadstart": + case "pause": + case "play": + case "playing": + case "progress": + case "ratechange": + case "seeked": + case "seeking": + case "stalled": + case "suspend": + case "timeupdate": + case "volumechange": + case "waiting": + return {}; + + case "toggle": + return {}; + + default: + return {}; + } + + +} + + class Interpreter { constructor(root) { this.root = root; @@ -125,22 +334,8 @@ class Interpreter { mounted_dom_id: parseInt(real_id), contents: contents, }; - // console.log(evt); - rpc.call('user_event', evt).then((reply) => { - // console.log("reply received", reply); - this.stack.push(this.root); - - reply.map((reply) => { - let edits = reply.edits; - for (let x = 0; x < edits.length; x++) { - let edit = edits[x]; - let f = this[edit.type]; - f.call(this, edit); - } - }); - - }) + rpc.call('user_event', evt); }); } } @@ -197,286 +392,28 @@ class Interpreter { } } + handleEdits(edits) { + + // console.log("handling raw edits", rawedits); + // let edits = JSON.parse(rawedits); + // console.log("handling edits", edits); + + this.stack.push(this.root); + + for (let x = 0; x < edits.length; x++) { + let edit = edits[x]; + let f = this[edit.type]; + f.call(this, edit); + } + } } -async function initialize() { +function main() { let root = window.document.getElementById("_dioxusroot"); - const interpreter = new Interpreter(root); + window.interpreter = new Interpreter(root); + console.log(window.interpreter); - const reply = await rpc.call('initiate'); - - let pre_rendered = reply.pre_rendered; - if (pre_rendered !== undefined) { - root.innerHTML = pre_rendered; - } - - const edits = reply.edits; - - apply_edits(edits, interpreter); + rpc.call('initialize'); } -function apply_edits(edits, interpreter) { - // console.log(edits); - for (let x = 0; x < edits.length; x++) { - let edit = edits[x]; - let f = interpreter[edit.type]; - f.call(interpreter, edit); - } - - // // console.log("stack completed: ", interpreter.stack); -} - -function serialize_event(event) { - let serializer = SerializeMap[event.type]; - if (serializer === undefined) { - return {}; - } else { - return serializer(event); - } -} - -const SerializeMap = { - "copy": serialize_clipboard, - "cut": serialize_clipboard, - "paste": serialize_clipboard, - - "compositionend": serialize_composition, - "compositionstart": serialize_composition, - "compositionupdate": serialize_composition, - - "keydown": serialize_keyboard, - "keypress": serialize_keyboard, - "keyup": serialize_keyboard, - - "focus": serialize_focus, - "blur": serialize_focus, - - // "change": serialize_form, - "change": serialize_change, - - "input": serialize_form, - "invalid": serialize_form, - "reset": serialize_form, - "submit": serialize_form, - - "click": serialize_mouse, - "contextmenu": serialize_mouse, - "doubleclick": serialize_mouse, - "drag": serialize_mouse, - "dragend": serialize_mouse, - "dragenter": serialize_mouse, - "dragexit": serialize_mouse, - "dragleave": serialize_mouse, - "dragover": serialize_mouse, - "dragstart": serialize_mouse, - "drop": serialize_mouse, - "mousedown": serialize_mouse, - "mouseenter": serialize_mouse, - "mouseleave": serialize_mouse, - "mousemove": serialize_mouse, - "mouseout": serialize_mouse, - "mouseover": serialize_mouse, - "mouseup": serialize_mouse, - - "pointerdown": serialize_pointer, - "pointermove": serialize_pointer, - "pointerup": serialize_pointer, - "pointercancel": serialize_pointer, - "gotpointercapture": serialize_pointer, - "lostpointercapture": serialize_pointer, - "pointerenter": serialize_pointer, - "pointerleave": serialize_pointer, - "pointerover": serialize_pointer, - "pointerout": serialize_pointer, - - "select": serialize_selection, - - "touchcancel": serialize_touch, - "touchend": serialize_touch, - "touchmove": serialize_touch, - "touchstart": serialize_touch, - - "scroll": serialize_scroll, - - "wheel": serialize_wheel, - - "animationstart": serialize_animation, - "animationend": serialize_animation, - "animationiteration": serialize_animation, - - "transitionend": serialize_transition, - - "abort": serialize_media, - "canplay": serialize_media, - "canplaythrough": serialize_media, - "durationchange": serialize_media, - "emptied": serialize_media, - "encrypted": serialize_media, - "ended": serialize_media, - "error": serialize_media, - "loadeddata": serialize_media, - "loadedmetadata": serialize_media, - "loadstart": serialize_media, - "pause": serialize_media, - "play": serialize_media, - "playing": serialize_media, - "progress": serialize_media, - "ratechange": serialize_media, - "seeked": serialize_media, - "seeking": serialize_media, - "stalled": serialize_media, - "suspend": serialize_media, - "timeupdate": serialize_media, - "volumechange": serialize_media, - "waiting": serialize_media, - - "toggle": serialize_toggle -} - -function serialize_clipboard(_event) { - return {}; -} -function serialize_composition(event) { - return { - data: event.data - } -} -function serialize_keyboard(event) { - return { - char_code: event.charCode, - key: event.key, - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - key_code: event.keyCode, - shift_key: event.shiftKey, - locale: "locale", - // locale: event.locale, - location: event.location, - repeat: event.repeat, - which: event.which, - } -} -function serialize_focus(_event) { - return {} -} - -function serialize_change(event) { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - - return { - value: value - } -} -function serialize_form(event) { - let target = event.target; - let value = target.value ?? target.textContent; - return { - value: value - } -} -function serialize_mouse(event) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - } -} - -function serialize_pointer(event) { - return { - alt_key: event.altKey, - button: event.button, - buttons: event.buttons, - client_x: event.clientX, - client_y: event.clientY, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - page_x: event.pageX, - page_y: event.pageY, - screen_x: event.screenX, - screen_y: event.screenY, - shift_key: event.shiftKey, - pointer_id: event.pointerId, - width: event.width, - height: event.height, - pressure: event.pressure, - tangential_pressure: event.tangentialPressure, - tilt_x: event.tiltX, - tilt_y: event.tiltY, - twist: event.twist, - pointer_type: event.pointerType, - is_primary: event.isPrimary, - } -} - -function serialize_selection(event) { - return {} -} - -function serialize_touch(event) { - return { - alt_key: event.altKey, - ctrl_key: event.ctrlKey, - meta_key: event.metaKey, - shift_key: event.shiftKey, - - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - } -} -function serialize_scroll(event) { - return {} -} - -function serialize_wheel(event) { - return { - delta_x: event.deltaX, - delta_y: event.deltaY, - delta_z: event.deltaZ, - delta_mode: event.deltaMode, - } -} - -function serialize_animation(event) { - return { - animation_name: event.animationName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - } -} - -function serialize_transition(event) { - return { - property_name: event.propertyName, - elapsed_time: event.elapsedTime, - pseudo_element: event.pseudoElement, - } -} - -function serialize_media(event) { - return {} -} - -function serialize_toggle(event) { - return {} -} - - -initialize(); +main() diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 495deebdd..f40752906 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -5,7 +5,7 @@ use std::borrow::BorrowMut; use std::cell::{Cell, RefCell}; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::sync::atomic::AtomicBool; @@ -20,10 +20,10 @@ pub use wry; use wry::application::accelerator::{Accelerator, SysMods}; use wry::application::event::{ElementState, Event, StartCause, WindowEvent}; -use wry::application::event_loop::{self, ControlFlow, EventLoop}; +use wry::application::event_loop::{self, ControlFlow, EventLoop, EventLoopWindowTarget}; use wry::application::keyboard::{Key, KeyCode, ModifiersState}; use wry::application::menu::{MenuBar, MenuItem, MenuItemAttributes}; -use wry::application::window::Fullscreen; +use wry::application::window::{Fullscreen, WindowId}; use wry::webview::{WebView, WebViewBuilder}; use wry::{ application::menu, @@ -59,12 +59,6 @@ enum RpcEvent<'a> { Initialize { edits: Vec> }, } -#[derive(Debug)] -enum BridgeEvent { - Initialize(serde_json::Value), - Update(serde_json::Value), -} - #[derive(Serialize)] struct Response<'a> { pre_rendered: Option, @@ -88,111 +82,29 @@ pub fn run( // All of our webview windows are stored in a way that we can look them up later // The "DesktopContext" will provide functionality for spawning these windows - let mut webviews = HashMap::new(); + let mut webviews = HashMap::::new(); let event_loop = EventLoop::new(); let props_shared = Cell::new(Some(props)); // create local modifier state - let mut modifiers = ModifiersState::default(); + let modifiers = ModifiersState::default(); let quit_hotkey = Accelerator::new(SysMods::Cmd, KeyCode::KeyQ); - event_loop.run(move |event, event_loop, control_flow| { + let edit_queue = Arc::new(RwLock::new(VecDeque::new())); + let is_ready: Arc = Default::default(); + + event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; - match event { + match window_event { Event::NewEvents(StartCause::Init) => { - // create main menubar menu - let mut menu_bar_menu = MenuBar::new(); - - // create `first_menu` - let mut first_menu = MenuBar::new(); - - first_menu.add_native_item(MenuItem::About("Todos".to_string())); - first_menu.add_native_item(MenuItem::Services); - first_menu.add_native_item(MenuItem::Separator); - first_menu.add_native_item(MenuItem::Hide); - first_menu.add_native_item(MenuItem::HideOthers); - first_menu.add_native_item(MenuItem::ShowAll); - - first_menu.add_native_item(MenuItem::Quit); - first_menu.add_native_item(MenuItem::CloseWindow); - - // create second menu - let mut second_menu = MenuBar::new(); - - // second_menu.add_submenu("Sub menu", true, my_sub_menu); - second_menu.add_native_item(MenuItem::Copy); - second_menu.add_native_item(MenuItem::Paste); - second_menu.add_native_item(MenuItem::SelectAll); - - menu_bar_menu.add_submenu("First menu", true, first_menu); - menu_bar_menu.add_submenu("Second menu", true, second_menu); - - let window = WindowBuilder::new() - .with_maximized(true) - .with_menu(menu_bar_menu) - .with_title("Dioxus App") - .build(event_loop) - .unwrap(); + let window = create_window(event_loop); let window_id = window.id(); - - let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel(); - let my_props = props_shared.take().unwrap(); - - let sender = launch_vdom_with_tokio(root, my_props, event_tx); - - let locked_receiver = Rc::new(RefCell::new(event_rx)); - - let webview = WebViewBuilder::new(window) - .unwrap() - .with_url("wry://index.html") - .unwrap() - .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { - let mut rx = (*locked_receiver).borrow_mut(); - match req.method.as_str() { - "initiate" => { - if let Ok(BridgeEvent::Initialize(edits)) = rx.try_recv() { - Some(RpcResponse::new_result(req.id.take(), Some(edits))) - } else { - None - } - } - "user_event" => { - let event = events::trigger_from_serialized(req.params.unwrap()); - log::debug!("User event: {:?}", event); - - sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap(); - - if let Some(BridgeEvent::Update(edits)) = rx.blocking_recv() { - log::info!("bridge received message"); - Some(RpcResponse::new_result(req.id.take(), Some(edits))) - } else { - log::info!("none received message"); - None - } - } - _ => None, - } - }) - // Any content that that uses the `wry://` scheme will be shuttled through this handler as a "special case" - // For now, we only serve two pieces of content which get included as bytes into the final binary. - .with_custom_protocol("wry".into(), move |request| { - let path = request.uri().replace("wry://", ""); - let (data, meta) = match path.as_str() { - "index.html" => (include_bytes!("./index.html").to_vec(), "text/html"), - "index.html/index.js" => { - (include_bytes!("./index.js").to_vec(), "text/javascript") - } - _ => unimplemented!("path {}", path), - }; - - wry::http::ResponseBuilder::new().mimetype(meta).body(data) - }) - .build() - .unwrap(); - + let sender = + launch_vdom_with_tokio(root, props_shared.take().unwrap(), edit_queue.clone()); + let webview = create_webview(window, is_ready.clone(), sender); webviews.insert(window_id, webview); } @@ -206,29 +118,17 @@ pub fn run( *control_flow = ControlFlow::Exit; } } - // catch only pressed event - WindowEvent::KeyboardInput { event, .. } => { - log::debug!("keybowrd input"); - if quit_hotkey.matches(&modifiers, &event.physical_key) { - log::debug!("quitting"); + WindowEvent::Moved(pos) => { + // + } + WindowEvent::KeyboardInput { event, .. } => { + if quit_hotkey.matches(&modifiers, &event.physical_key) { webviews.remove(&window_id); if webviews.is_empty() { *control_flow = ControlFlow::Exit; } } - - // println!( - // "KeyEvent: `Shift` + `1` | logical_key: {:?}", - // &event.logical_key - // ); - // we can match manually without `Accelerator` - - // else if event.key_without_modifiers() == Key::Character("1") - // && modifiers.is_empty() - // { - // println!("KeyEvent: `1`"); - // } } WindowEvent::Resized(_) | WindowEvent::Moved(_) => { @@ -240,7 +140,18 @@ pub fn run( _ => {} }, - Event::MainEventsCleared => {} + Event::MainEventsCleared => { + // I hate this ready hack but it's needed to wait for the "onload" to occur + // We can't run any initializion scripts because the window isn't ready yet? + if is_ready.load(std::sync::atomic::Ordering::Relaxed) { + let mut queue = edit_queue.write().unwrap(); + let (id, view) = webviews.iter_mut().next().unwrap(); + while let Some(edit) = queue.pop_back() { + view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) + .unwrap(); + } + } + } Event::Resumed => {} Event::Suspended => {} Event::LoopDestroyed => {} @@ -250,19 +161,11 @@ pub fn run( }) } -pub fn start( - root: FC

, - config_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>, -) -> ((), ()) { - // - ((), ()) -} - // Create a new tokio runtime on a dedicated thread and then launch the apps VirtualDom. pub(crate) fn launch_vdom_with_tokio( root: FC

, props: P, - event_tx: tokio::sync::mpsc::UnboundedSender, + edit_queue: Arc>>, ) -> futures_channel::mpsc::UnboundedSender { let (sender, receiver) = futures_channel::mpsc::unbounded::(); let return_sender = sender.clone(); @@ -275,47 +178,107 @@ pub(crate) fn launch_vdom_with_tokio( .unwrap(); runtime.block_on(async move { - let mut vir = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver); - let _ = vir.get_scheduler_channel(); + let mut dom = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver); - let edits = vir.rebuild(); + let edits = dom.rebuild(); - // the receiving end expects something along these lines - #[derive(Serialize)] - struct Evt<'a> { - edits: Vec>, - } - - let edit_string = serde_json::to_value(Evt { edits: edits.edits }).unwrap(); - - event_tx - .send(BridgeEvent::Initialize(edit_string)) - .expect("Sending should not fail"); + edit_queue + .write() + .unwrap() + .push_front(serde_json::to_string(&edits.edits).unwrap()); loop { - vir.wait_for_work().await; - // we're running on our own thread, so we don't need to worry about blocking anything - // todo: maybe we want to schedule ourselves in - // on average though, the virtualdom running natively is stupid fast - - let mut muts = vir.work_with_deadline(|| false); - - log::debug!("finished running with deadline"); - - let mut edits = vec![]; - + dom.wait_for_work().await; + let mut muts = dom.work_with_deadline(|| false); while let Some(edit) = muts.pop() { - let edit_string = serde_json::to_value(Evt { edits: edit.edits }) - .expect("serializing edits should never fail"); - edits.push(edit_string); + edit_queue + .write() + .unwrap() + .push_front(serde_json::to_string(&edit.edits).unwrap()); } - - event_tx - .send(BridgeEvent::Update(serde_json::Value::Array(edits))) - .expect("Sending should not fail"); } }) }); return_sender } + +fn build_menu() -> MenuBar { + // create main menubar menu + let mut menu_bar_menu = MenuBar::new(); + + // create `first_menu` + let mut first_menu = MenuBar::new(); + + first_menu.add_native_item(MenuItem::About("Todos".to_string())); + first_menu.add_native_item(MenuItem::Services); + first_menu.add_native_item(MenuItem::Separator); + first_menu.add_native_item(MenuItem::Hide); + first_menu.add_native_item(MenuItem::HideOthers); + first_menu.add_native_item(MenuItem::ShowAll); + + first_menu.add_native_item(MenuItem::Quit); + first_menu.add_native_item(MenuItem::CloseWindow); + + // create second menu + let mut second_menu = MenuBar::new(); + + // second_menu.add_submenu("Sub menu", true, my_sub_menu); + second_menu.add_native_item(MenuItem::Copy); + second_menu.add_native_item(MenuItem::Paste); + second_menu.add_native_item(MenuItem::SelectAll); + + menu_bar_menu.add_submenu("First menu", true, first_menu); + menu_bar_menu.add_submenu("Second menu", true, second_menu); + + menu_bar_menu +} + +fn create_window(event_loop: &EventLoopWindowTarget<()>) -> Window { + WindowBuilder::new() + .with_maximized(true) + .with_menu(build_menu()) + .with_title("Dioxus App") + .build(event_loop) + .unwrap() +} + +fn create_webview( + window: Window, + is_ready: Arc, + sender: futures_channel::mpsc::UnboundedSender, +) -> WebView { + WebViewBuilder::new(window) + .unwrap() + .with_url("wry://index.html") + .unwrap() + .with_rpc_handler(move |_window: &Window, mut req: RpcRequest| { + match req.method.as_str() { + "user_event" => { + let event = events::trigger_from_serialized(req.params.unwrap()); + log::debug!("User event: {:?}", event); + sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap(); + } + "initialize" => { + is_ready.store(true, std::sync::atomic::Ordering::Relaxed); + } + _ => {} + } + + None + }) + // Any content that that uses the `wry://` scheme will be shuttled through this handler as a "special case" + // For now, we only serve two pieces of content which get included as bytes into the final binary. + .with_custom_protocol("wry".into(), move |request| { + let path = request.uri().replace("wry://", ""); + let (data, meta) = match path.as_str() { + "index.html" => (include_bytes!("./index.html").to_vec(), "text/html"), + "index.html/index.js" => (include_bytes!("./index.js").to_vec(), "text/javascript"), + _ => unimplemented!("path {}", path), + }; + + wry::http::ResponseBuilder::new().mimetype(meta).body(data) + }) + .build() + .unwrap() +} diff --git a/packages/desktop/src/serialize.js b/packages/desktop/src/serialize.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hooks/src/usestate.rs b/packages/hooks/src/usestate.rs index 1989fcd20..83db764ee 100644 --- a/packages/hooks/src/usestate.rs +++ b/packages/hooks/src/usestate.rs @@ -126,7 +126,10 @@ impl<'a, T: 'static> UseState<'a, T> { pub fn setter(&self) -> Rc { let slot = self.inner.wip.clone(); - Rc::new(move |new| *slot.borrow_mut() = Some(new)) + Rc::new(move |new| { + // + *slot.borrow_mut() = Some(new); + }) } pub fn for_async(&self) -> AsyncUseState {