mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Fixes and improvements
This commit is contained in:
parent
0b5c302aa2
commit
ae0d002f36
@ -24,6 +24,12 @@ macro(tactility_project_post project_name)
|
||||
idf_build_set_property(COMPILE_OPTIONS "-fno-omit-frame-pointer" APPEND)
|
||||
endif()
|
||||
|
||||
# Binds the app's references to its own symbols at link time. The C++ standard library's templates
|
||||
# (e.g. std::shared_ptr internals) keep default visibility despite -fvisibility=hidden, so without this
|
||||
# the linker leaves symbol relocations for them, which the ELF loader only resolves against the
|
||||
# firmware's exported symbols. On RISC-V that fails to load the app with "Can't find common <symbol>".
|
||||
idf_build_set_property(COMPILE_OPTIONS "-Wl,-Bsymbolic" APPEND)
|
||||
|
||||
# ESP-IDF's own <sys/ioctl.h> (esp_libc/platform_include) is found before app-module's, which adds
|
||||
# the terminal ioctls (TIOCGWINSZ) Tactility's ioctl() supports for app fds.
|
||||
# C and C++ only: assembly sources can't include a C header.
|
||||
|
||||
@ -84,6 +84,7 @@
|
||||
compatible = "galaxycore,gc9a01";
|
||||
horizontal-resolution = <240>;
|
||||
vertical-resolution = <240>;
|
||||
shape = <DISPLAY_SHAPE_CIRCLE>;
|
||||
mirror-y;
|
||||
invert-color;
|
||||
bgr-order;
|
||||
|
||||
@ -77,6 +77,9 @@
|
||||
compatible = "jadard,jd9853";
|
||||
horizontal-resolution = <172>;
|
||||
vertical-resolution = <320>;
|
||||
// The panel has rounded corners. The radius is estimated, as it isn't specified.
|
||||
shape = <DISPLAY_SHAPE_ROUNDED>;
|
||||
corner-radius = <40>;
|
||||
gap-x = <34>;
|
||||
invert-color;
|
||||
pin-dc = <&gpio0 45 GPIO_FLAG_NONE>;
|
||||
|
||||
@ -2,6 +2,8 @@ description: Galaxycore GC9A01 round display panel
|
||||
|
||||
bus: spi
|
||||
|
||||
include: ["display.yaml"]
|
||||
|
||||
compatible: "galaxycore,gc9a01"
|
||||
|
||||
properties:
|
||||
|
||||
@ -9,9 +9,13 @@ extern "C" {
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Gc9a01Config {
|
||||
// From display.yaml: a DisplayShapeType value and the corner radius for rounded displays
|
||||
uint8_t shape;
|
||||
uint16_t corner_radius;
|
||||
uint16_t horizontal_resolution;
|
||||
uint16_t vertical_resolution;
|
||||
int32_t gap_x;
|
||||
|
||||
@ -316,6 +316,11 @@ static error_t gc9a01_get_backlight(Device* device, Device** backlight) {
|
||||
|
||||
// endregion
|
||||
|
||||
static void gc9a01_get_shape(Device* device, DisplayShape* shape) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
*shape = { .shape = config->shape, .corner_radius = config->corner_radius };
|
||||
}
|
||||
|
||||
static const DisplayApi gc9a01_display_api = {
|
||||
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY |
|
||||
DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF |
|
||||
@ -343,6 +348,7 @@ static const DisplayApi gc9a01_display_api = {
|
||||
.get_frame_buffer_count = nullptr,
|
||||
.get_backlight = gc9a01_get_backlight,
|
||||
.has_capability = nullptr,
|
||||
.get_shape = gc9a01_get_shape,
|
||||
};
|
||||
|
||||
Driver gc9a01_driver = {
|
||||
|
||||
@ -2,6 +2,8 @@ description: Jadard JD9853 display panel
|
||||
|
||||
bus: spi
|
||||
|
||||
include: ["display.yaml"]
|
||||
|
||||
compatible: "jadard,jd9853"
|
||||
|
||||
properties:
|
||||
|
||||
@ -9,9 +9,13 @@ extern "C" {
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Jd9853Config {
|
||||
// From display.yaml: a DisplayShapeType value and the corner radius for rounded displays
|
||||
uint8_t shape;
|
||||
uint16_t corner_radius;
|
||||
uint16_t horizontal_resolution;
|
||||
uint16_t vertical_resolution;
|
||||
int32_t gap_x;
|
||||
|
||||
@ -329,6 +329,11 @@ static error_t jd9853_get_backlight(Device* device, Device** backlight) {
|
||||
|
||||
// endregion
|
||||
|
||||
static void jd9853_get_shape(Device* device, DisplayShape* shape) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
*shape = { .shape = config->shape, .corner_radius = config->corner_radius };
|
||||
}
|
||||
|
||||
static const DisplayApi jd9853_display_api = {
|
||||
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_CAP_SWAP_XY |
|
||||
DISPLAY_CAPABILITY_CAP_SET_GAP | DISPLAY_CAPABILITY_INVERT_COLOR | DISPLAY_CAPABILITY_ON_OFF |
|
||||
@ -356,6 +361,7 @@ static const DisplayApi jd9853_display_api = {
|
||||
.get_frame_buffer_count = nullptr,
|
||||
.get_backlight = jd9853_get_backlight,
|
||||
.has_capability = nullptr,
|
||||
.get_shape = jd9853_get_shape,
|
||||
};
|
||||
|
||||
Driver jd9853_driver = {
|
||||
|
||||
@ -13,7 +13,7 @@ These applications are not part of the Tactility operating system's main firmwar
|
||||
|
||||
The intentions behind picking the licenses for the subprojects:
|
||||
|
||||
1. Forks of the entire Tactility project required to be open source as: GPL v3.0 applies to the main firmware projects (see `Firmware/` and `Tactility/`).
|
||||
1. Forks of the entire Tactility project required to be open source as: GPL v3.0 applies to the main firmware project (see `Tactility/`).
|
||||
2. It should be possible to make closed source external applications using TactilitySDK and the libraries it includes (it must exclude software with a GPL license).
|
||||
3. It should be possible to make closed source forks that only contain TactilityKernel, the platform implementations (`Platforms/*`) and most of the device and driver implementations. As few as possible driver and device combinations should prevent this.
|
||||
|
||||
@ -30,12 +30,12 @@ The license texts that are relevant to this document:
|
||||
|
||||
**IMPORTANT:** This document may have inaccuracies. It mainly exists to create awareness about license differences.
|
||||
|
||||
The top-level projects `Firmware/` and `Tactility/` are licensed under `GPL v3.0`.
|
||||
The top-level project `Tactility/` is licensed under `GPL v3.0`.
|
||||
|
||||
Most devices are licensed with `Apache License v2.0` while some are implemented as `GPL v3.0` until author consent is given to change the license.
|
||||
New device implementations should be licensed under `Apache License v2.0`.
|
||||
|
||||
Most drivers have an `Apache License v2.0`, with exceptions such as `Drivers/gps-generic-module/`.
|
||||
Most drivers have an `Apache License v2.0`, with exceptions such as `Drivers/gps-meshtastic-module/`.
|
||||
Licensing may also differ for subprojects intended for use in external applications.
|
||||
|
||||
Driver subprojects aren't generally used directly in external app projects, but if they are, make sure to check their licenses.
|
||||
@ -65,7 +65,6 @@ Below is an overview of the licenses of some of the subprojects.
|
||||
| Project | License |
|
||||
|--------------------|-------------------------|
|
||||
| Tactility | GNU Public License v3.0 |
|
||||
| TactilityC | Apache License v2.0 |
|
||||
| TactilityFreeRTOS | Apache License v2.0 |
|
||||
| TactilityKernel | Apache License v2.0 |
|
||||
| Tests | (varies) |
|
||||
|
||||
@ -28,7 +28,7 @@ enum AppCategory {
|
||||
|
||||
/** Bit flags for AppManifest::flags. */
|
||||
enum AppManifestFlags {
|
||||
/** Excluded from generic app-browsing UIs (e.g. AppList, Settings) */
|
||||
/** Excluded from generic app-browsing UIs (e.g. Launcher, Settings) */
|
||||
APP_MANIFEST_FLAG_HIDDEN = 1 << 0,
|
||||
/** No window-manager dependency. Safe to start from a context with no GUI available. */
|
||||
APP_MANIFEST_FLAG_HEADLESS = 1 << 1,
|
||||
|
||||
@ -183,6 +183,7 @@ shared_symbol_code_point_names = [
|
||||
statusbar_symbol_code_point_names = [
|
||||
# Location tracking
|
||||
"location_on",
|
||||
"location_off",
|
||||
# Development server
|
||||
"cloud",
|
||||
# Low on available memory
|
||||
|
||||
@ -17,8 +17,8 @@ extern "C" {
|
||||
* @brief Trackball operating mode.
|
||||
*/
|
||||
enum LvglTrackballMode {
|
||||
/** Navigation via enc_diff (scroll wheel behavior). No cursor is shown. */
|
||||
LVGL_TRACKBALL_MODE_ENCODER,
|
||||
/** Navigation with arrow keys, and the button as the enter key. No cursor is shown. */
|
||||
LVGL_TRACKBALL_MODE_KEYS,
|
||||
/** Mouse cursor via point.x/point.y. A cursor is shown if an image was set via
|
||||
* lvgl_trackball_set_cursor_image(). */
|
||||
LVGL_TRACKBALL_MODE_POINTER
|
||||
@ -33,8 +33,8 @@ struct LvglTrackballSettings {
|
||||
/** When false, movement and button presses are drained from the device and discarded:
|
||||
* the indev reports no movement and stays released. */
|
||||
bool enabled;
|
||||
/** Encoder mode: steps per raw trackball tick. Must be >= 1. */
|
||||
uint8_t encoder_sensitivity;
|
||||
/** Keys mode: arrow key presses per raw trackball tick. Must be >= 1. */
|
||||
uint8_t key_sensitivity;
|
||||
/** Pointer mode: pixels per raw trackball tick. Must be >= 1. */
|
||||
uint8_t pointer_sensitivity;
|
||||
};
|
||||
@ -43,7 +43,7 @@ struct LvglTrackballSettings {
|
||||
* @brief Creates an lv_indev_t bound to the given TRACKBALL_TYPE device and registers a read
|
||||
* callback that polls the device through its TrackballApi.
|
||||
*
|
||||
* The indev starts in LVGL_TRACKBALL_MODE_ENCODER, enabled, with default sensitivities (see
|
||||
* The indev starts in LVGL_TRACKBALL_MODE_KEYS, enabled, with default sensitivities (see
|
||||
* lvgl_trackball_settings_get_default()). Call lvgl_trackball_set_settings() to change this,
|
||||
* e.g. to switch to pointer mode - each device added this way can independently run with or
|
||||
* without pointer support.
|
||||
@ -68,8 +68,8 @@ error_t lvgl_trackball_add(struct Device* device, lv_display_t* display, lv_inde
|
||||
void lvgl_trackball_remove(lv_indev_t* indev);
|
||||
|
||||
/**
|
||||
* @return the default (all fields explicitly set) LvglTrackballSettings: encoder mode, enabled,
|
||||
* encoder_sensitivity 1, pointer_sensitivity 10.
|
||||
* @return the default (all fields explicitly set) LvglTrackballSettings: keys mode, enabled,
|
||||
* key_sensitivity 1, pointer_sensitivity 10.
|
||||
*/
|
||||
struct LvglTrackballSettings lvgl_trackball_settings_get_default(void);
|
||||
|
||||
|
||||
@ -21,12 +21,12 @@ void lvgl_grid_navigation_add(lv_obj_t* container);
|
||||
void lvgl_grid_navigation_remove(lv_obj_t* container);
|
||||
|
||||
/**
|
||||
* @brief Moves the focus to the next or previous child in their order, for devices that can only step through widgets.
|
||||
* Past the last or first child, the focus moves to the next or previous object in the container's focus group.
|
||||
* @param[in] container a container that was passed to lvgl_grid_navigation_add()
|
||||
* @param[in] next true for the next child, false for the previous child
|
||||
* @brief Moves the focus to the next or previous widget, for devices that can only step through widgets.
|
||||
* The children of grid navigation containers are visited in their order: entering a container forwards focuses its first child, and backwards its last child.
|
||||
* @param[in] group the focus group
|
||||
* @param[in] next true for the next widget, false for the previous widget
|
||||
*/
|
||||
void lvgl_grid_navigation_focus_next(lv_obj_t* container, bool next);
|
||||
void lvgl_grid_navigation_step(lv_group_t* group, bool next);
|
||||
|
||||
/**
|
||||
* @param[in] container a container that was passed to lvgl_grid_navigation_add()
|
||||
|
||||
@ -9,7 +9,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/** Changes whenever the icon TTF subset, its codepoints file or the icon name lists change */
|
||||
#define LVGL_ICON_FONT_VERSION 0xEE67835Du
|
||||
#define LVGL_ICON_FONT_VERSION 0x1B608834u
|
||||
|
||||
// Icon names per icon font, resolved through the .codepoints file next to the TTF
|
||||
extern const char* const lvgl_icon_shared_names[];
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#pragma once
|
||||
|
||||
#define LVGL_ICON_STATUSBAR_LOCATION_ON "\xEF\x87\x9B"
|
||||
#define LVGL_ICON_STATUSBAR_LOCATION_OFF "\xEE\x83\x87"
|
||||
#define LVGL_ICON_STATUSBAR_CLOUD "\xEF\x85\x9C"
|
||||
#define LVGL_ICON_STATUSBAR_MEMORY "\xEE\x8C\xA2"
|
||||
#define LVGL_ICON_STATUSBAR_SD_CARD "\xEE\x98\xA3"
|
||||
|
||||
55
Modules/lvgl-module/include/lvgl/insets.h
Normal file
55
Modules/lvgl-module/include/lvgl/insets.h
Normal file
@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <tactility/drivers/display.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Margins in pixels on each side of a display */
|
||||
typedef struct {
|
||||
int32_t top;
|
||||
int32_t bottom;
|
||||
int32_t left;
|
||||
int32_t right;
|
||||
} LvglInsets;
|
||||
|
||||
/**
|
||||
* @brief Gets the shape of the display's visible area.
|
||||
* @param[in] display the display
|
||||
* @param[out] out_shape the shape: a rectangle when the display doesn't report one
|
||||
*/
|
||||
void lvgl_display_get_shape(lv_display_t* display, struct DisplayShape* out_shape);
|
||||
|
||||
/**
|
||||
* @brief Gets the margins that keep a rectangle inside the display's visible area.
|
||||
* They're all zero for rectangular displays.
|
||||
* @param[in] display the display
|
||||
* @param[out] out_insets the margins
|
||||
*/
|
||||
void lvgl_display_get_insets(lv_display_t* display, LvglInsets* out_insets);
|
||||
|
||||
/**
|
||||
* @brief Gets the horizontal margin on both sides of a row, for layouts that follow the display's edge.
|
||||
* @param[in] display the display
|
||||
* @param[in] y the row, in display coordinates
|
||||
* @return the margin in pixels on the left and on the right of the row
|
||||
*/
|
||||
int32_t lvgl_display_get_row_inset(lv_display_t* display, int32_t y);
|
||||
|
||||
/**
|
||||
* @brief Moves the object's content inward where it overlaps the curved edge of a round or rounded display.
|
||||
* The extra horizontal padding is added to the object's current padding, and follows its size changes.
|
||||
* Does nothing on rectangular displays.
|
||||
* @param[in] obj the object, with its own padding already set
|
||||
*/
|
||||
void lvgl_obj_add_edge_padding(lv_obj_t* obj);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -4,9 +4,22 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void lvgl_keyboard_on_start_lvgl();
|
||||
void lvgl_keyboard_on_stop_lvgl();
|
||||
|
||||
/**
|
||||
* @brief Translates a key codepoint into the LVGL key for the object that the indev's group focuses.
|
||||
* The arrow up and down keys move the focus to the previous and next widget, or within a grid navigation container.
|
||||
* @param[in] indev the indev that reports the key
|
||||
* @param[in] codepoint the key codepoint (see tactility/drivers/keyboard.h)
|
||||
* @return the LVGL key
|
||||
*/
|
||||
uint32_t lvgl_keyboard_translate_key(lv_indev_t* indev, uint32_t codepoint);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
12
Modules/lvgl-module/private/lvgl/insets_math.h
Normal file
12
Modules/lvgl-module/private/lvgl/insets_math.h
Normal file
@ -0,0 +1,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <lvgl/insets.h>
|
||||
|
||||
#include <tactility/drivers/display.h>
|
||||
|
||||
/** Calculates the insets of a display with the given shape and resolution */
|
||||
LvglInsets lvgl_insets_calculate(const DisplayShape& shape, int32_t width, int32_t height);
|
||||
|
||||
/** Calculates the horizontal margin of a row of a display with the given shape and resolution */
|
||||
int32_t lvgl_insets_calculate_row(const DisplayShape& shape, int32_t width, int32_t height, int32_t y);
|
||||
@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl/devices/keyboard.h>
|
||||
#include <lvgl/devices/device_context.h>
|
||||
#include <lvgl/devices/keyboard_private.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
|
||||
@ -56,14 +57,26 @@ static uint32_t codepoint_to_lv_key(uint32_t key) {
|
||||
case CODEPOINT_ARROW_UP: return LV_KEY_PREV;
|
||||
case CODEPOINT_ARROW_RIGHT: return LV_KEY_RIGHT;
|
||||
case CODEPOINT_ARROW_DOWN: return LV_KEY_NEXT;
|
||||
case CODEPOINT_FOCUS_NEXT: return LV_KEY_NEXT;
|
||||
case CODEPOINT_FOCUS_PREVIOUS: return LV_KEY_PREV;
|
||||
case CODEPOINT_HOME: return LV_KEY_HOME;
|
||||
case CODEPOINT_END: return LV_KEY_END;
|
||||
default: return key;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t lvgl_keyboard_translate_key(lv_indev_t* indev, uint32_t codepoint) {
|
||||
lv_group_t* group = lv_indev_get_group(indev);
|
||||
lv_obj_t* focused = group != nullptr ? lv_group_get_focused(group) : nullptr;
|
||||
if (lvgl_grid_navigation_is_container(focused)) {
|
||||
// Grid navigation moves the focus in two dimensions with the arrow keys
|
||||
if (codepoint == CODEPOINT_ARROW_UP) {
|
||||
return LV_KEY_UP;
|
||||
} else if (codepoint == CODEPOINT_ARROW_DOWN) {
|
||||
return LV_KEY_DOWN;
|
||||
}
|
||||
}
|
||||
return codepoint_to_lv_key(codepoint);
|
||||
}
|
||||
|
||||
static void lvgl_keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
|
||||
|
||||
@ -74,26 +87,19 @@ static void lvgl_keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
return;
|
||||
}
|
||||
|
||||
data->key = codepoint_to_lv_key(key_data.key);
|
||||
lv_group_t* group = lv_indev_get_group(indev);
|
||||
lv_obj_t* focused = group != nullptr ? lv_group_get_focused(group) : nullptr;
|
||||
if (lvgl_grid_navigation_is_container(focused)) {
|
||||
if (key_data.key == CODEPOINT_ARROW_UP) {
|
||||
// Grid navigation moves the focus in two dimensions with the arrow keys
|
||||
data->key = LV_KEY_UP;
|
||||
} else if (key_data.key == CODEPOINT_ARROW_DOWN) {
|
||||
data->key = LV_KEY_DOWN;
|
||||
} else if (key_data.key == CODEPOINT_FOCUS_NEXT || key_data.key == CODEPOINT_FOCUS_PREVIOUS) {
|
||||
// Devices that can only step through widgets visit the children in their order.
|
||||
// LVGL doesn't see these keys: the group would skip the children, as only the container is in the group.
|
||||
if (key_data.pressed) {
|
||||
lvgl_grid_navigation_focus_next(focused, key_data.key == CODEPOINT_FOCUS_NEXT);
|
||||
}
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
data->continue_reading = key_data.continue_reading;
|
||||
return;
|
||||
if (key_data.key == CODEPOINT_FOCUS_NEXT || key_data.key == CODEPOINT_FOCUS_PREVIOUS) {
|
||||
// Devices that can only step through widgets also visit the children of grid navigation containers.
|
||||
// LVGL doesn't see these keys: the group would skip the children, as only the container is in the group.
|
||||
if (key_data.pressed && group != nullptr) {
|
||||
lvgl_grid_navigation_step(group, key_data.key == CODEPOINT_FOCUS_NEXT);
|
||||
}
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
data->continue_reading = key_data.continue_reading;
|
||||
return;
|
||||
}
|
||||
|
||||
data->key = lvgl_keyboard_translate_key(indev, key_data.key);
|
||||
data->state = key_data.pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||||
data->continue_reading = key_data.continue_reading;
|
||||
}
|
||||
|
||||
@ -2,11 +2,15 @@
|
||||
#include <lvgl/devices/trackball.h>
|
||||
#include <lvgl/devices/device_context.h>
|
||||
#include <lvgl/devices/keyboard.h>
|
||||
#include <lvgl/devices/keyboard_private.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
|
||||
#include <tactility/drivers/keyboard.h>
|
||||
#include <tactility/drivers/trackball.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
constexpr auto* TAG = "lvgl_trackball";
|
||||
|
||||
@ -16,6 +20,11 @@ struct LvglTrackballCtx {
|
||||
int32_t cursor_y;
|
||||
lv_obj_t* cursor;
|
||||
const void* cursor_image_src;
|
||||
// Keys mode: arrow key presses that are still to be reported
|
||||
uint32_t pending_codepoint;
|
||||
int32_t pending_count;
|
||||
// Keys mode: the LVGL key that is reported as pressed, or 0
|
||||
uint32_t pressed_key;
|
||||
};
|
||||
|
||||
static inline int32_t clamp(int32_t value, int32_t min_value, int32_t max_value) {
|
||||
@ -68,6 +77,49 @@ static void hide_cursor(LvglTrackballCtx* ctx) {
|
||||
lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
// Reports one key event per read: the button as the enter key, and movement as arrow key presses along its strongest axis.
|
||||
static void read_keys(lv_indev_t* indev, LvglTrackballCtx* ctx, lv_indev_data_t* data, int32_t dx, int32_t dy, bool button_pressed) {
|
||||
if (dx != 0 || dy != 0) {
|
||||
uint32_t codepoint;
|
||||
if (std::abs(dx) >= std::abs(dy)) {
|
||||
codepoint = dx > 0 ? CODEPOINT_ARROW_RIGHT : CODEPOINT_ARROW_LEFT;
|
||||
} else {
|
||||
codepoint = dy > 0 ? CODEPOINT_ARROW_DOWN : CODEPOINT_ARROW_UP;
|
||||
}
|
||||
const int32_t steps = std::max(std::abs(dx), std::abs(dy)) * static_cast<int32_t>(ctx->settings.key_sensitivity);
|
||||
// A change of direction drops the presses that weren't reported yet
|
||||
if (codepoint != ctx->pending_codepoint) {
|
||||
ctx->pending_codepoint = codepoint;
|
||||
ctx->pending_count = 0;
|
||||
}
|
||||
ctx->pending_count = clamp(ctx->pending_count + steps, 0, INT16_MAX);
|
||||
lv_display_trigger_activity(lv_indev_get_display(indev));
|
||||
}
|
||||
|
||||
if (ctx->pressed_key == LV_KEY_ENTER && button_pressed) {
|
||||
data->key = LV_KEY_ENTER;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
} else if (ctx->pressed_key != 0) {
|
||||
// Every press is released before the next event, so LVGL sees each arrow key press
|
||||
data->key = ctx->pressed_key;
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
ctx->pressed_key = 0;
|
||||
data->continue_reading = ctx->pending_count > 0;
|
||||
} else if (button_pressed) {
|
||||
ctx->pressed_key = LV_KEY_ENTER;
|
||||
data->key = LV_KEY_ENTER;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
lv_display_trigger_activity(lv_indev_get_display(indev));
|
||||
} else if (ctx->pending_count > 0) {
|
||||
ctx->pending_count--;
|
||||
ctx->pressed_key = lvgl_keyboard_translate_key(indev, ctx->pending_codepoint);
|
||||
data->key = ctx->pressed_key;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
}
|
||||
}
|
||||
|
||||
static void lvgl_trackball_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
auto* wrapper = static_cast<LvglDeviceContext*>(lv_indev_get_driver_data(indev));
|
||||
auto* ctx = static_cast<LvglTrackballCtx*>(wrapper->context);
|
||||
@ -83,25 +135,22 @@ static void lvgl_trackball_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
|
||||
lv_display_t* display = lv_indev_get_display(indev);
|
||||
|
||||
if (ctx->settings.mode == LVGL_TRACKBALL_MODE_ENCODER) {
|
||||
int32_t ticks = (dx + dy) * static_cast<int32_t>(ctx->settings.encoder_sensitivity);
|
||||
data->enc_diff = static_cast<int16_t>(clamp(ticks, INT16_MIN, INT16_MAX));
|
||||
if (ticks != 0) {
|
||||
lv_display_trigger_activity(display);
|
||||
}
|
||||
} else {
|
||||
int32_t max_x = display != nullptr ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
|
||||
int32_t max_y = display != nullptr ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
|
||||
ctx->cursor_x = clamp(ctx->cursor_x + dx * static_cast<int32_t>(ctx->settings.pointer_sensitivity), 0, max_x);
|
||||
ctx->cursor_y = clamp(ctx->cursor_y + dy * static_cast<int32_t>(ctx->settings.pointer_sensitivity), 0, max_y);
|
||||
data->point.x = static_cast<int16_t>(ctx->cursor_x);
|
||||
data->point.y = static_cast<int16_t>(ctx->cursor_y);
|
||||
}
|
||||
|
||||
bool pressed = false;
|
||||
if (ctx->settings.enabled) {
|
||||
trackball_get_button_pressed(wrapper->device, &pressed);
|
||||
}
|
||||
|
||||
if (ctx->settings.mode == LVGL_TRACKBALL_MODE_KEYS) {
|
||||
read_keys(indev, ctx, data, dx, dy, pressed);
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t max_x = display != nullptr ? lv_display_get_original_horizontal_resolution(display) - 1 : 0;
|
||||
int32_t max_y = display != nullptr ? lv_display_get_original_vertical_resolution(display) - 1 : 0;
|
||||
ctx->cursor_x = clamp(ctx->cursor_x + dx * static_cast<int32_t>(ctx->settings.pointer_sensitivity), 0, max_x);
|
||||
ctx->cursor_y = clamp(ctx->cursor_y + dy * static_cast<int32_t>(ctx->settings.pointer_sensitivity), 0, max_y);
|
||||
data->point.x = static_cast<int16_t>(ctx->cursor_x);
|
||||
data->point.y = static_cast<int16_t>(ctx->cursor_y);
|
||||
data->state = pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||||
|
||||
if (pressed) {
|
||||
@ -113,9 +162,9 @@ extern "C" {
|
||||
|
||||
LvglTrackballSettings lvgl_trackball_settings_get_default() {
|
||||
return LvglTrackballSettings {
|
||||
.mode = LVGL_TRACKBALL_MODE_ENCODER,
|
||||
.mode = LVGL_TRACKBALL_MODE_KEYS,
|
||||
.enabled = true,
|
||||
.encoder_sensitivity = 1,
|
||||
.key_sensitivity = 1,
|
||||
.pointer_sensitivity = 10,
|
||||
};
|
||||
}
|
||||
@ -147,7 +196,7 @@ error_t lvgl_trackball_add(struct Device* device, lv_display_t* display, lv_inde
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER);
|
||||
lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD);
|
||||
lv_indev_set_read_cb(indev, lvgl_trackball_read_cb);
|
||||
lv_indev_set_driver_data(indev, wrapper);
|
||||
if (display != nullptr) {
|
||||
@ -155,10 +204,10 @@ error_t lvgl_trackball_add(struct Device* device, lv_display_t* display, lv_inde
|
||||
}
|
||||
recenter_cursor(ctx, indev);
|
||||
|
||||
// Encoder indevs are useless without a group (LVGL dispatch bails out immediately if indev->group is NULL)
|
||||
// Keypad indevs are useless without a group (LVGL dispatch bails out immediately if indev->group is NULL)
|
||||
// Use the keyboard group for now, until it's refactored into a proper global/shared group. See ideas.md
|
||||
// When refactoring this, you probably want to remove the calls to lvgl_keyboard_* below.
|
||||
if (ctx->settings.mode == LVGL_TRACKBALL_MODE_ENCODER) {
|
||||
if (ctx->settings.mode == LVGL_TRACKBALL_MODE_KEYS) {
|
||||
lvgl_keyboard_enable(indev);
|
||||
}
|
||||
|
||||
@ -188,8 +237,8 @@ error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackbal
|
||||
}
|
||||
|
||||
if (
|
||||
(settings->mode != LVGL_TRACKBALL_MODE_ENCODER && settings->mode != LVGL_TRACKBALL_MODE_POINTER) ||
|
||||
settings->encoder_sensitivity == 0 ||
|
||||
(settings->mode != LVGL_TRACKBALL_MODE_KEYS && settings->mode != LVGL_TRACKBALL_MODE_POINTER) ||
|
||||
settings->key_sensitivity == 0 ||
|
||||
settings->pointer_sensitivity == 0
|
||||
) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
@ -203,6 +252,8 @@ error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackbal
|
||||
ctx->settings = *settings;
|
||||
|
||||
if (mode_changed) {
|
||||
ctx->pending_count = 0;
|
||||
ctx->pressed_key = 0;
|
||||
if (settings->mode == LVGL_TRACKBALL_MODE_POINTER) {
|
||||
lvgl_keyboard_disable(indev);
|
||||
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
||||
@ -210,12 +261,12 @@ error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackbal
|
||||
show_cursor(ctx, indev);
|
||||
} else {
|
||||
hide_cursor(ctx);
|
||||
lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER);
|
||||
lv_indev_set_type(indev, LV_INDEV_TYPE_KEYPAD);
|
||||
lvgl_keyboard_enable(indev);
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor visibility only tracks the enabled toggle in pointer mode - in encoder mode it must
|
||||
// Cursor visibility only tracks the enabled toggle in pointer mode - in keys mode it must
|
||||
// stay hidden regardless of enabled, otherwise this unconditionally un-hides the cursor
|
||||
// hide_cursor() just hid above (enabled is independent of mode, and defaults to true).
|
||||
if (ctx->cursor != nullptr && ctx->settings.mode == LVGL_TRACKBALL_MODE_POINTER) {
|
||||
|
||||
@ -65,27 +65,47 @@ void lvgl_grid_navigation_remove(lv_obj_t* container) {
|
||||
|
||||
// Matches which children grid navigation can focus
|
||||
static bool is_focusable(lv_obj_t* obj) {
|
||||
return !lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN) && lv_obj_has_flag_any(obj, static_cast<lv_obj_flag_t>(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE));
|
||||
return !lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN) && lv_obj_has_flag(obj, static_cast<lv_obj_flag_t>(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE));
|
||||
}
|
||||
|
||||
void lvgl_grid_navigation_focus_next(lv_obj_t* container, bool next) {
|
||||
// Finds the next focusable child after (or before) the given index
|
||||
static lv_obj_t* find_focusable_child(lv_obj_t* container, int32_t index, bool next) {
|
||||
const auto child_count = static_cast<int32_t>(lv_obj_get_child_count(container));
|
||||
lv_obj_t* focused = lvgl_grid_navigation_get_focused(container);
|
||||
int32_t index = focused != nullptr ? lv_obj_get_index(focused) : (next ? -1 : child_count);
|
||||
for (index += next ? 1 : -1; index >= 0 && index < child_count; index += next ? 1 : -1) {
|
||||
auto* child = lv_obj_get_child(container, index);
|
||||
if (is_focusable(child)) {
|
||||
lv_gridnav_set_focused(container, child, LV_ANIM_ON);
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void lvgl_grid_navigation_step(lv_group_t* group, bool next) {
|
||||
lv_obj_t* focused = lv_group_get_focused(group);
|
||||
if (lvgl_grid_navigation_is_container(focused)) {
|
||||
lv_obj_t* focused_child = lvgl_grid_navigation_get_focused(focused);
|
||||
const int32_t index = focused_child != nullptr ? lv_obj_get_index(focused_child) : (next ? -1 : static_cast<int32_t>(lv_obj_get_child_count(focused)));
|
||||
lv_obj_t* child = find_focusable_child(focused, index, next);
|
||||
if (child != nullptr) {
|
||||
lv_gridnav_set_focused(focused, child, LV_ANIM_ON);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lv_group_t* group = lv_obj_get_group(container);
|
||||
if (group != nullptr) {
|
||||
if (next) {
|
||||
lv_group_focus_next(group);
|
||||
} else {
|
||||
lv_group_focus_prev(group);
|
||||
lv_group_set_editing(group, false);
|
||||
if (next) {
|
||||
lv_group_focus_next(group);
|
||||
} else {
|
||||
lv_group_focus_prev(group);
|
||||
}
|
||||
|
||||
// Grid navigation restores the child that was focused last, but stepping enters at the side it came from
|
||||
lv_obj_t* entered = lv_group_get_focused(group);
|
||||
if (lvgl_grid_navigation_is_container(entered)) {
|
||||
const int32_t start = next ? -1 : static_cast<int32_t>(lv_obj_get_child_count(entered));
|
||||
lv_obj_t* child = find_focusable_child(entered, start, next);
|
||||
if (child != nullptr) {
|
||||
lv_gridnav_set_focused(entered, child, LV_ANIM_ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,6 +58,7 @@ const size_t lvgl_icon_shared_name_count = sizeof(lvgl_icon_shared_names) / size
|
||||
|
||||
const char* const lvgl_icon_statusbar_names[] = {
|
||||
"location_on",
|
||||
"location_off",
|
||||
"cloud",
|
||||
"memory",
|
||||
"sd_card",
|
||||
|
||||
78
Modules/lvgl-module/source/insets.cpp
Normal file
78
Modules/lvgl-module/source/insets.cpp
Normal file
@ -0,0 +1,78 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/insets_math.h>
|
||||
#include <lvgl/devices/device_context.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <new>
|
||||
|
||||
static DisplayShape get_shape(lv_display_t* display) {
|
||||
DisplayShape shape = { .shape = DISPLAY_SHAPE_RECTANGLE, .corner_radius = 0 };
|
||||
const auto* wrapper = static_cast<const LvglDeviceContext*>(lv_display_get_driver_data(display));
|
||||
if (wrapper != nullptr && wrapper->device != nullptr) {
|
||||
display_get_shape(wrapper->device, &shape);
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
struct EdgePadding {
|
||||
lv_obj_t* obj;
|
||||
// The padding before the display's edge was added
|
||||
int32_t base_left;
|
||||
int32_t base_right;
|
||||
};
|
||||
|
||||
static void update_edge_padding(void* data) {
|
||||
const auto* padding = static_cast<const EdgePadding*>(data);
|
||||
lv_display_t* display = lv_obj_get_display(padding->obj);
|
||||
lv_area_t coords;
|
||||
lv_obj_get_coords(padding->obj, &coords);
|
||||
// The curve is widest at the object's edge that is nearest to the display's top or bottom
|
||||
const int32_t inset = std::max(lvgl_display_get_row_inset(display, coords.y1), lvgl_display_get_row_inset(display, coords.y2));
|
||||
const int32_t right_edge = lv_display_get_horizontal_resolution(display) - 1;
|
||||
lv_obj_set_style_pad_left(padding->obj, padding->base_left + std::max<int32_t>(0, inset - coords.x1), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_right(padding->obj, padding->base_right + std::max<int32_t>(0, inset - (right_edge - coords.x2)), LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
// The position is final once the layout pass that changed the size has finished
|
||||
static void on_size_changed(lv_event_t* event) {
|
||||
lv_async_call(update_edge_padding, lv_event_get_user_data(event));
|
||||
}
|
||||
|
||||
static void on_deleted(lv_event_t* event) {
|
||||
auto* padding = static_cast<EdgePadding*>(lv_event_get_user_data(event));
|
||||
lv_async_call_cancel(update_edge_padding, padding);
|
||||
delete padding;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
void lvgl_obj_add_edge_padding(lv_obj_t* obj) {
|
||||
if (get_shape(lv_obj_get_display(obj)).shape == DISPLAY_SHAPE_RECTANGLE) {
|
||||
return;
|
||||
}
|
||||
auto* padding = new(std::nothrow) EdgePadding {
|
||||
.obj = obj,
|
||||
.base_left = lv_obj_get_style_pad_left(obj, LV_PART_MAIN),
|
||||
.base_right = lv_obj_get_style_pad_right(obj, LV_PART_MAIN)
|
||||
};
|
||||
if (padding == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_obj_add_event_cb(obj, on_size_changed, LV_EVENT_SIZE_CHANGED, padding);
|
||||
lv_obj_add_event_cb(obj, on_deleted, LV_EVENT_DELETE, padding);
|
||||
}
|
||||
|
||||
void lvgl_display_get_shape(lv_display_t* display, DisplayShape* out_shape) {
|
||||
*out_shape = get_shape(display);
|
||||
}
|
||||
|
||||
void lvgl_display_get_insets(lv_display_t* display, LvglInsets* out_insets) {
|
||||
*out_insets = lvgl_insets_calculate(get_shape(display), lv_display_get_horizontal_resolution(display), lv_display_get_vertical_resolution(display));
|
||||
}
|
||||
|
||||
int32_t lvgl_display_get_row_inset(lv_display_t* display, int32_t y) {
|
||||
return lvgl_insets_calculate_row(get_shape(display), lv_display_get_horizontal_resolution(display), lv_display_get_vertical_resolution(display), y);
|
||||
}
|
||||
|
||||
}
|
||||
57
Modules/lvgl-module/source/insets_math.cpp
Normal file
57
Modules/lvgl-module/source/insets_math.cpp
Normal file
@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl/insets_math.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// The part of a corner's square that a circle with the given radius cuts off along its diagonal
|
||||
static int32_t corner_inset(int32_t radius) {
|
||||
return static_cast<int32_t>(std::ceil(radius * (1.0 - M_SQRT1_2)));
|
||||
}
|
||||
|
||||
// The horizontal distance between a circle's edge and its bounding box at the given distance from the circle's center row
|
||||
static int32_t circle_row_inset(int32_t radius, int32_t distance_from_center) {
|
||||
if (distance_from_center >= radius) {
|
||||
return radius;
|
||||
}
|
||||
const double half_width = std::sqrt(static_cast<double>(radius) * radius - static_cast<double>(distance_from_center) * distance_from_center);
|
||||
return static_cast<int32_t>(std::ceil(radius - half_width));
|
||||
}
|
||||
|
||||
LvglInsets lvgl_insets_calculate(const DisplayShape& shape, int32_t width, int32_t height) {
|
||||
switch (shape.shape) {
|
||||
case DISPLAY_SHAPE_ROUNDED: {
|
||||
const int32_t radius = std::min<int32_t>(shape.corner_radius, std::min(width, height) / 2);
|
||||
const int32_t inset = corner_inset(radius);
|
||||
return { .top = inset, .bottom = inset, .left = 0, .right = 0 };
|
||||
}
|
||||
case DISPLAY_SHAPE_CIRCLE: {
|
||||
const int32_t diameter = std::min(width, height);
|
||||
const int32_t inset = corner_inset(diameter / 2);
|
||||
const int32_t horizontal = inset + (width - diameter) / 2;
|
||||
const int32_t vertical = inset + (height - diameter) / 2;
|
||||
return { .top = vertical, .bottom = vertical, .left = horizontal, .right = horizontal };
|
||||
}
|
||||
default:
|
||||
return { .top = 0, .bottom = 0, .left = 0, .right = 0 };
|
||||
}
|
||||
}
|
||||
|
||||
int32_t lvgl_insets_calculate_row(const DisplayShape& shape, int32_t width, int32_t height, int32_t y) {
|
||||
switch (shape.shape) {
|
||||
case DISPLAY_SHAPE_ROUNDED: {
|
||||
const int32_t radius = std::min<int32_t>(shape.corner_radius, std::min(width, height) / 2);
|
||||
// Distance from the row to the center row of the nearest corner's circle
|
||||
const int32_t distance = std::max(radius - y, y - (height - 1 - radius));
|
||||
return distance > 0 ? circle_row_inset(radius, distance) : 0;
|
||||
}
|
||||
case DISPLAY_SHAPE_CIRCLE: {
|
||||
const int32_t diameter = std::min(width, height);
|
||||
const int32_t radius = diameter / 2;
|
||||
const int32_t distance = std::abs(y - height / 2);
|
||||
return std::min(circle_row_inset(radius, distance) + (width - diameter) / 2, width / 2);
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@
|
||||
|
||||
#include <lvgl/widgets/sliderbox.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/badge.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
@ -65,9 +66,14 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
// lvgl_grid_navigation
|
||||
DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_add),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_remove),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_focus_next),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_step),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_get_focused),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_is_container),
|
||||
// lvgl_insets
|
||||
DEFINE_MODULE_SYMBOL(lvgl_display_get_shape),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_display_get_insets),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_display_get_row_inset),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_obj_add_edge_padding),
|
||||
// lvgl_theme
|
||||
DEFINE_MODULE_SYMBOL(lvgl_theme_get_settings),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_theme_get_default_settings),
|
||||
|
||||
@ -137,7 +137,7 @@ extern const lv_obj_class_t lvgl_badge_class;
|
||||
#define CHIP_SHAPE (theme->config.is_compact ? CORNER_COMPACT : CORNER_SMALL)
|
||||
#define CHIP_PAD_HOR (theme->config.is_compact ? 3 : SPACE_4)
|
||||
#define CHIP_PAD_VER (theme->config.is_compact ? 2 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
#define BADGE_COLOR COLOR_PRIMARY
|
||||
#define BADGE_COLOR COLOR_SECONDARY
|
||||
#define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
#define SCROLLBAR_COLOR COLOR_OUTLINE
|
||||
#define SCROLLBAR_OPACITY (LV_OPA_40)
|
||||
@ -161,7 +161,7 @@ extern const lv_obj_class_t lvgl_badge_class;
|
||||
#define TEXT_BUTTON_LABEL_COLOR COLOR_PRIMARY
|
||||
#define TEXT_BUTTON_PRESSED_CONTAINER_COLOR COLOR_PRIMARY
|
||||
#define TEXT_BUTTON_PAD_HOR (theme->config.is_compact ? 3 : SPACE_3)
|
||||
#define ICON_BUTTON_COLOR COLOR_ON_SURFACE_VARIANT
|
||||
#define ICON_BUTTON_COLOR COLOR_ON_SURFACE
|
||||
#define ICON_BUTTON_CONTAINER_SHAPE (theme->config.is_compact ? CORNER_COMPACT : CORNER_LARGE)
|
||||
#define ICON_BUTTON_FILLED_COLOR COLOR_PRIMARY
|
||||
#define ICON_BUTTON_FILLED_ICON_COLOR COLOR_ON_PRIMARY
|
||||
@ -396,7 +396,7 @@ typedef struct {
|
||||
lv_style_t checkbox_box_selected;
|
||||
#endif
|
||||
lv_style_t sliderbox;
|
||||
#if LV_USE_SLIDER
|
||||
#if LV_USE_BAR || LV_USE_SLIDER
|
||||
lv_style_t slider_track_size;
|
||||
#endif
|
||||
#if LV_USE_SLIDER
|
||||
@ -874,7 +874,7 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_bg_opa(&theme->styles.app_bar_button_pressed, APP_BAR_PRESSED_OPACITY);
|
||||
|
||||
style_init_reset(&theme->styles.app_bar_focus_ring);
|
||||
lv_style_set_outline_color(&theme->styles.app_bar_focus_ring, APP_BAR_CONTENT_COLOR);
|
||||
lv_style_set_outline_color(&theme->styles.app_bar_focus_ring, FOCUS_RING_COLOR);
|
||||
lv_style_set_outline_width(&theme->styles.app_bar_focus_ring, APP_BAR_FOCUS_WIDTH);
|
||||
lv_style_set_outline_pad(&theme->styles.app_bar_focus_ring, -APP_BAR_FOCUS_WIDTH);
|
||||
lv_style_set_outline_opa(&theme->styles.app_bar_focus_ring, LV_OPA_COVER);
|
||||
@ -957,7 +957,7 @@ static void style_init(my_theme_t * theme)
|
||||
style_init_reset(&theme->styles.sliderbox);
|
||||
lv_style_set_pad_column(&theme->styles.sliderbox, SLIDERBOX_GAP);
|
||||
|
||||
#if LV_USE_SLIDER
|
||||
#if LV_USE_BAR || LV_USE_SLIDER
|
||||
style_init_reset(&theme->styles.slider_track_size);
|
||||
lv_style_set_height(&theme->styles.slider_track_size, SLIDER_TRACK_HEIGHT);
|
||||
#endif
|
||||
@ -1838,6 +1838,13 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
lv_obj_add_style(obj, &theme->styles.badge, 0);
|
||||
return;
|
||||
}
|
||||
#if LV_USE_IMAGE
|
||||
if(lv_obj_check_type(obj, &lv_image_class)) {
|
||||
/* image */
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring_inset, LV_STATE_FOCUS_KEY);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if(lv_obj_check_type(obj, &lvgl_chip_class)) {
|
||||
/* chip */
|
||||
lv_obj_add_style(obj, &theme->styles.chip, 0);
|
||||
@ -1878,6 +1885,7 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
#if LV_USE_BAR
|
||||
if(lv_obj_check_type(obj, &lv_bar_class)) {
|
||||
/* bar */
|
||||
lv_obj_add_style(obj, &theme->styles.slider_track_size, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.progress_track, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.progress_indicator, LV_PART_INDICATOR);
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY);
|
||||
|
||||
@ -250,7 +250,7 @@ typedef struct {
|
||||
lv_style_t checkbox_box_selected;
|
||||
#endif
|
||||
lv_style_t sliderbox;
|
||||
#if LV_USE_SLIDER
|
||||
#if LV_USE_BAR || LV_USE_SLIDER
|
||||
lv_style_t slider_track_size;
|
||||
#endif
|
||||
#if LV_USE_SLIDER
|
||||
@ -688,7 +688,7 @@ static void style_init(my_theme_t * theme)
|
||||
style_init_reset(&theme->styles.sliderbox);
|
||||
lv_style_set_pad_column(&theme->styles.sliderbox, SLIDERBOX_GAP);
|
||||
|
||||
#if LV_USE_SLIDER
|
||||
#if LV_USE_BAR || LV_USE_SLIDER
|
||||
style_init_reset(&theme->styles.slider_track_size);
|
||||
lv_style_set_height(&theme->styles.slider_track_size, SLIDER_TRACK_HEIGHT);
|
||||
#endif
|
||||
@ -1470,6 +1470,7 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
#if LV_USE_BAR
|
||||
if(lv_obj_check_type(obj, &lv_bar_class)) {
|
||||
/* bar */
|
||||
lv_obj_add_style(obj, &theme->styles.slider_track_size, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.slider_track, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.slider_active_track, LV_PART_INDICATOR);
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY);
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
#include <lvgl/widgets/spinner.h>
|
||||
@ -100,6 +101,9 @@ lv_obj_t* lvgl_toolbar_create(lv_obj_t* parent, const char* title) {
|
||||
toolbar->nav_action_callback = nullptr;
|
||||
|
||||
lv_obj_center(obj);
|
||||
|
||||
lvgl_obj_add_edge_padding(obj);
|
||||
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
// The arrow keys move between the close button and the actions, and up and down leave the toolbar
|
||||
|
||||
22
Modules/lvgl-module/tests/CMakeLists.txt
Normal file
22
Modules/lvgl-module/tests/CMakeLists.txt
Normal file
@ -0,0 +1,22 @@
|
||||
project(LvglModuleTests)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||
add_executable(LvglModuleTests EXCLUDE_FROM_ALL
|
||||
${TEST_SOURCES}
|
||||
${PROJECT_SOURCE_DIR}/../source/insets_math.cpp
|
||||
)
|
||||
|
||||
target_include_directories(LvglModuleTests PRIVATE
|
||||
${DOCTESTINC}
|
||||
${PROJECT_SOURCE_DIR}/../include
|
||||
${PROJECT_SOURCE_DIR}/../private
|
||||
)
|
||||
|
||||
add_test(NAME LvglModuleTests COMMAND LvglModuleTests)
|
||||
|
||||
target_link_libraries(LvglModuleTests PUBLIC
|
||||
TactilityKernel
|
||||
lvgl
|
||||
)
|
||||
62
Modules/lvgl-module/tests/source/insets_math_test.cpp
Normal file
62
Modules/lvgl-module/tests/source/insets_math_test.cpp
Normal file
@ -0,0 +1,62 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <lvgl/insets_math.h>
|
||||
|
||||
static constexpr DisplayShape RECTANGLE = { .shape = DISPLAY_SHAPE_RECTANGLE, .corner_radius = 0 };
|
||||
static constexpr DisplayShape CIRCLE = { .shape = DISPLAY_SHAPE_CIRCLE, .corner_radius = 0 };
|
||||
static constexpr DisplayShape ROUNDED_40 = { .shape = DISPLAY_SHAPE_ROUNDED, .corner_radius = 40 };
|
||||
|
||||
TEST_CASE("a rectangle has no insets") {
|
||||
const auto insets = lvgl_insets_calculate(RECTANGLE, 320, 240);
|
||||
CHECK_EQ(insets.top, 0);
|
||||
CHECK_EQ(insets.bottom, 0);
|
||||
CHECK_EQ(insets.left, 0);
|
||||
CHECK_EQ(insets.right, 0);
|
||||
CHECK_EQ(lvgl_insets_calculate_row(RECTANGLE, 320, 240, 0), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("a circle's insets fit the inscribed square") {
|
||||
const auto insets = lvgl_insets_calculate(CIRCLE, 240, 240);
|
||||
// 120 * (1 - 1/sqrt(2)) = 35.1
|
||||
CHECK_EQ(insets.top, 36);
|
||||
CHECK_EQ(insets.bottom, 36);
|
||||
CHECK_EQ(insets.left, 36);
|
||||
CHECK_EQ(insets.right, 36);
|
||||
}
|
||||
|
||||
TEST_CASE("a circle on a wider panel is centered horizontally") {
|
||||
const auto insets = lvgl_insets_calculate(CIRCLE, 280, 240);
|
||||
CHECK_EQ(insets.top, 36);
|
||||
CHECK_EQ(insets.left, 56);
|
||||
CHECK_EQ(insets.right, 56);
|
||||
}
|
||||
|
||||
TEST_CASE("a circle's row inset follows its edge") {
|
||||
CHECK_EQ(lvgl_insets_calculate_row(CIRCLE, 240, 240, 120), 0);
|
||||
CHECK_EQ(lvgl_insets_calculate_row(CIRCLE, 240, 240, 0), 120);
|
||||
// Halfway between the center and the top: 120 - sqrt(120^2 - 60^2) = 16.1
|
||||
CHECK_EQ(lvgl_insets_calculate_row(CIRCLE, 240, 240, 60), 17);
|
||||
}
|
||||
|
||||
TEST_CASE("a rounded rectangle has insets at the top and bottom only") {
|
||||
const auto insets = lvgl_insets_calculate(ROUNDED_40, 172, 320);
|
||||
// 40 * (1 - 1/sqrt(2)) = 11.7
|
||||
CHECK_EQ(insets.top, 12);
|
||||
CHECK_EQ(insets.bottom, 12);
|
||||
CHECK_EQ(insets.left, 0);
|
||||
CHECK_EQ(insets.right, 0);
|
||||
}
|
||||
|
||||
TEST_CASE("a rounded rectangle's row inset is only in the corners") {
|
||||
CHECK_EQ(lvgl_insets_calculate_row(ROUNDED_40, 172, 320, 0), 40);
|
||||
CHECK_EQ(lvgl_insets_calculate_row(ROUNDED_40, 172, 320, 319), 40);
|
||||
CHECK_EQ(lvgl_insets_calculate_row(ROUNDED_40, 172, 320, 40), 0);
|
||||
CHECK_EQ(lvgl_insets_calculate_row(ROUNDED_40, 172, 320, 160), 0);
|
||||
}
|
||||
|
||||
TEST_CASE("a corner radius larger than half the panel is limited") {
|
||||
const DisplayShape huge = { .shape = DISPLAY_SHAPE_ROUNDED, .corner_radius = 500 };
|
||||
const auto insets = lvgl_insets_calculate(huge, 172, 320);
|
||||
// Limited to 86: 86 * (1 - 1/sqrt(2)) = 25.2
|
||||
CHECK_EQ(insets.top, 26);
|
||||
}
|
||||
2
Modules/lvgl-module/tests/source/main.cpp
Normal file
2
Modules/lvgl-module/tests/source/main.cpp
Normal file
@ -0,0 +1,2 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include "doctest.h"
|
||||
@ -200,7 +200,7 @@
|
||||
@chip-pad-ver: if($isCompact, 2, 6dp);
|
||||
|
||||
// Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite
|
||||
@badge-color: @color-primary;
|
||||
@badge-color: @color-secondary;
|
||||
@badge-size: if($isCompact, 4, 6dp);
|
||||
|
||||
// Scrollbar
|
||||
@ -232,7 +232,7 @@
|
||||
@text-button-pad-hor: if($isCompact, 3, @space-3);
|
||||
|
||||
// Icon button (dialog and window header buttons)
|
||||
@icon-button-color: @color-on-surface-variant;
|
||||
@icon-button-color: @color-on-surface;
|
||||
|
||||
// Filled and tonal icon buttons: Material 3 large icon buttons with a container
|
||||
@icon-button-container-shape: if($isCompact, @corner-compact, @corner-large);
|
||||
@ -687,7 +687,7 @@ app_bar_button_pressed {
|
||||
}
|
||||
|
||||
app_bar_focus_ring {
|
||||
outlineColor: @app-bar-content-color;
|
||||
outlineColor: @focus-ring-color;
|
||||
outlineWidth: @app-bar-focus-width;
|
||||
outlinePad: -@app-bar-focus-width;
|
||||
outlineOpa: cover;
|
||||
@ -1378,6 +1378,11 @@ apply(badge) {
|
||||
badge: default;
|
||||
}
|
||||
|
||||
// Focusable images, e.g. the statusbar icons
|
||||
apply(image) {
|
||||
focus_ring_inset: state.focusKey;
|
||||
}
|
||||
|
||||
apply(chip) {
|
||||
chip: default;
|
||||
transition_exit: default;
|
||||
@ -1410,6 +1415,7 @@ apply(slider) {
|
||||
}
|
||||
|
||||
apply(bar) {
|
||||
slider_track_size: default;
|
||||
progress_track: default;
|
||||
progress_indicator: part.indicator;
|
||||
focus_ring: state.focusKey;
|
||||
|
||||
@ -1070,6 +1070,7 @@ apply(slider) {
|
||||
}
|
||||
|
||||
apply(bar) {
|
||||
slider_track_size: default;
|
||||
slider_track: default;
|
||||
slider_active_track: part.indicator;
|
||||
focus_ring: state.focusKey;
|
||||
|
||||
@ -26,6 +26,13 @@ void statusbar_icon_set_image(int8_t id, const std::string& image);
|
||||
/** Update the visibility for an icon on the statusbar. Does not need to be called with LVGL lock. */
|
||||
void statusbar_icon_set_visibility(int8_t id, bool visible);
|
||||
|
||||
/**
|
||||
* Sets the app that starts when the icon is clicked. Does not need to be called with LVGL lock.
|
||||
* @param[in] id the icon id
|
||||
* @param[in] appId the app id, or an empty string to make the icon not clickable
|
||||
*/
|
||||
void statusbar_icon_set_app(int8_t id, const std::string& appId);
|
||||
|
||||
int statusbar_get_height();
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -19,7 +19,7 @@ struct AppGridItem {
|
||||
};
|
||||
|
||||
/**
|
||||
* Paged, non-scrolling grid of app tiles (icon above name) with "<" and ">" toolbar buttons.
|
||||
* Paged, non-scrolling grid of app tiles (icon above name) with "<" and ">" page buttons.
|
||||
* Must outlive the widgets it creates.
|
||||
*/
|
||||
class AppGrid final {
|
||||
@ -39,9 +39,19 @@ public:
|
||||
|
||||
explicit AppGrid(const Callbacks& callbacks) : callbacks(callbacks) {}
|
||||
|
||||
/** Shows the tile icons in the theme's primary color. Call before creating the widgets. */
|
||||
void setPrimaryColorIcons(bool enabled) { primaryColorIcons = enabled; }
|
||||
|
||||
/** Adds the paging buttons to the toolbar, creates the grid below it and populates it. */
|
||||
void createWidgets(lv_obj_t* parent, lv_obj_t* toolbar);
|
||||
|
||||
/**
|
||||
* Creates the grid and populates it. The page number and the page buttons go in pageBar, which is hidden when there's only one page. The page buttons wrap around.
|
||||
* @param[in] parent the container for the grid
|
||||
* @param[in] pageBar an empty container
|
||||
*/
|
||||
void createWidgetsWithPageBar(lv_obj_t* parent, lv_obj_t* pageBar);
|
||||
|
||||
/**
|
||||
* Rebuilds the grid asynchronously, so it is safe to call from a tile's own event.
|
||||
* @param[in] keepSelection when true, the selected app stays selected and its page is shown
|
||||
@ -53,11 +63,16 @@ private:
|
||||
Callbacks callbacks;
|
||||
std::vector<AppGridItem> items;
|
||||
uint32_t page = 0;
|
||||
uint32_t pageCount = 1;
|
||||
bool primaryColorIcons = false;
|
||||
bool keepSelectionOnRepopulate = true;
|
||||
lv_obj_t* grid = nullptr;
|
||||
lv_obj_t* prevButton = nullptr;
|
||||
lv_obj_t* nextButton = nullptr;
|
||||
lv_obj_t* pageBar = nullptr;
|
||||
lv_obj_t* pageLabel = nullptr;
|
||||
|
||||
void createGrid(lv_obj_t* parent);
|
||||
void populate();
|
||||
|
||||
static void onDeferredRepopulate(void* userData);
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::applist {
|
||||
namespace tt::app::launcher {
|
||||
|
||||
/** Favourited app ids, persisted one per line under the data root. */
|
||||
class Favourites final {
|
||||
@ -81,7 +81,7 @@ private:
|
||||
|
||||
static std::string filePath() {
|
||||
char path[128];
|
||||
if (app_paths_get_user_data_path("tactility.applist", "favourites", path, sizeof(path)) != ERROR_NONE) {
|
||||
if (app_paths_get_user_data_path("tactility.launcher", "favourites", path, sizeof(path)) != ERROR_NONE) {
|
||||
return "";
|
||||
}
|
||||
return path;
|
||||
@ -42,7 +42,6 @@ namespace app {
|
||||
namespace appearancesettings { extern const ::AppManifest manifest; }
|
||||
namespace apphubdetails { extern const ::AppManifest manifest; }
|
||||
namespace apppackagedetails { extern const ::AppManifest manifest; }
|
||||
namespace applist { extern const ::AppManifest manifest; }
|
||||
namespace apppackagelist { extern const ::AppManifest manifest; }
|
||||
namespace audiosettings { extern const ::AppManifest manifest; }
|
||||
namespace boot { extern const ::AppManifest manifest; }
|
||||
@ -110,7 +109,6 @@ static void registerInternalApps() {
|
||||
app_manager_add(&app::apppackagedetails::manifest);
|
||||
app_manager_add(&app::apphub::manifest);
|
||||
app_manager_add(&app::apphubdetails::manifest);
|
||||
app_manager_add(&app::applist::manifest);
|
||||
app_manager_add(&app::apppackagelist::manifest);
|
||||
if (service::audio::isAvailable()) {
|
||||
app_manager_add(&app::audiosettings::manifest);
|
||||
|
||||
@ -74,13 +74,23 @@ void AppGrid::onPrevPressed(lv_event_t* e) {
|
||||
auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
|
||||
if (self->page > 0) {
|
||||
self->page--;
|
||||
self->populate();
|
||||
} else if (self->pageBar != nullptr) {
|
||||
self->page = self->pageCount - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
self->populate();
|
||||
}
|
||||
|
||||
void AppGrid::onNextPressed(lv_event_t* e) {
|
||||
auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
|
||||
self->page++;
|
||||
if (self->page + 1 < self->pageCount) {
|
||||
self->page++;
|
||||
} else if (self->pageBar != nullptr) {
|
||||
self->page = 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
self->populate();
|
||||
}
|
||||
|
||||
@ -140,11 +150,17 @@ void AppGrid::populate() {
|
||||
}
|
||||
}
|
||||
page = std::min(page, page_count - 1);
|
||||
pageCount = page_count;
|
||||
|
||||
lv_obj_set_state(prevButton, LV_STATE_DISABLED, page == 0);
|
||||
lv_obj_set_state(nextButton, LV_STATE_DISABLED, page + 1 >= page_count);
|
||||
lv_obj_set_flag(prevButton, LV_OBJ_FLAG_HIDDEN, page_count <= 1);
|
||||
lv_obj_set_flag(nextButton, LV_OBJ_FLAG_HIDDEN, page_count <= 1);
|
||||
if (pageBar != nullptr) {
|
||||
lv_obj_set_flag(pageBar, LV_OBJ_FLAG_HIDDEN, page_count <= 1);
|
||||
lv_label_set_text_fmt(pageLabel, "%lu/%lu", static_cast<unsigned long>(page + 1), static_cast<unsigned long>(page_count));
|
||||
} else {
|
||||
lv_obj_set_state(prevButton, LV_STATE_DISABLED, page == 0);
|
||||
lv_obj_set_state(nextButton, LV_STATE_DISABLED, page + 1 >= page_count);
|
||||
lv_obj_set_flag(prevButton, LV_OBJ_FLAG_HIDDEN, page_count <= 1);
|
||||
lv_obj_set_flag(nextButton, LV_OBJ_FLAG_HIDDEN, page_count <= 1);
|
||||
}
|
||||
|
||||
lv_obj_t* focusedTile = nullptr;
|
||||
const uint32_t first = page * layout.pageSize;
|
||||
@ -166,6 +182,9 @@ void AppGrid::populate() {
|
||||
lv_obj_set_style_text_align(icon, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT);
|
||||
lv_obj_set_size(icon, layout.iconSize, layout.iconSize);
|
||||
lv_label_set_text(icon, item.icon);
|
||||
if (primaryColorIcons) {
|
||||
lv_obj_set_style_text_color(icon, lv_theme_get_color_primary(icon), LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
lv_obj_t* label = lv_label_create(tile);
|
||||
lv_obj_set_style_text_font(label, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||
@ -200,10 +219,7 @@ void AppGrid::populate() {
|
||||
}
|
||||
}
|
||||
|
||||
void AppGrid::createWidgets(lv_obj_t* parent, lv_obj_t* toolbar) {
|
||||
prevButton = lvgl_toolbar_add_text_button_action(toolbar, "<", onPrevPressed, this);
|
||||
nextButton = lvgl_toolbar_add_text_button_action(toolbar, ">", onNextPressed, this);
|
||||
|
||||
void AppGrid::createGrid(lv_obj_t* parent) {
|
||||
grid = lv_obj_create(parent);
|
||||
lv_obj_set_width(grid, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(grid, 1);
|
||||
@ -215,11 +231,17 @@ void AppGrid::createWidgets(lv_obj_t* parent, lv_obj_t* toolbar) {
|
||||
lv_obj_remove_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
|
||||
// The arrow keys move between the tiles in rows and columns
|
||||
lvgl_grid_navigation_add(grid);
|
||||
lv_obj_add_event_cb(grid, onGridDeleted, LV_EVENT_DELETE, this);
|
||||
}
|
||||
|
||||
void AppGrid::createWidgets(lv_obj_t* parent, lv_obj_t* toolbar) {
|
||||
prevButton = lvgl_toolbar_add_text_button_action(toolbar, "<", onPrevPressed, this);
|
||||
nextButton = lvgl_toolbar_add_text_button_action(toolbar, ">", onNextPressed, this);
|
||||
|
||||
createGrid(parent);
|
||||
|
||||
// Resolves the grid's flex_grow height, which the page size is derived from.
|
||||
lv_obj_update_layout(parent);
|
||||
|
||||
lv_obj_add_event_cb(grid, onGridDeleted, LV_EVENT_DELETE, this);
|
||||
lv_obj_add_event_cb(grid, onGridSizeChanged, LV_EVENT_SIZE_CHANGED, this);
|
||||
|
||||
populate();
|
||||
@ -231,4 +253,32 @@ void AppGrid::createWidgets(lv_obj_t* parent, lv_obj_t* toolbar) {
|
||||
}
|
||||
}
|
||||
|
||||
void AppGrid::createWidgetsWithPageBar(lv_obj_t* parent, lv_obj_t* pageBar) {
|
||||
createGrid(parent);
|
||||
|
||||
this->pageBar = pageBar;
|
||||
lv_obj_set_flex_flow(pageBar, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(pageBar, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
pageLabel = lv_label_create(pageBar);
|
||||
prevButton = lvgl_icon_button_create(pageBar);
|
||||
lv_label_set_text(lv_label_create(prevButton), "<");
|
||||
lv_obj_add_event_cb(prevButton, onPrevPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||
nextButton = lvgl_icon_button_create(pageBar);
|
||||
lv_label_set_text(lv_label_create(nextButton), ">");
|
||||
lv_obj_add_event_cb(nextButton, onNextPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||
// The arrow keys move between the page buttons
|
||||
lvgl_grid_navigation_add(pageBar);
|
||||
|
||||
// Resolves the grid's flex_grow height, which the page size is derived from.
|
||||
lv_obj_update_layout(parent);
|
||||
lv_obj_add_event_cb(grid, onGridSizeChanged, LV_EVENT_SIZE_CHANGED, this);
|
||||
|
||||
populate();
|
||||
|
||||
if (!lvgl_indev_exists(LV_INDEV_TYPE_POINTER) && lv_obj_get_child_count(grid) > 0) {
|
||||
lv_group_focus_obj(grid);
|
||||
lv_gridnav_set_focused(grid, lv_obj_get_child(grid, 0), LV_ANIM_OFF);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -523,10 +523,18 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_spinbox_set_range(font_size_spinbox, MIN_FONT_SIZE, MAX_FONT_SIZE);
|
||||
lv_spinbox_set_digit_format(font_size_spinbox, 2, 0);
|
||||
lv_spinbox_set_step(font_size_spinbox, 1);
|
||||
// Wide enough for the digits and the room that the text field keeps for its cursor, so it never scrolls the digits out of view.
|
||||
// The size is set before the value, because setting the value scrolls to the cursor using the current size.
|
||||
const lv_font_t* font_size_font = lv_obj_get_style_text_font(font_size_spinbox, LV_PART_MAIN);
|
||||
lv_point_t digits_size;
|
||||
lv_text_get_size(&digits_size, "00", font_size_font, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
|
||||
const int32_t font_size_content_width = digits_size.x + lv_font_get_line_height(font_size_font);
|
||||
lv_obj_set_width(font_size_spinbox, font_size_content_width +
|
||||
lv_obj_get_style_pad_left(font_size_spinbox, LV_PART_MAIN) + lv_obj_get_style_pad_right(font_size_spinbox, LV_PART_MAIN) +
|
||||
2 * lv_obj_get_style_border_width(font_size_spinbox, LV_PART_MAIN));
|
||||
lv_obj_set_style_text_align(font_size_spinbox, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
|
||||
const uint16_t font_size = ctx->pendingSettings.fontSize != 0 ? ctx->pendingSettings.fontSize : static_cast<uint16_t>(TT_FONT_DEFAULT_SIZE);
|
||||
lv_spinbox_set_value(font_size_spinbox, font_size);
|
||||
lv_obj_set_width(font_size_spinbox, lv_font_get_line_height(lv_obj_get_style_text_font(font_size_spinbox, LV_PART_MAIN)) * 3);
|
||||
lv_obj_set_style_text_align(font_size_spinbox, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
|
||||
// The value is changed with the buttons, so the digit cursor isn't shown
|
||||
lv_obj_set_style_bg_opa(font_size_spinbox, LV_OPA_TRANSP, LV_PART_CURSOR);
|
||||
lv_obj_add_event_cb(font_size_spinbox, onFontSizeChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
@ -1,219 +0,0 @@
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/package_manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
#include <Tactility/app/alertdialog/AlertDialog.h>
|
||||
#include <Tactility/app/applist/Favourites.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
namespace tt::app::applist {
|
||||
|
||||
constexpr auto* TAG = "AppList";
|
||||
|
||||
namespace {
|
||||
|
||||
struct IconEntry {
|
||||
const char* id;
|
||||
const char* icon;
|
||||
};
|
||||
|
||||
constexpr IconEntry ICONS[] = {
|
||||
{"tactility.apphub", LVGL_ICON_SHARED_DOWNLOAD},
|
||||
{"tactility.camera", LVGL_ICON_SHARED_CAMERA},
|
||||
{"tactility.chat", LVGL_ICON_SHARED_FORUM},
|
||||
{"tactility.i2cscanner", LVGL_ICON_SHARED_CABLE},
|
||||
{"tactility.notes", LVGL_ICON_SHARED_EDIT_NOTE},
|
||||
{"tactility.screenshot", LVGL_ICON_SHARED_IMAGE},
|
||||
{"tactility.systeminfo", LVGL_ICON_SHARED_DEVICES},
|
||||
{"tactility.terminal", LVGL_ICON_SHARED_TERMINAL},
|
||||
{"tactility.webserversettings", LVGL_ICON_SHARED_CLOUD},
|
||||
};
|
||||
|
||||
const char* appIcon(const ::AppManifest* manifest) {
|
||||
for (const auto& entry : ICONS) {
|
||||
if (!strcmp(manifest->id, entry.id)) return entry.icon;
|
||||
}
|
||||
return LVGL_ICON_SHARED_DEPLOYED_CODE;
|
||||
}
|
||||
|
||||
void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
|
||||
manifests->push_back(*manifest);
|
||||
}
|
||||
|
||||
// Apps from installed packages this device can't run (wrong device, too little RAM)
|
||||
void collectIncompatibleAppIds(const AppPackage* package, void* context) {
|
||||
if (app_package_manifest_is_compatible(&package->package)) {
|
||||
return;
|
||||
}
|
||||
auto* ids = static_cast<std::vector<std::string>*>(context);
|
||||
for (size_t i = 0; i < package->app_id_count; i++) {
|
||||
ids->emplace_back(package->app_ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<AppGridItem> collectItems(void* userData);
|
||||
void onAppClicked(const ::AppManifest& manifest, void* userData);
|
||||
void onAppLongPressed(const ::AppManifest& manifest, void* userData);
|
||||
void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData);
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
Favourites favourites;
|
||||
AppGrid grid { AppGrid::Callbacks {
|
||||
.collect = collectItems,
|
||||
.onClicked = onAppClicked,
|
||||
.onLongPressed = onAppLongPressed,
|
||||
.onKey = onAppKey,
|
||||
.userData = this
|
||||
} };
|
||||
};
|
||||
|
||||
std::vector<AppGridItem> collectItems(void* userData) {
|
||||
const auto* ctx = static_cast<Context*>(userData);
|
||||
const std::vector<std::string> favouriteIds = ctx->favourites.load();
|
||||
|
||||
std::vector<::AppManifest> collected;
|
||||
app_manager_for_each_manifest(collectManifest, &collected);
|
||||
std::vector<std::string> incompatibleIds;
|
||||
app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds);
|
||||
std::erase_if(collected, [&](const ::AppManifest& manifest) {
|
||||
const bool is_valid_category = (manifest.category == APP_CATEGORY_USER) || (manifest.category == APP_CATEGORY_SYSTEM);
|
||||
if (!is_valid_category || (manifest.flags & APP_MANIFEST_FLAG_HIDDEN) != 0) {
|
||||
return true;
|
||||
}
|
||||
return std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end();
|
||||
});
|
||||
|
||||
std::vector<AppGridItem> items;
|
||||
items.reserve(collected.size());
|
||||
for (const auto& manifest : collected) {
|
||||
items.push_back({ manifest, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) });
|
||||
}
|
||||
std::ranges::sort(items, [](const AppGridItem& a, const AppGridItem& b) {
|
||||
if (a.highlighted != b.highlighted) {
|
||||
return a.highlighted;
|
||||
}
|
||||
return strcmp(a.manifest.name, b.manifest.name) < 0;
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
void onAppClicked(const ::AppManifest& manifest, void* userData) {
|
||||
const auto* ctx = static_cast<Context*>(userData);
|
||||
uint32_t instanceId = 0;
|
||||
AppStartContext context;
|
||||
if (app_start_context_from_id(manifest.id, &context) == ERROR_NONE && app_start_with_context(&context, &instanceId) == ERROR_NONE) {
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
}
|
||||
|
||||
void toggleFavourite(Context* ctx, const char* appId, bool keepSelection) {
|
||||
if (!ctx->favourites.toggle(appId)) {
|
||||
LOG_E(TAG, "Failed to save favourites");
|
||||
}
|
||||
ctx->grid.requestRepopulate(keepSelection);
|
||||
}
|
||||
|
||||
void onAppLongPressed(const ::AppManifest& manifest, void* userData) {
|
||||
toggleFavourite(static_cast<Context*>(userData), manifest.id, false);
|
||||
}
|
||||
|
||||
void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData) {
|
||||
if (key == 'f' || key == 'F') {
|
||||
toggleFavourite(static_cast<Context*>(userData), manifest.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onHelpPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
alertdialog::start(ctx->appInstanceId, "Help", "Long-press an app, or press F, to toggle it as a favorite.");
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
// Flex column + flex_grow so the toolbar/grid split recomputes on layout instead of going stale after a resize.
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* toolbar = lvgl_toolbar_create(parent, "Apps");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
ctx->grid.createWidgets(parent, toolbar);
|
||||
auto* help_button = lvgl_toolbar_add_text_button_action(toolbar, LVGL_ICON_SHARED_HELP, onHelpPressed, ctx);
|
||||
lv_obj_set_style_text_font(help_button, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||
Context ctx { appInstanceId };
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
|
||||
while (true) {
|
||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||
|
||||
bool shouldClose = false;
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||
switch (event.type) {
|
||||
case APP_EVENT_CLOSE:
|
||||
shouldClose = true;
|
||||
break;
|
||||
case APP_EVENT_RESULT:
|
||||
app_manager_stop(event.result.launch_id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (shouldClose) break;
|
||||
}
|
||||
if (shouldClose) break;
|
||||
}
|
||||
|
||||
window_manager_remove(window);
|
||||
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&event_group);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern const ::AppManifest manifest = {
|
||||
.id = "tactility.applist",
|
||||
.name = "Apps",
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
.stack = { .depth = 5120, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@ -64,8 +64,8 @@ void collectPackage(const ::AppPackage* pkg, void* context) {
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
// Flex column + flex_grow; see AppList.cpp's createWidgets() for why a fixed height computed
|
||||
// once from lv_obj_get_content_height(parent) goes stale.
|
||||
// Flex column + flex_grow, because a fixed height computed once from
|
||||
// lv_obj_get_content_height(parent) goes stale after a resize.
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
|
||||
@ -27,10 +27,12 @@ struct Context {
|
||||
PubSub<service::audio::AudioEvent>::SubscriptionHandle audioSubscription = nullptr;
|
||||
|
||||
lv_obj_t* inputEnabledSwitch = nullptr;
|
||||
lv_obj_t* inputCard = nullptr;
|
||||
lv_obj_t* inputMuteSwitch = nullptr;
|
||||
lv_obj_t* inputVolumeSlider = nullptr;
|
||||
|
||||
lv_obj_t* outputEnabledSwitch = nullptr;
|
||||
lv_obj_t* outputCard = nullptr;
|
||||
lv_obj_t* outputMuteSwitch = nullptr;
|
||||
lv_obj_t* outputVolumeSlider = nullptr;
|
||||
};
|
||||
@ -42,15 +44,19 @@ void onBackPressed(lv_event_t* event) {
|
||||
}
|
||||
|
||||
void onInputEnabledSwitch(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
||||
service::audio::setInputEnabled(enabled);
|
||||
lv_obj_set_flag(ctx->inputCard, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
}
|
||||
|
||||
void onOutputEnabledSwitch(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
||||
service::audio::setOutputEnabled(enabled);
|
||||
lv_obj_set_flag(ctx->outputCard, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
}
|
||||
|
||||
void onInputMuteSwitch(lv_event_t* event) {
|
||||
@ -77,17 +83,6 @@ void onOutputVolumeSlider(lv_event_t* event) {
|
||||
service::audio::setOutputVolume(percent);
|
||||
}
|
||||
|
||||
lv_obj_t* createSection(lv_obj_t* parent, const char* title) {
|
||||
auto* title_label = lv_label_create(parent);
|
||||
lv_label_set_text(title_label, title);
|
||||
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// An unstyled container, so the rows don't paint over the card
|
||||
lv_obj_t* createRow(lv_obj_t* parent) {
|
||||
auto* row = lv_obj_create(parent);
|
||||
@ -96,6 +91,32 @@ lv_obj_t* createRow(lv_obj_t* parent) {
|
||||
return row;
|
||||
}
|
||||
|
||||
struct Section {
|
||||
lv_obj_t* enabledSwitch;
|
||||
lv_obj_t* card;
|
||||
};
|
||||
|
||||
// The title has the device's enable switch on its right. The card with the device's settings is hidden while it's disabled.
|
||||
Section createSection(lv_obj_t* parent, const char* title, bool enabled, lv_event_cb_t onEnabledChanged, void* userData) {
|
||||
auto* header = createRow(parent);
|
||||
|
||||
auto* title_label = lv_label_create(header);
|
||||
lv_label_set_text(title_label, title);
|
||||
lv_obj_align(title_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
auto* sw = lv_switch_create(header);
|
||||
lv_obj_align(sw, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_set_state(sw, LV_STATE_CHECKED, enabled);
|
||||
lv_obj_add_event_cb(sw, onEnabledChanged, LV_EVENT_VALUE_CHANGED, userData);
|
||||
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flag(card, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
|
||||
return { sw, card };
|
||||
}
|
||||
|
||||
lv_obj_t* createSwitchRow(lv_obj_t* parent, const char* label, lv_event_cb_t cb, void* userData) {
|
||||
auto* row = createRow(parent);
|
||||
|
||||
@ -127,8 +148,9 @@ lv_obj_t* createSliderRow(lv_obj_t* parent, const char* label, int32_t initialVa
|
||||
|
||||
void refresh(Context* ctx) {
|
||||
if (ctx->inputEnabledSwitch) {
|
||||
if (service::audio::isInputEnabled()) lv_obj_add_state(ctx->inputEnabledSwitch, LV_STATE_CHECKED);
|
||||
else lv_obj_remove_state(ctx->inputEnabledSwitch, LV_STATE_CHECKED);
|
||||
const bool enabled = service::audio::isInputEnabled();
|
||||
lv_obj_set_state(ctx->inputEnabledSwitch, LV_STATE_CHECKED, enabled);
|
||||
lv_obj_set_flag(ctx->inputCard, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
}
|
||||
if (ctx->inputMuteSwitch) {
|
||||
if (service::audio::isInputMuted()) lv_obj_add_state(ctx->inputMuteSwitch, LV_STATE_CHECKED);
|
||||
@ -139,8 +161,9 @@ void refresh(Context* ctx) {
|
||||
}
|
||||
|
||||
if (ctx->outputEnabledSwitch) {
|
||||
if (service::audio::isOutputEnabled()) lv_obj_add_state(ctx->outputEnabledSwitch, LV_STATE_CHECKED);
|
||||
else lv_obj_remove_state(ctx->outputEnabledSwitch, LV_STATE_CHECKED);
|
||||
const bool enabled = service::audio::isOutputEnabled();
|
||||
lv_obj_set_state(ctx->outputEnabledSwitch, LV_STATE_CHECKED, enabled);
|
||||
lv_obj_set_flag(ctx->outputCard, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
}
|
||||
if (ctx->outputMuteSwitch) {
|
||||
if (service::audio::isOutputMuted()) lv_obj_add_state(ctx->outputMuteSwitch, LV_STATE_CHECKED);
|
||||
@ -177,17 +200,19 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
// device (e.g. a dedicated input codec with no output codec bound) should
|
||||
// only show the section it actually has, not a dead section for the other.
|
||||
if (service::audio::isInputAvailable()) {
|
||||
auto* input_section = createSection(main_wrapper, "Microphone");
|
||||
ctx->inputEnabledSwitch = createSwitchRow(input_section, "Enabled", onInputEnabledSwitch, ctx);
|
||||
ctx->inputMuteSwitch = createSwitchRow(input_section, "Mute", onInputMuteSwitch, ctx);
|
||||
ctx->inputVolumeSlider = createSliderRow(input_section, "Volume", static_cast<int32_t>(service::audio::getInputVolume()), onInputVolumeSlider, ctx);
|
||||
const auto input_section = createSection(main_wrapper, "Microphone", service::audio::isInputEnabled(), onInputEnabledSwitch, ctx);
|
||||
ctx->inputEnabledSwitch = input_section.enabledSwitch;
|
||||
ctx->inputCard = input_section.card;
|
||||
ctx->inputMuteSwitch = createSwitchRow(input_section.card, "Mute", onInputMuteSwitch, ctx);
|
||||
ctx->inputVolumeSlider = createSliderRow(input_section.card, "Volume", static_cast<int32_t>(service::audio::getInputVolume()), onInputVolumeSlider, ctx);
|
||||
}
|
||||
|
||||
if (service::audio::isOutputAvailable()) {
|
||||
auto* output_section = createSection(main_wrapper, "Speaker");
|
||||
ctx->outputEnabledSwitch = createSwitchRow(output_section, "Enabled", onOutputEnabledSwitch, ctx);
|
||||
ctx->outputMuteSwitch = createSwitchRow(output_section, "Mute", onOutputMuteSwitch, ctx);
|
||||
ctx->outputVolumeSlider = createSliderRow(output_section, "Volume", static_cast<int32_t>(service::audio::getOutputVolume()), onOutputVolumeSlider, ctx);
|
||||
const auto output_section = createSection(main_wrapper, "Speaker", service::audio::isOutputEnabled(), onOutputEnabledSwitch, ctx);
|
||||
ctx->outputEnabledSwitch = output_section.enabledSwitch;
|
||||
ctx->outputCard = output_section.card;
|
||||
ctx->outputMuteSwitch = createSwitchRow(output_section.card, "Mute", onOutputMuteSwitch, ctx);
|
||||
ctx->outputVolumeSlider = createSliderRow(output_section.card, "Volume", static_cast<int32_t>(service::audio::getOutputVolume()), onOutputVolumeSlider, ctx);
|
||||
}
|
||||
|
||||
// isAvailable() only reflects that the audio-stream device exists, not that any
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/FileLock.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/paths.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <dirent.h>
|
||||
@ -14,7 +15,10 @@ namespace tt::app::files {
|
||||
constexpr auto* TAG = "Files";
|
||||
|
||||
State::State() {
|
||||
setEntriesForPath("/");
|
||||
char data_root[FILE_MAX_PATH_STRING_LENGTH];
|
||||
if (paths_get_data_root_path(data_root, sizeof(data_root)) != ERROR_NONE || !setEntriesForPath(data_root)) {
|
||||
setEntriesForPath("/");
|
||||
}
|
||||
}
|
||||
|
||||
std::string State::getSelectedChildPath() const {
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/package_manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/icons/launcher.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
|
||||
@ -18,13 +21,11 @@
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/freertos/semphr.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/memory.h>
|
||||
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
#include <Tactility/app/launcher/Favourites.h>
|
||||
#include <Tactility/app/setup/Setup.h>
|
||||
#include <Tactility/settings/BootSettings.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
@ -35,32 +36,107 @@ constexpr auto* TAG = "Launcher";
|
||||
|
||||
namespace {
|
||||
|
||||
int32_t computeButtonMargin(int32_t available_span, int32_t total_button_size) {
|
||||
const int32_t usable = std::max<int32_t>(0, available_span - (3 * total_button_size));
|
||||
return std::min<int32_t>(usable / 16, total_button_size / 2);
|
||||
struct IconEntry {
|
||||
const char* id;
|
||||
const char* icon;
|
||||
};
|
||||
|
||||
constexpr IconEntry ICONS[] = {
|
||||
{"tactility.apphub", LVGL_ICON_SHARED_DOWNLOAD},
|
||||
{"tactility.camera", LVGL_ICON_SHARED_CAMERA},
|
||||
{"tactility.chat", LVGL_ICON_SHARED_FORUM},
|
||||
{"tactility.files", LVGL_ICON_SHARED_FOLDER},
|
||||
{"tactility.i2cscanner", LVGL_ICON_SHARED_CABLE},
|
||||
{"tactility.notes", LVGL_ICON_SHARED_EDIT_NOTE},
|
||||
{"tactility.screenshot", LVGL_ICON_SHARED_IMAGE},
|
||||
{"tactility.settings", LVGL_ICON_SHARED_SETTINGS},
|
||||
{"tactility.systeminfo", LVGL_ICON_SHARED_DEVICES},
|
||||
{"tactility.terminal", LVGL_ICON_SHARED_TERMINAL},
|
||||
{"tactility.webserversettings", LVGL_ICON_SHARED_CLOUD},
|
||||
};
|
||||
|
||||
// Hidden apps that are still shown in the launcher
|
||||
constexpr const char* SHOWN_HIDDEN_APP_IDS[] = {
|
||||
"tactility.files",
|
||||
"tactility.settings",
|
||||
};
|
||||
|
||||
const char* appIcon(const ::AppManifest* manifest) {
|
||||
for (const auto& entry : ICONS) {
|
||||
if (!strcmp(manifest->id, entry.id)) return entry.icon;
|
||||
}
|
||||
return LVGL_ICON_SHARED_DEPLOYED_CODE;
|
||||
}
|
||||
|
||||
// Spreads the buttons along the display's longest side. The button padding comes from the theme.
|
||||
void applyButtonMargins(lv_obj_t* buttonsWrapper, bool isLandscape) {
|
||||
const uint32_t child_count = lv_obj_get_child_count(buttonsWrapper);
|
||||
if (child_count == 0) {
|
||||
bool isShown(const ::AppManifest& manifest) {
|
||||
if (manifest.category != APP_CATEGORY_USER && manifest.category != APP_CATEGORY_SYSTEM) {
|
||||
return false;
|
||||
}
|
||||
if ((manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
|
||||
return true;
|
||||
}
|
||||
return std::ranges::any_of(SHOWN_HIDDEN_APP_IDS, [&](const char* id) { return strcmp(manifest.id, id) == 0; });
|
||||
}
|
||||
|
||||
void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
|
||||
manifests->push_back(*manifest);
|
||||
}
|
||||
|
||||
// Apps from installed packages this device can't run (wrong device, too little RAM)
|
||||
void collectIncompatibleAppIds(const AppPackage* package, void* context) {
|
||||
if (app_package_manifest_is_compatible(&package->package)) {
|
||||
return;
|
||||
}
|
||||
auto* first_button = lv_obj_get_child(buttonsWrapper, 0);
|
||||
const auto total_button_size = static_cast<int32_t>(lvgl_get_launcher_icon_font_height()) +
|
||||
lv_obj_get_style_pad_left(first_button, LV_PART_MAIN) + lv_obj_get_style_pad_right(first_button, LV_PART_MAIN);
|
||||
const auto* display = lv_obj_get_display(buttonsWrapper);
|
||||
const auto span = isLandscape ? lv_display_get_horizontal_resolution(display) : lv_display_get_vertical_resolution(display);
|
||||
const int32_t margin = computeButtonMargin(span, total_button_size);
|
||||
for (uint32_t i = 0; i < child_count; i++) {
|
||||
auto* button = lv_obj_get_child(buttonsWrapper, i);
|
||||
lv_obj_set_style_margin_hor(button, isLandscape ? margin : 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_margin_ver(button, isLandscape ? 0 : margin, LV_STATE_DEFAULT);
|
||||
auto* ids = static_cast<std::vector<std::string>*>(context);
|
||||
for (size_t i = 0; i < package->app_id_count; i++) {
|
||||
ids->emplace_back(package->app_ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void onAppPressed(lv_event_t* e) {
|
||||
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
|
||||
std::vector<AppGridItem> collectItems(void* userData);
|
||||
void onAppClicked(const ::AppManifest& manifest, void* userData);
|
||||
void onAppLongPressed(const ::AppManifest& manifest, void* userData);
|
||||
void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData);
|
||||
|
||||
struct Context {
|
||||
Favourites favourites;
|
||||
AppGrid grid { AppGrid::Callbacks {
|
||||
.collect = collectItems,
|
||||
.onClicked = onAppClicked,
|
||||
.onLongPressed = onAppLongPressed,
|
||||
.onKey = onAppKey,
|
||||
.userData = this
|
||||
} };
|
||||
};
|
||||
|
||||
std::vector<AppGridItem> collectItems(void* userData) {
|
||||
const auto* ctx = static_cast<Context*>(userData);
|
||||
const std::vector<std::string> favouriteIds = ctx->favourites.load();
|
||||
|
||||
std::vector<::AppManifest> collected;
|
||||
app_manager_for_each_manifest(collectManifest, &collected);
|
||||
std::vector<std::string> incompatibleIds;
|
||||
app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds);
|
||||
std::erase_if(collected, [&](const ::AppManifest& manifest) {
|
||||
return !isShown(manifest) || std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end();
|
||||
});
|
||||
|
||||
std::vector<AppGridItem> items;
|
||||
items.reserve(collected.size());
|
||||
for (const auto& manifest : collected) {
|
||||
items.push_back({ manifest, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) });
|
||||
}
|
||||
std::ranges::sort(items, [](const AppGridItem& a, const AppGridItem& b) {
|
||||
if (a.highlighted != b.highlighted) {
|
||||
return a.highlighted;
|
||||
}
|
||||
return strcmp(a.manifest.name, b.manifest.name) < 0;
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
void startApp(const char* appId) {
|
||||
uint32_t instance_id = 0;
|
||||
AppStartContext context;
|
||||
if (app_start_context_from_id(appId, &context) == ERROR_NONE) {
|
||||
@ -68,26 +144,39 @@ void onAppPressed(lv_event_t* e) {
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* createAppButton(lv_obj_t* parent, LvglIconButtonVariant variant, const char* imageFile, const char* appId) {
|
||||
const auto button_size = lvgl_get_launcher_icon_font_height();
|
||||
auto* apps_button = lvgl_icon_button_create_variant(parent, variant);
|
||||
|
||||
// create the image first
|
||||
auto* button_image = lv_image_create(apps_button);
|
||||
lv_obj_set_style_text_font(button_image, lvgl_get_launcher_icon_font(), LV_STATE_DEFAULT);
|
||||
lv_image_set_src(button_image, imageFile);
|
||||
|
||||
// Ensure it's square (Material Symbols are slightly wider than tall)
|
||||
lv_obj_set_size(button_image, button_size, button_size);
|
||||
|
||||
lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId);
|
||||
|
||||
return apps_button;
|
||||
void onAppClicked(const ::AppManifest& manifest, void*) {
|
||||
startApp(manifest.id);
|
||||
}
|
||||
|
||||
bool shouldShowPowerButton() {
|
||||
bool show_power_button = false;
|
||||
device_for_each_of_type(&POWER_SUPPLY_TYPE, &show_power_button, [](Device* device, void* context) {
|
||||
void toggleFavourite(Context* ctx, const char* appId, bool keepSelection) {
|
||||
if (!ctx->favourites.toggle(appId)) {
|
||||
LOG_E(TAG, "Failed to save favourites");
|
||||
}
|
||||
ctx->grid.requestRepopulate(keepSelection);
|
||||
}
|
||||
|
||||
void onAppLongPressed(const ::AppManifest& manifest, void* userData) {
|
||||
toggleFavourite(static_cast<Context*>(userData), manifest.id, false);
|
||||
}
|
||||
|
||||
void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData) {
|
||||
if (key == 'f' || key == 'F') {
|
||||
toggleFavourite(static_cast<Context*>(userData), manifest.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
void onShortcutPressed(lv_event_t* e) {
|
||||
startApp(static_cast<const char*>(lv_event_get_user_data(e)));
|
||||
}
|
||||
|
||||
bool isAppRegistered(const char* appId) {
|
||||
::AppManifest manifest;
|
||||
return app_manager_find_manifest(appId, &manifest) == ERROR_NONE;
|
||||
}
|
||||
|
||||
bool supportsPowerOff() {
|
||||
bool supported = false;
|
||||
device_for_each_of_type(&POWER_SUPPLY_TYPE, &supported, [](Device* device, void* context) {
|
||||
if (device_is_ready(device) && power_supply_supports_power_off(device)) {
|
||||
*static_cast<bool*>(context) = true;
|
||||
return false; // stop iterating
|
||||
@ -95,98 +184,49 @@ bool shouldShowPowerButton() {
|
||||
return true; // continue iterating
|
||||
}
|
||||
});
|
||||
return show_power_button;
|
||||
return supported;
|
||||
}
|
||||
|
||||
void onButtonsWrapperResized(lv_event_t* e);
|
||||
|
||||
// The screen object outlives this window's own widgets (lvgl-window-manager deletes and
|
||||
// recreates only the topmost window's widget on every app switch, not the screen itself), so
|
||||
// the LV_EVENT_SIZE_CHANGED callback registered on it must be removed once buttons_wrapper is
|
||||
// destroyed, to avoid a dangling user-data pointer the next time the display rotates while a
|
||||
// different window is topmost.
|
||||
void onButtonsWrapperDeleted(lv_event_t* e) {
|
||||
auto* buttons_wrapper = lv_event_get_target_obj(e);
|
||||
auto* screen = lv_obj_get_screen(buttons_wrapper);
|
||||
lv_obj_remove_event_cb_with_user_data(screen, onButtonsWrapperResized, buttons_wrapper);
|
||||
void addShortcut(lv_obj_t* parent, const char* icon, const char* appId) {
|
||||
auto* button = lvgl_icon_button_create(parent);
|
||||
auto* label = lv_label_create(button);
|
||||
lv_obj_set_style_text_font(label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(label, icon);
|
||||
lv_obj_add_event_cb(button, onShortcutPressed, LV_EVENT_SHORT_CLICKED, const_cast<char*>(appId));
|
||||
}
|
||||
|
||||
// Re-applies the flex direction and per-button margins when the display orientation changes
|
||||
// while the launcher is the visible window (these are decided once at createWidgets() based on
|
||||
// the resolution at that time, so a later rotation needs this to catch up).
|
||||
void onButtonsWrapperResized(lv_event_t* e) {
|
||||
auto* buttons_wrapper = static_cast<lv_obj_t*>(lv_event_get_user_data(e));
|
||||
const auto* display = lv_obj_get_display(buttons_wrapper);
|
||||
|
||||
const auto horizontal_px = lv_display_get_horizontal_resolution(display);
|
||||
const auto vertical_px = lv_display_get_vertical_resolution(display);
|
||||
const bool is_landscape_display = horizontal_px >= vertical_px;
|
||||
const auto current_flow = lv_obj_get_style_flex_flow(buttons_wrapper, LV_PART_MAIN);
|
||||
const bool was_landscape = current_flow == LV_FLEX_FLOW_ROW;
|
||||
if (is_landscape_display == was_landscape) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_set_flex_flow(buttons_wrapper, is_landscape_display ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN);
|
||||
applyButtonMargins(buttons_wrapper, is_landscape_display);
|
||||
lv_obj_t* createBarSection(lv_obj_t* parent, lv_flex_align_t align) {
|
||||
auto* section = lv_obj_create(parent);
|
||||
lv_obj_set_size(section, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(section, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(section, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(section, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(section, align, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_remove_flag(section, LV_OBJ_FLAG_SCROLLABLE);
|
||||
return section;
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void*) {
|
||||
auto* buttons_wrapper = lv_obj_create(parent);
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
lv_obj_align(buttons_wrapper, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_obj_set_size(buttons_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_border_width(buttons_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_grow(buttons_wrapper, 1);
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
// Fix for button selection
|
||||
lv_obj_set_style_pad_all(buttons_wrapper, 6, LV_STATE_DEFAULT);
|
||||
// Power off on the left, the page number and page buttons on the right
|
||||
auto* bottom_bar = createBarSection(parent, LV_FLEX_ALIGN_START);
|
||||
lv_obj_set_width(bottom_bar, LV_PCT(100));
|
||||
lvgl_obj_add_edge_padding(bottom_bar);
|
||||
auto* left_section = createBarSection(bottom_bar, LV_FLEX_ALIGN_START);
|
||||
lv_obj_set_flex_grow(left_section, 1);
|
||||
auto* page_bar = createBarSection(bottom_bar, LV_FLEX_ALIGN_END);
|
||||
|
||||
const auto* display = lv_obj_get_display(parent);
|
||||
const auto horizontal_px = lv_display_get_horizontal_resolution(display);
|
||||
const auto vertical_px = lv_display_get_vertical_resolution(display);
|
||||
const bool is_landscape_display = horizontal_px >= vertical_px;
|
||||
if (is_landscape_display) {
|
||||
lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_ROW);
|
||||
} else {
|
||||
lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
}
|
||||
// The monochrome theme's primary color is black, also on a black background
|
||||
ctx->grid.setPrimaryColorIcons(!lvgl_theme_is_mono());
|
||||
ctx->grid.createWidgetsWithPageBar(parent, page_bar);
|
||||
lv_obj_move_foreground(bottom_bar);
|
||||
|
||||
// The app list is the main action, so it stands out more than the others
|
||||
// Filled buttons would be solid black blocks with the monochrome theme
|
||||
const auto variant = lvgl_theme_is_mono() ? LVGL_ICON_BUTTON_STANDARD : LVGL_ICON_BUTTON_FILLED;
|
||||
auto* app_list_button = createAppButton(buttons_wrapper, variant, LVGL_ICON_LAUNCHER_APPS, "tactility.applist");
|
||||
createAppButton(buttons_wrapper, variant, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files");
|
||||
createAppButton(buttons_wrapper, variant, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings");
|
||||
applyButtonMargins(buttons_wrapper, is_landscape_display);
|
||||
// The arrow keys move between the buttons, in a row or a column
|
||||
lvgl_grid_navigation_add(buttons_wrapper);
|
||||
|
||||
// The launcher's container is several levels below the screen, and LVGL only sends
|
||||
// LV_EVENT_SIZE_CHANGED to the screen object itself on a resolution change - so the
|
||||
// handler is attached there, with buttons_wrapper passed through as user data.
|
||||
lv_obj_add_event_cb(lv_obj_get_screen(parent), onButtonsWrapperResized, LV_EVENT_SIZE_CHANGED, buttons_wrapper);
|
||||
lv_obj_add_event_cb(buttons_wrapper, onButtonsWrapperDeleted, LV_EVENT_DELETE, nullptr);
|
||||
|
||||
// Some devices (e.g. T-Lora Pager) have no other way to power off, so the
|
||||
// button stays in the launcher; the confirmation flow lives in the PowerOff app.
|
||||
if (shouldShowPowerButton()) {
|
||||
auto* power_button = lvgl_icon_button_create(parent);
|
||||
lv_obj_set_style_pad_all(power_button, 8, 0);
|
||||
lv_obj_align(power_button, LV_ALIGN_BOTTOM_MID, 0, -10);
|
||||
lv_obj_add_event_cb(power_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)"tactility.poweroff");
|
||||
|
||||
auto* power_label = lv_label_create(power_button);
|
||||
lv_label_set_text(power_label, LV_SYMBOL_POWER);
|
||||
}
|
||||
|
||||
// If we don't have a touch device, we assume there's some other kind of input like a keyboard, an encoder or button control
|
||||
// In that scenario we want to automatically have the app list button selected so the user doesn't have to press the widget selection
|
||||
// an extra time.
|
||||
if (!device_has_active_by_type(&POINTER_TYPE)) {
|
||||
// lv_obj_update_layout(parent); // Resolve flex layout first, so focus/state invalidate against final coords
|
||||
lv_group_focus_obj(buttons_wrapper);
|
||||
lv_gridnav_set_focused(buttons_wrapper, app_list_button, LV_ANIM_OFF);
|
||||
if (isAppRegistered("tactility.poweroff") && supportsPowerOff()) {
|
||||
addShortcut(left_section, LVGL_ICON_SHARED_POWER_SETTINGS_NEW, "tactility.poweroff");
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,30 +258,6 @@ void runAutoStart() {
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t AUTO_START_STACK_DEPTH = 4096 / sizeof(StackType_t);
|
||||
|
||||
struct AutoStartTaskContext {
|
||||
SemaphoreHandle_t done;
|
||||
};
|
||||
|
||||
void autoStartTaskMain(void* param) {
|
||||
auto* context = static_cast<AutoStartTaskContext*>(param);
|
||||
runAutoStart();
|
||||
xSemaphoreGive(context->done);
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
// runAutoStart() reads from flash, and apps run on PSRAM when available.
|
||||
// Launcher stays in memory, so we prefer to keep the PSRAM task and temporarily run an IRAM task for auto start logic.
|
||||
void runAutoStartIsolated() {
|
||||
AutoStartTaskContext context { .done = xSemaphoreCreateBinary() };
|
||||
check(context.done != nullptr);
|
||||
TaskHandle_t auto_start_task = nullptr;
|
||||
check(xTaskCreate(autoStartTaskMain, "LauncherAutoStart", AUTO_START_STACK_DEPTH, &context, tskIDLE_PRIORITY, &auto_start_task) == pdPASS);
|
||||
xSemaphoreTake(context.done, portMAX_DELAY);
|
||||
vSemaphoreDelete(context.done);
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||
|
||||
@ -251,9 +267,10 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr);
|
||||
Context ctx;
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
|
||||
runAutoStartIsolated();
|
||||
runAutoStart();
|
||||
|
||||
// The launcher is meant to stay resident (it's the home screen) - it only gives up its
|
||||
// thread when app-module's scheduler asks it to (e.g. another new-model app is started).
|
||||
@ -285,8 +302,7 @@ extern const ::AppManifest manifest = {
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
// No file IO, so callstack can be in external RAM
|
||||
.stack = { .depth = 3072 , .desired_memory_capability = MEMORY_CAPABILITY_EXTERNAL }
|
||||
.stack = { .depth = 5120, .desired_memory_capability = 0 }
|
||||
};
|
||||
|
||||
// Kept for Tactility/Private/Tactility/app/launcher/Launcher.h's existing declaration (still
|
||||
|
||||
@ -55,7 +55,7 @@ const char* appIcon(const ::AppManifest* manifest) {
|
||||
}
|
||||
|
||||
void onAppClicked(const ::AppManifest& manifest, void*) {
|
||||
// Fire-and-forget top-level navigation, same as AppList's own app-launch buttons.
|
||||
// Fire-and-forget top-level navigation, same as the Launcher's own app-launch buttons.
|
||||
uint32_t instanceId = 0;
|
||||
// Re-resolved by id against the live ledger, not the (possibly stale) cached manifest above:
|
||||
// fails gracefully if the app was uninstalled since this button was built, instead of handing
|
||||
@ -154,7 +154,7 @@ extern const ::AppManifest manifest = {
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
.stack = { .depth = 4000, .desired_memory_capability = 0 },
|
||||
.stack = { .depth = 5120, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -131,6 +131,7 @@ MemoryBarWidgets createMemoryBar(lv_obj_t* parent, const char* label) {
|
||||
lv_obj_set_style_pad_all(container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_bg_opa(container, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* left_label = lv_label_create(container);
|
||||
|
||||
@ -40,6 +40,11 @@ void onClosePressed(lv_event_t* event) {
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onCanvasClicked(lv_event_t* event) {
|
||||
auto* toolbar = static_cast<lv_obj_t*>(lv_event_get_user_data(event));
|
||||
lv_obj_set_flag(toolbar, LV_OBJ_FLAG_HIDDEN, !lv_obj_has_flag(toolbar, LV_OBJ_FLAG_HIDDEN));
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* root, void* userData) {
|
||||
auto* ctx = static_cast<WindowContext*>(userData);
|
||||
|
||||
@ -47,16 +52,21 @@ void createWidgets(lv_obj_t* root, void* userData) {
|
||||
lv_obj_set_style_pad_all(root, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_row(root, 0, LV_STATE_DEFAULT);
|
||||
|
||||
// The toolbar only offers a touch close button, so it is left out to give the terminal more room
|
||||
if (lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) {
|
||||
auto* toolbar = lvgl_toolbar_create(root, "Terminal");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClosePressed, ctx);
|
||||
}
|
||||
|
||||
auto* canvas = lv_canvas_create(root);
|
||||
lv_obj_set_width(canvas, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(canvas, 1);
|
||||
|
||||
// The toolbar only offers a touch close button. It's hidden to give the terminal all the room, and touching the terminal shows it on top.
|
||||
if (lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) {
|
||||
auto* toolbar = lvgl_toolbar_create(root, "Terminal");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClosePressed, ctx);
|
||||
lv_obj_add_flag(toolbar, LV_OBJ_FLAG_FLOATING);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
lv_obj_add_flag(toolbar, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(canvas, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(canvas, onCanvasClicked, LV_EVENT_SHORT_CLICKED, toolbar);
|
||||
}
|
||||
|
||||
ctx->renderer.attachCanvas(canvas);
|
||||
}
|
||||
|
||||
|
||||
@ -25,13 +25,13 @@ extern const ::AppManifest manifest;
|
||||
|
||||
constexpr auto* TAG = "TrackballSettings";
|
||||
|
||||
// Convert mode to dropdown index (dropdown order: Encoder=0, Pointer=1)
|
||||
// Convert mode to dropdown index (dropdown order: Keys=0, Pointer=1)
|
||||
static uint32_t modeToDropdownIndex(LvglTrackballMode mode) {
|
||||
switch (mode) {
|
||||
case LVGL_TRACKBALL_MODE_ENCODER: return 0;
|
||||
case LVGL_TRACKBALL_MODE_KEYS: return 0;
|
||||
case LVGL_TRACKBALL_MODE_POINTER: return 1;
|
||||
}
|
||||
return 0; // default to Encoder
|
||||
return 0; // default to Keys
|
||||
}
|
||||
|
||||
static lv_indev_t* findFirstTrackballIndev() {
|
||||
@ -65,7 +65,7 @@ struct Context {
|
||||
lv_indev_t* trackballIndev = nullptr;
|
||||
lv_obj_t* switchTrackball = nullptr;
|
||||
lv_obj_t* trackballModeDropdown = nullptr;
|
||||
lv_obj_t* encoderSensitivitySlider = nullptr;
|
||||
lv_obj_t* keySensitivitySlider = nullptr;
|
||||
lv_obj_t* pointerSensitivitySlider = nullptr;
|
||||
};
|
||||
|
||||
@ -97,11 +97,11 @@ void onTrackballSwitch(lv_event_t* e) {
|
||||
// Enable/disable controls based on trackball state
|
||||
if (enabled) {
|
||||
if (ctx->trackballModeDropdown) lv_obj_clear_state(ctx->trackballModeDropdown, LV_STATE_DISABLED);
|
||||
if (ctx->encoderSensitivitySlider) lv_obj_clear_state(ctx->encoderSensitivitySlider, LV_STATE_DISABLED);
|
||||
if (ctx->keySensitivitySlider) lv_obj_clear_state(ctx->keySensitivitySlider, LV_STATE_DISABLED);
|
||||
if (ctx->pointerSensitivitySlider) lv_obj_clear_state(ctx->pointerSensitivitySlider, LV_STATE_DISABLED);
|
||||
} else {
|
||||
if (ctx->trackballModeDropdown) lv_obj_add_state(ctx->trackballModeDropdown, LV_STATE_DISABLED);
|
||||
if (ctx->encoderSensitivitySlider) lv_obj_add_state(ctx->encoderSensitivitySlider, LV_STATE_DISABLED);
|
||||
if (ctx->keySensitivitySlider) lv_obj_add_state(ctx->keySensitivitySlider, LV_STATE_DISABLED);
|
||||
if (ctx->pointerSensitivitySlider) lv_obj_add_state(ctx->pointerSensitivitySlider, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
@ -110,10 +110,10 @@ void onTrackballModeChanged(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
uint32_t selected = lv_dropdown_get_selected(ctx->trackballModeDropdown);
|
||||
|
||||
// Validate selection matches expected enum values (dropdown order: Encoder=0, Pointer=1)
|
||||
// Validate selection matches expected enum values (dropdown order: Keys=0, Pointer=1)
|
||||
LvglTrackballMode mode;
|
||||
switch (selected) {
|
||||
case 0: mode = LVGL_TRACKBALL_MODE_ENCODER; break;
|
||||
case 0: mode = LVGL_TRACKBALL_MODE_KEYS; break;
|
||||
case 1: mode = LVGL_TRACKBALL_MODE_POINTER; break;
|
||||
default: return; // Invalid selection, ignore
|
||||
}
|
||||
@ -125,10 +125,10 @@ void onTrackballModeChanged(lv_event_t* e) {
|
||||
applyLive(ctx);
|
||||
}
|
||||
|
||||
void onEncoderSensitivityChanged(lv_event_t* e) {
|
||||
void onKeySensitivityChanged(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
int32_t value = lv_slider_get_value(ctx->encoderSensitivitySlider);
|
||||
ctx->tbSettings.encoder_sensitivity = static_cast<uint8_t>(value);
|
||||
int32_t value = lv_slider_get_value(ctx->keySensitivitySlider);
|
||||
ctx->tbSettings.key_sensitivity = static_cast<uint8_t>(value);
|
||||
ctx->updated = true;
|
||||
|
||||
// Apply immediately
|
||||
@ -195,7 +195,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_align(tb_mode_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->trackballModeDropdown = lv_dropdown_create(tb_mode_wrapper);
|
||||
lv_dropdown_set_options(ctx->trackballModeDropdown, "Encoder\nPointer");
|
||||
lv_dropdown_set_options(ctx->trackballModeDropdown, "Keys\nPointer");
|
||||
lv_obj_align(ctx->trackballModeDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_dropdown_set_selected(ctx->trackballModeDropdown, modeToDropdownIndex(ctx->tbSettings.mode));
|
||||
lv_obj_add_event_cb(ctx->trackballModeDropdown, onTrackballModeChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
@ -205,25 +205,25 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_add_state(ctx->trackballModeDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
// Encoder sensitivity slider
|
||||
auto* enc_sens_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(enc_sens_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_hor(enc_sens_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(enc_sens_wrapper, 0, LV_STATE_DEFAULT);
|
||||
// Key sensitivity slider
|
||||
auto* key_sens_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(key_sens_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_hor(key_sens_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(key_sens_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* enc_sens_label = lv_label_create(enc_sens_wrapper);
|
||||
lv_label_set_text(enc_sens_label, "Encoder Speed");
|
||||
lv_obj_align(enc_sens_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
auto* key_sens_label = lv_label_create(key_sens_wrapper);
|
||||
lv_label_set_text(key_sens_label, "Key Speed");
|
||||
lv_obj_align(key_sens_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->encoderSensitivitySlider = lv_slider_create(enc_sens_wrapper);
|
||||
lv_slider_set_range(ctx->encoderSensitivitySlider, 1, 10);
|
||||
lv_slider_set_value(ctx->encoderSensitivitySlider, ctx->tbSettings.encoder_sensitivity, LV_ANIM_OFF);
|
||||
lv_obj_set_width(ctx->encoderSensitivitySlider, LV_PCT(50));
|
||||
lv_obj_align(ctx->encoderSensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->encoderSensitivitySlider, onEncoderSensitivityChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
ctx->keySensitivitySlider = lv_slider_create(key_sens_wrapper);
|
||||
lv_slider_set_range(ctx->keySensitivitySlider, 1, 10);
|
||||
lv_slider_set_value(ctx->keySensitivitySlider, ctx->tbSettings.key_sensitivity, LV_ANIM_OFF);
|
||||
lv_obj_set_width(ctx->keySensitivitySlider, LV_PCT(50));
|
||||
lv_obj_align(ctx->keySensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->keySensitivitySlider, onKeySensitivityChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
if (!ctx->tbSettings.enabled) {
|
||||
lv_obj_add_state(ctx->encoderSensitivitySlider, LV_STATE_DISABLED);
|
||||
lv_obj_add_state(ctx->keySensitivitySlider, LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
// Pointer sensitivity slider
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#include <Tactility/lvgl/Lvgl.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
@ -22,6 +24,7 @@
|
||||
#include <app/manager.h>
|
||||
|
||||
#include <lvgl/devices/keyboard.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/devices/pointer.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
@ -79,7 +82,8 @@ static lv_obj_t* windowManagerScreenInit(lv_obj_t* root) {
|
||||
lv_obj_set_style_radius(vertical_container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_remove_flag(vertical_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lvgl::statusbar_create(vertical_container);
|
||||
// The statusbar fits itself into the top of round and rounded displays
|
||||
auto* statusbar = lvgl::statusbar_create(vertical_container);
|
||||
|
||||
auto* app_container = lv_obj_create(vertical_container);
|
||||
lv_obj_set_style_pad_all(app_container, 0, LV_STATE_DEFAULT);
|
||||
@ -89,6 +93,21 @@ static lv_obj_t* windowManagerScreenInit(lv_obj_t* root) {
|
||||
lv_obj_set_flex_flow(app_container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_remove_flag(app_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
// Keeps the apps inside the visible area of round displays.
|
||||
// On rounded displays, the widgets along the edges move their content inward from the corners instead.
|
||||
lv_display_t* display = lv_obj_get_display(root);
|
||||
DisplayShape shape;
|
||||
lvgl_display_get_shape(display, &shape);
|
||||
if (shape.shape == DISPLAY_SHAPE_CIRCLE) {
|
||||
LvglInsets insets;
|
||||
lvgl_display_get_insets(display, &insets);
|
||||
lv_obj_update_layout(statusbar);
|
||||
lv_obj_set_style_pad_top(app_container, std::max<int32_t>(0, insets.top - lv_obj_get_height(statusbar)), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_bottom(app_container, insets.bottom, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_left(app_container, insets.left, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_right(app_container, insets.right, LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
// Parented to root (not app_container/vertical_container) so it overlays on top of
|
||||
// everything, including the statusbar, regardless of which app is showing. Hidden until a
|
||||
// focused textarea shows it (see lvgl_keyboard_add_textarea()/textarea_show_keyboard()).
|
||||
|
||||
@ -12,9 +12,16 @@
|
||||
#include <tactility/system_event.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/theme.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
@ -35,6 +42,7 @@ static void onUpdateTime();
|
||||
|
||||
struct StatusbarIcon {
|
||||
std::string image;
|
||||
std::string appId;
|
||||
bool visible = false;
|
||||
bool claimed = false;
|
||||
};
|
||||
@ -159,6 +167,22 @@ static void update_icon(lv_obj_t* image, const StatusbarIcon* icon) {
|
||||
} else {
|
||||
lv_obj_add_flag(image, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
lv_obj_set_flag(image, LV_OBJ_FLAG_CLICKABLE, !icon->appId.empty());
|
||||
}
|
||||
|
||||
static void onIconClicked(lv_event_t* event) {
|
||||
const auto index = reinterpret_cast<intptr_t>(lv_event_get_user_data(event));
|
||||
statusbar_data.mutex.lock();
|
||||
const std::string app_id = statusbar_data.icons[index].appId;
|
||||
statusbar_data.mutex.unlock();
|
||||
if (app_id.empty()) {
|
||||
return;
|
||||
}
|
||||
AppStartContext context;
|
||||
uint32_t instance_id = 0;
|
||||
if (app_start_context_from_id(app_id.c_str(), &context) == ERROR_NONE) {
|
||||
app_start_with_context(&context, &instance_id);
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
@ -168,42 +192,81 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
|
||||
auto* statusbar = reinterpret_cast<Statusbar*>(obj);
|
||||
|
||||
// The statusbar has no theme styles: it shows the themed container background behind it and inherits its text colour
|
||||
if (STATUSBAR_COLORS_INVERTED) {
|
||||
// A horizontal line separates the statusbar from the rest of the UI below it
|
||||
lv_obj_set_style_border_color(obj, lv_obj_get_style_text_color(obj, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(obj, 1, LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
lv_obj_set_width(obj, LV_PCT(100));
|
||||
lv_obj_set_style_pad_ver(obj, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_hor(obj, 2, LV_STATE_DEFAULT);
|
||||
lv_obj_center(obj);
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
auto icon_size = lvgl_get_statusbar_icon_font_height();
|
||||
auto ui_density = lvgl_get_ui_density();
|
||||
auto icon_padding = (ui_density != LVGL_UI_DENSITY_COMPACT) ? static_cast<uint32_t>(icon_size * 0.2f) : 2;
|
||||
lv_obj_set_style_pad_column(obj, icon_padding, LV_STATE_DEFAULT);
|
||||
|
||||
statusbar->time = lv_label_create(obj);
|
||||
lv_obj_set_style_margin_left(statusbar->time, 4, LV_STATE_DEFAULT);
|
||||
update_time(statusbar);
|
||||
lv_display_t* display = lv_obj_get_display(obj);
|
||||
DisplayShape shape;
|
||||
lvgl_display_get_shape(display, &shape);
|
||||
|
||||
auto* left_spacer = lv_obj_create(obj);
|
||||
lv_obj_remove_flag(left_spacer, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(left_spacer, 1, 1);
|
||||
lv_obj_set_flex_grow(left_spacer, 1);
|
||||
lv_obj_set_style_bg_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT);
|
||||
// The parent of the icons, which grid navigation moves through
|
||||
lv_obj_t* icon_row;
|
||||
if (shape.shape == DISPLAY_SHAPE_CIRCLE) {
|
||||
// Two centered rows, as the top of a circle is narrow: the time, and the icons below it
|
||||
const int32_t diameter = std::min(lv_display_get_horizontal_resolution(display), lv_display_get_vertical_resolution(display));
|
||||
lv_obj_set_height(obj, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_top(obj, diameter / 24, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
statusbar->time = lv_label_create(obj);
|
||||
update_time(statusbar);
|
||||
|
||||
icon_row = lv_obj_create(obj);
|
||||
lv_obj_remove_style_all(icon_row);
|
||||
lv_obj_set_size(icon_row, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_ver(icon_row, static_cast<int32_t>(icon_size / 10), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(icon_row, icon_padding, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(icon_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(icon_row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
} else {
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(obj, icon_padding, LV_STATE_DEFAULT);
|
||||
|
||||
if (shape.shape == DISPLAY_SHAPE_ROUNDED) {
|
||||
// Moves the content inward from the rounded corners, measured at the top of the icons
|
||||
const int32_t content_top = (statusbar_get_height() - static_cast<int32_t>(icon_size)) / 2;
|
||||
lv_obj_set_style_pad_hor(obj, 2 + lvgl_display_get_row_inset(display, content_top), LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
statusbar->time = lv_label_create(obj);
|
||||
lv_obj_set_style_margin_left(statusbar->time, 4, LV_STATE_DEFAULT);
|
||||
update_time(statusbar);
|
||||
|
||||
auto* left_spacer = lv_obj_create(obj);
|
||||
lv_obj_remove_flag(left_spacer, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(left_spacer, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(left_spacer, 1, 1);
|
||||
lv_obj_set_flex_grow(left_spacer, 1);
|
||||
lv_obj_set_style_bg_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT);
|
||||
|
||||
icon_row = obj;
|
||||
}
|
||||
|
||||
// The monochrome theme's focus ring doesn't contrast with the statusbar, so the icons get their own
|
||||
const bool own_focus_ring = lvgl_theme_is_mono();
|
||||
const int32_t focus_width = (ui_density != LVGL_UI_DENSITY_COMPACT) ? 2 : 1;
|
||||
const lv_color_t focus_color = lv_obj_get_style_text_color(obj, LV_PART_MAIN);
|
||||
|
||||
statusbar_data.mutex.lock(MAX_TICKS);
|
||||
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
|
||||
auto* image = lv_image_create(obj);
|
||||
auto* image = lv_image_create(icon_row);
|
||||
lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work
|
||||
lv_obj_set_style_text_font(image, lvgl_get_statusbar_icon_font(), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(image, 0, LV_STATE_DEFAULT);
|
||||
if (own_focus_ring) {
|
||||
lv_obj_set_style_outline_color(image, focus_color, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_set_style_outline_width(image, focus_width, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_set_style_outline_pad(image, -focus_width, LV_STATE_FOCUS_KEY);
|
||||
}
|
||||
lv_obj_add_event_cb(image, onIconClicked, LV_EVENT_SHORT_CLICKED, reinterpret_cast<void*>(static_cast<intptr_t>(i)));
|
||||
statusbar->icons[i] = image;
|
||||
|
||||
update_icon(image, &(statusbar_data.icons[i]));
|
||||
@ -218,6 +281,15 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
});
|
||||
statusbar_data.mutex.unlock();
|
||||
|
||||
// Without a touchscreen, the icons are selected with the keys
|
||||
if (lvgl_indev_exists(LV_INDEV_TYPE_KEYPAD)) {
|
||||
lvgl_grid_navigation_add(icon_row);
|
||||
}
|
||||
// Only the icon row is focused when it isn't the statusbar itself
|
||||
if (icon_row != obj && lv_obj_get_group(obj) != nullptr) {
|
||||
lv_group_remove_obj(obj);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@ -287,6 +359,7 @@ void statusbar_icon_remove(int8_t id) {
|
||||
icon->claimed = false;
|
||||
icon->visible = false;
|
||||
icon->image = "";
|
||||
icon->appId = "";
|
||||
statusbar_data.mutex.unlock();
|
||||
statusbar_data.pubsub->publish(nullptr);
|
||||
}
|
||||
@ -317,6 +390,16 @@ void statusbar_icon_set_visibility(int8_t id, bool visible) {
|
||||
statusbar_data.pubsub->publish(nullptr);
|
||||
}
|
||||
|
||||
void statusbar_icon_set_app(int8_t id, const std::string& appId) {
|
||||
check(id >= 0 && id < STATUSBAR_ICON_LIMIT);
|
||||
statusbar_data.mutex.lock();
|
||||
StatusbarIcon* icon = &statusbar_data.icons[id];
|
||||
check(icon->claimed);
|
||||
icon->appId = appId;
|
||||
statusbar_data.mutex.unlock();
|
||||
statusbar_data.pubsub->publish(nullptr);
|
||||
}
|
||||
|
||||
int statusbar_get_height() {
|
||||
const auto icon_size = lvgl_get_statusbar_icon_font_height();
|
||||
const auto vertical_padding = static_cast<uint32_t>((static_cast<float>(icon_size) * 0.1f));
|
||||
|
||||
@ -55,6 +55,7 @@ bool MemoryCheckerService::onStart(ServiceContext& service) {
|
||||
lock.lock();
|
||||
|
||||
statusbarIconId = lvgl::statusbar_icon_add(LVGL_ICON_STATUSBAR_MEMORY, false);
|
||||
lvgl::statusbar_icon_set_app(statusbarIconId, "tactility.systeminfo");
|
||||
lvgl::statusbar_icon_set_visibility(statusbarIconId, false);
|
||||
|
||||
timer.setCallbackPriority(Thread::Priority::Lower);
|
||||
|
||||
@ -85,7 +85,7 @@ static const char* getBluetoothStatusIcon(tt::bluetooth::RadioState state, bool
|
||||
using enum tt::bluetooth::RadioState;
|
||||
case Off:
|
||||
case OffPending:
|
||||
return nullptr; // hidden when off
|
||||
return LVGL_ICON_STATUSBAR_BLUETOOTH_DISABLED;
|
||||
case OnPending:
|
||||
case On:
|
||||
if (connected) return LVGL_ICON_STATUSBAR_BLUETOOTH_CONNECTED;
|
||||
@ -144,7 +144,7 @@ class StatusbarService final : public Service {
|
||||
Mutex mutex;
|
||||
std::unique_ptr<Timer> updateTimer;
|
||||
int8_t gps_icon_id;
|
||||
bool gps_last_state = false;
|
||||
const char* gps_last_icon = nullptr;
|
||||
int8_t bt_icon_id;
|
||||
const char* bt_last_icon = nullptr;
|
||||
int8_t wifi_icon_id;
|
||||
@ -165,15 +165,20 @@ class StatusbarService final : public Service {
|
||||
}
|
||||
|
||||
void updateGpsIcon() {
|
||||
bool show_icon = device_has_active_by_type(&GPS_TYPE);
|
||||
if (gps_last_state != show_icon) {
|
||||
if (show_icon) {
|
||||
lvgl::statusbar_icon_set_image(gps_icon_id, LVGL_ICON_STATUSBAR_LOCATION_ON);
|
||||
const char* icon = nullptr;
|
||||
if (device_has_active_by_type(&GPS_TYPE)) {
|
||||
icon = LVGL_ICON_STATUSBAR_LOCATION_ON;
|
||||
} else if (device_exists_of_type(&GPS_TYPE)) {
|
||||
icon = LVGL_ICON_STATUSBAR_LOCATION_OFF;
|
||||
}
|
||||
if (gps_last_icon != icon) {
|
||||
if (icon != nullptr) {
|
||||
lvgl::statusbar_icon_set_image(gps_icon_id, icon);
|
||||
lvgl::statusbar_icon_set_visibility(gps_icon_id, true);
|
||||
} else {
|
||||
lvgl::statusbar_icon_set_visibility(gps_icon_id, false);
|
||||
}
|
||||
gps_last_state = show_icon;
|
||||
gps_last_icon = icon;
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,6 +342,12 @@ public:
|
||||
sdcard_icon_id = lvgl::statusbar_icon_add();
|
||||
wifi_icon_id = lvgl::statusbar_icon_add();
|
||||
power_icon_id = lvgl::statusbar_icon_add();
|
||||
lvgl::statusbar_icon_set_app(gps_icon_id, "tactility.gpssettings");
|
||||
lvgl::statusbar_icon_set_app(bt_icon_id, "tactility.btmanage");
|
||||
lvgl::statusbar_icon_set_app(usb_icon_id, "tactility.usbsettings");
|
||||
lvgl::statusbar_icon_set_app(sdcard_icon_id, "tactility.files");
|
||||
lvgl::statusbar_icon_set_app(wifi_icon_id, "tactility.wifimanage");
|
||||
lvgl::statusbar_icon_set_app(power_icon_id, "tactility.power");
|
||||
}
|
||||
|
||||
~StatusbarService() override {
|
||||
|
||||
@ -206,6 +206,7 @@ bool WebServerService::onStart(ServiceContext& service) {
|
||||
|
||||
// Create statusbar icon (hidden initially, shown when server actually starts)
|
||||
statusbarIconId = lvgl::statusbar_icon_add();
|
||||
lvgl::statusbar_icon_set_app(statusbarIconId, "tactility.webserversettings");
|
||||
lvgl::statusbar_icon_set_visibility(statusbarIconId, false);
|
||||
|
||||
// Load and cache settings once at boot
|
||||
|
||||
@ -26,8 +26,8 @@ constexpr auto* KEY_TRACKBALL_MODE = "trackballMode";
|
||||
constexpr auto* KEY_ENCODER_SENSITIVITY = "encoderSensitivity";
|
||||
constexpr auto* KEY_POINTER_SENSITIVITY = "pointerSensitivity";
|
||||
|
||||
constexpr uint8_t MIN_ENCODER_SENSITIVITY = 1;
|
||||
constexpr uint8_t MAX_ENCODER_SENSITIVITY = 10;
|
||||
constexpr uint8_t MIN_KEY_SENSITIVITY = 1;
|
||||
constexpr uint8_t MAX_KEY_SENSITIVITY = 10;
|
||||
constexpr uint8_t MIN_POINTER_SENSITIVITY = 1;
|
||||
constexpr uint8_t MAX_POINTER_SENSITIVITY = 10;
|
||||
|
||||
@ -64,13 +64,13 @@ bool load(LvglTrackballSettings& settings) {
|
||||
return s == "1" || s == "true" || s == "True" || s == "TRUE";
|
||||
};
|
||||
settings.enabled = (tb_enabled != map.end()) ? isTrueValue(tb_enabled->second) : true;
|
||||
settings.mode = (tb_mode != map.end() && tb_mode->second == "1") ? LVGL_TRACKBALL_MODE_POINTER : LVGL_TRACKBALL_MODE_ENCODER;
|
||||
settings.mode = (tb_mode != map.end() && tb_mode->second == "1") ? LVGL_TRACKBALL_MODE_POINTER : LVGL_TRACKBALL_MODE_KEYS;
|
||||
auto default_settings = lvgl_trackball_settings_get_default();
|
||||
settings.encoder_sensitivity = (enc_sens != map.end()) ? safeParseUint8(enc_sens->second, default_settings.encoder_sensitivity) : default_settings.encoder_sensitivity;
|
||||
settings.key_sensitivity = (enc_sens != map.end()) ? safeParseUint8(enc_sens->second, default_settings.key_sensitivity) : default_settings.key_sensitivity;
|
||||
settings.pointer_sensitivity = (ptr_sens != map.end()) ? safeParseUint8(ptr_sens->second, default_settings.pointer_sensitivity) : default_settings.pointer_sensitivity;
|
||||
|
||||
// Clamp values to valid ranges
|
||||
settings.encoder_sensitivity = std::clamp(settings.encoder_sensitivity, MIN_ENCODER_SENSITIVITY, MAX_ENCODER_SENSITIVITY);
|
||||
settings.key_sensitivity = std::clamp(settings.key_sensitivity, MIN_KEY_SENSITIVITY, MAX_KEY_SENSITIVITY);
|
||||
settings.pointer_sensitivity = std::clamp(settings.pointer_sensitivity, MIN_POINTER_SENSITIVITY, MAX_POINTER_SENSITIVITY);
|
||||
|
||||
return true;
|
||||
@ -92,7 +92,7 @@ bool save(const LvglTrackballSettings& settings) {
|
||||
std::map<std::string, std::string> map;
|
||||
map[KEY_TRACKBALL_ENABLED] = settings.enabled ? "1" : "0";
|
||||
map[KEY_TRACKBALL_MODE] = (settings.mode == LVGL_TRACKBALL_MODE_POINTER) ? "1" : "0";
|
||||
map[KEY_ENCODER_SENSITIVITY] = std::to_string(std::clamp(settings.encoder_sensitivity, MIN_ENCODER_SENSITIVITY, MAX_ENCODER_SENSITIVITY));
|
||||
map[KEY_ENCODER_SENSITIVITY] = std::to_string(std::clamp(settings.key_sensitivity, MIN_KEY_SENSITIVITY, MAX_KEY_SENSITIVITY));
|
||||
map[KEY_POINTER_SENSITIVITY] = std::to_string(std::clamp(settings.pointer_sensitivity, MIN_POINTER_SENSITIVITY, MAX_POINTER_SENSITIVITY));
|
||||
auto settings_path = getSettingsFilePath();
|
||||
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
||||
|
||||
11
TactilityKernel/bindings/display.yaml
Normal file
11
TactilityKernel/bindings/display.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
description: Common display properties
|
||||
|
||||
properties:
|
||||
shape:
|
||||
type: int
|
||||
default: DISPLAY_SHAPE_RECTANGLE
|
||||
description: Shape of the visible area (DISPLAY_SHAPE_RECTANGLE, DISPLAY_SHAPE_ROUNDED or DISPLAY_SHAPE_CIRCLE)
|
||||
corner-radius:
|
||||
type: int
|
||||
default: 0
|
||||
description: Corner radius in pixels, for DISPLAY_SHAPE_ROUNDED
|
||||
@ -10,6 +10,27 @@ extern "C" {
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
/**
|
||||
* @brief The shape of the display's visible area.
|
||||
*/
|
||||
enum DisplayShapeType {
|
||||
DISPLAY_SHAPE_RECTANGLE = 0,
|
||||
/** A rectangle with rounded corners */
|
||||
DISPLAY_SHAPE_ROUNDED = 1,
|
||||
/** A circle with the diameter of the shortest side, centered on the panel */
|
||||
DISPLAY_SHAPE_CIRCLE = 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The visible area of a display. Display bindings that include display.yaml get it from the devicetree.
|
||||
*/
|
||||
struct DisplayShape {
|
||||
/** A DisplayShapeType value */
|
||||
uint8_t shape;
|
||||
/** The corner radius in pixels, for DISPLAY_SHAPE_ROUNDED */
|
||||
uint16_t corner_radius;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Optional capabilities of the display.
|
||||
*/
|
||||
@ -255,6 +276,14 @@ struct DisplayApi {
|
||||
* @return true if all specified capabilities are available for this device instance
|
||||
*/
|
||||
bool (*has_capability)(struct Device* device, uint32_t capability);
|
||||
|
||||
/**
|
||||
* @brief Gets the shape of the display's visible area.
|
||||
* @warning Nullable. When null, display_get_shape() reports a rectangle.
|
||||
* @param[in] device the display device
|
||||
* @param[out] shape the shape
|
||||
*/
|
||||
void (*get_shape)(struct Device* device, struct DisplayShape* shape);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -387,6 +416,13 @@ uint8_t display_get_frame_buffer_count(struct Device* device);
|
||||
*/
|
||||
error_t display_get_backlight(struct Device* device, struct Device** backlight);
|
||||
|
||||
/**
|
||||
* @brief Gets the shape of the display's visible area.
|
||||
* @param[in] device the display device
|
||||
* @param[out] shape the shape: a rectangle when the driver doesn't report one
|
||||
*/
|
||||
void display_get_shape(struct Device* device, struct DisplayShape* shape);
|
||||
|
||||
extern const struct DeviceType DISPLAY_TYPE;
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@ -12,6 +12,16 @@ extern "C" {
|
||||
#define FILE_MAX_PATH_LENGTH 255
|
||||
#define FILE_MAX_PATH_STRING_LENGTH (FILE_MAX_PATH_LENGTH + 1)
|
||||
|
||||
/**
|
||||
* @brief Get the root path of the storage that holds the user data (e.g. the SD card).
|
||||
* @param[out] out_path buffer to store the path (no trailing "/")
|
||||
* @param[in] out_path_size size of the output buffer
|
||||
* @retval ERROR_NOT_FOUND if the configured storage location isn't available (e.g. no SD card)
|
||||
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t paths_get_data_root_path(char* out_path, size_t out_path_size);
|
||||
|
||||
/**
|
||||
* @brief Get the root path for user data. Survives OS upgrades.
|
||||
* @param[out] out_path buffer to store the path (no trailing "/")
|
||||
|
||||
@ -157,6 +157,16 @@ error_t display_get_backlight(Device* device, Device** backlight) {
|
||||
return error;
|
||||
}
|
||||
|
||||
void display_get_shape(Device* device, DisplayShape* shape) {
|
||||
const auto* driver = device_get_driver(device);
|
||||
const auto* api = DISPLAY_DRIVER_API(driver);
|
||||
if (api->get_shape == nullptr) {
|
||||
*shape = { .shape = DISPLAY_SHAPE_RECTANGLE, .corner_radius = 0 };
|
||||
return;
|
||||
}
|
||||
api->get_shape(device, shape);
|
||||
}
|
||||
|
||||
const struct DeviceType DISPLAY_TYPE {
|
||||
.name = "display"
|
||||
};
|
||||
|
||||
@ -8,7 +8,9 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
static error_t paths_get_data_root_path(char* out_path, size_t out_path_size) {
|
||||
extern "C" {
|
||||
|
||||
error_t paths_get_data_root_path(char* out_path, size_t out_path_size) {
|
||||
#if defined(CONFIG_TT_USER_DATA_LOCATION_INTERNAL)
|
||||
// Registered by platform-esp32/platform-posix's own module start() (see file_system_add()'s
|
||||
// "data" name), rather than a literal here: on POSIX that filesystem's real path is resolved
|
||||
@ -38,8 +40,6 @@ static error_t paths_get_data_root_path(char* out_path, size_t out_path_size) {
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t paths_get_data_path(char* out_path, size_t out_path_size) {
|
||||
// Sized for a real, realpath()-resolved absolute path (see paths_get_data_root_path()'s
|
||||
// CONFIG_TT_USER_DATA_LOCATION_INTERNAL branch), not just the short literal this used to be.
|
||||
|
||||
@ -173,6 +173,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(display_get_frame_buffer),
|
||||
DEFINE_MODULE_SYMBOL(display_get_frame_buffer_count),
|
||||
DEFINE_MODULE_SYMBOL(display_get_backlight),
|
||||
DEFINE_MODULE_SYMBOL(display_get_shape),
|
||||
DEFINE_MODULE_SYMBOL(DISPLAY_TYPE),
|
||||
// file system
|
||||
DEFINE_MODULE_SYMBOL(file_system_mount),
|
||||
@ -271,6 +272,7 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(led_strip_get_length),
|
||||
DEFINE_MODULE_SYMBOL(LED_STRIP_TYPE),
|
||||
// drivers/paths
|
||||
DEFINE_MODULE_SYMBOL(paths_get_data_root_path),
|
||||
DEFINE_MODULE_SYMBOL(paths_get_data_path),
|
||||
DEFINE_MODULE_SYMBOL(paths_get_temp_path),
|
||||
// drivers/pointer
|
||||
|
||||
@ -14,6 +14,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/app-posix-module/tests ${CMAKE_CURR
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/http-module/tests ${CMAKE_CURRENT_BINARY_DIR}/http-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/binfont-module/tests ${CMAKE_CURRENT_BINARY_DIR}/binfont-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/lodepng-module/tests ${CMAKE_CURRENT_BINARY_DIR}/lodepng-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/lvgl-module/tests ${CMAKE_CURRENT_BINARY_DIR}/lvgl-module)
|
||||
|
||||
add_custom_target(build-tests)
|
||||
add_dependencies(build-tests ServiceModuleTests)
|
||||
@ -27,3 +28,4 @@ add_dependencies(build-tests AppPosixModuleTests)
|
||||
add_dependencies(build-tests HttpModuleTests)
|
||||
add_dependencies(build-tests BinFontModuleTests)
|
||||
add_dependencies(build-tests LodePngModuleTests)
|
||||
add_dependencies(build-tests LvglModuleTests)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user