Files
RobustToolbox/Robust.Client/UserInterface/Control.cs
2019-07-29 23:08:49 +02:00

2351 lines
78 KiB
C#

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
{
/// <summary>
/// A node in the GUI system.
/// See https://github.com/space-wizards/RobustToolbox/wiki/UI-System-Tutorial for some basic concepts.
/// </summary>
[PublicAPI]
[ControlWrap("Control")]
// ReSharper disable once RequiredBaseTypesIsNotInherited
public partial class Control : IDisposable
{
private readonly Dictionary<string, (Control, int orderedIndex)> _children =
new Dictionary<string, (Control, int)>();
private readonly List<Control> _orderedChildren = new List<Control>();
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<string, (object value, GodotAssetScene source)>
_toApplyPropertyMapping;
private GrowDirection _growHorizontal;
private GrowDirection _growVertical;
private static Dictionary<string, Type> _manualNodeTypeTranslations;
public event Action<Control> OnMinimumSizeChanged;
public event Action<Control> OnVisibilityChanged;
private int _uniqueChildId;
private SizeFlags _sizeFlagsHorizontal = SizeFlags.Fill;
private SizeFlags _sizeFlagsVertical = SizeFlags.Fill;
/// <summary>
/// The name of this control.
/// Names must be unique between the siblings of the control.
/// </summary>
[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);
}
}
}
/// <summary>
/// Our parent inside the control tree.
/// </summary>
/// <remarks>
/// This cannot be changed directly. Use <see cref="AddChild" /> and such on the parent to change it.
/// </remarks>
[ViewVariables]
public Control Parent { get; private set; }
internal IUserInterfaceManagerInternal UserInterfaceManagerInternal { get; }
/// <summary>
/// The UserInterfaceManager we belong to, for convenience.
/// </summary>
public IUserInterfaceManager UserInterfaceManager => UserInterfaceManagerInternal;
/// <summary>
/// Gets an ordered enumerable over all the children of this control.
/// </summary>
[ViewVariables]
public OrderedChildCollection Children { get; }
[ViewVariables] public int ChildCount => _orderedChildren.Count;
/// <summary>
/// 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.
/// </summary>
protected virtual ResourcePath ScenePath => null;
/// <summary>
/// The value of an anchor that is exactly on the begin of the parent control.
/// </summary>
public const float AnchorBegin = 0;
/// <summary>
/// The value of an anchor that is exactly on the end of the parent control.
/// </summary>
public const float AnchorEnd = 1;
/// <summary>
/// Specifies the anchor of the bottom edge of the control.
/// </summary>
[ViewVariables]
public float AnchorBottom
{
get => _anchorBottom;
set
{
_anchorBottom = value;
_updateLayout();
}
}
/// <summary>
/// Specifies the anchor of the left edge of the control.
/// </summary>
[ViewVariables]
public float AnchorLeft
{
get => _anchorLeft;
set
{
_anchorLeft = value;
_updateLayout();
}
}
/// <summary>
/// Specifies the anchor of the right edge of the control.
/// </summary>
[ViewVariables]
public float AnchorRight
{
get => _anchorRight;
set
{
_anchorRight = value;
_updateLayout();
}
}
/// <summary>
/// Specifies the anchor of the top edge of the control.
/// </summary>
[ViewVariables]
public float AnchorTop
{
get => _anchorTop;
set
{
_anchorTop = value;
_updateLayout();
}
}
/// <summary>
/// Specifies the margin of the right edge of the control.
/// </summary>
[ViewVariables]
public float MarginRight
{
get => _marginRight;
set
{
_marginRight = value;
_updateLayout();
}
}
/// <summary>
/// Specifies the margin of the left edge of the control.
/// </summary>
[ViewVariables]
public float MarginLeft
{
get => _marginLeft;
set
{
_marginLeft = value;
_updateLayout();
}
}
/// <summary>
/// Specifies the margin of the top edge of the control.
/// </summary>
[ViewVariables]
public float MarginTop
{
get => _marginTop;
set
{
_marginTop = value;
_updateLayout();
}
}
/// <summary>
/// Specifies the margin of the bottom edge of the control.
/// </summary>
[ViewVariables]
public float MarginBottom
{
get => _marginBottom;
set
{
_marginBottom = value;
_updateLayout();
}
}
/// <summary>
/// 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.
/// </summary>
/// <seealso cref="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;
}
}
/// <summary>
/// Whether or not this control and its children are visible.
/// </summary>
/// <seealso cref="VisibleInTree"/>
[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);
}
}
}
/// <summary>
/// Whether or not this control is an (possibly indirect) child of
/// <see cref="IUserInterfaceManager.RootControl"/>
/// </summary>
public bool IsInsideTree { get; internal set; }
private void _propagateExitTree()
{
IsInsideTree = false;
_exitedTree();
foreach (var child in _orderedChildren)
{
child._propagateExitTree();
}
}
/// <summary>
/// Called when the control is removed from the root control tree.
/// </summary>
/// <seealso cref="EnteredTree"/>
protected virtual void ExitedTree()
{
}
private void _exitedTree()
{
ExitedTree();
UserInterfaceManagerInternal.ControlRemovedFromTree(this);
}
private void _propagateEnterTree()
{
IsInsideTree = true;
_enteredTree();
foreach (var child in _orderedChildren)
{
child._propagateEnterTree();
}
}
/// <summary>
/// Called when the control enters the root control tree.
/// </summary>
/// <seealso cref="ExitedTree"/>
protected virtual void EnteredTree()
{
}
private void _enteredTree()
{
EnteredTree();
}
/// <summary>
/// Called when the <see cref="UIScale"/> for this control changes.
/// </summary>
protected internal virtual void UIScaleChanged()
{
MinimumSizeChanged();
}
/// <summary>
/// The amount of "real" pixels a virtual pixel takes up.
/// The higher the number, the bigger the interface.
/// </summary>
protected float UIScale => UserInterfaceManager.UIScale;
/// <summary>
/// The size of this control, in virtual pixels.
/// </summary>
/// <seealso cref="PixelSize"/>
/// <seealso cref="Width"/>
/// <seealso cref="Height"/>
[ViewVariables]
public Vector2 Size
{
get => _size;
set
{
var (diffX, diffY) = value - _sizeByMargins;
_marginRight += diffX;
_marginBottom += diffY;
_updateLayout();
}
}
/// <summary>
/// The size of this control, in physical pixels.
/// </summary>
[ViewVariables]
public Vector2i PixelSize => (Vector2i) (_size * UserInterfaceManager.UIScale);
/// <summary>
/// A <see cref="UIBox2"/> with the top left at 0,0 and the size equal to <see cref="Size"/>.
/// </summary>
/// <seealso cref="PixelSizeBox"/>
public UIBox2 SizeBox => new UIBox2(Vector2.Zero, Size);
/// <summary>
/// A <see cref="UIBox2i"/> with the top left at 0,0 and the size equal to <see cref="PixelSize"/>.
/// </summary>
/// <seealso cref="SizeBox"/>
public UIBox2i PixelSizeBox => new UIBox2i(Vector2i.Zero, PixelSize);
/// <summary>
/// The width of the control, in virtual pixels.
/// </summary>
/// <seealso cref="PixelWidth"/>
public float Width => Size.X;
/// <summary>
/// The height of the control, in virtual pixels.
/// </summary>
/// <seealso cref="PixelHeight"/>
public float Height => Size.Y;
/// <summary>
/// The width of the control, in physical pixels.
/// </summary>
/// <seealso cref="Width"/>
public int PixelWidth => PixelSize.X;
/// <summary>
/// The height of the control, in physical pixels.
/// </summary>
/// <seealso cref="Height"/>
public int PixelHeight => PixelSize.Y;
/// <summary>
/// The position of the top left corner of the control, in virtual pixels.
/// This is relative to the position of the parent.
/// </summary>
/// <seealso cref="PixelPosition"/>
/// <seealso cref="GlobalPosition"/>
[ViewVariables]
public Vector2 Position
{
get => _position;
set
{
var (diffX, diffY) = value - _position;
_marginTop += diffY;
_marginBottom += diffY;
_marginLeft += diffX;
_marginRight += diffX;
_updateLayout();
}
}
/// <summary>
/// The position of the top left corner of the control, in physical pixels.
/// </summary>
/// <seealso cref="Position"/>
[ViewVariables]
public Vector2i PixelPosition => (Vector2i) (_position * UserInterfaceManager.UIScale);
/// <summary>
/// The position of the top left corner of the control, in virtual pixels.
/// This is not relative to the parent.
/// </summary>
/// <seealso cref="GlobalPosition"/>
/// <seealso cref="Position"/>
[ViewVariables]
public Vector2 GlobalPosition
{
get
{
var offset = Position;
var parent = Parent;
while (parent != null)
{
offset += parent.Position;
parent = parent.Parent;
}
return offset;
}
}
/// <summary>
/// The position of the top left corner of the control, in physical pixels.
/// This is not relative to the parent.
/// </summary>
/// <seealso cref="GlobalPosition"/>
[ViewVariables]
public Vector2i GlobalPixelPosition
{
get
{
var offset = PixelPosition;
var parent = Parent;
while (parent != null)
{
offset += parent.PixelPosition;
parent = parent.Parent;
}
return offset;
}
}
/// <summary>
/// Represents the "rectangle" of the control relative to the parent, in virtual pixels.
/// </summary>
/// <seealso cref="PixelRect"/>
public UIBox2 Rect => UIBox2.FromDimensions(_position, _size);
/// <summary>
/// Represents the "rectangle" of the control relative to the parent, in physical pixels.
/// </summary>
/// <seealso cref="Rect"/>
public UIBox2i PixelRect => UIBox2i.FromDimensions(PixelPosition, PixelSize);
/// <summary>
/// The tooltip that is shown when the mouse is hovered over this control for a bit.
/// </summary>
/// <remarks>
/// If empty or null, no tooltip is shown in the first place.
/// </remarks>
public string ToolTip { get; set; }
/// <summary>
/// The mode that controls how mouse filtering works. See the enum for how it functions.
/// </summary>
[ViewVariables]
public MouseFilterMode MouseFilter { get; set; } = MouseFilterMode.Stop;
/// <summary>
/// Whether this control can take keyboard focus.
/// Keyboard focus is necessary for the control to receive keyboard events.
/// </summary>
/// <seealso cref="KeyboardFocusOnClick"/>
[ViewVariables]
public bool CanKeyboardFocus
{
get => _canKeyboardFocus;
set
{
if (_canKeyboardFocus == value)
{
return;
}
_canKeyboardFocus = value;
if (!value)
{
ReleaseKeyboardFocus();
}
}
}
/// <summary>
/// Whether the control will automatically receive keyboard focus (if possible) when clicked on.
/// </summary>
/// <remarks>
/// Obviously, <see cref="CanKeyboardFocus"/> must be set to true for this to work.
/// </remarks>
public bool KeyboardFocusOnClick { get; set; }
/// <summary>
/// Determines how the control will move on the horizontal axis to ensure it is at its minimum size.
/// See <see cref="GrowDirection"/> for more information.
/// </summary>
[ViewVariables]
public GrowDirection GrowHorizontal
{
get => _growHorizontal;
set
{
_growHorizontal = value;
_updateLayout();
}
}
/// <summary>
/// Determines how the control will move on the vertical axis to ensure it is at its minimum size.
/// See <see cref="GrowDirection"/> for more information.
/// </summary>
[ViewVariables]
public GrowDirection GrowVertical
{
get => _growVertical;
set
{
_growVertical = value;
_updateLayout();
}
}
/// <summary>
/// Horizontal size flags for container layout.
/// </summary>
[ViewVariables]
public SizeFlags SizeFlagsHorizontal
{
get => _sizeFlagsHorizontal;
set
{
_sizeFlagsHorizontal = value;
if (Parent is Container container)
{
container.SortChildren();
}
}
}
/// <summary>
/// Vertical size flags for container layout.
/// </summary>
[ViewVariables]
public SizeFlags SizeFlagsVertical
{
get => _sizeFlagsVertical;
set
{
_sizeFlagsVertical = value;
if (Parent is Container container)
{
container.SortChildren();
}
}
}
/// <summary>
/// Stretch ratio used to give shared of the available space in case multiple siblings are set to expand
/// in a container
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value is less than or equal to 0.
/// </exception>
[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();
}
}
}
/// <summary>
/// Whether to clip drawing of this control and its children to its rectangle.
/// </summary>
/// <remarks>
/// By default, controls (and their children) can render outside their rectangle.
/// If this is set, rendering is hard clipped to it.
/// </remarks>
/// <seealso cref="RectDrawClipMargin"/>
[ViewVariables]
public bool RectClipContent { get; set; }
/// <summary>
/// A margin around this control. If this control + this margin is outside its parent's <see cref="RectClipContent" />,
/// it will not be drawn.
/// </summary>
/// <remarks>
/// 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?
/// </remarks>
/// <seealso cref="RectClipContent"/>
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.
/// <summary>
/// An override for the modulate self from the style sheet.
/// </summary>
/// <seealso cref="ActualModulateSelf" />
public Color? ModulateSelfOverride { get; set; }
/// <summary>
/// Modulates the color of this control and all its children when drawing.
/// </summary>
/// <remarks>
/// Modulation is multiplying or tinting the color basically.
/// </remarks>
public Color Modulate { get; set; } = Color.White;
/// <summary>
/// The value used to modulate this control (and not its siblings) with on top of <see cref="Modulate"/>
/// when drawing.
/// </summary>
/// <remarks>
/// By default this value is pulled from CSS, or <see cref="ModulateSelfOverride"/> if available.
///
/// Modulation is multiplying or tinting the color basically.
/// </remarks>
public Color ActualModulateSelf
{
get
{
if (ModulateSelfOverride.HasValue)
{
return ModulateSelfOverride.Value;
}
if (TryGetStyleProperty(StylePropertyModulateSelf, out Color modulate))
{
return modulate;
}
return Color.White;
}
}
/// <summary>
/// A combination of <see cref="CustomMinimumSize" /> and <see cref="CalculateMinimumSize" />,
/// Whichever is greater.
/// Use this for whenever you need the *actual* minimum size of something.
/// </summary>
/// <remarks>
/// This is in virtual pixels.
/// </remarks>
/// <seealso cref="CombinedPixelMinimumSize"/>
[ViewVariables]
public Vector2 CombinedMinimumSize
{
get
{
if (!_calculatedMinimumSize.HasValue)
{
_updateMinimumSize();
DebugTools.Assert(_calculatedMinimumSize.HasValue);
}
return Vector2.ComponentMax(CustomMinimumSize, _calculatedMinimumSize.Value);
}
}
/// <summary>
/// The <see cref="CombinedMinimumSize"/>, in physical pixels.
/// </summary>
public Vector2i CombinedPixelMinimumSize => (Vector2i) (CombinedMinimumSize * UIScale);
/// <summary>
/// A custom minimum size. If the control-calculated size is is smaller than this, this is used instead.
/// </summary>
/// <seealso cref="CalculateMinimumSize" />
/// <seealso cref="CombinedMinimumSize" />
[ViewVariables]
public Vector2 CustomMinimumSize
{
get => _customMinimumSize;
set
{
_customMinimumSize = Vector2.ComponentMax(Vector2.Zero, value);
MinimumSizeChanged();
}
}
private void _updateMinimumSize()
{
_calculatedMinimumSize = Vector2.ComponentMax(Vector2.Zero, CalculateMinimumSize());
}
/// <summary>
/// Default constructor.
/// The name of the control is decided based on type.
/// </summary>
public Control()
{
UserInterfaceManagerInternal = IoCManager.Resolve<IUserInterfaceManagerInternal>();
StyleClasses = new StyleClassCollection(this);
Children = new OrderedChildCollection(this);
SetDefaults();
if (ScenePath != null)
{
_manualNodeSetup();
}
Name = GetType().Name;
Initialize();
_applyPropertyMap();
}
/// <param name="name">The name the component will have.</param>
public Control(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Name must not be null or whitespace.", nameof(name));
}
UserInterfaceManagerInternal = IoCManager.Resolve<IUserInterfaceManagerInternal>();
StyleClasses = new StyleClassCollection(this);
Children = new OrderedChildCollection(this);
SetDefaults();
if (ScenePath != null)
{
_manualNodeSetup();
}
Name = name;
Initialize();
_applyPropertyMap();
}
/// <summary>
/// Use this to do various initialization of the control.
/// Ranging from spawning children to prefetching them for later referencing.
/// </summary>
protected virtual void Initialize()
{
}
/// <summary>
/// A bad idea.
/// </summary>
protected virtual void SetDefaults()
{
}
private void _manualNodeSetup()
{
DebugTools.AssertNotNull(ScenePath);
if (_manualNodeTypeTranslations == null)
{
_initManualNodeTypeTranslations();
}
DebugTools.AssertNotNull(_manualNodeTypeTranslations);
var resourceCache = IoCManager.Resolve<IResourceCache>();
var asset = (GodotAssetScene) resourceCache.GetResource<GodotAssetResource>(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<GodotAssetScene> inheritedSceneStack, GodotAssetScene asset,
IResourceCache resourceCache)
{
var parentMapping = new Dictionary<string, Control> {["."] = baseControl};
var propertyMapping =
new Dictionary<(string parent, string name), Dictionary<string, (object value, GodotAssetScene source)>
>();
// 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<string, (object value, GodotAssetScene source)>();
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<GodotAssetResource>(
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<IResourceCache>();
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<GodotAssetScene>) _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<GodotAssetScene>();
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<GodotAssetResource>(
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<string, Type>();
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
foreach (var type in reflectionManager.FindTypesWithAttribute<ControlWrapAttribute>())
{
var attr = type.GetCustomAttribute<ControlWrapAttribute>();
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;
}
}
/// <summary>
/// Retrieves and instances the object pointed to by either a
/// sub resource or ext resource reference in a godot asset.
/// </summary>
/// <param name="context">The asset in which said object is referenced.</param>
/// <param name="value">
/// The <see cref="GodotAsset.TokenSubResource" /> or <see cref="GodotAsset.TokenExtResource" />
/// </param>
/// <typeparam name="T">
/// The expected type of the resource. This is not a godot type but our equivalent.
/// </typeparam>
private protected T GetGodotResource<T>(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<IResourceCache>().GetResource<GodotAssetResource>(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;
}
/// <summary>
/// Called to render this control.
/// </summary>
/// <remarks>
/// 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 <see cref="Size"/>.
/// </remarks>
/// <param name="handle">A handle that can be used to draw.</param>
protected internal virtual void Draw(DrawingHandleScreen handle)
{
}
internal virtual void DrawInternal(IRenderHandle renderHandle)
{
Draw(renderHandle.DrawingHandleScreen);
}
public void UpdateDraw()
{
}
/// <summary>
/// Called when this modal control is closed.
/// Only used for controls that are actually modals.
/// </summary>
protected internal virtual void ModalRemoved()
{
}
public bool Disposed { get; private set; }
/// <summary>
/// Dispose this control, its own scene control, and all its children.
/// Basically the big delete button.
/// </summary>
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);
}
/// <summary>
/// Dispose all children, but leave this one intact.
/// </summary>
public void DisposeAllChildren()
{
// Cache because the children modify the dictionary.
var children = new List<Control>(Children);
foreach (var child in children)
{
child.Dispose();
}
}
/// <summary>
/// Remove all the children from this control.
/// </summary>
public void RemoveAllChildren()
{
foreach (var child in Children.ToList())
{
RemoveChild(child);
}
}
/// <summary>
/// Make this child an orphan. e.g. remove it from its parent if it has one.
/// </summary>
public void Orphan()
{
Parent?.RemoveChild(this);
}
/// <summary>
/// Make the provided control a parent of this control.
/// </summary>
/// <param name="child">The control to make a child of this control.</param>
/// <exception cref="InvalidOperationException">
/// Thrown if we already have a component with the same name,
/// or the provided component is still parented to a different control.
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="child" /> is <c>null</c>.
/// </exception>
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);
}
/// <summary>
/// Called after a new child is added to this control.
/// </summary>
/// <param name="newChild">The new child.</param>
protected virtual void ChildAdded(Control newChild)
{
}
/// <summary>
/// Called when this control gets made a child of a different control.
/// </summary>
/// <param name="newParent">The new parent component.</param>
protected virtual void Parented(Control newParent)
{
Restyle();
DoUpdateLayout();
}
/// <summary>
/// Removes the provided child from this control.
/// </summary>
/// <param name="child">The child to remove.</param>
/// <exception cref="InvalidOperationException">
/// Thrown if the provided child is not one of this control's children.
/// </exception>
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);
}
/// <summary>
/// Called when a child is removed from this child.
/// </summary>
/// <param name="child">The former child.</param>
protected virtual void ChildRemoved(Control child)
{
}
/// <summary>
/// Called when this control is removed as child from the former parent.
/// </summary>
protected virtual void Deparented()
{
}
/// <summary>
/// Called when the order index of a child changes.
/// </summary>
/// <param name="child">The child that was changed.</param>
/// <param name="oldIndex">The previous index of the child.</param>
/// <param name="newIndex">The new index of the child.</param>
protected virtual void ChildMoved(Control child, int oldIndex, int newIndex)
{
}
/// <summary>
/// Override this to calculate a minimum size for this control.
/// Do NOT call this directly to get the minimum size for layout purposes!
/// Use <see cref="CombinedMinimumSize" /> for the ACTUAL minimum size.
/// </summary>
protected virtual Vector2 CalculateMinimumSize()
{
return Vector2.Zero;
}
/// <summary>
/// Tells the GUI system that the minimum size of this control may have changed,
/// so that say containers will re-sort it if necessary.
/// </summary>
public void MinimumSizeChanged()
{
_calculatedMinimumSize = null;
_updateLayout();
OnMinimumSizeChanged?.Invoke(this);
}
/// <summary>
/// Called to test whether this control has a certain point,
/// for the purposes of finding controls under the cursor.
/// </summary>
/// <param name="point">The relative point, in virtual pixels.</param>
/// <returns>True if this control does have the point and should be counted as a hit.</returns>
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;
}
/// <summary>
/// Gets a child of this control with the specified name.
/// </summary>
/// <param name="name">
/// The name of the child. This name can use / as delimiter to get grandchildren controls and so on.
/// </param>
/// <typeparam name="T">The type to cast the found control to, if it is found.</typeparam>
/// <returns>The control.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown if the child with the specified name does not exist.
/// </exception>
/// <exception cref="InvalidCastException">
/// Thrown if the control exists, but it is of the wrong type.
/// </exception>
public T GetChild<T>(string name) where T : Control
{
return (T) GetChild(name);
}
private static readonly char[] SectionSplitDelimiter = {'/'};
/// <summary>
/// Gets the immediate child of this control with the specified index.
/// </summary>
/// <param name="index">The index of the child.</param>
/// <returns>The child.</returns>
public Control GetChild(int index)
{
return _orderedChildren[index];
}
/// <summary>
/// Gets a child of this control with the specified name.
/// </summary>
/// <param name="name">
/// The name of the child. This name can use / as delimiter to get grandchildren controls and so on.
/// </param>
/// <returns>The control.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown if the child with the specified name does not exist.
/// </exception>
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}");
}
/// <summary>
/// Try-get version of <see cref="GetChild{T}"/>.
/// Note that it still throws if the node is found but the type invalid.
/// </summary>
public bool TryGetChild<T>(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;
}
/// <summary>
/// Try-get version of <see cref="GetChild(string)"/>.
/// </summary>
public bool TryGetChild(string name, out Control child)
{
if (_children.TryGetValue(name, out var childEntry))
{
child = childEntry.Item1;
return true;
}
child = default;
return false;
}
/// <summary>
/// See if this control has an immediate child with the specified name.
/// </summary>
public bool HasChild(string name)
{
return _children.ContainsKey(name);
}
/// <summary>
/// Gets the "index" in the parent.
/// This index is used for ordering of actions like input and drawing among siblings.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if this control has no parent.
/// </exception>
public int GetPositionInParent()
{
if (Parent == null)
{
throw new InvalidOperationException("This control has no parent!");
}
return Parent._children[Name].orderedIndex;
}
/// <summary>
/// Sets the index of this control in the parent.
/// This pretty much corresponds to layout and drawing order in relation to its siblings.
/// </summary>
/// <param name="position"></param>
/// <exception cref="InvalidOperationException">This control has no parent.</exception>
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);
}
/// <summary>
/// Makes this the first control among its siblings,
/// So that it's first in things such as drawing order.
/// </summary>
/// <exception cref="InvalidOperationException">This control has no parent.</exception>
public void SetPositionFirst()
{
SetPositionInParent(0);
}
/// <summary>
/// Makes this the last control among its siblings,
/// So that it's last in things such as drawing order.
/// </summary>
/// <exception cref="InvalidOperationException">This control has no parent.</exception>
public void SetPositionLast()
{
SetPositionInParent(Parent.ChildCount - 1);
}
/// <summary>
/// Called when this control receives keyboard focus.
/// </summary>
protected internal virtual void FocusEntered()
{
}
/// <summary>
/// Called when this control loses keyboard focus.
/// </summary>
protected internal virtual void FocusExited()
{
}
/// <summary>
/// Check if this control currently has keyboard focus.
/// </summary>
/// <returns></returns>
public bool HasKeyboardFocus()
{
return UserInterfaceManager.KeyboardFocused == this;
}
/// <summary>
/// Grab keyboard focus if this control doesn't already have it.
/// </summary>
/// <remarks>
/// <see cref="CanKeyboardFocus"/> must be true for this to work.
/// </remarks>
public void GrabKeyboardFocus()
{
UserInterfaceManager.GrabKeyboardFocus(this);
}
/// <summary>
/// Release keyboard focus from this control if it has it.
/// If a different control has keyboard focus, nothing happens.
/// </summary>
public void ReleaseKeyboardFocus()
{
UserInterfaceManager.ReleaseKeyboardFocus(this);
}
/// <summary>
/// Called when the size of the control changes.
/// </summary>
protected virtual void Resized()
{
}
/// <summary>
/// Sets an anchor AND a margin preset. This is most likely the method you want.
/// </summary>
public void SetAnchorAndMarginPreset(LayoutPreset preset, LayoutPresetMode mode = LayoutPresetMode.MinSize,
int margin = 0)
{
SetAnchorPreset(preset);
SetMarginsPreset(preset, mode, margin);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="preset">
/// The preset to apply to the anchors.
/// </param>
/// <param name="keepMargin">
/// 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.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <paramref name="preset" /> isn't a valid preset value.
/// </exception>
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);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="preset"></param>
/// <param name="resizeMode"></param>
/// <param name="margin">Some extra margin to add depending on the preset chosen.</param>
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,
}
/// <seealso cref="Control.SetMarginsPreset" />
[Flags]
[PublicAPI]
public enum LayoutPresetMode : byte
{
/// <summary>
/// Reset control size to minimum size.
/// </summary>
MinSize = 0,
/// <summary>
/// Reset height to minimum but keep width the same.
/// </summary>
KeepWidth = 1,
/// <summary>
/// Reset width to minimum but keep height the same.
/// </summary>
KeepHeight = 2,
/// <summary>
/// Do not modify control size at all.
/// </summary>
KeepSize = KeepWidth | KeepHeight,
}
/// <summary>
/// Controls how a control changes size when inside a container.
/// </summary>
[Flags]
[PublicAPI]
public enum SizeFlags : byte
{
/// <summary>
/// Shrink to the begin of the specified axis.
/// </summary>
None = 0,
/// <summary>
/// Fill as much space as possible in a container, without pushing others.
/// </summary>
Fill = 1,
/// <summary>
/// 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 <see cref="SizeFlagsStretchRatio" />
/// </summary>
Expand = 2,
/// <summary>
/// Combination of <see cref="Fill" /> and <see cref="Expand" />.
/// </summary>
FillExpand = 3,
/// <summary>
/// Shrink inside a container, aligning to the center.
/// </summary>
ShrinkCenter = 4,
/// <summary>
/// Shrink inside a container, aligning to the end.
/// </summary>
ShrinkEnd = 8,
}
/// <summary>
/// Controls how the control should move when its wanted size (controlled by anchors/margins) is smaller
/// than its minimum size.
/// </summary>
public enum GrowDirection : byte
{
/// <summary>
/// The control will expand to the bottom right to reach its minimum size.
/// </summary>
End = 0,
/// <summary>
/// The control will expand to the top left to reach its minimum size.
/// </summary>
Begin,
/// <summary>
/// The control will expand on all axes equally to reach its minimum size.
/// </summary>
Both
}
internal void DoUpdate(ProcessFrameEventArgs args)
{
Update(args);
foreach (var child in Children)
{
child.DoUpdate(args);
}
}
/// <summary>
/// This is called every process frame.
/// </summary>
protected virtual void Update(ProcessFrameEventArgs args)
{
}
internal void DoFrameUpdate(RenderFrameEventArgs args)
{
FrameUpdate(args);
foreach (var child in Children)
{
child.DoFrameUpdate(args);
}
}
/// <summary>
/// This is called before every render frame.
/// </summary>
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;
}
/// <summary>
/// Updates the indices stored inside <see cref="_children" />.
/// </summary>
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})";
}
/// <summary>
/// Mode that will be tested when testing controls to invoke mouse button events on.
/// </summary>
public enum MouseFilterMode
{
/// <summary>
/// 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.
/// </summary>
Pass = 1,
/// <summary>
/// The control will be able to receive mouse button events like <see cref="Pass" />,
/// but the event will be stopped and handled even if the relevant events do not handle it.
/// </summary>
Stop = 0,
/// <summary>
/// The control will not be considered at all, and will not have any effects.
/// </summary>
Ignore = 2,
}
public class OrderedChildCollection : ICollection<Control>, IReadOnlyCollection<Control>
{
private readonly Control Owner;
public OrderedChildCollection(Control owner)
{
Owner = owner;
}
public Enumerator GetEnumerator()
{
return new Enumerator(Owner);
}
IEnumerator<Control> IEnumerable<Control>.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<Control>.Count => Owner.ChildCount;
int IReadOnlyCollection<Control>.Count => Owner.ChildCount;
public bool IsReadOnly => false;
public struct Enumerator : IEnumerator<Control>
{
private List<Control>.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;
}
}
}
}