Refactor app loading and window management (#609)

This commit is contained in:
Ken Van Hoeylandt
2026-08-11 23:40:59 +02:00
committed by GitHub
parent dc3f6104b8
commit 37c507544b
243 changed files with 16034 additions and 10865 deletions
@@ -1,98 +0,0 @@
#pragma once
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/ElfApp.h>
#include <Tactility/Bundle.h>
#include <tactility/check.h>
#include <tactility/log.h>
#include <Tactility/Mutex.h>
#include <memory>
#include <utility>
namespace tt::app {
enum class State {
Initial, // AppInstance was created, but the state hasn't advanced yet
Created, // App was placed into memory
Showing, // App view was created
Hiding, // App view was destroyed
Destroyed // App was removed from memory
};
/**
* Thread-safe app instance.
*/
class AppInstance : public AppContext {
Mutex mutex;
const std::shared_ptr<AppManifest> manifest;
State state = State::Initial;
LaunchId launchId;
Flags flags = { .hideStatusbar = true };
/** @brief Optional parameters to start the app with
* When these are stored in the app struct, the struct takes ownership.
* Do not mutate after app creation.
*/
std::shared_ptr<const Bundle> parameters;
std::shared_ptr<App> app;
static std::shared_ptr<App> createApp(
const std::shared_ptr<AppManifest>& manifest
) {
if (manifest->appLocation.isInternal()) {
assert(manifest->createApp != nullptr);
return manifest->createApp();
} else if (manifest->appLocation.isExternal()) {
if (manifest->createApp != nullptr) {
LOG_W("AppInstance", "Manifest specifies createApp, but this is not used with external apps");
}
#ifdef ESP_PLATFORM
return createElfApp(manifest);
#else
check(false, "not supported");
#endif
} else {
check(false, "not implemented");
}
}
public:
explicit AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId) :
manifest(manifest),
launchId(launchId),
app(createApp(manifest))
{}
AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId, std::shared_ptr<const Bundle> parameters) :
manifest(manifest),
launchId(launchId),
parameters(std::move(parameters)),
app(createApp(manifest))
{}
~AppInstance() override = default;
LaunchId getLaunchId() const { return launchId; }
void setState(State state);
State getState() const;
const AppManifest& getManifest() const override;
Flags getFlags() const;
void setFlags(Flags flags);
Flags& mutableFlags() { return flags; } // TODO: locking mechanism
std::shared_ptr<const Bundle> getParameters() const override;
std::unique_ptr<AppPaths> getPaths() const override;
std::shared_ptr<App> getApp() const override { return app; }
};
} // namespace
@@ -1,14 +0,0 @@
#pragma once
#include <Tactility/app/AppManifest.h>
#include <string>
namespace tt::app {
bool isValidId(const std::string& id);
/** Parses a manifest.properties file, auto-detecting the V1 (sectioned) or V2 (flat) format from its first line. */
bool parseManifest(const std::string& filePath, AppManifest& manifest);
}
@@ -1,23 +0,0 @@
#pragma once
#include <Tactility/app/AppManifest.h>
#include <map>
#include <string>
namespace tt::app {
bool getValueFromManifest(const std::map<std::string, std::string>& map, const std::string& key, std::string& output);
bool isValidManifestVersion(const std::string& version);
bool isValidAppVersionName(const std::string& version);
bool isValidAppVersionCode(const std::string& version);
bool isValidName(const std::string& name);
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map. */
bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest& manifest);
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map. */
bool parseManifestV2(const std::map<std::string, std::string>& map, AppManifest& manifest);
}
@@ -5,11 +5,13 @@
namespace tt::app::btmanage {
typedef void (*OnBtToggled)(bool enable);
typedef void (*OnScanToggled)(bool enable);
// `context` is this app instance's Context* (see BtManagePrivate.h) - the new app-module has no
// global "current app" accessor, so callbacks need it threaded through explicitly.
typedef void (*OnBtToggled)(void* context, bool enable);
typedef void (*OnScanToggled)(void* context, bool enable);
typedef void (*OnConnectPeer)(const std::array<uint8_t, 6>& addr, int profileId);
typedef void (*OnDisconnectPeer)(const std::array<uint8_t, 6>& addr, int profileId);
typedef void (*OnPairPeer)(const std::array<uint8_t, 6>& addr);
typedef void (*OnPairPeer)(void* context, const std::array<uint8_t, 6>& addr);
typedef void (*OnForgetPeer)(const std::array<uint8_t, 6>& addr);
struct Bindings {
@@ -3,7 +3,6 @@
#include "./View.h"
#include "./State.h"
#include <Tactility/app/App.h>
#include <Tactility/Mutex.h>
#include <Tactility/bluetooth/Bluetooth.h>
#include <tactility/drivers/bluetooth.h>
@@ -13,54 +12,37 @@
namespace tt::app::btmanage {
class BtManage final : public App {
struct Context {
uint32_t appInstanceId;
Mutex mutex;
Bindings bindings = { };
Bindings bindings {};
State state;
View view = View(&bindings, &state);
bool isViewEnabled = false;
Device* btDevice = nullptr;
bool callbackRegistered = false;
// Bumped by onHide() to invalidate any BT event already dispatched to the main
// task for this show/hide session (BtManage is reused across hide/show cycles -
// e.g. launching BtPeerSettings pushes it on top and hides this instance without
// destroying it). Kept in its own heap allocation, independent of BtManage's
// lifetime, so a dispatched callback can check it without touching a possibly
// already-destroyed `this`.
// Bumped right before the BT event callback is unregistered at the end of appMain(), to
// invalidate any BT event already dispatched to the main task for this instance. Kept in
// its own heap allocation, independent of Context's (stack-local) lifetime, so a dispatched
// callback can check it without touching a possibly already-destroyed Context.
std::shared_ptr<std::atomic<int>> generation = std::make_shared<std::atomic<int>>(0);
public:
void onBtEvent(const struct BtEvent& event);
BtManage();
void lock();
void unlock();
void onShow(AppContext& app, lv_obj_t* parent) override;
void onHide(AppContext& app) override;
Bindings& getBindings() { return bindings; }
State& getState() { return state; }
void requestViewUpdate();
std::shared_ptr<std::atomic<int>> getGeneration() const { return generation; }
// Re-attempts registering the device event callback. Needed because the BLE driver
// only allocates its callback list while the device is started/on: a registration
// attempted while the radio is off silently no-ops, so this must be called again
// right after a successful bluetooth::start(). Idempotent: no-ops if already
// registered for this device, so it's safe to call from both onShow() and here.
void registerDeviceCallback(Device* dev);
// Call after bluetooth::stop(): the driver frees its callback list on stop, so the
// registration state must be cleared here too, without touching the (now-dangling)
// driver-side list.
void forgetCallbackRegistration();
void lock() { mutex.lock(); }
void unlock() { mutex.unlock(); }
};
void onBtEvent(Context* ctx, const struct BtEvent& event);
void requestViewUpdate(Context* ctx);
// Re-attempts registering the device event callback. Needed because the BLE driver only
// allocates its callback list while the device is started/on: a registration attempted while
// the radio is off silently no-ops, so this must be called again right after a successful
// bluetooth::start(). Idempotent: no-ops if already registered for this device.
void registerDeviceCallback(Context* ctx, Device* dev);
// Call after bluetooth::stop(): the driver frees its callback list on stop, so the
// registration state must be cleared here too, without touching the (now-dangling) driver-side
// list.
void forgetCallbackRegistration(Context* ctx);
} // namespace tt::app::btmanage
@@ -3,9 +3,7 @@
#include "./Bindings.h"
#include "./State.h"
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppPaths.h>
#include <cstdint>
#include <lvgl.h>
namespace tt::app::btmanage {
@@ -14,7 +12,9 @@ class View final {
Bindings* bindings;
State* state;
std::unique_ptr<AppPaths> paths;
// Passed through to onBtToggled/onScanToggled/onPairPeer via lv_obj user_data - see
// Bindings.h. Set in init(), before any callback can fire.
void* context = nullptr;
lv_obj_t* root = nullptr;
lv_obj_t* enable_switch = nullptr;
lv_obj_t* enable_on_boot_switch = nullptr;
@@ -34,7 +34,7 @@ public:
View(Bindings* bindings, State* state) : bindings(bindings), state(state) {}
void init(const AppContext& app, lv_obj_t* parent);
void init(void* context, lv_obj_t* parent);
void update();
};
@@ -10,37 +10,32 @@
#include "ChatView.h"
#include "ChatSettings.h"
#include <Tactility/app/App.h>
#include <Tactility/service/espnow/EspNow.h>
#include <cstdint>
#include <string>
namespace tt::app::chat {
class ChatApp final : public App {
// Replaces the old ChatApp (tt::app::App subclass) under the thread-per-app model. Declared
// here (rather than local to ChatApp.cpp's anonymous namespace, as most converted apps do)
// because ChatView - a separate translation unit - also needs to reference it.
struct Context {
uint32_t appInstanceId;
ChatState state;
ChatView view = ChatView(this, &state);
service::espnow::ReceiverSubscription receiveSubscription = -1;
ChatSettingsData settings;
bool isFirstLaunch = false;
void onReceive(const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length);
void enableEspNow();
void disableEspNow();
public:
void onCreate(AppContext& appContext) override;
void onDestroy(AppContext& appContext) override;
void onShow(AppContext& context, lv_obj_t* parent) override;
void sendMessage(const std::string& text);
void applySettings(const std::string& nickname, const std::string& keyHex);
void switchChannel(const std::string& chatChannel);
const ChatSettingsData& getSettings() const { return settings; }
~ChatApp() override = default;
};
void enableEspNow(Context* ctx);
void disableEspNow(Context* ctx);
void sendMessage(Context* ctx, const std::string& text);
void applySettings(Context* ctx, const std::string& nickname, const std::string& keyHex);
void switchChannel(Context* ctx, const std::string& chatChannel);
} // namespace tt::app::chat
#endif // CONFIG_SOC_WIFI_SUPPORTED || CONFIG_SLAVE_SOC_WIFI_SUPPORTED
@@ -9,18 +9,16 @@
#include "ChatState.h"
#include "ChatSettings.h"
#include <Tactility/app/AppContext.h>
#include <esp_now.h>
#include <lvgl.h>
namespace tt::app::chat {
class ChatApp;
struct Context;
class ChatView {
ChatApp* app;
Context* app;
ChatState* state;
lv_obj_t* toolbar = nullptr;
@@ -45,6 +43,7 @@ class ChatView {
static void addMessageToList(lv_obj_t* msgList, const StoredMessage& msg);
static void onBackPressed(lv_event_t* e);
static void onSendClicked(lv_event_t* e);
static void onSettingsClicked(lv_event_t* e);
static void onSettingsSave(lv_event_t* e);
@@ -54,7 +53,7 @@ class ChatView {
static void onChannelCancel(lv_event_t* e);
public:
ChatView(ChatApp* app, ChatState* state) : app(app), state(state) {}
ChatView(Context* app, ChatState* state) : app(app), state(state) {}
~ChatView() = default;
ChatView(const ChatView&) = delete;
@@ -62,7 +61,7 @@ public:
ChatView(ChatView&&) = delete;
ChatView& operator=(ChatView&&) = delete;
void init(AppContext& appContext, lv_obj_t* parent);
void init(lv_obj_t* parent);
void displayMessage(const StoredMessage& msg);
void refreshMessageList();
@@ -1,11 +0,0 @@
#pragma once
#ifdef ESP_PLATFORM
namespace tt::app::development {
void start();
}
#endif // ESP_PLATFORM
+8 -7
View File
@@ -2,8 +2,7 @@
#include "./State.h"
#include <Tactility/app/AppManifest.h>
#include <cstdint>
#include <lvgl.h>
#include <memory>
@@ -11,7 +10,8 @@ namespace tt::app::files {
class View final {
std::shared_ptr<State> state;
uint32_t appInstanceId = 0;
size_t current_start_index = 0;
size_t last_loaded_index = 0;
const size_t MAX_BATCH = 50;
@@ -24,7 +24,7 @@ class View final {
lv_obj_t* paste_button = nullptr;
std::string installAppPath = { 0 };
LaunchId installAppLaunchId = 0;
uint32_t installDialogId = 0;
void showActions();
void showActionsForDirectory();
@@ -39,9 +39,10 @@ public:
explicit View(const std::shared_ptr<State>& state) : state(state) {}
void init(const AppContext& appContext, lv_obj_t* parent);
void init(uint32_t appInstanceId, lv_obj_t* parent);
void update(size_t start_index = 0);
void onBackPressed();
void onNavigateUpPressed();
void onDirEntryPressed(uint32_t index);
void onDirEntryLongPressed(int32_t index);
@@ -54,8 +55,8 @@ public:
void onPastePressed();
void onEjectPressed();
void onDirEntryListScrollBegin();
void onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle);
void deinit(const AppContext& appContext);
void onResult(uint32_t launchId, int32_t result);
void deinit();
private:
@@ -1,7 +1,5 @@
#pragma once
#include <Tactility/Bundle.h>
namespace tt::app::fileselection {
enum class Mode {
@@ -9,6 +7,4 @@ enum class Mode {
ExistingOrNew = 1
};
Mode getMode(const Bundle& bundle);
}
@@ -3,14 +3,13 @@
#include "./State.h"
#include "./FileSelectionPrivate.h"
#include <Tactility/app/AppManifest.h>
#include <lvgl.h>
#include <memory>
namespace tt::app::fileselection {
class View final {
uint32_t appInstanceId;
std::shared_ptr<State> state;
lv_obj_t* dir_entry_list = nullptr;
@@ -22,11 +21,15 @@ class View final {
void onTapFile(const std::string&path, const std::string&filename);
static void onSelectButtonPressed(lv_event_t* event);
static void onPathTextChanged(lv_event_t* event);
/** Emits an async APP_EVENT_CLOSE for appInstanceId - see FileSelection.cpp's appMain() for
* why this indirection (rather than calling app_manager_stop() here) is required. */
static void onBackPressedCallback(lv_event_t* event);
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
public:
explicit View(const std::shared_ptr<State>& state, std::function<void(const std::string& path)> onFileSelected) :
explicit View(uint32_t appInstanceId, const std::shared_ptr<State>& state, std::function<void(const std::string& path)> onFileSelected) :
appInstanceId(appInstanceId),
state(state),
on_file_selected(std::move(onFileSelected))
{}
@@ -1,9 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
#include <cstdint>
namespace tt::app::i2cscanner {
LaunchId start();
uint32_t start();
}
@@ -1,9 +1,7 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::launcher {
LaunchId start();
uint32_t start();
}
@@ -1,9 +1,5 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::localesettings {
LaunchId start();
}
// Intentionally empty: LocaleSettings.cpp has no external callers (verified via repo-wide
// grep during its thread-per-app conversion), so it no longer exposes a start() wrapper. This
// header is kept as a placeholder in case that changes; nothing currently includes it.
@@ -1,10 +1,8 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::setup {
LaunchId start();
void start();
/** @return true if the setup wizard has already run to completion */
bool isCompleted();
@@ -1,9 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
#include <cstdint>
namespace tt::app::timedatesettings {
LaunchId start();
uint32_t start();
}
@@ -1,13 +1,20 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
#include <cstdint>
#include <string>
namespace tt::app::timezone {
LaunchId start(bool saveTimeZone = false);
/**
* @return the started dialog's instance id; result (0 = Ok, 1 = Cancelled) is delivered to
* @a callerAppInstanceId as APP_EVENT_RESULT once the user picks a time zone - call
* getLastName()/getLastCode() right after receiving it, on result == 0.
*/
uint32_t start(uint32_t callerAppInstanceId, bool saveTimeZone = false);
std::string getResultName(const Bundle& bundle);
std::string getResultCode(const Bundle& bundle);
/** @return the name/code from the last time zone picked by any TimeZone dialog instance. Only
* one dialog is expected to be open at a time. */
std::string getLastName();
std::string getLastCode();
}
@@ -1,14 +0,0 @@
#pragma once
#include <Tactility/service/wifi/WifiApSettings.h>
namespace tt::app::wificonnect {
typedef void (*OnConnectSsid)(const service::wifi::settings::WifiApSettings& settings, bool store, void* context);
typedef struct {
OnConnectSsid onConnectSsid;
void* onConnectSsidContext;
} Bindings;
} // namespace
@@ -1,24 +0,0 @@
#pragma once
#include <Tactility/Mutex.h>
#include <Tactility/service/wifi/WifiApSettings.h>
namespace tt::app::wificonnect {
class State final {
Mutex lock;
service::wifi::settings::WifiApSettings apSettings;
bool connectionError = false;
bool connecting = false;
public:
void setConnectionError(bool error);
bool hasConnectionError() const;
void setApSettings(const service::wifi::settings::WifiApSettings& newSettings);
void setConnecting(bool isConnecting);
bool isConnecting() const;
};
} // namespace
@@ -1,45 +0,0 @@
#pragma once
#include "./Bindings.h"
#include "./State.h"
#include <Tactility/app/AppContext.h>
#include <lvgl.h>
namespace tt::app::wificonnect {
class WifiConnect;
class View final {
Bindings* bindings;
State* state;
public:
lv_obj_t* ssid_textarea = nullptr;
lv_obj_t* ssid_error = nullptr;
lv_obj_t* password_textarea = nullptr;
lv_obj_t* password_error = nullptr;
lv_obj_t* connect_button = nullptr;
lv_obj_t* remember_switch = nullptr;
lv_obj_t* connecting_spinner = nullptr;
lv_obj_t* connection_error = nullptr;
lv_group_t* group = nullptr;
View(Bindings* bindings, State* state) :
bindings(bindings),
state(state)
{}
void init(AppContext& app, lv_obj_t* parent);
void update();
void createBottomButtons(lv_obj_t* parent);
void setLoading(bool loading);
void resetErrors();
};
} // namespace
@@ -1,54 +1,12 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/app/wificonnect/Bindings.h>
#include <Tactility/app/wificonnect/State.h>
#include <Tactility/app/wificonnect/View.h>
#include <Tactility/Mutex.h>
#include <Tactility/service/wifi/Wifi.h>
#include <string>
namespace tt::app::wificonnect {
class WifiConnect final : public App {
Mutex mutex;
State state;
Bindings bindings = {
.onConnectSsid = nullptr,
.onConnectSsidContext = nullptr
};
View view = View(&bindings, &state);
PubSub<service::wifi::WifiEvent>::SubscriptionHandle wifiSubscription;
bool viewEnabled = false;
void onWifiEvent(service::wifi::WifiEvent event);
public:
WifiConnect();
~WifiConnect() override;
void lock();
void unlock();
void onShow(AppContext& app, lv_obj_t* parent) override;
void onHide(AppContext& app) override;
State& getState() { return state; }
Bindings& getBindings() { return bindings; }
View& getView() { return view; }
void requestViewUpdate();
};
/**
* Start the app with optional pre-filled fields.
*/
LaunchId start(const std::string& ssid = "", const std::string& password = "");
bool optSsidParameter(const std::shared_ptr<const Bundle>& bundle, std::string& ssid);
bool optPasswordParameter(const std::shared_ptr<const Bundle>& bundle, std::string& password);
void start(const std::string& ssid = "", const std::string& password = "");
} // namespace
@@ -3,9 +3,7 @@
#include "./Bindings.h"
#include "./State.h"
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppPaths.h>
#include <cstdint>
#include <lvgl.h>
namespace tt::app::wifimanage {
@@ -14,7 +12,7 @@ class View final {
Bindings* bindings;
State* state;
std::unique_ptr<AppPaths> paths;
uint32_t appInstanceId = 0;
lv_obj_t* root = nullptr;
lv_obj_t* enable_switch = nullptr;
lv_obj_t* enable_on_boot_switch = nullptr;
@@ -36,7 +34,7 @@ public:
View(Bindings* bindings, State* state) : bindings(bindings), state(state) {}
void init(const AppContext& app, lv_obj_t* parent);
void init(uint32_t appInstanceId, lv_obj_t* parent);
void update();
};
@@ -3,39 +3,11 @@
#include "./View.h"
#include "./State.h"
#include <Tactility/app/App.h>
#include <Tactility/PubSub.h>
#include <Tactility/Mutex.h>
#include <Tactility/service/wifi/Wifi.h>
namespace tt::app::wifimanage {
class WifiManage final : public App {
PubSub<service::wifi::WifiEvent>::SubscriptionHandle wifiSubscription = nullptr;
Mutex mutex;
Bindings bindings = { };
State state;
View view = View(&bindings, &state);
bool isViewEnabled = false;
void onWifiEvent(service::wifi::WifiEvent event);
public:
WifiManage();
void lock();
void unlock();
void onShow(AppContext& app, lv_obj_t* parent) override;
void onHide(AppContext& app) override;
Bindings& getBindings() { return bindings; }
State& getState() { return state; }
void requestViewUpdate();
};
} // namespace
// Context (the app's actual runtime state) is defined inside WifiManage.cpp's own anonymous
// namespace - View.cpp doesn't need it (Bindings*/State* pointers and a raw appInstanceId are
// threaded through explicitly instead, see View.h/View.cpp). This header now only bundles
// View.h/State.h for convenience, same as before.
@@ -1,107 +0,0 @@
#pragma once
#include <Tactility/MessageQueue.h>
#include <Tactility/PubSub.h>
#include <Tactility/RecursiveMutex.h>
#include <Tactility/service/Service.h>
#include <Tactility/service/loader/Loader.h>
#include <Tactility/Semaphore.h>
#include <tactility/concurrent/dispatcher.h>
#include <lvgl/devices/keyboard.h>
namespace tt::service::gui {
/**
* Output a log warning if the current task is the GUI task.
* This is meant for code that should either create their own task or use a different task to execute on.
* @param[in] context a descriptive name or label that refers to the caller of this function
*/
void warnIfRunningOnGuiTask(const char* context);
class GuiService final : public Service {
// Thread and lock
Thread* thread = nullptr;
DispatcherHandle_t dispatcher = nullptr;
bool exitRequested = false;
RecursiveMutex mutex;
PubSub<loader::LoaderService::Event>::SubscriptionHandle loader_pubsub_subscription = nullptr;
// Signaled by hideApp() once App::onHide() has actually finished running on the GUI
// task. onLoaderEvent() blocks on this (still on the Loader thread, inside the
// synchronous pubsub publish() call) before returning from the ApplicationHiding
// branch, so LoaderService::transitionAppToState(Hiding) can't return - and therefore
// the immediately-following Destroyed transition (which unloads an ELF app's code via
// esp_elf_deinit) can't run - until onHide() has fully completed. Without this, the
// ELF's code/data can be unmapped while onHide() (and anything it spawned, like a
// camera capture task) is still executing it.
Semaphore hideDoneSem { 1, 0 };
// Layers and Canvas
lv_obj_t* appRootWidget = nullptr;
lv_obj_t* statusbarWidget = nullptr;
// App-specific
std::shared_ptr<app::AppInstance> appToRender = nullptr;
LvglSoftwareKeyboard software_keyboard = {};
bool isStarted = false;
static int32_t guiMain();
static void onGuiDispatch(void* context);
void onLoaderEvent(loader::LoaderService::Event event);
lv_obj_t* createAppViews(lv_obj_t* parent);
void redraw();
void lock() const {
check(mutex.lock(pdMS_TO_TICKS(1000)));
}
void unlock() const {
mutex.unlock();
}
void showApp(std::shared_ptr<app::AppInstance> app);
void hideApp();
public:
bool onStart(ServiceContext& service) override;
void onStop(ServiceContext& service) override;
/**
* Show the on-screen keyboard.
* @param[in] textarea the textarea to focus the input for
*/
void softwareKeyboardShow(lv_obj_t* textarea);
/**
* Hide the on-screen keyboard.
* Has no effect when the keyboard is not visible.
*/
void softwareKeyboardHide();
void keyboardAddTextArea(lv_obj_t* textarea);
/**
* The on-screen keyboard is only shown when both of these conditions are true:
* - there is no hardware keyboard
* - TT_CONFIG_FORCE_ONSCREEN_KEYBOARD is set to true in tactility_config.h
* @return if we should show a on-screen keyboard for text input inside our apps
*/
bool softwareKeyboardIsEnabled();
};
std::shared_ptr<GuiService> findService();
} // namespace