Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Modules/app-module/include/app/manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ error_t app_manager_get_topmost_instance_id(AppInstanceId* out_app_instance_id);
*/
error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);

/**
* Resolves any instance's manifest id string, not just the topmost one - e.g. for a window that
* is buried in the window manager's stack.
* @param[in] app_instance_id the instance to look up
* @param[out] buffer always NULL-terminated on return (at least "" is written when
* @a buffer_size > 0; nothing is written when @a buffer_size == 0)
* @retval ERROR_NOT_FOUND no such instance (any more), or it has no registered manifest id
* @retval ERROR_BUFFER_OVERFLOW @a buffer_size is too small to hold the id (including the NULL
* terminator)
* @retval ERROR_NONE on success
*/
error_t app_manager_get_instance_app_id(AppInstanceId app_instance_id, char* buffer, size_t buffer_size);

/**
* Registers @a path as a directory to scan for app manifests - each direct subdirectory of
* @a path is expected to hold a manifest.properties (see app/package_manifest.h), matching the
Expand Down
23 changes: 23 additions & 0 deletions Modules/app-module/source/manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,29 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
return ERROR_NONE;
}

error_t app_manager_get_instance_app_id(AppInstanceId app_instance_id, char* buffer, size_t buffer_size) {
if (buffer_size == 0) {
return ERROR_BUFFER_OVERFLOW;
}
buffer[0] = '\0';

auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator == ledger.instances.end() || iterator->second.manifestId.empty()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
const std::string& manifest_id = iterator->second.manifestId;
if (manifest_id.size() >= buffer_size) {
mutex_unlock(&ledger.mutex);
return ERROR_BUFFER_OVERFLOW;
}
memcpy(buffer, manifest_id.c_str(), manifest_id.size() + 1);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}

} // extern "C"

namespace {
Expand Down
1 change: 1 addition & 0 deletions Modules/app-module/source/module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_manager_for_each_package),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_instance_id),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_id),
DEFINE_MODULE_SYMBOL(app_manager_get_instance_app_id),
DEFINE_MODULE_SYMBOL(app_manager_install_path_add),
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>

#include <stddef.h>
#include <stdint.h>

#ifdef __cplusplus
Expand All @@ -16,6 +17,9 @@ extern "C" {

typedef uint32_t WindowId;

/** A WindowId that never refers to a window: window_manager_create() returns a non-zero id. */
#define WINDOW_ID_NONE 0

enum WindowState {
/** id is the current topmost window and has live widgets. */
WINDOW_STATE_GRANTED,
Expand Down Expand Up @@ -133,6 +137,39 @@ 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);

/** One entry of the window stack, see window_manager_get_windows(). */
struct WindowInfo {
WindowId id;
AppInstanceId app_instance_id;
};

/**
* Copies the window stack, bottom-most first (the topmost window is the last entry).
* Only touches the module's own state (no LVGL calls), so it's safe to call from LVGL event
* callbacks.
* @param[out] out receives the entries; may be NULL to only get the count
* @param[in] offset the index of the first entry to copy (0 is the bottom-most window)
* @param[in] capacity the number of entries @a out can hold
* @return the number of windows in the stack (which may exceed offset + capacity)
*/
size_t window_manager_get_windows(struct WindowInfo* out, size_t offset, size_t capacity);

