Przeglądaj źródła

wip: add warp liveview proof of concept

Jonathan Kelley 3 lat temu
rodzic
commit
e0900ca256

+ 22 - 7
packages/liveview/Cargo.toml

@@ -9,14 +9,29 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-axum = { version = "0.4.2", optional = true, features = ["ws", "headers"] }
+tokio = { version = "1", features = ["full"] }
+warp = "0.3"
+futures-util = { version = "0.3", default-features = false, features = [
+    "sink",
+] }
+futures-channel = { version = "0.3.17", features = ["sink"] }
+pretty_env_logger = "0.4"
+tokio-stream = { version = "0.1.1", features = ["net"] }
+dioxus-core = { path = "../core", features = ["serialize"] }
+dioxus-html = { path = "../html", features = ["serialize"] }
+serde = { version = "1.0.136", features = ["derive"] }
+serde_json = "1.0.79"
+
+# axum = { version = "0.4.2", optional = true, features = ["ws", "headers"] }
+# serde = { version = "1.0.136", features = ["derive"] }
+# serde_json = "1.0.79"
 
 [features]
-default = ["axum"]
+# default = ["axum"]
 
 [dev-dependencies]
-tokio = { version = "1.14.0", features = ["full"] }
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["env-filter"] }
-tower-http = { version = "0.2.0", features = ["fs", "trace"] }
-headers = "0.3"
+# tokio = { version = "1.14.0", features = ["full"] }
+# tracing = "0.1"
+# tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+# tower-http = { version = "0.2.0", features = ["fs", "trace"] }
+# headers = "0.3"

+ 26 - 0
packages/liveview/cloud/Cargo.toml

@@ -0,0 +1,26 @@
+[package]
+name = "cloud"
+version = "0.0.0"
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+tokio = { version = "1", features = ["full"] }
+warp = "0.3"
+futures-util = { version = "0.3", default-features = false, features = [
+    "sink",
+] }
+futures-channel = { version = "0.3.17", features = ["sink"] }
+pretty_env_logger = "0.4"
+tokio-stream = { version = "0.1.1", features = ["net"] }
+
+dioxus = { git = "https://github.com/dioxuslabs/dioxus" }
+dioxus-html = { git = "https://github.com/dioxuslabs/dioxus", features = [
+    "serialize",
+] }
+dioxus-core = { git = "https://github.com/dioxuslabs/dioxus", features = [
+    "serialize",
+] }
+serde = { version = "1.0.136", features = ["derive"] }
+serde_json = "1.0.79"

+ 213 - 0
packages/liveview/src/events.rs

