feat: get web working properly

This commit is contained in:
Jonathan Kelley 2022-11-30 17:21:10 -05:00
parent 3c19def550
commit 18d6b1ad6f
26 changed files with 674 additions and 1422 deletions

View file

@ -12,13 +12,13 @@ members = [
"packages/desktop",
"packages/mobile",
"packages/interpreter",
# "packages/tui",
"packages/fermi",
"packages/tui",
"packages/liveview",
"packages/autofmt",
"packages/rsx",
"packages/native-core",
"packages/native-core-macro",
# "packages/native-core",
# "packages/native-core-macro",
# "packages/edit-stream",
"docs/guide",
]

32
examples/simple_todo.rs Normal file
View file

@ -0,0 +1,32 @@
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app);
}
fn app(cx: Scope) -> Element {
let mut idx = use_state(cx, || 0);
let onhover = |h| println!("go!");
cx.render(rsx! {
div {
button { onclick: move |_| idx += 1, "+" }
button { onclick: move |_| idx -= 1, "-" }
ul {
(0..**idx).map(|i| rsx! {
Child { i: i, onhover: onhover }
})
}
}
})
}
#[inline_props]
fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
cx.render(rsx! {
li {
onmouseover: move |e| onhover.call(e),
"{i}"
}
})
}

View file

