mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 09:37:37 +08:00
添加应用级别窗口按钮,以改善linux wayland下系统窗口按钮失效的问题 (#1119)
* feat(window): add app-level window controls with settings toggle Add a persistent settings toggle to enable app-level minimize/maximize/close controls and hide system decorations when enabled, providing a Wayland-friendly fallback for broken native titlebar interactions. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(window): restrict app-level window controls to Linux only and fix startup flicker - Guard useAppWindowControls with isLinux() in App.tsx so it's always false on macOS/Windows even if persisted as true - Wrap set_decorations call in lib.rs with #[cfg(target_os = "linux")] - Only show the toggle in WindowSettings on Linux - Skip setDecorations effect while settingsData is still loading to prevent the Rust-side decoration state from being overridden by the undefined->false fallback, which caused a brief title bar flicker --------- Co-authored-by: wzk <wx13571681304@outlook.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
+143
-8
@@ -9,6 +9,10 @@ import {
|
||||
Plus,
|
||||
Settings,
|
||||
ArrowLeft,
|
||||
Minus,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
X,
|
||||
Book,
|
||||
Wrench,
|
||||
RefreshCw,
|
||||
@@ -22,6 +26,7 @@ import {
|
||||
Shield,
|
||||
Cpu,
|
||||
} from "lucide-react";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { Provider, VisibleApps } from "@/types";
|
||||
import type { EnvConflict } from "@/types/env";
|
||||
import { useProvidersQuery, useSettingsQuery } from "@/lib/query";
|
||||
@@ -99,9 +104,8 @@ interface WebDavSyncStatusUpdatedPayload {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
||||
const DEFAULT_DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px
|
||||
const HEADER_HEIGHT = 64; // px
|
||||
const CONTENT_TOP_OFFSET = DRAG_BAR_HEIGHT + HEADER_HEIGHT;
|
||||
|
||||
const STORAGE_KEY = "cc-switch-last-app";
|
||||
const VALID_APPS: AppId[] = [
|
||||
@@ -153,12 +157,17 @@ function App() {
|
||||
const [currentView, setCurrentView] = useState<View>(getInitialView);
|
||||
const [settingsDefaultTab, setSettingsDefaultTab] = useState("general");
|
||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||
const [isWindowMaximized, setIsWindowMaximized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(VIEW_STORAGE_KEY, currentView);
|
||||
}, [currentView]);
|
||||
|
||||
const { data: settingsData } = useSettingsQuery();
|
||||
const useAppWindowControls =
|
||||
isLinux() && (settingsData?.useAppWindowControls ?? false);
|
||||
const dragBarHeight = useAppWindowControls ? 32 : DEFAULT_DRAG_BAR_HEIGHT;
|
||||
const contentTopOffset = dragBarHeight + HEADER_HEIGHT;
|
||||
const visibleApps: VisibleApps = settingsData?.visibleApps ?? {
|
||||
claude: true,
|
||||
codex: true,
|
||||
@@ -392,6 +401,51 @@ function App() {
|
||||
};
|
||||
}, [queryClient, t]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
let unlistenResize: (() => void) | undefined;
|
||||
|
||||
const setupWindowStateSync = async () => {
|
||||
try {
|
||||
const currentWindow = getCurrentWindow();
|
||||
const syncWindowMaximizedState = async () => {
|
||||
const maximized = await currentWindow.isMaximized();
|
||||
if (active) {
|
||||
setIsWindowMaximized(maximized);
|
||||
}
|
||||
};
|
||||
|
||||
await syncWindowMaximizedState();
|
||||
unlistenResize = await currentWindow.onResized(() => {
|
||||
void syncWindowMaximizedState();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to sync window maximized state", error);
|
||||
}
|
||||
};
|
||||
|
||||
void setupWindowStateSync();
|
||||
return () => {
|
||||
active = false;
|
||||
unlistenResize?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// settingsData 未加载时跳过,避免用 fallback false 覆盖 Rust 侧已设好的装饰状态
|
||||
if (!settingsData) return;
|
||||
|
||||
const syncWindowDecorations = async () => {
|
||||
try {
|
||||
await getCurrentWindow().setDecorations(!useAppWindowControls);
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to update window decorations", error);
|
||||
}
|
||||
};
|
||||
|
||||
void syncWindowDecorations();
|
||||
}, [useAppWindowControls, settingsData]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkEnvOnStartup = async () => {
|
||||
try {
|
||||
@@ -734,6 +788,44 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const notifyWindowControlError = (error: unknown) => {
|
||||
toast.error(
|
||||
t("notifications.windowControlFailed", {
|
||||
defaultValue: "窗口控制失败:{{error}}",
|
||||
error: extractErrorMessage(error),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleWindowMinimize = async () => {
|
||||
try {
|
||||
await getCurrentWindow().minimize();
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to minimize window", error);
|
||||
notifyWindowControlError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWindowToggleMaximize = async () => {
|
||||
try {
|
||||
const currentWindow = getCurrentWindow();
|
||||
await currentWindow.toggleMaximize();
|
||||
setIsWindowMaximized(await currentWindow.isMaximized());
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to toggle maximize", error);
|
||||
notifyWindowControlError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWindowClose = async () => {
|
||||
try {
|
||||
await getCurrentWindow().close();
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to close window", error);
|
||||
notifyWindowControlError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
const content = (() => {
|
||||
switch (currentView) {
|
||||
@@ -880,14 +972,57 @@ function App() {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col h-screen overflow-hidden bg-background text-foreground selection:bg-primary/30"
|
||||
style={{ overflowX: "hidden", paddingTop: CONTENT_TOP_OFFSET }}
|
||||
style={{ overflowX: "hidden", paddingTop: contentTopOffset }}
|
||||
>
|
||||
{DRAG_BAR_HEIGHT > 0 && (
|
||||
{(dragBarHeight > 0 || useAppWindowControls) && (
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 z-[60]"
|
||||
className="fixed top-0 left-0 right-0 z-[70] flex items-center justify-end px-2"
|
||||
data-tauri-drag-region
|
||||
style={{ WebkitAppRegion: "drag", height: DRAG_BAR_HEIGHT } as any}
|
||||
/>
|
||||
style={{ WebkitAppRegion: "drag", height: dragBarHeight } as any}
|
||||
>
|
||||
{useAppWindowControls && (
|
||||
<div
|
||||
className="flex items-center gap-1"
|
||||
style={{ WebkitAppRegion: "no-drag" } as any}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => void handleWindowMinimize()}
|
||||
title={t("header.windowMinimize")}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => void handleWindowToggleMaximize()}
|
||||
title={
|
||||
isWindowMaximized
|
||||
? t("header.windowRestore")
|
||||
: t("header.windowMaximize")
|
||||
}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
{isWindowMaximized ? (
|
||||
<Minimize2 className="w-4 h-4" />
|
||||
) : (
|
||||
<Maximize2 className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => void handleWindowClose()}
|
||||
title={t("header.windowClose")}
|
||||
className="h-7 w-7 hover:bg-red-500/15 hover:text-red-500"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showEnvBanner && envConflicts.length > 0 && (
|
||||
<EnvWarningBanner
|
||||
@@ -920,7 +1055,7 @@ function App() {
|
||||
style={
|
||||
{
|
||||
...DRAG_REGION_STYLE,
|
||||
top: DRAG_BAR_HEIGHT,
|
||||
top: dragBarHeight,
|
||||
height: HEADER_HEIGHT,
|
||||
} as any
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user