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.Linq;
6using NUnit.Framework;
7using osu.Framework.Extensions.EnumExtensions;
8using osu.Framework.Graphics;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Cursor;
11using osu.Framework.Graphics.Shapes;
12using osu.Framework.Graphics.UserInterface;
13using osu.Framework.Utils;
14using osu.Framework.Testing;
15using osuTK;
16using osuTK.Graphics;
17using osuTK.Input;
18
19namespace osu.Framework.Tests.Visual.UserInterface
20{
21 public class TestSceneContextMenu : ManualInputManagerTestScene
22 {
23 protected override Container<Drawable> Content => contextMenuContainer ?? base.Content;
24
25 private readonly TestContextMenuContainer contextMenuContainer;
26
27 public TestSceneContextMenu()
28 {
29 base.Content.Add(contextMenuContainer = new TestContextMenuContainer { RelativeSizeAxes = Axes.Both });
30 }
31
32 [SetUp]
33 public void Setup() => Schedule(Clear);
34
35 [Test]
36 public void TestMenuOpenedOnClick()
37 {
38 Drawable box = null;
39
40 addBoxStep(b => box = b, 1);
41 clickBoxStep(() => box);
42
43 assertMenuState(true);
44 }
45
46 [Test]
47 public void TestMenuClosedOnClickOutside()
48 {
49 Drawable box = null;
50
51 addBoxStep(b => box = b, 1);
52 clickBoxStep(() => box);
53
54 clickOutsideStep();
55 assertMenuState(false);
56 }
57
58 [Test]
59 public void TestMenuTransferredToNewTarget()
60 {
61 Drawable box1 = null;
62 Drawable box2 = null;
63
64 addBoxStep(b =>
65 {
66 box1 = b.With(d =>
67 {
68 d.X = -100;
69 d.Colour = Color4.Green;
70 });
71 }, 1);
72 addBoxStep(b =>
73 {
74 box2 = b.With(d =>
75 {
76 d.X = 100;
77 d.Colour = Color4.Red;
78 });
79 }, 1);
80
81 clickBoxStep(() => box1);
82 clickBoxStep(() => box2);
83
84 assertMenuState(true);
85 assertMenuInCentre(() => box2);
86 }
87
88 [Test]
89 public void TestMenuHiddenWhenTargetHidden()
90 {
91 Drawable box = null;
92
93 addBoxStep(b => box = b, 1);
94 clickBoxStep(() => box);
95
96 AddStep("hide box", () => box.Hide());
97 assertMenuState(false);
98 }
99
100 [Test]
101 public void TestMenuTracksMovement()
102 {
103 Drawable box = null;
104
105 addBoxStep(b => box = b, 1);
106 clickBoxStep(() => box);
107
108 AddStep("move box", () => box.X += 100);
109 assertMenuInCentre(() => box);
110 }
111
112 [TestCase(Anchor.TopLeft)]
113 [TestCase(Anchor.TopCentre)]
114 [TestCase(Anchor.TopRight)]
115 [TestCase(Anchor.CentreLeft)]
116 [TestCase(Anchor.CentreRight)]
117 [TestCase(Anchor.BottomLeft)]
118 [TestCase(Anchor.BottomCentre)]
119 [TestCase(Anchor.BottomRight)]
120 public void TestMenuOnScreenWhenTargetPartlyOffScreen(Anchor anchor)
121 {
122 Drawable box = null;
123
124 addBoxStep(b => box = b, 5);
125 clickBoxStep(() => box);
126
127 AddStep($"move box to {anchor.ToString()}", () =>
128 {
129 box.Anchor = anchor;
130 box.X -= 5;
131 box.Y -= 5;
132 });
133
134 assertMenuOnScreen(true);
135 }
136
137 [TestCase(Anchor.TopLeft)]
138 [TestCase(Anchor.TopCentre)]
139 [TestCase(Anchor.TopRight)]
140 [TestCase(Anchor.CentreLeft)]
141 [TestCase(Anchor.CentreRight)]
142 [TestCase(Anchor.BottomLeft)]
143 [TestCase(Anchor.BottomCentre)]
144 [TestCase(Anchor.BottomRight)]
145 public void TestMenuNotOnScreenWhenTargetSignificantlyOffScreen(Anchor anchor)
146 {
147 Drawable box = null;
148
149 addBoxStep(b => box = b, 5);
150 clickBoxStep(() => box);
151
152 AddStep($"move box to {anchor.ToString()}", () =>
153 {
154 box.Anchor = anchor;
155
156 if (anchor.HasFlagFast(Anchor.x0))
157 box.X -= contextMenuContainer.CurrentMenu.DrawWidth + 10;
158 else if (anchor.HasFlagFast(Anchor.x2))
159 box.X += 10;
160
161 if (anchor.HasFlagFast(Anchor.y0))
162 box.Y -= contextMenuContainer.CurrentMenu.DrawHeight + 10;
163 else if (anchor.HasFlagFast(Anchor.y2))
164 box.Y += 10;
165 });
166
167 assertMenuOnScreen(false);
168 }
169
170 [Test]
171 public void TestReturnNullInNestedDrawableOpensParentMenu()
172 {
173 Drawable box2 = null;
174
175 addBoxStep(_ => { }, 2);
176 addBoxStep(b => box2 = b, null);
177
178 clickBoxStep(() => box2);
179 assertMenuState(true);
180 assertMenuItems(2);
181 }
182
183 [Test]
184 public void TestReturnEmptyInNestedDrawableBlocksMenuOpening()
185 {
186 Drawable box2 = null;
187
188 addBoxStep(_ => { }, 2);
189 addBoxStep(b => box2 = b);
190
191 clickBoxStep(() => box2);
192 assertMenuState(false);
193 }
194
195 private void clickBoxStep(Func<Drawable> getBoxFunc)
196 {
197 AddStep("right-click box", () =>
198 {
199 InputManager.MoveMouseTo(getBoxFunc());
200 InputManager.Click(MouseButton.Right);
201 });
202 }
203
204 private void clickOutsideStep()
205 {
206 AddStep("click outside", () =>
207 {
208 InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopLeft);
209 InputManager.Click(MouseButton.Right);
210 });
211 }
212
213 private void addBoxStep(Action<Drawable> boxFunc, int actionCount) => addBoxStep(boxFunc, Enumerable.Repeat(new Action(() => { }), actionCount).ToArray());
214
215 private void addBoxStep(Action<Drawable> boxFunc, params Action[] actions)
216 {
217 AddStep("add box", () =>
218 {
219 var box = new BoxWithContextMenu(actions)
220 {
221 Anchor = Anchor.Centre,
222 Origin = Anchor.Centre,
223 Size = new Vector2(200),
224 };
225
226 Add(box);
227 boxFunc?.Invoke(box);
228 });
229 }
230
231 private void assertMenuState(bool opened)
232 => AddAssert($"menu {(opened ? "opened" : "closed")}", () => (contextMenuContainer.CurrentMenu?.State == MenuState.Open) == opened);
233
234 private void assertMenuInCentre(Func<Drawable> getBoxFunc)
235 => AddAssert("menu in centre of box", () => Precision.AlmostEquals(contextMenuContainer.CurrentMenu.ScreenSpaceDrawQuad.TopLeft, getBoxFunc().ScreenSpaceDrawQuad.Centre));
236
237 private void assertMenuOnScreen(bool expected) => AddAssert($"menu {(expected ? "on" : "off")} screen", () =>
238 {
239 var inputQuad = InputManager.ScreenSpaceDrawQuad;
240 var menuQuad = contextMenuContainer.CurrentMenu.ScreenSpaceDrawQuad;
241
242 bool result = inputQuad.Contains(menuQuad.TopLeft + new Vector2(1, 1))
243 && inputQuad.Contains(menuQuad.TopRight + new Vector2(-1, 1))
244 && inputQuad.Contains(menuQuad.BottomLeft + new Vector2(1, -1))
245 && inputQuad.Contains(menuQuad.BottomRight + new Vector2(-1, -1));
246
247 return result == expected;
248 });
249
250 private void assertMenuItems(int expectedCount) => AddAssert($"menu contains {expectedCount} item(s)", () => contextMenuContainer.CurrentMenu.Items.Count == expectedCount);
251
252 private class BoxWithContextMenu : Box, IHasContextMenu
253 {
254 private readonly Action[] actions;
255
256 public BoxWithContextMenu(Action[] actions)
257 {
258 this.actions = actions;
259 }
260
261 public MenuItem[] ContextMenuItems => actions?.Select((a, i) => new MenuItem($"Item {i}", a)).ToArray();
262 }
263
264 private class TestContextMenuContainer : BasicContextMenuContainer
265 {
266 public Menu CurrentMenu { get; private set; }
267
268 protected override Menu CreateMenu() => CurrentMenu = base.CreateMenu();
269 }
270 }
271}