Procházet zdrojové kódy

feat: move webview to wry

Jonathan Kelley před 4 roky
rodič
revize
99d94b6

+ 5 - 5
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!" }
     })
 };
 ```

+ 5 - 2
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 {

+ 3 - 6
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!" }
         }
     })
 };

+ 20 - 3
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<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();

+ 1 - 1
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,
     },

+ 23 - 0
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;
+    }
+}

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

+ 81 - 0
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;
+    }
+  }
+}

+ 3 - 3
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<DomEdits<'bump>>,
+    pub edits: Vec<DomEdit<'bump>>,
     pub node_counter: u64,
 }
 impl WebviewDom<'_> {

+ 23 - 15
packages/webview/src/index.html

@@ -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>

+ 159 - 64
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<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 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 event_loop = EventLoop::new();
+
+        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)?);
-
-        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");
+        let edits = Arc::new(RwLock::new(Some(serde_json::to_value(real_dom.edits)?)));
+
+        // let ref_edits = Arc::new(serde_json::to_string(&real_dom.edits)?);
+
+        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,