@@ -0,0 +1,213 @@
+//! Convert a serialized event to an event trigger
+
+use std::any::Any;
+use std::sync::Arc;
+
+use dioxus_core::{ElementId, EventPriority, UserEvent};
+use dioxus_html::on::*;
+
+#[derive(serde::Serialize, serde::Deserialize)]
+pub(crate) struct IpcMessage {
+    pub method: String,
+    pub params: serde_json::Value,
+}
+
+impl IpcMessage {
+    pub(crate) fn method(&self) -> &str {
+        self.method.as_str()
+    }
+
+    pub(crate) fn params(self) -> serde_json::Value {
+        self.params
+    }
+}
+
+pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
+    match serde_json::from_str(payload) {
+        Ok(message) => Some(message),
+        Err(e) => None,
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+struct ImEvent {
+    event: String,
+    mounted_dom_id: u64,
+    contents: serde_json::Value,
+}
+
+pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
+    let ImEvent {
+        event,
+        mounted_dom_id,
+        contents,
+    } = serde_json::from_value(val).unwrap();
+
+    let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
+
+    let name = event_name_from_type(&event);
+    let event = make_synthetic_event(&event, contents);
+
+    UserEvent {
+        name,
+        priority: EventPriority::Low,
+        scope_id: None,
+        element: mounted_dom_id,
+        data: event,
+    }
+}
+
+fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc<dyn Any + Send + Sync> {
+    match name {
+        "copy" | "cut" | "paste" => {
+            //
+            Arc::new(ClipboardData {})
+        }
+        "compositionend" | "compositionstart" | "compositionupdate" => {
+            Arc::new(serde_json::from_value::<CompositionData>(val).unwrap())
+        }
+        "keydown" | "keypress" | "keyup" => {
+            let evt = serde_json::from_value::<KeyboardData>(val).unwrap();
+            Arc::new(evt)
+        }
+        "focus" | "blur" | "focusout" | "focusin" => {
+            //
+            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" => {
+            Arc::new(serde_json::from_value::<FormData>(val).unwrap())
+        }
+
+        "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
+        | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
+        | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => {
+            Arc::new(serde_json::from_value::<MouseData>(val).unwrap())
+        }
+        "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
+        | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
+            Arc::new(serde_json::from_value::<PointerData>(val).unwrap())
+        }
+        "select" => {
+            //
+            Arc::new(serde_json::from_value::<SelectionData>(val).unwrap())
+        }
+
+        "touchcancel" | "touchend" | "touchmove" | "touchstart" => {
+            Arc::new(serde_json::from_value::<TouchData>(val).unwrap())
+        }
+
+        "scroll" => Arc::new(()),
+
+        "wheel" => Arc::new(serde_json::from_value::<WheelData>(val).unwrap()),
+
+        "animationstart" | "animationend" | "animationiteration" => {
+            Arc::new(serde_json::from_value::<AnimationData>(val).unwrap())
+        }
+
+        "transitionend" => Arc::new(serde_json::from_value::<TransitionData>(val).unwrap()),
+
+        "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(()),
+    }
+}
+
+fn event_name_from_type(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",
+        "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")
+        }
+    }
+}

+ 0 - 0
packages/liveview/src/supported/actix_handler.rs → packages/liveview/src/index.html


+ 582 - 0
packages/liveview/src/interpreter.js

