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 = std::max(a: std::ceil(x: Rect.x + Rect.w - (m_ContentAreaRect.x + m_ContentScrollOffset)), b: m_ContentSize);
75 else
76 m_ContentSize = std::max(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 = std::min(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: std::min(a: MaxScrollMultiplier, b: (TopScrollPosition - MousePos) * ScrollSpeedFactor));
137 else if(MousePos > BottomScrollPosition)
138 ScrollRelative(Direction: SCROLLRELATIVE_DOWN, SpeedMultiplier: std::min(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 {
203 ScrollbarBg.Margin(Cut: m_Params.m_ScrollbarMargin, pOtherRect: &m_RailRect);
204 }
205
206 return ScrollbarBg;
207}
208
209void CScrollRegion::DrawBackground(const CUIRect &ScrollbarBg)
210{
211 // only show scrollbar if required
212 if(ScrollbarShown())
213 {
214 if(m_Params.m_ScrollbarBgColor.a > 0.0f)
215 {
216 int Corners = m_Params.m_ScrollHorizontal ? IGraphics::CORNER_B : IGraphics::CORNER_R;
217 ScrollbarBg.Draw(Color: m_Params.m_ScrollbarBgColor, Corners, Rounding: 4.0f);
218 }
219 if(m_Params.m_RailBgColor.a > 0.0f)
220 {
221 float Rounding = m_Params.m_ScrollHorizontal ? m_RailRect.h / 2.0f : m_RailRect.w / 2.0f;
222 m_RailRect.Draw(Color: m_Params.m_RailBgColor, Corners: IGraphics::CORNER_ALL, Rounding);
223 }
224 }
225 if(m_Params.m_ClipBgColor.a > 0.0f)
226 {
227 int CornersPartial = m_Params.m_ScrollHorizontal ? IGraphics::CORNER_T : IGraphics::CORNER_L;
228 m_ContentAreaRect.Draw(Color: m_Params.m_ClipBgColor, Corners: ScrollbarShown() ? CornersPartial : IGraphics::CORNER_ALL, Rounding: 4.0f);
229 }
230}
231
232void CScrollRegion::DoScrollInput()
233{
234 if(m_ScrollDirection != SCROLLRELATIVE_NONE || Ui()->HotScrollRegion() == this)
235 {
236 bool ProgrammaticScroll = false;
237 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_SCROLL_UP))
238 m_ScrollDirection = SCROLLRELATIVE_UP;
239 else if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_SCROLL_DOWN))
240 m_ScrollDirection = SCROLLRELATIVE_DOWN;
241 else
242 ProgrammaticScroll = true;
243
244 if(!ProgrammaticScroll)
245 m_ScrollSpeedMultiplier = 1.0f;
246
247 if(m_ScrollDirection != SCROLLRELATIVE_NONE)
248 {
249 const bool IsPageScroll = Input()->AltIsPressed();
250 const float ScrollUnit = IsPageScroll && !ProgrammaticScroll ? ContentAreaSize() : m_Params.m_ScrollUnit;
251
252 m_AnimTimeMax = g_Config.m_UiSmoothScrollTime / 1000.0f;
253 m_AnimTime = m_AnimTimeMax;
254 m_AnimInitScrollPos = m_ScrollPos;
255 m_AnimTargetScrollPos = (ProgrammaticScroll ? m_ScrollPos : m_AnimTargetScrollPos) + (int)m_ScrollDirection * ScrollUnit * m_ScrollSpeedMultiplier;
256 m_ScrollDirection = SCROLLRELATIVE_NONE;
257 m_ScrollSpeedMultiplier = 1.0f;
258 }
259 }
260}
261
262void CScrollRegion::UpdateHotScrollRegion()
263{
264 CUIRect RegionRect = m_ContentAreaRect;
265 if(m_Params.m_ScrollHorizontal)
266 RegionRect.h += m_Params.m_ScrollbarThickness;
267 else
268 RegionRect.w += m_Params.m_ScrollbarThickness;
269
270 if(Ui()->Enabled() && Ui()->MouseHovered(pRect: &RegionRect))
271 {
272 Ui()->SetHotScrollRegion(this);
273 }
274}
275
276void CScrollRegion::AdvanceAnimation()
277{
278 if(m_RequestScrollPos >= 0.0f)
279 {
280 m_AnimTargetScrollPos = m_RequestScrollPos;
281 m_AnimTime = 0.0f;
282 m_RequestScrollPos = -1.0f;
283 }
284
285 m_AnimTargetScrollPos = std::clamp(val: m_AnimTargetScrollPos, lo: 0.0f, hi: MaxScroll());
286
287 if(absolute(a: m_AnimInitScrollPos - m_AnimTargetScrollPos) < 0.5f)
288 m_AnimTime = 0.0f;
289
290 if(m_AnimTime > 0.0f)
291 {
292 m_AnimTime -= Client()->RenderFrameTime();
293 if(m_AnimTime < 0.0f)
294 {
295 m_AnimTime = 0.0f;
296 }
297 float AnimProgress = (1.0f - std::pow(x: m_AnimTime / m_AnimTimeMax, y: 3.0f)); // cubic ease out
298 m_ScrollPos = m_AnimInitScrollPos + (m_AnimTargetScrollPos - m_AnimInitScrollPos) * AnimProgress;
299 }
300 else
301 {
302 m_ScrollPos = m_AnimTargetScrollPos;
303 }
304}
305
306void CScrollRegion::DoSlider()
307{
308 const float RailSize = m_Params.m_ScrollHorizontal ? m_RailRect.w : m_RailRect.h;
309 const float SliderSize = std::max(a: m_Params.m_SliderMinSize, b: ContentAreaSize() / m_ContentSize * RailSize);
310
311 CUIRect Slider = m_RailRect;
312 float &SliderPos = m_Params.m_ScrollHorizontal ? Slider.x : Slider.y;
313 if(m_Params.m_ScrollHorizontal)
314 Slider.w = SliderSize;
315 else
316 Slider.h = SliderSize;
317
318 const float MaxSlider = RailSize - SliderSize;
319
320 SliderPos += m_ScrollPos / MaxScroll() * MaxSlider;
321
322 const void *pId = &m_ScrollPos;
323 const float MousePos = m_Params.m_ScrollHorizontal ? Ui()->MouseX() : Ui()->MouseY();
324 const bool WasActive = Ui()->ActiveItem() == pId;
325 Ui()->DoButtonLogic(pId, Checked: 0, pRect: &m_RailRect, Flags: BUTTONFLAG_LEFT); // Result ignored, we only care about the button becoming and being active
326 if(Ui()->CheckActiveItem(pId))
327 {
328 if(!WasActive)
329 {
330 m_SliderGrabPos = Ui()->MouseHovered(pRect: &Slider) ? (MousePos - SliderPos) : (SliderSize / 2.0f);
331 m_SliderGrabPos = std::clamp(val: m_SliderGrabPos, lo: 0.0f, hi: SliderSize);
332 }
333 m_ScrollPos += (MousePos - (SliderPos + m_SliderGrabPos)) / MaxSlider * MaxScroll();
334 m_AnimTargetScrollPos = m_ScrollPos;
335 m_AnimTime = 0.0f;
336 }
337
338 m_ScrollPos = std::clamp(val: m_ScrollPos, lo: 0.0f, hi: MaxScroll());
339 m_ContentScrollOffset = -m_ScrollPos;
340
341 const float Rounding = m_Params.m_ScrollHorizontal ? Slider.h / 2.0f : Slider.w / 2.0f;
342 Slider.Draw(Color: m_Params.SliderColor(Active: Ui()->CheckActiveItem(pId), Hovered: Ui()->HotItem() == pId), Corners: IGraphics::CORNER_ALL, Rounding);
343}
344