Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 8 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 25 additions & 1 deletion packages/cli/src/server/desktop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,21 @@ 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();
move |info: notify::Result<notify::Event>| {
let Ok(e) = info else {
return;
};

watch_event(
e,
&mut last_update_time,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/server/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(", ")
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"

Expand Down
146 changes: 145 additions & 1 deletion packages/desktop/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::{
sync::Arc,
};
use tao::{
dpi::{PhysicalPosition, PhysicalSize},
event::Event,
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
window::WindowId,
Expand All @@ -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
///
Expand All @@ -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(),
Expand All @@ -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",
Expand All @@ -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)
}

Expand All @@ -102,14 +113,32 @@ 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,
not(target_os = "android"),
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));
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down
15 changes: 9 additions & 6 deletions packages/desktop/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions packages/desktop/src/ipc.rs
Original file line number Diff line number Diff line change
@@ -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),

Expand All @@ -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
Expand Down
Loading
Loading