A game framework written with osu! in mind.
at master 368 lines 12 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 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}