Files
RobustToolbox/Robust.Client/UserInterface/Control.cs
T
69c1161562 FormattedMessage/DebugConsole performance improvements (#5244)
* Add VisibilityChanged virtual to Control

* Defer updating invisible OutputPanels on UIScale change

DebugConsole falls under this when not hidden, and it significantly improves perf of e.g. resizing the window when there's a lot of stuff in there.

* Avoid redundant UI Scale updates on window resize.

Window resizing can change the UI scale, due to the auto-scaling system. This system had multiple perf issues:

UI scale was set and propagated even if it didn't change (system disabled, not effective, etc). This was just wasted processing.

UI scale was updated for every window resize event. When the game is lagging (due to the aforementioned UI scale updates being expensive...) this means multiple window resize events in a single frame ALL cause a UI scale update, which is useless.

UI scale updates from resizing now avoid doing *nothing* and are deferred until later in the frame for natural batching.

* Reduce allocations/memory usage of various rich-text related things

Just allocate a buncha dictionaries what could possibly go wrong.

I kept to non-breaking-changes which means this couldn't as effective as it should be.

There's some truly repulsive stuff here. Ugh.

* Cap debug console content size.

It's a CVar.

OutputPanel has been switched to use a new RingBufferList datastructure to make removal of the oldest entry efficient.

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-06-20 17:22:12 +10:00

1134 lines
38 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Avalonia.Metadata;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.Themes;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Animations;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface
{
/// <summary>
/// A node in the GUI system.
/// See https://docs.spacestation14.io/en/engine/user-interface for some basic concepts.
/// </summary>
[PublicAPI]
[Virtual]
public partial class Control : IDisposable
{
private readonly List<Control> _orderedChildren = new();
private bool _visible = true;
// Determines if this control requires space, even when
// it's visibility has been set to false
private bool _reservesSpace = false;
// _marginSetSize is the size calculated by the margins,
// but it's different from _size if min size is higher.
private bool _canKeyboardFocus;
public event Action<Control>? OnVisibilityChanged;
/// <summary>
/// The name of this control.
/// Names must be unique between the siblings of the control.
/// </summary>
[ViewVariables]
public string? Name { get; set; }
// ReSharper disable once ValueParameterNotUsed
public AccessLevel? Access { set { } }
/// <summary>
/// If true, this control will always be rendered, even if other UI rendering is disabled.
/// </summary>
/// <remarks>
/// Useful for e.g. primary viewports.
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)] public bool AlwaysRender { get; set; }
/// <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; }
public NameScope? NameScope;
//public void AttachNameScope(Dictionary<string, Control> nameScope)
//{
// _nameScope = nameScope;
//}
public UITheme Theme { get; internal set; }
private UITheme? _themeOverride;
public UITheme? ThemeOverride
{
get => _themeOverride;
set
{
if (_themeOverride == value)
return;
_themeOverride = value;
ThemeUpdateRecursive(Parent?.Theme ?? UserInterfaceManager.CurrentTheme);
}
}
protected virtual void OnThemeUpdated()
{
}
public void ThemeUpdateRecursive(UITheme theme)
{
theme = _themeOverride ?? theme;
if (Theme == theme)
return;
Theme = theme;
OnThemeUpdated();
foreach (var child in Children)
{
child.ThemeUpdateRecursive(Theme);
}
}
public NameScope? FindNameScope()
{
foreach (var control in this.GetSelfAndLogicalAncestors())
{
if (control.NameScope != null) return control.NameScope;
}
return null;
}
public T FindControl<T>(string name) where T : Control
{
var nameScope = FindNameScope();
if (nameScope == null)
{
throw new ArgumentException("No Namespace found for Control");
}
var value = nameScope.Find(name);
if (value == null)
{
throw new ArgumentException($"No Control with the name {name} found");
}
if (value is not T ret)
{
throw new ArgumentException($"Control with name {name} had invalid type {value.GetType()}");
}
return ret;
}
internal IUserInterfaceManagerInternal UserInterfaceManagerInternal { get; }
/// <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; }
[Content]
public virtual ICollection<Control> XamlChildren { get; protected set; }
[ViewVariables] public int ChildCount => _orderedChildren.Count;
/// <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 is UIRoot)
{
return true;
}
}
return false;
}
}
/// <summary>
/// Whether or not this control and its children are visible.
/// </summary>
/// <seealso cref="VisibleInTree"/>
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
public bool Visible
{
get => _visible;
set
{
if (_visible == value)
{
return;
}
_visible = value;
_propagateVisibilityChanged(value);
// TODO: unhardcode this.
// Many containers ignore children if they're invisible, so that's why we're replicating that here.
Parent?.InvalidateMeasure();
InvalidateMeasure();
}
}
/// <summary>
/// Called when this control's visibility in the control tree changed.
/// </summary>
protected virtual void VisibilityChanged(bool newVisible)
{
}
private void _propagateVisibilityChanged(bool newVisible)
{
VisibilityChanged(newVisible);
OnVisibilityChanged?.Invoke(this);
if (!VisibleInTree)
{
UserInterfaceManagerInternal.ControlHidden(this);
}
foreach (var child in _orderedChildren)
{
if (newVisible || child._visible)
{
child._propagateVisibilityChanged(newVisible);
}
}
}
/// <summary>
/// Whether or not this control and its children require
/// space to be reserved, even when not visible.
/// </summary>
/// <seealso cref="ReservesSpace"/>
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
public bool ReservesSpace
{
get => _reservesSpace;
set
{
if (_reservesSpace == value)
{
return;
}
_reservesSpace = value;
// TODO: unhardcode this.
// Many containers ignore children if they're invisible, so that's why we're replicating that here.
Parent?.InvalidateMeasure();
InvalidateMeasure();
}
}
/// <summary>
/// Whether or not this control is an (possibly indirect) child of
/// <see cref="IUserInterfaceManager.RootControl"/>
/// </summary>
[ViewVariables]
public bool IsInsideTree => Root != null;
[ViewVariables]
public virtual UIRoot? Root { get; internal set; }
private void _propagateExitTree()
{
Root = null;
_exitedTree();
foreach (var child in _orderedChildren)
{
child._propagateExitTree();
}
}
/// <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(UIRoot root)
{
Root = root;
_enteredTree();
foreach (var child in _orderedChildren)
{
child._propagateEnterTree(root);
}
}
/// <summary>
/// Called when the control enters the root control tree.
/// </summary>
/// <seealso cref="ExitedTree"/>
protected virtual void EnteredTree()
{
}
private void _enteredTree()
{
EnteredTree();
}
/// <summary>
/// Simple text tooltip that is shown when the mouse is hovered over this control for a bit.
/// See <see cref="TooltipSupplier"/> or <see cref="OnShowTooltip"/> for a more customizable alternative.
/// No effect when TooltipSupplier is specified.
/// </summary>
/// <remarks>
/// If empty or null, no tooltip is shown in the first place (but OnShowTooltip and OnHideTooltip
/// events are still fired).
/// </remarks>
public string? ToolTip { get; set; }
/// <summary>
/// Overrides the global tooltip delay, showing the tooltip for this
/// control within the specified number of seconds.
/// </summary>
public float? TooltipDelay { get; set; }
/// <summary>
/// Should the tooltip track the mouse cursor.
/// </summary>
public bool TrackingTooltip { get; set; }
/// <summary>
/// When a tooltip should be shown for this control, this will be invoked to
/// produce a control which will serve as the tooltip (doing nothing if null is returned).
/// This is the generally recommended way to implement custom tooltips for controls, as it takes
/// care of the various edge cases for showing / hiding the tooltip.
/// For an even more customizable approach, <see cref="OnShowTooltip"/>
///
/// The returned control will be added to PopupRoot, and positioned
/// within the user interface under the current mouse position to avoid going off the edge of the
/// screen. When the tooltip should be hidden, the control will be hidden by removing it from the tree.
///
/// It is expected that the returned control remains within PopupRoot. Other classes should
/// not move it around in the tree or move it out of PopupRoot, but may access and modify
/// the control and its children via <see cref="SuppliedTooltip"/>.
/// </summary>
/// <remarks>
/// Returning a new instance of a tooltip control every time is usually fine. If for some
/// reason constructing the tooltip control is expensive, it MAY be fine to cache + reuse a single instance but this
/// approach has not yet been tested.
/// </remarks>
public TooltipSupplier? TooltipSupplier { get; set; }
/// <summary>
/// Invoked when the mouse is hovered over this control for a bit and a tooltip
/// should be shown. Can be used as an alternative to ToolTip or TooltipSupplier to perform custom tooltip
/// logic such as showing a more complex tooltip control.
///
/// Any custom tooltip controls should typically be added
/// as a child of UserInterfaceManager.PopupRoot
/// Handlers can use <see cref="Tooltips.PositionTooltip(Control)"/> to assist with positioning
/// custom tooltip controls.
/// </summary>
public event EventHandler? OnShowTooltip;
/// <summary>
/// If this control is currently showing a tooltip provided via TooltipSupplier,
/// returns that tooltip. Do not move this control within the tree, it should remain in PopupRoot.
/// Also, as it may be hidden (removed from tree) at any time, saving a reference to this is a Bad Idea.
/// </summary>
public Control? SuppliedTooltip => UserInterfaceManagerInternal.GetSuppliedTooltipFor(this);
/// <summary>
/// Manually hide the tooltip currently being shown for this control, if there is one.
/// </summary>
public void HideTooltip()
{
UserInterfaceManagerInternal.HideTooltipFor(this);
}
internal void PerformShowTooltip()
{
OnShowTooltip?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Invoked when this control is showing a tooltip which should now be hidden.
/// </summary>
public event EventHandler? OnHideTooltip;
internal void PerformHideTooltip()
{
OnHideTooltip?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// The mode that controls how mouse filtering works. See the enum for how it functions.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public MouseFilterMode MouseFilter { get; set; } = MouseFilterMode.Ignore;
/// <summary>
/// Whether this control can take keyboard focus.
/// Keyboard focus is necessary for the control to receive keyboard events.
/// </summary>
/// <seealso cref="KeyboardFocusOnClick"/>
[ViewVariables(VVAccess.ReadWrite)]
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>
/// 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"/>
[ViewVariables(VVAccess.ReadWrite)]
public int RectDrawClipMargin { get; set; } = 10;
// You may wonder why Modulate isn't stylesheet controlled, but ModulateSelf is.
// Reason is simple: I'm fucking lazy.
// I'm expecting this comment to last much longer than the problem it's pointing out.
/// <summary>
/// An override for the modulate self from the style sheet.
/// </summary>
/// <seealso cref="ActualModulateSelf" />
[ViewVariables(VVAccess.ReadWrite)]
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>
[ViewVariables(VVAccess.ReadWrite)]
[Animatable]
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>
/// 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);
Theme = UserInterfaceManagerInternal.CurrentTheme;
XamlChildren = Children;
}
/// <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);
}
protected internal virtual void PreRenderChildren(ref ControlRenderArguments args)
{
}
protected internal virtual void PostRenderChildren(ref ControlRenderArguments args)
{
}
protected internal virtual void RenderChildOverride(ref ControlRenderArguments args, int childIndex, Vector2i position)
{
RenderControl(ref args, childIndex, position);
}
public ref struct ControlRenderArguments
{
public IRenderHandle Handle;
public ref int Total;
public Vector2i Position;
public Color Modulate;
public UIBox2i? ScissorBox;
public ref Matrix3x2 CoordinateTransform;
}
protected void RenderControl(ref ControlRenderArguments args, int childIndex, Vector2i position)
{
UserInterfaceManagerInternal.RenderControl(args.Handle, ref args.Total, GetChild(childIndex), position, args.Modulate, args.ScissorBox, args.CoordinateTransform);
}
public void UpdateDraw()
{
}
/// <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);
Disposed = true;
}
protected virtual void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
UserInterfaceManagerInternal.HideTooltipFor(this);
DisposeAllChildren();
Parent?.RemoveChild(this);
OnKeyBindDown = null;
}
/// <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()
{
DebugTools.Assert(!Disposed, "Control has been disposed.");
foreach (var child in Children.ToArray())
{
// This checks fails in some obscure cases like using the element inspector in the dev window.
// Why? Well I could probably spend 15 minutes in a debugger to find out,
// but I'd probably still end up with this fix.
if (child.Parent == this)
RemoveChild(child);
}
}
/// <summary>
/// Make this child an orphan. i.e. remove it from its parent if it has one.
/// </summary>
public void Orphan()
{
DebugTools.Assert(!Disposed, "Control has been disposed.");
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)
{
DebugTools.Assert(!Disposed, "Control has been disposed.");
if (child == null) throw new ArgumentNullException(nameof(child));
if (child.Parent != null)
{
throw new InvalidOperationException("This component is still parented. Deparent it before adding it.");
}
DebugTools.Assert(!child.Disposed, "Child is disposed.");
if (child == this)
{
throw new InvalidOperationException("You can't parent something to itself!");
}
// Ensure this control isn't a parent of ours.
// Doesn't need to happen if the control has no children of course.
if (child.ChildCount != 0)
{
for (var parent = Parent; parent != null; parent = parent.Parent)
{
if (parent == child)
{
throw new ArgumentException("This control is one of our parents!", nameof(child));
}
}
}
child.Parent = this;
_orderedChildren.Add(child);
child.Parented(this);
if (Root != null)
{
child._propagateEnterTree(Root);
}
ChildAdded(child);
}
public event Action<Control>? OnChildAdded;
/// <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)
{
OnChildAdded?.Invoke(newChild);
newChild.ThemeUpdateRecursive(Theme);
InvalidateMeasure();
}
/// <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)
{
StylesheetUpdateRecursive();
InvalidateMeasure();
}
/// <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)
{
DebugTools.Assert(!Disposed, "Control has been disposed.");
if (child.Parent != this)
{
throw new InvalidOperationException("The provided control is not a direct child of this control.");
}
_orderedChildren.Remove(child);
child.Parent = null;
child.Deparented();
if (IsInsideTree)
{
child._propagateExitTree();
}
ChildRemoved(child);
}
public event Action<Control>? OnChildRemoved;
/// <summary>
/// Called when a child is removed from this child.
/// </summary>
/// <param name="child">The former child.</param>
protected virtual void ChildRemoved(Control child)
{
OnChildRemoved?.Invoke(child);
InvalidateMeasure();
}
/// <summary>
/// Called when this control is removed as child from the former parent.
/// </summary>
protected virtual void Deparented()
{
}
public event Action<ControlChildMovedEventArgs>? OnChildMoved;
/// <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)
{
OnChildMoved?.Invoke(new ControlChildMovedEventArgs(child, oldIndex, newIndex));
}
/// <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)
{
var size = Size;
return point.X >= 0 && point.X <= size.X && point.Y >= 0 && point.Y <= size.Y;
}
/// <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 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._orderedChildren.IndexOf(this);
}
/// <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;
}
// If it was at the top index and we re-add it there then don't throw.
Parent._orderedChildren.RemoveAt(posInParent);
if (position == Parent._orderedChildren.Count)
{
Parent._orderedChildren.Add(this);
}
else
{
Parent._orderedChildren.Insert(position, this);
}
Parent.ChildMoved(this, posInParent, position);
}
/// <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()
{
if (Parent == null)
{
throw new InvalidOperationException("No parent to change position in.");
}
SetPositionInParent(Parent.ChildCount - 1);
}
/// <summary>
/// Called when this control receives keyboard focus.
/// </summary>
protected internal virtual void KeyboardFocusEntered()
{
}
/// <summary>
/// Called when this control loses keyboard focus (corresponds to UserInterfaceManager.KeyboardFocused).
/// </summary>
protected internal virtual void KeyboardFocusExited()
{
}
/// <summary>
/// Fired when a control loses control focus for any reason. See <see cref="IUserInterfaceManager.ControlFocused"/>.
/// </summary>
/// <remarks>
/// Controls which have some sort of drag / drop behavior should usually implement this method (typically by cancelling the drag drop).
/// Otherwise, if a user clicks down LMB over one control to initiate a drag, then clicks RMB down
/// over a different control while still holding down LMB, the control being dragged will now lose focus
/// and will no longer receive the keyup for the LMB, thus won't cancel the drag.
/// This should also be considered for controls which have any special KeyBindUp behavior - consider
/// what would happen if the control lost focus and never received the KeyBindUp.
///
/// There is no corresponding ControlFocusEntered - if a control wants to handle that situation they should simply
/// handle KeyBindDown as that's the only way a control would gain focus.
/// </remarks>
protected internal virtual void ControlFocusExited()
{
}
/// <summary>
/// Check if this control currently has keyboard focus.
/// </summary>
/// <returns></returns>
public virtual 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);
}
public event Action? OnResized;
/// <summary>
/// Called when the size of the control changes.
/// </summary>
protected virtual void Resized()
{
OnResized?.Invoke();
}
internal int DoFrameUpdateRecursive(FrameEventArgs args)
{
var total = 1;
FrameUpdate(args);
foreach (var child in Children)
{
total += child.DoFrameUpdateRecursive(args);
}
return total;
}
/// <summary>
/// This is called before every render frame.
/// </summary>
protected virtual void FrameUpdate(FrameEventArgs args)
{
ProcessAnimations(args);
}
// These are separate from StandardCursorShape so that
// in the future we could have an API to override the styling.
public override string ToString()
{
return $"{Name} ({GetType().Name})";
}
/// <summary>
/// Mode that will be tested when testing controls to invoke mouse button events on.
/// </summary>
public enum MouseFilterMode : byte
{
/// <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 sealed class OrderedChildCollection : ICollection<Control>, IReadOnlyCollection<Control>
{
private readonly Control Owner;
public OrderedChildCollection(Control owner)
{
Owner = owner;
}
public Enumerator GetEnumerator()
{
return new(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();
}
}
}
}
public delegate Control? TooltipSupplier(Control sender);
public readonly struct ControlChildMovedEventArgs
{
public ControlChildMovedEventArgs(Control control, int oldIndex, int newIndex)
{
Control = control;
OldIndex = oldIndex;
NewIndex = newIndex;
}
public readonly Control Control;
public readonly int OldIndex;
public readonly int NewIndex;
}
}