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.Diagnostics;
6using System.Linq;
7using NUnit.Framework;
8using osu.Framework.Graphics;
9using osu.Framework.Graphics.Containers;
10using osu.Framework.Graphics.Shapes;
11using osu.Framework.Input.Events;
12using osu.Framework.Utils;
13using osu.Framework.Testing;
14using osuTK;
15using osuTK.Graphics;
16using osuTK.Input;
17
18namespace osu.Framework.Tests.Visual.Containers
19{
20 public class TestSceneScrollContainer : ManualInputManagerTestScene
21 {
22 private ScrollContainer<Drawable> scrollContainer;
23
24 [SetUp]
25 public void Setup() => Schedule(Clear);
26
27 [TestCase(0)]
28 [TestCase(100)]
29 public void TestScrollTo(float clampExtension)
30 {
31 const float container_height = 100;
32 const float box_height = 400;
33
34 AddStep("Create scroll container", () =>
35 {
36 Add(scrollContainer = new BasicScrollContainer
37 {
38 Anchor = Anchor.Centre,
39 Origin = Anchor.Centre,
40 Size = new Vector2(container_height),
41 ClampExtension = clampExtension,
42 Child = new Box { Size = new Vector2(100, box_height) }
43 });
44 });
45
46 scrollTo(-100, box_height - container_height, clampExtension);
47 checkPosition(0);
48
49 scrollTo(100, box_height - container_height, clampExtension);
50 checkPosition(100);
51
52 scrollTo(300, box_height - container_height, clampExtension);
53 checkPosition(300);
54
55 scrollTo(400, box_height - container_height, clampExtension);
56 checkPosition(300);
57
58 scrollTo(500, box_height - container_height, clampExtension);
59 checkPosition(300);
60 }
61
62 private FillFlowContainer fill;
63
64 [Test]
65 public void TestScrollIntoView()
66 {
67 const float item_height = 25;
68
69 AddStep("Create scroll container", () =>
70 {
71 Add(scrollContainer = new BasicScrollContainer
72 {
73 Anchor = Anchor.Centre,
74 Origin = Anchor.Centre,
75 Size = new Vector2(item_height * 4),
76 Child = fill = new FillFlowContainer
77 {
78 RelativeSizeAxes = Axes.X,
79 AutoSizeAxes = Axes.Y,
80 Direction = FillDirection.Vertical,
81 },
82 });
83
84 for (int i = 0; i < 8; i++)
85 {
86 fill.Add(new Box
87 {
88 Colour = new Color4(RNG.NextSingle(1), RNG.NextSingle(1), RNG.NextSingle(1), 1),
89 RelativeSizeAxes = Axes.X,
90 Height = item_height,
91 });
92 }
93 });
94
95 // simple last item (hits bottom of view)
96 scrollIntoView(7, item_height * 4);
97
98 // position doesn't change when item in view
99 scrollIntoView(6, item_height * 4);
100
101 // scroll in reverse without overscrolling
102 scrollIntoView(1, item_height);
103
104 // scroll forwards with small (non-zero) view
105 // current position will change on restore size
106 scrollIntoView(7, item_height * 7, heightAdjust: 15, expectedPostAdjustPosition: 100);
107
108 // scroll backwards with small (non-zero) view
109 // current position won't change on restore size
110 scrollIntoView(2, item_height * 2, heightAdjust: 15, expectedPostAdjustPosition: item_height * 2);
111
112 // test forwards scroll with zero container height
113 scrollIntoView(7, item_height * 7, heightAdjust: 0, expectedPostAdjustPosition: item_height * 4);
114
115 // test backwards scroll with zero container height
116 scrollIntoView(2, item_height * 2, heightAdjust: 0, expectedPostAdjustPosition: item_height * 2);
117 }
118
119 [TestCase(false)]
120 [TestCase(true)]
121 public void TestDraggingScroll(bool withClampExtension)
122 {
123 AddStep("Create scroll container", () =>
124 {
125 Add(scrollContainer = new BasicScrollContainer
126 {
127 Anchor = Anchor.Centre,
128 Origin = Anchor.Centre,
129 Size = new Vector2(200),
130 ClampExtension = withClampExtension ? 100 : 0,
131 Child = new Box { Size = new Vector2(200, 300) }
132 });
133 });
134
135 AddStep("Click and drag scrollcontainer", () =>
136 {
137 InputManager.MoveMouseTo(scrollContainer);
138 InputManager.PressButton(MouseButton.Left);
139 // Required for the dragging state to be set correctly.
140 InputManager.MoveMouseTo(scrollContainer.ToScreenSpace(scrollContainer.LayoutRectangle.Centre + new Vector2(10f)));
141 });
142
143 AddStep("Move mouse up", () => InputManager.MoveMouseTo(scrollContainer.ScreenSpaceDrawQuad.Centre - new Vector2(0, 400)));
144 checkPosition(withClampExtension ? 200 : 100);
145 AddStep("Move mouse down", () => InputManager.MoveMouseTo(scrollContainer.ScreenSpaceDrawQuad.Centre + new Vector2(0, 400)));
146 checkPosition(withClampExtension ? -100 : 0);
147 AddStep("Release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
148 checkPosition(0);
149 }
150
151 [Test]
152 public void TestContentAnchors()
153 {
154 AddStep("Create scroll container with centre-left content", () =>
155 {
156 Add(scrollContainer = new BasicScrollContainer
157 {
158 Anchor = Anchor.Centre,
159 Origin = Anchor.Centre,
160 Size = new Vector2(300),
161 ScrollContent =
162 {
163 Anchor = Anchor.CentreLeft,
164 Origin = Anchor.CentreLeft,
165 },
166 Child = new Box { Size = new Vector2(300, 400) }
167 });
168 });
169
170 AddStep("Scroll to 0", () => scrollContainer.ScrollTo(0, false));
171 AddAssert("Content position at top", () => Precision.AlmostEquals(scrollContainer.ScreenSpaceDrawQuad.TopLeft, scrollContainer.ScrollContent.ScreenSpaceDrawQuad.TopLeft));
172 }
173
174 [Test]
175 public void TestClampedScrollbar()
176 {
177 AddStep("Create scroll container", () =>
178 {
179 Add(scrollContainer = new ClampedScrollbarScrollContainer
180 {
181 Anchor = Anchor.Centre,
182 Origin = Anchor.Centre,
183 Size = new Vector2(500),
184 Child = new FillFlowContainer
185 {
186 AutoSizeAxes = Axes.Both,
187 Direction = FillDirection.Vertical,
188 Children = new[]
189 {
190 new Box { Size = new Vector2(500) },
191 new Box { Size = new Vector2(500) },
192 new Box { Size = new Vector2(500) },
193 }
194 }
195 });
196 });
197
198 AddStep("scroll to end", () => scrollContainer.ScrollToEnd(false));
199 checkScrollbarPosition(250);
200
201 AddStep("scroll to start", () => scrollContainer.ScrollToStart(false));
202 checkScrollbarPosition(0);
203 }
204
205 [Test]
206 public void TestClampedScrollbarDrag()
207 {
208 ClampedScrollbarScrollContainer clampedContainer = null;
209
210 AddStep("Create scroll container", () =>
211 {
212 Add(scrollContainer = clampedContainer = new ClampedScrollbarScrollContainer
213 {
214 Anchor = Anchor.Centre,
215 Origin = Anchor.Centre,
216 Size = new Vector2(500),
217 Child = new FillFlowContainer
218 {
219 AutoSizeAxes = Axes.Both,
220 Direction = FillDirection.Vertical,
221 Children = new[]
222 {
223 new Box { Size = new Vector2(500) },
224 new Box { Size = new Vector2(500) },
225 new Box { Size = new Vector2(500) },
226 }
227 }
228 });
229 });
230
231 AddStep("Click scroll bar", () =>
232 {
233 InputManager.MoveMouseTo(clampedContainer.Scrollbar);
234 InputManager.PressButton(MouseButton.Left);
235 });
236
237 // Position at mouse down
238 checkScrollbarPosition(0);
239
240 AddStep("begin drag", () =>
241 {
242 // Required for the dragging state to be set correctly.
243 InputManager.MoveMouseTo(clampedContainer.Scrollbar.ToScreenSpace(clampedContainer.Scrollbar.LayoutRectangle.Centre + new Vector2(0, -10f)));
244 });
245
246 AddStep("Move mouse up", () => InputManager.MoveMouseTo(scrollContainer.ScreenSpaceDrawQuad.TopRight - new Vector2(0, 20)));
247 checkScrollbarPosition(0);
248 AddStep("Move mouse down", () => InputManager.MoveMouseTo(scrollContainer.ScreenSpaceDrawQuad.BottomRight + new Vector2(0, 20)));
249 checkScrollbarPosition(250);
250 AddStep("Release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
251 checkScrollbarPosition(250);
252 }
253
254 [Test]
255 public void TestHandleKeyboardRepeatAfterRemoval()
256 {
257 AddStep("create scroll container", () =>
258 {
259 Add(scrollContainer = new RepeatCountingScrollContainer
260 {
261 Anchor = Anchor.Centre,
262 Origin = Anchor.Centre,
263 Size = new Vector2(500),
264 Child = new FillFlowContainer
265 {
266 AutoSizeAxes = Axes.Both,
267 Direction = FillDirection.Vertical,
268 Children = new[]
269 {
270 new Box { Size = new Vector2(500) },
271 new Box { Size = new Vector2(500) },
272 new Box { Size = new Vector2(500) },
273 }
274 }
275 });
276 });
277
278 AddStep("move mouse to scroll container", () => InputManager.MoveMouseTo(scrollContainer));
279 AddStep("press page down and remove scroll container", () => InputManager.PressKey(Key.PageDown));
280 AddStep("remove scroll container", () =>
281 {
282 Remove(scrollContainer);
283 ((RepeatCountingScrollContainer)scrollContainer).RepeatCount = 0;
284 });
285
286 AddWaitStep("wait for repeats", 5);
287 }
288
289 [Test]
290 public void TestEmptyScrollContainerDoesNotHandleScrollAndDrag()
291 {
292 AddStep("create scroll container", () =>
293 {
294 Add(scrollContainer = new InputHandlingScrollContainer
295 {
296 Anchor = Anchor.Centre,
297 Origin = Anchor.Centre,
298 Size = new Vector2(500),
299 });
300 });
301
302 AddStep("Perform scroll", () =>
303 {
304 InputManager.MoveMouseTo(scrollContainer);
305 InputManager.ScrollVerticalBy(50);
306 });
307
308 AddAssert("Scroll was not handled", () =>
309 {
310 var inputHandlingScrollContainer = (InputHandlingScrollContainer)scrollContainer;
311 return inputHandlingScrollContainer.ScrollHandled.HasValue && !inputHandlingScrollContainer.ScrollHandled.Value;
312 });
313
314 AddStep("Perform drag", () =>
315 {
316 InputManager.MoveMouseTo(scrollContainer);
317 InputManager.PressButton(MouseButton.Left);
318 InputManager.MoveMouseTo(scrollContainer, new Vector2(50));
319 });
320
321 AddAssert("Drag was not handled", () =>
322 {
323 var inputHandlingScrollContainer = (InputHandlingScrollContainer)scrollContainer;
324 return inputHandlingScrollContainer.DragHandled.HasValue && !inputHandlingScrollContainer.DragHandled.Value;
325 });
326 }
327
328 private void scrollIntoView(int index, float expectedPosition, float? heightAdjust = null, float? expectedPostAdjustPosition = null)
329 {
330 if (heightAdjust != null)
331 AddStep("set container height zero", () => scrollContainer.Height = heightAdjust.Value);
332
333 AddStep($"scroll {index} into view", () => scrollContainer.ScrollIntoView(fill.Skip(index).First()));
334 AddUntilStep($"{index} is visible", () => !fill.Skip(index).First().IsMaskedAway);
335 checkPosition(expectedPosition);
336
337 if (heightAdjust != null)
338 {
339 Debug.Assert(expectedPostAdjustPosition != null, nameof(expectedPostAdjustPosition) + " != null");
340
341 AddStep("restore height", () => scrollContainer.Height = 100);
342 checkPosition(expectedPostAdjustPosition.Value);
343 }
344 }
345
346 private void scrollTo(float position, float scrollContentHeight, float extension)
347 {
348 float clampedTarget = Math.Clamp(position, -extension, scrollContentHeight + extension);
349
350 float immediateScrollPosition = 0;
351
352 AddStep($"scroll to {position}", () =>
353 {
354 scrollContainer.ScrollTo(position, false);
355 immediateScrollPosition = scrollContainer.Current;
356 });
357
358 AddAssert($"immediately scrolled to {clampedTarget}", () => Precision.AlmostEquals(clampedTarget, immediateScrollPosition, 1));
359 }
360
361 private void checkPosition(float expected) => AddUntilStep($"position at {expected}", () => Precision.AlmostEquals(expected, scrollContainer.Current, 1));
362
363 private void checkScrollbarPosition(float expected) =>
364 AddUntilStep($"scrollbar position at {expected}", () => Precision.AlmostEquals(expected, scrollContainer.InternalChildren[1].DrawPosition.Y, 1));
365
366 private class RepeatCountingScrollContainer : BasicScrollContainer
367 {
368 public int RepeatCount { get; set; }
369
370 protected override bool OnKeyDown(KeyDownEvent e)
371 {
372 if (e.Repeat)
373 RepeatCount++;
374
375 return base.OnKeyDown(e);
376 }
377 }
378
379 private class ClampedScrollbarScrollContainer : BasicScrollContainer
380 {
381 public new ScrollbarContainer Scrollbar => base.Scrollbar;
382
383 protected override ScrollbarContainer CreateScrollbar(Direction direction) => new ClampedScrollbar(direction);
384
385 private class ClampedScrollbar : BasicScrollbar
386 {
387 protected internal override float MinimumDimSize => 250;
388
389 public ClampedScrollbar(Direction direction)
390 : base(direction)
391 {
392 }
393 }
394 }
395
396 private class InputHandlingScrollContainer : BasicScrollContainer
397 {
398 public bool? ScrollHandled { get; private set; }
399 public bool? DragHandled { get; private set; }
400
401 protected override bool OnScroll(ScrollEvent e)
402 {
403 ScrollHandled = base.OnScroll(e);
404 return ScrollHandled.Value;
405 }
406
407 protected override bool OnDragStart(DragStartEvent e)
408 {
409 DragHandled = base.OnDragStart(e);
410 return DragHandled.Value;
411 }
412 }
413 }
414}