1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "ui_scrollregion.h"
4
5#include <base/vmath.h>
6
7#include <engine/client.h>
8#include <engine/keys.h>
9#include <engine/shared/config.h>
10
11CScrollRegion::CScrollRegion()
12{
13 Reset();
14}
15
16void CScrollRegion::Reset()
17{
18 m_ScrollPos = 0.0f;
19 m_ContentSize = 0.0f;
20 m_RequestScrollPos = -1.0f;
21 m_ScrollDirection = SCROLLRELATIVE_NONE;
22 m_ScrollSpeedMultiplier = 1.0f;
23
24 m_AnimTimeMax = 0.0f;
25 m_AnimTime = 0.0f;
26 m_AnimInitScrollPos = 0.0f;
27 m_AnimTargetScrollPos = 0.0f;
28
29 m_ContentAreaRect = m_RailRect = m_LastAddedRect = CUIRect{.x: 0.0f, .y: 0.0f, .w: 0.0f, .h: 0.0f};
30 m_SliderGrabPos = 0.0f;
31 m_ContentScrollOffset = 0.0f;
32 m_Params = CScrollRegionParams();
33}
34
35void CScrollRegion::Begin(CUIRect *pClipRect, const CScrollRegionParams *pParams)
36{
37 if(pParams)
38 m_Params = *pParams;
39 m_ContentAreaRect = *pClipRect;
40
41 CUIRect ScrollbarBg = SplitContentArea();
42 DrawBackground(ScrollbarBg);
43
44 if(!ContentOverflows())
45 m_ContentScrollOffset = 0.0f;
46 m_ContentSize = 0.0f;
47
48 Ui()->ClipEnable(pRect: &m_ContentAreaRect);
49 *pClipRect = m_ContentAreaRect;
50 if(m_Params.m_ScrollHorizontal)
51 pClipRect->x += m_ContentScrollOffset;
52 else
53 pClipRect->y += m_ContentScrollOffset;
54}
55
56void CScrollRegion::End()
57{
58 Ui()->ClipDisable();
59
60 // only show scrollbar if content overflows
61 if(!ContentOverflows())
62 return;
63
64 DoScrollInput();
65 UpdateHotScrollRegion();
66 AdvanceAnimation();
67 DoSlider();
68}
69
70bool CScrollRegion::AddRect(const CUIRect &Rect, bool ShouldScrollHere)
71{
72 m_LastAddedRect = Rect;
73 if(m_Params.m_ScrollHorizontal)
74 m_ContentSize = maximum(a: std::ceil(x: Rect.x + Rect.w - (m_ContentAreaRect.x + m_ContentScrollOffset)), b: m_ContentSize);
75 else
76 m_ContentSize = maximum(a: std::ceil(x: Rect.y + Rect.h - (m_ContentAreaRect.y + m_ContentScrollOffset)), b: m_ContentSize);
77 if(ShouldScrollHere)
78 ScrollHere();
79 return !RectClipped(Rect);
80}
81
82void CScrollRegion::ScrollHere(EScrollOption Option)
83{
84 const float LastAddedPos = m_Params.m_ScrollHorizontal ? m_LastAddedRect.x : m_LastAddedRect.y;
85 const float LastAddedSize = m_Params.m_ScrollHorizontal ? m_LastAddedRect.w : m_LastAddedRect.h;
86 const float MinHeight = minimum(a: ContentAreaSize(), b: LastAddedSize);
87 const float TopScroll = LastAddedPos - (ContentAreaPos() + m_ContentScrollOffset);
88
89 switch(Option)
90 {
91 case SCROLLHERE_TOP:
92 m_RequestScrollPos = TopScroll;
93 break;
94
95 case SCROLLHERE_BOTTOM:
96 m_RequestScrollPos = TopScroll - (ContentAreaSize() - MinHeight);
97 break;
98
99 case SCROLLHERE_KEEP_IN_VIEW:
100 default:
101 const float DeltaPos = LastAddedPos - ContentAreaPos();
102 if(DeltaPos < 0)
103 m_RequestScrollPos = TopScroll;
104 else if(DeltaPos > (ContentAreaSize() - MinHeight))
105 m_RequestScrollPos = TopScroll - (ContentAreaSize() - MinHeight);
106 break;
107 }
108}
109
110void CScrollRegion::ScrollRelative(EScrollRelative Direction, float SpeedMultiplier)
111{
112 m_ScrollDirection = Direction;
113 m_ScrollSpeedMultiplier = SpeedMultiplier;
114}
115
116void CScrollRegion::ScrollRelativeDirect(vec2 ScrollAmount)
117{
118 if(m_Params.m_ScrollHorizontal)
119 m_RequestScrollPos = std::clamp(val: m_ScrollPos + ScrollAmount.x, lo: 0.0f, hi: m_ContentSize - ContentAreaSize());
120 else
121 m_RequestScrollPos = std::clamp(val: m_ScrollPos + ScrollAmount.y, lo: 0.0f, hi: m_ContentSize - ContentAreaSize());
122}
123
124void CScrollRegion::DoEdgeScrolling()
125{
126 if(!ContentOverflows())
127 return;
128
129 const float ScrollBorderSize = 20.0f;
130 const float MaxScrollMultiplier = 2.0f;
131 const float ScrollSpeedFactor = MaxScrollMultiplier / ScrollBorderSize;
132 const float TopScrollPosition = ContentAreaPos() + ScrollBorderSize;
133 const float BottomScrollPosition = ContentAreaPos() + ContentAreaSize() - ScrollBorderSize;
134 const float MousePos = m_Params.m_ScrollHorizontal ? Ui()->MouseX() : Ui()->MouseY();
135 if(MousePos < TopScrollPosition)
136 ScrollRelative(Direction: SCROLLRELATIVE_UP, SpeedMultiplier: minimum(a: MaxScrollMultiplier, b: (TopScrollPosition - MousePos) * ScrollSpeedFactor));
137 else if(MousePos > BottomScrollPosition)
138 ScrollRelative(Direction: SCROLLRELATIVE_DOWN, SpeedMultiplier: minimum(a: MaxScrollMultiplier, b: (MousePos - BottomScrollPosition) * ScrollSpeedFactor));
139}
140
141bool CScrollRegion::RectClipped(const CUIRect &Rect) const
142{
143 return (m_ContentAreaRect.x > (Rect.x + Rect.w) || (m_ContentAreaRect.x + m_ContentAreaRect.w) < Rect.x || m_ContentAreaRect.y > (Rect.y + Rect.h) || (m_ContentAreaRect.y + m_ContentAreaRect.h) < Rect.y);
144}
145
146bool CScrollRegion::ContentOverflows() const
147{
148 return m_Params.m_ScrollHorizontal ? m_ContentSize > m_ContentAreaRect.w : m_ContentSize > m_ContentAreaRect.h;
149}
150
151bool CScrollRegion::ScrollbarShown() const
152{
153 return ContentOverflows() || m_Params.m_ForceShowScrollbar;
154}
155
156bool CScrollRegion::Animating() const
157{
158 return m_AnimTime > 0.0f;
159}
160
161bool CScrollRegion::Active() const
162{
163 return Ui()->ActiveItem() == &m_ScrollPos;
164}
165
166float CScrollRegion::ContentAreaPos() const
167{
168 return m_Params.m_ScrollHorizontal ? m_ContentAreaRect.x : m_ContentAreaRect.y;
169}
170
171float CScrollRegion::ContentAreaSize() const
172{
173 return m_Params.m_ScrollHorizontal ? m_ContentAreaRect.w : m_ContentAreaRect.h;
174}
175
176float CScrollRegion::MaxScroll() const
177{
178 return m_ContentSize - ContentAreaSize();
179}
180
181CUIRect CScrollRegion::SplitContentArea()
182{
183 CUIRect ScrollbarBg;
184 if(m_Params.m_ScrollHorizontal)
185 m_ContentAreaRect.HSplitBottom(Cut: m_Params.m_ScrollbarThickness, pTop: ScrollbarShown() ? &m_ContentAreaRect : nullptr, pBottom: &ScrollbarBg);
186 else
187 m_ContentAreaRect.VSplitRight(Cut: m_Params.m_ScrollbarThickness, pLeft: ScrollbarShown() ? &m_ContentAreaRect : nullptr, pRight: &ScrollbarBg);
188 if(m_Params.m_ScrollbarNoOuterMargin)
189 {
190 if(m_Params.m_ScrollHorizontal)
191 {
192 ScrollbarBg.VMargin(Cut: m_Params.m_ScrollbarMargin, pOtherRect: &m_RailRect);
193 m_RailRect.HSplitTop(Cut: m_Params.m_ScrollbarMargin, pTop: nullptr, pBottom: &m_RailRect);
194 }
195 else
196 {
197 ScrollbarBg.HMargin(Cut: m_Params.m_ScrollbarMargin, pOtherRect: &m_RailRect);
198 m_RailRect.VSplitLeft(Cut: m_Params.m_ScrollbarMargin, pLeft: nullptr, pRight: &m_RailRect);
199 }
200 }
201 else
202 ScrollbarBg.Margin(Cut: m_Params.m_ScrollbarMargin, pOtherRect: &m_RailRect);
203
204 return ScrollbarBg;
205}
206
207void CScrollRegion::DrawBackground(const CUIRect &ScrollbarBg)
208{
209 // only show scrollbar if required
210 if(ScrollbarShown())
211 {
212 if(m_Params.m_ScrollbarBgColor.a > 0.0f)
213 {
214 int Corners = m_Params.m_ScrollHorizontal ? IGraphics::CORNER_B : IGraphics::CORNER_R;
215 ScrollbarBg.Draw(Color: m_Params.m_ScrollbarBgColor, Corners, Rounding: 4.0f);
216 }
217 if(m_Params.m_RailBgColor.a > 0.0f)
218 {
219 float Rounding = m_Params.m_ScrollHorizontal ? m_RailRect.h / 2.0f : m_RailRect.w / 2.0f;
220 m_RailRect.Draw(Color: m_Params.m_RailBgColor, Corners: IGraphics::CORNER_ALL, Rounding);
221 }
222 }
223 if(m_Params.m_ClipBgColor.a > 0.0f)
224 {
225 int CornersPartial = m_Params.m_ScrollHorizontal ? IGraphics::CORNER_T : IGraphics::CORNER_L;
226 m_ContentAreaRect.Draw(Color: m_Params.m_ClipBgColor, Corners: ScrollbarShown() ? CornersPartial : IGraphics::CORNER_ALL, Rounding: 4.0f);
227 }
228}
229
230void CScrollRegion::DoScrollInput()
231{
232 if(m_ScrollDirection != SCROLLRELATIVE_NONE || Ui()->HotScrollRegion() == this)
233 {
234 bool ProgrammaticScroll = false;
235 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_SCROLL_UP))
236 m_ScrollDirection = SCROLLRELATIVE_UP;
237 else if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_SCROLL_DOWN))
238 m_ScrollDirection = SCROLLRELATIVE_DOWN;
239 else
240 ProgrammaticScroll = true;
241
242 if(!ProgrammaticScroll)
243 m_ScrollSpeedMultiplier = 1.0f;
244
245 if(m_ScrollDirection != SCROLLRELATIVE_NONE)
246 {
247 const bool IsPageScroll = Input()->AltIsPressed();
248 const float ScrollUnit = IsPageScroll && !ProgrammaticScroll ? ContentAreaSize() : m_Params.m_ScrollUnit;
249
250 m_AnimTimeMax = g_Config.m_UiSmoothScrollTime / 1000.0f;
251 m_AnimTime = m_AnimTimeMax;
252 m_AnimInitScrollPos = m_ScrollPos;
253 m_AnimTargetScrollPos = (ProgrammaticScroll ? m_ScrollPos : m_AnimTargetScrollPos) + (int)m_ScrollDirection * ScrollUnit * m_ScrollSpeedMultiplier;
254 m_ScrollDirection = SCROLLRELATIVE_NONE;
255 m_ScrollSpeedMultiplier = 1.0f;
256 }
257 }
258}
259
260void CScrollRegion::UpdateHotScrollRegion()
261{
262 CUIRect RegionRect = m_ContentAreaRect;
263 if(m_Params.m_ScrollHorizontal)
264 RegionRect.h += m_Params.m_ScrollbarThickness;
265 else
266 RegionRect.w += m_Params.m_ScrollbarThickness;
267
268 if(Ui()->Enabled() && Ui()->MouseHovered(pRect: &RegionRect))
269 {
270 Ui()->SetHotScrollRegion(this);
271 }
272}
273
274void CScrollRegion::AdvanceAnimation()
275{
276 if(m_RequestScrollPos >= 0.0f)
277 {
278 m_AnimTargetScrollPos = m_RequestScrollPos;
279 m_AnimTime = 0.0f;
280 m_RequestScrollPos = -1.0f;
281 }
282
283 m_AnimTargetScrollPos = std::clamp(val: m_AnimTargetScrollPos, lo: 0.0f, hi: MaxScroll());
284
285 if(absolute(a: m_AnimInitScrollPos - m_AnimTargetScrollPos) < 0.5f)
286 m_AnimTime = 0.0f;
287
288 if(m_AnimTime > 0.0f)
289 {
290 m_AnimTime -= Client()->RenderFrameTime();
291 if(m_AnimTime < 0.0f)
292 {
293 m_AnimTime = 0.0f;
294 }
295 float AnimProgress = (1.0f - std::pow(x: m_AnimTime / m_AnimTimeMax, y: 3.0f)); // cubic ease out
296 m_ScrollPos = m_AnimInitScrollPos + (m_AnimTargetScrollPos - m_AnimInitScrollPos) * AnimProgress;
297 }
298 else
299 {
300 m_ScrollPos = m_AnimTargetScrollPos;
301 }
302}
303
304void CScrollRegion::DoSlider()
305{
306 const float RailSize = m_Params.m_ScrollHorizontal ? m_RailRect.w : m_RailRect.h;
307 const float SliderSize = maximum(a: m_Params.m_SliderMinSize, b: ContentAreaSize() / m_ContentSize * RailSize);
308
309 CUIRect Slider = m_RailRect;
310 float &SliderPos = m_Params.m_ScrollHorizontal ? Slider.x : Slider.y;
311 if(m_Params.m_ScrollHorizontal)
312 Slider.w = SliderSize;
313 else
314 Slider.h = SliderSize;
315
316 const float MaxSlider = RailSize - SliderSize;
317
318 SliderPos += m_ScrollPos / MaxScroll() * MaxSlider;
319
320 bool Grabbed = false;
321 const void *pId = &m_ScrollPos;
322 const bool InsideSlider = Ui()->MouseHovered(pRect: &Slider);
323 const bool InsideRail = Ui()->MouseHovered(pRect: &m_RailRect);
324
325 float MousePos = m_Params.m_ScrollHorizontal ? Ui()->MouseX() : Ui()->MouseY();
326 if(Ui()->CheckActiveItem(pId) && Ui()->MouseButton(Index: 0))
327 {
328 m_ScrollPos += (MousePos - (SliderPos + m_SliderGrabPos)) / MaxSlider * MaxScroll();
329 m_SliderGrabPos = std::clamp(val: m_SliderGrabPos, lo: 0.0f, hi: SliderSize);
330 m_AnimTargetScrollPos = m_ScrollPos;
331 m_AnimTime = 0.0f;
332 Grabbed = true;
333 }
334 else if(InsideSlider)
335 {
336 if(!Ui()->MouseButton(Index: 0))
337 Ui()->SetHotItem(pId);
338
339 if(!Ui()->CheckActiveItem(pId) && Ui()->MouseButtonClicked(Index: 0))
340 {
341 Ui()->SetActiveItem(pId);
342 m_SliderGrabPos = MousePos - SliderPos;
343 m_AnimTargetScrollPos = m_ScrollPos;
344 m_AnimTime = 0.0f;
345 }
346 }
347 else if(InsideRail && Ui()->MouseButtonClicked(Index: 0))
348 {
349 m_ScrollPos += (MousePos - (SliderPos + SliderSize / 2.0f)) / MaxSlider * MaxScroll();
350 Ui()->SetHotItem(pId);
351 Ui()->SetActiveItem(pId);
352 m_SliderGrabPos = SliderSize / 2.0f;
353 m_AnimTargetScrollPos = m_ScrollPos;
354 m_AnimTime = 0.0f;
355 }
356
357 if(Ui()->CheckActiveItem(pId) && !Ui()->MouseButton(Index: 0))
358 {
359 Ui()->SetActiveItem(nullptr);
360 }
361
362 m_ScrollPos = std::clamp(val: m_ScrollPos, lo: 0.0f, hi: MaxScroll());
363 m_ContentScrollOffset = -m_ScrollPos;
364
365 float Rounding = m_Params.m_ScrollHorizontal ? Slider.h / 2.0f : Slider.w / 2.0f;
366 Slider.Draw(Color: m_Params.SliderColor(Active: Grabbed, Hovered: Ui()->HotItem() == pId), Corners: IGraphics::CORNER_ALL, Rounding);
367}
368