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 osu.Framework.Extensions;
6using osu.Framework.Extensions.EnumExtensions;
7using osu.Framework.Graphics.Containers;
8using osu.Framework.Graphics.Shapes;
9using osu.Framework.Input.Events;
10using osuTK;
11using osuTK.Input;
12
13namespace osu.Framework.Graphics.UserInterface
14{
15 /// <summary>
16 /// A <see cref="Popover"/> is a transient view that appears above other on-screen content.
17 /// It typically is activated by another control and includes an arrow pointing to the location from which it emerged.
18 /// (loosely paraphrasing: https://developer.apple.com/design/human-interface-guidelines/ios/views/popovers/)
19 /// </summary>
20 public abstract class Popover : FocusedOverlayContainer
21 {
22 protected override bool BlockPositionalInput => true;
23
24 public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Body.ReceivePositionalInputAt(screenSpacePos) || Arrow.ReceivePositionalInputAt(screenSpacePos);
25
26 public override bool HandleNonPositionalInput => State.Value == Visibility.Visible;
27
28 protected override bool OnKeyDown(KeyDownEvent e)
29 {
30 if (e.Key == Key.Escape)
31 {
32 this.HidePopover();
33 return true;
34 }
35
36 return base.OnKeyDown(e);
37 }
38
39 /// <summary>
40 /// The <see cref="Anchor"/> that this <see cref="Popover"/> is to be attached to the triggering UI control by.
41 /// </summary>
42 public Anchor PopoverAnchor
43 {
44 get => Anchor;
45 set
46 {
47 BoundingBoxContainer.Origin = value;
48 BoundingBoxContainer.Anchor = value.Opposite();
49
50 Body.Anchor = Body.Origin = value;
51 Arrow.Anchor = value;
52 Arrow.Rotation = getRotationFor(value);
53 Arrow.Alpha = value == Anchor.Centre ? 0 : 1;
54 AnchorUpdated(value);
55 }
56 }
57
58 /// <summary>
59 /// The container holding all of this popover's elements (the <see cref="Body"/> and the <see cref="Arrow"/>).
60 /// </summary>
61 internal Container BoundingBoxContainer { get; }
62
63 /// <summary>
64 /// The background box of the popover.
65 /// </summary>
66 protected Box Background { get; }
67
68 /// <summary>
69 /// The arrow of this <see cref="Popover"/>, pointing at the component which triggered it.
70 /// </summary>
71 protected internal Drawable Arrow { get; }
72
73 /// <summary>
74 /// The body of this <see cref="Popover"/>, containing the actual contents.
75 /// </summary>
76 protected internal Container Body { get; }
77
78 protected override Container<Drawable> Content { get; } = new Container { AutoSizeAxes = Axes.Both };
79
80 protected Popover()
81 {
82 base.AddInternal(BoundingBoxContainer = new Container
83 {
84 AutoSizeAxes = Axes.Both,
85 Children = new[]
86 {
87 Arrow = CreateArrow(),
88 Body = new Container
89 {
90 AutoSizeAxes = Axes.Both,
91 Children = new Drawable[]
92 {
93 Background = new Box
94 {
95 RelativeSizeAxes = Axes.Both,
96 },
97 Content
98 },
99 }
100 }
101 });
102 }
103
104 /// <summary>
105 /// Creates an arrow drawable that points away from the given <see cref="Anchor"/>.
106 /// </summary>
107 protected abstract Drawable CreateArrow();
108
109 protected override void PopIn() => this.FadeIn();
110 protected override void PopOut() => this.FadeOut();
111
112 /// <summary>
113 /// Called when <see cref="Anchor"/> is set.
114 /// Can be used to apply custom layout updates to the subcomponents.
115 /// </summary>
116 protected virtual void AnchorUpdated(Anchor anchor)
117 {
118 }
119
120 private float getRotationFor(Anchor anchor)
121 {
122 switch (anchor)
123 {
124 case Anchor.TopLeft:
125 return -45;
126
127 case Anchor.TopCentre:
128 default:
129 return 0;
130
131 case Anchor.TopRight:
132 return 45;
133
134 case Anchor.CentreLeft:
135 return -90;
136
137 case Anchor.CentreRight:
138 return 90;
139
140 case Anchor.BottomLeft:
141 return -135;
142
143 case Anchor.BottomCentre:
144 return -180;
145
146 case Anchor.BottomRight:
147 return 135;
148 }
149 }
150
151 protected internal sealed override void AddInternal(Drawable drawable) => throw new InvalidOperationException($"Use {nameof(Content)} instead.");
152
153 #region Sizing delegation
154
155 // Popovers rely on being 0x0 sized and placed exactly at the attachment point to their drawable for layouting logic.
156 // This can cause undesirable results if somebody tries to directly set the Width/Height of a popover, expecting the body to be resized.
157 // This is done via shadowing rather than overrides, because we still want framework to read the base 0x0 size.
158
159 public new float Width
160 {
161 get => Body.Width;
162 set
163 {
164 if (Body.AutoSizeAxes.HasFlagFast(Axes.X))
165 Body.AutoSizeAxes &= ~Axes.X;
166
167 Body.Width = value;
168 }
169 }
170
171 public new float Height
172 {
173 get => Body.Height;
174 set
175 {
176 if (Body.AutoSizeAxes.HasFlagFast(Axes.Y))
177 Body.AutoSizeAxes &= ~Axes.Y;
178
179 Body.Height = value;
180 }
181 }
182
183 public new Vector2 Size
184 {
185 get => Body.Size;
186 set
187 {
188 Width = value.X;
189 Height = value.Y;
190 }
191 }
192
193 #endregion
194 }
195}