wip: overhaul event system
This commit is contained in:
1 parent
82953f2ac3
commit
7a03c1d2b4
34 files changed
+2488
-1468
No files matched your search
+2
-1
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"rust-analyzer.inlayHints.enable": true
|
||||
"rust-analyzer.inlayHints.enable": true,
|
||||
"rust-analyzer.cargo.allFeatures": true
|
||||
}
|
||||
@@ -19,15 +19,16 @@ thiserror = "1.0.23"
|
||||
log = "0.4.13"
|
||||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
html-escape = "0.2.9"
|
||||
wry = "0.11.0"
|
||||
# wry = { version = "0.12.2", git = "https://github.com/jkelleyrtp/wry.git", branch = "jk/fnmut_rpc" }
|
||||
wry = "0.12.2"
|
||||
tokio = { version = "1.12.0", features = ["full"] }
|
||||
futures-channel = "0.3.16"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-html = { path = "../html" }
|
||||
tide = "0.15.0"
|
||||
tide-websockets = "0.3.0"
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
dioxus-hooks = { path = "../hooks" }
|
||||
# thiserror = "1.0.23"
|
||||
# log = "0.4.13"
|
||||
# fern = { version = "0.6.0", features = ["colored"] }
|
||||
|
||||
+17
-19
@@ -6,26 +6,19 @@ Dioxus-webview is an attempt at making a simpler "Tauri" where creating desktop
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
dioxus_desktop::new(|cx, props|{
|
||||
let (count, set_count) = use_state(cx, || 0);
|
||||
cx.render(html! {
|
||||
<div>
|
||||
<h1> "Dioxus Desktop Demo" </h1>
|
||||
<p> "Count is {count}"</p>
|
||||
<button onclick=|_| set_count(count + 1) >
|
||||
"Click to increment"
|
||||
</button>
|
||||
</div>
|
||||
})
|
||||
})
|
||||
.configure_webview(|view| {
|
||||
// custom webview config options
|
||||
})
|
||||
.launch()
|
||||
.await;
|
||||
fn main() {
|
||||
dioxus_desktop::new(App, |c| c)
|
||||
.launch()
|
||||
.await;
|
||||
}
|
||||
static App: FC<()> = |cx, props|{
|
||||
let (count, set_count) = use_state(cx, || 0);
|
||||
rsx!(cx, div {
|
||||
h1 { "Dioxus Desktop Demo" }
|
||||
p { "Count is {count}"}
|
||||
button { onclick: move |_| count += 1}
|
||||
})
|
||||
};
|
||||
```
|
||||
|
||||
and then to create a native .app:
|
||||
@@ -45,3 +38,8 @@ By bridging the native process, desktop apps can access full multithreading powe
|
||||
Dioxus-desktop is a pure liveview application where all of the state and event handlers are proxied through the liveview and into the native process. For pure server-based liveview, this would normally be too slow (in both render performance and latency), but because the VDom is local, desktop apps are just as fast as Electron.
|
||||
|
||||
Dioxus-desktop leverages dioxus-liveview under the hood, but with convenience wrappers around setting up the VDom bridge, proxying events, and serving the initial WebSys-Renderer. The backend is served by Tide, so an async runtime _is_ needed - we recommend async-std in Tokio mode.
|
||||
|
||||
|
||||
## Async Runtime
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
fn main() {
|
||||
let (window_loop, tasks) = dioxus_desktop::start(App, |c| c);
|
||||
|
||||
std::thread::spawn(move || {
|
||||
//
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
runtime.block_on(async move {
|
||||
let mut vir = VirtualDom::new_with_props(root, props);
|
||||
let channel = vir.get_event_sender();
|
||||
loop {
|
||||
vir.wait_for_work().await;
|
||||
let edits = vir.run_with_deadline(|| false);
|
||||
let edit_string = serde_json::to_string(&edits[0].edits).unwrap();
|
||||
event_tx.send(edit_string).unwrap();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
window_loop.run();
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx| {
|
||||
//
|
||||
cx.render(rsx!(div {}))
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
use dioxus_core as dioxus;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_core_macro::*;
|
||||
use dioxus_hooks::*;
|
||||
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
fn main() {
|
||||
dioxus_desktop::set_up_logging(true);
|
||||
dioxus_desktop::launch(App, |c| c).unwrap();
|
||||
}
|
||||
|
||||
enum Scene {
|
||||
ClientsList,
|
||||
NewClientForm,
|
||||
Settings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Client {
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx, _| {
|
||||
let scene = use_state(cx, || Scene::ClientsList);
|
||||
let clients = use_ref(cx, || vec![] as Vec<Client>);
|
||||
|
||||
let firstname = use_state(cx, || String::new());
|
||||
let lastname = use_state(cx, || String::new());
|
||||
let description = use_state(cx, || String::new());
|
||||
|
||||
let scene = match *scene {
|
||||
Scene::ClientsList => {
|
||||
rsx!(cx, div { class: "crm"
|
||||
h2 { "List of clients" margin_bottom: "10px" }
|
||||
div { class: "clients" margin_left: "10px"
|
||||
{clients.read().iter().map(|client| rsx!(
|
||||
div { class: "client" style: "margin-bottom: 50px"
|
||||
p { "First Name: {client.first_name}" }
|
||||
p { "Last Name: {client.last_name}" }
|
||||
p {"Description: {client.description}"}
|
||||
})
|
||||
)}
|
||||
}
|
||||
button { class: "pure-button pure-button-primary" onclick: move |_| scene.set(Scene::NewClientForm), "Add New" }
|
||||
button { class: "pure-button" onclick: move |_| scene.set(Scene::Settings), "Settings" }
|
||||
})
|
||||
}
|
||||
Scene::NewClientForm => {
|
||||
let add_new = move |_| {
|
||||
clients.write().push(Client {
|
||||
description: (*description).clone(),
|
||||
first_name: (*firstname).clone(),
|
||||
last_name: (*lastname).clone(),
|
||||
});
|
||||
description.set(String::new());
|
||||
firstname.set(String::new());
|
||||
lastname.set(String::new());
|
||||
};
|
||||
rsx!(cx, div { class: "crm"
|
||||
h2 {"Add new client" margin_bottom: "10px" }
|
||||
form { class: "pure-form"
|
||||
input { class: "new-client firstname" placeholder: "First name" value: "{firstname}"
|
||||
oninput: move |evt| firstname.set(evt.value())
|
||||
}
|
||||
input { class: "new-client lastname" placeholder: "Last name" value: "{lastname}"
|
||||
oninput: move |evt| lastname.set(evt.value())
|
||||
}
|
||||
textarea { class: "new-client description" placeholder: "Description" value: "{description}"
|
||||
oninput: move |evt| description.set(evt.value())
|
||||
}
|
||||
}
|
||||
button { class: "pure-button pure-button-primary", onclick: {add_new}, "Add New" }
|
||||
button { class: "pure-button", onclick: move |_| scene.set(Scene::ClientsList), "Go Back" }
|
||||
})
|
||||
}
|
||||
Scene::Settings => {
|
||||
rsx!(cx, div {
|
||||
h2 { "Settings" margin_bottom: "10px" }
|
||||
button {
|
||||
background: "rgb(202, 60, 60)"
|
||||
class: "pure-button pure-button-primary"
|
||||
onclick: move |_| {
|
||||
clients.write().clear();
|
||||
scene.set(Scene::ClientsList);
|
||||
},
|
||||
"Remove all clients"
|
||||
}
|
||||
button {
|
||||
class: "pure-button pure-button-primary"
|
||||
onclick: move |_| scene.set(Scene::ClientsList),
|
||||
"Go Back"
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
rsx!(cx, body {
|
||||
link {
|
||||
rel: "stylesheet"
|
||||
href: "https://unpkg.com/purecss@2.0.6/build/pure-min.css"
|
||||
integrity: "sha384-Uu6IeWbM+gzNVXJcM9XV3SohHtmWE+3VGi496jvgX1jyvDTXfdK+rfZc8C1Aehk5"
|
||||
crossorigin: "anonymous"
|
||||
}
|
||||
margin_left: "35%"
|
||||
h1 {"Dioxus CRM Example"}
|
||||
{scene}
|
||||
})
|
||||
};
|
||||
@@ -5,20 +5,14 @@ use dioxus_core_macro::*;
|
||||
use dioxus_html as dioxus_elements;
|
||||
|
||||
fn main() {
|
||||
std::thread::spawn(|| {
|
||||
let mut vdom = VirtualDom::new(App);
|
||||
let f = async_std::task::block_on(vdom.wait_for_work());
|
||||
});
|
||||
let a = 10;
|
||||
// async_std::task::spawn_blocking(|| async move {
|
||||
// });
|
||||
dioxus_desktop::launch(App, |c| c).unwrap();
|
||||
}
|
||||
|
||||
static App: FC<()> = |cx, props| {
|
||||
//
|
||||
cx.render(rsx!(
|
||||
div {
|
||||
"hello world!"
|
||||
}
|
||||
{(0..10).map(|f| rsx!( div {"abc {f}"}))}
|
||||
))
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
fn main() {
|
||||
tauri::AppBuilder::default().setup(move |app, name| {
|
||||
//
|
||||
let window = app.get_window();
|
||||
|
||||
let window = app.get_window();
|
||||
tauri::spawn(|| async move {
|
||||
//
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4,30 +4,36 @@ use dioxus_core::DomEdit;
|
||||
use wry::{
|
||||
application::{
|
||||
error::OsError,
|
||||
event_loop::EventLoopWindowTarget,
|
||||
event_loop::{EventLoop, EventLoopWindowTarget},
|
||||
menu::MenuBar,
|
||||
window::{Fullscreen, Icon, Window, WindowBuilder},
|
||||
},
|
||||
webview::{RpcRequest, RpcResponse},
|
||||
webview::{RpcRequest, RpcResponse, WebView},
|
||||
};
|
||||
|
||||
pub struct DesktopConfig<'a> {
|
||||
pub window: WindowBuilder,
|
||||
pub(crate) manual_edits: Option<DomEdit<'a>>,
|
||||
pub(crate) manual_edits: Option<Vec<DomEdit<'a>>>,
|
||||
pub(crate) pre_rendered: Option<String>,
|
||||
pub(crate) event_handler: Option<Box<dyn Fn(&mut EventLoop<()>, &mut WebView)>>,
|
||||
}
|
||||
|
||||
impl DesktopConfig<'_> {
|
||||
impl<'a> DesktopConfig<'a> {
|
||||
/// Initializes a new `WindowBuilder` with default values.
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
event_handler: None,
|
||||
window: Default::default(),
|
||||
pre_rendered: None,
|
||||
manual_edits: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_edits(&mut self, edits: Vec<DomEdit<'a>>) {
|
||||
self.manual_edits = Some(edits);
|
||||
}
|
||||
|
||||
pub fn with_prerendered(&mut self, content: String) -> &mut Self {
|
||||
self.pre_rendered = Some(content);
|
||||
self
|
||||
|
||||
@@ -22,69 +22,100 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
|
||||
let mut data: Vec<ImEvent> = serde_json::from_value(val).unwrap();
|
||||
let data = data.drain(..).next().unwrap();
|
||||
|
||||
let event = SyntheticEvent::MouseEvent(MouseEvent(Arc::new(WebviewMouseEvent)));
|
||||
let scope = ScopeId(data.scope as usize);
|
||||
let mounted_dom_id = Some(ElementId(data.mounted_dom_id as usize));
|
||||
|
||||
UserEvent {
|
||||
name: todo!(),
|
||||
name: "click",
|
||||
event,
|
||||
scope,
|
||||
mounted_dom_id,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WebviewMouseEvent;
|
||||
impl MouseEventInner for WebviewMouseEvent {
|
||||
fn alt_key(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn button(&self) -> i16 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn buttons(&self) -> u16 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn client_x(&self) -> i32 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn client_y(&self) -> i32 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn ctrl_key(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn meta_key(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn page_x(&self) -> i32 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn page_y(&self) -> i32 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn screen_x(&self) -> i32 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn screen_y(&self) -> i32 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn shift_key(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_modifier_state(&self, key_code: &str) -> bool {
|
||||
todo!()
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,212 +9,7 @@
|
||||
<div id="_dioxusroot">
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
class Interpreter {
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
this.stack = [root];
|
||||
this.listeners = {
|
||||
"onclick": {}
|
||||
};
|
||||
this.lastNodeWasText = false;
|
||||
this.nodes = [root, root, root, root];
|
||||
}
|
||||
|
||||
top() {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
|
||||
pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
|
||||
PushRoot(edit) {
|
||||
const id = edit.id;
|
||||
const node = this.nodes[id];
|
||||
console.log("pushing root ", node, "with id", id);
|
||||
this.stack.push(node);
|
||||
}
|
||||
|
||||
PopRoot(edit) {
|
||||
this.stack.pop();
|
||||
}
|
||||
|
||||
AppendChildren(edit) {
|
||||
let root = this.stack[this.stack.length - (edit.many + 1)];
|
||||
for (let i = 0; i < edit.many; i++) {
|
||||
console.log("popping ", i, edit.many);
|
||||
let node = this.pop();
|
||||
root.appendChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
ReplaceWith(edit) {
|
||||
|
||||
let root = this.stack[this.stack.length - (edit.many + 1)];
|
||||
let els = [];
|
||||
|
||||
for (let i = 0; i < edit.many; i++) {
|
||||
els.push(this.pop());
|
||||
}
|
||||
|
||||
root.replaceWith(...els);
|
||||
}
|
||||
|
||||
Remove(edit) {
|
||||
const node = this.stack.pop();
|
||||
node.remove();
|
||||
}
|
||||
|
||||
RemoveAllChildren(edit) {}
|
||||
|
||||
CreateTextNode(edit) {
|
||||
const node = document.createTextNode(edit.text);
|
||||
this.nodes[edit.id] = node;
|
||||
this.stack.push(node);
|
||||
}
|
||||
|
||||
CreateElement(edit) {
|
||||
const tagName = edit.tag;
|
||||
const el = document.createElement(tagName);
|
||||
this.nodes[edit.id] = el;
|
||||
console.log(`creating element: `, edit);
|
||||
this.stack.push(el);
|
||||
}
|
||||
|
||||
CreateElementNs(edit) {
|
||||
const tagName = edit.tag;
|
||||
console.log(`creating namespaced element: `, edit);
|
||||
this.stack.push(document.createElementNS(edit.ns, edit.tag));
|
||||
}
|
||||
|
||||
CreatePlaceholder(edit) {
|
||||
const a = `this.stack.push(document.createElement(" pre"))`;
|
||||
this.stack.push(document.createComment("vroot"));
|
||||
}
|
||||
|
||||
NewEventListener(edit) {
|
||||
const element_id = edit.element_id;
|
||||
const event_name = edit.event_name;
|
||||
const mounted_node_id = edit.mounted_node_id;
|
||||
const scope = edit.scope;
|
||||
|
||||
const element = this.top();
|
||||
element.setAttribute(`dioxus-event-${event_name}`, `${scope}.${mounted_node_id}`);
|
||||
|
||||
console.log("listener map is", this.listeners);
|
||||
if (this.listeners[event_name] === undefined) {
|
||||
console.log("adding listener!");
|
||||
this.listeners[event_name] = "bla";
|
||||
this.root.addEventListener(event_name, (event) => {
|
||||
const target = event.target;
|
||||
const type = event.type;
|
||||
const val = target.getAttribute(`dioxus-event-${event_name}`);
|
||||
const fields = val.split(".");
|
||||
const scope_id = parseInt(fields[0]);
|
||||
const real_id = parseInt(fields[1]);
|
||||
|
||||
console.log(`parsed event with scope_id ${scope_id} and real_id ${real_id}`);
|
||||
|
||||
rpc.call('user_event', {
|
||||
event: event_name,
|
||||
scope: scope_id,
|
||||
mounted_dom_id: real_id,
|
||||
}).then((reply) => {
|
||||
console.log(reply);
|
||||
this.stack.push(this.root);
|
||||
|
||||
let edits = reply.edits;
|
||||
|
||||
for (let x = 0; x < edits.length; x++) {
|
||||
let edit = edits[x];
|
||||
console.log(edit);
|
||||
|
||||
let f = this[edit.type];
|
||||
f.call(this, edit);
|
||||
}
|
||||
|
||||
console.log("initiated");
|
||||
}).catch((err) => {
|
||||
console.log("failed to initiate", err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
RemoveEventListener(edit) {}
|
||||
|
||||
SetText(edit) {
|
||||
this.top().textContent = edit.text;
|
||||
}
|
||||
|
||||
SetAttribute(edit) {
|
||||
const name = edit.field;
|
||||
const value = edit.value;
|
||||
const ns = edit.ns;
|
||||
const node = this.top(this.stack);
|
||||
if (ns == "style") {
|
||||
node.style[name] = value;
|
||||
} else if (ns !== undefined) {
|
||||
node.setAttributeNS(ns, name, value);
|
||||
} else {
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
if (name === "value") {
|
||||
node.value = value;
|
||||
}
|
||||
if (name === "checked") {
|
||||
node.checked = true;
|
||||
}
|
||||
if (name === "selected") {
|
||||
node.selected = true;
|
||||
}
|
||||
}
|
||||
RemoveAttribute(edit) {
|
||||
const name = edit.field;
|
||||
const node = this.top(this.stack);
|
||||
node.removeAttribute(name);
|
||||
|
||||
if (name === "value") {
|
||||
node.value = null;
|
||||
}
|
||||
if (name === "checked") {
|
||||
node.checked = false;
|
||||
}
|
||||
if (name === "selected") {
|
||||
node.selected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function initialize() {
|
||||
const reply = await rpc.call('initiate');
|
||||
let root = window.document.getElementById("_dioxusroot");
|
||||
const interpreter = new Interpreter(root);
|
||||
console.log(reply);
|
||||
|
||||
let pre_rendered = reply.pre_rendered;
|
||||
if (pre_rendered !== undefined) {
|
||||
root.innerHTML = pre_rendered;
|
||||
}
|
||||
|
||||
const edits = reply.edits;
|
||||
|
||||
for (let x = 0; x < edits.length; x++) {
|
||||
let edit = edits[x];
|
||||
console.log(edit);
|
||||
|
||||
let f = interpreter[edit.type];
|
||||
f.call(interpreter, edit);
|
||||
}
|
||||
|
||||
console.log("stack completed: ", interpreter.stack);
|
||||
}
|
||||
console.log("initializing...");
|
||||
initialize();
|
||||
<script type="text/javascript" src="/index.js">
|
||||
</script>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,216 @@
|
||||
|
||||
class Interpreter {
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
this.stack = [root];
|
||||
this.listeners = {
|
||||
"onclick": {}
|
||||
};
|
||||
this.lastNodeWasText = false;
|
||||
this.nodes = [root, root, root, root];
|
||||
}
|
||||
|
||||
top() {
|
||||
return this.stack[this.stack.length - 1];
|
||||
}
|
||||
|
||||
pop() {
|
||||
return this.stack.pop();
|
||||
}
|
||||
|
||||
PushRoot(edit) {
|
||||
const id = edit.id;
|
||||
const node = this.nodes[id];
|
||||
console.log("pushing root ", node, "with id", id);
|
||||
this.stack.push(node);
|
||||
}
|
||||
|
||||
PopRoot(_edit) {
|
||||
this.stack.pop();
|
||||
}
|
||||
|
||||
AppendChildren(edit) {
|
||||
let root = this.stack[this.stack.length - (1 + edit.many)];
|
||||
|
||||
let to_add = this.stack.splice(this.stack.length - edit.many);
|
||||
|
||||
for (let i = 0; i < edit.many; i++) {
|
||||
root.appendChild(to_add[i]);
|
||||
}
|
||||
}
|
||||
|
||||
ReplaceWith(edit) {
|
||||
console.log(edit);
|
||||
let root = this.nodes[edit.root];
|
||||
let els = this.stack.splice(this.stack.length - edit.m);
|
||||
|
||||
console.log(root);
|
||||
console.log(els);
|
||||
|
||||
|
||||
root.replaceWith(...els);
|
||||
}
|
||||
|
||||
Remove(edit) {
|
||||
let node = this.nodes[edit.element_id];
|
||||
node.remove();
|
||||
}
|
||||
|
||||
CreateTextNode(edit) {
|
||||
const node = document.createTextNode(edit.text);
|
||||
this.nodes[edit.id] = node;
|
||||
this.stack.push(node);
|
||||
}
|
||||
|
||||
CreateElement(edit) {
|
||||
const tagName = edit.tag;
|
||||
const el = document.createElement(tagName);
|
||||
this.nodes[edit.id] = el;
|
||||
this.stack.push(el);
|
||||
}
|
||||
|
||||
CreateElementNs(edit) {
|
||||
let el = document.createElementNS(edit.ns, edit.tag);
|
||||
this.stack.push(el);
|
||||
this.nodes[edit.id] = el;
|
||||
}
|
||||
|
||||
CreatePlaceholder(edit) {
|
||||
let el = document.createElement("pre");
|
||||
// let el = document.createComment("vroot");
|
||||
this.stack.push(el);
|
||||
this.nodes[edit.id] = el;
|
||||
}
|
||||
|
||||
RemoveEventListener(edit) { }
|
||||
|
||||
SetText(edit) {
|
||||
this.top().textContent = edit.text;
|
||||
}
|
||||
|
||||
SetAttribute(edit) {
|
||||
const name = edit.field;
|
||||
const value = edit.value;
|
||||
const ns = edit.ns;
|
||||
const node = this.top(this.stack);
|
||||
if (ns == "style") {
|
||||
node.style[name] = value;
|
||||
} else if (ns !== undefined) {
|
||||
node.setAttributeNS(ns, name, value);
|
||||
} else {
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
if (name === "value") {
|
||||
node.value = value;
|
||||
}
|
||||
if (name === "checked") {
|
||||
node.checked = true;
|
||||
}
|
||||
if (name === "selected") {
|
||||
node.selected = true;
|
||||
}
|
||||
}
|
||||
RemoveAttribute(edit) {
|
||||
const name = edit.field;
|
||||
const node = this.top(this.stack);
|
||||
node.removeAttribute(name);
|
||||
|
||||
if (name === "value") {
|
||||
node.value = null;
|
||||
}
|
||||
if (name === "checked") {
|
||||
node.checked = false;
|
||||
}
|
||||
if (name === "selected") {
|
||||
node.selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
InsertAfter(edit) {
|
||||
let old = this.nodes[edit.element_id];
|
||||
let new_nodes = this.stack.splice(edit.many);
|
||||
old.after(...new_nodes);
|
||||
}
|
||||
|
||||
InsertBefore(edit) {
|
||||
let old = this.nodes[edit.element_id];
|
||||
let new_nodes = this.stack.splice(edit.many);
|
||||
old.before(...new_nodes);
|
||||
}
|
||||
|
||||
NewEventListener(edit) {
|
||||
const event_name = edit.event_name;
|
||||
const mounted_node_id = edit.mounted_node_id;
|
||||
const scope = edit.scope;
|
||||
|
||||
const element = this.top();
|
||||
element.setAttribute(`dioxus-event-${event_name}`, `${scope}.${mounted_node_id}`);
|
||||
|
||||
console.log("listener map is", this.listeners);
|
||||
if (this.listeners[event_name] === undefined) {
|
||||
console.log("adding listener!");
|
||||
this.listeners[event_name] = "bla";
|
||||
this.root.addEventListener(event_name, (event) => {
|
||||
const target = event.target;
|
||||
const val = target.getAttribute(`dioxus-event-${event_name}`);
|
||||
const fields = val.split(".");
|
||||
const scope_id = parseInt(fields[0]);
|
||||
const real_id = parseInt(fields[1]);
|
||||
|
||||
console.log(`parsed event with scope_id ${scope_id} and real_id ${real_id}`);
|
||||
|
||||
rpc.call('user_event', {
|
||||
event: event_name,
|
||||
scope: scope_id,
|
||||
mounted_dom_id: real_id,
|
||||
}).then((reply) => {
|
||||
console.log(reply);
|
||||
this.stack.push(this.root);
|
||||
|
||||
let edits = reply.edits;
|
||||
|
||||
for (let x = 0; x < edits.length; x++) {
|
||||
let edit = edits[x];
|
||||
console.log(edit);
|
||||
|
||||
let f = this[edit.type];
|
||||
f.call(this, edit);
|
||||
}
|
||||
|
||||
console.log("initiated");
|
||||
}).catch((err) => {
|
||||
console.log("failed to initiate", err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
const reply = await rpc.call('initiate');
|
||||
let root = window.document.getElementById("_dioxusroot");
|
||||
const interpreter = new Interpreter(root);
|
||||
console.log(reply);
|
||||
|
||||
let pre_rendered = reply.pre_rendered;
|
||||
if (pre_rendered !== undefined) {
|
||||
root.innerHTML = pre_rendered;
|
||||
}
|
||||
|
||||
const edits = reply.edits;
|
||||
|
||||
apply_edits(edits, interpreter);
|
||||
}
|
||||
|
||||
function apply_edits(edits, interpreter) {
|
||||
for (let x = 0; x < edits.length; x++) {
|
||||
let edit = edits[x];
|
||||
console.log(edit);
|
||||
let f = interpreter[edit.type];
|
||||
f.call(interpreter, edit);
|
||||
}
|
||||
|
||||
console.log("stack completed: ", interpreter.stack);
|
||||
}
|
||||
|
||||
initialize();
|
||||
+159
-111
@@ -4,20 +4,28 @@
|
||||
//!
|
||||
|
||||
use std::borrow::BorrowMut;
|
||||
use std::cell::RefCell;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use cfg::DesktopConfig;
|
||||
use dioxus_core::scheduler::SchedulerMsg;
|
||||
use dioxus_core::*;
|
||||
// use futures_channel::mpsc::UnboundedSender;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod logging;
|
||||
|
||||
pub use logging::set_up_logging;
|
||||
pub use wry;
|
||||
|
||||
use wry::application::event::{Event, WindowEvent};
|
||||
use wry::application::event_loop::{ControlFlow, EventLoop};
|
||||
use wry::application::event_loop::{self, ControlFlow, EventLoop};
|
||||
use wry::application::window::Fullscreen;
|
||||
use wry::webview::WebViewBuilder;
|
||||
use wry::webview::{WebView, WebViewBuilder};
|
||||
use wry::{
|
||||
application::window::{Window, WindowBuilder},
|
||||
webview::{RpcRequest, RpcResponse},
|
||||
@@ -45,10 +53,16 @@ pub fn launch_with_props<P: Properties + 'static + Send + Sync>(
|
||||
run(root, props, builder)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
enum RpcEvent<'a> {
|
||||
Initialize { edits: Vec<DomEdit<'a>> },
|
||||
}
|
||||
|
||||
enum BridgeEvent {
|
||||
Initialize(serde_json::Value),
|
||||
Update(serde_json::Value),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Response<'a> {
|
||||
pre_rendered: Option<String>,
|
||||
@@ -60,72 +74,175 @@ pub fn run<T: Properties + 'static + Send + Sync>(
|
||||
props: T,
|
||||
user_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
|
||||
) -> anyhow::Result<()> {
|
||||
run_with_edits(root, props, user_builder, None)
|
||||
}
|
||||
|
||||
pub fn run_with_edits<
|
||||
F: for<'a, 'b> FnOnce(&'a mut DesktopConfig<'b>) -> &'a mut DesktopConfig<'b>,
|
||||
T: Properties + 'static + Send + Sync,
|
||||
>(
|
||||
root: FC<T>,
|
||||
props: T,
|
||||
user_builder: F,
|
||||
redits: Option<Vec<DomEdit<'static>>>,
|
||||
) -> anyhow::Result<()> {
|
||||
/*
|
||||
|
||||
|
||||
*/
|
||||
|
||||
let mut cfg = DesktopConfig::new();
|
||||
user_builder(&mut cfg);
|
||||
let DesktopConfig {
|
||||
window,
|
||||
manual_edits,
|
||||
pre_rendered,
|
||||
..
|
||||
} = cfg;
|
||||
|
||||
let event_loop = EventLoop::new();
|
||||
let window = window.build(&event_loop)?;
|
||||
|
||||
let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
let sender = launch_vdom_with_tokio(root, props, event_tx.clone());
|
||||
|
||||
let locked_receiver = Rc::new(RefCell::new(event_rx));
|
||||
|
||||
let webview = WebViewBuilder::new(window)?
|
||||
.with_url("wry://src/index.html")?
|
||||
.with_rpc_handler(move |_window: &Window, mut req: RpcRequest| {
|
||||
match req.method.as_str() {
|
||||
"initiate" => {
|
||||
//
|
||||
let mut rx = (*locked_receiver).borrow_mut();
|
||||
|
||||
match rx.try_recv() {
|
||||
Ok(BridgeEvent::Initialize(edits)) => {
|
||||
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
"user_event" => {
|
||||
//
|
||||
let data = req.params.unwrap();
|
||||
log::debug!("Data: {:#?}", data);
|
||||
let event = events::trigger_from_serialized(data);
|
||||
sender.unbounded_send(SchedulerMsg::UiEvent(event)).unwrap();
|
||||
|
||||
let mut rx = (*locked_receiver).borrow_mut();
|
||||
|
||||
match rx.blocking_recv() {
|
||||
Some(BridgeEvent::Update(edits)) => {
|
||||
log::info!("Passing response back");
|
||||
Some(RpcResponse::new_result(req.id.take(), Some(edits)))
|
||||
}
|
||||
None => {
|
||||
log::error!("Sender half is gone");
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
log::error!("No update event received");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => todo!("this message failed"),
|
||||
}
|
||||
})
|
||||
// this isn't quite portable unfortunately :(
|
||||
// todo: figure out a way to allow us to create the index.html with the index.js file separately
|
||||
// it's a bit easier to hack with
|
||||
.with_custom_protocol("wry".into(), move |request| {
|
||||
use std::fs::{canonicalize, read};
|
||||
use wry::http::ResponseBuilder;
|
||||
// Remove url scheme
|
||||
let path = request.uri().replace("wry://", "");
|
||||
// Read the file content from file path
|
||||
let content = read(canonicalize(&path)?)?;
|
||||
|
||||
// Return asset contents and mime types based on file extentions
|
||||
// If you don't want to do this manually, there are some crates for you.
|
||||
// Such as `infer` and `mime_guess`.
|
||||
let (data, meta) = if path.ends_with(".html") {
|
||||
(content, "text/html")
|
||||
} else if path.ends_with(".js") {
|
||||
(content, "text/javascript")
|
||||
} else if path.ends_with(".png") {
|
||||
(content, "image/png")
|
||||
} else {
|
||||
unimplemented!();
|
||||
};
|
||||
|
||||
ResponseBuilder::new().mimetype(meta).body(data)
|
||||
})
|
||||
.build()?;
|
||||
|
||||
run_event_loop(event_loop, webview, event_tx);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start<P: 'static + Send>(
|
||||
root: FC<P>,
|
||||
config_builder: impl for<'a, 'b> FnOnce(&'b mut DesktopConfig<'a>) -> &'b mut DesktopConfig<'a>,
|
||||
) -> ((), ()) {
|
||||
//
|
||||
((), ())
|
||||
}
|
||||
|
||||
// Create a new tokio runtime on a dedicated thread and then launch the apps VirtualDom.
|
||||
fn launch_vdom_with_tokio<C: Send + 'static>(
|
||||
root: FC<C>,
|
||||
props: C,
|
||||
event_tx: tokio::sync::mpsc::UnboundedSender<BridgeEvent>,
|
||||
) -> futures_channel::mpsc::UnboundedSender<SchedulerMsg> {
|
||||
// Spawn the virtualdom onto its own thread
|
||||
// if it wants to spawn multithreaded tasks, it can use the executor directly
|
||||
|
||||
let (sender, receiver) = futures_channel::mpsc::unbounded::<SchedulerMsg>();
|
||||
|
||||
let sender_2 = sender.clone();
|
||||
std::thread::spawn(move || {
|
||||
//
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
// We create the runtim as multithreaded, so you can still "spawn" onto multiple threads
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
runtime.block_on(async move {
|
||||
let mut vir = VirtualDom::new_with_props(root, props);
|
||||
let channel = vir.get_event_sender();
|
||||
let mut vir = VirtualDom::new_with_props_and_scheduler(root, props, sender, receiver);
|
||||
let _ = vir.get_event_sender();
|
||||
|
||||
let edits = vir.rebuild();
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Evt<'a> {
|
||||
edits: Vec<DomEdit<'a>>,
|
||||
}
|
||||
|
||||
// let msg = RpcEvent::Initialize { edits: edits.edits };
|
||||
let edit_string = serde_json::to_value(Evt { edits: edits.edits }).unwrap();
|
||||
match event_tx.send(BridgeEvent::Initialize(edit_string)) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
loop {
|
||||
vir.wait_for_work().await;
|
||||
let edits = vir.run_with_deadline(|| false);
|
||||
let edit_string = serde_json::to_string(&edits[0].edits).unwrap();
|
||||
event_tx.send(edit_string).unwrap();
|
||||
log::info!("{}", vir);
|
||||
|
||||
let mut muts = vir.run_with_deadline(|| false);
|
||||
log::info!("muts {:#?}", muts);
|
||||
while let Some(edit) = muts.pop() {
|
||||
let edit_string = serde_json::to_value(Evt { edits: edit.edits }).unwrap();
|
||||
match event_tx.send(BridgeEvent::Update(edit_string)) {
|
||||
Ok(_) => {}
|
||||
Err(er) => {
|
||||
log::error!("Sending should not fail {}", er);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("mutations sent on channel");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let dioxus_requsted = Arc::new(AtomicBool::new(false));
|
||||
sender_2
|
||||
}
|
||||
|
||||
let webview = WebViewBuilder::new(window)?
|
||||
.with_url(&format!("data:text/html,{}", HTML_CONTENT))?
|
||||
.with_rpc_handler(move |_window: &Window, mut req: RpcRequest| {
|
||||
match req.method.as_str() {
|
||||
"initiate" => {}
|
||||
"user_event" => {}
|
||||
_ => todo!("this message failed"),
|
||||
}
|
||||
todo!()
|
||||
})
|
||||
.build()?;
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
fn run_event_loop(
|
||||
event_loop: EventLoop<()>,
|
||||
webview: WebView,
|
||||
event_tx: tokio::sync::mpsc::UnboundedSender<BridgeEvent>,
|
||||
) {
|
||||
let _ = event_tx.clone();
|
||||
event_loop.run(move |event, target, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
match event {
|
||||
@@ -146,74 +263,5 @@ pub fn run_with_edits<
|
||||
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
// let edits = if let Some(edits) = &redits {
|
||||
// serde_json::to_value(edits).unwrap()
|
||||
// } else {
|
||||
// let mut lock = vdom.write().unwrap();
|
||||
// // let mut reg_lock = registry.write().unwrap();
|
||||
// // Create the thin wrapper around the registry to collect the edits into
|
||||
// let mut real = dom::WebviewDom::new();
|
||||
// let pre = pre_rendered.clone();
|
||||
// let response = match pre {
|
||||
// Some(content) => {
|
||||
// lock.rebuild_in_place().unwrap();
|
||||
// Response {
|
||||
// edits: Vec::new(),
|
||||
// pre_rendered: Some(content),
|
||||
// }
|
||||
// }
|
||||
// None => {
|
||||
// //
|
||||
// let edits = {
|
||||
// // let mut edits = Vec::new();
|
||||
// todo!()
|
||||
// // lock.rebuild(&mut real, &mut edits).unwrap();
|
||||
// // edits
|
||||
// };
|
||||
// Response {
|
||||
// edits,
|
||||
// pre_rendered: None,
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
// serde_json::to_value(&response).unwrap()
|
||||
// };
|
||||
// // Return the edits into the webview runtime
|
||||
// Some(RpcResponse::new_result(req.id.take(), Some(edits)))
|
||||
|
||||
// log::debug!("User event received");
|
||||
// // let registry = registry.clone();
|
||||
// let vdom = vdom.clone();
|
||||
// let response = async_std::task::block_on(async move {
|
||||
// let mut lock = vdom.write().unwrap();
|
||||
// // let mut reg_lock = registry.write().unwrap();
|
||||
// // a deserialized event
|
||||
// let data = req.params.unwrap();
|
||||
// log::debug!("Data: {:#?}", data);
|
||||
// let event = trigger_from_serialized(data);
|
||||
// // lock.queue_event(event);
|
||||
// // Create the thin wrapper around the registry to collect the edits into
|
||||
// let mut real = dom::WebviewDom::new();
|
||||
// // Serialize the edit stream
|
||||
// //
|
||||
// let mut edits = Vec::new();
|
||||
// // lock.run(&mut real, &mut edits)
|
||||
// // .await
|
||||
// // .expect("failed to progress");
|
||||
// let response = Response {
|
||||
// edits,
|
||||
// pre_rendered: None,
|
||||
// };
|
||||
// let response = serde_json::to_value(&response).unwrap();
|
||||
// // Give back the registry into its slot
|
||||
// // *reg_lock = Some(real.consume());
|
||||
// // Return the edits into the webview runtime
|
||||
// Some(RpcResponse::new_result(req.id.take(), Some(response)))
|
||||
// });
|
||||
// response
|
||||
// // spawn a task to clean up the garbage
|
||||
@@ -0,0 +1,51 @@
|
||||
pub fn set_up_logging(enabled: bool) {
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
|
||||
if !enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
// configure colors for the whole line
|
||||
let colors_line = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
.warn(Color::Yellow)
|
||||
// we actually don't need to specify the color for debug and info, they are white by default
|
||||
.info(Color::White)
|
||||
.debug(Color::White)
|
||||
// depending on the terminals color scheme, this is the same as the background color
|
||||
.trace(Color::BrightBlack);
|
||||
|
||||
// configure colors for the name of the level.
|
||||
// since almost all of them are the same as the color for the whole line, we
|
||||
// just clone `colors_line` and overwrite our changes
|
||||
let colors_level = colors_line.clone().info(Color::Green);
|
||||
// here we set up our fern Dispatch
|
||||
|
||||
// when running tests in batch, the logger is re-used, so ignore the logger error
|
||||
let _ = fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{color_line}[{level}{color_line}] {message}\x1B[0m",
|
||||
color_line = format_args!(
|
||||
"\x1B[{}m",
|
||||
colors_line.get_color(&record.level()).to_fg_str()
|
||||
),
|
||||
level = colors_level.color(record.level()),
|
||||
message = message,
|
||||
));
|
||||
})
|
||||
// set the default log level. to filter out verbose log messages from dependencies, set
|
||||
// this to Warn and overwrite the log level for your crate.
|
||||
.level(log::LevelFilter::Debug)
|
||||
// .level(log::LevelFilter::Warn)
|
||||
// change log levels for individual modules. Note: This looks for the record's target
|
||||
// field which defaults to the module path but can be overwritten with the `target`
|
||||
// parameter:
|
||||
// `info!(target="special_target", "This log message is about special_target");`
|
||||
// .level_for("dioxus", log::LevelFilter::Debug)
|
||||
// .level_for("dioxus", log::LevelFilter::Info)
|
||||
// .level_for("pretty_colored", log::LevelFilter::Trace)
|
||||
// output to stdout
|
||||
.chain(std::io::stdout())
|
||||
.apply();
|
||||
}
|
||||
Reference in new issue
Block a user