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 gfx->setTextSize(1);
174
175 // Find widest label
176 for (uint8_t i = 0; i < itemCount_; i++) {
177 if (items_[i].label) {
178 uint16_t w, h;
179 gfx->getTextBounds(items_[i].label, 0, 0, &x1, &y1, &w, &h);
180 if (w > maxLabelWidth) maxLabelWidth = w;
181 }
182 }
183
184 // Also check title width
185 if (title_) {
186 uint16_t w, h;
187 gfx->getTextBounds(title_, 0, 0, &x1, &y1, &w, &h);
188 if (w > maxLabelWidth) maxLabelWidth = w;
189 }
190
191 uint16_t boxWidth = maxLabelWidth + BOX_PADDING * 2 + 10;
192 if (boxWidth < MIN_BOX_WIDTH) boxWidth = MIN_BOX_WIDTH;
193 if (boxWidth > MAX_BOX_WIDTH) boxWidth = MAX_BOX_WIDTH;
194
195 uint8_t visibleCount = std::min(itemCount_, VISIBLE_ITEMS);
196 uint16_t boxHeight = TITLE_HEIGHT + (visibleCount * ITEM_HEIGHT) + BOX_PADDING * 2;
197
198 // Center the box
199 int boxX = (screenWidth - boxWidth) / 2;
200 int boxY = (screenHeight - boxHeight) / 2;
201
202 // Draw box background
203 render::drawDialogFrame(gfx, boxX, boxY, boxWidth, boxHeight);
204
205 // Draw title
206 gfx->setTextColor(EPD_WHITE);
207 gfx->fillRect(boxX + 2, boxY + 2, boxWidth - 4, TITLE_HEIGHT, EPD_BLACK);
208 gfx->setCursor(boxX + BOX_PADDING, boxY + 4);
209 if (title_) {
210 render::printText(gfx, title_);
211 }
212
213 // Draw items
214 int itemY = boxY + TITLE_HEIGHT + BOX_PADDING;
215 for (uint8_t i = 0; i < visibleCount; i++) {
216 uint8_t itemIndex = scrollPos_ + i;
217 if (itemIndex >= itemCount_) break;
218
219 const ContextMenuItem& item = items_[itemIndex];
220
221 // Clear item area
222 gfx->fillRect(boxX + BOX_PADDING - 2, itemY, boxWidth - BOX_PADDING * 2 + 4, ITEM_HEIGHT, EPD_WHITE);
223
224 // Highlight selected
225 if (itemIndex == selection_) {
226 gfx->fillRect(boxX + BOX_PADDING - 2, itemY, boxWidth - BOX_PADDING * 2 + 4, ITEM_HEIGHT - 1, EPD_BLACK);
227 gfx->setTextColor(EPD_WHITE);
228 } else {
229 gfx->setTextColor(EPD_BLACK);
230 }
231
232 gfx->setCursor(boxX + BOX_PADDING, itemY + 2);
233 if (item.label) {
234 render::printText(gfx, item.label);
235 }
236
237 itemY += ITEM_HEIGHT;
238 }
239
240 // Scroll indicators if needed
241 if (itemCount_ > VISIBLE_ITEMS) {
242 int indicatorX = boxX + boxWidth - BOX_PADDING;
243 int indicatorY = boxY + TITLE_HEIGHT + BOX_PADDING;
244
245 // Up arrow
246 if (scrollPos_ > 0) {
247 gfx->fillTriangle(
248 indicatorX, indicatorY + 6,
249 indicatorX - 3, indicatorY + 2,
250 indicatorX + 3, indicatorY + 2,
251 EPD_BLACK
252 );
253 }
254
255 // Down arrow
256 if (scrollPos_ + VISIBLE_ITEMS < itemCount_) {
257 int arrowY = boxY + boxHeight - BOX_PADDING - 8;
258 gfx->fillTriangle(
259 indicatorX, arrowY,
260 indicatorX - 3, arrowY + 4,
261 indicatorX + 3, arrowY + 4,
262 EPD_BLACK
263 );
264 }
265 }
266
267 dirty_ = false;
268}
269
273
275
283ContextMenuView* showContextMenu(const char* title, const ContextMenuItem* items, uint8_t count) {
284 s_sharedContextMenu.init(title, items, count);
286 return &s_sharedContextMenu;
287}
288
296
297} // 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:34
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(Gdey029T94 *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:10
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