// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Framework.Logging; using osu.Framework.Statistics; using osu.Framework.Threading; using osu.Framework.Timing; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.OpenGL; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.States; using osu.Framework.Layout; using osu.Framework.Testing; using osu.Framework.Utils; using osuTK.Input; using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Framework.Graphics { /// /// Drawables are the basic building blocks of a scene graph in this framework. /// Anything that is visible or that the user interacts with has to be a Drawable. /// /// For example: /// - Boxes /// - Sprites /// - Collections of Drawables /// /// Drawables are always rectangular in shape in their local coordinate system, /// which makes them quad-shaped in arbitrary (linearly transformed) coordinate systems. /// [ExcludeFromDynamicCompile] public abstract partial class Drawable : Transformable, IDisposable, IDrawable { #region Construction and disposal protected Drawable() { total_count.Value++; AddLayout(drawInfoBacking); AddLayout(drawSizeBacking); AddLayout(screenSpaceDrawQuadBacking); AddLayout(drawColourInfoBacking); AddLayout(requiredParentSizeToFitBacking); } private static readonly GlobalStatistic total_count = GlobalStatistics.Get(nameof(Drawable), "Total constructed"); internal bool IsLongRunning => GetType().GetCustomAttribute() != null; /// /// Disposes this drawable. /// public void Dispose() { //we can't dispose if we are mid-load, else our children may get in a bad state. lock (LoadLock) Dispose(true); GC.SuppressFinalize(this); } protected internal bool IsDisposed { get; private set; } /// /// Disposes this drawable. /// protected virtual void Dispose(bool isDisposing) { if (IsDisposed) return; UnbindAllBindables(); // Bypass expensive operations as a result of setting the Parent property, by setting the field directly. parent = null; ChildID = 0; OnUpdate = null; Invalidated = null; OnDispose?.Invoke(); OnDispose = null; for (int i = 0; i < drawNodes.Length; i++) drawNodes[i]?.Dispose(); IsDisposed = true; } /// /// Whether this Drawable should be disposed when it is automatically removed from /// its due to being false. /// public virtual bool DisposeOnDeathRemoval => RemoveCompletedTransforms; private static readonly ConcurrentDictionary> unbind_action_cache = new ConcurrentDictionary>(); /// /// Recursively invokes on this and all s further down the scene graph. /// internal virtual void UnbindAllBindablesSubTree() => UnbindAllBindables(); private void cacheUnbindActions() { foreach (var type in GetType().EnumerateBaseTypes()) { if (unbind_action_cache.TryGetValue(type, out _)) return; // List containing all the delegates to perform the unbinds var actions = new List>(); // Generate delegates to unbind fields actions.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(f => typeof(IUnbindable).IsAssignableFrom(f.FieldType)) .Select(f => new Action(target => ((IUnbindable)f.GetValue(target))?.UnbindAll()))); // Delegates to unbind properties are intentionally not generated. // Properties with backing fields (including automatic properties) will be picked up by the field unbind delegate generation, // while ones without backing fields (like get-only properties that delegate to another drawable's bindable) should not be unbound here. unbind_action_cache[type] = target => { foreach (var a in actions) { try { a(target); } catch (Exception e) { Logger.Error(e, $"Failed to unbind a local bindable in {type.ReadableName()}"); } } }; } } private bool unbindComplete; /// /// Unbinds all s stored as fields or properties in this . /// internal virtual void UnbindAllBindables() { if (unbindComplete) return; unbindComplete = true; foreach (var type in GetType().EnumerateBaseTypes()) { if (unbind_action_cache.TryGetValue(type, out var existing)) existing?.Invoke(this); } OnUnbindAllBindables?.Invoke(); } #endregion #region Loading /// /// Whether this Drawable is fully loaded. /// This is true iff has run once on this . /// public bool IsLoaded => loadState >= LoadState.Loaded; private volatile LoadState loadState; /// /// Describes the current state of this Drawable within the loading pipeline. /// public LoadState LoadState => loadState; /// /// The thread on which the operation started, or null if has not started loading. /// internal Thread LoadThread { get; private set; } internal readonly object LoadLock = new object(); private static readonly StopwatchClock perf_clock = new StopwatchClock(true); /// /// Load this drawable from an async context. /// Because we can't be sure of the disposal state, it is returned as a bool rather than thrown as in . /// /// The clock we should use by default. /// The dependency tree we will inherit by default. May be extended via /// Whether this call is being executed from a directly async context (not a parent). /// Whether the load was successful. internal bool LoadFromAsync(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies, bool isDirectAsyncContext = false) { lock (LoadLock) { if (IsDisposed) return false; Load(clock, dependencies, isDirectAsyncContext); return true; } } /// /// Loads this drawable, including the gathering of dependencies and initialisation of required resources. /// /// The clock we should use by default. /// The dependency tree we will inherit by default. May be extended via /// Whether this call is being executed from a directly async context (not a parent). internal void Load(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies, bool isDirectAsyncContext = false) { lock (LoadLock) { if (!isDirectAsyncContext && IsLongRunning) throw new InvalidOperationException("Tried to load a long-running drawable in a non-direct async context. See https://git.io/Je1YF for more details."); if (IsDisposed) throw new ObjectDisposedException(ToString(), "Attempting to load an already disposed drawable."); if (loadState == LoadState.NotLoaded) { Trace.Assert(loadState == LoadState.NotLoaded); loadState = LoadState.Loading; load(clock, dependencies); loadState = LoadState.Ready; } } } private void load(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies) { LoadThread = Thread.CurrentThread; UpdateClock(clock); double timeBefore = DebugUtils.LogPerformanceIssues ? perf_clock.CurrentTime : 0; RequestsNonPositionalInput = HandleInputCache.RequestsNonPositionalInput(this); RequestsPositionalInput = HandleInputCache.RequestsPositionalInput(this); RequestsNonPositionalInputSubTree = RequestsNonPositionalInput; RequestsPositionalInputSubTree = RequestsPositionalInput; InjectDependencies(dependencies); cacheUnbindActions(); LoadAsyncComplete(); if (timeBefore > 1000) { double loadDuration = perf_clock.CurrentTime - timeBefore; bool blocking = ThreadSafety.IsUpdateThread; double allowedDuration = blocking ? 16 : 100; if (loadDuration > allowedDuration) { Logger.Log($@"{ToString()} took {loadDuration:0.00}ms to load" + (blocking ? " (and blocked the update thread)" : " (async)"), LoggingTarget.Performance, blocking ? LogLevel.Important : LogLevel.Verbose); } } } /// /// Injects dependencies from an into this . /// /// The dependencies to inject. protected virtual void InjectDependencies(IReadOnlyDependencyContainer dependencies) => dependencies.Inject(this); /// /// Runs once on the update thread after loading has finished. /// private bool loadComplete() { if (loadState < LoadState.Ready) return false; loadState = LoadState.Loaded; // From a synchronous point of view, this is the first time the Drawable receives a parent. // If this Drawable calculated properties such as DrawInfo that depend on the parent state before this point, they must be re-validated in the now-correct state. // A "parent" source is faked since Child+Self states are always assumed valid if they only access local Drawable states (e.g. Colour but not DrawInfo). // Only layout flags are required, as non-layout flags are always propagated by the parent. Invalidate(Invalidation.Layout, InvalidationSource.Parent); LoadComplete(); OnLoadComplete?.Invoke(this); OnLoadComplete = null; return true; } /// /// Invoked after dependency injection has completed for this and all /// children if this is a . /// /// /// This method is invoked in the potentially asynchronous context of prior to /// this becoming = true. /// protected virtual void LoadAsyncComplete() { } /// /// Invoked after this has finished loading. /// /// /// This method is invoked on the update thread inside this 's . /// protected virtual void LoadComplete() { } #endregion #region Sorting (CreationID / Depth) /// /// Captures the order in which Drawables were added to a . Each Drawable /// is assigned a monotonically increasing ID upon being added to a . This /// ID is unique within the . /// The primary use case of this ID is stable sorting of Drawables with equal . /// internal ulong ChildID { get; set; } /// /// Whether this drawable has been added to a parent . Note that this does NOT imply that /// has been set. /// This is primarily used to block properties such as that strictly rely on the value of /// to alert the user of an invalid operation. /// internal bool IsPartOfComposite => ChildID != 0; /// /// Whether this drawable is part of its parent's . /// public bool IsAlive { get; internal set; } private float depth; /// /// Controls which Drawables are behind or in front of other Drawables. /// This amounts to sorting Drawables by their . /// A Drawable with higher than another Drawable is /// drawn behind the other Drawable. /// public float Depth { get => depth; set { if (IsPartOfComposite) { throw new InvalidOperationException( $"May not change {nameof(Depth)} while inside a parent {nameof(CompositeDrawable)}." + $"Use the parent's {nameof(CompositeDrawable.ChangeInternalChildDepth)} or {nameof(Container.ChangeChildDepth)} instead."); } depth = value; } } #endregion #region Periodic tasks (events, Scheduler, Transforms, Update) /// /// This event is fired after the method is called at the end of /// . It should be used when a simple action should be performed /// at the end of every update call which does not warrant overriding the Drawable. /// public event Action OnUpdate; /// /// This event is fired after the method is called. /// It should be used when a simple action should be performed /// when the Drawable is loaded which does not warrant overriding the Drawable. /// This event is automatically cleared after being invoked. /// public event Action OnLoadComplete; /// . /// Fired after the method is called. /// internal event Action Invalidated; /// /// Fired after the method is called. /// internal event Action OnDispose; /// /// Fired after the method is called. /// internal event Action OnUnbindAllBindables; /// /// A lock exclusively used for initial acquisition/construction of the . /// private static readonly object scheduler_acquisition_lock = new object(); private Scheduler scheduler; /// /// A lazily-initialized scheduler used to schedule tasks to be invoked in future s calls. /// The tasks are invoked at the beginning of the method before anything else. /// protected internal Scheduler Scheduler { get { if (scheduler != null) return scheduler; lock (scheduler_acquisition_lock) return scheduler ??= new Scheduler(() => ThreadSafety.IsUpdateThread, Clock); } } /// /// Updates this Drawable and all Drawables further down the scene graph. /// Called once every frame. /// /// False if the drawable should not be updated. public virtual bool UpdateSubTree() { if (IsDisposed) throw new ObjectDisposedException(ToString(), "Disposed Drawables may never be in the scene graph."); if (ProcessCustomClock) customClock?.ProcessFrame(); if (loadState < LoadState.Ready) return false; if (loadState == LoadState.Ready) loadComplete(); Debug.Assert(loadState == LoadState.Loaded); UpdateTransforms(); if (!IsPresent) return true; if (scheduler != null) { int amountScheduledTasks = scheduler.Update(); FrameStatistics.Add(StatisticsCounterType.ScheduleInvk, amountScheduledTasks); } Update(); OnUpdate?.Invoke(this); return true; } /// /// Updates all masking calculations for this . /// This occurs post- to ensure that all updates have taken place. /// /// The parent that triggered this update on this . /// The that defines the masking bounds. /// Whether masking calculations have taken place. public virtual bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) { if (!IsPresent) return false; if (HasProxy && source != proxy) return false; IsMaskedAway = ComputeIsMaskedAway(maskingBounds); return true; } /// /// Computes whether this is masked away. /// /// The that defines the masking bounds. /// Whether this is currently masked away. protected virtual bool ComputeIsMaskedAway(RectangleF maskingBounds) => !Precision.AlmostIntersects(maskingBounds, ScreenSpaceDrawQuad.AABBFloat); /// /// Performs a once-per-frame update specific to this Drawable. A more elegant alternative to /// when deriving from . Note, that this /// method is always called before Drawables further down the scene graph are updated. /// protected virtual void Update() { } #endregion #region Position / Size (with margin) private Vector2 position { get => new Vector2(x, y); set { x = value.X; y = value.Y; } } /// /// Positional offset of to in the /// 's coordinate system. May be in absolute or relative units /// (controlled by ). /// public Vector2 Position { get => position; set { if (position == value) return; if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Position)} must be finite, but is {value}."); Axes changedAxes = Axes.None; if (position.X != value.X) changedAxes |= Axes.X; if (position.Y != value.Y) changedAxes |= Axes.Y; position = value; invalidateParentSizeDependencies(Invalidation.MiscGeometry, changedAxes); } } private float x; private float y; /// /// X component of . /// public float X { get => x; set { if (x == value) return; if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(X)} must be finite, but is {value}."); x = value; invalidateParentSizeDependencies(Invalidation.MiscGeometry, Axes.X); } } /// /// Y component of . /// public float Y { get => y; set { if (y == value) return; if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(Y)} must be finite, but is {value}."); y = value; invalidateParentSizeDependencies(Invalidation.MiscGeometry, Axes.Y); } } private Axes relativePositionAxes; /// /// Controls which of are relative w.r.t. /// 's size (from 0 to 1) rather than absolute. /// The set in this property are ignored by automatically sizing /// parents. /// /// /// When setting this property, the is converted such that /// remains invariant. /// public Axes RelativePositionAxes { get => relativePositionAxes; set { if (value == relativePositionAxes) return; // Convert coordinates from relative to absolute or vice versa Vector2 conversion = relativeToAbsoluteFactor; if ((value & Axes.X) > (relativePositionAxes & Axes.X)) X = Precision.AlmostEquals(conversion.X, 0) ? 0 : X / conversion.X; else if ((relativePositionAxes & Axes.X) > (value & Axes.X)) X *= conversion.X; if ((value & Axes.Y) > (relativePositionAxes & Axes.Y)) Y = Precision.AlmostEquals(conversion.Y, 0) ? 0 : Y / conversion.Y; else if ((relativePositionAxes & Axes.Y) > (value & Axes.Y)) Y *= conversion.Y; relativePositionAxes = value; updateBypassAutoSizeAxes(); } } /// /// Absolute positional offset of to /// in the 's coordinate system. /// public Vector2 DrawPosition { get { Vector2 offset = Vector2.Zero; if (Parent != null && RelativePositionAxes != Axes.None) { offset = Parent.RelativeChildOffset; if (!RelativePositionAxes.HasFlagFast(Axes.X)) offset.X = 0; if (!RelativePositionAxes.HasFlagFast(Axes.Y)) offset.Y = 0; } return ApplyRelativeAxes(RelativePositionAxes, Position - offset, FillMode.Stretch); } } private Vector2 size { get => new Vector2(width, height); set { width = value.X; height = value.Y; } } /// /// Size of this Drawable in the 's coordinate system. /// May be in absolute or relative units (controlled by ). /// public virtual Vector2 Size { get => size; set { if (size == value) return; if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Size)} must be finite, but is {value}."); Axes changedAxes = Axes.None; if (size.X != value.X) changedAxes |= Axes.X; if (size.Y != value.Y) changedAxes |= Axes.Y; size = value; invalidateParentSizeDependencies(Invalidation.DrawSize, changedAxes); } } private float width; private float height; /// /// X component of . /// public virtual float Width { get => width; set { if (width == value) return; if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(Width)} must be finite, but is {value}."); width = value; invalidateParentSizeDependencies(Invalidation.DrawSize, Axes.X); } } /// /// Y component of . /// public virtual float Height { get => height; set { if (height == value) return; if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(Height)} must be finite, but is {value}."); height = value; invalidateParentSizeDependencies(Invalidation.DrawSize, Axes.Y); } } private Axes relativeSizeAxes; /// /// Controls which are relative sizes w.r.t. 's size /// (from 0 to 1) in the 's coordinate system, rather than absolute sizes. /// The set in this property are ignored by automatically sizing /// parents. /// /// /// If an axis becomes relatively sized and its component of was previously 0, /// then it automatically becomes 1. In all other cases is converted such that /// remains invariant across changes of this property. /// public virtual Axes RelativeSizeAxes { get => relativeSizeAxes; set { if (value == relativeSizeAxes) return; // In some cases we cannot easily preserve our size, and so we simply invalidate and // leave correct sizing to the user. if (fillMode != FillMode.Stretch && (value == Axes.Both || relativeSizeAxes == Axes.Both)) Invalidate(Invalidation.DrawSize); else { // Convert coordinates from relative to absolute or vice versa Vector2 conversion = relativeToAbsoluteFactor; if ((value & Axes.X) > (relativeSizeAxes & Axes.X)) Width = Precision.AlmostEquals(conversion.X, 0) ? 0 : Width / conversion.X; else if ((relativeSizeAxes & Axes.X) > (value & Axes.X)) Width *= conversion.X; if ((value & Axes.Y) > (relativeSizeAxes & Axes.Y)) Height = Precision.AlmostEquals(conversion.Y, 0) ? 0 : Height / conversion.Y; else if ((relativeSizeAxes & Axes.Y) > (value & Axes.Y)) Height *= conversion.Y; } relativeSizeAxes = value; if (relativeSizeAxes.HasFlagFast(Axes.X) && Width == 0) Width = 1; if (relativeSizeAxes.HasFlagFast(Axes.Y) && Height == 0) Height = 1; updateBypassAutoSizeAxes(); OnSizingChanged(); } } private readonly LayoutValue drawSizeBacking = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); /// /// Absolute size of this Drawable in the 's coordinate system. /// public Vector2 DrawSize => drawSizeBacking.IsValid ? drawSizeBacking : drawSizeBacking.Value = ApplyRelativeAxes(RelativeSizeAxes, Size, FillMode); /// /// X component of . /// public float DrawWidth => DrawSize.X; /// /// Y component of . /// public float DrawHeight => DrawSize.Y; private MarginPadding margin; /// /// Size of an empty region around this Drawable used to manipulate /// layout. Does not affect or the region of accepted input, /// but does affect . /// public MarginPadding Margin { get => margin; set { if (margin.Equals(value)) return; if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Margin)} must be finite, but is {value}."); margin = value; Invalidate(Invalidation.MiscGeometry); } } /// /// Absolute size of this Drawable's layout rectangle in the 's /// coordinate system; i.e. with the addition of . /// public Vector2 LayoutSize => DrawSize + new Vector2(margin.TotalHorizontal, margin.TotalVertical); /// /// Absolutely sized rectangle for drawing in the 's coordinate system. /// Based on . /// public RectangleF DrawRectangle { get { Vector2 s = DrawSize; return new RectangleF(0, 0, s.X, s.Y); } } /// /// Absolutely sized rectangle for layout in the 's coordinate system. /// Based on and . /// public RectangleF LayoutRectangle { get { Vector2 s = LayoutSize; return new RectangleF(-margin.Left, -margin.Top, s.X, s.Y); } } /// /// Helper function for converting potentially relative coordinates in the /// 's space to absolute coordinates based on which /// axes are relative. /// /// Describes which axes are relative. /// The coordinates to convert. /// The to be used. /// Absolute coordinates in 's space. protected Vector2 ApplyRelativeAxes(Axes relativeAxes, Vector2 v, FillMode fillMode) { if (relativeAxes != Axes.None) { Vector2 conversion = relativeToAbsoluteFactor; if (relativeAxes.HasFlagFast(Axes.X)) v.X *= conversion.X; if (relativeAxes.HasFlagFast(Axes.Y)) v.Y *= conversion.Y; // FillMode only makes sense if both axes are relatively sized as the general rule // for n-dimensional aspect preservation is to simply take the minimum or the maximum // scale among all active axes. For single axes the minimum / maximum is just the // value itself. if (relativeAxes == Axes.Both && fillMode != FillMode.Stretch) { if (fillMode == FillMode.Fill) v = new Vector2(Math.Max(v.X, v.Y * fillAspectRatio)); else if (fillMode == FillMode.Fit) v = new Vector2(Math.Min(v.X, v.Y * fillAspectRatio)); v.Y /= fillAspectRatio; } } return v; } /// /// Conversion factor from relative to absolute coordinates in the 's space. /// private Vector2 relativeToAbsoluteFactor => Parent?.RelativeToAbsoluteFactor ?? Vector2.One; private Axes bypassAutoSizeAxes; private void updateBypassAutoSizeAxes() { var value = RelativePositionAxes | RelativeSizeAxes | bypassAutoSizeAdditionalAxes; if (bypassAutoSizeAxes != value) { var changedAxes = bypassAutoSizeAxes ^ value; bypassAutoSizeAxes = value; if (((Parent?.AutoSizeAxes ?? 0) & changedAxes) != 0) Parent?.Invalidate(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child); } } private Axes bypassAutoSizeAdditionalAxes; /// /// Controls which are ignored by parent automatic sizing. /// Most notably, and do not affect /// automatic sizing to avoid circular size dependencies. /// public Axes BypassAutoSizeAxes { get => bypassAutoSizeAxes; set { bypassAutoSizeAdditionalAxes = value; updateBypassAutoSizeAxes(); } } /// /// Computes the bounding box of this drawable in its parent's space. /// public virtual RectangleF BoundingBox => ToParentSpace(LayoutRectangle).AABBFloat; /// /// Called whenever the of this drawable is changed, or when the are changed if this drawable is a . /// protected virtual void OnSizingChanged() { } #endregion #region Scale / Shear / Rotation private Vector2 scale = Vector2.One; /// /// Base relative scaling factor around . /// public Vector2 Scale { get => scale; set { if (Math.Abs(value.X) < Precision.FLOAT_EPSILON) value.X = Precision.FLOAT_EPSILON; if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON) value.Y = Precision.FLOAT_EPSILON; if (scale == value) return; if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Scale)} must be finite, but is {value}."); bool wasPresent = IsPresent; scale = value; if (IsPresent != wasPresent) Invalidate(Invalidation.MiscGeometry | Invalidation.Presence); else Invalidate(Invalidation.MiscGeometry); } } private float fillAspectRatio = 1; /// /// The desired ratio of width to height when under the effect of a non-stretching /// and being . /// public float FillAspectRatio { get => fillAspectRatio; set { if (fillAspectRatio == value) return; if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(FillAspectRatio)} must be finite, but is {value}."); if (value == 0) throw new ArgumentException($@"{nameof(FillAspectRatio)} must be non-zero."); fillAspectRatio = value; if (fillMode != FillMode.Stretch && RelativeSizeAxes == Axes.Both) Invalidate(Invalidation.DrawSize); } } private FillMode fillMode; /// /// Controls the behavior of when it is set to . /// Otherwise, this member has no effect. By default, stretching is used, which simply scales /// this drawable's according to 's /// disregarding this drawable's . Other values of preserve . /// public FillMode FillMode { get => fillMode; set { if (fillMode == value) return; fillMode = value; Invalidate(Invalidation.DrawSize); } } /// /// Relative scaling factor around . /// protected virtual Vector2 DrawScale => Scale; private Vector2 shear = Vector2.Zero; /// /// Relative shearing factor. The X dimension is relative w.r.t. /// and the Y dimension relative w.r.t. . /// public Vector2 Shear { get => shear; set { if (shear == value) return; if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Shear)} must be finite, but is {value}."); shear = value; Invalidate(Invalidation.MiscGeometry); } } private float rotation; /// /// Rotation in degrees around . /// public float Rotation { get => rotation; set { if (value == rotation) return; if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(Rotation)} must be finite, but is {value}."); rotation = value; Invalidate(Invalidation.MiscGeometry); } } #endregion #region Origin / Anchor private Anchor origin = Anchor.TopLeft; /// /// The origin of this . /// /// If the provided value does not exist in the enumeration. public virtual Anchor Origin { get => origin; set { if (origin == value) return; if (value == 0) throw new ArgumentException("Cannot set origin to 0.", nameof(value)); origin = value; Invalidate(Invalidation.MiscGeometry); } } private Vector2 customOrigin; /// /// The origin of this expressed in relative coordinates from the top-left corner of . /// /// If is . public Vector2 RelativeOriginPosition { get { if (Origin == Anchor.Custom) throw new InvalidOperationException(@"Can not obtain relative origin position for custom origins."); Vector2 result = Vector2.Zero; if (origin.HasFlagFast(Anchor.x1)) result.X = 0.5f; else if (origin.HasFlagFast(Anchor.x2)) result.X = 1; if (origin.HasFlagFast(Anchor.y1)) result.Y = 0.5f; else if (origin.HasFlagFast(Anchor.y2)) result.Y = 1; return result; } } /// /// The origin of this expressed in absolute coordinates from the top-left corner of . /// /// If the provided value is not finite. public virtual Vector2 OriginPosition { get { Vector2 result; if (Origin == Anchor.Custom) result = customOrigin; else if (Origin == Anchor.TopLeft) result = Vector2.Zero; else result = computeAnchorPosition(LayoutSize, Origin); return result - new Vector2(margin.Left, margin.Top); } set { if (customOrigin == value && Origin == Anchor.Custom) return; if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(OriginPosition)} must be finite, but is {value}."); customOrigin = value; Origin = Anchor.Custom; Invalidate(Invalidation.MiscGeometry); } } private Anchor anchor = Anchor.TopLeft; /// /// Specifies where is attached to the /// in the coordinate system with origin at the top left corner of the /// 's . /// Can either be one of 9 relative positions (0, 0.5, and 1 in x and y) /// or a fixed absolute position via . /// public Anchor Anchor { get => anchor; set { if (anchor == value) return; if (value == 0) throw new ArgumentException("Cannot set anchor to 0.", nameof(value)); anchor = value; Invalidate(Invalidation.MiscGeometry); } } private Vector2 customRelativeAnchorPosition; /// /// Specifies in relative coordinates where is attached /// to the in the coordinate system with origin at the top /// left corner of the 's , and /// a value of referring to the bottom right corner of /// the 's . /// public Vector2 RelativeAnchorPosition { get { if (Anchor == Anchor.Custom) return customRelativeAnchorPosition; Vector2 result = Vector2.Zero; if (anchor.HasFlagFast(Anchor.x1)) result.X = 0.5f; else if (anchor.HasFlagFast(Anchor.x2)) result.X = 1; if (anchor.HasFlagFast(Anchor.y1)) result.Y = 0.5f; else if (anchor.HasFlagFast(Anchor.y2)) result.Y = 1; return result; } set { if (customRelativeAnchorPosition == value && Anchor == Anchor.Custom) return; if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeAnchorPosition)} must be finite, but is {value}."); customRelativeAnchorPosition = value; Anchor = Anchor.Custom; Invalidate(Invalidation.MiscGeometry); } } /// /// Specifies in absolute coordinates where is attached /// to the in the coordinate system with origin at the top /// left corner of the 's . /// public Vector2 AnchorPosition => RelativeAnchorPosition * Parent?.ChildSize ?? Vector2.Zero; /// /// Helper function to compute an absolute position given an absolute size and /// a relative . /// /// Absolute size /// Relative /// Absolute position private static Vector2 computeAnchorPosition(Vector2 size, Anchor anchor) { Vector2 result = Vector2.Zero; if (anchor.HasFlagFast(Anchor.x1)) result.X = size.X / 2f; else if (anchor.HasFlagFast(Anchor.x2)) result.X = size.X; if (anchor.HasFlagFast(Anchor.y1)) result.Y = size.Y / 2f; else if (anchor.HasFlagFast(Anchor.y2)) result.Y = size.Y; return result; } #endregion #region Colour / Alpha / Blending private ColourInfo colour = Color4.White; /// /// Colour of this in sRGB space. Can contain individual colours for all four /// corners of this , which are then interpolated, but can also be assigned /// just a single colour. Implicit casts from and from exist. /// public ColourInfo Colour { get => colour; set { if (colour.Equals(value)) return; colour = value; Invalidate(Invalidation.Colour); } } private float alpha = 1.0f; /// /// Multiplicative alpha factor applied on top of and its existing /// alpha channel(s). /// public float Alpha { get => alpha; set { if (alpha == value) return; bool wasPresent = IsPresent; alpha = value; if (IsPresent != wasPresent) Invalidate(Invalidation.Colour | Invalidation.Presence); else Invalidate(Invalidation.Colour); } } private const float visibility_cutoff = 0.0001f; /// /// Determines whether this Drawable is present based on its value. /// Can be forced always on with . /// public virtual bool IsPresent => AlwaysPresent || Alpha > visibility_cutoff && Math.Abs(Scale.X) > Precision.FLOAT_EPSILON && Math.Abs(Scale.Y) > Precision.FLOAT_EPSILON; private bool alwaysPresent; /// /// If true, forces to always be true. In other words, /// this drawable is always considered for layout, input, and drawing, regardless /// of alpha value. /// public bool AlwaysPresent { get => alwaysPresent; set { if (alwaysPresent == value) return; bool wasPresent = IsPresent; alwaysPresent = value; if (IsPresent != wasPresent) Invalidate(Invalidation.Presence); } } private BlendingParameters blending; /// /// Determines how this Drawable is blended with other already drawn Drawables. /// Inherits the 's by default. /// public BlendingParameters Blending { get => blending; set { if (blending == value) return; blending = value; Invalidate(Invalidation.Colour); } } #endregion #region Timekeeping private IFrameBasedClock customClock; private IFrameBasedClock clock; /// /// The clock of this drawable. Used for keeping track of time across /// frames. By default is inherited from . /// If set, then the provided value is used as a custom clock and the /// 's clock is ignored. /// public override IFrameBasedClock Clock { get => clock; set { customClock = value; UpdateClock(customClock); } } /// /// Updates the clock to be used. Has no effect if this drawable /// uses a custom clock. /// /// The new clock to be used. internal virtual void UpdateClock(IFrameBasedClock clock) { this.clock = customClock ?? clock; scheduler?.UpdateClock(this.clock); } /// /// Whether should be automatically invoked on this 's /// in . This should only be set to false in scenarios where the clock is updated elsewhere. /// public bool ProcessCustomClock = true; private double lifetimeStart = double.MinValue; private double lifetimeEnd = double.MaxValue; /// /// Invoked after or has changed. /// internal event Action LifetimeChanged; /// /// The time at which this drawable becomes valid (and is considered for drawing). /// public virtual double LifetimeStart { get => lifetimeStart; set { if (lifetimeStart == value) return; lifetimeStart = value; LifetimeChanged?.Invoke(this); } } /// /// The time at which this drawable is no longer valid (and is considered for disposal). /// public virtual double LifetimeEnd { get => lifetimeEnd; set { if (lifetimeEnd == value) return; lifetimeEnd = value; LifetimeChanged?.Invoke(this); } } /// /// Whether this drawable should currently be alive. /// This is queried by the framework to decide the state of this drawable for the next frame. /// protected internal virtual bool ShouldBeAlive { get { if (LifetimeStart == double.MinValue && LifetimeEnd == double.MaxValue) return true; return Time.Current >= LifetimeStart && Time.Current < LifetimeEnd; } } /// /// Whether to remove the drawable from its parent's children when it's not alive. /// public virtual bool RemoveWhenNotAlive => Parent == null || Time.Current > LifetimeStart; #endregion #region Parenting (scene graph operations, including ProxyDrawable) /// /// Retrieve the first parent in the tree which derives from . /// As this is performing an upward tree traversal, avoid calling every frame. /// /// The first parent . protected InputManager GetContainingInputManager() => FindClosestParent(); private CompositeDrawable parent; /// /// The parent of this drawable in the scene graph. /// public CompositeDrawable Parent { get => parent; internal set { if (IsDisposed) throw new ObjectDisposedException(ToString(), "Disposed Drawables may never get a parent and return to the scene graph."); if (value == null) ChildID = 0; if (parent == value) return; if (value != null && parent != null) throw new InvalidOperationException("May not add a drawable to multiple containers."); parent = value; Invalidate(InvalidationFromParentSize | Invalidation.Colour | Invalidation.Presence | Invalidation.Parent); if (parent != null) { //we should already have a clock at this point (from our LoadRequested invocation) //this just ensures we have the most recent parent clock. //we may want to consider enforcing that parent.Clock == clock here. UpdateClock(parent.Clock); } } } /// /// Find the closest parent of a specified type. /// /// /// This can be a potentially expensive operation and should be used with discretion. /// /// The type to match. /// The first matching parent, or null if no parent of type is found. internal T FindClosestParent() where T : class, IDrawable { Drawable cursor = this; while ((cursor = cursor.Parent) != null) { if (cursor is T match) return match; } return default; } /// /// Refers to the original if this drawable was created via /// . Otherwise refers to this. /// internal virtual Drawable Original => this; /// /// True iff has been called before. /// public bool HasProxy => proxy != null; /// /// True iff this is not a proxy of any . /// public bool IsProxy => Original != this; private Drawable proxy; /// /// Creates a proxy drawable which can be inserted elsewhere in the scene graph. /// Will cause the original instance to not render itself. /// Creating multiple proxies is not supported and will result in an /// . /// public Drawable CreateProxy() { if (proxy != null) throw new InvalidOperationException("Multiple proxies are not supported."); return proxy = new ProxyDrawable(this); } /// /// Validates a for use by the proxy of this . /// This is used exclusively by , and should not be used otherwise. /// /// The index of the in which the proxy should use. /// The frame for which the was created. This is the parameter used for validation. internal virtual void ValidateProxyDrawNode(int treeIndex, ulong frame) => proxy.ValidateProxyDrawNode(treeIndex, frame); #endregion #region Caching & invalidation (for things too expensive to compute every frame) /// /// Was this Drawable masked away completely during the last frame? /// This is measured conservatively, i.e. it is only true when the Drawable was /// actually masked away, but it may be false, even if the Drawable was masked away. /// internal bool IsMaskedAway { get; private set; } private readonly LayoutValue screenSpaceDrawQuadBacking = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); protected virtual Quad ComputeScreenSpaceDrawQuad() => ToScreenSpace(DrawRectangle); /// /// The screen-space quad this drawable occupies. /// public virtual Quad ScreenSpaceDrawQuad => screenSpaceDrawQuadBacking.IsValid ? screenSpaceDrawQuadBacking : screenSpaceDrawQuadBacking.Value = ComputeScreenSpaceDrawQuad(); private readonly LayoutValue drawInfoBacking = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); private DrawInfo computeDrawInfo() { DrawInfo di = Parent?.DrawInfo ?? new DrawInfo(null); Vector2 pos = DrawPosition + AnchorPosition; Vector2 drawScale = DrawScale; if (Parent != null) pos += Parent.ChildOffset; di.ApplyTransform(pos, drawScale, Rotation, Shear, OriginPosition); return di; } /// /// Contains the linear transformation of this that is used during draw. /// public virtual DrawInfo DrawInfo => drawInfoBacking.IsValid ? drawInfoBacking : drawInfoBacking.Value = computeDrawInfo(); private readonly LayoutValue drawColourInfoBacking = new LayoutValue( Invalidation.Colour | Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence, conditions: (s, i) => { if ((i & Invalidation.Colour) > 0) return true; return !s.Colour.HasSingleColour || (s.drawColourInfoBacking.IsValid && !s.drawColourInfoBacking.Value.Colour.HasSingleColour); }); /// /// Contains the colour and blending information of this that are used during draw. /// public virtual DrawColourInfo DrawColourInfo => drawColourInfoBacking.IsValid ? drawColourInfoBacking : drawColourInfoBacking.Value = computeDrawColourInfo(); private DrawColourInfo computeDrawColourInfo() { DrawColourInfo ci = Parent?.DrawColourInfo ?? new DrawColourInfo(null); BlendingParameters localBlending = Blending; if (Parent != null) localBlending.CopyFromParent(ci.Blending); localBlending.ApplyDefaultToInherited(); ci.Blending = localBlending; ColourInfo ourColour = alpha != 1 ? colour.MultiplyAlpha(alpha) : colour; if (ci.Colour.HasSingleColour) ci.Colour.ApplyChild(ourColour); else { Debug.Assert(Parent != null, $"The {nameof(ci)} of null parents should always have the single colour white, and therefore this branch should never be hit."); // Cannot use ToParentSpace here, because ToParentSpace depends on DrawInfo to be completed // ReSharper disable once PossibleNullReferenceException Quad interp = Quad.FromRectangle(DrawRectangle) * (DrawInfo.Matrix * Parent.DrawInfo.MatrixInverse); Vector2 parentSize = Parent.DrawSize; ci.Colour.ApplyChild(ourColour, new Quad( Vector2.Divide(interp.TopLeft, parentSize), Vector2.Divide(interp.TopRight, parentSize), Vector2.Divide(interp.BottomLeft, parentSize), Vector2.Divide(interp.BottomRight, parentSize))); } return ci; } private readonly LayoutValue requiredParentSizeToFitBacking = new LayoutValue(Invalidation.RequiredParentSizeToFit); private Vector2 computeRequiredParentSizeToFit() { // Auxiliary variables required for the computation Vector2 ap = AnchorPosition; Vector2 rap = RelativeAnchorPosition; Vector2 ratio1 = new Vector2( rap.X <= 0 ? 0 : 1 / rap.X, rap.Y <= 0 ? 0 : 1 / rap.Y); Vector2 ratio2 = new Vector2( rap.X >= 1 ? 0 : 1 / (1 - rap.X), rap.Y >= 1 ? 0 : 1 / (1 - rap.Y)); RectangleF bbox = BoundingBox; // Compute the required size of the parent such that we fit in snugly when positioned // at our relative anchor in the parent. Vector2 topLeftOffset = ap - bbox.TopLeft; Vector2 topLeftSize1 = topLeftOffset * ratio1; Vector2 topLeftSize2 = -topLeftOffset * ratio2; Vector2 bottomRightOffset = ap - bbox.BottomRight; Vector2 bottomRightSize1 = bottomRightOffset * ratio1; Vector2 bottomRightSize2 = -bottomRightOffset * ratio2; // Expand bounds according to clipped offset return Vector2.ComponentMax( Vector2.ComponentMax(topLeftSize1, topLeftSize2), Vector2.ComponentMax(bottomRightSize1, bottomRightSize2)); } /// /// Returns the size of the smallest axis aligned box in parent space which /// encompasses this drawable while preserving this drawable's /// . /// If a component of is smaller than zero /// or larger than one, then it is impossible to preserve /// while fitting into the parent, and thus returns /// zero in that dimension; i.e. we no longer fit into the parent. /// This behavior is prominent with non-centre and non-custom values. /// internal Vector2 RequiredParentSizeToFit => requiredParentSizeToFitBacking.IsValid ? requiredParentSizeToFitBacking : requiredParentSizeToFitBacking.Value = computeRequiredParentSizeToFit(); /// /// The flags which this has been invalidated with, grouped by . /// private InvalidationList invalidationList = new InvalidationList(Invalidation.All); private readonly List layoutMembers = new List(); /// /// Adds a layout member that will be invalidated when its is invalidated. /// /// The layout member to add. protected void AddLayout(LayoutMember member) { if (LoadState > LoadState.NotLoaded) throw new InvalidOperationException($"{nameof(LayoutMember)}s cannot be added after {nameof(Drawable)}s have started loading. Consider adding in the constructor."); layoutMembers.Add(member); member.Parent = this; } /// /// Validates the super-tree of this for the given flags. /// /// /// This is internally invoked by , and should not be invoked manually. /// /// The flags to validate with. internal void ValidateSuperTree(Invalidation validationType) { if (invalidationList.Validate(validationType)) Parent?.ValidateSuperTree(validationType); } // Make sure we start out with a value of 1 such that ApplyDrawNode is always called at least once public long InvalidationID { get; private set; } = 1; /// /// Invalidates the layout of this . /// /// The flags to invalidate with. /// The source that triggered the invalidation. /// If any layout was invalidated. public bool Invalidate(Invalidation invalidation = Invalidation.All, InvalidationSource source = InvalidationSource.Self) => invalidate(invalidation, source); /// /// Invalidates the layout of this . /// /// The flags to invalidate with. /// The source that triggered the invalidation. /// Whether to propagate the invalidation to the parent of this . /// Only has an effect if is . /// If any layout was invalidated. private bool invalidate(Invalidation invalidation = Invalidation.All, InvalidationSource source = InvalidationSource.Self, bool propagateToParent = true) { if (source != InvalidationSource.Child && source != InvalidationSource.Parent && source != InvalidationSource.Self) throw new InvalidOperationException($"A {nameof(Drawable)} can only be invalidated with a singular {nameof(source)} (child, parent, or self)."); if (LoadState < LoadState.Ready) return false; // Changes in the colour of children don't affect parents. if (source == InvalidationSource.Child) invalidation &= ~Invalidation.Colour; if (invalidation == Invalidation.None) return false; // If the invalidation originated locally, propagate to the immediate parent. // Note: This is done _before_ invalidation is blocked below, since the parent always needs to be aware of changes even if the Drawable's invalidation state hasn't changed. // This is for only propagating once, otherwise it would propagate all the way to the root Drawable. if (propagateToParent && source == InvalidationSource.Self) Parent?.Invalidate(invalidation, InvalidationSource.Child); // Perform the invalidation. if (!invalidationList.Invalidate(source, invalidation)) return false; // A DrawNode invalidation always invalidates. bool anyInvalidated = (invalidation & Invalidation.DrawNode) > 0; // Invalidate all layout members foreach (var member in layoutMembers) { // Only invalidate layout members that accept the given source. if ((member.Source & source) == 0) continue; // Remove invalidation flags that don't refer to the layout member. Invalidation memberInvalidation = invalidation & member.Invalidation; if (memberInvalidation == 0) continue; if (member.Conditions?.Invoke(this, memberInvalidation) != false) anyInvalidated |= member.Invalidate(); } // Allow any custom invalidation to take place. anyInvalidated |= OnInvalidate(invalidation, source); if (anyInvalidated) InvalidationID++; Invalidated?.Invoke(this); return anyInvalidated; } /// /// Invoked when the layout of this was invalidated. /// /// /// This should be used to perform any custom invalidation logic that cannot be described as a layout. /// /// /// This does not ensure that the parent containers have been updated before us, thus operations involving /// parent states (e.g. ) should not be executed in an overridden implementation. /// /// The flags that the this was invalidated with. /// The source that triggered the invalidation. /// Whether any custom invalidation was performed. Must be true if the for this is to be invalidated. protected virtual bool OnInvalidate(Invalidation invalidation, InvalidationSource source) => false; public Invalidation InvalidationFromParentSize { get { Invalidation result = Invalidation.DrawInfo; if (RelativeSizeAxes != Axes.None) result |= Invalidation.DrawSize; if (RelativePositionAxes != Axes.None) result |= Invalidation.MiscGeometry; return result; } } /// /// A fast path for invalidating ourselves and our parent's children size dependencies whenever a size or position change occurs. /// /// The to invalidate with. /// The that were affected. private void invalidateParentSizeDependencies(Invalidation invalidation, Axes changedAxes) { // We're invalidating the parent manually, so we should not propagate it upwards. invalidate(invalidation, InvalidationSource.Self, false); // The fast path, which performs an invalidation on the parent along with optimisations for bypassed sizing axes. Parent?.InvalidateChildrenSizeDependencies(invalidation, changedAxes, this); } #endregion #region DrawNode private readonly DrawNode[] drawNodes = new DrawNode[GLWrapper.MAX_DRAW_NODES]; /// /// Generates the for ourselves. /// /// The frame which the sub-tree should be generated for. /// The index of the to use. /// Whether the creation of a new should be forced, rather than re-using an existing . /// A complete and updated , or null if the would be invisible. internal virtual DrawNode GenerateDrawNodeSubtree(ulong frame, int treeIndex, bool forceNewDrawNode) { DrawNode node = drawNodes[treeIndex]; if (node == null || forceNewDrawNode) { drawNodes[treeIndex] = node = CreateDrawNode(); FrameStatistics.Increment(StatisticsCounterType.DrawNodeCtor); } if (InvalidationID != node.InvalidationID) { node.ApplyState(); FrameStatistics.Increment(StatisticsCounterType.DrawNodeAppl); } return node; } /// /// Creates a draw node capable of containing all information required to draw this drawable. /// /// The created draw node. protected virtual DrawNode CreateDrawNode() => new DrawNode(this); #endregion #region DrawInfo-based coordinate system conversions /// /// Accepts a vector in local coordinates and converts it to coordinates in another Drawable's space. /// /// A vector in local coordinates. /// The drawable in which space we want to transform the vector to. /// The vector in other's coordinates. public Vector2 ToSpaceOfOtherDrawable(Vector2 input, IDrawable other) { if (other == this) return input; return Vector2Extensions.Transform(Vector2Extensions.Transform(input, DrawInfo.Matrix), other.DrawInfo.MatrixInverse); } /// /// Accepts a rectangle in local coordinates and converts it to coordinates in another Drawable's space. /// /// A rectangle in local coordinates. /// The drawable in which space we want to transform the rectangle to. /// The rectangle in other's coordinates. public Quad ToSpaceOfOtherDrawable(RectangleF input, IDrawable other) { if (other == this) return input; return Quad.FromRectangle(input) * (DrawInfo.Matrix * other.DrawInfo.MatrixInverse); } /// /// Accepts a vector in local coordinates and converts it to coordinates in Parent's space. /// /// A vector in local coordinates. /// The vector in Parent's coordinates. public Vector2 ToParentSpace(Vector2 input) => ToSpaceOfOtherDrawable(input, Parent); /// /// Accepts a rectangle in local coordinates and converts it to a quad in Parent's space. /// /// A rectangle in local coordinates. /// The quad in Parent's coordinates. public Quad ToParentSpace(RectangleF input) => ToSpaceOfOtherDrawable(input, Parent); /// /// Accepts a vector in local coordinates and converts it to coordinates in screen space. /// /// A vector in local coordinates. /// The vector in screen coordinates. public Vector2 ToScreenSpace(Vector2 input) => Vector2Extensions.Transform(input, DrawInfo.Matrix); /// /// Accepts a rectangle in local coordinates and converts it to a quad in screen space. /// /// A rectangle in local coordinates. /// The quad in screen coordinates. public Quad ToScreenSpace(RectangleF input) => Quad.FromRectangle(input) * DrawInfo.Matrix; /// /// Accepts a vector in screen coordinates and converts it to coordinates in local space. /// /// A vector in screen coordinates. /// The vector in local coordinates. public Vector2 ToLocalSpace(Vector2 screenSpacePos) => Vector2Extensions.Transform(screenSpacePos, DrawInfo.MatrixInverse); /// /// Accepts a quad in screen coordinates and converts it to coordinates in local space. /// /// A quad in screen coordinates. /// The quad in local coordinates. public Quad ToLocalSpace(Quad screenSpaceQuad) => screenSpaceQuad * DrawInfo.MatrixInverse; #endregion #region Interaction / Input /// /// Handle a UI event. /// /// The event to be handled. /// If the event supports blocking, returning true will make the event to not propagating further. protected virtual bool Handle(UIEvent e) => false; /// /// Trigger a UI event with set to this . /// /// The event. Its will be modified. /// The result of event handler. public bool TriggerEvent(UIEvent e) { e.Target = this; switch (e) { case MouseMoveEvent mouseMove: return OnMouseMove(mouseMove); case HoverEvent hover: return OnHover(hover); case HoverLostEvent hoverLost: OnHoverLost(hoverLost); return false; case MouseDownEvent mouseDown: return OnMouseDown(mouseDown); case MouseUpEvent mouseUp: OnMouseUp(mouseUp); return false; case ClickEvent click: return OnClick(click); case DoubleClickEvent doubleClick: return OnDoubleClick(doubleClick); case DragStartEvent dragStart: return OnDragStart(dragStart); case DragEvent drag: OnDrag(drag); return false; case DragEndEvent dragEnd: OnDragEnd(dragEnd); return false; case ScrollEvent scroll: return OnScroll(scroll); case FocusEvent focus: OnFocus(focus); return false; case FocusLostEvent focusLost: OnFocusLost(focusLost); return false; case KeyDownEvent keyDown: return OnKeyDown(keyDown); case KeyUpEvent keyUp: OnKeyUp(keyUp); return false; case TouchDownEvent touchDown: return OnTouchDown(touchDown); case TouchMoveEvent touchMove: OnTouchMove(touchMove); return false; case TouchUpEvent touchUp: OnTouchUp(touchUp); return false; case JoystickPressEvent joystickPress: return OnJoystickPress(joystickPress); case JoystickReleaseEvent joystickRelease: OnJoystickRelease(joystickRelease); return false; case JoystickAxisMoveEvent joystickAxisMove: return OnJoystickAxisMove(joystickAxisMove); case MidiDownEvent midiDown: return OnMidiDown(midiDown); case MidiUpEvent midiUp: OnMidiUp(midiUp); return false; case TabletPenButtonPressEvent tabletPenButtonPress: return OnTabletPenButtonPress(tabletPenButtonPress); case TabletPenButtonReleaseEvent tabletPenButtonRelease: OnTabletPenButtonRelease(tabletPenButtonRelease); return false; case TabletAuxiliaryButtonPressEvent tabletAuxiliaryButtonPress: return OnTabletAuxiliaryButtonPress(tabletAuxiliaryButtonPress); case TabletAuxiliaryButtonReleaseEvent tabletAuxiliaryButtonRelease: OnTabletAuxiliaryButtonRelease(tabletAuxiliaryButtonRelease); return false; default: return Handle(e); } } [Obsolete("Use TriggerClick instead.")] // Can be removed 20220203 public bool Click() => TriggerClick(); /// /// Triggers a left click event for this . /// /// Whether the click event is handled. public bool TriggerClick() => TriggerEvent(new ClickEvent(GetContainingInputManager()?.CurrentState ?? new InputState(), MouseButton.Left)); #region Individual event handlers /// /// An event that occurs every time the mouse is moved while hovering this . /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnMouseMove(MouseMoveEvent e) => Handle(e); /// /// An event that occurs when the mouse starts hovering this . /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnHover(HoverEvent e) => Handle(e); /// /// An event that occurs when the mouse stops hovering this . /// /// /// This is guaranteed to be invoked if was invoked. /// /// The containing information about the input event. protected virtual void OnHoverLost(HoverLostEvent e) => Handle(e); /// /// An event that occurs when a is pressed on this . /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnMouseDown(MouseDownEvent e) => Handle(e); /// /// An event that occurs when a that was pressed on this is released. /// /// /// This is guaranteed to be invoked if was invoked. /// /// The containing information about the input event. protected virtual void OnMouseUp(MouseUpEvent e) => Handle(e); /// /// An event that occurs when a is clicked on this . /// /// /// This will only be invoked on the s that received a previous invocation /// which are still present in the input queue (via ) when the click occurs.
/// This will not occur if a drag was started ( was invoked) or a double-click occurred ( was invoked). ///
/// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnClick(ClickEvent e) => Handle(e); /// /// An event that occurs when a is double-clicked on this . /// /// /// This will only be invoked on the that returned true from a previous invocation. /// /// The containing information about the input event. /// Whether to block the next event from occurring. protected virtual bool OnDoubleClick(DoubleClickEvent e) => Handle(e); /// /// An event that occurs when the mouse starts dragging on this . /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnDragStart(DragStartEvent e) => Handle(e); /// /// An event that occurs every time the mouse moves while dragging this . /// /// /// This will only be invoked on the that returned true from a previous invocation. /// /// The containing information about the input event. protected virtual void OnDrag(DragEvent e) => Handle(e); /// /// An event that occurs when the mouse stops dragging this . /// /// /// This will only be invoked on the that returned true from a previous invocation. /// /// The containing information about the input event. protected virtual void OnDragEnd(DragEndEvent e) => Handle(e); /// /// An event that occurs when the mouse wheel is scrolled on this . /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnScroll(ScrollEvent e) => Handle(e); /// /// An event that occurs when this gains focus. /// /// /// This will only be invoked on the that returned true from both and a previous invocation. /// /// The containing information about the input event. protected virtual void OnFocus(FocusEvent e) => Handle(e); /// /// An event that occurs when this loses focus. /// /// /// This will only be invoked on the that previously had focus ( was invoked). /// /// The containing information about the input event. protected virtual void OnFocusLost(FocusLostEvent e) => Handle(e); /// /// An event that occurs when a is pressed. /// /// /// Repeat events can only be invoked on the s that received a previous non-repeat invocation /// which are still present in the input queue (via ) when the repeat occurs. /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnKeyDown(KeyDownEvent e) => Handle(e); /// /// An event that occurs when a is released. /// /// /// This is guaranteed to be invoked if was invoked. /// /// The containing information about the input event. protected virtual void OnKeyUp(KeyUpEvent e) => Handle(e); /// /// An event that occurs when a is active. /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnTouchDown(TouchDownEvent e) => Handle(e); /// /// An event that occurs every time an active has moved while hovering this . /// /// /// This will only be invoked on the s that received a previous invocation from the source of this touch. /// This will not occur if the touch has been activated then deactivated without moving from its initial position. /// /// The containing information about the input event. protected virtual void OnTouchMove(TouchMoveEvent e) => Handle(e); /// /// An event that occurs when a is not active. /// /// /// This is guaranteed to be invoked if was invoked. /// /// The containing information about the input event. protected virtual void OnTouchUp(TouchUpEvent e) => Handle(e); /// /// An event that occurs when a is pressed. /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnJoystickPress(JoystickPressEvent e) => Handle(e); /// /// An event that occurs when a is released. /// /// /// This is guaranteed to be invoked if was invoked. /// /// The containing information about the input event. protected virtual void OnJoystickRelease(JoystickReleaseEvent e) => Handle(e); /// /// An event that occurs when a is moved. /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnJoystickAxisMove(JoystickAxisMoveEvent e) => Handle(e); /// /// An event that occurs when a is pressed. /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnMidiDown(MidiDownEvent e) => Handle(e); /// /// An event that occurs when a is released. /// /// /// This is guaranteed to be invoked if was invoked. /// /// The containing information about the input event. protected virtual void OnMidiUp(MidiUpEvent e) => Handle(e); /// /// An event that occurs when a is pressed. /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnTabletPenButtonPress(TabletPenButtonPressEvent e) => Handle(e); /// /// An event that occurs when a is released. /// /// /// This is guaranteed to be invoked if was invoked. /// /// The containing information about the input event. protected virtual void OnTabletPenButtonRelease(TabletPenButtonReleaseEvent e) => Handle(e); /// /// An event that occurs when a is pressed. /// /// The containing information about the input event. /// Whether to block the event from propagating to other s in the hierarchy. protected virtual bool OnTabletAuxiliaryButtonPress(TabletAuxiliaryButtonPressEvent e) => Handle(e); /// /// An event that occurs when a is released. /// /// /// This is guaranteed to be invoked if was invoked. /// /// The containing information about the input event. protected virtual void OnTabletAuxiliaryButtonRelease(TabletAuxiliaryButtonReleaseEvent e) => Handle(e); #endregion /// /// Whether this drawable should receive non-positional input. This does not mean that the drawable will immediately handle the received input, but that it may handle it at some point. /// internal bool RequestsNonPositionalInput { get; private set; } /// /// Whether this drawable should receive positional input. This does not mean that the drawable will immediately handle the received input, but that it may handle it at some point. /// internal bool RequestsPositionalInput { get; private set; } /// /// Conservatively approximates whether there is a descendant which in the sub-tree rooted at this drawable /// to enable sub-tree skipping optimization for input handling. /// internal bool RequestsNonPositionalInputSubTree; /// /// Conservatively approximates whether there is a descendant which in the sub-tree rooted at this drawable /// to enable sub-tree skipping optimization for input handling. /// internal bool RequestsPositionalInputSubTree; /// /// Whether this handles non-positional input. /// This value is true by default if or any non-positional (e.g. keyboard related) "On-" input methods are overridden. /// public virtual bool HandleNonPositionalInput => RequestsNonPositionalInput; /// /// Whether this handles positional input. /// This value is true by default if or any positional (i.e. mouse related) "On-" input methods are overridden. /// public virtual bool HandlePositionalInput => RequestsPositionalInput; /// /// Nested class which is used for caching , values obtained via reflection. /// private static class HandleInputCache { private static readonly ConcurrentDictionary positional_cached_values = new ConcurrentDictionary(); private static readonly ConcurrentDictionary non_positional_cached_values = new ConcurrentDictionary(); private static readonly string[] positional_input_methods = { nameof(Handle), nameof(OnMouseMove), nameof(OnHover), nameof(OnHoverLost), nameof(OnMouseDown), nameof(OnMouseUp), nameof(OnClick), nameof(OnDoubleClick), nameof(OnDragStart), nameof(OnDrag), nameof(OnDragEnd), nameof(OnScroll), nameof(OnFocus), nameof(OnFocusLost), nameof(OnTouchDown), nameof(OnTouchMove), nameof(OnTouchUp), nameof(OnTabletPenButtonPress), nameof(OnTabletPenButtonRelease) }; private static readonly string[] non_positional_input_methods = { nameof(Handle), nameof(OnFocus), nameof(OnFocusLost), nameof(OnKeyDown), nameof(OnKeyUp), nameof(OnJoystickPress), nameof(OnJoystickRelease), nameof(OnJoystickAxisMove), nameof(OnTabletAuxiliaryButtonPress), nameof(OnTabletAuxiliaryButtonRelease), nameof(OnMidiDown), nameof(OnMidiUp) }; private static readonly Type[] positional_input_interfaces = { typeof(IHasTooltip), typeof(IHasCustomTooltip), typeof(IHasContextMenu), typeof(IHasPopover), }; private static readonly Type[] non_positional_input_interfaces = { typeof(IKeyBindingHandler), }; private static readonly string[] positional_input_properties = { nameof(HandlePositionalInput), }; private static readonly string[] non_positional_input_properties = { nameof(HandleNonPositionalInput), nameof(AcceptsFocus), }; public static bool RequestsNonPositionalInput(Drawable drawable) => get(drawable, non_positional_cached_values, false); public static bool RequestsPositionalInput(Drawable drawable) => get(drawable, positional_cached_values, true); private static bool get(Drawable drawable, ConcurrentDictionary cache, bool positional) { var type = drawable.GetType(); if (!cache.TryGetValue(type, out var value)) { value = compute(type, positional); cache.TryAdd(type, value); } return value; } private static bool compute([NotNull] Type type, bool positional) { var inputMethods = positional ? positional_input_methods : non_positional_input_methods; foreach (var inputMethod in inputMethods) { // check for any input method overrides which are at a higher level than drawable. var method = type.GetMethod(inputMethod, BindingFlags.Instance | BindingFlags.NonPublic); Debug.Assert(method != null); if (method.DeclaringType != typeof(Drawable)) return true; } var inputInterfaces = positional ? positional_input_interfaces : non_positional_input_interfaces; foreach (var inputInterface in inputInterfaces) { // check if this type implements any interface which requires a drawable to handle input. if (inputInterface.IsAssignableFrom(type)) return true; } var inputProperties = positional ? positional_input_properties : non_positional_input_properties; foreach (var inputProperty in inputProperties) { var property = type.GetProperty(inputProperty); Debug.Assert(property != null); if (property.DeclaringType != typeof(Drawable)) return true; } return false; } } /// /// Check whether we have active focus. /// public bool HasFocus { get; internal set; } /// /// If true, we are eagerly requesting focus. If nothing else above us has (or is requesting focus) we will get it. /// /// In order to get focused, must be true. public virtual bool RequestsFocus => false; /// /// If true, we will gain focus (receiving priority on keyboard input) (and receive an event) on returning true in . /// public virtual bool AcceptsFocus => false; /// /// Whether this Drawable is currently hovered over. /// /// This is updated only if is true. public bool IsHovered { get; internal set; } /// /// Whether this Drawable is currently being dragged. /// public bool IsDragged { get; internal set; } /// /// Determines whether this drawable receives positional input when the mouse is at the /// given screen-space position. /// /// The screen-space position where input could be received. /// True if input is received at the given screen-space position. public virtual bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Contains(screenSpacePos); /// /// Computes whether a given screen-space position is contained within this drawable. /// Mouse input events are only received when this function is true, or when the drawable /// is in focus. /// /// The screen space position to be checked against this drawable. public virtual bool Contains(Vector2 screenSpacePos) => DrawRectangle.Contains(ToLocalSpace(screenSpacePos)); /// /// Whether non-positional input should be propagated to the sub-tree rooted at this drawable. /// public virtual bool PropagateNonPositionalInputSubTree => IsPresent && RequestsNonPositionalInputSubTree; /// /// Whether positional input should be propagated to the sub-tree rooted at this drawable. /// public virtual bool PropagatePositionalInputSubTree => IsPresent && RequestsPositionalInputSubTree && !IsMaskedAway; /// /// Whether clicks should be blocked when this drawable is in a dragged state. /// /// /// This is queried when a click is to be actuated. /// public virtual bool DragBlocksClick => true; /// /// This method is responsible for building a queue of Drawables to receive non-positional input in reverse order. /// /// The input queue to be built. /// Whether blocking at s should be allowed. /// Returns false if we should skip this sub-tree. internal virtual bool BuildNonPositionalInputQueue(List queue, bool allowBlocking = true) { if (!PropagateNonPositionalInputSubTree) return false; if (HandleNonPositionalInput) queue.Add(this); return true; } /// /// This method is responsible for building a queue of Drawables to receive positional input in reverse order. /// /// The screen space position of the positional input. /// The input queue to be built. /// Returns false if we should skip this sub-tree. internal virtual bool BuildPositionalInputQueue(Vector2 screenSpacePos, List queue) { if (!PropagatePositionalInputSubTree) return false; if (HandlePositionalInput && ReceivePositionalInputAt(screenSpacePos)) queue.Add(this); return true; } internal sealed override void EnsureTransformMutationAllowed() => EnsureMutationAllowed(nameof(Transforms)); /// /// Check whether the current thread is valid for operating on thread-safe properties. /// /// The member to be operated on, used only for describing failures in exception messages. /// If the current thread is not valid. internal void EnsureMutationAllowed(string member) { switch (LoadState) { case LoadState.NotLoaded: break; case LoadState.Loading: if (Thread.CurrentThread != LoadThread) throw new InvalidThreadForMutationException(LoadState, member, "not on the load thread"); break; case LoadState.Ready: // Allow mutating from the load thread since parenting containers may still be in the loading state if (Thread.CurrentThread != LoadThread && !ThreadSafety.IsUpdateThread) throw new InvalidThreadForMutationException(LoadState, member, "not on the load or update threads"); break; case LoadState.Loaded: if (!ThreadSafety.IsUpdateThread) throw new InvalidThreadForMutationException(LoadState, member, "not on the update thread"); break; } } #endregion #region Transforms protected internal ScheduledDelegate Schedule(Action action) => Scheduler.AddDelayed(action, TransformDelay); /// /// Make this drawable automatically clean itself up after all transforms have finished playing. /// Can be delayed using Delay(). /// public void Expire(bool calculateLifetimeStart = false) { if (clock == null) { LifetimeEnd = double.MinValue; return; } LifetimeEnd = LatestTransformEndTime; if (calculateLifetimeStart) { double min = double.MaxValue; foreach (Transform t in Transforms) { if (t.StartTime < min) min = t.StartTime; } LifetimeStart = min < int.MaxValue ? min : int.MinValue; } } /// /// Hide sprite instantly. /// public virtual void Hide() => this.FadeOut(); /// /// Show sprite instantly. /// public virtual void Show() => this.FadeIn(); #endregion #region Effects /// /// Returns the drawable created by applying the given effect to this drawable. This method may add this drawable to a container. /// If this drawable should be the child of another container, make sure to add the created drawable to the container instead of this drawable. /// /// The type of the drawable that results from applying the given effect. /// The effect to apply to this drawable. /// The action that should get called to initialize the created drawable before it is returned. /// The drawable created by applying the given effect to this drawable. public T WithEffect(IEffect effect, Action initializationAction = null) where T : Drawable { var result = effect.ApplyTo(this); initializationAction?.Invoke(result); return result; } #endregion /// /// A name used to identify this Drawable internally. /// public string Name = string.Empty; public override string ToString() { string shortClass = GetType().ReadableName(); if (!string.IsNullOrEmpty(Name)) return $@"{Name} ({shortClass})"; else return shortClass; } /// /// Creates a new instance of an empty . /// public static Drawable Empty() => new EmptyDrawable(); private class EmptyDrawable : Drawable { } public class InvalidThreadForMutationException : InvalidOperationException { public InvalidThreadForMutationException(LoadState loadState, string member, string invalidThreadContextDescription) : base($"Cannot mutate the {member} of a {loadState} {nameof(Drawable)} while {invalidThreadContextDescription}. " + $"Consider using {nameof(Schedule)} to schedule the mutation operation.") { } } } /// /// Specifies which type of properties are being invalidated. /// [Flags] public enum Invalidation { /// /// has changed. No change to or /// is assumed unless indicated by additional flags. /// DrawInfo = 1, /// /// has changed. /// DrawSize = 1 << 1, /// /// Captures all other geometry changes than , such as /// , , and . /// MiscGeometry = 1 << 2, /// /// has changed. /// Colour = 1 << 3, /// /// has to be invoked on all old draw nodes. /// This flag never propagates to children. /// DrawNode = 1 << 4, /// /// has changed. /// Presence = 1 << 5, /// /// A has changed. /// Unlike other flags, this propagates to all children regardless of their state. /// Parent = 1 << 6, /// /// No invalidation. /// None = 0, /// /// has to be recomputed. /// RequiredParentSizeToFit = MiscGeometry | DrawSize, /// /// All possible things are affected. /// All = DrawNode | RequiredParentSizeToFit | Colour | DrawInfo | Presence, /// /// Only the layout flags. /// Layout = All & ~(DrawNode | Parent) } /// /// General enum to specify an "anchor" or "origin" point from the standard 9 points on a rectangle. /// x and y counterparts can be accessed using bitwise flags. /// [Flags] public enum Anchor { TopLeft = y0 | x0, TopCentre = y0 | x1, TopRight = y0 | x2, CentreLeft = y1 | x0, Centre = y1 | x1, CentreRight = y1 | x2, BottomLeft = y2 | x0, BottomCentre = y2 | x1, BottomRight = y2 | x2, /// /// The vertical counterpart is at "Top" position. /// y0 = 1, /// /// The vertical counterpart is at "Centre" position. /// y1 = 1 << 1, /// /// The vertical counterpart is at "Bottom" position. /// y2 = 1 << 2, /// /// The horizontal counterpart is at "Left" position. /// x0 = 1 << 3, /// /// The horizontal counterpart is at "Centre" position. /// x1 = 1 << 4, /// /// The horizontal counterpart is at "Right" position. /// x2 = 1 << 5, /// /// The user is manually updating the outcome, so we shouldn't. /// Custom = 1 << 6, } [Flags] public enum Axes { None = 0, X = 1, Y = 1 << 1, Both = X | Y, } [Flags] public enum Edges { None = 0, Top = 1, Left = 1 << 1, Bottom = 1 << 2, Right = 1 << 3, Horizontal = Left | Right, Vertical = Top | Bottom, All = Top | Left | Bottom | Right, } public enum Direction { Horizontal, Vertical, } public enum RotationDirection { [Description("Clockwise")] Clockwise, [Description("Counterclockwise")] Counterclockwise, } /// /// Possible states of a within the loading pipeline. /// public enum LoadState { /// /// Not loaded, and no load has been initiated yet. /// NotLoaded, /// /// Currently loading (possibly and usually on a background thread via ). /// Loading, /// /// Loading is complete, but has not yet been finalized on the update thread /// ( has not been called yet, which /// always runs on the update thread and requires ). /// Ready, /// /// Loading is fully completed and the Drawable is now part of the scene graph. /// Loaded } /// /// Controls the behavior of when it is set to . /// public enum FillMode { /// /// Completely fill the parent with a relative size of 1 at the cost of stretching the aspect ratio (default). /// Stretch, /// /// Always maintains aspect ratio while filling the portion of the parent's size denoted by the relative size. /// A relative size of 1 results in completely filling the parent by scaling the smaller axis of the drawable to fill the parent. /// Fill, /// /// Always maintains aspect ratio while fitting into the portion of the parent's size denoted by the relative size. /// A relative size of 1 results in fitting exactly into the parent by scaling the larger axis of the drawable to fit into the parent. /// Fit, } }