GCC Code Coverage Report


./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
0 of 211, 0 excluded
0.0%
Functions:
0 of 16, 0 excluded
0.0%
Branches:
0 of 299, 0 excluded
0.0%

libs/mrgui/src/eu/mrgui/widgets.cc
Line Branch Exec Source
1 #include "eu/mrgui/widgets.h"
2
3 #include <SDL_timer.h>
4
5 #include "eu/render/canvas.h"
6 #include "eu/render/font.h"
7
8 namespace eu::mrgui
9 {
10 namespace
11 {
12 /// djb2 initial hash
13 constexpr u32 initial_hash = 5381;
14
15 /// djb2 hash function
16 ✗ u32 hash(const unsigned char* str, std::size_t length, u32 seed)
17 {
18 ✗ u32 hash = seed;
19
20 ✗ for (std::size_t index = 0; index < length; index += 1)
21 {
22 ✗ auto c = str[index];
23 ✗ hash = ((hash << 5) + hash) + c;
24 }
25
26 ✗ return hash;
27 }
28 }
29
30 ✗ [[nodiscard]] u32 IdStack::get(const std::string& str) const
31 {
32 ✗ return hash(reinterpret_cast<const unsigned char*>(str.data()), str.size(), last_id());
33 }
34
35 ✗ [[nodiscard]] u32 IdStack::get(int id) const
36 {
37 ✗ return hash(reinterpret_cast<unsigned char*>(&id), sizeof(int), last_id());
38 }
39 ✗ void IdStack::push(const std::string& str)
40 {
41 ✗ stack.push_back(get(str));
42 ✗ }
43 ✗ void IdStack::push(int id)
44 {
45 ✗ stack.push_back(get(id));
46 ✗ }
47 ✗ void IdStack::pop()
48 {
49 ✗ stack.pop_back();
50 ✗ }
51 ✗ [[nodiscard]] u32 IdStack::last_id() const
52 {
53 ✗ return stack.empty() ? initial_hash : *stack.rbegin();
54 }
55
56
57
58 ✗ void UiState::begin()
59 {
60 ✗ hot_item = std::nullopt;
61 ✗ }
62
63 ✗ void UiState::end()
64 {
65 ✗ if (mousedown)
66 {
67 ✗ if (active_item == std::nullopt)
68 {
69 ✗ active_item = std::nullopt;
70 ✗ active_item_locked = true;
71 }
72 }
73 else
74 {
75 ✗ active_item = std::nullopt;
76 ✗ active_item_locked = false;
77 }
78
79 ✗ if (key && key->key == SDLK_TAB)
80 {
81 ✗ kbd_item = std::nullopt;
82 }
83 ✗ key = std::nullopt;
84 ✗ keychar = std::nullopt;
85 ✗ }
86
87 ✗ [[nodiscard]] bool UiState::is_active_free() const
88 {
89 ✗ return active_item_locked == false && active_item.has_value() == false;
90 }
91
92 // Simple button IMGUI widget
93 ✗ bool button_logic(u32 id, const Rect& rect, UiState& uistate)
94 {
95 ✗ if (is_within(uistate.mouse, rect))
96 {
97 ✗ uistate.hot_item = id;
98 ✗ if (uistate.is_active_free() && uistate.mousedown)
99 {
100 ✗ uistate.active_item = id;
101 }
102 }
103
104 ✗ if (uistate.kbd_item == std::nullopt)
105 {
106 ✗ uistate.kbd_item = id;
107 }
108
109 // keyboard interaction
110 ✗ if (uistate.kbd_item == id)
111 {
112 ✗ if (uistate.key.has_value())
113 {
114 ✗ const auto k = *uistate.key;
115 ✗ uistate.key = std::nullopt;
116 ✗ switch (k.key)
117 {
118 ✗ case SDLK_TAB:
119 ✗ uistate.kbd_item = std::nullopt;
120 ✗ if ((k.mod & SDL_KMOD_SHIFT) != 0)
121 {
122 ✗ uistate.kbd_item = uistate.last_widget;
123 }
124 ✗ break;
125 ✗ case SDLK_RETURN:
126 ✗ return true;
127 ✗ default:
128 // ignore
129 ✗ break;
130 }
131 }
132 }
133
134 ✗ uistate.last_widget = id;
135
136 // If button is hot and active, but mouse button is not
137 // down, the user must have clicked the button.
138 ✗ if (uistate.mousedown == false &&
139 ✗ uistate.hot_item == id &&
140 ✗ uistate.active_item == id)
141 {
142 ✗ return true;
143 }
144
145 // Otherwise, no clicky.
146 ✗ return false;
147 }
148
149 ✗ bool basic_button(u32 id, const Rect& rect, UiState& uistate, render::SpriteBatch* batch)
150 {
151 ✗ const auto ret = button_logic(id, rect, uistate);
152
153 ✗ if (uistate.kbd_item == id)
154 {
155 ✗ render::Quad{
156 .tint = colors::red_vermillion
157 ✗ }.draw(batch, rect.with_inset(Lrtb{ -4 }).with_offset({ 8, -8 }));
158 }
159
160 // shadow
161 ✗ render::Quad{
162 .tint = colors::black
163 ✗ }.draw(batch, rect.with_offset({ 8.0f, -8.0f }));
164
165 ✗ if (uistate.hot_item == id)
166 {
167 ✗ if (uistate.active_item == id)
168 {
169 // Button is both 'hot' and 'active'
170 ✗ render::Quad{ .tint = colors::white }.draw(batch, rect.with_offset({ 2.0f, -2.0f }));
171 }
172 else
173 {
174 // Button is merely 'hot'
175 ✗ render::Quad{ .tint = colors::white }.draw(batch, rect);
176 }
177 }
178 else
179 {
180 // button is not hot, but it may be active
181 ✗ render::Quad{ .tint = colors::orange }.draw(batch, rect);
182 // todo(Gustav): add grays
183 }
184
185 ✗ return ret;
186 }
187
188 ✗ bool button_texture(u32 id, const Rect& rect, UiState& uistate, render::SpriteBatch* batch, const render::Texture2d* texture, const Rect& active, const Rect& hot, const Rect& normal)
189 {
190 ✗ const auto ret = button_logic(id, rect, uistate);
191
192 ✗ if (uistate.kbd_item == id)
193 {
194 // keep?
195 ✗ render::Quad{
196 .tint = colors::red_vermillion
197 ✗ }.draw(batch, rect.with_inset(Lrtb{ -4 }).with_offset({ 8, -8 }));
198 }
199
200 ✗ if (uistate.hot_item == id)
201 {
202 ✗ if (uistate.active_item == id)
203 {
204 // Button is both 'hot' and 'active'
205 ✗ render::Quad{ .texture = texture, .texturecoord = active }.draw(batch, rect);
206 }
207 else
208 {
209 // Button is merely 'hot'
210 ✗ render::Quad{ .texture = texture, .texturecoord = hot }.draw(batch, rect);
211 }
212 }
213 else
214 {
215 // button is not hot, but it may be active
216 ✗ render::Quad{ .texture = texture, .texturecoord = normal }.draw(batch, rect);
217 // todo(Gustav): add grays
218 }
219
220 ✗ return ret;
221 }
222
223 ✗ bool slider(u32 id, float* val, const Rect& rect, UiState& uistate, render::SpriteBatch* batch)
224 {
225 ✗ const auto grab_size = 16.0f;
226 ✗ const auto inner = rect.with_inset(Lrtb{ 3.0f });
227 ✗ const auto avail = inner.get_size().x - grab_size;
228 ✗ const auto pos = avail * *val;
229
230 ✗ const auto grabber_rect = Rect::from_size({ grab_size, inner.get_size().y }).with_bottom_left_at({ inner.left + pos, inner.bottom });
231
232 ✗ if (is_within(uistate.mouse, inner))
233 {
234 ✗ uistate.hot_item = id;
235 ✗ if (uistate.is_active_free() && uistate.mousedown)
236 {
237 ✗ uistate.active_item = id;
238 }
239 }
240
241 ✗ if (uistate.kbd_item == std::nullopt)
242 {
243 ✗ uistate.kbd_item = id;
244 }
245
246 ✗ if (uistate.kbd_item == id)
247 {
248 ✗ render::Quad{ .tint = colors::red_vermillion }.draw(batch, rect.with_inset(Lrtb{ -6 }));
249 }
250
251 ✗ render::Quad{ .tint = colors::orange }.draw(batch, rect);
252 ✗ render::Quad{ .tint = uistate.active_item == id && uistate.hot_item == id ? colors::white : colors::green_bluish }.draw(batch, grabber_rect);
253 ✗ if (uistate.kbd_item == id)
254 {
255 ✗ if (uistate.key.has_value())
256 {
257 ✗ const auto k = *uistate.key;
258 ✗ uistate.key = std::nullopt;
259 ✗ constexpr float change = 0.1f;
260
261 ✗ switch (k.key)
262 {
263 ✗ case SDLK_TAB:
264 ✗ uistate.kbd_item = std::nullopt;
265 ✗ if ((k.mod & SDL_KMOD_SHIFT) != 0)
266 {
267 ✗ uistate.kbd_item = uistate.last_widget;
268 }
269 ✗ break;
270 ✗ case SDLK_LEFT:
271 ✗ if (*val > 0)
272 {
273 ✗ *val = std::max(0.0f, *val - change);
274 ✗ return true;
275 }
276 ✗ break;
277 ✗ case SDLK_RIGHT:
278 ✗ if (*val < 1)
279 {
280 ✗ *val = std::min(1.0f, *val + change);
281 ✗ return true;
282 }
283 ✗ break;
284 ✗ default:
285 // ignore
286 ✗ break;
287 }
288 }
289 }
290
291 ✗ uistate.last_widget = id;
292
293 ✗ if (uistate.active_item == id)
294 {
295 ✗ const auto new_range = to_01(inner, uistate.mouse);
296 ✗ const auto new_value = keep_within01(new_range.x);
297 ✗ if (*val != new_value)
298 {
299 ✗ *val = new_value;
300 ✗ return true;
301 }
302 }
303
304 ✗ return false;
305 }
306
307
308 ✗ void draw_text(render::SpriteBatch* batch, render::DrawableFont* font, const std::string& str, float size, const v2& pos, const Rgb& color)
309 {
310 ✗ render::DrawableText text{ font };
311 ✗ text.set_text(str);
312 ✗ text.set_size(size);
313 ✗ text.compile();
314 ✗ text.draw(batch, pos, color, color);
315 ✗ }
316
317 ✗ bool textfield(u32 id, std::string* val, render::DrawableFont* font, float size, const v2& pos, UiState& uistate, render::SpriteBatch* batch)
318 {
319 using namespace std;
320 ✗ bool changed = false;
321
322 ✗ auto& text_state = uistate.text_state;
323
324 // Layout
325 ✗ render::DrawableText text{ font };
326 ✗ text.set_text(*val);
327 ✗ text.set_size(size);
328 ✗ text.compile();
329 ✗ const auto rect = text.get_extents().with_offset(pos).with_inset(Lrtb{ -6 });
330
331 ✗ if (is_within(uistate.mouse, rect)) {
332 ✗ uistate.hot_item = id;
333 ✗ if (uistate.active_item == std::nullopt && uistate.mousedown)
334 {
335 ✗ uistate.active_item = id;
336 ✗ text_state.focus(val);
337 }
338 }
339 ✗ if (uistate.kbd_item == std::nullopt)
340 {
341 ✗ uistate.kbd_item = id;
342 }
343
344 ✗ if (uistate.kbd_item == id) {
345 ✗ render::Quad{ .tint = colors::red_vermillion }.draw(batch, rect.with_inset(Lrtb{ -6 }));
346 }
347 ✗ if (uistate.active_item == id || uistate.hot_item == id) {
348 ✗ render::Quad{ .tint = colors::white }.draw(batch, rect.with_inset(Lrtb{ -6 }));
349 }
350 else {
351 ✗ render::Quad{ .tint = colors::green_bluish }.draw(batch, rect.with_inset(Lrtb{ -6 }));
352 }
353
354 // Handle input
355 ✗ if (uistate.kbd_item == id) {
356 ✗ if (uistate.key.has_value()) {
357 ✗ auto k = uistate.key.value();
358 ✗ const auto shift = k.mod & SDL_KMOD_SHIFT;
359 ✗ switch (k.key) {
360 ✗ case SDLK_LEFT: text_state.onKeyLeft(shift != 0, val); changed = true; break;
361 ✗ case SDLK_RIGHT: text_state.onKeyRight(shift != 0, val); changed = true; break;
362 ✗ case SDLK_HOME: text_state.onKeyLineStart(shift != 0, val); changed = true; break;
363 ✗ case SDLK_END: text_state.onKeyLineEnd(shift != 0, val); changed = true; break;
364 ✗ case SDLK_BACKSPACE: text_state.onKeyBackspace(shift != 0, val); changed = true; break;
365 ✗ case SDLK_DELETE: text_state.onKeyDelete(shift != 0, val); changed = true; break;
366 ✗ case SDLK_RETURN: // ignore
367 case SDLK_TAB:
368 ✗ uistate.kbd_item = std::nullopt;
369 ✗ if (shift != 0)
370 {
371 ✗ uistate.kbd_item = uistate.last_widget;
372 }
373 ✗ uistate.key = std::nullopt;
374 ✗ break;
375 ✗ default: break;
376 }
377 }
378 ✗ if (uistate.keychar.has_value() && *uistate.keychar >= 32 && *uistate.keychar < 127) {
379 ✗ const char c = *uistate.keychar;
380 ✗ text_state.onChar(c, val);
381 ✗ changed = true;
382 }
383 }
384
385 // Mouse click to set cursor
386 ✗ if (uistate.mousedown == false && uistate.hot_item == id && uistate.active_item == id) {
387 // Map mouse x to character index
388 ✗ text_state.click(val, uistate.mouse.x - rect.left, uistate.mouse.y - rect.top);
389 ✗ uistate.kbd_item = id;
390 }
391
392 ✗ uistate.last_widget = id;
393
394 static int last_cursor = -10;
395 ✗ const auto current_cursor = text_state.state.cursor;
396 ✗ if (last_cursor != current_cursor)
397 {
398 ✗ LOG_INFO("Cursor position: {}", current_cursor);
399 ✗ last_cursor = current_cursor;
400 }
401
402 // Draw text
403 ✗ text.draw(batch, pos, colors::black, colors::black);
404
405 // Draw cursor
406 ✗ if (uistate.kbd_item == id) {
407 ✗ const auto is_cursor_blink_visible = (SDL_GetTicks() >> 8) & 1;
408 ✗ if (is_cursor_blink_visible != 0) {
409 // Compute cursor x
410 ✗ float cursor_x = rect.left;
411 ✗ if (text_state.state.cursor > 0 && std::cmp_less_equal(text_state.state.cursor, val->size())) {
412 ✗ const std::string substr = val->substr(0, text_state.state.cursor);
413 ✗ render::DrawableText t{ font };
414 ✗ t.set_text(substr);
415 ✗ t.set_size(size);
416 ✗ t.compile();
417 ✗ cursor_x = rect.left + t.get_extents().get_size().x;
418 ✗ }
419 ✗ const auto ascent = font->ascent * size;
420 ✗ const auto descent = font->descent * size;
421 ✗ render::Quad{ .tint = colors::black }.draw(batch, Rect::from_size({ 1, ascent + -descent }).with_bottom_left_at({ cursor_x, pos.y + descent }));
422 }
423 }
424
425 ✗ return changed;
426 ✗ }
427
428
429 }
430