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