@ -57,7 +57,10 @@ pub fn app(cx: Scope<()>) -> Element {
placeholder: "What needs to be done?",
value: "{draft}",
autofocus: "true",
oninput: move |evt| draft.set(evt.value.clone()),
oninput: move |evt| {
println!("calling oninput");
draft.set(evt.value.clone());
},
onkeydown: move |evt| {
if evt.key() == Key::Enter && !draft.is_empty() {
todos.make_mut().insert(
@ -121,6 +124,8 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element {
let completed = if todo.checked { "completed" } else { "" };
let editing = if **is_editing { "editing" } else { "" };
println!("rendering todo entry");
cx.render(rsx!{
li {
class: "{completed} {editing}",

View file

@ -13,7 +13,7 @@ pub(crate) struct ElementRef {
pub path: ElementPath,
// The actual template
pub template: *mut VNode<'static>,
pub template: *const VNode<'static>,
}
#[derive(Clone, Copy)]
@ -58,10 +58,22 @@ impl VirtualDom {
}
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
assert_ne!(el, ElementId(0));
if el.0 == 0 {
panic!(
"Invalid element set to 0 - {:#?}",
std::backtrace::Backtrace::force_capture()
)
}
println!("reclaiming {:?}", el);
self.elements.try_remove(el.0)
}
pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) {
let node: *const VNode = node as *const _;
self.elements[el.0].template = unsafe { std::mem::transmute(node) };
}
// Drop a scope and all its children
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
let scope = self.scopes.get(id.0).unwrap();
@ -73,21 +85,15 @@ impl VirtualDom {
}
}
let scope = self.scopes.get(id.0).unwrap();
if let Some(root) = unsafe { scope.as_ref().previous_frame().try_load_node() } {
let root = unsafe { root.extend_lifetime_ref() };
if let RenderReturn::Sync(Ok(node)) = root {
self.drop_scope_inner(node)
}
}
let scope = self.scopes.get(id.0).unwrap().as_ref();
let scope = self.scopes.get_mut(id.0).unwrap();
scope.props.take();
// Drop all the hooks once the children are dropped
// this means we'll drop hooks bottom-up
for hook in scope.hook_list.borrow_mut().drain(..) {
for hook in scope.hook_list.get_mut().drain(..) {
println!("dropping hook !");
drop(unsafe { BumpBox::from_raw(hook) });
println!("hook dropped !");
}
}

View file

@ -129,7 +129,7 @@ impl<'b> VirtualDom {
AttributeValue::Listener(_) => {
self.mutations.push(NewEventListener {
// all listeners start with "on"
event_name: &unbounded_name[2..],
name: &unbounded_name[2..],
scope: cur_scope,
id,
})

View file

@ -1,3 +1,5 @@
use std::cell::Cell;
use crate::{
arena::ElementId,
factory::RenderReturn,
@ -59,10 +61,17 @@ impl<'b> VirtualDom {
}
fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) {
println!(
"diffing node {:#?},\n\n{:#?}",
left_template.template.id, right_template.template.id
);
if left_template.template.id != right_template.template.id {
return self.light_diff_templates(left_template, right_template);
}
println!("diffing attributs...");
for (left_attr, right_attr) in left_template
.dynamic_attrs
.iter()
@ -73,6 +82,11 @@ impl<'b> VirtualDom {
.mounted_element
.set(left_attr.mounted_element.get());
// We want to make sure anything listener that gets pulled is valid
if let AttributeValue::Listener(_) = right_attr.value {
self.update_template(left_attr.mounted_element.get(), right_template);
}
if left_attr.value != right_attr.value || left_attr.volatile {
// todo: add more types of attribute values
match right_attr.value {
@ -92,6 +106,8 @@ impl<'b> VirtualDom {
}
}
println!("diffing dyn nodes...");
for (idx, (left_node, right_node)) in left_template
.dynamic_nodes
.iter()
@ -101,13 +117,24 @@ impl<'b> VirtualDom {
match (left_node, right_node) {
(Text(left), Text(right)) => self.diff_vtext(left, right),
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
(Placeholder(left), Placeholder(right)) => {
right.set(left.get());
}
(Component(left), Component(right)) => {
self.diff_vcomponent(left, right, right_template, idx)
}
_ => self.replace(left_template, right_template),
(Placeholder(left), Fragment(right)) => {
self.replace_placeholder_with_nodes(left, right)
}
(Fragment(left), Placeholder(right)) => {
self.replace_nodes_with_placeholder(left, right)
}
_ => todo!(),
};
}
println!("applying roots...");
// Make sure the roots get transferred over
for (left, right) in left_template
.root_ids
@ -118,6 +145,30 @@ impl<'b> VirtualDom {
}
}
fn replace_placeholder_with_nodes(&mut self, l: &'b Cell<ElementId>, r: &'b [VNode<'b>]) {
let m = self.create_children(r);
let id = l.get();
self.mutations.push(Mutation::ReplaceWith { id, m });
self.reclaim(id);
}
fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell<ElementId>) {
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
let placeholder = self.next_element(&l[0], &[]);
r.set(placeholder);
self.mutations
.push(Mutation::CreatePlaceholder { id: placeholder });
// Remove the old nodes, except for onea
let first = self.replace_inner(&l[0]);
self.remove_nodes(&l[1..]);
self.mutations
.push(Mutation::ReplaceWith { id: first, m: 1 });
self.try_reclaim(first);
}
fn diff_vcomponent(
&mut self,
left: &'b VComponent<'b>,
@ -140,6 +191,7 @@ impl<'b> VirtualDom {
};
self.mutations
.push(Mutation::ReplaceWith { id, m: created });
self.drop_scope(left.scope.get().unwrap());
return;
}
@ -154,15 +206,12 @@ impl<'b> VirtualDom {
// If the props are static, then we try to memoize by setting the new with the old
// The target scopestate still has the reference to the old props, so there's no need to update anything
// This also implicitly drops the new props since they're not used
if left.static_props && unsafe { old.memoize(new.as_ref()) } {
if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } {
return;
}
// If the props are dynamic *or* the memoization failed, then we need to diff the props
// First, move over the props from the old to the new, dropping old props in the process
self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new.as_ref()) };
right.props.set(Some(new));
self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new) };
// Now run the component and diff it
self.run_scope(scope_id);
@ -335,7 +384,7 @@ impl<'b> VirtualDom {
};
}
fn diff_non_empty_fragment(&mut self, new: &'b [VNode<'b>], old: &'b [VNode<'b>]) {
fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
let new_is_keyed = new[0].key.is_some();
let old_is_keyed = old[0].key.is_some();
debug_assert!(
@ -346,6 +395,9 @@ impl<'b> VirtualDom {
old.iter().all(|o| o.key.is_some() == old_is_keyed),
"all siblings must be keyed or all siblings must be non-keyed"
);
println!("Diffing fragment {:?}, {:?}", old.len(), new.len());
if new_is_keyed && old_is_keyed {
self.diff_keyed_children(old, new);
} else {
@ -368,7 +420,9 @@ impl<'b> VirtualDom {
debug_assert!(!new.is_empty());
debug_assert!(!old.is_empty());
match old.len().cmp(&new.len()) {
println!("Diffing non keyed children");
match dbg!(old.len().cmp(&new.len())) {
Ordering::Greater => self.remove_nodes(&old[new.len()..]),
Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()),
Ordering::Equal => {}

View file

@ -158,7 +158,7 @@ pub enum Mutation<'a> {
/// Create a new Event Listener.
NewEventListener {
/// The name of the event to listen for.
event_name: &'a str,
name: &'a str,
/// The ID of the node to attach the listener to.
scope: ScopeId,
@ -169,11 +169,11 @@ pub enum Mutation<'a> {
/// Remove an existing Event Listener.
RemoveEventListener {
/// The name of the event to remove.
name: &'a str,
/// The ID of the node to remove.
id: ElementId,
/// The name of the event to remove.
event: &'a str,
},
Remove {
id: ElementId,

View file

@ -28,7 +28,7 @@ impl VirtualDom {
parent,
id,
height,
props,
props: Some(props),
placeholder: Default::default(),
node_arena_1: BumpFrame::new(50),
node_arena_2: BumpFrame::new(50),
@ -86,6 +86,8 @@ impl VirtualDom {
}
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
println!("Running scope {:?}", scope_id);
// Cycle to the next frame and then reset it
// This breaks any latent references, invalidating every pointer referencing into it.
// Remove all the outdated listeners
@ -100,7 +102,8 @@ impl VirtualDom {
scope.hook_idx.set(0);
// safety: due to how we traverse the tree, we know that the scope is not currently aliased
let props: &dyn AnyProps = mem::transmute(&*scope.props);
let props = scope.props.as_ref().unwrap().as_ref();
let props: &dyn AnyProps = mem::transmute(props);
props.render(scope).extend_lifetime()
};

View file

@ -82,7 +82,7 @@ pub struct ScopeState {
pub(crate) tasks: Rc<Scheduler>,
pub(crate) spawned_tasks: HashSet<TaskId>,
pub(crate) props: Box<dyn AnyProps<'static>>,
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
pub(crate) placeholder: Cell<Option<ElementId>>,
}

View file

@ -362,6 +362,8 @@ impl VirtualDom {
let target_path = el_ref.path;
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
println!("{:?} \n {:?} \n {:?}", attr, name, element);
let this_path = template.template.attr_paths[idx];
// listeners are required to be prefixed with "on", but they come back to the virtualdom with that missing
@ -383,6 +385,8 @@ impl VirtualDom {
}
}
println!("calling listeners: {:?}", listeners);
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
// We check the bubble state between each call to see if the event has been stopped from bubbling
for listener in listeners.drain(..).rev() {
@ -399,6 +403,8 @@ impl VirtualDom {
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
}
println!("all listeners exhausted");
}
/// Wait for the scheduler to have any work.
@ -448,12 +454,15 @@ impl VirtualDom {
}
}
/// Swap the current mutations with a new
fn finalize(&mut self) -> Mutations {
// todo: make this a routine
let mut out = Mutations::default();
std::mem::swap(&mut self.mutations, &mut out);
out
/// Process all events in the queue until there are no more left
pub fn process_events(&mut self) {
while let Ok(Some(msg)) = self.rx.try_next() {
match msg {
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
}
}
}
/// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom from scratch.
@ -515,6 +524,7 @@ impl VirtualDom {
///
/// If no suspense trees are present
pub async fn render_with_deadline(&mut self, deadline: impl Future<Output = ()>) -> Mutations {
println!("render with deadline");
pin_mut!(deadline);
loop {
@ -600,6 +610,14 @@ impl VirtualDom {
}
}
}
/// Swap the current mutations with a new
fn finalize(&mut self) -> Mutations {
// todo: make this a routine
let mut out = Mutations::default();
std::mem::swap(&mut self.mutations, &mut out);
out
}
}
impl Drop for VirtualDom {

View file

@ -70,7 +70,7 @@ fn dual_stream() {
[
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
SetAttribute { name: "class", value: "123", id: ElementId(1), ns: None },
NewEventListener { event_name: "click", scope: ScopeId(0), id: ElementId(1) },
NewEventListener { name: "click", scope: ScopeId(0), id: ElementId(1) },
HydrateText { path: &[0, 0], value: "123", id: ElementId(2) },
AppendChildren { m: 1 }
],

View file

@ -0,0 +1,49 @@
use dioxus::prelude::*;
use dioxus_core::ElementId;
use std::rc::Rc;
#[test]
fn miri_rollover() {
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
for x in 0..3 {
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
dom.process_events();
dom.render_immediate();
}
}
fn app(cx: Scope) -> Element {
let mut idx = use_state(cx, || 0);
let onhover = |h| println!("go!");
cx.render(rsx! {
div {
button {
onclick: move |_| {
idx += 1;
println!("Clicked");
},
"+"
}
button { onclick: move |_| idx -= 1, "-" }
ul {
(0..**idx).map(|i| rsx! {
Child { i: i, onhover: onhover }
})
}
}
})
}
#[inline_props]
fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
cx.render(rsx! {
li {
onmouseover: move |e| onhover.call(e),
"{i}"
}
})
}

View file

@ -56,7 +56,7 @@ impl DesktopController {
let mut queue = edit_queue.lock().unwrap();
queue.push(serde_json::to_string(&edits.template_mutations).unwrap());
queue.push(serde_json::to_string(&edits.edits).unwrap());
proxy.send_event(UserWindowEvent::Update).unwrap();
proxy.send_event(UserWindowEvent::EditsReady).unwrap();
}
loop {
@ -67,7 +67,7 @@ impl DesktopController {
let name = value.event.clone();
let el_id = ElementId(value.mounted_dom_id);
if let Some(evt) = decode_event(value) {
dom.handle_event(&name, evt, el_id, true);
dom.handle_event(&name, evt, el_id, dioxus_html::events::event_bubbles(&name));
}
}
}
@ -81,7 +81,7 @@ impl DesktopController {
let mut queue = edit_queue.lock().unwrap();
queue.push(serde_json::to_string(&muts.template_mutations).unwrap());
queue.push(serde_json::to_string(&muts.edits).unwrap());
let _ = proxy.send_event(UserWindowEvent::Update);
let _ = proxy.send_event(UserWindowEvent::EditsReady);
}
}
})
@ -116,6 +116,8 @@ impl DesktopController {
let (_id, view) = self.webviews.iter_mut().next().unwrap();
println!("processing pending edits {:?}", new_queue.len());
for edit in new_queue.drain(..) {
view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit))
.unwrap();

View file

@ -161,7 +161,8 @@ impl DesktopContext {
#[derive(Debug)]
pub enum UserWindowEvent {
Update,
EditsReady,
Initialize,
CloseWindow,
DragWindow,
@ -213,7 +214,7 @@ pub(super) fn handler(
println!("user_event: {:?}", user_event);
match user_event {
Update => desktop.try_load_ready_webviews(),
Initialize | EditsReady => desktop.try_load_ready_webviews(),
CloseWindow => *control_flow = ControlFlow::Exit,
DragWindow => {
// if the drag_window has any errors, we don't do anything

View file

@ -154,7 +154,7 @@ pub fn launch_with_props<P: 'static + Send>(root: Component<P>, props: P, mut cf
"initialize" => {
is_ready.store(true, std::sync::atomic::Ordering::Relaxed);
println!("initializing...");
let _ = proxy.send_event(UserWindowEvent::Update);
let _ = proxy.send_event(UserWindowEvent::EditsReady);
}
"browser_open" => {
let data = message.params();

View file

@ -60,90 +60,90 @@ pub use touch::*;
pub use transition::*;
pub use wheel::*;
// pub fn event_bubbles(evt: &str) -> bool {
// match evt {
// "copy" => true,
// "cut" => true,
// "paste" => true,
// "compositionend" => true,
// "compositionstart" => true,
// "compositionupdate" => true,
// "keydown" => true,
// "keypress" => true,
// "keyup" => true,
// "focus" => false,
// "focusout" => true,
// "focusin" => true,
// "blur" => false,
// "change" => true,
// "input" => true,
// "invalid" => true,
// "reset" => true,
// "submit" => true,
// "click" => true,
// "contextmenu" => true,
// "doubleclick" => true,
// "dblclick" => true,
// "drag" => true,
// "dragend" => true,
// "dragenter" => false,
// "dragexit" => false,
// "dragleave" => true,
// "dragover" => true,
// "dragstart" => true,
// "drop" => true,
// "mousedown" => true,
// "mouseenter" => false,
// "mouseleave" => false,
// "mousemove" => true,
// "mouseout" => true,
// "scroll" => false,
// "mouseover" => true,
// "mouseup" => true,
// "pointerdown" => true,
// "pointermove" => true,
// "pointerup" => true,
// "pointercancel" => true,
// "gotpointercapture" => true,
// "lostpointercapture" => true,
// "pointerenter" => false,
// "pointerleave" => false,
// "pointerover" => true,
// "pointerout" => true,
// "select" => true,
// "touchcancel" => true,
// "touchend" => true,
// "touchmove" => true,
// "touchstart" => true,
// "wheel" => true,
// "abort" => false,
// "canplay" => false,
// "canplaythrough" => false,
// "durationchange" => false,
// "emptied" => false,
// "encrypted" => true,
// "ended" => false,
// "error" => false,
// "loadeddata" => false,
// "loadedmetadata" => false,
// "loadstart" => false,
// "pause" => false,
// "play" => false,
// "playing" => false,
// "progress" => false,
// "ratechange" => false,
// "seeked" => false,
// "seeking" => false,
// "stalled" => false,
// "suspend" => false,
// "timeupdate" => false,
// "volumechange" => false,
// "waiting" => false,
// "animationstart" => true,
// "animationend" => true,
// "animationiteration" => true,
// "transitionend" => true,
// "toggle" => true,
// _ => panic!("unsupported event type {:?}", evt),
// }
// }
pub fn event_bubbles(evt: &str) -> bool {
match evt {
"copy" => true,
"cut" => true,
"paste" => true,
"compositionend" => true,
"compositionstart" => true,
"compositionupdate" => true,
"keydown" => true,
"keypress" => true,
"keyup" => true,
"focus" => false,
"focusout" => true,
"focusin" => true,
"blur" => false,
"change" => true,
"input" => true,
"invalid" => true,
"reset" => true,
"submit" => true,
"click" => true,
"contextmenu" => true,
"doubleclick" => true,
"dblclick" => true,
"drag" => true,
"dragend" => true,
"dragenter" => false,
"dragexit" => false,
"dragleave" => true,
"dragover" => true,
"dragstart" => true,
"drop" => true,
"mousedown" => true,
"mouseenter" => false,
"mouseleave" => false,
"mousemove" => true,
"mouseout" => true,
"scroll" => false,
"mouseover" => true,
"mouseup" => true,
"pointerdown" => true,
"pointermove" => true,
"pointerup" => true,
"pointercancel" => true,
"gotpointercapture" => true,
"lostpointercapture" => true,
"pointerenter" => false,
"pointerleave" => false,
"pointerover" => true,
"pointerout" => true,
"select" => true,
"touchcancel" => true,
"touchend" => true,
"touchmove" => true,
"touchstart" => true,
"wheel" => true,
"abort" => false,
"canplay" => false,
"canplaythrough" => false,
"durationchange" => false,
"emptied" => false,
"encrypted" => true,
"ended" => false,
"error" => false,
"loadeddata" => false,
"loadedmetadata" => false,
"loadstart" => false,
"pause" => false,
"play" => false,
"playing" => false,
"progress" => false,
"ratechange" => false,
"seeked" => false,
"seeking" => false,
"stalled" => false,
"suspend" => false,
"timeupdate" => false,
"volumechange" => false,
"waiting" => false,
"animationstart" => true,
"animationend" => true,
"animationiteration" => true,
"transitionend" => true,
"toggle" => true,
_ => true,
}
}

View file

@ -18,8 +18,8 @@ pub mod events;
pub mod geometry;
mod global_attributes;
pub mod input_data;
// #[cfg(feature = "wasm-bind")]
// mod web_sys_bind;
#[cfg(feature = "wasm-bind")]
mod web_sys_bind;
pub use elements::*;
pub use events::*;

View file

@ -1,9 +1,9 @@
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
use crate::on::{
use crate::events::{
AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData,
TransitionData, WheelData,
};
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
use keyboard_types::{Code, Key, Modifiers};
use std::convert::TryInto;
use std::str::FromStr;

View file

@ -2,7 +2,7 @@
use js_sys::Function;
use wasm_bindgen::prelude::*;
use web_sys::{Element, Node};
use web_sys::Element;
#[wasm_bindgen(module = "/src/interpreter.js")]
extern "C" {
@ -12,86 +12,83 @@ extern "C" {
pub fn new(arg: Element) -> Interpreter;
#[wasm_bindgen(method)]
pub fn SetNode(this: &Interpreter, id: usize, node: Node);
pub fn AppendChildren(this: &Interpreter, many: u32);
#[wasm_bindgen(method)]
pub fn AppendChildren(this: &Interpreter, root: Option<u64>, children: Vec<u64>);
pub fn AssignId(this: &Interpreter, path: &[u8], id: u32);
#[wasm_bindgen(method)]
pub fn ReplaceWith(this: &Interpreter, root: Option<u64>, nodes: Vec<u64>);
pub fn CreateElement(this: &Interpreter, tag: &str);
#[wasm_bindgen(method)]
pub fn InsertAfter(this: &Interpreter, root: Option<u64>, nodes: Vec<u64>);
pub fn CreateElementNs(this: &Interpreter, tag: &str, ns: &str);
#[wasm_bindgen(method)]
pub fn InsertBefore(this: &Interpreter, root: Option<u64>, nodes: Vec<u64>);
pub fn CreatePlaceholder(this: &Interpreter, id: u32);
#[wasm_bindgen(method)]
pub fn Remove(this: &Interpreter, root: Option<u64>);
pub fn CreateStaticPlaceholder(this: &Interpreter);
#[wasm_bindgen(method)]
pub fn CreateTextNode(this: &Interpreter, text: JsValue, root: Option<u64>);
pub fn CreateTextPlaceholder(this: &Interpreter);
#[wasm_bindgen(method)]
pub fn CreateElement(this: &Interpreter, tag: &str, root: Option<u64>, children: u32);
pub fn CreateStaticText(this: &Interpreter, value: &str);
#[wasm_bindgen(method)]
pub fn CreateElementNs(
this: &Interpreter,
tag: &str,
root: Option<u64>,
ns: &str,
children: u32,
);
pub fn CreateTextNode(this: &Interpreter, value: JsValue, id: u32);
#[wasm_bindgen(method)]
pub fn CreatePlaceholder(this: &Interpreter, root: Option<u64>);
pub fn HydrateText(this: &Interpreter, path: &[u8], value: &str, id: u32);
#[wasm_bindgen(method)]
pub fn LoadTemplate(this: &Interpreter, name: &str, index: u32, id: u32);
#[wasm_bindgen(method)]
pub fn ReplaceWith(this: &Interpreter, id: u32, m: u32);
#[wasm_bindgen(method)]
pub fn ReplacePlaceholder(this: &Interpreter, path: &[u8], m: u32);
#[wasm_bindgen(method)]
pub fn InsertAfter(this: &Interpreter, id: u32, n: u32);
#[wasm_bindgen(method)]
pub fn InsertBefore(this: &Interpreter, id: u32, n: u32);
#[wasm_bindgen(method)]
pub fn SaveTemplate(this: &Interpreter, name: &str, m: u32);
#[wasm_bindgen(method)]
pub fn SetAttribute(this: &Interpreter, id: u32, name: &str, value: JsValue, ns: Option<&str>);
#[wasm_bindgen(method)]
pub fn SetStaticAttribute(this: &Interpreter, name: &str, value: JsValue, ns: Option<&str>);
#[wasm_bindgen(method)]
pub fn SetBoolAttribute(this: &Interpreter, id: u32, name: &str, value: bool);
#[wasm_bindgen(method)]
pub fn SetText(this: &Interpreter, id: u32, text: JsValue);
#[wasm_bindgen(method)]
pub fn NewEventListener(
this: &Interpreter,
name: &str,
root: Option<u64>,
id: u32,
handler: &Function,
bubbles: bool,
);
#[wasm_bindgen(method)]
pub fn RemoveEventListener(this: &Interpreter, root: Option<u64>, name: &str, bubbles: bool);
pub fn RemoveEventListener(this: &Interpreter, name: &str, id: u32);
#[wasm_bindgen(method)]
pub fn SetText(this: &Interpreter, root: Option<u64>, text: JsValue);
pub fn RemoveAttribute(this: &Interpreter, id: u32, field: &str, ns: Option<&str>);
#[wasm_bindgen(method)]
pub fn SetAttribute(
this: &Interpreter,
root: Option<u64>,
field: &str,
value: JsValue,
ns: Option<&str>,
);
pub fn Remove(this: &Interpreter, id: u32);
#[wasm_bindgen(method)]
pub fn RemoveAttribute(this: &Interpreter, root: Option<u64>, field: &str, ns: Option<&str>);
#[wasm_bindgen(method)]
pub fn CloneNode(this: &Interpreter, root: Option<u64>, new_id: u64);
#[wasm_bindgen(method)]
pub fn CloneNodeChildren(this: &Interpreter, root: Option<u64>, new_ids: Vec<u64>);
#[wasm_bindgen(method)]
pub fn FirstChild(this: &Interpreter);
#[wasm_bindgen(method)]
pub fn NextSibling(this: &Interpreter);
#[wasm_bindgen(method)]
pub fn ParentNode(this: &Interpreter);
#[wasm_bindgen(method)]
pub fn StoreWithId(this: &Interpreter, id: u64);
#[wasm_bindgen(method)]
pub fn SetLastNode(this: &Interpreter, id: u64);
pub fn PushRoot(this: &Interpreter, id: u32);
}

View file

@ -265,7 +265,14 @@ export class Interpreter {
SaveTemplate(name, m) {
this.templates[name] = this.stack.splice(this.stack.length - m);
}
CreateStaticText(text) {
this.CreateTextNode(text);
}
CreateTextPlaceholder() {
this.CreateRawText("placeholder");
}
handleEdit(edit) {
console.log(edit);
switch (edit.type) {
case "AppendChildren":
this.AppendChildren(edit.m);
@ -283,13 +290,13 @@ export class Interpreter {
this.CreatePlaceholder(edit.id);
break;
case "CreateStaticText":
this.CreateTextNode(edit.value);
this.CreateStaticText(edit.value)
break;
case "CreateStaticPlaceholder":
this.CreateStaticPlaceholder();
break;
case "CreateTextPlaceholder":
this.CreateRawText("placeholder");
this.CreateTextPlaceholder();
break;
case "CreateStaticText":
this.CreateRawText(edit.value);
@ -301,7 +308,7 @@ export class Interpreter {
this.HydrateText(edit.path, edit.value, edit.id);
break;
case "LoadTemplate":
this.LoadTemplate(edit.name, edit.index);
this.LoadTemplate(edit.name, edit.index, edit.id);
break;
case "SaveTemplate":
this.SaveTemplate(edit.name, edit.m);

View file

@ -77,7 +77,7 @@ features = [
]
[features]
default = ["panic_hook", "hydrate"]
default = ["panic_hook"]
panic_hook = ["console_error_panic_hook"]
hydrate = []

View file

@ -7,11 +7,12 @@
//! - tests to ensure dyn_into works for various event types.
//! - Partial delegation?>
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
use dioxus_html::event_bubbles;
use dioxus_core::{ElementId, Mutation, Mutations};
use dioxus_html::{event_bubbles, CompositionData, FormData};
use dioxus_interpreter_js::Interpreter;
use futures_channel::mpsc;
use js_sys::Function;
use std::{any::Any, rc::Rc, sync::Arc};
use std::{any::Any, rc::Rc};
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use web_sys::{Document, Element, Event, HtmlElement};
@ -26,68 +27,26 @@ pub struct WebsysDom {
}
impl WebsysDom {
pub fn new(cfg: Config, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
pub fn new(cfg: Config, event_channel: mpsc::UnboundedSender<Event>) -> Self {
// eventually, we just want to let the interpreter do all the work of decoding events into our event type
let callback: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
let mut target = event
.target()
.expect("missing target")
.dyn_into::<Element>()
.expect("not a valid element");
_ = event_channel.unbounded_send(event.clone());
let typ = event.type_();
// if let Ok(synthetic_event) = decoded {
// // Try to prevent default if the attribute is set
// if let Some(node) = target.dyn_ref::<HtmlElement>() {
// if let Some(name) = node.get_attribute("dioxus-prevent-default") {
// if name == synthetic_event.name
// || name.trim_start_matches("on") == synthetic_event.name
// {
// log::trace!("Preventing default");
// event.prevent_default();
// }
// }
// }
let decoded: anyhow::Result<UserEvent> = loop {
match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
Some(Ok(id)) => {
break Ok(UserEvent {
name: event_name_from_typ(&typ),
data: virtual_event_from_websys_event(event.clone(), target.clone()),
element: Some(ElementId(id)),
scope_id: None,
priority: dioxus_core::EventPriority::Medium,
bubbles: event.bubbles(),
});
}
Some(Err(e)) => {
break Err(e.into());
}
None => {
// walk the tree upwards until we actually find an event target
if let Some(parent) = target.parent_element() {
target = parent;
} else {
break Ok(UserEvent {
name: event_name_from_typ(&typ),
data: virtual_event_from_websys_event(
event.clone(),
target.clone(),
),
element: None,
scope_id: None,
priority: dioxus_core::EventPriority::Low,
bubbles: event.bubbles(),
});
}
}
}
};
if let Ok(synthetic_event) = decoded {
// Try to prevent default if the attribute is set
if let Some(node) = target.dyn_ref::<HtmlElement>() {
if let Some(name) = node.get_attribute("dioxus-prevent-default") {
if name == synthetic_event.name
|| name.trim_start_matches("on") == synthetic_event.name
{
log::trace!("Preventing default");
event.prevent_default();
}
}
}
sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event))
}
// sender_callback.as_ref()(SchedulerMsg::Event(synthetic_event))
// }
});
// a match here in order to avoid some error during runtime browser test
@ -104,75 +63,56 @@ impl WebsysDom {
}
}
pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
pub fn apply_edits(&mut self, mut edits: Vec<Mutation>) {
use Mutation::*;
let i = &self.interpreter;
for edit in edits.drain(..) {
match edit {
DomEdit::AppendChildren { root, children } => {
self.interpreter.AppendChildren(root, children);
}
DomEdit::ReplaceWith { root, nodes } => self.interpreter.ReplaceWith(root, nodes),
DomEdit::InsertAfter { root, nodes } => self.interpreter.InsertAfter(root, nodes),
DomEdit::InsertBefore { root, nodes } => self.interpreter.InsertBefore(root, nodes),
DomEdit::Remove { root } => self.interpreter.Remove(root),
DomEdit::CreateElement {
root,
tag,
children,
} => self.interpreter.CreateElement(tag, root, children),
DomEdit::CreateElementNs {
root,
tag,
AppendChildren { m } => i.AppendChildren(m as u32),
AssignId { path, id } => i.AssignId(path, id.0 as u32),
CreateElement { name } => i.CreateElement(name),
CreateElementNamespace { name, namespace } => i.CreateElementNs(name, namespace),
CreatePlaceholder { id } => i.CreatePlaceholder(id.0 as u32),
CreateStaticPlaceholder => i.CreateStaticPlaceholder(),
CreateTextPlaceholder => i.CreateTextPlaceholder(),
CreateStaticText { value } => i.CreateStaticText(value),
CreateTextNode { value, id } => i.CreateTextNode(value.into(), id.0 as u32),
HydrateText { path, value, id } => i.HydrateText(path, value, id.0 as u32),
LoadTemplate { name, index, id } => i.LoadTemplate(name, index as u32, id.0 as u32),
ReplaceWith { id, m } => i.ReplaceWith(id.0 as u32, m as u32),
ReplacePlaceholder { path, m } => i.ReplacePlaceholder(path, m as u32),
InsertAfter { id, m } => i.InsertAfter(id.0 as u32, m as u32),
InsertBefore { id, m } => i.InsertBefore(id.0 as u32, m as u32),
SaveTemplate { name, m } => i.SaveTemplate(name, m as u32),
SetAttribute {
name,
value,
id,
ns,
children,
} => self.interpreter.CreateElementNs(tag, root, ns, children),
DomEdit::CreatePlaceholder { root } => self.interpreter.CreatePlaceholder(root),
DomEdit::NewEventListener {
event_name, root, ..
} => {
} => i.SetAttribute(id.0 as u32, name, value.into(), ns),
SetStaticAttribute { name, value, ns } => {
i.SetStaticAttribute(name, value.into(), ns)
}
SetBoolAttribute { name, value, id } => {
i.SetBoolAttribute(id.0 as u32, name, value)
}
SetText { value, id } => i.SetText(id.0 as u32, value.into()),
NewEventListener { name, scope, id } => {
let handler: &Function = self.handler.as_ref().unchecked_ref();
self.interpreter.NewEventListener(
event_name,
root,
name,
id.0 as u32,
handler,
event_bubbles(event_name),
event_bubbles(&name[2..]),
);
}
DomEdit::RemoveEventListener { root, event } => self
.interpreter
.RemoveEventListener(root, event, event_bubbles(event)),
DomEdit::RemoveAttribute { root, name, ns } => {
self.interpreter.RemoveAttribute(root, name, ns)
}
DomEdit::CreateTextNode { text, root } => {
let text = JsValue::from_str(text);
self.interpreter.CreateTextNode(text, root)
}
DomEdit::SetText { root, text } => {
let text = JsValue::from_str(text);
self.interpreter.SetText(root, text)
}
DomEdit::SetAttribute {
root,
field,
value,
ns,
} => {
let value = JsValue::from_str(&value.to_string());
self.interpreter.SetAttribute(root, field, value, ns)
}
DomEdit::CloneNode { id, new_id } => self.interpreter.CloneNode(id, new_id),
DomEdit::CloneNodeChildren { id, new_ids } => {
self.interpreter.CloneNodeChildren(id, new_ids)
}
DomEdit::FirstChild {} => self.interpreter.FirstChild(),
DomEdit::NextSibling {} => self.interpreter.NextSibling(),
DomEdit::ParentNode {} => self.interpreter.ParentNode(),
DomEdit::StoreWithId { id } => self.interpreter.StoreWithId(id),
DomEdit::SetLastNode { id } => self.interpreter.SetLastNode(id),
RemoveEventListener { name, id } => i.RemoveEventListener(name, id.0 as u32),
Remove { id } => i.Remove(id.0 as u32),
PushRoot { id } => i.PushRoot(id.0 as u32),
// Mutation::RemoveEventListener { root, name: event } => self
// .interpreter
// .RemoveEventListener(root, event, event_bubbles(event)),
}
}
}
@ -187,129 +127,54 @@ unsafe impl Sync for DioxusWebsysEvent {}
// todo: some of these events are being casted to the wrong event type.
// We need tests that simulate clicks/etc and make sure every event type works.
fn virtual_event_from_websys_event(
event: web_sys::Event,
target: Element,
) -> Arc<dyn Any + Send + Sync> {
use dioxus_html::on::*;
pub fn virtual_event_from_websys_event(event: web_sys::Event, target: Element) -> Rc<dyn Any> {
use dioxus_html::events::*;
match event.type_().as_str() {
"copy" | "cut" | "paste" => Arc::new(ClipboardData {}),
"copy" | "cut" | "paste" => Rc::new(ClipboardData {}),
"compositionend" | "compositionstart" | "compositionupdate" => {
let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap();
Arc::new(CompositionData {
data: evt.data().unwrap_or_default(),
})
make_composition_event(&event)
}
"keydown" | "keypress" | "keyup" => Arc::new(KeyboardData::from(event)),
"focus" | "blur" | "focusout" | "focusin" => Arc::new(FocusData {}),
"keydown" | "keypress" | "keyup" => Rc::new(KeyboardData::from(event)),
"focus" | "blur" | "focusout" | "focusin" => Rc::new(FocusData {}),
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
// don't have a good solution with the serialized event problem
"change" | "input" | "invalid" | "reset" | "submit" => {
let value: String = target
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| {
// todo: special case more input types
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => "true".to_string(),
false => "false".to_string(),
}
},
_ => {
input.value()
}
}
})
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
})
// select elements are NOT input events - because - why woudn't they be??
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlSelectElement| input.value())
})
.or_else(|| {
target
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.text_content()
})
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
"change" | "input" | "invalid" | "reset" | "submit" => read_input_to_data(target),
let mut values = std::collections::HashMap::new();
// try to fill in form values
if let Some(form) = target.dyn_ref::<web_sys::HtmlFormElement>() {
let elements = form.elements();
for x in 0..elements.length() {
let element = elements.item(x).unwrap();
if let Some(name) = element.get_attribute("name") {
let value: Option<String> = element
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| {
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => Some("true".to_string()),
false => Some("false".to_string()),
}
},
"radio" => {
match input.checked() {
true => Some(input.value()),
false => None,
}
}
_ => Some(input.value())
}
})
.or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value())))
.or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value())))
.or_else(|| Some(element.dyn_ref::<web_sys::HtmlElement>().unwrap().text_content()))
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
if let Some(value) = value {
values.insert(name, value);
}
}
}
}
Arc::new(FormData { value, values })
}
"click" | "contextmenu" | "dblclick" | "doubleclick" | "drag" | "dragend" | "dragenter"
| "dragexit" | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown"
| "mouseenter" | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
Arc::new(MouseData::from(event))
Rc::new(MouseData::from(event))
}
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
Arc::new(PointerData::from(event))
Rc::new(PointerData::from(event))
}
"select" => Arc::new(SelectionData {}),
"touchcancel" | "touchend" | "touchmove" | "touchstart" => Arc::new(TouchData::from(event)),
"select" => Rc::new(SelectionData {}),
"touchcancel" | "touchend" | "touchmove" | "touchstart" => Rc::new(TouchData::from(event)),
"scroll" => Arc::new(()),
"wheel" => Arc::new(WheelData::from(event)),
"scroll" => Rc::new(()),
"wheel" => Rc::new(WheelData::from(event)),
"animationstart" | "animationend" | "animationiteration" => {
Arc::new(AnimationData::from(event))
Rc::new(AnimationData::from(event))
}
"transitionend" => Arc::new(TransitionData::from(event)),
"transitionend" => Rc::new(TransitionData::from(event)),
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}),
"toggle" => Arc::new(ToggleData {}),
| "timeupdate" | "volumechange" | "waiting" => Rc::new(MediaData {}),
"toggle" => Rc::new(ToggleData {}),
_ => Arc::new(()),
_ => Rc::new(()),
}
}
fn make_composition_event(event: &Event) -> Rc<CompositionData> {
let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap();
Rc::new(CompositionData {
data: evt.data().unwrap_or_default(),
})
}
pub(crate) fn load_document() -> Document {
web_sys::window()
.expect("should have access to the Window")
@ -317,92 +182,86 @@ pub(crate) fn load_document() -> Document {
.expect("should have access to the Document")
}
fn event_name_from_typ(typ: &str) -> &'static str {
match typ {
"copy" => "copy",
"cut" => "cut",
"paste" => "paste",
"compositionend" => "compositionend",
"compositionstart" => "compositionstart",
"compositionupdate" => "compositionupdate",
"keydown" => "keydown",
"keypress" => "keypress",
"keyup" => "keyup",
"focus" => "focus",
"focusout" => "focusout",
"focusin" => "focusin",
"blur" => "blur",
"change" => "change",
"input" => "input",
"invalid" => "invalid",
"reset" => "reset",
"submit" => "submit",
"click" => "click",
"contextmenu" => "contextmenu",
"doubleclick" => "doubleclick",
"dblclick" => "dblclick",
"drag" => "drag",
"dragend" => "dragend",
"dragenter" => "dragenter",
"dragexit" => "dragexit",
"dragleave" => "dragleave",
"dragover" => "dragover",
"dragstart" => "dragstart",
"drop" => "drop",
"mousedown" => "mousedown",
"mouseenter" => "mouseenter",
"mouseleave" => "mouseleave",
"mousemove" => "mousemove",
"mouseout" => "mouseout",
"mouseover" => "mouseover",
"mouseup" => "mouseup",
"pointerdown" => "pointerdown",
"pointermove" => "pointermove",
"pointerup" => "pointerup",
"pointercancel" => "pointercancel",
"gotpointercapture" => "gotpointercapture",
"lostpointercapture" => "lostpointercapture",
"pointerenter" => "pointerenter",
"pointerleave" => "pointerleave",
"pointerover" => "pointerover",
"pointerout" => "pointerout",
"select" => "select",
"touchcancel" => "touchcancel",
"touchend" => "touchend",
"touchmove" => "touchmove",
"touchstart" => "touchstart",
"scroll" => "scroll",
"wheel" => "wheel",
"animationstart" => "animationstart",
"animationend" => "animationend",
"animationiteration" => "animationiteration",
"transitionend" => "transitionend",
"abort" => "abort",
"canplay" => "canplay",
"canplaythrough" => "canplaythrough",
"durationchange" => "durationchange",
"emptied" => "emptied",
"encrypted" => "encrypted",
"ended" => "ended",
"error" => "error",
"loadeddata" => "loadeddata",
"loadedmetadata" => "loadedmetadata",
"loadstart" => "loadstart",
"pause" => "pause",
"play" => "play",
"playing" => "playing",
"progress" => "progress",
"ratechange" => "ratechange",
"seeked" => "seeked",
"seeking" => "seeking",
"stalled" => "stalled",
"suspend" => "suspend",
"timeupdate" => "timeupdate",
"volumechange" => "volumechange",
"waiting" => "waiting",
"toggle" => "toggle",
a => {
panic!("unsupported event type {:?}", a);
fn read_input_to_data(target: Element) -> Rc<FormData> {
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
// don't have a good solution with the serialized event problem
let value: String = target
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| {
// todo: special case more input types
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => "true".to_string(),
false => "false".to_string(),
}
},
_ => {
input.value()
}
}
})
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
})
// select elements are NOT input events - because - why woudn't they be??
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlSelectElement| input.value())
})
.or_else(|| {
target
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.text_content()
})
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
let mut values = std::collections::HashMap::new();
// try to fill in form values
if let Some(form) = target.dyn_ref::<web_sys::HtmlFormElement>() {
let elements = form.elements();
for x in 0..elements.length() {
let element = elements.item(x).unwrap();
if let Some(name) = element.get_attribute("name") {
let value: Option<String> = element
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| {
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => Some("true".to_string()),
false => Some("false".to_string()),
}
},
"radio" => {
match input.checked() {
true => Some(input.value()),
false => None,
}
}
_ => Some(input.value())
}
})
.or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlTextAreaElement| Some(input.value())))
.or_else(|| element.dyn_ref().map(|input: &web_sys::HtmlSelectElement| Some(input.value())))
.or_else(|| Some(element.dyn_ref::<web_sys::HtmlElement>().unwrap().text_content()))
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
if let Some(value) = value {
values.insert(name, value);
}
}
}
}
Rc::new(FormData {
value,
values,
files: None,
})
}

