|
| 1 | +import 'dart:async'; |
| 2 | + |
1 | 3 | import 'package:flet/src/extensions/control.dart'; |
2 | 4 | import 'package:flet/src/utils/events.dart'; |
3 | 5 | import 'package:flet/src/utils/numbers.dart'; |
| 6 | +import 'package:flutter/gestures.dart'; |
4 | 7 | import 'package:flutter/material.dart'; |
5 | 8 | import 'package:window_manager/window_manager.dart'; |
6 | 9 |
|
7 | 10 | import '../models/control.dart'; |
8 | 11 | import '../widgets/error.dart'; |
9 | 12 | import 'base_controls.dart'; |
10 | 13 |
|
11 | | -class WindowDragAreaControl extends StatelessWidget { |
| 14 | +class WindowDragAreaControl extends StatefulWidget { |
12 | 15 | final Control control; |
13 | 16 |
|
14 | 17 | const WindowDragAreaControl({super.key, required this.control}); |
15 | 18 |
|
16 | 19 | @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; |
19 | 40 |
|
20 | | - var content = control.buildWidget("content"); |
| 41 | + final now = DateTime.now(); |
| 42 | + final timeDiff = now.difference(_lastTapUpTime!); |
| 43 | + final posDiff = (event.position - _lastTapUpPosition!).distance; |
21 | 44 |
|
| 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"); |
22 | 88 | if (content == null) { |
23 | 89 | return const ErrorControl( |
24 | 90 | "WindowDragArea.content must be provided and visible"); |
25 | 91 | } |
26 | 92 |
|
27 | | - final wda = GestureDetector( |
| 93 | + Widget dragArea = GestureDetector( |
28 | 94 | behavior: HitTestBehavior.translucent, |
29 | 95 | onPanStart: (DragStartDetails details) { |
| 96 | + // Start moving the window. |
30 | 97 | 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()); |
34 | 100 | }, |
35 | 101 | 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()); |
39 | 103 | }, |
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, |
56 | 104 | child: content, |
57 | 105 | ); |
58 | 106 |
|
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); |
60 | 121 | } |
61 | 122 | } |
0 commit comments