/**
* Moves a window to the top of the stack and builds its widgets, deleting the widgets of
* whichever window was topmost. The raised app keeps running: its widgets are rebuilt through
* the @a create_widgets callback it registered, so the app's own state (in its Context) survives.
* This is what "home" (raise the launcher) and the recents switcher (raise another app) use.
* @warning Takes the window manager's lifecycle mutex, which is held while app windows are
* built. Do NOT call this from an LVGL event callback: a concurrent
* window_manager_create()/remove() holding that mutex may be waiting for the LVGL lock, which
* the callback holds. Defer it with tt::dispatchOnMainDispatcher() instead (see
* SystemGestures.cpp).
* @retval ERROR_NONE the window was raised, or already is topmost
* @retval ERROR_INVALID_STATE window_manager_start() hasn't been called
* @retval ERROR_NOT_FOUND no window with this id exists
*/
error_t window_manager_raise(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
Expand Down
2 changes: 2 additions & 0 deletions Modules/lvgl-window-manager-module/source/module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(window_manager_create_ext),
DEFINE_MODULE_SYMBOL(window_manager_remove),
DEFINE_MODULE_SYMBOL(window_manager_get_state),
DEFINE_MODULE_SYMBOL(window_manager_get_windows),
DEFINE_MODULE_SYMBOL(window_manager_raise),
DEFINE_MODULE_SYMBOL(window_manager_await_state_change),
DEFINE_MODULE_SYMBOL(window_manager_overlay_show),
DEFINE_MODULE_SYMBOL(window_manager_overlay_hide),
Expand Down
76 changes: 76 additions & 0 deletions Modules/lvgl-window-manager-module/source/window_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,82 @@ WindowState window_manager_get_state(WindowId id) {
return is_top ? WINDOW_STATE_GRANTED : WINDOW_STATE_REVOKED;
}

size_t window_manager_get_windows(WindowInfo* out, size_t offset, size_t capacity) {
auto& s = state();
mutex_lock(&s.mutex);
const size_t count = s.windows.size();
if (out != nullptr) {
for (size_t i = 0; i < capacity && offset + i < count; i++) {
out[i] = WindowInfo { s.windows[offset + i].id, s.windows[offset + i].app_instance_id };
}
}
mutex_unlock(&s.mutex);
return count;
}

error_t window_manager_raise(WindowId id) {
auto& s = state();

// Same serialization as create()/remove(), see lifecycle_mutex's comment. Do not call this
// from an LVGL event callback - see this function's warning in the header.
mutex_lock(&s.lifecycle_mutex);

mutex_lock(&s.mutex);
if (!s.started) {
mutex_unlock(&s.mutex);
mutex_unlock(&s.lifecycle_mutex);
return ERROR_INVALID_STATE;
}
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 ERROR_NOT_FOUND;
}
if (iterator + 1 == s.windows.end()) {
// Already topmost: its widgets are live and there's nothing to rebuild.
mutex_unlock(&s.mutex);
mutex_unlock(&s.lifecycle_mutex);
return ERROR_NONE;
}

// The current topmost window is about to lose its widgets. Claim its waiter and capture what
// its destroy_widgets needs, exactly like create() does when it supersedes it.
WindowWaitSignal* waiter = claim_waiter_locked(s.windows.back());
WindowDestroyWidgetsFn old_destroy_widgets = s.windows.back().destroy_widgets;
void* old_user_data = s.windows.back().user_data;
lv_obj_t* old_widget = s.top_widget;
s.top_widget = nullptr;

// A buried window never has a waiting signal (it's only ever set while topmost).
const WindowRecord record = *iterator;
s.windows.erase(iterator);
s.windows.push_back(record);
lv_obj_t* content = s.content_root_widget;
mutex_unlock(&s.mutex);

give_and_release(waiter);

delete_widget(old_widget, old_destroy_widgets, old_user_data);
lv_obj_t* new_widget = build_window_widget(content, record.create_widgets, record.user_data);

mutex_lock(&s.mutex);
bool still_topmost = !s.windows.empty() && s.windows.back().id == id;
if (still_topmost) {
s.top_widget = new_widget;
new_widget = nullptr; // consumed
}
mutex_unlock(&s.mutex);

// Another window became topmost while this one was building (e.g. a concurrent create()).
// The raised record stays in the stack; its widgets are rebuilt whenever it surfaces again.
delete_widget(new_widget, record.destroy_widgets, record.user_data);

mutex_unlock(&s.lifecycle_mutex);
return ERROR_NONE;
}

