Skip to content

Commit 840dde9

Browse files
fix: WindowDragArea dragging delays (#5795)
* improve `WindowDragArea` double-tap handling * feat: `Offset.distance` property * reorder Offset props * improve webview docs * improve webview docs * Set Markdown value at initialization in tests Assigns the 'value' property of the Markdown control during initialization instead of within each test. Also removes the 'similarity_threshold' parameter from screenshot assertions for consistency. --------- Co-authored-by: Feodor Fitsner <feodor@appveyor.com>
1 parent 3280dfb commit 840dde9

File tree

3 files changed

+128
-56
lines changed

3 files changed

+128
-56
lines changed
Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,122 @@
1+
import 'dart:async';
2+
13
import 'package:flet/src/extensions/control.dart';
24
import 'package:flet/src/utils/events.dart';
35
import 'package:flet/src/utils/numbers.dart';
6+
import 'package:flutter/gestures.dart';
47
import 'package:flutter/material.dart';
58
import 'package:window_manager/window_manager.dart';
69

710
import '../models/control.dart';
811
import '../widgets/error.dart';
912
import 'base_controls.dart';
1013

11-
class WindowDragAreaControl extends StatelessWidget {
14+
class WindowDragAreaControl extends StatefulWidget {
1215
final Control control;
1316

1417
const WindowDragAreaControl({super.key, required this.control});
1518

1619
@override
17-
Widget build(BuildContext context) {
18-
debugPrint("WindowDragArea build: ${control.id}");
20+
State<WindowDragAreaControl> createState() => _WindowDragAreaControlState();
21+
}
22+
23+
class _WindowDragAreaControlState extends State<WindowDragAreaControl> {
24+
/// Timestamp of the last tap-up event, used for double-tap detection.
25+
DateTime? _lastTapUpTime;
26+
27+
/// Position of the last tap-up event, used for double-tap detection.
28+
Offset? _lastTapUpPosition;
29+
30+
/// Whether double-tap-to-maximize behavior is enabled.
31+
bool get _maximizable => widget.control.getBool("maximizable", true)!;
32+
33+
/// Called when the user presses down inside the drag area.
34+
///
35+
/// If a recent tap-up occurred close in time and space (within Flutter’s
36+
/// [kDoubleTapTimeout] and [kDoubleTapSlop]), this is treated as a double-tap
37+
/// and triggers a maximize/unmaximize toggle.
38+
void _handlePointerDown(PointerDownEvent event) {
39+
if (!_maximizable || _lastTapUpTime == null) return;
1940

20-
var content = control.buildWidget("content");
41+
final now = DateTime.now();
42+
final timeDiff = now.difference(_lastTapUpTime!);
43+
final posDiff = (event.position - _lastTapUpPosition!).distance;
2144

45+
// If tap timing and distance match Flutter's double-tap thresholds
46+
// — treat this as a double-tap.
47+
if (timeDiff <= kDoubleTapTimeout && posDiff <= kDoubleTapSlop) {
48+
_resetDoubleTapState();
49+
unawaited(_toggleMaximize());
50+
}
51+
}
52+
53+
/// Called when the user lifts their finger or mouse button.
54+
///
55+
/// Records the time and position so the next pointer down can detect
56+
/// a double-tap sequence.
57+
void _handlePointerUp(PointerUpEvent event) {
58+
if (!_maximizable) return;
59+
_lastTapUpTime = DateTime.now();
60+
_lastTapUpPosition = event.position;
61+
}
62+
63+
/// Clears any stored double-tap tracking information.
64+
void _resetDoubleTapState() {
65+
_lastTapUpTime = null;
66+
_lastTapUpPosition = null;
67+
}
68+
69+
/// Toggles between maximized and restored window states.
70+
Future<void> _toggleMaximize() async {
71+
final isMaximized = await windowManager.isMaximized();
72+
73+
if (isMaximized) {
74+
await windowManager.unmaximize();
75+
} else {
76+
await windowManager.maximize();
77+
}
78+
79+
widget.control
80+
.triggerEvent("double_tap", isMaximized ? "unmaximize" : "maximize");
81+
}
82+
83+
@override
84+
Widget build(BuildContext context) {
85+
debugPrint("WindowDragArea build: ${widget.control.id}");
86+
87+
final content = widget.control.buildWidget("content");
2288
if (content == null) {
2389
return const ErrorControl(
2490
"WindowDragArea.content must be provided and visible");
2591
}
2692

27-
final wda = GestureDetector(
93+
Widget dragArea = GestureDetector(
2894
behavior: HitTestBehavior.translucent,
2995
onPanStart: (DragStartDetails details) {
96+
// Start moving the window.
3097
windowManager.startDragging();
31-
if (control.getBool("on_drag_start", false)!) {
32-
control.triggerEvent("drag_start", details.toMap());
33-
}
98+
99+
widget.control.triggerEvent("drag_start", details.toMap());
34100
},
35101
onPanEnd: (DragEndDetails details) {
36-
if (control.getBool("on_drag_end", false)!) {
37-
control.triggerEvent("drag_end", details.toMap());
38-
}
102+
widget.control.triggerEvent("drag_end", details.toMap());
39103
},
40-
onDoubleTap: control.getBool("maximizable", true)!
41-
? () async {
42-
final isMaximized = await windowManager.isMaximized();
43-
if (isMaximized) {
44-
windowManager.unmaximize();
45-
} else {
46-
windowManager.maximize();
47-
}
48-
49-
// trigger event
50-
if (control.getBool("on_double_tap", false)!) {
51-
control.triggerEvent(
52-
"double_tap", isMaximized ? "unmaximize" : "maximize");
53-
}
54-
}
55-
: null,
56104
child: content,
57105
);
58106

59-
return LayoutControl(control: control, child: wda);
107+
// If maximization is enabled, wrap with a listener to detect double-taps.
108+
// Using a [Listener] instead of the above [GestureDetector] ensures the
109+
// widget doesn’t block or consume gestures from its children.
110+
if (_maximizable) {
111+
dragArea = Listener(
112+
behavior: HitTestBehavior.translucent,
113+
onPointerDown: _handlePointerDown,
114+
onPointerUp: _handlePointerUp,
115+
onPointerCancel: (_) => _resetDoubleTapState(),
116+
child: dragArea,
117+
);
118+
}
119+
120+
return LayoutControl(control: widget.control, child: dragArea);
60121
}
61122
}

0 commit comments

Comments
 (0)