feat: move webview to wry

This commit is contained in:
Jonathan Kelley 2021-07-08 12:01:31 -04:00
parent 2547da36a0
commit 99d94b69ab
11 changed files with 419 additions and 96 deletions

View file

@ -8,13 +8,13 @@
Dioxus is a portable, performant, and ergonomic framework for building cross-platform user experiences in Rust.
```rust
fn Example(cx: Context<()>) -> VNode {
let name = use_state(cx, || "..?");
fn App(cx: Context<()>) -> VNode {
let mut count = use_state(cx, || 0);
cx.render(rsx! {
h1 { "Hello, {name}" }
button { "?", onclick: move |_| name.set("world!")}
button { "?", onclick: move |_| name.set("Dioxus 🎉")}
h1 { "Hi-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
})
};
```

View file

@ -21,8 +21,11 @@ use dioxus::prelude::*;
const STYLE: &str = include_str!("./assets/calculator.css");
fn main() {
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 {

View file

@ -20,12 +20,9 @@ static App: FC<()> = |cx| {
cx.render(rsx! {
div {
h1 { "Dioxus Desktop Demo" }
p { "Count is {count}" }
button {
"Click to increment"
onclick: move |_| count += 1
}
h1 { "Hifive counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
}
})
};

View file

@ -102,8 +102,11 @@ where
impl<'a, T: 'static> UseState<'a, T> {
/// Tell the Dioxus Scheduler that we need to be processed
pub fn needs_update(&self) {
if !self.inner.update_scheuled.get() {
self.inner.update_scheuled.set(true);
(self.inner.callback)();
}
}
pub fn set(&self, new_val: T) {
self.needs_update();
@ -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> {
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));
}
}
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
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> {
current_val: T,
update_scheuled: Cell<bool>,
callback: Rc<dyn Fn()>,
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(),
callback: cx.schedule_update(),
wip: RefCell::new(None),
update_scheuled: Cell::new(false),
},
move |hook| {
log::debug!("addr of hook: {:#?}", hook as *const _);
hook.update_scheuled.set(false);
let mut new_val = hook.wip.borrow_mut();
if new_val.is_some() {
hook.current_val = new_val.take().unwrap();

View file

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
/// network or through FFI boundaries.
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum DomEdits<'bump> {
pub enum DomEdit<'bump> {
PushRoot {
root: u64,
},

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;
}
}

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);
// }

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;
}
}
}

View file

@ -4,13 +4,13 @@ use dioxus_core as dioxus;
use dioxus_core::prelude::*;
use dioxus_core::{
diff::RealDom,
serialize::DomEdits,
serialize::DomEdit,
virtual_dom::{RealDomNode, VirtualDom},
};
use DomEdits::*;
use DomEdit::*;
pub struct WebviewDom<'bump> {
pub edits: Vec<DomEdits<'bump>>,
pub edits: Vec<DomEdit<'bump>>,
pub node_counter: u64,
}
impl WebviewDom<'_> {

View file

@ -1,20 +1,22 @@
<!-- a js-only interpreter for the dioxus patch stream :) -->
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta charset="UTF-8" />
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />
<!-- <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" /> -->
</head>
<body>
<div></div>
</body>
<script>
class Interpreter {
constructor(root) {
this.stack = [root];
this.listeners = {};
this.lastNodeWasText = false;
this.nodes = {
0: root
@ -30,6 +32,7 @@
}
}
class OPTABLE {
PushRoot(self, edit) {
const id = edit.root;
@ -37,7 +40,6 @@
self.stack.push(node);
}
AppendChild(self, edit) {
// todo: prevent merging of text nodes
const node = self.pop();
self.top().appendChild(node);
}
@ -52,14 +54,13 @@
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}`);
console.log(`creating element: `, edit);
self.stack.push(document.createElement(tagName));
}
CreateElementNs(self, edit) {
@ -69,10 +70,8 @@
self.stack.push(document.createElement("pre"));
}
NewEventListener(self, edit) {
// todo
}
RemoveEventListener(self, edit) {
// todo
}
SetText(self, edit) {
self.top().textContent = edit.text;
@ -83,7 +82,6 @@
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;
}
@ -99,7 +97,6 @@
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;
}
@ -110,11 +107,9 @@
node.selected = false;
}
}
}
const op_table = new OPTABLE();
const interpreter = new Interpreter(window.document.body);
function EditListReceived(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>
</html>

View file

@ -1,24 +1,28 @@
use std::borrow::BorrowMut;
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 web_view::{escape, Handle};
use web_view::{WVResult, WebView, WebViewBuilder};
use dioxus_core::{prelude::*, serialize::DomEdit};
use wry::{
application::window::{Window, WindowBuilder},
webview::{RpcRequest, RpcResponse},
};
mod dom;
static HTML_CONTENT: &'static str = include_str!("./index.html");
pub fn launch(
root: FC<()>,
builder: impl FnOnce(DioxusWebviewBuilder) -> DioxusWebviewBuilder,
builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
) -> anyhow::Result<()> {
launch_with_props(root, (), builder)
}
pub fn launch_with_props<P: Properties + 'static>(
root: FC<P>,
props: P,
builder: impl FnOnce(DioxusWebviewBuilder) -> DioxusWebviewBuilder,
builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
) -> anyhow::Result<()> {
WebviewRenderer::run(root, props, builder)
}
@ -29,85 +33,125 @@ pub struct WebviewRenderer<T> {
/// The root component used to render the Webview
root: FC<T>,
}
enum InnerEvent {
Initiate(Handle<()>),
enum RpcEvent<'a> {
Initialize {
//
edits: Vec<DomEdit<'a>>,
},
}
impl<T: Properties + 'static> WebviewRenderer<T> {
pub fn run(
root: FC<T>,
props: T,
user_builder: impl FnOnce(DioxusWebviewBuilder) -> DioxusWebviewBuilder,
user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder,
) -> anyhow::Result<()> {
let (sender, receiver) = channel::<InnerEvent>();
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::WindowBuilder,
},
webview::WebViewBuilder,
};
let DioxusWebviewBuilder {
title,
width,
height,
resizable,
debug,
frameless,
visible,
min_width,
min_height,
} = user_builder(DioxusWebviewBuilder::new());
let event_loop = EventLoop::new();
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();
let window = user_builder(WindowBuilder::new()).build(&event_loop)?;
let mut vdom = VirtualDom::new_with_props(root, props);
let mut real_dom = dom::WebviewDom::new();
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 {
view.step()
.expect("should not fail")
.expect("should not fail");
std::thread::sleep(std::time::Duration::from_millis(15));
// let ref_edits = Arc::new(serde_json::to_string(&real_dom.edits)?);
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");
let handler = move |window: &Window, mut req: RpcRequest| {
//
let d = edits.clone();
match req.method.as_str() {
"initiate" => {
let mut ed = d.write().unwrap();
let edits = match ed.as_mut() {
Some(ed) => Some(ed.take()),
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.
/// 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`
pub fn new(root: FC<T>, builder: impl FnOnce() -> WVResult<WebView<'static, ()>>) -> Self {
Self { root }
}
// pub fn new(root: FC<T>, builder: impl FnOnce() -> WVResult<WebView<'static, ()>>) -> Self {
// Self { root }
// }
/// Create a new text renderer from an existing Virtual DOM.
/// 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(crate) title: &'a str,
pub(crate) width: i32,