Theming WIP

This commit is contained in:
Ken Van Hoeylandt 2026-10-06 22:23:23 +02:00
parent f8172446fd
commit 7638602201
38 changed files with 7370 additions and 510 deletions

View File

@ -115,13 +115,6 @@ if (DEFINED ENV{ESP_IDF_VERSION})
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=utime" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=rmdir" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_button_create" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_dropdown_create" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_list_create" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_list_add_button" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_obj_create" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_obj_set_flex_flow" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_switch_create" APPEND)
idf_build_set_property(LINK_OPTIONS "-Wl,--wrap=lv_textarea_create" APPEND)
else ()

View File

@ -7,7 +7,9 @@
## Higher Priority
- Terminal app should always run in non-LVGL mode if no PSRAM is present.
- Update to LVGL 9.6.x (ESP-IDF and POSIX)
- Terminal app: on devices with pointer device: LVGL rendering mode should hide the toolbar by default, but tapping the terminal should toggle the visibility of the toolbar.
- Terminal app: should always run in non-LVGL mode if no PSRAM is present.
- Crash app should work without LVGL, like Boot app. It should wait for any input (keyboard or pointer) to continue.
- NimBLE looses pairing key after reboot
- CrashDiagnostics shouldn't show a QR when there's no callstack

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Creates a card: a container that groups related content and stands out from its background.
* The theme styles it as a card. Plain containers (lv_obj_create()) don't stand out, as they share the background colour.
* @param[in] parent the parent object for the new card
* @return the created card
*/
lv_obj_t* lvgl_card_create(lv_obj_t* parent);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,38 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
/** The look of an icon button, as styled by the theme */
enum LvglIconButtonVariant {
/** No container, the icon uses the theme's icon colour */
LVGL_ICON_BUTTON_STANDARD,
/** Filled with the primary colour, for the most important action */
LVGL_ICON_BUTTON_FILLED,
/** Filled with a softer secondary colour, for other prominent actions */
LVGL_ICON_BUTTON_TONAL
};
/**
* @brief Creates a button that shows an icon (or short text) without a filled background.
* It behaves like a regular button, but the theme styles it as an icon button.
* @param[in] parent the parent object for the new button
* @return the created button
*/
lv_obj_t* lvgl_icon_button_create(lv_obj_t* parent);
/**
* @brief Creates an icon button with the specified look.
* @param[in] parent the parent object for the new button
* @param[in] variant the look of the button
* @return the created button
*/
lv_obj_t* lvgl_icon_button_create_variant(lv_obj_t* parent, enum LvglIconButtonVariant variant);
#ifdef __cplusplus
}
#endif

View File

@ -1,8 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#define LV_USE_PRIVATE_API 1 // For the lv_theme_t declaration
#include <lvgl/devices/display.h>
#include <lvgl/lvgl.h>
#include <lvgl/ppa.h>
#include <tactility/device.h>
@ -12,6 +11,9 @@
#include <lvgl/devices/device_context.h>
#include "../themes/lv_theme_material.h"
#include "../themes/lv_theme_material_mono.h"
#include <stdlib.h>
#ifdef ESP_PLATFORM
@ -20,37 +22,34 @@
constexpr auto* TAG = "lvgl_display";
#if LV_USE_THEME_MONO
static lv_style_t mono_button_style;
static bool mono_button_style_initialized = false;
static lv_theme_t mono_theme_extension;
static void mono_theme_extension_apply(lv_theme_t*, lv_obj_t* obj) {
if (lv_obj_check_type(obj, &lv_button_class)) {
lv_obj_add_style(obj, &mono_button_style, LV_PART_MAIN | LV_STATE_DEFAULT);
}
}
// The mono theme draws a border around every button. This removes it in the default state, keeping the pressed border and focus outline.
static void lvgl_display_extend_mono_theme(lv_display_t* disp) {
if (!mono_button_style_initialized) {
lv_style_init(&mono_button_style);
lv_style_set_border_width(&mono_button_style, 0);
mono_button_style_initialized = true;
}
// LVGL prefers the default and simple themes when they are also enabled, so only the mono theme itself is extended
lv_theme_t* mono_theme = lv_display_get_theme(disp);
if (mono_theme == nullptr || mono_theme != lv_theme_mono_get()) {
return;
}
mono_theme_extension = *mono_theme;
lv_theme_set_parent(&mono_theme_extension, mono_theme);
lv_theme_set_apply_cb(&mono_theme_extension, mono_theme_extension_apply);
lv_display_set_theme(disp, &mono_theme_extension);
}
// The themes are generated with LSS from Modules/lvgl-module/themes/*.lss
static void lvgl_display_apply_theme(lv_display_t* disp, struct Device* device, lv_color_format_t color_format) {
#if defined(LV_THEME_DEFAULT_DARK) && LV_THEME_DEFAULT_DARK
bool is_dark = true;
#else
bool is_dark = false;
#endif
bool is_compact = lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT;
// Slow-refresh panels (e-paper) can't show animations
bool animations_enabled = !display_has_capability(device, DISPLAY_CAPABILITY_SLOW_REFRESH);
lv_theme_t* theme;
if (color_format == LV_COLOR_FORMAT_I1 || color_format == LV_COLOR_FORMAT_L8) {
lv_theme_material_mono_config_t config;
lv_theme_material_mono_config_init(&config);
config.is_compact = is_compact;
config.animations_enabled = animations_enabled;
theme = lv_theme_material_mono_init(disp, &config);
} else {
lv_theme_material_config_t config;
lv_theme_material_config_init(&config);
config.is_dark = is_dark;
config.is_compact = is_compact;
config.animations_enabled = animations_enabled;
theme = lv_theme_material_init(disp, &config);
}
lv_display_set_theme(disp, theme);
}
struct LvglDisplayCtx {
void* buf1;
@ -504,9 +503,7 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig*
return ERROR_OUT_OF_MEMORY;
}
#if LV_USE_THEME_MONO
lvgl_display_extend_mono_theme(disp);
#endif
lvgl_display_apply_theme(disp, device, lv_color_format);
ctx->render_mode = render_mode;
lv_display_set_color_format(disp, lv_color_format);

View File

