1
0
Эх сурвалжийг харах

Resume window position/size, watch cargo/dioxus tomls, fix css reverting during hotreloading, allow menubar events to be captured from within dioxus (#2116)

* Fix: css hotreloading being invalidated, watcher not watching cargo/dioxus tomls, add feature to restore window state

* Make clappy hippier

* remove console log

* use simpler css invalidator

* Less flash, remove log on web hotreload

* Fix floating window managed behavior on mac

* clippy...
Jonathan Kelley 1 жил өмнө
parent
commit
e923c6462c

+ 12 - 0
Cargo.lock

@@ -2153,6 +2153,7 @@ name = "dioxus-desktop"
 version = "0.5.0-alpha.2"
 dependencies = [
  "async-trait",
+ "cocoa",
  "core-foundation",
  "dioxus",
  "dioxus-cli-config",
@@ -2175,6 +2176,7 @@ dependencies = [
  "rustc-hash",
  "serde",
  "serde_json",
+ "signal-hook",
  "slab",
  "tao",
  "thiserror",
@@ -7869,6 +7871,16 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
 
+[[package]]
+name = "signal-hook"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
 [[package]]
 name = "signal-hook-registry"
 version = "1.4.1"

+ 25 - 1
packages/cli/src/server/desktop/mod.rs

@@ -111,6 +111,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
         .exec()
         .unwrap();
     let target_dir = metadata.target_directory.as_std_path();
+
     let _ = create_dir_all(target_dir); // `_all` is for good measure and future-proofness.
     let path = target_dir.join("dioxusin");
     clear_paths(&path);
@@ -141,6 +142,7 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
                                         .flat_map(|v| v.templates.values().copied())
                                         .collect()
                                 };
+
                                 for template in templates {
                                     if !send_msg(
                                         HotReloadMsg::UpdateTemplate(template),
@@ -282,7 +284,29 @@ impl DesktopPlatform {
         config: &CrateConfig,
         rust_flags: Option<String>,
     ) -> Result<BuildResult> {
-        self.currently_running_child.0.kill()?;
+        // Gracefully shtudown the desktop app
+        // It might have a receiver to do some cleanup stuff
+        let pid = self.currently_running_child.0.id();
+
+        // on unix, we can send a signal to the process to shut down
+        #[cfg(unix)]
+        {
+            _ = Command::new("kill")
+                .args(["-s", "TERM", &pid.to_string()])
+                .spawn();
+        }
+
+        // on windows, use the `taskkill` command
+        #[cfg(windows)]
+        {
+            _ = Command::new("taskkill")
+                .args(["/F", "/PID", &pid.to_string()])
+                .spawn();
+        }
+
+        // Todo: add a timeout here to kill the process if it doesn't shut down within a reasonable time
+        self.currently_running_child.0.wait()?;
+
         let (child, result) = start_desktop(config, self.skip_assets, rust_flags)?;
         self.currently_running_child = child;
         Ok(result)

+ 6 - 2
packages/cli/src/server/mod.rs

@@ -56,9 +56,14 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
     // file watcher: check file change
     let mut allow_watch_path = config.dioxus_config.web.watcher.watch_path.clone();
 
-    // Extend the watch path to include the assets directory - this is so we can hotreload CSS and other assets
+    // Extend the watch path to include the assets directory - this is so we can hotreload CSS and other assets by default
     allow_watch_path.push(config.dioxus_config.application.asset_dir.clone());
 
+    // Extend the watch path to include Cargo.toml and Dioxus.toml
+    allow_watch_path.push("Cargo.toml".to_string().into());
+    allow_watch_path.push("Dioxus.toml".to_string().into());
+    allow_watch_path.dedup();
+
     // Create the file watcher
     let mut watcher = notify::recommended_watcher({
         let watcher_config = config.clone();
@@ -66,7 +71,6 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
             let Ok(e) = info else {
                 return;
             };
-
             watch_event(
                 e,
                 &mut last_update_time,

+ 1 - 1
packages/cli/src/server/output.rs

@@ -112,7 +112,7 @@ pub fn print_console_info(
             .watch_path
             .iter()
             .cloned()
-            .chain(Some(config.dioxus_config.application.asset_dir.clone()))
+            .chain(["Cargo.toml", "Dioxus.toml"].iter().map(PathBuf::from))
             .map(|f| f.display().to_string())
             .collect::<Vec<String>>()
             .join(", ")

+ 2 - 0
packages/desktop/Cargo.toml

@@ -49,6 +49,7 @@ futures-util = { workspace = true }
 urlencoding = "2.1.2"
 async-trait = "0.1.68"
 tao = { version = "0.26.1", features = ["rwh_05"] }
+signal-hook = "0.3.17"
 
 [target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
 global-hotkey = "0.5.0"
@@ -62,6 +63,7 @@ objc = "0.2.7"
 objc_id = "0.1.1"
 
 [target.'cfg(target_os = "macos")'.dependencies]
+cocoa = "0.25"
 core-foundation = "0.9.3"
 objc = "0.2.7"
 

+ 145 - 1
packages/desktop/src/app.rs

@@ -18,6 +18,7 @@ use std::{
     sync::Arc,
 };
 use tao::{
+    dpi::{PhysicalPosition, PhysicalSize},
     event::Event,
     event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
     window::WindowId,
@@ -35,6 +36,7 @@ pub(crate) struct App {
     pub(crate) is_visible_before_start: bool,
     pub(crate) window_behavior: WindowCloseBehaviour,
     pub(crate) webviews: HashMap<WindowId, WebviewInstance>,
+    pub(crate) float_all: bool,
 
     /// This single blob of state is shared between all the windows so they have access to the runtime state
     ///
@@ -61,6 +63,7 @@ impl App {
             webviews: HashMap::new(),
             control_flow: ControlFlow::Wait,
             unmounted_dom: Cell::new(Some(virtual_dom)),
+            float_all: cfg!(debug_assertions),
             cfg: Cell::new(Some(cfg)),
             shared: Rc::new(SharedContext {
                 event_handlers: WindowEventHandlers::default(),
@@ -78,6 +81,10 @@ impl App {
         #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
         app.set_global_hotkey_handler();
 
+        // Wire up the menubar receiver - this way any component can key into the menubar actions
+        #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+        app.set_menubar_receiver();
+
         // Allow hotreloading to work - but only in debug mode
         #[cfg(all(
             feature = "hot-reload",
@@ -87,6 +94,10 @@ impl App {
         ))]
         app.connect_hotreload();
 
+        #[cfg(debug_assertions)]
+        #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+        app.connect_preserve_window_state_handler();
+
         (event_loop, app)
     }
 
@@ -102,6 +113,20 @@ impl App {
         self.shared.shortcut_manager.call_handlers(event);
     }
 
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    pub fn handle_menu_event(&mut self, event: muda::MenuEvent) {
+        if event.id() == "dioxus-float-top" {
+            for webview in self.webviews.values() {
+                webview
+                    .desktop_context
+                    .window
+                    .set_always_on_top(self.float_all);
+            }
+        }
+
+        self.float_all = !self.float_all;
+    }
+
     #[cfg(all(
         feature = "hot-reload",
         debug_assertions,
@@ -109,7 +134,11 @@ impl App {
         not(target_os = "ios")
     ))]
     pub fn connect_hotreload(&self) {
-        dioxus_hot_reload::connect({
+        let Ok(cfg) = dioxus_cli_config::CURRENT_CONFIG.as_ref() else {
+            return;
+        };
+
+        dioxus_hot_reload::connect_at(cfg.target_dir.join("dioxusin"), {
             let proxy = self.shared.proxy.clone();
             move |template| {
                 let _ = proxy.send_event(UserWindowEvent::HotReloadEvent(template));
@@ -169,6 +198,10 @@ impl App {
 
         let webview = WebviewInstance::new(cfg, virtual_dom, self.shared.clone());
 
+        // And then attempt to resume from state
+        #[cfg(debug_assertions)]
+        self.resume_from_state(&webview);
+
         let id = webview.desktop_context.window.id();
         self.webviews.insert(id, webview);
     }
@@ -356,6 +389,117 @@ impl App {
             _ = receiver.send_event(UserWindowEvent::GlobalHotKeyEvent(t));
         }));
     }
+
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    fn set_menubar_receiver(&self) {
+        let receiver = self.shared.proxy.clone();
+
+        // The event loop becomes the menu receiver
+        // This means we don't need to poll the receiver on every tick - we just get the events as they come in
+        // This is a bit more efficient than the previous implementation, but if someone else sets a handler, the
+        // receiver will become inert.
+        muda::MenuEvent::set_event_handler(Some(move |t| {
+            // todo: should we unset the event handler when the app shuts down?
+            _ = receiver.send_event(UserWindowEvent::MudaMenuEvent(t));
+        }));
+    }
+
+    /// Do our best to preserve state about the window when the event loop is destroyed
+    ///
+    /// This will attempt to save the window position, size, and monitor into the environment before
+    /// closing. This way, when the app is restarted, it can attempt to restore the window to the same
+    /// position and size it was in before, making a better DX.
+    pub(crate) fn handle_loop_destroyed(&self) {
+        #[cfg(debug_assertions)]
+        self.persist_window_state();
+    }
+
+    #[cfg(debug_assertions)]
+    fn persist_window_state(&self) {
+        if let Some(webview) = self.webviews.values().next() {
+            let window = &webview.desktop_context.window;
+
+            let monitor = window.current_monitor().unwrap();
+            let position = window.outer_position().unwrap();
+            let size = window.outer_size();
+
+            let x = position.x;
+            let y = position.y;
+
+            // This is to work around a bug in how tao handles inner_size on macOS
+            // We *want* to use inner_size, but that's currently broken, so we use outer_size instead and then an adjustment
+            //
+            // https://github.com/tauri-apps/tao/issues/889
+            let adjustment = match window.is_decorated() {
+                true if cfg!(target_os = "macos") => 56,
+                _ => 0,
+            };
+
+            let state = PreservedWindowState {
+                x,
+                y,
+                width: size.width.max(200),
+                height: size.height.saturating_sub(adjustment).max(200),
+                monitor: monitor.name().unwrap().to_string(),
+            };
+
+            if let Ok(state) = serde_json::to_string(&state) {
+                // Write this to the target dir so we can pick back up in resume_from_state
+                if let Ok(cfg) = dioxus_cli_config::CURRENT_CONFIG.as_ref() {
+                    let path = cfg.target_dir.join("window_state.json");
+                    _ = std::fs::write(path, state);
+                }
+            }
+        }
+    }
+
+    // Write this to the target dir so we can pick back up
+    #[cfg(debug_assertions)]
+    fn resume_from_state(&mut self, webview: &WebviewInstance) {
+        if let Ok(cfg) = dioxus_cli_config::CURRENT_CONFIG.as_ref() {
+            let path = cfg.target_dir.join("window_state.json");
+            if let Ok(state) = std::fs::read_to_string(path) {
+                if let Ok(state) = serde_json::from_str::<PreservedWindowState>(&state) {
+                    let window = &webview.desktop_context.window;
+                    let position = (state.x, state.y);
+                    let size = (state.width, state.height);
+                    window.set_outer_position(PhysicalPosition::new(position.0, position.1));
+                    window.set_inner_size(PhysicalSize::new(size.0, size.1));
+                }
+            }
+        }
+    }
+
+    /// Wire up a receiver to sigkill that lets us preserve the window state
+    /// Whenever sigkill is sent, we shut down the app and save the window state
+    #[cfg(debug_assertions)]
+    fn connect_preserve_window_state_handler(&self) {
+        // Wire up the trap
+        let target = self.shared.proxy.clone();
+        std::thread::spawn(move || {
+            use signal_hook::consts::{SIGINT, SIGTERM};
+            let sigkill = signal_hook::iterator::Signals::new([SIGTERM, SIGINT]);
+            if let Ok(mut sigkill) = sigkill {
+                for _ in sigkill.forever() {
+                    if target.send_event(UserWindowEvent::Shutdown).is_err() {
+                        std::process::exit(0);
+                    }
+
+                    // give it a moment for the event to be processed
+                    std::thread::sleep(std::time::Duration::from_secs(1));
+                }
+            }
+        });
+    }
+}
+
+#[derive(Debug, serde::Serialize, serde::Deserialize)]
+struct PreservedWindowState {
+    x: i32,
+    y: i32,
+    width: u32,
+    height: u32,
+    monitor: String,
 }
 
 /// Different hide implementations per platform

+ 9 - 6
packages/desktop/src/config.rs

@@ -41,12 +41,15 @@ impl Config {
     /// Initializes a new `WindowBuilder` with default values.
     #[inline]
     pub fn new() -> Self {
-        let window = WindowBuilder::new().with_title(
-            dioxus_cli_config::CURRENT_CONFIG
-                .as_ref()
-                .map(|c| c.dioxus_config.application.name.clone())
-                .unwrap_or("Dioxus App".to_string()),
-        );
+        let window: WindowBuilder = WindowBuilder::new()
+            .with_title(
+                dioxus_cli_config::CURRENT_CONFIG
+                    .as_ref()
+                    .map(|c| c.dioxus_config.application.name.clone())
+                    .unwrap_or("Dioxus App".to_string()),
+            )
+            // During development we want the window to be on top so we can see it while we work
+            .with_always_on_top(cfg!(debug_assertions));
 
         Self {
             window,

+ 7 - 0
packages/desktop/src/ipc.rs

@@ -1,12 +1,16 @@
 use serde::{Deserialize, Serialize};
 use tao::window::WindowId;
 
+#[non_exhaustive]
 #[derive(Debug, Clone)]
 pub enum UserWindowEvent {
     /// A global hotkey event
     #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
     GlobalHotKeyEvent(global_hotkey::GlobalHotKeyEvent),
 
+    #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+    MudaMenuEvent(muda::MenuEvent),
+
     /// Poll the virtualdom
     Poll(WindowId),
 
@@ -27,6 +31,9 @@ pub enum UserWindowEvent {
 
     /// Close a given window (could be any window!)
     CloseWindow(WindowId),
+
+    /// Gracefully shutdown the entire app
+    Shutdown,
 }
 
 /// A message struct that manages the communication between the webview and the eventloop code

+ 5 - 0
packages/desktop/src/launch.rs

@@ -20,6 +20,7 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf
 
         match window_event {
             Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(),
+            Event::LoopDestroyed => app.handle_loop_destroyed(),
             Event::WindowEvent {
                 event, window_id, ..
             } => match event {
@@ -32,10 +33,14 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, desktop_config: Conf
                 UserWindowEvent::Poll(id) => app.poll_vdom(id),
                 UserWindowEvent::NewWindow => app.handle_new_window(),
                 UserWindowEvent::CloseWindow(id) => app.handle_close_msg(id),
+                UserWindowEvent::Shutdown => app.control_flow = tao::event_loop::ControlFlow::Exit,
 
                 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
                 UserWindowEvent::GlobalHotKeyEvent(evnt) => app.handle_global_hotkey(evnt),
 
+                #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+                UserWindowEvent::MudaMenuEvent(evnt) => app.handle_menu_event(evnt),
+
                 #[cfg(all(
                     feature = "hot-reload",
                     debug_assertions,

+ 10 - 0
packages/desktop/src/menubar.rs

@@ -93,6 +93,16 @@ mod desktop_platforms {
             .append_items(&[&MenuItem::new("Toggle Developer Tools", true, None)])
             .unwrap();
 
+        // By default we float the window on top in dev mode, but let the user disable it
+        help_menu
+            .append_items(&[&MenuItem::with_id(
+                "dioxus-float-top",
+                "Float on Top (dev mode only)",
+                true,
+                None,
+            )])
+            .unwrap();
+
         menu.append_items(&[&window_menu, &edit_menu, &help_menu])
             .unwrap();
 

+ 29 - 3
packages/desktop/src/webview.rs

@@ -54,6 +54,20 @@ impl WebviewInstance {
 
         let window = window.build(&shared.target).unwrap();
 
+        // https://developer.apple.com/documentation/appkit/nswindowcollectionbehavior/nswindowcollectionbehaviormanaged
+        #[cfg(target_os = "macos")]
+        {
+            use cocoa::appkit::NSWindowCollectionBehavior;
+            use cocoa::base::id;
+            use objc::{msg_send, sel, sel_impl};
+            use tao::platform::macos::WindowExtMacOS;
+
+            unsafe {
+                let window: id = window.ns_window() as id;
+                let _: () = msg_send![window, setCollectionBehavior: NSWindowCollectionBehavior::NSWindowCollectionBehaviorManaged];
+            }
+        }
+
         let mut web_context = WebContext::new(cfg.data_dir.clone());
         let edit_queue = EditQueue::default();
         let file_hover = NativeFileHover::default();
@@ -131,7 +145,18 @@ impl WebviewInstance {
             .with_transparent(cfg.window.window.transparent)
             .with_url("dioxus://index.html/")
             .with_ipc_handler(ipc_handler)
-            .with_navigation_handler(|var| var.contains("dioxus")) // prevent all navigations
+            .with_navigation_handler(|var| {
+                // We don't want to allow any navigation
+                // We only want to serve the index file and assets
+                if var.starts_with("dioxus://") || var.starts_with("http://dioxus.") {
+                    true
+                } else {
+                    if var.starts_with("http://") || var.starts_with("https://") {
+                        _ = webbrowser::open(&var);
+                    }
+                    false
+                }
+            }) // prevent all navigations
             .with_asynchronous_custom_protocol(String::from("dioxus"), request_handler)
             .with_web_context(&mut web_context)
             .with_file_drop_handler(file_drop_handler);
@@ -228,8 +253,9 @@ impl WebviewInstance {
     pub fn kick_stylsheets(&self) {
         // run eval in the webview to kick the stylesheets by appending a query string
         // we should do something less clunky than this
-        _ = self.desktop_context
+        _ = self
+            .desktop_context
             .webview
-            .evaluate_script("document.querySelectorAll('link[rel=\"stylesheet\"]').forEach((el) => el.href = el.href + \"?\" + Math.random());");
+            .evaluate_script("window.interpreter.kickAllStylesheetsOnPage()");
     }
 }

+ 2 - 0
packages/dioxus/src/lib.rs

@@ -9,6 +9,8 @@ pub use dioxus_core;
 #[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
 mod launch;
 
+pub use launch::launch;
+
 #[cfg(feature = "hooks")]
 #[cfg_attr(docsrs, doc(cfg(feature = "hooks")))]
 pub use dioxus_hooks as hooks;

+ 26 - 18
packages/hot-reload/src/lib.rs

@@ -29,32 +29,40 @@ pub enum HotReloadMsg {
 }
 
 /// Connect to the hot reloading listener. The callback provided will be called every time a template change is detected
-pub fn connect(mut callback: impl FnMut(HotReloadMsg) + Send + 'static) {
-    std::thread::spawn(move || {
-        // get the cargo manifest directory, where the target dir lives
-        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+pub fn connect(callback: impl FnMut(HotReloadMsg) + Send + 'static) {
+    let Ok(_manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else {
+        return;
+    };
 
-        // walk the path until we a find a socket named `dioxusin` inside that folder's target directory
-        loop {
-            let maybe = path.join("target").join("dioxusin");
+    // get the cargo manifest directory, where the target dir lives
+    let mut path = PathBuf::from(_manifest_dir);
 
-            if maybe.exists() {
-                path = maybe;
-                break;
-            }
+    // walk the path until we a find a socket named `dioxusin` inside that folder's target directory
+    loop {
+        let maybe = path.join("target").join("dioxusin");
 
-            // It's likely we're running under just cargo and not dx
-            path = match path.parent() {
-                Some(parent) => parent.to_path_buf(),
-                None => return,
-            };
+        if maybe.exists() {
+            path = maybe;
+            break;
         }
 
+        // It's likely we're running under just cargo and not dx
+        path = match path.parent() {
+            Some(parent) => parent.to_path_buf(),
+            None => return,
+        };
+    }
+
+    connect_at(path, callback);
+}
+
+pub fn connect_at(socket: PathBuf, mut callback: impl FnMut(HotReloadMsg) + Send + 'static) {
+    std::thread::spawn(move || {
         // There might be a socket since the we're not running under the hot reloading server
-        let Ok(socket) = LocalSocketStream::connect(path.clone()) else {
+        let Ok(socket) = LocalSocketStream::connect(socket.clone()) else {
             println!(
                 "could not find hot reloading server at {:?}, make sure it's running",
-                path
+                socket
             );
             return;
         };

+ 1 - 1
packages/interpreter/src/js/hash.txt

@@ -1 +1 @@
-12655652627
+13799725074

+ 1 - 1
packages/interpreter/src/js/native.js

@@ -1 +1 @@
-function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files:{files:{a:[1,2,3]}}}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;liveview;constructor(editsPath){super();this.editsPath=editsPath}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message)}event.preventDefault()}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){const target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target);let body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event,target),this.liveview){if(target instanceof HTMLInputElement&&(event.type==="change"||event.type==="input")){if(target.getAttribute("type")==="file")this.readFiles(target,contents,bubbles,realId,name)}}else{const message=this.serializeIpcMessage("user_event",body);this.ipc.postMessage(message)}}preventDefaults(event,target){let preventDefaultRequests=null;if(target instanceof Element)preventDefaultRequests=target.getAttribute("dioxus-prevent-default");if(preventDefaultRequests&&preventDefaultRequests.includes(`on${event.type}`))event.preventDefault();if(event.type==="submit")event.preventDefault();if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target,preventDefaultRequests)}handleClickNavigate(event,target,preventDefaultRequests){if(!this.intercept_link_redirects)return;if(target.tagName==="BUTTON"&&event.type=="submit")event.preventDefault();let a_element=target.closest("a");if(a_element==null)return;event.preventDefault();let elementShouldPreventDefault=preventDefaultRequests&&preventDefaultRequests.includes("onclick"),aElementShouldPreventDefault=a_element.getAttribute("dioxus-prevent-default"),linkShouldPreventDefault=aElementShouldPreventDefault&&aElementShouldPreventDefault.includes("onclick");if(!elementShouldPreventDefault&&!linkShouldPreventDefault){const href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{if(headless)this.run_from_bytes(bytes);else requestAnimationFrame(()=>this.run_from_bytes(bytes));this.waitForRequest(headless)})}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){const file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};const message=this.serializeIpcMessage("user_event",{name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}export{NativeInterpreter};
+function retriveValues(event,target){let contents={values:{}},form=target.closest("form");if(form){if(event.type==="input"||event.type==="change"||event.type==="submit"||event.type==="reset"||event.type==="click")contents=retrieveFormValues(form)}return contents}function retrieveFormValues(form){const formData=new FormData(form),contents={};return formData.forEach((value,key)=>{if(contents[key])contents[key].push(value);else contents[key]=[value]}),{valid:form.checkValidity(),values:contents}}function retriveSelectValue(target){let options=target.selectedOptions,values=[];for(let i=0;i<options.length;i++)values.push(options[i].value);return values}function serializeEvent(event,target){let contents={},extend=(obj)=>contents={...contents,...obj};if(event instanceof WheelEvent)extend(serializeWheelEvent(event));if(event instanceof MouseEvent)extend(serializeMouseEvent(event));if(event instanceof KeyboardEvent)extend(serializeKeyboardEvent(event));if(event instanceof InputEvent)extend(serializeInputEvent(event,target));if(event instanceof PointerEvent)extend(serializePointerEvent(event));if(event instanceof AnimationEvent)extend(serializeAnimationEvent(event));if(event instanceof TransitionEvent)extend({property_name:event.propertyName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement});if(event instanceof CompositionEvent)extend({data:event.data});if(event instanceof DragEvent)extend(serializeDragEvent(event));if(event instanceof FocusEvent)extend({});if(event instanceof ClipboardEvent)extend({});if(typeof TouchEvent!=="undefined"&&event instanceof TouchEvent)extend(serializeTouchEvent(event));if(event.type==="submit"||event.type==="reset"||event.type==="click"||event.type==="change"||event.type==="input")extend(serializeInputEvent(event,target));if(event instanceof DragEvent);return contents}var serializeInputEvent=function(event,target){let contents={};if(target instanceof HTMLElement){let values=retriveValues(event,target);contents.values=values.values,contents.valid=values.valid}if(event.target instanceof HTMLInputElement){let target2=event.target,value=target2.value??target2.textContent??"";if(target2.type==="checkbox")value=target2.checked?"true":"false";else if(target2.type==="radio")value=target2.value;contents.value=value}if(event.target instanceof HTMLTextAreaElement)contents.value=event.target.value;if(event.target instanceof HTMLSelectElement)contents.value=retriveSelectValue(event.target).join(",");if(contents.value===void 0)contents.value="";return contents},serializeWheelEvent=function(event){return{delta_x:event.deltaX,delta_y:event.deltaY,delta_z:event.deltaZ,delta_mode:event.deltaMode}},serializeTouchEvent=function(event){return{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,changed_touches:event.changedTouches,target_touches:event.targetTouches,touches:event.touches}},serializePointerEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey,pointer_id:event.pointerId,width:event.width,height:event.height,pressure:event.pressure,tangential_pressure:event.tangentialPressure,tilt_x:event.tiltX,tilt_y:event.tiltY,twist:event.twist,pointer_type:event.pointerType,is_primary:event.isPrimary}},serializeMouseEvent=function(event){return{alt_key:event.altKey,button:event.button,buttons:event.buttons,client_x:event.clientX,client_y:event.clientY,ctrl_key:event.ctrlKey,meta_key:event.metaKey,offset_x:event.offsetX,offset_y:event.offsetY,page_x:event.pageX,page_y:event.pageY,screen_x:event.screenX,screen_y:event.screenY,shift_key:event.shiftKey}},serializeKeyboardEvent=function(event){return{char_code:event.charCode,is_composing:event.isComposing,key:event.key,alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,key_code:event.keyCode,shift_key:event.shiftKey,location:event.location,repeat:event.repeat,which:event.which,code:event.code}},serializeAnimationEvent=function(event){return{animation_name:event.animationName,elapsed_time:event.elapsedTime,pseudo_element:event.pseudoElement}},serializeDragEvent=function(event){return{mouse:{alt_key:event.altKey,ctrl_key:event.ctrlKey,meta_key:event.metaKey,shift_key:event.shiftKey,...serializeMouseEvent(event)},files:{files:{a:[1,2,3]}}}};var getTargetId=function(target){if(!(target instanceof Node))return null;let ourTarget=target,realId=null;while(realId==null){if(ourTarget===null)return null;if(ourTarget instanceof Element)realId=ourTarget.getAttribute("data-dioxus-id");ourTarget=ourTarget.parentNode}return parseInt(realId)},JSChannel_;if(RawInterpreter!==void 0&&RawInterpreter!==null)JSChannel_=RawInterpreter;class NativeInterpreter extends JSChannel_{intercept_link_redirects;ipc;editsPath;kickStylesheets;liveview;constructor(editsPath){super();this.editsPath=editsPath,this.kickStylesheets=!1}initialize(root){this.intercept_link_redirects=!0,this.liveview=!1,window.addEventListener("dragover",function(e){if(e.target instanceof Element&&e.target.tagName!="INPUT")e.preventDefault()},!1),window.addEventListener("drop",function(e){if(!(e.target instanceof Element))return;e.preventDefault()},!1),window.addEventListener("click",(event)=>{const target=event.target;if(target instanceof HTMLInputElement&&target.getAttribute("type")==="file"){let target_id=getTargetId(target);if(target_id!==null){const message=this.serializeIpcMessage("file_dialog",{event:"change&input",accept:target.getAttribute("accept"),directory:target.getAttribute("webkitdirectory")==="true",multiple:target.hasAttribute("multiple"),target:target_id,bubbles:event.bubbles});this.ipc.postMessage(message)}event.preventDefault()}}),this.ipc=window.ipc;const handler=(event)=>this.handleEvent(event,event.type,!0);super.initialize(root,handler)}serializeIpcMessage(method,params={}){return JSON.stringify({method,params})}scrollTo(id,behavior){const node=this.nodes[id];if(node instanceof HTMLElement)node.scrollIntoView({behavior})}getClientRect(id){const node=this.nodes[id];if(node instanceof HTMLElement){const rect=node.getBoundingClientRect();return{type:"GetClientRect",origin:[rect.x,rect.y],size:[rect.width,rect.height]}}}setFocus(id,focus){const node=this.nodes[id];if(node instanceof HTMLElement)if(focus)node.focus();else node.blur()}loadChild(array){let node=this.stack[this.stack.length-1];for(let i=0;i<array.length;i++){let end=array[i];for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}handleEvent(event,name,bubbles){const target=event.target,realId=getTargetId(target),contents=serializeEvent(event,target);let body={name,data:contents,element:realId,bubbles};if(this.preventDefaults(event,target),this.liveview){if(target instanceof HTMLInputElement&&(event.type==="change"||event.type==="input")){if(target.getAttribute("type")==="file")this.readFiles(target,contents,bubbles,realId,name)}}else{const message=this.serializeIpcMessage("user_event",body);this.ipc.postMessage(message)}}preventDefaults(event,target){let preventDefaultRequests=null;if(target instanceof Element)preventDefaultRequests=target.getAttribute("dioxus-prevent-default");if(preventDefaultRequests&&preventDefaultRequests.includes(`on${event.type}`))event.preventDefault();if(event.type==="submit")event.preventDefault();if(target instanceof Element&&event.type==="click")this.handleClickNavigate(event,target,preventDefaultRequests)}handleClickNavigate(event,target,preventDefaultRequests){if(!this.intercept_link_redirects)return;if(target.tagName==="BUTTON"&&event.type=="submit")event.preventDefault();let a_element=target.closest("a");if(a_element==null)return;event.preventDefault();let elementShouldPreventDefault=preventDefaultRequests&&preventDefaultRequests.includes("onclick"),aElementShouldPreventDefault=a_element.getAttribute("dioxus-prevent-default"),linkShouldPreventDefault=aElementShouldPreventDefault&&aElementShouldPreventDefault.includes("onclick");if(!elementShouldPreventDefault&&!linkShouldPreventDefault){const href=a_element.getAttribute("href");if(href!==""&&href!==null&&href!==void 0)this.ipc.postMessage(this.serializeIpcMessage("browser_open",{href}))}}waitForRequest(headless){fetch(new Request(this.editsPath)).then((response)=>response.arrayBuffer()).then((bytes)=>{if(headless)this.run_from_bytes(bytes);else requestAnimationFrame(()=>{this.run_from_bytes(bytes)});this.waitForRequest(headless)})}kickAllStylesheetsOnPage(){let stylesheets=document.querySelectorAll("link[rel=stylesheet]");for(let i=0;i<stylesheets.length;i++){let sheet=stylesheets[i];fetch(sheet.href,{cache:"reload"}).then(()=>{sheet.href=sheet.href+"?"+Math.random()})}}async readFiles(target,contents,bubbles,realId,name){let files=target.files,file_contents={};for(let i=0;i<files.length;i++){const file=files[i];file_contents[file.name]=Array.from(new Uint8Array(await file.arrayBuffer()))}contents.files={files:file_contents};const message=this.serializeIpcMessage("user_event",{name,element:realId,data:contents,bubbles});this.ipc.postMessage(message)}}export{NativeInterpreter};

+ 20 - 2
packages/interpreter/src/ts/native.ts

@@ -21,6 +21,7 @@ export class NativeInterpreter extends JSChannel_ {
   intercept_link_redirects: boolean;
   ipc: any;
   editsPath: string;
+  kickStylesheets: boolean;
 
   // eventually we want to remove liveview and build it into the server-side-events of fullstack
   // however, for now we need to support it since SSE in fullstack doesn't exist yet
@@ -29,6 +30,7 @@ export class NativeInterpreter extends JSChannel_ {
   constructor(editsPath: string) {
     super();
     this.editsPath = editsPath;
+    this.kickStylesheets = false;
   }
 
   initialize(root: HTMLElement): void {
@@ -272,14 +274,30 @@ export class NativeInterpreter extends JSChannel_ {
           // @ts-ignore
           this.run_from_bytes(bytes);
         } else {
-          // @ts-ignore
-          requestAnimationFrame(() => this.run_from_bytes(bytes));
+          requestAnimationFrame(() => {
+            // @ts-ignore
+            this.run_from_bytes(bytes)
+          });
         }
         this.waitForRequest(headless);
       });
   }
 
 
+  kickAllStylesheetsOnPage() {
+    // If this function is being called and we have not explicitly set kickStylesheets to true, then we should
+    // force kick the stylesheets, regardless if they have a dioxus attribute or not
+    // This happens when any hotreload happens.
+    let stylesheets = document.querySelectorAll("link[rel=stylesheet]");
+    for (let i = 0; i < stylesheets.length; i++) {
+      let sheet = stylesheets[i] as HTMLLinkElement;
+      // Using `cache: reload` will force the browser to re-fetch the stylesheet and bust the cache
+      fetch(sheet.href, { cache: "reload" }).then(() => {
+        sheet.href = sheet.href + "?" + Math.random();
+      });
+    }
+  }
+
   //  A liveview only function
   // Desktop will intercept the event before it hits this
   async readFiles(target: HTMLInputElement, contents: SerializedEvent, bubbles: boolean, realId: NodeId, name: string) {

+ 2 - 4
packages/web/src/hot_reload.rs

@@ -50,14 +50,12 @@ pub(crate) fn init() -> UnboundedReceiver<Template> {
                     .query_selector_all("link[rel=stylesheet]")
                     .unwrap();
 
-                console::log_1(&links.clone().into());
+                let noise = js_sys::Math::random();
 
                 for x in 0..links.length() {
-                    console::log_1(&x.into());
-
                     let link: Element = links.get(x).unwrap().unchecked_into();
                     let href = link.get_attribute("href").unwrap();
-                    _ = link.set_attribute("href", &format!("{}?{}", href, js_sys::Math::random()));
+                    _ = link.set_attribute("href", &format!("{}?{}", href, noise));
                 }
             }
         }