UX improvements: AppList, Settings, non-touch devices (#671)

- Implement double size icon assets in `lvgl-module`
- `AppList` and `Settings` app now show a paginated grid of icons
- T-Lora Pager gets a larger font to improve readability (14 -> 16 pixels)
- Improve default widget selection in several apps (`PowerOff`, `AlertDialog`, `AppList`, `Settings`, `Boot`)
- USB HID LVGL device is now only created when necessary. And the code is called in a safer way.
- Renamed “Region & Language” to “Locale.”
- The terminal toolbar is shown only when pointer input is available.
This commit is contained in:
Ken Van Hoeylandt 2026-10-04 01:07:23 +02:00 committed by GitHub
parent 34ca63ec3c
commit 2b6b4b5167
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 7990 additions and 232 deletions

View File

@ -22,3 +22,4 @@ display.dpi=227
lvgl.colorDepth=16 lvgl.colorDepth=16
lvgl.dpi=150 lvgl.dpi=150
lvgl.uiDensity=compact lvgl.uiDensity=compact
lvgl.fontSize=16

View File

@ -14,6 +14,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
GET_PROPERTY_VALUE(sdkconfig_text "CONFIG_TT_LVGL_STATUSBAR_ICON_SIZE" statusbar_symbol_size) GET_PROPERTY_VALUE(sdkconfig_text "CONFIG_TT_LVGL_STATUSBAR_ICON_SIZE" statusbar_symbol_size)
GET_PROPERTY_VALUE(sdkconfig_text "CONFIG_TT_LVGL_LAUNCHER_ICON_SIZE" launcher_symbol_size) GET_PROPERTY_VALUE(sdkconfig_text "CONFIG_TT_LVGL_LAUNCHER_ICON_SIZE" launcher_symbol_size)
GET_PROPERTY_VALUE(sdkconfig_text "CONFIG_TT_LVGL_SHARED_ICON_SIZE" shared_symbol_size) GET_PROPERTY_VALUE(sdkconfig_text "CONFIG_TT_LVGL_SHARED_ICON_SIZE" shared_symbol_size)
math(EXPR shared_symbol_2x_size "${shared_symbol_size} * 2")
else () else ()
# Default values for simulator # Default values for simulator
set(font_size_small 10) set(font_size_small 10)
@ -22,6 +23,7 @@ else ()
set(statusbar_symbol_size 20) set(statusbar_symbol_size 20)
set(launcher_symbol_size 36) set(launcher_symbol_size 36)
set(shared_symbol_size 16) set(shared_symbol_size 16)
set(shared_symbol_2x_size 32)
endif () endif ()
message("Font sizes:") message("Font sizes:")
@ -32,6 +34,7 @@ message("Icon sizes:")
message(" - statusbar: ${statusbar_symbol_size}") message(" - statusbar: ${statusbar_symbol_size}")
message(" - launcher: ${launcher_symbol_size}") message(" - launcher: ${launcher_symbol_size}")
message(" - shared: ${shared_symbol_size}") message(" - shared: ${shared_symbol_size}")
message(" - shared_2x: ${shared_symbol_2x_size}")
# #################################### # ####################################
# Read device properties # Read device properties
@ -71,6 +74,7 @@ file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
list(APPEND SOURCE_FILES "source-fonts/material_symbols_statusbar_${statusbar_symbol_size}.c") list(APPEND SOURCE_FILES "source-fonts/material_symbols_statusbar_${statusbar_symbol_size}.c")
list(APPEND SOURCE_FILES "source-fonts/material_symbols_launcher_${launcher_symbol_size}.c") list(APPEND SOURCE_FILES "source-fonts/material_symbols_launcher_${launcher_symbol_size}.c")
list(APPEND SOURCE_FILES "source-fonts/material_symbols_shared_${shared_symbol_size}.c") list(APPEND SOURCE_FILES "source-fonts/material_symbols_shared_${shared_symbol_size}.c")
list(APPEND SOURCE_FILES "source-fonts/material_symbols_shared_${shared_symbol_2x_size}.c")
list(APPEND REQUIRES_LIST list(APPEND REQUIRES_LIST
TactilityKernel TactilityKernel
@ -119,6 +123,8 @@ target_compile_definitions(${MODULE_NAME} PUBLIC
"-DTT_LVGL_LAUNCHER_FONT_ICON_SYMBOL=material_symbols_launcher_${launcher_symbol_size}" "-DTT_LVGL_LAUNCHER_FONT_ICON_SYMBOL=material_symbols_launcher_${launcher_symbol_size}"
"-DTT_LVGL_SHARED_FONT_ICON_SIZE=${shared_symbol_size}" "-DTT_LVGL_SHARED_FONT_ICON_SIZE=${shared_symbol_size}"
"-DTT_LVGL_SHARED_FONT_ICON_SYMBOL=material_symbols_shared_${shared_symbol_size}" "-DTT_LVGL_SHARED_FONT_ICON_SYMBOL=material_symbols_shared_${shared_symbol_size}"
"-DTT_LVGL_SHARED_FONT_ICON_2X_SIZE=${shared_symbol_2x_size}"
"-DTT_LVGL_SHARED_FONT_ICON_2X_SYMBOL=material_symbols_shared_${shared_symbol_2x_size}"
# UiDensity # UiDensity
"-DTT_LVGL_UI_DENSITY=${ui_density_variable}" "-DTT_LVGL_UI_DENSITY=${ui_density_variable}"
) )

View File

@ -1,3 +1,4 @@
#!/usr/bin/env python3
import os import os
import urllib.request import urllib.request
@ -10,7 +11,7 @@ def download_file(url: str, filename: str):
def generate(bpp, size, font_file: str, symbols: list, output: str): def generate(bpp, size, font_file: str, symbols: list, output: str):
output_file_name = f"{output}_{size}.c" output_file_name = f"{output}_{size}.c"
output_path = os.path.join("..", "source-fonts", output_file_name) output_path = os.path.join("source-fonts", output_file_name)
print(f"Generating {output_file_name}") print(f"Generating {output_file_name}")
cmd = "lv_font_conv --no-compress --no-prefilter --bpp {} --size {} --font {} -r {} --format lvgl -o {} --force-fast-kern-format".format(bpp, size, font_file, ",".join(symbols), output_path) cmd = "lv_font_conv --no-compress --no-prefilter --bpp {} --size {} --font {} -r {} --format lvgl -o {} --force-fast-kern-format".format(bpp, size, font_file, ",".join(symbols), output_path)
ret = os.system(cmd) ret = os.system(cmd)
@ -46,7 +47,7 @@ def generate_icon_fonts(font_file, font_sizes, symbols, output):
def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_name: str): def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_name: str):
filename = f"{variable_name}.h" filename = f"{variable_name}.h"
print(f"Generating {filename}") print(f"Generating {filename}")
output_path = os.path.join("..", "include", "lvgl", "icons", filename) output_path = os.path.join("include", "lvgl", "icons", filename)
with open(output_path, 'w') as f: with open(output_path, 'w') as f:
f.write("// SPDX-License-Identifier: Apache-2.0\n\n") f.write("// SPDX-License-Identifier: Apache-2.0\n\n")
f.write("#pragma once\n\n") f.write("#pragma once\n\n")
@ -165,7 +166,10 @@ shared_symbol_sizes = [
16, # T-Deck reference, with montserrat 14 in lv_list as icon with text 16, # T-Deck reference, with montserrat 14 in lv_list as icon with text
20, 20,
24, 24,
32 32,
40,
48,
64
] ]
statusbar_symbol_sizes = [ statusbar_symbol_sizes = [

View File

@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <lvgl.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Checks whether any registered LVGL input device is of the given type.
* @warning Caller must hold the LVGL lock.
* @param[in] type the input device type to look for
* @return true if at least one input device of the given type exists
*/
bool lvgl_indev_exists(lv_indev_type_t type);
#ifdef __cplusplus
}
#endif

View File

@ -17,6 +17,9 @@ enum LvglFontSize {
const lv_font_t* lvgl_get_shared_icon_font(void); const lv_font_t* lvgl_get_shared_icon_font(void);
uint32_t lvgl_get_shared_icon_font_height(void); uint32_t lvgl_get_shared_icon_font_height(void);
const lv_font_t* lvgl_get_shared_icon_font_2x(void);
uint32_t lvgl_get_shared_icon_font_2x_height(void);
const lv_font_t* lvgl_get_text_font(enum LvglFontSize font_size); const lv_font_t* lvgl_get_text_font(enum LvglFontSize font_size);
uint32_t lvgl_get_text_font_height(enum LvglFontSize font_size); uint32_t lvgl_get_text_font_height(enum LvglFontSize font_size);

View File

@ -1,13 +0,0 @@
#pragma once
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
bool lvgl_has_indev_of_type(lv_indev_type_t type);
#ifdef __cplusplus
}
#endif

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 30 px * Size: 30 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 30 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o ../source-fonts/material_symbols_launcher_30.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 30 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o source-fonts/material_symbols_launcher_30.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 36 px * Size: 36 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 36 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o ../source-fonts/material_symbols_launcher_36.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 36 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o source-fonts/material_symbols_launcher_36.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 42 px * Size: 42 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 42 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o ../source-fonts/material_symbols_launcher_42.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 42 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o source-fonts/material_symbols_launcher_42.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 48 px * Size: 48 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 48 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o ../source-fonts/material_symbols_launcher_48.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 48 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o source-fonts/material_symbols_launcher_48.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 64 px * Size: 64 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 64 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o ../source-fonts/material_symbols_launcher_64.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 64 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o source-fonts/material_symbols_launcher_64.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 72 px * Size: 72 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 72 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o ../source-fonts/material_symbols_launcher_72.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 72 --font MaterialSymbolsRounded.ttf -r 0xE5C3,0xE2C7,0xE8B8 --format lvgl -o source-fonts/material_symbols_launcher_72.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 12 px * Size: 12 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_12.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o source-fonts/material_symbols_shared_12.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 16 px * Size: 16 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_16.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o source-fonts/material_symbols_shared_16.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 20 px * Size: 20 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_20.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o source-fonts/material_symbols_shared_20.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 24 px * Size: 24 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 24 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_24.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 24 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o source-fonts/material_symbols_shared_24.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 32 px * Size: 32 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 32 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o ../source-fonts/material_symbols_shared_32.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 32 --font MaterialSymbolsRounded.ttf -r 0xE145,0xE5C3,0xE770,0xEF40,0xEBCC,0xEFE6,0xEF4A,0xE5CD,0xF15C,0xE668,0xE92E,0xE326,0xEB97,0xE745,0xEC1C,0xE2C7,0xF720,0xF090,0xE8AF,0xE30F,0xE8FD,0xE9F4,0xE3F4,0xE316,0xE90F,0xEA07,0xE9B9,0xE159,0xE5D2,0xE28D,0xE5D4,0xE405,0xE89C,0xF8C7,0xE5D5,0xEF7A,0xE8B8,0xE9F7,0xE55D,0xF028,0xE1E0,0xE63E,0xE1A7,0xEB8E,0xEAD6,0xE3AF,0xFFF75 --format lvgl -o source-fonts/material_symbols_shared_32.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 12 px * Size: 12 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o ../source-fonts/material_symbols_statusbar_12.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 12 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o source-fonts/material_symbols_statusbar_12.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 16 px * Size: 16 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o ../source-fonts/material_symbols_statusbar_16.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 16 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o source-fonts/material_symbols_statusbar_16.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 20 px * Size: 20 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o ../source-fonts/material_symbols_statusbar_20.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 20 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o source-fonts/material_symbols_statusbar_20.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* Size: 30 px * Size: 30 px
* Bpp: 2 * Bpp: 2
* Opts: --no-compress --no-prefilter --bpp 2 --size 30 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o ../source-fonts/material_symbols_statusbar_30.c --force-fast-kern-format * Opts: --no-compress --no-prefilter --bpp 2 --size 30 --font MaterialSymbolsRounded.ttf -r 0xF1DB,0xF15C,0xE322,0xE623,0xF057,0xF0B0,0xEBE4,0xEBD6,0xEBE1,0xF065,0xE1DA,0xF064,0xF257,0xF256,0xF255,0xF254,0xF253,0xF252,0xF24F,0xF250,0xE1A7,0xE60F,0xE1A8,0xE1A9,0xE1E0 --format lvgl -o source-fonts/material_symbols_statusbar_30.c --force-fast-kern-format
******************************************************************************/ ******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE #ifdef LV_LVGL_H_INCLUDE_SIMPLE

View File

@ -1,11 +1,10 @@
#include <lvgl/devices/indev_private.h> #include <lvgl/devices/indev.h>
extern "C" { extern "C" {
bool lvgl_has_indev_of_type(lv_indev_type_t type) { bool lvgl_indev_exists(lv_indev_type_t type) {
for (lv_indev_t* indev = lv_indev_get_next(nullptr); indev != nullptr; indev = lv_indev_get_next(indev)) { for (lv_indev_t* indev = lv_indev_get_next(nullptr); indev != nullptr; indev = lv_indev_get_next(indev)) {
lv_indev_type_t to_check = lv_indev_get_type(indev); if (lv_indev_get_type(indev) == type) {
if (to_check == type) {
return true; return true;
} }
} }

View File

@ -12,6 +12,7 @@ extern const lv_font_t TT_LVGL_TEXT_FONT_LARGE_SYMBOL;
extern const lv_font_t TT_LVGL_LAUNCHER_FONT_ICON_SYMBOL; extern const lv_font_t TT_LVGL_LAUNCHER_FONT_ICON_SYMBOL;
extern const lv_font_t TT_LVGL_STATUSBAR_FONT_ICON_SYMBOL; extern const lv_font_t TT_LVGL_STATUSBAR_FONT_ICON_SYMBOL;
extern const lv_font_t TT_LVGL_SHARED_FONT_ICON_SYMBOL; extern const lv_font_t TT_LVGL_SHARED_FONT_ICON_SYMBOL;
extern const lv_font_t TT_LVGL_SHARED_FONT_ICON_2X_SYMBOL;
uint32_t lvgl_get_text_font_height(enum LvglFontSize font_size) { uint32_t lvgl_get_text_font_height(enum LvglFontSize font_size) {
switch (font_size) { switch (font_size) {
@ -34,6 +35,10 @@ uint32_t lvgl_get_shared_icon_font_height() { return TT_LVGL_SHARED_FONT_ICON_SI
const lv_font_t* lvgl_get_shared_icon_font() { return &TT_LVGL_SHARED_FONT_ICON_SYMBOL; } const lv_font_t* lvgl_get_shared_icon_font() { return &TT_LVGL_SHARED_FONT_ICON_SYMBOL; }
uint32_t lvgl_get_shared_icon_font_2x_height() { return TT_LVGL_SHARED_FONT_ICON_SIZE * 2; }
const lv_font_t* lvgl_get_shared_icon_font_2x() { return &TT_LVGL_SHARED_FONT_ICON_2X_SYMBOL; }
uint32_t lvgl_get_launcher_icon_font_height() { return TT_LVGL_LAUNCHER_FONT_ICON_SIZE; } uint32_t lvgl_get_launcher_icon_font_height() { return TT_LVGL_LAUNCHER_FONT_ICON_SIZE; }
const lv_font_t* lvgl_get_launcher_icon_font() { return &TT_LVGL_LAUNCHER_FONT_ICON_SYMBOL; } const lv_font_t* lvgl_get_launcher_icon_font() { return &TT_LVGL_LAUNCHER_FONT_ICON_SYMBOL; }

View File

@ -4,6 +4,7 @@
#include <lvgl/module.h> #include <lvgl/module.h>
#include <lvgl/devices/display.h> #include <lvgl/devices/display.h>
#include <lvgl/devices/indev.h>
#include <lvgl/devices/keyboard.h> #include <lvgl/devices/keyboard.h>
#include <lvgl/devices/pointer.h> #include <lvgl/devices/pointer.h>
@ -37,6 +38,8 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
// lvgl_display // lvgl_display
DEFINE_MODULE_SYMBOL(lvgl_display_add), DEFINE_MODULE_SYMBOL(lvgl_display_add),
DEFINE_MODULE_SYMBOL(lvgl_display_remove), DEFINE_MODULE_SYMBOL(lvgl_display_remove),
// lvgl_indev
DEFINE_MODULE_SYMBOL(lvgl_indev_exists),
// lvgl_keyboard // lvgl_keyboard
DEFINE_MODULE_SYMBOL(lvgl_keyboard_add), DEFINE_MODULE_SYMBOL(lvgl_keyboard_add),
DEFINE_MODULE_SYMBOL(lvgl_keyboard_remove), DEFINE_MODULE_SYMBOL(lvgl_keyboard_remove),

View File

@ -402,6 +402,7 @@ static bool api_hid_subscribe(struct Device* device, UsbHidQueueHandle event_que
if (!ctx || !event_queue) return false; if (!ctx || !event_queue) return false;
if (xSemaphoreTake(ctx->sub_mutex, pdMS_TO_TICKS(100)) != pdTRUE) return false; if (xSemaphoreTake(ctx->sub_mutex, pdMS_TO_TICKS(100)) != pdTRUE) return false;
bool added = false; bool added = false;
bool is_new = false;
for (int i = 0; i < MAX_SUBSCRIBERS; i++) { for (int i = 0; i < MAX_SUBSCRIBERS; i++) {
if (ctx->subscribers[i] == static_cast<QueueHandle_t>(event_queue)) { if (ctx->subscribers[i] == static_cast<QueueHandle_t>(event_queue)) {
added = true; added = true;
@ -410,9 +411,22 @@ static bool api_hid_subscribe(struct Device* device, UsbHidQueueHandle event_que
if (!ctx->subscribers[i]) { if (!ctx->subscribers[i]) {
ctx->subscribers[i] = static_cast<QueueHandle_t>(event_queue); ctx->subscribers[i] = static_cast<QueueHandle_t>(event_queue);
added = true; added = true;
is_new = true;
break; break;
} }
} }
// Reports devices that connected before this subscription. Done under sub_mutex so it is ordered with publish_event().
if (is_new) {
auto queue = static_cast<QueueHandle_t>(event_queue);
if (ctx->kb_handle.load() != nullptr) {
UsbHidEvent evt = { .type = USB_HID_EVENT_KEYBOARD_CONNECTED };
xQueueSend(queue, &evt, 0);
}
if (ctx->mouse_connected) {
UsbHidEvent evt = { .type = USB_HID_EVENT_MOUSE_CONNECTED };
xQueueSend(queue, &evt, 0);
}
}
xSemaphoreGive(ctx->sub_mutex); xSemaphoreGive(ctx->sub_mutex);
if (!added) LOG_W(TAG, "subscriber list full"); if (!added) LOG_W(TAG, "subscriber list full");
return added; return added;

View File

@ -3,7 +3,9 @@
namespace tt::lvgl { namespace tt::lvgl {
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
/** Creates the USB keyboard and mouse indevs. Must be called from LvglModuleConfig.on_start. */
void startUsbHidInput(); void startUsbHidInput();
/** Deletes the indevs created by startUsbHidInput(). Must be called from LvglModuleConfig.on_stop, before LVGL detaches its devices. */
void stopUsbHidInput(); void stopUsbHidInput();
#else #else
inline void startUsbHidInput() {} inline void startUsbHidInput() {}

View File

@ -0,0 +1,74 @@
#pragma once
#include <app/manifest.h>
#include <lvgl.h>
#include <cstdint>
#include <vector>
namespace tt::app {
struct AppGridItem {
/** Owned copy, so a tile never references the app ledger directly */
::AppManifest manifest;
/** Shared icon font glyph */
const char* icon;
/** Rendered with an accent colour when not selected */
bool highlighted;
};
/**
* Paged, non-scrolling grid of app tiles (icon above name) with "<" and ">" toolbar buttons.
* Must outlive the widgets it creates.
*/
class AppGrid final {
public:
struct Callbacks {
/** Returns the items in display order. Called on every rebuild. */
std::vector<AppGridItem> (*collect)(void* userData);
void (*onClicked)(const ::AppManifest& manifest, void* userData);
/** Optional */
void (*onLongPressed)(const ::AppManifest& manifest, void* userData);
/** Optional */
void (*onKey)(const ::AppManifest& manifest, uint32_t key, void* userData);
void* userData;
};
explicit AppGrid(const Callbacks& callbacks) : callbacks(callbacks) {}
/** 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);
/**
* 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
*/
void requestRepopulate(bool keepSelection);
private:
Callbacks callbacks;
std::vector<AppGridItem> items;
uint32_t page = 0;
bool keepSelectionOnRepopulate = true;
lv_obj_t* grid = nullptr;
lv_obj_t* prevButton = nullptr;
lv_obj_t* nextButton = nullptr;
void populate();
static void onDeferredRepopulate(void* userData);
static void onGridDeleted(lv_event_t* e);
static void onGridSizeChanged(lv_event_t* e);
static void onPrevPressed(lv_event_t* e);
static void onNextPressed(lv_event_t* e);
static void onTileClicked(lv_event_t* e);
static void onTileLongPressed(lv_event_t* e);
static void onTileKey(lv_event_t* e);
static void onTileFocusChanged(lv_event_t* e);
};
}

View File

@ -0,0 +1,240 @@
#include <Tactility/app/AppGrid.h>
#include <lvgl/devices/indev.h>
#include <lvgl/fonts.h>
#include <lvgl/widgets/toolbar.h>
#include <algorithm>
#include <string>
namespace tt::app {
namespace {
constexpr int32_t TILE_GAP = 6;
constexpr uint32_t ICON_COLOR_SELECTED = 0x33BBFF;
constexpr uint32_t ICON_COLOR_HIGHLIGHTED = 0xF876E9;
struct PageLayout {
int32_t iconSize;
int32_t pad;
int32_t tileWidth;
int32_t tileHeight;
int32_t topOffset;
uint32_t pageSize;
};
PageLayout computePageLayout(lv_obj_t* grid) {
const auto icon_size = static_cast<int32_t>(lvgl_get_shared_icon_font_2x_height());
const auto pad = icon_size / 16;
const auto gap = TILE_GAP;
const auto text_height = lv_font_get_line_height(lvgl_get_text_font(FONT_SIZE_SMALL));
const auto tile_min_width = 2 * icon_size + 2 * pad;
const auto tile_height = 2 * pad + icon_size + pad / 2 + text_height;
const auto available_width = lv_obj_get_content_width(grid);
const auto available_height = lv_obj_get_content_height(grid);
const auto columns = std::max<int32_t>(1, (available_width + gap) / (tile_min_width + gap));
const auto rows = std::max<int32_t>(1, (available_height + gap) / (tile_height + gap));
return {
.iconSize = icon_size,
.pad = pad,
.tileWidth = std::max<int32_t>(tile_min_width, (available_width - (columns - 1) * gap) / columns),
.tileHeight = tile_height,
.topOffset = std::max<int32_t>(0, (available_height - (rows * tile_height + (rows - 1) * gap)) / 2),
.pageSize = static_cast<uint32_t>(columns * rows)
};
}
lv_color_t getIconColor(bool focused, bool highlighted) {
if (focused) {
return lv_color_hex(ICON_COLOR_SELECTED);
}
return highlighted ? lv_color_hex(ICON_COLOR_HIGHLIGHTED) : lv_color_white();
}
const AppGridItem* getTileItem(lv_event_t* e) {
return static_cast<const AppGridItem*>(lv_obj_get_user_data(lv_event_get_target_obj(e)));
}
} // namespace
void AppGrid::onDeferredRepopulate(void* userData) {
static_cast<AppGrid*>(userData)->populate();
}
void AppGrid::onGridDeleted(lv_event_t* e) {
// Cancels a still-pending repopulate scheduled right before the grid itself was deleted (e.g. the app closing)
lv_async_call_cancel(onDeferredRepopulate, lv_event_get_user_data(e));
}
// Deferred: page size depends on the grid size, which is final only after the layout pass that triggered this.
void AppGrid::onGridSizeChanged(lv_event_t* e) {
lv_async_call(onDeferredRepopulate, lv_event_get_user_data(e));
}
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();
}
}
void AppGrid::onNextPressed(lv_event_t* e) {
auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
self->page++;
self->populate();
}
void AppGrid::onTileClicked(lv_event_t* e) {
const auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
self->callbacks.onClicked(getTileItem(e)->manifest, self->callbacks.userData);
}
void AppGrid::onTileLongPressed(lv_event_t* e) {
const auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
self->callbacks.onLongPressed(getTileItem(e)->manifest, self->callbacks.userData);
}
void AppGrid::onTileKey(lv_event_t* e) {
const auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
self->callbacks.onKey(getTileItem(e)->manifest, lv_event_get_key(e), self->callbacks.userData);
}
void AppGrid::onTileFocusChanged(lv_event_t* e) {
lv_obj_t* tile = lv_event_get_target_obj(e);
lv_obj_t* icon = lv_obj_get_child(tile, 0);
const bool focused = lv_event_get_code(e) == LV_EVENT_FOCUSED;
lv_obj_set_style_text_color(icon, getIconColor(focused, getTileItem(e)->highlighted), LV_STATE_DEFAULT);
}
void AppGrid::requestRepopulate(bool keepSelection) {
keepSelectionOnRepopulate = keepSelection;
lv_async_call(onDeferredRepopulate, this);
}
void AppGrid::populate() {
// Captured before lv_obj_clean() deletes the currently focused tile below: LVGL moves
// focus elsewhere as it leaves the group, and creating replacement tiles doesn't restore
// it, so the app id is remembered here and re-focused on its new tile once rebuilt.
std::string focusedAppId;
lv_group_t* group = lv_group_get_default();
if (group != nullptr && keepSelectionOnRepopulate) {
lv_obj_t* focused = lv_group_get_focused(group);
if (focused != nullptr && lv_obj_get_parent(focused) == grid) {
focusedAppId = static_cast<const AppGridItem*>(lv_obj_get_user_data(focused))->manifest.id;
}
}
keepSelectionOnRepopulate = true;
// Old tiles hold pointers into items, so they are deleted before items is replaced.
lv_obj_clean(grid);
items = callbacks.collect(callbacks.userData);
// Centers a full page of rows, so a partially filled last page keeps its rows in the same positions.
lv_obj_set_style_pad_top(grid, 0, LV_STATE_DEFAULT);
const PageLayout layout = computePageLayout(grid);
lv_obj_set_style_pad_top(grid, layout.topOffset, LV_STATE_DEFAULT);
const uint32_t item_count = items.size();
const uint32_t page_count = std::max<uint32_t>(1, (item_count + layout.pageSize - 1) / layout.pageSize);
// Follows the focused app onto its new page, e.g. after a reorder moved it.
if (!focusedAppId.empty()) {
const auto it = std::ranges::find_if(items, [&](const AppGridItem& item) {
return focusedAppId == item.manifest.id;
});
if (it != items.end()) {
page = static_cast<uint32_t>(it - items.begin()) / layout.pageSize;
}
}
page = std::min(page, page_count - 1);
lv_obj_set_state(prevButton, LV_STATE_DISABLED, page == 0);
lv_obj_set_state(nextButton, LV_STATE_DISABLED, page + 1 >= page_count);
// The toolbar wraps each action button, so the wrapper is hidden to free its space too
lv_obj_set_flag(lv_obj_get_parent(prevButton), LV_OBJ_FLAG_HIDDEN, page_count <= 1);
lv_obj_set_flag(lv_obj_get_parent(nextButton), LV_OBJ_FLAG_HIDDEN, page_count <= 1);
lv_obj_t* focusedTile = nullptr;
const uint32_t first = page * layout.pageSize;
const uint32_t last = std::min(item_count, first + layout.pageSize);
for (uint32_t i = first; i < last; i++) {
const auto& item = items[i];
lv_obj_t* tile = lv_button_create(grid);
lv_obj_set_size(tile, layout.tileWidth, layout.tileHeight);
lv_obj_set_style_pad_all(tile, layout.pad, LV_STATE_DEFAULT);
lv_obj_set_style_pad_row(tile, layout.pad / 2, LV_STATE_DEFAULT);
lv_obj_set_style_shadow_width(tile, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(tile, LV_OPA_TRANSP, LV_STATE_DEFAULT);
lv_obj_set_style_outline_width(tile, 0, LV_STATE_FOCUSED);
lv_obj_set_style_outline_width(tile, 0, LV_STATE_FOCUS_KEY);
lv_obj_set_flex_flow(tile, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(tile, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_user_data(tile, const_cast<AppGridItem*>(&item));
lv_obj_t* icon = lv_label_create(tile);
lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_font_2x(), LV_STATE_DEFAULT);
lv_obj_set_style_text_align(icon, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT);
lv_obj_set_size(icon, layout.iconSize, layout.iconSize);
lv_obj_set_style_text_color(icon, getIconColor(false, item.highlighted), LV_STATE_DEFAULT);
lv_label_set_text(icon, item.icon);
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);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT);
lv_obj_set_width(label, LV_PCT(100));
lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_DOTS);
lv_label_set_text(label, item.manifest.name);
lv_obj_add_event_cb(tile, onTileClicked, LV_EVENT_SHORT_CLICKED, this);
if (callbacks.onLongPressed != nullptr) {
lv_obj_add_event_cb(tile, onTileLongPressed, LV_EVENT_LONG_PRESSED, this);
}
if (callbacks.onKey != nullptr) {
lv_obj_add_event_cb(tile, onTileKey, LV_EVENT_KEY, this);
}
lv_obj_add_event_cb(tile, onTileFocusChanged, LV_EVENT_FOCUSED, nullptr);
lv_obj_add_event_cb(tile, onTileFocusChanged, LV_EVENT_DEFOCUSED, nullptr);
if (!focusedAppId.empty() && focusedAppId == item.manifest.id) {
focusedTile = tile;
}
}
if (focusedTile != nullptr) {
lv_group_focus_obj(focusedTile);
lv_obj_add_state(focusedTile, LV_STATE_FOCUS_KEY);
}
}
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);
grid = lv_obj_create(parent);
lv_obj_set_width(grid, LV_PCT(100));
lv_obj_set_flex_grow(grid, 1);
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_style_pad_all(grid, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_row(grid, TILE_GAP, LV_STATE_DEFAULT);
lv_obj_set_style_pad_column(grid, TILE_GAP, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(grid, 0, LV_STATE_DEFAULT);
lv_obj_remove_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
// 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();
const bool next_visible = !lv_obj_has_flag(lv_obj_get_parent(nextButton), LV_OBJ_FLAG_HIDDEN);
if (next_visible && !lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) {
lv_group_focus_obj(nextButton);
lv_obj_add_state(nextButton, LV_STATE_FOCUS_KEY);
}
}
}

View File

@ -12,6 +12,7 @@
#include <tactility/log.h> #include <tactility/log.h>
#include <lvgl.h> #include <lvgl.h>
#include <lvgl/devices/indev.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
namespace tt::app::alertdialog { namespace tt::app::alertdialog {
@ -55,7 +56,7 @@ void onButtonPressed(lv_event_t* e) {
app_event_emit_close(btnCtx->ctx->appInstanceId); app_event_emit_close(btnCtx->ctx->appInstanceId);
} }
void createButton(Context* ctx, lv_obj_t* parent, const std::string& text, int32_t index) { lv_obj_t* createButton(Context* ctx, lv_obj_t* parent, const std::string& text, int32_t index) {
lv_obj_t* button = lv_button_create(parent); lv_obj_t* button = lv_button_create(parent);
lv_obj_t* button_label = lv_label_create(button); lv_obj_t* button_label = lv_label_create(button);
lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(button_label, LV_ALIGN_CENTER, 0, 0);
@ -63,6 +64,7 @@ void createButton(Context* ctx, lv_obj_t* parent, const std::string& text, int32
auto* btnCtx = new ButtonContext { ctx, index }; auto* btnCtx = new ButtonContext { ctx, index };
lv_obj_add_event_cb(button, onButtonPressed, LV_EVENT_SHORT_CLICKED, btnCtx); lv_obj_add_event_cb(button, onButtonPressed, LV_EVENT_SHORT_CLICKED, btnCtx);
lv_obj_add_event_cb(button, onButtonDeleted, LV_EVENT_DELETE, btnCtx); lv_obj_add_event_cb(button, onButtonDeleted, LV_EVENT_DELETE, btnCtx);
return button;
} }
void createWidgets(lv_obj_t* parent, void* userData) { void createWidgets(lv_obj_t* parent, void* userData) {
@ -92,8 +94,17 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_style_pad_column(button_wrapper, 12, LV_STATE_DEFAULT); lv_obj_set_style_pad_column(button_wrapper, 12, LV_STATE_DEFAULT);
lv_obj_set_style_pad_ver(button_wrapper, 4, LV_STATE_DEFAULT); lv_obj_set_style_pad_ver(button_wrapper, 4, LV_STATE_DEFAULT);
lv_obj_t* first_button = nullptr;
for (int32_t index = 0; index < argc - 2; index++) { for (int32_t index = 0; index < argc - 2; index++) {
createButton(ctx, button_wrapper, argv[2 + index], index); lv_obj_t* button = createButton(ctx, button_wrapper, argv[2 + index], index);
if (first_button == nullptr) {
first_button = button;
}
}
if (first_button != nullptr && !lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) {
lv_group_focus_obj(first_button);
lv_obj_add_state(first_button, LV_STATE_FOCUS_KEY);
} }
} }

View File

@ -7,6 +7,7 @@
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
#include <Tactility/app/AppGrid.h>
#include <Tactility/app/alertdialog/AlertDialog.h> #include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/app/applist/Favourites.h> #include <Tactility/app/applist/Favourites.h>
@ -19,7 +20,6 @@
#include <vector> #include <vector>
#include <lvgl/icons/shared.h> #include <lvgl/icons/shared.h>
#include <lvgl/fonts.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
namespace tt::app::applist { namespace tt::app::applist {
@ -28,12 +28,6 @@ constexpr auto* TAG = "AppList";
namespace { namespace {
struct Context {
uint32_t appInstanceId;
Favourites favourites;
std::vector<::AppManifest> manifests;
};
struct IconEntry { struct IconEntry {
const char* id; const char* id;
const char* icon; const char* icon;
@ -58,82 +52,6 @@ const char* appIcon(const ::AppManifest* manifest) {
return LVGL_ICON_SHARED_DEPLOYED_CODE; return LVGL_ICON_SHARED_DEPLOYED_CODE;
} }
void populateList(lv_obj_t* list);
// Named (not a lambda) so lv_async_call_cancel() below has a stable function pointer to match against.
void deferredRepopulate(void* userData) {
populateList(static_cast<lv_obj_t*>(userData));
}
void onListDeleted(lv_event_t* e) {
// Cancels a still-pending deferredRepopulate() scheduled by a long-press/key-press right
// before the list itself was deleted (e.g. the app closing)
// Otherwise it would run later against this now-dangling lv_obj_t*.
lv_async_call_cancel(deferredRepopulate, lv_event_get_target_obj(e));
}
void onAppPressed(lv_event_t* e) {
const auto* manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
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) {
lv_obj_t* list = lv_obj_get_parent(lv_event_get_target_obj(e));
auto* ctx = static_cast<Context*>(lv_obj_get_user_data(list));
app_event_emit_close(ctx->appInstanceId);
}
}
void onAppLongPressed(lv_event_t* e) {
const auto* manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
lv_obj_t* list = lv_obj_get_parent(lv_event_get_target_obj(e));
auto* ctx = static_cast<Context*>(lv_obj_get_user_data(list));
if (!ctx->favourites.toggle(manifest->id)) {
LOG_E(TAG, "Failed to save favourites");
}
// Deferred: populateList() deletes this button via lv_obj_clean() while its own long-press is still dispatching.
lv_async_call(deferredRepopulate, list);
}
void onAppKeyPressed(lv_event_t* e) {
const uint32_t key = lv_event_get_key(e);
if (key != 'f' && key != 'F') {
return;
}
const auto* manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
lv_obj_t* list = lv_obj_get_parent(lv_event_get_target_obj(e));
auto* ctx = static_cast<Context*>(lv_obj_get_user_data(list));
if (!ctx->favourites.toggle(manifest->id)) {
LOG_E(TAG, "Failed to save favourites");
}
// Deferred: populateList() deletes this button via lv_obj_clean() while its own key event is still dispatching.
lv_async_call(deferredRepopulate, list);
}
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.");
}
lv_obj_t* createAppWidget(const ::AppManifest* manifest, lv_obj_t* list, bool favourite) {
// Plain "*" prefix, not an icon: shared Material Symbols font is subsetted and has no star glyph.
const std::string label = favourite ? (std::string("* ") + manifest->name) : manifest->name;
lv_obj_t* btn = lv_list_add_button(list, appIcon(manifest), label.c_str());
lv_obj_t* image = lv_obj_get_child(btn, 0);
lv_obj_set_style_text_font(image, lvgl_get_shared_icon_font(), LV_PART_MAIN);
// Not read by any event here (those get the manifest via their own callback user data) - lets
// populateList() identify a button by app id after a rebuild, to restore focus onto it.
lv_obj_set_user_data(btn, const_cast<::AppManifest*>(manifest));
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, const_cast<::AppManifest*>(manifest));
lv_obj_add_event_cb(btn, &onAppLongPressed, LV_EVENT_LONG_PRESSED, const_cast<::AppManifest*>(manifest));
lv_obj_add_event_cb(btn, &onAppKeyPressed, LV_EVENT_KEY, const_cast<::AppManifest*>(manifest));
return btn;
}
void collectManifest(const ::AppManifest* manifest, void* context) { void collectManifest(const ::AppManifest* manifest, void* context) {
auto* manifests = static_cast<std::vector<::AppManifest>*>(context); auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
manifests->push_back(*manifest); manifests->push_back(*manifest);
@ -150,25 +68,25 @@ void collectIncompatibleAppIds(const AppPackage* package, void* context) {
} }
} }
void populateList(lv_obj_t* list) { std::vector<AppGridItem> collectItems(void* userData);
// Captured before lv_obj_clean() deletes the currently focused button below: LVGL moves void onAppClicked(const ::AppManifest& manifest, void* userData);
// focus elsewhere as it leaves the group, and creating replacement buttons doesn't restore void onAppLongPressed(const ::AppManifest& manifest, void* userData);
// it, so the app id is remembered here and re-focused on its new button once rebuilt. void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData);
std::string focusedAppId;
lv_group_t* group = lv_group_get_default();
if (group != nullptr) {
lv_obj_t* focused = lv_group_get_focused(group);
if (focused != nullptr && lv_obj_get_parent(focused) == list) {
const auto* focusedManifest = static_cast<const ::AppManifest*>(lv_obj_get_user_data(focused));
if (focusedManifest != nullptr) {
focusedAppId = focusedManifest->id;
}
}
}
lv_obj_clean(list); struct Context {
uint32_t appInstanceId;
Favourites favourites;
AppGrid grid { AppGrid::Callbacks {
.collect = collectItems,
.onClicked = onAppClicked,
.onLongPressed = onAppLongPressed,
.onKey = onAppKey,
.userData = this
} };
};
auto* ctx = static_cast<Context*>(lv_obj_get_user_data(list)); std::vector<AppGridItem> collectItems(void* userData) {
const auto* ctx = static_cast<Context*>(userData);
const std::vector<std::string> favouriteIds = ctx->favourites.load(); const std::vector<std::string> favouriteIds = ctx->favourites.load();
std::vector<::AppManifest> collected; std::vector<::AppManifest> collected;
@ -176,55 +94,74 @@ void populateList(lv_obj_t* list) {
std::vector<std::string> incompatibleIds; std::vector<std::string> incompatibleIds;
app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds); app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds);
std::erase_if(collected, [&](const ::AppManifest& manifest) { 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(); return std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end();
}); });
std::ranges::sort(collected, [&](const ::AppManifest& a, const ::AppManifest& b) {
const bool aFavourite = Favourites::contains(favouriteIds, a.id); std::vector<AppGridItem> items;
const bool bFavourite = Favourites::contains(favouriteIds, b.id); items.reserve(collected.size());
if (aFavourite != bFavourite) { for (const auto& manifest : collected) {
return aFavourite; items.push_back({ manifest, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) });
} }
return strcmp(a.name, b.name) < 0; 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;
}); });
// Old buttons (and every pointer into the previous ctx->manifests) are already gone via return items;
// lv_obj_clean() above, so this is safe to replace wholesale before building new ones. }
ctx->manifests = std::move(collected);
lv_obj_t* focusedButton = nullptr; void onAppClicked(const ::AppManifest& manifest, void* userData) {
for (const auto& manifest: ctx->manifests) { const auto* ctx = static_cast<Context*>(userData);
bool is_valid_category = (manifest.category == APP_CATEGORY_USER) || (manifest.category == APP_CATEGORY_SYSTEM); uint32_t instanceId = 0;
if (is_valid_category && (manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) { AppStartContext context;
lv_obj_t* btn = createAppWidget(&manifest, list, Favourites::contains(favouriteIds, manifest.id)); if (app_start_context_from_id(manifest.id, &context) == ERROR_NONE && app_start_with_context(&context, &instanceId) == ERROR_NONE) {
if (!focusedAppId.empty() && focusedAppId == manifest.id) { app_event_emit_close(ctx->appInstanceId);
focusedButton = btn;
}
}
} }
}
if (focusedButton != nullptr) { void toggleFavourite(Context* ctx, const char* appId, bool keepSelection) {
lv_group_focus_obj(focusedButton); if (!ctx->favourites.toggle(appId)) {
lv_obj_add_state(focusedButton, LV_STATE_FOCUS_KEY); 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) { void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData); auto* ctx = static_cast<Context*>(userData);
// Flex column + flex_grow so the toolbar/list split recomputes on layout instead of going stale after a resize. // 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_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = lvgl_toolbar_create(parent, "Apps"); auto* toolbar = lvgl_toolbar_create(parent, "Apps");
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
ctx->grid.createWidgets(parent, toolbar);
lvgl_toolbar_add_text_button_action(toolbar, "?", onHelpPressed, ctx); lvgl_toolbar_add_text_button_action(toolbar, "?", onHelpPressed, ctx);
lv_obj_t* list = lv_list_create(parent);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
lv_obj_set_user_data(list, ctx);
lv_obj_add_event_cb(list, &onListDeleted, LV_EVENT_DELETE, nullptr);
populateList(list);
} }
int32_t appMain(int argc, char* argv[]) { int32_t appMain(int argc, char* argv[]) {
@ -274,7 +211,7 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM, .category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) }, .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN, .flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 4000, .desired_memory_capability = 0 }, .stack = { .depth = 5120, .desired_memory_capability = 0 },
}; };
} // namespace } // namespace

View File

@ -27,6 +27,7 @@
#include <Tactility/settings/DisplaySettings.h> #include <Tactility/settings/DisplaySettings.h>
#include <lvgl.h> #include <lvgl.h>
#include <lvgl/devices/indev.h>
#include <atomic> #include <atomic>
#include <format> #include <format>
@ -214,6 +215,13 @@ void createSplashWidgets(lv_obj_t* root, void*) {
#endif #endif
} }
void selectIfNoPointer(lv_obj_t* button) {
if (!lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) {
lv_group_focus_obj(button);
lv_obj_add_state(button, LV_STATE_FOCUS_KEY);
}
}
void createSdCardMissingWidgets(lv_obj_t* root, void*) { void createSdCardMissingWidgets(lv_obj_t* root, void*) {
lvgl::obj_set_style_bg_blacken(root); lvgl::obj_set_style_bg_blacken(root);
lv_obj_set_style_border_width(root, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(root, 0, LV_STATE_DEFAULT);
@ -235,6 +243,7 @@ void createSdCardMissingWidgets(lv_obj_t* root, void*) {
esp_restart(); esp_restart();
#endif #endif
}, LV_EVENT_SHORT_CLICKED, nullptr); }, LV_EVENT_SHORT_CLICKED, nullptr);
selectIfNoPointer(button);
} }
// Replaces the splash with a self-contained error screen (no dependency on the old alertdialog // Replaces the splash with a self-contained error screen (no dependency on the old alertdialog
@ -267,6 +276,7 @@ void createUsbBootModeFailedWidgets(lv_obj_t* root, void*) {
esp_restart(); esp_restart();
#endif #endif
}, LV_EVENT_SHORT_CLICKED, nullptr); }, LV_EVENT_SHORT_CLICKED, nullptr);
selectIfNoPointer(button);
} }
void showUsbBootModeFailedScreen() { void showUsbBootModeFailedScreen() {

View File

@ -109,7 +109,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto* toolbar = lvgl_toolbar_create(parent, "Region & Language"); auto* toolbar = lvgl_toolbar_create(parent, "Locale");
// The global toolbar nav callback only knows how to stop old-model apps. // The global toolbar nav callback only knows how to stop old-model apps.
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
@ -180,7 +180,7 @@ int32_t appMain(int argc, char* argv[]) {
extern const ::AppManifest manifest = { extern const ::AppManifest manifest = {
.id = "tactility.localesettings", .id = "tactility.localesettings",
.name = "Region & Language", .name = "Locale",
.category = APP_CATEGORY_SETTINGS, .category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }, .location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
.flags = 0, .flags = 0,

View File

@ -9,6 +9,7 @@
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
#include <lvgl.h> #include <lvgl.h>
#include <lvgl/devices/indev.h>
#include <lvgl/fonts.h> #include <lvgl/fonts.h>
#include <tactility/check.h> #include <tactility/check.h>
#include <tactility/device.h> #include <tactility/device.h>
@ -133,6 +134,11 @@ void createWidgets(lv_obj_t* parent, void* userData) {
auto* no_label = lv_label_create(no_button); auto* no_label = lv_label_create(no_button);
lv_label_set_text(no_label, "No"); lv_label_set_text(no_label, "No");
lv_obj_add_event_cb(no_button, onNoPressed, LV_EVENT_SHORT_CLICKED, ctx); lv_obj_add_event_cb(no_button, onNoPressed, LV_EVENT_SHORT_CLICKED, ctx);
if (!lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) {
lv_group_focus_obj(yes_button);
lv_obj_add_state(yes_button, LV_STATE_FOCUS_KEY);
}
} }
int32_t appMain(int argc, char* argv[]) { int32_t appMain(int argc, char* argv[]) {

View File

@ -6,8 +6,9 @@
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
#include <Tactility/app/AppGrid.h>
#include <lvgl/icons/shared.h> #include <lvgl/icons/shared.h>
#include <lvgl/fonts.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <tactility/check.h> #include <tactility/check.h>
@ -21,14 +22,6 @@ namespace tt::app::settings {
namespace { namespace {
struct Context {
uint32_t appInstanceId;
// Owned copies backing every button's user data, so a button never references the ledger
// directly - safe even if an external app is uninstalled while Settings stays open. Built
// once, in createWidgets() below (this list is never repopulated).
std::vector<::AppManifest> manifests;
};
struct IconEntry { struct IconEntry {
const char* id; const char* id;
const char* icon; const char* icon;
@ -60,37 +53,55 @@ const char* appIcon(const ::AppManifest* manifest) {
return LVGL_ICON_SHARED_DEPLOYED_CODE; return LVGL_ICON_SHARED_DEPLOYED_CODE;
} }
void onAppPressed(lv_event_t* e) { 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 AppList's own app-launch buttons.
const auto* manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
uint32_t instanceId = 0; uint32_t instanceId = 0;
// Re-resolved by id against the live ledger, not the (possibly stale) cached manifest above: // 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 // fails gracefully if the app was uninstalled since this button was built, instead of handing
// app_manager_start_internal() a pointer it would store for the new instance's whole lifetime. // app_manager_start_internal() a pointer it would store for the new instance's whole lifetime.
AppStartContext context; AppStartContext context;
if (app_start_context_from_id(manifest->id, &context) == ERROR_NONE) { if (app_start_context_from_id(manifest.id, &context) == ERROR_NONE) {
app_start_with_context(&context, &instanceId); app_start_with_context(&context, &instanceId);
} }
} }
void onBackPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
app_event_emit_close(ctx->appInstanceId);
}
void createWidget(const ::AppManifest* manifest, lv_obj_t* list) {
check(list);
auto* btn = lv_list_add_button(list, appIcon(manifest), manifest->name);
lv_obj_t* image = lv_obj_get_child(btn, 0);
lv_obj_set_style_text_font(image, lvgl_get_shared_icon_font(), LV_PART_MAIN);
lv_obj_add_event_cb(btn, &onAppPressed, LV_EVENT_SHORT_CLICKED, const_cast<::AppManifest*>(manifest));
}
void collectManifest(const ::AppManifest* manifest, void* context) { void collectManifest(const ::AppManifest* manifest, void* context) {
auto* manifests = static_cast<std::vector<::AppManifest>*>(context); auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
manifests->push_back(*manifest); manifests->push_back(*manifest);
} }
std::vector<AppGridItem> collectItems(void*) {
std::vector<::AppManifest> collected;
app_manager_for_each_manifest(collectManifest, &collected);
std::ranges::sort(collected, [](const ::AppManifest& a, const ::AppManifest& b) {
return strcmp(a.name, b.name) < 0;
});
std::vector<AppGridItem> items;
for (const auto& manifest : collected) {
if (manifest.category == APP_CATEGORY_SETTINGS && (manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
items.push_back({ manifest, appIcon(&manifest), false });
}
}
return items;
}
struct Context {
uint32_t appInstanceId;
AppGrid grid { AppGrid::Callbacks {
.collect = collectItems,
.onClicked = onAppClicked,
.onLongPressed = nullptr,
.onKey = nullptr,
.userData = nullptr
} };
};
void onBackPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
app_event_emit_close(ctx->appInstanceId);
}
void createWidgets(lv_obj_t* parent, void* userData) { void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData); auto* ctx = static_cast<Context*>(userData);
@ -99,23 +110,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
auto* toolbar = lvgl_toolbar_create(parent, "Settings"); auto* toolbar = lvgl_toolbar_create(parent, "Settings");
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
ctx->grid.createWidgets(parent, toolbar);
auto* list = lv_list_create(parent);
lv_obj_set_width(list, LV_PCT(100));
lv_obj_set_flex_grow(list, 1);
std::vector<::AppManifest> collected;
app_manager_for_each_manifest(collectManifest, &collected);
std::ranges::sort(collected, [](const ::AppManifest& a, const ::AppManifest& b) {
return strcmp(a.name, b.name) < 0;
});
ctx->manifests = std::move(collected);
for (const auto& manifest: ctx->manifests) {
if (manifest.category == APP_CATEGORY_SETTINGS && (manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
createWidget(&manifest, list);
}
}
} }
int32_t appMain(int argc, char* argv[]) { int32_t appMain(int argc, char* argv[]) {
@ -158,7 +153,7 @@ extern const ::AppManifest manifest = {
.category = APP_CATEGORY_SYSTEM, .category = APP_CATEGORY_SYSTEM,
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) }, .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
.flags = APP_MANIFEST_FLAG_HIDDEN, .flags = APP_MANIFEST_FLAG_HIDDEN,
.stack = { .depth = 2400, .desired_memory_capability = 0 }, .stack = { .depth = 4000, .desired_memory_capability = 0 },
}; };
} // namespace } // namespace

View File

@ -16,6 +16,7 @@
#include <tactility/device.h> #include <tactility/device.h>
#include <tactility/drivers/display.h> #include <tactility/drivers/display.h>
#include <tactility/module.h> #include <tactility/module.h>
#include <lvgl/devices/indev.h>
#include <lvgl/module.h> #include <lvgl/module.h>
#include <lvgl/widgets/toolbar.h> #include <lvgl/widgets/toolbar.h>
#include <lvgl_window_manager/window_manager.h> #include <lvgl_window_manager/window_manager.h>
@ -46,8 +47,11 @@ void createWidgets(lv_obj_t* root, void* userData) {
lv_obj_set_style_pad_all(root, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_all(root, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_row(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"); auto* toolbar = lvgl_toolbar_create(root, "Terminal");
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClosePressed, ctx); lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClosePressed, ctx);
}
auto* canvas = lv_canvas_create(root); auto* canvas = lv_canvas_create(root);
lv_obj_set_width(canvas, LV_PCT(100)); lv_obj_set_width(canvas, LV_PCT(100));

View File

@ -182,6 +182,31 @@ static void keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
data->state = LV_INDEV_STATE_RELEASED; data->state = LV_INDEV_STATE_RELEASED;
} }
// The pointer indev only exists while a mouse is connected, so apps can tell whether pointer input is available.
static void addMouseIndev(UsbHidInputCtx* ctx) {
if (ctx->mouse_indev != nullptr) {
return;
}
ctx->mouse_indev = lv_indev_create();
lv_indev_set_type(ctx->mouse_indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(ctx->mouse_indev, mouse_read_cb);
lv_indev_set_user_data(ctx->mouse_indev, ctx);
if (ctx->mouse_cursor != nullptr) {
lv_indev_set_cursor(ctx->mouse_indev, ctx->mouse_cursor);
lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN);
}
}
static void removeMouseIndev(UsbHidInputCtx* ctx) {
if (ctx->mouse_indev != nullptr) {
lv_indev_delete(ctx->mouse_indev);
ctx->mouse_indev = nullptr;
}
if (ctx->mouse_cursor != nullptr) {
lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN);
}
}
static void usbHidInputTask(void* arg) { static void usbHidInputTask(void* arg) {
auto* ctx = static_cast<UsbHidInputCtx*>(arg); auto* ctx = static_cast<UsbHidInputCtx*>(arg);
LOG_I(TAG, "started"); LOG_I(TAG, "started");
@ -192,14 +217,6 @@ static void usbHidInputTask(void* arg) {
// gets disabled mid-read. // gets disabled mid-read.
lvgl_lock(); lvgl_lock();
ctx->mouse_indev = lv_indev_create();
lv_indev_set_type(ctx->mouse_indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(ctx->mouse_indev, mouse_read_cb);
lv_indev_set_user_data(ctx->mouse_indev, ctx);
if (ctx->mouse_cursor != nullptr) {
lv_indev_set_cursor(ctx->mouse_indev, ctx->mouse_cursor);
}
ctx->kb_indev = lv_indev_create(); ctx->kb_indev = lv_indev_create();
lv_indev_set_type(ctx->kb_indev, LV_INDEV_TYPE_KEYPAD); lv_indev_set_type(ctx->kb_indev, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(ctx->kb_indev, keyboard_read_cb); lv_indev_set_read_cb(ctx->kb_indev, keyboard_read_cb);
@ -282,17 +299,17 @@ static void usbHidInputTask(void* arg) {
break; break;
case USB_HID_EVENT_MOUSE_CONNECTED: case USB_HID_EVENT_MOUSE_CONNECTED:
ctx->mouse_connected = true; ctx->mouse_connected = true;
if (ctx->mouse_cursor && lvgl_try_lock(pdMS_TO_TICKS(200))) { // Blocking lock: a skipped connect would leave the mouse without an indev
lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN); lvgl_lock();
addMouseIndev(ctx);
lvgl_unlock(); lvgl_unlock();
}
break; break;
case USB_HID_EVENT_MOUSE_DISCONNECTED: case USB_HID_EVENT_MOUSE_DISCONNECTED:
ctx->mouse_connected = false; ctx->mouse_connected = false;
if (ctx->mouse_cursor && lvgl_try_lock(pdMS_TO_TICKS(200))) { ctx->mouse_btn1.store(false);
lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN); lvgl_lock();
removeMouseIndev(ctx);
lvgl_unlock(); lvgl_unlock();
}
break; break;
default: default:
break; break;

View File

@ -78,7 +78,10 @@ extern const struct DeviceType USB_HOST_HID_TYPE;
/** Returns true if any HID device (keyboard or mouse) is currently connected. */ /** Returns true if any HID device (keyboard or mouse) is currently connected. */
bool usb_host_hid_is_connected(struct Device* device); bool usb_host_hid_is_connected(struct Device* device);
/** Subscribe a FreeRTOS queue to receive UsbHidEvent items from the HID driver. */ /**
* Subscribe a FreeRTOS queue to receive UsbHidEvent items from the HID driver.
* A new subscriber immediately receives a *_CONNECTED event for each device that is already connected.
*/
bool usb_host_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue); bool usb_host_hid_subscribe(struct Device* device, UsbHidQueueHandle event_queue);
/** Unsubscribe a previously subscribed queue. */ /** Unsubscribe a previously subscribed queue. */