View file

@ -54,21 +54,23 @@
// - Do the VDOM work during the idlecallback
// - Do DOM work in the next requestAnimationFrame callback
use std::rc::Rc;
use std::{rc::Rc, time::Duration};
pub use crate::cfg::Config;
use crate::dom::virtual_event_from_websys_event;
pub use crate::util::use_eval;
use dioxus_core::prelude::Component;
use dioxus_core::SchedulerMsg;
use dioxus_core::VirtualDom;
use dioxus_core::{Element, ElementId, Scope, VirtualDom};
use futures_util::{pin_mut, FutureExt, StreamExt};
use gloo_timers::future::sleep;
use web_sys::Event;
mod cache;
mod cfg;
mod dom;
#[cfg(any(feature = "hot-reload", debug_assertions))]
mod hot_reload;
#[cfg(feature = "hydrate")]
mod rehydrate;
// #[cfg(any(feature = "hot-reload", debug_assertions))]
// mod hot_reload;
// #[cfg(feature = "hydrate")]
// mod rehydrate;
// mod ric_raf;
mod util;
@ -92,7 +94,7 @@ mod util;
/// render!(div {"hello world"})
/// }
/// ```
pub fn launch(root_component: Component) {
pub fn launch(root_component: fn(Scope) -> Element) {
launch_with_props(root_component, (), Config::default());
}
@ -115,7 +117,7 @@ pub fn launch(root_component: Component) {
/// })
/// }
/// ```
pub fn launch_cfg(root: Component, config: Config) {
pub fn launch_cfg(root: fn(Scope) -> Element, config: Config) {
launch_with_props(root, (), config)
}
@ -143,10 +145,11 @@ pub fn launch_cfg(root: Component, config: Config) {
/// render!(div {"hello {cx.props.name}"})
/// }
/// ```
pub fn launch_with_props<T>(root_component: Component<T>, root_properties: T, config: Config)
where
T: Send + 'static,
{
pub fn launch_with_props<T: 'static>(
root_component: fn(Scope<T>) -> Element,
root_properties: T,
config: Config,
) {
wasm_bindgen_futures::spawn_local(run_with_props(root_component, root_properties, config));
}
@ -162,7 +165,9 @@ where
/// wasm_bindgen_futures::spawn_local(app_fut);
/// }
/// ```
pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T, cfg: Config) {
pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_props: T, cfg: Config) {
log::info!("Starting up");
let mut dom = VirtualDom::new_with_props(root, root_props);
#[cfg(feature = "panic_hook")]
@ -170,8 +175,8 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
console_error_panic_hook::set_once();
}
#[cfg(any(feature = "hot-reload", debug_assertions))]
hot_reload::init(&dom);
// #[cfg(any(feature = "hot-reload", debug_assertions))]
// hot_reload::init(&dom);
for s in crate::cache::BUILTIN_INTERNED_STRINGS {
wasm_bindgen::intern(s);
@ -180,50 +185,48 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
wasm_bindgen::intern(s);
}
let tasks = dom.get_scheduler_channel();
// a let should_hydrate = cfg.hydrate;
let sender_callback: Rc<dyn Fn(SchedulerMsg)> =
Rc::new(move |event| tasks.unbounded_send(event).unwrap());
let (tx, mut rx) = futures_channel::mpsc::unbounded();
let should_hydrate = cfg.hydrate;
let mut websys_dom = dom::WebsysDom::new(cfg, tx);
let mut websys_dom = dom::WebsysDom::new(cfg, sender_callback);
log::info!("rebuilding app");
log::trace!("rebuilding app");
if should_hydrate {
// todo: we need to split rebuild and initialize into two phases
// it's a waste to produce edits just to get the vdom loaded
let _ = dom.rebuild();
#[cfg(feature = "hydrate")]
#[allow(unused_variables)]
if let Err(err) = websys_dom.rehydrate(&dom) {
log::error!(
"Rehydration failed {:?}. Rebuild DOM into element from scratch",
&err
);
websys_dom.root.set_text_content(None);
// errrrr we should split rebuild into two phases
// one that initializes things and one that produces edits
let edits = dom.rebuild();
websys_dom.apply_edits(edits.edits);
}
} else {
let edits = dom.rebuild();
websys_dom.apply_edits(edits.edits);
}
// let mut work_loop = ric_raf::RafLoop::new();
let edits = dom.rebuild();
websys_dom.apply_edits(edits.template_mutations);
websys_dom.apply_edits(edits.edits);
loop {
log::trace!("waiting for work");
// if virtualdom has nothing, wait for it to have something before requesting idle time
// if there is work then this future resolves immediately.
dom.wait_for_work().await;
let mut res = {
let work = dom.wait_for_work().fuse();
pin_mut!(work);
futures_util::select! {
_ = work => None,
evt = rx.next() => evt
}
};
while let Some(evt) = res {
let name = evt.type_();
let element = walk_event_for_id(&evt);
let bubbles = dioxus_html::event_bubbles(name.as_str());
if let Some((element, target)) = element {
let data = virtual_event_from_websys_event(evt, target);
dom.handle_event(name.as_str(), data, element, bubbles);
}
res = rx.try_next().transpose().unwrap().ok();
}
let deadline = sleep(Duration::from_millis(50));
let edits = dom.render_with_deadline(deadline).await;
log::trace!("working..");
@ -232,14 +235,85 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
// run the virtualdom work phase until the frame deadline is reached
// let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some());
let mutations = dom.work_with_deadline(|| false);
// wait for the animation frame to fire so we can apply our changes
// work_loop.wait_for_raf().await;
for edit in mutations {
// actually apply our changes during the animation frame
websys_dom.apply_edits(edit.edits);
// for edit in mutations {
// // actually apply our changes during the animation frame
websys_dom.apply_edits(edits.template_mutations);
websys_dom.apply_edits(edits.edits);
// }
}
}
fn walk_event_for_id(event: &Event) -> Option<(ElementId, web_sys::Element)> {
use wasm_bindgen::{closure::Closure, JsCast, JsValue};
use web_sys::{Document, Element, Event, HtmlElement};
let mut target = event
.target()
.expect("missing target")
.dyn_into::<Element>()
.expect("not a valid element");
// break Ok(UserEvent {
// name: event_name_from_typ(&typ),
// data: virtual_event_from_websys_event(event.clone(), target.clone()),
// element: Some(ElementId(id)),
// scope_id: None,
// priority: dioxus_core::EventPriority::Medium,
// bubbles: event.bubbles(),
// });
// break Ok(UserEvent {
// name: event_name_from_typ(&typ),
// data: virtual_event_from_websys_event(event.clone(), target.clone()),
// element: None,
// scope_id: None,
// priority: dioxus_core::EventPriority::Low,
// bubbles: event.bubbles(),
// });
loop {
match target.get_attribute("data-dioxus-id").map(|f| f.parse()) {
Some(Ok(id)) => return Some((ElementId(id), target)),
Some(Err(_)) => return None,
// walk the tree upwards until we actually find an event target
None => match target.parent_element() {
Some(parent) => target = parent,
None => return None,
},
}
}
}
// if should_hydrate {
// // todo: we need to split rebuild and initialize into two phases
// // it's a waste to produce edits just to get the vdom loaded
// let _ = dom.rebuild();
// #[cfg(feature = "hydrate")]
// #[allow(unused_variables)]
// if let Err(err) = websys_dom.rehydrate(&dom) {
// log::error!(
// "Rehydration failed {:?}. Rebuild DOM into element from scratch",
// &err
// );
// websys_dom.root.set_text_content(None);
// // errrrr we should split rebuild into two phases
// // one that initializes things and one that produces edits
// let edits = dom.rebuild();
// websys_dom.apply_edits(edits.edits);
// }
// } else {
// let edits = dom.rebuild();
// websys_dom.apply_edits(edits.template_mutations);
// websys_dom.apply_edits(edits.edits);
// }
// let mut work_loop = ric_raf::RafLoop::new();

View file

@ -1,856 +0,0 @@
//! Implementation of a renderer for Dioxus on the web.
//!
//! Oustanding todos:
//! - Removing event listeners (delegation)
//! - Passive event listeners
//! - no-op event listener patch for safari
//! - tests to ensure dyn_into works for various event types.
//! - Partial delegation?>
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, ScopeId, UserEvent};
use rustc_hash::FxHashMap;
use std::{any::Any, fmt::Debug, rc::Rc, sync::Arc};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
CssStyleDeclaration, Document, Element, Event, HtmlElement, HtmlInputElement,
HtmlOptionElement, HtmlTextAreaElement, Node,
};
use crate::{nodeslab::NodeSlab, Config};
pub struct WebsysDom {
stack: Stack,
/// A map from ElementID (index) to Node
pub(crate) nodes: NodeSlab,
document: Document,
pub(crate) root: Element,
sender_callback: Rc<dyn Fn(SchedulerMsg)>,
// map of listener types to number of those listeners
// This is roughly a delegater
// TODO: check how infero delegates its events - some are more performant
listeners: FxHashMap<&'static str, ListenerEntry>,
}
type ListenerEntry = (usize, Closure<dyn FnMut(&Event)>);
impl WebsysDom {
pub fn new(cfg: Config, sender_callback: Rc<dyn Fn(SchedulerMsg)>) -> Self {
let document = load_document();
let nodes = NodeSlab::new(2000);
let listeners = FxHashMap::default();
let mut stack = Stack::with_capacity(10);
let root = load_document().get_element_by_id(&cfg.rootname).unwrap();
let root_node = root.clone().dyn_into::<Node>().unwrap();
stack.push(root_node);
Self {
stack,
nodes,
listeners,
document,
sender_callback,
root,
}
}
pub fn apply_edits(&mut self, mut edits: Vec<DomEdit>) {
for edit in edits.drain(..) {
match edit {
DomEdit::PushRoot { root } => self.push(root),
DomEdit::AppendChildren { many } => self.append_children(many),
DomEdit::ReplaceWith { m, root } => self.replace_with(m, root),
DomEdit::Remove { root } => self.remove(root),
DomEdit::CreateTextNode { text, root: id } => self.create_text_node(text, id),
DomEdit::CreateElement { tag, root: id } => self.create_element(tag, None, id),
DomEdit::CreateElementNs { tag, root: id, ns } => {
self.create_element(tag, Some(ns), id)
}
DomEdit::CreatePlaceholder { root: id } => self.create_placeholder(id),
DomEdit::NewEventListener {
event_name,
scope,
root: mounted_node_id,
} => self.new_event_listener(event_name, scope, mounted_node_id),
DomEdit::RemoveEventListener { event, root } => {
self.remove_event_listener(event, root)
}
DomEdit::SetText { text, root } => self.set_text(text, root),
DomEdit::SetAttribute {
field,
value,
ns,
root,
} => self.set_attribute(field, value, ns, root),
DomEdit::RemoveAttribute { name, root } => self.remove_attribute(name, root),
DomEdit::InsertAfter { n, root } => self.insert_after(n, root),
DomEdit::InsertBefore { n, root } => self.insert_before(n, root),
}
}
}
fn push(&mut self, root: u64) {
let key = root as usize;
let domnode = &self.nodes[key];
let real_node: Node = match domnode {
Some(n) => n.clone(),
None => todo!(),
};
self.stack.push(real_node);
}
fn append_children(&mut self, many: u32) {
let root: Node = self
.stack
.list
.get(self.stack.list.len() - (1 + many as usize))
.unwrap()
.clone();
// We need to make sure to add comments between text nodes
// We ensure that the text siblings are patched by preventing the browser from merging
// neighboring text nodes. Originally inspired by some of React's work from 2016.
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
// -> https://github.com/facebook/react/pull/5753
/*
todo: we need to track this for replacing/insert after/etc
*/
let mut last_node_was_text = false;
for child in self
.stack
.list
.drain((self.stack.list.len() - many as usize)..)
{
if child.dyn_ref::<web_sys::Text>().is_some() {
if last_node_was_text {
let comment_node = self
.document
.create_comment("dioxus")
.dyn_into::<Node>()
.unwrap();
root.append_child(&comment_node).unwrap();
}
last_node_was_text = true;
} else {
last_node_was_text = false;
}
root.append_child(&child).unwrap();
}
}
fn replace_with(&mut self, m: u32, root: u64) {
let old = self.nodes[root as usize].as_ref().unwrap();
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - m as usize)..)
.collect();
if let Some(el) = old.dyn_ref::<Element>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.replace_with_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
el.replace_with_with_node(&arr).unwrap();
}
}
fn remove(&mut self, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if let Some(element) = node.dyn_ref::<Element>() {
element.remove();
} else {
if let Some(parent) = node.parent_node() {
parent.remove_child(&node).unwrap();
}
}
}
fn create_placeholder(&mut self, id: u64) {
self.create_element("pre", None, id);
self.set_attribute("hidden", "", None, id);
}
fn create_text_node(&mut self, text: &str, id: u64) {
let textnode = self
.document
.create_text_node(text)
.dyn_into::<Node>()
.unwrap();
self.stack.push(textnode.clone());
self.nodes[(id as usize)] = Some(textnode);
}
fn create_element(&mut self, tag: &str, ns: Option<&'static str>, id: u64) {
let tag = wasm_bindgen::intern(tag);
let el = match ns {
Some(ns) => self
.document
.create_element_ns(Some(ns), tag)
.unwrap()
.dyn_into::<Node>()
.unwrap(),
None => self
.document
.create_element(tag)
.unwrap()
.dyn_into::<Node>()
.unwrap(),
};
use smallstr::SmallString;
use std::fmt::Write;
let mut s: SmallString<[u8; 8]> = smallstr::SmallString::new();
write!(s, "{}", id).unwrap();
let el2 = el.dyn_ref::<Element>().unwrap();
el2.set_attribute("dioxus-id", s.as_str()).unwrap();
self.stack.push(el.clone());
self.nodes[(id as usize)] = Some(el);
}
fn new_event_listener(&mut self, event: &'static str, _scope: ScopeId, _real_id: u64) {
let event = wasm_bindgen::intern(event);
// attach the correct attributes to the element
// these will be used by accessing the event's target
// This ensures we only ever have one handler attached to the root, but decide
// dynamically when we want to call a listener.
let el = self.stack.top();
let el = el.dyn_ref::<Element>().unwrap();
el.set_attribute("dioxus-event", event).unwrap();
// Register the callback to decode
if let Some(entry) = self.listeners.get_mut(event) {
entry.0 += 1;
} else {
let trigger = self.sender_callback.clone();
let c: Box<dyn FnMut(&Event)> = Box::new(move |event: &web_sys::Event| {
// "Result" cannot be received from JS
// Instead, we just build and immediately execute a closure that returns result
match decode_trigger(event) {
Ok(synthetic_event) => {
let target = event.target().unwrap();
if let Some(node) = target.dyn_ref::<HtmlElement>() {
if let Some(name) = node.get_attribute("dioxus-prevent-default") {
if name == synthetic_event.name
|| name.trim_start_matches("on") == synthetic_event.name
{
log::trace!("Preventing default");
event.prevent_default();
}
}
}
trigger.as_ref()(SchedulerMsg::Event(synthetic_event))
}
Err(e) => log::error!("Error decoding Dioxus event attribute. {:#?}", e),
};
});
let handler = Closure::wrap(c);
self.root
.add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref())
.unwrap();
// Increment the listeners
self.listeners.insert(event.into(), (1, handler));
}
}
fn remove_event_listener(&mut self, _event: &str, _root: u64) {
todo!()
}
fn set_text(&mut self, text: &str, root: u64) {
let el = self.nodes[root as usize].as_ref().unwrap();
el.set_text_content(Some(text))
}
fn set_attribute(&mut self, name: &str, value: &str, ns: Option<&str>, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if ns == Some("style") {
if let Some(el) = node.dyn_ref::<Element>() {
let el = el.dyn_ref::<HtmlElement>().unwrap();
let style_dc: CssStyleDeclaration = el.style();
style_dc.set_property(name, value).unwrap();
}
} else {
let fallback = || {
let el = node.dyn_ref::<Element>().unwrap();
el.set_attribute(name, value).unwrap()
};
match name {
"dangerous_inner_html" => {
if let Some(el) = node.dyn_ref::<Element>() {
el.set_inner_html(value);
}
}
"value" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
/*
if the attribute being set is the same as the value of the input, then don't bother setting it.
This is used in controlled components to keep the cursor in the right spot.
this logic should be moved into the virtualdom since we have the notion of "volatile"
*/
if input.value() != value {
input.set_value(value);
}
} else if let Some(node) = node.dyn_ref::<HtmlTextAreaElement>() {
if name == "value" {
node.set_value(value);
}
} else {
fallback();
}
}
"checked" => {
if let Some(input) = node.dyn_ref::<HtmlInputElement>() {
match value {
"true" => input.set_checked(true),
"false" => input.set_checked(false),
_ => fallback(),
}
} else {
fallback();
}
}
"selected" => {
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
node.set_selected(true);
} else {
fallback();
}
}
_ => {
// https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
if value == "false" {
if let Some(el) = node.dyn_ref::<Element>() {
match name {
"allowfullscreen"
| "allowpaymentrequest"
| "async"
| "autofocus"
| "autoplay"
| "checked"
| "controls"
| "default"
| "defer"
| "disabled"
| "formnovalidate"
| "hidden"
| "ismap"
| "itemscope"
| "loop"
| "multiple"
| "muted"
| "nomodule"
| "novalidate"
| "open"
| "playsinline"
| "readonly"
| "required"
| "reversed"
| "selected"
| "truespeed" => {
let _ = el.remove_attribute(name);
}
_ => {
let _ = el.set_attribute(name, value);
}
};
}
} else {
fallback();
}
}
}
}
}
fn remove_attribute(&mut self, name: &str, root: u64) {
let node = self.nodes[root as usize].as_ref().unwrap();
if let Some(node) = node.dyn_ref::<web_sys::Element>() {
node.remove_attribute(name).unwrap();
}
if let Some(node) = node.dyn_ref::<HtmlInputElement>() {
// Some attributes are "volatile" and don't work through `removeAttribute`.
if name == "value" {
node.set_value("");
}
if name == "checked" {
node.set_checked(false);
}
}
if let Some(node) = node.dyn_ref::<HtmlOptionElement>() {
if name == "selected" {
node.set_selected(true);
}
}
}
fn insert_after(&mut self, n: u32, root: u64) {
let old = self.nodes[root as usize].as_ref().unwrap();
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - n as usize)..)
.collect();
if let Some(el) = old.dyn_ref::<Element>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::CharacterData>() {
el.after_with_node(&arr).unwrap();
} else if let Some(el) = old.dyn_ref::<web_sys::DocumentType>() {
el.after_with_node(&arr).unwrap();
}
}
fn insert_before(&mut self, n: u32, root: u64) {
let anchor = self.nodes[root as usize].as_ref().unwrap();
if n == 1 {
let before = self.stack.pop();
anchor
.parent_node()
.unwrap()
.insert_before(&before, Some(&anchor))
.unwrap();
} else {
let arr: js_sys::Array = self
.stack
.list
.drain((self.stack.list.len() - n as usize)..)
.collect();
if let Some(el) = anchor.dyn_ref::<Element>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::CharacterData>() {
el.before_with_node(&arr).unwrap();
} else if let Some(el) = anchor.dyn_ref::<web_sys::DocumentType>() {
el.before_with_node(&arr).unwrap();
}
}
}
}
#[derive(Debug, Default)]
struct Stack {
list: Vec<Node>,
}
impl Stack {
#[inline]
fn with_capacity(cap: usize) -> Self {
Stack {
list: Vec::with_capacity(cap),
}
}
#[inline]
fn push(&mut self, node: Node) {
self.list.push(node);
}
#[inline]
fn pop(&mut self) -> Node {
self.list.pop().unwrap()
}
fn top(&self) -> &Node {
match self.list.last() {
Some(a) => a,
None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
}
}
}
pub struct DioxusWebsysEvent(web_sys::Event);
// safety: currently the web is not multithreaded and our VirtualDom exists on the same thread
unsafe impl Send for DioxusWebsysEvent {}
unsafe impl Sync for DioxusWebsysEvent {}
// todo: some of these events are being casted to the wrong event type.
// We need tests that simulate clicks/etc and make sure every event type works.
fn virtual_event_from_websys_event(event: web_sys::Event) -> Arc<dyn Any + Send + Sync> {
use dioxus_html::on::*;
use dioxus_html::KeyCode;
match event.type_().as_str() {
"copy" | "cut" | "paste" => Arc::new(ClipboardData {}),
"compositionend" | "compositionstart" | "compositionupdate" => {
let evt: &web_sys::CompositionEvent = event.dyn_ref().unwrap();
Arc::new(CompositionData {
data: evt.data().unwrap_or_default(),
})
}
"keydown" | "keypress" | "keyup" => {
let evt: &web_sys::KeyboardEvent = event.dyn_ref().unwrap();
Arc::new(KeyboardData {
alt_key: evt.alt_key(),
char_code: evt.char_code(),
key: evt.key(),
key_code: KeyCode::from_raw_code(evt.key_code() as u8),
ctrl_key: evt.ctrl_key(),
location: evt.location() as usize,
meta_key: evt.meta_key(),
repeat: evt.repeat(),
shift_key: evt.shift_key(),
which: evt.which() as usize,
})
}
"focus" | "blur" => Arc::new(FocusData {}),
// todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy
// don't have a good solution with the serialized event problem
"change" | "input" | "invalid" | "reset" | "submit" => {
let evt: &web_sys::Event = event.dyn_ref().unwrap();
let target: web_sys::EventTarget = evt.target().unwrap();
let value: String = (&target)
.dyn_ref()
.map(|input: &web_sys::HtmlInputElement| {
// todo: special case more input types
match input.type_().as_str() {
"checkbox" => {
match input.checked() {
true => "true".to_string(),
false => "false".to_string(),
}
},
_ => {
input.value()
}
}
})
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlTextAreaElement| input.value())
})
// select elements are NOT input events - because - why woudn't they be??
.or_else(|| {
target
.dyn_ref()
.map(|input: &web_sys::HtmlSelectElement| input.value())
})
.or_else(|| {
target
.dyn_ref::<web_sys::HtmlElement>()
.unwrap()
.text_content()
})
.expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener");
Arc::new(FormData { value })
}
"click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
| "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
| "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
let evt: &web_sys::MouseEvent = event.dyn_ref().unwrap();
Arc::new(MouseData {
alt_key: evt.alt_key(),
button: evt.button(),
buttons: evt.buttons(),
client_x: evt.client_x(),
client_y: evt.client_y(),
ctrl_key: evt.ctrl_key(),
meta_key: evt.meta_key(),
screen_x: evt.screen_x(),
screen_y: evt.screen_y(),
shift_key: evt.shift_key(),
page_x: evt.page_x(),
page_y: evt.page_y(),
})
}
"pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
| "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
let evt: &web_sys::PointerEvent = event.dyn_ref().unwrap();
Arc::new(PointerData {
alt_key: evt.alt_key(),
button: evt.button(),
buttons: evt.buttons(),
client_x: evt.client_x(),
client_y: evt.client_y(),
ctrl_key: evt.ctrl_key(),
meta_key: evt.meta_key(),
page_x: evt.page_x(),
page_y: evt.page_y(),
screen_x: evt.screen_x(),
screen_y: evt.screen_y(),
shift_key: evt.shift_key(),
pointer_id: evt.pointer_id(),
width: evt.width(),
height: evt.height(),
pressure: evt.pressure(),
tangential_pressure: evt.tangential_pressure(),
tilt_x: evt.tilt_x(),
tilt_y: evt.tilt_y(),
twist: evt.twist(),
pointer_type: evt.pointer_type(),
is_primary: evt.is_primary(),
// get_modifier_state: evt.get_modifier_state(),
})
}
"select" => Arc::new(SelectionData {}),
"touchcancel" | "touchend" | "touchmove" | "touchstart" => {
let evt: &web_sys::TouchEvent = event.dyn_ref().unwrap();
Arc::new(TouchData {
alt_key: evt.alt_key(),
ctrl_key: evt.ctrl_key(),
meta_key: evt.meta_key(),
shift_key: evt.shift_key(),
})
}
"scroll" => Arc::new(()),
"wheel" => {
let evt: &web_sys::WheelEvent = event.dyn_ref().unwrap();
Arc::new(WheelData {
delta_x: evt.delta_x(),
delta_y: evt.delta_y(),
delta_z: evt.delta_z(),
delta_mode: evt.delta_mode(),
})
}
"animationstart" | "animationend" | "animationiteration" => {
let evt: &web_sys::AnimationEvent = event.dyn_ref().unwrap();
Arc::new(AnimationData {
elapsed_time: evt.elapsed_time(),
animation_name: evt.animation_name(),
pseudo_element: evt.pseudo_element(),
})
}
"transitionend" => {
let evt: &web_sys::TransitionEvent = event.dyn_ref().unwrap();
Arc::new(TransitionData {
elapsed_time: evt.elapsed_time(),
property_name: evt.property_name(),
pseudo_element: evt.pseudo_element(),
})
}
"abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
| "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
| "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
| "timeupdate" | "volumechange" | "waiting" => Arc::new(MediaData {}),
"toggle" => Arc::new(ToggleData {}),
_ => Arc::new(()),
}
}
/// This function decodes a websys event and produces an EventTrigger
/// With the websys implementation, we attach a unique key to the nodes
fn decode_trigger(event: &web_sys::Event) -> anyhow::Result<UserEvent> {
use anyhow::Context;
let target = event
.target()
.expect("missing target")
.dyn_into::<Element>()
.expect("not a valid element");
let typ = event.type_();
let element_id = target
.get_attribute("dioxus-id")
.context("Could not find element id on event target")?
.parse()?;
Ok(UserEvent {
name: event_name_from_typ(&typ),
data: virtual_event_from_websys_event(event.clone()),
element: Some(ElementId(element_id)),
scope_id: None,
priority: dioxus_core::EventPriority::Medium,
})
}
pub(crate) fn load_document() -> Document {
web_sys::window()
.expect("should have access to the Window")
.document()
.expect("should have access to the Document")
}
fn event_name_from_typ(typ: &str) -> &'static str {
match typ {
"copy" => "copy",
"cut" => "cut",
"paste" => "paste",
"compositionend" => "compositionend",
"compositionstart" => "compositionstart",
"compositionupdate" => "compositionupdate",
"keydown" => "keydown",
"keypress" => "keypress",
"keyup" => "keyup",
"focus" => "focus",
"blur" => "blur",
"change" => "change",
"input" => "input",
"invalid" => "invalid",
"reset" => "reset",
"submit" => "submit",
"click" => "click",
"contextmenu" => "contextmenu",
"doubleclick" => "doubleclick",
"drag" => "drag",
"dragend" => "dragend",
"dragenter" => "dragenter",
"dragexit" => "dragexit",
"dragleave" => "dragleave",
"dragover" => "dragover",
"dragstart" => "dragstart",
"drop" => "drop",
"mousedown" => "mousedown",
"mouseenter" => "mouseenter",
"mouseleave" => "mouseleave",
"mousemove" => "mousemove",
"mouseout" => "mouseout",
"mouseover" => "mouseover",
"mouseup" => "mouseup",
"pointerdown" => "pointerdown",
"pointermove" => "pointermove",
"pointerup" => "pointerup",
"pointercancel" => "pointercancel",
"gotpointercapture" => "gotpointercapture",
"lostpointercapture" => "lostpointercapture",
"pointerenter" => "pointerenter",
"pointerleave" => "pointerleave",
"pointerover" => "pointerover",
"pointerout" => "pointerout",
"select" => "select",
"touchcancel" => "touchcancel",
"touchend" => "touchend",
"touchmove" => "touchmove",
"touchstart" => "touchstart",
"scroll" => "scroll",
"wheel" => "wheel",
"animationstart" => "animationstart",
"animationend" => "animationend",
"animationiteration" => "animationiteration",
"transitionend" => "transitionend",
"abort" => "abort",
"canplay" => "canplay",
"canplaythrough" => "canplaythrough",
"durationchange" => "durationchange",
"emptied" => "emptied",
"encrypted" => "encrypted",
"ended" => "ended",
"error" => "error",
"loadeddata" => "loadeddata",
"loadedmetadata" => "loadedmetadata",
"loadstart" => "loadstart",
"pause" => "pause",
"play" => "play",
"playing" => "playing",
"progress" => "progress",
"ratechange" => "ratechange",
"seeked" => "seeked",
"seeking" => "seeking",
"stalled" => "stalled",
"suspend" => "suspend",
"timeupdate" => "timeupdate",
"volumechange" => "volumechange",
"waiting" => "waiting",
"toggle" => "toggle",
_ => {
panic!("unsupported event type")
}
}
}
//! This module provides a mirror of the VirtualDOM Element Slab using a Vector.
use std::ops::{Index, IndexMut};
use web_sys::Node;
pub(crate) struct NodeSlab {
nodes: Vec<Option<Node>>,
}
impl NodeSlab {
pub fn new(capacity: usize) -> NodeSlab {
let nodes = Vec::with_capacity(capacity);
NodeSlab { nodes }
}
}
impl Index<usize> for NodeSlab {
type Output = Option<Node>;
fn index(&self, index: usize) -> &Self::Output {
&self.nodes[index]
}
}
impl IndexMut<usize> for NodeSlab {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
if index >= self.nodes.capacity() * 3 {
panic!("Trying to mutate an element way too far out of bounds");
}
if index + 1 > self.nodes.len() {
self.nodes.resize_with(index + 1, || None);
}
&mut self.nodes[index]
}
}
#[derive(Debug, Default)]
struct Stack {
list: Vec<Node>,
}
impl Stack {
#[inline]
fn with_capacity(cap: usize) -> Self {
Stack {
list: Vec::with_capacity(cap),
}
}
#[inline]
fn push(&mut self, node: Node) {
self.list.push(node);
}
#[inline]
fn pop(&mut self) -> Node {
self.list.pop().unwrap()
}
fn top(&self) -> &Node {
match self.list.last() {
Some(a) => a,
None => panic!("Called 'top' of an empty stack, make sure to push the root first"),
}
}
}

