Files
RobustToolbox/Robust.Client/UserInterface/UserInterfaceManager.Input.cs
metalgearsloth ab6bd19817 Fix mouse hover not updating for new controls (#5313)
* Fix mouse hover not updating for new controls

It only ever updated on movement previously. The issue is for example if a new window shows up where the mouse is (or any control) it doesn't handle it. Just checking it every frame AFAIK shouldn't be that expensive. Worst case we just have some flag to check it that gets set on <mouse movement OR controls changed>.

* review
2024-08-31 11:45:59 +10:00

566 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface;
internal partial class UserInterfaceManager
{
private float _tooltipTimer;
private ICursor? _worldCursor;
private bool _needUpdateActiveCursor;
[ViewVariables] public Control? KeyboardFocused { get; private set; }
[ViewVariables] public Control? CurrentlyHovered { get; private set; }
private Control? _controlFocused;
[ViewVariables]
public Control? ControlFocused
{
get => _controlFocused;
set
{
if (_controlFocused == value)
return;
_controlFocused?.ControlFocusExited();
_controlFocused = value;
}
}
// set to null when not counting down
private float? _tooltipDelay;
private bool _showingTooltip;
private Control? _suppliedTooltip;
private const float TooltipDelay = 0.25f;
private readonly Dictionary<BoundKeyFunction, Control> _focusedControls = new();
private WindowRoot? _focusedRoot;
private static (Control control, Vector2 rel)? MouseFindControlAtPos(Control control, Vector2 position)
{
for (var i = control.ChildCount - 1; i >= 0; i--)
{
var child = control.GetChild(i);
if (!child.Visible || child.RectClipContent && !child.PixelRect.Contains((Vector2i) position))
{
continue;
}
var maybeFoundOnChild = MouseFindControlAtPos(child, position - child.PixelPosition);
if (maybeFoundOnChild != null)
{
return maybeFoundOnChild;
}
}
if (control.MouseFilter != Control.MouseFilterMode.Ignore && control.HasPoint(position / control.UIScale))
{
return (control, position);
}
return null;
}
public void KeyBindDown(BoundKeyEventArgs args)
{
if (args.Function == EngineKeyFunctions.CloseModals && _modalStack.Count != 0)
{
bool closedAny = false;
for (var i = _modalStack.Count - 1; i >= 0; i--)
{
var top = _modalStack[i];
if (top is not Popup {CloseOnEscape: false})
{
RemoveModal(top);
closedAny = true;
}
}
if (closedAny)
{
args.Handle();
}
return;
}
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
if (control == null)
{
return;
}
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
args.PointerLocation.Position - control.GlobalPixelPosition);
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindDown(ev));
if (guiArgs.Handled)
{
args.Handle();
}
// Attempt to ensure that keybind-up events get raised after a keybind-down.
DebugTools.Assert(!_focusedControls.TryGetValue(args.Function, out var existing)
|| !existing.VisibleInTree
|| args.IsRepeat && existing == control);
_focusedControls[args.Function] = control;
OnKeyBindDown?.Invoke(control);
}
public void KeyBindUp(BoundKeyEventArgs args)
{
// Only raise keybind-up for the control on which we previously raised keybind-down
if (!_focusedControls.Remove(args.Function, out var control) || !control.VisibleInTree)
return;
var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus,
args.PointerLocation.Position / control.UIScale - control.GlobalPosition,
args.PointerLocation.Position - control.GlobalPixelPosition);
_doGuiInput(control, guiArgs, (c, ev) => c.KeyBindUp(ev));
// Always mark this as handled.
// The only case it should not be is if we do not have a control to click on,
// in which case we never reach this.
args.Handle();
}
public void MouseMove(MouseMoveEventArgs mouseMoveEventArgs)
{
_resetTooltipTimer();
// Update which control is considered hovered.
var newHovered = MouseGetControl(mouseMoveEventArgs.Position);
SetHovered(newHovered);
var target = ControlFocused ?? CurrentlyHovered;
if (target != null)
{
var pos = mouseMoveEventArgs.Position.Position;
var guiArgs = new GUIMouseMoveEventArgs(mouseMoveEventArgs.Relative / target.UIScale,
target,
pos / target.UIScale, mouseMoveEventArgs.Position,
pos / target.UIScale - target.GlobalPosition,
pos - target.GlobalPixelPosition);
_doMouseGuiInput(target, guiArgs, (c, ev) => c.MouseMove(ev));
}
}
public void UpdateHovered()
{
var ctrl = MouseGetControl(_inputManager.MouseScreenPosition);
SetHovered(ctrl);
}
public void SetHovered(Control? control)
{
if (control == CurrentlyHovered)
return;
_clearTooltip();
CurrentlyHovered?.MouseExited();
CurrentlyHovered = control;
CurrentlyHovered?.MouseEntered();
if (CurrentlyHovered != null)
{
_tooltipDelay = CurrentlyHovered.TooltipDelay ?? TooltipDelay;
}
else
{
_tooltipDelay = null;
}
_needUpdateActiveCursor = true;
}
private void UpdateActiveCursor()
{
// Consider mouse input focus first so that dragging windows don't act up etc.
var cursorTarget = ControlFocused ?? CurrentlyHovered;
if (cursorTarget == null)
{
_clyde.SetCursor(_worldCursor);
return;
}
if (cursorTarget.CustomCursorShape != null)
{
_clyde.SetCursor(cursorTarget.CustomCursorShape);
return;
}
var shape = cursorTarget.DefaultCursorShape switch
{
Control.CursorShape.Arrow => StandardCursorShape.Arrow,
Control.CursorShape.IBeam => StandardCursorShape.IBeam,
Control.CursorShape.Hand => StandardCursorShape.Hand,
Control.CursorShape.Crosshair => StandardCursorShape.Crosshair,
Control.CursorShape.VResize => StandardCursorShape.VResize,
Control.CursorShape.HResize => StandardCursorShape.HResize,
_ => StandardCursorShape.Arrow
};
_clyde.SetCursor(_clyde.GetStandardCursor(shape));
}
public void MouseWheel(MouseWheelEventArgs args)
{
var control = MouseGetControl(args.Position);
if (control == null)
{
return;
}
args.Handle();
var pos = args.Position.Position;
var guiArgs = new GUIMouseWheelEventArgs(args.Delta, control,
pos / control.UIScale, args.Position,
pos / control.UIScale - control.GlobalPosition, pos - control.GlobalPixelPosition);
_doMouseGuiInput(control, guiArgs, (c, ev) => c.MouseWheel(ev), true);
}
public void TextEntered(TextEnteredEventArgs textEnteredEvent)
{
if (KeyboardFocused == null)
{
return;
}
var guiArgs = new GUITextEnteredEventArgs(KeyboardFocused, textEnteredEvent);
KeyboardFocused.TextEntered(guiArgs);
}
public void TextEditing(TextEditingEventArgs textEvent)
{
if (KeyboardFocused == null)
{
return;
}
var guiArgs = new GUITextEditingEventArgs(KeyboardFocused, textEvent);
KeyboardFocused.TextEditing(guiArgs);
}
public ScreenCoordinates MousePositionScaled => ScreenToUIPosition(_inputManager.MouseScreenPosition);
private static void _doMouseGuiInput<T>(Control? control, T guiEvent, Action<Control, T> action,
bool ignoreStop = false)
where T : GUIMouseEventArgs
{
while (control != null)
{
guiEvent.SourceControl = control;
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
{
action(control, guiEvent);
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
{
break;
}
}
guiEvent.RelativePosition += control.Position;
guiEvent.RelativePixelPosition += control.PixelPosition;
control = control.Parent;
}
}
private static void _doGuiInput(
Control? control,
GUIBoundKeyEventArgs guiEvent,
Action<Control, GUIBoundKeyEventArgs> action,
bool ignoreStop = false)
{
while (control != null)
{
if (control.MouseFilter != Control.MouseFilterMode.Ignore)
{
action(control, guiEvent);
if (guiEvent.Handled || (!ignoreStop && control.MouseFilter == Control.MouseFilterMode.Stop))
{
break;
}
}
guiEvent.RelativePosition += control.Position;
guiEvent.RelativePixelPosition += control.PixelPosition;
control = control.Parent;
}
}
private void _clearTooltip()
{
if (!_showingTooltip) return;
if (_suppliedTooltip != null)
{
PopupRoot.RemoveChild(_suppliedTooltip);
_suppliedTooltip = null;
}
CurrentlyHovered?.PerformHideTooltip();
_resetTooltipTimer();
_showingTooltip = false;
}
public void CursorChanged(Control control)
{
if (control == ControlFocused || control == CurrentlyHovered)
{
_needUpdateActiveCursor = true;
}
}
public void HideTooltipFor(Control control)
{
if (CurrentlyHovered == control)
{
_clearTooltip();
}
}
public bool HandleCanFocusDown(
ScreenCoordinates pointerPosition,
[NotNullWhen(true)] out (Control control, Vector2i rel)? hitData)
{
var hit = MouseGetControlAndRel(pointerPosition);
var pos = pointerPosition.Position;
// If we have a modal open and the mouse down was outside it, close said modal.
for (var i = _modalStack.Count - 1; i >= 0; i--)
{
var top = _modalStack[i];
var offset = pos - top.GlobalPixelPosition;
if (!top.HasPoint(offset / top.UIScale))
{
if (top.MouseFilter != Control.MouseFilterMode.Stop)
{
if (top is not Popup {CloseOnClick: false})
{
RemoveModal(top);
}
}
else
{
ControlFocused = top;
hitData = null;
return false; // prevent anything besides the top modal control from receiving input
}
}
else
{
break;
}
}
if (hit == null)
{
ReleaseKeyboardFocus();
hitData = null;
return false;
}
var (control, rel) = hit.Value;
if (control != KeyboardFocused)
{
ReleaseKeyboardFocus();
}
ControlFocused = control;
if (ControlFocused.CanKeyboardFocus && ControlFocused.KeyboardFocusOnClick)
{
ControlFocused.GrabKeyboardFocus();
}
hitData = (control, (Vector2i) rel);
return true;
}
public void HandleCanFocusUp()
{
ControlFocused = null;
}
public ScreenCoordinates ScreenToUIPosition(ScreenCoordinates coordinates)
{
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
return default;
return new ScreenCoordinates(coordinates.Position / root.UIScale, coordinates.Window);
}
public ICursor? WorldCursor
{
get => _worldCursor;
set
{
_worldCursor = value;
_needUpdateActiveCursor = true;
}
}
private (Control control, Vector2 rel)? MouseGetControlAndRel(ScreenCoordinates coordinates)
{
if (!_windowsToRoot.TryGetValue(coordinates.Window, out var root))
return null;
return MouseFindControlAtPos(root, coordinates.Position);
}
public Control? MouseGetControl(ScreenCoordinates coordinates)
{
return MouseGetControlAndRel(coordinates)?.control;
}
public Control? GetSuppliedTooltipFor(Control control)
{
return CurrentlyHovered == control ? _suppliedTooltip : null;
}
/// <summary>
/// Converts
/// </summary>
/// <param name="args">Event data values for a bound key state change.</param>
private bool OnUIKeyBindStateChanged(BoundKeyEventArgs args)
{
if (args.State == BoundKeyState.Down)
{
KeyBindDown(args);
}
else
{
KeyBindUp(args);
}
// If we are in a focused control or doing a CanFocus, return true
// So that InputManager doesn't propagate events to simulation.
if (!args.CanFocus && KeyboardFocused != null)
{
return true;
}
return false;
}
/// <inheritdoc />
public void GrabKeyboardFocus(Control control)
{
if (control == null)
{
throw new ArgumentNullException(nameof(control));
}
if (!control.CanKeyboardFocus)
{
throw new ArgumentException("Control cannot get keyboard focus.", nameof(control));
}
if (control == KeyboardFocused)
{
return;
}
ReleaseKeyboardFocus();
KeyboardFocused = control;
KeyboardFocused.KeyboardFocusEntered();
}
public void ReleaseKeyboardFocus()
{
var oldFocused = KeyboardFocused;
oldFocused?.KeyboardFocusExited();
KeyboardFocused = null;
}
public void ReleaseKeyboardFocus(Control ifControl)
{
if (ifControl == null)
{
throw new ArgumentNullException(nameof(ifControl));
}
if (ifControl == KeyboardFocused)
{
ReleaseKeyboardFocus();
}
}
private void _resetTooltipTimer()
{
_tooltipTimer = 0;
}
private void _showTooltip()
{
if (_showingTooltip) return;
_showingTooltip = true;
var hovered = CurrentlyHovered;
if (hovered == null)
{
return;
}
// show supplied tooltip if there is one
if (hovered.TooltipSupplier != null)
{
_suppliedTooltip = hovered.TooltipSupplier.Invoke(hovered);
}
else if (!String.IsNullOrWhiteSpace(hovered.ToolTip))
{
// show simple tooltip if there is one
var tooltip = new Tooltip()
{
Text = hovered.ToolTip,
Tracking = hovered.TrackingTooltip,
};
_suppliedTooltip = tooltip;
}
if (_suppliedTooltip == null)
return;
PopupRoot.AddChild(_suppliedTooltip);
Tooltips.PositionTooltip(_suppliedTooltip);
hovered.PerformShowTooltip();
}
public Vector2? CalcRelativeMousePositionFor(Control control, ScreenCoordinates mousePosScaled)
{
var (pos, window) = mousePosScaled;
var root = control.Root;
if (root?.Window == null || root.Window.Id != window)
return null;
return pos - control.GlobalPosition;
}
}