implement on mounted for desktop

This commit is contained in:
Evan Almloff 2023-03-20 16:10:34 -05:00
parent cb5cb56ad3
commit 7636c046fa
8 changed files with 450 additions and 24 deletions

View file

@ -3,6 +3,7 @@ use std::rc::Rc;
use std::rc::Weak;
use crate::create_new_window;
use crate::element::QueryEngine;
use crate::eval::EvalResult;
use crate::events::IpcMessage;
use crate::shortcut::IntoKeyCode;
@ -62,6 +63,9 @@ pub struct DesktopContext {
/// The receiver for eval results since eval is async
pub(super) eval: tokio::sync::broadcast::Sender<Value>,
/// The receiver for queries about elements
pub(super) query: QueryEngine,
pub(super) pending_windows: WebviewQueue,
pub(crate) event_loop: EventLoopWindowTarget<UserWindowEvent>,
@ -97,6 +101,7 @@ impl DesktopContext {
proxy,
event_loop,
eval: tokio::sync::broadcast::channel(8).0,
query: Default::default(),
pending_windows: webviews,
event_handlers,
shortcut_manager,

View file

@ -0,0 +1,208 @@
use std::{cell::RefCell, rc::Rc};
use dioxus_core::ElementId;
use dioxus_html::{
MountedResult, MountedReturn, MountedReturnData, NodeUpdate, NodeUpdateData,
RenderedElementBacking,
};
use slab::Slab;
use wry::webview::WebView;
/// A mounted element passed to onmounted events
pub struct DesktopElement {
id: ElementId,
webview: Rc<WebView>,
query: QueryEngine,
}
impl DesktopElement {
pub(crate) fn new(id: ElementId, webview: Rc<WebView>, query: QueryEngine) -> Self {
Self { id, webview, query }
}
/// Get the id of the element
pub fn id(&self) -> ElementId {
self.id
}
/// Get the webview the element is mounted in
pub fn webview(&self) -> &Rc<WebView> {
&self.webview
}
}
impl RenderedElementBacking for DesktopElement {
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
}
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures_util::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let fut = self
.query
.new_query(self.id, NodeUpdateData::GetClientRect {}, &self.webview)
.resolve();
Box::pin(async move {
match fut.await {
Some(MountedReturnData::GetClientRect(rect)) => Ok(rect),
Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::MismatchedReturn),
)),
None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(
DesktopQueryError::FailedToQuery,
))),
}
})
}
fn scroll_to(
&self,
behavior: dioxus_html::ScrollBehavior,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let fut = self
.query
.new_query(
self.id,
NodeUpdateData::ScrollTo { behavior },
&self.webview,
)
.resolve();
Box::pin(async move {
match fut.await {
Some(MountedReturnData::ScrollTo(())) => Ok(()),
Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::MismatchedReturn),
)),
None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(
DesktopQueryError::FailedToQuery,
))),
}
})
}
fn set_focus(
&self,
focus: bool,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let fut = self
.query
.new_query(self.id, NodeUpdateData::SetFocus { focus }, &self.webview)
.resolve();
Box::pin(async move {
match fut.await {
Some(MountedReturnData::SetFocus(())) => Ok(()),
Some(_) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::MismatchedReturn),
)),
None => MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(
DesktopQueryError::FailedToQuery,
))),
}
})
}
}
#[derive(Default, Clone)]
struct SharedSlab {
slab: Rc<RefCell<Slab<()>>>,
}
#[derive(Clone)]
pub(crate) struct QueryEngine {
sender: Rc<tokio::sync::broadcast::Sender<MountedReturn>>,
active_requests: SharedSlab,
}
impl Default for QueryEngine {
fn default() -> Self {
let (sender, _) = tokio::sync::broadcast::channel(8);
Self {
sender: Rc::new(sender),
active_requests: SharedSlab::default(),
}
}
}
impl QueryEngine {
fn new_query(&self, id: ElementId, update: NodeUpdateData, webview: &WebView) -> Query {
let request_id = self.active_requests.slab.borrow_mut().insert(());
let update = NodeUpdate {
id: id.0 as u32,
request_id,
data: update,
};
// start the query
webview
.evaluate_script(&format!(
"window.interpreter.handleNodeUpdate({})",
serde_json::to_string(&update).unwrap()
))
.unwrap();
Query {
slab: self.active_requests.clone(),
id: request_id,
reciever: self.sender.subscribe(),
}
}
pub fn send(&self, data: MountedReturn) {
self.sender.send(data).unwrap();
}
}
struct Query {
slab: SharedSlab,
id: usize,
reciever: tokio::sync::broadcast::Receiver<MountedReturn>,
}
impl Query {
async fn resolve(mut self) -> Option<MountedReturnData> {
let result = loop {
match self.reciever.recv().await {
Ok(result) => {
if result.id == self.id {
break result.data;
}
}
Err(_) => {
break None;
}
}
};
// Remove the query from the slab
self.slab.slab.borrow_mut().remove(self.id);
result
}
}
#[derive(Debug)]
enum DesktopQueryError {
FailedToQuery,
MismatchedReturn,
}
impl std::fmt::Display for DesktopQueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"),
DesktopQueryError::MismatchedReturn => {
write!(f, "The return type did not match the query")
}
}
}
}
impl std::error::Error for DesktopQueryError {}

