CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
ListView.cpp
Go to the documentation of this file.
1
7
12#include "cdc_ui/ViewStack.h"
13#include "cdc_ui/I18n.h"
14#include "cdc_hal/IDisplay.h"
15#include "cdc_core/Raii.h"
16#include "cdc_log.h"
17#include <goodisplay/gdey029T94.h>
18
19static const char* TAG = "ListView";
20
27static constexpr int TITLE_Y = 5;
28static constexpr int LIST_START_Y = 30;
29static constexpr int ITEM_PADDING_X = 10;
31
38static constexpr uint8_t VISIBLE_ITEMS = 4;
39
40namespace cdc::ui {
41
49void ListView::init(const char* title, const ListItem* items, uint16_t count) {
50 title_ = title;
51 items_ = items;
52 itemCount_ = count > MAX_ITEMS ? MAX_ITEMS : count;
53
54 // Only reset position if not preserving (for back-navigation)
55 if (!preservePosition_) {
56 selection_ = 0;
57 scrollPos_ = 0;
58 } else {
59 preservePosition_ = false;
60 if (selection_ >= itemCount_) {
61 selection_ = itemCount_ > 0 ? itemCount_ - 1 : 0;
62 }
63 ensureVisible();
64 }
65 visibleItems_ = VISIBLE_ITEMS;
66 dirty_ = true;
67
68 LOG_D(TAG, "init: title='%s', items=%d, visible=%d", title, itemCount_, visibleItems_);
69}
70
76void ListView::setSelection(uint16_t index) {
77 if (index < itemCount_ && index != selection_) {
78 selection_ = index;
79 ensureVisible();
80 dirty_ = true;
81 }
82}
83
89 if (items_ && selection_ < itemCount_) {
90 return &items_[selection_];
91 }
92 return nullptr;
93}
94
100void ListView::navigate(bool down) {
101 if (itemCount_ == 0) return;
102
103 if (down) {
104 if (selection_ < itemCount_ - 1) {
105 selection_++;
106 } else {
107 // Wrap-around: bottom to top
108 selection_ = 0;
109 scrollPos_ = 0;
110 }
111 } else {
112 if (selection_ > 0) {
113 selection_--;
114 } else {
115 // Wrap-around: top to bottom
116 selection_ = itemCount_ - 1;
117 }
118 }
119
120 ensureVisible();
121 dirty_ = true;
122
123 LOG_D(TAG, "navigate: sel=%d, scroll=%d", selection_, scrollPos_);
124}
125
130void ListView::ensureVisible() {
131 if (selection_ >= scrollPos_ + visibleItems_) {
132 scrollPos_ = selection_ - visibleItems_ + 1;
133 }
134 if (selection_ < scrollPos_) {
135 scrollPos_ = selection_;
136 }
137}
138
145 switch (key) {
146 case KEY_UP: {
147 cdc::core::RecursiveMutexGuard guard(editMutex_);
148 navigate(false);
150 }
151
152 case KEY_DOWN: {
153 cdc::core::RecursiveMutexGuard guard(editMutex_);
154 navigate(true);
156 }
157
158 case KEY_YES: { // Select
159 // Snapshot the target under the lock; the callback may re-enter the
160 // call stack (plugin dispatch) and must not run while editMutex_ is
161 // held, so release before invoking it.
162 void* userData = nullptr;
163 uint16_t sel = 0;
164 bool valid = false;
165 {
166 cdc::core::RecursiveMutexGuard guard(editMutex_);
167 if (onSelect_ && items_ && selection_ < itemCount_) {
168 sel = selection_;
169 userData = items_[selection_].userData;
170 valid = true;
171 }
172 }
173 if (valid) {
174 onSelect_(sel, userData);
176 }
177 // No select callback bound: leave KEY_YES free for the plugin
178 // (delivered via the KEY_PRESSED EventBus) instead of swallowing it.
180 }
181
182 case KEY_MENU: { // Context menu
183 if (!onMenu_) return InputResult::IGNORED;
184 void* userData = nullptr;
185 uint16_t sel = 0xFFFF; // sentinel: empty list / no selection
186 {
187 cdc::core::RecursiveMutexGuard guard(editMutex_);
188 if (items_ && selection_ < itemCount_) {
189 sel = selection_;
190 userData = items_[selection_].userData;
191 }
192 }
193 onMenu_(sel, userData);
195 }
196
197 case KEY_NO: // Back
199
200 default:
202 }
203}
204
206 cdc::core::RecursiveMutexGuard guard(editMutex_);
207 if (itemCount_ == 0) return InputResult::IGNORED;
208 switch (key) {
210 case KEY_DOWN: setSelection(itemCount_ - 1); return InputResult::CONSUMED;
211 default: return InputResult::IGNORED;
212 }
213}
214
219const char* ListView::getFooterHint() const {
220 if (customFooter_) {
221 return customFooter_;
222 }
223 if (customHint_) {
224 return customHint_;
225 }
226 return ui::tr("core.hint_ok_back");
227}
228
234void ListView::render(bool partial) {
236 if (!display) return;
237
238 auto* gfx = static_cast<Gdey029T94*>(display->getNativeHandle());
239 if (!gfx) return;
240
241 // Serialise against a cross-task writer that may re-point items_ / free the
242 // backing buffer mid-render (plugin list edits). No-op when unset.
243 cdc::core::RecursiveMutexGuard guard(editMutex_);
244
245 const uint16_t width = display->getWidth();
246 const uint16_t height = display->getHeight();
247
248 if (!partial) {
249 gfx->fillScreen(EPD_WHITE);
250 }
251
252 gfx->setFont(nullptr); // 6x8 built-in (CP437): never inherit a leaked GFX font
253 gfx->setTextColor(EPD_BLACK);
254 gfx->setTextSize(1);
255 gfx->setTextWrap(false);
256
257 // Title + underline
258 render::drawHeaderLeft(gfx, title_, ITEM_PADDING_X, TITLE_Y, width);
259
260 // Items
261 const int rowWidth = width - SCROLL_INDICATOR_WIDTH;
262 for (uint8_t i = 0; i < visibleItems_; i++) {
263 uint16_t itemIndex = scrollPos_ + i;
264 int y = LIST_START_Y + i * itemHeight_;
265 drawRow(gfx, itemIndex, y, rowWidth);
266 }
267
268 // Empty placeholder (after item rects so it's not overpainted)
269 if (itemCount_ == 0 && emptyText_) {
270 int16_t x1, y1;
271 uint16_t w, h;
272 gfx->setTextColor(EPD_BLACK);
273 gfx->setTextSize(1);
274 gfx->getTextBounds(emptyText_, 0, 0, &x1, &y1, &w, &h);
275 int x = (width - static_cast<int>(w)) / 2;
276 int y = LIST_START_Y + (visibleItems_ * itemHeight_) / 2 - h / 2;
277 gfx->setCursor(x < 0 ? 0 : x, y);
278 render::printText(gfx, emptyText_);
279 }
280
281 // Scroll indicators
282 if (itemCount_ > visibleItems_) {
283 int indicatorX = width - SCROLL_INDICATOR_WIDTH;
284 int listHeight = visibleItems_ * itemHeight_;
285 render::drawScrollIndicator(gfx, indicatorX, LIST_START_Y, listHeight,
286 itemCount_, visibleItems_, scrollPos_);
287 }
288
289 // Footer with position counter
290 char positionStr[16];
291 const char* prefix = nullptr;
292 if (itemCount_ > 0) {
293 snprintf(positionStr, sizeof(positionStr), "%u/%u ", selection_ + 1, itemCount_);
294 prefix = positionStr;
295 }
296 const char* hint = getFooterHint();
297 render::drawFooterBar(gfx, width, height, prefix, hint, true);
298
299 dirty_ = false;
300}
301
310void ListView::drawRow(Gdey029T94* gfx, uint16_t itemIndex, int y, int rowWidth) {
311 gfx->setFont(nullptr);
312 gfx->setTextSize(1);
313 gfx->setTextWrap(false);
314
315 // Clear item area
316 gfx->fillRect(0, y, rowWidth, itemHeight_, EPD_WHITE);
317
318 if (itemIndex >= itemCount_) return;
319
320 const ListItem& item = items_[itemIndex];
321 bool isSelected = (itemIndex == selection_);
322
323 if (isSelected) {
324 gfx->fillRect(2, y + 1, rowWidth - 4, itemHeight_ - 2, EPD_BLACK);
325 gfx->setTextColor(EPD_WHITE);
326 } else {
327 gfx->setTextColor(EPD_BLACK);
328 }
329
330 bool handled = false;
331 if (itemRenderer_) {
332 handled = itemRenderer_(gfx, item, itemIndex,
333 0, y, rowWidth, itemHeight_,
334 isSelected, itemRendererCtx_);
335 }
336
337 if (!handled) {
338 int textX = ITEM_PADDING_X;
339 if (item.icon) {
340 char iconStr[2] = {static_cast<char>(item.icon), '\0'};
341 gfx->setCursor(textX, y + 4);
342 render::printText(gfx, iconStr);
343 if (item.iconDisabled) {
344 uint16_t color = isSelected ? EPD_WHITE : EPD_BLACK;
345 gfx->drawLine(textX, y + 10, textX + 6, y + 10, color);
346 }
347 textX += 10;
348 } else {
349 uint16_t bullet_color = isSelected ? EPD_WHITE : EPD_BLACK;
350 gfx->fillCircle(textX + 2, y + itemHeight_ / 2, 2, bullet_color);
351 textX += 9;
352 }
353
354 gfx->setCursor(textX, y + 4);
355 if (item.label) {
356 render::printTruncated(gfx, item.label, rowWidth - textX - 2);
357 }
358 }
359}
360
370void ListView::updateItem(uint16_t index) {
371 cdc::core::RecursiveMutexGuard guard(editMutex_);
372 if (!items_ || index >= itemCount_) return;
373 markDirty();
374}
375
383
389void ListView::insertItem(uint16_t index) {
390 cdc::core::RecursiveMutexGuard guard(editMutex_);
391 if (!items_ || itemCount_ >= MAX_ITEMS) return;
392 if (index > itemCount_) index = itemCount_;
393 uint16_t oldCount = itemCount_;
394 itemCount_++;
395 // Keep the selected item selected: it shifts down when inserting at or
396 // before it.
397 if (oldCount > 0 && index <= selection_ && selection_ + 1 < itemCount_) {
398 selection_++;
399 }
400 ensureVisible();
401 markDirty();
402}
403
409void ListView::removeItem(uint16_t index) {
410 cdc::core::RecursiveMutexGuard guard(editMutex_);
411 if (!items_ || itemCount_ == 0 || index >= itemCount_) return;
412 itemCount_--;
413 if (selection_ > index) {
414 selection_--;
415 }
416 if (selection_ >= itemCount_) {
417 selection_ = itemCount_ > 0 ? itemCount_ - 1 : 0;
418 }
419 ensureVisible();
420 markDirty();
421}
422
426
428
438ListView* showListView(const char* title, const ListItem* items, uint16_t count,
439 ListView::SelectCallback onSelect, const char* hint) {
440 s_sharedListView.init(title, items, count);
441 s_sharedListView.setOnSelect(onSelect);
442 if (hint) {
443 s_sharedListView.setHint(hint);
444 }
446 return &s_sharedListView;
447}
448
449} // namespace cdc::ui
static const char * TAG
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
constexpr int SCROLL_INDICATOR_WIDTH
Scroll indicator column width in pixels.
static constexpr uint8_t VISIBLE_ITEMS
Visible item count derived from available list area.
Definition ListView.cpp:38
static constexpr int LIST_START_Y
Definition ListView.cpp:28
static constexpr int ITEM_PADDING_X
Definition ListView.cpp:29
static constexpr int TITLE_Y
Display layout constants.
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_D(tag, fmt,...)
Definition cdc_log.h:148
Scoped guard for a FreeRTOS recursive mutex.
Definition Raii.h:245
void repaintPartial()
Requests a redraw; the actual repaint happens once per render cycle.
Definition ListView.cpp:380
const char * getFooterHint() const override
Returns footer hint text for this list.
Definition ListView.cpp:219
void(*)(uint16_t index, void *userData) SelectCallback
Definition ListView.h:46
const ListItem * getSelectedItem() const
Returns the currently selected item.
Definition ListView.cpp:88
static constexpr uint16_t MAX_ITEMS
Definition ListView.h:36
InputResult onKey(char key) override
Handles key input for list navigation and actions.
Definition ListView.cpp:144
void insertItem(uint16_t index)
Reflects a caller-side insertion at index and marks dirty.
Definition ListView.cpp:389
void setSelection(uint16_t index)
Sets the selected item index.
Definition ListView.cpp:76
void render(bool partial) override
Renders list rows, selection, scroll indicators, and footer.
Definition ListView.cpp:234
InputResult onLongPress(char key) override
Definition ListView.cpp:205
void init(const char *title, const ListItem *items, uint16_t count)
Initializes list data and selection state.
Definition ListView.cpp:49
void removeItem(uint16_t index)
Reflects a caller-side removal at index and marks dirty.
Definition ListView.cpp:409
void updateItem(uint16_t index)
Marks the list dirty after the caller updated a backing item.
Definition ListView.cpp:370
void markDirty() override
Definition IView.h:186
const char * customFooter_
Definition IView.h:203
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:34
void push(IView *view, void *context=nullptr)
bool valid
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
constexpr int SCROLL_INDICATOR_WIDTH
Scroll indicator column width in pixels.
void drawFooterBar(Gdey029T94 *gfx, uint16_t width, uint16_t height, const char *prefix, const char *hint, bool force=false)
Draws footer bar with optional prefix and hint text.
void drawHeaderLeft(Gdey029T94 *gfx, const char *title, int x, int y, uint16_t width, int underlineOffset=18)
void printTruncated(Gdey029T94 *gfx, const char *text, int maxWidthPx)
Print text at the current cursor, truncated with an ellipsis to fit maxWidthPx. Caller must have alre...
void printText(Gdey029T94 *gfx, const char *text)
Draws CP437 text with the built-in 6x8 glyph font, byte-for-byte.
void drawScrollIndicator(Gdey029T94 *gfx, int x, int y, int listHeight, uint16_t totalItems, uint16_t visibleItems, uint16_t scrollPos)
Draws scroll arrows and scrollbar thumb.
Centralized key-code constants for cdc_views.
Definition IModule.h:8
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:208
static ListView s_sharedListView
Convenience factory/helper function.
Definition ListView.cpp:427
static constexpr char KEY_MENU
Open context menu / digit '3'.
Definition KeyCodes.h:47
static constexpr char KEY_DOWN
Move selection down (numeric '8').
Definition KeyCodes.h:35
Gdey029T94 * display
InputResult
Definition IView.h:10
static constexpr char KEY_NO
Cancel / Back / Backspace.
Definition KeyCodes.h:44
static constexpr int TITLE_Y
Layout constants mirror the ones used by T9InputView.
ListView * showListView(const char *title, const ListItem *items, uint16_t count, ListView::SelectCallback onSelect, const char *hint=nullptr)
Shows a shared list view instance with selection callback.
Definition ListView.cpp:438
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
const char * label
Definition ListView.h:16