From 99d94b69aba192be7e41ebe891aca4bb4f6cae88 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 8 Jul 2021 12:01:31 -0400 Subject: [PATCH] feat: move webview to wry --- README.md | 10 +- examples/model.rs | 7 +- examples/webview.rs | 9 +- packages/core/src/hooks.rs | 23 +++- packages/core/src/serialize.rs | 2 +- packages/webview/examples/test.rs | 23 ++++ packages/webview/src/blah.html | 99 ++++++++++++++ packages/webview/src/blah.js | 81 +++++++++++ packages/webview/src/dom.rs | 6 +- packages/webview/src/index.html | 38 +++--- packages/webview/src/lib.rs | 217 +++++++++++++++++++++--------- 11 files changed, 419 insertions(+), 96 deletions(-) create mode 100644 packages/webview/examples/test.rs create mode 100644 packages/webview/src/blah.html create mode 100644 packages/webview/src/blah.js diff --git a/README.md b/README.md index 408c533fd..cddbd2beb 100644 --- a/README.md +++ b/README.md @@ -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!" } }) }; ``` diff --git a/examples/model.rs b/examples/model.rs index 798fe2ed6..6ff982143 100644 --- a/examples/model.rs +++ b/examples/model.rs @@ -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 { diff --git a/examples/webview.rs b/examples/webview.rs index 4d962fd6f..f55e87ff8 100644 --- a/examples/webview.rs +++ b/examples/webview.rs @@ -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!" } } }) }; diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs index 78f90ba57..fa376bd6e 100644 --- a/packages/core/src/hooks.rs +++ b/packages/core/src/hooks.rs @@ -102,7 +102,10 @@ where impl<'a, T: 'static> UseState<'a, T> { /// Tell the Dioxus Scheduler that we need to be processed 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) { @@ -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> Add for UseState<'a, T> { type Output = T; @@ -156,6 +159,18 @@ impl<'a, T: Copy + Add> AddAssign for UseState<'a, T> { self.set(self.inner.current_val.add(rhs)); } } +impl<'a, T: Copy + Sub> Sub 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> SubAssign 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 { current_val: T, + update_scheuled: Cell, callback: Rc, wip: RefCell>, } @@ -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(); diff --git a/packages/core/src/serialize.rs b/packages/core/src/serialize.rs index b04e321fa..681660823 100644 --- a/packages/core/src/serialize.rs +++ b/packages/core/src/serialize.rs @@ -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, }, diff --git a/packages/webview/examples/test.rs b/packages/webview/examples/test.rs new file mode 100644 index 000000000..fe1542766 --- /dev/null +++ b/packages/webview/examples/test.rs @@ -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; + } +} diff --git a/packages/webview/src/blah.html b/packages/webview/src/blah.html new file mode 100644 index 000000000..a02ce6398 --- /dev/null +++ b/packages/webview/src/blah.html @@ -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); +// } diff --git a/packages/webview/src/blah.js b/packages/webview/src/blah.js new file mode 100644 index 000000000..062b8596d --- /dev/null +++ b/packages/webview/src/blah.js @@ -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; + } + } +} diff --git a/packages/webview/src/dom.rs b/packages/webview/src/dom.rs index b360e9b68..05153bd76 100644 --- a/packages/webview/src/dom.rs +++ b/packages/webview/src/dom.rs @@ -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>, + pub edits: Vec>, pub node_counter: u64, } impl WebviewDom<'_> { diff --git a/packages/webview/src/index.html b/packages/webview/src/index.html index 01a7e9982..d6d96ad80 100644 --- a/packages/webview/src/index.html +++ b/packages/webview/src/index.html @@ -1,20 +1,22 @@ - - - - + + + +
+ diff --git a/packages/webview/src/lib.rs b/packages/webview/src/lib.rs index e5be7fad4..5760d6eb5 100644 --- a/packages/webview/src/lib.rs +++ b/packages/webview/src/lib.rs @@ -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( root: FC

, 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 { /// The root component used to render the Webview root: FC, } - -enum InnerEvent { - Initiate(Handle<()>), +enum RpcEvent<'a> { + Initialize { + // + edits: Vec>, + }, } impl WebviewRenderer { pub fn run( root: FC, props: T, - user_builder: impl FnOnce(DioxusWebviewBuilder) -> DioxusWebviewBuilder, + user_builder: impl FnOnce(WindowBuilder) -> WindowBuilder, ) -> anyhow::Result<()> { - let (sender, receiver) = channel::(); + 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, builder: impl FnOnce() -> WVResult>) -> Self { - Self { root } - } + // pub fn new(root: FC, builder: impl FnOnce() -> WVResult>) -> 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 WebviewRenderer { } } +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Serialize, Deserialize)] +struct MessageParameters { + message: String, +} + +fn HANDLER(window: &Window, mut req: RpcRequest) -> Option { + 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::>(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::>(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,