mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-08-17 23:55:04 +00:00
- Auto-select widgets in Launcher and apps with toolbars on devices without touch. - Improved USB HID input reliability, cleanup - Updated PSRAM settings to improve boot stability on supported devices. - Prevented duplicate Wi-Fi event subscriptions during screen rebuilds. - Updated docs - Fixes in WifiManage and WifiConnect - Reduced main task stack size - Moved USB HID stack size to PSRAM when available - app_manager_find_manifest() now returns a copy instead of a pointer
516 lines
19 KiB
C++
516 lines
19 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
#include <lvgl_window_manager/window_manager.h>
|
|
|
|
#include <app/instance.h>
|
|
|
|
#include <lvgl/lvgl.h>
|
|
|
|
#include <tactility/check.h>
|
|
#include <tactility/concurrent/mutex.h>
|
|
#include <tactility/freertos/semphr.h>
|
|
|
|
#include <algorithm>
|
|
#include <new>
|
|
#include <vector>
|
|
|
|
constexpr auto* TAG = "window_manager";
|
|
|
|
namespace {
|
|
|
|
/**
|
|
* Completion signal for a single window_manager_await_state_change() call.
|
|
*
|
|
* Heap-allocated with its own refcount, protected by WindowManagerState::mutex (not atomic).
|
|
* It can't be owned solely by the WindowRecord: window_manager_create()/remove() claim
|
|
* (read + clear) a window's signal under the lock, then give it after releasing that lock.
|
|
* The refcount lets whichever side finishes last - the waiting task waking up, or the
|
|
* claimer after giving the semaphore - safely delete it.
|
|
*/
|
|
struct WindowWaitSignal {
|
|
SemaphoreHandle_t semaphore;
|
|
/** Starts at 1, owned by window_manager_await_state_change() until it's done waiting.
|
|
* Whoever claims this signal from a WindowRecord (see claim_waiter_locked()) takes an
|
|
* extra reference for as long as it takes to give the semaphore. Reaching 0 deletes it. */
|
|
int refcount = 1;
|
|
};
|
|
|
|
struct WindowRecord {
|
|
WindowId id;
|
|
uint32_t app_instance_id;
|
|
WindowCreateWidgetsFn create_widgets;
|
|
WindowDestroyWidgetsFn destroy_widgets;
|
|
void* user_data;
|
|
|
|
/** Set by window_manager_await_state_change() when a task is blocked waiting on this
|
|
* window (see that function's @warning: at most one concurrent awaiter per window).
|
|
* Per-window rather than a single manager-wide slot, because a stacked window manager
|
|
* serving several app tasks can have more than one window with a live await() call
|
|
* outstanding, even though only one is ever topmost/GRANTED at a time. */
|
|
WindowWaitSignal* waiting_signal = nullptr;
|
|
};
|
|
|
|
struct WindowManagerState {
|
|
/** Mutex for read/write operations. Shortly held. */
|
|
Mutex mutex {};
|
|
|
|
/** Serializes the full start()/stop()/create()/remove() transitions against each other,
|
|
* including LVGL work done after `mutex` is released, such as a create_widgets() or
|
|
* screen_init() callback. Without it, window_manager_stop() could free
|
|
* real_root_widget/content_root_widget/top_widget out from under a concurrent create() or
|
|
* remove() that captured one of those pointers under `mutex` but only uses it afterward,
|
|
* via build_window_widget()/delete_widget(). */
|
|
Mutex lifecycle_mutex {};
|
|
|
|
bool started = false;
|
|
WindowManagerScreenInitFn screen_init = nullptr;
|
|
|
|
/** The raw, full-size container window_manager_start() creates; owns (and deletion
|
|
* cascades to) whatever the screen-init callback added under it. */
|
|
lv_obj_t* real_root_widget = nullptr;
|
|
/** The stable parent each window's own widget is created under. Normally
|
|
* real_root_widget itself, but the screen-init callback may return a nested content
|
|
* widget to use instead. */
|
|
lv_obj_t* content_root_widget = nullptr;
|
|
|
|
WindowId next_id = 1;
|
|
/** windows.back() is topmost; only it ever has a live widget (top_widget). */
|
|
std::vector<WindowRecord> windows;
|
|
lv_obj_t* top_widget = nullptr;
|
|
|
|
WindowManagerState() {
|
|
mutex_construct(&mutex);
|
|
mutex_construct(&lifecycle_mutex);
|
|
}
|
|
};
|
|
|
|
WindowManagerState& state() {
|
|
static WindowManagerState instance;
|
|
return instance;
|
|
}
|
|
|
|
lv_obj_t* build_window_widget(lv_obj_t* content, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
|
if (content == nullptr) {
|
|
return nullptr;
|
|
}
|
|
lvgl_lock();
|
|
lv_obj_t* widget = lv_obj_create(content);
|
|
lv_obj_set_size(widget, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_set_style_pad_all(widget, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_border_width(widget, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_radius(widget, 0, LV_STATE_DEFAULT);
|
|
// Plain layout container, not meant to scroll on its own - every app already does this
|
|
// for its own root object. Without it, a sub-pixel flex-layout overflow here can show the
|
|
// theme's scrollbar styling as a thin line hugging this widget's edges.
|
|
lv_obj_remove_flag(widget, LV_OBJ_FLAG_SCROLLABLE);
|
|
if (create_widgets != nullptr) {
|
|
create_widgets(widget, user_data);
|
|
}
|
|
lvgl_unlock();
|
|
return widget;
|
|
}
|
|
|
|
// destroy_widgets, if set, is called inside the same LVGL-locked section as the deletion - see
|
|
// WindowDestroyWidgetsFn's warnings about what it may safely do from in here.
|
|
void delete_widget(lv_obj_t* widget, WindowDestroyWidgetsFn destroy_widgets = nullptr, void* user_data = nullptr) {
|
|
if (widget == nullptr) {
|
|
return;
|
|
}
|
|
lvgl_lock();
|
|
if (destroy_widgets != nullptr) {
|
|
destroy_widgets(user_data);
|
|
}
|
|
lv_obj_delete(widget);
|
|
lvgl_unlock();
|
|
}
|
|
|
|
// Call while holding WindowManagerState::mutex. Transfers ownership of `window`'s waiting
|
|
// signal, if any, to the caller, taking an extra reference on the caller's behalf. The
|
|
// caller must pass the result to give_and_release() exactly once, outside the lock.
|
|
WindowWaitSignal* claim_waiter_locked(WindowRecord& window) {
|
|
WindowWaitSignal* signal = window.waiting_signal;
|
|
window.waiting_signal = nullptr;
|
|
if (signal != nullptr) {
|
|
signal->refcount++;
|
|
}
|
|
return signal;
|
|
}
|
|
|
|
// Gives `signal`'s semaphore, waking window_manager_await_state_change() if it's still
|
|
// waiting, then releases the caller's reference from claim_waiter_locked(). Deletes the
|
|
// signal if that was the last reference. No-op if `signal` is NULL.
|
|
void give_and_release(WindowWaitSignal* signal) {
|
|
if (signal == nullptr) {
|
|
return;
|
|
}
|
|
|
|
xSemaphoreGive(signal->semaphore);
|
|
|
|
auto& s = state();
|
|
mutex_lock(&s.mutex);
|
|
bool should_delete = (--signal->refcount == 0);
|
|
mutex_unlock(&s.mutex);
|
|
if (should_delete) {
|
|
vSemaphoreDelete(signal->semaphore);
|
|
delete signal;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
|
|
void window_manager_configure(WindowManagerScreenInitFn screen_init) {
|
|
auto& s = state();
|
|
|
|
// Serializes against window_manager_start()/stop()
|
|
mutex_lock(&s.lifecycle_mutex);
|
|
|
|
mutex_lock(&s.mutex);
|
|
if (!s.started) {
|
|
s.screen_init = screen_init;
|
|
} else {
|
|
LOG_W(TAG, "Ignoring window_manager_configure: module is already started");
|
|
}
|
|
mutex_unlock(&s.mutex);
|
|
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
}
|
|
|
|
error_t window_manager_start(void) {
|
|
auto& s = state();
|
|
|
|
// Held for the whole transition, including the LVGL work below done with `mutex`
|
|
// released. Blocks a concurrent start() from also passing the `started` check and
|
|
// building its own root widget, and blocks a concurrent stop() from running while this
|
|
// start is still mid-flight.
|
|
mutex_lock(&s.lifecycle_mutex);
|
|
|
|
mutex_lock(&s.mutex);
|
|
if (s.started) {
|
|
mutex_unlock(&s.mutex);
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
WindowManagerScreenInitFn screen_init = s.screen_init;
|
|
mutex_unlock(&s.mutex);
|
|
|
|
lv_obj_t* real_widget = nullptr;
|
|
lv_obj_t* content_widget = nullptr;
|
|
|
|
lvgl_lock();
|
|
lv_obj_t* screen = lv_screen_active();
|
|
if (screen != nullptr) {
|
|
real_widget = lv_obj_create(screen);
|
|
lv_obj_set_size(real_widget, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_set_style_pad_all(real_widget, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_border_width(real_widget, 0, LV_STATE_DEFAULT);
|
|
lv_obj_set_style_radius(real_widget, 0, LV_STATE_DEFAULT);
|
|
// See build_window_widget()'s identical flag removal for why.
|
|
lv_obj_remove_flag(real_widget, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
content_widget = (screen_init != nullptr) ? screen_init(real_widget) : nullptr;
|
|
if (content_widget == nullptr) {
|
|
content_widget = real_widget;
|
|
}
|
|
}
|
|
lvgl_unlock();
|
|
|
|
if (real_widget == nullptr) {
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
// A previous stop() may have left window records behind for an app that's still running
|
|
// (see window_manager_stop()'s comment). Rebuild the topmost one now, the same way
|
|
// window_manager_remove() rebuilds when a buried window resurfaces. Otherwise that app's
|
|
// task stays blocked in its own event loop forever, with no window and no signal telling
|
|
// it to rebuild one.
|
|
WindowCreateWidgetsFn top_create_widgets = nullptr;
|
|
WindowDestroyWidgetsFn top_destroy_widgets = nullptr;
|
|
void* top_user_data = nullptr;
|
|
WindowId top_id = 0;
|
|
bool has_top = false;
|
|
|
|
mutex_lock(&s.mutex);
|
|
s.real_root_widget = real_widget;
|
|
s.content_root_widget = content_widget;
|
|
s.started = true;
|
|
if (!s.windows.empty()) {
|
|
top_create_widgets = s.windows.back().create_widgets;
|
|
top_destroy_widgets = s.windows.back().destroy_widgets;
|
|
top_user_data = s.windows.back().user_data;
|
|
top_id = s.windows.back().id;
|
|
has_top = true;
|
|
}
|
|
mutex_unlock(&s.mutex);
|
|
|
|
if (has_top) {
|
|
lv_obj_t* new_widget = build_window_widget(content_widget, top_create_widgets, top_user_data);
|
|
|
|
mutex_lock(&s.mutex);
|
|
bool still_topmost = !s.windows.empty() && s.windows.back().id == top_id;
|
|
if (still_topmost) {
|
|
s.top_widget = new_widget;
|
|
new_widget = nullptr; // consumed
|
|
}
|
|
mutex_unlock(&s.mutex);
|
|
|
|
// The window stack changed while we were building, e.g. a concurrent remove() -
|
|
// discard what we just made.
|
|
delete_widget(new_widget, top_destroy_widgets, top_user_data);
|
|
}
|
|
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
error_t window_manager_stop(void) {
|
|
auto& s = state();
|
|
|
|
// See window_manager_start(): blocks until any in-flight start() has finished, or failed,
|
|
// before this stop observes or tears down state.
|
|
mutex_lock(&s.lifecycle_mutex);
|
|
|
|
mutex_lock(&s.mutex);
|
|
if (!s.started) {
|
|
mutex_unlock(&s.mutex);
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
lv_obj_t* widget = s.real_root_widget;
|
|
// Claim every window's waiter before tearing down. Normally only the topmost window has
|
|
// one set, but every window's widget is torn down here, so every one is checked.
|
|
std::vector<WindowWaitSignal*> waiters;
|
|
for (auto& window : s.windows) {
|
|
if (auto* signal = claim_waiter_locked(window); signal != nullptr) {
|
|
waiters.push_back(signal);
|
|
}
|
|
}
|
|
// Only the topmost window has a live widget - it's the only one whose destroy_widgets needs
|
|
// to fire.
|
|
WindowDestroyWidgetsFn top_destroy_widgets = !s.windows.empty() ? s.windows.back().destroy_widgets : nullptr;
|
|
void* top_user_data = !s.windows.empty() ? s.windows.back().user_data : nullptr;
|
|
s.real_root_widget = nullptr;
|
|
s.content_root_widget = nullptr;
|
|
s.top_widget = nullptr;
|
|
// Deliberately not s.windows.clear(): this tears down only the LVGL widget tree, not the
|
|
// window records. On a real full shutdown every app has already removed its own window via
|
|
// window_manager_remove(), so the list is empty anyway and this is a no-op. But a caller can
|
|
// also stop()/start() this module on its own, temporarily, while apps keep running
|
|
// underneath - for example one borrowing the display/touch hardware directly. Those apps'
|
|
// tasks stay alive, blocked in their own event loops, with no way to know they need to call
|
|
// window_manager_create() again. Keeping the records lets window_manager_start() rebuild the
|
|
// topmost one automatically instead of leaving that app stuck with no window forever.
|
|
s.started = false;
|
|
mutex_unlock(&s.mutex);
|
|
|
|
for (WindowWaitSignal* waiter : waiters) {
|
|
give_and_release(waiter);
|
|
}
|
|
|
|
// Deleting the real widget cascades to everything under it - chrome and top_widget alike.
|
|
delete_widget(widget, top_destroy_widgets, top_user_data);
|
|
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
WindowId window_manager_create(AppInstanceId app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
|
return window_manager_create_ext(app_instance_id, create_widgets, nullptr, user_data);
|
|
}
|
|
|
|
WindowId window_manager_create_ext(AppInstanceId app_instance_id, WindowCreateWidgetsFn create_widgets, WindowDestroyWidgetsFn destroy_widgets, void* user_data) {
|
|
if (app_instance_id == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (destroy_widgets != nullptr) {
|
|
check(create_widgets != nullptr);
|
|
}
|
|
|
|
auto& s = state();
|
|
|
|
// See lifecycle_mutex's comment: blocks a concurrent window_manager_stop() (or another
|
|
// create()/remove()) from touching real_root_widget/content_root_widget/top_widget while
|
|
// this call still holds pointers to them.
|
|
mutex_lock(&s.lifecycle_mutex);
|
|
|
|
mutex_lock(&s.mutex);
|
|
if (!s.started) {
|
|
mutex_unlock(&s.mutex);
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
return 0;
|
|
}
|
|
lv_obj_t* content = s.content_root_widget;
|
|
lv_obj_t* old_top_widget = s.top_widget;
|
|
// The current topmost window, if any, is about to be superseded - claim its waiter here
|
|
// so it gets notified below, and grab its destroy_widgets so it can be told its widget is
|
|
// about to go away.
|
|
WindowWaitSignal* waiter = nullptr;
|
|
WindowDestroyWidgetsFn old_destroy_widgets = nullptr;
|
|
void* old_user_data = nullptr;
|
|
if (!s.windows.empty()) {
|
|
waiter = claim_waiter_locked(s.windows.back());
|
|
old_destroy_widgets = s.windows.back().destroy_widgets;
|
|
old_user_data = s.windows.back().user_data;
|
|
}
|
|
s.top_widget = nullptr;
|
|
WindowId new_id = s.next_id++;
|
|
s.windows.push_back(WindowRecord { new_id, app_instance_id, create_widgets, destroy_widgets, user_data });
|
|
mutex_unlock(&s.mutex);
|
|
|
|
give_and_release(waiter);
|
|
|
|
delete_widget(old_top_widget, old_destroy_widgets, old_user_data);
|
|
lv_obj_t* new_widget = build_window_widget(content, create_widgets, user_data);
|
|
|
|
mutex_lock(&s.mutex);
|
|
bool still_topmost = !s.windows.empty() && s.windows.back().id == new_id;
|
|
if (still_topmost) {
|
|
s.top_widget = new_widget;
|
|
new_widget = nullptr; // consumed
|
|
}
|
|
mutex_unlock(&s.mutex);
|
|
|
|
// Another window became topmost while we were building, e.g. a concurrent create() from
|
|
// another app thread - discard what we just made.
|
|
delete_widget(new_widget, destroy_widgets, user_data);
|
|
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
return new_id;
|
|
}
|
|
|
|
void window_manager_remove(WindowId id) {
|
|
auto& s = state();
|
|
|
|
// See lifecycle_mutex's comment: blocks a concurrent window_manager_stop() (or another
|
|
// create()/remove()) from touching real_root_widget/content_root_widget/top_widget while
|
|
// this call still holds pointers to them.
|
|
mutex_lock(&s.lifecycle_mutex);
|
|
|
|
mutex_lock(&s.mutex);
|
|
auto iterator = std::find_if(s.windows.begin(), s.windows.end(),
|
|
[id](const WindowRecord& window) { return window.id == id; });
|
|
if (iterator == s.windows.end()) {
|
|
mutex_unlock(&s.mutex);
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
return;
|
|
}
|
|
bool was_topmost = (iterator + 1 == s.windows.end());
|
|
// The window being removed owns its own waiter, if any. A waiter is only ever registered
|
|
// while its window is topmost (see window_manager_await_state_change()); if this window had
|
|
// since stopped being topmost without being removed, window_manager_create() would already
|
|
// have claimed and cleared it. So a buried window's waiting_signal is always already null.
|
|
WindowWaitSignal* waiter = claim_waiter_locked(*iterator);
|
|
WindowDestroyWidgetsFn removed_destroy_widgets = iterator->destroy_widgets;
|
|
void* removed_user_data = iterator->user_data;
|
|
s.windows.erase(iterator);
|
|
|
|
lv_obj_t* content = s.content_root_widget;
|
|
lv_obj_t* old_widget = nullptr;
|
|
WindowCreateWidgetsFn next_create_widgets = nullptr;
|
|
WindowDestroyWidgetsFn next_destroy_widgets = nullptr;
|
|
void* next_user_data = nullptr;
|
|
WindowId next_id = 0;
|
|
bool has_next = false;
|
|
|
|
if (was_topmost) {
|
|
old_widget = s.top_widget;
|
|
s.top_widget = nullptr;
|
|
if (!s.windows.empty()) {
|
|
next_create_widgets = s.windows.back().create_widgets;
|
|
next_destroy_widgets = s.windows.back().destroy_widgets;
|
|
next_user_data = s.windows.back().user_data;
|
|
next_id = s.windows.back().id;
|
|
has_next = true;
|
|
}
|
|
}
|
|
mutex_unlock(&s.mutex);
|
|
|
|
give_and_release(waiter);
|
|
|
|
if (!was_topmost) {
|
|
// A buried window was removed; the topmost window's widgets are unaffected.
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
return;
|
|
}
|
|
|
|
delete_widget(old_widget, removed_destroy_widgets, removed_user_data);
|
|
lv_obj_t* new_widget = has_next ? build_window_widget(content, next_create_widgets, next_user_data) : nullptr;
|
|
|
|
mutex_lock(&s.mutex);
|
|
bool still_topmost = has_next && !s.windows.empty() && s.windows.back().id == next_id;
|
|
if (still_topmost) {
|
|
s.top_widget = new_widget;
|
|
new_widget = nullptr; // consumed
|
|
}
|
|
mutex_unlock(&s.mutex);
|
|
|
|
delete_widget(new_widget, next_destroy_widgets, next_user_data);
|
|
|
|
mutex_unlock(&s.lifecycle_mutex);
|
|
}
|
|
|
|
WindowState window_manager_get_state(WindowId id) {
|
|
auto& s = state();
|
|
mutex_lock(&s.mutex);
|
|
bool is_top = !s.windows.empty() && s.windows.back().id == id;
|
|
mutex_unlock(&s.mutex);
|
|
return is_top ? WINDOW_STATE_GRANTED : WINDOW_STATE_REVOKED;
|
|
}
|
|
|
|
WindowState window_manager_await_state_change(WindowId id, TickType_t timeout) {
|
|
auto& s = state();
|
|
|
|
// Uses a dedicated semaphore rather than this task's default FreeRTOS notification.
|
|
// Other subsystems, e.g. app_event.cpp's AppEventSubscription, share that same slot - an
|
|
// unrelated notification delivered to this task could otherwise wake this wait early.
|
|
auto* signal = new (std::nothrow) WindowWaitSignal();
|
|
if (signal == nullptr) {
|
|
return window_manager_get_state(id);
|
|
}
|
|
signal->semaphore = xSemaphoreCreateBinary();
|
|
if (signal->semaphore == nullptr) {
|
|
delete signal;
|
|
return window_manager_get_state(id);
|
|
}
|
|
|
|
mutex_lock(&s.mutex);
|
|
bool is_top = !s.windows.empty() && s.windows.back().id == id;
|
|
if (!is_top) {
|
|
mutex_unlock(&s.mutex);
|
|
vSemaphoreDelete(signal->semaphore);
|
|
delete signal;
|
|
return WINDOW_STATE_REVOKED;
|
|
}
|
|
// At most one concurrent awaiter per window; see this function's @warning.
|
|
check(s.windows.back().waiting_signal == nullptr);
|
|
s.windows.back().waiting_signal = signal;
|
|
mutex_unlock(&s.mutex);
|
|
|
|
xSemaphoreTake(signal->semaphore, timeout);
|
|
|
|
// Deregister ourselves if a create()/remove() hasn't already claimed us. This is the
|
|
// ordinary, intended wakeup path; without it, a later create()/remove() could read a
|
|
// signal that's already been given away here. Re-locate the record by id, since it may
|
|
// have been erased by window_manager_remove() while we waited. Either way, release our
|
|
// own reference - whichever side finishes last, us or a claimer, is the one that deletes
|
|
// it.
|
|
mutex_lock(&s.mutex);
|
|
auto iterator = std::find_if(s.windows.begin(), s.windows.end(),
|
|
[id](const WindowRecord& window) { return window.id == id; });
|
|
if (iterator != s.windows.end() && iterator->waiting_signal == signal) {
|
|
iterator->waiting_signal = nullptr;
|
|
}
|
|
bool should_delete = (--signal->refcount == 0);
|
|
mutex_unlock(&s.mutex);
|
|
if (should_delete) {
|
|
vSemaphoreDelete(signal->semaphore);
|
|
delete signal;
|
|
}
|
|
|
|
return window_manager_get_state(id);
|
|
}
|
|
|
|
} // extern "C"
|