@@ -0,0 +1,582 @@
+export function main() {
+  let root = window.document.getElementById("main");
+  if (root != null) {
+    window.interpreter = new Interpreter(root);
+    window.ipc.postMessage(serializeIpcMessage("initialize"));
+  }
+}
+export class Interpreter {
+  constructor(root) {
+    this.root = root;
+    this.stack = [root];
+    this.listeners = {};
+    this.handlers = {};
+    this.lastNodeWasText = false;
+    this.nodes = [root];
+  }
+  top() {
+    return this.stack[this.stack.length - 1];
+  }
+  pop() {
+    return this.stack.pop();
+  }
+  PushRoot(root) {
+    const node = this.nodes[root];
+    this.stack.push(node);
+  }
+  AppendChildren(many) {
+    let root = this.stack[this.stack.length - (1 + many)];
+    let to_add = this.stack.splice(this.stack.length - many);
+    for (let i = 0; i < many; i++) {
+      root.appendChild(to_add[i]);
+    }
+  }
+  ReplaceWith(root_id, m) {
+    let root = this.nodes[root_id];
+    let els = this.stack.splice(this.stack.length - m);
+    root.replaceWith(...els);
+  }
+  InsertAfter(root, n) {
+    let old = this.nodes[root];
+    let new_nodes = this.stack.splice(this.stack.length - n);
+    old.after(...new_nodes);
+  }
+  InsertBefore(root, n) {
+    let old = this.nodes[root];
+    let new_nodes = this.stack.splice(this.stack.length - n);
+    old.before(...new_nodes);
+  }
+  Remove(root) {
+    let node = this.nodes[root];
+    if (node !== undefined) {
+      node.remove();
+    }
+  }
+  CreateTextNode(text, root) {
+    // todo: make it so the types are okay
+    const node = document.createTextNode(text);
+    this.nodes[root] = node;
+    this.stack.push(node);
+  }
+  CreateElement(tag, root) {
+    const el = document.createElement(tag);
+    // el.setAttribute("data-dioxus-id", `${root}`);
+    this.nodes[root] = el;
+    this.stack.push(el);
+  }
+  CreateElementNs(tag, root, ns) {
+    let el = document.createElementNS(ns, tag);
+    this.stack.push(el);
+    this.nodes[root] = el;
+  }
+  CreatePlaceholder(root) {
+    let el = document.createElement("pre");
+    el.hidden = true;
+    this.stack.push(el);
+    this.nodes[root] = el;
+  }
+  NewEventListener(event_name, root, handler) {
+    const element = this.nodes[root];
+    element.setAttribute("data-dioxus-id", `${root}`);
+    if (this.listeners[event_name] === undefined) {
+      this.listeners[event_name] = 0;
+      this.handlers[event_name] = handler;
+      this.root.addEventListener(event_name, handler);
+    } else {
+      this.listeners[event_name]++;
+    }
+  }
+  RemoveEventListener(root, event_name) {
+    const element = this.nodes[root];
+    element.removeAttribute(`data-dioxus-id`);
+    this.listeners[event_name]--;
+    if (this.listeners[event_name] === 0) {
+      this.root.removeEventListener(event_name, this.handlers[event_name]);
+      delete this.listeners[event_name];
+      delete this.handlers[event_name];
+    }
+  }
+  SetText(root, text) {
+    this.nodes[root].textContent = text;
+  }
+  SetAttribute(root, field, value, ns) {
+    const name = field;
+    const node = this.nodes[root];
+    if (ns === "style") {
+      // @ts-ignore
+      node.style[name] = value;
+    } else if (ns != null || ns != undefined) {
+      node.setAttributeNS(ns, name, value);
+    } else {
+      switch (name) {
+        case "value":
+          if (value !== node.value) {
+            node.value = value;
+          }
+          break;
+        case "checked":
+          node.checked = value === "true";
+          break;
+        case "selected":
+          node.selected = value === "true";
+          break;
+        case "dangerous_inner_html":
+          node.innerHTML = value;
+          break;
+        default:
+          // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364
+          if (value === "false" && bool_attrs.hasOwnProperty(name)) {
+            node.removeAttribute(name);
+          } else {
+            node.setAttribute(name, value);
+          }
+      }
+    }
+  }
+  RemoveAttribute(root, name) {
+    const node = this.nodes[root];
+
+    if (name === "value") {
+      node.value = "";
+    } else if (name === "checked") {
+      node.checked = false;
+    } else if (name === "selected") {
+      node.selected = false;
+    } else if (name === "dangerous_inner_html") {
+      node.innerHTML = "";
+    } else {
+      node.removeAttribute(name);
+    }
+  }
+  handleEdits(edits) {
+    this.stack.push(this.root);
+    for (let edit of edits) {
+      this.handleEdit(edit);
+    }
+  }
+  handleEdit(edit) {
+    switch (edit.type) {
+      case "PushRoot":
+        this.PushRoot(edit.root);
+        break;
+      case "AppendChildren":
+        this.AppendChildren(edit.many);
+        break;
+      case "ReplaceWith":
+        this.ReplaceWith(edit.root, edit.m);
+        break;
+      case "InsertAfter":
+        this.InsertAfter(edit.root, edit.n);
+        break;
+      case "InsertBefore":
+        this.InsertBefore(edit.root, edit.n);
+        break;
+      case "Remove":
+        this.Remove(edit.root);
+        break;
+      case "CreateTextNode":
+        this.CreateTextNode(edit.text, edit.root);
+        break;
+      case "CreateElement":
+        this.CreateElement(edit.tag, edit.root);
+        break;
+      case "CreateElementNs":
+        this.CreateElementNs(edit.tag, edit.root, edit.ns);
+        break;
+      case "CreatePlaceholder":
+        this.CreatePlaceholder(edit.root);
+        break;
+      case "RemoveEventListener":
+        this.RemoveEventListener(edit.root, edit.event_name);
+        break;
+      case "NewEventListener":
+        // this handler is only provided on desktop implementations since this
+        // method is not used by the web implementation
+        let handler = (event) => {
+          let target = event.target;
+          if (target != null) {
+            let realId = target.getAttribute(`data-dioxus-id`);
+            let shouldPreventDefault = target.getAttribute(
+              `dioxus-prevent-default`
+            );
+
+            if (event.type === "click") {
+              // todo call prevent default if it's the right type of event
+              if (shouldPreventDefault !== `onclick`) {
+                if (target.tagName === "A") {
+                  event.preventDefault();
+                  const href = target.getAttribute("href");
+                  if (href !== "" && href !== null && href !== undefined) {
+                    window.ipc.postMessage(
+                      serializeIpcMessage("browser_open", { href })
+                    );
+                  }
+                }
+              }
+
+              // also prevent buttons from submitting
+              if (target.tagName === "BUTTON") {
+                event.preventDefault();
+              }
+            }
+            // walk the tree to find the real element
+            while (realId == null) {
+              // we've reached the root we don't want to send an event
+              if (target.parentElement === null) {
+                return;
+              }
+
+              target = target.parentElement;
+              realId = target.getAttribute(`data-dioxus-id`);
+            }
+
+            shouldPreventDefault = target.getAttribute(
+              `dioxus-prevent-default`
+            );
+
+            let contents = serialize_event(event);
+
+            if (shouldPreventDefault === `on${event.type}`) {
+              event.preventDefault();
+            }
+            if (event.type === "submit") {
+              event.preventDefault();
+            }
+
+            if (target.tagName === "FORM") {
+              for (let x = 0; x < target.elements.length; x++) {
+                let element = target.elements[x];
+                let name = element.getAttribute("name");
+                if (name != null) {
+                  if (element.getAttribute("type") === "checkbox") {
+                    // @ts-ignore
+                    contents.values[name] = element.checked ? "true" : "false";
+                  } else {
+                    // @ts-ignore
+                    contents.values[name] =
+                      element.value ?? element.textContent;
+                  }
+                }
+              }
+            }
+
+            if (realId == null) {
+              return;
+            }
+            window.ipc.postMessage(
+              serializeIpcMessage("user_event", {
+                event: edit.event_name,
+                mounted_dom_id: parseInt(realId),
+                contents: contents,
+              })
+            );
+          }
+        };
+        this.NewEventListener(edit.event_name, edit.root, handler);
+        break;
+      case "SetText":
+        this.SetText(edit.root, edit.text);
+        break;
+      case "SetAttribute":
+        this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
+        break;
+      case "RemoveAttribute":
+        this.RemoveAttribute(edit.root, edit.name);
+        break;
+    }
+  }
+}
+
+export function serialize_event(event) {
+  switch (event.type) {
+    case "copy":
+    case "cut":
+    case "past": {
+      return {};
+    }
+    case "compositionend":
+    case "compositionstart":
+    case "compositionupdate": {
+      let { data } = event;
+      return {
+        data,
+      };
+    }
+    case "keydown":
+    case "keypress":
+    case "keyup": {
+      let {
+        charCode,
+        key,
+        altKey,
+        ctrlKey,
+        metaKey,
+        keyCode,
+        shiftKey,
+        location,
+        repeat,
+        which,
+      } = event;
+      return {
+        char_code: charCode,
+        key: key,
+        alt_key: altKey,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        key_code: keyCode,
+        shift_key: shiftKey,
+        location: location,
+        repeat: repeat,
+        which: which,
+        locale: "locale",
+      };
+    }
+    case "focus":
+    case "blur": {
+      return {};
+    }
+    case "change": {
+      let target = event.target;
+      let value;
+      if (target.type === "checkbox" || target.type === "radio") {
+        value = target.checked ? "true" : "false";
+      } else {
+        value = target.value ?? target.textContent;
+      }
+      return {
+        value: value,
+        values: {},
+      };
+    }
+    case "input":
+    case "invalid":
+    case "reset":
+    case "submit": {
+      let target = event.target;
+      let value = target.value ?? target.textContent;
+      if (target.type === "checkbox") {
+        value = target.checked ? "true" : "false";
+      }
+      return {
+        value: value,
+        values: {},
+      };
+    }
+    case "click":
+    case "contextmenu":
+    case "doubleclick":
+    case "drag":
+    case "dragend":
+    case "dragenter":
+    case "dragexit":
+    case "dragleave":
+    case "dragover":
+    case "dragstart":
+    case "drop":
+    case "mousedown":
+    case "mouseenter":
+    case "mouseleave":
+    case "mousemove":
+    case "mouseout":
+    case "mouseover":
+    case "mouseup": {
+      const {
+        altKey,
+        button,
+        buttons,
+        clientX,
+        clientY,
+        ctrlKey,
+        metaKey,
+        pageX,
+        pageY,
+        screenX,
+        screenY,
+        shiftKey,
+      } = event;
+      return {
+        alt_key: altKey,
+        button: button,
+        buttons: buttons,
+        client_x: clientX,
+        client_y: clientY,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        page_x: pageX,
+        page_y: pageY,
+        screen_x: screenX,
+        screen_y: screenY,
+        shift_key: shiftKey,
+      };
+    }
+    case "pointerdown":
+    case "pointermove":
+    case "pointerup":
+    case "pointercancel":
+    case "gotpointercapture":
+    case "lostpointercapture":
+    case "pointerenter":
+    case "pointerleave":
+    case "pointerover":
+    case "pointerout": {
+      const {
+        altKey,
+        button,
+        buttons,
+        clientX,
+        clientY,
+        ctrlKey,
+        metaKey,
+        pageX,
+        pageY,
+        screenX,
+        screenY,
+        shiftKey,
+        pointerId,
+        width,
+        height,
+        pressure,
+        tangentialPressure,
+        tiltX,
+        tiltY,
+        twist,
+        pointerType,
+        isPrimary,
+      } = event;
+      return {
+        alt_key: altKey,
+        button: button,
+        buttons: buttons,
+        client_x: clientX,
+        client_y: clientY,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        page_x: pageX,
+        page_y: pageY,
+        screen_x: screenX,
+        screen_y: screenY,
+        shift_key: shiftKey,
+        pointer_id: pointerId,
+        width: width,
+        height: height,
+        pressure: pressure,
+        tangential_pressure: tangentialPressure,
+        tilt_x: tiltX,
+        tilt_y: tiltY,
+        twist: twist,
+        pointer_type: pointerType,
+        is_primary: isPrimary,
+      };
+    }
+    case "select": {
+      return {};
+    }
+    case "touchcancel":
+    case "touchend":
+    case "touchmove":
+    case "touchstart": {
+      const { altKey, ctrlKey, metaKey, shiftKey } = event;
+      return {
+        // changed_touches: event.changedTouches,
+        // target_touches: event.targetTouches,
+        // touches: event.touches,
+        alt_key: altKey,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        shift_key: shiftKey,
+      };
+    }
+    case "scroll": {
+      return {};
+    }
+    case "wheel": {
+      const { deltaX, deltaY, deltaZ, deltaMode } = event;
+      return {
+        delta_x: deltaX,
+        delta_y: deltaY,
+        delta_z: deltaZ,
+        delta_mode: deltaMode,
+      };
+    }
+    case "animationstart":
+    case "animationend":
+    case "animationiteration": {
+      const { animationName, elapsedTime, pseudoElement } = event;
+      return {
+        animation_name: animationName,
+        elapsed_time: elapsedTime,
+        pseudo_element: pseudoElement,
+      };
+    }
+    case "transitionend": {
+      const { propertyName, elapsedTime, pseudoElement } = event;
+      return {
+        property_name: propertyName,
+        elapsed_time: elapsedTime,
+        pseudo_element: pseudoElement,
+      };
+    }
+    case "abort":
+    case "canplay":
+    case "canplaythrough":
+    case "durationchange":
+    case "emptied":
+    case "encrypted":
+    case "ended":
+    case "error":
+    case "loadeddata":
+    case "loadedmetadata":
+    case "loadstart":
+    case "pause":
+    case "play":
+    case "playing":
+    case "progress":
+    case "ratechange":
+    case "seeked":
+    case "seeking":
+    case "stalled":
+    case "suspend":
+    case "timeupdate":
+    case "volumechange":
+    case "waiting": {
+      return {};
+    }
+    case "toggle": {
+      return {};
+    }
+    default: {
+      return {};
+    }
+  }
+}
+function serializeIpcMessage(method, params = {}) {
+  return JSON.stringify({ method, params });
+}
+const bool_attrs = {
+  allowfullscreen: true,
+  allowpaymentrequest: true,
+  async: true,
+  autofocus: true,
+  autoplay: true,
+  checked: true,
+  controls: true,
+  default: true,
+  defer: true,
+  disabled: true,
+  formnovalidate: true,
+  hidden: true,
+  ismap: true,
+  itemscope: true,
+  loop: true,
+  multiple: true,
+  muted: true,
+  nomodule: true,
+  novalidate: true,
+  open: true,
+  playsinline: true,
+  readonly: true,
+  required: true,
+  reversed: true,
+  selected: true,
+  truespeed: true,
+};

