Refactor app loading and window management (#609)
This commit is contained in:
committed by
GitHub
parent
dc3f6104b8
commit
37c507544b
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(lvgl-window-manager-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel lvgl-module app-module
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module lvgl_window_manager_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,124 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/instance.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef uint32_t WindowId;
|
||||
|
||||
enum WindowState {
|
||||
/** id is the current topmost window and has live widgets. */
|
||||
WINDOW_STATE_GRANTED,
|
||||
/** id is not currently topmost - either buried under a newer window (its widgets don't
|
||||
* exist right now, but it may resurface and get rebuilt if everything above it is removed)
|
||||
* or it no longer exists at all (removed). */
|
||||
WINDOW_STATE_REVOKED,
|
||||
};
|
||||
|
||||
/**
|
||||
* Called once by window_manager_start(), given the real root widget (a raw, full-size
|
||||
* container created directly under the default display's active screen). May add extra chrome
|
||||
* (e.g. a statusbar) as children of @a root_widget.
|
||||
* @param[in] root_widget the real root widget; owned by this module, deleted automatically
|
||||
* (along with everything added under it) by window_manager_stop()
|
||||
* @return the widget windows should actually be placed into - @a root_widget itself, or a
|
||||
* child of it. Returning NULL falls back to @a root_widget.
|
||||
* @warning Called on the LVGL task with the LVGL lock already held.
|
||||
* @warning Also called with window-manager's internal lifecycle_mutex held (non-recursive) -
|
||||
* do NOT call window_manager_start()/window_manager_stop()/window_manager_create()/
|
||||
* window_manager_remove() or any other window-manager API from this callback, that would
|
||||
* deadlock.
|
||||
*/
|
||||
typedef lv_obj_t* (*WindowManagerScreenInitFn)(lv_obj_t* root_widget);
|
||||
|
||||
/**
|
||||
* Configures the screen-init callback window_manager_start() invokes to build the root/content
|
||||
* widgets. Pass NULL to restore the default (no chrome - the raw root widget is used directly).
|
||||
* @warning Must be called before window_manager_start(); has no effect once already started.
|
||||
*/
|
||||
void window_manager_configure(WindowManagerScreenInitFn screen_init);
|
||||
|
||||
/**
|
||||
* Creates the root widget (under the default display's active screen) and, via the configured
|
||||
* screen-init callback, whatever chrome/content widget it wants around it. Idempotent - a
|
||||
* second call while already started is a no-op.
|
||||
* @retval ERROR_RESOURCE no default display is active (lv_screen_active() returned NULL)
|
||||
* @retval ERROR_NONE on success (including if already started)
|
||||
*/
|
||||
error_t window_manager_start(void);
|
||||
|
||||
/**
|
||||
* Deletes the root widget created by window_manager_start() (and everything under it - any
|
||||
* chrome plus whatever the topmost window had drawn), removing it from the display, and drops
|
||||
* every tracked window. Idempotent - a second call while already stopped is a no-op.
|
||||
*/
|
||||
error_t window_manager_stop(void);
|
||||
|
||||
/**
|
||||
* Called to populate a window's widgets: once by window_manager_create() when the window is
|
||||
* first created, and again later by window_manager_remove() if this window resurfaces as the
|
||||
* new topmost after whatever was above it is removed. Only the current topmost window ever has
|
||||
* live widgets - everything below it in the stack exists as tracked state only.
|
||||
* @param[in] root a fresh, full-size container created directly under the content widget for
|
||||
* this window; deleted automatically once this window stops being topmost
|
||||
* @param[in] user_data whatever was passed to window_manager_create() for this window
|
||||
* @warning Called on the LVGL task with the LVGL lock already held.
|
||||
* @warning May run on a different kernel thread than the one that called window_manager_create()
|
||||
* for this window - the rebuild-on-remove path runs on whichever thread called
|
||||
* window_manager_remove() for the window that used to be on top (e.g. a dialog's own thread as
|
||||
* it closes). Do NOT rely on thread_local state set by this window's own app thread; use
|
||||
* @a user_data instead.
|
||||
* @warning Also called with window-manager's internal lifecycle_mutex held (non-recursive) -
|
||||
* do NOT call window_manager_start()/window_manager_stop()/window_manager_create()/
|
||||
* window_manager_remove() or any other window-manager API from this callback, that would
|
||||
* deadlock.
|
||||
*/
|
||||
typedef void (*WindowCreateWidgetsFn)(lv_obj_t* root, void* user_data);
|
||||
|
||||
/**
|
||||
* Creates a new window on top of the stack (last created = topmost). Deletes the previously
|
||||
* topmost window's widgets (if any) and builds this window's widgets immediately via
|
||||
* @a create_widgets - only the topmost window ever has live widgets.
|
||||
* @param[in] app_instance_id the application instance this window belongs to, should not be 0
|
||||
* @param[in] user_data opaque; passed back to @a create_widgets on every call, including a
|
||||
* later rebuild triggered by window_manager_remove() - see its @warning about which thread that
|
||||
* can run on. Typically the calling app's own Context*.
|
||||
* @return the new window's id, or 0 if window_manager_start() hasn't been called
|
||||
*/
|
||||
WindowId window_manager_create(AppInstanceId app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data);
|
||||
|
||||
/**
|
||||
* Removes a window, wherever it is in the stack - not necessarily the topmost one. If it was
|
||||
* topmost, its widgets are deleted and whichever window is now on top (if any) has its
|
||||
* create_widgets called again to rebuild its widgets.
|
||||
*/
|
||||
void window_manager_remove(WindowId id);
|
||||
|
||||
/** @return the current state of @a id; WINDOW_STATE_REVOKED if @a id is buried or doesn't exist. */
|
||||
enum WindowState window_manager_get_state(WindowId id);
|
||||
|
||||
/**
|
||||
* Blocks the calling task until @a id's state changes away from WINDOW_STATE_GRANTED, or
|
||||
* @a timeout elapses. Returns immediately with WINDOW_STATE_REVOKED if @a id isn't currently
|
||||
* topmost (nothing to wait for).
|
||||
* @warning At most one task may have an outstanding await() call per window at a time (each
|
||||
* window tracks a single waiter). A second concurrent call for the same @a id asserts. Calls
|
||||
* for different windows (e.g. from different app tasks in a stacked window manager) don't
|
||||
* conflict with each other.
|
||||
* @return the state after waking (or immediately, if there was nothing to wait for)
|
||||
*/
|
||||
enum WindowState window_manager_await_state_change(WindowId id, TickType_t timeout);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl_window_manager/module.h>
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
const ModuleSymbol lvgl_window_manager_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(window_manager_create),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_remove),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_get_state),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_await_state_change),
|
||||
// terminator
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
Module lvgl_window_manager_module = {
|
||||
.name = "lvgl-window-manager",
|
||||
.start = window_manager_start,
|
||||
.stop = window_manager_stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = lvgl_window_manager_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
|
||||
void delete_widget(lv_obj_t* widget) {
|
||||
if (widget == nullptr) {
|
||||
return;
|
||||
}
|
||||
lvgl_lock();
|
||||
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;
|
||||
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_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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
mutex_unlock(&s.lifecycle_mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
WindowId window_manager_create(AppInstanceId app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
||||
if (app_instance_id == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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.
|
||||
WindowWaitSignal* waiter = !s.windows.empty() ? claim_waiter_locked(s.windows.back()) : nullptr;
|
||||
s.top_widget = nullptr;
|
||||
WindowId new_id = s.next_id++;
|
||||
s.windows.push_back(WindowRecord { new_id, app_instance_id, create_widgets, user_data });
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
give_and_release(waiter);
|
||||
|
||||
delete_widget(old_top_widget);
|
||||
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);
|
||||
|
||||
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);
|
||||
s.windows.erase(iterator);
|
||||
|
||||
lv_obj_t* content = s.content_root_widget;
|
||||
lv_obj_t* old_widget = nullptr;
|
||||
WindowCreateWidgetsFn next_create_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_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);
|
||||
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);
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user