View file

@ -5,6 +5,7 @@
mod cfg;
mod desktop_context;
mod element;
mod escape;
mod eval;
mod events;
@ -19,7 +20,8 @@ pub use desktop_context::{
};
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
use dioxus_core::*;
use dioxus_html::HtmlEvent;
use dioxus_html::{HtmlEvent, MountedData, MountedReturn};
use element::DesktopElement;
pub use eval::{use_eval, EvalResult};
use futures_util::{pin_mut, FutureExt};
use shortcut::ShortcutRegistry;
@ -220,19 +222,69 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
}
EventData::Ipc(msg) if msg.method() == "user_event" => {
let evt = match serde_json::from_value::<HtmlEvent>(msg.params()) {
let params = msg.params();
let evt = match serde_json::from_value::<HtmlEvent>(params) {
Ok(value) => value,
Err(_) => return,
};
let HtmlEvent {
element,
name,
bubbles,
data,
} = evt;
let view = webviews.get_mut(&event.1).unwrap();
view.dom
.handle_event(&evt.name, evt.data.into_any(), evt.element, evt.bubbles);
// check for a mounted event placeholder and replace it with a desktop specific element
let as_any = if let dioxus_html::EventData::Mounted = &data {
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query;
let element = DesktopElement::new(element, view.webview.clone(), query);
Rc::new(MountedData::new(element))
} else {
data.into_any()
};
view.dom.handle_event(&name, as_any, element, bubbles);
send_edits(view.dom.render_immediate(), &view.webview);
}
EventData::Ipc(msg) if msg.method() == "node_update" => {
let params = msg.params();
println!("node_update: {:?}", params);
// check for a mounted event
let evt = match serde_json::from_value::<MountedReturn>(params) {
Ok(value) => value,
Err(err) => {
println!("node_update: {:?}", err);
return;
}
};
let view = webviews.get(&event.1).unwrap();
let query = view
.dom
.base_scope()
.consume_context::<DesktopContext>()
.unwrap()
.query;
println!("node_update: {:?}", evt);
query.send(evt);
}
EventData::Ipc(msg) if msg.method() == "initialize" => {
let view = webviews.get_mut(&event.1).unwrap();
send_edits(view.dom.rebuild(), &view.webview);

View file

@ -5,12 +5,15 @@ use euclid::Rect;
use std::{
any::Any,
fmt::{Display, Formatter},
future::Future,
pin::Pin,
rc::Rc,
};
/// An Element that has been rendered and allows reading and modifying information about it.
///
/// Different platforms will have different implementations and different levels of support for this trait. Renderers that do not support specific features will return `None` for those queries.
// we can not use async_trait here because it does not create a trait that is object safe
pub trait RenderedElementBacking {
/// Get the renderer specific element for the given id
fn get_raw_element(&self) -> MountedResult<&dyn Any> {
@ -18,26 +21,35 @@ pub trait RenderedElementBacking {
}
/// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
fn get_client_rect(&self) -> MountedResult<Rect<f64, f64>> {
Err(MountedError::NotSupported)
#[allow(clippy::type_complexity)]
fn get_client_rect(&self) -> Pin<Box<dyn Future<Output = MountedResult<Rect<f64, f64>>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
/// Scroll to make the element visible
fn scroll_to(&self, _behavior: ScrollBehavior) -> MountedResult<()> {
Err(MountedError::NotSupported)
fn scroll_to(
&self,
_behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
/// Set the focus on the element
fn set_focus(&self, _focus: bool) -> MountedResult<()> {
Err(MountedError::NotSupported)
fn set_focus(&self, _focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
Box::pin(async { Err(MountedError::NotSupported) })
}
}
impl RenderedElementBacking for () {}
/// The way that scrolling should be performed
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub enum ScrollBehavior {
/// Scroll to the element immediately
#[cfg_attr(feature = "serialize", serde(rename = "instant"))]
Instant,
/// Scroll to the element smoothly
#[cfg_attr(feature = "serialize", serde(rename = "smooth"))]
Smooth,
}
@ -62,18 +74,18 @@ impl MountedData {
}
/// Get the bounding rectangle of the element relative to the viewport (this does not include the scroll position)
pub fn get_client_rect(&self) -> MountedResult<Rect<f64, f64>> {
self.inner.get_client_rect()
pub async fn get_client_rect(&self) -> MountedResult<Rect<f64, f64>> {
self.inner.get_client_rect().await
}
/// Scroll to make the element visible
pub fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> {
self.inner.scroll_to(behavior)
pub async fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> {
self.inner.scroll_to(behavior).await
}
/// Set the focus on the element
pub fn set_focus(&self, focus: bool) -> MountedResult<()> {
self.inner.set_focus(focus)
pub async fn set_focus(&self, focus: bool) -> MountedResult<()> {
self.inner.set_focus(focus).await
}
}

View file

@ -2,6 +2,7 @@ use std::{any::Any, rc::Rc};
use crate::events::*;
use dioxus_core::ElementId;
use euclid::Rect;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug, Clone, PartialEq)]
@ -113,6 +114,9 @@ fn fun_name(
// Toggle
"toggle" => Toggle(de(data)?),
// Mounted
"mounted" => Mounted,
// ImageData => "load" | "error";
// OtherData => "abort" | "afterprint" | "beforeprint" | "beforeunload" | "hashchange" | "languagechange" | "message" | "offline" | "online" | "pagehide" | "pageshow" | "popstate" | "rejectionhandled" | "storage" | "unhandledrejection" | "unload" | "userproximity" | "vrdisplayactivate" | "vrdisplayblur" | "vrdisplayconnect" | "vrdisplaydeactivate" | "vrdisplaydisconnect" | "vrdisplayfocus" | "vrdisplaypointerrestricted" | "vrdisplaypointerunrestricted" | "vrdisplaypresentchange";
other => {
@ -151,6 +155,7 @@ pub enum EventData {
Animation(AnimationData),
Transition(TransitionData),
Toggle(ToggleData),
Mounted,
}
impl EventData {
@ -172,6 +177,7 @@ impl EventData {
EventData::Animation(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Transition(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Toggle(data) => Rc::new(data) as Rc<dyn Any>,
EventData::Mounted => Rc::new(MountedData::new(())) as Rc<dyn Any>,
}
}
}
@ -215,3 +221,49 @@ fn test_back_and_forth() {
assert_eq!(data, p);
}
/// Message to update a node to support MountedData
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct NodeUpdate {
/// The id of the node to update
pub id: u32,
/// The id of the request
pub request_id: usize,
/// The data to update the node with
pub data: NodeUpdateData,
}
/// Message to update a node to support MountedData
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
pub enum NodeUpdateData {
SetFocus { focus: bool },
GetClientRect {},
ScrollTo { behavior: ScrollBehavior },
}
/// The result of a element query
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct MountedReturn {
/// A unique id for the query
pub id: usize,
/// The result of the query
pub data: Option<MountedReturnData>,
}
/// The data of a element query
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
serde(tag = "type")
)]
pub enum MountedReturnData {
SetFocus(()),
GetClientRect(Rect<f64, f64>),
ScrollTo(()),
}

View file

@ -9,6 +9,8 @@ use crate::{
};
use keyboard_types::{Code, Key, Modifiers};
use std::convert::TryInto;
use std::future::Future;
use std::pin::Pin;
use std::str::FromStr;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
@ -203,19 +205,25 @@ impl From<&web_sys::Element> for MountedData {
}
impl RenderedElementBacking for web_sys::Element {
fn get_client_rect(&self) -> MountedResult<euclid::Rect<f64, f64>> {
fn get_client_rect(
&self,
) -> Pin<Box<dyn Future<Output = MountedResult<euclid::Rect<f64, f64>>>>> {
let rect = self.get_bounding_client_rect();
Ok(euclid::Rect::new(
let result = Ok(euclid::Rect::new(
euclid::Point2D::new(rect.left(), rect.top()),
euclid::Size2D::new(rect.width(), rect.height()),
))
));
Box::pin(async { result })
}
fn get_raw_element(&self) -> MountedResult<&dyn std::any::Any> {
Ok(self)
}
fn scroll_to(&self, behavior: ScrollBehavior) -> MountedResult<()> {
fn scroll_to(
&self,
behavior: ScrollBehavior,
) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
match behavior {
ScrollBehavior::Instant => self.scroll_into_view_with_scroll_into_view_options(
ScrollIntoViewOptions::new().behavior(web_sys::ScrollBehavior::Instant),
@ -225,16 +233,18 @@ impl RenderedElementBacking for web_sys::Element {
),
}
Ok(())
Box::pin(async { Ok(()) })
}
fn set_focus(&self, focus: bool) -> MountedResult<()> {
self.dyn_ref::<web_sys::HtmlElement>()
fn set_focus(&self, focus: bool) -> Pin<Box<dyn Future<Output = MountedResult<()>>>> {
let result = self
.dyn_ref::<web_sys::HtmlElement>()
.ok_or_else(|| MountedError::OperationFailed(Box::new(FocusError(self.into()))))
.and_then(|e| {
(if focus { e.focus() } else { e.blur() })
.map_err(|err| MountedError::OperationFailed(Box::new(FocusError(err))))
})
});
Box::pin(async { result })
}
}

View file

@ -19,8 +19,10 @@ js-sys = { version = "0.3.56", optional = true }
web-sys = { version = "0.3.56", optional = true, features = ["Element", "Node"] }
sledgehammer_bindgen = { version = "0.1.3", optional = true }
sledgehammer_utils = { version = "0.1.0", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
[features]
default = []
serialize = ["serde"]
web = ["wasm-bindgen", "js-sys", "web-sys"]
sledgehammer = ["wasm-bindgen", "js-sys", "web-sys", "sledgehammer_bindgen", "sledgehammer_utils"]

View file

@ -204,6 +204,76 @@ class Interpreter {
node.removeAttribute(name);
}
}
GetClientRect(id) {
const node= this.nodes[id];
if (!node) {
return;
}
const rect = node.getBoundingClientRect();
return {
type: "GetClientRect",
origin: [
rect.x,
rect.y,
],
size: [
rect.width,
rect.height,
]
};
}
ScrollTo(id, behavior) {
const node = this.nodes[id];
if (!node) {
return;
}
node.scrollIntoView({
behavior: behavior
});
return {
type: "ScrollTo",
};
}
/// Set the focus on the element
SetFocus(id, focus) {
const node = this.nodes[id];
if (!node) {
return;
}
if (focus) {
node.focus();
} else {
node.blur();
}
return {
type: "SetFocus",
};
}
handleNodeUpdate(edit) {
let data;
switch (edit.data.type) {
case "SetFocus":
data = this.SetFocus(edit.id, edit.data.focus);
break;
case "ScrollTo":
data = this.ScrollTo(edit.id, edit.data.behavior);
break;
case "GetClientRect":
data = this.GetClientRect(edit.id);
break;
}
window.ipc.postMessage(
serializeIpcMessage("node_update", {
id: edit.request_id,
data: data
})
);
}
handleEdits(edits) {
for (let template of edits.templates) {
this.SaveTemplate(template);
@ -345,6 +415,19 @@ class Interpreter {
let bubbles = event_bubbles(edit.name);
// if this is a mounted listener, we send the event immediately
if (edit.name === "mounted") {
window.ipc.postMessage(
serializeIpcMessage("user_event", {
name: edit.name,
element: edit.id,
data: null,
bubbles,
})
);
}
// this handler is only provided on desktop implementations since this
// method is not used by the web implementation
let handler = (event) => {
@ -921,6 +1004,8 @@ function event_bubbles(event) {
return true;
case "toggle":
return true;
case "mounted":
return false;
}
return true;