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