From a14f3a3d8f92284151036c5499b726f9c2f01b1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Wed, 20 May 2026 10:47:45 +0200 Subject: [PATCH 1/6] migrate from poc --- src-tauri/Cargo.toml | 3 + src-tauri/src/bin/defguard-client.rs | 17 +- src-tauri/src/lib.rs | 2 +- src-tauri/src/tray.rs | 2 +- src-tauri/src/window.rs | 156 ------------ src-tauri/src/window_manager/macos.rs | 100 ++++++++ src-tauri/src/window_manager/mod.rs | 97 ++++++++ src-tauri/src/window_manager/windows.rs | 306 ++++++++++++++++++++++++ 8 files changed, 511 insertions(+), 172 deletions(-) delete mode 100644 src-tauri/src/window.rs create mode 100644 src-tauri/src/window_manager/macos.rs create mode 100644 src-tauri/src/window_manager/mod.rs create mode 100644 src-tauri/src/window_manager/windows.rs diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3ad98c6d..4425dd8c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -141,6 +141,9 @@ windows = { version = "0.62", features = [ "Win32", "Win32_System", "Win32_System_RemoteDesktop", + "Win32_Graphics_Gdi", + "Win32_UI_HiDpi", + "Win32_Foundation", ] } windows-acl = "0.3" windows-service = "0.8" diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 1428af49..df9173a5 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -29,11 +29,11 @@ use defguard_client::{ service, tray::{configure_tray_icon, setup_tray, show_main_window}, utils::load_log_targets, - window::*, + window_manager::*, LOG_FILENAME, VERSION, }; use log::{Level, LevelFilter}; -use tauri::{AppHandle, Builder, Manager, RunEvent, WebviewWindowBuilder, WindowEvent}; +use tauri::{AppHandle, Builder, Manager, RunEvent, WindowEvent}; use tauri_plugin_log::{Target, TargetKind}; #[macro_use] @@ -348,18 +348,7 @@ fn main() { let state = AppState::new(config, provisioning_config); app.manage(state); - // Open new UI window. - WebviewWindowBuilder::new(app, NEW_UI_WINDOW_ID, new_ui_url()) - .title("New UI") - .inner_size(NEW_UI_WIDTH, NEW_UI_HEIGHT) - .visible(false) - .build()?; - - // Open old UI window. - WebviewWindowBuilder::new(app, OLD_UI_WINDOW_ID, old_ui_url()) - .title("Old UI") - .inner_size(OLD_UI_WIDTH, OLD_UI_HEIGHT) - .build()?; + WindowManager::open_full_view(app_handle)?; info!("App setup completed, log level: {log_level}"); Ok(()) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2d3137f0..abcf5937 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -32,7 +32,7 @@ pub mod service; pub mod tray; pub mod utils; pub mod wg_config; -pub mod window; +pub mod window_manager; pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "-", env!("VERGEN_GIT_SHA")); pub const MIN_CORE_VERSION: Version = Version::new(1, 6, 0); diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 2452bdf5..2cebe81b 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -15,7 +15,7 @@ use crate::{ database::{models::location::Location, DB_POOL}, error::Error, events::EventKey, - window::{show_new_ui_window_near_tray, NEW_UI_WINDOW_ID, OLD_UI_WINDOW_ID}, + window_manager::{show_new_ui_window_near_tray, NEW_UI_WINDOW_ID, OLD_UI_WINDOW_ID}, ConnectionType, }; diff --git a/src-tauri/src/window.rs b/src-tauri/src/window.rs deleted file mode 100644 index dcfc1332..00000000 --- a/src-tauri/src/window.rs +++ /dev/null @@ -1,156 +0,0 @@ -use tauri::{ - webview::WebviewWindowBuilder, AppHandle, LogicalPosition, Manager, Monitor, PhysicalSize, - Position, WebviewUrl, WebviewWindow, -}; - -use crate::appstate::AppState; - -pub const NEW_UI_WINDOW_ID: &str = "new-ui"; -pub const OLD_UI_WINDOW_ID: &str = "old-ui"; -pub const NEW_UI_WIDTH: f64 = 360.0; -pub const NEW_UI_HEIGHT: f64 = 675.0; -pub const OLD_UI_WIDTH: f64 = 920.0; -pub const OLD_UI_HEIGHT: f64 = 720.0; -#[cfg(windows)] -const TASKBAR_HEIGHT: f64 = 48.0; -#[cfg(not(windows))] -const TASKBAR_HEIGHT: f64 = 0.0; - -#[must_use] -pub fn new_ui_url() -> WebviewUrl { - if cfg!(defguard_client_dev) { - WebviewUrl::External("http://localhost:5072".parse().unwrap()) - } else { - WebviewUrl::App("new-ui/".into()) - } -} - -#[must_use] -pub fn old_ui_url() -> WebviewUrl { - if cfg!(defguard_client_dev) { - WebviewUrl::External("http://localhost:5071".parse().unwrap()) - } else { - WebviewUrl::App("old-ui/index.html".into()) - } -} - -/// Try to get monitor at the given position, with a fall back to primary monitor, and then to the -/// first one on the list of available monitors. -fn get_monitor_for_position(app: &AppHandle, x: f64, y: f64) -> Option { - if let Ok(Some(monitor)) = app.monitor_from_point(x, y) { - return Some(monitor); - } - - if let Ok(Some(monitor)) = app.primary_monitor() { - return Some(monitor); - } - - // On macOS, it seems this is the only working method (as of Tauri 2.11), but fortunately it - // returns the current monitor as the first one. - if let Ok(mut monitors) = app.available_monitors() { - monitors.pop() - } else { - None - } -} - -fn get_tray_window_position( - app: &AppHandle, - size: PhysicalSize, -) -> Option> { - let app_state = app.state::(); - let tray_position = app_state.tray_click_position.lock().unwrap().to_owned()?; - - let monitor = get_monitor_for_position(app, tray_position.x, tray_position.y)?; - - let scale_factor = monitor.scale_factor(); - let monitor_position = monitor.position().to_logical::(scale_factor); - let monitor_size = monitor.size().to_logical::(scale_factor); - let tray_position = tray_position.to_logical::(scale_factor); - let window_size = size.to_logical::(scale_factor); - - let mut x = tray_position.x; - let mut y = tray_position.y; - - x = x.clamp( - monitor_position.x, - monitor_position.x + monitor_size.width - window_size.width, - ); - y = y.clamp( - monitor_position.y, - monitor_position.y + monitor_size.height - window_size.height - TASKBAR_HEIGHT, - ); - - Some(LogicalPosition::new(x, y)) -} - -fn position_window_near_tray(app: &AppHandle, window: &WebviewWindow) { - let size = window.outer_size().unwrap_or_default(); - if let Some(position) = get_tray_window_position(app, size) { - if let Err(err) = window.set_position(Position::Logical(position)) { - warn!("Failed to position window near tray icon: {err}"); - } - } -} - -fn show_new_ui_window_internal(app: &AppHandle, near_tray: bool) { - let window = if let Some(window) = app.get_webview_window(NEW_UI_WINDOW_ID) { - let _ = window.unminimize(); - window - } else { - WebviewWindowBuilder::new(app, NEW_UI_WINDOW_ID, new_ui_url()) - .title("New UI") - .inner_size(NEW_UI_WIDTH, NEW_UI_HEIGHT) - .build() - .unwrap() - }; - if near_tray { - position_window_near_tray(app, &window); - } - #[cfg(target_os = "macos")] - let _ = app.show(); - let _ = window.show(); - let _ = window.set_focus(); -} - -pub(crate) fn show_new_ui_window(app: &AppHandle) { - show_new_ui_window_internal(app, false); -} - -pub(crate) fn show_new_ui_window_near_tray(app: &AppHandle) { - show_new_ui_window_internal(app, true); -} - -#[tauri::command] -pub fn open_new_ui_window(app: AppHandle) { - show_new_ui_window(&app); -} - -#[tauri::command] -pub fn open_old_ui_window(app: AppHandle) { - let _window = WebviewWindowBuilder::new(&app, OLD_UI_WINDOW_ID, old_ui_url()) - .title("Old UI") - .inner_size(OLD_UI_WIDTH, OLD_UI_HEIGHT) - .build() - .unwrap(); -} - -#[tauri::command] -pub fn swap_to_old_ui(app: AppHandle) { - WebviewWindowBuilder::new(&app, OLD_UI_WINDOW_ID, old_ui_url()) - .title("Old UI") - .inner_size(OLD_UI_WIDTH, OLD_UI_HEIGHT) - .build() - .unwrap(); - if let Some(w) = app.get_webview_window(NEW_UI_WINDOW_ID) { - w.close().unwrap(); - } -} - -#[tauri::command] -pub fn swap_to_new_ui(app: AppHandle) { - show_new_ui_window(&app); - if let Some(w) = app.get_webview_window(OLD_UI_WINDOW_ID) { - w.close().unwrap(); - } -} diff --git a/src-tauri/src/window_manager/macos.rs b/src-tauri/src/window_manager/macos.rs new file mode 100644 index 00000000..bcef5ec9 --- /dev/null +++ b/src-tauri/src/window_manager/macos.rs @@ -0,0 +1,100 @@ +use tauri::{AppHandle, LogicalPosition, Manager, Monitor, PhysicalSize, Position, WebviewWindow}; + +use crate::appstate::AppState; +use crate::window_manager::{WindowManager, NEW_UI_WINDOW_ID, OLD_UI_WINDOW_ID, TASKBAR_HEIGHT}; + +/// Try to get monitor at the given position, with a fall back to primary monitor, and then to the +/// first one on the list of available monitors. +fn get_monitor_for_position(app: &AppHandle, x: f64, y: f64) -> Option { + if let Ok(Some(monitor)) = app.monitor_from_point(x, y) { + return Some(monitor); + } + + if let Ok(Some(monitor)) = app.primary_monitor() { + return Some(monitor); + } + + // On macOS, it seems this is the only working method (as of Tauri 2.11), but fortunately it + // returns the current monitor as the first one. + if let Ok(mut monitors) = app.available_monitors() { + monitors.pop() + } else { + None + } +} + +fn get_tray_window_position( + app: &AppHandle, + size: PhysicalSize, +) -> Option> { + let app_state = app.state::(); + let tray_position = app_state.tray_click_position.lock().unwrap().to_owned()?; + + let monitor = get_monitor_for_position(app, tray_position.x, tray_position.y)?; + + let scale_factor = monitor.scale_factor(); + let monitor_position = monitor.position().to_logical::(scale_factor); + let monitor_size = monitor.size().to_logical::(scale_factor); + let tray_position = tray_position.to_logical::(scale_factor); + let window_size = size.to_logical::(scale_factor); + + let mut x = tray_position.x; + let mut y = tray_position.y; + + x = x.clamp( + monitor_position.x, + monitor_position.x + monitor_size.width - window_size.width, + ); + y = y.clamp( + monitor_position.y, + monitor_position.y + monitor_size.height - window_size.height - TASKBAR_HEIGHT, + ); + + Some(LogicalPosition::new(x, y)) +} + +fn position_window_near_tray(app: &AppHandle, window: &WebviewWindow) { + let size = window.outer_size().unwrap_or_default(); + if let Some(position) = get_tray_window_position(app, size) { + if let Err(err) = window.set_position(Position::Logical(position)) { + warn!("Failed to position window near tray icon: {err}"); + } + } +} + +impl WindowManager { + pub fn open_tray( + app: &AppHandle, + _icon_x: i32, + _icon_y: i32, + _icon_width: u32, + _icon_height: u32, + ) -> tauri::Result { + let window = if let Some(window) = app.get_webview_window(NEW_UI_WINDOW_ID) { + let _ = window.unminimize(); + window + } else { + Self::build_tray_window(app)? + }; + position_window_near_tray(app, &window); + #[cfg(target_os = "macos")] + let _ = app.show(); + let _ = window.show(); + let _ = window.set_focus(); + Ok(window) + } + + pub fn open_full_view(app: &AppHandle) -> tauri::Result { + let window = if let Some(window) = app.get_webview_window(OLD_UI_WINDOW_ID) { + let _ = window.unminimize(); + window + } else { + Self::build_full_window(app)? + }; + #[cfg(target_os = "macos")] + let _ = app.show(); + let _ = window.show(); + let _ = window.set_focus(); + Ok(window) + } +} diff --git a/src-tauri/src/window_manager/mod.rs b/src-tauri/src/window_manager/mod.rs new file mode 100644 index 00000000..6544d814 --- /dev/null +++ b/src-tauri/src/window_manager/mod.rs @@ -0,0 +1,97 @@ +use tauri::{AppHandle, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; + +pub const NEW_UI_WINDOW_ID: &str = "new-ui"; +pub const OLD_UI_WINDOW_ID: &str = "old-ui"; +pub const NEW_UI_WIDTH: f64 = 380.0; +pub const NEW_UI_HEIGHT: f64 = 640.0; +pub const OLD_UI_WIDTH: f64 = 1280.0; +pub const OLD_UI_HEIGHT: f64 = 920.0; +pub const WINDOW_GAP: f64 = 20.0; + +#[cfg(windows)] +pub const TASKBAR_HEIGHT: f64 = 48.0; +#[cfg(not(windows))] +pub const TASKBAR_HEIGHT: f64 = 0.0; + +#[must_use] +pub fn new_ui_url() -> WebviewUrl { + if cfg!(defguard_client_dev) { + WebviewUrl::External("http://localhost:5072".parse().unwrap()) + } else { + WebviewUrl::App("new-ui/".into()) + } +} + +#[must_use] +pub fn old_ui_url() -> WebviewUrl { + if cfg!(defguard_client_dev) { + WebviewUrl::External("http://localhost:5071".parse().unwrap()) + } else { + WebviewUrl::App("old-ui/index.html".into()) + } +} + +pub struct WindowManager; + +impl WindowManager { + pub fn build_tray_window(app: &AppHandle) -> tauri::Result { + WebviewWindowBuilder::new(app, NEW_UI_WINDOW_ID, new_ui_url()) + .title("New UI") + .inner_size(NEW_UI_WIDTH, NEW_UI_HEIGHT) + .resizable(false) + .decorations(false) + .visible(false) + .always_on_top(true) + .skip_taskbar(true) + .build() + } + + pub fn build_full_window(app: &AppHandle) -> tauri::Result { + WebviewWindowBuilder::new(app, OLD_UI_WINDOW_ID, old_ui_url()) + .title("Old UI") + .inner_size(OLD_UI_WIDTH, OLD_UI_HEIGHT) + .decorations(true) + .build() + } +} + +#[cfg(target_os = "windows")] +pub mod windows; + +#[cfg(not(target_os = "windows"))] +pub mod macos; + +// Export tauri commands so they can be registered in main.rs +pub(crate) fn show_new_ui_window(app: &AppHandle) { + let _ = WindowManager::open_tray(app, 0, 0, 0, 0); +} + +pub(crate) fn show_new_ui_window_near_tray(app: &AppHandle) { + let _ = WindowManager::open_tray(app, 0, 0, 0, 0); +} + +#[tauri::command] +pub fn open_new_ui_window(app: AppHandle) { + show_new_ui_window(&app); +} + +#[tauri::command] +pub fn open_old_ui_window(app: AppHandle) { + let _ = WindowManager::open_full_view(&app); +} + +#[tauri::command] +pub fn swap_to_old_ui(app: AppHandle) { + let _ = WindowManager::open_full_view(&app); + if let Some(w) = tauri::Manager::get_webview_window(&app, NEW_UI_WINDOW_ID) { + w.close().unwrap(); + } +} + +#[tauri::command] +pub fn swap_to_new_ui(app: AppHandle) { + show_new_ui_window(&app); + if let Some(w) = tauri::Manager::get_webview_window(&app, OLD_UI_WINDOW_ID) { + w.close().unwrap(); + } +} diff --git a/src-tauri/src/window_manager/windows.rs b/src-tauri/src/window_manager/windows.rs new file mode 100644 index 00000000..6bac8e67 --- /dev/null +++ b/src-tauri/src/window_manager/windows.rs @@ -0,0 +1,306 @@ +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; + +use windows::Win32::Foundation::{LPARAM, RECT}; +use windows::Win32::Graphics::Gdi::{ + EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR, MONITORINFOEXW, +}; +use windows::Win32::UI::HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI}; + +use crate::window_manager::{ + WindowManager, NEW_UI_HEIGHT, NEW_UI_WIDTH, NEW_UI_WINDOW_ID, OLD_UI_HEIGHT, OLD_UI_WIDTH, + OLD_UI_WINDOW_ID, WINDOW_GAP, +}; + +#[derive(Debug, Clone, PartialEq)] +pub enum TaskbarPosition { + Bottom, + Top, + Left, + Right, + HiddenOrNone, +} + +#[derive(Debug, Clone)] +pub struct MonitorInfo { + pub name: String, + pub is_primary: bool, + pub physical_x: i32, + pub physical_y: i32, + pub physical_width: u32, + pub physical_height: u32, + pub scale_factor: f64, + pub taskbar_position: TaskbarPosition, + pub taskbar_size: u32, +} + +impl WindowManager { + pub fn get_monitors() -> Vec { + let mut monitors: Vec = Vec::new(); + + unsafe extern "system" fn monitor_enum_proc( + hmonitor: HMONITOR, + _hdc: HDC, + _rect: *mut RECT, + lparam: LPARAM, + ) -> windows::core::BOOL { + let monitors = &mut *(lparam.0 as *mut Vec); + + let mut info = MONITORINFOEXW::default(); + info.monitorInfo.cbSize = std::mem::size_of::() as u32; + + if GetMonitorInfoW(hmonitor, &mut info as *mut _ as *mut _).as_bool() { + // Name + let name_len = info + .szDevice + .iter() + .position(|&c| c == 0) + .unwrap_or(info.szDevice.len()); + let name = OsString::from_wide(&info.szDevice[..name_len]) + .to_string_lossy() + .into_owned(); + + let is_primary = (info.monitorInfo.dwFlags & 1) != 0; + + // DPI and Scaling + let mut dpi_x = 0; + let mut dpi_y = 0; + let scale_factor = if GetDpiForMonitor( + hmonitor, + MDT_EFFECTIVE_DPI, + &mut dpi_x, + &mut dpi_y, + ) + .is_ok() + { + dpi_x as f64 / 96.0 + } else { + 1.0 + }; + + let physical_x = info.monitorInfo.rcMonitor.left; + let physical_y = info.monitorInfo.rcMonitor.top; + let physical_width = (info.monitorInfo.rcMonitor.right + - info.monitorInfo.rcMonitor.left) + .unsigned_abs(); + let physical_height = (info.monitorInfo.rcMonitor.bottom + - info.monitorInfo.rcMonitor.top) + .unsigned_abs(); + + // Taskbar position and size + let mut taskbar_position = TaskbarPosition::HiddenOrNone; + let mut taskbar_size = 0; + + let mon = info.monitorInfo.rcMonitor; + let work = info.monitorInfo.rcWork; + + if work.bottom < mon.bottom { + taskbar_position = TaskbarPosition::Bottom; + taskbar_size = (mon.bottom - work.bottom).unsigned_abs(); + } else if work.top > mon.top { + taskbar_position = TaskbarPosition::Top; + taskbar_size = (work.top - mon.top).unsigned_abs(); + } else if work.left > mon.left { + taskbar_position = TaskbarPosition::Left; + taskbar_size = (work.left - mon.left).unsigned_abs(); + } else if work.right < mon.right { + taskbar_position = TaskbarPosition::Right; + taskbar_size = (mon.right - work.right).unsigned_abs(); + } + + monitors.push(MonitorInfo { + name, + is_primary, + physical_x, + physical_y, + physical_width, + physical_height, + scale_factor, + taskbar_position, + taskbar_size, + }); + } + + true.into() + } + + unsafe { + let _ = EnumDisplayMonitors( + None, + None, + Some(monitor_enum_proc), + LPARAM(&mut monitors as *mut _ as isize), + ); + } + + monitors + } + + pub fn open_tray( + app: &tauri::AppHandle, + icon_x: i32, + icon_y: i32, + icon_width: u32, + icon_height: u32, + ) -> tauri::Result { + let monitors = Self::get_monitors(); + let primary = monitors + .iter() + .find(|m| m.is_primary) + .unwrap_or(&monitors[0]); + + let window = if let Some(window) = tauri::Manager::get_webview_window(app, NEW_UI_WINDOW_ID) + { + let _ = window.unminimize(); + window + } else { + Self::build_tray_window(app)? + }; + + let logical_width = NEW_UI_WIDTH; + let logical_height = NEW_UI_HEIGHT; + + let physical_width = (logical_width * primary.scale_factor) as i32; + let physical_height = (logical_height * primary.scale_factor) as i32; + + let physical_gap = (WINDOW_GAP * primary.scale_factor) as i32; + + let work_left = primary.physical_x + + if primary.taskbar_position == TaskbarPosition::Left { + primary.taskbar_size as i32 + } else { + 0 + }; + let work_top = primary.physical_y + + if primary.taskbar_position == TaskbarPosition::Top { + primary.taskbar_size as i32 + } else { + 0 + }; + let work_right = primary.physical_x + primary.physical_width as i32 + - if primary.taskbar_position == TaskbarPosition::Right { + primary.taskbar_size as i32 + } else { + 0 + }; + let work_bottom = primary.physical_y + primary.physical_height as i32 + - if primary.taskbar_position == TaskbarPosition::Bottom { + primary.taskbar_size as i32 + } else { + 0 + }; + + let icon_center_x = icon_x + (icon_width as i32 / 2); + let default_x = icon_center_x - (physical_width / 2); + let max_x = work_right - physical_gap - physical_width; + let min_x = work_left + physical_gap; + let clamped_x = default_x.clamp(min_x, max_x); + + let icon_center_y = icon_y + (icon_height as i32 / 2); + let default_y = icon_center_y - (physical_height / 2); + let max_y = work_bottom - physical_gap - physical_height; + let min_y = work_top + physical_gap; + let clamped_y = default_y.clamp(min_y, max_y); + + let (final_x, final_y) = match primary.taskbar_position { + TaskbarPosition::Bottom => (clamped_x, work_bottom - physical_height - physical_gap), + TaskbarPosition::Top => (clamped_x, work_top + physical_gap), + TaskbarPosition::Left => (work_left + physical_gap, clamped_y), + TaskbarPosition::Right => (work_right - physical_width - physical_gap, clamped_y), + _ => (clamped_x, work_bottom - physical_height - physical_gap), + }; + + window.set_always_on_top(true)?; + window.set_position(tauri::PhysicalPosition::new(final_x, final_y))?; + window.show()?; + + let window_focus = window.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(50)); + // Toggle always_on_top to force Z-order above the Windows tray overflow popup + let _ = window_focus.set_always_on_top(false); + let _ = window_focus.set_always_on_top(true); + let _ = window_focus.set_focus(); + }); + + Ok(window) + } + + pub fn open_full_view(app: &tauri::AppHandle) -> tauri::Result { + let monitors = Self::get_monitors(); + let primary = monitors + .iter() + .find(|m| m.is_primary) + .unwrap_or(&monitors[0]); + + let window = if let Some(window) = tauri::Manager::get_webview_window(app, OLD_UI_WINDOW_ID) + { + let _ = window.unminimize(); + window + } else { + Self::build_full_window(app)? + }; + + let outer_size = window.outer_size().unwrap_or(tauri::PhysicalSize { + width: (OLD_UI_WIDTH * primary.scale_factor) as u32, + height: (OLD_UI_HEIGHT * primary.scale_factor) as u32, + }); + let inner_size = window.inner_size().unwrap_or(tauri::PhysicalSize { + width: (OLD_UI_WIDTH * primary.scale_factor) as u32, + height: (OLD_UI_HEIGHT * primary.scale_factor) as u32, + }); + + let physical_width = outer_size.width as i32; + let physical_height = outer_size.height as i32; + + // Windows invisible borders (shadows) are included in outer_size for decorated windows. + let border_thickness = (physical_width - (inner_size.width as i32)) / 2; + let visible_height = physical_height - border_thickness; + + let physical_gap = (WINDOW_GAP * primary.scale_factor) as i32; + + let center_x = primary.physical_x + (primary.physical_width as i32 / 2); + let center_y = primary.physical_y + (primary.physical_height as i32 / 2); + + let mut window_x = center_x - (physical_width / 2); + let mut window_y = center_y - (visible_height / 2); + + let taskbar_size = primary.taskbar_size as i32; + + match primary.taskbar_position { + TaskbarPosition::Bottom => { + let max_y = primary.physical_y + primary.physical_height as i32 + - taskbar_size + - physical_gap; + if window_y + visible_height > max_y { + window_y = max_y - visible_height; + } + } + TaskbarPosition::Top => { + let min_y = primary.physical_y + taskbar_size + physical_gap; + if window_y < min_y { + window_y = min_y; + } + } + TaskbarPosition::Left => { + let min_x = primary.physical_x + taskbar_size + physical_gap; + if window_x + border_thickness < min_x { + window_x = min_x - border_thickness; + } + } + TaskbarPosition::Right => { + let max_x = primary.physical_x + primary.physical_width as i32 + - taskbar_size + - physical_gap; + if window_x + physical_width - border_thickness > max_x { + window_x = max_x - physical_width + border_thickness; + } + } + _ => {} + } + + window.set_position(tauri::PhysicalPosition::new(window_x, window_y))?; + window.show()?; + Ok(window) + } +} From a6ada6426f21a6a1858eca67fd2b3da619043145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Wed, 20 May 2026 15:06:04 +0200 Subject: [PATCH 2/6] ui fixes and fix swapping windows --- .../pages/compact/CompactPage/CompactPage.tsx | 19 ++++ .../src/pages/compact/CompactPage/style.scss | 9 +- .../src/shared/components/Button/style.scss | 4 +- .../components/LocationCard/LocationCard.tsx | 8 +- .../components/MfaSelector/MfaSelector.tsx | 4 +- .../components/MfaSelector/style.scss | 16 +-- .../LocationCard/context/context.tsx | 13 ++- .../shared/components/LocationCard/style.scss | 15 ++- .../views/DefaultView/DefaultView.tsx | 6 +- .../LocationCard/views/DefaultView/style.scss | 13 ++- .../LocationCardMfaSettings.tsx | 42 ++++---- .../src/shared/components/Toggle/style.scss | 1 + .../components/WindowHeader/WindowHeader.tsx | 2 +- .../ConnectionWatcher/ConnectionsWatcher.tsx | 10 +- .../components/ConnectionWatcher/style.scss | 7 +- new-ui/src/shared/rust-api/api.ts | 3 + new-ui/src/shared/rust-api/types.ts | 1 + src-tauri/permissions/default.toml | 1 + src-tauri/src/bin/defguard-client.rs | 32 ++++-- src-tauri/src/tray.rs | 6 +- src-tauri/src/window_manager/macos.rs | 60 ++++++----- src-tauri/src/window_manager/mod.rs | 100 ++++++++++++++---- src-tauri/src/window_manager/windows.rs | 84 ++++++++++----- 23 files changed, 326 insertions(+), 130 deletions(-) diff --git a/new-ui/src/pages/compact/CompactPage/CompactPage.tsx b/new-ui/src/pages/compact/CompactPage/CompactPage.tsx index 23d47e31..53ed3731 100644 --- a/new-ui/src/pages/compact/CompactPage/CompactPage.tsx +++ b/new-ui/src/pages/compact/CompactPage/CompactPage.tsx @@ -1,14 +1,33 @@ import clsx from 'clsx'; import './style.scss'; +import { useMutation } from '@tanstack/react-query'; import type { JSX, PropsWithChildren } from 'react'; +import { IconButton } from '../../../shared/components/IconButton/IconButton'; +import { IconButtonVariant } from '../../../shared/components/IconButton/types'; +import { api } from '../../../shared/rust-api/api'; interface Props extends PropsWithChildren { containerProps?: JSX.IntrinsicElements['main']; } export const CompactPage = ({ children, containerProps }: Props) => { + const { mutate: closeWindow, isPending } = useMutation({ + mutationFn: api.closeTrayWindow, + }); + return (
+
+ { + if (!isPending) { + closeWindow(); + } + }} + /> +
{children}
); diff --git a/new-ui/src/pages/compact/CompactPage/style.scss b/new-ui/src/pages/compact/CompactPage/style.scss index 0e6146f5..b2c35275 100644 --- a/new-ui/src/pages/compact/CompactPage/style.scss +++ b/new-ui/src/pages/compact/CompactPage/style.scss @@ -1,4 +1,11 @@ .compact-page { box-sizing: border-box; - padding: 8px; + padding: var(--spacing-md); + position: relative; + + > .close-window { + position: absolute; + right: var(--spacing-md); + top: var(--spacing-md); + } } diff --git a/new-ui/src/shared/components/Button/style.scss b/new-ui/src/shared/components/Button/style.scss index 46828297..5cc175e4 100644 --- a/new-ui/src/shared/components/Button/style.scss +++ b/new-ui/src/shared/components/Button/style.scss @@ -24,6 +24,7 @@ display: inline-block; min-width: 0; user-select: none; + background-clip: padding-box; > .btn-content { display: inline-grid; @@ -60,6 +61,7 @@ .text { font: var(--btn-font); color: inherit; + user-select: none; @include animate(color); } @@ -84,7 +86,7 @@ &.size-primary { --btn-font: var(--t-button-label-primary); - --btn-size: 40px; + --btn-size: 36px; border-radius: 8px; padding: var(--spacing-sm) var(--spacing-lg); diff --git a/new-ui/src/shared/components/LocationCard/LocationCard.tsx b/new-ui/src/shared/components/LocationCard/LocationCard.tsx index 72b6cbc9..7216e34f 100644 --- a/new-ui/src/shared/components/LocationCard/LocationCard.tsx +++ b/new-ui/src/shared/components/LocationCard/LocationCard.tsx @@ -57,7 +57,12 @@ const LocationCardInner = ({ isOpen, onOpen, disableOpen }: InnerProps) => { data-network={location.network_id} data-id={location.id} > -
+
@@ -82,7 +87,6 @@ const LocationCardInner = ({ isOpen, onOpen, disableOpen }: InnerProps) => { icon={IconKind.ArrowSmall} variant={isOpen ? IconButtonVariant.SmallSelected : IconButtonVariant.Small} iconRotation={isOpen ? Direction.DOWN : Direction.RIGHT} - onClick={onOpen} /> )}
diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx b/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx index 7f1f65ce..490e2eb0 100644 --- a/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx @@ -25,11 +25,11 @@ export const MfaSelector = ({ case 'email': return 'mail'; case 'mobileapprove': - return 'mobile-lock'; + return 'mobile'; case 'oidc': return 'token'; case 'totp': - return 'mobile-lock'; + return 'lock-closed'; case 'biometric': return 'biometric'; } diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss b/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss index 3d33c107..2c2e9b1f 100644 --- a/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss @@ -3,6 +3,7 @@ --border: var(--border-default); --icon: var(--fg-white-80); --color: var(--fg-white-80); + --box-shadow: box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0); display: grid; grid-template-columns: 20px minmax(0, 1fr) 16px; @@ -13,26 +14,29 @@ user-select: none; align-items: center; box-sizing: border-box; + box-shadow: var(--box-shadow); padding: 0 var(--spacing-md); min-height: 40px; border-radius: 8px; cursor: pointer; transition-duration: 250ms; transition-timing-function: cubic-bezier(0.1, 0.9, 0.2, 1); - transition-property: border-color, background, color; + transition-property: border-color, background, color, box-shadow; + background-clip: padding-box; &:hover { --bg: var(--bg-white-5); --color: var(--fg-white-100); - --border: var(--border-default); + --border: var(--border-action-disabled); --icon: var(--fg-white-100); } &.selected { - --bg: var(--bg-white-5); + --bg: var(--bg-white-10); --color: var(--fg-white-100); - --border: transparent; + --border: var(--border-action-disabled); --icon: var(--fg-white-100); + --box-shadow: box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.05); } > .middle { @@ -40,7 +44,7 @@ flex-flow: row nowrap; align-items: center; justify-content: flex-start; - column-gap: var(--spacing-xs); + column-gap: var(--spacing-md); } .default-badge { @@ -71,6 +75,6 @@ .name { color: inherit; - font: var(--t-body-xs); + font: var(--t-body-sm-400); } } diff --git a/new-ui/src/shared/components/LocationCard/context/context.tsx b/new-ui/src/shared/components/LocationCard/context/context.tsx index b2e7e8b4..00005f97 100644 --- a/new-ui/src/shared/components/LocationCard/context/context.tsx +++ b/new-ui/src/shared/components/LocationCard/context/context.tsx @@ -1,5 +1,5 @@ import { createContext, type ReactNode, useCallback, useContext, useState } from 'react'; -import type { InstanceInfo, LocationInfo } from '../../../rust-api/types'; +import type { InstanceInfo, LocationInfo, MfaMethodValue } from '../../../rust-api/types'; import { MfaMethod } from '../../../rust-api/types'; import { LocationCardViews, type LocationCardViewsValue } from './types'; @@ -10,6 +10,8 @@ interface LocationCardContextValue { previousView: LocationCardViewsValue | null; setView: (view: LocationCardViewsValue) => void; startMfa: () => void; + localMfaMethod: MfaMethodValue; + setLocalMfaMethod: (method: MfaMethodValue) => void; } const LocationCardContext = createContext(null); @@ -37,6 +39,9 @@ export const LocationCardProvider = ({ const [currentView, setCurrentView] = useState( location.active ? LocationCardViews.Connected : LocationCardViews.Default, ); + const [localMfaMethod, setLocalMfaMethod] = useState( + location.mfa_method ?? MfaMethod.Totp, + ); const setView = useCallback( (view: LocationCardViewsValue) => { @@ -47,7 +52,7 @@ export const LocationCardProvider = ({ ); const startMfa = useCallback(() => { - switch (location.mfa_method) { + switch (localMfaMethod) { case MfaMethod.Totp: setView(LocationCardViews.MfaTotp); break; @@ -61,7 +66,7 @@ export const LocationCardProvider = ({ setView(LocationCardViews.MfaMobile); break; } - }, [location.mfa_method, setView]); + }, [localMfaMethod, setView]); return ( {children} diff --git a/new-ui/src/shared/components/LocationCard/style.scss b/new-ui/src/shared/components/LocationCard/style.scss index 9221d88a..c8a68b12 100644 --- a/new-ui/src/shared/components/LocationCard/style.scss +++ b/new-ui/src/shared/components/LocationCard/style.scss @@ -1,7 +1,7 @@ .location-card { - border-radius: 12px; + border-radius: 16px; box-sizing: border-box; - padding: var(--spacing-md) var(--spacing-lg); + padding: var(--spacing-lg); background-color: var(--bg-dark-blue-40); > .top-track { @@ -11,6 +11,17 @@ justify-content: flex-start; user-select: none; + &.interactive { + cursor: pointer; + + &:hover { + > .right > .icon-button { + --bg: var(--c-white-20); + --icon: var(--c-white-100); + } + } + } + > .left { display: flex; flex-flow: row; diff --git a/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx b/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx index acfb49a9..0b5ce633 100644 --- a/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/DefaultView/DefaultView.tsx @@ -38,7 +38,11 @@ export const DefaultView = () => { { updateRouting({ connectionType: location.connection_type, diff --git a/new-ui/src/shared/components/LocationCard/views/DefaultView/style.scss b/new-ui/src/shared/components/LocationCard/views/DefaultView/style.scss index b32113ff..658c869b 100644 --- a/new-ui/src/shared/components/LocationCard/views/DefaultView/style.scss +++ b/new-ui/src/shared/components/LocationCard/views/DefaultView/style.scss @@ -5,9 +5,10 @@ grid-template-rows: 1fr; align-items: center; column-gap: var(--spacing-md); + user-select: none; > .name { - font: var(--t-body-xs-500); + font: var(--t-body-sm-400); color: var(--fg-white-100); } @@ -17,10 +18,12 @@ display: inline-flex; flex-flow: row nowrap; align-items: center; + justify-content: center; padding: 0 4px; - height: 18px; - width: 32px; - background-color: var(--bg-white-100); + min-height: 20px; + width: 36px; + background-color: transparent; + border: 1px solid var(--bg-white-60); p { font: var(--font-family-body); @@ -28,7 +31,7 @@ font-weight: 500; line-height: 16px; letter-spacing: 0.11px; - color: var(--fg-action); + color: var(--fg-white-60); } } } diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/LocationCardMfaSettings.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/LocationCardMfaSettings.tsx index c8314ae6..04b066f6 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/LocationCardMfaSettings.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaSettings/LocationCardMfaSettings.tsx @@ -30,14 +30,18 @@ export const LocationCardMfaSettings = () => { }, }); - const { previousView, setView, location } = useLocationCardContext(); + const { previousView, setView, location, localMfaMethod, setLocalMfaMethod } = + useLocationCardContext(); - const mfaMethod = location.mfa_method ?? MfaMethod.Totp; + const locationDefaultMfaMethod = location.mfa_method ?? MfaMethod.Totp; - const [selectedPref, setSelectedPref] = useState( - mfaMethod ?? MfaMethod.Totp, + const [selectedMethod, setSelectedPref] = useState( + localMfaMethod ?? MfaMethod.Totp, ); + const isFromDefault = previousView === LocationCardViews.Default; + const [setAsDefault, setSetAsDefault] = useState(true); + const MfaFactorsList = useMemo((): MfaMethodValue[] => { if (location.location_mfa_mode === LocationMfaMode.Internal) { return [MfaMethod.Totp, MfaMethod.Email, MfaMethod.MobileApprove]; @@ -46,13 +50,14 @@ export const LocationCardMfaSettings = () => { }, [location.location_mfa_mode]); const handleSubmit = () => { - if (selectedPref !== mfaMethod) { + setLocalMfaMethod(selectedMethod); + if ((isFromDefault || setAsDefault) && selectedMethod !== locationDefaultMfaMethod) { setMfaMethod({ locationId: location.id, - mfaMethod: selectedPref, + mfaMethod: selectedMethod, }); - setView(previousView ?? LocationCardViews.Default); } + setView(previousView ?? LocationCardViews.Default); }; return ( @@ -70,21 +75,19 @@ export const LocationCardMfaSettings = () => { setSelectedPref(factor)} /> ))}
- + {!isFromDefault && ( + setSetAsDefault((prev) => !prev)} + text="Set as default MFA method" + /> + )} { />
diff --git a/new-ui/src/shared/components/Toggle/style.scss b/new-ui/src/shared/components/Toggle/style.scss index f8eb8a32..971629e5 100644 --- a/new-ui/src/shared/components/Toggle/style.scss +++ b/new-ui/src/shared/components/Toggle/style.scss @@ -26,6 +26,7 @@ border: var(--border-1) solid var(--border); min-width: 36px; flex-shrink: 0; + background-clip: padding-box; @include animate(border-color, background-color); diff --git a/new-ui/src/shared/components/WindowHeader/WindowHeader.tsx b/new-ui/src/shared/components/WindowHeader/WindowHeader.tsx index ecac32c8..f78c6ae0 100644 --- a/new-ui/src/shared/components/WindowHeader/WindowHeader.tsx +++ b/new-ui/src/shared/components/WindowHeader/WindowHeader.tsx @@ -11,7 +11,7 @@ export const WindowHeader = ({ variant }: Props) => {
-

Defguard VPN Client

+

Defguard VPN Client

diff --git a/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/ConnectionsWatcher.tsx b/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/ConnectionsWatcher.tsx index 5f3308bc..6e2afcf2 100644 --- a/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/ConnectionsWatcher.tsx +++ b/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/ConnectionsWatcher.tsx @@ -4,10 +4,11 @@ import { FloatingPortal, size as floatingSize, offset, + safePolygon, shift, - useClick, useDismiss, useFloating, + useHover, useInteractions, } from '@floating-ui/react'; import { useMutation, useQuery } from '@tanstack/react-query'; @@ -57,9 +58,8 @@ export const ConnectionWatcher = () => { whileElementsMounted: autoUpdate, }); - const click = useClick(context, { - toggle: true, - enabled: connected, + const hover = useHover(context, { + handleClose: safePolygon(), }); const dismiss = useDismiss(context, { @@ -67,7 +67,7 @@ export const ConnectionWatcher = () => { outsidePress: true, }); - const { getFloatingProps, getReferenceProps } = useInteractions([click, dismiss]); + const { getFloatingProps, getReferenceProps } = useInteractions([hover, dismiss]); return ( <> diff --git a/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/style.scss b/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/style.scss index e40bfe4c..3f2842d0 100644 --- a/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/style.scss +++ b/new-ui/src/shared/components/WindowHeader/components/ConnectionWatcher/style.scss @@ -26,7 +26,6 @@ display: flex; flex-flow: row nowrap; align-items: center; - cursor: pointer; padding: 0 var(--spacing-xs); background-color: var(--bg-success); @@ -55,6 +54,8 @@ font: var(--t-menu-title); color: var(--fg-white-60); min-height: 24px; + line-height: 24px; + font-weight: 400; } .connection { @@ -70,6 +71,10 @@ svg circle { fill: var(--bg-success); } + + p { + font: var(--t-menu-text); + } } .disconnect { diff --git a/new-ui/src/shared/rust-api/api.ts b/new-ui/src/shared/rust-api/api.ts index 4628ab90..435966e9 100644 --- a/new-ui/src/shared/rust-api/api.ts +++ b/new-ui/src/shared/rust-api/api.ts @@ -128,6 +128,8 @@ const getEdgeRequestHeaders = async (): Promise => { const swapToOldUi = async () => invoke(TauriCommand.SwapToOldUi); +const closeTrayWindow = async () => invoke(TauriCommand.CloseTrayWindow); + export const api = { getEdgeRequestHeaders, // Instances @@ -168,4 +170,5 @@ export const api = { disconnectLocations, // Window swapToOldUi, + closeTrayWindow, }; diff --git a/new-ui/src/shared/rust-api/types.ts b/new-ui/src/shared/rust-api/types.ts index f82a6716..708d1b51 100644 --- a/new-ui/src/shared/rust-api/types.ts +++ b/new-ui/src/shared/rust-api/types.ts @@ -104,6 +104,7 @@ export const TauriCommand = { DisconnectLocations: 'disconnect_locations', //Window SwapToOldUi: 'swap_to_old_ui', + CloseTrayWindow: 'close_tray_window', } as const; export type TauriCommand = (typeof TauriCommand)[keyof typeof TauriCommand]; diff --git a/src-tauri/permissions/default.toml b/src-tauri/permissions/default.toml index 303597ed..610c903d 100644 --- a/src-tauri/permissions/default.toml +++ b/src-tauri/permissions/default.toml @@ -34,6 +34,7 @@ commands.allow = [ "open_old_ui_window", "swap_to_new_ui", "swap_to_old_ui", + "close_tray_window", "all_active_connections", "disconnect_locations", "get_posture_data", diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index df9173a5..34a85400 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -188,18 +188,22 @@ fn main() { open_old_ui_window, swap_to_new_ui, swap_to_old_ui, + close_tray_window, all_active_connections, disconnect_locations, ]) .on_window_event(|window, event| { if let WindowEvent::CloseRequested { api, .. } = event { - #[cfg(not(target_os = "macos"))] - let _ = window.hide(); + // Only prevent close on the tray (new-ui) window; let other windows close normally. + if window.label() == NEW_UI_WINDOW_ID { + #[cfg(not(target_os = "macos"))] + let _ = window.hide(); - #[cfg(target_os = "macos")] - let _ = tauri::AppHandle::hide(window.app_handle()); + #[cfg(target_os = "macos")] + let _ = tauri::AppHandle::hide(window.app_handle()); - api.prevent_close(); + api.prevent_close(); + } } }) // Initialize plugins here, except for `tauri_plugin_log` which is handled in `setup()`. @@ -214,7 +218,6 @@ fn main() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_notification::init()) - .plugin(tauri_plugin_window_state::Builder::new().build()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_process::init()) @@ -348,7 +351,22 @@ fn main() { let state = AppState::new(config, provisioning_config); app.manage(state); - WindowManager::open_full_view(app_handle)?; + #[cfg(target_os = "linux")] + { + let _ = WindowManager::open_full_view(app_handle); + } + #[cfg(not(target_os = "linux"))] + { + let has_locations = tauri::async_runtime::block_on( + defguard_client::window_manager::has_non_service_locations() + ); + if has_locations { + WindowManager::open_tray(app_handle)?; + } else { + info!("No locations found, spawning full view on startup."); + let _ = WindowManager::open_full_view(app_handle); + } + } info!("App setup completed, log level: {log_level}"); Ok(()) diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 2cebe81b..4339c6bd 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -148,8 +148,9 @@ pub async fn setup_tray(app: &AppHandle) -> Result<(), Error> { .show_menu_on_left_click(true) .on_tray_icon_event(|icon, event| { store_tray_click_position(icon.app_handle(), &event); - if let TrayIconEvent::DoubleClick { + if let TrayIconEvent::Click { button: MouseButton::Left, + button_state: MouseButtonState::Up, .. } = event { @@ -166,8 +167,9 @@ pub async fn setup_tray(app: &AppHandle) -> Result<(), Error> { .show_menu_on_left_click(false) .on_tray_icon_event(|icon, event| { store_tray_click_position(icon.app_handle(), &event); - if let TrayIconEvent::DoubleClick { + if let TrayIconEvent::Click { button: MouseButton::Left, + button_state: MouseButtonState::Up, .. } = event { diff --git a/src-tauri/src/window_manager/macos.rs b/src-tauri/src/window_manager/macos.rs index bcef5ec9..770e9c92 100644 --- a/src-tauri/src/window_manager/macos.rs +++ b/src-tauri/src/window_manager/macos.rs @@ -1,7 +1,7 @@ use tauri::{AppHandle, LogicalPosition, Manager, Monitor, PhysicalSize, Position, WebviewWindow}; use crate::appstate::AppState; -use crate::window_manager::{WindowManager, NEW_UI_WINDOW_ID, OLD_UI_WINDOW_ID, TASKBAR_HEIGHT}; +use crate::window_manager::{WindowManager, NEW_UI_WINDOW_ID, OLD_UI_WINDOW_ID}; /// Try to get monitor at the given position, with a fall back to primary monitor, and then to the /// first one on the list of available monitors. @@ -28,29 +28,43 @@ fn get_tray_window_position( size: PhysicalSize, ) -> Option> { let app_state = app.state::(); - let tray_position = app_state.tray_click_position.lock().unwrap().to_owned()?; - let monitor = get_monitor_for_position(app, tray_position.x, tray_position.y)?; + if let Some(tray_position) = app_state.tray_click_position.lock().unwrap().to_owned() { + let monitor = get_monitor_for_position(app, tray_position.x, tray_position.y)?; - let scale_factor = monitor.scale_factor(); - let monitor_position = monitor.position().to_logical::(scale_factor); - let monitor_size = monitor.size().to_logical::(scale_factor); - let tray_position = tray_position.to_logical::(scale_factor); - let window_size = size.to_logical::(scale_factor); + let scale_factor = monitor.scale_factor(); + let monitor_position = monitor.position().to_logical::(scale_factor); + let monitor_size = monitor.size().to_logical::(scale_factor); + let tray_position = tray_position.to_logical::(scale_factor); + let window_size = size.to_logical::(scale_factor); - let mut x = tray_position.x; - let mut y = tray_position.y; + let mut x = tray_position.x; + let mut y = tray_position.y; - x = x.clamp( - monitor_position.x, - monitor_position.x + monitor_size.width - window_size.width, - ); - y = y.clamp( - monitor_position.y, - monitor_position.y + monitor_size.height - window_size.height - TASKBAR_HEIGHT, - ); + x = x.clamp( + monitor_position.x, + monitor_position.x + monitor_size.width - window_size.width, + ); + y = y.clamp( + monitor_position.y, + monitor_position.y + monitor_size.height - window_size.height, + ); - Some(LogicalPosition::new(x, y)) + Some(LogicalPosition::new(x, y)) + } else { + let monitor = app.primary_monitor().ok().flatten()?; + let scale_factor = monitor.scale_factor(); + let monitor_position = monitor.position().to_logical::(scale_factor); + let monitor_size = monitor.size().to_logical::(scale_factor); + let window_size = size.to_logical::(scale_factor); + + let gap = crate::window_manager::WINDOW_GAP; + + let x = monitor_position.x + monitor_size.width - window_size.width - gap; + let y = monitor_position.y + gap; + + Some(LogicalPosition::new(x, y)) + } } fn position_window_near_tray(app: &AppHandle, window: &WebviewWindow) { @@ -63,13 +77,7 @@ fn position_window_near_tray(app: &AppHandle, window: &WebviewWindow) { } impl WindowManager { - pub fn open_tray( - app: &AppHandle, - _icon_x: i32, - _icon_y: i32, - _icon_width: u32, - _icon_height: u32, - ) -> tauri::Result { + pub fn open_tray(app: &AppHandle) -> tauri::Result { let window = if let Some(window) = app.get_webview_window(NEW_UI_WINDOW_ID) { let _ = window.unminimize(); window diff --git a/src-tauri/src/window_manager/mod.rs b/src-tauri/src/window_manager/mod.rs index 6544d814..08f9e0ae 100644 --- a/src-tauri/src/window_manager/mod.rs +++ b/src-tauri/src/window_manager/mod.rs @@ -1,5 +1,15 @@ use tauri::{AppHandle, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; +use crate::database::{models::location::Location, DB_POOL}; + +/// Returns `true` if there are any non-service locations in the database. +pub async fn has_non_service_locations() -> bool { + match Location::all(&*DB_POOL, false).await { + Ok(locations) => !locations.is_empty(), + Err(_) => false, + } +} + pub const NEW_UI_WINDOW_ID: &str = "new-ui"; pub const OLD_UI_WINDOW_ID: &str = "old-ui"; pub const NEW_UI_WIDTH: f64 = 380.0; @@ -8,14 +18,9 @@ pub const OLD_UI_WIDTH: f64 = 1280.0; pub const OLD_UI_HEIGHT: f64 = 920.0; pub const WINDOW_GAP: f64 = 20.0; -#[cfg(windows)] -pub const TASKBAR_HEIGHT: f64 = 48.0; -#[cfg(not(windows))] -pub const TASKBAR_HEIGHT: f64 = 0.0; - #[must_use] pub fn new_ui_url() -> WebviewUrl { - if cfg!(defguard_client_dev) { + if cfg!(any(defguard_client_dev, debug_assertions)) { WebviewUrl::External("http://localhost:5072".parse().unwrap()) } else { WebviewUrl::App("new-ui/".into()) @@ -24,7 +29,7 @@ pub fn new_ui_url() -> WebviewUrl { #[must_use] pub fn old_ui_url() -> WebviewUrl { - if cfg!(defguard_client_dev) { + if cfg!(any(defguard_client_dev, debug_assertions)) { WebviewUrl::External("http://localhost:5071".parse().unwrap()) } else { WebviewUrl::App("old-ui/index.html".into()) @@ -36,7 +41,7 @@ pub struct WindowManager; impl WindowManager { pub fn build_tray_window(app: &AppHandle) -> tauri::Result { WebviewWindowBuilder::new(app, NEW_UI_WINDOW_ID, new_ui_url()) - .title("New UI") + .title("Defguard") .inner_size(NEW_UI_WIDTH, NEW_UI_HEIGHT) .resizable(false) .decorations(false) @@ -48,7 +53,7 @@ impl WindowManager { pub fn build_full_window(app: &AppHandle) -> tauri::Result { WebviewWindowBuilder::new(app, OLD_UI_WINDOW_ID, old_ui_url()) - .title("Old UI") + .title("Defguard") .inner_size(OLD_UI_WIDTH, OLD_UI_HEIGHT) .decorations(true) .build() @@ -63,11 +68,11 @@ pub mod macos; // Export tauri commands so they can be registered in main.rs pub(crate) fn show_new_ui_window(app: &AppHandle) { - let _ = WindowManager::open_tray(app, 0, 0, 0, 0); + let _ = WindowManager::open_tray(app); } pub(crate) fn show_new_ui_window_near_tray(app: &AppHandle) { - let _ = WindowManager::open_tray(app, 0, 0, 0, 0); + show_new_ui_window(app); } #[tauri::command] @@ -82,16 +87,73 @@ pub fn open_old_ui_window(app: AppHandle) { #[tauri::command] pub fn swap_to_old_ui(app: AppHandle) { - let _ = WindowManager::open_full_view(&app); - if let Some(w) = tauri::Manager::get_webview_window(&app, NEW_UI_WINDOW_ID) { - w.close().unwrap(); - } + tracing::info!("swap_to_old_ui called"); + tauri::async_runtime::spawn(async move { + // Sleep briefly to let the IPC handler return + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + tracing::info!("swap_to_old_ui task: Opening full view"); + match WindowManager::open_full_view(&app) { + Ok(_) => { + tracing::info!( + "swap_to_old_ui task: open_full_view succeeded, sleeping before destroy" + ); + tokio::time::sleep(std::time::Duration::from_millis(150)).await; + if let Some(w) = tauri::Manager::get_webview_window(&app, NEW_UI_WINDOW_ID) { + tracing::info!("swap_to_old_ui task: Destroying new-ui window"); + if let Err(e) = w.destroy() { + tracing::error!( + "swap_to_old_ui task: Failed to destroy new-ui window: {:?}", + e + ); + } + } else { + tracing::warn!("swap_to_old_ui task: new-ui window not found to destroy"); + } + } + Err(e) => { + tracing::error!("swap_to_old_ui task: open_full_view failed: {:?}", e); + } + } + }); +} + +#[tauri::command] +pub fn close_tray_window(app: AppHandle) { + tracing::info!("close_tray_window called"); + tauri::async_runtime::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + if let Some(w) = tauri::Manager::get_webview_window(&app, NEW_UI_WINDOW_ID) { + tracing::info!("close_tray_window task: Destroying new-ui window"); + if let Err(e) = w.destroy() { + tracing::error!( + "close_tray_window task: Failed to destroy new-ui window: {:?}", + e + ); + } + } else { + tracing::warn!("close_tray_window task: new-ui window not found to destroy"); + } + }); } #[tauri::command] pub fn swap_to_new_ui(app: AppHandle) { - show_new_ui_window(&app); - if let Some(w) = tauri::Manager::get_webview_window(&app, OLD_UI_WINDOW_ID) { - w.close().unwrap(); - } + tracing::info!("swap_to_new_ui called"); + tauri::async_runtime::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + tracing::info!("swap_to_new_ui task: Showing new UI window"); + show_new_ui_window(&app); + tokio::time::sleep(std::time::Duration::from_millis(150)).await; + if let Some(w) = tauri::Manager::get_webview_window(&app, OLD_UI_WINDOW_ID) { + tracing::info!("swap_to_new_ui task: Destroying old-ui window"); + if let Err(e) = w.destroy() { + tracing::error!( + "swap_to_new_ui task: Failed to destroy old-ui window: {:?}", + e + ); + } + } else { + tracing::warn!("swap_to_new_ui task: old-ui window not found to destroy"); + } + }); } diff --git a/src-tauri/src/window_manager/windows.rs b/src-tauri/src/window_manager/windows.rs index 6bac8e67..670f5eed 100644 --- a/src-tauri/src/window_manager/windows.rs +++ b/src-tauri/src/window_manager/windows.rs @@ -136,13 +136,9 @@ impl WindowManager { monitors } - pub fn open_tray( - app: &tauri::AppHandle, - icon_x: i32, - icon_y: i32, - icon_width: u32, - icon_height: u32, - ) -> tauri::Result { + pub fn open_tray(app: &tauri::AppHandle) -> tauri::Result { + let state = tauri::Manager::state::(app); + let tray_pos = *state.tray_click_position.lock().unwrap(); let monitors = Self::get_monitors(); let primary = monitors .iter() @@ -190,24 +186,37 @@ impl WindowManager { 0 }; - let icon_center_x = icon_x + (icon_width as i32 / 2); - let default_x = icon_center_x - (physical_width / 2); - let max_x = work_right - physical_gap - physical_width; - let min_x = work_left + physical_gap; - let clamped_x = default_x.clamp(min_x, max_x); - - let icon_center_y = icon_y + (icon_height as i32 / 2); - let default_y = icon_center_y - (physical_height / 2); - let max_y = work_bottom - physical_gap - physical_height; - let min_y = work_top + physical_gap; - let clamped_y = default_y.clamp(min_y, max_y); - - let (final_x, final_y) = match primary.taskbar_position { - TaskbarPosition::Bottom => (clamped_x, work_bottom - physical_height - physical_gap), - TaskbarPosition::Top => (clamped_x, work_top + physical_gap), - TaskbarPosition::Left => (work_left + physical_gap, clamped_y), - TaskbarPosition::Right => (work_right - physical_width - physical_gap, clamped_y), - _ => (clamped_x, work_bottom - physical_height - physical_gap), + let (final_x, final_y) = if let Some(pos) = tray_pos { + let icon_x = pos.x as i32; + let icon_y = pos.y as i32; + let icon_width = 0; + let icon_height = 0; + + let icon_center_x = icon_x + (icon_width as i32 / 2); + let default_x = icon_center_x - (physical_width / 2); + let max_x = work_right - physical_gap - physical_width; + let min_x = work_left + physical_gap; + let clamped_x = default_x.clamp(min_x, max_x); + + let icon_center_y = icon_y + (icon_height as i32 / 2); + let default_y = icon_center_y - (physical_height / 2); + let max_y = work_bottom - physical_gap - physical_height; + let min_y = work_top + physical_gap; + let clamped_y = default_y.clamp(min_y, max_y); + + match primary.taskbar_position { + TaskbarPosition::Bottom => { + (clamped_x, work_bottom - physical_height - physical_gap) + } + TaskbarPosition::Top => (clamped_x, work_top + physical_gap), + TaskbarPosition::Left => (work_left + physical_gap, clamped_y), + TaskbarPosition::Right => (work_right - physical_width - physical_gap, clamped_y), + _ => (clamped_x, work_bottom - physical_height - physical_gap), + } + } else { + let x = work_right - physical_width - physical_gap; + let y = work_bottom - physical_height - physical_gap; + (x, y) }; window.set_always_on_top(true)?; @@ -227,28 +236,44 @@ impl WindowManager { } pub fn open_full_view(app: &tauri::AppHandle) -> tauri::Result { + log::info!("open_full_view: Getting monitors"); let monitors = Self::get_monitors(); + log::info!("open_full_view: Found {} monitors", monitors.len()); let primary = monitors .iter() .find(|m| m.is_primary) .unwrap_or(&monitors[0]); + log::info!( + "open_full_view: Primary monitor scale factor: {}", + primary.scale_factor + ); + log::info!("open_full_view: Checking if old-ui window exists"); let window = if let Some(window) = tauri::Manager::get_webview_window(app, OLD_UI_WINDOW_ID) { + log::info!("open_full_view: old-ui window exists, unminimizing"); let _ = window.unminimize(); window } else { - Self::build_full_window(app)? + log::info!("open_full_view: old-ui window does not exist, building it"); + let win = Self::build_full_window(app)?; + log::info!("open_full_view: old-ui window built successfully"); + win }; + log::info!("open_full_view: Querying outer_size"); let outer_size = window.outer_size().unwrap_or(tauri::PhysicalSize { width: (OLD_UI_WIDTH * primary.scale_factor) as u32, height: (OLD_UI_HEIGHT * primary.scale_factor) as u32, }); + log::info!("open_full_view: outer_size = {:?}", outer_size); + + log::info!("open_full_view: Querying inner_size"); let inner_size = window.inner_size().unwrap_or(tauri::PhysicalSize { width: (OLD_UI_WIDTH * primary.scale_factor) as u32, height: (OLD_UI_HEIGHT * primary.scale_factor) as u32, }); + log::info!("open_full_view: inner_size = {:?}", inner_size); let physical_width = outer_size.width as i32; let physical_height = outer_size.height as i32; @@ -299,8 +324,15 @@ impl WindowManager { _ => {} } + log::info!( + "open_full_view: Setting position to ({}, {})", + window_x, + window_y + ); window.set_position(tauri::PhysicalPosition::new(window_x, window_y))?; + log::info!("open_full_view: Position set, showing window"); window.show()?; + log::info!("open_full_view: Window shown successfully"); Ok(window) } } From aa5a3f53141d83bca09f3d68a592abe8fc1c36ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Wed, 20 May 2026 15:06:51 +0200 Subject: [PATCH 3/6] clippy fix --- src-tauri/src/window_manager/windows.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/window_manager/windows.rs b/src-tauri/src/window_manager/windows.rs index 670f5eed..56c5dc46 100644 --- a/src-tauri/src/window_manager/windows.rs +++ b/src-tauri/src/window_manager/windows.rs @@ -192,13 +192,13 @@ impl WindowManager { let icon_width = 0; let icon_height = 0; - let icon_center_x = icon_x + (icon_width as i32 / 2); + let icon_center_x = icon_x + (icon_width / 2); let default_x = icon_center_x - (physical_width / 2); let max_x = work_right - physical_gap - physical_width; let min_x = work_left + physical_gap; let clamped_x = default_x.clamp(min_x, max_x); - let icon_center_y = icon_y + (icon_height as i32 / 2); + let icon_center_y = icon_y + (icon_height / 2); let default_y = icon_center_y - (physical_height / 2); let max_y = work_bottom - physical_gap - physical_height; let min_y = work_top + physical_gap; From d9c6d00d5f94d5a4ec81f66bb382085f1b12bc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Wed, 20 May 2026 15:19:17 +0200 Subject: [PATCH 4/6] update styles --- new-ui/src/shared/components/Divider/style.scss | 2 +- .../components/LocationCard/components/MfaSelector/style.scss | 4 ++-- src/pages/client/clientAPI/clientApi.ts | 3 +-- src/shared/defguard-ui | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/new-ui/src/shared/components/Divider/style.scss b/new-ui/src/shared/components/Divider/style.scss index a75bd2fb..e47a4fee 100644 --- a/new-ui/src/shared/components/Divider/style.scss +++ b/new-ui/src/shared/components/Divider/style.scss @@ -1,6 +1,6 @@ .divider { --divider-line-size: 1px; - --divider-color: var(--bg-white-20); + --divider-color: var(--bg-white-10); user-select: none; diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss b/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss index 2c2e9b1f..16432580 100644 --- a/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/style.scss @@ -3,7 +3,7 @@ --border: var(--border-default); --icon: var(--fg-white-80); --color: var(--fg-white-80); - --box-shadow: box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0); + --box-shadow: box-shadow: 0 4px 4px 0 rgb(0 0 0 / 0%); display: grid; grid-template-columns: 20px minmax(0, 1fr) 16px; @@ -36,7 +36,7 @@ --color: var(--fg-white-100); --border: var(--border-action-disabled); --icon: var(--fg-white-100); - --box-shadow: box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.05); + --box-shadow: box-shadow: 0 4px 4px 0 rgb(0 0 0 / 5%); } > .middle { diff --git a/src/pages/client/clientAPI/clientApi.ts b/src/pages/client/clientAPI/clientApi.ts index 4278415d..8d0aec4a 100644 --- a/src/pages/client/clientAPI/clientApi.ts +++ b/src/pages/client/clientAPI/clientApi.ts @@ -130,8 +130,7 @@ const stopGlobalLogWatcher = async (): Promise => const getAppConfig = async (): Promise => invokeWrapper('command_get_app_config'); -const getPostureData = async (): Promise => - invokeWrapper('get_posture_data'); +const getPostureData = async (): Promise => invokeWrapper('get_posture_data'); const getProvisioningConfig = async (): Promise => invokeWrapper('get_provisioning_config'); diff --git a/src/shared/defguard-ui b/src/shared/defguard-ui index 1110ba80..4afa896c 160000 --- a/src/shared/defguard-ui +++ b/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 1110ba807491689efc40a2e28383ea1c6186fcad +Subproject commit 4afa896cdc09c711ce867442818fe0e7b7b9839f From 10cb0c2fa8df970aa694db9add011a6ab3d3ea70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Wed, 20 May 2026 22:45:24 +0200 Subject: [PATCH 5/6] clippy fixes --- src-tauri/src/window_manager/macos.rs | 3 ++- src-tauri/src/window_manager/mod.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/window_manager/macos.rs b/src-tauri/src/window_manager/macos.rs index 770e9c92..d93964dc 100644 --- a/src-tauri/src/window_manager/macos.rs +++ b/src-tauri/src/window_manager/macos.rs @@ -28,8 +28,9 @@ fn get_tray_window_position( size: PhysicalSize, ) -> Option> { let app_state = app.state::(); + let tray_click_position = app_state.tray_click_position.lock().unwrap().to_owned(); - if let Some(tray_position) = app_state.tray_click_position.lock().unwrap().to_owned() { + if let Some(tray_position) = tray_click_position { let monitor = get_monitor_for_position(app, tray_position.x, tray_position.y)?; let scale_factor = monitor.scale_factor(); diff --git a/src-tauri/src/window_manager/mod.rs b/src-tauri/src/window_manager/mod.rs index 08f9e0ae..814e113e 100644 --- a/src-tauri/src/window_manager/mod.rs +++ b/src-tauri/src/window_manager/mod.rs @@ -3,6 +3,7 @@ use tauri::{AppHandle, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; use crate::database::{models::location::Location, DB_POOL}; /// Returns `true` if there are any non-service locations in the database. +#[cfg(not(target_os = "linux"))] pub async fn has_non_service_locations() -> bool { match Location::all(&*DB_POOL, false).await { Ok(locations) => !locations.is_empty(), From 0cb529171b7e6c3c6016695d2886c6205a13ac4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Wed, 20 May 2026 22:51:24 +0200 Subject: [PATCH 6/6] Update mod.rs --- src-tauri/src/window_manager/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src-tauri/src/window_manager/mod.rs b/src-tauri/src/window_manager/mod.rs index 814e113e..780e41cc 100644 --- a/src-tauri/src/window_manager/mod.rs +++ b/src-tauri/src/window_manager/mod.rs @@ -1,5 +1,6 @@ use tauri::{AppHandle, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; +#[cfg(not(target_os = "linux"))] use crate::database::{models::location::Location, DB_POOL}; /// Returns `true` if there are any non-service locations in the database.