using System; using System.Collections.Generic; using Robust.Client.Console; using Robust.Client.Input; using Robust.Client.Interfaces.Graphics; using Robust.Client.Interfaces.Graphics.ClientEye; using Robust.Client.Interfaces.Input; using Robust.Client.Interfaces.State; using Robust.Client.Interfaces.UserInterface; using Robust.Client.Player; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Configuration; using Robust.Shared.Input; using Robust.Shared.Interfaces.Configuration; using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Resources; using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Timing; namespace Robust.Client.UserInterface { internal sealed class UserInterfaceManager : IDisposable, IUserInterfaceManagerInternal, IPostInjectInit { [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IClyde _displayManager = default!; [Dependency] private readonly IClientConsole _console = default!; [Dependency] private readonly IResourceManager _resourceManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IClientNetManager _netManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!; public UITheme ThemeDefaults { get; private set; } = default!; public Stylesheet? Stylesheet { get => _stylesheet; set { _stylesheet = value; if (RootControl?.Stylesheet != null) { RootControl.StylesheetUpdateRecursive(); } } } public Control? KeyboardFocused { get; private set; } // When a control receives a mouse down it must also receive a mouse up and mouse moves, always. // So we keep track of which control is "focused" by the mouse. private Control? _controlFocused; public LayoutContainer StateRoot { get; private set; } = default!; public PopupContainer ModalRoot { get; private set; } = default!; public Control? CurrentlyHovered { get; private set; } = default!; public float UIScale { get; private set; } = 1; public Control RootControl { get; private set; } = default!; public LayoutContainer WindowRoot { get; private set; } = default!; public LayoutContainer PopupRoot { get; private set; } = default!; public DebugConsole DebugConsole { get; private set; } = default!; public IDebugMonitors DebugMonitors => _debugMonitors; private DebugMonitors _debugMonitors = default!; private readonly List _modalStack = new List(); private bool _rendering = true; private float _tooltipTimer; private Tooltip _tooltip = default!; private const float TooltipDelay = 1; private readonly Queue _styleUpdateQueue = new Queue(); private readonly Queue _layoutUpdateQueue = new Queue(); private Stylesheet? _stylesheet; private ICursor? _worldCursor; private bool _needUpdateActiveCursor; public void Initialize() { UIScale = _configurationManager.GetCVar("display.uiScale"); ThemeDefaults = new UIThemeDummy(); _initializeCommon(); DebugConsole = new DebugConsole(_console, _resourceManager); RootControl.AddChild(DebugConsole); _debugMonitors = new DebugMonitors(_gameTiming, _playerManager, _eyeManager, _inputManager, _stateManager, _displayManager, _netManager, _mapManager); RootControl.AddChild(_debugMonitors); _inputManager.SetInputCommand(EngineKeyFunctions.ShowDebugConsole, InputCmdHandler.FromDelegate(session => DebugConsole.Toggle())); _inputManager.SetInputCommand(EngineKeyFunctions.ShowDebugMonitors, InputCmdHandler.FromDelegate(enabled: session => { DebugMonitors.Visible = true; }, disabled: session => { DebugMonitors.Visible = false; })); _inputManager.SetInputCommand(EngineKeyFunctions.HideUI, InputCmdHandler.FromDelegate( enabled: session => _rendering = false, disabled: session => _rendering = true)); _inputManager.UIKeyBindStateChanged += OnUIKeyBindStateChanged; } private void _initializeCommon() { RootControl = new Control { Name = "UIRoot", MouseFilter = Control.MouseFilterMode.Ignore, IsInsideTree = true }; RootControl.Size = _displayManager.ScreenSize / UIScale; _displayManager.OnWindowResized += args => _updateRootSize(); StateRoot = new LayoutContainer { Name = "StateRoot", MouseFilter = Control.MouseFilterMode.Ignore }; RootControl.AddChild(StateRoot); WindowRoot = new LayoutContainer { Name = "WindowRoot", MouseFilter = Control.MouseFilterMode.Ignore }; RootControl.AddChild(WindowRoot); PopupRoot = new LayoutContainer { Name = "PopupRoot", MouseFilter = Control.MouseFilterMode.Ignore }; RootControl.AddChild(PopupRoot); ModalRoot = new PopupContainer { Name = "ModalRoot", MouseFilter = Control.MouseFilterMode.Ignore, }; RootControl.AddChild(ModalRoot); _tooltip = new Tooltip(); PopupRoot.AddChild(_tooltip); _tooltip.Visible = false; } public void InitializeTesting() { ThemeDefaults = new UIThemeDummy(); _initializeCommon(); } public void Dispose() { RootControl?.Dispose(); } public void Update(FrameEventArgs args) { RootControl.DoUpdate(args); } public void FrameUpdate(FrameEventArgs args) { RootControl.DoFrameUpdate(args); // Process queued style & layout updates. while (_styleUpdateQueue.Count != 0) { var control = _styleUpdateQueue.Dequeue(); if (control.Disposed) { continue; } control.DoStyleUpdate(); } while (_layoutUpdateQueue.Count != 0) { var control = _layoutUpdateQueue.Dequeue(); if (control.Disposed) { continue; } control.DoLayoutUpdate(); } _tooltipTimer -= args.DeltaSeconds; if (_tooltipTimer <= 0) { _showTooltip(); } if (_needUpdateActiveCursor) { _needUpdateActiveCursor = false; UpdateActiveCursor(); } } public bool HandleCanFocusDown(Vector2 pointerPosition) { var control = MouseGetControl(pointerPosition); // If we have a modal open and the mouse down was outside it, close said modal. while (_modalStack.Count != 0) { var top = _modalStack[^1]; var offset = pointerPosition - top.GlobalPixelPosition; if (!top.HasPoint(offset / UIScale)) { RemoveModal(top); } else { break; } } ReleaseKeyboardFocus(); if (control == null) { return false; } _controlFocused = control; if (_controlFocused.CanKeyboardFocus && _controlFocused.KeyboardFocusOnClick) { _controlFocused.GrabKeyboardFocus(); } return true; } public void HandleCanFocusUp() { _controlFocused = null; } public void KeyBindDown(BoundKeyEventArgs args) { if (args.Function == EngineKeyFunctions.CloseModals && _modalStack.Count != 0) { while (_modalStack.Count > 0) { var top = _modalStack[^1]; RemoveModal(top); } args.Handle(); return; } var control = KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position); if (control == null) { return; } var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus, args.PointerLocation.Position / UIScale - control.GlobalPosition, args.PointerLocation.Position - control.GlobalPixelPosition); _doGuiInput(control, guiArgs, (c, ev) => c.KeyBindDown(ev)); if (guiArgs.Handled) { args.Handle(); } } public void KeyBindUp(BoundKeyEventArgs args) { var control = _controlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation.Position); if (control == null) { return; } var guiArgs = new GUIBoundKeyEventArgs(args.Function, args.State, args.PointerLocation, args.CanFocus, args.PointerLocation.Position / 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); if (newHovered != CurrentlyHovered) { _clearTooltip(); CurrentlyHovered?.MouseExited(); CurrentlyHovered = newHovered; CurrentlyHovered?.MouseEntered(); _needUpdateActiveCursor = true; } var target = _controlFocused ?? newHovered; if (target != null) { var guiArgs = new GUIMouseMoveEventArgs(mouseMoveEventArgs.Relative / UIScale, target, mouseMoveEventArgs.Position / UIScale, mouseMoveEventArgs.Position, mouseMoveEventArgs.Position / UIScale - target.GlobalPosition, mouseMoveEventArgs.Position - target.GlobalPixelPosition); _doMouseGuiInput(target, guiArgs, (c, ev) => c.MouseMove(ev)); } } private void UpdateActiveCursor() { // Consider mouse input focus first so that dragging windows don't act up etc. var cursorTarget = _controlFocused ?? CurrentlyHovered; if (cursorTarget == null) { _displayManager.SetCursor(_worldCursor); return; } if (cursorTarget.CustomCursorShape != null) { _displayManager.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 }; _displayManager.SetCursor(_displayManager.GetStandardCursor(shape)); } public void MouseWheel(MouseWheelEventArgs args) { var control = MouseGetControl(args.Position); if (control == null) { return; } args.Handle(); var guiArgs = new GUIMouseWheelEventArgs(args.Delta, control, args.Position / UIScale, args.Position, args.Position / UIScale - control.GlobalPosition, args.Position - control.GlobalPixelPosition); _doMouseGuiInput(control, guiArgs, (c, ev) => c.MouseWheel(ev), true); } public void TextEntered(TextEventArgs textEvent) { if (KeyboardFocused == null) { return; } var guiArgs = new GUITextEventArgs(KeyboardFocused, textEvent.CodePoint); KeyboardFocused.TextEntered(guiArgs); } public void DisposeAllComponents() { RootControl.DisposeAllChildren(); } public void Popup(string contents, string title = "Alert!") { var popup = new SS14Window { Title = title }; popup.Contents.AddChild(new Label {Text = contents}); popup.OpenCenteredMinSize(); } public Control? MouseGetControl(Vector2 coordinates) { return _mouseFindControlAtPos(RootControl, coordinates); } public Vector2 MousePositionScaled => ScreenToUIPosition(_inputManager.MouseScreenPosition); public Vector2 ScreenToUIPosition(Vector2 position) { return position / UIScale; } public Vector2 ScreenToUIPosition(ScreenCoordinates coordinates) { return ScreenToUIPosition(coordinates.Position); } /// 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.FocusEntered(); } public void ReleaseKeyboardFocus() { var oldFocused = KeyboardFocused; oldFocused?.FocusExited(); KeyboardFocused = null; } public void ReleaseKeyboardFocus(Control ifControl) { if (ifControl == null) { throw new ArgumentNullException(nameof(ifControl)); } if (ifControl == KeyboardFocused) { ReleaseKeyboardFocus(); } } public ICursor? WorldCursor { get => _worldCursor; set { _worldCursor = value; _needUpdateActiveCursor = true; } } public void ControlHidden(Control control) { // Does the same thing but it could later be changed so.. ControlRemovedFromTree(control); } public void ControlRemovedFromTree(Control control) { ReleaseKeyboardFocus(control); RemoveModal(control); if (control == CurrentlyHovered) { control.MouseExited(); CurrentlyHovered = null; } if (control == _controlFocused) { _controlFocused = null; } } public void PushModal(Control modal) { _modalStack.Add(modal); } public void RemoveModal(Control modal) { if (_modalStack.Remove(modal)) { modal.ModalRemoved(); } } public void Render(IRenderHandle renderHandle) { if (!_rendering) { return; } _render(renderHandle, RootControl, Vector2i.Zero, Color.White, null); } public void QueueStyleUpdate(Control control) { _styleUpdateQueue.Enqueue(control); } public void QueueLayoutUpdate(Control control) { _layoutUpdateQueue.Enqueue(control); } public void CursorChanged(Control control) { if (control == _controlFocused || control == CurrentlyHovered) { _needUpdateActiveCursor = true; } } private static void _render(IRenderHandle renderHandle, Control control, Vector2i position, Color modulate, UIBox2i? scissorBox) { if (!control.Visible) { return; } // Manual clip test with scissor region as optimization. var controlBox = UIBox2i.FromDimensions(position, control.PixelSize); if (scissorBox != null) { var clipMargin = control.RectDrawClipMargin; var clipTestBox = new UIBox2i(controlBox.Left - clipMargin, controlBox.Top - clipMargin, controlBox.Right + clipMargin, controlBox.Bottom + clipMargin); if (!scissorBox.Value.Intersects(clipTestBox)) { return; } } var handle = renderHandle.DrawingHandleScreen; handle.SetTransform(position, Angle.Zero, Vector2.One); modulate *= control.Modulate; handle.Modulate = modulate * control.ActualModulateSelf; var clip = control.RectClipContent; var scissorRegion = scissorBox; if (clip) { scissorRegion = controlBox; if (scissorBox != null) { // Make the final scissor region a sub region of scissorBox var s = scissorBox.Value; var result = s.Intersection(scissorRegion.Value); if (result == null) { // Uhm... No intersection so... don't draw anything at all? return; } scissorRegion = result.Value; } renderHandle.SetScissor(scissorRegion); } control.DrawInternal(renderHandle); handle.UseShader(null); foreach (var child in control.Children) { _render(renderHandle, child, position + child.PixelPosition, modulate, scissorRegion); } if (clip) { renderHandle.SetScissor(scissorBox); } } private Control? _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 / UIScale)) { return control; } return null; } private static void _doMouseGuiInput(Control? control, T guiEvent, Action 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, T guiEvent, Action action, bool ignoreStop = false) where T : GUIBoundKeyEventArgs { 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() { _tooltip.Visible = false; _resetTooltipTimer(); } private void _resetTooltipTimer() { _tooltipTimer = TooltipDelay; } private void _showTooltip() { var hovered = CurrentlyHovered; if (hovered == null || string.IsNullOrWhiteSpace(hovered.ToolTip)) { return; } _tooltip.Visible = true; _tooltip.Text = hovered.ToolTip; LayoutContainer.SetPosition(_tooltip, MousePositionScaled); var (right, bottom) = _tooltip.Position + _tooltip.Size; if (right > RootControl.Size.X) { LayoutContainer.SetPosition(_tooltip, (RootControl.Size.X - _tooltip.Size.X, _tooltip.Position.Y)); } if (bottom > RootControl.Size.Y) { LayoutContainer.SetPosition(_tooltip, (_tooltip.Position.X, RootControl.Size.Y - _tooltip.Size.Y)); } } void IPostInjectInit.PostInject() { _configurationManager.RegisterCVar("display.uiScale", 1f, CVar.ARCHIVE, _uiScaleChanged); } private void _uiScaleChanged(float newValue) { UIScale = newValue; if (RootControl == null) { return; } _propagateUIScaleChanged(RootControl); _updateRootSize(); } private static void _propagateUIScaleChanged(Control control) { control.UIScaleChanged(); foreach (var child in control.Children) { _propagateUIScaleChanged(child); } } private void _updateRootSize() { RootControl.Size = _displayManager.ScreenSize / UIScale; } /// /// Converts /// /// Event data values for a bound key state change. private void OnUIKeyBindStateChanged(BoundKeyEventArgs args) { if (!args.CanFocus && KeyboardFocused != null) { args.Handle(); } if (args.State == BoundKeyState.Down) { KeyBindDown(args); } else { KeyBindUp(args); } } } }