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
WidgetInputRouter.cs
1using System.Drawing;
2using System.Numerics;
10
11namespace Gondwana.Widgets;
12
16public sealed class WidgetInputRouter : IDisposable
17{
21 public const int MousePointerId = -1;
22
23 private static readonly MouseButton[] _mouseButtons =
24 [
25 MouseButton.Left,
26 MouseButton.Right,
28 ];
29
30 private readonly object _syncRoot = new();
31 private readonly List<WidgetBase> _widgets = [];
32 private readonly Dictionary<int, PointerCapture> _touchCaptures = [];
33 private readonly HashSet<int> _unhandledTouchContacts = [];
34 private readonly Dictionary<int, TappedEventArgs> _pendingTaps = [];
35
36 private readonly RenderSurfaceHostBase _renderSurfaceHost;
37 private readonly KeyboardEventPoller? _keyboardEventPoller;
38 private readonly MouseEventPoller? _mouseEventPoller;
39 private readonly TouchEventPoller? _touchEventPoller;
40
41 private PointerCapture? _mouseCapture;
42 private WidgetHit? _hoveredHit;
43 private WidgetBase? _focusedWidget;
44
45 private bool _isStarted;
46 private bool _disposed;
47
55 public WidgetInputRouter(RenderSurfaceHostBase renderSurfaceHost,
56 KeyboardEventPoller? keyboardEventPoller,
57 MouseEventPoller? mouseEventPoller,
58 TouchEventPoller? touchEventPoller)
59 {
60 _renderSurfaceHost = renderSurfaceHost ?? throw new ArgumentNullException(nameof(renderSurfaceHost));
61
62 _keyboardEventPoller = keyboardEventPoller;
63 _mouseEventPoller = mouseEventPoller;
64 _touchEventPoller = touchEventPoller;
65 }
66
73 public WidgetBase? FocusedWidget => _focusedWidget;
74
84 public event Action<PrimaryPointerActionEventArgs>? UnhandledPrimaryAction;
85
90 public void Register(WidgetBase widget)
91 {
92 ObjectDisposedException.ThrowIf(_disposed, this);
93 ArgumentNullException.ThrowIfNull(widget);
94
95 if (!ReferenceEquals(widget.RenderSurfaceHost, _renderSurfaceHost))
96 {
97 throw new ArgumentException("The widget belongs to a different RenderSurfaceHost.", nameof(widget));
98 }
99
100 lock (_syncRoot)
101 {
102 if (_widgets.Contains(widget))
103 return;
104
105 _widgets.Add(widget);
106 widget.Disposing += OnWidgetDisposing;
107 }
108 }
109
114 public void Unregister(WidgetBase widget)
115 {
116 ArgumentNullException.ThrowIfNull(widget);
117 Unregister(widget, dispatchPointerUp: true);
118 }
119
124 public void BringToFront(WidgetBase widget)
125 {
126 ObjectDisposedException.ThrowIf(_disposed, this);
127 ArgumentNullException.ThrowIfNull(widget);
128
129 lock (_syncRoot)
130 {
131 if (!_widgets.Remove(widget))
132 throw new InvalidOperationException("The widget is not registered.");
133
134 _widgets.Add(widget);
135 }
136 }
137
142 public void Focus(WidgetBase? widget)
143 {
144 if (ReferenceEquals(_focusedWidget, widget))
145 return;
146
147 if (widget is not null && !CanReceiveKeyboardInput(widget))
148 return;
149
150 WidgetBase? previous = _focusedWidget;
151 _focusedWidget = widget;
152
153 previous?.DispatchFocusLost();
154 widget?.DispatchFocusGained();
155 }
156
160 public void ClearFocus()
161 {
162 Focus(null);
163 }
164
168 public void Start()
169 {
170 ObjectDisposedException.ThrowIf(_disposed, this);
171
172 if (_isStarted)
173 return;
174
175 if (_keyboardEventPoller is not null)
176 _keyboardEventPoller.KeyDown += OnKeyDown;
177
178 if (_mouseEventPoller is not null)
179 _mouseEventPoller.MouseEvent += OnMouseEvent;
180
181 if (_touchEventPoller is not null)
182 {
183 _touchEventPoller.TouchBegan += OnTouchBegan;
184 _touchEventPoller.TouchMoved += OnTouchMoved;
185 _touchEventPoller.TouchEnded += OnTouchEnded;
186 _touchEventPoller.TouchEvent += OnTouchGesture;
187 }
188
189 WidgetInputRouterRegistry.Attach(
190 _renderSurfaceHost,
191 this);
192
193 _isStarted = true;
194 }
195
199 public void Stop()
200 {
201 if (!_isStarted)
202 return;
203
204 WidgetInputRouterRegistry.Detach(
205 _renderSurfaceHost,
206 this);
207
208 if (_keyboardEventPoller is not null)
209 _keyboardEventPoller.KeyDown -= OnKeyDown;
210
211 if (_mouseEventPoller is not null)
212 _mouseEventPoller.MouseEvent -= OnMouseEvent;
213
214 if (_touchEventPoller is not null)
215 {
216 _touchEventPoller.TouchBegan -= OnTouchBegan;
217 _touchEventPoller.TouchMoved -= OnTouchMoved;
218 _touchEventPoller.TouchEnded -= OnTouchEnded;
219 _touchEventPoller.TouchEvent -= OnTouchGesture;
220 }
221
222 ClearFocus();
223
224 _mouseCapture = null;
225 _touchCaptures.Clear();
226 _unhandledTouchContacts.Clear();
227 _pendingTaps.Clear();
228 _hoveredHit = null;
229 _isStarted = false;
230 }
231
235 public void Dispose()
236 {
237 if (_disposed)
238 return;
239
240 Stop();
241
242 WidgetBase[] widgets;
243
244 lock (_syncRoot)
245 {
246 widgets = [.. _widgets];
247 _widgets.Clear();
248 }
249
250 foreach (WidgetBase widget in widgets)
251 widget.Disposing -= OnWidgetDisposing;
252
253 _disposed = true;
254 }
255
256 internal void NotifyWidgetHidden(WidgetBase widget)
257 {
258 ArgumentNullException.ThrowIfNull(widget);
259 ReleaseInputState(widget, dispatchPointerUp: true);
260 }
261
262 internal void NotifyWidgetInputDisabled(WidgetBase widget)
263 {
264 ArgumentNullException.ThrowIfNull(widget);
265 ReleaseInputState(widget, dispatchPointerUp: true);
266 }
267
268 internal void NotifyWidgetPointerInputDisabled(WidgetBase widget)
269 {
270 ArgumentNullException.ThrowIfNull(widget);
271 ReleasePointerState(widget, dispatchPointerUp: true);
272 }
273
274 internal void NotifyWidgetKeyboardFocusDisabled(WidgetBase widget)
275 {
276 ArgumentNullException.ThrowIfNull(widget);
277 ReleaseKeyboardFocus(widget);
278 }
279
280 private void OnKeyDown(KeyDownEventArgs args)
281 {
282 if (!_isStarted || _disposed)
283 return;
284
285 if (!int.TryParse(args.KeyConfig.Key, out int key))
286 return;
287
288 RouteKeyboardInput(key, args.KeyAction, args.Modifiers);
289 }
290
291 internal void RouteKeyboardInput(int key, KeyAction action, KeyboardModifierState modifiers)
292 {
293 WidgetBase? target = _focusedWidget;
294 if (target is not null && !CanReceiveKeyboardInput(target))
295 {
296 ClearFocus();
297 target = null;
298 }
299
300 if (target is not null)
301 {
302 var focusedArgs = new WidgetKeyboardEventArgs(target, key, action, modifiers);
303 target.DispatchKeyboardInput(focusedArgs);
304 if (focusedArgs.Handled)
305 return;
306 }
307
308 WidgetBase[] snapshot;
309 lock (_syncRoot)
310 snapshot = [.. _widgets];
311
312 // Most recently activated/registered eligible bar wins, just like pointer routing.
313 for (int index = snapshot.Length - 1; index >= 0; index--)
314 {
315 WidgetBase widget = snapshot[index];
316 if (widget is not IWidgetKeyboardFallback fallback || !IsRegistered(widget) ||
317 !HasValidTarget(widget) || !widget.Visible ||
318 !widget.IsInputEnabled || !widget.IsKeyboardInputEnabled)
319 continue;
320
321 var fallbackArgs = new WidgetKeyboardEventArgs(widget, key, action, modifiers);
322 fallback.HandleUnhandledKeyboardInput(fallbackArgs);
323 if (fallbackArgs.Handled)
324 return;
325 }
326 }
327
328 private void OnMouseEvent(MouseEventArgs args)
329 {
330 if (!_isStarted || _disposed)
331 return;
332
333 Point currentPosition = args.CurrentPosition;
334 WidgetHit? initialHit = HitTest(currentPosition);
335
336 UpdateMouseHover(initialHit, currentPosition, args.Tick);
337
338 foreach (MouseButton mouseButton in _mouseButtons)
339 {
340 if (args.IsButtonJustPressed(mouseButton))
341 {
342 ProcessMouseDown(initialHit, currentPosition, mouseButton, args.Tick);
343 }
344 }
345
346 // A widget hit or an existing pointer capture takes precedence over gameplay.
347 if (args.LeftButtonJustPressed && initialHit is null && _mouseCapture is null &&
348 GetTopmostViewAt(currentPosition) is View gameplayView)
349 {
350 UnhandledPrimaryAction?.Invoke(new PrimaryPointerActionEventArgs(
351 gameplayView, currentPosition, isTouch: false, MousePointerId));
352 }
353
354 if (args.PreviousPosition != currentPosition)
355 {
356 ProcessMouseMove(args.PreviousPosition, currentPosition, args.Tick);
357 }
358
359 if (args.ScrollDelta != 0 && initialHit is not null && IsPointerRoutable(initialHit.Widget, initialHit.View))
360 {
361 initialHit.Widget.DispatchMouseWheel(new WidgetMouseWheelEventArgs(
362 initialHit.Widget,
363 initialHit.View,
364 new PointF(currentPosition.X, currentPosition.Y),
365 args.ScrollDelta,
366 args.Tick));
367 }
368
369 foreach (MouseButton mouseButton in _mouseButtons)
370 {
371 if (args.IsButtonJustReleased(mouseButton))
372 {
373 ProcessMouseUp(currentPosition, mouseButton, args.Tick);
374 }
375 }
376
377 UpdateMouseHover(HitTest(currentPosition), currentPosition, args.Tick);
378 }
379
380 private void ProcessMouseDown(WidgetHit? hit,
381 Point position,
382 MouseButton mouseButton,
383 long tick)
384 {
385 if (_mouseCapture is not null || hit is null)
386 return;
387
388 FocusFromPointer(hit.Widget);
389
390 WidgetPointerButtonEnum widgetButton = MapMouseButton(mouseButton);
391
392 hit.Widget.DispatchPointerDown(CreatePointerArgs(hit.Widget,
393 hit.View,
394 position,
395 widgetButton,
396 tick,
398
399 _mouseCapture = new PointerCapture(hit.Widget,
400 hit.View,
401 widgetButton,
402 position,
403 hit.InstanceOffset);
404 }
405
406 private void ProcessMouseMove(Point previousPosition,
407 Point currentPosition,
408 long tick)
409 {
410 PointerCapture? capture = GetValidMouseCapture(tick);
411 WidgetHit? hit = capture is null ? HitTest(currentPosition) : null;
412 WidgetBase? recipient = capture?.Widget ?? hit?.Widget;
413 View? view = capture?.View ?? hit?.View;
414
415 if (recipient is null || view is null)
416 return;
417
418 var delta = new Vector2(currentPosition.X - previousPosition.X,
419 currentPosition.Y - previousPosition.Y);
420
421 WidgetPointerButtonEnum button = capture?.Button ?? WidgetPointerButtonEnum.None;
422
423 recipient.DispatchPointerMove(CreatePointerArgs(recipient,
424 view,
425 currentPosition,
426 button,
427 tick,
429 deltaPx: delta));
430
431 if (capture is not null)
432 capture.LastPosition = currentPosition;
433 }
434
435 private void ProcessMouseUp(Point position,
436 MouseButton mouseButton,
437 long tick)
438 {
439 WidgetPointerButtonEnum releasedButton = MapMouseButton(mouseButton);
440 PointerCapture? capture = GetValidMouseCapture(tick);
441
442 if (capture is null || capture.Button != releasedButton)
443 return;
444
445 WidgetBase recipient = capture.Widget;
446
447 recipient.DispatchPointerUp(CreatePointerArgs(recipient,
448 capture.View,
449 position,
450 releasedButton,
451 tick,
453
454 WidgetHit? releaseHit = HitTest(position);
455
456 if (IsSameHit(releaseHit, recipient, capture.View) && recipient.GetWrappedHitOffset(capture.View, position) == capture.InstanceOffset)
457 {
458 recipient.DispatchPointerClick(CreatePointerArgs(recipient,
459 capture.View,
460 position,
461 releasedButton,
462 tick,
464 clickCount: 1));
465 }
466
467 _mouseCapture = null;
468 }
469
470 private void UpdateMouseHover(WidgetHit? hit,
471 Point position,
472 long tick)
473 {
474 if (IsSameHit(_hoveredHit, hit))
475 return;
476
477 WidgetHit? previous = _hoveredHit;
478 _hoveredHit = hit;
479
480 if (previous is not null)
481 {
482 previous.Widget.DispatchPointerLeave(CreatePointerArgs(previous.Widget,
483 previous.View,
484 position,
486 tick,
488 }
489
490 if (hit is not null)
491 {
492 hit.Widget.DispatchPointerEnter(CreatePointerArgs(hit.Widget,
493 hit.View,
494 position,
496 tick,
498 }
499 }
500
501 private PointerCapture? GetValidMouseCapture(long tick)
502 {
503 if (_mouseCapture is null)
504 return null;
505
506 if (IsPointerRoutable(_mouseCapture.Widget, _mouseCapture.View))
507 return _mouseCapture;
508
509 _mouseCapture.Widget.DispatchPointerUp(CreatePointerArgs(_mouseCapture.Widget,
510 _mouseCapture.View,
511 _mouseCapture.LastPosition,
512 _mouseCapture.Button,
513 tick,
515
516 _mouseCapture = null;
517 return null;
518 }
519
520 private void OnTouchBegan(object? sender, TouchEventArgs args)
521 {
522 if (!_isStarted || _disposed)
523 return;
524
525 TouchPoint touch = args.Touch;
526
527 // A poller pause/reset may discard TouchEnded. IDs can be reused after
528 // resuming, so treat every emitted TouchBegan as a new logical contact
529 // boundary and reset both gameplay eligibility and widget capture for the ID.
530 _unhandledTouchContacts.Remove(touch.Id);
531 _touchCaptures.Remove(touch.Id);
532 _pendingTaps.Remove(touch.Id);
533
534 WidgetHit? hit = HitTest(touch.Position);
535
536 if (hit is null)
537 {
538 if (GetTopmostViewAt(touch.Position) is not null)
539 _unhandledTouchContacts.Add(touch.Id);
540 return;
541 }
542
543 FocusFromPointer(hit.Widget);
544
545 hit.Widget.DispatchPointerDown(CreatePointerArgs(hit.Widget,
546 hit.View,
547 touch.Position,
549 args.Tick,
550 touch.Id));
551
552 _touchCaptures[touch.Id] = new PointerCapture(hit.Widget,
553 hit.View,
555 touch.Position,
556 hit.InstanceOffset);
557 }
558
559 private void OnTouchMoved(object? sender, TouchEventArgs args)
560 {
561 if (!_isStarted || _disposed)
562 return;
563
564 TouchPoint touch = args.Touch;
565
566 if (!_touchCaptures.TryGetValue(touch.Id, out PointerCapture? capture))
567 return;
568
569 if (!IsPointerRoutable(capture.Widget, capture.View))
570 {
571 capture.Widget.DispatchPointerUp(CreatePointerArgs(capture.Widget,
572 capture.View,
573 capture.LastPosition,
575 args.Tick,
576 touch.Id));
577
578 _touchCaptures.Remove(touch.Id);
579 return;
580 }
581
582 var delta = new Vector2(touch.Position.X - capture.LastPosition.X,
583 touch.Position.Y - capture.LastPosition.Y);
584
585 capture.Widget.DispatchPointerMove(CreatePointerArgs(capture.Widget,
586 capture.View,
587 touch.Position,
589 args.Tick,
590 touch.Id,
591 deltaPx: delta));
592
593 capture.LastPosition = touch.Position;
594 }
595
596 private void OnTouchEnded(object? sender, TouchEventArgs args)
597 {
598 if (!_isStarted || _disposed)
599 return;
600
601 TouchPoint touch = args.Touch;
602
603 // The poller's tap recognizer dispatches the gesture during TouchEnded,
604 // before the router clears this contact's eligibility.
605 bool wasUnhandled = _unhandledTouchContacts.Remove(touch.Id);
606 bool hasPendingTap = _pendingTaps.Remove(touch.Id, out TappedEventArgs? tap);
607
608 if (hasPendingTap && wasUnhandled && tap is not null && touch.Phase != TouchPhase.Cancelled)
609 {
610 View? releaseView = GetTopmostViewAt(touch.Position);
611 View? tapView = GetTopmostViewAt(tap.Position);
612
613 if (releaseView is not null &&
614 releaseView == tapView &&
615 HitTest(touch.Position) is null &&
616 HitTest(tap.Position) is null)
617 {
618 UnhandledPrimaryAction?.Invoke(new PrimaryPointerActionEventArgs(
619 releaseView, tap.Position, isTouch: true, tap.TouchId));
620 }
621 }
622
623 if (!_touchCaptures.Remove(touch.Id, out PointerCapture? capture))
624 return;
625
626 WidgetBase recipient = capture.Widget;
627
628 recipient.DispatchPointerUp(CreatePointerArgs(recipient,
629 capture.View,
630 touch.Position,
632 args.Tick,
633 touch.Id, instanceOffset: capture.InstanceOffset));
634
635 WidgetHit? releaseHit = HitTest(touch.Position);
636
637 if (touch.Phase != TouchPhase.Cancelled && IsSameHit(releaseHit, recipient, capture.View) && recipient.GetWrappedHitOffset(capture.View, touch.Position) == capture.InstanceOffset)
638 {
639 recipient.DispatchPointerClick(CreatePointerArgs(recipient,
640 capture.View,
641 touch.Position,
643 args.Tick,
644 touch.Id,
645 clickCount: 1, instanceOffset: capture.InstanceOffset));
646 }
647 }
648
649 private void OnTouchGesture(GestureEventArgs args)
650 {
651 if (!_isStarted || _disposed || !args.IsTap || args.Tap is null)
652 return;
653
654 var tap = args.Tap;
655 if (!_unhandledTouchContacts.Contains(tap.TouchId))
656 return;
657
658 _pendingTaps[tap.TouchId] = tap;
659 }
660
661 private WidgetHit? HitTest(Point screenPositionPx)
662 {
663 View? view = GetTopmostViewAt(screenPositionPx);
664
665 if (view is null)
666 return null;
667
668 WidgetBase[] snapshot;
669
670 lock (_syncRoot)
671 snapshot = [.. _widgets];
672
673 for (int index = snapshot.Length - 1; index >= 0; index--)
674 {
675 WidgetBase widget = snapshot[index];
676
677 if (widget.HitTest(view, screenPositionPx))
678 return new WidgetHit(widget, view, widget.GetWrappedHitOffset(view, screenPositionPx) ?? PointF.Empty);
679 }
680
681 return null;
682 }
683
684 private View? GetTopmostViewAt(Point screenPositionPx)
685 {
686 if (_renderSurfaceHost.RenderSurfaceAdapter is not null &&
687 (screenPositionPx.X < 0 || screenPositionPx.Y < 0 ||
688 screenPositionPx.X >= _renderSurfaceHost.Backbuffer.Width ||
689 screenPositionPx.Y >= _renderSurfaceHost.Backbuffer.Height))
690 return null;
691
692 var views = _renderSurfaceHost.ViewManager.Views;
693
694 for (int index = views.Count - 1; index >= 0; index--)
695 {
696 View view = views[index];
697
698 if (view.Viewport.TargetRectPx.Contains(screenPositionPx))
699 return view;
700 }
701
702 return null;
703 }
704
705 private bool IsRegistered(WidgetBase widget)
706 {
707 lock (_syncRoot)
708 return _widgets.Contains(widget);
709 }
710
711 private bool IsManagedView(View view)
712 {
713 return _renderSurfaceHost.ViewManager.Views.Contains(view);
714 }
715
716 private bool HasValidTarget(WidgetBase widget,
717 View? routedView = null)
718 {
719 if (widget.Mode == DirectDrawingMode.SceneLayer)
720 return widget.SceneLayer is not null;
721
722 if (widget.View is null || !IsManagedView(widget.View))
723 return false;
724
725 return routedView is null || ReferenceEquals(widget.View, routedView);
726 }
727
728 private bool IsPointerRoutable(WidgetBase widget,
729 View view)
730 {
731 return IsRegistered(widget) &&
732 IsManagedView(view) &&
733 ReferenceEquals(widget.RenderSurfaceHost, _renderSurfaceHost) &&
734 HasValidTarget(widget, view) &&
735 widget.IsInputEnabled &&
736 widget.IsPointerInputEnabled &&
737 widget.Visible;
738 }
739
740 private bool CanReceiveKeyboardInput(WidgetBase widget)
741 {
742 return IsRegistered(widget) &&
743 ReferenceEquals(widget.RenderSurfaceHost, _renderSurfaceHost) &&
744 HasValidTarget(widget) &&
745 widget.Visible &&
746 widget.IsInputEnabled &&
747 widget.IsKeyboardInputEnabled &&
748 widget.CanReceiveFocus;
749 }
750
751 private void FocusFromPointer(WidgetBase widget)
752 {
753 if (widget.IsInputEnabled &&
754 widget.IsKeyboardInputEnabled &&
755 widget.CanReceiveFocus)
756 {
757 Focus(widget);
758 }
759 }
760
761 private void ReleaseInputState(
762 WidgetBase widget,
763 bool dispatchPointerUp)
764 {
765 ReleasePointerState(
766 widget,
767 dispatchPointerUp);
768
769 ReleaseKeyboardFocus(widget);
770 }
771
772 private void ReleasePointerState(WidgetBase widget,
773 bool dispatchPointerUp)
774 {
775 if (ReferenceEquals(_hoveredHit?.Widget, widget))
776 _hoveredHit = null;
777
778 if (ReferenceEquals(_mouseCapture?.Widget, widget))
779 {
780 PointerCapture capture = _mouseCapture;
781
782 if (dispatchPointerUp)
783 {
784 widget.DispatchPointerUp(CreatePointerArgs(widget,
785 capture.View,
786 capture.LastPosition,
787 capture.Button,
788 tick: 0,
789 pointerId: MousePointerId));
790 }
791
792 _mouseCapture = null;
793 }
794
795 int[] touchIds = _touchCaptures.Where(x => ReferenceEquals(x.Value.Widget, widget))
796 .Select(x => x.Key)
797 .ToArray();
798
799 foreach (int touchId in touchIds)
800 {
801 PointerCapture capture = _touchCaptures[touchId];
802
803 if (dispatchPointerUp)
804 {
805 widget.DispatchPointerUp(CreatePointerArgs(widget,
806 capture.View,
807 capture.LastPosition,
809 tick: 0,
810 pointerId: touchId));
811 }
812
813 _touchCaptures.Remove(touchId);
814 }
815 }
816
817 private void ReleaseKeyboardFocus(WidgetBase widget)
818 {
819 if (ReferenceEquals(_focusedWidget, widget))
820 Focus(null);
821 }
822
823 private static bool IsSameHit(WidgetHit? left,
824 WidgetHit? right)
825 {
826 if (left is null || right is null)
827 return left is null && right is null;
828
829 return ReferenceEquals(left.Widget, right.Widget)
830 && ReferenceEquals(left.View, right.View);
831 }
832
833 private static bool IsSameHit(WidgetHit? hit,
834 WidgetBase widget,
835 View view)
836 {
837 return hit is not null
838 && ReferenceEquals(hit.Widget, widget)
839 && ReferenceEquals(hit.View, view);
840 }
841
842 private WidgetPointerEventArgs CreatePointerArgs(WidgetBase widget,
843 View view,
844 Point position,
846 long tick,
847 int pointerId,
848 int clickCount = 0,
849 Vector2 deltaPx = default,
850 PointF? instanceOffset = null)
851 {
852 return new WidgetPointerEventArgs(widget,
853 view,
854 new PointF(position.X, position.Y),
855 button,
856 clickCount,
857 deltaPx,
858 tick,
859 pointerId)
860 {
861 WrappedOffsetWorldPx = instanceOffset ?? (pointerId == MousePointerId && _mouseCapture is { } mouse && ReferenceEquals(mouse.Widget, widget)
862 ? mouse.InstanceOffset
863 : _touchCaptures.TryGetValue(pointerId, out var touch) && ReferenceEquals(touch.Widget, widget)
864 ? touch.InstanceOffset
865 : widget.GetWrappedHitOffset(view, position) ?? PointF.Empty)
866 };
867 }
868
869 private static WidgetPointerButtonEnum MapMouseButton(MouseButton button)
870 {
871 return button switch
872 {
873 MouseButton.Left => WidgetPointerButtonEnum.Left,
874 MouseButton.Right => WidgetPointerButtonEnum.Right,
875 MouseButton.Middle => WidgetPointerButtonEnum.Middle,
876 _ => WidgetPointerButtonEnum.None
877 };
878 }
879
880 private void OnWidgetDisposing(object? sender, IDirectDrawable drawing)
881 {
882 if (drawing is WidgetBase widget)
883 {
884 Unregister(widget, dispatchPointerUp: false);
885 }
886 }
887
888 private void Unregister(WidgetBase widget,
889 bool dispatchPointerUp)
890 {
891 bool removed;
892
893 lock (_syncRoot)
894 removed = _widgets.Remove(widget);
895
896 if (!removed)
897 return;
898
899 widget.Disposing -= OnWidgetDisposing;
900
901 ReleaseInputState(widget, dispatchPointerUp);
902 }
903
904 private sealed class WidgetHit
905 {
912 public WidgetHit(WidgetBase widget,
913 View view,
914 PointF instanceOffset)
915 {
916 Widget = widget;
917 View = view;
918 InstanceOffset = instanceOffset;
919 }
920
924 public PointF InstanceOffset { get; }
925
929 public WidgetBase Widget { get; }
930
934 public View View { get; }
935 }
936
937 private sealed class PointerCapture
938 {
947 public PointerCapture(WidgetBase widget,
948 View view,
950 Point lastPosition,
951 PointF instanceOffset)
952 {
953 Widget = widget;
954 View = view;
955 Button = button;
956 LastPosition = lastPosition;
957 InstanceOffset = instanceOffset;
958 }
959
963 public WidgetBase Widget { get; }
964
968 public View View { get; }
969
973 public WidgetPointerButtonEnum Button { get; }
974
981 public Point LastPosition { get; set; }
985 public PointF InstanceOffset { get; }
986 }
987}
RenderSurfaceHostBase RenderSurfaceHost
Gets the render surface host shared by the composite and its children.
virtual DirectComposite Clear()
Removes every child without disposing them.
KeyboardModifierState Modifiers
Gets the state of keyboard modifier keys (Shift, Ctrl, Alt) at the time this event was generated....
KeyEventConfiguration KeyConfig
Gets the configuration details for the keyboard key that generated this event, including the key iden...
KeyAction KeyAction
Gets the type of action that occurred for this key, indicating whether the key was initially pressed,...
string Key
Gets the identifier of the keyboard key associated with this configuration. This can be a key name su...
Cross-platform singleton keyboard handler with throttling and manual input state feeding.
Point PreviousPosition
Gets the previous position of the mouse cursor before the current polling interval....
bool LeftButtonJustPressed
Gets a value indicating whether the left mouse button was pressed during the current polling interval...
Point CurrentPosition
Gets the current position of the mouse cursor at the time this event was generated....
long Tick
Gets the engine tick at the time this mouse event was generated.
bool IsButtonJustReleased(MouseButton button)
Determines whether the specified mouse button was released during the current polling interval.
int ScrollDelta
Gets the accumulated scroll wheel delta since the last poll, measured in implementation-defined units...
bool IsButtonJustPressed(MouseButton button)
Determines whether the specified mouse button was pressed during the current polling interval.
Provides centralized polling and event management for mouse input, including button presses,...
TappedEventArgs? Tap
Gets data specific to a tap gesture, or null when GestureType is not GestureType.Tap.
bool IsTap
Gets a value indicating whether this event represents a tap gesture. When true, Tap is non-null.
TouchPoint Touch
Gets the touch contact point associated with this event.
long Tick
Gets the engine tick at which this touch event was emitted.
Provides centralized polling and event management for touch input. This singleton class monitors touc...
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
Viewport Viewport
Gets the viewport that defines the screen-space rectangle, zoom level, and other rendering parameters...
Definition View.cs:44
Rectangle TargetRectPx
Screen-space rectangle (in SCREEN / RenderSurface pixels) where this view is drawn....
Definition Viewport.cs:35
Provides the base implementation for widgets that can be drawn and receive pointer,...
Definition WidgetBase.cs:12
void Focus(WidgetBase? widget)
Assigns keyboard focus to the specified widget.
void Unregister(WidgetBase widget)
Unregisters a widget so that it no longer receives routed input.
void ClearFocus()
Clears the current keyboard focus.
void Start()
Starts routing input events from the configured pollers to registered widgets.
void BringToFront(WidgetBase widget)
Moves a registered widget to the front of the input routing order.
WidgetBase? FocusedWidget
Gets the widget that currently has keyboard focus.
void Stop()
Stops routing input events and releases current pointer and focus state.
void Register(WidgetBase widget)
Registers a widget so that it can receive routed input.
Action< PrimaryPointerActionEventArgs >? UnhandledPrimaryAction
Occurs when a primary gameplay action was not claimed by a widget. Mouse left-button presses fire on ...
WidgetInputRouter(RenderSurfaceHostBase renderSurfaceHost, KeyboardEventPoller? keyboardEventPoller, MouseEventPoller? mouseEventPoller, TouchEventPoller? touchEventPoller)
Initializes a new instance of the WidgetInputRouter class.
void Dispose()
Releases the resources used by the router and unregisters all widgets.
const int MousePointerId
Identifies mouse pointer events in routed widget input.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.
@ View
Performs direct drawing at the view level using screen-space coordinates.
KeyboardModifierState
Represents the state of keyboard modifier keys (Shift, Ctrl, Alt) that can be pressed simultaneously ...
KeyAction
Represents the type of action performed on a keyboard key, indicating whether the key was pressed,...
Definition KeyAction.cs:10
MouseButton
Represents mouse button identifiers that can be used individually or combined to represent multiple s...
@ Middle
The middle mouse button, usually implemented as a clickable scroll wheel. This is often used for spec...
readonly record struct TouchPoint(int Id, Point Position, TouchPhase Phase)
Represents a single touch contact point, capturing its unique identifier, screen position,...
TouchPhase
Describes the current phase of a single touch contact point in its lifecycle.
Definition TouchPhase.cs:7
WidgetPointerButtonEnum
Represents a pointer button used by a widget interaction.