277 lines
7.7 KiB
Dart
277 lines
7.7 KiB
Dart
|
|
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<WindowControls> createState() => _WindowControlsState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _WindowControlsState extends State<WindowControls> {
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
unawaited(_windowControlsBinding.ensureInitialized());
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _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<bool>(
|
||
|
|
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<bool> isMaximized = ValueNotifier(false);
|
||
|
|
|
||
|
|
Future<void> 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,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|