1 /*
2 	This file is part of Warzone 2100.
3 	Copyright (C) 1999-2004  Eidos Interactive
4 	Copyright (C) 2005-2020  Warzone 2100 Project
5 
6 	Warzone 2100 is free software; you can redistribute it and/or modify
7 	it under the terms of the GNU General Public License as published by
8 	the Free Software Foundation; either version 2 of the License, or
9 	(at your option) any later version.
10 
11 	Warzone 2100 is distributed in the hope that it will be useful,
12 	but WITHOUT ANY WARRANTY; without even the implied warranty of
13 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 	GNU General Public License for more details.
15 
16 	You should have received a copy of the GNU General Public License
17 	along with Warzone 2100; if not, write to the Free Software
18 	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 /** @file
21  *  The main interface functions to the widget library
22  */
23 
24 #include "lib/framework/frame.h"
25 #include "lib/framework/string_ext.h"
26 #include "lib/framework/utf.h"
27 #include "lib/ivis_opengl/textdraw.h"
28 #include "lib/ivis_opengl/pieblitfunc.h"
29 #include "lib/ivis_opengl/piestate.h"
30 #include "lib/ivis_opengl/screen.h"
31 #include "lib/gamelib/gtime.h"
32 
33 #include "widget.h"
34 
35 #include <utility>
36 #if defined(WZ_CC_MSVC)
37 #include "widgbase.h"		// this is generated on the pre-build event.
38 #endif
39 #include "widgint.h"
40 
41 #include "form.h"
42 #include "label.h"
43 #include "button.h"
44 #include "editbox.h"
45 #include "bar.h"
46 #include "slider.h"
47 #include "tip.h"
48 
49 #include <algorithm>
50 #include <unordered_set>
51 #include <deque>
52 
53 static	bool	bWidgetsActive = true;
54 
55 /* The widget the mouse is over this update */
56 static auto psMouseOverWidget = std::weak_ptr<WIDGET>();
57 static auto psClickDownWidgetScreen = std::shared_ptr<W_SCREEN>();
58 static auto psMouseOverWidgetScreen = std::shared_ptr<W_SCREEN>();
59 
60 static WIDGET_AUDIOCALLBACK AudioCallback = nullptr;
61 static SWORD HilightAudioID = -1;
62 static SWORD ClickedAudioID = -1;
63 static SWORD ErrorAudioID = -1;
64 
65 static WIDGET_KEY lastReleasedKey_DEPRECATED = WKEY_NONE;
66 
67 static std::deque<std::shared_ptr<WIDGET>> widgetDeletionQueue;
68 static std::vector<std::function<void()>> widgetScheduledTasks;
69 
70 static bool debugBoundingBoxesOnly = false;
71 
72 #ifdef DEBUG
73 #include "lib/framework/demangle.hpp"
74 static std::unordered_set<const WIDGET*> debugLiveWidgets;
75 static std::shared_ptr<W_SCREEN> psCurrentlyRunningScreen;
76 #endif
77 
78 struct OverlayScreen
79 {
80 	std::shared_ptr<W_SCREEN> psScreen;
81 	uint16_t zOrder;
82 };
83 static std::vector<OverlayScreen> overlays;
84 static std::unordered_set<std::shared_ptr<W_SCREEN>> overlaySet; // for quick lookup
85 static std::unordered_set<std::shared_ptr<W_SCREEN>> overlaysToDelete;
86 
runScheduledTasks()87 static void runScheduledTasks()
88 {
89 	auto tasksCopy = std::move(widgetScheduledTasks);
90 	for (auto task: tasksCopy)
91 	{
92 		task();
93 	}
94 }
95 
_widgDebugAssertIfRunningScreen(const char * function)96 static inline void _widgDebugAssertIfRunningScreen(const char *function)
97 {
98 #ifdef DEBUG
99 	if (psCurrentlyRunningScreen != nullptr)
100 	{
101 		debug(LOG_INFO, "%s is being called from within a screen's click / run handlers", function);
102 		if (assertEnabled)
103 		{
104 			(void)wz_assert(psCurrentlyRunningScreen == nullptr);
105 		}
106 	}
107 #else
108 	// do nothing
109 #endif
110 }
111 #define widgDebugAssertIfRunningScreen() _widgDebugAssertIfRunningScreen(__FUNCTION__)
112 
113 /* Initialise the widget module */
widgInitialise()114 bool widgInitialise()
115 {
116 	tipInitialise();
117 	return true;
118 }
119 
120 
121 // Reset the widgets.
122 //
widgReset(void)123 void widgReset(void)
124 {
125 	tipInitialise();
126 }
127 
128 
129 /* Shut down the widget module */
widgShutDown(void)130 void widgShutDown(void)
131 {
132 	psClickDownWidgetScreen = nullptr;
133 	psMouseOverWidgetScreen = nullptr;
134 	tipShutdown();
135 	overlays.clear();
136 	overlaySet.clear();
137 	overlaysToDelete.clear();
138 #ifdef DEBUG
139 	if (!debugLiveWidgets.empty())
140 	{
141 		// Some widgets still exist - there may be reference cycles, non-cleared references, or other bugs
142 		for (auto widget : debugLiveWidgets)
143 		{
144 			debug(LOG_ERROR, "Widget not cleaned up: %p (type: %s)", (const void*)widget, cxxDemangle(typeid(*widget).name()).c_str());
145 		}
146 		ASSERT(debugLiveWidgets.empty(), "There are %zu widgets that were not cleaned up.", debugLiveWidgets.size());
147 	}
148 #endif
149 }
150 
widgScheduleTask(std::function<void ()> task)151 void widgScheduleTask(std::function<void ()> task)
152 {
153 	widgetScheduledTasks.push_back(task);
154 }
155 
widgRegisterOverlayScreen(const std::shared_ptr<W_SCREEN> & psScreen,uint16_t zOrder)156 void widgRegisterOverlayScreen(const std::shared_ptr<W_SCREEN> &psScreen, uint16_t zOrder)
157 {
158 	OverlayScreen newOverlay {psScreen, zOrder};
159 	auto it = std::find_if(overlays.begin(), overlays.end(), [psScreen](const OverlayScreen& overlay) -> bool {
160 		return overlay.psScreen == psScreen;
161 	});
162 	if (it != overlays.end())
163 	{
164 		// screen already exists in overlay stack
165 		if (zOrder == it->zOrder)
166 		{
167 			// no need to update - duplicate call (same zOrder)
168 			return;
169 		}
170 		else
171 		{
172 			// remove the existing entry
173 			overlays.erase(it);
174 			it = overlays.end();
175 			// fall-through to inserting it again in the new zOrder
176 		}
177 	}
178 	// the screens are stored in decreasing z-order
179 	overlays.insert(std::upper_bound(overlays.begin(), overlays.end(), newOverlay, [](const OverlayScreen& a, const OverlayScreen& b){ return a.zOrder > b.zOrder; }), newOverlay);
180 	overlaySet.insert(psScreen);
181 }
182 
183 // Note: If priorScreen is not the "regular" screen (i.e. if priorScreen is an overlay screen) it should already be registered
widgRegisterOverlayScreenOnTopOfScreen(const std::shared_ptr<W_SCREEN> & psScreen,const std::shared_ptr<W_SCREEN> & priorScreen)184 void widgRegisterOverlayScreenOnTopOfScreen(const std::shared_ptr<W_SCREEN> &psScreen, const std::shared_ptr<W_SCREEN> &priorScreen)
185 {
186 	auto it = std::find_if(overlays.begin(), overlays.end(), [priorScreen](const OverlayScreen& overlay) -> bool {
187 		return overlay.psScreen == priorScreen;
188 	});
189 	if (it != overlays.end())
190 	{
191 		OverlayScreen newOverlay {psScreen, it->zOrder}; // use the same z-order as priorScreen, but insert *before* it in the list (i.e. "above" it, since overlays are stored in decreasing z-order)
192 		overlays.insert(it, newOverlay);
193 		overlaySet.insert(psScreen);
194 	}
195 	else
196 	{
197 		// priorScreen does not exist in the overlays list, so it is probably the "regular" screen
198 		// just insert this overlay at the bottom of the overlay list
199 		OverlayScreen newOverlay {psScreen, 0};
200 		overlays.insert(overlays.end(), newOverlay);
201 		overlaySet.insert(psScreen);
202 	}
203 }
204 
widgRemoveOverlayScreen(const std::shared_ptr<W_SCREEN> & psScreen)205 void widgRemoveOverlayScreen(const std::shared_ptr<W_SCREEN> &psScreen)
206 {
207 	auto it = std::find_if(overlays.begin(), overlays.end(), [psScreen](const OverlayScreen& overlay) -> bool {
208 		return overlay.psScreen == psScreen;
209 	});
210 	if (it != overlays.end())
211 	{
212 		overlaysToDelete.insert(psScreen);
213 	}
214 }
215 
cleanupDeletedOverlays()216 static void cleanupDeletedOverlays()
217 {
218 	if (overlaysToDelete.empty()) { return; }
219 	overlays.erase(
220 		std::remove_if(
221 			overlays.begin(), overlays.end(),
222 			[](const OverlayScreen& a) { return overlaysToDelete.count(a.psScreen) > 0; }
223 		),
224 		overlays.end()
225 	);
226 	for (const auto& overlayToDelete : overlaysToDelete)
227 	{
228 		overlaySet.erase(overlayToDelete);
229 	}
230 	overlaysToDelete.clear();
231 }
232 
isMouseOverWidget() const233 bool WIDGET::isMouseOverWidget() const
234 {
235 	return psMouseOverWidget.lock().get() == this;
236 }
237 
setTransparentToClicks(bool hasClickTransparency)238 void WIDGET::setTransparentToClicks(bool hasClickTransparency)
239 {
240 	isTransparentToClicks = hasClickTransparency;
241 }
242 
transparentToClicks() const243 bool WIDGET::transparentToClicks() const
244 {
245 	return isTransparentToClicks;
246 }
247 
248 template<typename Iterator>
iterateOverlayScreens(Iterator iter,Iterator end,const std::function<bool (const OverlayScreen & overlay)> & func)249 static inline void iterateOverlayScreens(Iterator iter, Iterator end, const std::function<bool (const OverlayScreen& overlay)>& func)
250 {
251 	ASSERT_OR_RETURN(, func.operator bool(), "Requires a valid func");
252 	for ( ; iter != end; ++iter )
253 	{
254 		if (!func(*iter))
255 		{
256 			break; // stop enumerating
257 		}
258 	}
259 	// now that we aren't in the middle of enumerating overlays, handling removing any that were queued for deletion
260 	cleanupDeletedOverlays();
261 }
262 
263 // enumerate the overlay screens in decreasing z-order (i.e. "top-down")
forEachOverlayScreen(const std::function<bool (const OverlayScreen & overlay)> & func)264 static inline void forEachOverlayScreen(const std::function<bool (const OverlayScreen& overlay)>& func)
265 {
266 	// the screens are stored in decreasing z-order
267 	iterateOverlayScreens(overlays.cbegin(), overlays.cend(), func);
268 }
269 
270 // enumerate the overlay screens in increasing z-order (i.e. "bottom-up")
forEachOverlayScreenBottomUp(const std::function<bool (const OverlayScreen & overlay)> & func)271 static inline void forEachOverlayScreenBottomUp(const std::function<bool (const OverlayScreen& overlay)>& func)
272 {
273 	iterateOverlayScreens(overlays.crbegin(), overlays.crend(), func);
274 }
275 
isScreenARegisteredOverlay(const std::shared_ptr<W_SCREEN> & psScreen)276 static bool isScreenARegisteredOverlay(const std::shared_ptr<W_SCREEN> &psScreen)
277 {
278 	if (!psScreen) { return false; }
279 	return overlaySet.count(psScreen) > 0;
280 }
281 
isMouseOverScreenOverlayChild(int mx,int my)282 bool isMouseOverScreenOverlayChild(int mx, int my)
283 {
284 	if (psMouseOverWidgetScreen != nullptr)
285 	{
286 		return isScreenARegisteredOverlay(psMouseOverWidgetScreen);
287 	}
288 	bool bMouseIsOverOverlayChild = false;
289 	if (auto mouseOverWidget = psMouseOverWidget.lock())
290 	{
291 		// Fallback - the mouse is over a widget, but the widget does not have a screen pointer
292 		// Hit-test each overlay screen to identify if the mouse is over one
293 		// (Note: This can currently occur with DropdownWidget members.)
294 		forEachOverlayScreen([mx, my, &bMouseIsOverOverlayChild](const OverlayScreen& overlay)
295 		{
296 			auto psRoot = overlay.psScreen->psForm;
297 
298 			// Hit-test all children of root form
299 			for (const auto &psCurr: psRoot->children())
300 			{
301 				if (!psCurr->visible() || !psCurr->hitTest(mx, my))
302 				{
303 					continue;  // Skip any hidden widgets, or widgets the click missed.
304 				}
305 
306 				// hit test succeeded for child widget
307 				bMouseIsOverOverlayChild = true;
308 				return false; // stop enumerating
309 			}
310 			return true; // keep enumerating
311 		});
312 	}
313 	return bMouseIsOverOverlayChild;
314 }
315 
isMouseClickDownOnScreenOverlayChild()316 bool isMouseClickDownOnScreenOverlayChild()
317 {
318 	if (!psClickDownWidgetScreen) { return false; }
319 	return isScreenARegisteredOverlay(psClickDownWidgetScreen);
320 }
321 
deleteOldWidgets()322 static void deleteOldWidgets()
323 {
324 	while (!widgetDeletionQueue.empty())
325 	{
326 		std::shared_ptr<WIDGET> guiltyWidget = widgetDeletionQueue.front();
327 		widgDelete(guiltyWidget.get());
328 		widgetDeletionQueue.pop_front();
329 	}
330 }
331 
W_INIT()332 W_INIT::W_INIT()
333 	: formID(0)
334 	, majorID(0)
335 	, id(0)
336 	, style(0)
337 	, x(0), y(0)
338 	, width(0), height(0)
339 	, pDisplay(nullptr)
340 	, pCallback(nullptr)
341 	, pUserData(nullptr)
342 	, UserData(0)
343 	, calcLayout(nullptr)
344 	, onDelete(nullptr)
345 	, customHitTest(nullptr)
346 	, initPUserDataFunc(nullptr)
347 {}
348 
WIDGET(W_INIT const * init,WIDGET_TYPE type)349 WIDGET::WIDGET(W_INIT const *init, WIDGET_TYPE type)
350 	: id(init->id)
351 	, type(type)
352 	, style(init->style)
353 	, displayFunction(init->pDisplay)
354 	, callback(init->pCallback)
355 	, pUserData(init->pUserData)
356 	, UserData(init->UserData)
357 	, calcLayout(init->calcLayout)
358 	, onDelete(init->onDelete)
359 	, customHitTest(init->customHitTest)
360 	, dim(init->x, init->y, init->width, init->height)
361 	, dirty(true)
362 {
363 	/* Initialize and set the pUserData if necessary */
364 	if (init->initPUserDataFunc != nullptr)
365 	{
366 		assert(pUserData == nullptr); // if the initPUserDataFunc is set, pUserData should not be already set
367 		pUserData = init->initPUserDataFunc();
368 	}
369 
370 	// if calclayout is not null, call it
371 	callCalcLayout();
372 
373 #ifdef DEBUG
374 	debugLiveWidgets.insert(this);
375 #endif
376 }
377 
WIDGET(WIDGET_TYPE type)378 WIDGET::WIDGET(WIDGET_TYPE type)
379 	: id(0xFFFFEEEEu)
380 	, type(type)
381 	, style(0)
382 	, displayFunction(nullptr)
383 	, callback(nullptr)
384 	, pUserData(nullptr)
385 	, UserData(0)
386 	, calcLayout(nullptr)
387 	, onDelete(nullptr)
388 	, customHitTest(nullptr)
389 	, dim(0, 0, 1, 1)
390 	, dirty(true)
391 {
392 #ifdef DEBUG
393 	debugLiveWidgets.insert(this);
394 #endif
395 }
396 
~WIDGET()397 WIDGET::~WIDGET()
398 {
399 	if (onDelete != nullptr)
400 	{
401 		onDelete(this);	// Call the onDelete function to handle any extra logic
402 	}
403 
404 	tipStop(this);  // Stop showing tooltip, if we are.
405 
406 #ifdef DEBUG
407 	debugLiveWidgets.erase(this);
408 #endif
409 }
410 
deleteLater()411 void WIDGET::deleteLater()
412 {
413 	auto shared_widget_ptr = shared_from_this();
414 	ASSERT_OR_RETURN(, std::find(widgetDeletionQueue.begin(), widgetDeletionQueue.end(), shared_widget_ptr) == widgetDeletionQueue.end(), "Called deleteLater() twice on the same widget.");
415 	widgetDeletionQueue.push_back(shared_widget_ptr);
416 }
417 
setGeometry(WzRect const & r)418 void WIDGET::setGeometry(WzRect const &r)
419 {
420 	if (dim == r)
421 	{
422 		return;  // Nothing to do.
423 	}
424 	dim = r;
425 	geometryChanged();
426 	dirty = true;
427 }
428 
screenSizeDidChange(int oldWidth,int oldHeight,int newWidth,int newHeight)429 void WIDGET::screenSizeDidChange(int oldWidth, int oldHeight, int newWidth, int newHeight)
430 {
431 	// Default implementation of screenSizeDidChange calls its own calcLayout callback function (if present)
432 	callCalcLayout();
433 
434 	// Then propagates the event to all children
435 	for (auto const &psCurr: childWidgets)
436 	{
437 		psCurr->screenSizeDidChange(oldWidth, oldHeight, newWidth, newHeight);
438 	}
439 }
440 
setCalcLayout(const WIDGET_CALCLAYOUT_FUNC & calcLayoutFunc)441 void WIDGET::setCalcLayout(const WIDGET_CALCLAYOUT_FUNC& calcLayoutFunc)
442 {
443 	calcLayout = calcLayoutFunc;
444 	callCalcLayout();
445 }
446 
callCalcLayout()447 void WIDGET::callCalcLayout()
448 {
449 	if (calcLayout)
450 	{
451 		calcLayout(this, screenWidth, screenHeight, screenWidth, screenHeight);
452 	}
453 #ifdef DEBUG
454 //	// FOR DEBUGGING:
455 //	// To help track down WIDGETs missing a calc layout function
456 //	// (because something isn't properly re-adjusting when live window resizing occurs)
457 //	// uncomment the else branch below.
458 //	else
459 //	{
460 //		debug(LOG_ERROR, "Object is missing calc layout function");
461 //	}
462 #endif
463 }
464 
setOnDelete(const WIDGET_ONDELETE_FUNC & onDeleteFunc)465 void WIDGET::setOnDelete(const WIDGET_ONDELETE_FUNC& onDeleteFunc)
466 {
467 	onDelete = onDeleteFunc;
468 }
469 
setCustomHitTest(const WIDGET_HITTEST_FUNC & newCustomHitTestFunc)470 void WIDGET::setCustomHitTest(const WIDGET_HITTEST_FUNC& newCustomHitTestFunc)
471 {
472 	customHitTest = newCustomHitTestFunc;
473 }
474 
attach(const std::shared_ptr<WIDGET> & widget)475 void WIDGET::attach(const std::shared_ptr<WIDGET> &widget)
476 {
477 	ASSERT_OR_RETURN(, widget != nullptr && widget->parentWidget.expired(), "Bad attach.");
478 	widget->parentWidget = shared_from_this();
479 	widget->setScreenPointer(screenPointer.lock());
480 	childWidgets.push_back(widget);
481 }
482 
detach(const std::shared_ptr<WIDGET> & widget)483 void WIDGET::detach(const std::shared_ptr<WIDGET> &widget)
484 {
485 	ASSERT_OR_RETURN(, widget != nullptr && !widget->parentWidget.expired(), "Bad detach.");
486 
487 	widget->parentWidget.reset();
488 	widget->setScreenPointer(nullptr);
489 	childWidgets.erase(std::find(childWidgets.begin(), childWidgets.end(), widget));
490 
491 	widgetLost(widget.get());
492 }
493 
setScreenPointer(const std::shared_ptr<W_SCREEN> & screen)494 void WIDGET::setScreenPointer(const std::shared_ptr<W_SCREEN> &screen)
495 {
496 	if (screenPointer.lock() == screen)
497 	{
498 		return;
499 	}
500 
501 	if (isMouseOverWidget())
502 	{
503 		psMouseOverWidget.reset();
504 	}
505 
506 	if (auto lockedScreen = screenPointer.lock())
507 	{
508 		if (lockedScreen->hasFocus(*this))
509 		{
510 			lockedScreen->psFocus.reset();
511 		}
512 		if (lockedScreen->isLastHighlight(*this))
513 		{
514 			lockedScreen->lastHighlight.reset();
515 		}
516 	}
517 
518 	screenPointer = screen;
519 	for (auto const &child: childWidgets)
520 	{
521 		child->setScreenPointer(screen);
522 	}
523 }
524 
widgetLost(WIDGET * widget)525 void WIDGET::widgetLost(WIDGET *widget)
526 {
527 	if (auto lockedParent = parentWidget.lock())
528 	{
529 		lockedParent->widgetLost(widget);  // We don't care about the lost widget, maybe the parent does. (Special code for W_TABFORM.)
530 	}
531 }
532 
~W_SCREEN()533 W_SCREEN::~W_SCREEN()
534 {
535 	if (auto focusedWidget = psFocus.lock())
536 	{
537 		// must trigger a resignation of focus
538 		focusedWidget->focusLost();
539 	}
540 	psFocus.reset();
541 }
542 
initialize()543 void W_SCREEN::initialize()
544 {
545 	W_FORMINIT sInit;
546 	sInit.id = 0;
547 	sInit.style = WFORM_PLAIN | WFORM_INVISIBLE;
548 	sInit.x = 0;
549 	sInit.y = 0;
550 	sInit.width = screenWidth - 1;
551 	sInit.height = screenHeight - 1;
552 
553 	psForm = std::make_shared<W_FORM>(&sInit);
554 	psForm->screenPointer = shared_from_this();
555 }
556 
screenSizeDidChange(unsigned int oldWidth,unsigned int oldHeight,unsigned int newWidth,unsigned int newHeight)557 void W_SCREEN::screenSizeDidChange(unsigned int oldWidth, unsigned int oldHeight, unsigned int newWidth, unsigned int newHeight)
558 {
559 	// resize the top-level form
560 	psForm->setGeometry(0, 0, screenWidth - 1, screenHeight - 1);
561 
562 	// inform the top-level form of the event
563 	psForm->screenSizeDidChange(oldWidth, oldHeight, newWidth, newHeight);
564 }
565 
566 /* Check whether an ID has been used on a form */
widgCheckIDForm(WIDGET * psForm,UDWORD id)567 static bool widgCheckIDForm(WIDGET *psForm, UDWORD id)
568 {
569 	if (psForm->id == id)
570 	{
571 		return true;
572 	}
573 	for (auto const &child: psForm->children())
574 	{
575 		if (widgCheckIDForm(child.get(), id))
576 		{
577 			return true;
578 		}
579 	}
580 	return false;
581 }
582 
widgAddWidget(const std::shared_ptr<W_SCREEN> & psScreen,W_INIT const * psInit,const std::shared_ptr<WIDGET> & widget)583 static bool widgAddWidget(const std::shared_ptr<W_SCREEN> &psScreen, W_INIT const *psInit, const std::shared_ptr<WIDGET> &widget)
584 {
585 	ASSERT_OR_RETURN(false, widget != nullptr, "Invalid widget");
586 	ASSERT_OR_RETURN(false, psScreen != nullptr, "Invalid screen pointer");
587 	ASSERT_OR_RETURN(false, !widgCheckIDForm(psScreen->psForm.get(), psInit->id), "ID number has already been used (%d)", psInit->id);
588 	// Find the form to add the widget to.
589 	W_FORM *psParent;
590 	if (psInit->formID == 0)
591 	{
592 		/* Add to the base form */
593 		psParent = psScreen->psForm.get();
594 	}
595 	else
596 	{
597 		psParent = (W_FORM *)widgGetFromID(psScreen, psInit->formID);
598 	}
599 	ASSERT_OR_RETURN(false, psParent != nullptr && psParent->type == WIDG_FORM, "Could not find parent form from formID");
600 
601 	psParent->attach(widget);
602 	return true;
603 }
604 
605 /* Add a form to the widget screen */
widgAddForm(const std::shared_ptr<W_SCREEN> & psScreen,const W_FORMINIT * psInit)606 W_FORM *widgAddForm(const std::shared_ptr<W_SCREEN> &psScreen, const W_FORMINIT *psInit)
607 {
608 	ASSERT_OR_RETURN(nullptr, psScreen != nullptr, "Invalid screen pointer");
609 	std::shared_ptr<W_FORM> psForm = nullptr;
610 	if (psInit->style & WFORM_CLICKABLE)
611 	{
612 		psForm = std::make_shared<W_CLICKFORM>(psInit);
613 	}
614 	else
615 	{
616 		psForm = std::make_shared<W_FORM>(psInit);
617 	}
618 
619 	return widgAddWidget(psScreen, psInit, psForm) ? psForm.get() : nullptr;
620 }
621 
622 /* Add a label to the widget screen */
widgAddLabel(const std::shared_ptr<W_SCREEN> & psScreen,const W_LABINIT * psInit)623 W_LABEL *widgAddLabel(const std::shared_ptr<W_SCREEN> &psScreen, const W_LABINIT *psInit)
624 {
625 	ASSERT_OR_RETURN(nullptr, psScreen != nullptr, "Invalid screen pointer");
626 	auto psLabel = std::make_shared<W_LABEL>(psInit);
627 	return widgAddWidget(psScreen, psInit, psLabel) ? psLabel.get() : nullptr;
628 }
629 
630 /* Add a button to the widget screen */
widgAddButton(const std::shared_ptr<W_SCREEN> & psScreen,const W_BUTINIT * psInit)631 W_BUTTON *widgAddButton(const std::shared_ptr<W_SCREEN> &psScreen, const W_BUTINIT *psInit)
632 {
633 	ASSERT_OR_RETURN(nullptr, psScreen != nullptr, "Invalid screen pointer");
634 	auto psButton = std::make_shared<W_BUTTON>(psInit);
635 	return widgAddWidget(psScreen, psInit, psButton) ? psButton.get() : nullptr;
636 }
637 
638 /* Add an edit box to the widget screen */
widgAddEditBox(const std::shared_ptr<W_SCREEN> & psScreen,const W_EDBINIT * psInit)639 W_EDITBOX *widgAddEditBox(const std::shared_ptr<W_SCREEN> &psScreen, const W_EDBINIT *psInit)
640 {
641 	ASSERT_OR_RETURN(nullptr, psScreen != nullptr, "Invalid screen pointer");
642 	auto psEdBox = std::make_shared<W_EDITBOX>(psInit);
643 	return widgAddWidget(psScreen, psInit, psEdBox) ? psEdBox.get() : nullptr;
644 }
645 
646 /* Add a bar graph to the widget screen */
widgAddBarGraph(const std::shared_ptr<W_SCREEN> & psScreen,const W_BARINIT * psInit)647 W_BARGRAPH *widgAddBarGraph(const std::shared_ptr<W_SCREEN> &psScreen, const W_BARINIT *psInit)
648 {
649 	ASSERT_OR_RETURN(nullptr, psScreen != nullptr, "Invalid screen pointer");
650 	auto psBarGraph = std::make_shared<W_BARGRAPH>(psInit);
651 	return widgAddWidget(psScreen, psInit, psBarGraph) ? psBarGraph.get() : nullptr;
652 }
653 
654 /* Add a slider to a form */
widgAddSlider(const std::shared_ptr<W_SCREEN> & psScreen,const W_SLDINIT * psInit)655 W_SLIDER *widgAddSlider(const std::shared_ptr<W_SCREEN> &psScreen, const W_SLDINIT *psInit)
656 {
657 	ASSERT_OR_RETURN(nullptr, psScreen != nullptr, "Invalid screen pointer");
658 	auto psSlider = std::make_shared<W_SLIDER>(psInit);
659 	return widgAddWidget(psScreen, psInit, psSlider) ? psSlider.get() : nullptr;
660 }
661 
662 /* Delete a widget from the screen */
widgDelete(WIDGET * widget)663 void widgDelete(WIDGET *widget)
664 {
665 	if (!widget)
666 	{
667 		return;
668 	}
669 	widgDebugAssertIfRunningScreen();
670 
671 	if (auto lockedScreen = widget->screenPointer.lock())
672 	{
673 		if (auto widgetWithFocus = lockedScreen->getWidgetWithFocus())
674 		{
675 			if (widgetWithFocus->hasAncestor(widget))
676 			{
677 				// must trigger a resignation of focus
678 				lockedScreen->setFocus(nullptr);
679 			}
680 		}
681 		if (lockedScreen->psForm.get() == widget)
682 		{
683 			lockedScreen->psForm = nullptr;
684 		}
685 	}
686 
687 	if (auto parent = widget->parent())
688 	{
689 		parent->detach(widget);
690 	}
691 }
692 
693 /* Delete a widget from the screen */
widgDelete(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)694 void widgDelete(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
695 {
696 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
697 	widgDelete(widgGetFromID(psScreen, id));
698 }
widgDeleteLater(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)699 void widgDeleteLater(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
700 {
701 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
702 	auto psWidget = widgGetFromID(psScreen, id);
703 	if (psWidget)
704 	{
705 		psWidget->deleteLater();
706 	}
707 }
708 
709 /* Find a widget on a form from its id number */
widgFormGetFromID(const std::shared_ptr<WIDGET> & widget,UDWORD id)710 std::shared_ptr<WIDGET> widgFormGetFromID(const std::shared_ptr<WIDGET>& widget, UDWORD id)
711 {
712 	if (widget->id == id)
713 	{
714 		return widget->shared_from_this();
715 	}
716 	for (auto const &child: widget->children())
717 	{
718 		std::shared_ptr<WIDGET> w = widgFormGetFromID(child, id);
719 		if (w != nullptr)
720 		{
721 			return w;
722 		}
723 	}
724 	return nullptr;
725 }
726 
727 /* Find a widget in a screen from its ID number */
widgGetFromID(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)728 WIDGET *widgGetFromID(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
729 {
730 	ASSERT_OR_RETURN(nullptr, psScreen != nullptr, "Invalid screen pointer");
731 	return widgFormGetFromID(psScreen->psForm, id).get();
732 }
733 
widgHide(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)734 void widgHide(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
735 {
736 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
737 	WIDGET *psWidget = widgGetFromID(psScreen, id);
738 	ASSERT_OR_RETURN(, psWidget != nullptr, "Couldn't find widget from id.");
739 
740 	psWidget->hide();
741 }
742 
widgReveal(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)743 void widgReveal(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
744 {
745 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
746 	WIDGET *psWidget = widgGetFromID(psScreen, id);
747 	ASSERT_OR_RETURN(, psWidget != nullptr, "Couldn't find widget from id.");
748 
749 	psWidget->show();
750 }
751 
752 /* Return the ID of the widget the mouse was over this frame */
widgGetMouseOver(const std::shared_ptr<W_SCREEN> & psScreen)753 UDWORD widgGetMouseOver(const std::shared_ptr<W_SCREEN> &psScreen)
754 {
755 	ASSERT_OR_RETURN(0, psScreen != nullptr, "Invalid screen pointer");
756 	/* Don't actually need the screen parameter at the moment - but it might be
757 	   handy if psMouseOverWidget needs to stop being a static and moves into
758 	   the screen structure */
759 
760 	if (auto lockedMouserOverWidget = psMouseOverWidget.lock())
761 	{
762 		return lockedMouserOverWidget->id;
763 	}
764 
765 	return 0;
766 }
767 
isMouseOverSomeWidget(const std::shared_ptr<W_SCREEN> & psScreen)768 bool isMouseOverSomeWidget(const std::shared_ptr<W_SCREEN> &psScreen)
769 {
770 	if (auto lockedMouserOverWidget = psMouseOverWidget.lock())
771 	{
772 		return lockedMouserOverWidget != psScreen->psForm;
773 	}
774 
775 	return false;
776 }
777 
778 /* Return the user data for a widget */
widgGetUserData(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)779 void *widgGetUserData(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
780 {
781 	ASSERT_OR_RETURN(nullptr, psScreen != nullptr, "Invalid screen pointer");
782 	WIDGET	*psWidget;
783 
784 	psWidget = widgGetFromID(psScreen, id);
785 	if (psWidget)
786 	{
787 		return psWidget->pUserData;
788 	}
789 
790 	return nullptr;
791 }
792 
793 
794 /* Return the user data for a widget */
widgGetUserData2(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)795 UDWORD widgGetUserData2(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
796 {
797 	ASSERT_OR_RETURN(0, psScreen != nullptr, "Invalid screen pointer");
798 	WIDGET	*psWidget;
799 
800 	psWidget = widgGetFromID(psScreen, id);
801 	if (psWidget)
802 	{
803 		return psWidget->UserData;
804 	}
805 
806 	return 0;
807 }
808 
809 
810 /* Set user data for a widget */
widgSetUserData(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id,void * UserData)811 void widgSetUserData(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id, void *UserData)
812 {
813 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
814 	WIDGET	*psWidget;
815 
816 	psWidget = widgGetFromID(psScreen, id);
817 	if (psWidget)
818 	{
819 		psWidget->pUserData = UserData;
820 	}
821 }
822 
823 /* Set user data for a widget */
widgSetUserData2(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id,UDWORD UserData)824 void widgSetUserData2(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id, UDWORD UserData)
825 {
826 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
827 	WIDGET	*psWidget;
828 
829 	psWidget = widgGetFromID(psScreen, id);
830 	if (psWidget)
831 	{
832 		psWidget->UserData = UserData;
833 	}
834 }
835 
setTip(std::string)836 void WIDGET::setTip(std::string)
837 {
838 	ASSERT(false, "Can't set widget type %u's tip.", type);
839 }
840 
841 /* Set tip string for a widget */
widgSetTip(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id,std::string pTip)842 void widgSetTip(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id, std::string pTip)
843 {
844 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
845 	WIDGET *psWidget = widgGetFromID(psScreen, id);
846 
847 	if (!psWidget)
848 	{
849 		return;
850 	}
851 
852 	psWidget->setTip(std::move(pTip));
853 }
854 
855 /* Return which key was used to press the last returned widget */
widgGetButtonKey_DEPRECATED(const std::shared_ptr<W_SCREEN> & psScreen)856 UDWORD widgGetButtonKey_DEPRECATED(const std::shared_ptr<W_SCREEN> &psScreen)
857 {
858 	ASSERT_OR_RETURN(lastReleasedKey_DEPRECATED, psScreen != nullptr, "Invalid screen pointer");
859 	/* Don't actually need the screen parameter at the moment - but it might be
860 	   handy if released needs to stop being a static and moves into
861 	   the screen structure */
862 
863 	return lastReleasedKey_DEPRECATED;
864 }
865 
getState()866 unsigned WIDGET::getState()
867 {
868 	ASSERT(false, "Can't get widget type %u's state.", type);
869 	return 0;
870 }
871 
872 /* Get a button or clickable form's state */
widgGetButtonState(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)873 UDWORD widgGetButtonState(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
874 {
875 	ASSERT_OR_RETURN(0, psScreen != nullptr, "Invalid screen pointer");
876 	/* Get the button */
877 	WIDGET *psWidget = widgGetFromID(psScreen, id);
878 	ASSERT_OR_RETURN(0, psWidget, "Couldn't find widget by ID %u", id);
879 
880 	return psWidget->getState();
881 }
882 
setFlash(bool)883 void WIDGET::setFlash(bool)
884 {
885 	ASSERT(false, "Can't set widget type %u's flash.", type);
886 }
887 
widgSetButtonFlash(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)888 void widgSetButtonFlash(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
889 {
890 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
891 	/* Get the button */
892 	WIDGET *psWidget = widgGetFromID(psScreen, id);
893 	ASSERT_OR_RETURN(, psWidget, "Couldn't find widget by ID %u", id);
894 
895 	psWidget->setFlash(true);
896 }
897 
widgClearButtonFlash(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)898 void widgClearButtonFlash(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
899 {
900 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
901 	/* Get the button */
902 	WIDGET *psWidget = widgGetFromID(psScreen, id);
903 	ASSERT_OR_RETURN(, psWidget, "Couldn't find widget by ID %u", id);
904 
905 	psWidget->setFlash(false);
906 }
907 
908 
setState(unsigned)909 void WIDGET::setState(unsigned)
910 {
911 	ASSERT(false, "Can't set widget type %u's state.", type);
912 }
913 
914 /* Set a button or clickable form's state */
widgSetButtonState(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id,UDWORD state)915 void widgSetButtonState(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id, UDWORD state)
916 {
917 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
918 	/* Get the button */
919 	WIDGET *psWidget = widgGetFromID(psScreen, id);
920 	ASSERT_OR_RETURN(, psWidget, "Couldn't find widget by ID %u", id);
921 
922 	psWidget->setState(state);
923 }
924 
getString() const925 WzString WIDGET::getString() const
926 {
927 	ASSERT(false, "Can't get widget type %u's string.", type);
928 	return WzString();
929 }
930 
931 /* Return a pointer to a buffer containing the current string of a widget.
932  * NOTE: The string must be copied out of the buffer
933  */
widgGetString(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)934 const char *widgGetString(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
935 {
936 	ASSERT_OR_RETURN("", psScreen != nullptr, "Invalid screen pointer");
937 	const WIDGET *psWidget = widgGetFromID(psScreen, id);
938 	ASSERT_OR_RETURN("", psWidget, "Couldn't find widget by ID %u", id);
939 
940 	static WzString ret;  // Must be static so it isn't immediately freed when this function returns.
941 	ret = psWidget->getString();
942 	return ret.toUtf8().c_str();
943 }
944 
widgGetWzString(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id)945 const WzString& widgGetWzString(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id)
946 {
947 	static WzString emptyString = WzString();
948 	ASSERT_OR_RETURN(emptyString, psScreen != nullptr, "Invalid screen pointer");
949 	const WIDGET *psWidget = widgGetFromID(psScreen, id);
950 	ASSERT_OR_RETURN(emptyString, psWidget, "Couldn't find widget by ID %u", id);
951 
952 	static WzString ret;  // Must be static so it isn't immediately freed when this function returns.
953 	ret = psWidget->getString();
954 	return ret;
955 }
956 
setString(WzString)957 void WIDGET::setString(WzString)
958 {
959 	ASSERT(false, "Can't set widget type %u's string.", type);
960 }
961 
962 /* Set the text in a widget */
widgSetString(const std::shared_ptr<W_SCREEN> & psScreen,UDWORD id,const char * pText)963 void widgSetString(const std::shared_ptr<W_SCREEN> &psScreen, UDWORD id, const char *pText)
964 {
965 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
966 	/* Get the widget */
967 	WIDGET *psWidget = widgGetFromID(psScreen, id);
968 	ASSERT_OR_RETURN(, psWidget, "Couldn't find widget by ID %u", id);
969 
970 	if (psWidget->type == WIDG_EDITBOX && psScreen->hasFocus(*psWidget))
971 	{
972 		psScreen->setFocus(nullptr);
973 	}
974 
975 	psWidget->setString(WzString::fromUtf8(pText));
976 }
977 
processCallbacksRecursive(W_CONTEXT * psContext)978 void WIDGET::processCallbacksRecursive(W_CONTEXT *psContext)
979 {
980 	/* Go through all the widgets on the form */
981 	for (auto const &psCurr: childWidgets)
982 	{
983 		/* Call the callback */
984 		if (psCurr->callback)
985 		{
986 			psCurr->callback(psCurr.get(), psContext);
987 		}
988 
989 		/* and then recurse */
990 		W_CONTEXT sFormContext(psContext);
991 		sFormContext.mx = psContext->mx - psCurr->x();
992 		sFormContext.my = psContext->my - psCurr->y();
993 		sFormContext.xOffset = psContext->xOffset + psCurr->x();
994 		sFormContext.yOffset = psContext->yOffset + psCurr->y();
995 		psCurr->processCallbacksRecursive(&sFormContext);
996 	}
997 }
998 
999 /* Process all the widgets on a form.
1000  * mx and my are the coords of the mouse relative to the form origin.
1001  */
runRecursive(W_CONTEXT * psContext)1002 void WIDGET::runRecursive(W_CONTEXT *psContext)
1003 {
1004 	/* Process the form's widgets */
1005 	for (auto const &psCurr: childWidgets)
1006 	{
1007 		/* Skip any hidden widgets */
1008 		if (!psCurr->visible())
1009 		{
1010 			continue;
1011 		}
1012 
1013 		/* Found a sub form, so set up the context */
1014 		W_CONTEXT sFormContext(psContext);
1015 		sFormContext.mx = psContext->mx - psCurr->x();
1016 		sFormContext.my = psContext->my - psCurr->y();
1017 		sFormContext.xOffset = psContext->xOffset + psCurr->x();
1018 		sFormContext.yOffset = psContext->yOffset + psCurr->y();
1019 
1020 		/* Process it */
1021 		psCurr->runRecursive(&sFormContext);
1022 
1023 		/* Run the widget */
1024 		psCurr->run(psContext);
1025 	}
1026 }
1027 
hitTest(int x,int y)1028 bool WIDGET::hitTest(int x, int y)
1029 {
1030 	// default hit-testing bounding rect (based on the widget's x, y, width, height)
1031 	bool hitTestResult = dim.contains(x, y);
1032 
1033 	if(customHitTest)
1034 	{
1035 		// if the default bounding-rect hit-test succeeded, use the custom hit-testing func
1036 		hitTestResult = hitTestResult && customHitTest(this, x, y);
1037 	}
1038 
1039 	return hitTestResult;
1040 }
1041 
processClickRecursive(W_CONTEXT * psContext,WIDGET_KEY key,bool wasPressed)1042 bool WIDGET::processClickRecursive(W_CONTEXT *psContext, WIDGET_KEY key, bool wasPressed)
1043 {
1044 	bool didProcessClick = false;
1045 
1046 	W_CONTEXT shiftedContext(psContext);
1047 	shiftedContext.mx = psContext->mx - x();
1048 	shiftedContext.my = psContext->my - y();
1049 	shiftedContext.xOffset = psContext->xOffset + x();
1050 	shiftedContext.yOffset = psContext->yOffset + y();
1051 
1052 	bool skipProcessingChildren = false;
1053 	if (type == WIDG_FORM && ((W_FORM *)this)->disableChildren)
1054 	{
1055 		skipProcessingChildren = true;
1056 	}
1057 
1058 	if (!skipProcessingChildren)
1059 	{
1060 		// Process subwidgets.
1061 		for (auto const &psCurr: childWidgets)
1062 		{
1063 			if (!psCurr->visible() || !psCurr->hitTest(shiftedContext.mx, shiftedContext.my))
1064 			{
1065 				continue;  // Skip any hidden widgets, or widgets the click missed.
1066 			}
1067 
1068 			// Process it (recursively).
1069 			didProcessClick = psCurr->processClickRecursive(&shiftedContext, key, wasPressed) || didProcessClick;
1070 		}
1071 	}
1072 
1073 	if (!visible())
1074 	{
1075 		// special case for root form
1076 		// since the processClickRecursive of children is only called if they are visible
1077 		// this should only trigger for the root form of a screen that's been set to invisible
1078 		return didProcessClick;
1079 	}
1080 
1081 	if (psMouseOverWidget.expired())
1082 	{
1083 		psMouseOverWidget = shared_from_this();  // Mark that the mouse is over a widget (if we haven't already).
1084 	}
1085 
1086 	if (isMouseOverWidget())
1087 	{
1088 		if (auto lockedScreen = screenPointer.lock())
1089 		{
1090 			if (!lockedScreen->isLastHighlight(*this))
1091 			{
1092 				if (auto lockedLastHighligh = lockedScreen->lastHighlight.lock())
1093 				{
1094 					lockedLastHighligh->highlightLost();
1095 				}
1096 				lockedScreen->lastHighlight = shared_from_this();  // Mark that the mouse is over a widget (if we haven't already).
1097 				highlight(psContext);
1098 			}
1099 			if (psMouseOverWidgetScreen != lockedScreen)
1100 			{
1101 				if (psMouseOverWidgetScreen)
1102 				{
1103 					// ensure the last mouseOverWidgetScreen receives highlightLost event
1104 					if (auto lockedLastHighlight = psMouseOverWidgetScreen->lastHighlight.lock())
1105 					{
1106 						lockedLastHighlight->highlightLost();
1107 					}
1108 					psMouseOverWidgetScreen->lastHighlight.reset();
1109 				}
1110 				// update psMouseOverWidgetScreen
1111 				psMouseOverWidgetScreen = lockedScreen;
1112 			}
1113 		}
1114 		else
1115 		{
1116 			// Note: There are rare exceptions where this can happen, if a WIDGET is doing something funky like drawing widgets it hasn't attached.
1117 			psMouseOverWidgetScreen = nullptr;
1118 		}
1119 
1120 		if (key == WKEY_NONE)
1121 		{
1122 			// We're just checking mouse position, and this isn't a click, but return that we handled it
1123 			return true;
1124 		}
1125 	}
1126 
1127 	if (key == WKEY_NONE)
1128 	{
1129 		return didProcessClick;  // Just checking mouse position, not a click.
1130 	}
1131 
1132 	if (isMouseOverWidget())
1133 	{
1134 		auto psClickedWidget = shared_from_this();
1135 		if (isTransparentToClicks)
1136 		{
1137 			do {
1138 				psClickedWidget = psClickedWidget->parent();
1139 			} while (psClickedWidget != nullptr && psClickedWidget->isTransparentToClicks);
1140 			if (psClickedWidget == nullptr)
1141 			{
1142 				return false;
1143 			}
1144 		}
1145 		if (wasPressed)
1146 		{
1147 			psClickedWidget->clicked(psContext, key);
1148 			psClickDownWidgetScreen = psClickedWidget->screenPointer.lock();
1149 		}
1150 		else
1151 		{
1152 			psClickedWidget->released(psContext, key);
1153 			psClickDownWidgetScreen.reset();
1154 		}
1155 		didProcessClick = true;
1156 	}
1157 
1158 	return didProcessClick;
1159 }
1160 
1161 
1162 /* Execute a set of widgets for one cycle.
1163  * Returns a list of activated widgets.
1164  */
widgRunScreen(const std::shared_ptr<W_SCREEN> & psScreen)1165 WidgetTriggers const &widgRunScreen(const std::shared_ptr<W_SCREEN> &psScreen)
1166 {
1167 	static WidgetTriggers assertReturn;
1168 	ASSERT_OR_RETURN(assertReturn, psScreen != nullptr, "Invalid screen pointer");
1169 	psScreen->retWidgets.clear();
1170 	cleanupDeletedOverlays();
1171 
1172 	/* Initialise the context */
1173 	W_CONTEXT sContext = W_CONTEXT::ZeroContext();
1174 	psMouseOverWidget.reset();
1175 
1176 #ifdef DEBUG
1177 	psCurrentlyRunningScreen = psScreen;
1178 #endif
1179 
1180 	// Note which keys have been pressed
1181 	lastReleasedKey_DEPRECATED = WKEY_NONE;
1182 	if (getWidgetsStatus())
1183 	{
1184 		MousePresses const &clicks = inputGetClicks();
1185 		for (MousePresses::const_iterator c = clicks.begin(); c != clicks.end(); ++c)
1186 		{
1187 			WIDGET_KEY wkey;
1188 			switch (c->key)
1189 			{
1190 			case MOUSE_LMB: wkey = WKEY_PRIMARY; break;
1191 			case MOUSE_RMB: wkey = WKEY_SECONDARY; break;
1192 			default: continue;  // Who cares about other mouse buttons?
1193 			}
1194 			bool pressed;
1195 			switch (c->action)
1196 			{
1197 			case MousePress::Press: pressed = true; break;
1198 			case MousePress::Release: pressed = false; break;
1199 			default: continue;
1200 			}
1201 			sContext.mx = c->pos.x;
1202 			sContext.my = c->pos.y;
1203 			bool didProcessClick = false;
1204 			forEachOverlayScreen([&sContext, &didProcessClick, wkey, pressed](const OverlayScreen& overlay) -> bool
1205 			{
1206 				didProcessClick = overlay.psScreen->psForm->processClickRecursive(&sContext, wkey, pressed);
1207 				return !didProcessClick;
1208 			});
1209 			if (!didProcessClick)
1210 			{
1211 				psScreen->psForm->processClickRecursive(&sContext, wkey, pressed);
1212 			}
1213 
1214 			lastReleasedKey_DEPRECATED = wkey;
1215 		}
1216 	}
1217 
1218 	sContext.mx = mouseX();
1219 	sContext.my = mouseY();
1220 	bool didProcessClick = false;
1221 	forEachOverlayScreen([&sContext, &didProcessClick](const OverlayScreen& overlay) -> bool
1222 	{
1223 		didProcessClick = overlay.psScreen->psForm->processClickRecursive(&sContext, WKEY_NONE, true);  // Update highlights and psMouseOverWidget.
1224 		return !didProcessClick;
1225 	});
1226 	if (!didProcessClick)
1227 	{
1228 		psScreen->psForm->processClickRecursive(&sContext, WKEY_NONE, true);  // Update highlights and psMouseOverWidget.
1229 	}
1230 	if (psMouseOverWidget.lock() == nullptr)
1231 	{
1232 		psMouseOverWidgetScreen.reset();
1233 	}
1234 
1235 	/* Process the screen's widgets */
1236 	forEachOverlayScreen([&sContext](const OverlayScreen& overlay) -> bool
1237 	{
1238 		overlay.psScreen->psForm->runRecursive(&sContext);
1239 		return true;
1240 	});
1241 	psScreen->psForm->runRecursive(&sContext);
1242 
1243 #ifdef DEBUG
1244 	psCurrentlyRunningScreen = nullptr;
1245 #endif
1246 
1247 	runScheduledTasks();
1248 	deleteOldWidgets();  // Delete any widgets that called deleteLater() while being run.
1249 
1250 	/* Return the ID of a pressed button or finished edit box if any */
1251 	return psScreen->retWidgets;
1252 }
1253 
1254 /* Set the id number for widgRunScreen to return */
setReturn(const std::shared_ptr<WIDGET> & psWidget)1255 void W_SCREEN::setReturn(const std::shared_ptr<WIDGET> &psWidget)
1256 {
1257 	WidgetTrigger trigger;
1258 	trigger.widget = psWidget;
1259 	retWidgets.push_back(trigger);
1260 }
1261 
displayRecursive(WidgetGraphicsContext const & context)1262 void WIDGET::displayRecursive(WidgetGraphicsContext const &context)
1263 {
1264 	if (context.clipContains(geometry())) {
1265 		if (debugBoundingBoxesOnly)
1266 		{
1267 			// Display bounding boxes.
1268 			PIELIGHT col;
1269 			col.byte.r = 128 + iSinSR(realTime, 2000, 127); col.byte.g = 128 + iSinSR(realTime + 667, 2000, 127); col.byte.b = 128 + iSinSR(realTime + 1333, 2000, 127); col.byte.a = 128;
1270 			iV_Box(context.getXOffset() + x(), context.getYOffset() + y(), context.getXOffset() + x() + width() - 1, context.getYOffset() + y() + height() - 1, col);
1271 		}
1272 		else if (displayFunction)
1273 		{
1274 			displayFunction(this, context.getXOffset(), context.getYOffset());
1275 		}
1276 		else
1277 		{
1278 			// Display widget.
1279 			display(context.getXOffset(), context.getYOffset());
1280 		}
1281 	}
1282 
1283 	if (type == WIDG_FORM && ((W_FORM *)this)->disableChildren)
1284 	{
1285 		return;
1286 	}
1287 
1288 	auto childrenContext = context.translatedBy(x(), y());
1289 
1290 	// If this is a clickable form, the widgets on it have to move when it's down.
1291 	if (type == WIDG_FORM && (((W_FORM *)this)->style & WFORM_NOCLICKMOVE) == 0)
1292 	{
1293 		if ((((W_FORM *)this)->style & WFORM_CLICKABLE) != 0 &&
1294 		    (((W_CLICKFORM *)this)->state & (WBUT_DOWN | WBUT_LOCK | WBUT_CLICKLOCK)) != 0)
1295 		{
1296 			childrenContext = childrenContext.translatedBy(1, 1);
1297 		}
1298 	}
1299 
1300 	// Display the widgets on this widget.
1301 	for (auto const &child: childWidgets)
1302 	{
1303 		if (child->visible())
1304 		{
1305 			child->displayRecursive(childrenContext);
1306 		}
1307 	}
1308 }
1309 
1310 /* Display the screen's widgets in their current state
1311  * (Call after calling widgRunScreen, this allows the input
1312  *  processing to be separated from the display of the widgets).
1313  */
widgDisplayScreen(const std::shared_ptr<W_SCREEN> & psScreen)1314 void widgDisplayScreen(const std::shared_ptr<W_SCREEN> &psScreen)
1315 {
1316 	ASSERT_OR_RETURN(, psScreen != nullptr, "Invalid screen pointer");
1317 	// To toggle debug bounding boxes: Press: Left Shift   --  --  --------------
1318 	//                                        Left Ctrl  ------------  --  --  ----
1319 	static const int debugSequence[] = { -1, 0, 1, 3, 1, 3, 1, 3, 2, 3, 2, 3, 2, 3, 1, 0, -1};
1320 	static int const *debugLoc = debugSequence;
1321 	static bool debugBoundingBoxes = false;
1322 	int debugCode = keyDown(KEY_LCTRL) + 2 * keyDown(KEY_LSHIFT);
1323 	debugLoc = debugLoc[1] == -1 ? debugSequence : debugLoc[0] == debugCode ? debugLoc : debugLoc[1] == debugCode ? debugLoc + 1 : debugSequence;
1324 	debugBoundingBoxes = debugBoundingBoxes ^ (debugLoc[1] == -1);
1325 
1326 	cleanupDeletedOverlays();
1327 
1328 	/* Process any user callback functions */
1329 	W_CONTEXT sContext = W_CONTEXT::ZeroContext();
1330 	sContext.mx = mouseX();
1331 	sContext.my = mouseY();
1332 	psScreen->psForm->processCallbacksRecursive(&sContext);
1333 
1334 	// Display the widgets.
1335 	psScreen->psForm->displayRecursive();
1336 
1337 	// Always overlays on-top (i.e. draw them last)
1338 	forEachOverlayScreenBottomUp([&sContext](const OverlayScreen& overlay) -> bool
1339 	{
1340 		overlay.psScreen->psForm->processCallbacksRecursive(&sContext);
1341 		overlay.psScreen->psForm->displayRecursive();
1342 		return true;
1343 	}); // <- enumerate in *increasing* z-order for drawing
1344 
1345 	deleteOldWidgets();  // Delete any widgets that called deleteLater() while being displayed.
1346 
1347 	/* Display the tool tip if there is one */
1348 	tipDisplay();
1349 
1350 	if (debugBoundingBoxes)
1351 	{
1352 		debugBoundingBoxesOnly = true;
1353 		psScreen->psForm->displayRecursive();
1354 		debugBoundingBoxesOnly = false;
1355 	}
1356 }
1357 
setFocus(const std::shared_ptr<WIDGET> & widget)1358 void W_SCREEN::setFocus(const std::shared_ptr<WIDGET> &widget)
1359 {
1360 	if (auto locked = psFocus.lock())
1361 	{
1362 		locked->focusLost();
1363 	}
1364 	psFocus = widget;
1365 }
1366 
WidgSetAudio(WIDGET_AUDIOCALLBACK Callback,SWORD HilightID,SWORD ClickedID,SWORD ErrorID)1367 void WidgSetAudio(WIDGET_AUDIOCALLBACK Callback, SWORD HilightID, SWORD ClickedID, SWORD ErrorID)
1368 {
1369 	AudioCallback = Callback;
1370 	HilightAudioID = HilightID;
1371 	ClickedAudioID = ClickedID;
1372 	ErrorAudioID = ErrorID;
1373 }
1374 
WidgGetAudioCallback(void)1375 WIDGET_AUDIOCALLBACK WidgGetAudioCallback(void)
1376 {
1377 	return AudioCallback;
1378 }
1379 
WidgGetHilightAudioID(void)1380 SWORD WidgGetHilightAudioID(void)
1381 {
1382 	return HilightAudioID;
1383 }
1384 
WidgGetClickedAudioID(void)1385 SWORD WidgGetClickedAudioID(void)
1386 {
1387 	return ClickedAudioID;
1388 }
1389 
WidgGetErrorAudioID(void)1390 SWORD WidgGetErrorAudioID(void)
1391 {
1392 	return ErrorAudioID;
1393 }
1394 
setWidgetsStatus(bool var)1395 void setWidgetsStatus(bool var)
1396 {
1397 	bWidgetsActive = var;
1398 }
1399 
getWidgetsStatus()1400 bool getWidgetsStatus()
1401 {
1402 	return bWidgetsActive;
1403 }
1404 
clipContains(WzRect const & rect) const1405 bool WidgetGraphicsContext::clipContains(WzRect const& rect) const
1406 {
1407 	return !clipped || clipRect.contains({offset.x + rect.x(), offset.y + rect.y(), rect.width(), rect.height()});
1408 }
1409 
translatedBy(int32_t x,int32_t y) const1410 WidgetGraphicsContext WidgetGraphicsContext::translatedBy(int32_t x, int32_t y) const
1411 {
1412 	WidgetGraphicsContext newContext(*this);
1413 	newContext.offset.x += x;
1414 	newContext.offset.y += y;
1415 	return newContext;
1416 }
1417 
clippedBy(WzRect const & newRect) const1418 WidgetGraphicsContext WidgetGraphicsContext::clippedBy(WzRect const &newRect) const
1419 {
1420 	WidgetGraphicsContext newContext(*this);
1421 	newContext.clipRect = {
1422 		offset.x + newRect.x(),
1423 		offset.y + newRect.y(),
1424 		newRect.width(),
1425 		newRect.height()
1426 	};
1427 
1428 	if (clipped) {
1429 		newContext.clipRect = newContext.clipRect.intersectionWith(clipRect);
1430 	}
1431 
1432 	newContext.clipped = true;
1433 
1434 	return newContext;
1435 }
1436