using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using JetBrains.Annotations; using Robust.Client.Graphics.Drawing; using Robust.Client.Interfaces.Graphics; using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.Interfaces.UserInterface; using Robust.Client.ResourceManagement.ResourceTypes; using Robust.Client.UserInterface.Controls; using Robust.Client.Utility; using Robust.Shared.Interfaces.Reflection; using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Robust.Client.UserInterface { /// /// A node in the GUI system. /// See https://github.com/space-wizards/RobustToolbox/wiki/UI-System-Tutorial for some basic concepts. /// [PublicAPI] [ControlWrap("Control")] // ReSharper disable once RequiredBaseTypesIsNotInherited public partial class Control : IDisposable { private readonly Dictionary _children = new Dictionary(); private readonly List _orderedChildren = new List(); private string _name; private float _anchorBottom; private float _anchorLeft; private float _anchorRight; private float _anchorTop; private float _marginRight; private float _marginLeft; private float _marginTop; private float _marginBottom; private bool _visible = true; private Vector2 _position; // _marginSetSize is the size calculated by the margins, // but it's different from _size if min size is higher. private Vector2 _sizeByMargins; private Vector2 _size; private bool _canKeyboardFocus; private float _sizeFlagsStretchRatio = 1; private Vector2? _calculatedMinimumSize; private Vector2 _customMinimumSize; private Dictionary _toApplyPropertyMapping; private GrowDirection _growHorizontal; private GrowDirection _growVertical; private static Dictionary _manualNodeTypeTranslations; public event Action OnMinimumSizeChanged; public event Action OnVisibilityChanged; private int _uniqueChildId; private SizeFlags _sizeFlagsHorizontal = SizeFlags.Fill; private SizeFlags _sizeFlagsVertical = SizeFlags.Fill; /// /// The name of this control. /// Names must be unique between the siblings of the control. /// [ViewVariables] public string Name { get => _name; set { if (value == _name) { return; } if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException("New name may not be null or whitespace.", nameof(value)); } var index = 0; if (Parent != null) { if (Parent.HasChild(value)) { throw new ArgumentException($"Parent already has a child with name {value}."); } index = Parent._children[_name].orderedIndex; Parent._children.Remove(_name); } _name = value; if (Parent != null) { Parent._children[_name] = (this, index); } } } /// /// 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; } 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; } [ViewVariables] public int ChildCount => _orderedChildren.Count; /// /// Path to the .tscn file for this scene in the VFS. /// This is mainly intended for content loading tscn files. /// Don't use it from the engine. /// protected virtual ResourcePath ScenePath => null; /// /// The value of an anchor that is exactly on the begin of the parent control. /// public const float AnchorBegin = 0; /// /// The value of an anchor that is exactly on the end of the parent control. /// public const float AnchorEnd = 1; /// /// Specifies the anchor of the bottom edge of the control. /// [ViewVariables] public float AnchorBottom { get => _anchorBottom; set { _anchorBottom = value; _updateLayout(); } } /// /// Specifies the anchor of the left edge of the control. /// [ViewVariables] public float AnchorLeft { get => _anchorLeft; set { _anchorLeft = value; _updateLayout(); } } /// /// Specifies the anchor of the right edge of the control. /// [ViewVariables] public float AnchorRight { get => _anchorRight; set { _anchorRight = value; _updateLayout(); } } /// /// Specifies the anchor of the top edge of the control. /// [ViewVariables] public float AnchorTop { get => _anchorTop; set { _anchorTop = value; _updateLayout(); } } /// /// Specifies the margin of the right edge of the control. /// [ViewVariables] public float MarginRight { get => _marginRight; set { _marginRight = value; _updateLayout(); } } /// /// Specifies the margin of the left edge of the control. /// [ViewVariables] public float MarginLeft { get => _marginLeft; set { _marginLeft = value; _updateLayout(); } } /// /// Specifies the margin of the top edge of the control. /// [ViewVariables] public float MarginTop { get => _marginTop; set { _marginTop = value; _updateLayout(); } } /// /// Specifies the margin of the bottom edge of the control. /// [ViewVariables] public float MarginBottom { get => _marginBottom; set { _marginBottom = value; _updateLayout(); } } /// /// 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 == UserInterfaceManager.RootControl) { return true; } } return false; } } /// /// Whether or not this control and its children are visible. /// /// [ViewVariables] public bool Visible { get => _visible; set { if (_visible == value) { return; } _visible = value; _propagateVisibilityChanged(value); } } private void _propagateVisibilityChanged(bool 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 is an (possibly indirect) child of /// /// public bool IsInsideTree { get; internal set; } private void _propagateExitTree() { IsInsideTree = false; _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() { IsInsideTree = true; _enteredTree(); foreach (var child in _orderedChildren) { child._propagateEnterTree(); } } /// /// Called when the control enters the root control tree. /// /// protected virtual void EnteredTree() { } private void _enteredTree() { EnteredTree(); } /// /// Called when the for this control changes. /// protected internal virtual void UIScaleChanged() { MinimumSizeChanged(); } /// /// The amount of "real" pixels a virtual pixel takes up. /// The higher the number, the bigger the interface. /// protected float UIScale => UserInterfaceManager.UIScale; /// /// The size of this control, in virtual pixels. /// /// /// /// [ViewVariables] public Vector2 Size { get => _size; set { var (diffX, diffY) = value - _sizeByMargins; _marginRight += diffX; _marginBottom += diffY; _updateLayout(); } } /// /// The size of this control, in physical pixels. /// [ViewVariables] public Vector2i PixelSize => (Vector2i) (_size * UserInterfaceManager.UIScale); /// /// A with the top left at 0,0 and the size equal to . /// /// public UIBox2 SizeBox => new UIBox2(Vector2.Zero, Size); /// /// A with the top left at 0,0 and the size equal to . /// /// public UIBox2i PixelSizeBox => new UIBox2i(Vector2i.Zero, PixelSize); /// /// The width of the control, in virtual pixels. /// /// public float Width => Size.X; /// /// The height of the control, in virtual pixels. /// /// public float Height => Size.Y; /// /// The width of the control, in physical pixels. /// /// public int PixelWidth => PixelSize.X; /// /// The height of the control, in physical pixels. /// /// public int PixelHeight => PixelSize.Y; /// /// The position of the top left corner of the control, in virtual pixels. /// This is relative to the position of the parent. /// /// /// [ViewVariables] public Vector2 Position { get => _position; set { var (diffX, diffY) = value - _position; _marginTop += diffY; _marginBottom += diffY; _marginLeft += diffX; _marginRight += diffX; _updateLayout(); } } /// /// The position of the top left corner of the control, in physical pixels. /// /// [ViewVariables] public Vector2i PixelPosition => (Vector2i) (_position * UserInterfaceManager.UIScale); /// /// The position of the top left corner of the control, in virtual pixels. /// This is not relative to the parent. /// /// /// [ViewVariables] public Vector2 GlobalPosition { get { var offset = Position; var parent = Parent; while (parent != null) { offset += parent.Position; parent = parent.Parent; } return offset; } } /// /// The position of the top left corner of the control, in physical pixels. /// This is not relative to the parent. /// /// [ViewVariables] public Vector2i GlobalPixelPosition { get { var offset = PixelPosition; var parent = Parent; while (parent != null) { offset += parent.PixelPosition; parent = parent.Parent; } return offset; } } /// /// Represents the "rectangle" of the control relative to the parent, in virtual pixels. /// /// public UIBox2 Rect => UIBox2.FromDimensions(_position, _size); /// /// Represents the "rectangle" of the control relative to the parent, in physical pixels. /// /// public UIBox2i PixelRect => UIBox2i.FromDimensions(PixelPosition, PixelSize); /// /// The tooltip that is shown when the mouse is hovered over this control for a bit. /// /// /// If empty or null, no tooltip is shown in the first place. /// public string ToolTip { get; set; } /// /// The mode that controls how mouse filtering works. See the enum for how it functions. /// [ViewVariables] public MouseFilterMode MouseFilter { get; set; } = MouseFilterMode.Stop; /// /// Whether this control can take keyboard focus. /// Keyboard focus is necessary for the control to receive keyboard events. /// /// [ViewVariables] 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; } /// /// Determines how the control will move on the horizontal axis to ensure it is at its minimum size. /// See for more information. /// [ViewVariables] public GrowDirection GrowHorizontal { get => _growHorizontal; set { _growHorizontal = value; _updateLayout(); } } /// /// Determines how the control will move on the vertical axis to ensure it is at its minimum size. /// See for more information. /// [ViewVariables] public GrowDirection GrowVertical { get => _growVertical; set { _growVertical = value; _updateLayout(); } } /// /// Horizontal size flags for container layout. /// [ViewVariables] public SizeFlags SizeFlagsHorizontal { get => _sizeFlagsHorizontal; set { _sizeFlagsHorizontal = value; if (Parent is Container container) { container.SortChildren(); } } } /// /// Vertical size flags for container layout. /// [ViewVariables] public SizeFlags SizeFlagsVertical { get => _sizeFlagsVertical; set { _sizeFlagsVertical = value; if (Parent is Container container) { container.SortChildren(); } } } /// /// Stretch ratio used to give shared of the available space in case multiple siblings are set to expand /// in a container /// /// /// Thrown if the value is less than or equal to 0. /// [ViewVariables] public float SizeFlagsStretchRatio { get => _sizeFlagsStretchRatio; set { if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than zero."); } _sizeFlagsStretchRatio = value; if (Parent is Container container) { container.SortChildren(); } } } /// /// 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? /// /// 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. /// /// 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. /// 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; } } /// /// A combination of and , /// Whichever is greater. /// Use this for whenever you need the *actual* minimum size of something. /// /// /// This is in virtual pixels. /// /// [ViewVariables] public Vector2 CombinedMinimumSize { get { if (!_calculatedMinimumSize.HasValue) { _updateMinimumSize(); DebugTools.Assert(_calculatedMinimumSize.HasValue); } return Vector2.ComponentMax(CustomMinimumSize, _calculatedMinimumSize.Value); } } /// /// The , in physical pixels. /// public Vector2i CombinedPixelMinimumSize => (Vector2i) (CombinedMinimumSize * UIScale); /// /// A custom minimum size. If the control-calculated size is is smaller than this, this is used instead. /// /// /// [ViewVariables] public Vector2 CustomMinimumSize { get => _customMinimumSize; set { _customMinimumSize = Vector2.ComponentMax(Vector2.Zero, value); MinimumSizeChanged(); } } private void _updateMinimumSize() { _calculatedMinimumSize = Vector2.ComponentMax(Vector2.Zero, CalculateMinimumSize()); } /// /// 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); SetDefaults(); if (ScenePath != null) { _manualNodeSetup(); } Name = GetType().Name; Initialize(); _applyPropertyMap(); } /// The name the component will have. public Control(string name) { if (string.IsNullOrWhiteSpace(name)) { throw new ArgumentException("Name must not be null or whitespace.", nameof(name)); } UserInterfaceManagerInternal = IoCManager.Resolve(); StyleClasses = new StyleClassCollection(this); Children = new OrderedChildCollection(this); SetDefaults(); if (ScenePath != null) { _manualNodeSetup(); } Name = name; Initialize(); _applyPropertyMap(); } /// /// Use this to do various initialization of the control. /// Ranging from spawning children to prefetching them for later referencing. /// protected virtual void Initialize() { } /// /// A bad idea. /// protected virtual void SetDefaults() { } private void _manualNodeSetup() { DebugTools.AssertNotNull(ScenePath); if (_manualNodeTypeTranslations == null) { _initManualNodeTypeTranslations(); } DebugTools.AssertNotNull(_manualNodeTypeTranslations); var resourceCache = IoCManager.Resolve(); var asset = (GodotAssetScene) resourceCache.GetResource(ScenePath).Asset; // Go over the inherited scenes with a stack, // because you can theoretically have very deep scene inheritance. var (_, inheritedSceneStack) = _manualFollowSceneInheritance(asset, resourceCache, false); _manualApplyInheritedSceneStack(this, inheritedSceneStack, asset, resourceCache); } private static void _manualApplyInheritedSceneStack(Control baseControl, Stack inheritedSceneStack, GodotAssetScene asset, IResourceCache resourceCache) { var parentMapping = new Dictionary {["."] = baseControl}; var propertyMapping = new Dictionary<(string parent, string name), Dictionary >(); // Go over the inherited scenes bottom-first. while (inheritedSceneStack.Count != 0) { var inheritedAsset = inheritedSceneStack.Pop(); foreach (var node in inheritedAsset.Nodes) { { if (!propertyMapping.TryGetValue((node.Parent, node.Name), out var propMap)) { propMap = new Dictionary(); propertyMapping[(node.Parent, node.Name)] = propMap; } foreach (var (key, value) in node.Properties) { propMap[key] = (value, inheritedAsset); } } // It's the base control. if (node.Parent == null) { continue; } Control childControl; if (node.Type != null) { if (!_manualNodeTypeTranslations.TryGetValue(node.Type, out var type)) { type = typeof(Control); } childControl = (Control) Activator.CreateInstance(type); childControl.Name = node.Name; } else if (node.Instance != null) { var extResource = asset.GetExtResource(node.Instance.Value); DebugTools.Assert(extResource.Type == "PackedScene"); if (_manualNodeTypeTranslations.TryGetValue(extResource.Path, out var type)) { childControl = (Control) Activator.CreateInstance(type); } else { var subScene = (GodotAssetScene) resourceCache .GetResource( GodotPathUtility.GodotPathToResourcePath(extResource.Path)).Asset; childControl = ManualSpawnFromScene(subScene); } childControl.Name = node.Name; } else { // This happens if the node def is overriding properties of a node instantiated in an instance. continue; } parentMapping[node.Parent].AddChild(childControl); if (node.Parent == ".") { parentMapping[node.Name] = childControl; } else { parentMapping[$"{node.Parent}/{node.Name}"] = childControl; } } } // Apply all the properties. foreach (var ((parent, nodeName), propMap) in propertyMapping) { Control node; switch (parent) { case null: // Base control, which isn't initialized yet, so defer until after Initialize(). baseControl._toApplyPropertyMapping = propMap; continue; case ".": node = parentMapping[nodeName]; break; default: var parentNode = baseControl.GetChild(parent); node = parentNode.GetChild(nodeName); break; } // We need to defer this until AFTER Initialize() has ran because else everything blows up. foreach (var (key, (value, source)) in propMap) { node.SetGodotProperty(key, value, source); } } } private void _applyPropertyMap() { if (_toApplyPropertyMapping == null) { return; } foreach (var (key, (value, source)) in _toApplyPropertyMapping) { SetGodotProperty(key, value, source); } _toApplyPropertyMapping = null; } internal static Control ManualSpawnFromScene(GodotAssetScene scene) { if (_manualNodeTypeTranslations == null) { _initManualNodeTypeTranslations(); } DebugTools.AssertNotNull(_manualNodeTypeTranslations); var resourceCache = IoCManager.Resolve(); var (controlType, inheritedSceneStack) = _manualFollowSceneInheritance(scene, resourceCache, true); var control = (Control) Activator.CreateInstance(controlType); control.Name = scene.Nodes[0].Name; _manualApplyInheritedSceneStack(control, inheritedSceneStack, scene, resourceCache); return control; } private static (Type, Stack) _manualFollowSceneInheritance(GodotAssetScene scene, IResourceCache resourceCache, bool getType) { // Go over the inherited scenes with a stack, // because you can theoretically have very deep scene inheritance. var inheritedSceneStack = new Stack(); inheritedSceneStack.Push(scene); Type controlType = null; while (scene.Nodes[0].Instance != null) { var extResource = scene.GetExtResource(scene.Nodes[0].Instance.Value); DebugTools.Assert(extResource.Type == "PackedScene"); if (getType && _manualNodeTypeTranslations.TryGetValue(extResource.Path, out controlType)) { break; } scene = (GodotAssetScene) resourceCache.GetResource( GodotPathUtility.GodotPathToResourcePath(extResource.Path)).Asset; inheritedSceneStack.Push(scene); } if (controlType == null) { if (!getType || scene.Nodes[0].Type == null || !_manualNodeTypeTranslations.TryGetValue(scene.Nodes[0].Type, out controlType)) { controlType = typeof(Control); } } return (controlType, inheritedSceneStack); } private static void _initManualNodeTypeTranslations() { DebugTools.AssertNull(_manualNodeTypeTranslations); _manualNodeTypeTranslations = new Dictionary(); var reflectionManager = IoCManager.Resolve(); foreach (var type in reflectionManager.FindTypesWithAttribute()) { var attr = type.GetCustomAttribute(); if (attr.InstanceString != null) { _manualNodeTypeTranslations[attr.InstanceString] = type; } } } private protected virtual void SetGodotProperty(string property, object value, GodotAssetScene context) { switch (property) { case "margin_left": MarginLeft = (float) value; break; case "margin_right": MarginRight = (float) value; break; case "margin_top": MarginTop = (float) value; break; case "margin_bottom": MarginBottom = (float) value; break; case "anchor_left": AnchorLeft = (float) value; break; case "anchor_right": AnchorRight = (float) value; break; case "anchor_bottom": AnchorBottom = (float) value; break; case "anchor_top": AnchorTop = (float) value; break; case "mouse_filter": MouseFilter = (MouseFilterMode) (long) value; break; case "size_flags_horizontal": SizeFlagsHorizontal = (SizeFlags) (long) value; break; case "size_flags_vertical": SizeFlagsVertical = (SizeFlags) (long) value; break; case "rect_clip_content": RectClipContent = (bool) value; break; case "rect_min_size": CustomMinimumSize = (Vector2) value; break; } } /// /// Retrieves and instances the object pointed to by either a /// sub resource or ext resource reference in a godot asset. /// /// The asset in which said object is referenced. /// /// The or /// /// /// The expected type of the resource. This is not a godot type but our equivalent. /// private protected T GetGodotResource(GodotAsset context, object value) { GodotAsset.ResourceDef def; (GodotAsset, int) defContext; // Retrieve the actual ResourceDef for the resource requested. if (value is GodotAsset.TokenExtResource ext) { var extRef = context.GetExtResource(ext); var resPath = GodotPathUtility.GodotPathToResourcePath(extRef.Path); var res = IoCManager.Resolve().GetResource(resPath); def = ((GodotAssetRes) res.Asset).MainResource; defContext = (res.Asset, 0); } else if (value is GodotAsset.TokenSubResource sub) { def = context.SubResources[(int) sub.ResourceId]; defContext = (context, (int) sub.ResourceId); } else { throw new ArgumentException("Value must be a TokenExtResource or a TokenSubResource", nameof(value)); } // See if we've cached it. if (UserInterfaceManagerInternal.GodotResourceInstanceCache.TryGetValue(defContext, out var result)) { return (T) result; } // If not, here comes the mess of turning that into a native sane type. if (def.Type == "StyleBoxFlat") { var box = new StyleBoxFlat(); if (def.Properties.TryGetValue("bg_color", out var val)) { box.BackgroundColor = (Color) val; } result = box; } else { throw new NotImplementedException(); } UserInterfaceManagerInternal.GodotResourceInstanceCache[defContext] = result; return (T) result; } /// /// 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) { } internal virtual void DrawInternal(IRenderHandle renderHandle) { Draw(renderHandle.DrawingHandleScreen); } 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. /// public void Dispose() { if (Disposed) { return; } Dispose(true); GC.SuppressFinalize(this); Disposed = true; } protected virtual void Dispose(bool disposing) { if (!disposing) { return; } DisposeAllChildren(); Parent?.RemoveChild(this); OnKeyDown = null; } ~Control() { Dispose(false); } /// /// Dispose all children, but leave this one intact. /// 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() { foreach (var child in Children.ToList()) { RemoveChild(child); } } /// /// Make this child an orphan. e.g. remove it from its parent if it has one. /// public void Orphan() { 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) { 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."); } 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)); } } } var origChildName = child.Name; var childName = origChildName; while (_children.ContainsKey(childName)) { childName = $"{origChildName}_{++_uniqueChildId}"; } if (origChildName != childName) { child.Name = childName; } child.Parent = this; _children[child.Name] = (child, _orderedChildren.Count); _orderedChildren.Add(child); child.Parented(this); if (IsInsideTree) { child._propagateEnterTree(); } ChildAdded(child); } /// /// Called after a new child is added to this control. /// /// The new child. protected virtual void ChildAdded(Control newChild) { } /// /// Called when this control gets made a child of a different control. /// /// The new parent component. protected virtual void Parented(Control newParent) { Restyle(); DoUpdateLayout(); } /// /// 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) { if (!_children.ContainsKey(child.Name) || _children[child.Name].Item1 != child) { throw new InvalidOperationException("The provided control is not a direct child of this control."); } var index = _children[child.Name].orderedIndex; _orderedChildren.RemoveAt(index); _children.Remove(child.Name); _updateChildIndices(); child.Parent = null; child.Deparented(); if (IsInsideTree) { child._propagateExitTree(); } ChildRemoved(child); } /// /// Called when a child is removed from this child. /// /// The former child. protected virtual void ChildRemoved(Control child) { } /// /// Called when this control is removed as child from the former parent. /// protected virtual void Deparented() { } /// /// 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) { } /// /// Override this to calculate a minimum size for this control. /// Do NOT call this directly to get the minimum size for layout purposes! /// Use for the ACTUAL minimum size. /// protected virtual Vector2 CalculateMinimumSize() { return Vector2.Zero; } /// /// Tells the GUI system that the minimum size of this control may have changed, /// so that say containers will re-sort it if necessary. /// public void MinimumSizeChanged() { _calculatedMinimumSize = null; _updateLayout(); OnMinimumSizeChanged?.Invoke(this); } /// /// 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) { // This is effectively the same implementation as the default Godot one in Control.cpp. // That one gets ignored because to Godot it looks like we're ALWAYS implementing a custom HasPoint. var size = Size; return point.X >= 0 && point.X <= size.X && point.Y >= 0 && point.Y <= size.Y; } /// /// Gets a child of this control with the specified name. /// /// /// The name of the child. This name can use / as delimiter to get grandchildren controls and so on. /// /// The type to cast the found control to, if it is found. /// The control. /// /// Thrown if the child with the specified name does not exist. /// /// /// Thrown if the control exists, but it is of the wrong type. /// public T GetChild(string name) where T : Control { return (T) GetChild(name); } private static readonly char[] SectionSplitDelimiter = {'/'}; /// /// 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 a child of this control with the specified name. /// /// /// The name of the child. This name can use / as delimiter to get grandchildren controls and so on. /// /// The control. /// /// Thrown if the child with the specified name does not exist. /// public Control GetChild(string name) { if (name.IndexOf('/') != -1) { var current = this; foreach (var section in name.Split(SectionSplitDelimiter, StringSplitOptions.RemoveEmptyEntries)) { current = current.GetChild(section); } return current; } if (TryGetChild(name, out var control)) { return control; } throw new KeyNotFoundException($"No child UI element {name}"); } /// /// Try-get version of . /// Note that it still throws if the node is found but the type invalid. /// public bool TryGetChild(string name, out T child) where T : Control { if (_children.TryGetValue(name, out var control)) { child = (T) control.Item1; return true; } child = default; return false; } /// /// Try-get version of . /// public bool TryGetChild(string name, out Control child) { if (_children.TryGetValue(name, out var childEntry)) { child = childEntry.Item1; return true; } child = default; return false; } /// /// See if this control has an immediate child with the specified name. /// public bool HasChild(string name) { return _children.ContainsKey(name); } /// /// 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._children[Name].orderedIndex; } /// /// 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; } Parent._orderedChildren.RemoveAt(posInParent); Parent._orderedChildren.Insert(position, this); Parent._updateChildIndices(); 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() { SetPositionInParent(Parent.ChildCount - 1); } /// /// Called when this control receives keyboard focus. /// protected internal virtual void FocusEntered() { } /// /// Called when this control loses keyboard focus. /// protected internal virtual void FocusExited() { } /// /// Check if this control currently has keyboard focus. /// /// public 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); } /// /// Called when the size of the control changes. /// protected virtual void Resized() { } /// /// Sets an anchor AND a margin preset. This is most likely the method you want. /// public void SetAnchorAndMarginPreset(LayoutPreset preset, LayoutPresetMode mode = LayoutPresetMode.MinSize, int margin = 0) { SetAnchorPreset(preset); SetMarginsPreset(preset, mode, margin); } /// /// Changes all the anchors of a node at once to common presets. /// The result is that the anchors are laid out to be suitable for a preset. /// /// /// The preset to apply to the anchors. /// /// /// If this is true, the control margin values themselves will not be changed, /// and the control position and size will change according to the new anchor parameters. /// If false, the control margins will adjust so that the control position and size remains the same relative to its parent. /// /// /// Thrown if isn't a valid preset value. /// public void SetAnchorPreset(LayoutPreset preset, bool keepMargin = false) { // TODO: Implement keepMargin. // Left Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.BottomLeft: case LayoutPreset.CenterLeft: case LayoutPreset.LeftWide: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.Wide: case LayoutPreset.TopWide: case LayoutPreset.BottomWide: AnchorLeft = 0; break; case LayoutPreset.CenterTop: case LayoutPreset.CenterBottom: case LayoutPreset.Center: case LayoutPreset.VerticalCenterWide: AnchorLeft = 0.5f; break; case LayoutPreset.TopRight: case LayoutPreset.BottomRight: case LayoutPreset.CenterRight: case LayoutPreset.RightWide: AnchorLeft = 1; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Top Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.TopRight: case LayoutPreset.LeftWide: case LayoutPreset.TopWide: case LayoutPreset.Wide: case LayoutPreset.RightWide: case LayoutPreset.CenterTop: case LayoutPreset.VerticalCenterWide: AnchorTop = 0; break; case LayoutPreset.CenterLeft: case LayoutPreset.CenterRight: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.Center: AnchorTop = 0.5f; break; case LayoutPreset.CenterBottom: case LayoutPreset.BottomLeft: case LayoutPreset.BottomRight: case LayoutPreset.BottomWide: AnchorTop = 1; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Right Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.CenterLeft: case LayoutPreset.BottomLeft: case LayoutPreset.LeftWide: AnchorRight = 0; break; case LayoutPreset.CenterTop: case LayoutPreset.CenterBottom: case LayoutPreset.Center: case LayoutPreset.VerticalCenterWide: AnchorRight = 0.5f; break; case LayoutPreset.CenterRight: case LayoutPreset.TopRight: case LayoutPreset.Wide: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.TopWide: case LayoutPreset.BottomWide: case LayoutPreset.RightWide: case LayoutPreset.BottomRight: AnchorRight = 1; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Bottom Anchor. switch (preset) { case LayoutPreset.TopWide: case LayoutPreset.TopLeft: case LayoutPreset.TopRight: case LayoutPreset.CenterTop: AnchorBottom = 0; break; case LayoutPreset.CenterLeft: case LayoutPreset.CenterRight: case LayoutPreset.Center: case LayoutPreset.HorizontalCenterWide: AnchorBottom = 0.5f; break; case LayoutPreset.CenterBottom: case LayoutPreset.BottomLeft: case LayoutPreset.BottomRight: case LayoutPreset.LeftWide: case LayoutPreset.Wide: case LayoutPreset.RightWide: case LayoutPreset.VerticalCenterWide: case LayoutPreset.BottomWide: AnchorBottom = 1; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } } /// /// Changes all the margins of a control at once to common presets. /// The result is that the control is laid out as specified by the preset. /// /// /// /// Some extra margin to add depending on the preset chosen. public void SetMarginsPreset(LayoutPreset preset, LayoutPresetMode resizeMode = LayoutPresetMode.MinSize, int margin = 0) { var newSize = Size; var minSize = CombinedMinimumSize; if ((resizeMode & LayoutPresetMode.KeepWidth) == 0) { newSize = new Vector2(minSize.X, newSize.Y); } if ((resizeMode & LayoutPresetMode.KeepHeight) == 0) { newSize = new Vector2(newSize.X, minSize.Y); } var parentSize = Parent?.Size ?? Vector2.Zero; // Left Margin. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.BottomLeft: case LayoutPreset.CenterLeft: case LayoutPreset.LeftWide: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.Wide: case LayoutPreset.TopWide: case LayoutPreset.BottomWide: // The AnchorLeft bit is to reverse the effect of anchors, // So that the preset result is the same no matter what margins are set. _marginLeft = parentSize.X * (0 - AnchorLeft) + margin; break; case LayoutPreset.CenterTop: case LayoutPreset.CenterBottom: case LayoutPreset.Center: case LayoutPreset.VerticalCenterWide: _marginLeft = parentSize.X * (0.5f - AnchorLeft) - newSize.X / 2; break; case LayoutPreset.TopRight: case LayoutPreset.BottomRight: case LayoutPreset.CenterRight: case LayoutPreset.RightWide: _marginLeft = parentSize.X * (1 - AnchorLeft) - newSize.X - margin; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Top Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.TopRight: case LayoutPreset.LeftWide: case LayoutPreset.TopWide: case LayoutPreset.Wide: case LayoutPreset.RightWide: case LayoutPreset.CenterTop: case LayoutPreset.VerticalCenterWide: _marginTop = parentSize.Y * (0 - AnchorTop) + margin; break; case LayoutPreset.CenterLeft: case LayoutPreset.CenterRight: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.Center: _marginTop = parentSize.Y * (0.5f - AnchorTop) - newSize.Y / 2; break; case LayoutPreset.CenterBottom: case LayoutPreset.BottomLeft: case LayoutPreset.BottomRight: case LayoutPreset.BottomWide: _marginTop = parentSize.Y * (1 - AnchorTop) - newSize.Y - margin; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Right Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.CenterLeft: case LayoutPreset.BottomLeft: case LayoutPreset.LeftWide: _marginRight = parentSize.X * (0 - AnchorRight) + newSize.X + margin; break; case LayoutPreset.CenterTop: case LayoutPreset.CenterBottom: case LayoutPreset.Center: case LayoutPreset.VerticalCenterWide: _marginRight = parentSize.X * (0.5f - AnchorRight) + newSize.X; break; case LayoutPreset.CenterRight: case LayoutPreset.TopRight: case LayoutPreset.Wide: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.TopWide: case LayoutPreset.BottomWide: case LayoutPreset.RightWide: case LayoutPreset.BottomRight: _marginRight = parentSize.X * (1 - AnchorRight) - margin; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Bottom Anchor. switch (preset) { case LayoutPreset.TopWide: case LayoutPreset.TopLeft: case LayoutPreset.TopRight: case LayoutPreset.CenterTop: _marginBottom = parentSize.Y * (0 - AnchorBottom) + newSize.Y + margin; break; case LayoutPreset.CenterLeft: case LayoutPreset.CenterRight: case LayoutPreset.Center: case LayoutPreset.HorizontalCenterWide: _marginBottom = parentSize.Y * (0.5f - AnchorBottom) + newSize.Y; break; case LayoutPreset.CenterBottom: case LayoutPreset.BottomLeft: case LayoutPreset.BottomRight: case LayoutPreset.LeftWide: case LayoutPreset.Wide: case LayoutPreset.RightWide: case LayoutPreset.VerticalCenterWide: case LayoutPreset.BottomWide: _marginBottom = parentSize.Y * (1 - AnchorBottom) - margin; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } _updateLayout(); } public enum LayoutPreset : byte { TopLeft = 0, TopRight = 1, BottomLeft = 2, BottomRight = 3, CenterLeft = 4, CenterTop = 5, CenterRight = 6, CenterBottom = 7, Center = 8, LeftWide = 9, TopWide = 10, RightWide = 11, BottomWide = 12, VerticalCenterWide = 13, HorizontalCenterWide = 14, Wide = 15, } /// [Flags] [PublicAPI] public enum LayoutPresetMode : byte { /// /// Reset control size to minimum size. /// MinSize = 0, /// /// Reset height to minimum but keep width the same. /// KeepWidth = 1, /// /// Reset width to minimum but keep height the same. /// KeepHeight = 2, /// /// Do not modify control size at all. /// KeepSize = KeepWidth | KeepHeight, } /// /// Controls how a control changes size when inside a container. /// [Flags] [PublicAPI] public enum SizeFlags : byte { /// /// Shrink to the begin of the specified axis. /// None = 0, /// /// Fill as much space as possible in a container, without pushing others. /// Fill = 1, /// /// Fill as much space as possible in a container, pushing other nodes. /// The ratio of pushing if there's multiple set to expand is dependant on /// Expand = 2, /// /// Combination of and . /// FillExpand = 3, /// /// Shrink inside a container, aligning to the center. /// ShrinkCenter = 4, /// /// Shrink inside a container, aligning to the end. /// ShrinkEnd = 8, } /// /// Controls how the control should move when its wanted size (controlled by anchors/margins) is smaller /// than its minimum size. /// public enum GrowDirection : byte { /// /// The control will expand to the bottom right to reach its minimum size. /// End = 0, /// /// The control will expand to the top left to reach its minimum size. /// Begin, /// /// The control will expand on all axes equally to reach its minimum size. /// Both } internal void DoUpdate(ProcessFrameEventArgs args) { Update(args); foreach (var child in Children) { child.DoUpdate(args); } } /// /// This is called every process frame. /// protected virtual void Update(ProcessFrameEventArgs args) { } internal void DoFrameUpdate(RenderFrameEventArgs args) { FrameUpdate(args); foreach (var child in Children) { child.DoFrameUpdate(args); } } /// /// This is called before every render frame. /// protected virtual void FrameUpdate(RenderFrameEventArgs args) { } private void _updateLayout() { DoUpdateLayout(); } internal void DoUpdateLayout() { var (pSizeX, pSizeY) = Parent?._size ?? Vector2.Zero; // Calculate where the control "wants" to be by its anchors/margins. var top = _anchorTop * pSizeY + _marginTop; var left = _anchorLeft * pSizeX + _marginLeft; var right = _anchorRight * pSizeX + _marginRight; var bottom = _anchorBottom * pSizeY + _marginBottom; // The position we want. var (wPosX, wPosY) = (left, top); // The size we want. var (wSizeX, wSizeY) = (right - left, bottom - top); var (minSizeX, minSizeY) = CombinedMinimumSize; _handleLayoutOverflow(GrowHorizontal, minSizeX, wPosX, wSizeX, out var posX, out var sizeX); _handleLayoutOverflow(GrowVertical, minSizeY, wPosY, wSizeY, out var posY, out var sizeY); var oldSize = _size; _position = (posX, posY); _size = (sizeX, sizeY); _sizeByMargins = (wSizeX, wSizeY); // If size is different then child controls may need to be laid out differently. if (_size != oldSize) { Resized(); foreach (var child in _orderedChildren) { child._updateLayout(); } } } private static void _handleLayoutOverflow(GrowDirection direction, float minSize, float wPos, float wSize, out float pos, out float size) { var overflow = minSize - wSize; if (overflow <= 0) { pos = wPos; size = wSize; return; } switch (direction) { case GrowDirection.End: pos = wPos; break; case GrowDirection.Begin: pos = wPos - overflow; break; case GrowDirection.Both: pos = wPos - overflow / 2; break; default: throw new ArgumentOutOfRangeException(); } size = minSize; } /// /// Updates the indices stored inside . /// private void _updateChildIndices() { for (var i = 0; i < _orderedChildren.Count; i++) { var child = _orderedChildren[i]; _children[child._name] = (child, i); } } public enum CursorShape { Arrow = 0, IBeam = 1, PointingHand = 2, Cross = 3, Wait = 4, Busy = 5, Drag = 6, CanDrop = 7, Forbidden = 8, VSize = 9, HSize = 10, BDiagSize = 11, FDiagSize = 12, Move = 13, VSplit = 14, HSplit = 15, Help = 16, } public CursorShape DefaultCursorShape { get => default; set { } } 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 { /// /// 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 class OrderedChildCollection : ICollection, IReadOnlyCollection { private readonly Control Owner; public OrderedChildCollection(Control owner) { Owner = owner; } public Enumerator GetEnumerator() { return new Enumerator(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 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(); } } } [AttributeUsage(AttributeTargets.Class, Inherited = false)] [BaseTypeRequired(typeof(Control))] internal class ControlWrapAttribute : Attribute { public readonly string InstanceString; public ControlWrapAttribute(string instanceString) { InstanceString = instanceString; } } } }