mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
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:
parent
34ca63ec3c
commit
2b6b4b5167
@ -22,3 +22,4 @@ display.dpi=227
|
||||
lvgl.colorDepth=16
|
||||
lvgl.dpi=150
|
||||
lvgl.uiDensity=compact
|
||||
lvgl.fontSize=16
|
||||
|
||||
@ -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_LAUNCHER_ICON_SIZE" launcher_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 ()
|
||||
# Default values for simulator
|
||||
set(font_size_small 10)
|
||||
@ -22,6 +23,7 @@ else ()
|
||||
set(statusbar_symbol_size 20)
|
||||
set(launcher_symbol_size 36)
|
||||
set(shared_symbol_size 16)
|
||||
set(shared_symbol_2x_size 32)
|
||||
endif ()
|
||||
|
||||
message("Font sizes:")
|
||||
@ -32,6 +34,7 @@ message("Icon sizes:")
|
||||
message(" - statusbar: ${statusbar_symbol_size}")
|
||||
message(" - launcher: ${launcher_symbol_size}")
|
||||
message(" - shared: ${shared_symbol_size}")
|
||||
message(" - shared_2x: ${shared_symbol_2x_size}")
|
||||
|
||||
# ####################################
|
||||
# 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_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_2x_size}.c")
|
||||
|
||||
list(APPEND REQUIRES_LIST
|
||||
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_SHARED_FONT_ICON_SIZE=${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
|
||||
"-DTT_LVGL_UI_DENSITY=${ui_density_variable}"
|
||||
)
|
||||
|
||||
10
Modules/lvgl-module/assets/generate-all.py → Modules/lvgl-module/generate-icons.py
Normal file → Executable file
10
Modules/lvgl-module/assets/generate-all.py → Modules/lvgl-module/generate-icons.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
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):
|
||||
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}")
|
||||
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)
|
||||
@ -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):
|
||||
filename = f"{variable_name}.h"
|
||||
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:
|
||||
f.write("// SPDX-License-Identifier: Apache-2.0\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
|
||||
20,
|
||||
24,
|
||||
32
|
||||
32,
|
||||
40,
|
||||
48,
|
||||
64
|
||||
]
|
||||
|
||||
statusbar_symbol_sizes = [
|
||||
22
Modules/lvgl-module/include/lvgl/devices/indev.h
Normal file
22
Modules/lvgl-module/include/lvgl/devices/indev.h
Normal 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
|
||||
@ -17,6 +17,9 @@ enum LvglFontSize {
|
||||
const lv_font_t* lvgl_get_shared_icon_font(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);
|
||||
uint32_t lvgl_get_text_font_height(enum LvglFontSize font_size);
|
||||
|
||||
|
||||
@ -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
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 30 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 36 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 42 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 48 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 64 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 72 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 12 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 16 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 20 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 24 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 32 px
|
||||
* 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
|
||||
|
||||
1632
Modules/lvgl-module/source-fonts/material_symbols_shared_40.c
Normal file
1632
Modules/lvgl-module/source-fonts/material_symbols_shared_40.c
Normal file
File diff suppressed because it is too large
Load Diff
2130
Modules/lvgl-module/source-fonts/material_symbols_shared_48.c
Normal file
2130
Modules/lvgl-module/source-fonts/material_symbols_shared_48.c
Normal file
File diff suppressed because it is too large
Load Diff
3653
Modules/lvgl-module/source-fonts/material_symbols_shared_64.c
Normal file
3653
Modules/lvgl-module/source-fonts/material_symbols_shared_64.c
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 12 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 16 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 20 px
|
||||
* 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
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/*******************************************************************************
|
||||
* Size: 30 px
|
||||
* 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
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
#include <lvgl/devices/indev_private.h>
|
||||
#include <lvgl/devices/indev.h>
|
||||
|
||||
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)) {
|
||||
lv_indev_type_t to_check = lv_indev_get_type(indev);
|
||||
if (to_check == type) {
|
||||
if (lv_indev_get_type(indev) == type) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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_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_2X_SYMBOL;
|
||||
|
||||
uint32_t lvgl_get_text_font_height(enum LvglFontSize 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; }
|
||||
|
||||
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; }
|
||||
|
||||
const lv_font_t* lvgl_get_launcher_icon_font() { return &TT_LVGL_LAUNCHER_FONT_ICON_SYMBOL; }
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <lvgl/module.h>
|
||||
|
||||
#include <lvgl/devices/display.h>
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/devices/keyboard.h>
|
||||
#include <lvgl/devices/pointer.h>
|
||||
|
||||
@ -37,6 +38,8 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
// lvgl_display
|
||||
DEFINE_MODULE_SYMBOL(lvgl_display_add),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_display_remove),
|
||||
// lvgl_indev
|
||||
DEFINE_MODULE_SYMBOL(lvgl_indev_exists),
|
||||
// lvgl_keyboard
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_add),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_remove),
|
||||
|
||||
@ -402,6 +402,7 @@ static bool api_hid_subscribe(struct Device* device, UsbHidQueueHandle event_que
|
||||
if (!ctx || !event_queue) return false;
|
||||
if (xSemaphoreTake(ctx->sub_mutex, pdMS_TO_TICKS(100)) != pdTRUE) return false;
|
||||
bool added = false;
|
||||
bool is_new = false;
|
||||
for (int i = 0; i < MAX_SUBSCRIBERS; i++) {
|
||||
if (ctx->subscribers[i] == static_cast<QueueHandle_t>(event_queue)) {
|
||||
added = true;
|
||||
@ -410,9 +411,22 @@ static bool api_hid_subscribe(struct Device* device, UsbHidQueueHandle event_que
|
||||
if (!ctx->subscribers[i]) {
|
||||
ctx->subscribers[i] = static_cast<QueueHandle_t>(event_queue);
|
||||
added = true;
|
||||
is_new = true;
|
||||
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);
|
||||
if (!added) LOG_W(TAG, "subscriber list full");
|
||||
return added;
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
namespace tt::lvgl {
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
/** Creates the USB keyboard and mouse indevs. Must be called from LvglModuleConfig.on_start. */
|
||||
void startUsbHidInput();
|
||||
/** Deletes the indevs created by startUsbHidInput(). Must be called from LvglModuleConfig.on_stop, before LVGL detaches its devices. */
|
||||
void stopUsbHidInput();
|
||||
#else
|
||||
inline void startUsbHidInput() {}
|
||||
|
||||
74
Tactility/Private/Tactility/app/AppGrid.h
Normal file
74
Tactility/Private/Tactility/app/AppGrid.h
Normal 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);
|
||||
};
|
||||
|
||||
}
|
||||
240
Tactility/Source/app/AppGrid.cpp
Normal file
240
Tactility/Source/app/AppGrid.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -12,6 +12,7 @@
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
namespace tt::app::alertdialog {
|
||||
@ -55,7 +56,7 @@ void onButtonPressed(lv_event_t* e) {
|
||||
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_label = lv_label_create(button);
|
||||
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 };
|
||||
lv_obj_add_event_cb(button, onButtonPressed, LV_EVENT_SHORT_CLICKED, btnCtx);
|
||||
lv_obj_add_event_cb(button, onButtonDeleted, LV_EVENT_DELETE, btnCtx);
|
||||
return button;
|
||||
}
|
||||
|
||||
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_ver(button_wrapper, 4, LV_STATE_DEFAULT);
|
||||
|
||||
lv_obj_t* first_button = nullptr;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
#include <Tactility/app/alertdialog/AlertDialog.h>
|
||||
#include <Tactility/app/applist/Favourites.h>
|
||||
|
||||
@ -19,7 +20,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
namespace tt::app::applist {
|
||||
@ -28,12 +28,6 @@ constexpr auto* TAG = "AppList";
|
||||
|
||||
namespace {
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
Favourites favourites;
|
||||
std::vector<::AppManifest> manifests;
|
||||
};
|
||||
|
||||
struct IconEntry {
|
||||
const char* id;
|
||||
const char* icon;
|
||||
@ -58,82 +52,6 @@ const char* appIcon(const ::AppManifest* manifest) {
|
||||
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) {
|
||||
auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
|
||||
manifests->push_back(*manifest);
|
||||
@ -150,25 +68,25 @@ void collectIncompatibleAppIds(const AppPackage* package, void* context) {
|
||||
}
|
||||
}
|
||||
|
||||
void populateList(lv_obj_t* list) {
|
||||
// Captured before lv_obj_clean() deletes the currently focused button below: LVGL moves
|
||||
// focus elsewhere as it leaves the group, and creating replacement buttons doesn't restore
|
||||
// it, so the app id is remembered here and re-focused on its new button once rebuilt.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
std::vector<::AppManifest> collected;
|
||||
@ -176,55 +94,74 @@ void populateList(lv_obj_t* list) {
|
||||
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::ranges::sort(collected, [&](const ::AppManifest& a, const ::AppManifest& b) {
|
||||
const bool aFavourite = Favourites::contains(favouriteIds, a.id);
|
||||
const bool bFavourite = Favourites::contains(favouriteIds, b.id);
|
||||
if (aFavourite != bFavourite) {
|
||||
return aFavourite;
|
||||
|
||||
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.name, b.name) < 0;
|
||||
return strcmp(a.manifest.name, b.manifest.name) < 0;
|
||||
});
|
||||
// Old buttons (and every pointer into the previous ctx->manifests) are already gone via
|
||||
// lv_obj_clean() above, so this is safe to replace wholesale before building new ones.
|
||||
ctx->manifests = std::move(collected);
|
||||
return items;
|
||||
}
|
||||
|
||||
lv_obj_t* focusedButton = nullptr;
|
||||
for (const auto& manifest: ctx->manifests) {
|
||||
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) {
|
||||
lv_obj_t* btn = createAppWidget(&manifest, list, Favourites::contains(favouriteIds, manifest.id));
|
||||
if (!focusedAppId.empty() && focusedAppId == manifest.id) {
|
||||
focusedButton = btn;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (focusedButton != nullptr) {
|
||||
lv_group_focus_obj(focusedButton);
|
||||
lv_obj_add_state(focusedButton, LV_STATE_FOCUS_KEY);
|
||||
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/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_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);
|
||||
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[]) {
|
||||
@ -274,7 +211,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
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/devices/indev.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <format>
|
||||
@ -214,6 +215,13 @@ void createSplashWidgets(lv_obj_t* root, void*) {
|
||||
#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*) {
|
||||
lvgl::obj_set_style_bg_blacken(root);
|
||||
lv_obj_set_style_border_width(root, 0, LV_STATE_DEFAULT);
|
||||
@ -235,6 +243,7 @@ void createSdCardMissingWidgets(lv_obj_t* root, void*) {
|
||||
esp_restart();
|
||||
#endif
|
||||
}, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
selectIfNoPointer(button);
|
||||
}
|
||||
|
||||
// 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();
|
||||
#endif
|
||||
}, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
selectIfNoPointer(button);
|
||||
}
|
||||
|
||||
void showUsbBootModeFailedScreen() {
|
||||
|
||||
@ -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_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.
|
||||
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 = {
|
||||
.id = "tactility.localesettings",
|
||||
.name = "Region & Language",
|
||||
.name = "Locale",
|
||||
.category = APP_CATEGORY_SETTINGS,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||
.flags = 0,
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <tactility/check.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);
|
||||
lv_label_set_text(no_label, "No");
|
||||
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[]) {
|
||||
|
||||
@ -6,8 +6,9 @@
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
#include <tactility/check.h>
|
||||
|
||||
@ -21,14 +22,6 @@ namespace tt::app::settings {
|
||||
|
||||
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 {
|
||||
const char* id;
|
||||
const char* icon;
|
||||
@ -60,37 +53,55 @@ const char* appIcon(const ::AppManifest* manifest) {
|
||||
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.
|
||||
const auto* manifest = static_cast<const ::AppManifest*>(lv_event_get_user_data(e));
|
||||
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
|
||||
// app_manager_start_internal() a pointer it would store for the new instance's whole lifetime.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
|
||||
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) {
|
||||
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");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
ctx->grid.createWidgets(parent, toolbar);
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
@ -158,7 +153,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 = 2400, .desired_memory_capability = 0 },
|
||||
.stack = { .depth = 4000, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/module.h>
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/module.h>
|
||||
#include <lvgl/widgets/toolbar.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_row(root, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* toolbar = lvgl_toolbar_create(root, "Terminal");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClosePressed, ctx);
|
||||
// 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));
|
||||
|
||||
@ -182,6 +182,31 @@ static void keyboard_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||
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) {
|
||||
auto* ctx = static_cast<UsbHidInputCtx*>(arg);
|
||||
LOG_I(TAG, "started");
|
||||
@ -192,14 +217,6 @@ static void usbHidInputTask(void* arg) {
|
||||
// gets disabled mid-read.
|
||||
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();
|
||||
lv_indev_set_type(ctx->kb_indev, LV_INDEV_TYPE_KEYPAD);
|
||||
lv_indev_set_read_cb(ctx->kb_indev, keyboard_read_cb);
|
||||
@ -282,17 +299,17 @@ static void usbHidInputTask(void* arg) {
|
||||
break;
|
||||
case USB_HID_EVENT_MOUSE_CONNECTED:
|
||||
ctx->mouse_connected = true;
|
||||
if (ctx->mouse_cursor && lvgl_try_lock(pdMS_TO_TICKS(200))) {
|
||||
lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lvgl_unlock();
|
||||
}
|
||||
// Blocking lock: a skipped connect would leave the mouse without an indev
|
||||
lvgl_lock();
|
||||
addMouseIndev(ctx);
|
||||
lvgl_unlock();
|
||||
break;
|
||||
case USB_HID_EVENT_MOUSE_DISCONNECTED:
|
||||
ctx->mouse_connected = false;
|
||||
if (ctx->mouse_cursor && lvgl_try_lock(pdMS_TO_TICKS(200))) {
|
||||
lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lvgl_unlock();
|
||||
}
|
||||
ctx->mouse_btn1.store(false);
|
||||
lvgl_lock();
|
||||
removeMouseIndev(ctx);
|
||||
lvgl_unlock();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@ -78,7 +78,10 @@ extern const struct DeviceType USB_HOST_HID_TYPE;
|
||||
/** Returns true if any HID device (keyboard or mouse) is currently connected. */
|
||||
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);
|
||||
|
||||
/** Unsubscribe a previously subscribed queue. */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user