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
7Element::~Element()
8{
9 removeAll();
10}
11
12Element::Element()
13{
14 needsRedraw = true;
15}
16
18{
19 // whether or not we need to update the screen
20 bool ret = false;
21
22 // if we're hidden, don't process input
23 if (hidden) return ret;
24
25 // if 3ds mock, ignore top screen inputs
26#ifdef _3DS_MOCK
27 if (event->touchIn(0, 0, 400, 240)) return ret;
28#endif
29
30 // do any touch down, drag, or up events
31 if (touchable)
32 {
33 ret |= onTouchDown(event);
34 ret |= onTouchDrag(event);
35 ret |= onTouchUp(event);
36 }
37
38 // call process on subelements
39 for (int x = 0; x < this->elements.size(); x++)
40 if (this->elements.size() > x && this->elements[x])
41 ret |= this->elements[x]->process(event);
42
43 ret |= this->needsRedraw;
44 this->needsRedraw = false;
45
46 // if this variable is positive, decrease it, and force a redraw (acts like needsRedraw but over X redraws)
47 if (futureRedrawCounter > 0) {
49 ret |= true;
50 }
51
52 if (RootDisplay::idleCursorPulsing) {
53 // if we are using idle cursor pulsing, and this element's elastic counter is 0, force a redraw
54 return ret | (this->elasticCounter > 0);
55 }
56
57 return ret;
58}
59
61{
62 //if we're hidden, don't render
63 if (hidden) return;
64
65 // this needs to happen before any rendering
66 this->recalcPosition(parent);
67
68 // if we're in debug mode, draw an outline
69 if (this->hasBackground) {
70 // render the element background
71 this->renderBackground(true);
72 }
73 else if (RootDisplay::isDebug) {
74 backgroundColor = randomColor();
75 this->renderBackground(false);
76 }
77
78 // go through every subelement and run render
79 for (Element* subelement : elements)
80 {
81 subelement->render(this);
82 }
83
84 CST_Renderer* renderer = getRenderer();
85
86 // if we're touchable, and we have some animation counter left, draw a rectangle+overlay
87 if (this->touchable && this->elasticCounter > THICK_HIGHLIGHT)
88 {
89 auto marginSpacing = cornerRadius > 0 ? 0 : 5;
90 CST_Rect d = { this->xAbs - marginSpacing, this->yAbs - marginSpacing, this->width + marginSpacing*2, this->height + marginSpacing*2 };
91 if (cornerRadius > 0) {
92 // draw a rounded highlight instead
93 CST_roundedBoxRGBA(renderer, d.x, d.y, d.x + d.w, d.y + d.h,
94 cornerRadius, 0x10, 0xD9, 0xD9, 0x40);
95 } else {
96 CST_SetDrawBlend(renderer, true);
97 CST_SetDrawColorRGBA(renderer, 0x10, 0xD9, 0xD9, 0x40);
98 CST_FillRect(renderer, &d);
99 }
100 }
101
102 if (this->touchable && this->elasticCounter > NO_HIGHLIGHT)
103 {
104 auto marginSpacing = cornerRadius > 0 ? 0 : 5;
105 CST_Rect d = { this->xAbs - marginSpacing, this->yAbs - marginSpacing, this->width + marginSpacing*2, this->height + marginSpacing*2 };
106 if (this->elasticCounter == THICK_HIGHLIGHT)
107 {
108 int ticks = CST_GetTicks() / 100;
109 int pulseState = ticks % 20;
110 if (pulseState > 9) {
111 pulseState = 19 - pulseState;
112 }
113
114 if (!RootDisplay::idleCursorPulsing) {
115 // if we're not using idle cursor pulsing, just draw a simple rectangle
116 pulseState = 0;
117 }
118
119 // make it a little thicker by drawing more rectangles TODO: better way to do this?
120 auto decreaser = cornerRadius > 0 ? 0 : -2;
121 for (int x = decreaser; x <= 3; x++)
122 {
123 // draw a rectangle with varying brightness depending on the pulse state
124 int r = 0x10; //- 0x01 * pulseState;
125 int g = 0xD9 - 0x01 * pulseState;
126 int b = 0xD9 - 0x01 * pulseState;
127 int edgeMod = x==1 ? 0 : abs(x); // slight bias towards the inner
128 int a = fmax(0x0, 0xFF - 0x10 * pulseState * edgeMod);
129 CST_roundedRectangleRGBA(renderer, d.x + x, d.y + x, d.x + d.w - x, d.y + d.h - x, cornerRadius, r, g, b, a);
130 }
131 } else {
132 // simple rectangle, not pulsing
133 CST_roundedRectangleRGBA(renderer, d.x, d.y, d.x + d.w, d.y + d.h, cornerRadius, 0x10, 0xD9, 0xD9, 0xFF);
134 // and one inner rectangle too
135 CST_roundedRectangleRGBA(renderer, d.x + 1, d.y + 1, d.x + d.w - 1, d.y + d.h - 1, cornerRadius, 0x10, 0xD9, 0xD9, 0xFF);
136 }
137 }
138}
139
140void Element::recalcPosition(Element* parent) {
141 // calculate absolute x/y positions
142 if (parent && !isAbsolute)
143 {
144 this->xAbs = parent->xAbs + this->x;
145 this->yAbs = parent->yAbs + this->y;
146 } else {
147 this->xAbs = this->x;
148 this->yAbs = this->y;
149 }
150
151 // go through all constraints and apply them
152 for (Constraint* constraint : constraints)
153 {
154 constraint->apply(this);
155 }
156
157 // go through all animations and apply them
158 // TODO: animations can modify the actual positions, which can mess up constraints
159 if (animations.size() > 0) {
160 std::vector<Animation*> toRemove;
161 for (Animation* animation : animations)
162 {
163 // if there are any animations, we need to re-render
164 needsRedraw = true;
165
166 bool finished = animation->step();
167 if (finished) {
168 toRemove.push_back(animation);
169 }
170 }
171 for (Animation* animation : toRemove) {
172 animations.erase(std::remove(animations.begin(), animations.end(), animation), animations.end());
173 delete animation;
174 }
175 }
176}
177
178CST_Rect Element::getBounds()
179{
180 return {
181 .x = this->xAbs,
182 .y = this->yAbs,
183 .w = this->width,
184 .h = this->height,
185 };
186}
187
188void Element::renderBackground(bool fill) {
189 CST_Renderer* renderer = getRenderer();
190 CST_Rect bounds = getBounds();
191 auto r = backgroundColor.r * 0xFF;
192 auto g = backgroundColor.g * 0xFF;
193 auto b = backgroundColor.b * 0xFF;
194
195 if (cornerRadius > 0) {
196 const auto renderRect = fill ? CST_roundedBoxRGBA : CST_roundedRectangleRGBA;
197 renderRect(renderer, bounds.x, bounds.y, bounds.x + bounds.w, bounds.y + bounds.h,
198 cornerRadius, backgroundColor.r * 0xFF, backgroundColor.g * 0xFF, backgroundColor.b * 0xFF, 0xFF);
199 } else {
200 CST_SetDrawColorRGBA(renderer, r, g, b, 0xFF);
201 const auto renderRect = fill ? CST_FillRect : CST_DrawRect;
202 renderRect(renderer, &bounds);
203 }
204}
205
206void Element::position(int x, int y)
207{
208 this->x = x;
209 this->y = y;
210}
211
212bool Element::onTouchDown(InputEvents* event)
213{
214 if (!event->isTouchDown())
215 return false;
216
217 if (!event->touchIn(this->xAbs, this->yAbs, this->width, this->height))
218 return false;
219
220 // mouse pushed down, set variable
221 this->dragging = true;
222 this->lastMouseY = event->yPos;
223 this->lastMouseX = event->xPos;
224
225 // turn on deep highlighting during a touch down
226 if (this->touchable)
227 this->elasticCounter = DEEP_HIGHLIGHT;
228
229 return true;
230}
231
232bool Element::onTouchDrag(InputEvents* event)
233{
234 bool ret = false;
235
236 if (!event->isTouchDrag())
237 return false;
238
239 // if we're not in a deeplight (a touchdown event), draw our own drag highlight
240 if (this->elasticCounter != DEEP_HIGHLIGHT) {
241 if (event->touchIn(this->xAbs, this->yAbs, this->width, this->height)) {
242 // if there's currently _no_ highlight, and we're in a drag event on this element,
243 // so we should turn on the hover highlight
244 this->elasticCounter = THICK_HIGHLIGHT;
245 ret |= true;
246
247 // play a hover sound and vibrate
248 CST_LowRumble(event, 200);
249
250 // change the cursor to a hand
251 CST_SetCursor(CST_CURSOR_HAND);
252 } else {
253 auto initialElasticCounter = this->elasticCounter;
254
255 // we're in a drag event, but not for this element
256 this->elasticCounter = NO_HIGHLIGHT;
257
258 if (initialElasticCounter != NO_HIGHLIGHT) {
259 // change the cursor back to the arrow
260 CST_SetCursor(CST_CURSOR_ARROW);
261 ret |= true;
262 }
263
264 }
265 }
266
267 // minimum amount of wiggle allowed by finger before calling off a touch event
268 int TRESHOLD = 40 / SCALER / SCALER;
269
270 // we've dragged out of the icon, invalidate the click by invoking onTouchUp early
271 // check if we haven't drifted too far from the starting variable (treshold: 40)
272 if (this->dragging && (abs(event->yPos - this->lastMouseY) >= TRESHOLD || abs(event->xPos - this->lastMouseX) >= TRESHOLD))
273 {
274 ret |= (this->elasticCounter > 0);
275 auto prevElasticCounter = this->elasticCounter;
276 this->elasticCounter = NO_HIGHLIGHT;
277 if (prevElasticCounter != NO_HIGHLIGHT) {
278 // change the cursor back to the arrow
279 CST_SetCursor(CST_CURSOR_ARROW);
280 }
281 }
282
283 return ret;
284}
285
286bool Element::onTouchUp(InputEvents* event)
287{
288 if (!event->isTouchUp())
289 return false;
290
291 bool ret = false;
292
293 // ensure we were dragging first (originally checked the treshold above here, but now that actively invalidates it)
294 if (this->dragging)
295 {
296 // check that this click is in the right coordinates for this square
297 // and that a subscreen isn't already being shown
298 // TODO: allow buttons to activae this too?
299 if (event->touchIn(this->xAbs, this->yAbs, this->width, this->height))
300 {
301 // elasticCounter must be nonzero to allow a click through (highlight must be shown)
302 if (this->elasticCounter > 0)
303 {
304 // invoke this element's action
305 if (action != NULL) {
306 this->action();
307 ret |= true;
308 }
309 if (actionWithEvents != NULL) {
310 this->actionWithEvents(event);
311 ret |= true;
312 }
313 }
314 }
315 }
316
317 // release mouse
318 this->dragging = false;
319
320 // update if we were previously highlighted, cause we're about to remove it
321 ret |= (this->elasticCounter > 0);
322
323 this->elasticCounter = 0;
324
325 return ret;
326}
327
328void Element::append(Element *element)
329{
330 auto position = std::find(elements.begin(), elements.end(), element);
331 if (position == elements.end()) {
332 elements.push_back(element);
333 }
334}
335
336void Element::remove(Element *element)
337{
338 auto position = std::find(elements.begin(), elements.end(), element);
339 if (position != elements.end())
340 elements.erase(position);
341}
342
343void Element::wipeAll(bool delSelf)
344{
345 // delete's this element's children, then itself
346 for (auto child : elements) {
347 child->wipeAll(true);
348 }
349 elements.clear();
350
351 if (delSelf && !isProtected) {
352 delete this;
353 }
354}
355
356void Element::removeAll(bool moveToTrash)
357{
358 if (moveToTrash) {
359 // store in a list for free-ing up memory later
360 for (auto e : elements) {
361 RootDisplay::mainDisplay->trash.push_back(e);
362 }
363 }
364 elements.clear();
365 constraints.clear(); // remove all constraints too
366 animations.clear();
367}
368
369Element* Element::child(Element* child)
370{
371 this->elements.push_back(child);
372 child->parent = this;
373 child->recalcPosition(this);
374 return this;
375}
376
377Element* Element::setPosition(int x, int y)
378{
379 this->position(x, y);
380 return this;
381}
382
383Element* Element::setAction(std::function<void()> func)
384{
385 this->action = func;
386 return this;
387}
388
389Element* Element::centerHorizontallyIn(Element* parent)
390{
391 this->x = parent->width / 2 - this->width / 2;
392 return this;
393}
394
395Element* Element::centerVerticallyIn(Element* parent)
396{
397 this->y = parent->height / 2 - this->height / 2;
398 return this;
399}
400
401Element* Element::centerIn(Element* parent)
402{
403 return centerHorizontallyIn(parent)->centerVerticallyIn(parent);
404}
405
406Element* Element::setAbsolute(bool isAbs)
407{
408 isAbsolute = isAbs;
409 return this;
410}
411
412CST_Renderer* Element::getRenderer() {
413 return RootDisplay::renderer;
414}
415
416Element* Element::constrain(int flags, int padding)
417{
418 constraints.push_back(new Constraint(flags, padding));
419 return this;
420}
421
422Element* Element::animate(
423 int duration,
424 std::function<void(float)> onStep,
425 std::function<void()> onFinish
426) {
427 animations.push_back(new Animation(
428 CST_GetTicks(), duration, onStep, onFinish)
429 );
430
431 return this;
432}
433
434// Move an element up within its parent
435Element* Element::moveToFront() {
436 if (parent != NULL) {
437 parent->remove(this);
438 parent->child(this);
439 }
440 return this;
441}
442
443Element* Element::setTouchable(bool touchable)
444{
445 this->touchable = touchable;
446 return this;
447}
448
449void Element::screenshot(std::string path) {
450 // render the webview to a target that can be saved (TARGET ACCESS)
451 CST_Texture* target = SDL_CreateTexture(getRenderer(), SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, width, height);
452
453 // set the target texture
454 SDL_SetRenderTarget(getRenderer(), target);
455
456 // draw a white background first
457 SDL_SetRenderDrawColor(getRenderer(), 255, 255, 255, 255);
458 SDL_RenderClear(getRenderer());
459
460 // render the texture
461 render(parent);
462
463 // reset the target texture
464 SDL_SetRenderTarget(getRenderer(), NULL);
465
466 // save the surface to the path
467 CST_SavePNG(target, path.c_str());
468}
virtual bool process(InputEvents *event)
process any input that is received for this element
Definition: Element.cpp:17
bool hidden
whether this element should skip rendering or not
Definition: Element.hpp:107
bool dragging
whether or not this element is currently being dragged
Definition: Element.hpp:83
bool touchable
whether or not this element can be touched (highlights bounds)
Definition: Element.hpp:80
void screenshot(std::string path)
Take a screenshot of this element and its children, and save it to the given path.
Definition: Element.cpp:449
std::function< void()> action
the action to call (from binded callback) on touch or button selection https://stackoverflow....
Definition: Element.hpp:60
int futureRedrawCounter
whether this element needs a redraw for the next X redraws (decreases each time) (0 is no redraws)
Definition: Element.hpp:89
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:117
virtual void render(Element *parent)
display the current state of the display
Definition: Element.cpp:60
int lastMouseY
the last Y, X coordinate of the mouse (from a drag probably)
Definition: Element.hpp:92
void position(int x, int y)
position the element
Definition: Element.cpp:206
bool needsRedraw
whether or not this element needs the screen redrawn next time it's processed
Definition: Element.hpp:86
int width
width and height of this element (must be manually set, isn't usually calculated (but is in some case...
Definition: Element.hpp:120
Element * parent
the parent element (can sometimes be null if it isn't set)
Definition: Element.hpp:104
std::vector< Element * > elements
visible GUI child elements of this element
Definition: Element.hpp:64
bool touchIn(int x, int width, int y, int height)
whether or not a touch is detected within the specified rect in this cycle