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.Threading;
8using NUnit.Framework;
9using osu.Framework.Allocation;
10using osu.Framework.Bindables;
11using osu.Framework.Graphics;
12using osu.Framework.Graphics.Containers;
13using osu.Framework.Graphics.UserInterface;
14using osu.Framework.Testing;
15using osuTK;
16using osuTK.Input;
17
18namespace osu.Framework.Tests.Visual.UserInterface
19{
20 public class TestSceneRearrangeableListContainer : ManualInputManagerTestScene
21 {
22 private TestRearrangeableList list;
23
24 private Container listContainer;
25
26 [SetUp]
27 public void Setup() => Schedule(() =>
28 {
29 Child = listContainer = new Container
30 {
31 Anchor = Anchor.Centre,
32 Origin = Anchor.Centre,
33 Size = new Vector2(500, 300),
34 Child = list = new TestRearrangeableList { RelativeSizeAxes = Axes.Both }
35 };
36 });
37
38 [Test]
39 public void TestAddItem()
40 {
41 for (int i = 0; i < 5; i++)
42 {
43 int localI = i;
44
45 addItems(1);
46 AddAssert($"last item is \"{i}\"", () => list.ChildrenOfType<RearrangeableListItem<int>>().Last().Model == localI);
47 }
48 }
49
50 [Test]
51 public void TestBindBeforeLoad()
52 {
53 AddStep("create list", () => list = new TestRearrangeableList { RelativeSizeAxes = Axes.Both });
54 AddStep("bind list to items", () => list.Items.BindTo(new BindableList<int>(new[] { 1, 2, 3 })));
55 AddStep("add list to hierarchy", () => listContainer.Add(list));
56 }
57
58 [Test]
59 public void TestAddDuplicateItemsFails()
60 {
61 const int item = 1;
62
63 AddStep("add item 1", () => list.Items.Add(item));
64
65 AddAssert("add same item throws", () =>
66 {
67 try
68 {
69 list.Items.Add(item);
70 return false;
71 }
72 catch (InvalidOperationException)
73 {
74 return true;
75 }
76 });
77 }
78
79 [Test]
80 public void TestRemoveItem()
81 {
82 addItems(5);
83
84 for (int i = 0; i < 5; i++)
85 {
86 int localI = i;
87
88 AddStep($"remove item \"{i}\"", () => list.Items.Remove(localI));
89 AddAssert($"first item is not \"{i}\"", () => list.ChildrenOfType<RearrangeableListItem<int>>().FirstOrDefault()?.Model != localI);
90 }
91 }
92
93 [Test]
94 public void TestClearItems()
95 {
96 addItems(5);
97
98 AddStep("clear items", () => list.Items.Clear());
99
100 AddAssert("no items contained", () => !list.ChildrenOfType<RearrangeableListItem<string>>().Any());
101 }
102
103 [Test]
104 public void TestRearrangeByDrag()
105 {
106 addItems(5);
107
108 addDragSteps(1, 4, new[] { 0, 2, 3, 4, 1 });
109 addDragSteps(1, 3, new[] { 0, 2, 1, 3, 4 });
110 addDragSteps(0, 3, new[] { 2, 1, 3, 0, 4 });
111 addDragSteps(3, 4, new[] { 2, 1, 0, 4, 3 });
112 addDragSteps(4, 2, new[] { 4, 2, 1, 0, 3 });
113 addDragSteps(2, 4, new[] { 2, 4, 1, 0, 3 });
114 }
115
116 [Test]
117 public void TestRearrangeByDragWithHiddenItems()
118 {
119 addItems(6);
120
121 AddStep("hide item zero", () => list.ListContainer.First(i => i.Model == 0).Hide());
122
123 addDragSteps(2, 5, new[] { 0, 1, 3, 4, 5, 2 });
124 addDragSteps(2, 4, new[] { 0, 1, 3, 2, 4, 5 });
125 addDragSteps(1, 4, new[] { 0, 3, 2, 4, 1, 5 });
126 addDragSteps(4, 5, new[] { 0, 3, 2, 1, 5, 4 });
127 addDragSteps(5, 3, new[] { 0, 5, 3, 2, 1, 4 });
128 addDragSteps(3, 5, new[] { 0, 3, 5, 2, 1, 4 });
129 }
130
131 [Test]
132 public void TestRearrangeByDragAfterRemoval()
133 {
134 addItems(5);
135
136 addDragSteps(0, 4, new[] { 1, 2, 3, 4, 0 });
137 addDragSteps(1, 4, new[] { 2, 3, 4, 1, 0 });
138 addDragSteps(2, 4, new[] { 3, 4, 2, 1, 0 });
139 addDragSteps(3, 4, new[] { 4, 3, 2, 1, 0 });
140
141 AddStep("remove 3 and 2", () =>
142 {
143 list.Items.Remove(3);
144 list.Items.Remove(2);
145 });
146
147 addDragSteps(4, 0, new[] { 1, 0, 4 });
148 addDragSteps(0, 1, new[] { 0, 1, 4 });
149 addDragSteps(4, 0, new[] { 4, 0, 1 });
150 }
151
152 [Test]
153 public void TestRemoveAfterDragScrollThenTryRearrange()
154 {
155 addItems(5);
156
157 // Scroll
158 AddStep("move mouse to first item", () => InputManager.MoveMouseTo(getItem(0)));
159 AddStep("begin a drag", () => InputManager.PressButton(MouseButton.Left));
160 AddStep("move the mouse", () => InputManager.MoveMouseTo(getItem(0), new Vector2(0, 30)));
161 AddStep("end the drag", () => InputManager.ReleaseButton(MouseButton.Left));
162
163 AddStep("remove all but one item", () =>
164 {
165 for (int i = 0; i < 4; i++)
166 list.Items.Remove(getItem(i).Model);
167 });
168
169 // Drag
170 AddStep("move mouse to first dragger", () => InputManager.MoveMouseTo(getDragger(4)));
171 AddStep("begin a drag", () => InputManager.PressButton(MouseButton.Left));
172 AddStep("move the mouse", () => InputManager.MoveMouseTo(getDragger(4), new Vector2(0, 30)));
173 AddStep("end the drag", () => InputManager.ReleaseButton(MouseButton.Left));
174 }
175
176 [Test]
177 public void TestScrolledWhenDraggedToBoundaries()
178 {
179 addItems(100);
180
181 AddStep("scroll to item 50", () => list.ScrollTo(50));
182
183 float scrollPosition = 0;
184 AddStep("get scroll position", () => scrollPosition = list.ScrollPosition);
185
186 AddStep("move to 52", () =>
187 {
188 InputManager.MoveMouseTo(getDragger(52));
189 InputManager.PressButton(MouseButton.Left);
190 });
191
192 AddStep("drag to 0", () => InputManager.MoveMouseTo(getDragger(0), new Vector2(0, -1)));
193
194 AddUntilStep("scrolling up", () => list.ScrollPosition < scrollPosition);
195 AddUntilStep("52 is the first item", () => list.Items.First() == 52);
196
197 AddStep("drag to 99", () => InputManager.MoveMouseTo(getDragger(99), new Vector2(0, 1)));
198
199 AddUntilStep("scrolling down", () => list.ScrollPosition > scrollPosition);
200 AddUntilStep("52 is the last item", () => list.Items.Last() == 52);
201 }
202
203 [Test]
204 public void TestRearrangeWhileAddingItems()
205 {
206 addItems(2);
207
208 AddStep("grab item 0", () =>
209 {
210 InputManager.MoveMouseTo(getDragger(0));
211 InputManager.PressButton(MouseButton.Left);
212 });
213
214 AddStep("move to bottom", () => InputManager.MoveMouseTo(list.ToScreenSpace(list.LayoutRectangle.BottomLeft) + new Vector2(0, 10)));
215
216 addItems(10);
217
218 AddUntilStep("0 is the last item", () => list.Items.Last() == 0);
219 }
220
221 [Test]
222 public void TestRearrangeWhileRemovingItems()
223 {
224 addItems(50);
225
226 AddStep("grab item 0", () =>
227 {
228 InputManager.MoveMouseTo(getDragger(0));
229 InputManager.PressButton(MouseButton.Left);
230 });
231
232 AddStep("move to bottom", () => InputManager.MoveMouseTo(list.ToScreenSpace(list.LayoutRectangle.BottomLeft) + new Vector2(0, 20)));
233
234 int lastItem = 49;
235
236 AddRepeatStep("remove item", () =>
237 {
238 list.Items.Remove(lastItem--);
239 }, 25);
240
241 AddUntilStep("0 is the last item", () => list.Items.Last() == 0);
242
243 AddRepeatStep("remove item", () =>
244 {
245 list.Items.Remove(lastItem--);
246 }, 25);
247
248 AddStep("release button", () => InputManager.ReleaseButton(MouseButton.Left));
249 }
250
251 [Test]
252 public void TestNotScrolledToTopOnRemove()
253 {
254 addItems(100);
255
256 float scrollPosition = 0;
257 AddStep("scroll to item 50", () =>
258 {
259 list.ScrollTo(50);
260 scrollPosition = list.ScrollPosition;
261 });
262
263 AddStep("remove item 50", () => list.Items.Remove(50));
264
265 AddAssert("scroll hasn't changed", () => list.ScrollPosition == scrollPosition);
266 }
267
268 [Test]
269 public void TestRemoveDuringLoadAndReAdd()
270 {
271 TestDelayedLoadRearrangeableList delayedList = null;
272
273 AddStep("create list", () => Child = delayedList = new TestDelayedLoadRearrangeableList());
274
275 AddStep("add item 1", () => delayedList.Items.Add(1));
276 AddStep("remove item 1", () => delayedList.Items.Remove(1));
277 AddStep("add item 1", () => delayedList.Items.Add(1));
278 AddStep("allow load", () => delayedList.AllowLoad.Release(100));
279
280 AddUntilStep("only one item", () => delayedList.ChildrenOfType<BasicRearrangeableListItem<int>>().Count() == 1);
281 }
282
283 private void addDragSteps(int from, int to, int[] expectedSequence)
284 {
285 AddStep($"move to {from}", () =>
286 {
287 InputManager.MoveMouseTo(getDragger(from));
288 InputManager.PressButton(MouseButton.Left);
289 });
290
291 AddStep($"drag to {to}", () =>
292 {
293 var fromDragger = getDragger(from);
294 var toDragger = getDragger(to);
295
296 InputManager.MoveMouseTo(getDragger(to), fromDragger.ScreenSpaceDrawQuad.TopLeft.Y < toDragger.ScreenSpaceDrawQuad.TopLeft.Y ? new Vector2(0, 1) : new Vector2(0, -1));
297 });
298
299 assertSequence(expectedSequence);
300
301 AddStep("release button", () => InputManager.ReleaseButton(MouseButton.Left));
302 }
303
304 private void assertSequence(params int[] sequence)
305 {
306 AddAssert($"sequence is {string.Join(", ", sequence)}",
307 () => list.Items.SequenceEqual(sequence.Select(value => value)));
308 }
309
310 private void addItems(int count)
311 {
312 AddStep($"add {count} item(s)", () =>
313 {
314 int startId = list.Items.Count == 0 ? 0 : list.Items.Max() + 1;
315
316 for (int i = 0; i < count; i++)
317 list.Items.Add(startId + i);
318 });
319
320 AddUntilStep("wait for items to load", () => list.ItemMap.Values.All(i => i.IsLoaded));
321 }
322
323 private RearrangeableListItem<int> getItem(int index)
324 => list.ChildrenOfType<RearrangeableListItem<int>>().First(i => i.Model == index);
325
326 private BasicRearrangeableListItem<int>.Button getDragger(int index)
327 => list.ChildrenOfType<BasicRearrangeableListItem<int>>().First(i => i.Model == index)
328 .ChildrenOfType<BasicRearrangeableListItem<int>.Button>().First();
329
330 private class TestRearrangeableList : BasicRearrangeableListContainer<int>
331 {
332 public float ScrollPosition => ScrollContainer.Current;
333
334 public new IReadOnlyDictionary<int, RearrangeableListItem<int>> ItemMap => base.ItemMap;
335
336 public new FillFlowContainer<RearrangeableListItem<int>> ListContainer => base.ListContainer;
337
338 public void ScrollTo(int item)
339 => ScrollContainer.ScrollTo(this.ChildrenOfType<BasicRearrangeableListItem<int>>().First(i => i.Model == item), false);
340 }
341
342 private class TestDelayedLoadRearrangeableList : BasicRearrangeableListContainer<int>
343 {
344 public readonly SemaphoreSlim AllowLoad = new SemaphoreSlim(0, 100);
345
346 protected override BasicRearrangeableListItem<int> CreateBasicItem(int item) => new TestRearrangeableListItem(item, AllowLoad);
347
348 private class TestRearrangeableListItem : BasicRearrangeableListItem<int>
349 {
350 private readonly SemaphoreSlim allowLoad;
351
352 public TestRearrangeableListItem(int item, SemaphoreSlim allowLoad)
353 : base(item, false)
354 {
355 this.allowLoad = allowLoad;
356 }
357
358 [BackgroundDependencyLoader]
359 private void load()
360 {
361 if (!allowLoad.Wait(TimeSpan.FromSeconds(10)))
362 throw new TimeoutException();
363 }
364 }
365 }
366 }
367}