From e0900ca2560423d8437fa11ae6cb51d4ef9f3f21 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 12 Mar 2022 09:41:10 -0500 Subject: [PATCH] wip: add warp liveview proof of concept --- packages/liveview/Cargo.toml | 29 +- packages/liveview/cloud/Cargo.toml | 26 + packages/liveview/src/events.rs | 213 +++++++ .../actix_handler.rs => index.html} | 0 packages/liveview/src/interpreter.js | 582 ++++++++++++++++++ packages/liveview/src/lib.rs | 1 - packages/liveview/src/main.rs | 210 +++++++ .../liveview/src/supported/axum_handler.rs | 0 .../liveview/src/supported/tide_handler.rs | 0 9 files changed, 1053 insertions(+), 8 deletions(-) create mode 100644 packages/liveview/cloud/Cargo.toml create mode 100644 packages/liveview/src/events.rs rename packages/liveview/src/{supported/actix_handler.rs => index.html} (100%) create mode 100644 packages/liveview/src/interpreter.js delete mode 100644 packages/liveview/src/lib.rs create mode 100644 packages/liveview/src/main.rs delete mode 100644 packages/liveview/src/supported/axum_handler.rs delete mode 100644 packages/liveview/src/supported/tide_handler.rs diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index fb9b17a56..94d69a88d 100644 --- a/packages/liveview/Cargo.toml +++ b/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" diff --git a/packages/liveview/cloud/Cargo.toml b/packages/liveview/cloud/Cargo.toml new file mode 100644 index 000000000..2d71df5e6 --- /dev/null +++ b/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" diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs new file mode 100644 index 000000000..edfdaa521 --- /dev/null +++ b/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 { + 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 { + match name { + "copy" | "cut" | "paste" => { + // + Arc::new(ClipboardData {}) + } + "compositionend" | "compositionstart" | "compositionupdate" => { + Arc::new(serde_json::from_value::(val).unwrap()) + } + "keydown" | "keypress" | "keyup" => { + let evt = serde_json::from_value::(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::(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::(val).unwrap()) + } + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" + | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { + Arc::new(serde_json::from_value::(val).unwrap()) + } + "select" => { + // + Arc::new(serde_json::from_value::(val).unwrap()) + } + + "touchcancel" | "touchend" | "touchmove" | "touchstart" => { + Arc::new(serde_json::from_value::(val).unwrap()) + } + + "scroll" => Arc::new(()), + + "wheel" => Arc::new(serde_json::from_value::(val).unwrap()), + + "animationstart" | "animationend" | "animationiteration" => { + Arc::new(serde_json::from_value::(val).unwrap()) + } + + "transitionend" => Arc::new(serde_json::from_value::(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") + } + } +} diff --git a/packages/liveview/src/supported/actix_handler.rs b/packages/liveview/src/index.html similarity index 100% rename from packages/liveview/src/supported/actix_handler.rs rename to packages/liveview/src/index.html diff --git a/packages/liveview/src/interpreter.js b/packages/liveview/src/interpreter.js new file mode 100644 index 000000000..76da73752 --- /dev/null +++ b/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, +}; diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs deleted file mode 100644 index 8b1378917..000000000 --- a/packages/liveview/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/liveview/src/main.rs b/packages/liveview/src/main.rs new file mode 100644 index 000000000..592995e21 --- /dev/null +++ b/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>>>; + +#[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!(": {}", 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#" + + + Warp Chat + + +

Warp chat

+
+

Connecting...

+
+ + + + + +"#; diff --git a/packages/liveview/src/supported/axum_handler.rs b/packages/liveview/src/supported/axum_handler.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/liveview/src/supported/tide_handler.rs b/packages/liveview/src/supported/tide_handler.rs deleted file mode 100644 index e69de29bb..000000000