refactor(desktop): consolidate app on docked client
This commit is contained in:
669
apps/desktop/src-tauri/Cargo.lock
generated
669
apps/desktop/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "senlinai-full"
|
||||
name = "senlinai-app"
|
||||
version = "1.0.0"
|
||||
description = "森林AI 完整版桌面客户端"
|
||||
description = "森林AI 迷你停靠客户端"
|
||||
authors = ["森林AI"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "senlinai_workbench_lib"
|
||||
name = "senlinai_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
@@ -14,7 +14,6 @@ tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
|
||||
@@ -1,26 +1,66 @@
|
||||
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(|| "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()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let window = app
|
||||
.get_webview_window("main")
|
||||
.expect("main window must be configured");
|
||||
windows_appbar::register_right_edge(window.hwnd()?)?;
|
||||
windows_appbar::register(window.hwnd()?)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![dock_window])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("failed to build SenlinAI desktop app")
|
||||
.expect("failed to build SenlinAI App")
|
||||
.run(|_, event| {
|
||||
#[cfg(windows)]
|
||||
if matches!(event, tauri::RunEvent::ExitRequested { .. }) {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
senlinai_workbench_lib::run()
|
||||
senlinai_app_lib::run();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
//! 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.
|
||||
//! Windows system AppBar integration for the App side panel.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicU32, Ordering};
|
||||
|
||||
@@ -15,7 +12,7 @@ use windows::{
|
||||
UI::{
|
||||
Shell::{
|
||||
DefSubclassProc, RemoveWindowSubclass, SHAppBarMessage, SetWindowSubclass,
|
||||
ABE_RIGHT, ABM_ACTIVATE, ABM_NEW, ABM_QUERYPOS, ABM_REMOVE, ABM_SETPOS,
|
||||
ABE_LEFT, ABE_RIGHT, ABM_ACTIVATE, ABM_NEW, ABM_QUERYPOS, ABM_REMOVE, ABM_SETPOS,
|
||||
ABM_WINDOWPOSCHANGED, ABN_POSCHANGED, APPBARDATA,
|
||||
},
|
||||
WindowsAndMessaging::{
|
||||
@@ -30,18 +27,16 @@ const SUBCLASS_ID: usize = 0x5341_4142; // "SAAB" (SenlinAI 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 `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<()> {
|
||||
/// Registers the main App 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.AppBar.Callback")) };
|
||||
let callback_message = unsafe { RegisterWindowMessageW(w!("SenlinAI.App.AppBar.Callback")) };
|
||||
if callback_message == 0 {
|
||||
return Err(Error::from_win32());
|
||||
}
|
||||
@@ -62,7 +57,7 @@ pub fn register_right_edge(hwnd: HWND) -> Result<()> {
|
||||
CALLBACK_MESSAGE.store(callback_message, Ordering::Release);
|
||||
REGISTERED_HWND.store(hwnd.0 as isize, Ordering::Release);
|
||||
|
||||
if let Err(error) = layout_right_edge(hwnd) {
|
||||
if let Err(error) = layout(hwnd) {
|
||||
unregister();
|
||||
return Err(error);
|
||||
}
|
||||
@@ -70,6 +65,12 @@ pub fn register_right_edge(hwnd: HWND) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repositions the AppBar after the App 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);
|
||||
@@ -98,7 +99,7 @@ unsafe extern "system" fn appbar_subclass_proc(
|
||||
let callback_message = CALLBACK_MESSAGE.load(Ordering::Acquire);
|
||||
|
||||
if message == callback_message && wparam.0 as u32 == ABN_POSCHANGED {
|
||||
let _ = layout_right_edge(hwnd);
|
||||
let _ = layout(hwnd);
|
||||
} else if message == WM_ACTIVATE {
|
||||
let mut data = appbar_data(hwnd);
|
||||
SHAppBarMessage(ABM_ACTIVATE, &mut data);
|
||||
@@ -106,27 +107,25 @@ unsafe extern "system" fn appbar_subclass_proc(
|
||||
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);
|
||||
let _ = layout(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
DefSubclassProc(hwnd, message, wparam, lparam)
|
||||
}
|
||||
|
||||
fn layout_right_edge(hwnd: HWND) -> Result<()> {
|
||||
fn layout(hwnd: HWND) -> Result<()> {
|
||||
if LAYOUT_IN_PROGRESS.swap(true, Ordering::AcqRel) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let result = layout_right_edge_inner(hwnd);
|
||||
let result = layout_inner(hwnd);
|
||||
LAYOUT_IN_PROGRESS.store(false, Ordering::Release);
|
||||
result
|
||||
}
|
||||
|
||||
fn layout_right_edge_inner(hwnd: HWND) -> 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;
|
||||
@@ -138,16 +137,23 @@ fn layout_right_edge_inner(hwnd: HWND) -> Result<()> {
|
||||
};
|
||||
unsafe { GetMonitorInfoW(monitor, &mut monitor_info).ok()? };
|
||||
|
||||
let edge = EDGE.load(Ordering::Acquire);
|
||||
let mut data = appbar_data(hwnd);
|
||||
data.uEdge = ABE_RIGHT;
|
||||
data.uEdge = edge;
|
||||
data.rc = monitor_info.rcMonitor;
|
||||
data.rc.left = data.rc.right - width;
|
||||
if edge == ABE_LEFT {
|
||||
data.rc.right = data.rc.left + width;
|
||||
} else {
|
||||
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;
|
||||
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,
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "森林AI Full",
|
||||
"productName": "森林AI App",
|
||||
"version": "1.0.0",
|
||||
"identifier": "ai.senlin.workbench",
|
||||
"identifier": "ai.senlin.client",
|
||||
"build": {
|
||||
"frontendDist": "../../web_v1/dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "cd ../web_v1 && npm run dev",
|
||||
"beforeBuildCommand": "cd ../web_v1 && npm run build"
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:5180",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "森林AI Full",
|
||||
"width": 380,
|
||||
"label": "main",
|
||||
"title": "森林AI App",
|
||||
"width": 420,
|
||||
"height": 820,
|
||||
"minWidth": 300,
|
||||
"minHeight": 480,
|
||||
"resizable": false,
|
||||
"maximizable": false
|
||||
"minWidth": 360,
|
||||
"minHeight": 640,
|
||||
"resizable": true,
|
||||
"maximizable": false,
|
||||
"alwaysOnTop": true,
|
||||
"center": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
Reference in New Issue
Block a user