Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
WidgetDragBehavior.cs
1using System.Drawing;
2using System.Numerics;
5
6namespace Gondwana.Widgets;
7
11internal sealed class WidgetDragBehavior
12{
13 private readonly WidgetBase _owner;
14
15 private bool _isPointerDown;
16 private bool _suppressNextPointerClick;
17 private float _dragThresholdPx = 3f;
18 private PointF _dragStartScreenPositionPx;
19 private Vector2 _dragStartWidgetPositionPx;
20 private WidgetPointerButtonEnum _dragButton = WidgetPointerButtonEnum.None;
21 private int _dragPointerId;
22 private View? _dragView;
23 private Vector2 _dragStartPointerPositionPx;
24
25 internal WidgetDragBehavior(WidgetBase owner)
26 {
27 _owner = owner ?? throw new ArgumentNullException(nameof(owner));
28 }
29
30 internal bool IsDragEnabled { get; set; } = true;
31
32 internal bool IsDragging { get; private set; }
33
34 internal float DragThresholdPx
35 {
36 get => _dragThresholdPx;
37 set
38 {
39 if (value < 0f)
40 {
41 throw new ArgumentOutOfRangeException(
42 nameof(value),
43 value,
44 "The drag threshold cannot be negative.");
45 }
46
47 _dragThresholdPx = value;
48 }
49 }
50
51 internal bool CanStartDrag(
52 WidgetPointerEventArgs args)
53 {
54 return IsDragEnabled &&
55 args.IsPrimaryButton;
56 }
57
58 internal void ProcessPointerDown(WidgetPointerEventArgs args,
59 Func<WidgetPointerEventArgs, bool> canStartDrag)
60 {
61 if (_isPointerDown || !canStartDrag(args))
62 {
63 return;
64 }
65
66 _isPointerDown = true;
67 _suppressNextPointerClick = false;
68 IsDragging = false;
69
70 _dragStartScreenPositionPx = args.ScreenPositionPx;
71 _dragStartWidgetPositionPx = _owner.GetPosition();
72 _dragButton = args.Button;
73 _dragPointerId = args.PointerId;
74 _dragView = args.View;
75 _dragStartPointerPositionPx = GetPointerPositionInWidgetSpace(args.ScreenPositionPx, args.View);
76 }
77
78 internal void ProcessPointerMove(WidgetPointerEventArgs args,
79 Func<Vector2, View, Vector2> convertDelta,
80 Func<Vector2, Vector2> constrainPosition,
81 Action<WidgetDragEventArgs> dragStarted,
82 Action<WidgetDragEventArgs> dragged)
83 {
84 if (!_isPointerDown || args.PointerId != _dragPointerId)
85 return;
86
87 Vector2 totalScreenDeltaPx = GetTotalScreenDelta(args.ScreenPositionPx);
88
89 if (!IsDragging)
90 {
91 float thresholdSquared = DragThresholdPx * DragThresholdPx;
92 if (totalScreenDeltaPx.LengthSquared() < thresholdSquared)
93 return;
94
95 IsDragging = true;
96 _suppressNextPointerClick = true;
97
98 dragStarted(CreateDragEventArgs(args));
99
100 // The callback may cancel or hide the widget, which resets this state.
101 if (!IsDragging)
102 return;
103 }
104
105 ApplyDragPosition(totalScreenDeltaPx, _dragView ?? args.View, convertDelta, constrainPosition);
106
107 dragged(CreateDragEventArgs(args));
108 }
109
110 internal void ProcessPointerUp(WidgetPointerEventArgs args,
111 Func<Vector2, View, Vector2> convertDelta,
112 Func<Vector2, Vector2> constrainPosition,
113 Action<WidgetDragEventArgs> dragEnded)
114 {
115 if (!_isPointerDown || !IsMatchingRelease(args))
116 return;
117
118 WidgetDragEventArgs? dragArgs = null;
119
120 if (IsDragging)
121 {
122 Vector2 totalScreenDeltaPx = GetTotalScreenDelta(args.ScreenPositionPx);
123 ApplyDragPosition(totalScreenDeltaPx, _dragView ?? args.View, convertDelta, constrainPosition);
124
125 dragArgs = CreateDragEventArgs(args);
126 }
127
128 bool completedDrag = IsDragging;
129
130 ResetPointerState();
131
132 if (completedDrag && dragArgs is not null)
133 {
134 _suppressNextPointerClick = true;
135 dragEnded(dragArgs);
136 }
137 }
138
139 internal bool ShouldDispatchPointerClick(WidgetPointerEventArgs args)
140 {
141 if (!_suppressNextPointerClick)
142 return true;
143
144 _suppressNextPointerClick = false;
145 args.Handled = true;
146
147 return false;
148 }
149
150 internal Vector2 ConvertScreenDeltaToPositionDelta(Vector2 totalScreenDeltaPx,
151 View view)
152 {
153 ArgumentNullException.ThrowIfNull(view);
154
155 if (_owner.Mode == DirectDrawingMode.View)
156 return totalScreenDeltaPx;
157
158 var currentScreenPositionPx = new PointF(_dragStartScreenPositionPx.X + totalScreenDeltaPx.X,
159 _dragStartScreenPositionPx.Y + totalScreenDeltaPx.Y);
160
161 Vector2 currentPointerPositionPx = GetPointerPositionInWidgetSpace(currentScreenPositionPx, view);
162
163 return currentPointerPositionPx - _dragStartPointerPositionPx;
164 }
165
166 internal void ResetPointerState(bool clearClickSuppression = false)
167 {
168 _isPointerDown = false;
169 IsDragging = false;
170 _dragStartScreenPositionPx = PointF.Empty;
171 _dragStartWidgetPositionPx = Vector2.Zero;
172 _dragButton = WidgetPointerButtonEnum.None;
173 _dragPointerId = 0;
174 _dragView = null;
175 _dragStartPointerPositionPx = Vector2.Zero;
176
177 if (clearClickSuppression)
178 _suppressNextPointerClick = false;
179 }
180
181 private Vector2 GetPointerPositionInWidgetSpace(PointF screenPositionPx,
182 View view)
183 {
184 if (_owner.Mode == DirectDrawingMode.View)
185 return new Vector2(screenPositionPx.X, screenPositionPx.Y);
186
187 var sceneLayer = _owner.SceneLayer
188 ?? throw new InvalidOperationException("A scene-layer draggable widget must contain children attached to a SceneLayer.");
189
190 PointF worldPositionPx = view.ScreenPxToWorldPx(sceneLayer, screenPositionPx);
191
192 return new Vector2(worldPositionPx.X, worldPositionPx.Y);
193 }
194
195 private Vector2 GetTotalScreenDelta(PointF currentScreenPositionPx)
196 {
197 return new Vector2(currentScreenPositionPx.X - _dragStartScreenPositionPx.X,
198 currentScreenPositionPx.Y - _dragStartScreenPositionPx.Y);
199 }
200
201 private void ApplyDragPosition(Vector2 totalScreenDeltaPx,
202 View view,
203 Func<Vector2, View, Vector2> convertDelta,
204 Func<Vector2, Vector2> constrainPosition)
205 {
206 Vector2 positionDeltaPx = convertDelta(totalScreenDeltaPx, view);
207 Vector2 proposedPositionPx = _dragStartWidgetPositionPx + positionDeltaPx;
208
209 _owner.SetPosition(constrainPosition(proposedPositionPx));
210 }
211
212 private bool IsMatchingRelease(WidgetPointerEventArgs args)
213 {
214 return args.PointerId == _dragPointerId &&
215 (args.Button == WidgetPointerButtonEnum.None ||
216 args.Button == _dragButton);
217 }
218
219 private WidgetDragEventArgs CreateDragEventArgs(WidgetPointerEventArgs pointerArgs)
220 {
221 return new WidgetDragEventArgs(_owner,
222 _dragStartScreenPositionPx,
223 pointerArgs.ScreenPositionPx,
224 _dragButton,
225 pointerArgs.Tick,
226 pointerArgs.PointerId);
227 }
228}
PointF ScreenPxToWorldPx(SceneLayer layer, PointF screenPx)
Converts a screen-space pixel position into world-space coordinates for the specified scene layer,...
Definition View.cs:233
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.
@ View
Performs direct drawing at the view level using screen-space coordinates.
WidgetPointerButtonEnum
Represents a pointer button used by a widget interaction.