diff --git a/packages/desktop/headless_tests/events.rs b/packages/desktop/headless_tests/events.rs index 54a092580..c2142d88f 100644 --- a/packages/desktop/headless_tests/events.rs +++ b/packages/desktop/headless_tests/events.rs @@ -20,8 +20,9 @@ fn app() -> Element { let received = RECEIVED_EVENTS(); let expected = utils::EXPECTED_EVENTS(); - use_effect(move || { + use_memo(move || { println!("expecting {} events", utils::EXPECTED_EVENTS()); + println!("received {} events", RECEIVED_EVENTS()); }); if expected != 0 && received == expected { diff --git a/packages/desktop/src/edits.rs b/packages/desktop/src/edits.rs index 001aa7e21..b3532de07 100644 --- a/packages/desktop/src/edits.rs +++ b/packages/desktop/src/edits.rs @@ -1,4 +1,5 @@ -use std::{cell::RefCell, collections::VecDeque, rc::Rc}; +use std::cell::Cell; +use std::{cell::RefCell, collections::VecDeque, rc::Rc, task::Waker}; /// This handles communication between the requests that the webview makes and the interpreter. The interpreter constantly makes long running requests to the webview to get any edits that should be made to the DOM almost like server side events. /// It will hold onto the requests until the interpreter is ready to handle them and hold onto any pending edits until a new request is made. @@ -6,6 +7,11 @@ use std::{cell::RefCell, collections::VecDeque, rc::Rc}; pub(crate) struct EditQueue { queue: Rc>>>, responder: Rc>>, + // Stores any futures waiting for edits to be applied to the webview + // NOTE: We don't use a Notify here because we need polling the notify to be cancel safe + waiting_for_edits_flushed: Rc>>, + // If this webview is currently waiting for an edit to be flushed. We don't run the virtual dom while this is true to avoid running effects before the dom has been updated + edits_in_progress: Rc>, } impl EditQueue { @@ -14,16 +20,48 @@ impl EditQueue { if let Some(bytes) = queue.pop_back() { responder.respond(wry::http::Response::new(bytes)); } else { + // There are now no edits that need to be applied to the webview + self.edits_finished(); *self.responder.borrow_mut() = Some(responder); } } pub fn add_edits(&self, edits: Vec) { let mut responder = self.responder.borrow_mut(); + // There are pending edits that need to be applied to the webview before we run futures + self.start_edits(); if let Some(responder) = responder.take() { responder.respond(wry::http::Response::new(edits)); } else { self.queue.borrow_mut().push_front(edits); } } + + fn start_edits(&self) { + self.edits_in_progress.set(true); + } + + fn edits_finished(&self) { + for waker in self.waiting_for_edits_flushed.borrow_mut().drain(..) { + waker.wake(); + } + self.edits_in_progress.set(false); + } + + fn edits_in_progress(&self) -> bool { + self.edits_in_progress.get() + } + + /// Wait until all pending edits have been rendered in the webview + pub fn poll_edits_flushed(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<()> { + if self.edits_in_progress() { + let waker = cx.waker(); + self.waiting_for_edits_flushed + .borrow_mut() + .push(waker.clone()); + std::task::Poll::Pending + } else { + std::task::Poll::Ready(()) + } + } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 9d86e7654..fd1c3dd87 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -229,10 +229,16 @@ impl WebviewInstance { pub fn poll_vdom(&mut self) { let mut cx = std::task::Context::from_waker(&self.waker); - // Continously poll the virtualdom until it's pending + // Continuously poll the virtualdom until it's pending // Wait for work will return Ready when it has edits to be sent to the webview // It will return Pending when it needs to be polled again - nothing is ready loop { + // If we're waiting for a render, wait for it to finish before we continue + let edits_flushed_poll = self.desktop_context.edit_queue.poll_edits_flushed(&mut cx); + if edits_flushed_poll.is_pending() { + return; + } + { let fut = self.dom.wait_for_work(); pin_mut!(fut); diff --git a/packages/interpreter/src/js/hash.txt b/packages/interpreter/src/js/hash.txt index a543d1781..dc97efc04 100644 --- a/packages/interpreter/src/js/hash.txt +++ b/packages/interpreter/src/js/hash.txt @@ -1 +1 @@ -2770005544568683192 \ No newline at end of file +5713307201725207733 \ No newline at end of file diff --git a/packages/interpreter/src/js/native.js b/packages/interpreter/src/js/native.js index cae6146e3..20f3b5264 100644 --- a/packages/interpreter/src/js/native.js +++ b/packages/interpreter/src/js/native.js @@ -1 +1 @@ -function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(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}},serializePointerEvent=function(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}},serializeMouseEvent=function(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,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath){super();this.editsPath=editsPath,this.kickStylesheets=!1}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message),event.preventDefault()}}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;kresponse.arrayBuffer()).then((bytes)=>{if(headless)this.run_from_bytes(bytes);else this.enqueueBytes(bytes),requestAnimationFrame(()=>{this.flushQueuedBytes()});this.waitForRequest(headless)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;icontents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(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}},serializePointerEvent=function(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}},serializeMouseEvent=function(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,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){let files=void 0;if(event.dataTransfer&&event.dataTransfer.files&&event.dataTransfer.files.length>0)files={files:{placeholder:[]}};return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;kickStylesheets;queuedBytes=[];liveview;constructor(editsPath){super();this.editsPath=editsPath,this.kickStylesheets=!1}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message),event.preventDefault()}}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k{this.flushQueuedBytes(),this.waitForRequest(headless)})}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{this.rafEdits(headless,bytes)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i { const target = event.target; - if (target instanceof HTMLInputElement && target.getAttribute("type") === "file") { + if ( + target instanceof HTMLInputElement && + target.getAttribute("type") === "file" + ) { // Send a message to the host to open the file dialog if the target is a file input and has a dioxus id attached to it let target_id = getTargetId(target); if (target_id !== null) { @@ -79,12 +90,12 @@ export class NativeInterpreter extends JSChannel_ { } }); - // @ts-ignore - wry gives us this this.ipc = window.ipc; // make sure we pass the handler to the base interpreter - const handler: EventListener = (event) => this.handleEvent(event, event.type, true); + const handler: EventListener = (event) => + this.handleEvent(event, event.type, true); super.initialize(root, handler); } @@ -99,7 +110,9 @@ export class NativeInterpreter extends JSChannel_ { } } - getClientRect(id: NodeId): { type: string; origin: number[]; size: number[]; } | undefined { + getClientRect( + id: NodeId + ): { type: string; origin: number[]; size: number[] } | undefined { const node = this.nodes[id]; if (node instanceof HTMLElement) { const rect = node.getBoundingClientRect(); @@ -171,13 +184,15 @@ export class NativeInterpreter extends JSChannel_ { // liveview does not have syncronous event handling, so we need to send the event to the host if (this.liveview) { // Okay, so the user might've requested some files to be read - if (target instanceof HTMLInputElement && (event.type === "change" || event.type === "input")) { + if ( + target instanceof HTMLInputElement && + (event.type === "change" || event.type === "input") + ) { if (target.getAttribute("type") === "file") { this.readFiles(target, contents, bubbles, realId, name); } } } else { - const message = this.serializeIpcMessage("user_event", body); this.ipc.postMessage(message); @@ -195,8 +210,6 @@ export class NativeInterpreter extends JSChannel_ { } } - - // This should: // - prevent form submissions from navigating // - prevent anchor tags from navigating @@ -210,7 +223,10 @@ export class NativeInterpreter extends JSChannel_ { preventDefaultRequests = target.getAttribute(`dioxus-prevent-default`); } - if (preventDefaultRequests && preventDefaultRequests.includes(`on${event.type}`)) { + if ( + preventDefaultRequests && + preventDefaultRequests.includes(`on${event.type}`) + ) { event.preventDefault(); } @@ -224,7 +240,11 @@ export class NativeInterpreter extends JSChannel_ { } } - handleClickNavigate(event: Event, target: Element, preventDefaultRequests: string) { + handleClickNavigate( + event: Event, + target: Element, + preventDefaultRequests: string + ) { // todo call prevent default if it's the right type of event if (!this.intercept_link_redirects) { return; @@ -279,24 +299,30 @@ export class NativeInterpreter extends JSChannel_ { } } - waitForRequest(headless: boolean) { - fetch(new Request(this.editsPath)) - .then(response => response.arrayBuffer()) - .then(bytes => { - // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly - if (headless) { - // @ts-ignore - this.run_from_bytes(bytes); - } else { - this.enqueueBytes(bytes); - requestAnimationFrame(() => { - this.flushQueuedBytes(); - }); - } + // Run the edits the next animation frame + rafEdits(headless: boolean, bytes: ArrayBuffer) { + // In headless mode, the requestAnimationFrame callback is never called, so we need to run the bytes directly + if (headless) { + // @ts-ignore + this.run_from_bytes(bytes); + this.waitForRequest(headless); + } else { + this.enqueueBytes(bytes); + requestAnimationFrame(() => { + this.flushQueuedBytes(); + // With request animation frames, we use the next reqwest as a marker to know when the frame is done and it is safe to run effects this.waitForRequest(headless); }); + } } + waitForRequest(headless: boolean) { + fetch(new Request(this.editsPath)) + .then((response) => response.arrayBuffer()) + .then((bytes) => { + this.rafEdits(headless, bytes); + }); + } kickAllStylesheetsOnPage() { // If this function is being called and we have not explicitly set kickStylesheets to true, then we should @@ -314,7 +340,13 @@ export class NativeInterpreter extends JSChannel_ { // A liveview only function // Desktop will intercept the event before it hits this - async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) { + async readFiles( + target: HTMLInputElement, + contents: SerializedEvent, + bubbles: boolean, + realId: NodeId, + name: string + ) { let files = target.files!; let file_contents: { [name: string]: number[] } = {}; @@ -388,7 +420,6 @@ function getTargetId(target: EventTarget): NodeId | null { return parseInt(realId); } - // function applyFileUpload() { // let inputs = document.querySelectorAll("input"); // for (let input of inputs) {