using System; using System.Collections.Generic; using System.Numerics; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.ResourceManagement; using Robust.Client.State; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls.DebugMonitorControls; using Robust.Client.UserInterface.Stylesheets; using Robust.Shared; using Robust.Shared.Audio.Sources; using Robust.Shared.Configuration; using Robust.Shared.Exceptions; using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.Input.Binding; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Profiling; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Timing; using Robust.Shared.ViewVariables; namespace Robust.Client.UserInterface { internal sealed partial class UserInterfaceManager : IUserInterfaceManagerInternal { /// /// A type that will always be instantiated anyways. /// public static readonly Type XamlHotReloadWarmupType = typeof(DropDownDebugConsole); [Dependency] private IDependencyCollection _rootDependencies = default!; [Dependency] private IInputManager _inputManager = default!; [Dependency] private IFontManager _fontManager = default!; [Dependency] private IClydeInternal _clyde = default!; [Dependency] private IResourceCache _resourceCache = default!; [Dependency] private IEyeManager _eyeManager = default!; [Dependency] private IStateManager _stateManager = default!; [Dependency] private IPrototypeManager _protoManager = default!; [Dependency] private IUserInterfaceManagerInternal _userInterfaceManager = default!; [Dependency] private IDynamicTypeFactoryInternal _typeFactory = default!; [Dependency] private IUserInterfaceManager _uiManager = default!; [Dependency] private IConfigurationManager _configurationManager = default!; [Dependency] private ProfManager _prof = default!; [Dependency] private IReflectionManager _reflectionManager = default!; [Dependency] private IEntitySystemManager _systemManager = default!; [Dependency] private ILogManager _logManager = default!; [Dependency] private IRuntimeLog _runtime = default!; [Dependency] private IClipboardManager _clipboard = null!; private IAudioSource? _clickSource; private IAudioSource? _hoverSource; /// /// Upper limit on the number of times that controls can be measured / arranged each tick before being deferred /// to the next frame update. This is just meant to prevent infinite loops from completely locking up the UI. /// public const int ControlUpdateLimit = 25_000; private bool _obeyUpdateLimits = true; [ViewVariables] public InterfaceTheme ThemeDefaults { get; private set; } = default!; [ViewVariables] public Stylesheet? Stylesheet { get => _stylesheet; set { _stylesheet = value; foreach (var root in _roots) { if (root.Stylesheet == null) { root.StylesheetUpdateRecursive(); } } } } [ViewVariables] public ViewportContainer MainViewport { get; private set; } = default!; [ViewVariables] public LayoutContainer StateRoot { get; private set; } = default!; [ViewVariables] public PopupContainer ModalRoot => RootControl.ModalRoot; [ViewVariables] public WindowRoot RootControl { get; private set; } = default!; [ViewVariables] public LayoutContainer WindowRoot { get; private set; } = default!; [ViewVariables] public LayoutContainer PopupRoot => RootControl.PopupRoot; [ViewVariables] public DropDownDebugConsole DebugConsole { get; private set; } = default!; [ViewVariables] public IDebugMonitors DebugMonitors => _debugMonitors; private DebugMonitors _debugMonitors = default!; private bool _rendering = true; private readonly Queue _deferQueue = new(); private readonly Queue _styleUpdateQueue = new(); private readonly Queue _measureUpdateQueue = new(); private readonly Queue _arrangeUpdateQueue = new(); private Stylesheet? _stylesheet; private ISawmill _sawmillUI = default!; public ISawmill ControlSawmill { get; private set; } = default!; public event Action? OnKeyBindDown; public void Initialize() { _dependencies = new DependencyCollection(_rootDependencies); _configurationManager.OnValueChanged(CVars.DisplayUIScale, _uiScaleChanged, true); #if DEBUG _configurationManager.OnValueChanged(CVars.UIObeyUpdateLimits, _uiObeyUpdateLimitsChanged, true); #endif ThemeDefaults = new InterfaceThemeDummy(); _initScaling(); SetupControllers(); _initializeCommon(); DebugConsole = new DropDownDebugConsole(); RootControl.AddChild(DebugConsole); DebugConsole.SetPositionInParent(RootControl.ModalRoot.GetPositionInParent()); _debugMonitors = new DebugMonitors(); _rootDependencies.InjectDependencies(_debugMonitors); _debugMonitors.Init(); DebugConsole.BelowConsole.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; _inputManager.CheckUIIsFocused += OnIsUIFocused; _initThemes(); _stylesheet = new DefaultStylesheet(_resourceCache, this).Stylesheet; } private void _uiObeyUpdateLimitsChanged(bool newValue, in CVarChangeInfo info) { _obeyUpdateLimits = newValue; } public void PostInitialize() { _initializeScreens(); InitializeControllers(); } private void _initializeCommon() { _sawmillUI = _logManager.GetSawmill("ui"); ControlSawmill = _logManager.GetSawmill("ctrl"); RootControl = CreateWindowRoot(_clyde.MainWindow); RootControl.Name = "MainWindowRoot"; RootControl.DisableAutoScaling = false; _clyde.DestroyWindow += WindowDestroyed; _clyde.OnWindowFocused += ClydeOnWindowFocused; MainViewport = new MainViewportContainer(_eyeManager) { Name = "MainViewport" }; RootControl.AddChild(MainViewport); StateRoot = new LayoutContainer { Name = "StateRoot", MouseFilter = Control.MouseFilterMode.Ignore }; RootControl.AddChild(StateRoot); WindowRoot = new LayoutContainer { Name = "WindowRoot", MouseFilter = Control.MouseFilterMode.Ignore }; RootControl.AddChild(WindowRoot); RootControl.CreateRootControls(); } public void InitializeTesting() { ThemeDefaults = new InterfaceThemeDummy(); _initializeCommon(); } public event Action? OnPostDrawUIRoot; public void DeferAction(Action action) { _deferQueue.Enqueue(action); } private void WindowDestroyed(WindowDestroyedEventArgs args) { DestroyWindowRoot(args.Window); } public void FrameUpdate(FrameEventArgs args) { using (_prof.Group("Update")) { // Update hovered. Can't rely upon mouse movement due to New controls potentially coming up. UpdateHovered(); foreach (var root in _roots) { CheckRootUIScaleUpdate(root); using (_prof.Group("Root")) { var totalUpdated = root.DoFrameUpdateRecursive(args); _prof.WriteValue("Total", ProfData.Int32(totalUpdated)); } } } // Process queued style & layout updates. using (_prof.Group("Style")) { var total = 0; while (_styleUpdateQueue.Count != 0) { if (total >= ControlUpdateLimit && _obeyUpdateLimits) { _sawmillUI.Warning($"Hit style update limit. Queued: {_styleUpdateQueue.Count}. Next in queue: {_styleUpdateQueue.Peek()}. Parent: {_styleUpdateQueue.Peek().Parent}"); break; } var control = _styleUpdateQueue.Dequeue(); if (control.Disposed) continue; control.DoStyleUpdate(); total += 1; } _prof.WriteValue("Total", ProfData.Int32(total)); } using (_prof.Group("Measure")) { var total = 0; while (_measureUpdateQueue.Count != 0) { if (total >= ControlUpdateLimit && _obeyUpdateLimits) { _sawmillUI.Warning($"Hit measure update limit. Queued: {_measureUpdateQueue.Count}. Next in queue: {_measureUpdateQueue.Peek()}. Parent: {_measureUpdateQueue.Peek().Parent}"); break; } var control = _measureUpdateQueue.Dequeue(); if (control.Disposed) continue; RunMeasure(control); if (!control.IsMeasureValid && control.IsInsideTree) _sawmillUI.Warning($"Control's measure is invalid after measuring. Control: {control}. Parent: {control.Parent}."); control.InvalidateArrange(); total += 1; } _prof.WriteValue("Total", ProfData.Int32(total)); } using (_prof.Group("Arrange")) { var total = 0; while (_arrangeUpdateQueue.Count != 0) { if (total >= ControlUpdateLimit && _obeyUpdateLimits) { _sawmillUI.Warning($"Hit arrange update limit. Queued: {_arrangeUpdateQueue.Count}. Next in queue: {_arrangeUpdateQueue.Peek()}. Parent: {_arrangeUpdateQueue.Peek().Parent}"); break; } var control = _arrangeUpdateQueue.Dequeue(); if (control.Disposed) continue; RunArrange(control); if (!control.IsArrangeValid && control.IsInsideTree) _sawmillUI.Warning($"Control's arrangement is invalid after arranging. Control: {control}. Parent: {control.Parent}."); total += 1; } _prof.WriteValue("Total", ProfData.Int32(total)); } using (_prof.Group("Controllers")) { UpdateControllers(args); } // count down tooltip delay if we're not showing one yet and // are hovering the mouse over a control without moving it if (_tooltipDelay != null && !_showingTooltip) { _tooltipTimer += args.DeltaSeconds; if (_tooltipTimer >= _tooltipDelay) { _showTooltip(); } } if (_needUpdateActiveCursor) { _needUpdateActiveCursor = false; UpdateActiveCursor(); } using (_prof.Group("Deferred actions")) { while (_deferQueue.TryDequeue(out var action)) { action(); } } } public void RenderControl(in Control.ControlRenderArguments args, Control control) { var _ = 0; RenderControl(args.Handle, ref _, control, args.Position, args.Modulate, args.ScissorBox, args.CoordinateTransform); } public void RenderControl(IRenderHandle handle, Control control, Vector2i position) { var _ = 0; RenderControl(handle, ref _, control, position, Color.White, null, Matrix3x2.Identity); } public void RenderControl(IRenderHandle renderHandle, ref int total, Control control, Vector2i position, Color modulate, UIBox2i? scissorBox, Matrix3x2 coordinateTransform) { 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 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); } total += 1; var handle = renderHandle.DrawingHandleScreen; var oldXform = handle.GetTransform(); var xform = oldXform * Matrix3Helpers.CreateTransform(position, Angle.Zero, Vector2.One); handle.SetTransform(xform); modulate *= control.Modulate; if (_rendering || control.AlwaysRender) { // Handle modulation with care. var oldMod = handle.Modulate; handle.Modulate = modulate * control.ActualModulateSelf; control.Draw(renderHandle); handle.Modulate = oldMod; handle.UseShader(null); } handle.SetTransform(oldXform); var args = new Control.ControlRenderArguments() { Handle = renderHandle, Total = ref total, Modulate = modulate, ScissorBox = scissorRegion, CoordinateTransform = ref coordinateTransform }; control.PreRenderChildren(ref args); for (var index = 0; index < control.ChildCount; index++) { var child = control.GetChild(index); var pos = position + (Vector2i)Vector2.Transform(child.PixelPosition, coordinateTransform); control.RenderChildOverride(ref args, index, pos); } control.PostRenderChildren(ref args); if (clip) { renderHandle.SetScissor(scissorBox); } handle.SetTransform(oldXform); } public Color GetMainClearColor() => RootControl.ActualBgColor; /* * UI Sounds. * Some notes: * - Did not play click sound on all button presses because other stuff setting it shouldn't implicitly play the sound * Which turns this into opt-in rather than opt-out for existing behaviour. * This just means we have to manually fix buttons but that's okay. */ public void SetClickSound(IAudioSource? source) { if (!_configurationManager.GetCVar(CVars.InterfaceAudio)) return; _clickSource?.Dispose(); _clickSource = source; } public void ClickSound() { _clickSource?.Restart(); } public void SetHoverSound(IAudioSource? source) { if (!_configurationManager.GetCVar(CVars.InterfaceAudio)) return; _hoverSource?.Dispose(); _hoverSource = source; } public void HoverSound() { _hoverSource?.Restart(); } ~UserInterfaceManager() { ClearWindows(); } public UIRoot GetRootForMouse() { var pos = _inputManager.MouseScreenPosition; foreach (var root in _roots) { if (root.Window.Id == pos.Window) { return root; } } return RootControl; } } }