Files
CC-Switch/src/components/common/FullScreenPanel.tsx
T
Jason 64c068415e fix(linux): repair unresponsive UI on startup and full-screen panels
Linux users reported the window UI (including native title bar buttons)
couldn't receive clicks until manually maximizing and restoring the
window. Root causes: (1) Tauri webview did not acquire focus on startup
so first clicks were consumed by X11/Wayland click-to-activate
(Tauri #10746, wry #637); (2) GTK surface input region failed to
renegotiate on the visible:false + show() path under some
WebKitGTK/compositor combinations.

- Add linux_fix::nudge_main_window helper that performs set_focus plus
  a ±1px no-op resize after window show, with a 500ms reconciliation
  readback to compensate for dropped resize requests on slow
  compositors.
- Wire the helper into every window re-show path: normal startup,
  deeplink, single_instance, tray show_main, and lightweight exit.
- Set WEBKIT_DISABLE_COMPOSITING_MODE=1 at startup to avoid resize
  crashes and Wayland surface negotiation issues.
- Remove data-tauri-drag-region on Linux from App.tsx header and the
  shared FullScreenPanel (used by all provider/MCP/workspace forms)
  to avoid Tauri #13440 in Wayland sessions. Extract drag-region
  constants to src/lib/platform.ts for reuse.

All Rust changes are gated by #[cfg(target_os = "linux")]; frontend
changes preserve macOS/Windows behavior via runtime isLinux() checks.
Known limitation: tiling Wayland compositors ignore set_size, so
GDK_BACKEND=x11 remains the user-side workaround.
2026-04-09 16:49:13 +08:00

159 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from "react";
import { createPortal } from "react-dom";
import { motion, AnimatePresence } from "framer-motion";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
isWindows,
isLinux,
DRAG_REGION_ATTR,
DRAG_REGION_STYLE,
} from "@/lib/platform";
import { isTextEditableTarget } from "@/utils/domUtils";
interface FullScreenPanelProps {
isOpen: boolean;
title: string;
onClose: () => void;
children: React.ReactNode;
footer?: React.ReactNode;
}
const DRAG_BAR_HEIGHT = isWindows() || isLinux() ? 0 : 28; // px - match App.tsx
const HEADER_HEIGHT = 64; // px - match App.tsx
/**
* Reusable full-screen panel component
* Handles portal rendering, header with back button, and footer
* Uses solid theme colors without transparency
*/
export const FullScreenPanel: React.FC<FullScreenPanelProps> = ({
isOpen,
title,
onClose,
children,
footer,
}) => {
React.useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
// ESC 键关闭面板
const onCloseRef = React.useRef(onClose);
React.useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
React.useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
// 子组件(例如 Radix 的 Select/Dialog/Dropdown)如果已经消费了 ESC,就不要再关闭整个面板
if (event.defaultPrevented) {
return;
}
if (isTextEditableTarget(event.target)) {
return; // 让输入框自己处理 ESC(比如清空、失焦等)
}
event.stopPropagation(); // 阻止事件继续冒泡到 window,避免触发 App.tsx 的全局监听
onCloseRef.current();
}
};
// 使用冒泡阶段监听,让子组件(如 Radix UI)优先处理 ESC
window.addEventListener("keydown", handleKeyDown, false);
return () => {
window.removeEventListener("keydown", handleKeyDown, false);
};
}, [isOpen]);
return createPortal(
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-[60] flex flex-col"
style={{ backgroundColor: "hsl(var(--background))" }}
>
{/* Drag region - match App.tsx. Linux 上 DRAG_BAR_HEIGHT=0
直接跳过整个元素;macOS 保留 28px 拖拽占位。 */}
{DRAG_BAR_HEIGHT > 0 && (
<div
data-tauri-drag-region
style={
{
WebkitAppRegion: "drag",
height: DRAG_BAR_HEIGHT,
} as React.CSSProperties
}
/>
)}
{/* Header - match App.tsx */}
<div
className="flex-shrink-0 flex items-center"
{...DRAG_REGION_ATTR}
style={
{
...DRAG_REGION_STYLE,
backgroundColor: "hsl(var(--background))",
height: HEADER_HEIGHT,
} as React.CSSProperties
}
>
<div
className="px-6 w-full flex items-center gap-4"
{...DRAG_REGION_ATTR}
style={{ ...DRAG_REGION_STYLE } as React.CSSProperties}
>
<Button
type="button"
variant="outline"
size="icon"
onClick={onClose}
className="rounded-lg select-none"
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
>
<ArrowLeft className="h-4 w-4" />
</Button>
<h2 className="text-lg font-semibold text-foreground select-none">
{title}
</h2>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto scroll-overlay">
<div className="px-6 py-6 space-y-6 w-full">{children}</div>
</div>
{/* Footer */}
{footer && (
<div
className="flex-shrink-0 py-4 border-t border-border-default"
style={{ backgroundColor: "hsl(var(--background))" }}
>
<div className="px-6 flex items-center justify-end gap-3">
{footer}
</div>
</div>
)}
</motion.div>
)}
</AnimatePresence>,
document.body,
);
};