diff --git a/index.html b/index.html index 8ae63972..ab267537 100644 --- a/index.html +++ b/index.html @@ -121,6 +121,10 @@ } + + + + Pachtop diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8e367910..4817e2b3 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -84,7 +84,10 @@ name = "app" version = "0.7.1" dependencies = [ "chrono", + "cocoa 0.25.0", + "hex_color", "log", + "objc", "serde", "serde_json", "sysinfo", @@ -93,8 +96,11 @@ dependencies = [ "tauri-plugin-autostart", "tauri-plugin-log", "tauri-plugin-store", + "tauri-plugin-window-state", "tokio", "ts-rs", + "windows 0.52.0", + "winver", ] [[package]] @@ -347,6 +353,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -652,6 +667,22 @@ dependencies = [ "objc", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + [[package]] name = "cocoa-foundation" version = "0.1.2" @@ -1740,6 +1771,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex_color" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "home" version = "0.5.9" @@ -3944,7 +3984,7 @@ dependencies = [ "bitflags 1.3.2", "cairo-rs", "cc", - "cocoa", + "cocoa 0.24.1", "core-foundation", "core-graphics 0.22.3", "crossbeam-channel", @@ -4027,7 +4067,7 @@ dependencies = [ "anyhow", "base64 0.21.7", "bytes", - "cocoa", + "cocoa 0.24.1", "dirs-next", "dunce", "embed_plist", @@ -4179,6 +4219,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tauri-plugin-window-state" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa47eaa4047a7b51064caff32f0c6282e2c5adc6ceacdd493ecf1b01fa4b0eaa" +dependencies = [ + "bincode", + "bitflags 2.5.0", + "log", + "serde", + "serde_json", + "tauri", + "thiserror", +] + [[package]] name = "tauri-runtime" version = "0.14.3" @@ -4207,7 +4262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2af45aeb15b1cadb4ca91248423f4438a0864b836298cecb436892afbfdff4" dependencies = [ "arboard", - "cocoa", + "cocoa 0.24.1", "gtk", "percent-encoding", "rand 0.8.5", @@ -5123,6 +5178,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.5", +] + [[package]] name = "windows" version = "0.56.0" @@ -5553,6 +5618,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winver" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0e7162b9e282fd75a0a832cce93994bdb21208d848a418cd05a5fdd9b9ab33" +dependencies = [ + "windows 0.48.0", +] + [[package]] name = "wl-clipboard-rs" version = "0.8.1" @@ -5581,7 +5655,7 @@ checksum = "3c689900e022bb67b0d9728fb817bbef2b9da7ebd6c79aade5f0c32fe4c18c73" dependencies = [ "base64 0.13.1", "block", - "cocoa", + "cocoa 0.24.1", "core-graphics 0.22.3", "crossbeam-channel", "dunce", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c3e47fef..84f44d20 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,14 +19,28 @@ chrono = {version = "0.4.23", features= ["serde"] } # rusqlite = { version = "0.28.0", features = ["bundled", "chrono"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.6.5", features = ["api-all", "system-tray", "updater"] } +tauri = { version = "1.6.5", features = ["api-all", "system-tray", "updater", "window-start-dragging"] } sysinfo = "0.29.11" tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } +tauri-plugin-window-state = "0.1.0" log = "^0.4" tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] } ts-rs = "8.1" +hex_color = "3.0.0" + +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.25.0" +objc = "0.2.7" + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.52.0", features = [ + "Win32_Graphics_Dwm", + "Win32_Foundation", + "Win32_UI_Controls", +] } +winver = "1" # surrealdb = { version = "1.5.0" } diff --git a/src-tauri/src/mac/mod.rs b/src-tauri/src/mac/mod.rs new file mode 100644 index 00000000..61b63f17 --- /dev/null +++ b/src-tauri/src/mac/mod.rs @@ -0,0 +1 @@ +pub mod window; diff --git a/src-tauri/src/mac/window.rs b/src-tauri/src/mac/window.rs new file mode 100644 index 00000000..1330bfba --- /dev/null +++ b/src-tauri/src/mac/window.rs @@ -0,0 +1,390 @@ +use hex_color::HexColor; +use tauri::{App, Manager, Runtime, Window}; + +// If anything breaks on macOS, this should be the place which is broken +// We have to override Tauri (Tao) 's built-in NSWindowDelegate implementation with a +// custom implementation so we can emit events on full screen mode changes. +// Our custom implementation tries to mock the Tauri implementation. So please do refer to the relevant parts + +// Apple's NSWindowDelegate reference: https://developer.apple.com/documentation/appkit/nswindowdelegate?language=objc +// Tao's Window Delegate Implementation: https://github.com/tauri-apps/tao/blob/dev/src/platform_impl/macos/window_delegate.rs + +#[allow(dead_code)] +pub enum ToolbarThickness { + Thick, + Medium, + Thin, +} + +const WINDOW_CONTROL_PAD_X: f64 = 15.0; +const WINDOW_CONTROL_PAD_Y: f64 = 23.0; + +pub trait WindowExt { + #[cfg(target_os = "macos")] + fn set_transparent_titlebar(&self); +} + +#[cfg(target_os = "macos")] +unsafe fn set_transparent_titlebar(id: cocoa::base::id) { + use cocoa::appkit::NSWindow; + + id.setTitlebarAppearsTransparent_(cocoa::base::YES); + id.setTitleVisibility_(cocoa::appkit::NSWindowTitleVisibility::NSWindowTitleHidden); +} + +struct UnsafeWindowHandle(*mut std::ffi::c_void); +unsafe impl Send for UnsafeWindowHandle {} +unsafe impl Sync for UnsafeWindowHandle {} + +#[cfg(target_os = "macos")] +fn update_window_theme(window: &tauri::Window, color: HexColor) { + use cocoa::appkit::{ + NSAppearance, NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight, NSWindow, + }; + + let brightness = (color.r as u64 + color.g as u64 + color.b as u64) / 3; + + unsafe { + let window_handle = UnsafeWindowHandle(window.ns_window().unwrap()); + + let _ = window.run_on_main_thread(move || { + let handle = window_handle; + + let selected_appearance = if brightness >= 128 { + NSAppearance(NSAppearanceNameVibrantLight) + } else { + NSAppearance(NSAppearanceNameVibrantDark) + }; + + NSWindow::setAppearance(handle.0 as cocoa::base::id, selected_appearance); + set_window_controls_pos( + handle.0 as cocoa::base::id, + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + ); + }); + } +} + +#[cfg(target_os = "macos")] +fn set_window_controls_pos(window: cocoa::base::id, x: f64, y: f64) { + use cocoa::{ + appkit::{NSView, NSWindow, NSWindowButton}, + foundation::NSRect, + }; + + unsafe { + let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton); + let miniaturize = window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton); + let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton); + + let title_bar_container_view = close.superview().superview(); + + let close_rect: NSRect = msg_send![close, frame]; + let button_height = close_rect.size.height; + + let title_bar_frame_height = button_height + y; + let mut title_bar_rect = NSView::frame(title_bar_container_view); + title_bar_rect.size.height = title_bar_frame_height; + title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height; + let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect]; + + let window_buttons = vec![close, miniaturize, zoom]; + let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x; + + for (i, button) in window_buttons.into_iter().enumerate() { + let mut rect: NSRect = NSView::frame(button); + rect.origin.x = x + (i as f64 * space_between); + button.setFrameOrigin(rect.origin); + } + } +} + +impl WindowExt for Window { + #[cfg(target_os = "macos")] + fn set_transparent_titlebar(&self) { + unsafe { + let id = self.ns_window().unwrap() as cocoa::base::id; + + set_transparent_titlebar(id); + + set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y); + } + } +} + +#[cfg(target_os = "macos")] +#[derive(Debug)] +struct HoppAppState { + window: Window, +} + +#[cfg(target_os = "macos")] +pub fn setup_mac_window(app: &mut App) { + use cocoa::appkit::NSWindow; + use cocoa::base::{id, BOOL}; + use cocoa::foundation::NSUInteger; + use objc::runtime::{Object, Sel}; + use std::ffi::c_void; + + fn with_hopp_app T, T>(this: &Object, func: F) { + let ptr = unsafe { + let x: *mut c_void = *this.get_ivar("hoppApp"); + &mut *(x as *mut HoppAppState) + }; + func(ptr); + } + + let window = app.get_window("main").unwrap(); + + unsafe { + let ns_win = window.ns_window().unwrap() as id; + + let current_delegate: id = ns_win.delegate(); + + extern "C" fn on_window_should_close(this: &Object, _cmd: Sel, sender: id) -> BOOL { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, windowShouldClose: sender] + } + } + extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowWillClose: notification]; + } + } + extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) { + unsafe { + with_hopp_app(&*this, |state| { + let id = state.window.ns_window().unwrap() as id; + + set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidResize: notification]; + } + } + extern "C" fn on_window_did_move(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidMove: notification]; + } + } + extern "C" fn on_window_did_change_backing_properties( + this: &Object, + _cmd: Sel, + notification: id, + ) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification]; + } + } + extern "C" fn on_window_did_become_key(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidBecomeKey: notification]; + } + } + extern "C" fn on_window_did_resign_key(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidResignKey: notification]; + } + } + extern "C" fn on_dragging_entered(this: &Object, _cmd: Sel, notification: id) -> BOOL { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, draggingEntered: notification] + } + } + extern "C" fn on_prepare_for_drag_operation( + this: &Object, + _cmd: Sel, + notification: id, + ) -> BOOL { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, prepareForDragOperation: notification] + } + } + extern "C" fn on_perform_drag_operation(this: &Object, _cmd: Sel, sender: id) -> BOOL { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, performDragOperation: sender] + } + } + extern "C" fn on_conclude_drag_operation(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, concludeDragOperation: notification]; + } + } + extern "C" fn on_dragging_exited(this: &Object, _cmd: Sel, notification: id) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, draggingExited: notification]; + } + } + extern "C" fn on_window_will_use_full_screen_presentation_options( + this: &Object, + _cmd: Sel, + window: id, + proposed_options: NSUInteger, + ) -> NSUInteger { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options] + } + } + extern "C" fn on_window_did_enter_full_screen(this: &Object, _cmd: Sel, notification: id) { + unsafe { + with_hopp_app(&*this, |state| { + state.window.emit("did-enter-fullscreen", ()).unwrap(); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidEnterFullScreen: notification]; + } + } + extern "C" fn on_window_will_enter_full_screen(this: &Object, _cmd: Sel, notification: id) { + unsafe { + with_hopp_app(&*this, |state| { + state.window.emit("will-enter-fullscreen", ()).unwrap(); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowWillEnterFullScreen: notification]; + } + } + extern "C" fn on_window_did_exit_full_screen(this: &Object, _cmd: Sel, notification: id) { + unsafe { + with_hopp_app(&*this, |state| { + state.window.emit("did-exit-fullscreen", ()).unwrap(); + + let id = state.window.ns_window().unwrap() as id; + set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidExitFullScreen: notification]; + } + } + extern "C" fn on_window_will_exit_full_screen(this: &Object, _cmd: Sel, notification: id) { + unsafe { + with_hopp_app(&*this, |state| { + state.window.emit("will-exit-fullscreen", ()).unwrap(); + }); + + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowWillExitFullScreen: notification]; + } + } + extern "C" fn on_window_did_fail_to_enter_full_screen( + this: &Object, + _cmd: Sel, + window: id, + ) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window]; + } + } + extern "C" fn on_effective_appearance_did_change( + this: &Object, + _cmd: Sel, + notification: id, + ) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification]; + } + } + extern "C" fn on_effective_appearance_did_changed_on_main_thread( + this: &Object, + _cmd: Sel, + notification: id, + ) { + unsafe { + let super_del: id = *this.get_ivar("super_delegate"); + let _: () = msg_send![ + super_del, + effectiveAppearanceDidChangedOnMainThread: notification + ]; + } + } + + // extern fn on_dealloc(this: &Object, cmd: Sel) { + // unsafe { + // let super_del: id = *this.get_ivar("super_delegate"); + // let _: () = msg_send![super_del, dealloc]; + // } + // } + + // extern fn on_mark_is_checking_zoomed_in(this: &Object, cmd: Sel) { + // unsafe { + // let super_del: id = *this.get_ivar("super_delegate"); + // let _: () = msg_send![super_del, markIsCheckingZoomedIn]; + // } + // } + + // extern fn on_clear_is_checking_zoomed_in(this: &Object, cmd: Sel) { + // unsafe { + // let super_del: id = *this.get_ivar("super_delegate"); + // let _: () = msg_send![super_del, clearIsCheckingZoomedIn]; + // } + // } + + // Are we deallocing this properly ? (I miss safe Rust :( ) + let app_state = HoppAppState { window }; + let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void; + + ns_win.setDelegate_(delegate!("MainWindowDelegate", { + window: id = ns_win, + hoppApp: *mut c_void = app_box, + toolbar: id = cocoa::base::nil, + super_delegate: id = current_delegate, + // (dealloc) => on_dealloc as extern fn(&Object, Sel), + // (markIsCheckingZoomedIn) => on_mark_is_checking_zoomed_in as extern fn(&Object, Sel), + // (clearIsCheckingZoomedIn) => on_clear_is_checking_zoomed_in as extern fn(&Object, Sel), + (windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL, + (windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id), + (windowDidResize:) => on_window_did_resize as extern fn(&Object, Sel, id), + (windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id), + (windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id), + (windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id), + (windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id), + (draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL, + (prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL, + (performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL, + (concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id), + (draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id), + (window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger, + (windowDidEnterFullScreen:) => on_window_did_enter_full_screen as extern fn(&Object, Sel, id), + (windowWillEnterFullScreen:) => on_window_will_enter_full_screen as extern fn(&Object, Sel, id), + (windowDidExitFullScreen:) => on_window_did_exit_full_screen as extern fn(&Object, Sel, id), + (windowWillExitFullScreen:) => on_window_will_exit_full_screen as extern fn(&Object, Sel, id), + (windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id), + (effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id), + (effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id) + })) + } + + app.get_window("main").unwrap().set_transparent_titlebar(); + + let window_handle = app.get_window("main").unwrap(); + update_window_theme(&window_handle, HexColor::WHITE); + + // Control window theme based on app update_window + app.listen_global("hopp-bg-changed", move |ev| { + let payload = serde_json::from_str::<&str>(ev.payload().unwrap()) + .unwrap() + .trim(); + + let color = HexColor::parse_rgb(payload).unwrap(); + + update_window_theme(&window_handle, color); + }); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 75e94bdf..682dbf03 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,6 +3,20 @@ windows_subsystem = "windows" )] +#[cfg(target_os = "macos")] +#[macro_use] +extern crate cocoa; + +#[cfg(target_os = "macos")] +#[macro_use] +extern crate objc; + +#[cfg(target_os = "macos")] +mod mac; + +#[cfg(target_os = "windows")] +mod win; + mod app; mod metrics; mod models; @@ -24,6 +38,8 @@ fn build_and_run_app(app: AppState) { let auto_start_plugin = tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec![])); + let window_state_plugin = tauri_plugin_window_state::Builder::default().build(); + let system_tray = SystemTray::new() .with_menu(SystemTrayMenu::new().add_item(CustomMenuItem::new("quit".to_string(), "Quit"))); @@ -31,6 +47,7 @@ fn build_and_run_app(app: AppState) { .plugin(log_plugin) .plugin(store_plugin) .plugin(auto_start_plugin) + .plugin(window_state_plugin) .setup(|app| { let window = app.get_window("main").unwrap(); let state = AppState::new(); @@ -49,6 +66,20 @@ fn build_and_run_app(app: AppState) { } }); + if cfg!(target_os = "macos") { + #[cfg(target_os = "macos")] + use mac::window::setup_mac_window; + + #[cfg(target_os = "macos")] + setup_mac_window(app); + } else if cfg!(target_os = "windows") { + #[cfg(target_os = "windows")] + use win::window::setup_win_window; + + #[cfg(target_os = "windows")] + setup_win_window(app); + } + Ok(()) }) .system_tray(system_tray) diff --git a/src-tauri/src/win/mod.rs b/src-tauri/src/win/mod.rs new file mode 100644 index 00000000..61b63f17 --- /dev/null +++ b/src-tauri/src/win/mod.rs @@ -0,0 +1 @@ +pub mod window; diff --git a/src-tauri/src/win/window.rs b/src-tauri/src/win/window.rs new file mode 100644 index 00000000..7d8c0da5 --- /dev/null +++ b/src-tauri/src/win/window.rs @@ -0,0 +1,96 @@ +use hex_color::HexColor; +use tauri::App; +use tauri::Manager; + +use std::mem::transmute; +use std::{ffi::c_void, mem::size_of, ptr}; + +use windows::Win32::UI::Controls::{ + WTA_NONCLIENT, WTNCA_NODRAWICON, WTNCA_NOMIRRORHELP, WTNCA_NOSYSMENU, +}; + +use windows::Win32::Foundation::BOOL; +use windows::Win32::Foundation::COLORREF; +use windows::Win32::Foundation::HWND; +use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute; +use windows::Win32::Graphics::Dwm::DWMWA_CAPTION_COLOR; +use windows::Win32::Graphics::Dwm::DWMWA_USE_IMMERSIVE_DARK_MODE; +use windows::Win32::UI::Controls::SetWindowThemeAttribute; +use windows::Win32::UI::Controls::WTNCA_NODRAWCAPTION; + +use winver::WindowsVersion; + +fn hex_color_to_colorref(color: HexColor) -> COLORREF { + // TODO: Remove this unsafe, This operation doesn't need to be unsafe! + unsafe { COLORREF(transmute::<[u8; 4], u32>([color.r, color.g, color.b, 0])) } +} + +struct WinThemeAttribute { + flag: u32, + mask: u32, +} + +#[cfg(target_os = "windows")] +fn update_bg_color(hwnd: &HWND, bg_color: HexColor) { + let use_dark_mode = BOOL::from(true); + + let final_color = hex_color_to_colorref(bg_color); + + unsafe { + DwmSetWindowAttribute( + HWND(hwnd.0), + DWMWA_USE_IMMERSIVE_DARK_MODE, + ptr::addr_of!(use_dark_mode) as *const c_void, + size_of::().try_into().unwrap(), + ) + .unwrap(); + } + + let version = WindowsVersion::detect().unwrap(); + if version >= WindowsVersion::new(10, 0, 22000) { + unsafe { + DwmSetWindowAttribute( + HWND(hwnd.0), + DWMWA_CAPTION_COLOR, + ptr::addr_of!(final_color) as *const c_void, + size_of::().try_into().unwrap(), + ) + .unwrap(); + } + } + + let flags = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON; + let mask = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON | WTNCA_NOSYSMENU | WTNCA_NOMIRRORHELP; + let options = WinThemeAttribute { flag: flags, mask }; + + unsafe { + SetWindowThemeAttribute( + HWND(hwnd.0), + WTA_NONCLIENT, + ptr::addr_of!(options) as *const c_void, + size_of::().try_into().unwrap(), + ) + .unwrap(); + } +} + +#[cfg(target_os = "windows")] +pub fn setup_win_window(app: &mut App) { + let window = app.get_window("main").unwrap(); + let win_handle = window.hwnd().unwrap(); + + let win_clone = win_handle.clone(); + + //TODO: Update this to update based on theme + app.listen_global("hopp-bg-changed", move |ev| { + let payload = serde_json::from_str::<&str>(ev.payload().unwrap()) + .unwrap() + .trim(); + + let color = HexColor::parse_rgb(payload).unwrap(); + + update_bg_color(&HWND(win_clone.0), color); + }); + + update_bg_color(&HWND(win_handle.0), HexColor::rgb(9, 9, 11)); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 89c16253..fe908328 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -111,7 +111,6 @@ "title": "pachtop", "alwaysOnTop": false, "width": 1920, - "minWidth": 450, "decorations": true } ]