diff --git a/src-tauri/gen/schemas/windows-schema.json b/src-tauri/gen/schemas/windows-schema.json index e35feebf..4776f4b7 100644 --- a/src-tauri/gen/schemas/windows-schema.json +++ b/src-tauri/gen/schemas/windows-schema.json @@ -2216,6 +2216,13 @@ "shell:allow-open" ] }, + { + "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.", + "type": "string", + "enum": [ + "shell:allow-spawn" + ] + }, { "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.", "type": "string", @@ -2244,6 +2251,13 @@ "shell:deny-open" ] }, + { + "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.", + "type": "string", + "enum": [ + "shell:deny-spawn" + ] + }, { "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.", "type": "string", @@ -2498,6 +2512,69 @@ "clipboard-manager:deny-write-text" ] }, + { + "description": "deep-link:default -> Allows reading the opened deep link via the get_current command", + "type": "string", + "enum": [ + "deep-link:default" + ] + }, + { + "description": "deep-link:allow-get-current -> Enables the get_current command without any pre-configured scope.", + "type": "string", + "enum": [ + "deep-link:allow-get-current" + ] + }, + { + "description": "deep-link:allow-is-registered -> Enables the is_registered command without any pre-configured scope.", + "type": "string", + "enum": [ + "deep-link:allow-is-registered" + ] + }, + { + "description": "deep-link:allow-register -> Enables the register command without any pre-configured scope.", + "type": "string", + "enum": [ + "deep-link:allow-register" + ] + }, + { + "description": "deep-link:allow-unregister -> Enables the unregister command without any pre-configured scope.", + "type": "string", + "enum": [ + "deep-link:allow-unregister" + ] + }, + { + "description": "deep-link:deny-get-current -> Denies the get_current command without any pre-configured scope.", + "type": "string", + "enum": [ + "deep-link:deny-get-current" + ] + }, + { + "description": "deep-link:deny-is-registered -> Denies the is_registered command without any pre-configured scope.", + "type": "string", + "enum": [ + "deep-link:deny-is-registered" + ] + }, + { + "description": "deep-link:deny-register -> Denies the register command without any pre-configured scope.", + "type": "string", + "enum": [ + "deep-link:deny-register" + ] + }, + { + "description": "deep-link:deny-unregister -> Denies the unregister command without any pre-configured scope.", + "type": "string", + "enum": [ + "deep-link:deny-unregister" + ] + }, { "type": "string", "enum": [ @@ -5323,6 +5400,13 @@ "shell:allow-open" ] }, + { + "description": "shell:allow-spawn -> Enables the spawn command without any pre-configured scope.", + "type": "string", + "enum": [ + "shell:allow-spawn" + ] + }, { "description": "shell:allow-stdin-write -> Enables the stdin_write command without any pre-configured scope.", "type": "string", @@ -5351,6 +5435,13 @@ "shell:deny-open" ] }, + { + "description": "shell:deny-spawn -> Denies the spawn command without any pre-configured scope.", + "type": "string", + "enum": [ + "shell:deny-spawn" + ] + }, { "description": "shell:deny-stdin-write -> Denies the stdin_write command without any pre-configured scope.", "type": "string", @@ -5533,6 +5624,13 @@ "updater:allow-check" ] }, + { + "description": "updater:allow-download -> Enables the download command without any pre-configured scope.", + "type": "string", + "enum": [ + "updater:allow-download" + ] + }, { "description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.", "type": "string", @@ -5540,6 +5638,13 @@ "updater:allow-download-and-install" ] }, + { + "description": "updater:allow-install -> Enables the install command without any pre-configured scope.", + "type": "string", + "enum": [ + "updater:allow-install" + ] + }, { "description": "updater:deny-check -> Denies the check command without any pre-configured scope.", "type": "string", @@ -5547,6 +5652,13 @@ "updater:deny-check" ] }, + { + "description": "updater:deny-download -> Denies the download command without any pre-configured scope.", + "type": "string", + "enum": [ + "updater:deny-download" + ] + }, { "description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.", "type": "string", @@ -5554,6 +5666,13 @@ "updater:deny-download-and-install" ] }, + { + "description": "updater:deny-install -> Denies the install command without any pre-configured scope.", + "type": "string", + "enum": [ + "updater:deny-install" + ] + }, { "description": "webview:default -> Default permissions for the plugin.", "type": "string", @@ -5897,6 +6016,13 @@ "window:allow-minimize" ] }, + { + "description": "window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "enum": [ + "window:allow-monitor-from-point" + ] + }, { "description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.", "type": "string", @@ -6331,6 +6457,13 @@ "window:deny-minimize" ] }, + { + "description": "window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "enum": [ + "window:deny-monitor-from-point" + ] + }, { "description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.", "type": "string", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 92a0fd39..b1f6daf9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -69,7 +69,10 @@ mod plugin; mod render; mod updates; mod window_menu; -mod tauri_plugin_traffic_light; +#[cfg(target_os = "macos")] +mod tauri_plugin_mac_window; +#[cfg(target_os = "windows")] +mod tauri_plugin_windows_window; async fn migrate_db(app_handle: &AppHandle, db: &Mutex>) -> Result<(), String> { let pool = &*db.lock().await; @@ -1533,41 +1536,49 @@ async fn cmd_check_for_updates( #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - tauri::Builder::default() + let mut builder = tauri::Builder::default() .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_window_state::Builder::default().with_denylist(&["settings"]).build()) + .plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_os::init()) - .plugin(tauri_plugin_fs::init()) - .plugin(tauri_plugin_traffic_light::init()) - .plugin( - tauri_plugin_log::Builder::default() - .targets([ - Target::new(TargetKind::Stdout), - Target::new(TargetKind::LogDir { file_name: None }), - Target::new(TargetKind::Webview), - ]) - .level_for("cookie_store", log::LevelFilter::Info) - .level_for("h2", log::LevelFilter::Info) - .level_for("hyper", log::LevelFilter::Info) - .level_for("hyper_rustls", log::LevelFilter::Info) - .level_for("reqwest", log::LevelFilter::Info) - .level_for("sqlx", log::LevelFilter::Warn) - .level_for("tao", log::LevelFilter::Info) - .level_for("tokio_util", log::LevelFilter::Info) - .level_for("tonic", log::LevelFilter::Info) - .level_for("tower", log::LevelFilter::Info) - .level_for("tracing", log::LevelFilter::Info) - .with_colors(ColoredLevelConfig::default()) - .level(if is_dev() { - log::LevelFilter::Trace - } else { - log::LevelFilter::Info - }) - .build(), - ) + .plugin(tauri_plugin_fs::init()); + + #[cfg(target_os = "windows")] { + builder = builder.plugin(tauri_plugin_windows_window::init()); + } + + #[cfg(target_os = "macos")] { + builder = builder.plugin(tauri_plugin_mac_window::init()); + } + + builder.plugin( + tauri_plugin_log::Builder::default() + .targets([ + Target::new(TargetKind::Stdout), + Target::new(TargetKind::LogDir { file_name: None }), + Target::new(TargetKind::Webview), + ]) + .level_for("cookie_store", log::LevelFilter::Info) + .level_for("h2", log::LevelFilter::Info) + .level_for("hyper", log::LevelFilter::Info) + .level_for("hyper_rustls", log::LevelFilter::Info) + .level_for("reqwest", log::LevelFilter::Info) + .level_for("sqlx", log::LevelFilter::Warn) + .level_for("tao", log::LevelFilter::Info) + .level_for("tokio_util", log::LevelFilter::Info) + .level_for("tonic", log::LevelFilter::Info) + .level_for("tower", log::LevelFilter::Info) + .level_for("tracing", log::LevelFilter::Info) + .with_colors(ColoredLevelConfig::default()) + .level(if is_dev() { + log::LevelFilter::Trace + } else { + log::LevelFilter::Info + }) + .build(), + ) .setup(|app| { let app_data_dir = app.path().app_data_dir().unwrap(); let app_config_dir = app.path().app_config_dir().unwrap(); @@ -1744,10 +1755,9 @@ fn is_dev() -> bool { fn create_nested_window(window: &WebviewWindow, label: &str, url: &str, title: &str) -> WebviewWindow { info!("Create new nested window label={label}"); - let pos = window.outer_position().unwrap(); let mut win_builder = tauri::WebviewWindowBuilder::new( window, - label, + format!("nested_{}_{}", window.label(), label), WebviewUrl::App(url.into()), ) .resizable(true) @@ -1756,14 +1766,7 @@ fn create_nested_window(window: &WebviewWindow, label: &str, url: &str, title: & .title(title) .parent(&window) .unwrap() - .position( - (pos.x + 20) as f64, - (pos.y + 20) as f64, - ) - .inner_size( - 500.0f64, - 300.0f64, - ); + .inner_size(700.0f64, 600.0f64); // Add macOS-only things #[cfg(target_os = "macos")] @@ -1827,7 +1830,7 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow { handle.set_menu(menu).expect("Failed to set app menu"); let window_num = handle.webview_windows().len(); - let label = format!("wnd_{}", window_num); + let label = format!("main_{}", window_num); info!("Create new window label={label}"); let mut win_builder = tauri::WebviewWindowBuilder::new( handle, diff --git a/src-tauri/src/tauri_plugin_traffic_light.rs b/src-tauri/src/tauri_plugin_mac_window.rs similarity index 80% rename from src-tauri/src/tauri_plugin_traffic_light.rs rename to src-tauri/src/tauri_plugin_mac_window.rs index 581f1aef..c1a032ce 100644 --- a/src-tauri/src/tauri_plugin_traffic_light.rs +++ b/src-tauri/src/tauri_plugin_mac_window.rs @@ -1,3 +1,4 @@ +use hex_color::HexColor; use objc::{msg_send, sel, sel_impl}; use rand::{distributions::Alphanumeric, Rng}; use tauri::{Manager, plugin::{Builder, TauriPlugin}, Runtime, Window}; @@ -6,23 +7,108 @@ const WINDOW_CONTROL_PAD_X: f64 = 13.0; const WINDOW_CONTROL_PAD_Y: f64 = 18.0; struct UnsafeWindowHandle(*mut std::ffi::c_void); + unsafe impl Send for UnsafeWindowHandle {} + unsafe impl Sync for UnsafeWindowHandle {} pub fn init() -> TauriPlugin { - Builder::new("traffic_light_positioner") + Builder::new("mac_window") .on_window_ready(|window| { - #[cfg(target_os = "macos")] - setup_traffic_light_positioner(window); + #[cfg(target_os = "macos")] { + setup_traffic_light_positioner(&window); + let h = window.app_handle(); + + let window_for_theme = window.clone(); + h.listen("yaak_bg_changed", move |ev| { + let payload = serde_json::from_str::<&str>(ev.payload()) + .unwrap() + .trim(); + let color = HexColor::parse_rgb(payload).unwrap(); + update_window_theme(window_for_theme.clone(), color); + }); + + let window_for_title = window.clone(); + h.listen("yaak_title_changed", move |ev| { + let payload = serde_json::from_str::<&str>(ev.payload()) + .unwrap() + .trim(); + update_window_title(window_for_title.clone(), payload.to_string()); + }); + } return; }) .build() } #[cfg(target_os = "macos")] -fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64) { +fn update_window_title(window: Window, title: String) { + use cocoa::{ + appkit::NSWindow, + base::nil, + foundation::NSString, + }; + + unsafe { + let window_handle = UnsafeWindowHandle(window.ns_window().unwrap()); + + let window2 = window.clone(); + let label = window.label().to_string(); + let _ = window.run_on_main_thread(move || { + let win_title = NSString::alloc(nil).init_str(&title); + let handle = window_handle; + NSWindow::setTitle_(handle.0 as cocoa::base::id, win_title); + position_traffic_lights( + UnsafeWindowHandle(window2.ns_window().expect("Failed to create window handle")), + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + label, + ); + }); + }} + +#[cfg(target_os = "macos")] +fn update_window_theme(window: 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; + let label = window.label().to_string(); + + unsafe { + let window_handle = UnsafeWindowHandle(window.ns_window().unwrap()); + + let window2 = window.clone(); + 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); + position_traffic_lights( + UnsafeWindowHandle(window2.ns_window().expect("Failed to create window handle")), + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + label, + ); + }); + } +} + +#[cfg(target_os = "macos")] +fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, label: String) { + if label.starts_with("nested_") { + return; + } + use cocoa::appkit::{NSView, NSWindow, NSWindowButton}; use cocoa::foundation::NSRect; + let ns_window = ns_window_handle.0 as cocoa::base::id; unsafe { let close = ns_window.standardWindowButton_(NSWindowButton::NSWindowCloseButton); @@ -59,7 +145,7 @@ struct WindowState { } #[cfg(target_os = "macos")] -pub fn setup_traffic_light_positioner(window: Window) { +pub fn setup_traffic_light_positioner(window: &Window) { use cocoa::delegate; use cocoa::appkit::NSWindow; use cocoa::base::{BOOL, id}; @@ -67,11 +153,11 @@ pub fn setup_traffic_light_positioner(window: Window) { use objc::runtime::{Object, Sel}; use std::ffi::c_void; - // Do the initial positioning position_traffic_lights( UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")), WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y, + window.label().to_string(), ); // Ensure they stay in place while resizing the window. @@ -86,6 +172,7 @@ pub fn setup_traffic_light_positioner(window: Window) { func(ptr); } + unsafe { let ns_win = window .ns_window() @@ -115,11 +202,11 @@ pub fn setup_traffic_light_positioner(window: Window) { .expect("NS window should exist on state to handle resize") as id; - #[cfg(target_os = "macos")] position_traffic_lights( - UnsafeWindowHandle(id as *mut std::ffi::c_void), + UnsafeWindowHandle(id as *mut c_void), WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y, + state.window.label().to_string(), ); }); @@ -248,9 +335,10 @@ pub fn setup_traffic_light_positioner(window: Window) { let id = state.window.ns_window().expect("Failed to emit event") as id; position_traffic_lights( - UnsafeWindowHandle(id as *mut std::ffi::c_void), + UnsafeWindowHandle(id as *mut c_void), WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y, + state.window.label().to_string(), ); }); @@ -309,10 +397,10 @@ pub fn setup_traffic_light_positioner(window: Window) { } } - // Are we deallocing this properly ? (I miss safe Rust :( ) + // Are we de-allocing this properly ? (I miss safe Rust :( ) let window_label = window.label().to_string(); - let app_state = WindowState { window }; + let app_state = WindowState { window: window.clone() }; let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void; let random_str: String = rand::thread_rng() .sample_iter(&Alphanumeric) diff --git a/src-tauri/src/tauri_plugin_windows_window.rs b/src-tauri/src/tauri_plugin_windows_window.rs new file mode 100644 index 00000000..169a0679 --- /dev/null +++ b/src-tauri/src/tauri_plugin_windows_window.rs @@ -0,0 +1,89 @@ +use hex_color::HexColor; +use tauri::{ Runtime, Window}; + +use std::mem::transmute; +use std::{ptr, ffi::c_void, mem::size_of}; +use tauri::plugin::{Builder, TauriPlugin}; + +use windows::Win32::UI::Controls::{WTA_NONCLIENT, WTNCA_NODRAWICON, WTNCA_NOSYSMENU, WTNCA_NOMIRRORHELP}; + +use windows::Win32::UI::Controls::SetWindowThemeAttribute; +use windows::Win32::UI::Controls::WTNCA_NODRAWCAPTION; +use windows::Win32::Graphics::Dwm::DWMWA_CAPTION_COLOR; +use windows::Win32::Foundation::COLORREF; +use windows::Win32::Foundation::BOOL; +use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute; +use windows::Win32::Foundation::HWND; +use windows::Win32::Graphics::Dwm::{DWMWA_USE_IMMERSIVE_DARK_MODE}; + +pub fn init() -> TauriPlugin { + Builder::new("windows_window") + .on_window_ready(|window| { + #[cfg(target_os = "windows")] + setup_win_window(window); + return; + }) + .build() +} + +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(); + + 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 }; + + 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(window: Window) { + let win_handle = window.hwnd().unwrap(); + let win_clone = win_handle.clone(); + + window.listen_global("yaak_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); + }); +} diff --git a/src-web/components/Settings/SettingsDialog.tsx b/src-web/components/Settings/SettingsDialog.tsx index ac347df6..87b6480a 100644 --- a/src-web/components/Settings/SettingsDialog.tsx +++ b/src-web/components/Settings/SettingsDialog.tsx @@ -32,7 +32,7 @@ export const SettingsDialog = ({ fullscreen }: Props) => { {fullscreen && (
Settings
diff --git a/src-web/components/SettingsDropdown.tsx b/src-web/components/SettingsDropdown.tsx index 5100903e..15864aab 100644 --- a/src-web/components/SettingsDropdown.tsx +++ b/src-web/components/SettingsDropdown.tsx @@ -14,7 +14,6 @@ import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; import { useDialog } from './DialogContext'; import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog'; -import { SettingsDialog } from './Settings/SettingsDialog'; export function SettingsDropdown() { const importData = useImportData(); @@ -26,13 +25,12 @@ export function SettingsDropdown() { const routes = useAppRoutes(); const workspaceId = useActiveWorkspaceId(); - const showSettings = () => { - dialog.show({ - id: 'settings', - size: 'dynamic', - noScroll: true, - noPadding: true, - render: () => , + const showSettings = async () => { + if (!workspaceId) return; + await invoke('cmd_new_nested_window', { + url: routes.paths.workspaceSettings({ workspaceId }), + label: 'settings', + title: 'Yaak Settings', }); }; @@ -63,20 +61,6 @@ export function SettingsDropdown() { }); }, }, - { - key: 'foo', - label: 'Foo', - hotKeyAction: 'hotkeys.showHelp', - leftSlot: , - onSelect: async () => { - if (!workspaceId) return; - await invoke('cmd_new_nested_window', { - url: routes.paths.workspaceSettings({ workspaceId }), - label: 'settings', - title: 'Yaak Settings', - }); - }, - }, { key: 'import-data', label: 'Import Data', diff --git a/src-web/components/WorkspaceHeader.tsx b/src-web/components/WorkspaceHeader.tsx index 9882f3fa..d9ce1d7d 100644 --- a/src-web/components/WorkspaceHeader.tsx +++ b/src-web/components/WorkspaceHeader.tsx @@ -40,7 +40,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
-
+
{(osInfo?.osType === 'linux' || osInfo?.osType === 'windows') && ( diff --git a/src-web/hooks/useSyncWindowTitle.ts b/src-web/hooks/useSyncWindowTitle.ts index 29b3dfa6..51a5c712 100644 --- a/src-web/hooks/useSyncWindowTitle.ts +++ b/src-web/hooks/useSyncWindowTitle.ts @@ -5,6 +5,7 @@ import { useActiveEnvironment } from './useActiveEnvironment'; import { useActiveRequest } from './useActiveRequest'; import { useActiveWorkspace } from './useActiveWorkspace'; import { useOsInfo } from './useOsInfo'; +import { emit } from '@tauri-apps/api/event'; export function useSyncWindowTitle() { const activeRequest = useActiveRequest(); @@ -26,13 +27,12 @@ export function useSyncWindowTitle() { newTitle += ` – ${fallbackRequestName(activeRequest)}`; } - // TODO: This resets the stoplight position so we can't use it on macOS yet. Perhaps - // we can + // TODO: This resets the stoplight position so we can't use it on macOS yet. So we send + // a custom command instead if (osInfo?.osType !== 'macos') { - console.log('DO IT', osInfo?.osType); getCurrent().setTitle(newTitle).catch(console.error); } else { - // emit('yaak_title_changed', newTitle).catch(console.error); + emit('yaak_title_changed', newTitle).catch(console.error); } }, [activeEnvironment, activeRequest, activeWorkspace, osInfo?.osType]); }