@ -10,6 +10,8 @@
#include <lvgl/devices/pointer.h>
#include <lvgl/widgets/sliderbox.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/icon_button.h>
#include <lvgl/widgets/spinner.h>
#include <lvgl/widgets/toolbar.h>
@ -56,6 +58,11 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
DEFINE_MODULE_SYMBOL(lvgl_pointer_add),
DEFINE_MODULE_SYMBOL(lvgl_pointer_get_slot_index),
DEFINE_MODULE_SYMBOL(lvgl_pointer_remove),
// lvgl_card
DEFINE_MODULE_SYMBOL(lvgl_card_create),
// lvgl_icon_button
DEFINE_MODULE_SYMBOL(lvgl_icon_button_create),
DEFINE_MODULE_SYMBOL(lvgl_icon_button_create_variant),
// lvgl_spinner
DEFINE_MODULE_SYMBOL(lvgl_spinner_create),
// lvgl_toolbar

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,59 @@
/* Generated by LSS. Do not edit. */
#ifndef LV_THEME_MATERIAL_H
#define LV_THEME_MATERIAL_H
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
bool is_dark;
bool is_compact;
bool animations_enabled;
lv_color_t color_primary;
lv_color_t color_secondary;
lv_color_t color_error;
const lv_font_t * font_small;
const lv_font_t * font_normal;
const lv_font_t * font_large;
} lv_theme_material_config_t;
/**
* Reset a config to its default values
* @param config the config to initialize
*/
void lv_theme_material_config_init(lv_theme_material_config_t * config);
/**
* Initialize the theme
* @param disp pointer to display (NULL for the default display)
* @param config the theme config, which is copied
* @return a pointer to reference this theme later
*/
lv_theme_t * lv_theme_material_init(lv_display_t * disp, const lv_theme_material_config_t * config);
/**
* Check if the theme is initialized
* @return true if initialized, false otherwise
*/
bool lv_theme_material_is_inited(void);
/**
* Get the theme
* @return a pointer to the theme, or NULL when not initialized
*/
lv_theme_t * lv_theme_material_get(void);
/**
* Deinitialize the theme
*/
void lv_theme_material_deinit(void);
#ifdef __cplusplus
}
#endif
#endif /* LV_THEME_MATERIAL_H */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
/* Generated by LSS. Do not edit. */
#ifndef LV_THEME_MATERIAL_MONO_H
#define LV_THEME_MATERIAL_MONO_H
#include <lvgl.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
bool is_dark;
bool is_compact;
bool animations_enabled;
lv_color_t color_primary;
lv_color_t color_secondary;
const lv_font_t * font_small;
const lv_font_t * font_normal;
const lv_font_t * font_large;
} lv_theme_material_mono_config_t;
/**
* Reset a config to its default values
* @param config the config to initialize
*/
void lv_theme_material_mono_config_init(lv_theme_material_mono_config_t * config);
/**
* Initialize the theme
* @param disp pointer to display (NULL for the default display)
* @param config the theme config, which is copied
* @return a pointer to reference this theme later
*/
lv_theme_t * lv_theme_material_mono_init(lv_display_t * disp, const lv_theme_material_mono_config_t * config);
/**
* Check if the theme is initialized
* @return true if initialized, false otherwise
*/
bool lv_theme_material_mono_is_inited(void);
/**
* Get the theme
* @return a pointer to the theme, or NULL when not initialized
*/
lv_theme_t * lv_theme_material_mono_get(void);
/**
* Deinitialize the theme
*/
void lv_theme_material_mono_deinit(void);
#ifdef __cplusplus
}
#endif
#endif /* LV_THEME_MATERIAL_MONO_H */

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: Apache-2.0
#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration
#include <lvgl/widgets/card.h>
// Not static: the theme styles cards by this class
extern "C" const lv_obj_class_t lvgl_card_class = {
.base_class = &lv_obj_class,
.constructor_cb = nullptr,
.destructor_cb = nullptr,
.event_cb = nullptr,
.user_data = nullptr,
.name = "card",
.width_def = 0,
.height_def = 0,
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
.group_def = LV_OBJ_CLASS_GROUP_DEF_INHERIT,
.instance_size = 0,
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
};
lv_obj_t* lvgl_card_create(lv_obj_t* parent) {
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_card_class, parent);
lv_obj_class_init_obj(obj);
return obj;
}

View File

@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0
#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration
#include <lvgl/widgets/icon_button.h>
// Not static: the theme styles icon buttons by these classes. Each variant is a separate class,
// because the theme applies its styles while the object is created.
extern "C" const lv_obj_class_t lvgl_icon_button_class = {
.base_class = &lv_button_class,
.constructor_cb = nullptr,
.destructor_cb = nullptr,
.event_cb = nullptr,
.user_data = nullptr,
.name = "icon_button",
.width_def = 0,
.height_def = 0,
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
.group_def = LV_OBJ_CLASS_GROUP_DEF_INHERIT,
.instance_size = 0,
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
};
extern "C" const lv_obj_class_t lvgl_icon_button_filled_class = {
.base_class = &lv_button_class,
.constructor_cb = nullptr,
.destructor_cb = nullptr,
.event_cb = nullptr,
.user_data = nullptr,
.name = "icon_button_filled",
.width_def = 0,
.height_def = 0,
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
.group_def = LV_OBJ_CLASS_GROUP_DEF_INHERIT,
.instance_size = 0,
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
};
extern "C" const lv_obj_class_t lvgl_icon_button_tonal_class = {
.base_class = &lv_button_class,
.constructor_cb = nullptr,
.destructor_cb = nullptr,
.event_cb = nullptr,
.user_data = nullptr,
.name = "icon_button_tonal",
.width_def = 0,
.height_def = 0,
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
.group_def = LV_OBJ_CLASS_GROUP_DEF_INHERIT,
.instance_size = 0,
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
};
static const lv_obj_class_t* get_variant_class(enum LvglIconButtonVariant variant) {
switch (variant) {
case LVGL_ICON_BUTTON_FILLED:
return &lvgl_icon_button_filled_class;
case LVGL_ICON_BUTTON_TONAL:
return &lvgl_icon_button_tonal_class;
default:
return &lvgl_icon_button_class;
}
}
lv_obj_t* lvgl_icon_button_create_variant(lv_obj_t* parent, enum LvglIconButtonVariant variant) {
lv_obj_t* obj = lv_obj_class_create_obj(get_variant_class(variant), parent);
lv_obj_class_init_obj(obj);
return obj;
}
lv_obj_t* lvgl_icon_button_create(lv_obj_t* parent) {
return lvgl_icon_button_create_variant(parent, LVGL_ICON_BUTTON_STANDARD);
}

View File

