506 lines
15 KiB
Dart
506 lines
15 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 桌面端自定义窗口标题栏
|
||
/// 创建时间: 2026-06-18
|
||
/// 更新时间: 2026-06-22
|
||
/// 作用: 软件样式标题栏,替代系统默认标题栏,支持动态主题+动态样式
|
||
/// 上次更新: "口"按钮改为弹出 3×2 窗口大小预设网格菜单
|
||
/// ============================================================
|
||
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:window_manager/window_manager.dart';
|
||
|
||
import '../../core/theme/app_theme.dart';
|
||
import '../../core/utils/logger.dart';
|
||
import '../../core/utils/platform/platform_utils.dart' as pu;
|
||
import '../../features/settings/providers/theme_settings_provider.dart';
|
||
import '../../shared/widgets/window_size_popup.dart';
|
||
|
||
/// 标题栏样式配置(动态样式)
|
||
class DesktopTitleBarStyle {
|
||
/// 标题栏高度
|
||
final double height;
|
||
|
||
/// 背景透明度(0.0-1.0,用于毛玻璃效果)
|
||
final double backgroundOpacity;
|
||
|
||
/// 模糊强度(毛玻璃效果)
|
||
final double blurRadius;
|
||
|
||
/// 按钮大小
|
||
final double buttonSize;
|
||
|
||
/// 按钮间距
|
||
final double buttonSpacing;
|
||
|
||
/// 标题字体大小
|
||
final double titleFontSize;
|
||
|
||
/// 标题字重
|
||
final FontWeight titleFontWeight;
|
||
|
||
/// 圆角半径(Windows 风格按钮)
|
||
final double buttonRadius;
|
||
|
||
const DesktopTitleBarStyle({
|
||
this.height = 32.0,
|
||
this.backgroundOpacity = 0.6,
|
||
this.blurRadius = 20.0,
|
||
this.buttonSize = 14.0,
|
||
this.buttonSpacing = 8.0,
|
||
this.titleFontSize = 13.0,
|
||
this.titleFontWeight = FontWeight.w500,
|
||
this.buttonRadius = 6.0,
|
||
});
|
||
|
||
/// macOS 风格默认样式
|
||
const DesktopTitleBarStyle.macOS()
|
||
: height = 32.0,
|
||
backgroundOpacity = 0.5,
|
||
blurRadius = 30.0,
|
||
buttonSize = 12.0,
|
||
buttonSpacing = 8.0,
|
||
titleFontSize = 13.0,
|
||
titleFontWeight = FontWeight.w500,
|
||
buttonRadius = 6.0;
|
||
|
||
/// Windows 风格默认样式
|
||
const DesktopTitleBarStyle.windows()
|
||
: height = 36.0,
|
||
backgroundOpacity = 0.7,
|
||
blurRadius = 20.0,
|
||
buttonSize = 14.0,
|
||
buttonSpacing = 0.0,
|
||
titleFontSize = 12.0,
|
||
titleFontWeight = FontWeight.w400,
|
||
buttonRadius = 4.0;
|
||
}
|
||
|
||
/// 桌面端自定义窗口标题栏
|
||
///
|
||
/// 软件样式标题栏,替代系统默认标题栏:
|
||
/// - macOS 风格:左侧红黄绿三圆点按钮 + 中间标题
|
||
/// - Windows 风格:左侧标题 + 右侧最小化/最大化/关闭方按钮
|
||
///
|
||
/// 支持动态主题(深色/浅色/AMOLED)和动态样式(高度/透明度/模糊等)。
|
||
/// 使用毛玻璃效果增强层次感。
|
||
class DesktopWindowTitleBar extends ConsumerStatefulWidget {
|
||
const DesktopWindowTitleBar({
|
||
super.key,
|
||
this.title,
|
||
this.style,
|
||
this.leading,
|
||
this.actions,
|
||
this.showTitle = true,
|
||
});
|
||
|
||
/// 标题文本(null 时显示应用名称"闲言")
|
||
final String? title;
|
||
|
||
/// 自定义样式(null 时根据平台自动选择)
|
||
final DesktopTitleBarStyle? style;
|
||
|
||
/// 标题栏左侧自定义 Widget(在按钮之后)
|
||
final Widget? leading;
|
||
|
||
/// 标题栏右侧自定义 Widget(在按钮之前)
|
||
final List<Widget>? actions;
|
||
|
||
/// 是否显示标题
|
||
final bool showTitle;
|
||
|
||
@override
|
||
ConsumerState<DesktopWindowTitleBar> createState() =>
|
||
_DesktopWindowTitleBarState();
|
||
}
|
||
|
||
class _DesktopWindowTitleBarState extends ConsumerState<DesktopWindowTitleBar>
|
||
with WindowListener {
|
||
bool _isMaximized = false;
|
||
bool _isHoveringClose = false;
|
||
bool _isHoveringMinimize = false;
|
||
bool _isHoveringMaximize = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
windowManager.addListener(this);
|
||
_refreshMaximizedState();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
windowManager.removeListener(this);
|
||
super.dispose();
|
||
}
|
||
|
||
/// 刷新窗口最大化状态
|
||
Future<void> _refreshMaximizedState() async {
|
||
if (!mounted) return;
|
||
try {
|
||
final maximized = await windowManager.isMaximized();
|
||
if (mounted && _isMaximized != maximized) {
|
||
setState(() => _isMaximized = maximized);
|
||
}
|
||
} catch (e) {
|
||
Log.w('DesktopWindowTitleBar._refreshMaximizedState 失败: $e');
|
||
}
|
||
}
|
||
|
||
@override
|
||
void onWindowMaximize() {
|
||
if (mounted) setState(() => _isMaximized = true);
|
||
}
|
||
|
||
@override
|
||
void onWindowUnmaximize() {
|
||
if (mounted) setState(() => _isMaximized = false);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = ref.watch(themeSettingsProvider);
|
||
final ext = AppTheme.ext(context);
|
||
final isDark = theme.isDark;
|
||
final isAmoled = theme.isAmoled;
|
||
|
||
// 根据平台选择默认样式
|
||
final style =
|
||
widget.style ??
|
||
(pu.isMacOS
|
||
? const DesktopTitleBarStyle.macOS()
|
||
: const DesktopTitleBarStyle.windows());
|
||
|
||
// 背景颜色:根据主题动态变化
|
||
final backgroundColor = _resolveBackgroundColor(isDark, isAmoled, ext);
|
||
|
||
// 前景颜色
|
||
final foregroundColor = isDark
|
||
? const Color(0xFFE5E5E7)
|
||
: const Color(0xFF1D1D1F);
|
||
|
||
// Windows: C++ HTTRANSPARENT 方案处理拖拽
|
||
// 子类化 Flutter 子窗口,WM_NCHITTEST 标题栏区域返回 HTTRANSPARENT,
|
||
// Windows 转发到父窗口,window_manager_plugin.cpp 返回 HTCAPTION → 原生拖拽。
|
||
// 无需 Dart 侧 startDragging(MethodChannel 延迟导致卡顿)。
|
||
if (pu.isWindows) {
|
||
return Container(
|
||
height: style.height,
|
||
color: backgroundColor.withValues(alpha: style.backgroundOpacity),
|
||
child: _buildWindowsLayout(style, foregroundColor),
|
||
);
|
||
}
|
||
|
||
// macOS: 使用 GestureDetector 处理拖拽和双击
|
||
return GestureDetector(
|
||
behavior: HitTestBehavior.translucent,
|
||
onPanDown: (_) {
|
||
windowManager.startDragging();
|
||
},
|
||
onDoubleTap: _toggleMaximize,
|
||
child: Container(
|
||
height: style.height,
|
||
color: backgroundColor.withValues(alpha: style.backgroundOpacity),
|
||
child: _buildMacOSLayout(style, foregroundColor),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 解析背景颜色(动态主题)
|
||
Color _resolveBackgroundColor(
|
||
bool isDark,
|
||
bool isAmoled,
|
||
AppThemeExtension ext,
|
||
) {
|
||
if (isAmoled) {
|
||
return const Color(0xFF000000);
|
||
}
|
||
if (isDark) {
|
||
return const Color(0xFF1C1C1E);
|
||
}
|
||
return const Color(0xFFF5F5F7);
|
||
}
|
||
|
||
// ============================================================
|
||
// macOS 风格布局:左侧红黄绿按钮 + 中间标题
|
||
// ============================================================
|
||
|
||
Widget _buildMacOSLayout(DesktopTitleBarStyle style, Color foregroundColor) {
|
||
return Row(
|
||
children: [
|
||
// 左侧:红黄绿按钮
|
||
SizedBox(
|
||
width: 72,
|
||
child: Row(
|
||
children: [
|
||
const SizedBox(width: 12),
|
||
_MacOSTrafficButton(
|
||
color: const Color(0xFFFF5F57),
|
||
icon: CupertinoIcons.xmark,
|
||
isHovering: _isHoveringClose,
|
||
onHover: (v) => setState(() => _isHoveringClose = v),
|
||
onTap: () => windowManager.close(),
|
||
size: style.buttonSize,
|
||
),
|
||
SizedBox(width: style.buttonSpacing),
|
||
_MacOSTrafficButton(
|
||
color: const Color(0xFFFEBC2E),
|
||
icon: CupertinoIcons.minus,
|
||
isHovering: _isHoveringMinimize,
|
||
onHover: (v) => setState(() => _isHoveringMinimize = v),
|
||
onTap: () => windowManager.minimize(),
|
||
size: style.buttonSize,
|
||
),
|
||
SizedBox(width: style.buttonSpacing),
|
||
_MacOSTrafficButton(
|
||
color: const Color(0xFF28C840),
|
||
icon: _isMaximized ? Icons.fullscreen_exit : Icons.fullscreen,
|
||
isHovering: _isHoveringMaximize,
|
||
onHover: (v) => setState(() => _isHoveringMaximize = v),
|
||
onTap: _toggleMaximize,
|
||
size: style.buttonSize,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
// 中间:标题 + 自定义 leading
|
||
Expanded(
|
||
child: Row(
|
||
children: [
|
||
if (widget.leading != null) widget.leading!,
|
||
if (widget.showTitle)
|
||
Expanded(
|
||
child: Center(
|
||
child: Text(
|
||
widget.title ?? '闲言',
|
||
style: TextStyle(
|
||
fontSize: style.titleFontSize,
|
||
fontWeight: style.titleFontWeight,
|
||
color: foregroundColor,
|
||
decoration: TextDecoration.none,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
if (widget.actions != null) ...widget.actions!,
|
||
],
|
||
),
|
||
),
|
||
// 右侧占位(保持对称)
|
||
const SizedBox(width: 72),
|
||
],
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// Windows 风格布局:左侧标题 + 右侧最小化/最大化/关闭
|
||
// ============================================================
|
||
|
||
Widget _buildWindowsLayout(
|
||
DesktopTitleBarStyle style,
|
||
Color foregroundColor,
|
||
) {
|
||
return Row(
|
||
children: [
|
||
// 左侧:标题 + 自定义 leading
|
||
Expanded(
|
||
child: Row(
|
||
children: [
|
||
if (widget.leading != null) widget.leading!,
|
||
if (widget.showTitle)
|
||
Expanded(
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(left: 12),
|
||
child: Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Text(
|
||
widget.title ?? '闲言',
|
||
style: TextStyle(
|
||
fontSize: style.titleFontSize,
|
||
fontWeight: style.titleFontWeight,
|
||
color: foregroundColor,
|
||
decoration: TextDecoration.none,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
if (widget.actions != null) ...widget.actions!,
|
||
],
|
||
),
|
||
),
|
||
// 右侧:最小化/最大化/关闭按钮
|
||
_WindowsControlButton(
|
||
icon: CupertinoIcons.minus,
|
||
iconSize: 16,
|
||
isHovering: _isHoveringMinimize,
|
||
onHover: (v) => setState(() => _isHoveringMinimize = v),
|
||
onTap: (_) => windowManager.minimize(),
|
||
foregroundColor: foregroundColor,
|
||
width: 46,
|
||
height: style.height,
|
||
),
|
||
_WindowsControlButton(
|
||
icon: _isMaximized ? Icons.fullscreen_exit : Icons.fullscreen,
|
||
iconSize: 14,
|
||
isHovering: _isHoveringMaximize,
|
||
onHover: (v) => setState(() => _isHoveringMaximize = v),
|
||
onTap: (btnCtx) => WindowSizePopup.show(btnCtx),
|
||
foregroundColor: foregroundColor,
|
||
width: 46,
|
||
height: style.height,
|
||
),
|
||
_WindowsControlButton(
|
||
icon: CupertinoIcons.xmark,
|
||
iconSize: 16,
|
||
isHovering: _isHoveringClose,
|
||
onHover: (v) => setState(() => _isHoveringClose = v),
|
||
onTap: (_) => windowManager.close(),
|
||
foregroundColor: foregroundColor,
|
||
width: 46,
|
||
height: style.height,
|
||
isClose: true,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// 切换最大化/还原
|
||
Future<void> _toggleMaximize() async {
|
||
try {
|
||
if (_isMaximized) {
|
||
await windowManager.unmaximize();
|
||
} else {
|
||
await windowManager.maximize();
|
||
}
|
||
} catch (e) {
|
||
Log.e('DesktopWindowTitleBar._toggleMaximize 失败: $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// macOS 风格红黄绿按钮
|
||
// ============================================================
|
||
|
||
class _MacOSTrafficButton extends StatefulWidget {
|
||
const _MacOSTrafficButton({
|
||
required this.color,
|
||
required this.icon,
|
||
required this.isHovering,
|
||
required this.onHover,
|
||
required this.onTap,
|
||
required this.size,
|
||
});
|
||
|
||
final Color color;
|
||
final IconData icon;
|
||
final bool isHovering;
|
||
final ValueChanged<bool> onHover;
|
||
final VoidCallback onTap;
|
||
final double size;
|
||
|
||
@override
|
||
State<_MacOSTrafficButton> createState() => _MacOSTrafficButtonState();
|
||
}
|
||
|
||
class _MacOSTrafficButtonState extends State<_MacOSTrafficButton> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// macOS 风格:圆形按钮,hover 时显示图标
|
||
final buttonDiameter = widget.size + 2;
|
||
|
||
return MouseRegion(
|
||
onEnter: (_) => widget.onHover(true),
|
||
onExit: (_) => widget.onHover(false),
|
||
child: GestureDetector(
|
||
onTap: widget.onTap,
|
||
child: Container(
|
||
width: buttonDiameter,
|
||
height: buttonDiameter,
|
||
decoration: BoxDecoration(
|
||
color: widget.color,
|
||
shape: BoxShape.circle,
|
||
border: Border.all(
|
||
color: widget.color.withValues(alpha: 0.5),
|
||
width: 0.5,
|
||
),
|
||
),
|
||
child: widget.isHovering
|
||
? Icon(
|
||
widget.icon,
|
||
size: widget.size * 0.6,
|
||
color: const Color(0xFF1D1D1F).withValues(alpha: 0.6),
|
||
)
|
||
: null,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Windows 风格方形按钮
|
||
// ============================================================
|
||
|
||
class _WindowsControlButton extends StatefulWidget {
|
||
const _WindowsControlButton({
|
||
required this.icon,
|
||
required this.iconSize,
|
||
required this.isHovering,
|
||
required this.onHover,
|
||
required this.onTap,
|
||
required this.foregroundColor,
|
||
required this.width,
|
||
required this.height,
|
||
this.isClose = false,
|
||
});
|
||
|
||
final IconData icon;
|
||
final double iconSize;
|
||
final bool isHovering;
|
||
final ValueChanged<bool> onHover;
|
||
|
||
/// 点击回调,传入按钮自身的 BuildContext 用于弹出菜单定位
|
||
final void Function(BuildContext buttonContext) onTap;
|
||
final Color foregroundColor;
|
||
final double width;
|
||
final double height;
|
||
final bool isClose;
|
||
|
||
@override
|
||
State<_WindowsControlButton> createState() => _WindowsControlButtonState();
|
||
}
|
||
|
||
class _WindowsControlButtonState extends State<_WindowsControlButton> {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// Windows 风格:方形按钮,hover 时背景变化
|
||
final hoverBg = widget.isClose
|
||
? const Color(0xFFC42B1C)
|
||
: widget.foregroundColor.withValues(alpha: 0.1);
|
||
final hoverIcon = widget.isClose
|
||
? CupertinoColors.white
|
||
: widget.foregroundColor;
|
||
|
||
return MouseRegion(
|
||
onEnter: (_) => widget.onHover(true),
|
||
onExit: (_) => widget.onHover(false),
|
||
child: GestureDetector(
|
||
onTap: () => widget.onTap(context),
|
||
child: Container(
|
||
width: widget.width,
|
||
height: widget.height,
|
||
color: widget.isHovering ? hoverBg : const Color(0x00000000),
|
||
child: Icon(
|
||
widget.icon,
|
||
size: widget.iconSize,
|
||
color: widget.isHovering ? hoverIcon : widget.foregroundColor,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|