A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using osuTK;
5using osuTK.Graphics;
6using osu.Framework.Allocation;
7using osu.Framework.Extensions.TypeExtensions;
8using osu.Framework.Graphics.Colour;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Effects;
11using osu.Framework.Graphics.Primitives;
12using osu.Framework.Graphics.Transforms;
13using osu.Framework.Input;
14using osu.Framework.Logging;
15using osu.Framework.Statistics;
16using osu.Framework.Threading;
17using osu.Framework.Timing;
18using System;
19using System.Collections.Concurrent;
20using System.Collections.Generic;
21using System.ComponentModel;
22using System.Diagnostics;
23using System.Linq;
24using System.Reflection;
25using System.Threading;
26using JetBrains.Annotations;
27using osu.Framework.Bindables;
28using osu.Framework.Development;
29using osu.Framework.Extensions.EnumExtensions;
30using osu.Framework.Graphics.Cursor;
31using osu.Framework.Graphics.OpenGL;
32using osu.Framework.Input.Bindings;
33using osu.Framework.Input.Events;
34using osu.Framework.Input.States;
35using osu.Framework.Layout;
36using osu.Framework.Testing;
37using osu.Framework.Utils;
38using osuTK.Input;
39using Container = osu.Framework.Graphics.Containers.Container;
40
41namespace osu.Framework.Graphics
42{
43 /// <summary>
44 /// Drawables are the basic building blocks of a scene graph in this framework.
45 /// Anything that is visible or that the user interacts with has to be a Drawable.
46 ///
47 /// For example:
48 /// - Boxes
49 /// - Sprites
50 /// - Collections of Drawables
51 ///
52 /// Drawables are always rectangular in shape in their local coordinate system,
53 /// which makes them quad-shaped in arbitrary (linearly transformed) coordinate systems.
54 /// </summary>
55 [ExcludeFromDynamicCompile]
56 public abstract partial class Drawable : Transformable, IDisposable, IDrawable
57 {
58 #region Construction and disposal
59
60 protected Drawable()
61 {
62 total_count.Value++;
63
64 AddLayout(drawInfoBacking);
65 AddLayout(drawSizeBacking);
66 AddLayout(screenSpaceDrawQuadBacking);
67 AddLayout(drawColourInfoBacking);
68 AddLayout(requiredParentSizeToFitBacking);
69 }
70
71 private static readonly GlobalStatistic<int> total_count = GlobalStatistics.Get<int>(nameof(Drawable), "Total constructed");
72
73 internal bool IsLongRunning => GetType().GetCustomAttribute<LongRunningLoadAttribute>() != null;
74
75 /// <summary>
76 /// Disposes this drawable.
77 /// </summary>
78 public void Dispose()
79 {
80 //we can't dispose if we are mid-load, else our children may get in a bad state.
81 lock (LoadLock) Dispose(true);
82
83 GC.SuppressFinalize(this);
84 }
85
86 protected internal bool IsDisposed { get; private set; }
87
88 /// <summary>
89 /// Disposes this drawable.
90 /// </summary>
91 protected virtual void Dispose(bool isDisposing)
92 {
93 if (IsDisposed)
94 return;
95
96 UnbindAllBindables();
97
98 // Bypass expensive operations as a result of setting the Parent property, by setting the field directly.
99 parent = null;
100 ChildID = 0;
101
102 OnUpdate = null;
103 Invalidated = null;
104
105 OnDispose?.Invoke();
106 OnDispose = null;
107
108 for (int i = 0; i < drawNodes.Length; i++)
109 drawNodes[i]?.Dispose();
110
111 IsDisposed = true;
112 }
113
114 /// <summary>
115 /// Whether this Drawable should be disposed when it is automatically removed from
116 /// its <see cref="Parent"/> due to <see cref="ShouldBeAlive"/> being false.
117 /// </summary>
118 public virtual bool DisposeOnDeathRemoval => RemoveCompletedTransforms;
119
120 private static readonly ConcurrentDictionary<Type, Action<object>> unbind_action_cache = new ConcurrentDictionary<Type, Action<object>>();
121
122 /// <summary>
123 /// Recursively invokes <see cref="UnbindAllBindables"/> on this <see cref="Drawable"/> and all <see cref="Drawable"/>s further down the scene graph.
124 /// </summary>
125 internal virtual void UnbindAllBindablesSubTree() => UnbindAllBindables();
126
127 private void cacheUnbindActions()
128 {
129 foreach (var type in GetType().EnumerateBaseTypes())
130 {
131 if (unbind_action_cache.TryGetValue(type, out _))
132 return;
133
134 // List containing all the delegates to perform the unbinds
135 var actions = new List<Action<object>>();
136
137 // Generate delegates to unbind fields
138 actions.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
139 .Where(f => typeof(IUnbindable).IsAssignableFrom(f.FieldType))
140 .Select(f => new Action<object>(target => ((IUnbindable)f.GetValue(target))?.UnbindAll())));
141
142 // Delegates to unbind properties are intentionally not generated.
143 // Properties with backing fields (including automatic properties) will be picked up by the field unbind delegate generation,
144 // while ones without backing fields (like get-only properties that delegate to another drawable's bindable) should not be unbound here.
145
146 unbind_action_cache[type] = target =>
147 {
148 foreach (var a in actions)
149 {
150 try
151 {
152 a(target);
153 }
154 catch (Exception e)
155 {
156 Logger.Error(e, $"Failed to unbind a local bindable in {type.ReadableName()}");
157 }
158 }
159 };
160 }
161 }
162
163 private bool unbindComplete;
164
165 /// <summary>
166 /// Unbinds all <see cref="Bindable{T}"/>s stored as fields or properties in this <see cref="Drawable"/>.
167 /// </summary>
168 internal virtual void UnbindAllBindables()
169 {
170 if (unbindComplete)
171 return;
172
173 unbindComplete = true;
174
175 foreach (var type in GetType().EnumerateBaseTypes())
176 {
177 if (unbind_action_cache.TryGetValue(type, out var existing))
178 existing?.Invoke(this);
179 }
180
181 OnUnbindAllBindables?.Invoke();
182 }
183
184 #endregion
185
186 #region Loading
187
188 /// <summary>
189 /// Whether this Drawable is fully loaded.
190 /// This is true iff <see cref="UpdateSubTree"/> has run once on this <see cref="Drawable"/>.
191 /// </summary>
192 public bool IsLoaded => loadState >= LoadState.Loaded;
193
194 private volatile LoadState loadState;
195
196 /// <summary>
197 /// Describes the current state of this Drawable within the loading pipeline.
198 /// </summary>
199 public LoadState LoadState => loadState;
200
201 /// <summary>
202 /// The thread on which the <see cref="Load"/> operation started, or null if <see cref="Drawable"/> has not started loading.
203 /// </summary>
204 internal Thread LoadThread { get; private set; }
205
206 internal readonly object LoadLock = new object();
207
208 private static readonly StopwatchClock perf_clock = new StopwatchClock(true);
209
210 /// <summary>
211 /// Load this drawable from an async context.
212 /// Because we can't be sure of the disposal state, it is returned as a bool rather than thrown as in <see cref="Load"/>.
213 /// </summary>
214 /// <param name="clock">The clock we should use by default.</param>
215 /// <param name="dependencies">The dependency tree we will inherit by default. May be extended via <see cref="CompositeDrawable.CreateChildDependencies"/></param>
216 /// <param name="isDirectAsyncContext">Whether this call is being executed from a directly async context (not a parent).</param>
217 /// <returns>Whether the load was successful.</returns>
218 internal bool LoadFromAsync(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies, bool isDirectAsyncContext = false)
219 {
220 lock (LoadLock)
221 {
222 if (IsDisposed)
223 return false;
224
225 Load(clock, dependencies, isDirectAsyncContext);
226 return true;
227 }
228 }
229
230 /// <summary>
231 /// Loads this drawable, including the gathering of dependencies and initialisation of required resources.
232 /// </summary>
233 /// <param name="clock">The clock we should use by default.</param>
234 /// <param name="dependencies">The dependency tree we will inherit by default. May be extended via <see cref="CompositeDrawable.CreateChildDependencies"/></param>
235 /// <param name="isDirectAsyncContext">Whether this call is being executed from a directly async context (not a parent).</param>
236 internal void Load(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies, bool isDirectAsyncContext = false)
237 {
238 lock (LoadLock)
239 {
240 if (!isDirectAsyncContext && IsLongRunning)
241 throw new InvalidOperationException("Tried to load a long-running drawable in a non-direct async context. See https://git.io/Je1YF for more details.");
242
243 if (IsDisposed)
244 throw new ObjectDisposedException(ToString(), "Attempting to load an already disposed drawable.");
245
246 if (loadState == LoadState.NotLoaded)
247 {
248 Trace.Assert(loadState == LoadState.NotLoaded);
249
250 loadState = LoadState.Loading;
251
252 load(clock, dependencies);
253
254 loadState = LoadState.Ready;
255 }
256 }
257 }
258
259 private void load(IFrameBasedClock clock, IReadOnlyDependencyContainer dependencies)
260 {
261 LoadThread = Thread.CurrentThread;
262
263 UpdateClock(clock);
264
265 double timeBefore = DebugUtils.LogPerformanceIssues ? perf_clock.CurrentTime : 0;
266
267 RequestsNonPositionalInput = HandleInputCache.RequestsNonPositionalInput(this);
268 RequestsPositionalInput = HandleInputCache.RequestsPositionalInput(this);
269
270 RequestsNonPositionalInputSubTree = RequestsNonPositionalInput;
271 RequestsPositionalInputSubTree = RequestsPositionalInput;
272
273 InjectDependencies(dependencies);
274
275 cacheUnbindActions();
276
277 LoadAsyncComplete();
278
279 if (timeBefore > 1000)
280 {
281 double loadDuration = perf_clock.CurrentTime - timeBefore;
282
283 bool blocking = ThreadSafety.IsUpdateThread;
284
285 double allowedDuration = blocking ? 16 : 100;
286
287 if (loadDuration > allowedDuration)
288 {
289 Logger.Log($@"{ToString()} took {loadDuration:0.00}ms to load" + (blocking ? " (and blocked the update thread)" : " (async)"), LoggingTarget.Performance,
290 blocking ? LogLevel.Important : LogLevel.Verbose);
291 }
292 }
293 }
294
295 /// <summary>
296 /// Injects dependencies from an <see cref="IReadOnlyDependencyContainer"/> into this <see cref="Drawable"/>.
297 /// </summary>
298 /// <param name="dependencies">The dependencies to inject.</param>
299 protected virtual void InjectDependencies(IReadOnlyDependencyContainer dependencies) => dependencies.Inject(this);
300
301 /// <summary>
302 /// Runs once on the update thread after loading has finished.
303 /// </summary>
304 private bool loadComplete()
305 {
306 if (loadState < LoadState.Ready) return false;
307
308 loadState = LoadState.Loaded;
309
310 // From a synchronous point of view, this is the first time the Drawable receives a parent.
311 // 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.
312 // 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).
313 // Only layout flags are required, as non-layout flags are always propagated by the parent.
314 Invalidate(Invalidation.Layout, InvalidationSource.Parent);
315
316 LoadComplete();
317
318 OnLoadComplete?.Invoke(this);
319 OnLoadComplete = null;
320 return true;
321 }
322
323 /// <summary>
324 /// Invoked after dependency injection has completed for this <see cref="Drawable"/> and all
325 /// children if this is a <see cref="CompositeDrawable"/>.
326 /// </summary>
327 /// <remarks>
328 /// This method is invoked in the potentially asynchronous context of <see cref="Load"/> prior to
329 /// this <see cref="Drawable"/> becoming <see cref="IsLoaded"/> = true.
330 /// </remarks>
331 protected virtual void LoadAsyncComplete()
332 {
333 }
334
335 /// <summary>
336 /// Invoked after this <see cref="Drawable"/> has finished loading.
337 /// </summary>
338 /// <remarks>
339 /// This method is invoked on the update thread inside this <see cref="Drawable"/>'s <see cref="UpdateSubTree"/>.
340 /// </remarks>
341 protected virtual void LoadComplete()
342 {
343 }
344
345 #endregion
346
347 #region Sorting (CreationID / Depth)
348
349 /// <summary>
350 /// Captures the order in which Drawables were added to a <see cref="CompositeDrawable"/>. Each Drawable
351 /// is assigned a monotonically increasing ID upon being added to a <see cref="CompositeDrawable"/>. This
352 /// ID is unique within the <see cref="Parent"/> <see cref="CompositeDrawable"/>.
353 /// The primary use case of this ID is stable sorting of Drawables with equal <see cref="Depth"/>.
354 /// </summary>
355 internal ulong ChildID { get; set; }
356
357 /// <summary>
358 /// Whether this drawable has been added to a parent <see cref="CompositeDrawable"/>. Note that this does NOT imply that
359 /// <see cref="Parent"/> has been set.
360 /// This is primarily used to block properties such as <see cref="Depth"/> that strictly rely on the value of <see cref="Parent"/>
361 /// to alert the user of an invalid operation.
362 /// </summary>
363 internal bool IsPartOfComposite => ChildID != 0;
364
365 /// <summary>
366 /// Whether this drawable is part of its parent's <see cref="CompositeDrawable.AliveInternalChildren"/>.
367 /// </summary>
368 public bool IsAlive { get; internal set; }
369
370 private float depth;
371
372 /// <summary>
373 /// Controls which Drawables are behind or in front of other Drawables.
374 /// This amounts to sorting Drawables by their <see cref="Depth"/>.
375 /// A Drawable with higher <see cref="Depth"/> than another Drawable is
376 /// drawn behind the other Drawable.
377 /// </summary>
378 public float Depth
379 {
380 get => depth;
381 set
382 {
383 if (IsPartOfComposite)
384 {
385 throw new InvalidOperationException(
386 $"May not change {nameof(Depth)} while inside a parent {nameof(CompositeDrawable)}." +
387 $"Use the parent's {nameof(CompositeDrawable.ChangeInternalChildDepth)} or {nameof(Container.ChangeChildDepth)} instead.");
388 }
389
390 depth = value;
391 }
392 }
393
394 #endregion
395
396 #region Periodic tasks (events, Scheduler, Transforms, Update)
397
398 /// <summary>
399 /// This event is fired after the <see cref="Update"/> method is called at the end of
400 /// <see cref="UpdateSubTree"/>. It should be used when a simple action should be performed
401 /// at the end of every update call which does not warrant overriding the Drawable.
402 /// </summary>
403 public event Action<Drawable> OnUpdate;
404
405 /// <summary>
406 /// This event is fired after the <see cref="LoadComplete"/> method is called.
407 /// It should be used when a simple action should be performed
408 /// when the Drawable is loaded which does not warrant overriding the Drawable.
409 /// This event is automatically cleared after being invoked.
410 /// </summary>
411 public event Action<Drawable> OnLoadComplete;
412
413 /// <summary>.
414 /// Fired after the <see cref="Invalidate"/> method is called.
415 /// </summary>
416 internal event Action<Drawable> Invalidated;
417
418 /// <summary>
419 /// Fired after the <see cref="Dispose(bool)"/> method is called.
420 /// </summary>
421 internal event Action OnDispose;
422
423 /// <summary>
424 /// Fired after the <see cref="UnbindAllBindables"/> method is called.
425 /// </summary>
426 internal event Action OnUnbindAllBindables;
427
428 /// <summary>
429 /// A lock exclusively used for initial acquisition/construction of the <see cref="Scheduler"/>.
430 /// </summary>
431 private static readonly object scheduler_acquisition_lock = new object();
432
433 private Scheduler scheduler;
434
435 /// <summary>
436 /// A lazily-initialized scheduler used to schedule tasks to be invoked in future <see cref="Update"/>s calls.
437 /// The tasks are invoked at the beginning of the <see cref="Update"/> method before anything else.
438 /// </summary>
439 protected internal Scheduler Scheduler
440 {
441 get
442 {
443 if (scheduler != null)
444 return scheduler;
445
446 lock (scheduler_acquisition_lock)
447 return scheduler ??= new Scheduler(() => ThreadSafety.IsUpdateThread, Clock);
448 }
449 }
450
451 /// <summary>
452 /// Updates this Drawable and all Drawables further down the scene graph.
453 /// Called once every frame.
454 /// </summary>
455 /// <returns>False if the drawable should not be updated.</returns>
456 public virtual bool UpdateSubTree()
457 {
458 if (IsDisposed)
459 throw new ObjectDisposedException(ToString(), "Disposed Drawables may never be in the scene graph.");
460
461 if (ProcessCustomClock)
462 customClock?.ProcessFrame();
463
464 if (loadState < LoadState.Ready)
465 return false;
466
467 if (loadState == LoadState.Ready)
468 loadComplete();
469
470 Debug.Assert(loadState == LoadState.Loaded);
471
472 UpdateTransforms();
473
474 if (!IsPresent)
475 return true;
476
477 if (scheduler != null)
478 {
479 int amountScheduledTasks = scheduler.Update();
480 FrameStatistics.Add(StatisticsCounterType.ScheduleInvk, amountScheduledTasks);
481 }
482
483 Update();
484 OnUpdate?.Invoke(this);
485 return true;
486 }
487
488 /// <summary>
489 /// Updates all masking calculations for this <see cref="Drawable"/>.
490 /// This occurs post-<see cref="UpdateSubTree"/> to ensure that all <see cref="Drawable"/> updates have taken place.
491 /// </summary>
492 /// <param name="source">The parent that triggered this update on this <see cref="Drawable"/>.</param>
493 /// <param name="maskingBounds">The <see cref="RectangleF"/> that defines the masking bounds.</param>
494 /// <returns>Whether masking calculations have taken place.</returns>
495 public virtual bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
496 {
497 if (!IsPresent)
498 return false;
499
500 if (HasProxy && source != proxy)
501 return false;
502
503 IsMaskedAway = ComputeIsMaskedAway(maskingBounds);
504 return true;
505 }
506
507 /// <summary>
508 /// Computes whether this <see cref="Drawable"/> is masked away.
509 /// </summary>
510 /// <param name="maskingBounds">The <see cref="RectangleF"/> that defines the masking bounds.</param>
511 /// <returns>Whether this <see cref="Drawable"/> is currently masked away.</returns>
512 protected virtual bool ComputeIsMaskedAway(RectangleF maskingBounds) => !Precision.AlmostIntersects(maskingBounds, ScreenSpaceDrawQuad.AABBFloat);
513
514 /// <summary>
515 /// Performs a once-per-frame update specific to this Drawable. A more elegant alternative to
516 /// <see cref="OnUpdate"/> when deriving from <see cref="Drawable"/>. Note, that this
517 /// method is always called before Drawables further down the scene graph are updated.
518 /// </summary>
519 protected virtual void Update()
520 {
521 }
522
523 #endregion
524
525 #region Position / Size (with margin)
526
527 private Vector2 position
528 {
529 get => new Vector2(x, y);
530 set
531 {
532 x = value.X;
533 y = value.Y;
534 }
535 }
536
537 /// <summary>
538 /// Positional offset of <see cref="Origin"/> to <see cref="RelativeAnchorPosition"/> in the
539 /// <see cref="Parent"/>'s coordinate system. May be in absolute or relative units
540 /// (controlled by <see cref="RelativePositionAxes"/>).
541 /// </summary>
542 public Vector2 Position
543 {
544 get => position;
545 set
546 {
547 if (position == value) return;
548
549 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Position)} must be finite, but is {value}.");
550
551 Axes changedAxes = Axes.None;
552
553 if (position.X != value.X)
554 changedAxes |= Axes.X;
555
556 if (position.Y != value.Y)
557 changedAxes |= Axes.Y;
558
559 position = value;
560
561 invalidateParentSizeDependencies(Invalidation.MiscGeometry, changedAxes);
562 }
563 }
564
565 private float x;
566 private float y;
567
568 /// <summary>
569 /// X component of <see cref="Position"/>.
570 /// </summary>
571 public float X
572 {
573 get => x;
574 set
575 {
576 if (x == value) return;
577
578 if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(X)} must be finite, but is {value}.");
579
580 x = value;
581
582 invalidateParentSizeDependencies(Invalidation.MiscGeometry, Axes.X);
583 }
584 }
585
586 /// <summary>
587 /// Y component of <see cref="Position"/>.
588 /// </summary>
589 public float Y
590 {
591 get => y;
592 set
593 {
594 if (y == value) return;
595
596 if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(Y)} must be finite, but is {value}.");
597
598 y = value;
599
600 invalidateParentSizeDependencies(Invalidation.MiscGeometry, Axes.Y);
601 }
602 }
603
604 private Axes relativePositionAxes;
605
606 /// <summary>
607 /// Controls which <see cref="Axes"/> of <see cref="Position"/> are relative w.r.t.
608 /// <see cref="Parent"/>'s size (from 0 to 1) rather than absolute.
609 /// The <see cref="Axes"/> set in this property are ignored by automatically sizing
610 /// parents.
611 /// </summary>
612 /// <remarks>
613 /// When setting this property, the <see cref="Position"/> is converted such that
614 /// <see cref="DrawPosition"/> remains invariant.
615 /// </remarks>
616 public Axes RelativePositionAxes
617 {
618 get => relativePositionAxes;
619 set
620 {
621 if (value == relativePositionAxes)
622 return;
623
624 // Convert coordinates from relative to absolute or vice versa
625 Vector2 conversion = relativeToAbsoluteFactor;
626 if ((value & Axes.X) > (relativePositionAxes & Axes.X))
627 X = Precision.AlmostEquals(conversion.X, 0) ? 0 : X / conversion.X;
628 else if ((relativePositionAxes & Axes.X) > (value & Axes.X))
629 X *= conversion.X;
630
631 if ((value & Axes.Y) > (relativePositionAxes & Axes.Y))
632 Y = Precision.AlmostEquals(conversion.Y, 0) ? 0 : Y / conversion.Y;
633 else if ((relativePositionAxes & Axes.Y) > (value & Axes.Y))
634 Y *= conversion.Y;
635
636 relativePositionAxes = value;
637
638 updateBypassAutoSizeAxes();
639 }
640 }
641
642 /// <summary>
643 /// Absolute positional offset of <see cref="Origin"/> to <see cref="RelativeAnchorPosition"/>
644 /// in the <see cref="Parent"/>'s coordinate system.
645 /// </summary>
646 public Vector2 DrawPosition
647 {
648 get
649 {
650 Vector2 offset = Vector2.Zero;
651
652 if (Parent != null && RelativePositionAxes != Axes.None)
653 {
654 offset = Parent.RelativeChildOffset;
655
656 if (!RelativePositionAxes.HasFlagFast(Axes.X))
657 offset.X = 0;
658
659 if (!RelativePositionAxes.HasFlagFast(Axes.Y))
660 offset.Y = 0;
661 }
662
663 return ApplyRelativeAxes(RelativePositionAxes, Position - offset, FillMode.Stretch);
664 }
665 }
666
667 private Vector2 size
668 {
669 get => new Vector2(width, height);
670 set
671 {
672 width = value.X;
673 height = value.Y;
674 }
675 }
676
677 /// <summary>
678 /// Size of this Drawable in the <see cref="Parent"/>'s coordinate system.
679 /// May be in absolute or relative units (controlled by <see cref="RelativeSizeAxes"/>).
680 /// </summary>
681 public virtual Vector2 Size
682 {
683 get => size;
684 set
685 {
686 if (size == value) return;
687
688 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Size)} must be finite, but is {value}.");
689
690 Axes changedAxes = Axes.None;
691
692 if (size.X != value.X)
693 changedAxes |= Axes.X;
694
695 if (size.Y != value.Y)
696 changedAxes |= Axes.Y;
697
698 size = value;
699
700 invalidateParentSizeDependencies(Invalidation.DrawSize, changedAxes);
701 }
702 }
703
704 private float width;
705 private float height;
706
707 /// <summary>
708 /// X component of <see cref="Size"/>.
709 /// </summary>
710 public virtual float Width
711 {
712 get => width;
713 set
714 {
715 if (width == value) return;
716
717 if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(Width)} must be finite, but is {value}.");
718
719 width = value;
720
721 invalidateParentSizeDependencies(Invalidation.DrawSize, Axes.X);
722 }
723 }
724
725 /// <summary>
726 /// Y component of <see cref="Size"/>.
727 /// </summary>
728 public virtual float Height
729 {
730 get => height;
731 set
732 {
733 if (height == value) return;
734
735 if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(Height)} must be finite, but is {value}.");
736
737 height = value;
738
739 invalidateParentSizeDependencies(Invalidation.DrawSize, Axes.Y);
740 }
741 }
742
743 private Axes relativeSizeAxes;
744
745 /// <summary>
746 /// Controls which <see cref="Axes"/> are relative sizes w.r.t. <see cref="Parent"/>'s size
747 /// (from 0 to 1) in the <see cref="Parent"/>'s coordinate system, rather than absolute sizes.
748 /// The <see cref="Axes"/> set in this property are ignored by automatically sizing
749 /// parents.
750 /// </summary>
751 /// <remarks>
752 /// If an axis becomes relatively sized and its component of <see cref="Size"/> was previously 0,
753 /// then it automatically becomes 1. In all other cases <see cref="Size"/> is converted such that
754 /// <see cref="DrawSize"/> remains invariant across changes of this property.
755 /// </remarks>
756 public virtual Axes RelativeSizeAxes
757 {
758 get => relativeSizeAxes;
759 set
760 {
761 if (value == relativeSizeAxes)
762 return;
763
764 // In some cases we cannot easily preserve our size, and so we simply invalidate and
765 // leave correct sizing to the user.
766 if (fillMode != FillMode.Stretch && (value == Axes.Both || relativeSizeAxes == Axes.Both))
767 Invalidate(Invalidation.DrawSize);
768 else
769 {
770 // Convert coordinates from relative to absolute or vice versa
771 Vector2 conversion = relativeToAbsoluteFactor;
772 if ((value & Axes.X) > (relativeSizeAxes & Axes.X))
773 Width = Precision.AlmostEquals(conversion.X, 0) ? 0 : Width / conversion.X;
774 else if ((relativeSizeAxes & Axes.X) > (value & Axes.X))
775 Width *= conversion.X;
776
777 if ((value & Axes.Y) > (relativeSizeAxes & Axes.Y))
778 Height = Precision.AlmostEquals(conversion.Y, 0) ? 0 : Height / conversion.Y;
779 else if ((relativeSizeAxes & Axes.Y) > (value & Axes.Y))
780 Height *= conversion.Y;
781 }
782
783 relativeSizeAxes = value;
784
785 if (relativeSizeAxes.HasFlagFast(Axes.X) && Width == 0) Width = 1;
786 if (relativeSizeAxes.HasFlagFast(Axes.Y) && Height == 0) Height = 1;
787
788 updateBypassAutoSizeAxes();
789
790 OnSizingChanged();
791 }
792 }
793
794 private readonly LayoutValue<Vector2> drawSizeBacking = new LayoutValue<Vector2>(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence);
795
796 /// <summary>
797 /// Absolute size of this Drawable in the <see cref="Parent"/>'s coordinate system.
798 /// </summary>
799 public Vector2 DrawSize => drawSizeBacking.IsValid ? drawSizeBacking : drawSizeBacking.Value = ApplyRelativeAxes(RelativeSizeAxes, Size, FillMode);
800
801 /// <summary>
802 /// X component of <see cref="DrawSize"/>.
803 /// </summary>
804 public float DrawWidth => DrawSize.X;
805
806 /// <summary>
807 /// Y component of <see cref="DrawSize"/>.
808 /// </summary>
809 public float DrawHeight => DrawSize.Y;
810
811 private MarginPadding margin;
812
813 /// <summary>
814 /// Size of an empty region around this Drawable used to manipulate
815 /// layout. Does not affect <see cref="DrawSize"/> or the region of accepted input,
816 /// but does affect <see cref="LayoutSize"/>.
817 /// </summary>
818 public MarginPadding Margin
819 {
820 get => margin;
821 set
822 {
823 if (margin.Equals(value)) return;
824
825 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Margin)} must be finite, but is {value}.");
826
827 margin = value;
828
829 Invalidate(Invalidation.MiscGeometry);
830 }
831 }
832
833 /// <summary>
834 /// Absolute size of this Drawable's layout rectangle in the <see cref="Parent"/>'s
835 /// coordinate system; i.e. <see cref="DrawSize"/> with the addition of <see cref="Margin"/>.
836 /// </summary>
837 public Vector2 LayoutSize => DrawSize + new Vector2(margin.TotalHorizontal, margin.TotalVertical);
838
839 /// <summary>
840 /// Absolutely sized rectangle for drawing in the <see cref="Parent"/>'s coordinate system.
841 /// Based on <see cref="DrawSize"/>.
842 /// </summary>
843 public RectangleF DrawRectangle
844 {
845 get
846 {
847 Vector2 s = DrawSize;
848 return new RectangleF(0, 0, s.X, s.Y);
849 }
850 }
851
852 /// <summary>
853 /// Absolutely sized rectangle for layout in the <see cref="Parent"/>'s coordinate system.
854 /// Based on <see cref="LayoutSize"/> and <see cref="margin"/>.
855 /// </summary>
856 public RectangleF LayoutRectangle
857 {
858 get
859 {
860 Vector2 s = LayoutSize;
861 return new RectangleF(-margin.Left, -margin.Top, s.X, s.Y);
862 }
863 }
864
865 /// <summary>
866 /// Helper function for converting potentially relative coordinates in the
867 /// <see cref="Parent"/>'s space to absolute coordinates based on which
868 /// axes are relative. <see cref="Axes"/>
869 /// </summary>
870 /// <param name="relativeAxes">Describes which axes are relative.</param>
871 /// <param name="v">The coordinates to convert.</param>
872 /// <param name="fillMode">The <see cref="FillMode"/> to be used.</param>
873 /// <returns>Absolute coordinates in <see cref="Parent"/>'s space.</returns>
874 protected Vector2 ApplyRelativeAxes(Axes relativeAxes, Vector2 v, FillMode fillMode)
875 {
876 if (relativeAxes != Axes.None)
877 {
878 Vector2 conversion = relativeToAbsoluteFactor;
879
880 if (relativeAxes.HasFlagFast(Axes.X))
881 v.X *= conversion.X;
882 if (relativeAxes.HasFlagFast(Axes.Y))
883 v.Y *= conversion.Y;
884
885 // FillMode only makes sense if both axes are relatively sized as the general rule
886 // for n-dimensional aspect preservation is to simply take the minimum or the maximum
887 // scale among all active axes. For single axes the minimum / maximum is just the
888 // value itself.
889 if (relativeAxes == Axes.Both && fillMode != FillMode.Stretch)
890 {
891 if (fillMode == FillMode.Fill)
892 v = new Vector2(Math.Max(v.X, v.Y * fillAspectRatio));
893 else if (fillMode == FillMode.Fit)
894 v = new Vector2(Math.Min(v.X, v.Y * fillAspectRatio));
895 v.Y /= fillAspectRatio;
896 }
897 }
898
899 return v;
900 }
901
902 /// <summary>
903 /// Conversion factor from relative to absolute coordinates in the <see cref="Parent"/>'s space.
904 /// </summary>
905 private Vector2 relativeToAbsoluteFactor => Parent?.RelativeToAbsoluteFactor ?? Vector2.One;
906
907 private Axes bypassAutoSizeAxes;
908
909 private void updateBypassAutoSizeAxes()
910 {
911 var value = RelativePositionAxes | RelativeSizeAxes | bypassAutoSizeAdditionalAxes;
912
913 if (bypassAutoSizeAxes != value)
914 {
915 var changedAxes = bypassAutoSizeAxes ^ value;
916 bypassAutoSizeAxes = value;
917 if (((Parent?.AutoSizeAxes ?? 0) & changedAxes) != 0)
918 Parent?.Invalidate(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child);
919 }
920 }
921
922 private Axes bypassAutoSizeAdditionalAxes;
923
924 /// <summary>
925 /// Controls which <see cref="Axes"/> are ignored by parent <see cref="Parent"/> automatic sizing.
926 /// Most notably, <see cref="RelativePositionAxes"/> and <see cref="RelativeSizeAxes"/> do not affect
927 /// automatic sizing to avoid circular size dependencies.
928 /// </summary>
929 public Axes BypassAutoSizeAxes
930 {
931 get => bypassAutoSizeAxes;
932 set
933 {
934 bypassAutoSizeAdditionalAxes = value;
935 updateBypassAutoSizeAxes();
936 }
937 }
938
939 /// <summary>
940 /// Computes the bounding box of this drawable in its parent's space.
941 /// </summary>
942 public virtual RectangleF BoundingBox => ToParentSpace(LayoutRectangle).AABBFloat;
943
944 /// <summary>
945 /// Called whenever the <see cref="RelativeSizeAxes"/> of this drawable is changed, or when the <see cref="Container{T}.AutoSizeAxes"/> are changed if this drawable is a <see cref="Container{T}"/>.
946 /// </summary>
947 protected virtual void OnSizingChanged()
948 {
949 }
950
951 #endregion
952
953 #region Scale / Shear / Rotation
954
955 private Vector2 scale = Vector2.One;
956
957 /// <summary>
958 /// Base relative scaling factor around <see cref="OriginPosition"/>.
959 /// </summary>
960 public Vector2 Scale
961 {
962 get => scale;
963 set
964 {
965 if (Math.Abs(value.X) < Precision.FLOAT_EPSILON)
966 value.X = Precision.FLOAT_EPSILON;
967 if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON)
968 value.Y = Precision.FLOAT_EPSILON;
969
970 if (scale == value)
971 return;
972
973 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Scale)} must be finite, but is {value}.");
974
975 bool wasPresent = IsPresent;
976
977 scale = value;
978
979 if (IsPresent != wasPresent)
980 Invalidate(Invalidation.MiscGeometry | Invalidation.Presence);
981 else
982 Invalidate(Invalidation.MiscGeometry);
983 }
984 }
985
986 private float fillAspectRatio = 1;
987
988 /// <summary>
989 /// The desired ratio of width to height when under the effect of a non-stretching <see cref="FillMode"/>
990 /// and <see cref="RelativeSizeAxes"/> being <see cref="Axes.Both"/>.
991 /// </summary>
992 public float FillAspectRatio
993 {
994 get => fillAspectRatio;
995 set
996 {
997 if (fillAspectRatio == value) return;
998
999 if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(FillAspectRatio)} must be finite, but is {value}.");
1000 if (value == 0) throw new ArgumentException($@"{nameof(FillAspectRatio)} must be non-zero.");
1001
1002 fillAspectRatio = value;
1003
1004 if (fillMode != FillMode.Stretch && RelativeSizeAxes == Axes.Both)
1005 Invalidate(Invalidation.DrawSize);
1006 }
1007 }
1008
1009 private FillMode fillMode;
1010
1011 /// <summary>
1012 /// Controls the behavior of <see cref="RelativeSizeAxes"/> when it is set to <see cref="Axes.Both"/>.
1013 /// Otherwise, this member has no effect. By default, stretching is used, which simply scales
1014 /// this drawable's <see cref="Size"/> according to <see cref="Parent"/>'s <see cref="CompositeDrawable.RelativeToAbsoluteFactor"/>
1015 /// disregarding this drawable's <see cref="FillAspectRatio"/>. Other values of <see cref="FillMode"/> preserve <see cref="FillAspectRatio"/>.
1016 /// </summary>
1017 public FillMode FillMode
1018 {
1019 get => fillMode;
1020 set
1021 {
1022 if (fillMode == value) return;
1023
1024 fillMode = value;
1025
1026 Invalidate(Invalidation.DrawSize);
1027 }
1028 }
1029
1030 /// <summary>
1031 /// Relative scaling factor around <see cref="OriginPosition"/>.
1032 /// </summary>
1033 protected virtual Vector2 DrawScale => Scale;
1034
1035 private Vector2 shear = Vector2.Zero;
1036
1037 /// <summary>
1038 /// Relative shearing factor. The X dimension is relative w.r.t. <see cref="Height"/>
1039 /// and the Y dimension relative w.r.t. <see cref="Width"/>.
1040 /// </summary>
1041 public Vector2 Shear
1042 {
1043 get => shear;
1044 set
1045 {
1046 if (shear == value) return;
1047
1048 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Shear)} must be finite, but is {value}.");
1049
1050 shear = value;
1051
1052 Invalidate(Invalidation.MiscGeometry);
1053 }
1054 }
1055
1056 private float rotation;
1057
1058 /// <summary>
1059 /// Rotation in degrees around <see cref="OriginPosition"/>.
1060 /// </summary>
1061 public float Rotation
1062 {
1063 get => rotation;
1064 set
1065 {
1066 if (value == rotation) return;
1067
1068 if (!float.IsFinite(value)) throw new ArgumentException($@"{nameof(Rotation)} must be finite, but is {value}.");
1069
1070 rotation = value;
1071
1072 Invalidate(Invalidation.MiscGeometry);
1073 }
1074 }
1075
1076 #endregion
1077
1078 #region Origin / Anchor
1079
1080 private Anchor origin = Anchor.TopLeft;
1081
1082 /// <summary>
1083 /// The origin of this <see cref="Drawable"/>.
1084 /// </summary>
1085 /// <exception cref="ArgumentException">If the provided value does not exist in the <see cref="osu.Framework.Graphics.Anchor"/> enumeration.</exception>
1086 public virtual Anchor Origin
1087 {
1088 get => origin;
1089 set
1090 {
1091 if (origin == value) return;
1092
1093 if (value == 0)
1094 throw new ArgumentException("Cannot set origin to 0.", nameof(value));
1095
1096 origin = value;
1097 Invalidate(Invalidation.MiscGeometry);
1098 }
1099 }
1100
1101 private Vector2 customOrigin;
1102
1103 /// <summary>
1104 /// The origin of this <see cref="Drawable"/> expressed in relative coordinates from the top-left corner of <see cref="DrawRectangle"/>.
1105 /// </summary>
1106 /// <exception cref="InvalidOperationException">If <see cref="Origin"/> is <see cref="osu.Framework.Graphics.Anchor.Custom"/>.</exception>
1107 public Vector2 RelativeOriginPosition
1108 {
1109 get
1110 {
1111 if (Origin == Anchor.Custom)
1112 throw new InvalidOperationException(@"Can not obtain relative origin position for custom origins.");
1113
1114 Vector2 result = Vector2.Zero;
1115 if (origin.HasFlagFast(Anchor.x1))
1116 result.X = 0.5f;
1117 else if (origin.HasFlagFast(Anchor.x2))
1118 result.X = 1;
1119
1120 if (origin.HasFlagFast(Anchor.y1))
1121 result.Y = 0.5f;
1122 else if (origin.HasFlagFast(Anchor.y2))
1123 result.Y = 1;
1124
1125 return result;
1126 }
1127 }
1128
1129 /// <summary>
1130 /// The origin of this <see cref="Drawable"/> expressed in absolute coordinates from the top-left corner of <see cref="DrawRectangle"/>.
1131 /// </summary>
1132 /// <exception cref="ArgumentException">If the provided value is not finite.</exception>
1133 public virtual Vector2 OriginPosition
1134 {
1135 get
1136 {
1137 Vector2 result;
1138 if (Origin == Anchor.Custom)
1139 result = customOrigin;
1140 else if (Origin == Anchor.TopLeft)
1141 result = Vector2.Zero;
1142 else
1143 result = computeAnchorPosition(LayoutSize, Origin);
1144
1145 return result - new Vector2(margin.Left, margin.Top);
1146 }
1147
1148 set
1149 {
1150 if (customOrigin == value && Origin == Anchor.Custom)
1151 return;
1152
1153 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(OriginPosition)} must be finite, but is {value}.");
1154
1155 customOrigin = value;
1156 Origin = Anchor.Custom;
1157
1158 Invalidate(Invalidation.MiscGeometry);
1159 }
1160 }
1161
1162 private Anchor anchor = Anchor.TopLeft;
1163
1164 /// <summary>
1165 /// Specifies where <see cref="Origin"/> is attached to the <see cref="Parent"/>
1166 /// in the coordinate system with origin at the top left corner of the
1167 /// <see cref="Parent"/>'s <see cref="DrawRectangle"/>.
1168 /// Can either be one of 9 relative positions (0, 0.5, and 1 in x and y)
1169 /// or a fixed absolute position via <see cref="RelativeAnchorPosition"/>.
1170 /// </summary>
1171 public Anchor Anchor
1172 {
1173 get => anchor;
1174 set
1175 {
1176 if (anchor == value) return;
1177
1178 if (value == 0)
1179 throw new ArgumentException("Cannot set anchor to 0.", nameof(value));
1180
1181 anchor = value;
1182 Invalidate(Invalidation.MiscGeometry);
1183 }
1184 }
1185
1186 private Vector2 customRelativeAnchorPosition;
1187
1188 /// <summary>
1189 /// Specifies in relative coordinates where <see cref="Origin"/> is attached
1190 /// to the <see cref="Parent"/> in the coordinate system with origin at the top
1191 /// left corner of the <see cref="Parent"/>'s <see cref="DrawRectangle"/>, and
1192 /// a value of <see cref="Vector2.One"/> referring to the bottom right corner of
1193 /// the <see cref="Parent"/>'s <see cref="DrawRectangle"/>.
1194 /// </summary>
1195 public Vector2 RelativeAnchorPosition
1196 {
1197 get
1198 {
1199 if (Anchor == Anchor.Custom)
1200 return customRelativeAnchorPosition;
1201
1202 Vector2 result = Vector2.Zero;
1203 if (anchor.HasFlagFast(Anchor.x1))
1204 result.X = 0.5f;
1205 else if (anchor.HasFlagFast(Anchor.x2))
1206 result.X = 1;
1207
1208 if (anchor.HasFlagFast(Anchor.y1))
1209 result.Y = 0.5f;
1210 else if (anchor.HasFlagFast(Anchor.y2))
1211 result.Y = 1;
1212
1213 return result;
1214 }
1215
1216 set
1217 {
1218 if (customRelativeAnchorPosition == value && Anchor == Anchor.Custom)
1219 return;
1220
1221 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeAnchorPosition)} must be finite, but is {value}.");
1222
1223 customRelativeAnchorPosition = value;
1224 Anchor = Anchor.Custom;
1225
1226 Invalidate(Invalidation.MiscGeometry);
1227 }
1228 }
1229
1230 /// <summary>
1231 /// Specifies in absolute coordinates where <see cref="Origin"/> is attached
1232 /// to the <see cref="Parent"/> in the coordinate system with origin at the top
1233 /// left corner of the <see cref="Parent"/>'s <see cref="DrawRectangle"/>.
1234 /// </summary>
1235 public Vector2 AnchorPosition => RelativeAnchorPosition * Parent?.ChildSize ?? Vector2.Zero;
1236
1237 /// <summary>
1238 /// Helper function to compute an absolute position given an absolute size and
1239 /// a relative <see cref="Graphics.Anchor"/>.
1240 /// </summary>
1241 /// <param name="size">Absolute size</param>
1242 /// <param name="anchor">Relative <see cref="Graphics.Anchor"/></param>
1243 /// <returns>Absolute position</returns>
1244 private static Vector2 computeAnchorPosition(Vector2 size, Anchor anchor)
1245 {
1246 Vector2 result = Vector2.Zero;
1247
1248 if (anchor.HasFlagFast(Anchor.x1))
1249 result.X = size.X / 2f;
1250 else if (anchor.HasFlagFast(Anchor.x2))
1251 result.X = size.X;
1252
1253 if (anchor.HasFlagFast(Anchor.y1))
1254 result.Y = size.Y / 2f;
1255 else if (anchor.HasFlagFast(Anchor.y2))
1256 result.Y = size.Y;
1257
1258 return result;
1259 }
1260
1261 #endregion
1262
1263 #region Colour / Alpha / Blending
1264
1265 private ColourInfo colour = Color4.White;
1266
1267 /// <summary>
1268 /// Colour of this <see cref="Drawable"/> in sRGB space. Can contain individual colours for all four
1269 /// corners of this <see cref="Drawable"/>, which are then interpolated, but can also be assigned
1270 /// just a single colour. Implicit casts from <see cref="SRGBColour"/> and from <see cref="Color4"/> exist.
1271 /// </summary>
1272 public ColourInfo Colour
1273 {
1274 get => colour;
1275 set
1276 {
1277 if (colour.Equals(value)) return;
1278
1279 colour = value;
1280
1281 Invalidate(Invalidation.Colour);
1282 }
1283 }
1284
1285 private float alpha = 1.0f;
1286
1287 /// <summary>
1288 /// Multiplicative alpha factor applied on top of <see cref="ColourInfo"/> and its existing
1289 /// alpha channel(s).
1290 /// </summary>
1291 public float Alpha
1292 {
1293 get => alpha;
1294 set
1295 {
1296 if (alpha == value)
1297 return;
1298
1299 bool wasPresent = IsPresent;
1300
1301 alpha = value;
1302
1303 if (IsPresent != wasPresent)
1304 Invalidate(Invalidation.Colour | Invalidation.Presence);
1305 else
1306 Invalidate(Invalidation.Colour);
1307 }
1308 }
1309
1310 private const float visibility_cutoff = 0.0001f;
1311
1312 /// <summary>
1313 /// Determines whether this Drawable is present based on its <see cref="Alpha"/> value.
1314 /// Can be forced always on with <see cref="AlwaysPresent"/>.
1315 /// </summary>
1316 public virtual bool IsPresent => AlwaysPresent || Alpha > visibility_cutoff && Math.Abs(Scale.X) > Precision.FLOAT_EPSILON && Math.Abs(Scale.Y) > Precision.FLOAT_EPSILON;
1317
1318 private bool alwaysPresent;
1319
1320 /// <summary>
1321 /// If true, forces <see cref="IsPresent"/> to always be true. In other words,
1322 /// this drawable is always considered for layout, input, and drawing, regardless
1323 /// of alpha value.
1324 /// </summary>
1325 public bool AlwaysPresent
1326 {
1327 get => alwaysPresent;
1328 set
1329 {
1330 if (alwaysPresent == value)
1331 return;
1332
1333 bool wasPresent = IsPresent;
1334
1335 alwaysPresent = value;
1336
1337 if (IsPresent != wasPresent)
1338 Invalidate(Invalidation.Presence);
1339 }
1340 }
1341
1342 private BlendingParameters blending;
1343
1344 /// <summary>
1345 /// Determines how this Drawable is blended with other already drawn Drawables.
1346 /// Inherits the <see cref="Parent"/>'s <see cref="Blending"/> by default.
1347 /// </summary>
1348 public BlendingParameters Blending
1349 {
1350 get => blending;
1351 set
1352 {
1353 if (blending == value)
1354 return;
1355
1356 blending = value;
1357
1358 Invalidate(Invalidation.Colour);
1359 }
1360 }
1361
1362 #endregion
1363
1364 #region Timekeeping
1365
1366 private IFrameBasedClock customClock;
1367 private IFrameBasedClock clock;
1368
1369 /// <summary>
1370 /// The clock of this drawable. Used for keeping track of time across
1371 /// frames. By default is inherited from <see cref="Parent"/>.
1372 /// If set, then the provided value is used as a custom clock and the
1373 /// <see cref="Parent"/>'s clock is ignored.
1374 /// </summary>
1375 public override IFrameBasedClock Clock
1376 {
1377 get => clock;
1378 set
1379 {
1380 customClock = value;
1381 UpdateClock(customClock);
1382 }
1383 }
1384
1385 /// <summary>
1386 /// Updates the clock to be used. Has no effect if this drawable
1387 /// uses a custom clock.
1388 /// </summary>
1389 /// <param name="clock">The new clock to be used.</param>
1390 internal virtual void UpdateClock(IFrameBasedClock clock)
1391 {
1392 this.clock = customClock ?? clock;
1393 scheduler?.UpdateClock(this.clock);
1394 }
1395
1396 /// <summary>
1397 /// Whether <see cref="IFrameBasedClock.ProcessFrame"/> should be automatically invoked on this <see cref="Drawable"/>'s <see cref="Clock"/>
1398 /// in <see cref="UpdateSubTree"/>. This should only be set to false in scenarios where the clock is updated elsewhere.
1399 /// </summary>
1400 public bool ProcessCustomClock = true;
1401
1402 private double lifetimeStart = double.MinValue;
1403 private double lifetimeEnd = double.MaxValue;
1404
1405 /// <summary>
1406 /// Invoked after <see cref="lifetimeStart"/> or <see cref="LifetimeEnd"/> has changed.
1407 /// </summary>
1408 internal event Action<Drawable> LifetimeChanged;
1409
1410 /// <summary>
1411 /// The time at which this drawable becomes valid (and is considered for drawing).
1412 /// </summary>
1413 public virtual double LifetimeStart
1414 {
1415 get => lifetimeStart;
1416 set
1417 {
1418 if (lifetimeStart == value) return;
1419
1420 lifetimeStart = value;
1421 LifetimeChanged?.Invoke(this);
1422 }
1423 }
1424
1425 /// <summary>
1426 /// The time at which this drawable is no longer valid (and is considered for disposal).
1427 /// </summary>
1428 public virtual double LifetimeEnd
1429 {
1430 get => lifetimeEnd;
1431 set
1432 {
1433 if (lifetimeEnd == value) return;
1434
1435 lifetimeEnd = value;
1436 LifetimeChanged?.Invoke(this);
1437 }
1438 }
1439
1440 /// <summary>
1441 /// Whether this drawable should currently be alive.
1442 /// This is queried by the framework to decide the <see cref="IsAlive"/> state of this drawable for the next frame.
1443 /// </summary>
1444 protected internal virtual bool ShouldBeAlive
1445 {
1446 get
1447 {
1448 if (LifetimeStart == double.MinValue && LifetimeEnd == double.MaxValue)
1449 return true;
1450
1451 return Time.Current >= LifetimeStart && Time.Current < LifetimeEnd;
1452 }
1453 }
1454
1455 /// <summary>
1456 /// Whether to remove the drawable from its parent's children when it's not alive.
1457 /// </summary>
1458 public virtual bool RemoveWhenNotAlive => Parent == null || Time.Current > LifetimeStart;
1459
1460 #endregion
1461
1462 #region Parenting (scene graph operations, including ProxyDrawable)
1463
1464 /// <summary>
1465 /// Retrieve the first parent in the tree which derives from <see cref="InputManager"/>.
1466 /// As this is performing an upward tree traversal, avoid calling every frame.
1467 /// </summary>
1468 /// <returns>The first parent <see cref="InputManager"/>.</returns>
1469 protected InputManager GetContainingInputManager() => FindClosestParent<InputManager>();
1470
1471 private CompositeDrawable parent;
1472
1473 /// <summary>
1474 /// The parent of this drawable in the scene graph.
1475 /// </summary>
1476 public CompositeDrawable Parent
1477 {
1478 get => parent;
1479 internal set
1480 {
1481 if (IsDisposed)
1482 throw new ObjectDisposedException(ToString(), "Disposed Drawables may never get a parent and return to the scene graph.");
1483
1484 if (value == null)
1485 ChildID = 0;
1486
1487 if (parent == value) return;
1488
1489 if (value != null && parent != null)
1490 throw new InvalidOperationException("May not add a drawable to multiple containers.");
1491
1492 parent = value;
1493 Invalidate(InvalidationFromParentSize | Invalidation.Colour | Invalidation.Presence | Invalidation.Parent);
1494
1495 if (parent != null)
1496 {
1497 //we should already have a clock at this point (from our LoadRequested invocation)
1498 //this just ensures we have the most recent parent clock.
1499 //we may want to consider enforcing that parent.Clock == clock here.
1500 UpdateClock(parent.Clock);
1501 }
1502 }
1503 }
1504
1505 /// <summary>
1506 /// Find the closest parent of a specified type.
1507 /// </summary>
1508 /// <remarks>
1509 /// This can be a potentially expensive operation and should be used with discretion.
1510 /// </remarks>
1511 /// <typeparam name="T">The type to match.</typeparam>
1512 /// <returns>The first matching parent, or null if no parent of type <typeparamref name="T"/> is found.</returns>
1513 internal T FindClosestParent<T>() where T : class, IDrawable
1514 {
1515 Drawable cursor = this;
1516
1517 while ((cursor = cursor.Parent) != null)
1518 {
1519 if (cursor is T match)
1520 return match;
1521 }
1522
1523 return default;
1524 }
1525
1526 /// <summary>
1527 /// Refers to the original if this drawable was created via
1528 /// <see cref="CreateProxy"/>. Otherwise refers to this.
1529 /// </summary>
1530 internal virtual Drawable Original => this;
1531
1532 /// <summary>
1533 /// True iff <see cref="CreateProxy"/> has been called before.
1534 /// </summary>
1535 public bool HasProxy => proxy != null;
1536
1537 /// <summary>
1538 /// True iff this <see cref="Drawable"/> is not a proxy of any <see cref="Drawable"/>.
1539 /// </summary>
1540 public bool IsProxy => Original != this;
1541
1542 private Drawable proxy;
1543
1544 /// <summary>
1545 /// Creates a proxy drawable which can be inserted elsewhere in the scene graph.
1546 /// Will cause the original instance to not render itself.
1547 /// Creating multiple proxies is not supported and will result in an
1548 /// <see cref="InvalidOperationException"/>.
1549 /// </summary>
1550 public Drawable CreateProxy()
1551 {
1552 if (proxy != null)
1553 throw new InvalidOperationException("Multiple proxies are not supported.");
1554
1555 return proxy = new ProxyDrawable(this);
1556 }
1557
1558 /// <summary>
1559 /// Validates a <see cref="DrawNode"/> for use by the proxy of this <see cref="Drawable"/>.
1560 /// This is used exclusively by <see cref="CompositeDrawable.addFromComposite"/>, and should not be used otherwise.
1561 /// </summary>
1562 /// <param name="treeIndex">The index of the <see cref="DrawNode"/> in <see cref="drawNodes"/> which the proxy should use.</param>
1563 /// <param name="frame">The frame for which the <see cref="DrawNode"/> was created. This is the parameter used for validation.</param>
1564 internal virtual void ValidateProxyDrawNode(int treeIndex, ulong frame) => proxy.ValidateProxyDrawNode(treeIndex, frame);
1565
1566 #endregion
1567
1568 #region Caching & invalidation (for things too expensive to compute every frame)
1569
1570 /// <summary>
1571 /// Was this Drawable masked away completely during the last frame?
1572 /// This is measured conservatively, i.e. it is only true when the Drawable was
1573 /// actually masked away, but it may be false, even if the Drawable was masked away.
1574 /// </summary>
1575 internal bool IsMaskedAway { get; private set; }
1576
1577 private readonly LayoutValue<Quad> screenSpaceDrawQuadBacking = new LayoutValue<Quad>(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence);
1578
1579 protected virtual Quad ComputeScreenSpaceDrawQuad() => ToScreenSpace(DrawRectangle);
1580
1581 /// <summary>
1582 /// The screen-space quad this drawable occupies.
1583 /// </summary>
1584 public virtual Quad ScreenSpaceDrawQuad => screenSpaceDrawQuadBacking.IsValid ? screenSpaceDrawQuadBacking : screenSpaceDrawQuadBacking.Value = ComputeScreenSpaceDrawQuad();
1585
1586 private readonly LayoutValue<DrawInfo> drawInfoBacking = new LayoutValue<DrawInfo>(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence);
1587
1588 private DrawInfo computeDrawInfo()
1589 {
1590 DrawInfo di = Parent?.DrawInfo ?? new DrawInfo(null);
1591
1592 Vector2 pos = DrawPosition + AnchorPosition;
1593 Vector2 drawScale = DrawScale;
1594
1595 if (Parent != null)
1596 pos += Parent.ChildOffset;
1597
1598 di.ApplyTransform(pos, drawScale, Rotation, Shear, OriginPosition);
1599
1600 return di;
1601 }
1602
1603 /// <summary>
1604 /// Contains the linear transformation of this <see cref="Drawable"/> that is used during draw.
1605 /// </summary>
1606 public virtual DrawInfo DrawInfo => drawInfoBacking.IsValid ? drawInfoBacking : drawInfoBacking.Value = computeDrawInfo();
1607
1608 private readonly LayoutValue<DrawColourInfo> drawColourInfoBacking = new LayoutValue<DrawColourInfo>(
1609 Invalidation.Colour | Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence,
1610 conditions: (s, i) =>
1611 {
1612 if ((i & Invalidation.Colour) > 0)
1613 return true;
1614
1615 return !s.Colour.HasSingleColour || (s.drawColourInfoBacking.IsValid && !s.drawColourInfoBacking.Value.Colour.HasSingleColour);
1616 });
1617
1618 /// <summary>
1619 /// Contains the colour and blending information of this <see cref="Drawable"/> that are used during draw.
1620 /// </summary>
1621 public virtual DrawColourInfo DrawColourInfo => drawColourInfoBacking.IsValid ? drawColourInfoBacking : drawColourInfoBacking.Value = computeDrawColourInfo();
1622
1623 private DrawColourInfo computeDrawColourInfo()
1624 {
1625 DrawColourInfo ci = Parent?.DrawColourInfo ?? new DrawColourInfo(null);
1626
1627 BlendingParameters localBlending = Blending;
1628
1629 if (Parent != null)
1630 localBlending.CopyFromParent(ci.Blending);
1631
1632 localBlending.ApplyDefaultToInherited();
1633
1634 ci.Blending = localBlending;
1635
1636 ColourInfo ourColour = alpha != 1 ? colour.MultiplyAlpha(alpha) : colour;
1637
1638 if (ci.Colour.HasSingleColour)
1639 ci.Colour.ApplyChild(ourColour);
1640 else
1641 {
1642 Debug.Assert(Parent != null,
1643 $"The {nameof(ci)} of null parents should always have the single colour white, and therefore this branch should never be hit.");
1644
1645 // Cannot use ToParentSpace here, because ToParentSpace depends on DrawInfo to be completed
1646 // ReSharper disable once PossibleNullReferenceException
1647 Quad interp = Quad.FromRectangle(DrawRectangle) * (DrawInfo.Matrix * Parent.DrawInfo.MatrixInverse);
1648 Vector2 parentSize = Parent.DrawSize;
1649
1650 ci.Colour.ApplyChild(ourColour,
1651 new Quad(
1652 Vector2.Divide(interp.TopLeft, parentSize),
1653 Vector2.Divide(interp.TopRight, parentSize),
1654 Vector2.Divide(interp.BottomLeft, parentSize),
1655 Vector2.Divide(interp.BottomRight, parentSize)));
1656 }
1657
1658 return ci;
1659 }
1660
1661 private readonly LayoutValue<Vector2> requiredParentSizeToFitBacking = new LayoutValue<Vector2>(Invalidation.RequiredParentSizeToFit);
1662
1663 private Vector2 computeRequiredParentSizeToFit()
1664 {
1665 // Auxiliary variables required for the computation
1666 Vector2 ap = AnchorPosition;
1667 Vector2 rap = RelativeAnchorPosition;
1668
1669 Vector2 ratio1 = new Vector2(
1670 rap.X <= 0 ? 0 : 1 / rap.X,
1671 rap.Y <= 0 ? 0 : 1 / rap.Y);
1672
1673 Vector2 ratio2 = new Vector2(
1674 rap.X >= 1 ? 0 : 1 / (1 - rap.X),
1675 rap.Y >= 1 ? 0 : 1 / (1 - rap.Y));
1676
1677 RectangleF bbox = BoundingBox;
1678
1679 // Compute the required size of the parent such that we fit in snugly when positioned
1680 // at our relative anchor in the parent.
1681 Vector2 topLeftOffset = ap - bbox.TopLeft;
1682 Vector2 topLeftSize1 = topLeftOffset * ratio1;
1683 Vector2 topLeftSize2 = -topLeftOffset * ratio2;
1684
1685 Vector2 bottomRightOffset = ap - bbox.BottomRight;
1686 Vector2 bottomRightSize1 = bottomRightOffset * ratio1;
1687 Vector2 bottomRightSize2 = -bottomRightOffset * ratio2;
1688
1689 // Expand bounds according to clipped offset
1690 return Vector2.ComponentMax(
1691 Vector2.ComponentMax(topLeftSize1, topLeftSize2),
1692 Vector2.ComponentMax(bottomRightSize1, bottomRightSize2));
1693 }
1694
1695 /// <summary>
1696 /// Returns the size of the smallest axis aligned box in parent space which
1697 /// encompasses this drawable while preserving this drawable's
1698 /// <see cref="RelativeAnchorPosition"/>.
1699 /// If a component of <see cref="RelativeAnchorPosition"/> is smaller than zero
1700 /// or larger than one, then it is impossible to preserve <see cref="RelativeAnchorPosition"/>
1701 /// while fitting into the parent, and thus <see cref="RelativeAnchorPosition"/> returns
1702 /// zero in that dimension; i.e. we no longer fit into the parent.
1703 /// This behavior is prominent with non-centre and non-custom <see cref="Anchor"/> values.
1704 /// </summary>
1705 internal Vector2 RequiredParentSizeToFit => requiredParentSizeToFitBacking.IsValid ? requiredParentSizeToFitBacking : requiredParentSizeToFitBacking.Value = computeRequiredParentSizeToFit();
1706
1707 /// <summary>
1708 /// The flags which this <see cref="Drawable"/> has been invalidated with, grouped by <see cref="InvalidationSource"/>.
1709 /// </summary>
1710 private InvalidationList invalidationList = new InvalidationList(Invalidation.All);
1711
1712 private readonly List<LayoutMember> layoutMembers = new List<LayoutMember>();
1713
1714 /// <summary>
1715 /// Adds a layout member that will be invalidated when its <see cref="LayoutMember.Invalidation"/> is invalidated.
1716 /// </summary>
1717 /// <param name="member">The layout member to add.</param>
1718 protected void AddLayout(LayoutMember member)
1719 {
1720 if (LoadState > LoadState.NotLoaded)
1721 throw new InvalidOperationException($"{nameof(LayoutMember)}s cannot be added after {nameof(Drawable)}s have started loading. Consider adding in the constructor.");
1722
1723 layoutMembers.Add(member);
1724 member.Parent = this;
1725 }
1726
1727 /// <summary>
1728 /// Validates the super-tree of this <see cref="Drawable"/> for the given <see cref="Invalidation"/> flags.
1729 /// </summary>
1730 /// <remarks>
1731 /// This is internally invoked by <see cref="LayoutMember"/>, and should not be invoked manually.
1732 /// </remarks>
1733 /// <param name="validationType">The <see cref="Invalidation"/> flags to validate with.</param>
1734 internal void ValidateSuperTree(Invalidation validationType)
1735 {
1736 if (invalidationList.Validate(validationType))
1737 Parent?.ValidateSuperTree(validationType);
1738 }
1739
1740 // Make sure we start out with a value of 1 such that ApplyDrawNode is always called at least once
1741 public long InvalidationID { get; private set; } = 1;
1742
1743 /// <summary>
1744 /// Invalidates the layout of this <see cref="Drawable"/>.
1745 /// </summary>
1746 /// <param name="invalidation">The flags to invalidate with.</param>
1747 /// <param name="source">The source that triggered the invalidation.</param>
1748 /// <returns>If any layout was invalidated.</returns>
1749 public bool Invalidate(Invalidation invalidation = Invalidation.All, InvalidationSource source = InvalidationSource.Self) => invalidate(invalidation, source);
1750
1751 /// <summary>
1752 /// Invalidates the layout of this <see cref="Drawable"/>.
1753 /// </summary>
1754 /// <param name="invalidation">The flags to invalidate with.</param>
1755 /// <param name="source">The source that triggered the invalidation.</param>
1756 /// <param name="propagateToParent">Whether to propagate the invalidation to the parent of this <see cref="Drawable"/>.
1757 /// Only has an effect if <paramref name="source"/> is <see cref="InvalidationSource.Self"/>.</param>
1758 /// <returns>If any layout was invalidated.</returns>
1759 private bool invalidate(Invalidation invalidation = Invalidation.All, InvalidationSource source = InvalidationSource.Self, bool propagateToParent = true)
1760 {
1761 if (source != InvalidationSource.Child && source != InvalidationSource.Parent && source != InvalidationSource.Self)
1762 throw new InvalidOperationException($"A {nameof(Drawable)} can only be invalidated with a singular {nameof(source)} (child, parent, or self).");
1763
1764 if (LoadState < LoadState.Ready)
1765 return false;
1766
1767 // Changes in the colour of children don't affect parents.
1768 if (source == InvalidationSource.Child)
1769 invalidation &= ~Invalidation.Colour;
1770
1771 if (invalidation == Invalidation.None)
1772 return false;
1773
1774 // If the invalidation originated locally, propagate to the immediate parent.
1775 // 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.
1776 // This is for only propagating once, otherwise it would propagate all the way to the root Drawable.
1777 if (propagateToParent && source == InvalidationSource.Self)
1778 Parent?.Invalidate(invalidation, InvalidationSource.Child);
1779
1780 // Perform the invalidation.
1781 if (!invalidationList.Invalidate(source, invalidation))
1782 return false;
1783
1784 // A DrawNode invalidation always invalidates.
1785 bool anyInvalidated = (invalidation & Invalidation.DrawNode) > 0;
1786
1787 // Invalidate all layout members
1788 foreach (var member in layoutMembers)
1789 {
1790 // Only invalidate layout members that accept the given source.
1791 if ((member.Source & source) == 0)
1792 continue;
1793
1794 // Remove invalidation flags that don't refer to the layout member.
1795 Invalidation memberInvalidation = invalidation & member.Invalidation;
1796 if (memberInvalidation == 0)
1797 continue;
1798
1799 if (member.Conditions?.Invoke(this, memberInvalidation) != false)
1800 anyInvalidated |= member.Invalidate();
1801 }
1802
1803 // Allow any custom invalidation to take place.
1804 anyInvalidated |= OnInvalidate(invalidation, source);
1805
1806 if (anyInvalidated)
1807 InvalidationID++;
1808
1809 Invalidated?.Invoke(this);
1810
1811 return anyInvalidated;
1812 }
1813
1814 /// <summary>
1815 /// Invoked when the layout of this <see cref="Drawable"/> was invalidated.
1816 /// </summary>
1817 /// <remarks>
1818 /// This should be used to perform any custom invalidation logic that cannot be described as a layout.
1819 /// </remarks>
1820 /// <remarks>
1821 /// This does not ensure that the parent containers have been updated before us, thus operations involving
1822 /// parent states (e.g. <see cref="DrawInfo"/>) should not be executed in an overridden implementation.
1823 /// </remarks>
1824 /// <param name="invalidation">The flags that the this <see cref="Drawable"/> was invalidated with.</param>
1825 /// <param name="source">The source that triggered the invalidation.</param>
1826 /// <returns>Whether any custom invalidation was performed. Must be true if the <see cref="DrawNode"/> for this <see cref="Drawable"/> is to be invalidated.</returns>
1827 protected virtual bool OnInvalidate(Invalidation invalidation, InvalidationSource source) => false;
1828
1829 public Invalidation InvalidationFromParentSize
1830 {
1831 get
1832 {
1833 Invalidation result = Invalidation.DrawInfo;
1834 if (RelativeSizeAxes != Axes.None)
1835 result |= Invalidation.DrawSize;
1836 if (RelativePositionAxes != Axes.None)
1837 result |= Invalidation.MiscGeometry;
1838 return result;
1839 }
1840 }
1841
1842 /// <summary>
1843 /// A fast path for invalidating ourselves and our parent's children size dependencies whenever a size or position change occurs.
1844 /// </summary>
1845 /// <param name="invalidation">The <see cref="Invalidation"/> to invalidate with.</param>
1846 /// <param name="changedAxes">The <see cref="Axes"/> that were affected.</param>
1847 private void invalidateParentSizeDependencies(Invalidation invalidation, Axes changedAxes)
1848 {
1849 // We're invalidating the parent manually, so we should not propagate it upwards.
1850 invalidate(invalidation, InvalidationSource.Self, false);
1851
1852 // The fast path, which performs an invalidation on the parent along with optimisations for bypassed sizing axes.
1853 Parent?.InvalidateChildrenSizeDependencies(invalidation, changedAxes, this);
1854 }
1855
1856 #endregion
1857
1858 #region DrawNode
1859
1860 private readonly DrawNode[] drawNodes = new DrawNode[GLWrapper.MAX_DRAW_NODES];
1861
1862 /// <summary>
1863 /// Generates the <see cref="DrawNode"/> for ourselves.
1864 /// </summary>
1865 /// <param name="frame">The frame which the <see cref="DrawNode"/> sub-tree should be generated for.</param>
1866 /// <param name="treeIndex">The index of the <see cref="DrawNode"/> to use.</param>
1867 /// <param name="forceNewDrawNode">Whether the creation of a new <see cref="DrawNode"/> should be forced, rather than re-using an existing <see cref="DrawNode"/>.</param>
1868 /// <returns>A complete and updated <see cref="DrawNode"/>, or null if the <see cref="DrawNode"/> would be invisible.</returns>
1869 internal virtual DrawNode GenerateDrawNodeSubtree(ulong frame, int treeIndex, bool forceNewDrawNode)
1870 {
1871 DrawNode node = drawNodes[treeIndex];
1872
1873 if (node == null || forceNewDrawNode)
1874 {
1875 drawNodes[treeIndex] = node = CreateDrawNode();
1876 FrameStatistics.Increment(StatisticsCounterType.DrawNodeCtor);
1877 }
1878
1879 if (InvalidationID != node.InvalidationID)
1880 {
1881 node.ApplyState();
1882 FrameStatistics.Increment(StatisticsCounterType.DrawNodeAppl);
1883 }
1884
1885 return node;
1886 }
1887
1888 /// <summary>
1889 /// Creates a draw node capable of containing all information required to draw this drawable.
1890 /// </summary>
1891 /// <returns>The created draw node.</returns>
1892 protected virtual DrawNode CreateDrawNode() => new DrawNode(this);
1893
1894 #endregion
1895
1896 #region DrawInfo-based coordinate system conversions
1897
1898 /// <summary>
1899 /// Accepts a vector in local coordinates and converts it to coordinates in another Drawable's space.
1900 /// </summary>
1901 /// <param name="input">A vector in local coordinates.</param>
1902 /// <param name="other">The drawable in which space we want to transform the vector to.</param>
1903 /// <returns>The vector in other's coordinates.</returns>
1904 public Vector2 ToSpaceOfOtherDrawable(Vector2 input, IDrawable other)
1905 {
1906 if (other == this)
1907 return input;
1908
1909 return Vector2Extensions.Transform(Vector2Extensions.Transform(input, DrawInfo.Matrix), other.DrawInfo.MatrixInverse);
1910 }
1911
1912 /// <summary>
1913 /// Accepts a rectangle in local coordinates and converts it to coordinates in another Drawable's space.
1914 /// </summary>
1915 /// <param name="input">A rectangle in local coordinates.</param>
1916 /// <param name="other">The drawable in which space we want to transform the rectangle to.</param>
1917 /// <returns>The rectangle in other's coordinates.</returns>
1918 public Quad ToSpaceOfOtherDrawable(RectangleF input, IDrawable other)
1919 {
1920 if (other == this)
1921 return input;
1922
1923 return Quad.FromRectangle(input) * (DrawInfo.Matrix * other.DrawInfo.MatrixInverse);
1924 }
1925
1926 /// <summary>
1927 /// Accepts a vector in local coordinates and converts it to coordinates in Parent's space.
1928 /// </summary>
1929 /// <param name="input">A vector in local coordinates.</param>
1930 /// <returns>The vector in Parent's coordinates.</returns>
1931 public Vector2 ToParentSpace(Vector2 input) => ToSpaceOfOtherDrawable(input, Parent);
1932
1933 /// <summary>
1934 /// Accepts a rectangle in local coordinates and converts it to a quad in Parent's space.
1935 /// </summary>
1936 /// <param name="input">A rectangle in local coordinates.</param>
1937 /// <returns>The quad in Parent's coordinates.</returns>
1938 public Quad ToParentSpace(RectangleF input) => ToSpaceOfOtherDrawable(input, Parent);
1939
1940 /// <summary>
1941 /// Accepts a vector in local coordinates and converts it to coordinates in screen space.
1942 /// </summary>
1943 /// <param name="input">A vector in local coordinates.</param>
1944 /// <returns>The vector in screen coordinates.</returns>
1945 public Vector2 ToScreenSpace(Vector2 input) => Vector2Extensions.Transform(input, DrawInfo.Matrix);
1946
1947 /// <summary>
1948 /// Accepts a rectangle in local coordinates and converts it to a quad in screen space.
1949 /// </summary>
1950 /// <param name="input">A rectangle in local coordinates.</param>
1951 /// <returns>The quad in screen coordinates.</returns>
1952 public Quad ToScreenSpace(RectangleF input) => Quad.FromRectangle(input) * DrawInfo.Matrix;
1953
1954 /// <summary>
1955 /// Accepts a vector in screen coordinates and converts it to coordinates in local space.
1956 /// </summary>
1957 /// <param name="screenSpacePos">A vector in screen coordinates.</param>
1958 /// <returns>The vector in local coordinates.</returns>
1959 public Vector2 ToLocalSpace(Vector2 screenSpacePos) => Vector2Extensions.Transform(screenSpacePos, DrawInfo.MatrixInverse);
1960
1961 /// <summary>
1962 /// Accepts a quad in screen coordinates and converts it to coordinates in local space.
1963 /// </summary>
1964 /// <param name="screenSpaceQuad">A quad in screen coordinates.</param>
1965 /// <returns>The quad in local coordinates.</returns>
1966 public Quad ToLocalSpace(Quad screenSpaceQuad) => screenSpaceQuad * DrawInfo.MatrixInverse;
1967
1968 #endregion
1969
1970 #region Interaction / Input
1971
1972 /// <summary>
1973 /// Handle a UI event.
1974 /// </summary>
1975 /// <param name="e">The event to be handled.</param>
1976 /// <returns>If the event supports blocking, returning true will make the event to not propagating further.</returns>
1977 protected virtual bool Handle(UIEvent e) => false;
1978
1979 /// <summary>
1980 /// Trigger a UI event with <see cref="UIEvent.Target"/> set to this <see cref="Drawable"/>.
1981 /// </summary>
1982 /// <param name="e">The event. Its <see cref="UIEvent.Target"/> will be modified.</param>
1983 /// <returns>The result of event handler.</returns>
1984 public bool TriggerEvent(UIEvent e)
1985 {
1986 e.Target = this;
1987
1988 switch (e)
1989 {
1990 case MouseMoveEvent mouseMove:
1991 return OnMouseMove(mouseMove);
1992
1993 case HoverEvent hover:
1994 return OnHover(hover);
1995
1996 case HoverLostEvent hoverLost:
1997 OnHoverLost(hoverLost);
1998 return false;
1999
2000 case MouseDownEvent mouseDown:
2001 return OnMouseDown(mouseDown);
2002
2003 case MouseUpEvent mouseUp:
2004 OnMouseUp(mouseUp);
2005 return false;
2006
2007 case ClickEvent click:
2008 return OnClick(click);
2009
2010 case DoubleClickEvent doubleClick:
2011 return OnDoubleClick(doubleClick);
2012
2013 case DragStartEvent dragStart:
2014 return OnDragStart(dragStart);
2015
2016 case DragEvent drag:
2017 OnDrag(drag);
2018 return false;
2019
2020 case DragEndEvent dragEnd:
2021 OnDragEnd(dragEnd);
2022 return false;
2023
2024 case ScrollEvent scroll:
2025 return OnScroll(scroll);
2026
2027 case FocusEvent focus:
2028 OnFocus(focus);
2029 return false;
2030
2031 case FocusLostEvent focusLost:
2032 OnFocusLost(focusLost);
2033 return false;
2034
2035 case KeyDownEvent keyDown:
2036 return OnKeyDown(keyDown);
2037
2038 case KeyUpEvent keyUp:
2039 OnKeyUp(keyUp);
2040 return false;
2041
2042 case TouchDownEvent touchDown:
2043 return OnTouchDown(touchDown);
2044
2045 case TouchMoveEvent touchMove:
2046 OnTouchMove(touchMove);
2047 return false;
2048
2049 case TouchUpEvent touchUp:
2050 OnTouchUp(touchUp);
2051 return false;
2052
2053 case JoystickPressEvent joystickPress:
2054 return OnJoystickPress(joystickPress);
2055
2056 case JoystickReleaseEvent joystickRelease:
2057 OnJoystickRelease(joystickRelease);
2058 return false;
2059
2060 case JoystickAxisMoveEvent joystickAxisMove:
2061 return OnJoystickAxisMove(joystickAxisMove);
2062
2063 case MidiDownEvent midiDown:
2064 return OnMidiDown(midiDown);
2065
2066 case MidiUpEvent midiUp:
2067 OnMidiUp(midiUp);
2068 return false;
2069
2070 case TabletPenButtonPressEvent tabletPenButtonPress:
2071 return OnTabletPenButtonPress(tabletPenButtonPress);
2072
2073 case TabletPenButtonReleaseEvent tabletPenButtonRelease:
2074 OnTabletPenButtonRelease(tabletPenButtonRelease);
2075 return false;
2076
2077 case TabletAuxiliaryButtonPressEvent tabletAuxiliaryButtonPress:
2078 return OnTabletAuxiliaryButtonPress(tabletAuxiliaryButtonPress);
2079
2080 case TabletAuxiliaryButtonReleaseEvent tabletAuxiliaryButtonRelease:
2081 OnTabletAuxiliaryButtonRelease(tabletAuxiliaryButtonRelease);
2082 return false;
2083
2084 default:
2085 return Handle(e);
2086 }
2087 }
2088
2089 [Obsolete("Use TriggerClick instead.")] // Can be removed 20220203
2090 public bool Click() => TriggerClick();
2091
2092 /// <summary>
2093 /// Triggers a left click event for this <see cref="Drawable"/>.
2094 /// </summary>
2095 /// <returns>Whether the click event is handled.</returns>
2096 public bool TriggerClick() => TriggerEvent(new ClickEvent(GetContainingInputManager()?.CurrentState ?? new InputState(), MouseButton.Left));
2097
2098 #region Individual event handlers
2099
2100 /// <summary>
2101 /// An event that occurs every time the mouse is moved while hovering this <see cref="Drawable"/>.
2102 /// </summary>
2103 /// <param name="e">The <see cref="MouseMoveEvent"/> containing information about the input event.</param>
2104 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2105 protected virtual bool OnMouseMove(MouseMoveEvent e) => Handle(e);
2106
2107 /// <summary>
2108 /// An event that occurs when the mouse starts hovering this <see cref="Drawable"/>.
2109 /// </summary>
2110 /// <param name="e">The <see cref="HoverEvent"/> containing information about the input event.</param>
2111 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2112 protected virtual bool OnHover(HoverEvent e) => Handle(e);
2113
2114 /// <summary>
2115 /// An event that occurs when the mouse stops hovering this <see cref="Drawable"/>.
2116 /// </summary>
2117 /// <remarks>
2118 /// This is guaranteed to be invoked if <see cref="OnHover"/> was invoked.
2119 /// </remarks>
2120 /// <param name="e">The <see cref="HoverLostEvent"/> containing information about the input event.</param>
2121 protected virtual void OnHoverLost(HoverLostEvent e) => Handle(e);
2122
2123 /// <summary>
2124 /// An event that occurs when a <see cref="MouseButton"/> is pressed on this <see cref="Drawable"/>.
2125 /// </summary>
2126 /// <param name="e">The <see cref="MouseDownEvent"/> containing information about the input event.</param>
2127 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2128 protected virtual bool OnMouseDown(MouseDownEvent e) => Handle(e);
2129
2130 /// <summary>
2131 /// An event that occurs when a <see cref="MouseButton"/> that was pressed on this <see cref="Drawable"/> is released.
2132 /// </summary>
2133 /// <remarks>
2134 /// This is guaranteed to be invoked if <see cref="OnMouseDown"/> was invoked.
2135 /// </remarks>
2136 /// <param name="e">The <see cref="MouseUpEvent"/> containing information about the input event.</param>
2137 protected virtual void OnMouseUp(MouseUpEvent e) => Handle(e);
2138
2139 /// <summary>
2140 /// An event that occurs when a <see cref="MouseButton"/> is clicked on this <see cref="Drawable"/>.
2141 /// </summary>
2142 /// <remarks>
2143 /// This will only be invoked on the <see cref="Drawable"/>s that received a previous <see cref="OnMouseDown"/> invocation
2144 /// which are still present in the input queue (via <see cref="BuildPositionalInputQueue"/>) when the click occurs.<br />
2145 /// This will not occur if a drag was started (<see cref="OnDragStart"/> was invoked) or a double-click occurred (<see cref="OnDoubleClick"/> was invoked).
2146 /// </remarks>
2147 /// <param name="e">The <see cref="ClickEvent"/> containing information about the input event.</param>
2148 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2149 protected virtual bool OnClick(ClickEvent e) => Handle(e);
2150
2151 /// <summary>
2152 /// An event that occurs when a <see cref="MouseButton"/> is double-clicked on this <see cref="Drawable"/>.
2153 /// </summary>
2154 /// <remarks>
2155 /// This will only be invoked on the <see cref="Drawable"/> that returned <code>true</code> from a previous <see cref="OnClick"/> invocation.
2156 /// </remarks>
2157 /// <param name="e">The <see cref="DoubleClickEvent"/> containing information about the input event.</param>
2158 /// <returns>Whether to block the next <see cref="OnClick"/> event from occurring.</returns>
2159 protected virtual bool OnDoubleClick(DoubleClickEvent e) => Handle(e);
2160
2161 /// <summary>
2162 /// An event that occurs when the mouse starts dragging on this <see cref="Drawable"/>.
2163 /// </summary>
2164 /// <param name="e">The <see cref="DragEvent"/> containing information about the input event.</param>
2165 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2166 protected virtual bool OnDragStart(DragStartEvent e) => Handle(e);
2167
2168 /// <summary>
2169 /// An event that occurs every time the mouse moves while dragging this <see cref="Drawable"/>.
2170 /// </summary>
2171 /// <remarks>
2172 /// This will only be invoked on the <see cref="Drawable"/> that returned <code>true</code> from a previous <see cref="OnDragStart"/> invocation.
2173 /// </remarks>
2174 /// <param name="e">The <see cref="DragEvent"/> containing information about the input event.</param>
2175 protected virtual void OnDrag(DragEvent e) => Handle(e);
2176
2177 /// <summary>
2178 /// An event that occurs when the mouse stops dragging this <see cref="Drawable"/>.
2179 /// </summary>
2180 /// <remarks>
2181 /// This will only be invoked on the <see cref="Drawable"/> that returned <code>true</code> from a previous <see cref="OnDragStart"/> invocation.
2182 /// </remarks>
2183 /// <param name="e">The <see cref="DragEndEvent"/> containing information about the input event.</param>
2184 protected virtual void OnDragEnd(DragEndEvent e) => Handle(e);
2185
2186 /// <summary>
2187 /// An event that occurs when the mouse wheel is scrolled on this <see cref="Drawable"/>.
2188 /// </summary>
2189 /// <param name="e">The <see cref="ScrollEvent"/> containing information about the input event.</param>
2190 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2191 protected virtual bool OnScroll(ScrollEvent e) => Handle(e);
2192
2193 /// <summary>
2194 /// An event that occurs when this <see cref="Drawable"/> gains focus.
2195 /// </summary>
2196 /// <remarks>
2197 /// This will only be invoked on the <see cref="Drawable"/> that returned <code>true</code> from both <see cref="AcceptsFocus"/> and a previous <see cref="OnClick"/> invocation.
2198 /// </remarks>
2199 /// <param name="e">The <see cref="FocusEvent"/> containing information about the input event.</param>
2200 protected virtual void OnFocus(FocusEvent e) => Handle(e);
2201
2202 /// <summary>
2203 /// An event that occurs when this <see cref="Drawable"/> loses focus.
2204 /// </summary>
2205 /// <remarks>
2206 /// This will only be invoked on the <see cref="Drawable"/> that previously had focus (<see cref="OnFocus"/> was invoked).
2207 /// </remarks>
2208 /// <param name="e">The <see cref="FocusLostEvent"/> containing information about the input event.</param>
2209 protected virtual void OnFocusLost(FocusLostEvent e) => Handle(e);
2210
2211 /// <summary>
2212 /// An event that occurs when a <see cref="Key"/> is pressed.
2213 /// </summary>
2214 /// <remarks>
2215 /// Repeat events can only be invoked on the <see cref="Drawable"/>s that received a previous non-repeat <see cref="OnKeyDown"/> invocation
2216 /// which are still present in the input queue (via <see cref="BuildNonPositionalInputQueue"/>) when the repeat occurs.
2217 /// </remarks>
2218 /// <param name="e">The <see cref="KeyDownEvent"/> containing information about the input event.</param>
2219 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2220 protected virtual bool OnKeyDown(KeyDownEvent e) => Handle(e);
2221
2222 /// <summary>
2223 /// An event that occurs when a <see cref="Key"/> is released.
2224 /// </summary>
2225 /// <remarks>
2226 /// This is guaranteed to be invoked if <see cref="OnKeyDown"/> was invoked.
2227 /// </remarks>
2228 /// <param name="e">The <see cref="KeyUpEvent"/> containing information about the input event.</param>
2229 protected virtual void OnKeyUp(KeyUpEvent e) => Handle(e);
2230
2231 /// <summary>
2232 /// An event that occurs when a <see cref="Touch"/> is active.
2233 /// </summary>
2234 /// <param name="e">The <see cref="TouchDownEvent"/> containing information about the input event.</param>
2235 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2236 protected virtual bool OnTouchDown(TouchDownEvent e) => Handle(e);
2237
2238 /// <summary>
2239 /// An event that occurs every time an active <see cref="Touch"/> has moved while hovering this <see cref="Drawable"/>.
2240 /// </summary>
2241 /// <remarks>
2242 /// This will only be invoked on the <see cref="Drawable"/>s that received a previous <see cref="OnTouchDown"/> invocation from the source of this touch.
2243 /// This will not occur if the touch has been activated then deactivated without moving from its initial position.
2244 /// </remarks>
2245 /// <param name="e">The <see cref="TouchMoveEvent"/> containing information about the input event.</param>
2246 protected virtual void OnTouchMove(TouchMoveEvent e) => Handle(e);
2247
2248 /// <summary>
2249 /// An event that occurs when a <see cref="Touch"/> is not active.
2250 /// </summary>
2251 /// <remarks>
2252 /// This is guaranteed to be invoked if <see cref="OnTouchDown"/> was invoked.
2253 /// </remarks>
2254 /// <param name="e">The <see cref="TouchUpEvent"/> containing information about the input event.</param>
2255 protected virtual void OnTouchUp(TouchUpEvent e) => Handle(e);
2256
2257 /// <summary>
2258 /// An event that occurs when a <see cref="JoystickButton"/> is pressed.
2259 /// </summary>
2260 /// <param name="e">The <see cref="JoystickPressEvent"/> containing information about the input event.</param>
2261 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2262 protected virtual bool OnJoystickPress(JoystickPressEvent e) => Handle(e);
2263
2264 /// <summary>
2265 /// An event that occurs when a <see cref="JoystickButton"/> is released.
2266 /// </summary>
2267 /// <remarks>
2268 /// This is guaranteed to be invoked if <see cref="OnJoystickPress"/> was invoked.
2269 /// </remarks>
2270 /// <param name="e">The <see cref="JoystickReleaseEvent"/> containing information about the input event.</param>
2271 protected virtual void OnJoystickRelease(JoystickReleaseEvent e) => Handle(e);
2272
2273 /// <summary>
2274 /// An event that occurs when a <see cref="JoystickAxis"/> is moved.
2275 /// </summary>
2276 /// <param name="e">The <see cref="JoystickAxisMoveEvent"/> containing information about the input event.</param>
2277 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2278 protected virtual bool OnJoystickAxisMove(JoystickAxisMoveEvent e) => Handle(e);
2279
2280 /// <summary>
2281 /// An event that occurs when a <see cref="MidiKey"/> is pressed.
2282 /// </summary>
2283 /// <param name="e">The <see cref="MidiDownEvent"/> containing information about the input event.</param>
2284 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2285 protected virtual bool OnMidiDown(MidiDownEvent e) => Handle(e);
2286
2287 /// <summary>
2288 /// An event that occurs when a <see cref="MidiKey"/> is released.
2289 /// </summary>
2290 /// <remarks>
2291 /// This is guaranteed to be invoked if <see cref="MidiDownEvent"/> was invoked.
2292 /// </remarks>
2293 /// <param name="e">The <see cref="MidiUpEvent"/> containing information about the input event.</param>
2294 protected virtual void OnMidiUp(MidiUpEvent e) => Handle(e);
2295
2296 /// <summary>
2297 /// An event that occurs when a <see cref="TabletPenButton"/> is pressed.
2298 /// </summary>
2299 /// <param name="e">The <see cref="TabletPenButtonPressEvent"/> containing information about the input event.</param>
2300 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2301 protected virtual bool OnTabletPenButtonPress(TabletPenButtonPressEvent e) => Handle(e);
2302
2303 /// <summary>
2304 /// An event that occurs when a <see cref="TabletPenButton"/> is released.
2305 /// </summary>
2306 /// <remarks>
2307 /// This is guaranteed to be invoked if <see cref="OnTabletPenButtonPress"/> was invoked.
2308 /// </remarks>
2309 /// <param name="e">The <see cref="TabletPenButtonReleaseEvent"/> containing information about the input event.</param>
2310 protected virtual void OnTabletPenButtonRelease(TabletPenButtonReleaseEvent e) => Handle(e);
2311
2312 /// <summary>
2313 /// An event that occurs when a <see cref="TabletAuxiliaryButton"/> is pressed.
2314 /// </summary>
2315 /// <param name="e">The <see cref="TabletAuxiliaryButtonPressEvent"/> containing information about the input event.</param>
2316 /// <returns>Whether to block the event from propagating to other <see cref="Drawable"/>s in the hierarchy.</returns>
2317 protected virtual bool OnTabletAuxiliaryButtonPress(TabletAuxiliaryButtonPressEvent e) => Handle(e);
2318
2319 /// <summary>
2320 /// An event that occurs when a <see cref="TabletAuxiliaryButton"/> is released.
2321 /// </summary>
2322 /// <remarks>
2323 /// This is guaranteed to be invoked if <see cref="OnTabletAuxiliaryButtonPress"/> was invoked.
2324 /// </remarks>
2325 /// <param name="e">The <see cref="TabletAuxiliaryButtonReleaseEvent"/> containing information about the input event.</param>
2326 protected virtual void OnTabletAuxiliaryButtonRelease(TabletAuxiliaryButtonReleaseEvent e) => Handle(e);
2327
2328 #endregion
2329
2330 /// <summary>
2331 /// 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.
2332 /// </summary>
2333 internal bool RequestsNonPositionalInput { get; private set; }
2334
2335 /// <summary>
2336 /// 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.
2337 /// </summary>
2338 internal bool RequestsPositionalInput { get; private set; }
2339
2340 /// <summary>
2341 /// Conservatively approximates whether there is a descendant which <see cref="RequestsNonPositionalInput"/> in the sub-tree rooted at this drawable
2342 /// to enable sub-tree skipping optimization for input handling.
2343 /// </summary>
2344 internal bool RequestsNonPositionalInputSubTree;
2345
2346 /// <summary>
2347 /// Conservatively approximates whether there is a descendant which <see cref="RequestsPositionalInput"/> in the sub-tree rooted at this drawable
2348 /// to enable sub-tree skipping optimization for input handling.
2349 /// </summary>
2350 internal bool RequestsPositionalInputSubTree;
2351
2352 /// <summary>
2353 /// Whether this <see cref="Drawable"/> handles non-positional input.
2354 /// This value is true by default if <see cref="Handle"/> or any non-positional (e.g. keyboard related) "On-" input methods are overridden.
2355 /// </summary>
2356 public virtual bool HandleNonPositionalInput => RequestsNonPositionalInput;
2357
2358 /// <summary>
2359 /// Whether this <see cref="Drawable"/> handles positional input.
2360 /// This value is true by default if <see cref="Handle"/> or any positional (i.e. mouse related) "On-" input methods are overridden.
2361 /// </summary>
2362 public virtual bool HandlePositionalInput => RequestsPositionalInput;
2363
2364 /// <summary>
2365 /// Nested class which is used for caching <see cref="Drawable.HandleNonPositionalInput"/>, <see cref="Drawable.HandlePositionalInput"/> values obtained via reflection.
2366 /// </summary>
2367 private static class HandleInputCache
2368 {
2369 private static readonly ConcurrentDictionary<Type, bool> positional_cached_values = new ConcurrentDictionary<Type, bool>();
2370 private static readonly ConcurrentDictionary<Type, bool> non_positional_cached_values = new ConcurrentDictionary<Type, bool>();
2371
2372 private static readonly string[] positional_input_methods =
2373 {
2374 nameof(Handle),
2375 nameof(OnMouseMove),
2376 nameof(OnHover),
2377 nameof(OnHoverLost),
2378 nameof(OnMouseDown),
2379 nameof(OnMouseUp),
2380 nameof(OnClick),
2381 nameof(OnDoubleClick),
2382 nameof(OnDragStart),
2383 nameof(OnDrag),
2384 nameof(OnDragEnd),
2385 nameof(OnScroll),
2386 nameof(OnFocus),
2387 nameof(OnFocusLost),
2388 nameof(OnTouchDown),
2389 nameof(OnTouchMove),
2390 nameof(OnTouchUp),
2391 nameof(OnTabletPenButtonPress),
2392 nameof(OnTabletPenButtonRelease)
2393 };
2394
2395 private static readonly string[] non_positional_input_methods =
2396 {
2397 nameof(Handle),
2398 nameof(OnFocus),
2399 nameof(OnFocusLost),
2400 nameof(OnKeyDown),
2401 nameof(OnKeyUp),
2402 nameof(OnJoystickPress),
2403 nameof(OnJoystickRelease),
2404 nameof(OnJoystickAxisMove),
2405 nameof(OnTabletAuxiliaryButtonPress),
2406 nameof(OnTabletAuxiliaryButtonRelease),
2407 nameof(OnMidiDown),
2408 nameof(OnMidiUp)
2409 };
2410
2411 private static readonly Type[] positional_input_interfaces =
2412 {
2413 typeof(IHasTooltip),
2414 typeof(IHasCustomTooltip),
2415 typeof(IHasContextMenu),
2416 typeof(IHasPopover),
2417 };
2418
2419 private static readonly Type[] non_positional_input_interfaces =
2420 {
2421 typeof(IKeyBindingHandler),
2422 };
2423
2424 private static readonly string[] positional_input_properties =
2425 {
2426 nameof(HandlePositionalInput),
2427 };
2428
2429 private static readonly string[] non_positional_input_properties =
2430 {
2431 nameof(HandleNonPositionalInput),
2432 nameof(AcceptsFocus),
2433 };
2434
2435 public static bool RequestsNonPositionalInput(Drawable drawable) => get(drawable, non_positional_cached_values, false);
2436
2437 public static bool RequestsPositionalInput(Drawable drawable) => get(drawable, positional_cached_values, true);
2438
2439 private static bool get(Drawable drawable, ConcurrentDictionary<Type, bool> cache, bool positional)
2440 {
2441 var type = drawable.GetType();
2442
2443 if (!cache.TryGetValue(type, out var value))
2444 {
2445 value = compute(type, positional);
2446 cache.TryAdd(type, value);
2447 }
2448
2449 return value;
2450 }
2451
2452 private static bool compute([NotNull] Type type, bool positional)
2453 {
2454 var inputMethods = positional ? positional_input_methods : non_positional_input_methods;
2455
2456 foreach (var inputMethod in inputMethods)
2457 {
2458 // check for any input method overrides which are at a higher level than drawable.
2459 var method = type.GetMethod(inputMethod, BindingFlags.Instance | BindingFlags.NonPublic);
2460
2461 Debug.Assert(method != null);
2462
2463 if (method.DeclaringType != typeof(Drawable))
2464 return true;
2465 }
2466
2467 var inputInterfaces = positional ? positional_input_interfaces : non_positional_input_interfaces;
2468
2469 foreach (var inputInterface in inputInterfaces)
2470 {
2471 // check if this type implements any interface which requires a drawable to handle input.
2472 if (inputInterface.IsAssignableFrom(type))
2473 return true;
2474 }
2475
2476 var inputProperties = positional ? positional_input_properties : non_positional_input_properties;
2477
2478 foreach (var inputProperty in inputProperties)
2479 {
2480 var property = type.GetProperty(inputProperty);
2481
2482 Debug.Assert(property != null);
2483
2484 if (property.DeclaringType != typeof(Drawable))
2485 return true;
2486 }
2487
2488 return false;
2489 }
2490 }
2491
2492 /// <summary>
2493 /// Check whether we have active focus.
2494 /// </summary>
2495 public bool HasFocus { get; internal set; }
2496
2497 /// <summary>
2498 /// If true, we are eagerly requesting focus. If nothing else above us has (or is requesting focus) we will get it.
2499 /// </summary>
2500 /// <remarks>In order to get focused, <see cref="HandleNonPositionalInput"/> must be true.</remarks>
2501 public virtual bool RequestsFocus => false;
2502
2503 /// <summary>
2504 /// If true, we will gain focus (receiving priority on keyboard input) (and receive an <see cref="OnFocus"/> event) on returning true in <see cref="OnClick"/>.
2505 /// </summary>
2506 public virtual bool AcceptsFocus => false;
2507
2508 /// <summary>
2509 /// Whether this Drawable is currently hovered over.
2510 /// </summary>
2511 /// <remarks>This is updated only if <see cref="HandlePositionalInput"/> is true.</remarks>
2512 public bool IsHovered { get; internal set; }
2513
2514 /// <summary>
2515 /// Whether this Drawable is currently being dragged.
2516 /// </summary>
2517 public bool IsDragged { get; internal set; }
2518
2519 /// <summary>
2520 /// Determines whether this drawable receives positional input when the mouse is at the
2521 /// given screen-space position.
2522 /// </summary>
2523 /// <param name="screenSpacePos">The screen-space position where input could be received.</param>
2524 /// <returns>True if input is received at the given screen-space position.</returns>
2525 public virtual bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Contains(screenSpacePos);
2526
2527 /// <summary>
2528 /// Computes whether a given screen-space position is contained within this drawable.
2529 /// Mouse input events are only received when this function is true, or when the drawable
2530 /// is in focus.
2531 /// </summary>
2532 /// <param name="screenSpacePos">The screen space position to be checked against this drawable.</param>
2533 public virtual bool Contains(Vector2 screenSpacePos) => DrawRectangle.Contains(ToLocalSpace(screenSpacePos));
2534
2535 /// <summary>
2536 /// Whether non-positional input should be propagated to the sub-tree rooted at this drawable.
2537 /// </summary>
2538 public virtual bool PropagateNonPositionalInputSubTree => IsPresent && RequestsNonPositionalInputSubTree;
2539
2540 /// <summary>
2541 /// Whether positional input should be propagated to the sub-tree rooted at this drawable.
2542 /// </summary>
2543 public virtual bool PropagatePositionalInputSubTree => IsPresent && RequestsPositionalInputSubTree && !IsMaskedAway;
2544
2545 /// <summary>
2546 /// Whether clicks should be blocked when this drawable is in a dragged state.
2547 /// </summary>
2548 /// <remarks>
2549 /// This is queried when a click is to be actuated.
2550 /// </remarks>
2551 public virtual bool DragBlocksClick => true;
2552
2553 /// <summary>
2554 /// This method is responsible for building a queue of Drawables to receive non-positional input in reverse order.
2555 /// </summary>
2556 /// <param name="queue">The input queue to be built.</param>
2557 /// <param name="allowBlocking">Whether blocking at <see cref="PassThroughInputManager"/>s should be allowed.</param>
2558 /// <returns>Returns false if we should skip this sub-tree.</returns>
2559 internal virtual bool BuildNonPositionalInputQueue(List<Drawable> queue, bool allowBlocking = true)
2560 {
2561 if (!PropagateNonPositionalInputSubTree)
2562 return false;
2563
2564 if (HandleNonPositionalInput)
2565 queue.Add(this);
2566
2567 return true;
2568 }
2569
2570 /// <summary>
2571 /// This method is responsible for building a queue of Drawables to receive positional input in reverse order.
2572 /// </summary>
2573 /// <param name="screenSpacePos">The screen space position of the positional input.</param>
2574 /// <param name="queue">The input queue to be built.</param>
2575 /// <returns>Returns false if we should skip this sub-tree.</returns>
2576 internal virtual bool BuildPositionalInputQueue(Vector2 screenSpacePos, List<Drawable> queue)
2577 {
2578 if (!PropagatePositionalInputSubTree)
2579 return false;
2580
2581 if (HandlePositionalInput && ReceivePositionalInputAt(screenSpacePos))
2582 queue.Add(this);
2583
2584 return true;
2585 }
2586
2587 internal sealed override void EnsureTransformMutationAllowed() => EnsureMutationAllowed(nameof(Transforms));
2588
2589 /// <summary>
2590 /// Check whether the current thread is valid for operating on thread-safe properties.
2591 /// </summary>
2592 /// <param name="member">The member to be operated on, used only for describing failures in exception messages.</param>
2593 /// <exception cref="InvalidThreadForMutationException">If the current thread is not valid.</exception>
2594 internal void EnsureMutationAllowed(string member)
2595 {
2596 switch (LoadState)
2597 {
2598 case LoadState.NotLoaded:
2599 break;
2600
2601 case LoadState.Loading:
2602 if (Thread.CurrentThread != LoadThread)
2603 throw new InvalidThreadForMutationException(LoadState, member, "not on the load thread");
2604
2605 break;
2606
2607 case LoadState.Ready:
2608 // Allow mutating from the load thread since parenting containers may still be in the loading state
2609 if (Thread.CurrentThread != LoadThread && !ThreadSafety.IsUpdateThread)
2610 throw new InvalidThreadForMutationException(LoadState, member, "not on the load or update threads");
2611
2612 break;
2613
2614 case LoadState.Loaded:
2615 if (!ThreadSafety.IsUpdateThread)
2616 throw new InvalidThreadForMutationException(LoadState, member, "not on the update thread");
2617
2618 break;
2619 }
2620 }
2621
2622 #endregion
2623
2624 #region Transforms
2625
2626 protected internal ScheduledDelegate Schedule(Action action) => Scheduler.AddDelayed(action, TransformDelay);
2627
2628 /// <summary>
2629 /// Make this drawable automatically clean itself up after all transforms have finished playing.
2630 /// Can be delayed using Delay().
2631 /// </summary>
2632 public void Expire(bool calculateLifetimeStart = false)
2633 {
2634 if (clock == null)
2635 {
2636 LifetimeEnd = double.MinValue;
2637 return;
2638 }
2639
2640 LifetimeEnd = LatestTransformEndTime;
2641
2642 if (calculateLifetimeStart)
2643 {
2644 double min = double.MaxValue;
2645
2646 foreach (Transform t in Transforms)
2647 {
2648 if (t.StartTime < min)
2649 min = t.StartTime;
2650 }
2651
2652 LifetimeStart = min < int.MaxValue ? min : int.MinValue;
2653 }
2654 }
2655
2656 /// <summary>
2657 /// Hide sprite instantly.
2658 /// </summary>
2659 public virtual void Hide() => this.FadeOut();
2660
2661 /// <summary>
2662 /// Show sprite instantly.
2663 /// </summary>
2664 public virtual void Show() => this.FadeIn();
2665
2666 #endregion
2667
2668 #region Effects
2669
2670 /// <summary>
2671 /// Returns the drawable created by applying the given effect to this drawable. This method may add this drawable to a container.
2672 /// If this drawable should be the child of another container, make sure to add the created drawable to the container instead of this drawable.
2673 /// </summary>
2674 /// <typeparam name="T">The type of the drawable that results from applying the given effect.</typeparam>
2675 /// <param name="effect">The effect to apply to this drawable.</param>
2676 /// <param name="initializationAction">The action that should get called to initialize the created drawable before it is returned.</param>
2677 /// <returns>The drawable created by applying the given effect to this drawable.</returns>
2678 public T WithEffect<T>(IEffect<T> effect, Action<T> initializationAction = null) where T : Drawable
2679 {
2680 var result = effect.ApplyTo(this);
2681 initializationAction?.Invoke(result);
2682 return result;
2683 }
2684
2685 #endregion
2686
2687 /// <summary>
2688 /// A name used to identify this Drawable internally.
2689 /// </summary>
2690 public string Name = string.Empty;
2691
2692 public override string ToString()
2693 {
2694 string shortClass = GetType().ReadableName();
2695
2696 if (!string.IsNullOrEmpty(Name))
2697 return $@"{Name} ({shortClass})";
2698 else
2699 return shortClass;
2700 }
2701
2702 /// <summary>
2703 /// Creates a new instance of an empty <see cref="Drawable"/>.
2704 /// </summary>
2705 public static Drawable Empty() => new EmptyDrawable();
2706
2707 private class EmptyDrawable : Drawable
2708 {
2709 }
2710
2711 public class InvalidThreadForMutationException : InvalidOperationException
2712 {
2713 public InvalidThreadForMutationException(LoadState loadState, string member, string invalidThreadContextDescription)
2714 : base($"Cannot mutate the {member} of a {loadState} {nameof(Drawable)} while {invalidThreadContextDescription}. "
2715 + $"Consider using {nameof(Schedule)} to schedule the mutation operation.")
2716 {
2717 }
2718 }
2719 }
2720
2721 /// <summary>
2722 /// Specifies which type of properties are being invalidated.
2723 /// </summary>
2724 [Flags]
2725 public enum Invalidation
2726 {
2727 /// <summary>
2728 /// <see cref="Drawable.DrawInfo"/> has changed. No change to <see cref="Drawable.RequiredParentSizeToFit"/> or <see cref="Drawable.DrawSize"/>
2729 /// is assumed unless indicated by additional flags.
2730 /// </summary>
2731 DrawInfo = 1,
2732
2733 /// <summary>
2734 /// <see cref="Drawable.DrawSize"/> has changed.
2735 /// </summary>
2736 DrawSize = 1 << 1,
2737
2738 /// <summary>
2739 /// Captures all other geometry changes than <see cref="Drawable.DrawSize"/>, such as
2740 /// <see cref="Drawable.Rotation"/>, <see cref="Drawable.Shear"/>, and <see cref="Drawable.DrawPosition"/>.
2741 /// </summary>
2742 MiscGeometry = 1 << 2,
2743
2744 /// <summary>
2745 /// <see cref="Drawable.Colour"/> has changed.
2746 /// </summary>
2747 Colour = 1 << 3,
2748
2749 /// <summary>
2750 /// <see cref="Graphics.DrawNode.ApplyState"/> has to be invoked on all old draw nodes.
2751 /// This <see cref="Invalidation"/> flag never propagates to children.
2752 /// </summary>
2753 DrawNode = 1 << 4,
2754
2755 /// <summary>
2756 /// <see cref="Drawable.IsPresent"/> has changed.
2757 /// </summary>
2758 Presence = 1 << 5,
2759
2760 /// <summary>
2761 /// A <see cref="Drawable.Parent"/> has changed.
2762 /// Unlike other <see cref="Invalidation"/> flags, this propagates to all children regardless of their <see cref="Drawable.IsAlive"/> state.
2763 /// </summary>
2764 Parent = 1 << 6,
2765
2766 /// <summary>
2767 /// No invalidation.
2768 /// </summary>
2769 None = 0,
2770
2771 /// <summary>
2772 /// <see cref="Drawable.RequiredParentSizeToFit"/> has to be recomputed.
2773 /// </summary>
2774 RequiredParentSizeToFit = MiscGeometry | DrawSize,
2775
2776 /// <summary>
2777 /// All possible things are affected.
2778 /// </summary>
2779 All = DrawNode | RequiredParentSizeToFit | Colour | DrawInfo | Presence,
2780
2781 /// <summary>
2782 /// Only the layout flags.
2783 /// </summary>
2784 Layout = All & ~(DrawNode | Parent)
2785 }
2786
2787 /// <summary>
2788 /// General enum to specify an "anchor" or "origin" point from the standard 9 points on a rectangle.
2789 /// x and y counterparts can be accessed using bitwise flags.
2790 /// </summary>
2791 [Flags]
2792 public enum Anchor
2793 {
2794 TopLeft = y0 | x0,
2795 TopCentre = y0 | x1,
2796 TopRight = y0 | x2,
2797
2798 CentreLeft = y1 | x0,
2799 Centre = y1 | x1,
2800 CentreRight = y1 | x2,
2801
2802 BottomLeft = y2 | x0,
2803 BottomCentre = y2 | x1,
2804 BottomRight = y2 | x2,
2805
2806 /// <summary>
2807 /// The vertical counterpart is at "Top" position.
2808 /// </summary>
2809 y0 = 1,
2810
2811 /// <summary>
2812 /// The vertical counterpart is at "Centre" position.
2813 /// </summary>
2814 y1 = 1 << 1,
2815
2816 /// <summary>
2817 /// The vertical counterpart is at "Bottom" position.
2818 /// </summary>
2819 y2 = 1 << 2,
2820
2821 /// <summary>
2822 /// The horizontal counterpart is at "Left" position.
2823 /// </summary>
2824 x0 = 1 << 3,
2825
2826 /// <summary>
2827 /// The horizontal counterpart is at "Centre" position.
2828 /// </summary>
2829 x1 = 1 << 4,
2830
2831 /// <summary>
2832 /// The horizontal counterpart is at "Right" position.
2833 /// </summary>
2834 x2 = 1 << 5,
2835
2836 /// <summary>
2837 /// The user is manually updating the outcome, so we shouldn't.
2838 /// </summary>
2839 Custom = 1 << 6,
2840 }
2841
2842 [Flags]
2843 public enum Axes
2844 {
2845 None = 0,
2846
2847 X = 1,
2848 Y = 1 << 1,
2849
2850 Both = X | Y,
2851 }
2852
2853 [Flags]
2854 public enum Edges
2855 {
2856 None = 0,
2857
2858 Top = 1,
2859 Left = 1 << 1,
2860 Bottom = 1 << 2,
2861 Right = 1 << 3,
2862
2863 Horizontal = Left | Right,
2864 Vertical = Top | Bottom,
2865
2866 All = Top | Left | Bottom | Right,
2867 }
2868
2869 public enum Direction
2870 {
2871 Horizontal,
2872 Vertical,
2873 }
2874
2875 public enum RotationDirection
2876 {
2877 [Description("Clockwise")]
2878 Clockwise,
2879
2880 [Description("Counterclockwise")]
2881 Counterclockwise,
2882 }
2883
2884 /// <summary>
2885 /// Possible states of a <see cref="Drawable"/> within the loading pipeline.
2886 /// </summary>
2887 public enum LoadState
2888 {
2889 /// <summary>
2890 /// Not loaded, and no load has been initiated yet.
2891 /// </summary>
2892 NotLoaded,
2893
2894 /// <summary>
2895 /// Currently loading (possibly and usually on a background thread via <see cref="CompositeDrawable.LoadComponentAsync{TLoadable}"/>).
2896 /// </summary>
2897 Loading,
2898
2899 /// <summary>
2900 /// Loading is complete, but has not yet been finalized on the update thread
2901 /// (<see cref="Drawable.LoadComplete"/> has not been called yet, which
2902 /// always runs on the update thread and requires <see cref="Drawable.IsAlive"/>).
2903 /// </summary>
2904 Ready,
2905
2906 /// <summary>
2907 /// Loading is fully completed and the Drawable is now part of the scene graph.
2908 /// </summary>
2909 Loaded
2910 }
2911
2912 /// <summary>
2913 /// Controls the behavior of <see cref="Drawable.RelativeSizeAxes"/> when it is set to <see cref="Axes.Both"/>.
2914 /// </summary>
2915 public enum FillMode
2916 {
2917 /// <summary>
2918 /// Completely fill the parent with a relative size of 1 at the cost of stretching the aspect ratio (default).
2919 /// </summary>
2920 Stretch,
2921
2922 /// <summary>
2923 /// Always maintains aspect ratio while filling the portion of the parent's size denoted by the relative size.
2924 /// A relative size of 1 results in completely filling the parent by scaling the smaller axis of the drawable to fill the parent.
2925 /// </summary>
2926 Fill,
2927
2928 /// <summary>
2929 /// Always maintains aspect ratio while fitting into the portion of the parent's size denoted by the relative size.
2930 /// 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.
2931 /// </summary>
2932 Fit,
2933 }
2934}