feat: wire up event delegator for webview

This commit is contained in:
Jonathan Kelley 2021-07-24 02:52:05 -04:00
parent 0a907b35c6
commit 7dfe89c958
21 changed files with 265 additions and 272 deletions

View file

@ -5,10 +5,9 @@ fn main() {
use dioxus_elements::{GlobalAttributes, SvgAttributes};
__cx.element(
dioxus_elements::button,
__cx.bump()
.alloc([dioxus::events::on::onclick(__cx, move |_| {})]),
__cx.bump().alloc([]),
__cx.bump().alloc([]),
[dioxus::events::on::onclick(__cx, move |_| {})],
[],
[],
None,
)
});

View file

@ -5,6 +5,7 @@
// use dioxus::prelude::*;
fn main() {
env_logger::init();
dioxus::desktop::launch(App, |cfg| cfg);
}
@ -23,7 +24,7 @@ const App: FC<()> = |cx| {
let operator = use_state(cx, || None as Option<Operator>);
let display_value = use_state(cx, || "".to_string());
let clear_display = display_value.eq("0");
let clear_display = display_value == "0";
let clear_text = if clear_display { "C" } else { "AC" };
let input_digit = move |num: u8| display_value.get_mut().push_str(num.to_string().as_str());

View file

@ -20,6 +20,7 @@ use dioxus::prelude::*;
const STYLE: &str = include_str!("./assets/calculator.css");
fn main() {
env_logger::init();
dioxus::desktop::launch(App, |cfg| cfg.with_title("Calculator Demo"))
.expect("failed to launch dioxus app");
}

View file

@ -12,6 +12,7 @@
use dioxus::prelude::*;
fn main() {
env_logger::init();
dioxus::desktop::launch(App, |c| c);
}

View file

@ -13,8 +13,6 @@
//!
//!
use crate::util::is_valid_svg_tag;
use {
proc_macro::TokenStream,
proc_macro2::{Span, TokenStream as TokenStream2},
@ -163,11 +161,11 @@ impl ToTokens for ToToksCtx<&Element> {
self.recurse(attr).to_tokens(tokens);
}
if is_valid_svg_tag(&name.to_string()) {
tokens.append_all(quote! {
.namespace(Some("http://www.w3.org/2000/svg"))
});
}
// if is_valid_svg_tag(&name.to_string()) {
// tokens.append_all(quote! {
// .namespace(Some("http://www.w3.org/2000/svg"))
// });
// }
match &self.inner.children {
MaybeExpr::Expr(expr) => tokens.append_all(quote! {

View file

@ -7,7 +7,6 @@ pub(crate) mod htm;
pub(crate) mod ifmt;
pub(crate) mod props;
pub(crate) mod rsx;
pub(crate) mod util;
#[proc_macro]
pub fn format_args_f(input: TokenStream) -> TokenStream {

View file

@ -1,4 +1,3 @@
use crate::util::is_valid_tag;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{

View file

@ -273,8 +273,7 @@ fn parse_rsx_element_field(
let ty: AttrType = match name_str.as_str() {
// short circuit early if style is using the special syntax
"style" if stream.peek(Token![:]) => {
stream.parse::<Token![:]>().unwrap();
"style" if stream.peek(token::Brace) => {
let inner;
syn::braced!(inner in stream);

View file

@ -1,179 +0,0 @@
// use lazy_static::lazy_static;
use once_cell::sync::Lazy;
use std::collections::hash_set::HashSet;
use syn::{parse::ParseBuffer, Expr};
pub fn try_parse_bracketed(stream: &ParseBuffer) -> syn::Result<Expr> {
let content;
syn::braced!(content in stream);
content.parse()
}
/// rsx! and html! macros support the html namespace as well as svg namespace
static HTML_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
"a",
"abbr",
"address",
"area",
"article",
"aside",
"audio",
"b",
"base",
"bdi",
"bdo",
"big",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"cite",
"code",
"col",
"colgroup",
"command",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"div",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"head",
"header",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"keygen",
"label",
"legend",
"li",
"link",
"main",
"map",
"mark",
"menu",
"menuitem",
"meta",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"param",
"picture",
"pre",
"progress",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"script",
"section",
"select",
"small",
"source",
"span",
"strong",
"style",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"track",
"u",
"ul",
"var",
"video",
"wbr",
]
.iter()
.cloned()
.collect()
});
static SVG_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
// SVTG
"svg", "path", "g", "text",
]
.iter()
.cloned()
.collect()
});
// these tags are reserved by dioxus for any reason
// They might not all be used
static RESERVED_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
// a fragment
"fragment",
]
.iter()
.cloned()
.collect()
});
/// Whether or not this tag is valid
///
/// ```
/// use html_validation::is_valid_tag;
///
/// assert_eq!(is_valid_tag("br"), true);
///
/// assert_eq!(is_valid_tag("random"), false);
/// ```
pub fn is_valid_tag(tag: &str) -> bool {
is_valid_html_tag(tag) || is_valid_svg_tag(tag) || is_valid_reserved_tag(tag)
}
pub fn is_valid_html_tag(tag: &str) -> bool {
HTML_TAGS.contains(tag)
}
pub fn is_valid_svg_tag(tag: &str) -> bool {
SVG_TAGS.contains(tag)
}
pub fn is_valid_reserved_tag(tag: &str) -> bool {
RESERVED_TAGS.contains(tag)
}

View file

@ -20,8 +20,7 @@ pub struct ElementId(pub usize);
impl ElementId {
pub fn as_u64(self) -> u64 {
todo!()
// self.0.as_ffi()
self.0 as u64
}
}

View file

@ -85,7 +85,7 @@ impl<'r, 'b> DiffMachine<'r, 'b> {
pub fn get_scope_mut(&mut self, id: &ScopeId) -> Option<&'b mut Scope> {
// ensure we haven't seen this scope before
// if we have, then we're trying to alias it, which is not allowed
debug_assert!(!self.seen_nodes.contains(id));
// debug_assert!(!self.seen_nodes.contains(id));
unsafe { self.vdom.get_scope_mut(*id) }
}
pub fn get_scope(&mut self, id: &ScopeId) -> Option<&'b Scope> {
@ -120,10 +120,11 @@ impl<'real, 'bump> DiffMachine<'real, 'bump> {
//
// each function call assumes the stack is fresh (empty).
pub fn diff_node(&mut self, old_node: &'bump VNode<'bump>, new_node: &'bump VNode<'bump>) {
let root = old_node
.dom_id
.get()
.expect("Should not be diffing old nodes that were never assigned");
// currently busted for components - need to fid
let root = old_node.dom_id.get().expect(&format!(
"Should not be diffing old nodes that were never assigned, {:#?}",
old_node
));
match (&old_node.kind, &new_node.kind) {
// Handle the "sane" cases first.

View file

@ -125,6 +125,8 @@ pub enum VirtualEvent {
hook_idx: usize,
domnode: Rc<Cell<Option<ElementId>>>,
},
// TOOD: make garbage collection its own dedicated event
// GarbageCollection {}
}
pub mod on {
@ -183,6 +185,7 @@ pub mod on {
where F: FnMut($wrapper) + 'a
{
let bump = &c.bump();
let cb: &mut dyn FnMut(VirtualEvent) = bump.alloc(move |evt: VirtualEvent| match evt {
VirtualEvent::$wrapper(event) => callback(event),
_ => unreachable!("Downcasted VirtualEvent to wrong event type - this is an internal bug!")
@ -190,8 +193,10 @@ pub mod on {
let callback: BumpBox<dyn FnMut(VirtualEvent) + 'a> = unsafe { BumpBox::from_raw(cb) };
let event_name = stringify!($name);
let shortname: &'static str = &event_name[2..];
Listener {
event: stringify!($name),
event: shortname,
mounted_node: Cell::new(None),
scope: c.scope.our_arena_idx,
callback: RefCell::new(callback),

View file

@ -10,9 +10,9 @@
//!
pub use crate::innerlude::{
format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventTrigger,
LazyNodes, NodeFactory, Properties, RealDom, ScopeId, VNode, VNodeKind, VirtualDom,
VirtualEvent, FC,
format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority,
EventTrigger, LazyNodes, NodeFactory, Properties, RealDom, ScopeId, VNode, VNodeKind,
VirtualDom, VirtualEvent, FC,
};
pub mod prelude {

View file

@ -249,13 +249,12 @@ impl<'a> NodeFactory<'a> {
// We take the references directly from the bump arena
//
//
// TODO: this code shouldn't necessarily be here of all places
// It would make more sense to do this in diffing
let mut queue = self.scope.listeners.borrow_mut();
for listener in listeners.iter() {
let long_listener: &Listener<'static> = unsafe { std::mem::transmute(listener) };
let long_listener: &'a Listener<'static> = unsafe { std::mem::transmute(listener) };
queue.push(long_listener as *const _)
}

View file

@ -108,6 +108,8 @@ impl Scope {
let next_frame = self.frames.prev_frame_mut();
next_frame.bump.reset();
// make sure we call the drop implementation on all the listeners
// this is important to not leak memory
self.listeners.borrow_mut().clear();
unsafe { self.hooks.reset() };

View file

@ -258,10 +258,10 @@ impl VirtualDom {
// but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework.
//
// A good project would be to remove all unsafe from this crate and move the unsafety into safer abstractions.
pub async fn progress_with_event<'s>(
pub async fn progress_with_event<'a, 's>(
&'s mut self,
realdom: &'s mut dyn RealDom<'s>,
edits: &mut Vec<DomEdit<'s>>,
realdom: &'a mut dyn RealDom<'s>,
edits: &'a mut Vec<DomEdit<'s>>,
) -> Result<()> {
let trigger = self.triggers.borrow_mut().pop().expect("failed");

View file

@ -20,13 +20,13 @@ log = "0.4.13"
fern = { version = "0.6.0", features = ["colored"] }
html-escape = "0.2.9"
wry = "0.11.0"
async-std = { version = "1.9.0", features = ["attributes"] }
[dev-dependencies]
dioxus-html = { path = "../html" }
tide = "0.15.0"
tide-websockets = "0.3.0"
async-std = { version = "1.9.0", features = ["attributes"] }
# thiserror = "1.0.23"
# log = "0.4.13"

View file

@ -0,0 +1,85 @@
//! Convert a serialized event to an event Trigger
//!
use std::rc::Rc;
use dioxus_core::{
events::{
on::{MouseEvent, MouseEventInner},
VirtualEvent,
},
ElementId, EventPriority, EventTrigger, ScopeId,
};
#[derive(serde::Serialize, serde::Deserialize)]
struct ImEvent {
event: String,
mounted_dom_id: u64,
scope: u64,
}
pub fn trigger_from_serialized(val: serde_json::Value) -> EventTrigger {
let mut data: Vec<ImEvent> = serde_json::from_value(val).unwrap();
let data = data.drain(..).next().unwrap();
let event = VirtualEvent::MouseEvent(MouseEvent(Rc::new(WebviewMouseEvent)));
let scope = ScopeId(data.scope as usize);
let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize));
let priority = EventPriority::High;
EventTrigger::new(event, scope, mounted_dom_id, priority)
}
#[derive(Debug)]
struct WebviewMouseEvent;
impl MouseEventInner for WebviewMouseEvent {
fn alt_key(&self) -> bool {
todo!()
}
fn button(&self) -> i16 {
todo!()
}
fn buttons(&self) -> u16 {
todo!()
}
fn client_x(&self) -> i32 {
todo!()
}
fn client_y(&self) -> i32 {
todo!()
}
fn ctrl_key(&self) -> bool {
todo!()
}
fn meta_key(&self) -> bool {
todo!()
}
fn page_x(&self) -> i32 {
todo!()
}
fn page_y(&self) -> i32 {
todo!()
}
fn screen_x(&self) -> i32 {
todo!()
}
fn screen_y(&self) -> i32 {
todo!()
}
fn shift_key(&self) -> bool {
todo!()
}
fn get_modifier_state(&self, key_code: &str) -> bool {
todo!()
}
}

View file

@ -5,10 +5,13 @@
<script>
class Interpreter {
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {};
this.listeners = {
"onclick": {}
};
this.lastNodeWasText = false;
this.nodes = [];
this.nodes = [root, root, root, root];
}
top() {
@ -18,66 +21,125 @@
pop() {
return this.stack.pop();
}
}
class OPTABLE {
PushRoot(self, edit) {
const id = edit.root;
const node = self.nodes[id];
self.stack.push(node);
PushRoot(edit) {
const id = edit.id;
const node = this.nodes[id];
console.log("pushing root ", node, "with id", id);
this.stack.push(node);
}
AppendChildren(self, edit) {
let root = self.stack[self.stack.length - (edit.many + 1)];
AppendChildren(edit) {
let root = this.stack[this.stack.length - (edit.many + 1)];
for (let i = 0; i < edit.many; i++) {
console.log("popping ", i, edit.many);
let node = self.pop();
let node = this.pop();
root.appendChild(node);
}
}
ReplaceWith(self, edit) {
let root = self.stack[self.stack.length - (edit.many + 1)];
ReplaceWith(edit) {
let root = this.stack[this.stack.length - (edit.many + 1)];
let els = [];
for (let i = 0; i < edit.many; i++) {
els.push(self.pop());
els.push(this.pop());
}
root.replaceWith(...els);
}
Remove(self, edit) {
const node = self.stack.pop();
Remove(edit) {
const node = this.stack.pop();
node.remove();
}
RemoveAllChildren(self, edit) {}
CreateTextNode(self, edit) {
self.stack.push(document.createTextNode(edit.text));
RemoveAllChildren(edit) {}
CreateTextNode(edit) {
const node = document.createTextNode(edit.text);
this.nodes[edit.id] = node;
this.stack.push(node);
}
CreateElement(self, edit) {
CreateElement(edit) {
const tagName = edit.tag;
const el = document.createElement(tagName);
this.nodes[edit.id] = el;
console.log(`creating element: `, edit);
self.stack.push(document.createElement(tagName));
this.stack.push(el);
}
CreateElementNs(self, edit) {
CreateElementNs(edit) {
const tagName = edit.tag;
console.log(`creating namespaced element: `, edit);
self.stack.push(document.createElementNS(edit.ns, edit.tag));
this.stack.push(document.createElementNS(edit.ns, edit.tag));
}
CreatePlaceholder(self, edit) {
const a = `self.stack.push(document.createElement("pre"))`;
self.stack.push(document.createComment("vroot"));
CreatePlaceholder(edit) {
const a = `this.stack.push(document.createElement(" pre"))`;
this.stack.push(document.createComment("vroot"));
}
NewEventListener(self, edit) {}
RemoveEventListener(self, edit) {}
SetText(self, edit) {
self.top().textContent = edit.text;
NewEventListener(edit) {
const element_id = edit.element_id;
const event_name = edit.event_name;
const mounted_node_id = edit.mounted_node_id;
const scope = edit.scope;
const element = this.top();
element.setAttribute(`dioxus-event-${event_name}`, `${scope}.${mounted_node_id}`);
console.log("listener map is", this.listeners);
if (this.listeners[event_name] === undefined) {
console.log("adding listener!");
this.listeners[event_name] = "bla";
this.root.addEventListener(event_name, (event) => {
const target = event.target;
const type = event.type;
const val = target.getAttribute(`dioxus-event-${event_name}`);
const fields = val.split(".");
const scope_id = parseInt(fields[0]);
const real_id = parseInt(fields[1]);
console.log(`parsed event with scope_id ${scope_id} and real_id ${real_id}`);
rpc.call('user_event', {
event: event_name,
scope: scope_id,
mounted_dom_id: real_id,
}).then((reply) => {
console.log(reply);
this.stack.push(this.root);
for (let x = 0; x < reply.length; x++) {
let edit = reply[x];
console.log(edit);
let f = this[edit.type];
f.call(this, edit);
}
console.log("initiated");
}).catch((err) => {
console.log("failed to initiate", err);
});
});
}
}
SetAttribute(self, edit) {
RemoveEventListener(edit) {}
SetText(edit) {
this.top().textContent = edit.text;
}
SetAttribute(edit) {
const name = edit.field;
const value = edit.value;
const ns = edit.ns;
const node = self.top(self.stack);
const node = this.top(this.stack);
if (ns == "style") {
node.style[name] = value;
} else if (ns !== undefined) {
@ -85,36 +147,35 @@
} else {
node.setAttribute(name, value);
}
if ((name === "value", self)) {
if (name === "value") {
node.value = value;
}
if ((name === "checked", self)) {
if (name === "checked") {
node.checked = true;
}
if ((name === "selected", self)) {
if (name === "selected") {
node.selected = true;
}
}
RemoveAttribute(self, edit) {
RemoveAttribute(edit) {
const name = edit.field;
const node = self.top(self.stack);
const node = this.top(this.stack);
node.removeAttribute(name);
if ((name === "value", self)) {
if (name === "value") {
node.value = null;
}
if ((name === "checked", self)) {
if (name === "checked") {
node.checked = false;
}
if ((name === "selected", self)) {
if (name === "selected") {
node.selected = false;
}
}
}
const op_table = new OPTABLE();
async function initialize() {
const reply = await rpc.call('initiate');
@ -124,7 +185,9 @@
for (let x = 0; x < reply.length; x++) {
let edit = reply[x];
console.log(edit);
op_table[edit.type](interpreter, edit);
let f = interpreter[edit.type];
f.call(interpreter, edit);
}
console.log("stack completed: ", interpreter.stack);

View file

@ -17,6 +17,8 @@ use wry::{
mod dom;
mod escape;
mod events;
use events::*;
static HTML_CONTENT: &'static str = include_str!("./index.html");
@ -59,6 +61,7 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
redits: Option<Vec<DomEdit<'static>>>,
) -> anyhow::Result<()> {
log::info!("hello edits");
let event_loop = EventLoop::new();
let window = user_builder(WindowBuilder::new()).build(&event_loop)?;
@ -101,24 +104,42 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
}
"user_event" => {
let mut lock = vdom.write().unwrap();
let mut reg_lock = registry.write().unwrap();
log::debug!("User event received");
// Create the thin wrapper around the registry to collect the edits into
let mut real = dom::WebviewDom::new(reg_lock.take().unwrap());
let registry = registry.clone();
let vdom = vdom.clone();
let response = async_std::task::block_on(async move {
let mut lock = vdom.write().unwrap();
let mut reg_lock = registry.write().unwrap();
// Serialize the edit stream
let edits = {
// a deserialized event
let data = req.params.unwrap();
log::debug!("Data: {:#?}", data);
let event = trigger_from_serialized(data);
lock.queue_event(event);
// Create the thin wrapper around the registry to collect the edits into
let mut real = dom::WebviewDom::new(reg_lock.take().unwrap());
// Serialize the edit stream
//
let mut edits = Vec::new();
lock.rebuild(&mut real, &mut edits).unwrap();
serde_json::to_value(edits).unwrap()
};
lock.progress_with_event(&mut real, &mut edits)
.await
.expect("failed to progress");
let edits = serde_json::to_value(edits).unwrap();
// Give back the registry into its slot
*reg_lock = Some(real.consume());
// Give back the registry into its slot
*reg_lock = Some(real.consume());
// Return the edits into the webview runtime
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
// Return the edits into the webview runtime
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
});
response
// spawn a task to clean up the garbage
}
_ => todo!("this message failed"),
}

View file

@ -212,8 +212,8 @@ impl<'a, T: Copy + Div<T, Output = T>> DivAssign<T> for UseState<'a, T> {
self.set(self.inner.current_val.div(rhs));
}
}
impl<'a, T: PartialEq<T>> PartialEq<T> for UseState<'a, T> {
fn eq(&self, other: &T) -> bool {
impl<'a, V, T: PartialEq<V>> PartialEq<V> for UseState<'a, T> {
fn eq(&self, other: &V) -> bool {
self.get() == other
}
}