1#pragma once
2
3#include "C4Facet.h"
4#include "C4Gui.h"
5#include "C4GuiEdit.h"
6#include "C4GuiResource.h"
7#include "C4MouseControl.h"
8#include "C4NumberParsing.h"
9#include "StdApp.h"
10
11#include <algorithm>
12#include <cstdint>
13#include <limits>
14#include <memory>
15
16/*
17 * LegacyClonk
18 *
19 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
20 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
21 *
22 * Distributed under the terms of the ISC license; see accompanying file
23 * "COPYING" for details.
24 *
25 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
26 * See accompanying file "TRADEMARK" for details.
27 *
28 * To redistribute this file separately, substitute the full license texts
29 * for the above references.
30 */
31
32namespace C4GUI
33{
34template <std::integral T>
35class SpinBox : public Edit
36{
37 using limits = std::numeric_limits<T>;
38
39 static constexpr std::int32_t ArrowHeight{8};
40 static constexpr std::int32_t ArrowWidth{13};
41 static constexpr std::int32_t ArrowMarginTop{2};
42 static constexpr std::int32_t ArrowMarginRight{1};
43 static constexpr std::int32_t ArrowMarginLeft{2};
44
45public:
46 SpinBox(const C4Rect &bounds, const bool focusEdit = false, const T minimum = limits::min(), const T maximum = limits::max()) : Edit{bounds, focusEdit}, minimum{minimum}, maximum{maximum}
47 {
48 UpdateSize();
49
50 UpdateMaxText();
51 SetValue(value: T{}, user: false);
52
53 C4CustomKey::Priority keyPrio = focusEdit ? C4CustomKey::PRIO_FocusCtrl : C4CustomKey::PRIO_Ctrl;
54 keyUp = std::make_unique<C4KeyBinding>(C4KeyCodeEx{K_UP}, "GUINumberEditUp", KEYSCOPE_Gui, new ControlKeyCB{*this, &SpinBox::OffsetValueCallback<+1>}, keyPrio);
55 keyDown = std::make_unique<C4KeyBinding>(C4KeyCodeEx{K_DOWN}, "GUINumberEditDown", KEYSCOPE_Gui, new ControlKeyCB{*this, &SpinBox::OffsetValueCallback<-1>}, keyPrio);
56 keyPageUp = std::make_unique<C4KeyBinding>(C4KeyCodeEx{K_PAGEUP}, "GUINumberEditPageUp", KEYSCOPE_Gui, new ControlKeyCB{*this, &SpinBox::OffsetValueCallback<+10>}, keyPrio);
57 keyPageDown = std::make_unique<C4KeyBinding>(C4KeyCodeEx{K_PAGEDOWN}, "GUINumberEditPageDown", KEYSCOPE_Gui, new ControlKeyCB{*this, &SpinBox::OffsetValueCallback<-10>}, keyPrio);
58 }
59
60 void SetMinimum(const T newMinimum)
61 {
62 minimum = newMinimum;
63 UpdateMaxText();
64 }
65
66 void SetMaximum(const T newMaximum)
67 {
68 maximum = newMaximum;
69 UpdateMaxText();
70 }
71
72 T GetValue()
73 {
74 const auto result = [this]
75 {
76 try
77 {
78 const std::string_view text{GetText()};
79 if (text.empty())
80 {
81 return T{};
82 }
83 return ParseNumber<T>(text);
84 }
85 catch (const NumberRangeError<T> &e)
86 {
87 return e.AtRangeLimit;
88 }
89 }();
90 return std::clamp(result, minimum, maximum);
91 }
92
93 void SetValue(const T value, const bool user)
94 {
95 const auto clamped = std::clamp(value, minimum, maximum);
96 SetText(text: std::to_string(clamped), fUser: user);
97 }
98
99protected:
100 void OnTextChange() override
101 {
102 bool changed{false};
103 std::string text{GetText()};
104 for (std::size_t i = (text.starts_with(x: '-') && minimum < 0) ? 1 : 0; i < text.size();)
105 {
106 const auto c = text[i];
107 if (c < '0' || c > '9')
108 {
109 text.erase(pos: i, n: 1);
110 if (iCursorPos >= i)
111 {
112 --iCursorPos;
113 }
114 changed = true;
115 continue;
116 }
117 ++i;
118 }
119 if (changed)
120 {
121 const auto cursorPos = iCursorPos;
122 SetText(text: text.c_str(), fUser: false);
123 iCursorPos = cursorPos;
124 }
125 }
126
127 InputResult OnFinishInput([[maybe_unused]] const bool pasting, [[maybe_unused]] const bool pastingMore) override
128 {
129 SetValue(value: GetValue(), user: true);
130 return IR_None;
131 }
132
133 void MouseInput(CMouse &mouse, const std::int32_t button, const std::int32_t x, const std::int32_t y, const std::uint32_t keyParam) override
134 {
135 switch (button)
136 {
137 case C4MC_Button_Wheel:
138 {
139 const auto delta = static_cast<short>(keyParam >> 16);
140 OffsetValue(change: delta < 0 ? -1 : +1, user: true);
141 return;
142 }
143 case C4MC_Button_LeftDown:
144 case C4MC_Button_LeftDouble:
145 if (arrowsRect.Wdt <= 0 || !Inside(x, arrowsRect.x - rcBounds.x, rcBounds.Wdt))
146 {
147 break;
148 }
149 if (mouse.pDragElement)
150 {
151 break;
152 }
153 mouse.pDragElement = this;
154 [[fallthrough]];
155 case C4MC_Button_LeftUp:
156 {
157 const auto isDownButton = y > arrowsRect.GetMiddleY() - rcBounds.y;
158 const auto prevPressed = upButtonPressed || downButtonPressed;
159 if (button == C4MC_Button_LeftUp)
160 {
161 upButtonPressed = false;
162 downButtonPressed = false;
163 }
164 else
165 {
166 OffsetValue(change: isDownButton ? -1 : +1, user: true);
167 (isDownButton ? downButtonPressed : upButtonPressed) = true;
168 }
169 if ((upButtonPressed || downButtonPressed) != prevPressed)
170 {
171 GUISound(szSound: "ArrowHit");
172 }
173 return;
174 }
175 }
176 Edit::MouseInput(rMouse&: mouse, iButton: button, iX: x, iY: y, dwKeyParam: keyParam);
177 }
178
179 void DoDragging(CMouse &mouse, const std::int32_t x, const std::int32_t y, const std::uint32_t keyParam) override
180 {
181 if (!upButtonPressed && !downButtonPressed)
182 {
183 Edit::DoDragging(rMouse&: mouse, iX: x, iY: y, dwKeyParam: keyParam);
184 }
185 }
186
187 void StopDragging(CMouse &mouse, const std::int32_t x, const std::int32_t y, const std::uint32_t keyParam) override
188 {
189 if (upButtonPressed || downButtonPressed)
190 {
191 MouseInput(mouse, button: C4MC_Button_LeftUp, x, y, keyParam);
192 }
193 else
194 {
195 Edit::StopDragging(rMouse&: mouse, iX: x, iY: y, dwKeyParam: keyParam);
196 }
197 }
198
199 void DrawElement(C4FacetEx &cgo) override
200 {
201 Edit::DrawElement(cgo);
202 if (arrowsRect.Wdt > 0)
203 {
204 static C4DrawTransform flipVerticalTransform = []{
205 C4DrawTransform t;
206 t.Set(fA: 1, fB: 0, fC: 0, fD: 0, fE: -1, fF: 0, fG: 0, fH: 0, fI: 1);
207 return t;
208 }();
209
210 C4Facet& arrow = GetRes()->fctSpinBoxArrow;
211
212 const auto x0 = cgo.TargetX + arrowsRect.x;
213 const auto y0 = cgo.TargetY + arrowsRect.y;
214 arrow.DrawT(sfcTarget: cgo.Surface, iX: x0 + upButtonPressed, iY: - (y0 + upButtonPressed + arrow.Hgt), iPhaseX: 0, iPhaseY: 0, pTransform: &flipVerticalTransform);
215 arrow.Draw(sfcTarget: cgo.Surface, iX: x0 + downButtonPressed, iY: y0 + arrowsRect.Hgt - arrow.Hgt + downButtonPressed);
216 }
217 }
218
219 void UpdateSize() override
220 {
221 if (ResizeToIdealWidth())
222 {
223 return;
224 }
225
226 // reserve space for up-/down arrows
227 auto& clientWidth = rcClientRect.Wdt;
228 if (clientWidth > 30)
229 {
230 clientWidth -= ArrowWidth + ArrowMarginLeft + ArrowMarginRight;
231 arrowsRect = {rcClientRect.x + clientWidth + ArrowMarginLeft, rcClientRect.y + ArrowMarginTop, ArrowWidth, rcClientRect.Hgt - ArrowMarginTop * 2};
232 }
233 else
234 {
235 arrowsRect = {0, 0, 0, 0};
236 }
237 }
238
239 bool ResizeToIdealWidth()
240 {
241 const auto digits = std::max(GetNumberLength(number: minimum), GetNumberLength(number: maximum));
242 const auto idealWidth = pFont->GetTextWidth(szText: std::string(digits, '0').c_str(), fCheckMarkup: false) + C4GUI_ScrollArrowWdt + 2;
243 if (idealWidth < rcBounds.Wdt)
244 {
245 rcBounds.Wdt = idealWidth;
246 SetBounds(rcBounds);
247 return true;
248 }
249
250 return false;
251 }
252
253private:
254 template <std::make_signed_t<T> change>
255 bool OffsetValueCallback()
256 {
257 OffsetValue(change, user: true);
258 return true;
259 }
260
261 void OffsetValue(const std::make_signed_t<T> change, const bool user)
262 {
263 const auto oldValue = GetValue();
264 if (change < 0 && oldValue < limits::min() - change)
265 {
266 SetValue(value: limits::min(), user);
267 }
268 else if (change > 0 && oldValue > limits::max() - change)
269 {
270 SetValue(value: limits::max(), user);
271 }
272 else
273 {
274 SetValue(value: oldValue + change, user);
275 }
276 OnTextChange();
277 }
278
279 void UpdateMaxText()
280 {
281 SetMaxText(std::max(GetNumberLength(number: minimum), GetNumberLength(number: maximum)));
282 }
283
284 static unsigned short GetNumberLength(T number)
285 {
286 if (number < 0)
287 {
288 // avoid overflow
289 if (number == limits::min())
290 {
291 number += 1;
292 }
293 number = -number;
294 }
295
296 if (number == 0)
297 {
298 return 1;
299 }
300 return checked_cast<unsigned short>(std::lround(std::log10(number))) + 1 + std::signed_integral<T>;
301 }
302
303 C4Rect arrowsRect;
304
305 std::unique_ptr<C4KeyBinding> keyUp;
306 std::unique_ptr<C4KeyBinding> keyDown;
307 std::unique_ptr<C4KeyBinding> keyPageUp;
308 std::unique_ptr<C4KeyBinding> keyPageDown;
309
310 T minimum;
311 T maximum;
312
313 bool upButtonPressed{false};
314 bool downButtonPressed{false};
315};
316}
317