using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Numerics; using Avalonia.Metadata; using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Themes; using Robust.Client.UserInterface.XAML; using Robust.Shared.Animations; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Robust.Client.UserInterface { /// /// A node in the GUI system. /// See https://docs.spacestation14.io/en/engine/user-interface for some basic concepts. /// [PublicAPI] [Virtual] public partial class Control : IDisposable { private readonly List _orderedChildren = new(); private bool _visible = true; // Determines if this control requires space, even when // it's visibility has been set to false private bool _reservesSpace = false; // _marginSetSize is the size calculated by the margins, // but it's different from _size if min size is higher. private bool _canKeyboardFocus; public event Action? OnVisibilityChanged; /// /// The name of this control. /// Names must be unique between the siblings of the control. /// [ViewVariables] public string? Name { get; set; } // ReSharper disable once ValueParameterNotUsed public AccessLevel? Access { set { } } /// /// If true, this control will always be rendered, even if other UI rendering is disabled. /// /// /// Useful for e.g. primary viewports. /// [ViewVariables(VVAccess.ReadWrite)] public bool AlwaysRender { get; set; } /// /// Our parent inside the control tree. /// /// /// This cannot be changed directly. Use and such on the parent to change it. /// [ViewVariables] public Control? Parent { get; private set; } public NameScope? NameScope; //public void AttachNameScope(Dictionary nameScope) //{ // _nameScope = nameScope; //} public virtual ISawmill Log => UserInterfaceManager.ControlSawmill; public UITheme Theme { get; internal set; } private UITheme? _themeOverride; public UITheme? ThemeOverride { get => _themeOverride; set { if (_themeOverride == value) return; _themeOverride = value; ThemeUpdateRecursive(Parent?.Theme ?? UserInterfaceManager.CurrentTheme); } } protected virtual void OnThemeUpdated() { } public void ThemeUpdateRecursive(UITheme theme) { theme = _themeOverride ?? theme; if (Theme == theme) return; Theme = theme; OnThemeUpdated(); foreach (var child in Children) { child.ThemeUpdateRecursive(Theme); } } public NameScope? FindNameScope() { foreach (var control in this.GetSelfAndLogicalAncestors()) { if (control.NameScope != null) return control.NameScope; } return null; } public T FindControl(string name) where T : Control { var nameScope = FindNameScope(); if (nameScope == null) { throw new ArgumentException("No Namespace found for Control"); } var value = nameScope.Find(name); if (value == null) { throw new ArgumentException($"No Control with the name {name} found"); } if (value is not T ret) { throw new ArgumentException($"Control with name {name} had invalid type {value.GetType()}"); } return ret; } internal IUserInterfaceManagerInternal UserInterfaceManagerInternal { get; } /// /// The UserInterfaceManager we belong to, for convenience. /// public IUserInterfaceManager UserInterfaceManager => UserInterfaceManagerInternal; /// /// Gets an ordered enumerable over all the children of this control. /// [ViewVariables] public OrderedChildCollection Children { get; } [Content] public virtual ICollection XamlChildren { get; protected set; } [ViewVariables] public int ChildCount => _orderedChildren.Count; /// /// Gets whether this control is at all visible. /// This means the control is part of the tree of the root control, and all of its parents are visible. /// /// [ViewVariables] public bool VisibleInTree { get { for (var parent = this; parent != null; parent = parent.Parent) { if (!parent.Visible) { return false; } if (parent is UIRoot) { return true; } } return false; } } /// /// Whether or not this control and its children are visible. /// /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] public bool Visible { get => _visible; set { if (_visible == value) { return; } _visible = value; _propagateVisibilityChanged(value); // TODO: unhardcode this. // Many containers ignore children if they're invisible, so that's why we're replicating that here. Parent?.InvalidateMeasure(); InvalidateMeasure(); } } /// /// Called when this control's visibility in the control tree changed. /// protected virtual void VisibilityChanged(bool newVisible) { } private void _propagateVisibilityChanged(bool newVisible) { VisibilityChanged(newVisible); OnVisibilityChanged?.Invoke(this); if (!VisibleInTree) { UserInterfaceManagerInternal.ControlHidden(this); } foreach (var child in _orderedChildren) { if (newVisible || child._visible) { child._propagateVisibilityChanged(newVisible); } } } /// /// Whether or not this control and its children require /// space to be reserved, even when not visible. /// /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] public bool ReservesSpace { get => _reservesSpace; set { if (_reservesSpace == value) { return; } _reservesSpace = value; // TODO: unhardcode this. // Many containers ignore children if they're invisible, so that's why we're replicating that here. Parent?.InvalidateMeasure(); InvalidateMeasure(); } } /// /// Whether or not this control is an (possibly indirect) child of /// /// [ViewVariables] public bool IsInsideTree => Root != null; [ViewVariables] public virtual UIRoot? Root { get; internal set; } private void _propagateExitTree() { Root = null; _exitedTree(); foreach (var child in _orderedChildren) { child._propagateExitTree(); } } /// /// Called when the control is removed from the root control tree. /// /// protected virtual void ExitedTree() { } private void _exitedTree() { ExitedTree(); UserInterfaceManagerInternal.ControlRemovedFromTree(this); } private void _propagateEnterTree(UIRoot root) { Root = root; _enteredTree(); foreach (var child in _orderedChildren) { child._propagateEnterTree(root); } } /// /// Called when the control enters the root control tree. /// /// protected virtual void EnteredTree() { } private void _enteredTree() { EnteredTree(); } /// /// Simple text tooltip that is shown when the mouse is hovered over this control for a bit. /// See or for a more customizable alternative. /// No effect when TooltipSupplier is specified. /// /// /// If empty or null, no tooltip is shown in the first place (but OnShowTooltip and OnHideTooltip /// events are still fired). /// public string? ToolTip { get; set; } /// /// Overrides the global tooltip delay, showing the tooltip for this /// control within the specified number of seconds. /// public float? TooltipDelay { get; set; } /// /// Should the tooltip track the mouse cursor. /// public bool TrackingTooltip { get; set; } /// /// When a tooltip should be shown for this control, this will be invoked to /// produce a control which will serve as the tooltip (doing nothing if null is returned). /// This is the generally recommended way to implement custom tooltips for controls, as it takes /// care of the various edge cases for showing / hiding the tooltip. /// For an even more customizable approach, /// /// The returned control will be added to PopupRoot, and positioned /// within the user interface under the current mouse position to avoid going off the edge of the /// screen. When the tooltip should be hidden, the control will be hidden by removing it from the tree. /// /// It is expected that the returned control remains within PopupRoot. Other classes should /// not move it around in the tree or move it out of PopupRoot, but may access and modify /// the control and its children via . /// /// /// Returning a new instance of a tooltip control every time is usually fine. If for some /// reason constructing the tooltip control is expensive, it MAY be fine to cache + reuse a single instance but this /// approach has not yet been tested. /// public TooltipSupplier? TooltipSupplier { get; set; } /// /// Invoked when the mouse is hovered over this control for a bit and a tooltip /// should be shown. Can be used as an alternative to ToolTip or TooltipSupplier to perform custom tooltip /// logic such as showing a more complex tooltip control. /// /// Any custom tooltip controls should typically be added /// as a child of UserInterfaceManager.PopupRoot /// Handlers can use to assist with positioning /// custom tooltip controls. /// public event EventHandler? OnShowTooltip; /// /// If this control is currently showing a tooltip provided via TooltipSupplier, /// returns that tooltip. Do not move this control within the tree, it should remain in PopupRoot. /// Also, as it may be hidden (removed from tree) at any time, saving a reference to this is a Bad Idea. /// public Control? SuppliedTooltip => UserInterfaceManagerInternal.GetSuppliedTooltipFor(this); /// /// Manually hide the tooltip currently being shown for this control, if there is one. /// public void HideTooltip() { UserInterfaceManagerInternal.HideTooltipFor(this); } internal void PerformShowTooltip() { OnShowTooltip?.Invoke(this, EventArgs.Empty); } /// /// Invoked when this control is showing a tooltip which should now be hidden. /// public event EventHandler? OnHideTooltip; internal void PerformHideTooltip() { OnHideTooltip?.Invoke(this, EventArgs.Empty); } /// /// The mode that controls how mouse filtering works. See the enum for how it functions. /// [ViewVariables(VVAccess.ReadWrite)] public MouseFilterMode MouseFilter { get; set; } = MouseFilterMode.Ignore; /// /// Whether this control can take keyboard focus. /// Keyboard focus is necessary for the control to receive keyboard events. /// /// [ViewVariables(VVAccess.ReadWrite)] public bool CanKeyboardFocus { get => _canKeyboardFocus; set { if (_canKeyboardFocus == value) { return; } _canKeyboardFocus = value; if (!value) { ReleaseKeyboardFocus(); } } } /// /// Whether the control will automatically receive keyboard focus (if possible) when clicked on. /// /// /// Obviously, must be set to true for this to work. /// public bool KeyboardFocusOnClick { get; set; } /// /// Whether to clip drawing of this control and its children to its rectangle. /// /// /// By default, controls (and their children) can render outside their rectangle. /// If this is set, rendering is hard clipped to it. /// /// [ViewVariables] public bool RectClipContent { get; set; } /// /// A margin around this control. If this control + this margin is outside its parent's , /// it will not be drawn. /// /// /// A control rectangle does not necessarily have to be listened to for drawing. /// So the problem is, how do we know where to stop trying to draw the control if it's clipped away? /// /// [ViewVariables(VVAccess.ReadWrite)] public int RectDrawClipMargin { get; set; } = 10; // You may wonder why Modulate isn't stylesheet controlled, but ModulateSelf is. // Reason is simple: I'm fucking lazy. // I'm expecting this comment to last much longer than the problem it's pointing out. /// /// An override for the modulate self from the style sheet. /// /// [ViewVariables(VVAccess.ReadWrite)] public Color? ModulateSelfOverride { get; set; } /// /// Modulates the color of this control and all its children when drawing. /// /// /// Modulation is multiplying or tinting the color basically. /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] public Color Modulate { get; set; } = Color.White; /// /// The value used to modulate this control (and not its siblings) with on top of /// when drawing. /// /// /// By default this value is pulled from CSS, or if available. /// /// Modulation is multiplying or tinting the color basically. /// public Color ActualModulateSelf { get { if (ModulateSelfOverride.HasValue) { return ModulateSelfOverride.Value; } if (TryGetStyleProperty(StylePropertyModulateSelf, out Color modulate)) { return modulate; } return Color.White; } } /// /// Default constructor. /// The name of the control is decided based on type. /// public Control() { UserInterfaceManagerInternal = IoCManager.Resolve(); _styleClasses = new StyleClassCollection(this); Children = new OrderedChildCollection(this); Theme = UserInterfaceManagerInternal.CurrentTheme; XamlChildren = Children; } /// /// Called to render this control. /// /// /// Drawing is done relative to the position of the control. /// It is also done in pixel space, so you should not directly use properties such as . /// /// A handle that can be used to draw. protected internal virtual void Draw(DrawingHandleScreen handle) { } protected internal virtual void Draw(IRenderHandle renderHandle) { Draw(renderHandle.DrawingHandleScreen); } protected internal virtual void PreRenderChildren(ref ControlRenderArguments args) { } protected internal virtual void PostRenderChildren(ref ControlRenderArguments args) { } protected internal virtual void RenderChildOverride(ref ControlRenderArguments args, int childIndex, Vector2i position) { RenderControl(ref args, childIndex, position); } public ref struct ControlRenderArguments { public IRenderHandle Handle; public ref int Total; public Vector2i Position; public Color Modulate; public UIBox2i? ScissorBox; public ref Matrix3x2 CoordinateTransform; } protected void RenderControl(ref ControlRenderArguments args, int childIndex, Vector2i position) { UserInterfaceManagerInternal.RenderControl(args.Handle, ref args.Total, GetChild(childIndex), position, args.Modulate, args.ScissorBox, args.CoordinateTransform); } public void UpdateDraw() { } /// /// Called when this modal control is closed. /// Only used for controls that are actually modals. /// protected internal virtual void ModalRemoved() { } public bool Disposed { get; private set; } /// /// Dispose this control, its own scene control, and all its children. /// Basically the big delete button. /// [Obsolete("Controls should only be removed from UI tree instead of being disposed")] public void Dispose() { if (Disposed) { return; } Dispose(true); Disposed = true; } [Obsolete("Controls should only be removed from UI tree instead of being disposed")] protected virtual void Dispose(bool disposing) { if (!disposing) { return; } UserInterfaceManagerInternal.HideTooltipFor(this); DisposeAllChildren(); Parent?.RemoveChild(this); OnKeyBindDown = null; } /// /// Dispose all children, but leave this one intact. /// [Obsolete("Use RemoveAllChildren")] public void DisposeAllChildren() { // Cache because the children modify the dictionary. var children = new List(Children); foreach (var child in children) { child.Dispose(); } } /// /// Remove all the children from this control. /// public void RemoveAllChildren() { DebugTools.Assert(!Disposed, "Control has been disposed."); foreach (var child in Children.ToArray()) { // This checks fails in some obscure cases like using the element inspector in the dev window. // Why? Well I could probably spend 15 minutes in a debugger to find out, // but I'd probably still end up with this fix. if (child.Parent == this) RemoveChild(child); } } /// /// Make this child an orphan. i.e. remove it from its parent if it has one. /// public void Orphan() { DebugTools.Assert(!Disposed, "Control has been disposed."); Parent?.RemoveChild(this); } /// /// Make the provided control a parent of this control. /// /// The control to make a child of this control. /// /// Thrown if we already have a component with the same name, /// or the provided component is still parented to a different control. /// /// /// is null. /// public void AddChild(Control child) { DebugTools.Assert(!Disposed, "Control has been disposed."); if (child == null) throw new ArgumentNullException(nameof(child)); if (child.Parent != null) { throw new InvalidOperationException("This component is still parented. Deparent it before adding it."); } DebugTools.Assert(!child.Disposed, "Child is disposed."); if (child == this) { throw new InvalidOperationException("You can't parent something to itself!"); } // Ensure this control isn't a parent of ours. // Doesn't need to happen if the control has no children of course. if (child.ChildCount != 0) { for (var parent = Parent; parent != null; parent = parent.Parent) { if (parent == child) { throw new ArgumentException("This control is one of our parents!", nameof(child)); } } } child.Parent = this; _orderedChildren.Add(child); child.Parented(this); if (Root != null) { child._propagateEnterTree(Root); } ChildAdded(child); } public event Action? OnChildAdded; /// /// Called after a new child is added to this control. /// /// The new child. protected virtual void ChildAdded(Control newChild) { OnChildAdded?.Invoke(newChild); newChild.ThemeUpdateRecursive(Theme); InvalidateMeasure(); } /// /// Called when this control gets made a child of a different control. /// /// The new parent component. protected virtual void Parented(Control newParent) { StylesheetUpdateRecursive(); InvalidateMeasure(); } /// /// Removes the provided child from this control. /// /// The child to remove. /// /// Thrown if the provided child is not one of this control's children. /// public void RemoveChild(Control child) { DebugTools.Assert(!Disposed, "Control has been disposed."); if (child.Parent != this) { throw new InvalidOperationException("The provided control is not a direct child of this control."); } var childIndex = _orderedChildren.IndexOf(child); RemoveChild(childIndex); } /// /// Removes the child at a specific index from this control. /// /// The index of the child to remove. /// /// Thrown if the provided child index is out of range /// public void RemoveChild(int childIndex) { DebugTools.Assert(!Disposed, "Control has been disposed."); var child = _orderedChildren[childIndex]; _orderedChildren.RemoveAt(childIndex); child.Parent = null; child.Deparented(); if (IsInsideTree) { child._propagateExitTree(); } ChildRemoved(child); } public event Action? OnChildRemoved; /// /// Called when a child is removed from this child. /// /// The former child. protected virtual void ChildRemoved(Control child) { OnChildRemoved?.Invoke(child); InvalidateMeasure(); } /// /// Called when this control is removed as child from the former parent. /// protected virtual void Deparented() { } public event Action? OnChildMoved; /// /// Called when the order index of a child changes. /// /// The child that was changed. /// The previous index of the child. /// The new index of the child. protected virtual void ChildMoved(Control child, int oldIndex, int newIndex) { OnChildMoved?.Invoke(new ControlChildMovedEventArgs(child, oldIndex, newIndex)); } /// /// Called to test whether this control has a certain point, /// for the purposes of finding controls under the cursor. /// /// The relative point, in virtual pixels. /// True if this control does have the point and should be counted as a hit. protected internal virtual bool HasPoint(Vector2 point) { var size = Size; return point.X >= 0 && point.X <= size.X && point.Y >= 0 && point.Y <= size.Y; } /// /// Gets the immediate child of this control with the specified index. /// /// The index of the child. /// The child. public Control GetChild(int index) { return _orderedChildren[index]; } /// /// Gets the "index" in the parent. /// This index is used for ordering of actions like input and drawing among siblings. /// /// /// Thrown if this control has no parent. /// public int GetPositionInParent() { if (Parent == null) { throw new InvalidOperationException("This control has no parent!"); } return Parent._orderedChildren.IndexOf(this); } /// /// Sets the index of this control in the parent. /// This pretty much corresponds to layout and drawing order in relation to its siblings. /// /// /// This control has no parent. public void SetPositionInParent(int position) { if (Parent == null) { throw new InvalidOperationException("No parent to change position in."); } var posInParent = GetPositionInParent(); if (posInParent == position) { return; } // If it was at the top index and we re-add it there then don't throw. Parent._orderedChildren.RemoveAt(posInParent); if (position == Parent._orderedChildren.Count) { Parent._orderedChildren.Add(this); } else { Parent._orderedChildren.Insert(position, this); } Parent.ChildMoved(this, posInParent, position); } /// /// Makes this the first control among its siblings, /// So that it's first in things such as drawing order. /// /// This control has no parent. public void SetPositionFirst() { SetPositionInParent(0); } /// /// Makes this the last control among its siblings, /// So that it's last in things such as drawing order. /// /// This control has no parent. public void SetPositionLast() { if (Parent == null) { throw new InvalidOperationException("No parent to change position in."); } SetPositionInParent(Parent.ChildCount - 1); } /// /// Called when this control receives keyboard focus. /// protected internal virtual void KeyboardFocusEntered() { } /// /// Called when this control loses keyboard focus (corresponds to UserInterfaceManager.KeyboardFocused). /// protected internal virtual void KeyboardFocusExited() { } /// /// Fired when a control loses control focus for any reason. See . /// /// /// Controls which have some sort of drag / drop behavior should usually implement this method (typically by cancelling the drag drop). /// Otherwise, if a user clicks down LMB over one control to initiate a drag, then clicks RMB down /// over a different control while still holding down LMB, the control being dragged will now lose focus /// and will no longer receive the keyup for the LMB, thus won't cancel the drag. /// This should also be considered for controls which have any special KeyBindUp behavior - consider /// what would happen if the control lost focus and never received the KeyBindUp. /// /// There is no corresponding ControlFocusEntered - if a control wants to handle that situation they should simply /// handle KeyBindDown as that's the only way a control would gain focus. /// protected internal virtual void ControlFocusExited() { } /// /// Check if this control currently has keyboard focus. /// /// public virtual bool HasKeyboardFocus() { return UserInterfaceManager.KeyboardFocused == this; } /// /// Grab keyboard focus if this control doesn't already have it. /// /// /// must be true for this to work. /// public void GrabKeyboardFocus() { UserInterfaceManager.GrabKeyboardFocus(this); } /// /// Release keyboard focus from this control if it has it. /// If a different control has keyboard focus, nothing happens. /// public void ReleaseKeyboardFocus() { UserInterfaceManager.ReleaseKeyboardFocus(this); } public event Action? OnResized; /// /// Called when the size of the control changes. /// protected virtual void Resized() { OnResized?.Invoke(); } internal int DoFrameUpdateRecursive(FrameEventArgs args) { if (!Visible) return 0; var total = 1; FrameUpdate(args); foreach (var child in Children) { total += child.DoFrameUpdateRecursive(args); } return total; } /// /// This is called before every render frame. /// protected virtual void FrameUpdate(FrameEventArgs args) { ProcessAnimations(args); } // These are separate from StandardCursorShape so that // in the future we could have an API to override the styling. public override string ToString() { return $"{Name} ({GetType().Name})"; } /// /// Mode that will be tested when testing controls to invoke mouse button events on. /// public enum MouseFilterMode : byte { /// /// The control will be able to receive mouse buttons events. /// Furthermore, if a control with this mode does get clicked, /// the event automatically gets marked as handled after every other candidate has been tried, /// so that the rest of the game does not receive it. /// Pass = 1, /// /// The control will be able to receive mouse button events like , /// but the event will be stopped and handled even if the relevant events do not handle it. /// Stop = 0, /// /// The control will not be considered at all, and will not have any effects. /// Ignore = 2, } public sealed class OrderedChildCollection : ICollection, IReadOnlyList { private readonly Control Owner; public OrderedChildCollection(Control owner) { Owner = owner; } public Enumerator GetEnumerator() { return new(Owner); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void Add(Control item) { Owner.AddChild(item); } public void Clear() { Owner.RemoveAllChildren(); } public bool Contains(Control item) { return item?.Parent == Owner; } public void CopyTo(Control[] array, int arrayIndex) { Owner._orderedChildren.CopyTo(array, arrayIndex); } public bool Remove(Control item) { if (item?.Parent != Owner) { return false; } DebugTools.AssertNotNull(Owner); Owner.RemoveChild(item); return true; } int ICollection.Count => Owner.ChildCount; int IReadOnlyCollection.Count => Owner.ChildCount; public Control this[int index] => Owner._orderedChildren[index]; public bool IsReadOnly => false; public struct Enumerator : IEnumerator { private List.Enumerator _enumerator; internal Enumerator(Control control) { _enumerator = control._orderedChildren.GetEnumerator(); } public bool MoveNext() { return _enumerator.MoveNext(); } public void Reset() { ((IEnumerator) _enumerator).Reset(); } public Control Current => _enumerator.Current; object IEnumerator.Current => Current; public void Dispose() { _enumerator.Dispose(); } } } } public delegate Control? TooltipSupplier(Control sender); public readonly struct ControlChildMovedEventArgs { public ControlChildMovedEventArgs(Control control, int oldIndex, int newIndex) { Control = control; OldIndex = oldIndex; NewIndex = newIndex; } public readonly Control Control; public readonly int OldIndex; public readonly int NewIndex; } }