CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
ViewStack.cpp
Go to the documentation of this file.
1
13
14#include "cdc_ui/ViewStack.h"
15#include "cdc_core/EventBus.h"
16#include "cdc_hal/IDisplay.h"
17#include "cdc_log.h"
18#include <cstring>
19
20static const char* TAG = "ViewStack";
21
22namespace cdc::ui {
23
27static bool isListView(const IView* view) {
28 return view && (std::strcmp(view->getName(), "ListView") == 0);
29}
30
34ViewStack& ViewStack::instance() {
35 static ViewStack instance;
36 instance.ensureMutex();
37 return instance;
38}
39
43void ViewStack::ensureMutex() {
44 if (!mutex_) {
45 mutex_ = xSemaphoreCreateRecursiveMutex();
46 }
47}
48
49namespace {
50
52class StackLock {
53public:
54 explicit StackLock(SemaphoreHandle_t m) : m_(m) {
55 if (m_) xSemaphoreTakeRecursive(m_, portMAX_DELAY);
56 }
57 ~StackLock() {
58 if (m_) xSemaphoreGiveRecursive(m_);
59 }
60 StackLock(const StackLock&) = delete;
61 StackLock& operator=(const StackLock&) = delete;
62private:
63 SemaphoreHandle_t m_;
64};
65
66} // anonymous namespace
67
68// ---------------------------------------------------------------------------
69// Unlocked helpers. Caller MUST hold mutex_.
70// ---------------------------------------------------------------------------
71
72void ViewStack::push_unlocked(IView* view, void* context) {
73 if (!view) {
74 LOG_W(TAG, "Attempted to push null view");
75 return;
76 }
77 if (exclusiveOwner_ && exclusiveOwner_ != view) {
78 LOG_W(TAG, "push('%s') blocked: exclusive lock held by %p",
79 view->getName(), exclusiveOwner_);
80 return;
81 }
82 if (depth_ >= MAX_DEPTH) {
83 LOG_E(TAG, "ViewStack overflow (max %d)", MAX_DEPTH);
84 return;
85 }
86
87 if (depth_ > 0 && stack_[depth_ - 1]) {
88 stack_[depth_ - 1]->onPause(); // counterpart to onResume() on pop
89 }
90 stack_[depth_++] = view;
91 view->onEnter(context);
92 needsFullRefresh_ = !(isListView(view) && isListView(depth_ > 1 ? stack_[depth_ - 2] : nullptr));
93
94 LOG_D(TAG, "Pushed view '%s' (depth=%d)", view->getName(), depth_);
95}
96
97void ViewStack::pop_unlocked() {
98 if (depth_ <= 1) {
99 LOG_W(TAG, "Cannot pop root view");
100 return;
101 }
102
103 IView* top = stack_[depth_ - 1];
104 if (exclusiveOwner_ && exclusiveOwner_ != top) {
105 LOG_W(TAG, "pop blocked: exclusive lock held by %p (top='%s')",
106 exclusiveOwner_, top ? top->getName() : "(null)");
107 return;
108 }
109
110 depth_--;
111 if (top) {
112 top->onExit();
113 LOG_D(TAG, "Popped view '%s' (depth=%d)", top->getName(), depth_);
114 }
115 stack_[depth_] = nullptr;
116
117 if (depth_ > 0 && stack_[depth_ - 1]) {
118 stack_[depth_ - 1]->onResume();
119 }
120 needsFullRefresh_ = !(isListView(stack_[depth_ - 1]) && isListView(top));
121}
122
123void ViewStack::hideModal_unlocked() {
124 if (modalDepth_ == 0) return;
125
126 IView* top = modals_[--modalDepth_];
127 modals_[modalDepth_] = nullptr;
128 LOG_D(TAG, "Hiding modal '%s' (depth=%d)", top->getName(), modalDepth_);
129 top->onExit();
130
131 // The dismissed modal must be erased and whatever it covered repainted, so
132 // force a full composite of the base view plus any modals still beneath.
133 needsFullRefresh_ = true;
134 if (modalDepth_ > 0) {
135 modals_[modalDepth_ - 1]->markDirty();
136 } else {
137 IView* view = (depth_ == 0) ? nullptr : stack_[depth_ - 1];
138 if (view) {
139 view->onResume();
140 view->markDirty();
141 }
142 }
143}
144
145void ViewStack::removeModal_unlocked(IView* modal) {
146 if (!modal) return;
147 int idx = -1;
148 for (uint8_t i = 0; i < modalDepth_; ++i) {
149 if (modals_[i] == modal) { idx = i; break; }
150 }
151 if (idx < 0) return;
152
153 modal->onExit();
154 LOG_D(TAG, "Removing modal '%s' (depth=%d)", modal->getName(), modalDepth_ - 1);
155 for (uint8_t i = static_cast<uint8_t>(idx); i + 1 < modalDepth_; ++i) {
156 modals_[i] = modals_[i + 1];
157 }
158 modalDepth_--;
159 modals_[modalDepth_] = nullptr;
160
161 needsFullRefresh_ = true;
162 if (modalDepth_ > 0) {
163 modals_[modalDepth_ - 1]->markDirty();
164 } else {
165 IView* view = (depth_ == 0) ? nullptr : stack_[depth_ - 1];
166 if (view) {
167 view->onResume();
168 view->markDirty();
169 }
170 }
171}
172
173// ---------------------------------------------------------------------------
174// Public API. Each entry point acquires the mutex once.
175// ---------------------------------------------------------------------------
176
177void ViewStack::push(IView* view, void* context) {
178 StackLock lock(mutex_);
179 push_unlocked(view, context);
180}
181
183 StackLock lock(mutex_);
184 pop_unlocked();
185}
186
187void ViewStack::replace(IView* view, void* context) {
188 StackLock lock(mutex_);
189 if (!view) {
190 LOG_W(TAG, "Attempted to replace with null view");
191 return;
192 }
193 if (depth_ == 0) {
194 push_unlocked(view, context);
195 return;
196 }
197
198 IView* top = stack_[depth_ - 1];
199 if (exclusiveOwner_ && exclusiveOwner_ != view && exclusiveOwner_ != top) {
200 LOG_W(TAG, "replace('%s') blocked: exclusive lock held by %p",
201 view->getName(), exclusiveOwner_);
202 return;
203 }
204
205 if (top) {
206 top->onExit();
207 LOG_D(TAG, "Replaced view '%s'", top->getName());
208 }
209
210 stack_[depth_ - 1] = view;
211 view->onEnter(context);
212 needsFullRefresh_ = !(isListView(view) && isListView(top));
213
214 LOG_D(TAG, "Replaced with view '%s'", view->getName());
215}
216
218 StackLock lock(mutex_);
219 while (depth_ > 1) {
220 pop_unlocked();
221 }
222}
223
225 StackLock lock(mutex_);
226 while (depth_ > 1) {
227 IView* cur = stack_[depth_ - 1];
228 if (cur == anchor) break;
229 pop_unlocked();
230 }
231}
232
233void ViewStack::popToDepth(uint8_t targetDepth) {
234 StackLock lock(mutex_);
235 while (depth_ > targetDepth && depth_ > 1) {
236 pop_unlocked();
237 }
238}
239
241 StackLock lock(mutex_);
242 if (depth_ == 0) return nullptr;
243 return stack_[depth_ - 1];
244}
245
246IView* ViewStack::at(uint8_t idx) const {
247 StackLock lock(mutex_);
248 if (idx >= depth_) return nullptr;
249 return stack_[idx];
250}
251
254 static_cast<uint8_t>(key));
255 StackLock lock(mutex_);
257
258 if (modalDepth_ > 0) {
259 // Input stays on the top modal and never falls through to the view (or
260 // lower modals) behind it.
261 InputResult result = modals_[modalDepth_ - 1]->onKey(key);
262 if (result == InputResult::REQUEST_POP) {
263 hideModal_unlocked();
264 }
265 return;
266 }
267
268 IView* view = (depth_ == 0) ? nullptr : stack_[depth_ - 1];
269 if (view) {
270 InputResult result = view->onKey(key);
271 if (result == InputResult::REQUEST_POP) {
272 pop_unlocked();
273 }
274 }
275}
276
279 static_cast<uint8_t>(key));
280 StackLock lock(mutex_);
281
282 if (modalDepth_ > 0) {
283 InputResult result = modals_[modalDepth_ - 1]->onLongPress(key);
284 // 'N' is the universal back/cancel gesture: hide the top modal unless it
285 // consumed the press itself. Other keys hide only on REQUEST_POP.
286 if (result == InputResult::REQUEST_POP ||
287 (key == 'N' && result != InputResult::CONSUMED)) {
288 hideModal_unlocked();
289 }
290 return;
291 }
292
293 IView* view = (depth_ == 0) ? nullptr : stack_[depth_ - 1];
294 if (!view) {
295 return;
296 }
297 InputResult result = view->onLongPress(key);
298 // 'N' is the universal back/cancel gesture: pop unless the view consumed it
299 // (e.g. an input view that cancels itself and notifies its owner). Other
300 // keys pop only on an explicit REQUEST_POP.
301 if (result == InputResult::REQUEST_POP ||
302 (key == 'N' && result != InputResult::CONSUMED && depth_ > 1)) {
303 pop_unlocked();
304 }
305}
306
307void ViewStack::dispatchTick(uint32_t nowMs) {
308 StackLock lock(mutex_);
309 if (modalDepth_ > 0) {
310 modals_[modalDepth_ - 1]->onTick(nowMs);
311 }
312 IView* view = (depth_ == 0) ? nullptr : stack_[depth_ - 1];
313 if (view) {
314 view->onTick(nowMs);
315 }
316}
317
318void ViewStack::render(bool synchronous) {
319 StackLock lock(mutex_);
320 IView* view = (depth_ == 0) ? nullptr : stack_[depth_ - 1];
321 if (!view) {
322 return;
323 }
324
326
327 if (modalDepth_ > 0) {
328 // Modals own the screen and stack on top of the base view. Repaint the
329 // base first (when dirty or on a forced full refresh, e.g. after a modal
330 // was dismissed) and then draw every modal bottom-to-top in the same
331 // pass, so the composite stays correct and nothing of a gone modal lingers.
332 bool baseDirty = view->needsRender();
333 bool anyModalDirty = false;
334 for (uint8_t i = 0; i < modalDepth_; ++i) {
335 if (modals_[i]->needsRender()) { anyModalDirty = true; break; }
336 }
337 if (!baseDirty && !anyModalDirty && !needsFullRefresh_) {
338 return;
339 }
340 if (baseDirty || needsFullRefresh_) {
341 view->render(false);
342 view->clearDirty();
343 }
344 for (uint8_t i = 0; i < modalDepth_; ++i) {
345 modals_[i]->render(true);
346 modals_[i]->clearDirty();
347 }
348 if (display) {
349 // A dirty base under a still-visible modal repaints the composite in
350 // the framebuffer; PARTIAL suffices. FULL is reserved for modal
351 // stack changes (needsFullRefresh_) to erase a dismissed modal.
352 hal::RefreshMode mode = needsFullRefresh_
354 if (synchronous) display->flushSync(mode);
355 else display->flush(mode);
356 }
357 needsFullRefresh_ = false;
358 return;
359 }
360
361 if (!view->needsRender()) {
362 return;
363 }
364 view->render(false);
365 view->clearDirty();
366
367 hal::RefreshMode mode = needsFullRefresh_
370 if (display) {
371 if (synchronous) display->flushSync(mode);
372 else display->flush(mode);
373 }
374 needsFullRefresh_ = false;
375}
376
378 StackLock lock(mutex_);
379 for (uint8_t i = 0; i < modalDepth_; ++i) {
380 if (modals_[i]->needsRender()) return true;
381 }
382 IView* view = (depth_ == 0) ? nullptr : stack_[depth_ - 1];
383 return view && view->needsRender();
384}
385
387 StackLock lock(mutex_);
388 if (!modal) return;
389 if (exclusiveOwner_) {
390 LOG_W(TAG, "showModal('%s') blocked: exclusive lock held by %p",
391 modal->getName(), exclusiveOwner_);
392 return;
393 }
394
395 // If this modal is already stacked, lift it back to the top rather than
396 // duplicating it (the shared toast/confirm singletons get re-shown in place).
397 for (uint8_t i = 0; i < modalDepth_; ++i) {
398 if (modals_[i] == modal) {
399 for (uint8_t j = i + 1; j < modalDepth_; ++j) modals_[j - 1] = modals_[j];
400 modalDepth_--;
401 break;
402 }
403 }
404
405 // Stack full: drop the oldest modal at the bottom to make room.
406 if (modalDepth_ >= MAX_MODAL_DEPTH) {
407 modals_[0]->onExit();
408 for (uint8_t i = 1; i < modalDepth_; ++i) modals_[i - 1] = modals_[i];
409 modalDepth_--;
410 }
411
412 // Stacking on top of an existing modal needs a full composite so a smaller
413 // new modal does not leave the previous one's edges showing around it. The
414 // first modal over the base view stays a partial (no toast-refresh regress).
415 if (modalDepth_ > 0) needsFullRefresh_ = true;
416
417 if (modalDepth_ > 0) {
418 modals_[modalDepth_ - 1]->onPause();
419 } else {
420 IView* base = (depth_ == 0) ? nullptr : stack_[depth_ - 1];
421 if (base) base->onPause(); // counterpart to onResume() in hideModal
422 }
423 modals_[modalDepth_++] = modal;
424 modal->onEnter(nullptr);
425 LOG_D(TAG, "Showing modal '%s' (depth=%d)", modal->getName(), modalDepth_);
426}
427
429 StackLock lock(mutex_);
430 hideModal_unlocked();
431}
432
434 StackLock lock(mutex_);
435 removeModal_unlocked(modal);
436}
437
438void ViewStack::setInactivityTimeout(InactivityCallback callback, uint32_t timeoutMs) {
439 StackLock lock(mutex_);
440 inactivityCallback_ = callback;
441 inactivityTimeoutMs_ = timeoutMs;
442 lastActivityMs_ = 0;
443 LOG_D(TAG, "Inactivity timeout set: %lu ms", timeoutMs);
444}
445
447 StackLock lock(mutex_);
448 lastActivityMs_ = 0;
449}
450
451void ViewStack::checkInactivity(uint32_t nowMs) {
452 InactivityCallback cb = nullptr;
453 bool triggered = false;
454 uint32_t elapsedForLog = 0;
455
456 {
457 StackLock lock(mutex_);
458 if (inactivityTimeoutMs_ == 0 || !inactivityCallback_) {
459 return;
460 }
461 if (lastActivityMs_ == 0) {
462 lastActivityMs_ = nowMs;
463 return;
464 }
465 uint32_t elapsed = nowMs - lastActivityMs_;
466 if (elapsed >= inactivityTimeoutMs_) {
467 cb = inactivityCallback_;
468 triggered = true;
469 elapsedForLog = elapsed;
470 lastActivityMs_ = nowMs;
471 }
472 }
473
474 if (cb && triggered) {
475 LOG_I(TAG, "Inactivity timeout triggered after %lu ms", elapsedForLog);
476 cb();
477 }
478}
479
480bool ViewStack::acquireExclusive(const void* owner) {
481 StackLock lock(mutex_);
482 if (!owner) {
483 LOG_W(TAG, "acquireExclusive called with null owner");
484 return false;
485 }
486 if (exclusiveOwner_ && exclusiveOwner_ != owner) {
487 LOG_W(TAG, "Exclusive lock already held by %p, refusing %p", exclusiveOwner_, owner);
488 return false;
489 }
490 exclusiveOwner_ = owner;
491 LOG_D(TAG, "Exclusive lock acquired by %p", owner);
492 return true;
493}
494
495bool ViewStack::releaseExclusive(const void* owner) {
496 StackLock lock(mutex_);
497 if (!exclusiveOwner_) {
498 return false;
499 }
500 if (exclusiveOwner_ != owner) {
501 LOG_W(TAG, "releaseExclusive: owner mismatch (held=%p, caller=%p)",
502 exclusiveOwner_, owner);
503 return false;
504 }
505 LOG_D(TAG, "Exclusive lock released by %p", owner);
506 exclusiveOwner_ = nullptr;
507 return true;
508}
509
510} // namespace cdc::ui
static const char * TAG
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_D(tag, fmt,...)
Definition cdc_log.h:148
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
static EventBus & instance()
Returns singleton event-bus instance.
Definition EventBus.cpp:19
bool publish(const Event &event, bool fromISR=false)
Publishes an event to the queue.
Definition EventBus.cpp:88
virtual InputResult onLongPress(char key)
Definition IView.h:101
virtual void clearDirty()=0
virtual const char * getName() const =0
virtual InputResult onKey(char key)=0
virtual void onTick(uint32_t nowMs)
Definition IView.h:109
virtual void onExit()=0
virtual void onEnter(void *context=nullptr)=0
virtual void onPause()
Definition IView.h:51
virtual void render(bool partial)=0
virtual bool needsRender() const =0
virtual bool prefersLightRefresh() const
Definition IView.h:85
void popToDepth(uint8_t targetDepth)
Pops views until the stack depth is at most targetDepth.
void setInactivityTimeout(InactivityCallback callback, uint32_t timeoutMs)
IView * current() const
void dispatchLongPress(char key)
void replace(IView *view, void *context=nullptr)
void dispatchKey(char key)
void dispatchTick(uint32_t nowMs)
void render(bool synchronous=false)
Render current view (and modal if present) and flush to display.
bool needsRender() const
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:34
void removeModal(IView *modal)
Remove a specific modal from any position in the modal stack.
void showModal(IView *modal)
bool releaseExclusive(const void *owner)
Releases exclusive ownership.
static constexpr uint8_t MAX_DEPTH
Definition ViewStack.h:20
void(*)() InactivityCallback
Definition ViewStack.h:196
bool acquireExclusive(const void *owner)
Acquires exclusive ownership of the view stack.
void checkInactivity(uint32_t nowMs)
void popToAnchor(IView *anchor)
Pops views until the specified anchor view is the current view.
void push(IView *view, void *context=nullptr)
IView * at(uint8_t depth) const
void resetInactivityTimer()
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
Centralized key-code constants for cdc_views.
Definition IModule.h:8
static bool isListView(const IView *view)
Checks whether a view is a ListView by runtime name.
Definition ViewStack.cpp:27
Gdey029T94 * display
InputResult
Definition IView.h:10
static const char * TAG