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 osu.Framework.Lists;
5using System.Collections.Generic;
6using System;
7using System.Diagnostics;
8using System.Linq;
9using System.Runtime.ExceptionServices;
10using System.Threading;
11using osuTK;
12using osuTK.Graphics;
13using osu.Framework.Graphics.Shaders;
14using osu.Framework.Extensions.IEnumerableExtensions;
15using osu.Framework.Graphics.Colour;
16using osu.Framework.Allocation;
17using osu.Framework.Graphics.Transforms;
18using osu.Framework.Timing;
19using osu.Framework.Threading;
20using osu.Framework.Statistics;
21using System.Threading.Tasks;
22using JetBrains.Annotations;
23using osu.Framework.Development;
24using osu.Framework.Extensions.EnumExtensions;
25using osu.Framework.Extensions.ExceptionExtensions;
26using osu.Framework.Graphics.Effects;
27using osu.Framework.Graphics.Primitives;
28using osu.Framework.Layout;
29using osu.Framework.Testing;
30using osu.Framework.Utils;
31
32namespace osu.Framework.Graphics.Containers
33{
34 /// <summary>
35 /// A drawable consisting of a composite of child drawables which are
36 /// manages by the composite object itself. Transformations applied to
37 /// a <see cref="CompositeDrawable"/> are also applied to its children.
38 /// Additionally, <see cref="CompositeDrawable"/>s support various effects, such as masking, edge effect,
39 /// padding, and automatic sizing depending on their children.
40 /// </summary>
41 [ExcludeFromDynamicCompile]
42 public abstract partial class CompositeDrawable : Drawable
43 {
44 #region Construction and disposal
45
46 /// <summary>
47 /// Constructs a <see cref="CompositeDrawable"/> that stores children.
48 /// </summary>
49 protected CompositeDrawable()
50 {
51 var childComparer = new ChildComparer(this);
52
53 internalChildren = new SortedList<Drawable>(childComparer);
54 aliveInternalChildren = new SortedList<Drawable>(childComparer);
55
56 AddLayout(childrenSizeDependencies);
57 }
58
59 [Resolved]
60 private Game game { get; set; }
61
62 /// <summary>
63 /// Create a local dependency container which will be used by our nested children.
64 /// If not overridden, the load-time parent's dependency tree will be used.
65 /// </summary>
66 /// <param name="parent">The parent <see cref="IReadOnlyDependencyContainer"/> which should be passed through if we want fallback lookups to work.</param>
67 /// <returns>A new dependency container to be stored for this Drawable.</returns>
68 protected virtual IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => DependencyActivator.MergeDependencies(this, parent);
69
70 /// <summary>
71 /// Contains all dependencies that can be injected into this CompositeDrawable's children using <see cref="BackgroundDependencyLoaderAttribute"/>.
72 /// Add or override dependencies by calling <see cref="DependencyContainer.Cache(object)"/>.
73 /// </summary>
74 public IReadOnlyDependencyContainer Dependencies { get; private set; }
75
76 protected sealed override void InjectDependencies(IReadOnlyDependencyContainer dependencies)
77 {
78 // get our dependencies from our parent, but allow local overriding of our inherited dependency container
79 Dependencies = CreateChildDependencies(dependencies);
80
81 base.InjectDependencies(dependencies);
82 }
83
84 private CancellationTokenSource disposalCancellationSource;
85
86 private WeakList<Drawable> loadingComponents;
87
88 private static readonly ThreadedTaskScheduler threaded_scheduler = new ThreadedTaskScheduler(4, nameof(LoadComponentsAsync));
89
90 private static readonly ThreadedTaskScheduler long_load_scheduler = new ThreadedTaskScheduler(4, nameof(LoadComponentsAsync));
91
92 /// <summary>
93 /// Loads a future child or grand-child of this <see cref="CompositeDrawable"/> asynchronously. <see cref="Dependencies"/>
94 /// and <see cref="Drawable.Clock"/> are inherited from this <see cref="CompositeDrawable"/>.
95 ///
96 /// Note that this will always use the dependencies and clock from this instance. If you must load to a nested container level,
97 /// consider using <see cref="DelayedLoadWrapper"/>
98 /// </summary>
99 /// <typeparam name="TLoadable">The type of the future future child or grand-child to be loaded.</typeparam>
100 /// <param name="component">The child or grand-child to be loaded.</param>
101 /// <param name="onLoaded">Callback to be invoked on the update thread after loading is complete.</param>
102 /// <param name="cancellation">An optional cancellation token.</param>
103 /// <param name="scheduler">The scheduler for <paramref name="onLoaded"/> to be invoked on. If null, the local scheduler will be used.</param>
104 /// <returns>The task which is used for loading and callbacks.</returns>
105 protected internal Task LoadComponentAsync<TLoadable>([NotNull] TLoadable component, Action<TLoadable> onLoaded = null, CancellationToken cancellation = default, Scheduler scheduler = null)
106 where TLoadable : Drawable
107 {
108 if (component == null) throw new ArgumentNullException(nameof(component));
109
110 return LoadComponentsAsync(component.Yield(), l => onLoaded?.Invoke(l.Single()), cancellation, scheduler);
111 }
112
113 /// <summary>
114 /// Loads a future child or grand-child of this <see cref="CompositeDrawable"/> synchronously and immediately. <see cref="Dependencies"/>
115 /// and <see cref="Drawable.Clock"/> are inherited from this <see cref="CompositeDrawable"/>.
116 /// <remarks>
117 /// This is generally useful if already in an asynchronous context and requiring forcefully (pre)loading content without adding it to the hierarchy.
118 /// </remarks>
119 /// </summary>
120 /// <typeparam name="TLoadable">The type of the future future child or grand-child to be loaded.</typeparam>
121 /// <param name="component">The child or grand-child to be loaded.</param>
122 protected void LoadComponent<TLoadable>(TLoadable component) where TLoadable : Drawable
123 => LoadComponents(component.Yield());
124
125 /// <summary>
126 /// Loads several future child or grand-child of this <see cref="CompositeDrawable"/> asynchronously. <see cref="Dependencies"/>
127 /// and <see cref="Drawable.Clock"/> are inherited from this <see cref="CompositeDrawable"/>.
128 ///
129 /// Note that this will always use the dependencies and clock from this instance. If you must load to a nested container level,
130 /// consider using <see cref="DelayedLoadWrapper"/>
131 /// </summary>
132 /// <typeparam name="TLoadable">The type of the future future child or grand-child to be loaded.</typeparam>
133 /// <param name="components">The children or grand-children to be loaded.</param>
134 /// <param name="onLoaded">Callback to be invoked on the update thread after loading is complete.</param>
135 /// <param name="cancellation">An optional cancellation token.</param>
136 /// <param name="scheduler">The scheduler for <paramref name="onLoaded"/> to be invoked on. If null, the local scheduler will be used.</param>
137 /// <returns>The task which is used for loading and callbacks.</returns>
138 protected internal Task LoadComponentsAsync<TLoadable>(IEnumerable<TLoadable> components, Action<IEnumerable<TLoadable>> onLoaded = null, CancellationToken cancellation = default,
139 Scheduler scheduler = null)
140 where TLoadable : Drawable
141 {
142 if (game == null)
143 throw new InvalidOperationException($"May not invoke {nameof(LoadComponentAsync)} prior to this {nameof(CompositeDrawable)} being loaded.");
144
145 if (IsDisposed)
146 throw new ObjectDisposedException(ToString());
147
148 disposalCancellationSource ??= new CancellationTokenSource();
149
150 var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(disposalCancellationSource.Token, cancellation);
151
152 var deps = new DependencyContainer(Dependencies);
153 deps.CacheValueAs(linkedSource.Token);
154
155 loadingComponents ??= new WeakList<Drawable>();
156
157 var loadables = components.ToList();
158
159 foreach (var d in loadables)
160 {
161 loadingComponents.Add(d);
162 d.OnLoadComplete += _ => loadingComponents.Remove(d);
163 }
164
165 var taskScheduler = loadables.Any(c => c.IsLongRunning) ? long_load_scheduler : threaded_scheduler;
166
167 return Task.Factory.StartNew(() => loadComponents(loadables, deps, true, linkedSource.Token), linkedSource.Token, TaskCreationOptions.HideScheduler, taskScheduler).ContinueWith(loaded =>
168 {
169 var exception = loaded.Exception?.AsSingular();
170
171 if (loadables.Count == 0)
172 return;
173
174 if (linkedSource.Token.IsCancellationRequested)
175 {
176 linkedSource.Dispose();
177 return;
178 }
179
180 (scheduler ?? Scheduler).Add(() =>
181 {
182 try
183 {
184 if (exception != null)
185 ExceptionDispatchInfo.Capture(exception).Throw();
186
187 if (!linkedSource.Token.IsCancellationRequested)
188 onLoaded?.Invoke(loadables);
189 }
190 finally
191 {
192 linkedSource.Dispose();
193 }
194 });
195 }, CancellationToken.None);
196 }
197
198 /// <summary>
199 /// Loads several future child or grand-child of this <see cref="CompositeDrawable"/> synchronously and immediately. <see cref="Dependencies"/>
200 /// and <see cref="Drawable.Clock"/> are inherited from this <see cref="CompositeDrawable"/>.
201 /// <remarks>
202 /// This is generally useful if already in an asynchronous context and requiring forcefully (pre)loading content without adding it to the hierarchy.
203 /// </remarks>
204 /// </summary>
205 /// <typeparam name="TLoadable">The type of the future future child or grand-child to be loaded.</typeparam>
206 /// <param name="components">The children or grand-children to be loaded.</param>
207 protected void LoadComponents<TLoadable>(IEnumerable<TLoadable> components) where TLoadable : Drawable
208 {
209 if (game == null)
210 throw new InvalidOperationException($"May not invoke {nameof(LoadComponent)} prior to this {nameof(CompositeDrawable)} being loaded.");
211
212 if (IsDisposed)
213 throw new ObjectDisposedException(ToString());
214
215 loadComponents(components.ToList(), Dependencies, false);
216 }
217
218 /// <summary>
219 /// Load the provided components. Any components which could not be loaded will be removed from the provided list.
220 /// </summary>
221 private void loadComponents<TLoadable>(List<TLoadable> components, IReadOnlyDependencyContainer dependencies, bool isDirectAsyncContext, CancellationToken cancellation = default)
222 where TLoadable : Drawable
223 {
224 for (var i = 0; i < components.Count; i++)
225 {
226 if (cancellation.IsCancellationRequested)
227 break;
228
229 if (!components[i].LoadFromAsync(Clock, dependencies, isDirectAsyncContext))
230 components.Remove(components[i--]);
231 }
232 }
233
234 [BackgroundDependencyLoader(true)]
235 private void load(ShaderManager shaders, CancellationToken? cancellation)
236 {
237 hasCustomDrawNode = GetType().GetMethod(nameof(CreateDrawNode))?.DeclaringType != typeof(CompositeDrawable);
238
239 Shader ??= shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
240
241 // We are in a potentially async context, so let's aggressively load all our children
242 // regardless of their alive state. this also gives children a clock so they can be checked
243 // for their correct alive state in the case LifetimeStart is set to a definite value.
244 foreach (var c in internalChildren)
245 {
246 cancellation?.ThrowIfCancellationRequested();
247 loadChild(c);
248 }
249 }
250
251 protected override void LoadAsyncComplete()
252 {
253 base.LoadAsyncComplete();
254
255 // At this point we can assume that we are loaded although we're not in the "ready" state, because we'll be given
256 // a "ready" state soon after this method terminates. Therefore we can perform an early check to add any alive children
257 // while we're still in an asynchronous context and avoid putting pressure on the main thread during UpdateSubTree.
258 CheckChildrenLife();
259 }
260
261 /// <summary>
262 /// Loads a <see cref="Drawable"/> child. This will not throw in the event of the load being cancelled.
263 /// </summary>
264 /// <param name="child">The <see cref="Drawable"/> child to load.</param>
265 private void loadChild(Drawable child)
266 {
267 try
268 {
269 if (IsDisposed)
270 throw new ObjectDisposedException(ToString(), "Disposed Drawables may not have children added.");
271
272 child.Load(Clock, Dependencies, false);
273
274 child.Parent = this;
275 }
276 catch (OperationCanceledException)
277 {
278 }
279 catch (AggregateException ae)
280 {
281 foreach (var e in ae.Flatten().InnerExceptions)
282 {
283 if (e is OperationCanceledException)
284 continue;
285
286 ExceptionDispatchInfo.Capture(e).Throw();
287 }
288 }
289 }
290
291 protected override void Dispose(bool isDisposing)
292 {
293 if (IsDisposed)
294 return;
295
296 disposalCancellationSource?.Cancel();
297 disposalCancellationSource?.Dispose();
298
299 InternalChildren?.ForEach(c => c.Dispose());
300
301 if (loadingComponents != null)
302 {
303 foreach (var d in loadingComponents)
304 d.Dispose();
305 }
306
307 OnAutoSize = null;
308 Dependencies = null;
309 schedulerAfterChildren = null;
310
311 base.Dispose(isDisposing);
312 }
313
314 #endregion
315
316 #region Children management
317
318 /// <summary>
319 /// Invoked when a child has entered <see cref="AliveInternalChildren"/>.
320 /// </summary>
321 internal event Action<Drawable> ChildBecameAlive;
322
323 /// <summary>
324 /// Invoked when a child has left <see cref="AliveInternalChildren"/>.
325 /// </summary>
326 internal event Action<Drawable> ChildDied;
327
328 /// <summary>
329 /// Fired after a child's <see cref="Drawable.Depth"/> is changed.
330 /// </summary>
331 internal event Action<Drawable> ChildDepthChanged;
332
333 /// <summary>
334 /// Gets or sets the only child in <see cref="InternalChildren"/>.
335 /// </summary>
336 [DebuggerBrowsable(DebuggerBrowsableState.Never)]
337 protected internal Drawable InternalChild
338 {
339 get
340 {
341 if (InternalChildren.Count != 1)
342 throw new InvalidOperationException($"Cannot call {nameof(InternalChild)} unless there's exactly one {nameof(Drawable)} in {nameof(InternalChildren)} (currently {InternalChildren.Count})!");
343
344 return InternalChildren[0];
345 }
346 set
347 {
348 ClearInternal();
349 AddInternal(value);
350 }
351 }
352
353 protected class ChildComparer : IComparer<Drawable>
354 {
355 private readonly CompositeDrawable owner;
356
357 public ChildComparer(CompositeDrawable owner)
358 {
359 this.owner = owner;
360 }
361
362 public int Compare(Drawable x, Drawable y) => owner.Compare(x, y);
363 }
364
365 /// <summary>
366 /// Compares two <see cref="InternalChildren"/> to determine their sorting.
367 /// </summary>
368 /// <param name="x">The first child to compare.</param>
369 /// <param name="y">The second child to compare.</param>
370 /// <returns>-1 if <paramref name="x"/> comes before <paramref name="y"/>, and 1 otherwise.</returns>
371 protected virtual int Compare(Drawable x, Drawable y)
372 {
373 if (x == null) throw new ArgumentNullException(nameof(x));
374 if (y == null) throw new ArgumentNullException(nameof(y));
375
376 int i = y.Depth.CompareTo(x.Depth);
377 if (i != 0) return i;
378
379 return x.ChildID.CompareTo(y.ChildID);
380 }
381
382 /// <summary>
383 /// Helper method comparing children by their depth first, and then by their reversed child ID.
384 /// </summary>
385 /// <param name="x">The first child to compare.</param>
386 /// <param name="y">The second child to compare.</param>
387 /// <returns>-1 if <paramref name="x"/> comes before <paramref name="y"/>, and 1 otherwise.</returns>
388 protected int CompareReverseChildID(Drawable x, Drawable y)
389 {
390 if (x == null) throw new ArgumentNullException(nameof(x));
391 if (y == null) throw new ArgumentNullException(nameof(y));
392
393 int i = y.Depth.CompareTo(x.Depth);
394 if (i != 0) return i;
395
396 return y.ChildID.CompareTo(x.ChildID);
397 }
398
399 private readonly SortedList<Drawable> internalChildren;
400
401 /// <summary>
402 /// This <see cref="CompositeDrawable"/> list of children. Assigning to this property will dispose all existing children of this <see cref="CompositeDrawable"/>.
403 /// </summary>
404 protected internal IReadOnlyList<Drawable> InternalChildren
405 {
406 get => internalChildren;
407 set => InternalChildrenEnumerable = value;
408 }
409
410 /// <summary>
411 /// Replaces all internal children of this <see cref="CompositeDrawable"/> with the elements contained in the enumerable.
412 /// </summary>
413 protected internal IEnumerable<Drawable> InternalChildrenEnumerable
414 {
415 set
416 {
417 ClearInternal();
418 AddRangeInternal(value);
419 }
420 }
421
422 private readonly SortedList<Drawable> aliveInternalChildren;
423 protected internal IReadOnlyList<Drawable> AliveInternalChildren => aliveInternalChildren;
424
425 /// <summary>
426 /// The index of a given child within <see cref="InternalChildren"/>.
427 /// </summary>
428 /// <returns>
429 /// If the child is found, its index. Otherwise, the negated index it would obtain
430 /// if it were added to <see cref="InternalChildren"/>.
431 /// </returns>
432 protected internal int IndexOfInternal(Drawable drawable)
433 {
434 int index = internalChildren.IndexOf(drawable);
435
436 if (index >= 0 && internalChildren[index].ChildID != drawable.ChildID)
437 throw new InvalidOperationException($@"A non-matching {nameof(Drawable)} was returned. Please ensure {GetType()}'s {nameof(Compare)} override implements a stable sort algorithm.");
438
439 return index;
440 }
441
442 /// <summary>
443 /// Checks whether a given child is contained within <see cref="InternalChildren"/>.
444 /// </summary>
445 protected internal bool ContainsInternal(Drawable drawable) => IndexOfInternal(drawable) >= 0;
446
447 /// <summary>
448 /// Removes a given child from this <see cref="InternalChildren"/>.
449 /// </summary>
450 /// <param name="drawable">The <see cref="Drawable"/> to be removed.</param>
451 /// <returns>False if <paramref name="drawable"/> was not a child of this <see cref="CompositeDrawable"/> and true otherwise.</returns>
452 protected internal virtual bool RemoveInternal(Drawable drawable)
453 {
454 EnsureChildMutationAllowed();
455
456 if (drawable == null)
457 throw new ArgumentNullException(nameof(drawable));
458
459 int index = IndexOfInternal(drawable);
460 if (index < 0)
461 return false;
462
463 internalChildren.RemoveAt(index);
464
465 if (drawable.IsAlive)
466 {
467 aliveInternalChildren.Remove(drawable);
468 ChildDied?.Invoke(drawable);
469 }
470
471 if (drawable.LoadState >= LoadState.Ready && drawable.Parent != this)
472 throw new InvalidOperationException($@"Removed a drawable ({drawable}) whose parent was not this ({this}), but {drawable.Parent}.");
473
474 drawable.Parent = null;
475 drawable.IsAlive = false;
476
477 if (AutoSizeAxes != Axes.None)
478 Invalidate(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child);
479
480 return true;
481 }
482
483 /// <summary>
484 /// Clear all of <see cref="InternalChildren"/>.
485 /// </summary>
486 /// <param name="disposeChildren">
487 /// Whether removed children should also get disposed.
488 /// Disposal will be recursive.
489 /// </param>
490 protected internal virtual void ClearInternal(bool disposeChildren = true)
491 {
492 EnsureChildMutationAllowed();
493
494 if (internalChildren.Count == 0) return;
495
496 foreach (Drawable t in internalChildren)
497 {
498 if (t.IsAlive)
499 ChildDied?.Invoke(t);
500
501 t.IsAlive = false;
502 t.Parent = null;
503
504 if (disposeChildren)
505 DisposeChildAsync(t);
506
507 Trace.Assert(t.Parent == null);
508 }
509
510 internalChildren.Clear();
511 aliveInternalChildren.Clear();
512 RequestsNonPositionalInputSubTree = RequestsNonPositionalInput;
513 RequestsPositionalInputSubTree = RequestsPositionalInput;
514
515 if (AutoSizeAxes != Axes.None)
516 Invalidate(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child);
517 }
518
519 /// <summary>
520 /// Used to assign a monotonically increasing ID to children as they are added. This member is
521 /// incremented whenever a child is added.
522 /// </summary>
523 private ulong currentChildID;
524
525 /// <summary>
526 /// Adds a child to <see cref="InternalChildren"/>.
527 /// </summary>
528 protected internal virtual void AddInternal(Drawable drawable)
529 {
530 EnsureChildMutationAllowed();
531
532 if (IsDisposed)
533 throw new ObjectDisposedException(ToString(), "Disposed Drawables may not have children added.");
534
535 if (drawable == null)
536 throw new ArgumentNullException(nameof(drawable), $"null {nameof(Drawable)}s may not be added to {nameof(CompositeDrawable)}.");
537
538 if (drawable == this)
539 throw new InvalidOperationException($"{nameof(CompositeDrawable)} may not be added to itself.");
540
541 // If the drawable's ChildId is not zero, then it was added to another parent even if it wasn't loaded
542 if (drawable.ChildID != 0)
543 throw new InvalidOperationException("May not add a drawable to multiple containers.");
544
545 drawable.ChildID = ++currentChildID;
546 drawable.RemoveCompletedTransforms = RemoveCompletedTransforms;
547
548 if (LoadState >= LoadState.Loading)
549 {
550 // If we're already loaded, we can eagerly allow children to be loaded
551
552 if (drawable.LoadState >= LoadState.Ready)
553 drawable.Parent = this;
554 else
555 loadChild(drawable);
556 }
557
558 internalChildren.Add(drawable);
559
560 if (AutoSizeAxes != Axes.None)
561 Invalidate(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child);
562 }
563
564 /// <summary>
565 /// Adds a range of children to <see cref="InternalChildren"/>. This is equivalent to calling
566 /// <see cref="AddInternal(Drawable)"/> on each element of the range in order.
567 /// </summary>
568 protected internal void AddRangeInternal(IEnumerable<Drawable> range)
569 {
570 if (range is IContainerEnumerable<Drawable>)
571 {
572 throw new InvalidOperationException($"Attempting to add a {nameof(IContainer)} as a range of children to {this}."
573 + $"If intentional, consider using the {nameof(IContainerEnumerable<Drawable>.Children)} property instead.");
574 }
575
576 foreach (Drawable d in range)
577 AddInternal(d);
578 }
579
580 /// <summary>
581 /// Changes the depth of an internal child. This affects ordering of <see cref="InternalChildren"/>.
582 /// </summary>
583 /// <param name="child">The child whose depth is to be changed.</param>
584 /// <param name="newDepth">The new depth value to be set.</param>
585 protected internal void ChangeInternalChildDepth(Drawable child, float newDepth)
586 {
587 EnsureChildMutationAllowed();
588
589 if (child.Depth == newDepth) return;
590
591 var index = IndexOfInternal(child);
592 if (index < 0)
593 throw new InvalidOperationException($"Can not change depth of drawable which is not contained within this {nameof(CompositeDrawable)}.");
594
595 internalChildren.RemoveAt(index);
596 var aliveIndex = aliveInternalChildren.IndexOf(child);
597 if (aliveIndex >= 0) // remove if found
598 aliveInternalChildren.RemoveAt(aliveIndex);
599
600 var chId = child.ChildID;
601 child.ChildID = 0; // ensure Depth-change does not throw an exception
602 child.Depth = newDepth;
603 child.ChildID = chId;
604
605 internalChildren.Add(child);
606 if (aliveIndex >= 0) // re-add if it used to be in aliveInternalChildren
607 aliveInternalChildren.Add(child);
608
609 ChildDepthChanged?.Invoke(child);
610 }
611
612 /// <summary>
613 /// Sorts all children of this <see cref="CompositeDrawable"/>.
614 /// </summary>
615 /// <remarks>
616 /// This can be used to re-sort the children if the result of <see cref="Compare"/> has changed.
617 /// </remarks>
618 protected internal void SortInternal()
619 {
620 EnsureChildMutationAllowed();
621
622 internalChildren.Sort();
623 aliveInternalChildren.Sort();
624 }
625
626 #endregion
627
628 #region Updating (per-frame periodic)
629
630 private Scheduler schedulerAfterChildren;
631
632 /// <summary>
633 /// A lazily-initialized scheduler used to schedule tasks to be invoked in future <see cref="UpdateAfterChildren"/>s calls.
634 /// The tasks are invoked at the beginning of the <see cref="UpdateAfterChildren"/> method before anything else.
635 /// </summary>
636 protected internal Scheduler SchedulerAfterChildren
637 {
638 get
639 {
640 if (schedulerAfterChildren != null)
641 return schedulerAfterChildren;
642
643 lock (LoadLock)
644 return schedulerAfterChildren ??= new Scheduler(() => ThreadSafety.IsUpdateThread, Clock);
645 }
646 }
647
648 /// <summary>
649 /// Updates the life status of <see cref="InternalChildren"/> according to their
650 /// <see cref="Drawable.ShouldBeAlive"/> property.
651 /// </summary>
652 /// <returns>True iff the life status of at least one child changed.</returns>
653 protected virtual bool UpdateChildrenLife()
654 {
655 // Can not have alive children if we are not loaded.
656 if (LoadState < LoadState.Ready)
657 return false;
658
659 if (!CheckChildrenLife())
660 return false;
661
662 return true;
663 }
664
665 /// <summary>
666 /// Checks whether the alive state of any child has changed and processes it. This will add or remove
667 /// children from <see cref="aliveInternalChildren"/> depending on their alive states.
668 /// <para>Note that this does NOT check the load state of this <see cref="CompositeDrawable"/> to check if it can hold any alive children.</para>
669 /// </summary>
670 /// <returns>Whether any child's alive state has changed.</returns>
671 protected virtual bool CheckChildrenLife()
672 {
673 bool anyAliveChanged = false;
674
675 for (int i = 0; i < internalChildren.Count; i++)
676 {
677 var state = checkChildLife(internalChildren[i]);
678
679 anyAliveChanged |= state.HasFlagFast(ChildLifeStateChange.MadeAlive) || state.HasFlagFast(ChildLifeStateChange.MadeDead);
680
681 if (state.HasFlagFast(ChildLifeStateChange.Removed))
682 i--;
683 }
684
685 FrameStatistics.Add(StatisticsCounterType.CCL, internalChildren.Count);
686
687 return anyAliveChanged;
688 }
689
690 /// <summary>
691 /// Checks whether the alive state of a child has changed and processes it. This will add or remove
692 /// the child from <see cref="aliveInternalChildren"/> depending on its alive state.
693 ///
694 /// This should only ever be called on a <see cref="CompositeDrawable"/>'s own <see cref="internalChildren"/>.
695 ///
696 /// <para>Note that this does NOT check the load state of this <see cref="CompositeDrawable"/> to check if it can hold any alive children.</para>
697 /// </summary>
698 /// <param name="child">The child to check.</param>
699 /// <returns>Whether the child's alive state has changed.</returns>
700 private ChildLifeStateChange checkChildLife(Drawable child)
701 {
702 ChildLifeStateChange state = ChildLifeStateChange.None;
703
704 if (child.ShouldBeAlive)
705 {
706 if (!child.IsAlive)
707 {
708 if (child.LoadState < LoadState.Ready)
709 {
710 // If we're already loaded, we can eagerly allow children to be loaded
711 loadChild(child);
712 if (child.LoadState < LoadState.Ready)
713 return ChildLifeStateChange.None;
714 }
715
716 MakeChildAlive(child);
717 state = ChildLifeStateChange.MadeAlive;
718 }
719 }
720 else
721 {
722 if (child.IsAlive || child.RemoveWhenNotAlive)
723 {
724 if (MakeChildDead(child))
725 state |= ChildLifeStateChange.Removed;
726
727 state |= ChildLifeStateChange.MadeDead;
728 }
729 }
730
731 return state;
732 }
733
734 [Flags]
735 private enum ChildLifeStateChange
736 {
737 None = 0,
738 MadeAlive = 1,
739 MadeDead = 1 << 1,
740 Removed = 1 << 2,
741 }
742
743 /// <summary>
744 /// Makes a child alive.
745 /// </summary>
746 /// <remarks>
747 /// Callers have to ensure that <paramref name="child"/> is of this <see cref="CompositeDrawable"/>'s non-alive <see cref="InternalChildren"/> and <see cref="LoadState"/> of the <paramref name="child"/> is at least <see cref="LoadState.Ready"/>.
748 /// </remarks>
749 /// <param name="child">The child of this <see cref="CompositeDrawable"/>> to make alive.</param>
750 protected void MakeChildAlive(Drawable child)
751 {
752 Debug.Assert(!child.IsAlive && child.LoadState >= LoadState.Ready);
753
754 // If the new child has the flag set, we should propagate the flag towards the root.
755 // We can stop at the ancestor which has the flag already set because further ancestors will also have the flag set.
756 if (child.RequestsNonPositionalInputSubTree)
757 {
758 for (var ancestor = this; ancestor != null && !ancestor.RequestsNonPositionalInputSubTree; ancestor = ancestor.Parent)
759 ancestor.RequestsNonPositionalInputSubTree = true;
760 }
761
762 if (child.RequestsPositionalInputSubTree)
763 {
764 for (var ancestor = this; ancestor != null && !ancestor.RequestsPositionalInputSubTree; ancestor = ancestor.Parent)
765 ancestor.RequestsPositionalInputSubTree = true;
766 }
767
768 aliveInternalChildren.Add(child);
769 child.IsAlive = true;
770
771 ChildBecameAlive?.Invoke(child);
772
773 // Layout invalidations on non-alive children are blocked, so they must be invalidated once when they become alive.
774 child.Invalidate(Invalidation.Layout, InvalidationSource.Parent);
775
776 // Notify ourselves that a child has become alive.
777 Invalidate(Invalidation.Presence, InvalidationSource.Child);
778 }
779
780 /// <summary>
781 /// Makes a child dead (not alive) and removes it if <see cref="Drawable.RemoveWhenNotAlive"/> of the <paramref name="child"/> is set.
782 /// </summary>
783 /// <remarks>
784 /// Callers have to ensure that <paramref name="child"/> is of this <see cref="CompositeDrawable"/>'s <see cref="AliveInternalChildren"/>.
785 /// </remarks>
786 /// <param name="child">The child of this <see cref="CompositeDrawable"/>> to make dead.</param>
787 /// <returns>Whether <paramref name="child"/> has been removed by death.</returns>
788 protected bool MakeChildDead(Drawable child)
789 {
790 if (child.IsAlive)
791 {
792 aliveInternalChildren.Remove(child);
793 child.IsAlive = false;
794
795 ChildDied?.Invoke(child);
796 }
797
798 bool removed = false;
799
800 if (child.RemoveWhenNotAlive)
801 {
802 RemoveInternal(child);
803
804 if (child.DisposeOnDeathRemoval)
805 DisposeChildAsync(child);
806
807 removed = true;
808 }
809
810 // Notify ourselves that a child has died.
811 Invalidate(Invalidation.Presence, InvalidationSource.Child);
812
813 return removed;
814 }
815
816 internal override void UnbindAllBindablesSubTree()
817 {
818 base.UnbindAllBindablesSubTree();
819
820 // TODO: this code can potentially be run from an update thread while a drawable is still loading (see ScreenStack as an example).
821 // while this is quite a bad issue, it is rare and generally happens in tests which have frame perfect behaviours.
822 // as such, for loop is used here intentionally to avoid collection modified exceptions for this (usually) non-critical failure.
823 // see https://github.com/ppy/osu-framework/issues/4054.
824 for (var i = 0; i < internalChildren.Count; i++)
825 {
826 Drawable child = internalChildren[i];
827 child.UnbindAllBindablesSubTree();
828 }
829 }
830
831 /// <summary>
832 /// Unbinds a child's bindings synchronously and queues an asynchronous disposal of the child.
833 /// </summary>
834 /// <param name="drawable">The child to dispose.</param>
835 internal void DisposeChildAsync(Drawable drawable)
836 {
837 drawable.UnbindAllBindablesSubTree();
838 AsyncDisposalQueue.Enqueue(drawable);
839 }
840
841 internal override void UpdateClock(IFrameBasedClock clock)
842 {
843 if (Clock == clock)
844 return;
845
846 base.UpdateClock(clock);
847 foreach (Drawable child in internalChildren)
848 child.UpdateClock(Clock);
849
850 schedulerAfterChildren?.UpdateClock(Clock);
851 }
852
853 /// <summary>
854 /// Specifies whether this <see cref="CompositeDrawable"/> requires an update of its children.
855 /// If the return value is false, then children are not updated and
856 /// <see cref="UpdateAfterChildren"/> is not called.
857 /// </summary>
858 protected virtual bool RequiresChildrenUpdate => !IsMaskedAway || !childrenSizeDependencies.IsValid;
859
860 public override bool UpdateSubTree()
861 {
862 if (!base.UpdateSubTree()) return false;
863
864 // We update our children's life even if we are invisible.
865 // Note, that this does not propagate down and may need
866 // generalization in the future.
867 UpdateChildrenLife();
868
869 // If we are not present then there is never a reason to check
870 // for children, as they should never affect our present status.
871 if (!IsPresent || !RequiresChildrenUpdate) return false;
872
873 UpdateAfterChildrenLife();
874
875 if (TypePerformanceMonitor.Active)
876 {
877 for (int i = 0; i < aliveInternalChildren.Count; ++i)
878 {
879 Drawable c = aliveInternalChildren[i];
880
881 TypePerformanceMonitor.BeginCollecting(c);
882 updateChild(c);
883 TypePerformanceMonitor.EndCollecting(c);
884 }
885 }
886 else
887 {
888 for (int i = 0; i < aliveInternalChildren.Count; ++i)
889 updateChild(aliveInternalChildren[i]);
890 }
891
892 if (schedulerAfterChildren != null)
893 {
894 int amountScheduledTasks = schedulerAfterChildren.Update();
895 FrameStatistics.Add(StatisticsCounterType.ScheduleInvk, amountScheduledTasks);
896 }
897
898 UpdateAfterChildren();
899
900 updateChildrenSizeDependencies();
901 UpdateAfterAutoSize();
902 return true;
903 }
904
905 private void updateChild(Drawable c)
906 {
907 Debug.Assert(c.LoadState >= LoadState.Ready);
908 c.UpdateSubTree();
909 }
910
911 /// <summary>
912 /// Updates all masking calculations for this <see cref="CompositeDrawable"/> and its <see cref="AliveInternalChildren"/>.
913 /// This occurs post-<see cref="UpdateSubTree"/> to ensure that all <see cref="Drawable"/> updates have taken place.
914 /// </summary>
915 /// <param name="source">The parent that triggered this update on this <see cref="Drawable"/>.</param>
916 /// <param name="maskingBounds">The <see cref="RectangleF"/> that defines the masking bounds.</param>
917 /// <returns>Whether masking calculations have taken place.</returns>
918 public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
919 {
920 if (!base.UpdateSubTreeMasking(source, maskingBounds))
921 return false;
922
923 if (IsMaskedAway)
924 return true;
925
926 if (aliveInternalChildren.Count == 0)
927 return true;
928
929 if (RequiresChildrenUpdate)
930 {
931 var childMaskingBounds = ComputeChildMaskingBounds(maskingBounds);
932
933 for (int i = 0; i < aliveInternalChildren.Count; i++)
934 aliveInternalChildren[i].UpdateSubTreeMasking(this, childMaskingBounds);
935 }
936
937 return true;
938 }
939
940 protected override bool ComputeIsMaskedAway(RectangleF maskingBounds)
941 {
942 if (!CanBeFlattened)
943 return base.ComputeIsMaskedAway(maskingBounds);
944
945 // The masking check is overly expensive (requires creation of ScreenSpaceDrawQuad)
946 // when only few children exist.
947 return aliveInternalChildren.Count >= amount_children_required_for_masking_check && base.ComputeIsMaskedAway(maskingBounds);
948 }
949
950 /// <summary>
951 /// Computes the <see cref="RectangleF"/> to be used as the masking bounds for all <see cref="AliveInternalChildren"/>.
952 /// </summary>
953 /// <param name="maskingBounds">The <see cref="RectangleF"/> that defines the masking bounds for this <see cref="CompositeDrawable"/>.</param>
954 /// <returns>The <see cref="RectangleF"/> to be used as the masking bounds for <see cref="AliveInternalChildren"/>.</returns>
955 protected virtual RectangleF ComputeChildMaskingBounds(RectangleF maskingBounds) => Masking ? RectangleF.Intersect(maskingBounds, ScreenSpaceDrawQuad.AABBFloat) : maskingBounds;
956
957 /// <summary>
958 /// Invoked after <see cref="UpdateChildrenLife"/> and <see cref="Drawable.IsPresent"/> state checks have taken place,
959 /// but before <see cref="Drawable.UpdateSubTree"/> is invoked for all <see cref="InternalChildren"/>.
960 /// This occurs after <see cref="Drawable.Update"/> has been invoked on this <see cref="CompositeDrawable"/>
961 /// </summary>
962 protected virtual void UpdateAfterChildrenLife()
963 {
964 }
965
966 /// <summary>
967 /// An opportunity to update state once-per-frame after <see cref="Drawable.Update"/> has been called
968 /// for all <see cref="InternalChildren"/>.
969 /// This is invoked prior to any autosize calculations of this <see cref="CompositeDrawable"/>.
970 /// </summary>
971 protected virtual void UpdateAfterChildren()
972 {
973 }
974
975 /// <summary>
976 /// Invoked after all autosize calculations have taken place.
977 /// </summary>
978 protected virtual void UpdateAfterAutoSize()
979 {
980 }
981
982 #endregion
983
984 #region Invalidation
985
986 protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
987 {
988 bool anyInvalidated = base.OnInvalidate(invalidation, source);
989
990 // Child invalidations should not propagate to other children.
991 if (source == InvalidationSource.Child)
992 return anyInvalidated;
993
994 // DrawNode invalidations should not propagate to children.
995 invalidation &= ~Invalidation.DrawNode;
996 if (invalidation == Invalidation.None)
997 return anyInvalidated;
998
999 IReadOnlyList<Drawable> targetChildren = aliveInternalChildren;
1000
1001 // Non-layout flags must be propagated to all children. As such, it is simplest + quickest to propagate all other relevant flags along with them.
1002 if ((invalidation & ~Invalidation.Layout) > 0)
1003 targetChildren = internalChildren;
1004
1005 for (int i = 0; i < targetChildren.Count; ++i)
1006 {
1007 Drawable c = targetChildren[i];
1008
1009 Invalidation childInvalidation = invalidation;
1010 if ((invalidation & Invalidation.RequiredParentSizeToFit) > 0)
1011 childInvalidation |= Invalidation.DrawInfo;
1012
1013 // Other geometry things like rotation, shearing, etc don't affect child properties.
1014 childInvalidation &= ~Invalidation.MiscGeometry;
1015
1016 // Relative positioning can however affect child geometry.
1017 if (c.RelativePositionAxes != Axes.None && (invalidation & Invalidation.DrawSize) > 0)
1018 childInvalidation |= Invalidation.MiscGeometry;
1019
1020 // No draw size changes if relative size axes does not propagate it downward.
1021 if (c.RelativeSizeAxes == Axes.None)
1022 childInvalidation &= ~Invalidation.DrawSize;
1023
1024 anyInvalidated |= c.Invalidate(childInvalidation, InvalidationSource.Parent);
1025 }
1026
1027 return anyInvalidated;
1028 }
1029
1030 /// <summary>
1031 /// Invalidates the children size dependencies of this <see cref="CompositeDrawable"/> when a child's position or size changes.
1032 /// </summary>
1033 /// <param name="invalidation">The <see cref="Invalidation"/> to invalidate with.</param>
1034 /// <param name="axes">The position or size <see cref="Axes"/> that changed.</param>
1035 /// <param name="source">The source <see cref="Drawable"/>.</param>
1036 internal void InvalidateChildrenSizeDependencies(Invalidation invalidation, Axes axes, Drawable source)
1037 {
1038 // Store the current state of the children size dependencies.
1039 // This state may be restored later if the invalidation proved to be unnecessary.
1040 bool wasValid = childrenSizeDependencies.IsValid;
1041
1042 // The invalidation still needs to occur as normal, since a derived CompositeDrawable may want to respond to children size invalidations.
1043 Invalidate(invalidation, InvalidationSource.Child);
1044
1045 // If all the changed axes were bypassed and an invalidation occurred, the children size dependencies can immediately be
1046 // re-validated without a recomputation, as a recomputation would not change the auto-sized size.
1047 if (wasValid && (axes & source.BypassAutoSizeAxes) == axes)
1048 childrenSizeDependencies.Validate();
1049 }
1050
1051 #endregion
1052
1053 #region DrawNode
1054
1055 private bool hasCustomDrawNode;
1056
1057 internal IShader Shader { get; private set; }
1058
1059 protected override DrawNode CreateDrawNode() => new CompositeDrawableDrawNode(this);
1060
1061 private bool forceLocalVertexBatch;
1062
1063 /// <summary>
1064 /// Whether to use a local vertex batch for rendering. If false, a parenting vertex batch will be used.
1065 /// </summary>
1066 public bool ForceLocalVertexBatch
1067 {
1068 get => forceLocalVertexBatch;
1069 protected set
1070 {
1071 if (forceLocalVertexBatch == value)
1072 return;
1073
1074 forceLocalVertexBatch = value;
1075
1076 Invalidate(Invalidation.DrawNode);
1077 }
1078 }
1079
1080 /// <summary>
1081 /// A flattened <see cref="CompositeDrawable"/> has its <see cref="DrawNode"/> merged into its parents'.
1082 /// In some cases, the <see cref="DrawNode"/> must always be generated and flattening should not occur.
1083 /// </summary>
1084 protected virtual bool CanBeFlattened =>
1085 // Masking composite DrawNodes define the masking area for their children.
1086 !Masking
1087 // Proxied drawables have their DrawNodes drawn elsewhere in the scene graph.
1088 && !HasProxy
1089 // Custom draw nodes may provide custom drawing procedures.
1090 && !hasCustomDrawNode;
1091
1092 private const int amount_children_required_for_masking_check = 2;
1093
1094 /// <summary>
1095 /// This function adds all children's <see cref="DrawNode"/>s to a target List, flattening the children of certain types
1096 /// of <see cref="CompositeDrawable"/> subtrees for optimization purposes.
1097 /// </summary>
1098 /// <param name="frame">The frame which <see cref="DrawNode"/>s should be generated for.</param>
1099 /// <param name="treeIndex">The index of the currently in-use <see cref="DrawNode"/> tree.</param>
1100 /// <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>
1101 /// <param name="j">The running index into the target List.</param>
1102 /// <param name="parentComposite">The <see cref="CompositeDrawable"/> whose children's <see cref="DrawNode"/>s to add.</param>
1103 /// <param name="target">The target list to fill with DrawNodes.</param>
1104 private static void addFromComposite(ulong frame, int treeIndex, bool forceNewDrawNode, ref int j, CompositeDrawable parentComposite, List<DrawNode> target)
1105 {
1106 SortedList<Drawable> children = parentComposite.aliveInternalChildren;
1107
1108 for (int i = 0; i < children.Count; ++i)
1109 {
1110 Drawable drawable = children[i];
1111
1112 if (!drawable.IsLoaded)
1113 continue;
1114
1115 if (!drawable.IsProxy)
1116 {
1117 if (!drawable.IsPresent)
1118 continue;
1119
1120 if (drawable.IsMaskedAway)
1121 continue;
1122
1123 CompositeDrawable composite = drawable as CompositeDrawable;
1124
1125 if (composite?.CanBeFlattened == true)
1126 {
1127 addFromComposite(frame, treeIndex, forceNewDrawNode, ref j, composite, target);
1128 continue;
1129 }
1130 }
1131
1132 DrawNode next = drawable.GenerateDrawNodeSubtree(frame, treeIndex, forceNewDrawNode);
1133 if (next == null)
1134 continue;
1135
1136 if (drawable.HasProxy)
1137 drawable.ValidateProxyDrawNode(treeIndex, frame);
1138 else
1139 {
1140 if (j < target.Count)
1141 target[j] = next;
1142 else
1143 target.Add(next);
1144 j++;
1145 }
1146 }
1147 }
1148
1149 internal override DrawNode GenerateDrawNodeSubtree(ulong frame, int treeIndex, bool forceNewDrawNode)
1150 {
1151 // No need for a draw node at all if there are no children and we are not glowing.
1152 if (aliveInternalChildren.Count == 0 && CanBeFlattened)
1153 return null;
1154
1155 DrawNode node = base.GenerateDrawNodeSubtree(frame, treeIndex, forceNewDrawNode);
1156
1157 if (!(node is ICompositeDrawNode cNode))
1158 return null;
1159
1160 cNode.Children ??= new List<DrawNode>(aliveInternalChildren.Count);
1161
1162 if (cNode.AddChildDrawNodes)
1163 {
1164 int j = 0;
1165 addFromComposite(frame, treeIndex, forceNewDrawNode, ref j, this, cNode.Children);
1166
1167 if (j < cNode.Children.Count)
1168 cNode.Children.RemoveRange(j, cNode.Children.Count - j);
1169 }
1170
1171 return node;
1172 }
1173
1174 #endregion
1175
1176 #region Transforms
1177
1178 /// <summary>
1179 /// Whether to remove completed transforms from the list of applicable transforms. Setting this to false allows for rewinding transforms.
1180 /// <para>
1181 /// This value is passed down to children.
1182 /// </para>
1183 /// </summary>
1184 public override bool RemoveCompletedTransforms
1185 {
1186 get => base.RemoveCompletedTransforms;
1187 internal set
1188 {
1189 if (base.RemoveCompletedTransforms == value)
1190 return;
1191
1192 base.RemoveCompletedTransforms = value;
1193
1194 foreach (var c in internalChildren)
1195 c.RemoveCompletedTransforms = RemoveCompletedTransforms;
1196 }
1197 }
1198
1199 public override void ApplyTransformsAt(double time, bool propagateChildren = false)
1200 {
1201 EnsureTransformMutationAllowed();
1202
1203 base.ApplyTransformsAt(time, propagateChildren);
1204
1205 if (!propagateChildren)
1206 return;
1207
1208 foreach (var c in internalChildren)
1209 c.ApplyTransformsAt(time, true);
1210 }
1211
1212 public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
1213 {
1214 EnsureTransformMutationAllowed();
1215
1216 base.ClearTransformsAfter(time, propagateChildren, targetMember);
1217
1218 if (!propagateChildren)
1219 return;
1220
1221 foreach (var c in internalChildren)
1222 c.ClearTransformsAfter(time, true, targetMember);
1223 }
1224
1225 internal override void AddDelay(double duration, bool propagateChildren = false)
1226 {
1227 if (duration == 0)
1228 return;
1229
1230 base.AddDelay(duration, propagateChildren);
1231
1232 if (propagateChildren)
1233 {
1234 foreach (var c in internalChildren)
1235 c.AddDelay(duration, true);
1236 }
1237 }
1238
1239 protected ScheduledDelegate ScheduleAfterChildren(Action action) => SchedulerAfterChildren.AddDelayed(action, TransformDelay);
1240
1241 public override IDisposable BeginAbsoluteSequence(double newTransformStartTime, bool recursive = true)
1242 {
1243 EnsureTransformMutationAllowed();
1244
1245 if (!recursive || internalChildren.Count == 0)
1246 return base.BeginAbsoluteSequence(newTransformStartTime, false);
1247
1248 List<AbsoluteSequenceSender> disposalActions = new List<AbsoluteSequenceSender>(internalChildren.Count + 1);
1249
1250 base.CollectAbsoluteSequenceActionsFromSubTree(newTransformStartTime, disposalActions);
1251
1252 foreach (var c in internalChildren)
1253 c.CollectAbsoluteSequenceActionsFromSubTree(newTransformStartTime, disposalActions);
1254
1255 return new ValueInvokeOnDisposal<List<AbsoluteSequenceSender>>(disposalActions, actions =>
1256 {
1257 foreach (var a in actions)
1258 a.Dispose();
1259 });
1260 }
1261
1262 internal override void CollectAbsoluteSequenceActionsFromSubTree(double newTransformStartTime, List<AbsoluteSequenceSender> actions)
1263 {
1264 base.CollectAbsoluteSequenceActionsFromSubTree(newTransformStartTime, actions);
1265
1266 foreach (var c in internalChildren)
1267 c.CollectAbsoluteSequenceActionsFromSubTree(newTransformStartTime, actions);
1268 }
1269
1270 public override void FinishTransforms(bool propagateChildren = false, string targetMember = null)
1271 {
1272 EnsureTransformMutationAllowed();
1273
1274 base.FinishTransforms(propagateChildren, targetMember);
1275
1276 if (propagateChildren)
1277 {
1278 foreach (var c in internalChildren)
1279 c.FinishTransforms(true, targetMember);
1280 }
1281 }
1282
1283 /// <summary>
1284 /// Helper function for creating and adding a <see cref="Transform{TValue, T}"/> that fades the current <see cref="EdgeEffect"/>.
1285 /// </summary>
1286 protected TransformSequence<CompositeDrawable> FadeEdgeEffectTo(float newAlpha, double duration = 0, Easing easing = Easing.None)
1287 {
1288 Color4 targetColour = EdgeEffect.Colour;
1289 targetColour.A = newAlpha;
1290 return FadeEdgeEffectTo(targetColour, duration, easing);
1291 }
1292
1293 /// <summary>
1294 /// Helper function for creating and adding a <see cref="Transform{TValue, T}"/> that fades the current <see cref="EdgeEffect"/>.
1295 /// </summary>
1296 protected TransformSequence<CompositeDrawable> FadeEdgeEffectTo(Color4 newColour, double duration = 0, Easing easing = Easing.None)
1297 {
1298 var effect = EdgeEffect;
1299 effect.Colour = newColour;
1300 return TweenEdgeEffectTo(effect, duration, easing);
1301 }
1302
1303 /// <summary>
1304 /// Helper function for creating and adding a <see cref="Transform{TValue, T}"/> that tweens the current <see cref="EdgeEffect"/>.
1305 /// </summary>
1306 protected TransformSequence<CompositeDrawable> TweenEdgeEffectTo(EdgeEffectParameters newParams, double duration = 0, Easing easing = Easing.None) =>
1307 this.TransformTo(nameof(EdgeEffect), newParams, duration, easing);
1308
1309 internal void EnsureChildMutationAllowed() => EnsureMutationAllowed(nameof(InternalChildren));
1310
1311 #endregion
1312
1313 #region Interaction / Input
1314
1315 public override bool Contains(Vector2 screenSpacePos)
1316 {
1317 float cRadius = effectiveCornerRadius;
1318 float cExponent = CornerExponent;
1319
1320 // Select a cheaper contains method when we don't need rounded edges.
1321 if (cRadius == 0.0f)
1322 return base.Contains(screenSpacePos);
1323
1324 return DrawRectangle.Shrink(cRadius).DistanceExponentiated(ToLocalSpace(screenSpacePos), cExponent) <= Math.Pow(cRadius, cExponent);
1325 }
1326
1327 /// <summary>
1328 /// Check whether a child should be considered for inclusion in <see cref="BuildNonPositionalInputQueue"/> and <see cref="BuildPositionalInputQueue"/>
1329 /// </summary>
1330 /// <param name="child">The drawable to be evaluated.</param>
1331 /// <returns>Whether or not the specified drawable should be considered when building input queues.</returns>
1332 protected virtual bool ShouldBeConsideredForInput(Drawable child) => child.LoadState == LoadState.Loaded;
1333
1334 internal override bool BuildNonPositionalInputQueue(List<Drawable> queue, bool allowBlocking = true)
1335 {
1336 if (!base.BuildNonPositionalInputQueue(queue, allowBlocking))
1337 return false;
1338
1339 for (int i = 0; i < aliveInternalChildren.Count; ++i)
1340 {
1341 if (ShouldBeConsideredForInput(aliveInternalChildren[i]))
1342 aliveInternalChildren[i].BuildNonPositionalInputQueue(queue, allowBlocking);
1343 }
1344
1345 return true;
1346 }
1347
1348 /// <summary>
1349 /// Determines whether the subtree of this <see cref="CompositeDrawable"/> should receive positional input when the mouse is at the given screen-space position.
1350 /// </summary>
1351 /// <remarks>
1352 /// By default, the subtree of this <see cref="CompositeDrawable"/> always receives input when masking is turned off, and only receives input if this
1353 /// <see cref="CompositeDrawable"/> also receives input when masking is turned on.
1354 /// </remarks>
1355 /// <param name="screenSpacePos">The screen-space position where input could be received.</param>
1356 /// <returns>True if the subtree should receive input at the given screen-space position.</returns>
1357 protected virtual bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => !Masking || ReceivePositionalInputAt(screenSpacePos);
1358
1359 internal override bool BuildPositionalInputQueue(Vector2 screenSpacePos, List<Drawable> queue)
1360 {
1361 if (!base.BuildPositionalInputQueue(screenSpacePos, queue))
1362 return false;
1363
1364 if (!ReceivePositionalInputAtSubTree(screenSpacePos))
1365 return false;
1366
1367 for (int i = 0; i < aliveInternalChildren.Count; ++i)
1368 {
1369 if (ShouldBeConsideredForInput(aliveInternalChildren[i]))
1370 aliveInternalChildren[i].BuildPositionalInputQueue(screenSpacePos, queue);
1371 }
1372
1373 return true;
1374 }
1375
1376 #endregion
1377
1378 #region Masking and related effects (e.g. round corners)
1379
1380 private bool masking;
1381
1382 /// <summary>
1383 /// If enabled, only the portion of children that falls within this <see cref="CompositeDrawable"/>'s
1384 /// shape is drawn to the screen.
1385 /// </summary>
1386 public bool Masking
1387 {
1388 get => masking;
1389 protected set
1390 {
1391 if (masking == value)
1392 return;
1393
1394 masking = value;
1395 Invalidate(Invalidation.DrawNode);
1396 }
1397 }
1398
1399 private float maskingSmoothness = 1;
1400
1401 /// <summary>
1402 /// Determines over how many pixels the alpha component smoothly fades out.
1403 /// Only has an effect when <see cref="Masking"/> is true.
1404 /// </summary>
1405 public float MaskingSmoothness
1406 {
1407 get => maskingSmoothness;
1408 protected set
1409 {
1410 //must be above zero to avoid a div-by-zero in the shader logic.
1411 value = Math.Max(0.01f, value);
1412
1413 if (maskingSmoothness == value)
1414 return;
1415
1416 maskingSmoothness = value;
1417 Invalidate(Invalidation.DrawNode);
1418 }
1419 }
1420
1421 private float cornerRadius;
1422
1423 /// <summary>
1424 /// Determines how large of a radius is masked away around the corners.
1425 /// Only has an effect when <see cref="Masking"/> is true.
1426 /// </summary>
1427 public float CornerRadius
1428 {
1429 get => cornerRadius;
1430 protected set
1431 {
1432 if (cornerRadius == value)
1433 return;
1434
1435 cornerRadius = value;
1436 Invalidate(Invalidation.DrawNode);
1437 }
1438 }
1439
1440 private float cornerExponent = 2f;
1441
1442 /// <summary>
1443 /// Determines how gentle the curve of the corner straightens. A value of 2 (default) results in
1444 /// circular arcs, a value of 2.5 results in something closer to apple's "continuous corner".
1445 /// Values between 2 and 10 result in varying degrees of "continuousness", where larger values are smoother.
1446 /// Values between 1 and 2 result in a "flatter" appearance than round corners.
1447 /// Values between 0 and 1 result in a concave, round corner as opposed to a convex round corner,
1448 /// where a value of 0.5 is a circular concave arc.
1449 /// Only has an effect when <see cref="Masking"/> is true and <see cref="CornerRadius"/> is non-zero.
1450 /// </summary>
1451 public float CornerExponent
1452 {
1453 get => cornerExponent;
1454 protected set
1455 {
1456 if (!Precision.DefinitelyBigger(value, 0) || value > 10)
1457 throw new ArgumentOutOfRangeException(nameof(CornerExponent), $"{nameof(CornerExponent)} may not be <=0 or >10 for numerical correctness.");
1458
1459 if (cornerExponent == value)
1460 return;
1461
1462 cornerExponent = value;
1463 Invalidate(Invalidation.DrawNode);
1464 }
1465 }
1466
1467 // This _hacky_ modification of the corner radius (obtained from playing around) ensures that the corner remains at roughly
1468 // equal size (perceptually) compared to the circular arc as the CornerExponent is adjusted within the range ~2-5.
1469 private float effectiveCornerRadius => CornerRadius * 0.8f * CornerExponent / 2 + 0.2f * CornerRadius;
1470
1471 private float borderThickness;
1472
1473 /// <summary>
1474 /// Determines how thick of a border to draw around the inside of the masked region.
1475 /// Only has an effect when <see cref="Masking"/> is true.
1476 /// The border only is drawn on top of children using a sprite shader.
1477 /// </summary>
1478 /// <remarks>
1479 /// Drawing borders is optimized heavily into our sprite shaders. As a consequence
1480 /// borders are only drawn correctly on top of quad-shaped children using our sprite
1481 /// shaders.
1482 /// </remarks>
1483 public float BorderThickness
1484 {
1485 get => borderThickness;
1486 protected set
1487 {
1488 if (borderThickness == value)
1489 return;
1490
1491 borderThickness = value;
1492 Invalidate(Invalidation.DrawNode);
1493 }
1494 }
1495
1496 private SRGBColour borderColour = Color4.Black;
1497
1498 /// <summary>
1499 /// Determines the color of the border controlled by <see cref="BorderThickness"/>.
1500 /// Only has an effect when <see cref="Masking"/> is true.
1501 /// </summary>
1502 public SRGBColour BorderColour
1503 {
1504 get => borderColour;
1505 protected set
1506 {
1507 if (borderColour.Equals(value))
1508 return;
1509
1510 borderColour = value;
1511 Invalidate(Invalidation.DrawNode);
1512 }
1513 }
1514
1515 private EdgeEffectParameters edgeEffect;
1516
1517 /// <summary>
1518 /// Determines an edge effect of this <see cref="CompositeDrawable"/>.
1519 /// Edge effects are e.g. glow or a shadow.
1520 /// Only has an effect when <see cref="Masking"/> is true.
1521 /// </summary>
1522 public EdgeEffectParameters EdgeEffect
1523 {
1524 get => edgeEffect;
1525 protected set
1526 {
1527 if (edgeEffect.Equals(value))
1528 return;
1529
1530 edgeEffect = value;
1531 Invalidate(Invalidation.DrawNode);
1532 }
1533 }
1534
1535 #endregion
1536
1537 #region Sizing
1538
1539 public override RectangleF BoundingBox
1540 {
1541 get
1542 {
1543 float cRadius = CornerRadius;
1544 if (cRadius == 0.0f)
1545 return base.BoundingBox;
1546
1547 RectangleF drawRect = LayoutRectangle.Shrink(cRadius);
1548
1549 // Inflate bounding box in parent space by the half-size of the bounding box of the
1550 // ellipse obtained by transforming the unit circle into parent space.
1551 Vector2 offset = ToParentSpace(Vector2.Zero);
1552 Vector2 u = ToParentSpace(new Vector2(cRadius, 0)) - offset;
1553 Vector2 v = ToParentSpace(new Vector2(0, cRadius)) - offset;
1554 Vector2 inflation = new Vector2(
1555 MathF.Sqrt(u.X * u.X + v.X * v.X),
1556 MathF.Sqrt(u.Y * u.Y + v.Y * v.Y)
1557 );
1558
1559 RectangleF result = ToParentSpace(drawRect).AABBFloat.Inflate(inflation);
1560 // The above algorithm will return incorrect results if the rounded corners are not fully visible.
1561 // To limit bad behavior we at least enforce here, that the bounding box with rounded corners
1562 // is never larger than the bounding box without.
1563 if (DrawSize.X < CornerRadius * 2 || DrawSize.Y < CornerRadius * 2)
1564 result.Intersect(base.BoundingBox);
1565
1566 return result;
1567 }
1568 }
1569
1570 private MarginPadding padding;
1571
1572 /// <summary>
1573 /// Shrinks the space children may occupy within this <see cref="CompositeDrawable"/>
1574 /// by the specified amount on each side.
1575 /// </summary>
1576 public MarginPadding Padding
1577 {
1578 get => padding;
1579 protected set
1580 {
1581 if (padding.Equals(value)) return;
1582
1583 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(Padding)} must be finite, but is {value}.");
1584
1585 padding = value;
1586
1587 foreach (Drawable c in internalChildren)
1588 c.Invalidate(c.InvalidationFromParentSize | Invalidation.MiscGeometry);
1589 }
1590 }
1591
1592 /// <summary>
1593 /// The size of the coordinate space revealed to <see cref="InternalChildren"/>.
1594 /// Captures the effect of e.g. <see cref="Padding"/>.
1595 /// </summary>
1596 public Vector2 ChildSize => DrawSize - new Vector2(Padding.TotalHorizontal, Padding.TotalVertical);
1597
1598 /// <summary>
1599 /// Positional offset applied to <see cref="InternalChildren"/>.
1600 /// Captures the effect of e.g. <see cref="Padding"/>.
1601 /// </summary>
1602 public Vector2 ChildOffset => new Vector2(Padding.Left, Padding.Top);
1603
1604 private Vector2 relativeChildSize = Vector2.One;
1605
1606 /// <summary>
1607 /// The size of the relative position/size coordinate space of children of this <see cref="CompositeDrawable"/>.
1608 /// Children positioned at this size will appear as if they were positioned at <see cref="Drawable.Position"/> = <see cref="Vector2.One"/> in this <see cref="CompositeDrawable"/>.
1609 /// </summary>
1610 public Vector2 RelativeChildSize
1611 {
1612 get => relativeChildSize;
1613 protected set
1614 {
1615 if (relativeChildSize == value)
1616 return;
1617
1618 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeChildSize)} must be finite, but is {value}.");
1619 if (value.X == 0 || value.Y == 0) throw new ArgumentException($@"{nameof(RelativeChildSize)} must be non-zero, but is {value}.");
1620
1621 relativeChildSize = value;
1622
1623 foreach (Drawable c in internalChildren)
1624 c.Invalidate(c.InvalidationFromParentSize);
1625 }
1626 }
1627
1628 private Vector2 relativeChildOffset = Vector2.Zero;
1629
1630 /// <summary>
1631 /// The offset of the relative position/size coordinate space of children of this <see cref="CompositeDrawable"/>.
1632 /// Children positioned at this offset will appear as if they were positioned at <see cref="Drawable.Position"/> = <see cref="Vector2.Zero"/> in this <see cref="CompositeDrawable"/>.
1633 /// </summary>
1634 public Vector2 RelativeChildOffset
1635 {
1636 get => relativeChildOffset;
1637 protected set
1638 {
1639 if (relativeChildOffset == value)
1640 return;
1641
1642 if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(RelativeChildOffset)} must be finite, but is {value}.");
1643
1644 relativeChildOffset = value;
1645
1646 foreach (Drawable c in internalChildren)
1647 c.Invalidate(c.InvalidationFromParentSize & ~Invalidation.DrawSize);
1648 }
1649 }
1650
1651 /// <summary>
1652 /// Conversion factor from relative to absolute coordinates in our space.
1653 /// </summary>
1654 public Vector2 RelativeToAbsoluteFactor => Vector2.Divide(ChildSize, RelativeChildSize);
1655
1656 /// <summary>
1657 /// Tweens the <see cref="RelativeChildSize"/> of this <see cref="CompositeDrawable"/>.
1658 /// </summary>
1659 /// <param name="newSize">The coordinate space to tween to.</param>
1660 /// <param name="duration">The tween duration.</param>
1661 /// <param name="easing">The tween easing.</param>
1662 protected TransformSequence<CompositeDrawable> TransformRelativeChildSizeTo(Vector2 newSize, double duration = 0, Easing easing = Easing.None) =>
1663 this.TransformTo(nameof(RelativeChildSize), newSize, duration, easing);
1664
1665 /// <summary>
1666 /// Tweens the <see cref="RelativeChildOffset"/> of this <see cref="CompositeDrawable"/>.
1667 /// </summary>
1668 /// <param name="newOffset">The coordinate space to tween to.</param>
1669 /// <param name="duration">The tween duration.</param>
1670 /// <param name="easing">The tween easing.</param>
1671 protected TransformSequence<CompositeDrawable> TransformRelativeChildOffsetTo(Vector2 newOffset, double duration = 0, Easing easing = Easing.None) =>
1672 this.TransformTo(nameof(RelativeChildOffset), newOffset, duration, easing);
1673
1674 public override Axes RelativeSizeAxes
1675 {
1676 get => base.RelativeSizeAxes;
1677 set
1678 {
1679 if ((AutoSizeAxes & value) != 0)
1680 throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time.");
1681
1682 base.RelativeSizeAxes = value;
1683 }
1684 }
1685
1686 private Axes autoSizeAxes;
1687
1688 /// <summary>
1689 /// Controls which <see cref="Axes"/> are automatically sized w.r.t. <see cref="InternalChildren"/>.
1690 /// Children's <see cref="Drawable.BypassAutoSizeAxes"/> are ignored for automatic sizing.
1691 /// Most notably, <see cref="Drawable.RelativePositionAxes"/> and <see cref="RelativeSizeAxes"/> of children
1692 /// do not affect automatic sizing to avoid circular size dependencies.
1693 /// It is not allowed to manually set <see cref="Size"/> (or <see cref="Width"/> / <see cref="Height"/>)
1694 /// on any <see cref="Axes"/> which are automatically sized.
1695 /// </summary>
1696 public virtual Axes AutoSizeAxes
1697 {
1698 get => autoSizeAxes;
1699 protected set
1700 {
1701 if (value == autoSizeAxes)
1702 return;
1703
1704 if ((RelativeSizeAxes & value) != 0)
1705 throw new InvalidOperationException("No axis can be relatively sized and automatically sized at the same time.");
1706
1707 autoSizeAxes = value;
1708 childrenSizeDependencies.Invalidate();
1709 OnSizingChanged();
1710 }
1711 }
1712
1713 /// <summary>
1714 /// The duration which automatic sizing should take. If zero, then it is instantaneous.
1715 /// Otherwise, this is equivalent to applying an automatic size via a resize transform.
1716 /// </summary>
1717 public float AutoSizeDuration { get; protected set; }
1718
1719 /// <summary>
1720 /// The type of easing which should be used for smooth automatic sizing when <see cref="AutoSizeDuration"/>
1721 /// is non-zero.
1722 /// </summary>
1723 public Easing AutoSizeEasing { get; protected set; }
1724
1725 /// <summary>
1726 /// Fired after this <see cref="CompositeDrawable"/>'s <see cref="Size"/> is updated through autosize.
1727 /// </summary>
1728 internal event Action OnAutoSize;
1729
1730 private readonly LayoutValue childrenSizeDependencies = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.Presence, InvalidationSource.Child);
1731
1732 public override float Width
1733 {
1734 get
1735 {
1736 if (!isComputingChildrenSizeDependencies && AutoSizeAxes.HasFlagFast(Axes.X))
1737 updateChildrenSizeDependencies();
1738 return base.Width;
1739 }
1740
1741 set
1742 {
1743 if ((AutoSizeAxes & Axes.X) != 0)
1744 throw new InvalidOperationException($"The width of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} can not be set manually.");
1745
1746 base.Width = value;
1747 }
1748 }
1749
1750 public override float Height
1751 {
1752 get
1753 {
1754 if (!isComputingChildrenSizeDependencies && AutoSizeAxes.HasFlagFast(Axes.Y))
1755 updateChildrenSizeDependencies();
1756 return base.Height;
1757 }
1758
1759 set
1760 {
1761 if ((AutoSizeAxes & Axes.Y) != 0)
1762 throw new InvalidOperationException($"The height of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} can not be set manually.");
1763
1764 base.Height = value;
1765 }
1766 }
1767
1768 private bool isComputingChildrenSizeDependencies;
1769
1770 public override Vector2 Size
1771 {
1772 get
1773 {
1774 if (!isComputingChildrenSizeDependencies && AutoSizeAxes != Axes.None)
1775 updateChildrenSizeDependencies();
1776 return base.Size;
1777 }
1778
1779 set
1780 {
1781 if ((AutoSizeAxes & Axes.Both) != 0)
1782 throw new InvalidOperationException($"The Size of a {nameof(CompositeDrawable)} with {nameof(AutoSizeAxes)} can not be set manually.");
1783
1784 base.Size = value;
1785 }
1786 }
1787
1788 private Vector2 computeAutoSize()
1789 {
1790 MarginPadding originalPadding = Padding;
1791 MarginPadding originalMargin = Margin;
1792
1793 try
1794 {
1795 Padding = new MarginPadding();
1796 Margin = new MarginPadding();
1797
1798 if (AutoSizeAxes == Axes.None) return DrawSize;
1799
1800 Vector2 maxBoundSize = Vector2.Zero;
1801
1802 // Find the maximum width/height of children
1803 foreach (Drawable c in aliveInternalChildren)
1804 {
1805 if (!c.IsPresent)
1806 continue;
1807
1808 Vector2 cBound = c.RequiredParentSizeToFit;
1809
1810 if (!c.BypassAutoSizeAxes.HasFlagFast(Axes.X))
1811 maxBoundSize.X = Math.Max(maxBoundSize.X, cBound.X);
1812
1813 if (!c.BypassAutoSizeAxes.HasFlagFast(Axes.Y))
1814 maxBoundSize.Y = Math.Max(maxBoundSize.Y, cBound.Y);
1815 }
1816
1817 if (!AutoSizeAxes.HasFlagFast(Axes.X))
1818 maxBoundSize.X = DrawSize.X;
1819 if (!AutoSizeAxes.HasFlagFast(Axes.Y))
1820 maxBoundSize.Y = DrawSize.Y;
1821
1822 return new Vector2(maxBoundSize.X, maxBoundSize.Y);
1823 }
1824 finally
1825 {
1826 Padding = originalPadding;
1827 Margin = originalMargin;
1828 }
1829 }
1830
1831 private void updateAutoSize()
1832 {
1833 if (AutoSizeAxes == Axes.None)
1834 return;
1835
1836 Vector2 b = computeAutoSize() + Padding.Total;
1837
1838 autoSizeResizeTo(new Vector2(
1839 AutoSizeAxes.HasFlagFast(Axes.X) ? b.X : base.Width,
1840 AutoSizeAxes.HasFlagFast(Axes.Y) ? b.Y : base.Height
1841 ), AutoSizeDuration, AutoSizeEasing);
1842
1843 //note that this is called before autoSize becomes valid. may be something to consider down the line.
1844 //might work better to add an OnRefresh event in Cached<> and invoke there.
1845 OnAutoSize?.Invoke();
1846 }
1847
1848 private void updateChildrenSizeDependencies()
1849 {
1850 isComputingChildrenSizeDependencies = true;
1851
1852 try
1853 {
1854 if (!childrenSizeDependencies.IsValid)
1855 {
1856 updateAutoSize();
1857 childrenSizeDependencies.Validate();
1858 }
1859 }
1860 finally
1861 {
1862 isComputingChildrenSizeDependencies = false;
1863 }
1864 }
1865
1866 private void autoSizeResizeTo(Vector2 newSize, double duration = 0, Easing easing = Easing.None)
1867 {
1868 var currentTransform = TransformsForTargetMember(nameof(baseSize)).FirstOrDefault() as AutoSizeTransform;
1869
1870 if ((currentTransform?.EndValue ?? Size) != newSize)
1871 {
1872 if (duration == 0)
1873 {
1874 if (currentTransform != null)
1875 ClearTransforms(false, nameof(baseSize));
1876 baseSize = newSize;
1877 }
1878 else
1879 this.TransformTo(this.PopulateTransform(new AutoSizeTransform { Rewindable = false }, newSize, duration, easing));
1880 }
1881 }
1882
1883 /// <summary>
1884 /// A helper property for <see cref="autoSizeResizeTo(Vector2, double, Easing)"/> to change the size of <see cref="CompositeDrawable"/>s with <see cref="AutoSizeAxes"/>.
1885 /// </summary>
1886 private Vector2 baseSize
1887 {
1888 get => new Vector2(base.Width, base.Height);
1889 set
1890 {
1891 base.Width = value.X;
1892 base.Height = value.Y;
1893 }
1894 }
1895
1896 private class AutoSizeTransform : TransformCustom<Vector2, CompositeDrawable>
1897 {
1898 public AutoSizeTransform()
1899 : base(nameof(baseSize))
1900 {
1901 }
1902 }
1903
1904 #endregion
1905 }
1906}