1 /*
2 C-Dogs SDL
3 A port of the legendary (and fun) action/arcade cdogs.
4
5 Copyright (c) 2013-2016, 2020 Cong Xu
6 All rights reserved.
7
8 Redistribution and use in source and binary forms, with or without
9 modification, are permitted provided that the following conditions are
10 met:
11
12 Redistributions of source code must retain the above copyright notice,
13 this list of conditions and the following disclaimer. Redistributions in
14 binary form must reproduce the above copyright notice, this list of
15 conditions and the following disclaimer in the documentation and/or other
16 materials provided with the distribution.
17
18 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
20 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
25 OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
27 OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
28 ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30 #include "ui_object.h"
31
32 #include <assert.h>
33 #include <string.h>
34
35 #include <cdogs/blit.h>
36 #include <cdogs/draw/drawtools.h>
37 #include <cdogs/font.h>
38
39 color_t bgColor = {32, 32, 64, 255};
40 color_t menuBGColor = {48, 48, 48, 255};
41 color_t hiliteColor = {96, 96, 96, 255};
42 #define TOOLTIP_PADDING 4
43
UIObjectCreate(UIType type,int id,struct vec2i pos,struct vec2i size)44 UIObject *UIObjectCreate(
45 UIType type, int id, struct vec2i pos, struct vec2i size)
46 {
47 UIObject *o;
48 CCALLOC(o, sizeof *o);
49 o->Type = type;
50 o->Id = id;
51 o->Pos = pos;
52 o->Size = size;
53 o->IsVisible = true;
54 o->ChangeDisablesContext = true;
55 switch (type)
56 {
57 case UITYPE_TEXTBOX:
58 o->u.Textbox.IsEditable = true;
59 break;
60 case UITYPE_CONTEXT_MENU:
61 // Context menu always starts as invisible
62 o->IsVisible = false;
63 break;
64 default:
65 // do nothing
66 break;
67 }
68 CArrayInit(&o->Children, sizeof o);
69 return o;
70 }
71
UIObjectSetDynamicLabel(UIObject * o,const char * label)72 void UIObjectSetDynamicLabel(UIObject *o, const char *label)
73 {
74 CSTRDUP(o->Label, label);
75 o->IsDynamicLabel = true;
76 }
77
UIButtonSetPic(UIObject * o,Pic * pic)78 void UIButtonSetPic(UIObject *o, Pic *pic)
79 {
80 assert(o->Type == UITYPE_BUTTON && "invalid UI type");
81 o->u.Button.Pic = pic;
82 if (svec2i_is_zero(o->Size))
83 {
84 o->Size = o->u.Button.Pic->size;
85 }
86 }
87
UIObjectCopy(const UIObject * o)88 UIObject *UIObjectCopy(const UIObject *o)
89 {
90 UIObject *res = UIObjectCreate(o->Type, o->Id, o->Pos, o->Size);
91 res->IsVisible = o->IsVisible;
92 res->Id2 = o->Id2;
93 res->Flags = o->Flags;
94 res->Tooltip = o->Tooltip;
95 res->Parent = o->Parent;
96 res->DoNotHighlight = o->DoNotHighlight;
97 res->Label = o->Label;
98 res->IsDynamicLabel = o->IsDynamicLabel;
99 if (o->IsDynamicLabel)
100 {
101 CSTRDUP(res->Label, o->Label);
102 }
103 res->Data = o->Data;
104 CASSERT(!o->IsDynamicData, "Cannot copy unknown dynamic data size");
105 res->IsDynamicData = false;
106 res->ChangeFunc = o->ChangeFunc;
107 res->ChangeFuncAlt = o->ChangeFuncAlt;
108 res->ChangeDisablesContext = o->ChangeDisablesContext;
109 res->OnFocusFunc = o->OnFocusFunc;
110 res->OnUnfocusFunc = o->OnUnfocusFunc;
111 res->CheckVisible = o->CheckVisible;
112 memcpy(&res->u, &o->u, sizeof res->u);
113 return res;
114 }
115
UIObjectDestroy(UIObject * o)116 void UIObjectDestroy(UIObject *o)
117 {
118 CFREE(o->Tooltip);
119 if (o->IsDynamicLabel)
120 {
121 CFREE(o->Label);
122 }
123 CA_FOREACH(UIObject *, obj, o->Children)
124 UIObjectDestroy(*obj);
125 CA_FOREACH_END()
126 CArrayTerminate(&o->Children);
127 if (o->IsDynamicData)
128 {
129 CFREE(o->Data);
130 }
131 switch (o->Type)
132 {
133 case UITYPE_TEXTBOX:
134 CFREE(o->u.Textbox.Hint);
135 break;
136 default:
137 // do nothing
138 break;
139 }
140 CFREE(o);
141 }
142
UIObjectAddChild(UIObject * o,UIObject * c)143 void UIObjectAddChild(UIObject *o, UIObject *c)
144 {
145 if (c == NULL)
146 {
147 return;
148 }
149 CArrayPushBack(&o->Children, &c);
150 c->Parent = o;
151 if (o->Type == UITYPE_CONTEXT_MENU)
152 {
153 // Resize context menu based on children
154 o->Size = svec2i_max(o->Size, svec2i_add(c->Pos, c->Size));
155 }
156 }
157
UIObjectHighlight(UIObject * o,const bool shift)158 void UIObjectHighlight(UIObject *o, const bool shift)
159 {
160 if (o->DoNotHighlight)
161 {
162 return;
163 }
164 if (o->Parent && o->Parent->Highlighted != o)
165 {
166 o->Parent->Highlighted = o;
167 UIObjectHighlight(o->Parent, shift);
168 }
169 if (o->OnFocusFunc)
170 {
171 o->OnFocusFunc(o, o->Data);
172 }
173 // Show any context menu children
174 CA_FOREACH(UIObject *, obj, o->Children)
175 if ((*obj)->Type == UITYPE_CONTEXT_MENU && !shift)
176 {
177 (*obj)->IsVisible = true;
178 if ((*obj)->OnFocusFunc)
179 {
180 (*obj)->OnFocusFunc(*obj, (*obj)->Data);
181 }
182 }
183 CA_FOREACH_END()
184 }
185
UIObjectIsHighlighted(UIObject * o)186 int UIObjectIsHighlighted(UIObject *o)
187 {
188 return o->Parent == NULL || o->Parent->Highlighted == o;
189 }
190
UIObjectUnhighlight(UIObject * o,const bool unhighlightParents)191 bool UIObjectUnhighlight(UIObject *o, const bool unhighlightParents)
192 {
193 bool changed = false;
194 if (o->Highlighted)
195 {
196 changed =
197 UIObjectUnhighlight(o->Highlighted, unhighlightParents) || changed;
198 }
199 o->Highlighted = NULL;
200 if (o->OnUnfocusFunc)
201 {
202 changed = o->OnUnfocusFunc(o->Data) || changed;
203 }
204 // Disable any context menu children
205 if (o->Type == UITYPE_CONTEXT_MENU)
206 {
207 o->IsVisible = false;
208 }
209 CA_FOREACH(UIObject *, obj, o->Children)
210 if ((*obj)->Type == UITYPE_CONTEXT_MENU)
211 {
212 (*obj)->IsVisible = false;
213 if ((*obj)->OnUnfocusFunc)
214 {
215 changed = (*obj)->OnUnfocusFunc((*obj)->Data) || changed;
216 }
217 }
218 CA_FOREACH_END()
219 // Unhighlight all parents
220 if (unhighlightParents && o->Parent != NULL)
221 {
222 // Prevent infinite loop
223 if (o->Parent->Highlighted == o)
224 {
225 o->Parent->Highlighted = NULL;
226 }
227 changed =
228 UIObjectUnhighlight(o->Parent, unhighlightParents) || changed;
229 }
230 return changed;
231 }
232
233 static void DisableContextMenuParents(UIObject *o);
UIObjectChange(UIObject * o,const int d,const bool shift)234 EditorResult UIObjectChange(UIObject *o, const int d, const bool shift)
235 {
236 // Activate change func if available
237 EditorResult result = EDITOR_RESULT_NONE;
238 if (o->ChangeFuncAlt && shift)
239 {
240 result = o->ChangeFuncAlt(o->Data, d);
241 }
242 else if (o->ChangeFunc)
243 {
244 result = o->ChangeFunc(o->Data, d);
245 }
246 if (result != EDITOR_RESULT_NONE && o->ChangeDisablesContext)
247 {
248 DisableContextMenuParents(o);
249 }
250 return result;
251 }
252 // Disable all parent context menus once the child is clicked
DisableContextMenuParents(UIObject * o)253 static void DisableContextMenuParents(UIObject *o)
254 {
255 // Don't disable if we are a textbox
256 if (o->Parent && o->Type != UITYPE_TEXTBOX)
257 {
258 if (o->Parent->Type == UITYPE_CONTEXT_MENU)
259 {
260 o->Parent->IsVisible = false;
261 }
262 DisableContextMenuParents(o->Parent);
263 }
264 }
265
UIObjectAddChar(UIObject * o,char c)266 EditorResult UIObjectAddChar(UIObject *o, char c)
267 {
268 if (!o)
269 {
270 return EDITOR_RESULT_NONE;
271 }
272 const EditorResult childResult = UIObjectAddChar(o->Highlighted, c);
273 if (o->Type != UITYPE_TEXTBOX)
274 {
275 return childResult;
276 }
277 else if (childResult != EDITOR_RESULT_NONE)
278 {
279 // See if there are highlighted textbox children;
280 // if so activate them instead
281 return childResult;
282 }
283 if (o->u.Textbox.TextSourceFunc)
284 {
285 // Dynamically-allocated char buf, expand
286 char **s = o->u.Textbox.TextSourceFunc(o->Data);
287 if (!s)
288 {
289 return EDITOR_RESULT_NONE;
290 }
291 size_t l = *s ? strlen(*s) : 0;
292 CREALLOC(*s, l + 2);
293 (*s)[l + 1] = 0;
294 (*s)[l] = c;
295 }
296 else
297 {
298 // Static char buf, simply append
299 char *s = o->u.Textbox.TextLinkFunc(o, o->Data);
300 size_t l = strlen(s);
301 if ((int)l >= o->u.Textbox.MaxLen)
302 {
303 return EDITOR_RESULT_NONE;
304 }
305 s[l + 1] = 0;
306 s[l] = c;
307 }
308 if (o->ChangeFunc)
309 {
310 return o->ChangeFunc(o->Data, 1);
311 }
312 return EDITOR_RESULT_NONE;
313 }
UIObjectDelChar(UIObject * o)314 EditorResult UIObjectDelChar(UIObject *o)
315 {
316 if (!o)
317 {
318 return EDITOR_RESULT_NONE;
319 }
320 const EditorResult childResult = UIObjectDelChar(o->Highlighted);
321 if (o->Type != UITYPE_TEXTBOX)
322 {
323 return childResult;
324 }
325 else if (childResult != EDITOR_RESULT_NONE)
326 {
327 // See if there are highlighted textbox children;
328 // if so activate them instead
329 return childResult;
330 }
331 char *s = o->u.Textbox.TextLinkFunc(o, o->Data);
332 if (!s || s[0] == '\0')
333 {
334 return EDITOR_RESULT_NONE;
335 }
336 s[strlen(s) - 1] = 0;
337 if (o->ChangeFunc)
338 {
339 return o->ChangeFunc(o->Data, -1);
340 }
341 return EDITOR_RESULT_NONE;
342 }
343
344 static int IsInside(
345 struct vec2i pos, struct vec2i rectPos, struct vec2i rectSize);
346
LabelGetText(UIObject * o)347 static const char *LabelGetText(UIObject *o)
348 {
349 assert(o->Type == UITYPE_LABEL && "invalid UIObject type");
350 if (o->Label)
351 {
352 return o->Label;
353 }
354 else if (o->u.LabelFunc)
355 {
356 return o->u.LabelFunc(o, o->Data);
357 }
358 return NULL;
359 }
360 typedef struct
361 {
362 UIObject *obj;
363 struct vec2i pos;
364 } UIObjectDrawContext;
UIObjectDrawAndAddChildren(UIObject * o,GraphicsDevice * g,struct vec2i pos,struct vec2i mouse,CArray * objs)365 static void UIObjectDrawAndAddChildren(
366 UIObject *o, GraphicsDevice *g, struct vec2i pos, struct vec2i mouse,
367 CArray *objs)
368 {
369 if (!o)
370 {
371 return;
372 }
373 if (o->CheckVisible)
374 {
375 o->CheckVisible(o, o->Data);
376 }
377 if (!o->IsVisible)
378 {
379 return;
380 }
381 int isHighlighted = UIObjectIsHighlighted(o);
382 struct vec2i oPos = svec2i_add(pos, o->Pos);
383 switch (o->Type)
384 {
385 case UITYPE_LABEL: {
386 const char *text = LabelGetText(o);
387 if (!text)
388 {
389 break;
390 }
391 color_t textMask = isHighlighted ? colorRed : colorWhite;
392 FontStrMaskWrap(text, oPos, textMask, o->Size.x);
393 }
394 break;
395 case UITYPE_TEXTBOX: {
396 int isText = !!o->u.Textbox.TextLinkFunc;
397 const char *text =
398 isText ? o->u.Textbox.TextLinkFunc(o, o->Data) : NULL;
399 int isEmptyText = !isText || !text || strlen(text) == 0;
400 color_t bracketMask = isHighlighted ? colorRed : colorWhite;
401 color_t textMask = isEmptyText ? colorGray : colorWhite;
402 int oPosX = oPos.x;
403 if (isEmptyText)
404 {
405 text = o->u.Textbox.Hint;
406 }
407 if (!o->u.Textbox.IsEditable)
408 {
409 textMask = bracketMask;
410 }
411 if (o->u.Textbox.IsEditable)
412 {
413 oPos = FontStrMask(ARROW_RIGHT, oPos, bracketMask);
414 }
415 oPos = FontStrMaskWrap(
416 text, oPos, textMask, o->Pos.x + o->Size.x - oPosX);
417 if (o->u.Textbox.IsEditable)
418 {
419 oPos = FontStrMask(ARROW_LEFT, oPos, bracketMask);
420 }
421 oPos.x = oPosX;
422 }
423 break;
424 case UITYPE_BUTTON: {
425 const bool isDown =
426 o->u.Button.IsDownFunc && o->u.Button.IsDownFunc(o->Data);
427 PicRender(
428 o->u.Button.Pic, g->gameWindow.renderer, oPos,
429 isDown ? colorGray : colorWhite, 0, svec2_one(), SDL_FLIP_NONE,
430 Rect2iZero());
431 }
432 break;
433 case UITYPE_CONTEXT_MENU: {
434 // Draw background
435 DrawRectangle(
436 g, svec2i_add(oPos, svec2i_scale(svec2i_one(), -TOOLTIP_PADDING)),
437 svec2i_add(
438 o->Size, svec2i_scale(svec2i_one(), 2 * TOOLTIP_PADDING)),
439 menuBGColor, true);
440 // Find if mouse over any children, and draw highlight
441 CA_FOREACH(UIObject *, child, o->Children)
442 if (IsInside(mouse, svec2i_add(oPos, (*child)->Pos), (*child)->Size))
443 {
444 DrawRectangle(
445 g, svec2i_add(oPos, (*child)->Pos), (*child)->Size,
446 hiliteColor, true);
447 }
448 CA_FOREACH_END()
449 }
450 break;
451 case UITYPE_CUSTOM:
452 o->u.CustomDrawFunc(o, g, pos, o->Data);
453 break;
454 default:
455 // do nothing
456 break;
457 }
458
459 // add children
460 if (objs != NULL)
461 {
462 CA_FOREACH(UIObject *, obj, o->Children)
463 if (!((*obj)->Flags & UI_ENABLED_WHEN_PARENT_HIGHLIGHTED_ONLY) ||
464 isHighlighted)
465 {
466 UIObjectDrawContext c;
467 c.obj = *obj;
468 c.pos = oPos;
469 CArrayPushBack(objs, &c);
470 }
471 CA_FOREACH_END()
472 }
473 }
UIObjectDraw(UIObject * o,GraphicsDevice * g,struct vec2i pos,struct vec2i mouse,CArray * drawObjs)474 void UIObjectDraw(
475 UIObject *o, GraphicsDevice *g, struct vec2i pos, struct vec2i mouse,
476 CArray *drawObjs)
477 {
478 // Draw this UIObject and its children in BFS order
479 // Maintain a queue of UIObjects to draw
480 if (drawObjs->elemSize == 0)
481 {
482 CArrayInit(drawObjs, sizeof(UIObjectDrawContext));
483 UIObjectDrawContext c;
484 c.obj = o;
485 c.pos = pos;
486 CArrayPushBack(drawObjs, &c);
487 for (int i = 0; i < (int)drawObjs->size; i++)
488 {
489 UIObjectDrawContext *cPtr = CArrayGet(drawObjs, i);
490 UIObjectDrawAndAddChildren(
491 cPtr->obj, g, cPtr->pos, mouse, drawObjs);
492 }
493 }
494 else
495 {
496 for (int i = 0; i < (int)drawObjs->size; i++)
497 {
498 UIObjectDrawContext *cPtr = CArrayGet(drawObjs, i);
499 UIObjectDrawAndAddChildren(cPtr->obj, g, cPtr->pos, mouse, NULL);
500 }
501 }
502 }
503
IsInside(struct vec2i pos,struct vec2i rectPos,struct vec2i rectSize)504 static int IsInside(
505 struct vec2i pos, struct vec2i rectPos, struct vec2i rectSize)
506 {
507 return pos.x >= rectPos.x && pos.x < rectPos.x + rectSize.x &&
508 pos.y >= rectPos.y && pos.y < rectPos.y + rectSize.y;
509 }
510
511 bool UITryGetObjectImpl(UIObject *o, const struct vec2i pos, UIObject **out);
UITryGetObject(UIObject * o,struct vec2i pos,UIObject ** out)512 bool UITryGetObject(UIObject *o, struct vec2i pos, UIObject **out)
513 {
514 if (o == NULL)
515 {
516 return false;
517 }
518 // Find the absolute coordinates of the UIObject, by recursing up to its
519 // parent
520 const UIObject *o2 = o->Parent;
521 while (o2 != NULL)
522 {
523 pos = svec2i_subtract(pos, o2->Pos);
524 o2 = o2->Parent;
525 }
526 return UITryGetObjectImpl(o, pos, out);
527 }
UITryGetObjectImpl(UIObject * o,const struct vec2i pos,UIObject ** out)528 bool UITryGetObjectImpl(UIObject *o, const struct vec2i pos, UIObject **out)
529 {
530 if (!o->IsVisible)
531 {
532 return false;
533 }
534 bool isHighlighted = UIObjectIsHighlighted(o);
535 CA_FOREACH(UIObject *, obj, o->Children)
536 if ((!((*obj)->Flags & UI_ENABLED_WHEN_PARENT_HIGHLIGHTED_ONLY) ||
537 isHighlighted) &&
538 (*obj)->IsVisible &&
539 UITryGetObjectImpl(*obj, svec2i_subtract(pos, o->Pos), out))
540 {
541 return true;
542 }
543 CA_FOREACH_END()
544 if (IsInside(pos, o->Pos, o->Size) && o->Type != UITYPE_CONTEXT_MENU)
545 {
546 *out = o;
547 return true;
548 }
549 return false;
550 }
551
UITooltipDraw(GraphicsDevice * device,struct vec2i pos,const char * s)552 void UITooltipDraw(GraphicsDevice *device, struct vec2i pos, const char *s)
553 {
554 struct vec2i bgSize = FontStrSize(s);
555 pos = svec2i_add(pos, svec2i(10, 10)); // add offset
556 DrawRectangle(
557 device, svec2i_add(pos, svec2i_scale(svec2i_one(), -TOOLTIP_PADDING)),
558 svec2i_add(bgSize, svec2i_scale(svec2i_one(), 2 * TOOLTIP_PADDING)),
559 bgColor, true);
560 FontStr(s, pos);
561 }
562