mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(electron): align Windows title bar hover
Use native Window Controls Overlay geometry and sampled background colors so caption hover and drag regions track the system title bar. Forward original Windows close-handler errors to the shared logger so failures retain their stack traces.
This commit is contained in:
1 parent
3db40375b6
commit
d734389f29
6 files changed
+77
-20
No files matched your search
@@ -25,9 +25,9 @@ export const logger = {
|
||||
ensureInit()
|
||||
appLogger.warn('app', message)
|
||||
},
|
||||
error: (message: string) => {
|
||||
error: (message: string, data?: unknown) => {
|
||||
ensureInit()
|
||||
appLogger.error('app', message)
|
||||
appLogger.error('app', message, data)
|
||||
},
|
||||
debug: (message: string) => {
|
||||
ensureInit()
|
||||
|
||||
@@ -93,7 +93,7 @@ function handleWindowsClose(win: BrowserWindow): void {
|
||||
},
|
||||
quit: requestAppQuit,
|
||||
onError: (error) => {
|
||||
logger.error(`Failed to handle Windows close request: ${error instanceof Error ? error.message : String(error)}`)
|
||||
logger.error('Failed to handle Windows close request', error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import type { BrowserWindow, TitleBarOverlayOptions } from 'electron'
|
||||
|
||||
const TITLE_BAR_OVERLAY_HEIGHT = 32
|
||||
const TITLE_BAR_OVERLAY_COLOR = 'rgba(0, 0, 0, 0)'
|
||||
let currentTitleBarOverlayColor: string | null = null
|
||||
|
||||
const TITLE_BAR_OVERLAY_PALETTE = {
|
||||
light: { symbolColor: '#52525b' },
|
||||
dark: { symbolColor: '#d4d4d8' },
|
||||
light: { backgroundColor: '#f9fafb', symbolColor: '#52525b' },
|
||||
dark: { backgroundColor: '#111827', symbolColor: '#d4d4d8' },
|
||||
} as const
|
||||
|
||||
export function getTitleBarOverlayOptions(isDark: boolean): TitleBarOverlayOptions {
|
||||
const mode = isDark ? 'dark' : 'light'
|
||||
return {
|
||||
color: TITLE_BAR_OVERLAY_COLOR,
|
||||
color: getTransparentOverlayColor(TITLE_BAR_OVERLAY_PALETTE[mode].backgroundColor),
|
||||
symbolColor: TITLE_BAR_OVERLAY_PALETTE[mode].symbolColor,
|
||||
height: TITLE_BAR_OVERLAY_HEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
export function getTitleBarOverlayOptionsForColor(color: string): TitleBarOverlayOptions {
|
||||
return {
|
||||
color: TITLE_BAR_OVERLAY_COLOR,
|
||||
// Electron derives the minimize/maximize hover color from the overlay RGB.
|
||||
// Preserve the sampled background channels while keeping the overlay transparent.
|
||||
color: getTransparentOverlayColor(color),
|
||||
symbolColor: getReadableSymbolColor(color),
|
||||
height: TITLE_BAR_OVERLAY_HEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +42,10 @@ export function resetCurrentTitleBarOverlayColor(): void {
|
||||
currentTitleBarOverlayColor = null
|
||||
}
|
||||
|
||||
function getTransparentOverlayColor(color: string): string {
|
||||
return /^#[0-9a-f]{6}$/i.test(color) ? `${color}00` : 'rgba(0, 0, 0, 0)'
|
||||
}
|
||||
|
||||
function getReadableSymbolColor(hexColor: string): string {
|
||||
const match = /^#([0-9a-f]{6})$/i.exec(hexColor)
|
||||
if (!match) return '#52525b'
|
||||
|
||||
@@ -69,14 +69,18 @@
|
||||
|
||||
/* 标题栏安全区域高度 - 页面顶部 padding 需要避开此区域
|
||||
* macOS: 红绿灯在左侧由 Sidebar 处理,右侧内容区无遮挡,使用较小值
|
||||
* Windows/Linux: 窗口控制按钮在右上角占据 32px 高度,需要完整避开 */
|
||||
* Windows: 跟随 Window Controls Overlay 提供的系统高度
|
||||
* Linux: 自定义窗口控制按钮固定占据 32px 高度 */
|
||||
:root {
|
||||
--titlebar-area-height: 1rem; /* 16px - macOS */
|
||||
}
|
||||
|
||||
.platform-windows,
|
||||
.platform-windows {
|
||||
--titlebar-area-height: env(titlebar-area-height, 2rem);
|
||||
}
|
||||
|
||||
.platform-linux {
|
||||
--titlebar-area-height: 2rem; /* 32px - 匹配 titleBarOverlay height */
|
||||
--titlebar-area-height: 2rem;
|
||||
}
|
||||
|
||||
/* 禁用窗口拖拽区域 - 用于弹窗等需要正常交互的元素 */
|
||||
|
||||
@@ -33,7 +33,7 @@ function close() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="title-bar">
|
||||
<div class="title-bar" :class="{ 'title-bar-windows': isWindows }">
|
||||
<!-- 左侧区域 - macOS 给红绿灯预留空间 -->
|
||||
<div v-if="isMac" class="traffic-light-spacer" />
|
||||
|
||||
@@ -100,6 +100,19 @@ function close() {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Window Controls Overlay exposes the system caption geometry through CSS env values. */
|
||||
.title-bar-windows {
|
||||
height: env(titlebar-area-height, 32px);
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.title-bar-windows .drag-region {
|
||||
width: env(titlebar-area-width, 100%);
|
||||
margin-left: env(titlebar-area-x, 0px);
|
||||
flex: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
/* Windows 窗口控制按钮容器 */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
|
||||
@@ -8,8 +8,11 @@ interface RgbaColor {
|
||||
a: number
|
||||
}
|
||||
|
||||
const TITLE_BAR_SAMPLE_Y = 16
|
||||
const TITLE_BAR_SAMPLE_RIGHT_OFFSET = 80
|
||||
interface WindowControlsOverlayLike extends EventTarget {
|
||||
visible: boolean
|
||||
getTitlebarAreaRect(): DOMRect
|
||||
}
|
||||
|
||||
const TRANSITION_SAMPLE_MS = 420
|
||||
|
||||
export function useWindowsTitleBarOverlay(triggers: WatchSource<unknown>[]): void {
|
||||
@@ -17,6 +20,7 @@ export function useWindowsTitleBarOverlay(triggers: WatchSource<unknown>[]): voi
|
||||
|
||||
let frameId = 0
|
||||
let lastColor = ''
|
||||
const windowControlsOverlay = getWindowControlsOverlay()
|
||||
|
||||
const syncTitleBarColor = () => {
|
||||
const color = sampleTitleBarBackground()
|
||||
@@ -50,11 +54,13 @@ export function useWindowsTitleBarOverlay(triggers: WatchSource<unknown>[]): voi
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
windowControlsOverlay?.addEventListener('geometrychange', handleResize)
|
||||
scheduleSync(TRANSITION_SAMPLE_MS)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
windowControlsOverlay?.removeEventListener('geometrychange', handleResize)
|
||||
if (frameId) {
|
||||
cancelAnimationFrame(frameId)
|
||||
}
|
||||
@@ -65,11 +71,13 @@ export function useWindowsTitleBarOverlay(triggers: WatchSource<unknown>[]): voi
|
||||
}
|
||||
}
|
||||
|
||||
// 采样 Windows 标题栏按钮区域下方的真实背景色,并用它推导原生按钮符号颜色。
|
||||
// 采样 Windows 标题栏按钮区域下方的真实背景色,并用它推导原生按钮符号和 hover 颜色。
|
||||
// 这里需要沿 elementsFromPoint 返回的层级合成透明背景,才能覆盖设置弹窗淡入淡出时的 blur/backdrop 场景。
|
||||
function sampleTitleBarBackground(): string | null {
|
||||
const x = Math.max(0, window.innerWidth - TITLE_BAR_SAMPLE_RIGHT_OFFSET)
|
||||
const y = TITLE_BAR_SAMPLE_Y
|
||||
const samplePoint = getTitleBarSamplePoint()
|
||||
if (!samplePoint) return null
|
||||
|
||||
const { x, y } = samplePoint
|
||||
const elements = document.elementsFromPoint(x, y)
|
||||
if (elements.length === 0) return null
|
||||
|
||||
@@ -87,6 +95,36 @@ function sampleTitleBarBackground(): string | null {
|
||||
return rgbaToHex(color)
|
||||
}
|
||||
|
||||
function getWindowControlsOverlay(): WindowControlsOverlayLike | undefined {
|
||||
return (navigator as Navigator & { windowControlsOverlay?: WindowControlsOverlayLike }).windowControlsOverlay
|
||||
}
|
||||
|
||||
function getTitleBarSamplePoint(): { x: number; y: number } | null {
|
||||
const overlay = getWindowControlsOverlay()
|
||||
if (!overlay?.visible) return null
|
||||
|
||||
const titleBarArea = overlay.getTitlebarAreaRect()
|
||||
if (titleBarArea.width <= 0 || titleBarArea.height <= 0 || window.innerWidth <= 0) return null
|
||||
|
||||
const leftControlsWidth = Math.max(0, titleBarArea.x)
|
||||
const rightControlsStart = titleBarArea.x + titleBarArea.width
|
||||
const rightControlsWidth = Math.max(0, window.innerWidth - rightControlsStart)
|
||||
|
||||
let x: number
|
||||
if (rightControlsWidth > 0) {
|
||||
x = rightControlsStart + rightControlsWidth / 2
|
||||
} else if (leftControlsWidth > 0) {
|
||||
x = leftControlsWidth / 2
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
x: Math.min(window.innerWidth - 1, Math.max(0, x)),
|
||||
y: Math.max(0, titleBarArea.y + titleBarArea.height / 2),
|
||||
}
|
||||
}
|
||||
|
||||
function getPageFallbackColor(): RgbaColor {
|
||||
const rootColor = parseCssColor(window.getComputedStyle(document.documentElement).backgroundColor)
|
||||
if (rootColor && rootColor.a > 0) return rootColor
|
||||
|
||||
Reference in new issue
Block a user