19static const char*
TAG =
"ViewStack";
30 return static_cast<uint8_t
>(a) <
static_cast<uint8_t
>(b) ? a : b;
44 constexpr uint16_t kBlack = 0x0000;
62void ViewStack::ensureMutex() {
64 mutex_ = xSemaphoreCreateRecursiveMutex();
73 explicit StackLock(SemaphoreHandle_t m) : m_(m) {
74 if (m_) xSemaphoreTakeRecursive(m_, portMAX_DELAY);
77 if (m_) xSemaphoreGiveRecursive(m_);
79 StackLock(
const StackLock&) =
delete;
80 StackLock& operator=(
const StackLock&) =
delete;
95void ViewStack::push_unlocked(
IView* view,
void* context) {
97 LOG_W(
TAG,
"Attempted to push null view");
100 if (exclusiveOwner_ && exclusiveOwner_ != view) {
101 LOG_W(
TAG,
"push('%s') blocked: exclusive lock held by %p",
102 view->getName(), exclusiveOwner_);
110 if (depth_ > 0 && stack_[depth_ - 1]) {
111 stack_[depth_ - 1]->onPause();
113 stack_[depth_++] = view;
114 view->onEnter(context);
117 escalatePending_unlocked(view->preferredEnterRefresh());
119 LOG_D(
TAG,
"Pushed view '%s' (depth=%d)", view->getName(), depth_);
122void ViewStack::pop_unlocked() {
128 IView* top = stack_[depth_ - 1];
129 if (exclusiveOwner_ && exclusiveOwner_ != top) {
130 LOG_W(
TAG,
"pop blocked: exclusive lock held by %p (top='%s')",
131 exclusiveOwner_, top ? top->getName() :
"(null)");
138 LOG_D(
TAG,
"Popped view '%s' (depth=%d)", top->getName(), depth_);
140 stack_[depth_] =
nullptr;
142 if (depth_ > 0 && stack_[depth_ - 1]) {
143 stack_[depth_ - 1]->onResume();
145 escalatePending_unlocked(stack_[depth_ - 1]->preferredEnterRefresh());
149void ViewStack::hideModal_unlocked() {
150 if (modalDepth_ == 0)
return;
152 IView* top = modals_[--modalDepth_];
153 modals_[modalDepth_] =
nullptr;
154 LOG_D(
TAG,
"Hiding modal '%s' (depth=%d)", top->getName(), modalDepth_);
161 needsCompositeRepaint_ =
true;
162 if (modalDepth_ > 0) {
163 modals_[modalDepth_ - 1]->markDirty();
165 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
169 escalatePending_unlocked(view->preferredEnterRefresh());
174void ViewStack::removeModal_unlocked(
IView* modal) {
177 for (uint8_t i = 0; i < modalDepth_; ++i) {
178 if (modals_[i] == modal) { idx = i;
break; }
183 LOG_D(
TAG,
"Removing modal '%s' (depth=%d)", modal->getName(), modalDepth_ - 1);
184 for (uint8_t i =
static_cast<uint8_t
>(idx); i + 1 < modalDepth_; ++i) {
185 modals_[i] = modals_[i + 1];
188 modals_[modalDepth_] =
nullptr;
190 needsCompositeRepaint_ =
true;
191 if (modalDepth_ > 0) {
192 modals_[modalDepth_ - 1]->markDirty();
194 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
198 escalatePending_unlocked(view->preferredEnterRefresh());
208 StackLock lock(mutex_);
209 push_unlocked(view, context);
213 StackLock lock(mutex_);
218 StackLock lock(mutex_);
220 LOG_W(
TAG,
"Attempted to replace with null view");
224 push_unlocked(view, context);
228 IView* top = stack_[depth_ - 1];
229 if (exclusiveOwner_ && exclusiveOwner_ != view && exclusiveOwner_ != top) {
230 LOG_W(
TAG,
"replace('%s') blocked: exclusive lock held by %p",
231 view->
getName(), exclusiveOwner_);
240 stack_[depth_ - 1] = view;
248 StackLock lock(mutex_);
255 StackLock lock(mutex_);
257 IView* cur = stack_[depth_ - 1];
258 if (cur == anchor)
break;
264 StackLock lock(mutex_);
265 while (depth_ > targetDepth && depth_ > 1) {
271 StackLock lock(mutex_);
272 if (depth_ == 0)
return nullptr;
273 return stack_[depth_ - 1];
277 StackLock lock(mutex_);
278 if (idx >= depth_)
return nullptr;
284 static_cast<uint8_t
>(key));
285 StackLock lock(mutex_);
288 if (modalDepth_ > 0) {
291 InputResult result = modals_[modalDepth_ - 1]->onKey(key);
293 hideModal_unlocked();
298 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
309 static_cast<uint8_t
>(key));
310 StackLock lock(mutex_);
312 if (modalDepth_ > 0) {
313 InputResult result = modals_[modalDepth_ - 1]->onLongPress(key);
318 hideModal_unlocked();
323 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
339 StackLock lock(mutex_);
340 if (modalDepth_ > 0) {
341 modals_[modalDepth_ - 1]->onTick(nowMs);
343 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
350 StackLock lock(mutex_);
351 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
358 if (modalDepth_ > 0) {
364 bool anyModalDirty =
false;
365 for (uint8_t i = 0; i < modalDepth_; ++i) {
366 if (modals_[i]->
needsRender()) { anyModalDirty =
true;
break; }
368 if (!baseDirty && !anyModalDirty && !needsCompositeRepaint_) {
371 if (baseDirty || needsCompositeRepaint_) {
376 for (uint8_t i = 0; i < modalDepth_; ++i) {
378 modals_[i]->render(
true);
379 modals_[i]->clearDirty();
385 if (synchronous)
display->flushSync(mode);
388 needsCompositeRepaint_ =
false;
404 if (synchronous)
display->flushSync(mode);
411 StackLock lock(mutex_);
412 for (uint8_t i = 0; i < modalDepth_; ++i) {
415 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
420 StackLock lock(mutex_);
422 if (exclusiveOwner_) {
423 LOG_W(
TAG,
"showModal('%s') blocked: exclusive lock held by %p",
424 modal->
getName(), exclusiveOwner_);
430 for (uint8_t i = 0; i < modalDepth_; ++i) {
431 if (modals_[i] == modal) {
432 for (uint8_t j = i + 1; j < modalDepth_; ++j) modals_[j - 1] = modals_[j];
439 if (modalDepth_ >= MAX_MODAL_DEPTH) {
440 modals_[0]->onExit();
441 for (uint8_t i = 1; i < modalDepth_; ++i) modals_[i - 1] = modals_[i];
448 if (modalDepth_ > 0) needsCompositeRepaint_ =
true;
450 if (modalDepth_ > 0) {
451 modals_[modalDepth_ - 1]->onPause();
453 IView* base = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
456 modals_[modalDepth_++] = modal;
458 LOG_D(
TAG,
"Showing modal '%s' (depth=%d)", modal->
getName(), modalDepth_);
462 StackLock lock(mutex_);
463 hideModal_unlocked();
467 StackLock lock(mutex_);
468 removeModal_unlocked(modal);
472 StackLock lock(mutex_);
473 escalatePending_unlocked(mode);
476 if (modalDepth_ > 0) {
477 modals_[modalDepth_ - 1]->markDirty();
478 }
else if (depth_ > 0 && stack_[depth_ - 1]) {
479 stack_[depth_ - 1]->markDirty();
484 StackLock lock(mutex_);
485 inactivityCallback_ = callback;
486 inactivityTimeoutMs_ = timeoutMs;
488 LOG_D(
TAG,
"Inactivity timeout set: %lu ms", timeoutMs);
492 StackLock lock(mutex_);
498 bool triggered =
false;
499 uint32_t elapsedForLog = 0;
502 StackLock lock(mutex_);
503 if (inactivityTimeoutMs_ == 0 || !inactivityCallback_) {
506 if (lastActivityMs_ == 0) {
507 lastActivityMs_ = nowMs;
510 uint32_t elapsed = nowMs - lastActivityMs_;
511 if (elapsed >= inactivityTimeoutMs_) {
512 cb = inactivityCallback_;
514 elapsedForLog = elapsed;
515 lastActivityMs_ = nowMs;
519 if (cb && triggered) {
520 LOG_I(
TAG,
"Inactivity timeout triggered after %lu ms", elapsedForLog);
526 StackLock lock(mutex_);
528 LOG_W(
TAG,
"acquireExclusive called with null owner");
531 if (exclusiveOwner_ && exclusiveOwner_ != owner) {
532 LOG_W(
TAG,
"Exclusive lock already held by %p, refusing %p", exclusiveOwner_, owner);
535 exclusiveOwner_ = owner;
536 LOG_D(
TAG,
"Exclusive lock acquired by %p", owner);
541 StackLock lock(mutex_);
542 if (!exclusiveOwner_) {
545 if (exclusiveOwner_ != owner) {
546 LOG_W(
TAG,
"releaseExclusive: owner mismatch (held=%p, caller=%p)",
547 exclusiveOwner_, owner);
550 LOG_D(
TAG,
"Exclusive lock released by %p", owner);
551 exclusiveOwner_ =
nullptr;
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
#define LOG_D(tag, fmt,...)
#define LOG_I(tag, fmt,...)
#define LOG_E(tag, fmt,...)
static EventBus & instance()
Returns singleton event-bus instance.
bool publish(const Event &event, bool fromISR=false)
Publishes an event to the queue.
virtual InputResult onLongPress(char key)
virtual hal::RefreshMode preferredEnterRefresh() const
virtual void clearDirty()=0
virtual const char * getName() const =0
virtual InputResult onKey(char key)=0
virtual void onTick(uint32_t nowMs)
virtual void onEnter(void *context=nullptr)=0
virtual void render(bool partial)=0
virtual bool needsRender() const =0
virtual bool prefersLightRefresh() const
void popToDepth(uint8_t targetDepth)
Pops views until the stack depth is at most targetDepth.
void setInactivityTimeout(InactivityCallback callback, uint32_t timeoutMs)
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.
static ViewStack & instance()
Returns singleton view-stack instance.
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
void forceRefresh(hal::RefreshMode mode)
Escalates the refresh mode of the next render.
void(*)() InactivityCallback
bool acquireExclusive(const void *owner)
Acquires exclusive ownership of the view stack.
InputResult dispatchLongPress(char key)
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.
static hal::RefreshMode strongerRefresh(hal::RefreshMode a, hal::RefreshMode b)
Returns the stronger of two refresh modes.
static void resetTextState(hal::IDisplay *display)
Reset the shared GFX text state to the defaults before a view renders.