diff --git a/apps/mini/src-tauri/Cargo.lock b/apps/mini/src-tauri/Cargo.lock index a266227..a08f31d 100644 --- a/apps/mini/src-tauri/Cargo.lock +++ b/apps/mini/src-tauri/Cargo.lock @@ -2520,6 +2520,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "windows", ] [[package]] diff --git a/apps/mini/src-tauri/Cargo.toml b/apps/mini/src-tauri/Cargo.toml index e8f72e1..e7323ea 100644 --- a/apps/mini/src-tauri/Cargo.toml +++ b/apps/mini/src-tauri/Cargo.toml @@ -16,3 +16,11 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", +] } diff --git a/apps/mini/src-tauri/src/lib.rs b/apps/mini/src-tauri/src/lib.rs index 533d6bc..e776e3f 100644 --- a/apps/mini/src-tauri/src/lib.rs +++ b/apps/mini/src-tauri/src/lib.rs @@ -1,36 +1,70 @@ -use tauri::{PhysicalPosition, PhysicalSize, Window}; +use tauri::Window; + +#[cfg(windows)] +use tauri::Manager; +#[cfg(not(windows))] +use tauri::{PhysicalPosition, PhysicalSize}; + +#[cfg(windows)] +mod windows_appbar; #[tauri::command] fn dock_window(window: Window, side: String) -> Result<(), String> { - let monitor = window - .current_monitor() - .map_err(|error| error.to_string())? - .ok_or_else(|| "无法识别当前屏幕".to_string())?; - let window_size = window.outer_size().map_err(|error| error.to_string())?; - let work_area = monitor.work_area(); - let dock_size = PhysicalSize::new(window_size.width, work_area.size.height); - let x = if side == "left" { - work_area.position.x - } else { - work_area.position.x + work_area.size.width as i32 - dock_size.width as i32 - }; + #[cfg(windows)] + { + let hwnd = window.hwnd().map_err(|error| error.to_string())?; + return windows_appbar::set_edge(hwnd, side == "left").map_err(|error| error.to_string()); + } - window - .set_size(dock_size) - .map_err(|error| error.to_string())?; - window - .set_position(PhysicalPosition::new(x, work_area.position.y)) - .map_err(|error| error.to_string())?; - window - .set_always_on_top(true) - .map_err(|error| error.to_string())?; - Ok(()) + #[cfg(not(windows))] + { + let monitor = window + .current_monitor() + .map_err(|error| error.to_string())? + .ok_or_else(|| "Unable to identify the current monitor".to_string())?; + let window_size = window.outer_size().map_err(|error| error.to_string())?; + let work_area = monitor.work_area(); + let dock_size = PhysicalSize::new(window_size.width, work_area.size.height); + let x = if side == "left" { + work_area.position.x + } else { + work_area.position.x + work_area.size.width as i32 - dock_size.width as i32 + }; + + window + .set_size(dock_size) + .map_err(|error| error.to_string())?; + window + .set_position(PhysicalPosition::new(x, work_area.position.y)) + .map_err(|error| error.to_string())?; + window + .set_always_on_top(true) + .map_err(|error| error.to_string())?; + Ok(()) + } } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .setup(|app| { + #[cfg(windows)] + { + let window = app + .get_webview_window("main") + .expect("main window must be configured"); + windows_appbar::register(window.hwnd()?)?; + } + + Ok(()) + }) .invoke_handler(tauri::generate_handler![dock_window]) - .run(tauri::generate_context!()) - .expect("failed to run 森林AI Mini"); + .build(tauri::generate_context!()) + .expect("failed to build SenlinAI Mini") + .run(|_, event| { + #[cfg(windows)] + if matches!(event, tauri::RunEvent::ExitRequested { .. }) { + windows_appbar::unregister(); + } + }); } diff --git a/apps/mini/src-tauri/src/windows_appbar.rs b/apps/mini/src-tauri/src/windows_appbar.rs new file mode 100644 index 0000000..1f16b31 --- /dev/null +++ b/apps/mini/src-tauri/src/windows_appbar.rs @@ -0,0 +1,178 @@ +//! Windows system AppBar integration for the Mini side panel. + +use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicU32, Ordering}; + +use windows::{ + core::{w, Error, Result}, + Win32::{ + Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM}, + Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST, + }, + UI::{ + Shell::{ + DefSubclassProc, RemoveWindowSubclass, SHAppBarMessage, SetWindowSubclass, + ABE_LEFT, ABE_RIGHT, ABM_ACTIVATE, ABM_NEW, ABM_QUERYPOS, ABM_REMOVE, ABM_SETPOS, + ABM_WINDOWPOSCHANGED, ABN_POSCHANGED, APPBARDATA, + }, + WindowsAndMessaging::{ + GetWindowRect, RegisterWindowMessageW, SetWindowPos, SWP_NOACTIVATE, SWP_NOZORDER, + WM_ACTIVATE, WM_WINDOWPOSCHANGED, + }, + }, + }, +}; + +const SUBCLASS_ID: usize = 0x5341_4D42; // "SAMB" (SenlinAI Mini AppBar) + +static CALLBACK_MESSAGE: AtomicU32 = AtomicU32::new(0); +static REGISTERED_HWND: AtomicIsize = AtomicIsize::new(0); +static EDGE: AtomicU32 = AtomicU32::new(ABE_RIGHT); +static LAYOUT_IN_PROGRESS: AtomicBool = AtomicBool::new(false); + +/// Registers the main Mini window as a right-edge AppBar. +pub fn register(hwnd: HWND) -> Result<()> { + if REGISTERED_HWND.load(Ordering::Acquire) != 0 { + return Ok(()); + } + + let callback_message = unsafe { RegisterWindowMessageW(w!("SenlinAI.Mini.AppBar.Callback")) }; + if callback_message == 0 { + return Err(Error::from_win32()); + } + + unsafe { + SetWindowSubclass(hwnd, Some(appbar_subclass_proc), SUBCLASS_ID, 0).ok()?; + } + + let mut data = appbar_data(hwnd); + data.uCallbackMessage = callback_message; + if unsafe { SHAppBarMessage(ABM_NEW, &mut data) } == 0 { + unsafe { + let _ = RemoveWindowSubclass(hwnd, Some(appbar_subclass_proc), SUBCLASS_ID); + } + return Err(Error::from_win32()); + } + + CALLBACK_MESSAGE.store(callback_message, Ordering::Release); + REGISTERED_HWND.store(hwnd.0 as isize, Ordering::Release); + + if let Err(error) = layout(hwnd) { + unregister(); + return Err(error); + } + + Ok(()) +} + +/// Repositions the AppBar after the Mini UI selects the left or right edge. +pub fn set_edge(hwnd: HWND, left: bool) -> Result<()> { + EDGE.store(if left { ABE_LEFT } else { ABE_RIGHT }, Ordering::Release); + layout(hwnd) +} + +/// Removes the AppBar reservation before the Tauri window is destroyed. +pub fn unregister() { + let raw_hwnd = REGISTERED_HWND.swap(0, Ordering::AcqRel); + CALLBACK_MESSAGE.store(0, Ordering::Release); + + if raw_hwnd == 0 { + return; + } + + let hwnd = HWND(raw_hwnd as *mut _); + let mut data = appbar_data(hwnd); + unsafe { + SHAppBarMessage(ABM_REMOVE, &mut data); + let _ = RemoveWindowSubclass(hwnd, Some(appbar_subclass_proc), SUBCLASS_ID); + } +} + +unsafe extern "system" fn appbar_subclass_proc( + hwnd: HWND, + message: u32, + wparam: WPARAM, + lparam: LPARAM, + _subclass_id: usize, + _reference_data: usize, +) -> LRESULT { + let callback_message = CALLBACK_MESSAGE.load(Ordering::Acquire); + + if message == callback_message && wparam.0 as u32 == ABN_POSCHANGED { + let _ = layout(hwnd); + } else if message == WM_ACTIVATE { + let mut data = appbar_data(hwnd); + SHAppBarMessage(ABM_ACTIVATE, &mut data); + } else if message == WM_WINDOWPOSCHANGED { + let mut data = appbar_data(hwnd); + SHAppBarMessage(ABM_WINDOWPOSCHANGED, &mut data); + + if !LAYOUT_IN_PROGRESS.load(Ordering::Acquire) { + let _ = layout(hwnd); + } + } + + DefSubclassProc(hwnd, message, wparam, lparam) +} + +fn layout(hwnd: HWND) -> Result<()> { + if LAYOUT_IN_PROGRESS.swap(true, Ordering::AcqRel) { + return Ok(()); + } + + let result = layout_inner(hwnd); + LAYOUT_IN_PROGRESS.store(false, Ordering::Release); + result +} + +fn layout_inner(hwnd: HWND) -> Result<()> { + let mut window_rect = RECT::default(); + unsafe { GetWindowRect(hwnd, &mut window_rect)? }; + let width = window_rect.right - window_rect.left; + + let monitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) }; + let mut monitor_info = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + unsafe { GetMonitorInfoW(monitor, &mut monitor_info).ok()? }; + + let edge = EDGE.load(Ordering::Acquire); + let mut data = appbar_data(hwnd); + data.uEdge = edge; + data.rc = monitor_info.rcMonitor; + if edge == ABE_LEFT { + data.rc.right = data.rc.left + width; + } else { + data.rc.left = data.rc.right - width; + } + + unsafe { + SHAppBarMessage(ABM_QUERYPOS, &mut data); + if edge == ABE_LEFT { + data.rc.right = data.rc.left + width; + } else { + data.rc.left = data.rc.right - width; + } + SHAppBarMessage(ABM_SETPOS, &mut data); + SetWindowPos( + hwnd, + None, + data.rc.left, + data.rc.top, + data.rc.right - data.rc.left, + data.rc.bottom - data.rc.top, + SWP_NOACTIVATE | SWP_NOZORDER, + )?; + } + + Ok(()) +} + +fn appbar_data(hwnd: HWND) -> APPBARDATA { + APPBARDATA { + cbSize: std::mem::size_of::() as u32, + hWnd: hwnd, + ..Default::default() + } +} diff --git a/apps/mini/src-tauri/tauri.conf.json b/apps/mini/src-tauri/tauri.conf.json index ef28f21..5f58f88 100644 --- a/apps/mini/src-tauri/tauri.conf.json +++ b/apps/mini/src-tauri/tauri.conf.json @@ -19,6 +19,7 @@ "minWidth": 360, "minHeight": 640, "resizable": true, + "maximizable": false, "alwaysOnTop": true, "center": false }