A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6using System.Linq;
7using NUnit.Framework;
8using osu.Framework.Extensions.Color4Extensions;
9using osu.Framework.Graphics;
10using osu.Framework.Graphics.Containers;
11using osu.Framework.Graphics.Shapes;
12using osu.Framework.Graphics.Sprites;
13using osu.Framework.Input;
14using osu.Framework.Input.Events;
15using osu.Framework.Input.StateChanges;
16using osu.Framework.Input.States;
17using osu.Framework.Testing;
18using osuTK;
19using osuTK.Graphics;
20using osuTK.Input;
21
22namespace osu.Framework.Tests.Visual.Input
23{
24 public class TestSceneTouchInput : ManualInputManagerTestScene
25 {
26 private static readonly TouchSource[] touch_sources = (TouchSource[])Enum.GetValues(typeof(TouchSource));
27
28 private Container<InputReceptor> receptors;
29
30 [SetUp]
31 public new void SetUp() => Schedule(() =>
32 {
33 Children = new Drawable[]
34 {
35 new Container
36 {
37 RelativeSizeAxes = Axes.Both,
38 Children = new Drawable[]
39 {
40 new Box
41 {
42 RelativeSizeAxes = Axes.Both,
43 Colour = Color4.Gray.Darken(2f),
44 },
45 new SpriteText
46 {
47 Anchor = Anchor.BottomCentre,
48 Origin = Anchor.BottomCentre,
49 Text = "Parent"
50 },
51 }
52 },
53 receptors = new Container<InputReceptor>
54 {
55 Padding = new MarginPadding { Bottom = 20f },
56 RelativeSizeAxes = Axes.Both,
57 ChildrenEnumerable = touch_sources.Select(s => new InputReceptor(s)
58 {
59 RelativePositionAxes = Axes.Both,
60 RelativeSizeAxes = Axes.Both,
61 Colour = Color4.Gray.Lighten((float)s / TouchState.MAX_TOUCH_COUNT),
62 X = (float)s / TouchState.MAX_TOUCH_COUNT,
63 })
64 },
65 new TestSceneTouchVisualiser.TouchVisualiser(),
66 };
67 });
68
69 private float getTouchXPos(TouchSource source) => receptors[(int)source].DrawPosition.X + 10f;
70 private Vector2 getTouchDownPos(TouchSource source) => receptors.ToScreenSpace(new Vector2(getTouchXPos(source), 1f));
71 private Vector2 getTouchMovePos(TouchSource source) => receptors.ToScreenSpace(new Vector2(getTouchXPos(source), receptors.DrawHeight / 2f));
72 private Vector2 getTouchUpPos(TouchSource source) => receptors.ToScreenSpace(new Vector2(getTouchXPos(source), receptors.DrawHeight - 1f));
73
74 [Test]
75 public void TestTouchInputHandling()
76 {
77 AddStep("activate touches", () =>
78 {
79 foreach (var s in touch_sources)
80 InputManager.BeginTouch(new Touch(s, getTouchDownPos(s)));
81 });
82
83 AddAssert("received correct event for each receptor", () =>
84 {
85 foreach (var r in receptors)
86 {
87 // attempt dequeuing from touch events queue.
88 if (!(r.TouchEvents.TryDequeue(out TouchEvent te) && te is TouchDownEvent touchDown))
89 return false;
90
91 // check correct provided information.
92 if (touchDown.ScreenSpaceTouch.Source != r.AssociatedSource ||
93 touchDown.ScreenSpaceTouch.Position != getTouchDownPos(r.AssociatedSource) ||
94 touchDown.ScreenSpaceTouchDownPosition != getTouchDownPos(r.AssociatedSource))
95 return false;
96
97 // check no other events popped up.
98 if (r.TouchEvents.Count > 0)
99 return false;
100 }
101
102 return true;
103 });
104
105 AddStep("move touches", () =>
106 {
107 foreach (var s in touch_sources)
108 InputManager.MoveTouchTo(new Touch(s, getTouchMovePos(s)));
109 });
110
111 AddAssert("received correct event for each receptor", () =>
112 {
113 foreach (var r in receptors)
114 {
115 if (!(r.TouchEvents.TryDequeue(out TouchEvent te) && te is TouchMoveEvent touchMove))
116 return false;
117
118 if (touchMove.ScreenSpaceTouch.Source != r.AssociatedSource ||
119 touchMove.ScreenSpaceTouch.Position != getTouchMovePos(r.AssociatedSource) ||
120 touchMove.ScreenSpaceLastTouchPosition != getTouchDownPos(r.AssociatedSource) ||
121 touchMove.ScreenSpaceTouchDownPosition != getTouchDownPos(r.AssociatedSource))
122 return false;
123
124 if (r.TouchEvents.Count > 0)
125 return false;
126 }
127
128 return true;
129 });
130
131 AddStep("move touches outside of area", () =>
132 {
133 foreach (var s in touch_sources)
134 InputManager.MoveTouchTo(new Touch(s, getTouchUpPos(s)));
135 });
136
137 AddAssert("received correct event for each receptor", () =>
138 {
139 foreach (var r in receptors)
140 {
141 if (!(r.TouchEvents.TryDequeue(out TouchEvent te) && te is TouchMoveEvent touchMove))
142 return false;
143
144 if (touchMove.ScreenSpaceTouch.Source != r.AssociatedSource ||
145 touchMove.ScreenSpaceTouch.Position != getTouchUpPos(r.AssociatedSource) ||
146 touchMove.ScreenSpaceLastTouchPosition != getTouchMovePos(r.AssociatedSource) ||
147 touchMove.ScreenSpaceTouchDownPosition != getTouchDownPos(r.AssociatedSource))
148 return false;
149
150 if (r.TouchEvents.Count > 0)
151 return false;
152 }
153
154 return true;
155 });
156
157 AddStep("deactivate touches out of receptors", () =>
158 {
159 foreach (var s in touch_sources)
160 InputManager.EndTouch(new Touch(s, getTouchUpPos(s)));
161 });
162
163 AddAssert("received correct event for each receptor", () =>
164 {
165 foreach (var r in receptors)
166 {
167 if (!(r.TouchEvents.TryDequeue(out TouchEvent te) && te is TouchUpEvent touchUp))
168 return false;
169
170 if (touchUp.ScreenSpaceTouch.Source != r.AssociatedSource ||
171 touchUp.ScreenSpaceTouch.Position != getTouchUpPos(r.AssociatedSource) ||
172 touchUp.ScreenSpaceTouchDownPosition != getTouchDownPos(r.AssociatedSource))
173 return false;
174
175 if (r.TouchEvents.Count > 0)
176 return false;
177 }
178
179 return true;
180 });
181
182 // All touch events have been handled, mouse input should not be performed.
183 // For simplicity, let's check whether we received mouse events or not.
184 AddAssert("no mouse input performed", () => receptors.All(r => r.MouseEvents.Count == 0));
185 }
186
187 [Test]
188 public void TestMouseInputAppliedFromLatestTouch()
189 {
190 InputReceptor firstReceptor = null, lastReceptor = null;
191
192 AddStep("setup receptors to receive mouse-from-touch", () =>
193 {
194 foreach (var r in receptors)
195 r.HandleTouch = _ => false;
196 });
197
198 AddStep("retrieve receptors", () =>
199 {
200 firstReceptor = receptors[(int)TouchSource.Touch1];
201 lastReceptor = receptors[(int)TouchSource.Touch10];
202 });
203
204 AddStep("activate first", () =>
205 {
206 InputManager.BeginTouch(new Touch(firstReceptor.AssociatedSource, getTouchDownPos(firstReceptor.AssociatedSource)));
207 });
208
209 AddAssert("received mouse-down event on first", () =>
210 {
211 // event #1: move mouse to first touch position.
212 if (!(firstReceptor.MouseEvents.TryDequeue(out MouseEvent me1) && me1 is MouseMoveEvent mouseMove))
213 return false;
214
215 if (mouseMove.ScreenSpaceMousePosition != getTouchDownPos(firstReceptor.AssociatedSource))
216 return false;
217
218 // event #2: press mouse left-button (from first touch activation).
219 if (!(firstReceptor.MouseEvents.TryDequeue(out MouseEvent me2) && me2 is MouseDownEvent mouseDown))
220 return false;
221
222 if (mouseDown.Button != MouseButton.Left ||
223 mouseDown.ScreenSpaceMousePosition != getTouchDownPos(firstReceptor.AssociatedSource) ||
224 mouseDown.ScreenSpaceMouseDownPosition != getTouchDownPos(firstReceptor.AssociatedSource))
225 return false;
226
227 return firstReceptor.MouseEvents.Count == 0;
228 });
229
230 // Activate each touch after first source and assert mouse has jumped to it.
231 foreach (var s in touch_sources.Skip(1))
232 {
233 Touch touch = default;
234
235 AddStep($"activate {s}", () => InputManager.BeginTouch(touch = new Touch(s, getTouchDownPos(s))));
236 AddAssert("mouse jumped to new touch", () => assertMouseOnTouchChange(touch, null, true));
237 }
238
239 Vector2? lastMovePosition = null;
240
241 // Move each touch inside area and assert regular mouse-move events received.
242 foreach (var s in touch_sources)
243 {
244 Touch touch = default;
245
246 AddStep($"move {s} inside area", () => InputManager.MoveTouchTo(touch = new Touch(s, getTouchMovePos(s))));
247 AddAssert("received regular mouse-move event", () =>
248 {
249 // ReSharper disable once AccessToModifiedClosure
250 var result = assertMouseOnTouchChange(touch, lastMovePosition, true);
251 lastMovePosition = touch.Position;
252 return result;
253 });
254 }
255
256 // Move each touch outside of area and assert no MouseMoveEvent expected to be received.
257 foreach (var s in touch_sources)
258 {
259 Touch touch = default;
260
261 AddStep($"move {s} outside of area", () => InputManager.MoveTouchTo(touch = new Touch(s, getTouchUpPos(s))));
262 AddAssert("no mouse-move event received", () =>
263 {
264 // ReSharper disable once AccessToModifiedClosure
265 var result = assertMouseOnTouchChange(touch, lastMovePosition, false);
266 lastMovePosition = touch.Position;
267 return result;
268 });
269 }
270
271 // Deactivate each touch but last touch and assert mouse did not jump to it.
272 foreach (var s in touch_sources.SkipLast(1))
273 {
274 AddStep($"deactivate {s}", () => InputManager.EndTouch(new Touch(s, getTouchUpPos(s))));
275 AddAssert("no mouse event received", () => receptors[(int)s].MouseEvents.Count == 0);
276 }
277
278 AddStep("deactivate last", () =>
279 {
280 InputManager.EndTouch(new Touch(lastReceptor.AssociatedSource, getTouchUpPos(lastReceptor.AssociatedSource)));
281 });
282
283 AddAssert("received mouse-up event", () =>
284 {
285 // First receptor is the one handling the mouse down event, mouse up would be raised to it.
286 if (!(firstReceptor.MouseEvents.TryDequeue(out MouseEvent me) && me is MouseUpEvent mouseUp))
287 return false;
288
289 if (mouseUp.Button != MouseButton.Left ||
290 mouseUp.ScreenSpaceMousePosition != getTouchUpPos(lastReceptor.AssociatedSource) ||
291 mouseUp.ScreenSpaceMouseDownPosition != getTouchDownPos(firstReceptor.AssociatedSource))
292 return false;
293
294 return firstReceptor.MouseEvents.Count == 0;
295 });
296
297 AddAssert("all events dequeued", () => receptors.All(r => r.MouseEvents.Count == 0));
298
299 bool assertMouseOnTouchChange(Touch touch, Vector2? lastPosition, bool expectsMouseMove)
300 {
301 var receptor = receptors[(int)touch.Source];
302
303 if (expectsMouseMove)
304 {
305 if (!(receptor.MouseEvents.TryDequeue(out MouseEvent me1) && me1 is MouseMoveEvent mouseMove))
306 return false;
307
308 if (mouseMove.ScreenSpaceMousePosition != touch.Position ||
309 (lastPosition != null && mouseMove.ScreenSpaceLastMousePosition != lastPosition.Value))
310 return false;
311 }
312
313 // Dequeue the "false drag" from first receptor to ensure there isn't any unexpected hidden event in this receptor.
314 if (!(firstReceptor.MouseEvents.TryDequeue(out MouseEvent me2) && me2 is DragEvent mouseDrag))
315 return false;
316
317 if (mouseDrag.Button != MouseButton.Left ||
318 mouseDrag.ScreenSpaceMousePosition != touch.Position ||
319 (lastPosition != null && mouseDrag.ScreenSpaceLastMousePosition != lastPosition.Value) ||
320 mouseDrag.ScreenSpaceMouseDownPosition != getTouchDownPos(firstReceptor.AssociatedSource))
321 return false;
322
323 return receptor.MouseEvents.Count == 0;
324 }
325 }
326
327 [Test]
328 public void TestMouseEventFromTouchIndication()
329 {
330 InputReceptor primaryReceptor = null;
331
332 AddStep("retrieve primary receptor", () => primaryReceptor = receptors[(int)TouchSource.Touch1]);
333 AddStep("setup receptors to discard mouse-from-touch events", () =>
334 {
335 primaryReceptor.HandleTouch = _ => false;
336 primaryReceptor.HandleMouse = e => !(e.CurrentState.Mouse.LastSource is ISourcedFromTouch);
337 });
338
339 AddStep("perform input on primary touch", () =>
340 {
341 InputManager.BeginTouch(new Touch(TouchSource.Touch1, getTouchDownPos(TouchSource.Touch1)));
342 InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, getTouchMovePos(TouchSource.Touch1)));
343 InputManager.EndTouch(new Touch(TouchSource.Touch1, getTouchUpPos(TouchSource.Touch1)));
344 });
345 AddAssert("no mouse event received", () => primaryReceptor.MouseEvents.Count == 0);
346
347 AddStep("perform input on mouse", () =>
348 {
349 InputManager.MoveMouseTo(getTouchDownPos(TouchSource.Touch1));
350 InputManager.PressButton(MouseButton.Left);
351 InputManager.MoveMouseTo(getTouchMovePos(TouchSource.Touch1));
352 InputManager.ReleaseButton(MouseButton.Left);
353 });
354 AddAssert("all mouse events received", () =>
355 {
356 // mouse moved.
357 if (!(primaryReceptor.MouseEvents.TryDequeue(out var me1) && me1 is MouseMoveEvent))
358 return false;
359
360 // left down.
361 if (!(primaryReceptor.MouseEvents.TryDequeue(out var me2) && me2 is MouseDownEvent))
362 return false;
363
364 // mouse dragged with left.
365 if (!(primaryReceptor.MouseEvents.TryDequeue(out var me3) && me3 is MouseMoveEvent))
366 return false;
367 if (!(primaryReceptor.MouseEvents.TryDequeue(out var me4) && me4 is DragEvent))
368 return false;
369
370 // left up.
371 if (!(primaryReceptor.MouseEvents.TryDequeue(out var me5) && me5 is MouseUpEvent))
372 return false;
373
374 return primaryReceptor.MouseEvents.Count == 0;
375 });
376 }
377
378 [Test]
379 public void TestMouseStillReleasedOnHierarchyInterference()
380 {
381 InputReceptor primaryReceptor = null;
382
383 AddStep("retrieve primary receptor", () => primaryReceptor = receptors[(int)TouchSource.Touch1]);
384 AddStep("setup handlers to receive mouse", () =>
385 {
386 primaryReceptor.HandleTouch = _ => false;
387 });
388
389 AddStep("begin touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getTouchDownPos(TouchSource.Touch1))));
390 AddAssert("primary receptor received mouse", () =>
391 {
392 bool event1 = primaryReceptor.MouseEvents.Dequeue() is MouseMoveEvent;
393 bool event2 = primaryReceptor.MouseEvents.Dequeue() is MouseDownEvent;
394 return event1 && event2 && primaryReceptor.MouseEvents.Count == 0;
395 });
396
397 AddStep("add drawable", () => primaryReceptor.Add(new InputReceptor(TouchSource.Touch1)
398 {
399 RelativeSizeAxes = Axes.Both,
400 HandleTouch = _ => true,
401 }));
402
403 AddStep("end touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getTouchDownPos(TouchSource.Touch1))));
404 AddAssert("primary receptor received mouse", () =>
405 {
406 bool event1 = primaryReceptor.MouseEvents.Dequeue() is MouseUpEvent;
407 return event1 && primaryReceptor.MouseEvents.Count == 0;
408 });
409 AddAssert("child receptor received nothing", () =>
410 primaryReceptor.TouchEvents.Count == 0 &&
411 primaryReceptor.MouseEvents.Count == 0);
412 }
413
414 private class InputReceptor : Container
415 {
416 public readonly TouchSource AssociatedSource;
417
418 public readonly Queue<TouchEvent> TouchEvents = new Queue<TouchEvent>();
419 public readonly Queue<MouseEvent> MouseEvents = new Queue<MouseEvent>();
420
421 public Func<TouchEvent, bool> HandleTouch;
422 public Func<MouseEvent, bool> HandleMouse;
423
424 protected override Container<Drawable> Content => content;
425
426 private readonly Container content;
427
428 public InputReceptor(TouchSource source)
429 {
430 AssociatedSource = source;
431
432 InternalChildren = new Drawable[]
433 {
434 new Box
435 {
436 RelativeSizeAxes = Axes.Both,
437 },
438 new SpriteText
439 {
440 X = 15f,
441 Anchor = Anchor.CentreLeft,
442 Origin = Anchor.CentreLeft,
443 Text = source.ToString(),
444 Colour = Color4.Black,
445 },
446 content = new Container
447 {
448 RelativeSizeAxes = Axes.Both,
449 }
450 };
451 }
452
453 protected override bool Handle(UIEvent e)
454 {
455 switch (e)
456 {
457 case TouchEvent te:
458 if (HandleTouch?.Invoke(te) != false)
459 {
460 TouchEvents.Enqueue(te);
461 return true;
462 }
463
464 break;
465
466 case MouseDownEvent _:
467 case MouseMoveEvent _:
468 case DragEvent _:
469 case MouseUpEvent _:
470 if (HandleMouse?.Invoke((MouseEvent)e) != false)
471 {
472 MouseEvents.Enqueue((MouseEvent)e);
473 return true;
474 }
475
476 break;
477
478 // not worth enqueuing, just handle for receiving drag.
479 case DragStartEvent dse:
480 return HandleMouse?.Invoke(dse) ?? true;
481 }
482
483 return false;
484 }
485 }
486 }
487}