A game framework written with osu! in mind.
at master 1906 lines 79 kB view raw
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}