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