mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Restore Windows custom titlebar controls so desktop builds can manage the window again
The Windows desktop shell was rendering custom minimize, maximize, and close buttons without the Tauri capabilities those commands require. The app was also starting with decorated windows and then trying to disable decorations at runtime, which is a weaker setup than the platform-specific config Tauri expects for custom chrome. This change grants the missing window permissions, moves Windows and macOS window chrome settings into platform-specific Tauri config files, removes the runtime decoration toggle, and adds a regression test that proves the custom controls invoke the Tauri window API. Constraint: Windows build validation will happen on the remote branch instead of this macOS workspace Rejected: Keep runtime set_decorations(false) on Windows | leaves window chrome behavior dependent on post-start native mutation Rejected: Ship only the capability fix | lower-risk hotfix, but it preserves the fragile cross-platform window config Confidence: medium Scope-risk: narrow Reversibility: clean Directive: Keep custom titlebar behavior in config and capabilities; avoid moving Windows decoration changes back into runtime setup Tested: cd desktop && bun run lint Tested: cd desktop && bun run test Not-tested: Native Windows click/drag behavior on an actual packaged build from this local machine Related: GitHub issue #62
This commit is contained in:
parent
f958bf7e0d
commit
e0b74cecae
@ -5,7 +5,10 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"shell:allow-open",
|
||||
{
|
||||
"identifier": "shell:allow-execute",
|
||||
|
||||
@ -394,12 +394,6 @@ pub fn run() {
|
||||
// 让 adapter 连上动态端口。
|
||||
spawn_and_track_adapters_sidecar(&app.handle());
|
||||
|
||||
let _window = app.get_webview_window("main").unwrap();
|
||||
|
||||
// Windows: hide native decorations — frontend renders custom titlebar
|
||||
#[cfg(target_os = "windows")]
|
||||
let _ = _window.set_decorations(false);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.build(tauri::generate_context!())
|
||||
|
||||
@ -18,8 +18,6 @@
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"decorations": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"transparent": false,
|
||||
"acceptFirstMouse": true
|
||||
}
|
||||
|
||||
18
desktop/src-tauri/tauri.macos.conf.json
Normal file
18
desktop/src-tauri/tauri.macos.conf.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Claude Code Haha",
|
||||
"width": 1440,
|
||||
"height": 960,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"decorations": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"transparent": false,
|
||||
"acceptFirstMouse": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
16
desktop/src-tauri/tauri.windows.conf.json
Normal file
16
desktop/src-tauri/tauri.windows.conf.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Claude Code Haha",
|
||||
"width": 1440,
|
||||
"height": 960,
|
||||
"minWidth": 960,
|
||||
"minHeight": 640,
|
||||
"decorations": false,
|
||||
"transparent": false,
|
||||
"acceptFirstMouse": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
69
desktop/src/components/layout/WindowControls.test.tsx
Normal file
69
desktop/src/components/layout/WindowControls.test.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
const minimize = vi.fn().mockResolvedValue(undefined)
|
||||
const toggleMaximize = vi.fn().mockResolvedValue(undefined)
|
||||
const close = vi.fn().mockResolvedValue(undefined)
|
||||
const isMaximized = vi.fn().mockResolvedValue(false)
|
||||
const onResized = vi.fn().mockResolvedValue(() => {})
|
||||
|
||||
vi.mock('@tauri-apps/api/window', () => ({
|
||||
getCurrentWindow: () => ({
|
||||
minimize,
|
||||
toggleMaximize,
|
||||
close,
|
||||
isMaximized,
|
||||
onResized,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('WindowControls', () => {
|
||||
const originalPlatform = navigator.platform
|
||||
|
||||
beforeEach(async () => {
|
||||
minimize.mockClear()
|
||||
toggleMaximize.mockClear()
|
||||
close.mockClear()
|
||||
isMaximized.mockClear()
|
||||
onResized.mockClear()
|
||||
|
||||
Object.defineProperty(window, '__TAURI_INTERNALS__', {
|
||||
configurable: true,
|
||||
value: {},
|
||||
})
|
||||
Object.defineProperty(navigator, 'platform', {
|
||||
configurable: true,
|
||||
value: 'Win32',
|
||||
})
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Reflect.deleteProperty(window, '__TAURI_INTERNALS__')
|
||||
Object.defineProperty(navigator, 'platform', {
|
||||
configurable: true,
|
||||
value: originalPlatform,
|
||||
})
|
||||
})
|
||||
|
||||
it('invokes Tauri window APIs for custom controls on Windows', async () => {
|
||||
const { WindowControls } = await import('./WindowControls')
|
||||
|
||||
render(<WindowControls />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'Minimize window' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Minimize window' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Maximize window' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close window' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(minimize).toHaveBeenCalledTimes(1)
|
||||
expect(toggleMaximize).toHaveBeenCalledTimes(1)
|
||||
expect(close).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -34,13 +34,20 @@ export function WindowControls() {
|
||||
return () => { unlisten?.() }
|
||||
}, [])
|
||||
|
||||
const runWindowAction = (action: () => Promise<void>) => {
|
||||
void action().catch((error) => {
|
||||
console.error('Window control action failed', error)
|
||||
})
|
||||
}
|
||||
|
||||
if (!showWindowControls || !win) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-stretch flex-shrink-0 -my-px">
|
||||
{/* Minimize */}
|
||||
<button
|
||||
onClick={() => win.minimize()}
|
||||
onClick={() => runWindowAction(() => win.minimize())}
|
||||
aria-label="Minimize window"
|
||||
className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors"
|
||||
>
|
||||
<svg width="10" height="1" viewBox="0 0 10 1">
|
||||
@ -50,7 +57,8 @@ export function WindowControls() {
|
||||
|
||||
{/* Maximize / Restore */}
|
||||
<button
|
||||
onClick={() => win.toggleMaximize()}
|
||||
onClick={() => runWindowAction(() => win.toggleMaximize())}
|
||||
aria-label={maximized ? 'Restore window' : 'Maximize window'}
|
||||
className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors"
|
||||
>
|
||||
{maximized ? (
|
||||
@ -67,7 +75,8 @@ export function WindowControls() {
|
||||
|
||||
{/* Close */}
|
||||
<button
|
||||
onClick={() => win.close()}
|
||||
onClick={() => runWindowAction(() => win.close())}
|
||||
aria-label="Close window"
|
||||
className="w-[46px] h-full flex items-center justify-center text-[var(--color-text-secondary)] hover:bg-[#e81123] hover:text-white transition-colors"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.2">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user