feat(mini): reserve screen edge with Windows appbar
This commit is contained in:
1
apps/mini/src-tauri/Cargo.lock
generated
1
apps/mini/src-tauri/Cargo.lock
generated
@@ -2520,6 +2520,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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",
|
||||
] }
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
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> {
|
||||
#[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());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let monitor = window
|
||||
.current_monitor()
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or_else(|| "无法识别当前屏幕".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);
|
||||
@@ -26,11 +42,29 @@ fn dock_window(window: Window, side: String) -> Result<(), String> {
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![dock_window])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run 森林AI Mini");
|
||||
.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])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("failed to build SenlinAI Mini")
|
||||
.run(|_, event| {
|
||||
#[cfg(windows)]
|
||||
if matches!(event, tauri::RunEvent::ExitRequested { .. }) {
|
||||
windows_appbar::unregister();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
178
apps/mini/src-tauri/src/windows_appbar.rs
Normal file
178
apps/mini/src-tauri/src/windows_appbar.rs
Normal file
@@ -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::<MONITORINFO>() 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::<APPBARDATA>() as u32,
|
||||
hWnd: hwnd,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
"minWidth": 360,
|
||||
"minHeight": 640,
|
||||
"resizable": true,
|
||||
"maximizable": false,
|
||||
"alwaysOnTop": true,
|
||||
"center": false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user