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 System.Reflection;
8using osu.Framework.Allocation;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Cursor;
11using osu.Framework.Graphics.Effects;
12using osu.Framework.Graphics.Primitives;
13using osu.Framework.Input;
14using osu.Framework.Input.Events;
15using osu.Framework.Utils;
16using osuTK;
17
18namespace osu.Framework.Graphics.Visualisation
19{
20 [Cached]
21 // Implementing IRequireHighFrequencyMousePosition is necessary to gain the ability to block high frequency mouse position updates.
22 internal class DrawVisualiser : OverlayContainer, IContainVisualisedDrawables, IRequireHighFrequencyMousePosition
23 {
24 public Vector2 ToolPosition
25 {
26 get => treeContainer.Position;
27 set => treeContainer.Position = value;
28 }
29
30 [Cached]
31 private readonly TreeContainer treeContainer;
32
33 private VisualisedDrawable highlightedTarget;
34 private readonly DrawableInspector drawableInspector;
35 private readonly InfoOverlay overlay;
36 private InputManager inputManager;
37
38 public DrawVisualiser()
39 {
40 RelativeSizeAxes = Axes.Both;
41 Children = new Drawable[]
42 {
43 overlay = new InfoOverlay(),
44 treeContainer = new TreeContainer
45 {
46 State = { BindTarget = State },
47 ChooseTarget = () =>
48 {
49 Searching = true;
50 Target = null;
51 },
52 GoUpOneParent = goUpOneParent,
53 ToggleInspector = toggleInspector,
54 },
55 new CursorContainer()
56 };
57
58 drawableInspector = treeContainer.DrawableInspector;
59
60 drawableInspector.State.ValueChanged += v =>
61 {
62 switch (v.NewValue)
63 {
64 case Visibility.Hidden:
65 // Dehighlight everything automatically if property display is closed
66 setHighlight(null);
67 break;
68 }
69 };
70 }
71
72 private void goUpOneParent()
73 {
74 Drawable lastHighlight = highlightedTarget?.Target;
75
76 var parent = Target?.Parent;
77
78 if (parent != null)
79 {
80 var lastVisualiser = targetVisualiser;
81
82 Target = parent;
83 lastVisualiser.SetContainer(targetVisualiser);
84
85 targetVisualiser.Expand();
86 }
87
88 // Rehighlight the last highlight
89 if (lastHighlight != null)
90 {
91 VisualisedDrawable visualised = targetVisualiser.FindVisualisedDrawable(lastHighlight);
92
93 if (visualised != null)
94 {
95 drawableInspector.Show();
96 setHighlight(visualised);
97 }
98 }
99 }
100
101 private void toggleInspector()
102 {
103 if (targetVisualiser == null)
104 return;
105
106 drawableInspector.ToggleVisibility();
107
108 if (drawableInspector.State.Value == Visibility.Visible)
109 setHighlight(targetVisualiser);
110 }
111
112 protected override void LoadComplete()
113 {
114 base.LoadComplete();
115 inputManager = GetContainingInputManager();
116 }
117
118 protected override bool Handle(UIEvent e) => Searching;
119
120 protected override void PopIn()
121 {
122 this.FadeIn(100);
123 Searching = Target == null;
124 }
125
126 protected override void PopOut()
127 {
128 this.FadeOut(100);
129
130 setHighlight(null);
131 drawableInspector.Hide();
132
133 recycleVisualisers();
134 }
135
136 void IContainVisualisedDrawables.AddVisualiser(VisualisedDrawable visualiser)
137 {
138 visualiser.RequestTarget = d =>
139 {
140 Target = d;
141 targetVisualiser.ExpandAll();
142 };
143
144 visualiser.HighlightTarget = d =>
145 {
146 drawableInspector.Show();
147
148 // Either highlight or dehighlight the target, depending on whether
149 // it is currently highlighted
150 setHighlight(d);
151 };
152
153 visualiser.Depth = 0;
154
155 treeContainer.Target = targetVisualiser = visualiser;
156 targetVisualiser.TopLevel = true;
157 }
158
159 void IContainVisualisedDrawables.RemoveVisualiser(VisualisedDrawable visualiser)
160 {
161 target = null;
162
163 targetVisualiser.TopLevel = false;
164 targetVisualiser = null;
165
166 treeContainer.Target = null;
167
168 if (Target == null)
169 drawableInspector.Hide();
170 }
171
172 private VisualisedDrawable targetVisualiser;
173 private Drawable target;
174
175 public Drawable Target
176 {
177 get => target;
178 set
179 {
180 if (target != null)
181 {
182 GetVisualiserFor(target).SetContainer(null);
183 targetVisualiser = null;
184 }
185
186 target = value;
187
188 if (target != null)
189 {
190 targetVisualiser = GetVisualiserFor(target);
191 targetVisualiser.SetContainer(this);
192 }
193 }
194 }
195
196 private Drawable cursorTarget;
197
198 protected override void Update()
199 {
200 base.Update();
201
202 updateCursorTarget();
203 overlay.Target = Searching ? cursorTarget : inputManager.HoveredDrawables.OfType<VisualisedDrawable>().FirstOrDefault()?.Target;
204 }
205
206 private void updateCursorTarget()
207 {
208 Drawable drawableTarget = null;
209 CompositeDrawable compositeTarget = null;
210 Quad? maskingQuad = null;
211
212 findTarget(inputManager);
213
214 cursorTarget = drawableTarget ?? compositeTarget;
215
216 // Finds the targeted drawable and composite drawable. The search stops if a drawable is targeted.
217 void findTarget(Drawable drawable)
218 {
219 if (drawable == this || drawable is Component)
220 return;
221
222 if (!drawable.IsPresent)
223 return;
224
225 if (drawable.AlwaysPresent && Precision.AlmostEquals(drawable.Alpha, 0f))
226 return;
227
228 if (drawable is CompositeDrawable composite)
229 {
230 Quad? oldMaskingQuad = maskingQuad;
231
232 // BufferedContainers implicitly mask via their frame buffer
233 if (composite.Masking || composite is BufferedContainer)
234 maskingQuad = composite.ScreenSpaceDrawQuad;
235
236 for (int i = composite.AliveInternalChildren.Count - 1; i >= 0; i--)
237 {
238 findTarget(composite.AliveInternalChildren[i]);
239
240 if (drawableTarget != null)
241 return;
242 }
243
244 maskingQuad = oldMaskingQuad;
245
246 if (!validForTarget(composite))
247 return;
248
249 compositeTarget ??= composite;
250
251 // Allow targeting composites that don't have any content but display a border/glow
252
253 if (!composite.Masking)
254 return;
255
256 if (composite.BorderThickness > 0 && composite.BorderColour.Linear.A > 0
257 || composite.EdgeEffect.Type != EdgeEffectType.None && composite.EdgeEffect.Radius > 0 && composite.EdgeEffect.Colour.Linear.A > 0)
258 {
259 drawableTarget = composite;
260 }
261 }
262 else
263 {
264 if (!validForTarget(drawable))
265 return;
266
267 // Special case for full-screen overlays that act as input receptors, but don't display anything
268 if (!hasCustomDrawNode(drawable))
269 return;
270
271 drawableTarget = drawable;
272 }
273 }
274
275 // Valid if the drawable contains the mouse position and the position wouldn't be masked by the parent
276 bool validForTarget(Drawable drawable)
277 => drawable.ScreenSpaceDrawQuad.Contains(inputManager.CurrentState.Mouse.Position)
278 && maskingQuad?.Contains(inputManager.CurrentState.Mouse.Position) != false;
279 }
280
281 private static readonly Dictionary<Type, bool> has_custom_drawnode_cache = new Dictionary<Type, bool>();
282
283 private bool hasCustomDrawNode(Drawable drawable)
284 {
285 var type = drawable.GetType();
286
287 if (has_custom_drawnode_cache.TryGetValue(type, out var existing))
288 return existing;
289
290 return has_custom_drawnode_cache[type] = type.GetMethod(nameof(CreateDrawNode), BindingFlags.Instance | BindingFlags.NonPublic)?.DeclaringType != typeof(Drawable);
291 }
292
293 public bool Searching { get; private set; }
294
295 private void setHighlight(VisualisedDrawable newHighlight)
296 {
297 if (highlightedTarget != null)
298 {
299 // Dehighlight the lastly highlighted target
300 highlightedTarget.IsHighlighted = false;
301 highlightedTarget = null;
302 }
303
304 if (newHighlight == null)
305 {
306 drawableInspector.InspectedDrawable.Value = null;
307 return;
308 }
309
310 // Only update when property display is visible
311 if (drawableInspector.State.Value == Visibility.Visible)
312 {
313 highlightedTarget = newHighlight;
314 newHighlight.IsHighlighted = true;
315
316 drawableInspector.InspectedDrawable.Value = newHighlight.Target;
317 }
318 }
319
320 protected override bool OnMouseDown(MouseDownEvent e) => Searching;
321
322 protected override bool OnClick(ClickEvent e)
323 {
324 if (Searching)
325 {
326 Target = cursorTarget?.Parent;
327
328 if (Target != null)
329 {
330 overlay.Target = null;
331 targetVisualiser.ExpandAll();
332
333 Searching = false;
334 return true;
335 }
336 }
337
338 return base.OnClick(e);
339 }
340
341 private readonly Dictionary<Drawable, VisualisedDrawable> visCache = new Dictionary<Drawable, VisualisedDrawable>();
342
343 public VisualisedDrawable GetVisualiserFor(Drawable drawable)
344 {
345 if (visCache.TryGetValue(drawable, out var existing))
346 return existing;
347
348 var vis = new VisualisedDrawable(drawable);
349 vis.OnDispose += () => visCache.Remove(vis.Target);
350
351 return visCache[drawable] = vis;
352 }
353
354 private void recycleVisualisers()
355 {
356 treeContainer.Target = null;
357
358 // We don't really know where the visualised drawables are, so we have to dispose them manually
359 // This is done as an optimisation so that events aren't handled while the visualiser is hidden
360 var visualisers = visCache.Values.ToList();
361 foreach (var v in visualisers)
362 v.Dispose();
363
364 target = null;
365 targetVisualiser = null;
366 }
367 }
368}