feat(desktop): reserve right edge with Windows appbar
This commit is contained in:
1
apps/desktop/src-tauri/Cargo.lock
generated
1
apps/desktop/src-tauri/Cargo.lock
generated
@@ -2841,6 +2841,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"windows",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -17,3 +17,11 @@ tauri = { version = "2", features = [] }
|
|||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows = { version = "0.61", features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Graphics_Gdi",
|
||||||
|
"Win32_UI_Shell",
|
||||||
|
"Win32_UI_WindowsAndMessaging",
|
||||||
|
] }
|
||||||
|
|||||||
@@ -1,7 +1,30 @@
|
|||||||
|
#[cfg(windows)]
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod windows_appbar;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.run(tauri::generate_context!())
|
.setup(|app| {
|
||||||
.expect("启动森林AI时发生错误");
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let window = app
|
||||||
|
.get_webview_window("main")
|
||||||
|
.expect("main window must be configured");
|
||||||
|
windows_appbar::register_right_edge(window.hwnd()?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.build(tauri::generate_context!())
|
||||||
|
.expect("failed to build SenlinAI desktop app")
|
||||||
|
.run(|_, event| {
|
||||||
|
#[cfg(windows)]
|
||||||
|
if matches!(event, tauri::RunEvent::ExitRequested { .. }) {
|
||||||
|
windows_appbar::unregister();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
172
apps/desktop/src-tauri/src/windows_appbar.rs
Normal file
172
apps/desktop/src-tauri/src/windows_appbar.rs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
//! Windows-only system AppBar support for the desktop side panel.
|
||||||
|
//!
|
||||||
|
//! An AppBar reserves desktop work area, so maximized windows do not cover the
|
||||||
|
//! right-hand SenlinAI panel. Tauri does not expose this Win32 shell feature.
|
||||||
|
|
||||||
|
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_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_4142; // "SAAB" (SenlinAI AppBar)
|
||||||
|
|
||||||
|
static CALLBACK_MESSAGE: AtomicU32 = AtomicU32::new(0);
|
||||||
|
static REGISTERED_HWND: AtomicIsize = AtomicIsize::new(0);
|
||||||
|
static LAYOUT_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Registers `hwnd` as the single right-edge application desktop toolbar.
|
||||||
|
///
|
||||||
|
/// The width is taken from the configured window width. The shell coordinates
|
||||||
|
/// the remaining dimensions with the taskbar and any other AppBars.
|
||||||
|
pub fn register_right_edge(hwnd: HWND) -> Result<()> {
|
||||||
|
if REGISTERED_HWND.load(Ordering::Acquire) != 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback_message = unsafe { RegisterWindowMessageW(w!("SenlinAI.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_right_edge(hwnd) {
|
||||||
|
unregister();
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_right_edge(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);
|
||||||
|
|
||||||
|
// A native move/resize must be reconciled with the shell reservation.
|
||||||
|
// Calls caused by our own SetWindowPos are ignored to avoid recursion.
|
||||||
|
if !LAYOUT_IN_PROGRESS.load(Ordering::Acquire) {
|
||||||
|
let _ = layout_right_edge(hwnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DefSubclassProc(hwnd, message, wparam, lparam)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_right_edge(hwnd: HWND) -> Result<()> {
|
||||||
|
if LAYOUT_IN_PROGRESS.swap(true, Ordering::AcqRel) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = layout_right_edge_inner(hwnd);
|
||||||
|
LAYOUT_IN_PROGRESS.store(false, Ordering::Release);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout_right_edge_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::<MONITORINFO>() as u32,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
unsafe { GetMonitorInfoW(monitor, &mut monitor_info).ok()? };
|
||||||
|
|
||||||
|
let mut data = appbar_data(hwnd);
|
||||||
|
data.uEdge = ABE_RIGHT;
|
||||||
|
data.rc = monitor_info.rcMonitor;
|
||||||
|
data.rc.left = data.rc.right - width;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// The shell first removes occupied AppBar/taskbar space. Restore our
|
||||||
|
// requested width, then commit the returned, conflict-free rectangle.
|
||||||
|
SHAppBarMessage(ABM_QUERYPOS, &mut data);
|
||||||
|
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::<APPBARDATA>() as u32,
|
||||||
|
hWnd: hwnd,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,8 +13,12 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "森林AI Full",
|
"title": "森林AI Full",
|
||||||
"width": 1280,
|
"width": 380,
|
||||||
"height": 820
|
"height": 820,
|
||||||
|
"minWidth": 300,
|
||||||
|
"minHeight": 480,
|
||||||
|
"resizable": false,
|
||||||
|
"maximizable": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
Reference in New Issue
Block a user