// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using JetBrains.Annotations; using osu.Framework.Graphics.Transforms; using osu.Framework.Lists; namespace osu.Framework.Graphics.Containers { /// /// Manages dynamically displaying a custom based on a model object. /// Useful for replacing s on the fly. /// public abstract class ModelBackedDrawable : CompositeDrawable { /// /// The currently displayed . Null if no drawable is displayed. /// protected Drawable DisplayedDrawable => displayedWrapper?.Content; /// /// The used to compare models to ensure that s are not updated unnecessarily. /// protected readonly IEqualityComparer Comparer; private T model; /// /// Gets or sets the model, potentially triggering the current to update. /// Subclasses should expose this via a nicer property name to better represent the data being set. /// protected T Model { get => model; set { if (model == null && value == null) return; if (Comparer.Equals(model, value)) return; model = value; if (IsLoaded) updateDrawable(); } } /// /// The wrapper which has the current displayed content. /// private DelayedLoadWrapper displayedWrapper; /// /// The wrapper which is currently loading, or has finished loading (i.e ). /// private DelayedLoadWrapper currentWrapper; /// /// Constructs a new with the default equality comparer. /// protected ModelBackedDrawable() : this(EqualityComparer.Default) { } /// /// Constructs a new with a custom equality function. /// /// The equality function. protected ModelBackedDrawable(Func func) : this(new FuncEqualityComparer(func)) { } /// /// Constructs a new with a custom . /// /// The comparer to use. protected ModelBackedDrawable(IEqualityComparer comparer) { Comparer = comparer; } protected override void LoadComplete() { base.LoadComplete(); updateDrawable(); } private void updateDrawable() { if (TransformImmediately) { // If loading to a new model and we've requested to transform immediately, load a null model to allow such transforms to occur loadDrawable(null); } loadDrawable(() => CreateDrawable(model)); } private void loadDrawable(Func createDrawableFunc) { // Remove the previous wrapper if the inner drawable hasn't finished loading. if (currentWrapper?.DelayedLoadCompleted == false) { RemoveInternal(currentWrapper); DisposeChildAsync(currentWrapper); } currentWrapper = createWrapper(createDrawableFunc, LoadDelay); if (currentWrapper == null) { OnLoadStarted(); finishLoad(currentWrapper); OnLoadFinished(); } else { AddInternal(currentWrapper); currentWrapper.DelayedLoadStarted += _ => OnLoadStarted(); currentWrapper.DelayedLoadComplete += _ => { finishLoad(currentWrapper); OnLoadFinished(); }; } } /// /// Invoked when a has finished loading its contents. /// May be invoked multiple times for each . /// /// The . private void finishLoad(DelayedLoadWrapper wrapper) { // Make the wrapper initially hidden. ApplyHideTransforms(wrapper); wrapper?.FinishTransforms(); var showTransforms = ApplyShowTransforms(wrapper); // If the wrapper hasn't changed then this invocation must be a result of a reload (e.g. DelayedLoadUnloadWrapper) // In that case, we do not want to apply hide transforms and expire the last wrapper. if (displayedWrapper != null && displayedWrapper != wrapper) { var lastWrapper = displayedWrapper; // If the new wrapper is non-null, we need to wait for the show transformation to complete before hiding the old wrapper, // otherwise, we can hide the old wrapper instantaneously and leave a blank display var hideTransforms = wrapper == null ? ApplyHideTransforms(lastWrapper) : ((Drawable)lastWrapper)?.Delay(TransformDuration)?.Append(ApplyHideTransforms); // Expire the last wrapper after the front-most transform has completed (the last wrapper is assumed to be invisible by that point) (showTransforms ?? hideTransforms)?.OnComplete(_ => lastWrapper?.Expire()); } displayedWrapper = wrapper; } /// /// Creates a which supports reloading. /// /// A function that creates the wrapped . /// The time before loading should begin. /// A or null if returns null. private DelayedLoadWrapper createWrapper(Func createContentFunc, double timeBeforeLoad) { var content = createContentFunc?.Invoke(); if (content == null) return null; return CreateDelayedLoadWrapper(() => { try { // optimisation to use already constructed object (used above for null check). return content ?? createContentFunc(); } finally { // consume initial object if not already. content = null; } }, timeBeforeLoad); } /// /// Invoked when the representation of a model begins loading. /// protected virtual void OnLoadStarted() { } /// /// Invoked when the representation of a model has finished loading. /// protected virtual void OnLoadFinished() { } /// /// Determines whether should be invoked immediately on the currently-displayed drawable when switching to a new model. /// protected virtual bool TransformImmediately => false; /// /// The default time in milliseconds for transforms applied through and . /// protected virtual double TransformDuration => 1000; /// /// The delay in milliseconds before s will begin loading. /// protected virtual double LoadDelay => 0; /// /// Allows subclasses to customise the . /// [NotNull] protected virtual DelayedLoadWrapper CreateDelayedLoadWrapper([NotNull] Func createContentFunc, double timeBeforeLoad) => new DelayedLoadWrapper(createContentFunc(), timeBeforeLoad); /// /// Creates a custom to display a model. /// /// The model that the should represent. /// A that represents , or null if no should be displayed. [CanBeNull] protected abstract Drawable CreateDrawable([CanBeNull] T model); /// /// Hides a drawable. /// /// The drawable that is to be hidden. /// The transform sequence. protected virtual TransformSequence ApplyHideTransforms([CanBeNull] Drawable drawable) => drawable?.FadeOut(TransformDuration, Easing.OutQuint); /// /// Shows a drawable. /// /// The drawable that is to be shown. /// The transform sequence. protected virtual TransformSequence ApplyShowTransforms([CanBeNull] Drawable drawable) => drawable?.FadeIn(TransformDuration, Easing.OutQuint); } }