23 private static readonly
MouseButton[] _mouseButtons =
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 = [];
41 private PointerCapture? _mouseCapture;
42 private WidgetHit? _hoveredHit;
45 private bool _isStarted;
46 private bool _disposed;
60 _renderSurfaceHost = renderSurfaceHost ??
throw new ArgumentNullException(nameof(renderSurfaceHost));
62 _keyboardEventPoller = keyboardEventPoller;
63 _mouseEventPoller = mouseEventPoller;
64 _touchEventPoller = touchEventPoller;
92 ObjectDisposedException.ThrowIf(_disposed,
this);
93 ArgumentNullException.ThrowIfNull(widget);
97 throw new ArgumentException(
"The widget belongs to a different RenderSurfaceHost.", nameof(widget));
102 if (_widgets.Contains(widget))
105 _widgets.Add(widget);
106 widget.Disposing += OnWidgetDisposing;
116 ArgumentNullException.ThrowIfNull(widget);
126 ObjectDisposedException.ThrowIf(_disposed,
this);
127 ArgumentNullException.ThrowIfNull(widget);
131 if (!_widgets.Remove(widget))
132 throw new InvalidOperationException(
"The widget is not registered.");
134 _widgets.Add(widget);
144 if (ReferenceEquals(_focusedWidget, widget))
147 if (widget is not
null && !CanReceiveKeyboardInput(widget))
151 _focusedWidget = widget;
153 previous?.DispatchFocusLost();
154 widget?.DispatchFocusGained();
170 ObjectDisposedException.ThrowIf(_disposed,
this);
175 if (_keyboardEventPoller is not
null)
176 _keyboardEventPoller.KeyDown += OnKeyDown;
178 if (_mouseEventPoller is not
null)
179 _mouseEventPoller.MouseEvent += OnMouseEvent;
181 if (_touchEventPoller is not
null)
183 _touchEventPoller.TouchBegan += OnTouchBegan;
184 _touchEventPoller.TouchMoved += OnTouchMoved;
185 _touchEventPoller.TouchEnded += OnTouchEnded;
186 _touchEventPoller.TouchEvent += OnTouchGesture;
189 WidgetInputRouterRegistry.Attach(
204 WidgetInputRouterRegistry.Detach(
208 if (_keyboardEventPoller is not
null)
209 _keyboardEventPoller.KeyDown -= OnKeyDown;
211 if (_mouseEventPoller is not
null)
212 _mouseEventPoller.MouseEvent -= OnMouseEvent;
214 if (_touchEventPoller is not
null)
216 _touchEventPoller.TouchBegan -= OnTouchBegan;
217 _touchEventPoller.TouchMoved -= OnTouchMoved;
218 _touchEventPoller.TouchEnded -= OnTouchEnded;
219 _touchEventPoller.TouchEvent -= OnTouchGesture;
224 _mouseCapture =
null;
225 _touchCaptures.Clear();
226 _unhandledTouchContacts.Clear();
227 _pendingTaps.Clear();
246 widgets = [.. _widgets];
251 widget.Disposing -= OnWidgetDisposing;
256 internal void NotifyWidgetHidden(
WidgetBase widget)
258 ArgumentNullException.ThrowIfNull(widget);
259 ReleaseInputState(widget, dispatchPointerUp:
true);
262 internal void NotifyWidgetInputDisabled(WidgetBase widget)
264 ArgumentNullException.ThrowIfNull(widget);
265 ReleaseInputState(widget, dispatchPointerUp:
true);
268 internal void NotifyWidgetPointerInputDisabled(WidgetBase widget)
270 ArgumentNullException.ThrowIfNull(widget);
271 ReleasePointerState(widget, dispatchPointerUp:
true);
274 internal void NotifyWidgetKeyboardFocusDisabled(WidgetBase widget)
276 ArgumentNullException.ThrowIfNull(widget);
277 ReleaseKeyboardFocus(widget);
280 private void OnKeyDown(KeyDownEventArgs args)
282 if (!_isStarted || _disposed)
293 WidgetBase? target = _focusedWidget;
294 if (target is not
null && !CanReceiveKeyboardInput(target))
300 if (target is not
null)
302 var focusedArgs =
new WidgetKeyboardEventArgs(target, key, action, modifiers);
303 target.DispatchKeyboardInput(focusedArgs);
304 if (focusedArgs.Handled)
308 WidgetBase[] snapshot;
310 snapshot = [.. _widgets];
313 for (
int index = snapshot.Length - 1; index >= 0; index--)
315 WidgetBase widget = snapshot[index];
316 if (widget is not IWidgetKeyboardFallback fallback || !IsRegistered(widget) ||
317 !HasValidTarget(widget) || !widget.Visible ||
318 !widget.IsInputEnabled || !widget.IsKeyboardInputEnabled)
321 var fallbackArgs =
new WidgetKeyboardEventArgs(widget, key, action, modifiers);
322 fallback.HandleUnhandledKeyboardInput(fallbackArgs);
323 if (fallbackArgs.Handled)
328 private void OnMouseEvent(MouseEventArgs args)
330 if (!_isStarted || _disposed)
334 WidgetHit? initialHit = HitTest(currentPosition);
336 UpdateMouseHover(initialHit, currentPosition, args.
Tick);
342 ProcessMouseDown(initialHit, currentPosition, mouseButton, args.
Tick);
348 GetTopmostViewAt(currentPosition) is
View gameplayView)
359 if (args.
ScrollDelta != 0 && initialHit is not
null && IsPointerRoutable(initialHit.Widget, initialHit.View))
361 initialHit.Widget.DispatchMouseWheel(
new WidgetMouseWheelEventArgs(
364 new PointF(currentPosition.X, currentPosition.Y),
373 ProcessMouseUp(currentPosition, mouseButton, args.
Tick);
377 UpdateMouseHover(HitTest(currentPosition), currentPosition, args.
Tick);
380 private void ProcessMouseDown(WidgetHit? hit,
385 if (_mouseCapture is not
null || hit is
null)
388 FocusFromPointer(hit.Widget);
392 hit.Widget.DispatchPointerDown(CreatePointerArgs(hit.Widget,
399 _mouseCapture =
new PointerCapture(hit.Widget,
406 private void ProcessMouseMove(Point previousPosition,
407 Point currentPosition,
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;
415 if (recipient is
null || view is
null)
418 var delta =
new Vector2(currentPosition.X - previousPosition.X,
419 currentPosition.Y - previousPosition.Y);
423 recipient.DispatchPointerMove(CreatePointerArgs(recipient,
431 if (capture is not
null)
432 capture.LastPosition = currentPosition;
435 private void ProcessMouseUp(Point position,
440 PointerCapture? capture = GetValidMouseCapture(tick);
442 if (capture is
null || capture.Button != releasedButton)
445 WidgetBase recipient = capture.Widget;
447 recipient.DispatchPointerUp(CreatePointerArgs(recipient,
454 WidgetHit? releaseHit = HitTest(position);
456 if (IsSameHit(releaseHit, recipient, capture.View) && recipient.GetWrappedHitOffset(capture.View, position) == capture.InstanceOffset)
458 recipient.DispatchPointerClick(CreatePointerArgs(recipient,
467 _mouseCapture =
null;
470 private void UpdateMouseHover(WidgetHit? hit,
474 if (IsSameHit(_hoveredHit, hit))
477 WidgetHit? previous = _hoveredHit;
480 if (previous is not
null)
482 previous.Widget.DispatchPointerLeave(CreatePointerArgs(previous.Widget,
492 hit.Widget.DispatchPointerEnter(CreatePointerArgs(hit.Widget,
501 private PointerCapture? GetValidMouseCapture(
long tick)
503 if (_mouseCapture is
null)
506 if (IsPointerRoutable(_mouseCapture.Widget, _mouseCapture.View))
507 return _mouseCapture;
509 _mouseCapture.Widget.DispatchPointerUp(CreatePointerArgs(_mouseCapture.Widget,
511 _mouseCapture.LastPosition,
512 _mouseCapture.Button,
516 _mouseCapture =
null;
520 private void OnTouchBegan(
object? sender, TouchEventArgs args)
522 if (!_isStarted || _disposed)
530 _unhandledTouchContacts.Remove(touch.Id);
531 _touchCaptures.Remove(touch.Id);
532 _pendingTaps.Remove(touch.Id);
534 WidgetHit? hit = HitTest(touch.Position);
538 if (GetTopmostViewAt(touch.Position) is not
null)
539 _unhandledTouchContacts.Add(touch.Id);
543 FocusFromPointer(hit.Widget);
545 hit.Widget.DispatchPointerDown(CreatePointerArgs(hit.Widget,
552 _touchCaptures[touch.Id] =
new PointerCapture(hit.Widget,
559 private void OnTouchMoved(
object? sender, TouchEventArgs args)
561 if (!_isStarted || _disposed)
566 if (!_touchCaptures.TryGetValue(touch.Id, out PointerCapture? capture))
569 if (!IsPointerRoutable(capture.Widget, capture.View))
571 capture.Widget.DispatchPointerUp(CreatePointerArgs(capture.Widget,
573 capture.LastPosition,
578 _touchCaptures.Remove(touch.Id);
582 var delta =
new Vector2(touch.Position.X - capture.LastPosition.X,
583 touch.Position.Y - capture.LastPosition.Y);
585 capture.Widget.DispatchPointerMove(CreatePointerArgs(capture.Widget,
593 capture.LastPosition = touch.Position;
596 private void OnTouchEnded(
object? sender, TouchEventArgs args)
598 if (!_isStarted || _disposed)
605 bool wasUnhandled = _unhandledTouchContacts.Remove(touch.Id);
606 bool hasPendingTap = _pendingTaps.Remove(touch.Id, out TappedEventArgs? tap);
608 if (hasPendingTap && wasUnhandled && tap is not
null && touch.Phase !=
TouchPhase.Cancelled)
610 View? releaseView = GetTopmostViewAt(touch.Position);
611 View? tapView = GetTopmostViewAt(tap.Position);
613 if (releaseView is not
null &&
614 releaseView == tapView &&
615 HitTest(touch.Position) is
null &&
616 HitTest(tap.Position) is
null)
619 releaseView, tap.Position, isTouch:
true, tap.TouchId));
623 if (!_touchCaptures.Remove(touch.Id, out PointerCapture? capture))
626 WidgetBase recipient = capture.Widget;
628 recipient.DispatchPointerUp(CreatePointerArgs(recipient,
633 touch.Id, instanceOffset: capture.InstanceOffset));
635 WidgetHit? releaseHit = HitTest(touch.Position);
637 if (touch.Phase !=
TouchPhase.Cancelled && IsSameHit(releaseHit, recipient, capture.View) && recipient.GetWrappedHitOffset(capture.View, touch.Position) == capture.InstanceOffset)
639 recipient.DispatchPointerClick(CreatePointerArgs(recipient,
645 clickCount: 1, instanceOffset: capture.InstanceOffset));
649 private void OnTouchGesture(GestureEventArgs args)
651 if (!_isStarted || _disposed || !args.
IsTap || args.
Tap is
null)
655 if (!_unhandledTouchContacts.Contains(tap.TouchId))
658 _pendingTaps[tap.TouchId] = tap;
661 private WidgetHit? HitTest(Point screenPositionPx)
663 View? view = GetTopmostViewAt(screenPositionPx);
668 WidgetBase[] snapshot;
671 snapshot = [.. _widgets];
673 for (
int index = snapshot.Length - 1; index >= 0; index--)
675 WidgetBase widget = snapshot[index];
677 if (widget.HitTest(view, screenPositionPx))
678 return new WidgetHit(widget, view, widget.GetWrappedHitOffset(view, screenPositionPx) ?? PointF.Empty);
684 private View? GetTopmostViewAt(Point screenPositionPx)
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))
692 var views = _renderSurfaceHost.ViewManager.Views;
694 for (
int index = views.Count - 1; index >= 0; index--)
696 View view = views[index];
705 private bool IsRegistered(WidgetBase widget)
708 return _widgets.Contains(widget);
711 private bool IsManagedView(
View view)
713 return _renderSurfaceHost.ViewManager.Views.Contains(view);
716 private bool HasValidTarget(WidgetBase widget,
717 View? routedView =
null)
720 return widget.SceneLayer is not
null;
722 if (widget.View is
null || !IsManagedView(widget.View))
725 return routedView is
null || ReferenceEquals(widget.View, routedView);
728 private bool IsPointerRoutable(WidgetBase widget,
731 return IsRegistered(widget) &&
732 IsManagedView(view) &&
733 ReferenceEquals(widget.RenderSurfaceHost, _renderSurfaceHost) &&
734 HasValidTarget(widget, view) &&
735 widget.IsInputEnabled &&
736 widget.IsPointerInputEnabled &&
740 private bool CanReceiveKeyboardInput(WidgetBase widget)
742 return IsRegistered(widget) &&
743 ReferenceEquals(widget.RenderSurfaceHost, _renderSurfaceHost) &&
744 HasValidTarget(widget) &&
746 widget.IsInputEnabled &&
747 widget.IsKeyboardInputEnabled &&
748 widget.CanReceiveFocus;
751 private void FocusFromPointer(WidgetBase widget)
753 if (widget.IsInputEnabled &&
754 widget.IsKeyboardInputEnabled &&
755 widget.CanReceiveFocus)
761 private void ReleaseInputState(
763 bool dispatchPointerUp)
769 ReleaseKeyboardFocus(widget);
772 private void ReleasePointerState(WidgetBase widget,
773 bool dispatchPointerUp)
775 if (ReferenceEquals(_hoveredHit?.Widget, widget))
778 if (ReferenceEquals(_mouseCapture?.Widget, widget))
780 PointerCapture capture = _mouseCapture;
782 if (dispatchPointerUp)
784 widget.DispatchPointerUp(CreatePointerArgs(widget,
786 capture.LastPosition,
792 _mouseCapture =
null;
795 int[] touchIds = _touchCaptures.Where(x => ReferenceEquals(x.Value.Widget, widget))
799 foreach (
int touchId
in touchIds)
801 PointerCapture capture = _touchCaptures[touchId];
803 if (dispatchPointerUp)
805 widget.DispatchPointerUp(CreatePointerArgs(widget,
807 capture.LastPosition,
810 pointerId: touchId));
813 _touchCaptures.Remove(touchId);
817 private void ReleaseKeyboardFocus(WidgetBase widget)
819 if (ReferenceEquals(_focusedWidget, widget))
823 private static bool IsSameHit(WidgetHit? left,
826 if (left is
null || right is
null)
827 return left is
null && right is
null;
829 return ReferenceEquals(left.Widget, right.Widget)
830 && ReferenceEquals(left.View, right.View);
833 private static bool IsSameHit(WidgetHit? hit,
837 return hit is not
null
838 && ReferenceEquals(hit.Widget, widget)
839 && ReferenceEquals(hit.View, view);
842 private WidgetPointerEventArgs CreatePointerArgs(WidgetBase widget,
849 Vector2 deltaPx =
default,
850 PointF? instanceOffset =
null)
852 return new WidgetPointerEventArgs(widget,
854 new PointF(position.X, position.Y),
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)
876 _ => WidgetPointerButtonEnum.None
880 private void OnWidgetDisposing(
object? sender, IDirectDrawable drawing)
882 if (drawing is WidgetBase widget)
889 bool dispatchPointerUp)
894 removed = _widgets.Remove(widget);
899 widget.Disposing -= OnWidgetDisposing;
901 ReleaseInputState(widget, dispatchPointerUp);
904 private sealed class WidgetHit
912 public WidgetHit(WidgetBase widget,
914 PointF instanceOffset)
918 InstanceOffset = instanceOffset;
924 public PointF InstanceOffset {
get; }
929 public WidgetBase Widget {
get; }
934 public View View {
get; }
937 private sealed class PointerCapture
947 public PointerCapture(WidgetBase widget,
951 PointF instanceOffset)
956 LastPosition = lastPosition;
957 InstanceOffset = instanceOffset;
963 public WidgetBase Widget {
get; }
968 public View View {
get; }
981 public Point LastPosition {
get;
set; }
985 public PointF InstanceOffset {
get; }