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 System.Linq;
7using osu.Framework.Graphics.Containers;
8using osu.Framework.Graphics.Sprites;
9using osuTK;
10using osuTK.Graphics;
11using osuTK.Input;
12using osu.Framework.Graphics.Shapes;
13using osu.Framework.Allocation;
14using osu.Framework.Extensions.IEnumerableExtensions;
15using osu.Framework.Graphics.Textures;
16using osu.Framework.Input.Events;
17
18namespace osu.Framework.Graphics.Visualisation
19{
20 internal class VisualisedDrawable : Container, IContainVisualisedDrawables
21 {
22 private const int line_height = 12;
23
24 public Drawable Target { get; }
25
26 private bool isHighlighted;
27
28 public bool IsHighlighted
29 {
30 get => isHighlighted;
31 set
32 {
33 isHighlighted = value;
34
35 updateColours();
36 if (value)
37 Expand();
38 }
39 }
40
41 public Action<Drawable> RequestTarget;
42 public Action<VisualisedDrawable> HighlightTarget;
43
44 private Box background;
45 private SpriteText text;
46 private SpriteText text2;
47 private Drawable previewBox;
48 private Drawable activityInvalidate;
49 private Drawable activityAutosize;
50 private Drawable activityLayout;
51 private VisualisedDrawableFlow flow;
52 private Container connectionContainer;
53
54 private const float row_width = 10;
55 private const float row_height = 20;
56
57 [Resolved]
58 private DrawVisualiser visualiser { get; set; }
59
60 [Resolved]
61 private TreeContainer tree { get; set; }
62
63 public VisualisedDrawable(Drawable d)
64 {
65 Target = d;
66 }
67
68 [BackgroundDependencyLoader]
69 private void load()
70 {
71 RelativeSizeAxes = Axes.X;
72 AutoSizeAxes = Axes.Y;
73
74 var spriteTarget = Target as Sprite;
75
76 AddRange(new Drawable[]
77 {
78 flow = new VisualisedDrawableFlow
79 {
80 Direction = FillDirection.Vertical,
81 RelativeSizeAxes = Axes.X,
82 AutoSizeAxes = Axes.Y,
83 Position = new Vector2(row_width, row_height)
84 },
85 new Container
86 {
87 AutoSizeAxes = Axes.Both,
88 Children = new[]
89 {
90 background = new Box
91 {
92 RelativeSizeAxes = Axes.Both,
93 Size = new Vector2(100, 1), // a bit of a hack, but works well enough.
94 Anchor = Anchor.Centre,
95 Origin = Anchor.Centre,
96 Colour = Color4.Transparent,
97 },
98 activityInvalidate = new Box
99 {
100 Colour = Color4.Yellow,
101 Size = new Vector2(2, line_height),
102 Anchor = Anchor.CentreLeft,
103 Origin = Anchor.CentreLeft,
104 Position = new Vector2(6, 0),
105 Alpha = 0
106 },
107 activityLayout = new Box
108 {
109 Colour = Color4.Orange,
110 Size = new Vector2(2, line_height),
111 Anchor = Anchor.CentreLeft,
112 Origin = Anchor.CentreLeft,
113 Position = new Vector2(3, 0),
114 Alpha = 0
115 },
116 activityAutosize = new Box
117 {
118 Colour = Color4.Red,
119 Size = new Vector2(2, line_height),
120 Anchor = Anchor.CentreLeft,
121 Origin = Anchor.CentreLeft,
122 Position = new Vector2(0, 0),
123 Alpha = 0
124 },
125 previewBox = spriteTarget?.Texture == null
126 ? previewBox = new Box
127 {
128 Colour = Color4.White,
129 Anchor = Anchor.CentreLeft,
130 Origin = Anchor.CentreLeft,
131 }
132 : new Sprite
133 {
134 // It's fine to only bypass the ref count, because this sprite will dispose along with the original sprite
135 Texture = new Texture(spriteTarget.Texture.TextureGL),
136 Scale = new Vector2(spriteTarget.Texture.DisplayWidth / spriteTarget.Texture.DisplayHeight, 1),
137 Anchor = Anchor.CentreLeft,
138 Origin = Anchor.CentreLeft,
139 },
140 new FillFlowContainer
141 {
142 AutoSizeAxes = Axes.Both,
143 Direction = FillDirection.Horizontal,
144 Spacing = new Vector2(5),
145 Position = new Vector2(24, 0),
146 Children = new Drawable[]
147 {
148 text = new SpriteText { Font = FrameworkFont.Regular },
149 text2 = new SpriteText { Font = FrameworkFont.Regular },
150 }
151 },
152 }
153 },
154 });
155
156 const float connection_width = 1;
157
158 AddInternal(connectionContainer = new Container
159 {
160 Colour = FrameworkColour.Green,
161 RelativeSizeAxes = Axes.Y,
162 Width = connection_width,
163 Children = new Drawable[]
164 {
165 new Box
166 {
167 RelativeSizeAxes = Axes.Both,
168 EdgeSmoothness = new Vector2(0.5f),
169 },
170 new Box
171 {
172 Anchor = Anchor.TopRight,
173 Origin = Anchor.CentreLeft,
174 Y = row_height / 2,
175 Width = row_width / 2,
176 EdgeSmoothness = new Vector2(0.5f),
177 }
178 }
179 });
180
181 previewBox.Position = new Vector2(9, 0);
182 previewBox.Size = new Vector2(line_height, line_height);
183
184 var compositeTarget = Target as CompositeDrawable;
185 compositeTarget?.AliveInternalChildren.ForEach(addChild);
186
187 updateSpecifics();
188 }
189
190 protected override void LoadComplete()
191 {
192 base.LoadComplete();
193
194 attachEvents();
195 updateColours();
196 }
197
198 public bool TopLevel
199 {
200 set => connectionContainer.Alpha = value ? 0 : 1;
201 }
202
203 private void attachEvents()
204 {
205 Target.Invalidated += onInvalidated;
206 Target.OnDispose += onDispose;
207
208 if (Target is CompositeDrawable da)
209 {
210 da.OnAutoSize += onAutoSize;
211 da.ChildBecameAlive += addChild;
212 da.ChildDied += removeChild;
213 da.ChildDepthChanged += depthChanged;
214 }
215
216 if (Target is FlowContainer<Drawable> df) df.OnLayout += onLayout;
217 }
218
219 private void detachEvents()
220 {
221 Target.Invalidated -= onInvalidated;
222 Target.OnDispose -= onDispose;
223
224 if (Target is CompositeDrawable da)
225 {
226 da.OnAutoSize -= onAutoSize;
227 da.ChildBecameAlive -= addChild;
228 da.ChildDied -= removeChild;
229 da.ChildDepthChanged -= depthChanged;
230 }
231
232 if (Target is FlowContainer<Drawable> df) df.OnLayout -= onLayout;
233 }
234
235 private void addChild(Drawable drawable)
236 {
237 // Make sure to never add the DrawVisualiser (recursive scenario)
238 if (drawable == visualiser) return;
239
240 // Don't add individual characters of SpriteText
241 if (Target is SpriteText) return;
242
243 visualiser.GetVisualiserFor(drawable).SetContainer(this);
244 }
245
246 private void removeChild(Drawable drawable)
247 {
248 var vis = visualiser.GetVisualiserFor(drawable);
249 if (vis.currentContainer == this)
250 vis.SetContainer(null);
251 }
252
253 private void depthChanged(Drawable drawable)
254 {
255 var vis = visualiser.GetVisualiserFor(drawable);
256
257 vis.currentContainer?.RemoveVisualiser(vis);
258 vis.currentContainer?.AddVisualiser(vis);
259 }
260
261 void IContainVisualisedDrawables.AddVisualiser(VisualisedDrawable visualiser)
262 {
263 visualiser.RequestTarget = d => RequestTarget?.Invoke(d);
264 visualiser.HighlightTarget = d => HighlightTarget?.Invoke(d);
265
266 visualiser.Depth = visualiser.Target.Depth;
267
268 flow.Add(visualiser);
269 }
270
271 void IContainVisualisedDrawables.RemoveVisualiser(VisualisedDrawable visualiser) => flow.Remove(visualiser);
272
273 public VisualisedDrawable FindVisualisedDrawable(Drawable drawable)
274 {
275 if (drawable == Target)
276 return this;
277
278 foreach (var child in flow)
279 {
280 var vis = child.FindVisualisedDrawable(drawable);
281 if (vis != null)
282 return vis;
283 }
284
285 return null;
286 }
287
288 protected override void Dispose(bool isDisposing)
289 {
290 detachEvents();
291 base.Dispose(isDisposing);
292 }
293
294 protected override bool OnHover(HoverEvent e)
295 {
296 updateColours();
297 return base.OnHover(e);
298 }
299
300 protected override void OnHoverLost(HoverLostEvent e)
301 {
302 updateColours();
303 base.OnHoverLost(e);
304 }
305
306 private void updateColours()
307 {
308 if (isHighlighted)
309 {
310 background.Colour = FrameworkColour.YellowGreen;
311 text.Colour = FrameworkColour.Blue;
312 text2.Colour = FrameworkColour.Blue;
313 }
314 else if (IsHovered)
315 {
316 background.Colour = FrameworkColour.BlueGreen;
317 text.Colour = Color4.White;
318 text2.Colour = FrameworkColour.YellowGreen;
319 }
320 else
321 {
322 background.Colour = Color4.Transparent;
323 text.Colour = Color4.White;
324 text2.Colour = FrameworkColour.YellowGreen;
325 }
326 }
327
328 protected override bool OnMouseDown(MouseDownEvent e)
329 {
330 if (e.Button == MouseButton.Right)
331 {
332 HighlightTarget?.Invoke(this);
333 return true;
334 }
335
336 return false;
337 }
338
339 protected override bool OnClick(ClickEvent e)
340 {
341 if (isExpanded)
342 Collapse();
343 else
344 Expand();
345 return true;
346 }
347
348 protected override bool OnDoubleClick(DoubleClickEvent e)
349 {
350 RequestTarget?.Invoke(Target);
351 return true;
352 }
353
354 private bool isExpanded = true;
355
356 public void Expand()
357 {
358 flow.FadeIn();
359 updateSpecifics();
360
361 isExpanded = true;
362 }
363
364 public void ExpandAll()
365 {
366 Expand();
367 flow.ForEach(f => f.Expand());
368 }
369
370 public void Collapse()
371 {
372 flow.FadeOut();
373 updateSpecifics();
374
375 isExpanded = false;
376 }
377
378 private void onAutoSize() => activityAutosize.FadeOutFromOne(1);
379
380 private void onLayout() => activityLayout.FadeOutFromOne(1);
381
382 private void onInvalidated(Drawable d) => activityInvalidate.FadeOutFromOne(1);
383
384 private void onDispose()
385 {
386 // May come from the disposal thread, in which case they won't ever be reused and the container doesn't need to be reset
387 Schedule(() => SetContainer(null));
388 }
389
390 private void updateSpecifics()
391 {
392 Vector2 posInTree = ToSpaceOfOtherDrawable(Vector2.Zero, tree);
393
394 if (posInTree.Y < -previewBox.DrawHeight || posInTree.Y > tree.Height)
395 {
396 text.Text = string.Empty;
397 return;
398 }
399
400 previewBox.Alpha = Math.Max(0.2f, Target.Alpha);
401 previewBox.Colour = Target.Colour;
402
403 int childCount = (Target as CompositeDrawable)?.InternalChildren.Count ?? 0;
404
405 text.Text = Target.ToString();
406 text2.Text = $"({Target.DrawPosition.X:#,0},{Target.DrawPosition.Y:#,0}) {Target.DrawSize.X:#,0}x{Target.DrawSize.Y:#,0}"
407 + (!isExpanded && childCount > 0 ? $@" ({childCount} children)" : string.Empty);
408
409 Alpha = Target.IsPresent ? 1 : 0.3f;
410 }
411
412 protected override void Update()
413 {
414 updateSpecifics();
415 base.Update();
416 }
417
418 private IContainVisualisedDrawables currentContainer;
419
420 /// <summary>
421 /// Moves this <see cref="VisualisedDrawable"/> to be contained by another target.
422 /// </summary>
423 /// <remarks>
424 /// The <see cref="VisualisedDrawable"/> is first removed from its current container via <see cref="IContainVisualisedDrawables.RemoveVisualiser"/>,
425 /// prior to being added to the new container via <see cref="IContainVisualisedDrawables.AddVisualiser"/>.
426 /// </remarks>
427 /// <param name="container">The target which should contain this <see cref="VisualisedDrawable"/>.</param>
428 public void SetContainer(IContainVisualisedDrawables container)
429 {
430 currentContainer?.RemoveVisualiser(this);
431
432 // The visualised may have previously been within a container (e.g. flow), which repositioned it
433 // We should make sure that the position is reset before it's added to another container
434 Y = 0;
435
436 container?.AddVisualiser(this);
437
438 currentContainer = container;
439 }
440
441 private class VisualisedDrawableFlow : FillFlowContainer<VisualisedDrawable>
442 {
443 public override IEnumerable<Drawable> FlowingChildren => AliveInternalChildren.Where(d => d.IsPresent).OrderBy(d => -d.Depth).ThenBy(d => ((VisualisedDrawable)d).Target.ChildID);
444 }
445 }
446}