import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; import 'window_chrome.dart'; final _windowControlsBinding = _WindowControlsBinding(); class WindowControls extends StatefulWidget { final Brightness? brightness; const WindowControls({super.key, this.brightness}); const WindowControls.forShellOverlay({super.key}) : brightness = null; const WindowControls.forDarkSurface({super.key}) : brightness = Brightness.dark; @override State createState() => _WindowControlsState(); } class _WindowControlsState extends State { @override void initState() { super.initState(); unawaited(_windowControlsBinding.ensureInitialized()); } Future _toggleMaximize() async { if (_windowControlsBinding.isMaximized.value) { await windowManager.unmaximize(); return; } await windowManager.maximize(); } @override Widget build(BuildContext context) { if (!Platform.isWindows && !Platform.isMacOS) { return const SizedBox.shrink(); } final resolvedBrightness = widget.brightness ?? Theme.of(context).brightness; final isLight = resolvedBrightness == Brightness.light; return ValueListenableBuilder( valueListenable: _windowControlsBinding.isMaximized, builder: (context, isMaximized, _) { final backgroundColor = isLight ? Colors.black.withValues(alpha: 0.14) : const Color(0xFF08090c).withValues(alpha: 0.72); final iconColor = isLight ? Colors.black.withValues(alpha: 0.65) : Colors.white.withValues(alpha: 0.85); final defaultHoverColor = isLight ? Colors.black.withValues(alpha: 0.10) : Colors.white.withValues(alpha: 0.15); const containerHeight = WindowChrome.controlsHeight; const containerRadius = 20.0; const buttonWidth = WindowChrome.buttonWidth; const iconSize = 16.0; return Container( height: containerHeight, decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(containerRadius), boxShadow: isLight ? null : const [ BoxShadow( color: Color(0x2E000000), blurRadius: 8, offset: Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _ControlButton( icon: Icons.remove_rounded, tooltip: '最小化', iconColor: iconColor, defaultHoverColor: defaultHoverColor, width: buttonWidth, height: containerHeight, iconSize: iconSize, borderRadius: containerRadius, onTap: windowManager.minimize, ), _ControlButton( icon: isMaximized ? Icons.filter_none_rounded : Icons.crop_square_rounded, tooltip: isMaximized ? '还原' : '最大化', iconColor: iconColor, defaultHoverColor: defaultHoverColor, width: buttonWidth, height: containerHeight, iconSize: iconSize, borderRadius: containerRadius, onTap: _toggleMaximize, ), _ControlButton( icon: Icons.close_rounded, tooltip: '关闭', iconColor: iconColor, defaultHoverColor: defaultHoverColor, hoverColor: const Color(0xFFE81123), pressedColor: const Color(0xFFC50F1F), width: buttonWidth, height: containerHeight, iconSize: iconSize, borderRadius: containerRadius, onTap: windowManager.close, ), ], ), ); }, ); } } class _WindowControlsBinding with WindowListener { final ValueNotifier isMaximized = ValueNotifier(false); Future ensureInitialized() async { if (!windowManager.listeners.contains(this)) { windowManager.addListener(this); } try { isMaximized.value = await windowManager.isMaximized(); } catch (_) {} } @override void onWindowMaximize() { isMaximized.value = true; } @override void onWindowUnmaximize() { isMaximized.value = false; } void reset() { if (windowManager.listeners.contains(this)) { windowManager.removeListener(this); } isMaximized.value = false; } } class _ControlButton extends StatefulWidget { final IconData icon; final String tooltip; final Color iconColor; final Color defaultHoverColor; final Color? hoverColor; final Color? pressedColor; final double width; final double height; final double iconSize; final double borderRadius; final VoidCallback onTap; const _ControlButton({ required this.icon, required this.tooltip, required this.iconColor, required this.defaultHoverColor, this.hoverColor, this.pressedColor, this.width = 36, this.height = 30, this.iconSize = 16, this.borderRadius = 20, required this.onTap, }); @override State<_ControlButton> createState() => _ControlButtonState(); } class _ControlButtonState extends State<_ControlButton> { bool _hovering = false; bool _pressing = false; bool _focused = false; late final FocusNode _focusNode; @override void initState() { super.initState(); _focusNode = FocusNode(); _focusNode.addListener(_onFocusChange); } @override void dispose() { _focusNode.removeListener(_onFocusChange); _focusNode.dispose(); super.dispose(); } void _onFocusChange() { setState(() => _focused = _focusNode.hasFocus); } bool get _active => _hovering || _focused; @override Widget build(BuildContext context) { final hoverBg = widget.hoverColor ?? widget.defaultHoverColor; final pressedBg = widget.pressedColor ?? widget.defaultHoverColor.withValues( alpha: widget.defaultHoverColor.a * 1.8, ); final Color bg; if (_pressing) { bg = widget.hoverColor != null ? (widget.pressedColor ?? hoverBg) : pressedBg; } else if (_active) { bg = hoverBg; } else { bg = Colors.transparent; } final bool useBrightIcon = (_active || _pressing) && widget.hoverColor != null; return Tooltip( message: widget.tooltip, child: Focus( focusNode: _focusNode, child: MouseRegion( onEnter: (_) => setState(() => _hovering = true), onExit: (_) => setState(() { _hovering = false; _pressing = false; }), child: GestureDetector( onTap: widget.onTap, onTapDown: (_) => setState(() => _pressing = true), onTapUp: (_) => setState(() => _pressing = false), onTapCancel: () => setState(() => _pressing = false), child: Container( width: widget.width, height: widget.height, decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(widget.borderRadius), ), alignment: Alignment.center, child: Icon( widget.icon, size: widget.iconSize, color: useBrightIcon ? Colors.white : widget.iconColor, ), ), ), ), ), ); } }