Files
xianyan/lib/features/desktop/desktop_window_title_bar.dart
Developer f7520b17b2 win提交
2026-06-22 03:50:59 +08:00

506 lines
15 KiB
Dart
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.
/// ============================================================
/// 闲言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 侧 startDraggingMethodChannel 延迟导致卡顿)。
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,
),
),
),
);
}
}