@ -17,7 +17,8 @@ typedef struct {
static void sliderbox_constructor(const lv_obj_class_t* classPointer, lv_obj_t* obj);
static lv_obj_class_t sliderbox_class = {
// Not static: the theme styles sliderboxes by this class
extern "C" const lv_obj_class_t lvgl_sliderbox_class = {
.base_class = &lv_obj_class,
.constructor_cb = &sliderbox_constructor,
.destructor_cb = nullptr,
@ -38,7 +39,6 @@ static void sliderbox_constructor(const lv_obj_class_t* classPointer, lv_obj_t*
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(obj, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_column(obj, 4, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(obj, 0, LV_STATE_DEFAULT);
}
@ -139,7 +139,7 @@ lv_obj_t* lvgl_sliderbox_create(lv_obj_t* parent, int32_t min, int32_t max, int3
step = 1;
}
lv_obj_t* obj = lv_obj_class_create_obj(&sliderbox_class, parent);
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_sliderbox_class, parent);
lv_obj_class_init_obj(obj);
auto* sliderBox = reinterpret_cast<SliderBox*>(obj);
@ -165,10 +165,12 @@ lv_obj_t* lvgl_sliderbox_create(lv_obj_t* parent, int32_t min, int32_t max, int3
// +4 padding wasn't enough margin for 3-digit values to never wrap; widened to
// a flat per-character estimate instead of trusting raw glyph width too tightly.
char buffer[16];
lv_text_attributes_t text_attributes;
lv_text_attributes_init(&text_attributes);
lv_snprintf(buffer, sizeof(buffer), "%" LV_PRId32, min);
auto width_min = lv_text_get_width(buffer, lv_strlen(buffer), lv_obj_get_style_text_font(sliderBox->valueLabel, LV_PART_MAIN), 0);
auto width_min = lv_text_get_width(buffer, lv_strlen(buffer), lv_obj_get_style_text_font(sliderBox->valueLabel, LV_PART_MAIN), &text_attributes);
lv_snprintf(buffer, sizeof(buffer), "%" LV_PRId32, max);
auto width_max = lv_text_get_width(buffer, lv_strlen(buffer), lv_obj_get_style_text_font(sliderBox->valueLabel, LV_PART_MAIN), 0);
auto width_max = lv_text_get_width(buffer, lv_strlen(buffer), lv_obj_get_style_text_font(sliderBox->valueLabel, LV_PART_MAIN), &text_attributes);
auto max_width = (width_min > width_max) ? width_min : width_max;
lv_obj_set_width(sliderBox->valueLabel, max_width + 8);

View File

@ -5,6 +5,7 @@
#include <lvgl/fonts.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/icon_button.h>
#include <lvgl/widgets/spinner.h>
#include <tactility/check.h>
@ -26,54 +27,11 @@ static const _lv_font_t* getToolbarFont(UiDensity uiDensity) {
}
}
static uint32_t getActionIconPadding(UiDensity uiDensity) {
/** The size of icon buttons, which leaves room for the theme's focus indicator */
static uint32_t getActionButtonSize(UiDensity uiDensity) {
auto toolbar_height = getToolbarHeight(uiDensity);
// Minimal 8 pixels total padding for selection/animation (4+4 pixels)
return (uiDensity != LVGL_UI_DENSITY_COMPACT) ? (uint32_t)(toolbar_height * 0.2f) : 8;
}
static bool is_monochrome(lv_obj_t* obj) {
return lv_display_get_color_format(lv_obj_get_display(obj)) == LV_COLOR_FORMAT_I1;
}
/**
* Makes a toolbar button transparent. Its icon or text is drawn in the accent colour when it's
* selected or pressed, instead of showing an outline.
*/
static void apply_button_style(lv_obj_t* button) {
lv_obj_set_style_bg_opa(button, LV_OPA_TRANSP, LV_STATE_DEFAULT);
lv_obj_set_style_shadow_width(button, 0, LV_STATE_DEFAULT);
// Colours can't show the selection on monochrome displays, so those keep the theme's outline
if (is_monochrome(button)) {
return;
}
const lv_color_t accent = lv_theme_get_color_primary(button);
const lv_state_t states[] = { LV_STATE_FOCUSED, LV_STATE_FOCUS_KEY, LV_STATE_PRESSED };
for (const lv_state_t state : states) {
lv_obj_set_style_outline_width(button, 0, state);
// Inherited by the button's label or image, including symbol images
lv_obj_set_style_text_color(button, accent, state);
}
}
/**
* Helps with button expansion and also with vertical alignment of content,
* as the parent flex doesn't allow for vertical alignment
*/
static lv_obj_t* create_action_wrapper(lv_obj_t* parent, UiDensity ui_density) {
auto* wrapper = lv_obj_create(parent);
auto toolbar_height = getToolbarHeight(ui_density);
lv_obj_set_size(wrapper, LV_SIZE_CONTENT, toolbar_height);
auto icon_padding = getActionIconPadding(ui_density);
auto icon_padding_half = icon_padding / 2;
lv_obj_set_style_pad_all(wrapper, icon_padding_half, LV_STATE_DEFAULT); // For selection and touch animation
lv_obj_set_style_bg_opa(wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_opa(wrapper, 0, LV_STATE_DEFAULT);
return wrapper;
auto padding = (uiDensity != LVGL_UI_DENSITY_COMPACT) ? (uint32_t)(toolbar_height * 0.2f) : 8;
return toolbar_height - padding;
}
typedef struct {
@ -88,7 +46,8 @@ typedef struct {
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj);
static lv_obj_class_t toolbar_class = {
// Not static: the theme styles toolbars by this class
extern "C" const lv_obj_class_t lvgl_toolbar_class = {
.base_class = &lv_obj_class,
.constructor_cb = &toolbar_constructor,
.destructor_cb = nullptr,
@ -96,7 +55,7 @@ static lv_obj_class_t toolbar_class = {
.user_data = nullptr,
.name = nullptr,
.width_def = LV_PCT(100),
.height_def = 0,
.height_def = LV_SIZE_CONTENT,
.editable = false,
.group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE,
.instance_size = sizeof(Toolbar),
@ -121,63 +80,40 @@ static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
lv_obj_add_flag(obj, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
}
static lv_obj_t* create_action_button(lv_obj_t* parent, UiDensity ui_density) {
auto button_size = getActionButtonSize(ui_density);
lv_obj_t* button = lvgl_icon_button_create(parent);
lv_obj_set_size(button, button_size, button_size);
return button;
}
lv_obj_t* lvgl_toolbar_create(lv_obj_t* parent, const char* title) {
auto ui_density = lvgl_get_ui_density();
auto toolbar_height = getToolbarHeight(ui_density);
toolbar_class.height_def = toolbar_height;
lv_obj_t* obj = lv_obj_class_create_obj(&toolbar_class, parent);
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_toolbar_class, parent);
lv_obj_class_init_obj(obj);
lv_obj_set_height(obj, toolbar_height);
lv_obj_set_height(obj, getToolbarHeight(ui_density));
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
toolbar->nav_action_callback = nullptr;
lv_obj_set_width(obj, LV_PCT(100));
lv_obj_set_style_pad_all(obj, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_column(obj, 0, LV_STATE_DEFAULT);
lv_obj_center(obj);
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
auto icon_padding = getActionIconPadding(ui_density);
auto* close_button_wrapper = create_action_wrapper(obj, ui_density);
toolbar->close_button = lv_button_create(close_button_wrapper);
apply_button_style(toolbar->close_button);
lv_obj_set_size(toolbar->close_button, toolbar_height - icon_padding, toolbar_height - icon_padding);
lv_obj_set_style_pad_all(toolbar->close_button, 0, LV_STATE_DEFAULT);
lv_obj_align(toolbar->close_button, LV_ALIGN_CENTER, 0, 0);
toolbar->close_button = create_action_button(obj, ui_density);
toolbar->close_button_image = lv_image_create(toolbar->close_button);
lv_obj_align(toolbar->close_button_image, LV_ALIGN_CENTER, 0, 0);
auto* title_wrapper = lv_obj_create(obj);
uint32_t title_left_padding = (ui_density != LVGL_UI_DENSITY_COMPACT) ? icon_padding : 2;
uint32_t title_right_padding = (ui_density != LVGL_UI_DENSITY_COMPACT) ? (icon_padding / 2) : 2;
lv_obj_set_size(title_wrapper, LV_SIZE_CONTENT, LV_PCT(100));
lv_obj_set_style_bg_opa(title_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_left(title_wrapper, title_left_padding, LV_STATE_DEFAULT);
lv_obj_set_style_pad_right(title_wrapper, title_right_padding, LV_STATE_DEFAULT);
lv_obj_set_style_pad_ver(title_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(title_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_grow(title_wrapper, 1);
toolbar->title_label = lv_label_create(title_wrapper);
toolbar->title_label = lv_label_create(obj);
lv_obj_set_style_text_font(toolbar->title_label, getToolbarFont(ui_density), LV_STATE_DEFAULT);
lv_label_set_text(toolbar->title_label, title);
lv_label_set_long_mode(toolbar->title_label, LV_LABEL_LONG_MODE_SCROLL);
lv_obj_set_style_text_align(toolbar->title_label, LV_TEXT_ALIGN_LEFT, LV_STATE_DEFAULT);
lv_obj_align(toolbar->title_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_width(toolbar->title_label, LV_PCT(100));
lv_obj_set_flex_grow(toolbar->title_label, 1);
toolbar->action_container = lv_obj_create(obj);
lv_obj_set_width(toolbar->action_container, LV_SIZE_CONTENT);
lv_obj_set_size(toolbar->action_container, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(toolbar->action_container, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_all(toolbar->action_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_column(toolbar->action_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(toolbar->action_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(toolbar->action_container, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_align(toolbar->action_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lvgl_toolbar_set_nav_action(obj, LV_SYMBOL_CLOSE, &default_nav_action, nullptr);
@ -215,19 +151,7 @@ static lv_obj_t* toolbar_add_button_action(lv_obj_t* obj, const char* imageOrBut
check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
toolbar->action_count++;
auto ui_density = lvgl_get_ui_density();
auto toolbar_height = getToolbarHeight(ui_density);
auto* wrapper = create_action_wrapper(toolbar->action_container, ui_density);
auto padding = getActionIconPadding(ui_density);
lv_obj_t* action_button = lv_button_create(wrapper);
lv_obj_set_size(action_button, toolbar_height - padding, toolbar_height - padding);
lv_obj_set_style_pad_all(action_button, 0, LV_STATE_DEFAULT);
lv_obj_align(action_button, LV_ALIGN_CENTER, 0, 0);
apply_button_style(action_button);
lv_obj_t* action_button = create_action_button(toolbar->action_container, lvgl_get_ui_density());
lv_obj_add_event_cb(action_button, callback, LV_EVENT_SHORT_CLICKED, user_data);
lv_obj_t* button_content;
if (isImage) {
@ -255,14 +179,7 @@ lv_obj_t* lvgl_toolbar_add_switch_action(lv_obj_t* obj) {
check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
toolbar->action_count++;
auto ui_density = lvgl_get_ui_density();
auto* wrapper = create_action_wrapper(toolbar->action_container, ui_density);
lv_obj_set_style_pad_hor(wrapper, 4, LV_STATE_DEFAULT);
lv_obj_t* widget = lv_switch_create(wrapper);
lv_obj_set_align(widget, LV_ALIGN_CENTER);
return widget;
return lv_switch_create(toolbar->action_container);
}
lv_obj_t* lvgl_toolbar_add_dropdown_action(lv_obj_t* obj, const char* options, lv_coord_t width, const char* text) {
@ -270,11 +187,7 @@ lv_obj_t* lvgl_toolbar_add_dropdown_action(lv_obj_t* obj, const char* options, l
check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
toolbar->action_count++;
auto ui_density = lvgl_get_ui_density();
auto* wrapper = create_action_wrapper(toolbar->action_container, ui_density);
lv_obj_set_style_pad_hor(wrapper, 4, LV_STATE_DEFAULT);
lv_obj_t* widget = lv_dropdown_create(wrapper);
lv_obj_t* widget = lv_dropdown_create(toolbar->action_container);
lv_dropdown_set_options(widget, options);
lv_dropdown_set_selected_highlight(widget, false);
if (width > 0) {
@ -283,7 +196,6 @@ lv_obj_t* lvgl_toolbar_add_dropdown_action(lv_obj_t* obj, const char* options, l
if (text != nullptr) {
lv_dropdown_set_text(widget, text);
}
lv_obj_set_align(widget, LV_ALIGN_CENTER);
return widget;
}
@ -293,11 +205,7 @@ lv_obj_t* lvgl_toolbar_add_spinner_action(lv_obj_t* obj) {
check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
toolbar->action_count++;
auto ui_density = lvgl_get_ui_density();
auto* wrapper = create_action_wrapper(toolbar->action_container, ui_density);
auto* spinner = lvgl_spinner_create(wrapper);
lv_obj_set_align(spinner, LV_ALIGN_CENTER);
auto* spinner = lvgl_spinner_create(toolbar->action_container);
if (lv_display_get_color_format(lv_obj_get_display(obj)) == LV_COLOR_FORMAT_L8) {
lv_obj_set_style_image_recolor(spinner, lv_theme_get_color_secondary(obj), LV_STATE_DEFAULT);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,6 @@ private:
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

@ -2,6 +2,7 @@
#include <lvgl/devices/indev.h>
#include <lvgl/fonts.h>
#include <lvgl/widgets/icon_button.h>
#include <lvgl/widgets/toolbar.h>
#include <algorithm>
@ -12,8 +13,6 @@ namespace tt::app {
namespace {
constexpr int32_t TILE_GAP = 6;
constexpr uint32_t ICON_COLOR_SELECTED = 0x33BBFF;
constexpr uint32_t ICON_COLOR_HIGHLIGHTED = 0xF876E9;
// Minimum label width, as a multiple of the label's font size
constexpr uint32_t MIN_LABEL_WIDTH_IN_FONT_SIZES = 6;
@ -49,18 +48,6 @@ PageLayout computePageLayout(lv_obj_t* grid) {
};
}
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();
}
// Accent colours can't be rendered in 1bpp, so tiles use theme colours and the theme's focus outline instead
bool isMonochrome(lv_obj_t* obj) {
return lv_display_get_color_format(lv_obj_get_display(obj)) == LV_COLOR_FORMAT_I1;
}
const AppGridItem* getTileItem(lv_event_t* e) {
return static_cast<const AppGridItem*>(lv_obj_get_user_data(lv_event_get_target_obj(e)));
}
@ -110,13 +97,6 @@ void AppGrid::onTileKey(lv_event_t* 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);
@ -160,27 +140,23 @@ void AppGrid::populate() {
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_set_flag(prevButton, LV_OBJ_FLAG_HIDDEN, page_count <= 1);
lv_obj_set_flag(nextButton, LV_OBJ_FLAG_HIDDEN, page_count <= 1);
lv_obj_t* focusedTile = nullptr;
const bool monochrome = isMonochrome(grid);
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);
// The theme styles the tile as an icon button: its focus, pressed and selected (favourite) states
lv_obj_t* tile = lvgl_icon_button_create(grid);
if (item.highlighted) {
lv_obj_add_state(tile, LV_STATE_CHECKED);
}
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);
if (!monochrome) {
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));
@ -189,9 +165,6 @@ void AppGrid::populate() {
lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_large_font(), 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);
if (!monochrome) {
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);
@ -209,10 +182,6 @@ void AppGrid::populate() {
if (callbacks.onKey != nullptr) {
lv_obj_add_event_cb(tile, onTileKey, LV_EVENT_KEY, this);
}
if (!monochrome) {
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;
@ -247,7 +216,7 @@ void AppGrid::createWidgets(lv_obj_t* parent, lv_obj_t* toolbar) {
populate();
const bool next_visible = !lv_obj_has_flag(lv_obj_get_parent(nextButton), LV_OBJ_FLAG_HIDDEN);
const bool next_visible = !lv_obj_has_flag(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/check.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/sliderbox.h>
#include <lvgl/widgets/toolbar.h>
@ -77,23 +78,26 @@ void onOutputVolumeSlider(lv_event_t* event) {
}
lv_obj_t* createSection(lv_obj_t* parent, const char* title) {
auto* wrapper = lv_obj_create(parent);
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_hor(wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
auto* title_label = lv_label_create(wrapper);
auto* title_label = lv_label_create(parent);
lv_label_set_text(title_label, title);
return wrapper;
auto* card = lvgl_card_create(parent);
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
return card;
}
// An unstyled container, so the rows don't paint over the card
lv_obj_t* createRow(lv_obj_t* parent) {
auto* row = lv_obj_create(parent);
lv_obj_remove_style_all(row);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
return row;
}
lv_obj_t* createSwitchRow(lv_obj_t* parent, const char* label, lv_event_cb_t cb, void* userData) {
auto* row = lv_obj_create(parent);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
auto* row = createRow(parent);
auto* row_label = lv_label_create(row);
lv_label_set_text(row_label, label);
@ -107,10 +111,7 @@ lv_obj_t* createSwitchRow(lv_obj_t* parent, const char* label, lv_event_cb_t cb,
}
lv_obj_t* createSliderRow(lv_obj_t* parent, const char* label, int32_t initialValue, lv_event_cb_t cb, void* userData) {
auto* row = lv_obj_create(parent);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
auto* row = createRow(parent);
auto* row_label = lv_label_create(row);
lv_label_set_text(row_label, label);

View File

@ -163,11 +163,17 @@ void View::updateScanning() {
void View::updatePeerList() {
lv_obj_clean(peers_list);
// Enable on boot row
auto* enable_on_boot_wrapper = lv_obj_create(peers_list);
// Enable on boot
// An unstyled row insets the wrapper like the list items' text: margins would make a full-width item overflow the list
auto* enable_on_boot_row = lv_obj_create(peers_list);
lv_obj_remove_style_all(enable_on_boot_row);
lv_obj_set_size(enable_on_boot_row, LV_PCT(100), LV_SIZE_CONTENT);
const int32_t enable_on_boot_inset = (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) ? 2 : LV_DPX(16);
lv_obj_set_style_pad_hor(enable_on_boot_row, enable_on_boot_inset, LV_STATE_DEFAULT);
auto* enable_on_boot_wrapper = lv_obj_create(enable_on_boot_row);
lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
auto* enable_label = lv_label_create(enable_on_boot_wrapper);
lv_label_set_text(enable_label, "Enable on boot");
@ -178,12 +184,6 @@ void View::updatePeerList() {
lv_obj_add_event_cb(enable_on_boot_switch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_add_event_cb(enable_on_boot_wrapper, onEnableOnBootParentClicked, LV_EVENT_SHORT_CLICKED, enable_on_boot_switch);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 2, LV_STATE_DEFAULT);
} else {
lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 8, LV_STATE_DEFAULT);
}
updateEnableOnBootToggle();
using enum bluetooth::RadioState;

View File

@ -177,7 +177,6 @@ void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->displaySettings = settings::display::loadOrGetDefault();
auto ui_density = lvgl_get_ui_density();
bool has_imu = device_exists_of_type(&IMU_TYPE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
@ -205,9 +204,6 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_size(brightness_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_hor(brightness_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(brightness_wrapper, 0, LV_STATE_DEFAULT);
if (ui_density != LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_ver(brightness_wrapper, 4, LV_STATE_DEFAULT);
}
auto* brightness_label = lv_label_create(brightness_wrapper);
lv_label_set_text(brightness_label, "Brightness");

View File

@ -552,15 +552,15 @@ void View::update(size_t start_index) {
});
if (is_root) {
lv_obj_add_flag(lv_obj_get_parent(navigate_up_button), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_remove_flag(lv_obj_get_parent(navigate_up_button), LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
}
if (state->hasClipboard() && !is_root) {
lv_obj_remove_flag(lv_obj_get_parent(paste_button), LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(paste_button, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(lv_obj_get_parent(paste_button), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(paste_button, LV_OBJ_FLAG_HIDDEN);
}
lvgl_unlock();
@ -579,7 +579,7 @@ void View::init(uint32_t appInstanceId, lv_obj_t* parent) {
new_file_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_FILE, &onNewFilePressedCallback, this);
new_folder_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_DIRECTORY, &onNewFolderPressedCallback, this);
paste_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_PASTE, &onPastePressedCallback, this);
lv_obj_add_flag(lv_obj_get_parent(paste_button), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(paste_button, LV_OBJ_FLAG_HIDDEN);
auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100));

View File

@ -239,23 +239,16 @@ 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);
uint8_t margin = (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) ? 2 : 8;
auto* toolbar = lvgl_toolbar_create(parent, "GPS");
// 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_add_text_button_action(toolbar, LV_SYMBOL_PLUS, onAddGpsPressed, ctx);
lv_obj_set_style_margin_bottom(toolbar, margin, LV_STATE_DEFAULT);
ctx->deviceListWrapper = lv_obj_create(parent);
lv_obj_set_size(ctx->deviceListWrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(ctx->deviceListWrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_grow(ctx->deviceListWrapper, 1);
lv_obj_set_style_border_width(ctx->deviceListWrapper, 0, 0);
lv_obj_set_style_pad_hor(ctx->deviceListWrapper, margin, 0);
lv_obj_set_style_pad_top(ctx->deviceListWrapper, 0, 0);
lv_obj_set_style_pad_bottom(ctx->deviceListWrapper, margin, 0);
lv_obj_set_style_pad_row(ctx->deviceListWrapper, margin, 0);
rebuildDeviceList(ctx);
updateDeviceStates(ctx);

View File

@ -10,6 +10,7 @@
#include <lvgl/icons/launcher.h>
#include <lvgl/fonts.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/icon_button.h>
#include <lvgl_window_manager/window_manager.h>
@ -32,19 +33,30 @@ constexpr auto* TAG = "Launcher";
namespace {
uint32_t getButtonPadding(UiDensity density, uint32_t buttonSize) {
if (density == LVGL_UI_DENSITY_COMPACT) {
return 0;
} else {
return buttonSize / 8;
}
}
int32_t computeButtonMargin(int32_t available_span, int32_t total_button_size) {
const int32_t usable = std::max<int32_t>(0, available_span - (3 * total_button_size));
return std::min<int32_t>(usable / 16, total_button_size / 2);
}
// Spreads the buttons along the display's longest side. The button padding comes from the theme.
void applyButtonMargins(lv_obj_t* buttonsWrapper, bool isLandscape) {
const uint32_t child_count = lv_obj_get_child_count(buttonsWrapper);
if (child_count == 0) {
return;
}
auto* first_button = lv_obj_get_child(buttonsWrapper, 0);
const auto total_button_size = static_cast<int32_t>(lvgl_get_launcher_icon_font_height()) +
lv_obj_get_style_pad_left(first_button, LV_PART_MAIN) + lv_obj_get_style_pad_right(first_button, LV_PART_MAIN);
const auto* display = lv_obj_get_display(buttonsWrapper);
const auto span = isLandscape ? lv_display_get_horizontal_resolution(display) : lv_display_get_vertical_resolution(display);
const int32_t margin = computeButtonMargin(span, total_button_size);
for (uint32_t i = 0; i < child_count; i++) {
auto* button = lv_obj_get_child(buttonsWrapper, i);
lv_obj_set_style_margin_hor(button, isLandscape ? margin : 0, LV_STATE_DEFAULT);
lv_obj_set_style_margin_ver(button, isLandscape ? 0 : margin, LV_STATE_DEFAULT);
}
}
void onAppPressed(lv_event_t* e) {
auto* appId = static_cast<const char*>(lv_event_get_user_data(e));
uint32_t instance_id = 0;
@ -54,28 +66,14 @@ void onAppPressed(lv_event_t* e) {
}
}
lv_obj_t* createAppButton(lv_obj_t* parent, UiDensity uiDensity, const char* imageFile, const char* appId, int32_t itemMargin, bool isLandscape) {
lv_obj_t* createAppButton(lv_obj_t* parent, LvglIconButtonVariant variant, const char* imageFile, const char* appId) {
const auto button_size = lvgl_get_launcher_icon_font_height();
const auto button_padding = getButtonPadding(uiDensity, button_size);
auto* apps_button = lv_button_create(parent);
lv_obj_set_style_pad_all(apps_button, static_cast<int32_t>(button_padding), LV_STATE_DEFAULT);
if (isLandscape) {
lv_obj_set_style_margin_hor(apps_button, itemMargin, LV_STATE_DEFAULT);
} else {
lv_obj_set_style_margin_ver(apps_button, itemMargin, LV_STATE_DEFAULT);
}
lv_obj_set_style_shadow_width(apps_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(apps_button, 0, LV_STATE_DEFAULT);
auto* apps_button = lvgl_icon_button_create_variant(parent, variant);
// create the image first
auto* button_image = lv_image_create(apps_button);
lv_obj_set_style_text_font(button_image, lvgl_get_launcher_icon_font(), LV_STATE_DEFAULT);
lv_image_set_src(button_image, imageFile);
lv_obj_set_style_text_color(button_image, lv_theme_get_color_primary(button_image), LV_STATE_DEFAULT);
lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, LV_STATE_DEFAULT);
// Ensure it's square (Material Symbols are slightly wider than tall)
lv_obj_set_size(button_image, button_size, button_size);
@ -118,10 +116,6 @@ void onButtonsWrapperResized(lv_event_t* e) {
auto* buttons_wrapper = static_cast<lv_obj_t*>(lv_event_get_user_data(e));
const auto* display = lv_obj_get_display(buttons_wrapper);
const auto button_size = lvgl_get_launcher_icon_font_height();
const auto button_padding = getButtonPadding(lvgl_get_ui_density(), button_size);
const auto total_button_size = button_size + (button_padding * 2);
const auto horizontal_px = lv_display_get_horizontal_resolution(display);
const auto vertical_px = lv_display_get_vertical_resolution(display);
const bool is_landscape_display = horizontal_px >= vertical_px;
@ -132,27 +126,12 @@ void onButtonsWrapperResized(lv_event_t* e) {
}
lv_obj_set_flex_flow(buttons_wrapper, is_landscape_display ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN);
const int32_t margin = is_landscape_display
? computeButtonMargin(horizontal_px, total_button_size)
: computeButtonMargin(vertical_px, total_button_size);
const uint32_t child_count = lv_obj_get_child_count(buttons_wrapper);
for (uint32_t i = 0; i < child_count; i++) {
auto* button = lv_obj_get_child(buttons_wrapper, i);
lv_obj_set_style_margin_hor(button, is_landscape_display ? margin : 0, LV_STATE_DEFAULT);
lv_obj_set_style_margin_ver(button, is_landscape_display ? 0 : margin, LV_STATE_DEFAULT);
}
applyButtonMargins(buttons_wrapper, is_landscape_display);
}
void createWidgets(lv_obj_t* parent, void*) {
auto* buttons_wrapper = lv_obj_create(parent);
auto ui_density = lvgl_get_ui_density();
const auto button_size = lvgl_get_launcher_icon_font_height();
const auto button_padding = getButtonPadding(ui_density, button_size);
const auto total_button_size = button_size + (button_padding * 2);
lv_obj_align(buttons_wrapper, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_size(buttons_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_border_width(buttons_wrapper, 0, LV_STATE_DEFAULT);
@ -171,13 +150,11 @@ void createWidgets(lv_obj_t* parent, void*) {
lv_obj_set_flex_flow(buttons_wrapper, LV_FLEX_FLOW_COLUMN);
}
const int32_t margin = is_landscape_display
? computeButtonMargin(lv_display_get_horizontal_resolution(display), total_button_size)
: computeButtonMargin(lv_display_get_vertical_resolution(display), total_button_size);
auto* app_list_button = createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_APPS, "tactility.applist", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings", margin, is_landscape_display);
// The app list is the main action, so it stands out more than the others
auto* app_list_button = createAppButton(buttons_wrapper, LVGL_ICON_BUTTON_FILLED, LVGL_ICON_LAUNCHER_APPS, "tactility.applist");
createAppButton(buttons_wrapper, LVGL_ICON_BUTTON_FILLED, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files");
createAppButton(buttons_wrapper, LVGL_ICON_BUTTON_FILLED, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings");
applyButtonMargins(buttons_wrapper, is_landscape_display);
// The launcher's container is several levels below the screen, and LVGL only sends
// LV_EVENT_SIZE_CHANGED to the screen object itself on a resolution change - so the
@ -188,16 +165,13 @@ void createWidgets(lv_obj_t* parent, void*) {
// Some devices (e.g. T-Lora Pager) have no other way to power off, so the
// button stays in the launcher; the confirmation flow lives in the PowerOff app.
if (shouldShowPowerButton()) {
auto* power_button = lv_button_create(parent);
auto* power_button = lvgl_icon_button_create(parent);
lv_obj_set_style_pad_all(power_button, 8, 0);
lv_obj_align(power_button, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_add_event_cb(power_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)"tactility.poweroff");
lv_obj_set_style_shadow_width(power_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(power_button, 0, LV_PART_MAIN);
auto* power_label = lv_label_create(power_button);
lv_label_set_text(power_label, LV_SYMBOL_POWER);
lv_obj_set_style_text_color(power_label, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
}
// If we don't have a touch device, we assume there's some other kind of input like a keyboard, an encoder or button control

View File

@ -14,6 +14,7 @@
#include <tactility/time.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/card.h>
#include <lvgl/widgets/toolbar.h>
#include <vector>
@ -170,10 +171,14 @@ void createWidgets(lv_obj_t* parent, void* userData) {
entry.device = device;
lv_obj_t* header = lv_label_create(wrapper);
lv_label_set_text_fmt(header, "%s:", device->name);
lv_label_set_text(header, device->name);
lv_obj_t* card = lvgl_card_create(wrapper);
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
if (power_supply_supports_charge_control(device)) {
lv_obj_t* switch_container = lv_obj_create(wrapper);
lv_obj_t* switch_container = lv_obj_create(card);
lv_obj_set_width(switch_container, LV_PCT(100));
lv_obj_set_height(switch_container, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(switch_container, 0, 0);
@ -193,7 +198,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
}
if (power_supply_supports_quick_charge(device)) {
lv_obj_t* qc_container = lv_obj_create(wrapper);
lv_obj_t* qc_container = lv_obj_create(card);
lv_obj_set_width(qc_container, LV_PCT(100));
lv_obj_set_height(qc_container, LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(qc_container, 0, 0);
@ -215,8 +220,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
PowerSupplyPropertyValue value;
for (auto property : DISPLAYED_PROPERTIES) {
if (power_supply_get_property(device, property, &value) == ERROR_NONE) {
lv_obj_t* label = lv_label_create(wrapper);
lv_obj_set_style_margin_left(label, 24, LV_STATE_DEFAULT);
lv_obj_t* label = lv_label_create(card);
setPropertyLabelText(label, property, value);
entry.propertyWidgets.push_back({ property, label });
}

View File

@ -145,12 +145,6 @@ MemoryBarWidgets createMemoryBar(lv_obj_t* parent, const char* label) {
lv_obj_set_width(bottom_label, LV_PCT(100));
lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_bottom(bottom_label, 2, LV_STATE_DEFAULT);
} else {
lv_obj_set_style_pad_bottom(bottom_label, 12, LV_STATE_DEFAULT);
}
return {bar, bottom_label};
}

View File

@ -149,7 +149,6 @@ void createWidgets(lv_obj_t* parent, void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->tbSettings = settings::trackball::loadOrGetDefault();
auto ui_density = lvgl_get_ui_density();
ctx->updated = false;
ctx->trackballIndev = findFirstTrackballIndev();
@ -211,9 +210,6 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_size(enc_sens_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_hor(enc_sens_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(enc_sens_wrapper, 0, LV_STATE_DEFAULT);
if (ui_density != LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_ver(enc_sens_wrapper, 4, LV_STATE_DEFAULT);
}
auto* enc_sens_label = lv_label_create(enc_sens_wrapper);
lv_label_set_text(enc_sens_label, "Encoder Speed");
@ -235,9 +231,6 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_size(ptr_sens_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_hor(ptr_sens_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(ptr_sens_wrapper, 0, LV_STATE_DEFAULT);
if (ui_density != LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_ver(ptr_sens_wrapper, 4, LV_STATE_DEFAULT);
}
auto* ptr_sens_label = lv_label_create(ptr_sens_wrapper);
lv_label_set_text(ptr_sens_label, "Pointer Speed");

View File

@ -142,10 +142,15 @@ void View::updateNetworkList() {
// Enable on boot
auto* enable_on_boot_wrapper = lv_obj_create(networks_list);
// An unstyled row insets the wrapper like the list items' text: margins would make a full-width item overflow the list
auto* enable_on_boot_row = lv_obj_create(networks_list);
lv_obj_remove_style_all(enable_on_boot_row);
lv_obj_set_size(enable_on_boot_row, LV_PCT(100), LV_SIZE_CONTENT);
const int32_t enable_on_boot_inset = (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) ? 2 : LV_DPX(16);
lv_obj_set_style_pad_hor(enable_on_boot_row, enable_on_boot_inset, LV_STATE_DEFAULT);
auto* enable_on_boot_wrapper = lv_obj_create(enable_on_boot_row);
lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
auto* enable_label = lv_label_create(enable_on_boot_wrapper);
lv_label_set_text(enable_label, "Enable on boot");
@ -156,12 +161,6 @@ void View::updateNetworkList() {
lv_obj_add_event_cb(enable_on_boot_switch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, bindings);
lv_obj_add_event_cb(enable_on_boot_wrapper, onEnableOnBootParentClicked, LV_EVENT_SHORT_CLICKED, enable_on_boot_switch);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 2, LV_STATE_DEFAULT);
} else {
lv_obj_set_style_pad_ver(enable_on_boot_wrapper, 8, LV_STATE_DEFAULT);
}
updateEnableOnBootToggle();
switch (state->getRadioState()) {

View File

@ -6,7 +6,6 @@
#include <Tactility/Tactility.h>
#include <app/event.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/scheduler.h>
#include <app/start.h>

View File

@ -74,7 +74,7 @@ static lv_obj_t* windowManagerScreenInit(lv_obj_t* root) {
lv_obj_set_flex_flow(vertical_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(vertical_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_pad_gap(vertical_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(vertical_container, lv_color_black(), LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(vertical_container, LV_OPA_TRANSP, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(vertical_container, 0, LV_STATE_DEFAULT);
lv_obj_set_style_radius(vertical_container, 0, LV_STATE_DEFAULT);
lv_obj_remove_flag(vertical_container, LV_OBJ_FLAG_SCROLLABLE);

View File

@ -168,21 +168,12 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
auto* statusbar = reinterpret_cast<Statusbar*>(obj);
const auto fg_color = STATUSBAR_COLORS_INVERTED ? lv_color_black() : lv_color_white();
const auto bg_color = STATUSBAR_COLORS_INVERTED ? lv_color_white() : lv_color_black();
lv_obj_set_style_bg_color(obj, bg_color, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_STATE_DEFAULT);
// Inverted statusbar creates a separator between statusbar and the rest of the UI below it
// The statusbar has no theme styles: it shows the themed container background behind it and inherits its text colour
if (STATUSBAR_COLORS_INVERTED) {
// Show border as a horizontal line on the bottom
lv_obj_set_style_border_color(obj, fg_color, LV_STATE_DEFAULT);
// A horizontal line separates the statusbar from the rest of the UI below it
lv_obj_set_style_border_color(obj, lv_obj_get_style_text_color(obj, LV_PART_MAIN), LV_STATE_DEFAULT);
lv_obj_set_style_border_side(obj, LV_BORDER_SIDE_BOTTOM, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 1, LV_STATE_DEFAULT);
} else {
// No border
lv_obj_set_style_border_color(obj, bg_color, LV_STATE_DEFAULT);
}
lv_obj_set_width(obj, LV_PCT(100));
@ -197,7 +188,6 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
lv_obj_set_style_pad_column(obj, icon_padding, LV_STATE_DEFAULT);
statusbar->time = lv_label_create(obj);
lv_obj_set_style_text_color(statusbar->time, fg_color, LV_STATE_DEFAULT);
lv_obj_set_style_margin_left(statusbar->time, 4, LV_STATE_DEFAULT);
update_time(statusbar);
@ -213,7 +203,6 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
auto* image = lv_image_create(obj);
lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work
lv_obj_set_style_text_font(image, lvgl_get_statusbar_icon_font(), LV_STATE_DEFAULT);
lv_obj_set_style_text_color(image, fg_color, LV_STATE_DEFAULT);
lv_obj_set_style_pad_all(image, 0, LV_STATE_DEFAULT);
statusbar->icons[i] = image;

View File

@ -1,24 +0,0 @@
#ifdef ESP_PLATFORM
#include <lvgl.h>
#include <lvgl/lvgl.h>
extern "C" {
extern lv_obj_t* __real_lv_button_create(lv_obj_t* parent);
lv_obj_t* __wrap_lv_button_create(lv_obj_t* parent) {
auto button = __real_lv_button_create(parent);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_all(button, 3, LV_STATE_DEFAULT);
lv_obj_set_style_radius(button, 3, LV_STATE_DEFAULT);
}
return button;
}
}
#endif // ESP_PLATFORM

View File

@ -1,30 +0,0 @@
#ifdef ESP_PLATFORM
#include <lvgl.h>
#include <lvgl/lvgl.h>
extern "C" {
extern lv_obj_t* __real_lv_dropdown_create(lv_obj_t* parent);
lv_obj_t* __wrap_lv_dropdown_create(lv_obj_t* parent) {
auto dropdown = __real_lv_dropdown_create(parent);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_all(dropdown, 2, LV_STATE_DEFAULT);
}
lv_obj_set_style_border_width(dropdown, 1, LV_PART_MAIN);
if (lv_display_get_color_format(lv_obj_get_display(parent)) == LV_COLOR_FORMAT_L8) {
lv_obj_set_style_border_color(dropdown, lv_theme_get_color_secondary(parent), LV_PART_MAIN);
} else {
lv_obj_set_style_border_color(dropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN);
}
return dropdown;
}
}
#endif // ESP_PLATFORM

View File

@ -1,36 +0,0 @@
#ifdef ESP_PLATFORM
#include <lvgl.h>
#include <lvgl/lvgl.h>
extern "C" {
extern lv_obj_t* __real_lv_list_create(lv_obj_t* parent);
extern lv_obj_t* __real_lv_list_add_button(lv_obj_t* list, const void* icon, const char* txt);
lv_obj_t* __wrap_lv_list_create(lv_obj_t* parent) {
auto* list = __real_lv_list_create(parent);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_row(list, 2, LV_STATE_DEFAULT);
lv_obj_set_style_pad_column(list, 2, LV_STATE_DEFAULT);
lv_obj_set_style_pad_all(list, 2, LV_STATE_DEFAULT);
}
return list;
}
lv_obj_t* __wrap_lv_list_add_button(lv_obj_t* list, const void* icon, const char* txt) {
auto* button = __real_lv_list_add_button(list, icon, txt);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_ver(button, 2, LV_STATE_DEFAULT);
}
return button;
}
}
#endif // ESP_PLATFORM

View File

@ -1,34 +0,0 @@
#ifdef ESP_PLATFORM
#include <lvgl.h>
#include <lvgl/lvgl.h>
extern "C" {
extern void __real_lv_obj_set_flex_flow(lv_obj_t* obj, lv_flex_flow_t flow);
extern lv_obj_t* __real_lv_obj_create(lv_obj_t* parent);
void __wrap_lv_obj_set_flex_flow(lv_obj_t* obj, lv_flex_flow_t flow) {
__real_lv_obj_set_flex_flow(obj, flow);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_gap(obj, 4, LV_STATE_DEFAULT);
}
}
lv_obj_t* __wrap_lv_obj_create(lv_obj_t* parent) {
auto obj = __real_lv_obj_create(parent);
// Screens and display layers (a null parent, also used by LVGL's own display setup) must stay borderless and unpadded
if (parent != nullptr && lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_all(obj, 2, LV_STATE_DEFAULT);
lv_obj_set_style_pad_gap(obj, 2, LV_STATE_DEFAULT);
lv_obj_set_style_radius(obj, 3, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 1, LV_STATE_DEFAULT);
}
return obj;
}
}
#endif // ESP_PLATFORM

View File

@ -1,27 +0,0 @@
#ifdef ESP_PLATFORM
#include <lvgl.h>
#include <lvgl/lvgl.h>
extern "C" {
extern lv_obj_t* __real_lv_switch_create(lv_obj_t* parent);
lv_obj_t* __wrap_lv_switch_create(lv_obj_t* parent) {
auto widget = __real_lv_switch_create(parent);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_size(widget, 25, 15, LV_STATE_DEFAULT);
}
if (lv_display_get_color_format(lv_obj_get_display(parent)) == LV_COLOR_FORMAT_L8) {
lv_obj_set_style_bg_color(widget, lv_theme_get_color_secondary(parent), LV_PART_MAIN);
}
return widget;
}
}
#endif // ESP_PLATFORM

View File

@ -10,20 +10,11 @@ extern lv_obj_t* __real_lv_textarea_create(lv_obj_t* parent);
lv_obj_t* __wrap_lv_textarea_create(lv_obj_t* parent) {
auto textarea = __real_lv_textarea_create(parent);
if (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_all(textarea, 2, LV_STATE_DEFAULT);
}
auto* software_keyboard = lvgl_software_keyboard_get_last();
if (software_keyboard != nullptr) {
lvgl_keyboard_add_textarea(software_keyboard, textarea);
}
if (lv_display_get_color_format(lv_obj_get_display(parent)) == LV_COLOR_FORMAT_L8) {
lv_obj_set_style_border_width(textarea, 1, LV_PART_MAIN);
lv_obj_set_style_border_color(textarea, lv_theme_get_color_secondary(parent), LV_PART_MAIN);
}
return textarea;
}

View File

@ -81,7 +81,7 @@ dependencies:
rules:
- if: "target in [esp32s3, esp32p4]"
espressif/esp_lvgl_port: "2.7.2"
lvgl/lvgl: "9.3.0"
lvgl/lvgl: "9.4.0"
epdiy:
git: https://github.com/vroland/epdiy.git
version: 2.1.3