feat(macos): set traffic-light position via Tauri builder API and sync with zoom
Use Tauri's native `traffic_light_position()` builder method to position macOS window controls instead of runtime objc2 calls. A global AtomicU32 tracks the current zoom level so newly created windows reflect the latest zoom. The frontend syncs zoom changes to the backend via a new `update_traffic_light_position` command. - Add `traffic_light_position()` to `apply_platform_window_style` builder - Add `CURRENT_ZOOM` atomic and `traffic_light_position()` helper - Register `update_traffic_light_position` Tauri command - Add `syncTrafficLightPosition` in appearance-provider to sync on zoom change, mount, and cross-tab storage events - Consolidate `ensure_windows_undecorated` calls into `post_window_setup` - Remove dead `on_window_resized` no-op and its Resized event listener Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
|
||||||
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
|
||||||
|
|
||||||
@@ -7,6 +9,23 @@ use crate::app_error::AppCommandError;
|
|||||||
use crate::db::AppDatabase;
|
use crate::db::AppDatabase;
|
||||||
use crate::models::FolderHistoryEntry;
|
use crate::models::FolderHistoryEntry;
|
||||||
|
|
||||||
|
/// Base traffic-light position (logical px) at 100 % zoom.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
const TRAFFIC_LIGHT_X: f64 = 12.0;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
const TRAFFIC_LIGHT_Y: f64 = 18.0;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
static CURRENT_ZOOM: AtomicU32 = AtomicU32::new(100);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn traffic_light_position() -> tauri::LogicalPosition<f64> {
|
||||||
|
let zoom = CURRENT_ZOOM.load(Ordering::Relaxed) as f64;
|
||||||
|
// Only Y scales with zoom: overlay content shifts vertically with
|
||||||
|
// font-size changes, but the horizontal inset remains constant.
|
||||||
|
tauri::LogicalPosition::new(TRAFFIC_LIGHT_X, TRAFFIC_LIGHT_Y * zoom / 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SettingsWindowState {
|
pub struct SettingsWindowState {
|
||||||
owner_window_label: Mutex<Option<String>>,
|
owner_window_label: Mutex<Option<String>>,
|
||||||
}
|
}
|
||||||
@@ -31,6 +50,7 @@ where
|
|||||||
builder
|
builder
|
||||||
.hidden_title(true)
|
.hidden_title(true)
|
||||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||||
|
.traffic_light_position(traffic_light_position())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -52,6 +72,11 @@ fn ensure_windows_undecorated(window: &tauri::WebviewWindow) {
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
fn ensure_windows_undecorated(_window: &tauri::WebviewWindow) {}
|
fn ensure_windows_undecorated(_window: &tauri::WebviewWindow) {}
|
||||||
|
|
||||||
|
/// Apply platform-specific post-creation setup.
|
||||||
|
pub(crate) fn post_window_setup(window: &tauri::WebviewWindow) {
|
||||||
|
ensure_windows_undecorated(window);
|
||||||
|
}
|
||||||
|
|
||||||
impl SettingsWindowState {
|
impl SettingsWindowState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -202,7 +227,7 @@ pub async fn open_folder_window(
|
|||||||
|
|
||||||
let label = folder_window_label(entry.id);
|
let label = folder_window_label(entry.id);
|
||||||
if let Some(existing) = app.get_webview_window(&label) {
|
if let Some(existing) = app.get_webview_window(&label) {
|
||||||
ensure_windows_undecorated(&existing);
|
post_window_setup(&existing);
|
||||||
let _ = existing.unminimize();
|
let _ = existing.unminimize();
|
||||||
existing
|
existing
|
||||||
.set_focus()
|
.set_focus()
|
||||||
@@ -224,7 +249,7 @@ pub async fn open_folder_window(
|
|||||||
let folder_window = apply_platform_window_style(builder)
|
let folder_window = apply_platform_window_style(builder)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppCommandError::window("Failed to open folder window", e.to_string()))?;
|
.map_err(|e| AppCommandError::window("Failed to open folder window", e.to_string()))?;
|
||||||
ensure_windows_undecorated(&folder_window);
|
post_window_setup(&folder_window);
|
||||||
|
|
||||||
// Close welcome and project-boot windows
|
// Close welcome and project-boot windows
|
||||||
if let Some(w) = app.get_webview_window("welcome") {
|
if let Some(w) = app.get_webview_window("welcome") {
|
||||||
@@ -280,7 +305,7 @@ pub async fn open_commit_window(
|
|||||||
let commit_window = apply_platform_window_style(builder)
|
let commit_window = apply_platform_window_style(builder)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppCommandError::window("Failed to open commit window", e.to_string()))?;
|
.map_err(|e| AppCommandError::window("Failed to open commit window", e.to_string()))?;
|
||||||
ensure_windows_undecorated(&commit_window);
|
post_window_setup(&commit_window);
|
||||||
if let Some(owner_window) = app.get_webview_window(&owner_label) {
|
if let Some(owner_window) = app.get_webview_window(&owner_label) {
|
||||||
if let Err(err) = owner_window.set_enabled(false) {
|
if let Err(err) = owner_window.set_enabled(false) {
|
||||||
let _ = commit_window.close();
|
let _ = commit_window.close();
|
||||||
@@ -309,7 +334,7 @@ pub async fn open_settings_window(
|
|||||||
) -> Result<(), AppCommandError> {
|
) -> Result<(), AppCommandError> {
|
||||||
let target_route = resolve_settings_target(section.as_deref(), agent_type.as_deref());
|
let target_route = resolve_settings_target(section.as_deref(), agent_type.as_deref());
|
||||||
if let Some(existing) = app.get_webview_window("settings") {
|
if let Some(existing) = app.get_webview_window("settings") {
|
||||||
ensure_windows_undecorated(&existing);
|
post_window_setup(&existing);
|
||||||
if section.is_some() || agent_type.is_some() {
|
if section.is_some() || agent_type.is_some() {
|
||||||
let target_path = format!("/{target_route}");
|
let target_path = format!("/{target_route}");
|
||||||
let target_json = serde_json::to_string(&target_path).map_err(|e| {
|
let target_json = serde_json::to_string(&target_path).map_err(|e| {
|
||||||
@@ -337,7 +362,7 @@ pub async fn open_settings_window(
|
|||||||
let settings_window = apply_platform_window_style(builder)
|
let settings_window = apply_platform_window_style(builder)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppCommandError::window("Failed to open settings window", e.to_string()))?;
|
.map_err(|e| AppCommandError::window("Failed to open settings window", e.to_string()))?;
|
||||||
ensure_windows_undecorated(&settings_window);
|
post_window_setup(&settings_window);
|
||||||
|
|
||||||
state.set_owner(owner_label);
|
state.set_owner(owner_label);
|
||||||
settings_window
|
settings_window
|
||||||
@@ -442,7 +467,7 @@ pub async fn open_merge_window(
|
|||||||
let merge_window = apply_platform_window_style(builder)
|
let merge_window = apply_platform_window_style(builder)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppCommandError::window("Failed to open merge window", e.to_string()))?;
|
.map_err(|e| AppCommandError::window("Failed to open merge window", e.to_string()))?;
|
||||||
ensure_windows_undecorated(&merge_window);
|
post_window_setup(&merge_window);
|
||||||
if let Some(owner_window) = app.get_webview_window(&owner_label) {
|
if let Some(owner_window) = app.get_webview_window(&owner_label) {
|
||||||
if let Err(err) = owner_window.set_enabled(false) {
|
if let Err(err) = owner_window.set_enabled(false) {
|
||||||
let _ = merge_window.close();
|
let _ = merge_window.close();
|
||||||
@@ -522,7 +547,7 @@ pub async fn cleanup_dangling_merge(app: &AppHandle, merge_window_label: &str) {
|
|||||||
|
|
||||||
pub fn open_welcome_window(app: &AppHandle) -> Result<(), AppCommandError> {
|
pub fn open_welcome_window(app: &AppHandle) -> Result<(), AppCommandError> {
|
||||||
if let Some(existing) = app.get_webview_window("welcome") {
|
if let Some(existing) = app.get_webview_window("welcome") {
|
||||||
ensure_windows_undecorated(&existing);
|
post_window_setup(&existing);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let url = WebviewUrl::App("welcome".into());
|
let url = WebviewUrl::App("welcome".into());
|
||||||
@@ -534,7 +559,7 @@ pub fn open_welcome_window(app: &AppHandle) -> Result<(), AppCommandError> {
|
|||||||
let welcome_window = apply_platform_window_style(builder)
|
let welcome_window = apply_platform_window_style(builder)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppCommandError::window("Failed to open welcome window", e.to_string()))?;
|
.map_err(|e| AppCommandError::window("Failed to open welcome window", e.to_string()))?;
|
||||||
ensure_windows_undecorated(&welcome_window);
|
post_window_setup(&welcome_window);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,7 +573,7 @@ pub async fn open_stash_window(
|
|||||||
let label = format!("stash-{folder_id}");
|
let label = format!("stash-{folder_id}");
|
||||||
|
|
||||||
if let Some(existing) = app.get_webview_window(&label) {
|
if let Some(existing) = app.get_webview_window(&label) {
|
||||||
ensure_windows_undecorated(&existing);
|
post_window_setup(&existing);
|
||||||
let _ = existing.unminimize();
|
let _ = existing.unminimize();
|
||||||
existing
|
existing
|
||||||
.set_focus()
|
.set_focus()
|
||||||
@@ -573,7 +598,7 @@ pub async fn open_stash_window(
|
|||||||
let stash_window = apply_platform_window_style(builder)
|
let stash_window = apply_platform_window_style(builder)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppCommandError::window("Failed to open stash window", e.to_string()))?;
|
.map_err(|e| AppCommandError::window("Failed to open stash window", e.to_string()))?;
|
||||||
ensure_windows_undecorated(&stash_window);
|
post_window_setup(&stash_window);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -588,7 +613,7 @@ pub async fn open_push_window(
|
|||||||
let label = format!("push-{folder_id}");
|
let label = format!("push-{folder_id}");
|
||||||
|
|
||||||
if let Some(existing) = app.get_webview_window(&label) {
|
if let Some(existing) = app.get_webview_window(&label) {
|
||||||
ensure_windows_undecorated(&existing);
|
post_window_setup(&existing);
|
||||||
let _ = existing.unminimize();
|
let _ = existing.unminimize();
|
||||||
existing
|
existing
|
||||||
.set_focus()
|
.set_focus()
|
||||||
@@ -613,7 +638,7 @@ pub async fn open_push_window(
|
|||||||
let push_window = apply_platform_window_style(builder)
|
let push_window = apply_platform_window_style(builder)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| AppCommandError::window("Failed to open push window", e.to_string()))?;
|
.map_err(|e| AppCommandError::window("Failed to open push window", e.to_string()))?;
|
||||||
ensure_windows_undecorated(&push_window);
|
post_window_setup(&push_window);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -625,7 +650,7 @@ pub async fn open_project_boot_window(
|
|||||||
source: Option<String>,
|
source: Option<String>,
|
||||||
) -> Result<(), AppCommandError> {
|
) -> Result<(), AppCommandError> {
|
||||||
if let Some(existing) = app.get_webview_window("project-boot") {
|
if let Some(existing) = app.get_webview_window("project-boot") {
|
||||||
ensure_windows_undecorated(&existing);
|
post_window_setup(&existing);
|
||||||
let _ = existing.unminimize();
|
let _ = existing.unminimize();
|
||||||
existing.set_focus().map_err(|e| {
|
existing.set_focus().map_err(|e| {
|
||||||
AppCommandError::window("Failed to focus project boot window", e.to_string())
|
AppCommandError::window("Failed to focus project boot window", e.to_string())
|
||||||
@@ -650,7 +675,7 @@ pub async fn open_project_boot_window(
|
|||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
AppCommandError::window("Failed to open project boot window", e.to_string())
|
AppCommandError::window("Failed to open project boot window", e.to_string())
|
||||||
})?;
|
})?;
|
||||||
ensure_windows_undecorated(&window);
|
post_window_setup(&window);
|
||||||
|
|
||||||
// Close welcome if opened from welcome
|
// Close welcome if opened from welcome
|
||||||
if source.as_deref() == Some("welcome") {
|
if source.as_deref() == Some("welcome") {
|
||||||
@@ -661,3 +686,15 @@ pub async fn open_project_boot_window(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Store the current zoom level so that newly created windows use the correct
|
||||||
|
/// traffic-light position. Existing windows are NOT repositioned at runtime.
|
||||||
|
#[cfg(feature = "tauri-runtime")]
|
||||||
|
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||||
|
pub async fn update_traffic_light_position(app: AppHandle, zoom: f64) -> Result<(), AppCommandError> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
CURRENT_ZOOM.store(zoom.clamp(50.0, 300.0) as u32, Ordering::Relaxed);
|
||||||
|
let _ = (app, zoom);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,9 @@ mod tauri_app {
|
|||||||
.title(&entry.name)
|
.title(&entry.name)
|
||||||
.inner_size(1260.0, 860.0)
|
.inner_size(1260.0, 860.0)
|
||||||
.min_inner_size(900.0, 600.0);
|
.min_inner_size(900.0, 600.0);
|
||||||
let _ = windows::apply_platform_window_style(builder).build();
|
if let Ok(w) = windows::apply_platform_window_style(builder).build() {
|
||||||
|
windows::post_window_setup(&w);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +336,7 @@ mod tauri_app {
|
|||||||
windows::open_stash_window,
|
windows::open_stash_window,
|
||||||
windows::open_push_window,
|
windows::open_push_window,
|
||||||
windows::open_project_boot_window,
|
windows::open_project_boot_window,
|
||||||
|
windows::update_traffic_light_position,
|
||||||
project_boot::detect_package_manager,
|
project_boot::detect_package_manager,
|
||||||
project_boot::create_shadcn_project,
|
project_boot::create_shadcn_project,
|
||||||
system_settings::get_system_proxy_settings,
|
system_settings::get_system_proxy_settings,
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ import {
|
|||||||
STORAGE_KEY_ZOOM_LEVEL,
|
STORAGE_KEY_ZOOM_LEVEL,
|
||||||
} from "@/lib/appearance-script"
|
} from "@/lib/appearance-script"
|
||||||
|
|
||||||
|
function syncTrafficLightPosition(zoom: number) {
|
||||||
|
if (typeof window === "undefined" || !("__TAURI_INTERNALS__" in window))
|
||||||
|
return
|
||||||
|
import("@/lib/tauri").then((t) =>
|
||||||
|
t.updateTrafficLightPosition(zoom).catch(() => {})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type AppearanceContextValue = {
|
type AppearanceContextValue = {
|
||||||
themeColor: ThemeColor
|
themeColor: ThemeColor
|
||||||
setThemeColor: (color: ThemeColor) => void
|
setThemeColor: (color: ThemeColor) => void
|
||||||
@@ -73,6 +81,7 @@ export function AppearanceProvider({
|
|||||||
const setZoomLevel = useCallback((zoom: ZoomLevel) => {
|
const setZoomLevel = useCallback((zoom: ZoomLevel) => {
|
||||||
setZoomLevelState(zoom)
|
setZoomLevelState(zoom)
|
||||||
document.documentElement.style.fontSize = `${(16 * zoom) / 100}px`
|
document.documentElement.style.fontSize = `${(16 * zoom) / 100}px`
|
||||||
|
syncTrafficLightPosition(zoom)
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY_ZOOM_LEVEL, String(zoom))
|
localStorage.setItem(STORAGE_KEY_ZOOM_LEVEL, String(zoom))
|
||||||
} catch {
|
} catch {
|
||||||
@@ -80,6 +89,12 @@ export function AppearanceProvider({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Sync traffic-light position on mount (initial zoom)
|
||||||
|
useEffect(() => {
|
||||||
|
syncTrafficLightPosition(zoomLevel)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 跨标签页同步:用户在另一个窗口改了设置时,本窗口实时跟进
|
// 跨标签页同步:用户在另一个窗口改了设置时,本窗口实时跟进
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onStorage = (e: StorageEvent) => {
|
const onStorage = (e: StorageEvent) => {
|
||||||
@@ -95,6 +110,7 @@ export function AppearanceProvider({
|
|||||||
if ((ZOOM_LEVELS as readonly number[]).includes(zoom)) {
|
if ((ZOOM_LEVELS as readonly number[]).includes(zoom)) {
|
||||||
setZoomLevelState(zoom)
|
setZoomLevelState(zoom)
|
||||||
document.documentElement.style.fontSize = `${(16 * zoom) / 100}px`
|
document.documentElement.style.fontSize = `${(16 * zoom) / 100}px`
|
||||||
|
syncTrafficLightPosition(zoom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,6 +442,12 @@ export async function mcpRemoveServer(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Appearance / window chrome
|
||||||
|
|
||||||
|
export async function updateTrafficLightPosition(zoom: number): Promise<void> {
|
||||||
|
return invoke("update_traffic_light_position", { zoom: zoom as number })
|
||||||
|
}
|
||||||
|
|
||||||
// Folder history commands
|
// Folder history commands
|
||||||
|
|
||||||
export async function loadFolderHistory(): Promise<FolderHistoryEntry[]> {
|
export async function loadFolderHistory(): Promise<FolderHistoryEntry[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user