GCC Code Coverage Report


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