瀏覽代碼

Add access to the Element attributes related to scrolling (#2338)

* Add access to the Element attributes related to scrolling

* Fix clippy warnings

* Restore interpreter/src/js/hash.txt content

* Update generated interpreter files

* Use euclid types as return types

* Remove redundant functions

* It's not necessary for PixelsSize to be in 3D

* Rename PixelsVector to PixelsVector3D and add a PixelsVector2D type

* Remove unused PixelsLength type
ASR-ASU 1 年之前
父節點
當前提交
460b70e0f0

+ 48 - 27
packages/desktop/src/element.rs

@@ -1,5 +1,8 @@
 use dioxus_core::ElementId;
-use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
+use dioxus_html::{
+    geometry::{PixelsRect, PixelsSize, PixelsVector2D},
+    MountedResult, RenderedElementBacking,
+};
 
 use crate::{desktop_context::DesktopContext, query::QueryEngine};
 
@@ -17,38 +20,56 @@ impl DesktopElement {
     }
 }
 
+macro_rules! scripted_getter {
+    ($meth_name:ident, $script:literal, $output_type:path) => {
+        fn $meth_name(
+            &self,
+        ) -> std::pin::Pin<
+            Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<$output_type>>>,
+        > {
+            let script = format!($script, id = self.id.0);
+
+            let fut = self
+                .query
+                .new_query::<Option<$output_type>>(&script, self.webview.clone())
+                .resolve();
+            Box::pin(async move {
+                match fut.await {
+                    Ok(Some(res)) => Ok(res),
+                    Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
+                        Box::new(DesktopQueryError::FailedToQuery),
+                    )),
+                    Err(err) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
+                        Box::new(err),
+                    )),
+                }
+            })
+        }
+    };
+}
+
 impl RenderedElementBacking for DesktopElement {
     fn as_any(&self) -> &dyn std::any::Any {
         self
     }
 
-    fn get_client_rect(
-        &self,
-    ) -> std::pin::Pin<
-        Box<
-            dyn futures_util::Future<
-                Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
-            >,
-        >,
-    > {
-        let script = format!("return window.interpreter.getClientRect({});", self.id.0);
+    scripted_getter!(
+        get_scroll_offset,
+        "return [window.interpreter.getScrollLeft({id}), window.interpreter.getScrollTop({id})]",
+        PixelsVector2D
+    );
 
-        let fut = self
-            .query
-            .new_query::<Option<Rect<f64, f64>>>(&script, self.webview.clone())
-            .resolve();
-        Box::pin(async move {
-            match fut.await {
-                Ok(Some(rect)) => Ok(rect),
-                Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
-                    Box::new(DesktopQueryError::FailedToQuery),
-                )),
-                Err(err) => {
-                    MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
-                }
-            }
-        })
-    }
+    scripted_getter!(
+        get_scroll_size,
+        "return [window.interpreter.getScrollWidth({id}), window.interpreter.getScrollHeight({id})]",
+        PixelsSize
+    );
+
+    scripted_getter!(
+        get_client_rect,
+        "return window.interpreter.getClientRect({id});",
+        PixelsRect
+    );
 
     fn scroll_to(
         &self,

+ 25 - 4
packages/html/src/events/mounted.rs

@@ -1,7 +1,5 @@
 //! Handles querying data from the renderer
 
-use euclid::Rect;
-
 use std::{
     fmt::{Display, Formatter},
     future::Future,
@@ -16,9 +14,20 @@ pub trait RenderedElementBacking: std::any::Any {
     /// return self as Any
     fn as_any(&self) -> &dyn std::any::Any;
 
+    /// Get the number of pixels that an element's content is scrolled
+    fn get_scroll_offset(&self) -> Pin<Box<dyn Future<Output = MountedResult<PixelsVector2D>>>> {
+        Box::pin(async { Err(MountedError::NotSupported) })
+    }
+
+    /// Get the size of an element's content, including content not visible on the screen due to overflow
+    #[allow(clippy::type_complexity)]
+    fn get_scroll_size(&self) -> Pin<Box<dyn Future<Output = MountedResult<PixelsSize>>>> {
+        Box::pin(async { Err(MountedError::NotSupported) })
+    }
+
     /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
     #[allow(clippy::type_complexity)]
-    fn get_client_rect(&self) -> Pin<Box<dyn Future<Output = MountedResult<Rect<f64, f64>>>>> {
+    fn get_client_rect(&self) -> Pin<Box<dyn Future<Output = MountedResult<PixelsRect>>>> {
         Box::pin(async { Err(MountedError::NotSupported) })
     }
 
@@ -74,8 +83,18 @@ impl MountedData {
         }
     }
 
+    /// Get the number of pixels that an element's content is scrolled
+    pub async fn get_scroll_offset(&self) -> MountedResult<PixelsVector2D> {
+        self.inner.get_scroll_offset().await
+    }
+
+    /// Get the size of an element's content, including content not visible on the screen due to overflow
+    pub async fn get_scroll_size(&self) -> MountedResult<PixelsSize> {
+        self.inner.get_scroll_size().await
+    }
+
     /// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
-    pub async fn get_client_rect(&self) -> MountedResult<Rect<f64, f64>> {
+    pub async fn get_client_rect(&self) -> MountedResult<PixelsRect> {
         self.inner.get_client_rect().await
     }
 
@@ -100,6 +119,8 @@ impl MountedData {
 
 use dioxus_core::Event;
 
+use crate::geometry::{PixelsRect, PixelsSize, PixelsVector2D};
+
 pub type MountedEvent = Event<MountedData>;
 
 impl_event! [

+ 11 - 5
packages/html/src/geometry.rs

@@ -27,8 +27,14 @@ pub type PagePoint = Point2D<f64, PageSpace>;
 
 /// A pixel unit: one unit corresponds to 1 pixel
 pub struct Pixels;
-/// A vector expressed in Pixels
-pub type PixelsVector = Vector3D<f64, Pixels>;
+/// A size expressed in Pixels
+pub type PixelsSize = Size2D<f64, Pixels>;
+/// A rectangle expressed in Pixels
+pub type PixelsRect = Rect<f64, Pixels>;
+/// A 2D vector expressed in Pixels
+pub type PixelsVector2D = Vector2D<f64, Pixels>;
+/// A 3D vector expressed in Pixels
+pub type PixelsVector3D = Vector3D<f64, Pixels>;
 
 /// A unit in terms of Lines
 ///
@@ -51,7 +57,7 @@ pub type PagesVector = Vector3D<f64, Pages>;
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 pub enum WheelDelta {
     /// Movement in Pixels
-    Pixels(PixelsVector),
+    Pixels(PixelsVector3D),
     /// Movement in Lines
     Lines(LinesVector),
     /// Movement in Pages
@@ -62,7 +68,7 @@ impl WheelDelta {
     /// Construct from the attributes of the web wheel event
     pub fn from_web_attributes(delta_mode: u32, delta_x: f64, delta_y: f64, delta_z: f64) -> Self {
         match delta_mode {
-            0 => WheelDelta::Pixels(PixelsVector::new(delta_x, delta_y, delta_z)),
+            0 => WheelDelta::Pixels(PixelsVector3D::new(delta_x, delta_y, delta_z)),
             1 => WheelDelta::Lines(LinesVector::new(delta_x, delta_y, delta_z)),
             2 => WheelDelta::Pages(PagesVector::new(delta_x, delta_y, delta_z)),
             _ => panic!("Invalid delta mode, {:?}", delta_mode),
@@ -71,7 +77,7 @@ impl WheelDelta {
 
     /// Convenience function for constructing a WheelDelta with pixel units
     pub fn pixels(x: f64, y: f64, z: f64) -> Self {
-        WheelDelta::Pixels(PixelsVector::new(x, y, z))
+        WheelDelta::Pixels(PixelsVector3D::new(x, y, z))
     }
 
     /// Convenience function for constructing a WheelDelta with line units

+ 26 - 5
packages/html/src/web_sys_bind/events.rs

@@ -4,7 +4,9 @@ use crate::events::{
     TransitionData, WheelData,
 };
 use crate::file_data::{FileEngine, HasFileData};
-use crate::geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint};
+use crate::geometry::{
+    ClientPoint, ElementPoint, PagePoint, PixelsRect, PixelsSize, PixelsVector2D, ScreenPoint,
+};
 use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
 use crate::prelude::*;
 use keyboard_types::{Code, Key, Modifiers};
@@ -423,13 +425,32 @@ impl From<&web_sys::Element> for MountedData {
 
 #[cfg(feature = "mounted")]
 impl crate::RenderedElementBacking for web_sys::Element {
+    fn get_scroll_offset(
+        &self,
+    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::MountedResult<PixelsVector2D>>>>
+    {
+        let left = self.scroll_left();
+        let top = self.scroll_top();
+        let result = Ok(PixelsVector2D::new(left as f64, top as f64));
+        Box::pin(async { result })
+    }
+
+    fn get_scroll_size(
+        &self,
+    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::MountedResult<PixelsSize>>>>
+    {
+        let left = self.scroll_left();
+        let top = self.scroll_top();
+        let result = Ok(PixelsSize::new(left as f64, top as f64));
+        Box::pin(async { result })
+    }
+
     fn get_client_rect(
         &self,
-    ) -> std::pin::Pin<
-        Box<dyn std::future::Future<Output = crate::MountedResult<euclid::Rect<f64, f64>>>>,
-    > {
+    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::MountedResult<PixelsRect>>>>
+    {
         let rect = self.get_bounding_client_rect();
-        let result = Ok(euclid::Rect::new(
+        let result = Ok(PixelsRect::new(
             euclid::Point2D::new(rect.left(), rect.top()),
             euclid::Size2D::new(rect.width(), rect.height()),
         ));

+ 1 - 1
packages/interpreter/src/js/hash.txt

@@ -1 +1 @@
-5713307201725207733
+8520528080524713002

+ 1 - 1
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;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...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;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;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<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){const target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target);let body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event,target),this.liveview){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)}}preventDefaults(event,target){let preventDefaultRequests=null;if(target instanceof Element)preventDefaultRequests=target.getAttribute("dioxus-prevent-default");if(preventDefaultRequests&&preventDefaultRequests.includes(`on${event.type}`))event.preventDefault();if(event.type==="submit")event.preventDefault();if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target,preventDefaultRequests)}handleClickNavigate(event,target,preventDefaultRequests){if(!this.intercept_link_redirects)return;if(target.tagName==="BUTTON"&&event.type=="submit")event.preventDefault();let a_element=target.closest("a");if(a_element==null)return;event.preventDefault();let elementShouldPreventDefault=preventDefaultRequests&&preventDefaultRequests.includes("onclick"),aElementShouldPreventDefault=a_element.getAttribute("dioxus-prevent-default"),linkShouldPreventDefault=aElementShouldPreventDefault&&aElementShouldPreventDefault.includes("onclick");if(!elementShouldPreventDefault&&!linkShouldPreventDefault){const href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}enqueueBytes(bytes){this.queuedBytes.push(bytes)}flushQueuedBytes(){const byteArray=this.queuedBytes;this.queuedBytes=[];for(let bytes of byteArray)this.run_from_bytes(bytes)}rafEdits(headless,bytes){if(headless)this.run_from_bytes(bytes),this.waitForRequest(headless);else this.enqueueBytes(bytes),requestAnimationFrame(()=>{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<stylesheets.length;i++){let sheet=stylesheets[i];fetch(sheet.href,{cache:"reload"}).then(()=>{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){const file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};const message=this.serializeIpcMessage("user_event",{name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}export{NativeInterpreter};
+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;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...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})}getScrollHeight(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollHeight}getScrollLeft(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollLeft}getScrollTop(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollTop}getScrollWidth(id){const node=this.nodes[id];if(node instanceof HTMLElement)return node.scrollWidth}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;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;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<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){const target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target);let body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event,target),this.liveview){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)}}preventDefaults(event,target){let preventDefaultRequests=null;if(target instanceof Element)preventDefaultRequests=target.getAttribute("dioxus-prevent-default");if(preventDefaultRequests&&preventDefaultRequests.includes(`on${event.type}`))event.preventDefault();if(event.type==="submit")event.preventDefault();if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target,preventDefaultRequests)}handleClickNavigate(event,target,preventDefaultRequests){if(!this.intercept_link_redirects)return;if(target.tagName==="BUTTON"&&event.type=="submit")event.preventDefault();let a_element=target.closest("a");if(a_element==null)return;event.preventDefault();let elementShouldPreventDefault=preventDefaultRequests&&preventDefaultRequests.includes("onclick"),aElementShouldPreventDefault=a_element.getAttribute("dioxus-prevent-default"),linkShouldPreventDefault=aElementShouldPreventDefault&&aElementShouldPreventDefault.includes("onclick");if(!elementShouldPreventDefault&&!linkShouldPreventDefault){const href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}enqueueBytes(bytes){this.queuedBytes.push(bytes)}flushQueuedBytes(){const byteArray=this.queuedBytes;this.queuedBytes=[];for(let bytes of byteArray)this.run_from_bytes(bytes)}rafEdits(headless,bytes){if(headless)this.run_from_bytes(bytes),this.waitForRequest(headless);else this.enqueueBytes(bytes),requestAnimationFrame(()=>{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<stylesheets.length;i++){let sheet=stylesheets[i];fetch(sheet.href,{cache:"reload"}).then(()=>{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){const file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};const message=this.serializeIpcMessage("user_event",{name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}export{NativeInterpreter};

+ 28 - 0
packages/interpreter/src/ts/native.ts

@@ -110,6 +110,34 @@ export class NativeInterpreter extends JSChannel_ {
     }
   }
 
+  getScrollHeight(id: NodeId): number | undefined {
+    const node = this.nodes[id];
+    if (node instanceof HTMLElement) {
+      return node.scrollHeight;
+    }
+  }
+
+  getScrollLeft(id: NodeId): number | undefined {
+    const node = this.nodes[id];
+    if (node instanceof HTMLElement) {
+      return node.scrollLeft;
+    }
+  }
+
+  getScrollTop(id: NodeId): number | undefined {
+    const node = this.nodes[id];
+    if (node instanceof HTMLElement) {
+      return node.scrollTop;
+    }
+  }
+
+  getScrollWidth(id: NodeId): number | undefined {
+    const node = this.nodes[id];
+    if (node instanceof HTMLElement) {
+      return node.scrollWidth;
+    }
+  }
+
   getClientRect(
     id: NodeId
   ): { type: string; origin: number[]; size: number[] } | undefined {

+ 49 - 28
packages/liveview/src/element.rs

@@ -1,5 +1,8 @@
 use dioxus_core::ElementId;
-use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
+use dioxus_html::{
+    geometry::{PixelsRect, PixelsSize, PixelsVector2D},
+    MountedResult, RenderedElementBacking,
+};
 
 use crate::query::QueryEngine;
 
@@ -16,38 +19,56 @@ impl LiveviewElement {
     }
 }
 
+macro_rules! scripted_getter {
+    ($meth_name:ident, $script:literal, $output_type:path) => {
+        fn $meth_name(
+            &self,
+        ) -> std::pin::Pin<
+            Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<$output_type>>>,
+        > {
+            let script = format!($script, id = self.id.0);
+
+            let fut = self
+                .query
+                .new_query::<Option<$output_type>>(&script)
+                .resolve();
+            Box::pin(async move {
+                match fut.await {
+                    Ok(Some(res)) => Ok(res),
+                    Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
+                        Box::new(DesktopQueryError::FailedToQuery),
+                    )),
+                    Err(err) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
+                        Box::new(err),
+                    )),
+                }
+            })
+        }
+    };
+}
+
 impl RenderedElementBacking for LiveviewElement {
     fn as_any(&self) -> &dyn std::any::Any {
         self
     }
 
-    fn get_client_rect(
-        &self,
-    ) -> std::pin::Pin<
-        Box<
-            dyn futures_util::Future<
-                Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
-            >,
-        >,
-    > {
-        let script = format!("return window.interpreter.getClientRect({});", self.id.0);
-
-        let fut = self
-            .query
-            .new_query::<Option<Rect<f64, f64>>>(&script)
-            .resolve();
-        Box::pin(async move {
-            match fut.await {
-                Ok(Some(rect)) => Ok(rect),
-                Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
-                    Box::new(DesktopQueryError::FailedToQuery),
-                )),
-                Err(err) => {
-                    MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
-                }
-            }
-        })
-    }
+    scripted_getter!(
+        get_scroll_offset,
+        "return [window.interpreter.getScrollLeft({id}), window.interpreter.getScrollTop({id})]",
+        PixelsVector2D
+    );
+
+    scripted_getter!(
+        get_scroll_size,
+        "return [window.interpreter.getScrollWidth({id}), window.interpreter.getScrollHeight({id})]",
+        PixelsSize
+    );
+
+    scripted_getter!(
+        get_client_rect,
+        "return window.interpreter.getClientRect({id});",
+        PixelsRect
+    );
 
     fn scroll_to(
         &self,