Files
FlClash/lib/common/window.dart
T

338 lines
9.6 KiB
Dart
Raw Normal View History

import 'dart:async';
2024-04-30 23:38:49 +08:00
import 'dart:io';
2024-10-27 16:59:23 +08:00
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
2025-10-14 15:13:52 +08:00
import 'package:fl_clash/models/config.dart';
import 'package:flutter/foundation.dart';
import 'package:material_ui/material_ui.dart';
2024-12-06 22:35:28 +08:00
import 'package:screen_retriever/screen_retriever.dart';
import 'package:window/window.dart';
2024-04-30 23:38:49 +08:00
class Window implements WindowPort {
2025-12-16 11:23:09 +08:00
static Window? _instance;
bool _supportsPosition = false;
WindowEffect? _blurEffect;
late final WindowVisibilityController _visibility =
WindowVisibilityController(
showWindow: _showWindow,
hideWindow: _hideWindow,
isWindowVisible: _isWindowVisible,
setSkipTaskbar: (skip) => desktopWindow.setSkipTaskbar(skip),
dockSettleDuration: system.isMacOS
? const Duration(seconds: 1)
: Duration.zero,
);
2025-12-16 11:23:09 +08:00
Window._internal();
factory Window() {
_instance ??= Window._internal();
return _instance!;
}
Future<void> init(int version, WindowProps props) async {
if (!safeModeBuild) {
if (system.isWindows) {
for (final scheme in protocolSchemes) {
protocol.register(scheme);
}
}
if (system.isLinux) {
unawaited(protocol.registerLinux(protocolSchemes));
}
}
await desktopWindow.ensureInitialized();
_supportsPosition = !system.isMacOS;
if (system.isLinux) {
_supportsPosition = await desktopWindow.isPositionSupported();
}
2025-06-07 01:48:34 +08:00
if (!system.isMacOS || version > 10) {
await desktopWindow.setTitleBarStyle(TitleBarStyle.hidden);
2025-02-09 18:39:38 +08:00
}
await desktopWindow.setSize(props.size);
await desktopWindow.setMinimumSize(const Size(380, 400));
await _windowPosition(props);
await desktopWindow.setPreventClose(true);
2025-10-14 15:13:52 +08:00
}
Future<void> _windowPosition(WindowProps props) async {
if (!_supportsPosition) {
return;
}
final position = props.left == null || props.top == null
? null
: restoredWindowPosition(
props,
displays: await screenRetriever.getAllDisplays(),
currentScale: system.isWindows ? _windowScale : null,
2024-12-06 22:35:28 +08:00
);
if (position == null) {
await desktopWindow.setAlignment(Alignment.center);
} else {
await desktopWindow.setPosition(position);
2024-09-08 21:21:21 +08:00
}
2024-04-30 23:38:49 +08:00
}
double get _windowScale =>
PlatformDispatcher.instance.implicitView?.devicePixelRatio ?? 1;
@override
Future<WindowProps?> captureNormalGeometry(WindowProps current) async {
final states = await Future.wait<bool>([
desktopWindow.isMaximized(),
desktopWindow.isFullScreen(),
desktopWindow.isMinimized(),
]);
if (states.any((state) => state)) {
return null;
}
final bounds = await desktopWindow.getBounds();
if (!bounds.width.isFinite ||
!bounds.height.isFinite ||
bounds.width <= 0 ||
bounds.height <= 0) {
return null;
}
final hasValidPosition =
bounds.left.isFinite && bounds.top.isFinite && _supportsPosition;
return current.copyWith(
width: bounds.width,
height: bounds.height,
left: hasValidPosition ? bounds.left : current.left,
top: hasValidPosition ? bounds.top : current.top,
scale: hasValidPosition && system.isWindows
? _windowScale
: current.scale,
);
}
/// A probe that finds no effect is not cached, so a failure at startup
/// does not pin the toggle to off for the rest of the session.
Future<WindowEffect> _resolveBlurEffect() async {
final cached = _blurEffect;
if (cached != null) {
return cached;
}
final candidates = system.isMacOS
? const [WindowEffect.blur]
: const [WindowEffect.acrylic, WindowEffect.blur];
for (final effect in candidates) {
if (await desktopWindow.isEffectSupported(effect)) {
return _blurEffect = effect;
}
}
return WindowEffect.none;
}
/// Only Windows 10's accent blur uses the tint; it draws nothing without one.
@override
Future<bool> setBlur({
required bool enabled,
required Brightness brightness,
required Color tint,
}) async {
if (system.isLinux) {
return false;
}
final effect = enabled ? await _resolveBlurEffect() : WindowEffect.none;
await desktopWindow.setEffect(
effect,
tint: system.isWindows
? tint.withValues(alpha: kSidebarBlurOpacity)
: null,
brightness: brightness,
);
return effect != WindowEffect.none;
}
/// Every desktop runner leaves the window hidden until [init] reveals it, so
/// a failure before that point would leave the error screen with no window.
Future<void> showInitFailure({required AsyncCallback onExit}) async {
desktopWindow.addListener(_InitFailureWindowListener(onExit));
try {
await desktopWindow.ensureInitialized();
await desktopWindow.setPreventClose(true);
await desktopWindow.setTitleBarStyle(TitleBarStyle.normal);
if (await desktopWindow.isVisible()) {
return;
}
await desktopWindow.setSize(const Size(680, 580));
await desktopWindow.center();
await desktopWindow.show();
await desktopWindow.focus();
} catch (e) {
commonPrint.log(
'show init failure window failed ${e.toString()}',
logLevel: LogLevel.warning,
);
}
}
@override
Future<void> show() => _visibility.show();
@override
Future<void> hide() => _visibility.hide();
@override
Future<void> toggle() => _visibility.toggle();
Future<void> _showWindow() async {
await desktopWindow.show();
await desktopWindow.focus();
2024-09-08 21:21:21 +08:00
}
Future<void> _hideWindow() async {
await desktopWindow.hide();
}
Future<bool> _isWindowVisible() async {
return desktopWindow.isVisible();
2024-04-30 23:38:49 +08:00
}
@override
2025-06-07 01:48:34 +08:00
Future<void> close() async {
await desktopWindow.close();
2026-02-02 10:14:13 +08:00
}
@override
2026-02-02 10:14:13 +08:00
void forceExit() {
2024-04-30 23:38:49 +08:00
exit(0);
}
}
2024-04-30 23:38:49 +08:00
/// On Windows the plugin scales by the window's monitor and screen_retriever by
/// each display's own, so only physical pixels compare across mixed scales.
@visibleForTesting
Offset? restoredWindowPosition(
WindowProps props, {
required List<Display> displays,
double? currentScale,
}) {
final savedScale = currentScale == null ? 1.0 : props.scale ?? currentScale;
final topLeft = Offset(props.left!, props.top!) * savedScale;
final bottomRight =
topLeft + props.size.bottomRight(Offset.zero) * savedScale;
final isVisible = displays.any((display) {
final position = display.visiblePosition;
if (position == null) {
return false;
}
final scale = currentScale == null
? 1.0
: display.scaleFactor?.toDouble() ?? 1.0;
final bounds = position & display.size;
final physical = Rect.fromLTRB(
bounds.left * scale,
bounds.top * scale,
bounds.right * scale,
bounds.bottom * scale,
);
return physical.contains(topLeft) || physical.contains(bottomRight);
});
return isVisible ? topLeft / (currentScale ?? 1.0) : null;
}
class _InitFailureWindowListener with WindowListener {
final AsyncCallback onExit;
_InitFailureWindowListener(this.onExit);
@override
void onWindowClose() => unawaited(onExit());
@override
void onWindowShouldTerminate() => unawaited(onExit());
}
/// Serializes visibility requests so a burst of hotkey toggles lands in
/// order, and holds back the Dock-hiding activation policy switch while a
/// preceding regular switch settles: flipping regular → accessory → regular
/// within about a second leaves macOS with stray Dock icons.
class WindowVisibilityController {
WindowVisibilityController({
required Future<void> Function() showWindow,
required Future<void> Function() hideWindow,
required Future<bool> Function() isWindowVisible,
required Future<void> Function(bool skip) setSkipTaskbar,
required this.dockSettleDuration,
}) : _showWindow = showWindow,
_hideWindow = hideWindow,
_isWindowVisible = isWindowVisible,
_setSkipTaskbar = setSkipTaskbar;
final Future<void> Function() _showWindow;
final Future<void> Function() _hideWindow;
final Future<bool> Function() _isWindowVisible;
final Future<void> Function(bool skip) _setSkipTaskbar;
final Duration dockSettleDuration;
Future<void>? _queue;
Timer? _dockSettleTimer;
bool _dockHidePending = false;
Future<void> show() => _enqueue(_show);
Future<void> hide() => _enqueue(_hide);
Future<void> toggle() => _enqueue(() async {
if (await _isWindowVisible()) {
await _hide();
} else {
await _show();
}
});
Future<void> _enqueue(Future<void> Function() step) {
final previous = _queue;
final result = previous == null ? step() : previous.then((_) => step());
final tail = result.catchError((_) {});
_queue = tail;
tail.whenComplete(() {
if (identical(_queue, tail)) {
_queue = null;
}
});
return result;
}
Future<void> _show() async {
_dockHidePending = false;
await _showWindow();
await _setSkipTaskbar(false);
_dockSettleTimer?.cancel();
_dockSettleTimer = dockSettleDuration == Duration.zero
? null
: Timer(dockSettleDuration, _onDockSettled);
}
Future<void> _hide() async {
await _hideWindow();
if (_dockSettleTimer?.isActive ?? false) {
_dockHidePending = true;
return;
}
await _setSkipTaskbar(true);
}
void _onDockSettled() {
_dockSettleTimer = null;
if (!_dockHidePending) {
return;
}
unawaited(
_enqueue(() async {
if (!_dockHidePending) {
return;
}
_dockHidePending = false;
await _setSkipTaskbar(true);
}),
);
2024-04-30 23:38:49 +08:00
}
}
final window = system.isDesktop ? Window() : null;