A game framework written with osu! in mind.
at master 21 kB view raw
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}