mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-11 18:10:04 +08:00
✨ feat(mac-window): 支持切换 macOS 原生窗口控制与原生全屏行为 (#288)
## 背景 当前 GoNavi 使用自定义无边框标题栏与右上角窗口按钮,在 macOS 下与系统原生窗口交互习惯存在明显差异: - 缺少左上角原生红黄绿窗口控制按钮 - 绿色按钮不具备 macOS 原生全屏 / Space 语义 - 标题栏交互和系统应用不够一致 这个 PR 为 macOS 增加了可切换的原生窗口控制模式,在尽量不影响现有跨平台行为的前提下,补齐 macOS 用户更熟悉的窗口体验。 ## 变更内容 - 在 `主题 -> 外观参数` 中新增 `使用 macOS 原生窗口控制` 开关 - 启用后: - 显示 macOS 左上角原生红黄绿按钮 - 隐藏现有右上角自定义窗口按钮 - 为标题栏内容预留原生按钮安全区域 - 优先使用 macOS 原生全屏行为 - 支持 `Control + Command + F` 切换全屏 - 修复原生全屏下按 `Esc` 会意外退出全屏的问题 - 补充窗口行为、边界条件和相关工具函数单元测试 ## 影响范围 - 仅影响 macOS 下启用该开关时的窗口样式与交互 - Windows/Linux 默认行为不变 - Windows 构建已验证通过 ## 验证结果 已完成以下验证: - [x] `npm run test` - [x] `npm run build` - [x] `go test ./...` - [x] macOS 手工验证通过 - [x] Windows 构建验证通过 ### macOS 手工验证项 - [x] 设置页可见 `使用 macOS 原生窗口控制` - [x] 开关关闭时,保留当前自定义标题栏与右上角按钮 - [x] 开关开启时,右上角自定义按钮隐藏 - [x] 开启后显示左上角原生红黄绿按钮 - [x] 绿色按钮进入原生全屏 - [x] 原生全屏进入独立 Space - [x] `Control + Command + F` 可切换全屏 - [x] 原生全屏下按 `Esc` 不再意外退出全屏 - [x] 浅色 / 深色主题下显示正常 - [x] 模糊与透明效果在普通窗口和全屏下均可正常工作 - [x] 最小化行为正常 ## 截图 / 演示 ### 历史窗口样式 - `MAC_历史版本窗口.png` <img width="1920" height="1080" alt="MAC_历史版本窗口" src="https://github.com/user-attachments/assets/4bd9176f-9d7e-43d1-9e1a-c7a6bfc0e28c" /> ### 设置项与菜单 - `MAC_菜单控制.png` <img width="1278" height="909" alt="MAC_菜单控制" src="https://github.com/user-attachments/assets/520da1b5-af59-4f1a-ba5d-36abdc03ef60" /> - `MAC_菜单控制_Dark.png` <img width="1119" height="861" alt="MAC_菜单控制_Dark" src="https://github.com/user-attachments/assets/b21af50e-b583-4895-b316-cc21b7498a51" /> - `MAC_恢复默认.png` <img width="1526" height="922" alt="MAC_恢复默认" src="https://github.com/user-attachments/assets/0129f69d-b2ca-45eb-847a-6b6cb37ca576" /> ### 原生窗口控制效果 - `MAC_窗口组件原生控制.png` <img width="1236" height="849" alt="MAC_窗口组件原生控制" src="https://github.com/user-attachments/assets/003dba09-d0a8-4999-8241-f2d1db078d1b" /> - `MAC_窗口组件原生控制2.png` <img width="1920" height="834" alt="MAC_窗口组件原生控制2" src="https://github.com/user-attachments/assets/241c94a6-955f-41f8-9b1d-b9a40d0a5251" /> - `MAC_切换后变化.png` <img width="1920" height="1080" alt="MAC_切换后变化" src="https://github.com/user-attachments/assets/52d8977b-9c64-4413-85d9-f94bdcdc0e53" /> ### 全屏、快捷键与 Space 行为 - `MAC_快捷键.png` <img width="1227" height="846" alt="MAC_快捷键" src="https://github.com/user-attachments/assets/2972cee3-3621-42f1-bd05-1e24eaded5ef" /> - `MAC_支持SPACE切换.png` <img width="353" height="251" alt="MAC_支持SPACE切换" src="https://github.com/user-attachments/assets/044974a6-64c4-4d0c-8ba9-3445af77f8e4" /> - `MAC_最大化.png` <img width="1920" height="1079" alt="MAC_最大化" src="https://github.com/user-attachments/assets/dbdf4cd4-0abd-4142-9c81-08c8c23af73b" /> ### 模糊与透明效果 - `MAC_模糊与透明.png` <img width="1267" height="954" alt="MAC_模糊与透明" src="https://github.com/user-attachments/assets/f5a3a377-805e-4d5f-a3f0-fa588d77d9f7" /> - `MAC_模糊与透明_全屏.png` <img width="1920" height="1080" alt="MAC_模糊与透明_全屏" src="https://github.com/user-attachments/assets/e20642ba-b828-47d0-a154-c23a7b15643d" /> ### 其他窗口行为 - `MAC_窗口最小化.png` <img width="276" height="129" alt="MAC_窗口最小化" src="https://github.com/user-attachments/assets/d7f758a0-072e-4c47-95e6-9539075f1d71" /> - `MAC_设置启动全屏-重新打开.png` <img width="1920" height="1080" alt="MAC_设置启动全屏-重新打开" src="https://github.com/user-attachments/assets/b033d102-5062-46cb-9c41-c6fe330df007" /> ### Windows 回归验证 - `WINDOWS_菜单.png` <img width="1920" height="1040" alt="WINDOWS_菜单" src="https://github.com/user-attachments/assets/3a295470-c1c6-42f5-a265-2cd38e9224cf" /> - `WINDOWS_全屏.png` <img width="1920" height="1040" alt="WINDOWS_全屏" src="https://github.com/user-attachments/assets/b254dc81-0c42-4024-9f91-3e029bfe29a0" /> ## 说明 - 本次实现优先保证 macOS 原生窗口交互一致性,而不是模拟系统按钮视觉 - 当前方案对非 macOS 平台保持兼容 - 如果窗口样式在切换当次未完全刷新,重启应用后可获得稳定表现
This commit is contained in:
@@ -68,6 +68,12 @@ func (a *App) SetWindowTranslucency(opacity float64, blur float64) {
|
||||
setMacWindowTranslucency(opacity, blur)
|
||||
}
|
||||
|
||||
// SetMacNativeWindowControls toggles macOS native traffic-light window controls.
|
||||
// On non-macOS platforms this is a no-op.
|
||||
func (a *App) SetMacNativeWindowControls(enabled bool) {
|
||||
setMacNativeWindowControls(enabled)
|
||||
}
|
||||
|
||||
// Shutdown is called when the app terminates
|
||||
func (a *App) Shutdown(ctx context.Context) {
|
||||
logger.Infof("应用开始关闭,准备释放资源")
|
||||
|
||||
70
internal/app/window_style_darwin.go
Normal file
70
internal/app/window_style_darwin.go
Normal file
@@ -0,0 +1,70 @@
|
||||
//go:build darwin
|
||||
|
||||
package app
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c -fblocks
|
||||
#cgo LDFLAGS: -framework Cocoa
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
|
||||
static void gonaviSetWindowButtonsVisible(NSWindow *window, BOOL visible) {
|
||||
if (window == nil) {
|
||||
return;
|
||||
}
|
||||
for (NSWindowButton buttonType = NSWindowCloseButton; buttonType <= NSWindowZoomButton; buttonType++) {
|
||||
NSButton *button = [window standardWindowButton:buttonType];
|
||||
if (button != nil) {
|
||||
[button setHidden:!visible];
|
||||
[button setEnabled:visible];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void gonaviApplyMacWindowStyle(BOOL enabled) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
for (NSWindow *window in [NSApp windows]) {
|
||||
if (window == nil) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSUInteger styleMask = [window styleMask];
|
||||
styleMask |= NSWindowStyleMaskClosable;
|
||||
styleMask |= NSWindowStyleMaskMiniaturizable;
|
||||
styleMask |= NSWindowStyleMaskResizable;
|
||||
|
||||
if (enabled) {
|
||||
styleMask |= NSWindowStyleMaskTitled;
|
||||
styleMask |= NSWindowStyleMaskFullSizeContentView;
|
||||
[window setStyleMask:styleMask];
|
||||
[window setTitleVisibility:NSWindowTitleHidden];
|
||||
[window setTitlebarAppearsTransparent:YES];
|
||||
[window setMovableByWindowBackground:YES];
|
||||
[window setCollectionBehavior:[window collectionBehavior] | NSWindowCollectionBehaviorFullScreenPrimary];
|
||||
gonaviSetWindowButtonsVisible(window, YES);
|
||||
} else {
|
||||
styleMask &= ~NSWindowStyleMaskTitled;
|
||||
styleMask &= ~NSWindowStyleMaskFullSizeContentView;
|
||||
[window setStyleMask:styleMask];
|
||||
[window setTitleVisibility:NSWindowTitleVisible];
|
||||
[window setTitlebarAppearsTransparent:NO];
|
||||
[window setMovableByWindowBackground:YES];
|
||||
gonaviSetWindowButtonsVisible(window, NO);
|
||||
}
|
||||
|
||||
[[window contentView] setNeedsDisplay:YES];
|
||||
[window invalidateShadow];
|
||||
}
|
||||
});
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func setMacNativeWindowControls(enabled bool) {
|
||||
state := resolveMacNativeWindowControlState(enabled)
|
||||
flag := C.BOOL(false)
|
||||
if state.ShowNativeButtons {
|
||||
flag = C.BOOL(true)
|
||||
}
|
||||
C.gonaviApplyMacWindowStyle(flag)
|
||||
}
|
||||
32
internal/app/window_style_logic.go
Normal file
32
internal/app/window_style_logic.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package app
|
||||
|
||||
type macNativeWindowControlState struct {
|
||||
ShowNativeButtons bool
|
||||
UseTitledWindow bool
|
||||
UseFullSizeContent bool
|
||||
HideWindowTitle bool
|
||||
TransparentTitlebar bool
|
||||
AllowNativeFullscreen bool
|
||||
}
|
||||
|
||||
func resolveMacNativeWindowControlState(enabled bool) macNativeWindowControlState {
|
||||
if enabled {
|
||||
return macNativeWindowControlState{
|
||||
ShowNativeButtons: true,
|
||||
UseTitledWindow: true,
|
||||
UseFullSizeContent: true,
|
||||
HideWindowTitle: true,
|
||||
TransparentTitlebar: true,
|
||||
AllowNativeFullscreen: true,
|
||||
}
|
||||
}
|
||||
|
||||
return macNativeWindowControlState{
|
||||
ShowNativeButtons: false,
|
||||
UseTitledWindow: false,
|
||||
UseFullSizeContent: false,
|
||||
HideWindowTitle: false,
|
||||
TransparentTitlebar: false,
|
||||
AllowNativeFullscreen: false,
|
||||
}
|
||||
}
|
||||
37
internal/app/window_style_logic_test.go
Normal file
37
internal/app/window_style_logic_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package app
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveMacNativeWindowControlStateEnabled(t *testing.T) {
|
||||
state := resolveMacNativeWindowControlState(true)
|
||||
|
||||
if !state.ShowNativeButtons {
|
||||
t.Fatal("expected native buttons to be visible when enabled")
|
||||
}
|
||||
if !state.UseTitledWindow || !state.UseFullSizeContent {
|
||||
t.Fatal("expected enabled state to request titled full-size content window")
|
||||
}
|
||||
if !state.HideWindowTitle || !state.TransparentTitlebar {
|
||||
t.Fatal("expected enabled state to hide title and use transparent titlebar")
|
||||
}
|
||||
if !state.AllowNativeFullscreen {
|
||||
t.Fatal("expected enabled state to allow native fullscreen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveMacNativeWindowControlStateDisabled(t *testing.T) {
|
||||
state := resolveMacNativeWindowControlState(false)
|
||||
|
||||
if state.ShowNativeButtons {
|
||||
t.Fatal("expected native buttons to be hidden when disabled")
|
||||
}
|
||||
if state.UseTitledWindow || state.UseFullSizeContent {
|
||||
t.Fatal("expected disabled state to avoid titled/full-size content window")
|
||||
}
|
||||
if state.HideWindowTitle || state.TransparentTitlebar {
|
||||
t.Fatal("expected disabled state to keep title visibility and opaque titlebar")
|
||||
}
|
||||
if state.AllowNativeFullscreen {
|
||||
t.Fatal("expected disabled state to avoid native fullscreen behavior")
|
||||
}
|
||||
}
|
||||
5
internal/app/window_style_stub.go
Normal file
5
internal/app/window_style_stub.go
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build !darwin
|
||||
|
||||
package app
|
||||
|
||||
func setMacNativeWindowControls(enabled bool) {}
|
||||
Reference in New Issue
Block a user