22 private static readonly
MouseButton[] _mouseButtons =
29 private readonly
object _syncRoot =
new();
30 private readonly List<WidgetBase> _widgets = [];
31 private readonly Dictionary<int, PointerCapture> _touchCaptures = [];
38 private PointerCapture? _mouseCapture;
39 private WidgetHit? _hoveredHit;
42 private bool _isStarted;
43 private bool _disposed;
57 _renderSurfaceHost = renderSurfaceHost ??
throw new ArgumentNullException(nameof(renderSurfaceHost));
59 _keyboardEventPoller = keyboardEventPoller;
60 _mouseEventPoller = mouseEventPoller;
61 _touchEventPoller = touchEventPoller;
78 ObjectDisposedException.ThrowIf(_disposed,
this);
79 ArgumentNullException.ThrowIfNull(widget);
83 throw new ArgumentException(
"The widget belongs to a different RenderSurfaceHost.", nameof(widget));
88 if (_widgets.Contains(widget))
92 widget.Disposing += OnWidgetDisposing;
102 ArgumentNullException.ThrowIfNull(widget);
112 ObjectDisposedException.ThrowIf(_disposed,
this);
113 ArgumentNullException.ThrowIfNull(widget);
117 if (!_widgets.Remove(widget))
118 throw new InvalidOperationException(
"The widget is not registered.");
120 _widgets.Add(widget);
130 if (ReferenceEquals(_focusedWidget, widget))
133 if (widget is not
null && !CanReceiveKeyboardInput(widget))
137 _focusedWidget = widget;
139 previous?.DispatchFocusLost();
140 widget?.DispatchFocusGained();
156 ObjectDisposedException.ThrowIf(_disposed,
this);
161 if (_keyboardEventPoller is not
null)
162 _keyboardEventPoller.KeyDown += OnKeyDown;
164 if (_mouseEventPoller is not
null)
165 _mouseEventPoller.MouseEvent += OnMouseEvent;
167 if (_touchEventPoller is not
null)
169 _touchEventPoller.TouchBegan += OnTouchBegan;
170 _touchEventPoller.TouchMoved += OnTouchMoved;
171 _touchEventPoller.TouchEnded += OnTouchEnded;
174 WidgetInputRouterRegistry.Attach(
189 WidgetInputRouterRegistry.Detach(
193 if (_keyboardEventPoller is not
null)
194 _keyboardEventPoller.KeyDown -= OnKeyDown;
196 if (_mouseEventPoller is not
null)
197 _mouseEventPoller.MouseEvent -= OnMouseEvent;
199 if (_touchEventPoller is not
null)
201 _touchEventPoller.TouchBegan -= OnTouchBegan;
202 _touchEventPoller.TouchMoved -= OnTouchMoved;
203 _touchEventPoller.TouchEnded -= OnTouchEnded;
208 _mouseCapture =
null;
209 _touchCaptures.Clear();
228 widgets = [.. _widgets];
233 widget.Disposing -= OnWidgetDisposing;
238 internal void NotifyWidgetHidden(
WidgetBase widget)
240 ArgumentNullException.ThrowIfNull(widget);
241 ReleaseInputState(widget, dispatchPointerUp:
true);
244 internal void NotifyWidgetInputDisabled(WidgetBase widget)
246 ArgumentNullException.ThrowIfNull(widget);
247 ReleaseInputState(widget, dispatchPointerUp:
true);
250 internal void NotifyWidgetPointerInputDisabled(WidgetBase widget)
252 ArgumentNullException.ThrowIfNull(widget);
253 ReleasePointerState(widget, dispatchPointerUp:
true);
256 internal void NotifyWidgetKeyboardFocusDisabled(WidgetBase widget)
258 ArgumentNullException.ThrowIfNull(widget);
259 ReleaseKeyboardFocus(widget);
262 private void OnKeyDown(KeyDownEventArgs args)
264 if (!_isStarted || _disposed)
275 WidgetBase? target = _focusedWidget;
276 if (target is not
null && !CanReceiveKeyboardInput(target))
282 if (target is not
null)
284 var focusedArgs =
new WidgetKeyboardEventArgs(target, key, action, modifiers);
285 target.DispatchKeyboardInput(focusedArgs);
286 if (focusedArgs.Handled)
290 WidgetBase[] snapshot;
292 snapshot = [.. _widgets];
295 for (
int index = snapshot.Length - 1; index >= 0; index--)
297 WidgetBase widget = snapshot[index];
298 if (widget is not IWidgetKeyboardFallback fallback || !IsRegistered(widget) ||
299 !HasValidTarget(widget) || !widget.Visible ||
300 !widget.IsInputEnabled || !widget.IsKeyboardInputEnabled)
303 var fallbackArgs =
new WidgetKeyboardEventArgs(widget, key, action, modifiers);
304 fallback.HandleUnhandledKeyboardInput(fallbackArgs);
305 if (fallbackArgs.Handled)
310 private void OnMouseEvent(MouseEventArgs args)
312 if (!_isStarted || _disposed)
316 WidgetHit? initialHit = HitTest(currentPosition);
318 UpdateMouseHover(initialHit, currentPosition, args.
Tick);
324 ProcessMouseDown(initialHit, currentPosition, mouseButton, args.
Tick);
333 if (args.
ScrollDelta != 0 && initialHit is not
null && IsPointerRoutable(initialHit.Widget, initialHit.View))
335 initialHit.Widget.DispatchMouseWheel(
new WidgetMouseWheelEventArgs(
338 new PointF(currentPosition.X, currentPosition.Y),
347 ProcessMouseUp(currentPosition, mouseButton, args.
Tick);
351 UpdateMouseHover(HitTest(currentPosition), currentPosition, args.
Tick);
354 private void ProcessMouseDown(WidgetHit? hit,
359 if (_mouseCapture is not
null || hit is
null)
362 FocusFromPointer(hit.Widget);
366 hit.Widget.DispatchPointerDown(CreatePointerArgs(hit.Widget,
373 _mouseCapture =
new PointerCapture(hit.Widget,
380 private void ProcessMouseMove(Point previousPosition,
381 Point currentPosition,
384 PointerCapture? capture = GetValidMouseCapture(tick);
385 WidgetHit? hit = capture is
null ? HitTest(currentPosition) : null;
386 WidgetBase? recipient = capture?.Widget ?? hit?.Widget;
387 View? view = capture?.View ?? hit?.View;
389 if (recipient is
null || view is
null)
392 var delta =
new Vector2(currentPosition.X - previousPosition.X,
393 currentPosition.Y - previousPosition.Y);
397 recipient.DispatchPointerMove(CreatePointerArgs(recipient,
405 if (capture is not
null)
406 capture.LastPosition = currentPosition;
409 private void ProcessMouseUp(Point position,
414 PointerCapture? capture = GetValidMouseCapture(tick);
416 if (capture is
null || capture.Button != releasedButton)
419 WidgetBase recipient = capture.Widget;
421 recipient.DispatchPointerUp(CreatePointerArgs(recipient,
428 WidgetHit? releaseHit = HitTest(position);
430 if (IsSameHit(releaseHit, recipient, capture.View) && recipient.GetWrappedHitOffset(capture.View, position) == capture.InstanceOffset)
432 recipient.DispatchPointerClick(CreatePointerArgs(recipient,
441 _mouseCapture =
null;
444 private void UpdateMouseHover(WidgetHit? hit,
448 if (IsSameHit(_hoveredHit, hit))
451 WidgetHit? previous = _hoveredHit;
454 if (previous is not
null)
456 previous.Widget.DispatchPointerLeave(CreatePointerArgs(previous.Widget,
466 hit.Widget.DispatchPointerEnter(CreatePointerArgs(hit.Widget,
475 private PointerCapture? GetValidMouseCapture(
long tick)
477 if (_mouseCapture is
null)
480 if (IsPointerRoutable(_mouseCapture.Widget, _mouseCapture.View))
481 return _mouseCapture;
483 _mouseCapture.Widget.DispatchPointerUp(CreatePointerArgs(_mouseCapture.Widget,
485 _mouseCapture.LastPosition,
486 _mouseCapture.Button,
490 _mouseCapture =
null;
494 private void OnTouchBegan(
object? sender, TouchEventArgs args)
496 if (!_isStarted || _disposed)
501 if (_touchCaptures.ContainsKey(touch.Id))
504 WidgetHit? hit = HitTest(touch.Position);
509 FocusFromPointer(hit.Widget);
511 hit.Widget.DispatchPointerDown(CreatePointerArgs(hit.Widget,
518 _touchCaptures[touch.Id] =
new PointerCapture(hit.Widget,
525 private void OnTouchMoved(
object? sender, TouchEventArgs args)
527 if (!_isStarted || _disposed)
532 if (!_touchCaptures.TryGetValue(touch.Id, out PointerCapture? capture))
535 if (!IsPointerRoutable(capture.Widget, capture.View))
537 capture.Widget.DispatchPointerUp(CreatePointerArgs(capture.Widget,
539 capture.LastPosition,
544 _touchCaptures.Remove(touch.Id);
548 var delta =
new Vector2(touch.Position.X - capture.LastPosition.X,
549 touch.Position.Y - capture.LastPosition.Y);
551 capture.Widget.DispatchPointerMove(CreatePointerArgs(capture.Widget,
559 capture.LastPosition = touch.Position;
562 private void OnTouchEnded(
object? sender, TouchEventArgs args)
564 if (!_isStarted || _disposed)
569 if (!_touchCaptures.Remove(touch.Id, out PointerCapture? capture))
572 WidgetBase recipient = capture.Widget;
574 recipient.DispatchPointerUp(CreatePointerArgs(recipient,
579 touch.Id, instanceOffset: capture.InstanceOffset));
581 WidgetHit? releaseHit = HitTest(touch.Position);
583 if (touch.Phase !=
TouchPhase.Cancelled && IsSameHit(releaseHit, recipient, capture.View) && recipient.GetWrappedHitOffset(capture.View, touch.Position) == capture.InstanceOffset)
585 recipient.DispatchPointerClick(CreatePointerArgs(recipient,
591 clickCount: 1, instanceOffset: capture.InstanceOffset));
595 private WidgetHit? HitTest(Point screenPositionPx)
597 View? view = GetTopmostViewAt(screenPositionPx);
602 WidgetBase[] snapshot;
605 snapshot = [.. _widgets];
607 for (
int index = snapshot.Length - 1; index >= 0; index--)
609 WidgetBase widget = snapshot[index];
611 if (widget.HitTest(view, screenPositionPx))
612 return new WidgetHit(widget, view, widget.GetWrappedHitOffset(view, screenPositionPx) ?? PointF.Empty);
618 private View? GetTopmostViewAt(Point screenPositionPx)
620 if (_renderSurfaceHost.RenderSurfaceAdapter is not
null &&
621 (screenPositionPx.X < 0 || screenPositionPx.Y < 0 ||
622 screenPositionPx.X >= _renderSurfaceHost.Backbuffer.Width ||
623 screenPositionPx.Y >= _renderSurfaceHost.Backbuffer.Height))
626 var views = _renderSurfaceHost.ViewManager.Views;
628 for (
int index = views.Count - 1; index >= 0; index--)
630 View view = views[index];
639 private bool IsRegistered(WidgetBase widget)
642 return _widgets.Contains(widget);
645 private bool IsManagedView(
View view)
647 return _renderSurfaceHost.ViewManager.Views.Contains(view);
650 private bool HasValidTarget(WidgetBase widget,
651 View? routedView =
null)
654 return widget.SceneLayer is not
null;
656 if (widget.View is
null || !IsManagedView(widget.View))
659 return routedView is
null || ReferenceEquals(widget.View, routedView);
662 private bool IsPointerRoutable(WidgetBase widget,
665 return IsRegistered(widget) &&
666 IsManagedView(view) &&
667 ReferenceEquals(widget.RenderSurfaceHost, _renderSurfaceHost) &&
668 HasValidTarget(widget, view) &&
669 widget.IsInputEnabled &&
670 widget.IsPointerInputEnabled &&
674 private bool CanReceiveKeyboardInput(WidgetBase widget)
676 return IsRegistered(widget) &&
677 ReferenceEquals(widget.RenderSurfaceHost, _renderSurfaceHost) &&
678 HasValidTarget(widget) &&
680 widget.IsInputEnabled &&
681 widget.IsKeyboardInputEnabled &&
682 widget.CanReceiveFocus;
685 private void FocusFromPointer(WidgetBase widget)
687 if (widget.IsInputEnabled &&
688 widget.IsKeyboardInputEnabled &&
689 widget.CanReceiveFocus)
695 private void ReleaseInputState(
697 bool dispatchPointerUp)
703 ReleaseKeyboardFocus(widget);
706 private void ReleasePointerState(WidgetBase widget,
707 bool dispatchPointerUp)
709 if (ReferenceEquals(_hoveredHit?.Widget, widget))
712 if (ReferenceEquals(_mouseCapture?.Widget, widget))
714 PointerCapture capture = _mouseCapture;
716 if (dispatchPointerUp)
718 widget.DispatchPointerUp(CreatePointerArgs(widget,
720 capture.LastPosition,
726 _mouseCapture =
null;
729 int[] touchIds = _touchCaptures.Where(x => ReferenceEquals(x.Value.Widget, widget))
733 foreach (
int touchId
in touchIds)
735 PointerCapture capture = _touchCaptures[touchId];
737 if (dispatchPointerUp)
739 widget.DispatchPointerUp(CreatePointerArgs(widget,
741 capture.LastPosition,
744 pointerId: touchId));
747 _touchCaptures.Remove(touchId);
751 private void ReleaseKeyboardFocus(WidgetBase widget)
753 if (ReferenceEquals(_focusedWidget, widget))
757 private static bool IsSameHit(WidgetHit? left,
760 if (left is
null || right is
null)
761 return left is
null && right is
null;
763 return ReferenceEquals(left.Widget, right.Widget)
764 && ReferenceEquals(left.View, right.View);
767 private static bool IsSameHit(WidgetHit? hit,
771 return hit is not
null
772 && ReferenceEquals(hit.Widget, widget)
773 && ReferenceEquals(hit.View, view);
776 private WidgetPointerEventArgs CreatePointerArgs(WidgetBase widget,
783 Vector2 deltaPx =
default,
784 PointF? instanceOffset =
null)
786 return new WidgetPointerEventArgs(widget,
788 new PointF(position.X, position.Y),
795 WrappedOffsetWorldPx = instanceOffset ?? (pointerId ==
MousePointerId && _mouseCapture is { } mouse && ReferenceEquals(mouse.Widget, widget)
796 ? mouse.InstanceOffset
797 : _touchCaptures.TryGetValue(pointerId, out var touch) && ReferenceEquals(touch.Widget, widget)
798 ? touch.InstanceOffset
799 : widget.GetWrappedHitOffset(view, position) ?? PointF.Empty)
810 _ => WidgetPointerButtonEnum.None
814 private void OnWidgetDisposing(
object? sender, IDirectDrawable drawing)
816 if (drawing is WidgetBase widget)
823 bool dispatchPointerUp)
828 removed = _widgets.Remove(widget);
833 widget.Disposing -= OnWidgetDisposing;
835 ReleaseInputState(widget, dispatchPointerUp);
838 private sealed class WidgetHit
846 public WidgetHit(WidgetBase widget,
848 PointF instanceOffset)
852 InstanceOffset = instanceOffset;
858 public PointF InstanceOffset {
get; }
863 public WidgetBase Widget {
get; }
868 public View View {
get; }
871 private sealed class PointerCapture
881 public PointerCapture(WidgetBase widget,
885 PointF instanceOffset)
890 LastPosition = lastPosition;
891 InstanceOffset = instanceOffset;
897 public WidgetBase Widget {
get; }
902 public View View {
get; }
915 public Point LastPosition {
get;
set; }
919 public PointF InstanceOffset {
get; }