summaryrefslogtreecommitdiff
path: root/src/ui/widget.c
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2020-07-21 15:06:52 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2020-07-21 15:07:38 +0300
commitd773b499e595a43b9b1ae449262dcf13cabf2d02 (patch)
treeb1baeb12025a04f8316636b5d0ab18e30ceedb2c /src/ui/widget.c
Initial commit
Borrowing the app skeleton from Bitwise Harmony.
Diffstat (limited to 'src/ui/widget.c')
-rw-r--r--src/ui/widget.c618
1 files changed, 618 insertions, 0 deletions
diff --git a/src/ui/widget.c b/src/ui/widget.c
new file mode 100644
index 00000000..49fcd7c0
--- /dev/null
+++ b/src/ui/widget.c
@@ -0,0 +1,618 @@
1#include "widget.h"
2
3#include "app.h"
4#include "command.h"
5#include "paint.h"
6#include "util.h"
7#include "window.h"
8
9#include <the_Foundation/ptrset.h>
10#include <SDL_mouse.h>
11#include <stdarg.h>
12
13iDeclareType(RootData)
14
15struct Impl_RootData {
16 iWidget *hover;
17 iWidget *mouseGrab;
18 iWidget *focus;
19 iPtrSet *onTop;
20 iPtrSet *pendingDestruction;
21};
22
23static iRootData rootData_;
24
25iPtrSet *onTop_RootData_(void) {
26 if (!rootData_.onTop) {
27 rootData_.onTop = new_PtrSet();
28 }
29 return rootData_.onTop;
30}
31
32void destroyPending_Widget(void) {
33 iForEach(PtrSet, i, rootData_.pendingDestruction) {
34 iWidget *widget = *i.value;
35 remove_PtrSet(onTop_RootData_(), widget);
36 iRelease(removeChild_Widget(widget->parent, widget));
37 remove_PtrSetIterator(&i);
38 }
39}
40
41iDefineObjectConstruction(Widget)
42
43void init_Widget(iWidget *d) {
44 init_String(&d->id);
45 d->flags = 0;
46 d->rect = zero_Rect();
47 d->bgColor = none_ColorId;
48 d->children = NULL;
49 d->parent = NULL;
50 d->commandHandler = NULL;
51}
52
53void deinit_Widget(iWidget *d) {
54 iReleasePtr(&d->children);
55 deinit_String(&d->id);
56}
57
58static void aboutToBeDestroyed_Widget_(iWidget *d) {
59 if (isFocused_Widget(d)) {
60 setFocus_Widget(NULL);
61 return;
62 }
63 if (isHover_Widget(d)) {
64 rootData_.hover = NULL;
65 }
66 iForEach(ObjectList, i, d->children) {
67 aboutToBeDestroyed_Widget_(as_Widget(i.object));
68 }
69}
70
71void destroy_Widget(iWidget *d) {
72 aboutToBeDestroyed_Widget_(d);
73 if (!rootData_.pendingDestruction) {
74 rootData_.pendingDestruction = new_PtrSet();
75 }
76 insert_PtrSet(rootData_.pendingDestruction, d);
77}
78
79void setId_Widget(iWidget *d, const char *id) {
80 setCStr_String(&d->id, id);
81}
82
83const iString *id_Widget(const iWidget *d) {
84 return &d->id;
85}
86
87int flags_Widget(const iWidget *d) {
88 return d->flags;
89}
90
91void setFlags_Widget(iWidget *d, int flags, iBool set) {
92 iChangeFlags(d->flags, flags, set);
93 if (flags & keepOnTop_WidgetFlag) {
94 if (set) {
95 insert_PtrSet(onTop_RootData_(), d);
96 }
97 else {
98 remove_PtrSet(onTop_RootData_(), d);
99 }
100 }
101}
102
103void setPos_Widget(iWidget *d, iInt2 pos) {
104 d->rect.pos = pos;
105}
106
107void setSize_Widget(iWidget *d, iInt2 size) {
108 d->rect.size = size;
109 setFlags_Widget(d, fixedSize_WidgetFlag, iTrue);
110}
111
112void setBackgroundColor_Widget(iWidget *d, int bgColor) {
113 d->bgColor = bgColor;
114}
115
116void setCommandHandler_Widget(iWidget *d, iBool (*handler)(iWidget *, const char *)) {
117 d->commandHandler = handler;
118}
119
120static int numExpandingChildren_Widget_(const iWidget *d) {
121 int count = 0;
122 iConstForEach(ObjectList, i, d->children) {
123 const iWidget *child = constAs_Widget(i.object);
124 if (flags_Widget(child) & expand_WidgetFlag) {
125 count++;
126 }
127 }
128 return count;
129}
130
131static int widestChild_Widget_(const iWidget *d) {
132 int width = 0;
133 iConstForEach(ObjectList, i, d->children) {
134 const iWidget *child = constAs_Widget(i.object);
135 width = iMax(width, child->rect.size.x);
136 }
137 return width;
138}
139
140static void setWidth_Widget_(iWidget *d, int width) {
141 if (~d->flags & fixedWidth_WidgetFlag) {
142 d->rect.size.x = width;
143 }
144}
145
146static void setHeight_Widget_(iWidget *d, int height) {
147 if (~d->flags & fixedHeight_WidgetFlag) {
148 d->rect.size.y = height;
149 }
150}
151
152void arrange_Widget(iWidget *d) {
153 if (d->flags & resizeToParentWidth_WidgetFlag) {
154 setWidth_Widget_(d, d->parent->rect.size.x);
155 }
156 if (d->flags & resizeToParentHeight_WidgetFlag) {
157 setHeight_Widget_(d, d->parent->rect.size.y);
158 }
159 /* The rest of the arrangement depends on child widgets. */
160 if (!d->children) {
161 return;
162 }
163 /* Resize children to fill the parent widget. */
164 const size_t childCount = size_ObjectList(d->children);
165 if (d->flags & resizeChildren_WidgetFlag) {
166 const int expCount = numExpandingChildren_Widget_(d);
167 /* Only resize the expanding children, not touching the others. */
168 if (expCount > 0) {
169 iInt2 avail = d->rect.size;
170 iConstForEach(ObjectList, i, d->children) {
171 const iWidget *child = constAs_Widget(i.object);
172 if (~child->flags & expand_WidgetFlag) {
173 subv_I2(&avail, child->rect.size);
174 }
175 }
176 avail = divi_I2(avail, expCount);
177 iForEach(ObjectList, j, d->children) {
178 iWidget *child = as_Widget(j.object);
179 if (child->flags & expand_WidgetFlag) {
180 if (d->flags & arrangeHorizontal_WidgetFlag) {
181 setWidth_Widget_(child, avail.x);
182 setHeight_Widget_(child, d->rect.size.y);
183 }
184 else if (d->flags & arrangeVertical_WidgetFlag) {
185 setWidth_Widget_(child, d->rect.size.x);
186 setHeight_Widget_(child, avail.y);
187 }
188 }
189 else {
190 /* Fill the off axis, though. */
191 if (d->flags & arrangeHorizontal_WidgetFlag) {
192 setHeight_Widget_(child, d->rect.size.y);
193 }
194 else if (d->flags & arrangeVertical_WidgetFlag) {
195 setWidth_Widget_(child, d->rect.size.x);
196 }
197 }
198 }
199 }
200 else {
201 /* Evenly size all children. */
202 iInt2 childSize = d->rect.size;
203 if (d->flags & arrangeHorizontal_WidgetFlag) {
204 childSize.x /= childCount;
205 }
206 else if (d->flags & arrangeVertical_WidgetFlag) {
207 childSize.y /= childCount;
208 }
209 iForEach(ObjectList, i, d->children) {
210 iWidget *child = as_Widget(i.object);
211 setWidth_Widget_(child, childSize.x);
212 setHeight_Widget_(child, childSize.y);
213 }
214 }
215 }
216 if (d->flags & resizeChildrenToWidestChild_WidgetFlag) {
217 const int widest = widestChild_Widget_(d);
218 iForEach(ObjectList, i, d->children) {
219 setWidth_Widget_(as_Widget(i.object), widest);
220 }
221 }
222 iInt2 pos = zero_I2();
223 iForEach(ObjectList, i, d->children) {
224 iWidget *child = as_Widget(i.object);
225 arrange_Widget(child);
226 if (d->flags & (arrangeHorizontal_WidgetFlag | arrangeVertical_WidgetFlag)) {
227 child->rect.pos = pos;
228 if (d->flags & arrangeHorizontal_WidgetFlag) {
229 pos.x += child->rect.size.x;
230 }
231 else {
232 pos.y += child->rect.size.y;
233 }
234 }
235 }
236 /* Update the size of the widget according to the arrangement. */
237 if (d->flags & arrangeSize_WidgetFlag) {
238 iRect bounds = zero_Rect();
239 iConstForEach(ObjectList, i, d->children) {
240 const iWidget *child = constAs_Widget(i.object);
241 if (isEmpty_Rect(bounds)) {
242 bounds = child->rect;
243 }
244 else {
245 bounds = union_Rect(bounds, child->rect);
246 }
247 }
248 if (d->flags & arrangeWidth_WidgetFlag) {
249 setWidth_Widget_(d, bounds.size.x);
250 /* Parent size changed, must update the children.*/
251 iForEach(ObjectList, j, d->children) {
252 iWidget *child = as_Widget(j.object);
253 if (child->flags & resizeToParentWidth_WidgetFlag) {
254 arrange_Widget(child);
255 }
256 }
257 }
258 if (d->flags & arrangeHeight_WidgetFlag) {
259 setHeight_Widget_(d, bounds.size.y);
260 /* Parent size changed, must update the children.*/
261 iForEach(ObjectList, j, d->children) {
262 iWidget *child = as_Widget(j.object);
263 if (child->flags & resizeToParentHeight_WidgetFlag) {
264 arrange_Widget(child);
265 }
266 }
267 }
268 }
269}
270
271iRect bounds_Widget(const iWidget *d) {
272 iRect bounds = d->rect;
273 for (const iWidget *w = d->parent; w; w = w->parent) {
274 addv_I2(&bounds.pos, w->rect.pos);
275 }
276 return bounds;
277}
278
279iInt2 localCoord_Widget(const iWidget *d, iInt2 coord) {
280 for (const iWidget *w = d; w; w = w->parent) {
281 subv_I2(&coord, w->rect.pos);
282 }
283 return coord;
284}
285
286iBool contains_Widget(const iWidget *d, iInt2 coord) {
287 const iRect bounds = { zero_I2(), d->rect.size };
288 return contains_Rect(bounds, localCoord_Widget(d, coord));
289}
290
291iLocalDef iBool isKeyboardEvent_(const SDL_Event *ev) {
292 return (ev->type == SDL_KEYUP || ev->type == SDL_KEYDOWN || ev->type == SDL_TEXTINPUT);
293}
294
295iLocalDef iBool isMouseEvent_(const SDL_Event *ev) {
296 return (ev->type == SDL_MOUSEWHEEL || ev->type == SDL_MOUSEMOTION ||
297 ev->type == SDL_MOUSEBUTTONUP || ev->type == SDL_MOUSEBUTTONDOWN);
298}
299
300static iBool filterEvent_Widget_(const iWidget *d, const SDL_Event *ev) {
301 const iBool isKey = isKeyboardEvent_(ev);
302 const iBool isMouse = isMouseEvent_(ev);
303 if (d->flags & disabled_WidgetFlag) {
304 if (isKey || isMouse) return iFalse;
305 }
306 if (d->flags & hidden_WidgetFlag) {
307 if (isMouse) return iFalse;
308 }
309 return iTrue;
310}
311
312void unhover_Widget(void) {
313 rootData_.hover = NULL;
314}
315
316iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
317 if (!d->parent) {
318 if (ev->type == SDL_MOUSEMOTION) {
319 /* Hover widget may change. */
320 rootData_.hover = NULL;
321 }
322 if (rootData_.focus && isKeyboardEvent_(ev)) {
323 /* Root dispatches keyboard events directly to the focused widget. */
324 if (dispatchEvent_Widget(rootData_.focus, ev)) {
325 return iTrue;
326 }
327 }
328 /* Root offers events first to widgets on top. */
329 iForEach(PtrSet, i, rootData_.onTop) {
330 iWidget *widget = *i.value;
331 if (isVisible_Widget(widget) && dispatchEvent_Widget(widget, ev)) {
332 return iTrue;
333 }
334 }
335 }
336 else if (ev->type == SDL_MOUSEMOTION && !rootData_.hover &&
337 flags_Widget(d) & hover_WidgetFlag && ~flags_Widget(d) & hidden_WidgetFlag &&
338 ~flags_Widget(d) & disabled_WidgetFlag) {
339 if (contains_Widget(d, init_I2(ev->motion.x, ev->motion.y))) {
340 rootData_.hover = d;
341 }
342 }
343 if (filterEvent_Widget_(d, ev)) {
344 /* Children may handle it first. Done in reverse so children drawn on top get to
345 handle the events first. */
346 iReverseForEach(ObjectList, i, d->children) {
347 iWidget *child = as_Widget(i.object);
348 if (child == rootData_.focus && isKeyboardEvent_(ev)) {
349 continue; /* Already dispatched. */
350 }
351 if (isVisible_Widget(child) && child->flags & keepOnTop_WidgetFlag) {
352 /* Already dispatched. */
353 continue;
354 }
355 if (dispatchEvent_Widget(child, ev)) {
356 return iTrue;
357 }
358 }
359 if (class_Widget(d)->processEvent(d, ev)) {
360 return iTrue;
361 }
362 }
363 return iFalse;
364}
365
366iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
367 if (ev->type == SDL_KEYDOWN) {
368 if (ev->key.keysym.sym == SDLK_TAB) {
369 setFocus_Widget(findFocusable_Widget(focus_Widget(),
370 ev->key.keysym.mod & KMOD_SHIFT
371 ? backward_WidgetFocusDir
372 : forward_WidgetFocusDir));
373 return iTrue;
374 }
375 }
376 switch (ev->type) {
377 case SDL_USEREVENT: {
378 if (ev->user.code == command_UserEventCode && d->commandHandler &&
379 d->commandHandler(d, ev->user.data1)) {
380 return iTrue;
381 }
382 break;
383 }
384 }
385 return iFalse;
386}
387
388void draw_Widget(const iWidget *d) {
389 if (d->flags & hidden_WidgetFlag) return;
390 if (d->bgColor >= 0) {
391 iPaint p;
392 init_Paint(&p);
393 fillRect_Paint(&p, bounds_Widget(d), d->bgColor);
394 }
395 iConstForEach(ObjectList, i, d->children) {
396 const iWidget *child = constAs_Widget(i.object);
397 if (~child->flags & keepOnTop_WidgetFlag && ~child->flags & hidden_WidgetFlag) {
398 class_Widget(child)->draw(child);
399 }
400 }
401 /* Root draws the on-top widgets on top of everything else. */
402 if (!d->parent) {
403 iConstForEach(PtrSet, i, onTop_RootData_()) {
404 draw_Widget(*i.value);
405 }
406 }
407}
408
409iAny *addChild_Widget(iWidget *d, iAnyObject *child) {
410 return addChildPos_Widget(d, child, back_WidgetAddPos);
411}
412
413iAny *addChildPos_Widget(iWidget *d, iAnyObject *child, enum iWidgetAddPos addPos) {
414 iAssert(child);
415 iAssert(d != child);
416 iWidget *widget = as_Widget(child);
417 iAssert(!widget->parent);
418 if (!d->children) {
419 d->children = new_ObjectList();
420 }
421 if (addPos == back_WidgetAddPos) {
422 pushBack_ObjectList(d->children, widget); /* ref */
423 }
424 else {
425 pushFront_ObjectList(d->children, widget); /* ref */
426 }
427 widget->parent = d;
428 return child;
429}
430
431iAny *addChildFlags_Widget(iWidget *d, iAnyObject *child, int childFlags) {
432 setFlags_Widget(child, childFlags, iTrue);
433 return addChild_Widget(d, child);
434}
435
436iAny *removeChild_Widget(iWidget *d, iAnyObject *child) {
437 ref_Object(child);
438 iBool found = iFalse;
439 iForEach(ObjectList, i, d->children) {
440 if (i.object == child) {
441 remove_ObjectListIterator(&i);
442 found = iTrue;
443 break;
444 }
445 }
446 iAssert(found);
447 ((iWidget *) child)->parent = NULL;
448 return child;
449}
450
451iAny *child_Widget(iWidget *d, size_t index) {
452 iForEach(ObjectList, i, d->children) {
453 if (index-- == 0) {
454 return i.object;
455 }
456 }
457 return NULL;
458}
459
460iAny *findChild_Widget(const iWidget *d, const char *id) {
461 if (cmp_String(id_Widget(d), id) == 0) {
462 return iConstCast(iAny *, d);
463 }
464 iConstForEach(ObjectList, i, d->children) {
465 iAny *found = findChild_Widget(constAs_Widget(i.object), id);
466 if (found) return found;
467 }
468 return NULL;
469}
470
471size_t childCount_Widget(const iWidget *d) {
472 if (!d->children) return 0;
473 return size_ObjectList(d->children);
474}
475
476iBool isVisible_Widget(const iWidget *d) {
477 for (const iWidget *w = d; w; w = w->parent) {
478 if (w->flags & hidden_WidgetFlag) {
479 return iFalse;
480 }
481 }
482 return iTrue;
483}
484
485iBool isDisabled_Widget(const iWidget *d) {
486 for (const iWidget *w = d; w; w = w->parent) {
487 if (w->flags & disabled_WidgetFlag) {
488 return iTrue;
489 }
490 }
491 return iFalse;
492}
493
494iBool isFocused_Widget(const iWidget *d) {
495 return rootData_.focus == d;
496}
497
498iBool isHover_Widget(const iWidget *d) {
499 return rootData_.hover == d;
500}
501
502iBool isSelected_Widget(const iWidget *d) {
503 return (d->flags & selected_WidgetFlag) != 0;
504}
505
506iBool isCommand_Widget(const iWidget *d, const SDL_Event *ev, const char *cmd) {
507 if (isCommand_UserEvent(ev, cmd)) {
508 const iWidget *src = pointer_Command(command_UserEvent(ev));
509 iAssert(!src || strstr(ev->user.data1, " ptr:"));
510 return src == d || hasParent_Widget(src, d);
511 }
512 return iFalse;
513}
514
515iBool hasParent_Widget(const iWidget *d, const iWidget *someParent) {
516 if (d) {
517 for (const iWidget *w = d->parent; w; w = w->parent) {
518 if (w == someParent) return iTrue;
519 }
520 }
521 return iFalse;
522}
523
524void setFocus_Widget(iWidget *d) {
525 if (rootData_.focus != d) {
526 if (rootData_.focus) {
527 iAssert(!contains_PtrSet(rootData_.pendingDestruction, rootData_.focus));
528 postCommand_Widget(rootData_.focus, "focus.lost");
529 }
530 rootData_.focus = d;
531 if (d) {
532 iAssert(flags_Widget(d) & focusable_WidgetFlag);
533 postCommand_Widget(d, "focus.gained");
534 }
535 }
536}
537
538iWidget *focus_Widget(void) {
539 return rootData_.focus;
540}
541
542iWidget *hover_Widget(void) {
543 return rootData_.hover;
544}
545
546static const iWidget *findFocusable_Widget_(const iWidget *d, const iWidget *startFrom,
547 iBool *getNext, enum iWidgetFocusDir focusDir) {
548 if (startFrom == d) {
549 *getNext = iTrue;
550 return NULL;
551 }
552 if ((d->flags & focusable_WidgetFlag) && isVisible_Widget(d) && !isDisabled_Widget(d) &&
553 *getNext) {
554 return d;
555 }
556 if (focusDir == forward_WidgetFocusDir) {
557 iConstForEach(ObjectList, i, d->children) {
558 const iWidget *found =
559 findFocusable_Widget_(constAs_Widget(i.object), startFrom, getNext, focusDir);
560 if (found) return found;
561 }
562 }
563 else {
564 iReverseConstForEach(ObjectList, i, d->children) {
565 const iWidget *found =
566 findFocusable_Widget_(constAs_Widget(i.object), startFrom, getNext, focusDir);
567 if (found) return found;
568 }
569 }
570 return NULL;
571}
572
573iAny *findFocusable_Widget(const iWidget *startFrom, enum iWidgetFocusDir focusDir) {
574 iWidget *root = get_Window()->root;
575 iBool getNext = (startFrom ? iFalse : iTrue);
576 const iWidget *found = findFocusable_Widget_(root, startFrom, &getNext, focusDir);
577 if (!found && startFrom) {
578 getNext = iTrue;
579 found = findFocusable_Widget_(root, NULL, &getNext, focusDir);
580 }
581 return iConstCast(iWidget *, found);
582}
583
584void setMouseGrab_Widget(iWidget *d) {
585 if (rootData_.mouseGrab != d) {
586 rootData_.mouseGrab = d;
587 SDL_CaptureMouse(d != NULL);
588 }
589}
590
591iWidget *mouseGrab_Widget(void) {
592 return rootData_.mouseGrab;
593}
594
595void postCommand_Widget(const iWidget *d, const char *cmd, ...) {
596 iString str;
597 init_String(&str); {
598 va_list args;
599 va_start(args, cmd);
600 vprintf_Block(&str.chars, cmd, args);
601 va_end(args);
602 }
603 iBool isGlobal = iFalse;
604 if (*cstr_String(&str) == '!') {
605 isGlobal = iTrue;
606 remove_Block(&str.chars, 0, 1);
607 }
608 if (!isGlobal) {
609 appendFormat_String(&str, " ptr:%p", d);
610 }
611 postCommandString_App(&str);
612 deinit_String(&str);
613}
614
615iBeginDefineClass(Widget)
616 .processEvent = processEvent_Widget,
617 .draw = draw_Widget,
618iEndDefineClass(Widget)