20static const char*
TAG =
"ViewStack";
28 return view && (std::strcmp(view->
getName(),
"ListView") == 0);
43void ViewStack::ensureMutex() {
45 mutex_ = xSemaphoreCreateRecursiveMutex();
54 explicit StackLock(SemaphoreHandle_t m) : m_(m) {
55 if (m_) xSemaphoreTakeRecursive(m_, portMAX_DELAY);
58 if (m_) xSemaphoreGiveRecursive(m_);
60 StackLock(
const StackLock&) =
delete;
61 StackLock& operator=(
const StackLock&) =
delete;
72void ViewStack::push_unlocked(
IView* view,
void* context) {
74 LOG_W(
TAG,
"Attempted to push null view");
77 if (exclusiveOwner_ && exclusiveOwner_ != view) {
78 LOG_W(
TAG,
"push('%s') blocked: exclusive lock held by %p",
79 view->getName(), exclusiveOwner_);
87 if (depth_ > 0 && stack_[depth_ - 1]) {
88 stack_[depth_ - 1]->onPause();
90 stack_[depth_++] = view;
91 view->onEnter(context);
92 needsFullRefresh_ = !(
isListView(view) &&
isListView(depth_ > 1 ? stack_[depth_ - 2] :
nullptr));
94 LOG_D(
TAG,
"Pushed view '%s' (depth=%d)", view->getName(), depth_);
97void ViewStack::pop_unlocked() {
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)");
113 LOG_D(
TAG,
"Popped view '%s' (depth=%d)", top->getName(), depth_);
115 stack_[depth_] =
nullptr;
117 if (depth_ > 0 && stack_[depth_ - 1]) {
118 stack_[depth_ - 1]->onResume();
123void ViewStack::hideModal_unlocked() {
124 if (modalDepth_ == 0)
return;
126 IView* top = modals_[--modalDepth_];
127 modals_[modalDepth_] =
nullptr;
128 LOG_D(
TAG,
"Hiding modal '%s' (depth=%d)", top->getName(), modalDepth_);
133 needsFullRefresh_ =
true;
134 if (modalDepth_ > 0) {
135 modals_[modalDepth_ - 1]->markDirty();
137 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
145void ViewStack::removeModal_unlocked(
IView* modal) {
148 for (uint8_t i = 0; i < modalDepth_; ++i) {
149 if (modals_[i] == modal) { idx = i;
break; }
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];
159 modals_[modalDepth_] =
nullptr;
161 needsFullRefresh_ =
true;
162 if (modalDepth_ > 0) {
163 modals_[modalDepth_ - 1]->markDirty();
165 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
178 StackLock lock(mutex_);
179 push_unlocked(view, context);
183 StackLock lock(mutex_);
188 StackLock lock(mutex_);
190 LOG_W(
TAG,
"Attempted to replace with null view");
194 push_unlocked(view, context);
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_);
210 stack_[depth_ - 1] = view;
218 StackLock lock(mutex_);
225 StackLock lock(mutex_);
227 IView* cur = stack_[depth_ - 1];
228 if (cur == anchor)
break;
234 StackLock lock(mutex_);
235 while (depth_ > targetDepth && depth_ > 1) {
241 StackLock lock(mutex_);
242 if (depth_ == 0)
return nullptr;
243 return stack_[depth_ - 1];
247 StackLock lock(mutex_);
248 if (idx >= depth_)
return nullptr;
254 static_cast<uint8_t
>(key));
255 StackLock lock(mutex_);
258 if (modalDepth_ > 0) {
261 InputResult result = modals_[modalDepth_ - 1]->onKey(key);
263 hideModal_unlocked();
268 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
279 static_cast<uint8_t
>(key));
280 StackLock lock(mutex_);
282 if (modalDepth_ > 0) {
283 InputResult result = modals_[modalDepth_ - 1]->onLongPress(key);
288 hideModal_unlocked();
293 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
308 StackLock lock(mutex_);
309 if (modalDepth_ > 0) {
310 modals_[modalDepth_ - 1]->onTick(nowMs);
312 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
319 StackLock lock(mutex_);
320 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
327 if (modalDepth_ > 0) {
333 bool anyModalDirty =
false;
334 for (uint8_t i = 0; i < modalDepth_; ++i) {
335 if (modals_[i]->
needsRender()) { anyModalDirty =
true;
break; }
337 if (!baseDirty && !anyModalDirty && !needsFullRefresh_) {
340 if (baseDirty || needsFullRefresh_) {
344 for (uint8_t i = 0; i < modalDepth_; ++i) {
345 modals_[i]->render(
true);
346 modals_[i]->clearDirty();
354 if (synchronous)
display->flushSync(mode);
357 needsFullRefresh_ =
false;
371 if (synchronous)
display->flushSync(mode);
374 needsFullRefresh_ =
false;
378 StackLock lock(mutex_);
379 for (uint8_t i = 0; i < modalDepth_; ++i) {
382 IView* view = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
387 StackLock lock(mutex_);
389 if (exclusiveOwner_) {
390 LOG_W(
TAG,
"showModal('%s') blocked: exclusive lock held by %p",
391 modal->
getName(), exclusiveOwner_);
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];
406 if (modalDepth_ >= MAX_MODAL_DEPTH) {
407 modals_[0]->onExit();
408 for (uint8_t i = 1; i < modalDepth_; ++i) modals_[i - 1] = modals_[i];
415 if (modalDepth_ > 0) needsFullRefresh_ =
true;
417 if (modalDepth_ > 0) {
418 modals_[modalDepth_ - 1]->onPause();
420 IView* base = (depth_ == 0) ?
nullptr : stack_[depth_ - 1];
423 modals_[modalDepth_++] = modal;
425 LOG_D(
TAG,
"Showing modal '%s' (depth=%d)", modal->
getName(), modalDepth_);
429 StackLock lock(mutex_);
430 hideModal_unlocked();
434 StackLock lock(mutex_);
435 removeModal_unlocked(modal);
439 StackLock lock(mutex_);
440 inactivityCallback_ = callback;
441 inactivityTimeoutMs_ = timeoutMs;
443 LOG_D(
TAG,
"Inactivity timeout set: %lu ms", timeoutMs);
447 StackLock lock(mutex_);
453 bool triggered =
false;
454 uint32_t elapsedForLog = 0;
457 StackLock lock(mutex_);
458 if (inactivityTimeoutMs_ == 0 || !inactivityCallback_) {
461 if (lastActivityMs_ == 0) {
462 lastActivityMs_ = nowMs;
465 uint32_t elapsed = nowMs - lastActivityMs_;
466 if (elapsed >= inactivityTimeoutMs_) {
467 cb = inactivityCallback_;
469 elapsedForLog = elapsed;
470 lastActivityMs_ = nowMs;
474 if (cb && triggered) {
475 LOG_I(
TAG,
"Inactivity timeout triggered after %lu ms", elapsedForLog);
481 StackLock lock(mutex_);
483 LOG_W(
TAG,
"acquireExclusive called with null owner");
486 if (exclusiveOwner_ && exclusiveOwner_ != owner) {
487 LOG_W(
TAG,
"Exclusive lock already held by %p, refusing %p", exclusiveOwner_, owner);
490 exclusiveOwner_ = owner;
491 LOG_D(
TAG,
"Exclusive lock acquired by %p", owner);
496 StackLock lock(mutex_);
497 if (!exclusiveOwner_) {
500 if (exclusiveOwner_ != owner) {
501 LOG_W(
TAG,
"releaseExclusive: owner mismatch (held=%p, caller=%p)",
502 exclusiveOwner_, owner);
505 LOG_D(
TAG,
"Exclusive lock released by %p", owner);
506 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 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 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.
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(*)() InactivityCallback
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.
static bool isListView(const IView *view)
Checks whether a view is a ListView by runtime name.