feat: move webview to wry

This commit is contained in:
Jonathan Kelley committed 2021-07-08 12:01:31 -04:00
1 parent 2547da36a0
commit 99d94b69ab
11 files changed
+419 -96

No files matched your search

+5 -5
View File
@@ -8,13 +8,13 @@
Dioxus is a portable, performant, and ergonomic framework for building cross-platform user experiences in Rust. Dioxus is a portable, performant, and ergonomic framework for building cross-platform user experiences in Rust.
```rust ```rust
fn Example(cx: Context<()>) -> VNode { fn App(cx: Context<()>) -> VNode {
let name = use_state(cx, || "..?"); let mut count = use_state(cx, || 0);
cx.render(rsx! { cx.render(rsx! {
h1 { "Hello, {name}" } h1 { "Hi-Five counter: {count}" }
button { "?", onclick: move |_| name.set("world!")} button { onclick: move |_| count += 1, "Up high!" }
button { "?", onclick: move |_| name.set("Dioxus 🎉")} button { onclick: move |_| count -= 1, "Down low!" }
}) })
}; };
``` ```
+5 -2
View File
@@ -21,8 +21,11 @@ use dioxus::prelude::*;
const STYLE: &str = include_str!("./assets/calculator.css"); const STYLE: &str = include_str!("./assets/calculator.css");
fn main() { fn main() {
dioxus::desktop::launch(App, |cfg| { dioxus::desktop::launch(App, |cfg| {
cfg.title("Calculator Demo").resizable(false).size(350, 550) cfg.with_title("Calculator Demo")
}); .with_resizable(true)
.with_skip_taskbar(true)
})
.expect("failed to launch dioxus app");
} }
enum Operator { enum Operator {
+3 -6
View File
@@ -20,12 +20,9 @@ static App: FC<()> = |cx| {
cx.render(rsx! { cx.render(rsx! {
div { div {
h1 { "Dioxus Desktop Demo" } h1 { "Hifive counter: {count}" }
p { "Count is {count}" } button { onclick: move |_| count += 1, "Up high!" }
button { button { onclick: move |_| count -= 1, "Down low!" }
"Click to increment"
onclick: move |_| count += 1
}
} }
}) })
}; };
+20 -3
View File
@@ -102,7 +102,10 @@ where
impl<'a, T: 'static> UseState<'a, T> { impl<'a, T: 'static> UseState<'a, T> {
/// Tell the Dioxus Scheduler that we need to be processed /// Tell the Dioxus Scheduler that we need to be processed
pub fn needs_update(&self) { pub fn needs_update(&self) {
(self.inner.callback)(); if !self.inner.update_scheuled.get() {
self.inner.update_scheuled.set(true);
(self.inner.callback)();
}
} }
pub fn set(&self, new_val: T) { pub fn set(&self, new_val: T) {
@@ -143,7 +146,7 @@ impl<'a, T: 'static> std::ops::Deref for UseState<'a, T> {
} }
} }
use std::ops::{Add, AddAssign}; use std::ops::{Add, AddAssign, Sub, SubAssign};
impl<'a, T: Copy + Add<T, Output = T>> Add<T> for UseState<'a, T> { impl<'a, T: Copy + Add<T, Output = T>> Add<T> for UseState<'a, T> {
type Output = T; type Output = T;
@@ -156,6 +159,18 @@ impl<'a, T: Copy + Add<T, Output = T>> AddAssign<T> for UseState<'a, T> {
self.set(self.inner.current_val.add(rhs)); self.set(self.inner.current_val.add(rhs));
} }
} }
impl<'a, T: Copy + Sub<T, Output = T>> Sub<T> for UseState<'a, T> {
type Output = T;
fn sub(self, rhs: T) -> Self::Output {
self.inner.current_val.sub(rhs)
}
}
impl<'a, T: Copy + Sub<T, Output = T>> SubAssign<T> for UseState<'a, T> {
fn sub_assign(&mut self, rhs: T) {
self.set(self.inner.current_val.sub(rhs));
}
}
// enable displaty for the handle // enable displaty for the handle
impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> { impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> {
@@ -165,6 +180,7 @@ impl<'a, T: 'static + Display> std::fmt::Display for UseState<'a, T> {
} }
struct UseStateInner<T: 'static> { struct UseStateInner<T: 'static> {
current_val: T, current_val: T,
update_scheuled: Cell<bool>,
callback: Rc<dyn Fn()>, callback: Rc<dyn Fn()>,
wip: RefCell<Option<T>>, wip: RefCell<Option<T>>,
} }
@@ -213,9 +229,10 @@ pub fn use_state<'a, 'c, T: 'static, F: FnOnce() -> T, P>(
current_val: initial_state_fn(), current_val: initial_state_fn(),
callback: cx.schedule_update(), callback: cx.schedule_update(),
wip: RefCell::new(None), wip: RefCell::new(None),
update_scheuled: Cell::new(false),
}, },
move |hook| { move |hook| {
log::debug!("addr of hook: {:#?}", hook as *const _); hook.update_scheuled.set(false);
let mut new_val = hook.wip.borrow_mut(); let mut new_val = hook.wip.borrow_mut();
if new_val.is_some() { if new_val.is_some() {
hook.current_val = new_val.take().unwrap(); hook.current_val = new_val.take().unwrap();
+1 -1
View File
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
/// network or through FFI boundaries. /// network or through FFI boundaries.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum DomEdits<'bump> { pub enum DomEdit<'bump> {
PushRoot { PushRoot {
root: u64, root: u64,
}, },
+23
View File
@@ -0,0 +1,23 @@
fn main() {
dioxus_webview::launch(App, |f| f.with_focus().with_maximized(true)).expect("Failed");
}
static App: FC<()> = |cx| {
//
cx.render(rsx!(
div {
"hello world!"
}
))
};
use dioxus_core as dioxus;
use dioxus_core::prelude::*;
mod dioxus_elements {
use super::*;
pub struct div;
impl DioxusElement for div {
const TAG_NAME: &'static str = "div";
const NAME_SPACE: Option<&'static str> = None;
}
}
+99
View File
@@ -0,0 +1,99 @@
class OPTABLE {
PushRoot(self, edit) {
const id = edit.root;
const node = self.nodes[id];
self.stack.push(node);
}
AppendChild(self, edit) {
// todo: prevent merging of text nodes
const node = self.pop();
self.top().appendChild(node);
}
ReplaceWith(self, edit) {
const newNode = self.pop();
const oldNode = self.pop();
oldNode.replaceWith(newNode);
self.stack.push(newNode);
}
Remove(self, edit) {
const node = self.stack.pop();
node.remove();
}
RemoveAllChildren(self, edit) {
// todo - we never actually call this one
}
CreateTextNode(self, edit) {
self.stack.push(document.createTextNode(edit.text));
}
CreateElement(self, edit) {
const tagName = edit.tag;
console.log(`creating element! ${edit}`);
self.stack.push(document.createElement(tagName));
}
CreateElementNs(self, edit) {
self.stack.push(document.createElementNS(edit.ns, edit.tag));
}
CreatePlaceholder(self, edit) {
self.stack.push(document.createElement("pre"));
}
NewEventListener(self, edit) {
// todo
}
RemoveEventListener(self, edit) {
// todo
}
SetText(self, edit) {
self.top().textContent = edit.text;
}
SetAttribute(self, edit) {
const name = edit.field;
const value = edit.value;
const node = self.top(self.stack);
node.setAttribute(name, value);
// Some attributes are "volatile" and don't work through `setAttribute`.
if ((name === "value", self)) {
node.value = value;
}
if ((name === "checked", self)) {
node.checked = true;
}
if ((name === "selected", self)) {
node.selected = true;
}
}
RemoveAttribute(self, edit) {
const name = edit.field;
const node = self.top(self.stack);
node.removeAttribute(name);
// Some attributes are "volatile" and don't work through `removeAttribute`.
if ((name === "value", self)) {
node.value = null;
}
if ((name === "checked", self)) {
node.checked = false;
}
if ((name === "selected", self)) {
node.selected = false;
}
}
}
// const op_table = new OPTABLE();
// const interpreter = new Interpreter(window.document.body);
// function EditListReceived(rawEditList) {
// let editList = JSON.parse(rawEditList);
// console.warn("hnelllo");
// editList.forEach(function (edit, index) {
// console.log(edit);
// op_table[edit.type](interpreter, edit);
// });
// }
// async function rinalize() {
// console.log("initialize...");
// let edits = await rpc.call("initiate");
// console.error(edits);
// }
+81
View File
@@ -0,0 +1,81 @@
class OPTABLE {
PushRoot(self, edit) {
const id = edit.root;
const node = self.nodes[id];
self.stack.push(node);
}
AppendChild(self, edit) {
// todo: prevent merging of text nodes
const node = self.pop();
self.top().appendChild(node);
}
ReplaceWith(self, edit) {
const newNode = self.pop();
const oldNode = self.pop();
oldNode.replaceWith(newNode);
self.stack.push(newNode);
}
Remove(self, edit) {
const node = self.stack.pop();
node.remove();
}
RemoveAllChildren(self, edit) {
// todo - we never actually call this one
}
CreateTextNode(self, edit) {
self.stack.push(document.createTextNode(edit.text));
}
CreateElement(self, edit) {
const tagName = edit.tag;
console.log(`creating element! ${edit}`);
self.stack.push(document.createElement(tagName));
}
CreateElementNs(self, edit) {
self.stack.push(document.createElementNS(edit.ns, edit.tag));
}
CreatePlaceholder(self, edit) {
self.stack.push(document.createElement("pre"));
}
NewEventListener(self, edit) {
// todo
}
RemoveEventListener(self, edit) {
// todo
}
SetText(self, edit) {
self.top().textContent = edit.text;
}
SetAttribute(self, edit) {
const name = edit.field;
const value = edit.value;
const node = self.top(self.stack);
node.setAttribute(name, value);
// Some attributes are "volatile" and don't work through `setAttribute`.
if ((name === "value", self)) {
node.value = value;
}
if ((name === "checked", self)) {
node.checked = true;
}
if ((name === "selected", self)) {
node.selected = true;
}
}
RemoveAttribute(self, edit) {
const name = edit.field;
const node = self.top(self.stack);
node.removeAttribute(name);
// Some attributes are "volatile" and don't work through `removeAttribute`.
if ((name === "value", self)) {
node.value = null;
}
if ((name === "checked", self)) {
node.checked = false;
}
if ((name === "selected", self)) {
node.selected = false;
}
}
}
+3 -3
View File
@@ -4,13 +4,13 @@ use dioxus_core as dioxus;
use dioxus_core::prelude::*; use dioxus_core::prelude::*;
use dioxus_core::{ use dioxus_core::{
diff::RealDom, diff::RealDom,
serialize::DomEdits, serialize::DomEdit,
virtual_dom::{RealDomNode, VirtualDom}, virtual_dom::{RealDomNode, VirtualDom},
}; };
use DomEdits::*; use DomEdit::*;
pub struct WebviewDom<'bump> { pub struct WebviewDom<'bump> {
pub edits: Vec<DomEdits<'bump>>, pub edits: Vec<DomEdit<'bump>>,
pub node_counter: u64, pub node_counter: u64,
} }
impl WebviewDom<'_> { impl WebviewDom<'_> {
+23 -15
View File
@@ -1,20 +1,22 @@
<!-- a js-only interpreter for the dioxus patch stream :) -->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" /> <!-- <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" /> -->
<meta charset="UTF-8" />
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
</head> </head>
<body> <body>
<div></div> <div></div>
</body> </body>
<script> <script>
class Interpreter { class Interpreter {
constructor(root) { constructor(root) {
this.stack = [root]; this.stack = [root];
this.listeners = {};
this.lastNodeWasText = false; this.lastNodeWasText = false;
this.nodes = { this.nodes = {
0: root 0: root
@@ -30,6 +32,7 @@
} }
} }
class OPTABLE { class OPTABLE {
PushRoot(self, edit) { PushRoot(self, edit) {
const id = edit.root; const id = edit.root;
@@ -37,7 +40,6 @@
self.stack.push(node); self.stack.push(node);
} }
AppendChild(self, edit) { AppendChild(self, edit) {
// todo: prevent merging of text nodes
const node = self.pop(); const node = self.pop();
self.top().appendChild(node); self.top().appendChild(node);
} }
@@ -52,14 +54,13 @@
node.remove(); node.remove();
} }
RemoveAllChildren(self, edit) { RemoveAllChildren(self, edit) {
// todo - we never actually call this one
} }
CreateTextNode(self, edit) { CreateTextNode(self, edit) {
self.stack.push(document.createTextNode(edit.text)); self.stack.push(document.createTextNode(edit.text));
} }
CreateElement(self, edit) { CreateElement(self, edit) {
const tagName = edit.tag; const tagName = edit.tag;
console.log(`creating element! ${edit}`); console.log(`creating element: `, edit);
self.stack.push(document.createElement(tagName)); self.stack.push(document.createElement(tagName));
} }
CreateElementNs(self, edit) { CreateElementNs(self, edit) {
@@ -69,10 +70,8 @@
self.stack.push(document.createElement("pre")); self.stack.push(document.createElement("pre"));
} }
NewEventListener(self, edit) { NewEventListener(self, edit) {
// todo
} }
RemoveEventListener(self, edit) { RemoveEventListener(self, edit) {
// todo
} }
SetText(self, edit) { SetText(self, edit) {
self.top().textContent = edit.text; self.top().textContent = edit.text;
@@ -83,7 +82,6 @@
const node = self.top(self.stack); const node = self.top(self.stack);
node.setAttribute(name, value); node.setAttribute(name, value);
// Some attributes are "volatile" and don't work through `setAttribute`.
if ((name === "value", self)) { if ((name === "value", self)) {
node.value = value; node.value = value;
} }
@@ -99,7 +97,6 @@
const node = self.top(self.stack); const node = self.top(self.stack);
node.removeAttribute(name); node.removeAttribute(name);
// Some attributes are "volatile" and don't work through `removeAttribute`.
if ((name === "value", self)) { if ((name === "value", self)) {
node.value = null; node.value = null;
} }
@@ -110,11 +107,9 @@
node.selected = false; node.selected = false;
} }
} }
} }
const op_table = new OPTABLE();
const interpreter = new Interpreter(window.document.body);
function EditListReceived(rawEditList) { function EditListReceived(rawEditList) {
let editList = JSON.parse(rawEditList); let editList = JSON.parse(rawEditList);
@@ -125,7 +120,20 @@
}); });
} }
external.invoke("initiate"); const op_table = new OPTABLE();
const interpreter = new Interpreter(window.document.body);
async function initialize() {
const reply = await rpc.call('initiate');
console.log(reply);
reply.forEach(function (edit, index) {
console.log(edit);
op_table[edit.type](interpreter, edit);
});
}
console.log("initializing...");
initialize();
</script> </script>
</html> </html>
+156 -61
View File
@@ -1,24 +1,28 @@
use std::borrow::BorrowMut;
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::sync::Arc; use std::sync::{Arc, RwLock};
use dioxus_core::prelude::*;
use dioxus_core::virtual_dom::VirtualDom; use dioxus_core::virtual_dom::VirtualDom;
use web_view::{escape, Handle}; use dioxus_core::{prelude::*, serialize::DomEdit};
use web_view::{WVResult, WebView, WebViewBuilder}; use wry::{
application::window::{Window, WindowBuilder},
webview::{RpcRequest, RpcResponse},
};
mod dom; mod dom;
static HTML_CONTENT: &'static str = include_str!("./index.html"); static HTML_CONTENT: &'static str = include_str!("./index.html");
pub fn launch( pub fn launch(
root: FC<()>, root: FC<()>,
builder: impl FnOnce(DioxusWebviewBuilder) -> DioxusWebviewBuilder, builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
launch_with_props(root, (), builder) launch_with_props(root, (), builder)
} }
pub fn launch_with_props<P: Properties + 'static>( pub fn launch_with_props<P: Properties + 'static>(
root: FC<P>, root: FC<P>,
props: P, props: P,
builder: impl FnOnce(DioxusWebviewBuilder) -> DioxusWebviewBuilder, builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
WebviewRenderer::run(root, props, builder) WebviewRenderer::run(root, props, builder)
} }
@@ -29,85 +33,125 @@ pub struct WebviewRenderer<T> {
/// The root component used to render the Webview /// The root component used to render the Webview
root: FC<T>, root: FC<T>,
} }
enum RpcEvent<'a> {
enum InnerEvent { Initialize {
Initiate(Handle<()>), //
edits: Vec<DomEdit<'a>>,
},
} }
impl<T: Properties + 'static> WebviewRenderer<T> { impl<T: Properties + 'static> WebviewRenderer<T> {
pub fn run( pub fn run(
root: FC<T>, root: FC<T>,
props: T, props: T,
user_builder: impl FnOnce(DioxusWebviewBuilder) -> DioxusWebviewBuilder, user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let (sender, receiver) = channel::<InnerEvent>(); use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
let DioxusWebviewBuilder { let event_loop = EventLoop::new();
title,
width,
height,
resizable,
debug,
frameless,
visible,
min_width,
min_height,
} = user_builder(DioxusWebviewBuilder::new());
let mut view = web_view::builder() let window = user_builder(WindowBuilder::new()).build(&event_loop)?;
.invoke_handler(|view, arg| {
let handle = view.handle();
sender
.send(InnerEvent::Initiate(handle))
.expect("should not fail");
Ok(())
})
.content(web_view::Content::Html(HTML_CONTENT))
.user_data(())
.title(title)
.size(width, height)
.resizable(resizable)
.debug(debug)
.frameless(frameless)
.visible(visible)
.min_size(min_width, min_height)
.build()
.unwrap();
let mut vdom = VirtualDom::new_with_props(root, props); let mut vdom = VirtualDom::new_with_props(root, props);
let mut real_dom = dom::WebviewDom::new(); let mut real_dom = dom::WebviewDom::new();
vdom.rebuild(&mut real_dom)?; vdom.rebuild(&mut real_dom)?;
let ref_edits = Arc::new(serde_json::to_string(&real_dom.edits)?); let edits = Arc::new(RwLock::new(Some(serde_json::to_value(real_dom.edits)?)));
loop { // let ref_edits = Arc::new(serde_json::to_string(&real_dom.edits)?);
view.step()
.expect("should not fail")
.expect("should not fail");
std::thread::sleep(std::time::Duration::from_millis(15));
if let Ok(event) = receiver.try_recv() { let handler = move |window: &Window, mut req: RpcRequest| {
if let InnerEvent::Initiate(handle) = event { //
let editlist = ref_edits.clone(); let d = edits.clone();
handle match req.method.as_str() {
.dispatch(move |view| { "initiate" => {
let escaped = escape(&editlist); let mut ed = d.write().unwrap();
view.eval(&format!("EditListReceived({});", escaped)) let edits = match ed.as_mut() {
}) Some(ed) => Some(ed.take()),
.expect("Dispatch failed"); None => None,
};
Some(RpcResponse::new_result(req.id.take(), edits))
}
_ => todo!("this message failed"),
}
};
let webview = WebViewBuilder::new(window)?
.with_url(&format!("data:text/html,{}", HTML_CONTENT))?
.with_rpc_handler(handler)
.build()?;
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent { event, .. } => {
//
match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
_ => {}
}
}
_ => {
// let _ = webview.resize();
} }
} }
} });
// let mut view = web_view::builder()
// .invoke_handler(|view, arg| {
// let handle = view.handle();
// sender
// .send(InnerEvent::Initiate(handle))
// .expect("should not fail");
// Ok(())
// })
// .content(web_view::Content::Html(HTML_CONTENT))
// .user_data(())
// .title(title)
// .size(width, height)
// .resizable(resizable)
// .debug(debug)
// .frameless(frameless)
// .visible(visible)
// .min_size(min_width, min_height)
// .build()
// .unwrap();
// loop {
// view.step()
// .expect("should not fail")
// .expect("should not fail");
// std::thread::sleep(std::time::Duration::from_millis(15));
// if let Ok(event) = receiver.try_recv() {
// if let InnerEvent::Initiate(handle) = event {
// let editlist = ref_edits.clone();
// handle
// .dispatch(move |view| {
// let escaped = escape(&editlist);
// view.eval(&format!("EditListReceived({});", escaped))
// })
// .expect("Dispatch failed");
// }
// }
// }
} }
/// Create a new text-renderer instance from a functional component root. /// Create a new text-renderer instance from a functional component root.
/// Automatically progresses the creation of the VNode tree to completion. /// Automatically progresses the creation of the VNode tree to completion.
/// ///
/// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom` /// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom`
pub fn new(root: FC<T>, builder: impl FnOnce() -> WVResult<WebView<'static, ()>>) -> Self { // pub fn new(root: FC<T>, builder: impl FnOnce() -> WVResult<WebView<'static, ()>>) -> Self {
Self { root } // Self { root }
} // }
/// Create a new text renderer from an existing Virtual DOM. /// Create a new text renderer from an existing Virtual DOM.
/// This will progress the existing VDom's events to completion. /// This will progress the existing VDom's events to completion.
@@ -126,6 +170,57 @@ impl<T: Properties + 'static> WebviewRenderer<T> {
} }
} }
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Serialize, Deserialize)]
struct MessageParameters {
message: String,
}
fn HANDLER(window: &Window, mut req: RpcRequest) -> Option<RpcResponse> {
use wry::{
application::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Fullscreen, Window, WindowBuilder},
},
webview::{RpcRequest, RpcResponse, WebViewBuilder},
};
let mut response = None;
if &req.method == "fullscreen" {
if let Some(params) = req.params.take() {
if let Ok(mut args) = serde_json::from_value::<Vec<bool>>(params) {
if !args.is_empty() {
if args.swap_remove(0) {
window.set_fullscreen(Some(Fullscreen::Borderless(None)));
} else {
window.set_fullscreen(None);
}
};
response = Some(RpcResponse::new_result(req.id.take(), None));
}
}
} else if &req.method == "send-parameters" {
if let Some(params) = req.params.take() {
if let Ok(mut args) = serde_json::from_value::<Vec<MessageParameters>>(params) {
let result = if !args.is_empty() {
let msg = args.swap_remove(0);
Some(Value::String(format!("Hello, {}!", msg.message)))
} else {
// NOTE: in the real-world we should send an error response here!
None
};
// Must always send a response as this is a `call()`
response = Some(RpcResponse::new_result(req.id.take(), result));
}
}
}
response
}
pub struct DioxusWebviewBuilder<'a> { pub struct DioxusWebviewBuilder<'a> {
pub(crate) title: &'a str, pub(crate) title: &'a str,
pub(crate) width: i32, pub(crate) width: i32,