+ 0 - 1
packages/liveview/src/lib.rs

@@ -1 +0,0 @@
-

+ 210 - 0
packages/liveview/src/main.rs

@@ -0,0 +1,210 @@
+// #![deny(warnings)]
+use std::collections::HashMap;
+use std::sync::{
+    atomic::{AtomicUsize, Ordering},
+    Arc,
+};
+
+use futures_util::{pin_mut, SinkExt, StreamExt, TryFutureExt};
+use tokio::sync::{mpsc, RwLock};
+use tokio_stream::wrappers::UnboundedReceiverStream;
+use warp::ws::{Message, WebSocket};
+use warp::Filter;
+
+mod events;
+
+/// Our global unique user id counter.
+static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1);
+
+/// Our state of currently connected users.
+///
+/// - Key is their id
+/// - Value is a sender of `warp::ws::Message`
+type Users = Arc<RwLock<HashMap<usize, mpsc::UnboundedSender<Message>>>>;
+
+#[tokio::main]
+async fn main() {
+    pretty_env_logger::init();
+
+    let state = Users::default();
+
+    let chat = warp::path("chat")
+        .and(warp::ws())
+        .and(warp::any().map(move || state.clone()))
+        .map(|ws: warp::ws::Ws, users| ws.on_upgrade(move |socket| user_connected(socket, users)));
+
+    let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML));
+
+    let routes = index.or(chat);
+
+    warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
+}
+
+async fn user_connected(ws: WebSocket, users: Users) {
+    // Use a counter to assign a new unique ID for this user.
+    let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed);
+
+    eprintln!("new chat user: {}", my_id);
+
+    // Split the socket into a sender and receive of messages.
+    let (mut user_ws_tx, mut user_ws_rx) = ws.split();
+
+    let (event_tx, event_rx) = mpsc::unbounded_channel();
+    let (edits_tx, edits_rx) = mpsc::unbounded_channel();
+
+    let mut edits_rx = UnboundedReceiverStream::new(edits_rx);
+    let mut event_rx = UnboundedReceiverStream::new(event_rx);
+
+    tokio::task::spawn_blocking(move || {
+        tokio::runtime::Runtime::new()
+            .unwrap()
+            .block_on(async move {
+                use dioxus::prelude::*;
+
+                fn app(cx: Scope) -> Element {
+                    let (count, set_count) = use_state(&cx, || 0);
+                    cx.render(rsx! {
+                        div { "hello world: {count}" }
+                        button {
+                            onclick: move |_| set_count(count + 1),
+                            "increment"
+                        }
+                    })
+                }
+
+                let mut vdom = VirtualDom::new(app);
+
+                let edits = vdom.rebuild();
+
+                let serialized = serde_json::to_string(&edits.edits).unwrap();
+                edits_tx.send(serialized).unwrap();
+
+                loop {
+                    use futures_util::future::{select, Either};
+
+                    let new_event = {
+                        let vdom_fut = vdom.wait_for_work();
+
+                        pin_mut!(vdom_fut);
+
+                        match select(event_rx.next(), vdom_fut).await {
+                            Either::Left((l, _)) => l,
+                            Either::Right((_, _)) => None,
+                        }
+                    };
+
+                    if let Some(new_event) = new_event {
+                        vdom.handle_message(dioxus::core::SchedulerMsg::Event(new_event));
+                    } else {
+                        let mutations = vdom.work_with_deadline(|| false);
+                        for mutation in mutations {
+                            let edits = serde_json::to_string(&mutation.edits).unwrap();
+                            edits_tx.send(edits).unwrap();
+                        }
+                    }
+                }
+            })
+    });
+
+    loop {
+        use futures_util::future::{select, Either};
+
+        match select(user_ws_rx.next(), edits_rx.next()).await {
+            Either::Left((l, _)) => {
+                if let Some(Ok(msg)) = l {
+                    if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) {
+                        let user_event = events::trigger_from_serialized(msg.params);
+                        event_tx.send(user_event).unwrap();
+                    }
+                }
+            }
+            Either::Right((edits, _)) => {
+                if let Some(edits) = edits {
+                    // send the edits to the client
+                    if user_ws_tx.send(Message::text(edits)).await.is_err() {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    // log::info!("");
+}
+
+async fn user_message(my_id: usize, msg: Message, users: &Users) {
+    // Skip any non-Text messages...
+    let msg = if let Ok(s) = msg.to_str() {
+        s
+    } else {
+        return;
+    };
+
+    let new_msg = format!("<User#{}>: {}", my_id, msg);
+
+    // New message from this user, send it to everyone else (except same uid)...
+    for (&uid, tx) in users.read().await.iter() {
+        if my_id != uid {
+            if let Err(_disconnected) = tx.send(Message::text(new_msg.clone())) {
+                // The tx is disconnected, our `user_disconnected` code
+                // should be happening in another task, nothing more to
+                // do here.
+            }
+        }
+    }
+}
+
+async fn user_disconnected(my_id: usize, users: &Users) {
+    eprintln!("good bye user: {}", my_id);
+
+    // Stream closed up, so remove from the user list
+    users.write().await.remove(&my_id);
+}
+
+static INDEX_HTML: &str = r#"<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <title>Warp Chat</title>
+    </head>
+    <body>
+        <h1>Warp chat</h1>
+        <div id="chat">
+            <p><em>Connecting...</em></p>
+        </div>
+        <input type="text" id="text" />
+        <button type="button" id="send">Send</button>
+        <script type="text/javascript">
+        const chat = document.getElementById('chat');
+        const text = document.getElementById('text');
+        const uri = 'ws://' + location.host + '/chat';
+        const ws = new WebSocket(uri);
+
+        function message(data) {
+            const line = document.createElement('p');
+            line.innerText = data;
+            chat.appendChild(line);
+        }
+
+        ws.onopen = function() {
+            chat.innerHTML = '<p><em>Connected!</em></p>';
+        };
+
+        ws.onmessage = function(msg) {
+            message(msg.data);
+        };
+
+        ws.onclose = function() {
+            chat.getElementsByTagName('em')[0].innerText = 'Disconnected!';
+        };
+
+        send.onclick = function() {
+            const msg = text.value;
+            ws.send(msg);
+            text.value = '';
+
+            message('<You>: ' + msg);
+        };
+        </script>
+    </body>
+</html>
+"#;

+ 0 - 0
packages/liveview/src/supported/axum_handler.rs


+ 0 - 0
packages/liveview/src/supported/tide_handler.rs