Chesto 0.9
A declarative and element-based library for creating GUIs on homebrew'd consoles
Element.cpp
1#include "RootDisplay.hpp"
2#include <algorithm>
3#include "Constraint.hpp"
4#include "Animation.hpp"
5#include <string>
6
7namespace Chesto {
8
9// Shared deleter for all elements - respects isProtected flag
10static void safeElementDeleter(Element* elem) {
11 if (elem && !elem->isProtected) {
12 delete elem;
13 }
14}
15
16Element::~Element()
17{
18 // unique_ptrs will automatically cleaned up
19 elements.clear();
20 constraints.clear();
21 animations.clear();
22}
23
24Element::Element()
25{
26 needsRedraw = true;
27}
28
30{
31 // whether or not we need to update the screen
32 bool ret = false;
33
34 // if we're hidden, don't process input
35 if (hidden) return ret;
36
37 if (parent) {
38 this->recalcPosition(parent);
39 }
40
41 // if 3ds mock, ignore top screen inputs
42#ifdef _3DS_MOCK
43 if (event->touchIn(0, 0, 400, 240)) return ret;
44#endif
45
46 // do any touch down, drag, or up events
47 if (touchable)
48 {
49 ret |= onTouchDown(event);
50 ret |= onTouchDrag(event);
51 ret |= onTouchUp(event);
52 // if an action would modify or free elements before TouchUp fires, use RootDisplay::deferAction instead
53 }
54
55 // call process on subelements
56 size_t elementCount = this->elements.size();
57 for (size_t x = 0; x < elementCount; x++)
58 {
59 // ensure element still exists before trying to process it
60 if (x < this->elements.size() && this->elements[x])
61 {
62 bool childHandled = this->elements[x]->process(event);
63 ret |= childHandled;
64
65 if (childHandled && this->elements.size() != elementCount) {
66 // size changed while we were processing, break out
67 break;
68 }
69 }
70 }
71
72 ret |= this->needsRedraw;
73 this->needsRedraw = false;
74
75 // if this variable is positive, decrease it, and force a redraw (acts like needsRedraw but over X redraws)
76 if (futureRedrawCounter > 0) {
78 ret |= true;
79 }
80
81 if (RootDisplay::idleCursorPulsing) {
82 // if we are using idle cursor pulsing, and this element's elastic counter is 0, force a redraw
83 return ret | (this->elasticCounter > 0);
84 }
85
86 return ret;
87}
88
90{
91 //if we're hidden, don't render
92 if (hidden) return;
93
94 // this needs to happen before any rendering
95 this->recalcPosition(parent);
96
97 // if we're in debug mode, draw an outline
98 if (this->hasBackground) {
99 // render the element background
100 this->renderBackground(true);
101 }
102 else if (RootDisplay::isDebug) {
103 backgroundColor = randomColor();
104 this->renderBackground(false);
105 }
106
107 // go through every subelement and run render
108 for (auto& subelement : elements)
109 {
110 subelement->render(this);
111 }
112
113 CST_Renderer* renderer = getRenderer();
114
115 // if we're touchable, and we have some animation counter left, draw a rectangle+overlay
116 if (this->touchable && this->elasticCounter > THICK_HIGHLIGHT)
117 {
118 float effectiveScale = getEffectiveScale();
119 int scaledWidth = (int)(this->width * effectiveScale);
120 int scaledHeight = (int)(this->height * effectiveScale);
121
122 auto marginSpacing = cornerRadius > 0 ? 0 : 5;
123 CST_Rect d = { this->xAbs - marginSpacing, this->yAbs - marginSpacing, scaledWidth + marginSpacing*2, scaledHeight + marginSpacing*2 };
124 if (cornerRadius > 0) {
125 // draw a rounded highlight instead
126 CST_roundedBoxRGBA(renderer, d.x, d.y, d.x + d.w, d.y + d.h,
127 cornerRadius, 0x10, 0xD9, 0xD9, 0x40);
128 } else {
129 CST_SetDrawBlend(renderer, true);
130 CST_SetDrawColorRGBA(renderer, 0x10, 0xD9, 0xD9, 0x40);
131 CST_FillRect(renderer, &d);
132 }
133 }
134
135 if (this->touchable && this->elasticCounter > NO_HIGHLIGHT)
136 {
137 float effectiveScale = getEffectiveScale();
138 int scaledWidth = (int)(this->width * effectiveScale);
139 int scaledHeight = (int)(this->height * effectiveScale);
140
141 auto marginSpacing = cornerRadius > 0 ? 0 : 5;
142 CST_Rect d = { this->xAbs - marginSpacing, this->yAbs - marginSpacing, scaledWidth + marginSpacing*2, scaledHeight + marginSpacing*2 };
143 if (this->elasticCounter == THICK_HIGHLIGHT)
144 {
145 int ticks = CST_GetTicks() / 100;
146 int pulseState = ticks % 20;
147 if (pulseState > 9) {
148 pulseState = 19 - pulseState;
149 }
150
151 if (!RootDisplay::idleCursorPulsing) {
152 // if we're not using idle cursor pulsing, just draw a simple rectangle
153 pulseState = 0;
154 }
155
156 // make it a little thicker by drawing more rectangles TODO: better way to do this?
157 auto decreaser = cornerRadius > 0 ? 0 : -2;
158 for (int x = decreaser; x <= 3; x++)
159 {
160 // draw a rectangle with varying brightness depending on the pulse state
161 int r = 0x10; //- 0x01 * pulseState;
162 int g = 0xD9 - 0x01 * pulseState;
163 int b = 0xD9 - 0x01 * pulseState;
164 int edgeMod = x==1 ? 0 : abs(x); // slight bias towards the inner
165 int a = fmax(0x0, 0xFF - 0x10 * pulseState * edgeMod);
166 CST_roundedRectangleRGBA(renderer, d.x + x, d.y + x, d.x + d.w - x, d.y + d.h - x, cornerRadius, r, g, b, a);
167 }
168 } else {
169 // simple rectangle, not pulsing
170 CST_roundedRectangleRGBA(renderer, d.x, d.y, d.x + d.w, d.y + d.h, cornerRadius, 0x10, 0xD9, 0xD9, 0xFF);
171 // and one inner rectangle too
172 CST_roundedRectangleRGBA(renderer, d.x + 1, d.y + 1, d.x + d.w - 1, d.y + d.h - 1, cornerRadius, 0x10, 0xD9, 0xD9, 0xFF);
173 }
174 }
175}
176
177void Element::recalcPosition(Element* parent) {
178 // go through all constraints and apply them
179 for (auto& constraint : constraints)
180 {
181 constraint->apply(this);
182 }
183
184 float effectiveScale = getEffectiveScale();
185
186 // calculate any absolute x/y positions after constraints are applied
187 if (parent && !isAbsolute)
188 {
189 if (constraints.empty()) {
190 this->xAbs = parent->xAbs + (int)(this->x * effectiveScale);
191 this->yAbs = parent->yAbs + (int)(this->y * effectiveScale);
192 } else {
193 this->xAbs = parent->xAbs + this->x;
194 this->yAbs = parent->yAbs + this->y;
195 }
196 } else {
197 // absolute positioning unaffected by scale or constraints
198 this->xAbs = this->x;
199 this->yAbs = this->y;
200 }
201
202 // go through all animations and apply them
203 if (animations.size() > 0) {
204 std::vector<size_t> toRemove;
205 for (size_t i = 0; i < animations.size(); i++)
206 {
207 auto& animation = animations[i];
208 // if there are any animations, we need to re-render
209 needsRedraw = true;
210
211 bool finished = animation->step();
212 if (finished) {
213 toRemove.push_back(i);
214 }
215 }
216
217 for (auto it = toRemove.rbegin(); it != toRemove.rend(); ++it) {
218 animations.erase(animations.begin() + *it);
219 }
220 }
221}
222
223float Element::getEffectiveScale() const {
224 // Combines global scale with per-element scale
225 return RootDisplay::globalScale * this->scale;
226}
227
228CST_Rect Element::getBounds()
229{
230 float effectiveScale = getEffectiveScale();
231 return {
232 .x = this->xAbs,
233 .y = this->yAbs,
234 .w = (int)(this->width * effectiveScale),
235 .h = (int)(this->height * effectiveScale),
236 };
237}
238
239void Element::renderBackground(bool fill) {
240 CST_Renderer* renderer = getRenderer();
241 CST_Rect bounds = getBounds();
242 auto r = backgroundColor.r * 0xFF;
243 auto g = backgroundColor.g * 0xFF;
244 auto b = backgroundColor.b * 0xFF;
245
246 if (cornerRadius > 0) {
247 const auto renderRect = fill ? CST_roundedBoxRGBA : CST_roundedRectangleRGBA;
248 renderRect(renderer, bounds.x, bounds.y, bounds.x + bounds.w, bounds.y + bounds.h,
249 cornerRadius, backgroundColor.r * 0xFF, backgroundColor.g * 0xFF, backgroundColor.b * 0xFF, backgroundOpacity);
250 } else {
251 CST_SetDrawColorRGBA(renderer, r, g, b, backgroundOpacity);
252 const auto renderRect = fill ? CST_FillRect : CST_DrawRect;
253 renderRect(renderer, &bounds);
254 }
255}
256
257void Element::position(int x, int y)
258{
259 this->x = x;
260 this->y = y;
261}
262
263bool Element::onTouchDown(InputEvents* event)
264{
265 if (!event->isTouchDown())
266 return false;
267
268 float effectiveScale = getEffectiveScale();
269 int scaledWidth = (int)(this->width * effectiveScale);
270 int scaledHeight = (int)(this->height * effectiveScale);
271
272 if (!event->touchIn(this->xAbs, this->yAbs, scaledWidth, scaledHeight))
273 return false;
274
275 // mouse pushed down, set variable
276 this->dragging = true;
277 this->lastMouseY = event->yPos;
278 this->lastMouseX = event->xPos;
279
280 // turn on deep highlighting during a touch down
281 if (this->touchable)
282 this->elasticCounter = DEEP_HIGHLIGHT;
283
284 return true;
285}
286
287bool Element::onTouchDrag(InputEvents* event)
288{
289 bool ret = false;
290
291 if (!event->isTouchDrag())
292 return false;
293
294 float effectiveScale = getEffectiveScale();
295 int scaledWidth = (int)(this->width * effectiveScale);
296 int scaledHeight = (int)(this->height * effectiveScale);
297
298 // if we're not in a deeplight (a touchdown event), draw our own drag highlight
299 if (this->elasticCounter != DEEP_HIGHLIGHT) {
300 if (event->touchIn(this->xAbs, this->yAbs, scaledWidth, scaledHeight)) {
301 // if there's currently _no_ highlight, and we're in a drag event on this element,
302 // so we should turn on the hover highlight
303 this->elasticCounter = THICK_HIGHLIGHT;
304 ret |= true;
305
306 // play a hover sound and vibrate
307 CST_LowRumble(event, 200);
308
309 // change the cursor to a hand
310 CST_SetCursor(CST_CURSOR_HAND);
311 } else {
312 auto initialElasticCounter = this->elasticCounter;
313
314 // we're in a drag event, but not for this element
315 this->elasticCounter = NO_HIGHLIGHT;
316
317 if (initialElasticCounter != NO_HIGHLIGHT) {
318 // change the cursor back to the arrow
319 CST_SetCursor(CST_CURSOR_ARROW);
320 ret |= true;
321 }
322
323 }
324 }
325
326 // minimum amount of wiggle allowed by finger before calling off a touch event
327 int TRESHOLD = 40 * effectiveScale;
328
329 // we've dragged out of the icon, invalidate the click by invoking onTouchUp early
330 // check if we haven't drifted too far from the starting variable (treshold: 40)
331 if (this->dragging && (abs(event->yPos - this->lastMouseY) >= TRESHOLD || abs(event->xPos - this->lastMouseX) >= TRESHOLD))
332 {
333 ret |= (this->elasticCounter > 0);
334 auto prevElasticCounter = this->elasticCounter;
335 this->elasticCounter = NO_HIGHLIGHT;
336 if (prevElasticCounter != NO_HIGHLIGHT) {
337 // change the cursor back to the arrow
338 CST_SetCursor(CST_CURSOR_ARROW);
339 }
340 }
341
342 return ret;
343}
344
345bool Element::onTouchUp(InputEvents* event)
346{
347 if (!event->isTouchUp())
348 return false;
349
350 bool ret = false;
351
352 float effectiveScale = getEffectiveScale();
353 int scaledWidth = (int)(this->width * effectiveScale);
354 int scaledHeight = (int)(this->height * effectiveScale);
355
356 // ensure we were dragging first (originally checked the treshold above here, but now that actively invalidates it)
357 if (this->dragging)
358 {
359 // check that this click is in the right coordinates for this square
360 // and that a subscreen isn't already being shown
361 // TODO: allow buttons to activae this too?
362 if (event->touchIn(this->xAbs, this->yAbs, scaledWidth, scaledHeight))
363 {
364 // elasticCounter must be nonzero to allow a click through (highlight must be shown)
365 if (this->elasticCounter > 0)
366 {
367 bool wasHighlighted = (this->elasticCounter > 0);
368
369 this->dragging = false;
370 this->elasticCounter = 0;
371
372 // dear future reader: if you're getting a UAF here, try using RootDisplay::deferAction() to schedule your action to run outside of the event processing loop
373 if (action != NULL) {
374 this->action();
375 return true;
376 }
377 if (actionWithEvents != NULL) {
378 this->actionWithEvents(event);
379 return true;
380 }
381
382 // If we get here, we had elasticCounter but no action
383 ret |= wasHighlighted;
384 }
385 }
386 }
387
388 // release mouse
389 this->dragging = false;
390
391 // update if we were previously highlighted, cause we're about to remove it
392 ret |= (this->elasticCounter > 0);
393
394 this->elasticCounter = 0;
395
396 return ret;
397}
398
399void Element::addNode(std::unique_ptr<Element> node)
400{
401 if (!node) {
402 #ifdef DEBUG
403 printf("[Chesto] Warning: Attempted to add null node to %p\n", this);
404 #endif
405 return;
406 }
407
408 Element* rawPtr = node.get();
409
410 // check if element already exists
411 for (const auto& existing : elements) {
412 if (existing.get() == rawPtr) {
413 #ifdef DEBUG
414 printf("[Chesto] Warning: Node %p already exists in parent %p\n", rawPtr, this);
415 #endif
416 return;
417 }
418 }
419
420 rawPtr->parent = this;
421
422 // transfers ownership and sets up safe deleter
423 Element* ptr = node.release();
424 elements.push_back(std::unique_ptr<Element, std::function<void(Element*)>>(
425 ptr,
426 safeElementDeleter
427 ));
428}
429
430void Element::addStackMember(Element* element)
431{
432 if (!element) return;
433
434 // check if element already exists
435 for (const auto& existing : elements) {
436 if (existing.get() == element) {
437 return;
438 }
439 }
440
441 element->parent = this;
442 element->isProtected = true; // Mark as protected
443
444 // For protected (stack-allocated) elements, use the shared safe deleter
445 // the deleter checks isProtected flag and skips deletion
446 elements.push_back(std::unique_ptr<Element, std::function<void(Element*)>>(
447 element,
448 safeElementDeleter
449 ));
450}
451
452
453void Element::remove(Element *element)
454{
455 // single element remove
456
457 auto position = std::find_if(elements.begin(), elements.end(),
458 [element](const std::unique_ptr<Element, std::function<void(Element*)>>& e) {
459 return e.get() == element;
460 });
461 if (position != elements.end())
462 elements.erase(position);
463}
464
465void Element::removeAll()
466{
467 elements.clear();
468 constraints.clear();
469 animations.clear();
470}
471
472Element* Element::setPosition(int x, int y)
473{
474 this->position(x, y);
475 return this;
476}
477
478Element* Element::setAction(std::function<void()> func)
479{
480 this->action = func;
481 return this;
482}
483
484Element* Element::setAbsolute(bool isAbs)
485{
486 isAbsolute = isAbs;
487 return this;
488}
489
490CST_Renderer* Element::getRenderer() {
491 return RootDisplay::renderer;
492}
493
494Element* Element::constrain(int flags, int padding)
495{
496 constraints.push_back(std::make_unique<Constraint>(flags, padding));
497 return this;
498}
499
500Element* Element::constrainToTarget(Element* target, int flags, int padding)
501{
502 constraints.push_back(std::make_unique<Constraint>(flags, padding, std::vector<Element*>{target}));
503 return this;
504}
505
506
507Element* Element::animate(
508 int duration,
509 std::function<void(float)> onStep,
510 std::function<void()> onFinish
511) {
512 animations.push_back(std::make_unique<Animation>(
513 CST_GetTicks(), duration, onStep, onFinish)
514 );
515
516 return this;
517}
518
519// Move an element up within its parent
520Element* Element::moveToFront() {
521 if (parent != NULL) {
522 // lookup this element in parent's vector
523 auto position = std::find_if(parent->elements.begin(), parent->elements.end(),
524 [this](const std::unique_ptr<Element, std::function<void(Element*)>>& e) {
525 return e.get() == this;
526 });
527
528 if (position != parent->elements.end()) {
529 // move it to the end
530 auto elem = std::move(*position);
531 parent->elements.erase(position);
532 parent->elements.push_back(std::move(elem));
533 }
534 }
535 return this;
536}
537
538Element* Element::setTouchable(bool touchable)
539{
540 this->touchable = touchable;
541 return this;
542}
543
544void Element::screenshot(std::string path) {
545 // render the webview to a target that can be saved (TARGET ACCESS)
546 CST_Texture* target = SDL_CreateTexture(getRenderer(), SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, width, height);
547
548 // set the target texture
549 SDL_SetRenderTarget(getRenderer(), target);
550
551 // draw a white background first
552 SDL_SetRenderDrawColor(getRenderer(), 255, 255, 255, 255);
553 SDL_RenderClear(getRenderer());
554
555 // render the texture
556 render(parent);
557
558 // reset the target texture
559 SDL_SetRenderTarget(getRenderer(), NULL);
560
561 // save the surface to the path
562 CST_SavePNG(target, path.c_str());
563}
564
565} // namespace Chesto
std::vector< std::unique_ptr< Element, std::function< void(Element *)> > > elements
visible GUI child elements of this element
Definition: Element.hpp:59
int width
width and height of this element (must be manually set, isn't usually calculated (but is in some case...
Definition: Element.hpp:132
virtual void render(Element *parent)
display the current state of the display
Definition: Element.cpp:89
bool hidden
whether this element should skip rendering or not
Definition: Element.hpp:119
virtual bool process(InputEvents *event)
process any input that is received for this element
Definition: Element.cpp:29
bool needsRedraw
whether or not this element needs the screen redrawn next time it's processed
Definition: Element.hpp:95
bool touchable
whether or not this element can be touched (highlights bounds)
Definition: Element.hpp:89
int lastMouseY
the last Y, X coordinate of the mouse (from a drag probably)
Definition: Element.hpp:101
Element * parent
the parent element (reference only, not owned)
Definition: Element.hpp:116
void screenshot(std::string path)
Take a screenshot of this element and its children, and save it to the given path.
Definition: Element.cpp:544
int elasticCounter
how much time is left in an elastic-type flick/scroll set by the last distance traveled in a scroll,...
Definition: Element.hpp:129
std::function< void()> action
the action to call (from binded callback) on touch or button selection https://stackoverflow....
Definition: Element.hpp:55
void position(int x, int y)
position the element
Definition: Element.cpp:257
int futureRedrawCounter
whether this element needs a redraw for the next X redraws (decreases each time) (0 is no redraws)
Definition: Element.hpp:98
bool dragging
whether or not this element is currently being dragged
Definition: Element.hpp:92
bool touchIn(int x, int width, int y, int height)
whether or not a touch is detected within the specified rect in this cycle