A game framework written with osu! in mind.
at master 207 lines 8.6 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 osu.Framework.Extensions; 6using osu.Framework.Extensions.EnumExtensions; 7using osu.Framework.Graphics.Containers; 8using osu.Framework.Graphics.Primitives; 9using osu.Framework.Graphics.UserInterface; 10using osu.Framework.Input.Events; 11using osu.Framework.Utils; 12using osuTK; 13 14#nullable enable 15 16namespace osu.Framework.Graphics.Cursor 17{ 18 public class PopoverContainer : Container 19 { 20 private readonly Container content; 21 private readonly Container dismissOnMouseDownContainer; 22 23 private IHasPopover? target; 24 private Popover? currentPopover; 25 26 protected override Container<Drawable> Content => content; 27 28 public PopoverContainer() 29 { 30 InternalChildren = new Drawable[] 31 { 32 content = new Container 33 { 34 RelativeSizeAxes = Axes.Both, 35 }, 36 dismissOnMouseDownContainer = new DismissOnMouseDownContainer 37 { 38 RelativeSizeAxes = Axes.Both 39 } 40 }; 41 } 42 43 /// <summary> 44 /// Sets the target drawable for this <see cref="PopoverContainer"/> to <paramref name="newTarget"/>. 45 /// </summary> 46 /// <remarks> 47 /// After calling this method, the previous popover shown in this <see cref="PopoverContainer"/> will be hidden. 48 /// This method can be called with a <see langword="null"/> argument to hide the currently-visible popover. 49 /// </remarks> 50 /// <returns><see langword="true"/> if a new popover was shown, <see langword="false"/> otherwise.</returns> 51 internal bool SetTarget(IHasPopover? newTarget) 52 { 53 currentPopover?.Hide(); 54 currentPopover?.Expire(); 55 56 target = newTarget; 57 58 var newPopover = target?.GetPopover(); 59 if (newPopover == null) 60 return false; 61 62 dismissOnMouseDownContainer.Add(currentPopover = newPopover); 63 currentPopover.Show(); 64 return true; 65 } 66 67 protected override void UpdateAfterChildren() 68 { 69 base.UpdateAfterChildren(); 70 71 if ((target as Drawable)?.FindClosestParent<PopoverContainer>() != this || target?.IsPresent != true) 72 { 73 SetTarget(null); 74 return; 75 } 76 77 updatePopoverPositioning(); 78 } 79 80 protected override void OnSizingChanged() 81 { 82 base.OnSizingChanged(); 83 84 // reset to none to prevent exceptions 85 content.RelativeSizeAxes = Axes.None; 86 content.AutoSizeAxes = Axes.None; 87 88 // in addition to using this.RelativeSizeAxes, sets RelativeSizeAxes on every axis that is neither relative size nor auto size 89 content.RelativeSizeAxes = Axes.Both & ~AutoSizeAxes; 90 content.AutoSizeAxes = AutoSizeAxes; 91 } 92 93 /// <summary> 94 /// The <see cref="Anchor"/>s to consider when auto-layouting the popover. 95 /// <see cref="Anchor.Centre"/> is not included, as it is used as a fallback if any other anchor fails. 96 /// </summary> 97 private static readonly Anchor[] candidate_anchors = 98 { 99 Anchor.TopLeft, 100 Anchor.TopCentre, 101 Anchor.TopRight, 102 Anchor.CentreLeft, 103 Anchor.CentreRight, 104 Anchor.BottomLeft, 105 Anchor.BottomCentre, 106 Anchor.BottomRight 107 }; 108 109 private void updatePopoverPositioning() 110 { 111 if (target == null || currentPopover == null) 112 return; 113 114 var targetLocalQuad = ToLocalSpace(target.ScreenSpaceDrawQuad); 115 116 Anchor bestAnchor = Anchor.Centre; 117 float biggestArea = 0; 118 119 float totalSize = Math.Max(DrawSize.X * DrawSize.Y, 1); 120 121 foreach (var anchor in candidate_anchors) 122 { 123 // Compute how much free space is available on this side of the target. 124 var availableSize = availableSizeAroundTargetForAnchor(targetLocalQuad, anchor); 125 float area = availableSize.X * availableSize.Y / totalSize; 126 127 // If the free space is insufficient for the popover to fit in, do not consider this anchor further. 128 if (availableSize.X < currentPopover.BoundingBoxContainer.DrawWidth || availableSize.Y < currentPopover.BoundingBoxContainer.DrawHeight) 129 continue; 130 131 // The heuristic used to find the "best" anchor is the biggest area of free space available in the popover container 132 // on the side of the anchor. 133 if (Precision.DefinitelyBigger(area, biggestArea, 0.01f)) 134 { 135 biggestArea = area; 136 bestAnchor = anchor; 137 } 138 } 139 140 currentPopover.PopoverAnchor = bestAnchor.Opposite(); 141 142 var positionOnQuad = bestAnchor.PositionOnQuad(targetLocalQuad); 143 currentPopover.Position = new Vector2(positionOnQuad.X - Padding.Left, positionOnQuad.Y - Padding.Top); 144 145 // While the side has been chosen to maximise the area of free space available, that doesn't mean that the popover's body 146 // will still fit in its entirety in the default configuration. 147 // To avoid this, offset the popover so that it fits in the bounds of this container. 148 var adjustment = new Vector2(); 149 150 var popoverContentLocalQuad = ToLocalSpace(currentPopover.Body.ScreenSpaceDrawQuad); 151 if (popoverContentLocalQuad.TopLeft.X < 0) 152 adjustment.X = -popoverContentLocalQuad.TopLeft.X; 153 else if (popoverContentLocalQuad.BottomRight.X > DrawWidth) 154 adjustment.X = DrawWidth - popoverContentLocalQuad.BottomRight.X; 155 if (popoverContentLocalQuad.TopLeft.Y < 0) 156 adjustment.Y = -popoverContentLocalQuad.TopLeft.Y; 157 else if (popoverContentLocalQuad.BottomRight.Y > DrawHeight) 158 adjustment.Y = DrawHeight - popoverContentLocalQuad.BottomRight.Y; 159 160 currentPopover.Position += adjustment; 161 162 // Even if the popover was moved, the arrow should stay fixed in place and point at the target's centre. 163 // In such a case, apply a counter-adjustment to the arrow position. 164 // The reason why just the body isn't moved is that the popover's autosize does not play well with that 165 // (setting X/Y on the body can lead BoundingBox to be larger than it actually needs to be, causing 1-frame-errors) 166 currentPopover.Arrow.Position = -adjustment; 167 } 168 169 /// <summary> 170 /// Computes the available size around the <paramref name="targetLocalQuad"/> on the side of it indicated by <paramref name="anchor"/> 171 /// </summary> 172 private Vector2 availableSizeAroundTargetForAnchor(Quad targetLocalQuad, Anchor anchor) 173 { 174 Vector2 availableSize = new Vector2(); 175 176 // left anchor = area to the left of the quad, right anchor = area to the right of the quad. 177 // for horizontal centre assume we have the whole quad width to work with. 178 if (anchor.HasFlagFast(Anchor.x0)) 179 availableSize.X = MathF.Max(0, targetLocalQuad.TopLeft.X); 180 else if (anchor.HasFlagFast(Anchor.x2)) 181 availableSize.X = MathF.Max(0, DrawWidth - targetLocalQuad.BottomRight.X); 182 else 183 availableSize.X = DrawWidth; 184 185 // top anchor = area above quad, bottom anchor = area below quad. 186 // for vertical centre assume we have the whole quad height to work with. 187 if (anchor.HasFlagFast(Anchor.y0)) 188 availableSize.Y = MathF.Max(0, targetLocalQuad.TopLeft.Y); 189 else if (anchor.HasFlagFast(Anchor.y2)) 190 availableSize.Y = MathF.Max(0, DrawHeight - targetLocalQuad.BottomRight.Y); 191 else 192 availableSize.Y = DrawHeight; 193 194 // the final size is the intersection of the X/Y areas. 195 return availableSize; 196 } 197 198 private class DismissOnMouseDownContainer : Container 199 { 200 protected override bool OnMouseDown(MouseDownEvent e) 201 { 202 this.HidePopover(); 203 return false; 204 } 205 } 206 } 207}