View file

@ -1,6 +1,5 @@
use crate::dom::WebsysDom;
use dioxus_core::{VNode, VirtualDom};
use dioxus_html::event_bubbles;
use wasm_bindgen::JsCast;
use web_sys::{Comment, Element, Node, Text};
@ -30,14 +29,15 @@ impl WebsysDom {
let mut last_node_was_text = false;
// Recursively rehydrate the dom from the VirtualDom
self.rehydrate_single(
&mut nodes,
&mut counter,
dom,
root_node,
&mut last_node_was_text,
)
todo!()
// // Recursively rehydrate the dom from the VirtualDom
// self.rehydrate_single(
// &mut nodes,
// &mut counter,
// dom,
// root_node,
// &mut last_node_was_text,
// )
}
fn rehydrate_single(
@ -164,7 +164,8 @@ impl WebsysDom {
VNode::Component(el) => {
let scope = dom.get_scope(el.scope.get().unwrap()).unwrap();
let node = scope.root_node();
self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?;
todo!()
// self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?;
}
VNode::TemplateRef(_) => todo!(),
}

View file

@ -50,7 +50,7 @@ fn rehydrates() {
let mut dom = VirtualDom::new(app);
let _ = dom.rebuild();
let out = dioxus_ssr::render_vdom_cfg(&dom, |c| c.pre_render(true));
let out = dioxus_ssr::render_vdom_cfg(&dom, Default::default());
window()
.unwrap()