Chesto 0.9
A declarative and element-based library for creating GUIs on homebrew'd consoles
DropDown.cpp
1#include "DropDown.hpp"
2#include "Button.hpp"
3#include "Constraint.hpp"
4#include "RootDisplay.hpp"
5#include <iostream>
6
7namespace Chesto {
8
9DropDown::DropDown(
10 int physicalButton,
11 std::vector<std::pair<std::string, std::string>> choices,
12 std::function<void(std::string)> onSelect,
13 int textSize,
14 std::string defaultChoice,
15 bool isDarkMode
16) : Button(defaultChoice.empty() ? "Select..." : ([&choices, &defaultChoice]() {
17 for (const auto& choice : choices) {
18 if (choice.first == defaultChoice) return choice.second;
19 }
20 return defaultChoice;
21 })(), physicalButton, isDarkMode, textSize, 0)
22{
23 this->choices = choices;
24 this->onSelect = onSelect;
25 this->selectedChoice = defaultChoice;
26
27 this->setAction([
28 this,
29 callback = this->onSelect,
30 isDarkMode
31 ]() {
32 RootDisplay::pushScreen(std::make_unique<DropDownChoices>(
33 this->choices,
34 callback,
35 isDarkMode
36 ));
37 });
38}
39
40bool DropDown::process(InputEvents* event) {
41 return Button::process(event);
42}
43
44// subclass of Screen
45DropDownChoices::DropDownChoices(
46 std::vector<std::pair<std::string, std::string>> choices,
47 std::function<void(std::string)> onSelect,
48 bool isDarkMode,
49 std::string header
50) : choices(choices),
51 onSelectCallback(onSelect),
52 isDarkMode(isDarkMode),
53 header(header)
54{
55 rebuildUI();
56
57 int widestWidth = 0;
58 for (const auto& choice : this->choices) {
59 std::string displayText = choice.second.empty() ? "(empty)" : choice.second;
60
61 auto choiceElement = std::make_unique<Button>(displayText, 0, isDarkMode, 20, 0);
62 widestWidth = std::max(widestWidth, choiceElement->width);
63
64 choiceElement->setAction([
65 this, // Capture this to access onSelectCallback member
66 choiceKey = choice.first
67 ]() {
68 // local copy is needed here to avoid it being destroyed after popScreen
69 std::string choiceKeyCopy = choiceKey;
70
71 // same as above?
72 auto callbackCopy = this->onSelectCallback;
73
74 // pop the dropdown screen (deletes 'this'!)
75 RootDisplay::popScreen();
76
77 // use local copies now
78 if (callbackCopy) {
79 callbackCopy(choiceKeyCopy);
80 }
81 });
82 container->add(std::move(choiceElement));
83 }
84
85 // make each choice the same width (widest)
86 for (const auto& child : container->elements) {
87 if (auto buttonChild = dynamic_cast<Button*>(child.get())) {
88 buttonChild->fixedWidth = widestWidth;
89 buttonChild->updateBounds();
90 }
91 }
92}
93
94void DropDownChoices::rebuildUI() {
95 removeAll();
96
97 // Create a full-screen scrollable list to hold everything
98 // TODO: a common way for Screen's to have all their content be scrollable without making a new element each time
99 auto scrollContainer = std::make_unique<ListElement>();
100 scrollContainer->width = SCREEN_WIDTH;
101 scrollContainer->height = SCREEN_HEIGHT;
102 scrollList = scrollContainer.get(); // Keep pointer for scrolling
103
104 // overlay and shade bg color
105 // also TODO: a common way to have the dim background exist and fade in
106 auto overlay = createNode<Element>();
107 overlay->width = SCREEN_WIDTH;
108 overlay->height = SCREEN_HEIGHT;
109 overlay->backgroundColor = fromRGB(0, 0, 0);
110 overlay->backgroundOpacity = 0x70;
111 overlay->cornerRadius = 1; // needed to force transparency
112 overlay->hasBackground = true;
113
114 // header text if specified
115 if (!header.empty()) {
116 auto headerText = std::make_unique<TextElement>(header.c_str(), 28);
117 headerText->constrain(ALIGN_CENTER_HORIZONTAL);
118 headerText->position(0, SCREEN_HEIGHT / 5 - 60);
119 scrollContainer->addNode(std::move(headerText));
120 }
121
122 // vertical container for the choices
123 auto containerPtr = std::make_unique<Container>(COL_LAYOUT, 20);
124 container = containerPtr.get();
125
126 // TODO: Chesto doesn't know about light/dark themes, these are hardcoded to match HBAS themes
127 if (isDarkMode) {
128 container->backgroundColor = fromRGB(0x2d, 0x2c, 0x31); // match theme dark background
129 } else {
130 container->backgroundColor = fromRGB(0xf5, 0xf5, 0xf5); // light gray for light mode
131 }
132 container->hasBackground = true;
133
134 // center the container horizontally within our full screen overlay
135 container->constrain(ALIGN_CENTER_HORIZONTAL);
136 // position the start of the dropdown in the upper fifth of the screen
137 container->y = SCREEN_HEIGHT / 5;
138
139 scrollContainer->addNode(std::move(containerPtr));
140
141 // not a part of the scrollable area
142 auto backBtn = createNode<Button>(i18n("dropdown.back"), B_BUTTON, isDarkMode, 15);
143 backBtn->constrain(ALIGN_BOTTOM | ALIGN_LEFT, 10);
144 backBtn->setAction([]() {
145 // hides the dropdown without any callback
146 RootDisplay::popScreen();
147 });
148
149 addNode(std::move(scrollContainer));
150}
151
152void DropDownChoices::render(Element* parent) {
153 super::render(parent);
154}
155
156bool DropDownChoices::process(InputEvents* event) {
157 // if we have an up or down button event, instead of scrolling the list element, move cursor selection within the container
158 if (event->held(A_BUTTON) && this->curHighlighted >= 0 && this->curHighlighted < (int)container->elements.size()) {
159 this->container->elements[this->curHighlighted]->action(); // fire that button's action
160 return true;
161 }
162
163 if (event->held(B_BUTTON)) {
164 // dismiss without a selection - pop this screen
165 RootDisplay::popScreen();
166 return true;
167 }
168
169 if (event->isTouch()) {
170 // unhighlight whatever may be highlighted
171 this->curHighlighted = -1;
172 } else {
173 if (event->isKeyDown() && event->held(UP_BUTTON | DOWN_BUTTON | LEFT_BUTTON | RIGHT_BUTTON)) {
174 // Similar to HBAS's AppList navigation logic
175
176 // look up whatever is currently chosen as the highlighted position
177 // and remove its highlight
178 if (curHighlighted >= 0 && curHighlighted < (int)container->elements.size() && container->elements[curHighlighted])
179 container->elements[curHighlighted]->elasticCounter = NO_HIGHLIGHT;
180
181 // adjust it by R for up and down
182 this->curHighlighted += -1 * (event->held(UP_BUTTON)) + (event->held(DOWN_BUTTON));
183
184 // don't let the cursor go out of bounds
185 if (curHighlighted >= (int)container->elements.size()) curHighlighted = container->elements.size() - 1;
186 if (curHighlighted < 0) curHighlighted = 0;
187
188 // highlight the new element
189 if (curHighlighted < (int)container->elements.size() && container->elements[curHighlighted])
190 container->elements[curHighlighted]->elasticCounter = THICK_HIGHLIGHT;
191
192 // Auto-scroll to keep highlighted element on screen (similar to AppList logic)
193 if (curHighlighted >= 0 && curHighlighted < (int)container->elements.size() && container->elements[curHighlighted] && scrollList) {
194 Element* curElement = container->elements[curHighlighted].get();
195
196 // Calculate the y-position of the currently highlighted element on screen (accounting for scroll)
197 // scrollList->y is the scroll offset, container->y is the initial position, curElement->y is relative to container
198 int normalizedY = container->y + curElement->y + scrollList->y;
199
200 // If element is going off the top of the screen, scroll down to keep it visible
201 if (normalizedY < 50) {
202 event->wheelScroll = 1;
203 }
204 // If element is going off the bottom of the screen, scroll up to keep it visible
205 else if (normalizedY + curElement->height > SCREEN_HEIGHT - 50) {
206 event->wheelScroll = -1;
207 }
208 }
209
210 return true;
211 }
212 }
213
214 return Screen::process(event);
215}
216
217} // namespace Chesto
std::vector< std::unique_ptr< Element, std::function< void(Element *)> > > elements
visible GUI child elements of this element
Definition: Element.hpp:59
bool held(int buttons)
whether or not a button is pressed during this cycle