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/system.h>
6#include <base/vmath.h>
7
8#include <engine/client.h>
9#include <engine/keys.h>
10#include <engine/shared/config.h>
11
12CScrollRegion::CScrollRegion()
13{
14 Reset();
15}
16
17void CScrollRegion::Reset()
18{
19 m_ScrollY = 0.0f;
20 m_ContentH = 0.0f;
21 m_RequestScrollY = -1.0f;
22 m_ScrollDirection = SCROLLRELATIVE_NONE;
23 m_ScrollSpeedMultiplier = 1.0f;
24
25 m_AnimTimeMax = 0.0f;
26 m_AnimTime = 0.0f;
27 m_AnimInitScrollY = 0.0f;
28 m_AnimTargetScrollY = 0.0f;
29
30 m_ClipRect = m_RailRect = m_LastAddedRect = CUIRect{.x: 0.0f, .y: 0.0f, .w: 0.0f, .h: 0.0f};
31 m_SliderGrabPos = 0.0f;
32 m_ContentScrollOff = vec2(0.0f, 0.0f);
33 m_Params = CScrollRegionParams();
34}
35
36void CScrollRegion::Begin(CUIRect *pClipRect, vec2 *pOutOffset, const CScrollRegionParams *pParams)
37{
38 if(pParams)
39 m_Params = *pParams;
40
41 const bool ContentOverflows = m_ContentH > pClipRect->h;
42 const bool ForceShowScrollbar = m_Params.m_Flags & CScrollRegionParams::FLAG_CONTENT_STATIC_WIDTH;
43
44 const bool HasScrollBar = ContentOverflows || ForceShowScrollbar;
45 CUIRect ScrollBarBg;
46 pClipRect->VSplitRight(Cut: m_Params.m_ScrollbarWidth, pLeft: HasScrollBar ? pClipRect : nullptr, pRight: &ScrollBarBg);
47 if(m_Params.m_ScrollbarNoMarginRight)
48 {
49 ScrollBarBg.HMargin(Cut: m_Params.m_ScrollbarMargin, pOtherRect: &m_RailRect);
50 m_RailRect.VSplitLeft(Cut: m_Params.m_ScrollbarMargin, pLeft: nullptr, pRight: &m_RailRect);
51 }
52 else
53 ScrollBarBg.Margin(Cut: m_Params.m_ScrollbarMargin, pOtherRect: &m_RailRect);
54
55 // only show scrollbar if required
56 if(HasScrollBar)
57 {
58 if(m_Params.m_ScrollbarBgColor.a > 0.0f)
59 ScrollBarBg.Draw(Color: m_Params.m_ScrollbarBgColor, Corners: IGraphics::CORNER_R, Rounding: 4.0f);
60 if(m_Params.m_RailBgColor.a > 0.0f)
61 m_RailRect.Draw(Color: m_Params.m_RailBgColor, Corners: IGraphics::CORNER_ALL, Rounding: m_RailRect.w / 2.0f);
62 }
63 if(!ContentOverflows)
64 m_ContentScrollOff.y = 0.0f;
65
66 if(m_Params.m_ClipBgColor.a > 0.0f)
67 pClipRect->Draw(Color: m_Params.m_ClipBgColor, Corners: HasScrollBar ? IGraphics::CORNER_L : IGraphics::CORNER_ALL, Rounding: 4.0f);
68
69 Ui()->ClipEnable(pRect: pClipRect);
70
71 m_ClipRect = *pClipRect;
72 m_ContentH = 0.0f;
73 *pOutOffset = m_ContentScrollOff;
74}
75
76void CScrollRegion::End()
77{
78 Ui()->ClipDisable();
79
80 // only show scrollbar if content overflows
81 if(m_ContentH <= m_ClipRect.h)
82 return;
83
84 // scroll wheel
85 CUIRect RegionRect = m_ClipRect;
86 RegionRect.w += m_Params.m_ScrollbarWidth;
87
88 if(m_ScrollDirection != SCROLLRELATIVE_NONE || Ui()->HotScrollRegion() == this)
89 {
90 bool ProgrammaticScroll = false;
91 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_SCROLL_UP))
92 m_ScrollDirection = SCROLLRELATIVE_UP;
93 else if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_SCROLL_DOWN))
94 m_ScrollDirection = SCROLLRELATIVE_DOWN;
95 else
96 ProgrammaticScroll = true;
97
98 if(!ProgrammaticScroll)
99 m_ScrollSpeedMultiplier = 1.0f;
100
101 if(m_ScrollDirection != SCROLLRELATIVE_NONE)
102 {
103 const bool IsPageScroll = Input()->AltIsPressed();
104 const float ScrollUnit = IsPageScroll && !ProgrammaticScroll ? m_ClipRect.h : m_Params.m_ScrollUnit;
105
106 m_AnimTimeMax = g_Config.m_UiSmoothScrollTime / 1000.0f;
107 m_AnimTime = m_AnimTimeMax;
108 m_AnimInitScrollY = m_ScrollY;
109 m_AnimTargetScrollY = (ProgrammaticScroll ? m_ScrollY : m_AnimTargetScrollY) + (int)m_ScrollDirection * ScrollUnit * m_ScrollSpeedMultiplier;
110 m_ScrollDirection = SCROLLRELATIVE_NONE;
111 m_ScrollSpeedMultiplier = 1.0f;
112 }
113 }
114
115 if(Ui()->Enabled() && Ui()->MouseHovered(pRect: &RegionRect))
116 {
117 Ui()->SetHotScrollRegion(this);
118 }
119
120 const float SliderHeight = maximum(a: m_Params.m_SliderMinHeight, b: m_ClipRect.h / m_ContentH * m_RailRect.h);
121
122 CUIRect Slider = m_RailRect;
123 Slider.h = SliderHeight;
124
125 const float MaxSlider = m_RailRect.h - SliderHeight;
126 const float MaxScroll = m_ContentH - m_ClipRect.h;
127
128 if(m_RequestScrollY >= 0.0f)
129 {
130 m_AnimTargetScrollY = m_RequestScrollY;
131 m_AnimTime = 0.0f;
132 m_RequestScrollY = -1.0f;
133 }
134
135 m_AnimTargetScrollY = std::clamp(val: m_AnimTargetScrollY, lo: 0.0f, hi: MaxScroll);
136
137 if(absolute(a: m_AnimInitScrollY - m_AnimTargetScrollY) < 0.5f)
138 m_AnimTime = 0.0f;
139
140 if(m_AnimTime > 0.0f)
141 {
142 m_AnimTime -= Client()->RenderFrameTime();
143 if(m_AnimTime < 0.0f)
144 {
145 m_AnimTime = 0.0f;
146 }
147 float AnimProgress = (1.0f - std::pow(x: m_AnimTime / m_AnimTimeMax, y: 3.0f)); // cubic ease out
148 m_ScrollY = m_AnimInitScrollY + (m_AnimTargetScrollY - m_AnimInitScrollY) * AnimProgress;
149 }
150 else
151 {
152 m_ScrollY = m_AnimTargetScrollY;
153 }
154
155 Slider.y += m_ScrollY / MaxScroll * MaxSlider;
156
157 bool Grabbed = false;
158 const void *pId = &m_ScrollY;
159 const bool InsideSlider = Ui()->MouseHovered(pRect: &Slider);
160 const bool InsideRail = Ui()->MouseHovered(pRect: &m_RailRect);
161
162 if(Ui()->CheckActiveItem(pId) && Ui()->MouseButton(Index: 0))
163 {
164 float MouseY = Ui()->MouseY();
165 m_ScrollY += (MouseY - (Slider.y + m_SliderGrabPos)) / MaxSlider * MaxScroll;
166 m_SliderGrabPos = std::clamp(val: m_SliderGrabPos, lo: 0.0f, hi: SliderHeight);
167 m_AnimTargetScrollY = m_ScrollY;
168 m_AnimTime = 0.0f;
169 Grabbed = true;
170 }
171 else if(InsideSlider)
172 {
173 if(!Ui()->MouseButton(Index: 0))
174 Ui()->SetHotItem(pId);
175
176 if(!Ui()->CheckActiveItem(pId) && Ui()->MouseButtonClicked(Index: 0))
177 {
178 Ui()->SetActiveItem(pId);
179 m_SliderGrabPos = Ui()->MouseY() - Slider.y;
180 m_AnimTargetScrollY = m_ScrollY;
181 m_AnimTime = 0.0f;
182 }
183 }
184 else if(InsideRail && Ui()->MouseButtonClicked(Index: 0))
185 {
186 m_ScrollY += (Ui()->MouseY() - (Slider.y + Slider.h / 2.0f)) / MaxSlider * MaxScroll;
187 Ui()->SetHotItem(pId);
188 Ui()->SetActiveItem(pId);
189 m_SliderGrabPos = Slider.h / 2.0f;
190 m_AnimTargetScrollY = m_ScrollY;
191 m_AnimTime = 0.0f;
192 }
193
194 if(Ui()->CheckActiveItem(pId) && !Ui()->MouseButton(Index: 0))
195 {
196 Ui()->SetActiveItem(nullptr);
197 }
198
199 m_ScrollY = std::clamp(val: m_ScrollY, lo: 0.0f, hi: MaxScroll);
200 m_ContentScrollOff.y = -m_ScrollY;
201
202 Slider.Draw(Color: m_Params.SliderColor(Active: Grabbed, Hovered: Ui()->HotItem() == pId), Corners: IGraphics::CORNER_ALL, Rounding: Slider.w / 2.0f);
203}
204
205bool CScrollRegion::AddRect(const CUIRect &Rect, bool ShouldScrollHere)
206{
207 m_LastAddedRect = Rect;
208 // Round up and add magic to fix pixel clipping at the end of the scrolling area
209 m_ContentH = maximum(a: std::ceil(x: Rect.y + Rect.h - (m_ClipRect.y + m_ContentScrollOff.y)) + HEIGHT_MAGIC_FIX, b: m_ContentH);
210 if(ShouldScrollHere)
211 ScrollHere();
212 return !RectClipped(Rect);
213}
214
215void CScrollRegion::ScrollHere(EScrollOption Option)
216{
217 const float MinHeight = minimum(a: m_ClipRect.h, b: m_LastAddedRect.h);
218 const float TopScroll = m_LastAddedRect.y - (m_ClipRect.y + m_ContentScrollOff.y);
219
220 switch(Option)
221 {
222 case SCROLLHERE_TOP:
223 m_RequestScrollY = TopScroll;
224 break;
225
226 case SCROLLHERE_BOTTOM:
227 m_RequestScrollY = TopScroll - (m_ClipRect.h - MinHeight);
228 break;
229
230 case SCROLLHERE_KEEP_IN_VIEW:
231 default:
232 const float DeltaY = m_LastAddedRect.y - m_ClipRect.y;
233 if(DeltaY < 0)
234 m_RequestScrollY = TopScroll;
235 else if(DeltaY > (m_ClipRect.h - MinHeight))
236 m_RequestScrollY = TopScroll - (m_ClipRect.h - MinHeight);
237 break;
238 }
239}
240
241void CScrollRegion::ScrollRelative(EScrollRelative Direction, float SpeedMultiplier)
242{
243 m_ScrollDirection = Direction;
244 m_ScrollSpeedMultiplier = SpeedMultiplier;
245}
246
247void CScrollRegion::ScrollRelativeDirect(float ScrollAmount)
248{
249 m_RequestScrollY = std::clamp(val: m_ScrollY + ScrollAmount, lo: 0.0f, hi: m_ContentH - m_ClipRect.h);
250}
251
252void CScrollRegion::DoEdgeScrolling()
253{
254 if(!ScrollbarShown())
255 return;
256
257 const float ScrollBorderSize = 20.0f;
258 const float MaxScrollMultiplier = 2.0f;
259 const float ScrollSpeedFactor = MaxScrollMultiplier / ScrollBorderSize;
260 const float TopScrollPosition = m_ClipRect.y + ScrollBorderSize;
261 const float BottomScrollPosition = m_ClipRect.y + m_ClipRect.h - ScrollBorderSize;
262 if(Ui()->MouseY() < TopScrollPosition)
263 ScrollRelative(Direction: SCROLLRELATIVE_UP, SpeedMultiplier: minimum(a: MaxScrollMultiplier, b: (TopScrollPosition - Ui()->MouseY()) * ScrollSpeedFactor));
264 else if(Ui()->MouseY() > BottomScrollPosition)
265 ScrollRelative(Direction: SCROLLRELATIVE_DOWN, SpeedMultiplier: minimum(a: MaxScrollMultiplier, b: (Ui()->MouseY() - BottomScrollPosition) * ScrollSpeedFactor));
266}
267
268bool CScrollRegion::RectClipped(const CUIRect &Rect) const
269{
270 return (m_ClipRect.x > (Rect.x + Rect.w) || (m_ClipRect.x + m_ClipRect.w) < Rect.x || m_ClipRect.y > (Rect.y + Rect.h) || (m_ClipRect.y + m_ClipRect.h) < Rect.y);
271}
272
273bool CScrollRegion::ScrollbarShown() const
274{
275 return m_ContentH > m_ClipRect.h;
276}
277
278bool CScrollRegion::Animating() const
279{
280 return m_AnimTime > 0.0f;
281}
282
283bool CScrollRegion::Active() const
284{
285 return Ui()->ActiveItem() == &m_ScrollY;
286}
287