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 System;
5using System.Collections.Generic;
6using JetBrains.Annotations;
7using osu.Framework.Graphics.Transforms;
8using osu.Framework.Lists;
9
10namespace osu.Framework.Graphics.Containers
11{
12 /// <summary>
13 /// Manages dynamically displaying a custom <see cref="Drawable"/> based on a model object.
14 /// Useful for replacing <see cref="Drawable"/>s on the fly.
15 /// </summary>
16 public abstract class ModelBackedDrawable<T> : CompositeDrawable
17 {
18 /// <summary>
19 /// The currently displayed <see cref="Drawable"/>. Null if no drawable is displayed.
20 /// </summary>
21 protected Drawable DisplayedDrawable => displayedWrapper?.Content;
22
23 /// <summary>
24 /// The <see cref="IEqualityComparer{T}"/> used to compare models to ensure that <see cref="Drawable"/>s are not updated unnecessarily.
25 /// </summary>
26 protected readonly IEqualityComparer<T> Comparer;
27
28 private T model;
29
30 /// <summary>
31 /// Gets or sets the model, potentially triggering the current <see cref="Drawable"/> to update.
32 /// Subclasses should expose this via a nicer property name to better represent the data being set.
33 /// </summary>
34 protected T Model
35 {
36 get => model;
37 set
38 {
39 if (model == null && value == null)
40 return;
41
42 if (Comparer.Equals(model, value))
43 return;
44
45 model = value;
46
47 if (IsLoaded)
48 updateDrawable();
49 }
50 }
51
52 /// <summary>
53 /// The wrapper which has the current displayed content.
54 /// </summary>
55 private DelayedLoadWrapper displayedWrapper;
56
57 /// <summary>
58 /// The wrapper which is currently loading, or has finished loading (i.e <see cref="displayedWrapper"/>).
59 /// </summary>
60 private DelayedLoadWrapper currentWrapper;
61
62 /// <summary>
63 /// Constructs a new <see cref="ModelBackedDrawable{T}"/> with the default <typeparamref name="T"/> equality comparer.
64 /// </summary>
65 protected ModelBackedDrawable()
66 : this(EqualityComparer<T>.Default)
67 {
68 }
69
70 /// <summary>
71 /// Constructs a new <see cref="ModelBackedDrawable{T}"/> with a custom equality function.
72 /// </summary>
73 /// <param name="func">The equality function.</param>
74 protected ModelBackedDrawable(Func<T, T, bool> func)
75 : this(new FuncEqualityComparer<T>(func))
76 {
77 }
78
79 /// <summary>
80 /// Constructs a new <see cref="ModelBackedDrawable{T}"/> with a custom <see cref="IEqualityComparer{T}"/>.
81 /// </summary>
82 /// <param name="comparer">The comparer to use.</param>
83 protected ModelBackedDrawable(IEqualityComparer<T> comparer)
84 {
85 Comparer = comparer;
86 }
87
88 protected override void LoadComplete()
89 {
90 base.LoadComplete();
91 updateDrawable();
92 }
93
94 private void updateDrawable()
95 {
96 if (TransformImmediately)
97 {
98 // If loading to a new model and we've requested to transform immediately, load a null model to allow such transforms to occur
99 loadDrawable(null);
100 }
101
102 loadDrawable(() => CreateDrawable(model));
103 }
104
105 private void loadDrawable(Func<Drawable> createDrawableFunc)
106 {
107 // Remove the previous wrapper if the inner drawable hasn't finished loading.
108 if (currentWrapper?.DelayedLoadCompleted == false)
109 {
110 RemoveInternal(currentWrapper);
111 DisposeChildAsync(currentWrapper);
112 }
113
114 currentWrapper = createWrapper(createDrawableFunc, LoadDelay);
115
116 if (currentWrapper == null)
117 {
118 OnLoadStarted();
119 finishLoad(currentWrapper);
120 OnLoadFinished();
121 }
122 else
123 {
124 AddInternal(currentWrapper);
125 currentWrapper.DelayedLoadStarted += _ => OnLoadStarted();
126 currentWrapper.DelayedLoadComplete += _ =>
127 {
128 finishLoad(currentWrapper);
129 OnLoadFinished();
130 };
131 }
132 }
133
134 /// <summary>
135 /// Invoked when a <see cref="DelayedLoadWrapper"/> has finished loading its contents.
136 /// May be invoked multiple times for each <see cref="DelayedLoadWrapper"/>.
137 /// </summary>
138 /// <param name="wrapper">The <see cref="DelayedLoadWrapper"/>.</param>
139 private void finishLoad(DelayedLoadWrapper wrapper)
140 {
141 // Make the wrapper initially hidden.
142 ApplyHideTransforms(wrapper);
143 wrapper?.FinishTransforms();
144
145 var showTransforms = ApplyShowTransforms(wrapper);
146
147 // If the wrapper hasn't changed then this invocation must be a result of a reload (e.g. DelayedLoadUnloadWrapper)
148 // In that case, we do not want to apply hide transforms and expire the last wrapper.
149 if (displayedWrapper != null && displayedWrapper != wrapper)
150 {
151 var lastWrapper = displayedWrapper;
152
153 // If the new wrapper is non-null, we need to wait for the show transformation to complete before hiding the old wrapper,
154 // otherwise, we can hide the old wrapper instantaneously and leave a blank display
155 var hideTransforms = wrapper == null
156 ? ApplyHideTransforms(lastWrapper)
157 : ((Drawable)lastWrapper)?.Delay(TransformDuration)?.Append(ApplyHideTransforms);
158
159 // Expire the last wrapper after the front-most transform has completed (the last wrapper is assumed to be invisible by that point)
160 (showTransforms ?? hideTransforms)?.OnComplete(_ => lastWrapper?.Expire());
161 }
162
163 displayedWrapper = wrapper;
164 }
165
166 /// <summary>
167 /// Creates a <see cref="DelayedLoadWrapper"/> which supports reloading.
168 /// </summary>
169 /// <param name="createContentFunc">A function that creates the wrapped <see cref="Drawable"/>.</param>
170 /// <param name="timeBeforeLoad">The time before loading should begin.</param>
171 /// <returns>A <see cref="DelayedLoadWrapper"/> or null if <paramref name="createContentFunc"/> returns null.</returns>
172 private DelayedLoadWrapper createWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
173 {
174 var content = createContentFunc?.Invoke();
175
176 if (content == null)
177 return null;
178
179 return CreateDelayedLoadWrapper(() =>
180 {
181 try
182 {
183 // optimisation to use already constructed object (used above for null check).
184 return content ?? createContentFunc();
185 }
186 finally
187 {
188 // consume initial object if not already.
189 content = null;
190 }
191 }, timeBeforeLoad);
192 }
193
194 /// <summary>
195 /// Invoked when the <see cref="Drawable"/> representation of a model begins loading.
196 /// </summary>
197 protected virtual void OnLoadStarted()
198 {
199 }
200
201 /// <summary>
202 /// Invoked when the <see cref="Drawable"/> representation of a model has finished loading.
203 /// </summary>
204 protected virtual void OnLoadFinished()
205 {
206 }
207
208 /// <summary>
209 /// Determines whether <see cref="ApplyHideTransforms"/> should be invoked immediately on the currently-displayed drawable when switching to a new model.
210 /// </summary>
211 protected virtual bool TransformImmediately => false;
212
213 /// <summary>
214 /// The default time in milliseconds for transforms applied through <see cref="ApplyHideTransforms"/> and <see cref="ApplyShowTransforms"/>.
215 /// </summary>
216 protected virtual double TransformDuration => 1000;
217
218 /// <summary>
219 /// The delay in milliseconds before <see cref="Drawable"/>s will begin loading.
220 /// </summary>
221 protected virtual double LoadDelay => 0;
222
223 /// <summary>
224 /// Allows subclasses to customise the <see cref="DelayedLoadWrapper"/>.
225 /// </summary>
226 [NotNull]
227 protected virtual DelayedLoadWrapper CreateDelayedLoadWrapper([NotNull] Func<Drawable> createContentFunc, double timeBeforeLoad) =>
228 new DelayedLoadWrapper(createContentFunc(), timeBeforeLoad);
229
230 /// <summary>
231 /// Creates a custom <see cref="Drawable"/> to display a model.
232 /// </summary>
233 /// <param name="model">The model that the <see cref="Drawable"/> should represent.</param>
234 /// <returns>A <see cref="Drawable"/> that represents <paramref name="model"/>, or null if no <see cref="Drawable"/> should be displayed.</returns>
235 [CanBeNull]
236 protected abstract Drawable CreateDrawable([CanBeNull] T model);
237
238 /// <summary>
239 /// Hides a drawable.
240 /// </summary>
241 /// <param name="drawable">The drawable that is to be hidden.</param>
242 /// <returns>The transform sequence.</returns>
243 protected virtual TransformSequence<Drawable> ApplyHideTransforms([CanBeNull] Drawable drawable)
244 => drawable?.FadeOut(TransformDuration, Easing.OutQuint);
245
246 /// <summary>
247 /// Shows a drawable.
248 /// </summary>
249 /// <param name="drawable">The drawable that is to be shown.</param>
250 /// <returns>The transform sequence.</returns>
251 protected virtual TransformSequence<Drawable> ApplyShowTransforms([CanBeNull] Drawable drawable)
252 => drawable?.FadeIn(TransformDuration, Easing.OutQuint);
253 }
254}