CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
ContextMenuView.cpp
Go to the documentation of this file.
1
6
10#include "cdc_ui/ViewStack.h"
11#include "cdc_hal/IDisplay.h"
12#include "cdc_log.h"
13#include <goodisplay/gdey029T94.h>
14#include <cstring>
15#include <algorithm>
16
17static const char* TAG = "ContextMenuView";
18
22static constexpr int BOX_PADDING = 8;
23static constexpr int TITLE_HEIGHT = 18;
24static constexpr int ITEM_HEIGHT = 16;
25static constexpr int MIN_BOX_WIDTH = 120;
26static constexpr int MAX_BOX_WIDTH = 200;
27
29static constexpr uint32_t kMenuTimeoutMs = 60000;
30
31namespace cdc::ui {
32
40void ContextMenuView::init(const char* title, const ContextMenuItem* items, uint8_t count) {
41 title_ = title;
42 itemCount_ = count > MAX_ITEMS ? MAX_ITEMS : count;
43 for (uint8_t i = 0; i < itemCount_; i++) {
44 items_[i] = items[i];
45 }
46 selection_ = 0;
47 scrollPos_ = 0;
48 lastActivityMs_ = 0;
49 dirty_ = true;
50
51 LOG_D(TAG, "init: title='%s', items=%d", title ? title : "(null)", itemCount_);
52}
53
59void ContextMenuView::navigate(bool down) {
60 if (itemCount_ == 0) return;
61
62 // Restart the inactivity timeout on navigation.
63 lastActivityMs_ = 0;
64
65 if (down) {
66 if (selection_ < itemCount_ - 1) {
67 selection_++;
68 } else {
69 selection_ = 0; // Wrap around
70 scrollPos_ = 0;
71 }
72 } else {
73 if (selection_ > 0) {
74 selection_--;
75 } else {
76 selection_ = itemCount_ - 1; // Wrap around
77 }
78 }
79
80 // Ensure visible
81 if (selection_ >= scrollPos_ + VISIBLE_ITEMS) {
82 scrollPos_ = selection_ - VISIBLE_ITEMS + 1;
83 }
84 if (selection_ < scrollPos_) {
85 scrollPos_ = selection_;
86 }
87
88 dirty_ = true;
89 LOG_D(TAG, "navigate: sel=%d, scroll=%d", selection_, scrollPos_);
90}
91
96void ContextMenuView::select() {
97 if (selection_ < itemCount_) {
98 const ContextMenuItem& item = items_[selection_];
99 LOG_D(TAG, "select: item='%s'", item.label ? item.label : "(null)");
100
101 // Hide menu first
103
104 // Then call callback
105 if (item.callback) {
106 item.callback();
107 }
108 }
109}
110
117 switch (key) {
118 case KEY_UP:
119 navigate(false);
121
122 case KEY_DOWN:
123 navigate(true);
125
126 case KEY_YES: // Select
127 select();
129
130 case KEY_NO: // Cancel
133
134 default:
136 }
137}
138
144void ContextMenuView::onTick(uint32_t nowMs) {
145 if (lastActivityMs_ == 0) {
146 lastActivityMs_ = nowMs;
147 return;
148 }
149 if (nowMs - lastActivityMs_ >= kMenuTimeoutMs) {
151 }
152}
153
159void ContextMenuView::render(bool partial) {
161 if (!display) return;
162
163 auto* gfx = static_cast<Gdey029T94*>(display->getNativeHandle());
164 if (!gfx) return;
165
166 const uint16_t screenWidth = display->getWidth();
167 const uint16_t screenHeight = display->getHeight();
168
169 // Calculate box dimensions
170 int16_t x1, y1;
171 uint16_t maxLabelWidth = 0;
172
173 // Find widest label
174 for (uint8_t i = 0; i < itemCount_; i++) {
175 if (items_[i].label) {
176 uint16_t w, h;
177 gfx->getTextBounds(items_[i].label, 0, 0, &x1, &y1, &w, &h);
178 if (w > maxLabelWidth) maxLabelWidth = w;
179 }
180 }
181
182 // Also check title width
183 if (title_) {
184 uint16_t w, h;
185 gfx->getTextBounds(title_, 0, 0, &x1, &y1, &w, &h);
186 if (w > maxLabelWidth) maxLabelWidth = w;
187 }
188
189 uint16_t boxWidth = maxLabelWidth + BOX_PADDING * 2 + 10;
190 if (boxWidth < MIN_BOX_WIDTH) boxWidth = MIN_BOX_WIDTH;
191 if (boxWidth > MAX_BOX_WIDTH) boxWidth = MAX_BOX_WIDTH;
192
193 uint8_t visibleCount = std::min(itemCount_, VISIBLE_ITEMS);
194 uint16_t boxHeight = TITLE_HEIGHT + (visibleCount * ITEM_HEIGHT) + BOX_PADDING * 2;
195
196 // Center the box
197 int boxX = (screenWidth - boxWidth) / 2;
198 int boxY = (screenHeight - boxHeight) / 2;
199
200 // Draw box background
201 render::drawDialogFrame(gfx, boxX, boxY, boxWidth, boxHeight);
202
203 // Draw title
204 gfx->setTextColor(EPD_WHITE);
205 gfx->fillRect(boxX + 2, boxY + 2, boxWidth - 4, TITLE_HEIGHT, EPD_BLACK);
206 gfx->setCursor(boxX + BOX_PADDING, boxY + 4);
207 if (title_) {
208 render::printText(gfx, title_);
209 }
210
211 // Draw items
212 int itemY = boxY + TITLE_HEIGHT + BOX_PADDING;
213 for (uint8_t i = 0; i < visibleCount; i++) {
214 uint8_t itemIndex = scrollPos_ + i;
215 if (itemIndex >= itemCount_) break;
216
217 const ContextMenuItem& item = items_[itemIndex];
218
219 // Clear item area
220 gfx->fillRect(boxX + BOX_PADDING - 2, itemY, boxWidth - BOX_PADDING * 2 + 4, ITEM_HEIGHT, EPD_WHITE);
221
222 // Highlight selected
223 if (itemIndex == selection_) {
224 gfx->fillRect(boxX + BOX_PADDING - 2, itemY, boxWidth - BOX_PADDING * 2 + 4, ITEM_HEIGHT - 1, EPD_BLACK);
225 gfx->setTextColor(EPD_WHITE);
226 } else {
227 gfx->setTextColor(EPD_BLACK);
228 }
229
230 gfx->setCursor(boxX + BOX_PADDING, itemY + 2);
231 if (item.label) {
232 render::printText(gfx, item.label);
233 }
234
235 itemY += ITEM_HEIGHT;
236 }
237
238 // Scroll indicators if needed
239 if (itemCount_ > VISIBLE_ITEMS) {
240 int indicatorX = boxX + boxWidth - BOX_PADDING;
241 int indicatorY = boxY + TITLE_HEIGHT + BOX_PADDING;
242
243 // Up arrow
244 if (scrollPos_ > 0) {
245 gfx->fillTriangle(
246 indicatorX, indicatorY + 6,
247 indicatorX - 3, indicatorY + 2,
248 indicatorX + 3, indicatorY + 2,
249 EPD_BLACK
250 );
251 }
252
253 // Down arrow
254 if (scrollPos_ + VISIBLE_ITEMS < itemCount_) {
255 int arrowY = boxY + boxHeight - BOX_PADDING - 8;
256 gfx->fillTriangle(
257 indicatorX, arrowY,
258 indicatorX - 3, arrowY + 4,
259 indicatorX + 3, arrowY + 4,
260 EPD_BLACK
261 );
262 }
263 }
264
265 dirty_ = false;
266}
267
271
273
281ContextMenuView* showContextMenu(const char* title, const ContextMenuItem* items, uint8_t count) {
282 s_sharedContextMenu.init(title, items, count);
284 return &s_sharedContextMenu;
285}
286
294
295} // namespace cdc::ui
static const char * TAG
static constexpr int MIN_BOX_WIDTH
static constexpr int ITEM_HEIGHT
static constexpr int MAX_BOX_WIDTH
static constexpr uint32_t kMenuTimeoutMs
Auto-dismiss timeout after the last interaction (ms).
static constexpr int TITLE_HEIGHT
static constexpr int BOX_PADDING
Layout constants.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_D(tag, fmt,...)
Definition cdc_log.h:148
static constexpr uint8_t MAX_ITEMS
static constexpr uint8_t VISIBLE_ITEMS
void onTick(uint32_t nowMs) override
Auto-dismisses the menu after a period of inactivity.
void render(bool partial) override
Renders the context menu popup.
InputResult onKey(char key) override
Handles key input for context menu navigation and actions.
void init(const char *title, const ContextMenuItem *items, uint8_t count)
Initializes context menu content and selection state.
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:53
void showModal(IView *modal)
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
void drawDialogFrame(Gdey029T94 *gfx, int x, int y, int w, int h)
Draws a framed dialog box with double border.
void printText(Adafruit_GFX *gfx, const char *text)
Draws CP437 text with the built-in 6x8 glyph font, byte-for-byte.
Centralized key-code constants for cdc_views.
Definition IModule.h:8
static constexpr char KEY_DOWN
Move selection down (numeric '8').
Definition KeyCodes.h:35
Gdey029T94 * display
static ContextMenuView s_sharedContextMenu
Convenience helper functions.
InputResult
Definition IView.h:11
static constexpr char KEY_NO
Cancel / Back / Backspace.
Definition KeyCodes.h:44
void hideContextMenu()
Hides the active context menu modal.
ContextMenuView * showContextMenu(const char *title, const ContextMenuItem *items, uint8_t count)
Shows the shared context menu instance as modal.
static const char * TAG
static constexpr char KEY_UP
Move selection up (numeric '2').
Definition KeyCodes.h:32
static constexpr char KEY_YES
Confirm / OK / Save.
Definition KeyCodes.h:41