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