refactor(desktop): consolidate app on docked client

This commit is contained in:
2026-07-25 19:51:12 +08:00
parent 8e15c06148
commit 1e1718caee
38 changed files with 2024 additions and 7513 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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 { .. }) {

View File

@@ -1,3 +1,3 @@
fn main() {
senlinai_workbench_lib::run()
senlinai_app_lib::run();
}

View File

@@ -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,

View File

@@ -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": {