WindowState window_manager_await_state_change(WindowId id, TickType_t timeout) {
auto& s = state();

Expand Down
8 changes: 8 additions & 0 deletions Tactility/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ menu "Tactility App"
bool "Set true when a touch screen calibration is required before the device is usable"
default n
depends on TT_TOUCH_CALIBRATION_SUPPORTED
config TT_SYSTEM_GESTURES
bool "System gestures for touch screens"
default y
help
Edge swipes: up from the bottom edge goes home (hold it for the recents switcher), and
right from the left edge closes the topmost app. Devices without a pointer input device
ignore this at runtime, so keypad/encoder-only devices are unaffected. Disable it to
leave the code out of the firmware entirely.
config TT_DISPLAY_IDLE_TICK_MS
int "Display idle tick interval (ms)"
default 50
Expand Down
56 changes: 56 additions & 0 deletions Tactility/Private/Tactility/Backlight.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

#include <tactility/device.h>
#include <tactility/drivers/backlight.h>
#include <tactility/drivers/display.h>

#include <stdint.h>

namespace tt::backlight {

/**
* The backlight of the device's first display, or nullptr when it has none.
* Boards not yet migrated to the kernel display driver register a placeholder display device with
* a NULL api, which has no backlight to hand out.
* @warning The caller owns the returned device and must release it with device_put().
*/
inline Device* findDisplayBacklight() {
Device* display = nullptr;
if (device_get_first_by_type(&DISPLAY_TYPE, &display) != ERROR_NONE) {
return nullptr;
}
if (device_get_driver(display)->api == nullptr) {
device_put(display);
return nullptr;
}
Device* found = nullptr;
display_get_backlight(display, &found);
device_put(display);
return found;
}

/** @return true when the backlight can be dimmed, rather than only switched on and off */
inline bool isDimmable(Device* backlight) {
const int32_t min = backlight_get_min_brightness(backlight);
const int32_t max = backlight_get_max_brightness(backlight);
// A 0-1 range is an on/off switch: every slider position would map to "on"
return max > min && !(min == 0 && max == 1);
}

/** @return the backlight's device units as a percentage of its range */
inline int32_t toPercent(Device* backlight, int32_t brightness) {
const int32_t min = backlight_get_min_brightness(backlight);
const int32_t range = backlight_get_max_brightness(backlight) - min;
return range > 0 ? ((brightness - min) * 100 + range / 2) / range : 100;
}

/** @return a percentage as the backlight's device units. 0% is the dimmest setting that is still on. */
inline int32_t toBrightness(Device* backlight, int32_t percent) {
const int32_t min = backlight_get_min_brightness(backlight);
const int32_t range = backlight_get_max_brightness(backlight) - min;
const int32_t brightness = min + (percent * range + 50) / 100;
// A brightness of 0 turns the backlight off
return brightness < 1 ? 1 : brightness;
}

}
14 changes: 14 additions & 0 deletions Tactility/Private/Tactility/app/AppIcon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include <app/manifest.h>

namespace tt::app {

/**
* The shared icon font character for an app, as the launcher and the recents switcher show it.
* @param[in] manifest the app to look up
* @return the icon, or a generic one for apps without a specific icon
*/
const char* getAppIcon(const ::AppManifest& manifest);

}
70 changes: 70 additions & 0 deletions Tactility/Private/Tactility/lvgl/SystemGestures.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#pragma once

#include <stdint.h>

namespace tt::lvgl {

/** The width of the strip along an edge in which a drag belongs to the system instead of the app. */
constexpr int32_t SYSTEM_GESTURE_EDGE_SIZE = 20;
/** The minimum travel for a drag that starts in an edge strip to count as a swipe. */
constexpr int32_t SYSTEM_GESTURE_SWIPE_DISTANCE = 40;

/** The screen edges that the system takes drags from. */
enum class SystemGestureEdge {
None,
Bottom,
Left,
};

/**
* Which system gesture a drag that starts at a screen edge belongs to.
* A drag that starts in a strip is the system's, whatever it is over: that's what makes the
* gestures work on top of scrollable apps. Everywhere else, apps keep their own swipes.
* @param[in] x,y where the press started, in screen coordinates
* @param[in] width,height the display size
* @return the edge that owns the drag, or SystemGestureEdge::None
*/
constexpr SystemGestureEdge systemGestureEdgeAt(int32_t x, int32_t y, int32_t width, int32_t height) {
if (y >= height - SYSTEM_GESTURE_EDGE_SIZE) {
return SystemGestureEdge::Bottom;
}
if (x < SYSTEM_GESTURE_EDGE_SIZE) {
return SystemGestureEdge::Left;
}
return SystemGestureEdge::None;
}

/**
* @return true when the travel from the press to the release point is an upward swipe, rather
* than a mostly-sideways drag
*/
constexpr bool systemGestureIsSwipeUp(int32_t pressX, int32_t pressY, int32_t x, int32_t y) {
const int32_t dy = pressY - y;
return dy >= SYSTEM_GESTURE_SWIPE_DISTANCE && dy > (x > pressX ? x - pressX : pressX - x);
}

/**
* @return true when the travel from the press to the release point is a rightward swipe, rather
* than a mostly-vertical drag
*/
constexpr bool systemGestureIsSwipeRight(int32_t pressX, int32_t pressY, int32_t x, int32_t y) {
const int32_t dx = x - pressX;
return dx >= SYSTEM_GESTURE_SWIPE_DISTANCE && dx > (y > pressY ? y - pressY : pressY - y);
}

/**
* Starts listening to the pointer devices for the system gestures: swipe up from the bottom edge
* to go home (holding it opens the recents switcher), and swipe right from the left edge to close
* the topmost app. Does nothing on devices without a pointer device, so keypad/encoder-only
* devices behave as before.
* @warning Caller must hold the LVGL lock.
*/
void systemGesturesAttach();

/**
* Stops listening to the pointer devices.
* @warning Caller must hold the LVGL lock.
*/
void systemGesturesDetach();

}
12 changes: 12 additions & 0 deletions Tactility/Private/Tactility/lvgl/SystemRecents.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

namespace tt::lvgl {

/**
* Shows the recent-apps switcher: the app stack as horizontal cards, most recent first.
* Tapping a card raises that app's window (it keeps running), swiping a card up closes that app.
* @warning Caller must hold the LVGL lock.
*/
void systemRecentsShow();

}
Loading