diff --git a/Buildscripts/DevicetreeCompiler/source/generator.py b/Buildscripts/DevicetreeCompiler/source/generator.py index 9a0ccf6cf..4869d26f2 100644 --- a/Buildscripts/DevicetreeCompiler/source/generator.py +++ b/Buildscripts/DevicetreeCompiler/source/generator.py @@ -2,6 +2,7 @@ from textwrap import dedent from source.models import * from .exception import DevicetreeException +from .printing import print_warning def write_include(file, include: IncludeC, verbose: bool): if verbose: @@ -59,6 +60,35 @@ def find_phandle(devices: list[Device], phandle: str): return f"&{get_device_node_name_safe(device)}" raise DevicetreeException(f"phandle '{phandle}' not found in devicetree") +def parse_number(text: str): + try: + return int(text, 0) + except ValueError: + return None + +def get_device_address(device: Device) -> str: + """The node's unit address (e.g. "display@0"), or else the first value of its "reg" property (e.g. an I2C address).""" + reg_property = find_device_property(device, "reg") + reg_value = None + if reg_property is not None: + if reg_property.type == "value": + reg_value = reg_property.value + elif reg_property.type == "values" and len(reg_property.value) > 0: + reg_value = str(reg_property.value[0]) + if device.node_address is None: + return reg_value if reg_value is not None else "0" + # Unit addresses are hexadecimal, like in Linux + try: + unit_address = int(device.node_address, 16) + except ValueError: + raise DevicetreeException(f"Unit address of {device.node_name}@{device.node_address} isn't a hexadecimal number") + # Like Linux's dtc, the unit address should match the first "reg" value + if reg_value is not None: + reg_number = parse_number(reg_value) + if reg_number is not None and unit_address != reg_number: + print_warning(f"{device.node_name}@{device.node_address} has a different reg value: {reg_value}") + return str(unit_address) + def property_to_string(property: DeviceProperty, devices: list[Device]) -> str: type = property.type if type == "value" or type == "int": @@ -277,7 +307,7 @@ def write_device_structs(file, device: Device, parent_device: Device, bindings: # Write config struct write_config(file, device, bindings, devices, type_name) # Write device struct - address_value = device.node_address if device.node_address is not None else "0" + address_value = get_device_address(device) file.write(f"static struct Device {node_name}" " = {\n") file.write(f"\t.address = {address_value},\n") file.write(f"\t.name = \"{device.node_name}\",\n") # Use original name diff --git a/Buildscripts/DevicetreeCompiler/tests/test_integration.py b/Buildscripts/DevicetreeCompiler/tests/test_integration.py index 6e21883dc..6f8392988 100644 --- a/Buildscripts/DevicetreeCompiler/tests/test_integration.py +++ b/Buildscripts/DevicetreeCompiler/tests/test_integration.py @@ -295,6 +295,86 @@ def test_array_property_defaults_to_null_when_absent(): print("PASSED") return True +def write_reg_config(tmp_dir, node): + config_dir = os.path.join(tmp_dir, "reg_data") + bindings_dir = os.path.join(config_dir, "bindings") + os.makedirs(bindings_dir) + + with open(os.path.join(config_dir, "module.yaml"), "w") as f: + f.write("dts: test.dts\nbindings: bindings") + + with open(os.path.join(config_dir, "test.dts"), "w") as f: + f.write(f"""/dts-v1/; + +/ {{ + compatible = "test,root"; + model = "Test Model"; + + {node} {{ + compatible = "test,reg-device"; + reg = <0x5D>; + }}; +}}; +""") + + with open(os.path.join(bindings_dir, "test,root.yaml"), "w") as f: + f.write("description: Test root binding\ncompatible: \"test,root\"\nproperties:\n model:\n type: string\n") + + with open(os.path.join(bindings_dir, "test,reg-device.yaml"), "w") as f: + f.write("description: Test reg binding\ncompatible: \"test,reg-device\"\nproperties:\n reg:\n type: int\n") + + return config_dir + +def test_reg_is_address_without_unit_address(): + print("Running test_reg_is_address_without_unit_address...") + with tempfile.TemporaryDirectory() as tmp_dir: + config_dir = write_reg_config(tmp_dir, "touch") + output_dir = os.path.join(tmp_dir, "output") + os.makedirs(output_dir) + + result = run_compiler(config_dir, output_dir) + + if result.returncode != 0: + print(f"FAILED: Compilation should have succeeded: {result.stderr} {result.stdout}") + return False + + with open(os.path.join(output_dir, "devicetree.c")) as f: + generated = f.read() + + if ".address = 0x5D," not in generated: + print(f"FAILED: Expected the reg value as address:\n{generated}") + return False + + print("PASSED") + return True + +def test_unit_address_mismatch_warns(): + print("Running test_unit_address_mismatch_warns...") + with tempfile.TemporaryDirectory() as tmp_dir: + config_dir = write_reg_config(tmp_dir, "touch@10") + output_dir = os.path.join(tmp_dir, "output") + os.makedirs(output_dir) + + result = run_compiler(config_dir, output_dir) + + if result.returncode != 0: + print(f"FAILED: Compilation should have succeeded: {result.stderr} {result.stdout}") + return False + + if "different reg value" not in result.stdout: + print(f"FAILED: Expected a warning, got: {result.stdout}") + return False + + with open(os.path.join(output_dir, "devicetree.c")) as f: + generated = f.read() + + if ".address = 16," not in generated: + print(f"FAILED: Expected the unit address as address:\n{generated}") + return False + + print("PASSED") + return True + def test_compile_missing_config(): print("Running test_compile_missing_config...") with tempfile.TemporaryDirectory() as output_dir: @@ -322,7 +402,9 @@ def test_compile_missing_config(): test_minmax_out_of_range_default_fails, test_minmax_symbolic_value_skips_validation, test_array_property_generates_static_array_and_length, - test_array_property_defaults_to_null_when_absent + test_array_property_defaults_to_null_when_absent, + test_reg_is_address_without_unit_address, + test_unit_address_mismatch_warns ] failed = 0 diff --git a/Devices/m5stack-tab5/Source/devices/devices_v1.cpp b/Devices/m5stack-tab5/Source/devices/devices_v1.cpp index b7d50b34a..72f587542 100644 --- a/Devices/m5stack-tab5/Source/devices/devices_v1.cpp +++ b/Devices/m5stack-tab5/Source/devices/devices_v1.cpp @@ -113,7 +113,7 @@ void tab5_create_devices_v1(Device* i2c0) { .dsi_bus_id = 0, .num_data_lanes = 2, .lane_bit_rate_mbps = 1000, - .dpi_clock_freq_mhz = 60, + .dpi_clock_freq_mhz = 80, .hsync_pulse_width = 40, .hsync_back_porch = 140, .hsync_front_porch = 40, diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 869c6dca0..2942c9153 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -7,6 +7,7 @@ ## Higher Priority +- Wi-Fi and Bluetooth settings app: scanning should start and gather results, then it should stop refreshing the scan results automatically. When scanning is finished, a paginated view should be shown with the results (as many as fit on the screen, but not more). There should be an icon on the toolbar to refresh scanning (looking glass? something else?) and this icon is only visible when the radio is enabled. When radio is enabled by the user, automatically start a scan. If the radio was already enabled and the app starts, start scanning. - 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. diff --git a/Modules/cpp-symbols-module/README.md b/Modules/cpp-symbols-module/README.md index 9f5b7f4e3..10f87a7b4 100644 --- a/Modules/cpp-symbols-module/README.md +++ b/Modules/cpp-symbols-module/README.md @@ -19,6 +19,7 @@ ESP_PLATFORM` in `source/module.cpp`; symbols outside that gate (e.g. `__cxa_pur - `operator new(unsigned int)` / `operator delete(void*, unsigned int)` (`_Znwj` / `_ZdlPvj`) - `operator new[](unsigned int)` / `operator delete[](void*, unsigned int)` (`_Znaj` / `_ZdaPvj`) +- `operator new[](unsigned int, std::nothrow_t const&)` (`_ZnajRKSt9nothrow_t`) - `operator delete(void*)` / `operator delete[](void*)` (`_ZdlPv` / `_ZdaPv`) - the unsized forms, used instead of the above when the compiler determines no size is needed - `std::nothrow` diff --git a/Modules/cpp-symbols-module/source/module.cpp b/Modules/cpp-symbols-module/source/module.cpp index 3a575ad28..5b541e867 100644 --- a/Modules/cpp-symbols-module/source/module.cpp +++ b/Modules/cpp-symbols-module/source/module.cpp @@ -31,6 +31,7 @@ extern "C" { extern void* _Znwj(uint32_t size); // operator new(unsigned int) extern void _ZdlPvj(void* p, uint64_t size); // operator delete(void*, unsigned int) extern void* _Znaj(uint32_t size); // operator new[](unsigned int) + extern void* _ZnajRKSt9nothrow_t(uint32_t size, const std::nothrow_t& tag); // operator new[](unsigned int, std::nothrow_t const&) extern void _ZdaPvj(void* p, uint64_t size); // operator delete[](void*, unsigned int) // Unsized forms: the compiler picks these over the sized ones above depending on context // (e.g. trivially-destructible types needing no array cookie), so both must be exported. @@ -572,6 +573,7 @@ static const ModuleSymbol SYMBOLS[] = { DEFINE_MODULE_SYMBOL(_Znwj), // operator new(unsigned int) DEFINE_MODULE_SYMBOL(_ZdlPvj), // operator delete(void*, unsigned int) DEFINE_MODULE_SYMBOL(_Znaj), // operator new[](unsigned int) + DEFINE_MODULE_SYMBOL(_ZnajRKSt9nothrow_t), // operator new[](unsigned int, std::nothrow_t const&) DEFINE_MODULE_SYMBOL(_ZdaPvj), // operator delete[](void*, unsigned int) DEFINE_MODULE_SYMBOL(_ZdlPv), // operator delete(void*) DEFINE_MODULE_SYMBOL(_ZdaPv), // operator delete[](void*) diff --git a/Modules/lvgl-module/generate-icons.py b/Modules/lvgl-module/generate-icons.py index 6712aa99c..3bfad102c 100755 --- a/Modules/lvgl-module/generate-icons.py +++ b/Modules/lvgl-module/generate-icons.py @@ -149,8 +149,10 @@ def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_nam "download", "forum", # Chat app "gamepad", + "hard_disk", # System Info "help", # Diceware help "hub", # App Hub + "info", # System Info "image", # Screenshot app "keyboard_arrow_up", "lightbulb", @@ -158,6 +160,7 @@ def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_nam "lists", # Chat app toolbar "mail", "menu", + "memory", # System Info "mop", "more_vert", "music_note", @@ -166,6 +169,7 @@ def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_nam "power_settings_new", # Power off for T-Lora Pager "refresh", # e.g. App Hub reload button "search", + "select_window_2", # System Info "settings", "toolbar", # Apps without custom icon "navigation", # GPS (settings) app @@ -177,6 +181,7 @@ def generate_icon_names(codepoint_map: dict, codepoint_names: list, variable_nam "logo_dev", "camera", "lightstrip", + "phone_android", # Display settings: orientation ] # Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded diff --git a/Modules/lvgl-module/include/lvgl/icons/names.h b/Modules/lvgl-module/include/lvgl/icons/names.h index 027f87720..fabc14950 100644 --- a/Modules/lvgl-module/include/lvgl/icons/names.h +++ b/Modules/lvgl-module/include/lvgl/icons/names.h @@ -9,7 +9,7 @@ extern "C" { #endif /** Changes whenever the icon TTF subset, its codepoints file or the icon name lists change */ -#define LVGL_ICON_FONT_VERSION 0x1B608834u +#define LVGL_ICON_FONT_VERSION 0xC28E1A92u // Icon names per icon font, resolved through the .codepoints file next to the TTF extern const char* const lvgl_icon_shared_names[]; diff --git a/Modules/lvgl-module/include/lvgl/icons/shared.h b/Modules/lvgl-module/include/lvgl/icons/shared.h index 19b8e2399..e9d473a02 100644 --- a/Modules/lvgl-module/include/lvgl/icons/shared.h +++ b/Modules/lvgl-module/include/lvgl/icons/shared.h @@ -22,8 +22,10 @@ #define LVGL_ICON_SHARED_DOWNLOAD "\xEF\x82\x90" #define LVGL_ICON_SHARED_FORUM "\xEE\xA2\xAF" #define LVGL_ICON_SHARED_GAMEPAD "\xEE\x8C\x8F" +#define LVGL_ICON_SHARED_HARD_DISK "\xEF\x8F\x9A" #define LVGL_ICON_SHARED_HELP "\xEE\xA3\xBD" #define LVGL_ICON_SHARED_HUB "\xEE\xA7\xB4" +#define LVGL_ICON_SHARED_INFO "\xEE\xA2\x8E" #define LVGL_ICON_SHARED_IMAGE "\xEE\x8F\xB4" #define LVGL_ICON_SHARED_KEYBOARD_ARROW_UP "\xEE\x8C\x96" #define LVGL_ICON_SHARED_LIGHTBULB "\xEE\xA4\x8F" @@ -31,6 +33,7 @@ #define LVGL_ICON_SHARED_LISTS "\xEE\xA6\xB9" #define LVGL_ICON_SHARED_MAIL "\xEE\x85\x99" #define LVGL_ICON_SHARED_MENU "\xEE\x97\x92" +#define LVGL_ICON_SHARED_MEMORY "\xEE\x8C\xA2" #define LVGL_ICON_SHARED_MOP "\xEE\x8A\x8D" #define LVGL_ICON_SHARED_MORE_VERT "\xEE\x97\x94" #define LVGL_ICON_SHARED_MUSIC_NOTE "\xEE\x90\x85" @@ -39,6 +42,7 @@ #define LVGL_ICON_SHARED_POWER_SETTINGS_NEW "\xEF\xA3\x87" #define LVGL_ICON_SHARED_REFRESH "\xEE\x97\x95" #define LVGL_ICON_SHARED_SEARCH "\xEE\xBD\xBA" +#define LVGL_ICON_SHARED_SELECT_WINDOW_2 "\xEF\x93\x88" #define LVGL_ICON_SHARED_SETTINGS "\xEE\xA2\xB8" #define LVGL_ICON_SHARED_TOOLBAR "\xEE\xA7\xB7" #define LVGL_ICON_SHARED_NAVIGATION "\xEE\x95\x9D" @@ -50,3 +54,4 @@ #define LVGL_ICON_SHARED_LOGO_DEV "\xEE\xAB\x96" #define LVGL_ICON_SHARED_CAMERA "\xEE\x8E\xAF" #define LVGL_ICON_SHARED_LIGHTSTRIP "\xF3\xBF\xBD\xB5" +#define LVGL_ICON_SHARED_PHONE_ANDROID "\xEF\x8B\x9B" diff --git a/Modules/lvgl-module/include/lvgl/lvgl.h b/Modules/lvgl-module/include/lvgl/lvgl.h index fee5541f7..8e1f0572f 100644 --- a/Modules/lvgl-module/include/lvgl/lvgl.h +++ b/Modules/lvgl-module/include/lvgl/lvgl.h @@ -49,11 +49,11 @@ void lvgl_unlock(void); bool lvgl_is_running(void); /** - * @brief Gets the desired UI density for the target hardware. - * The density is defined in the `device.properties` of a hardware device. - * This setting is read by CMakeLists.txt and passed as a target compile definition of the LVGL module. + * @brief Gets the UI density of the theme settings. + * @deprecated Use lvgl_theme_is_compact() from * @return the UI density */ +__attribute__((deprecated("Use lvgl_theme_is_compact()"))) enum UiDensity lvgl_get_ui_density(void); #ifdef __cplusplus diff --git a/Modules/lvgl-module/include/lvgl/theme.h b/Modules/lvgl-module/include/lvgl/theme.h index d22620945..01e1b8a4b 100644 --- a/Modules/lvgl-module/include/lvgl/theme.h +++ b/Modules/lvgl-module/include/lvgl/theme.h @@ -17,6 +17,11 @@ typedef struct { lv_color_t color_primary; lv_color_t color_secondary; lv_color_t color_error; + /** Smaller spacing and controls, for small screens (see lvgl_theme_is_compact()) */ + bool is_compact; + /** Use color_surface as the surface (screen background) colour, instead of the theme's own. Not used by the monochrome theme. */ + bool surface_override; + lv_color_t color_surface; } LvglThemeSettings; /** @@ -37,6 +42,13 @@ void lvgl_theme_get_default_settings(LvglThemeSettings* settings); */ bool lvgl_theme_is_mono(void); +/** + * @brief Checks whether the theme settings use the compact UI density: smaller spacing and controls, for small screens. + * The device's default density is defined in its `device.properties`. + * @return true when the UI is compact + */ +bool lvgl_theme_is_compact(void); + /** * @brief Sets the theme settings. They are used for displays that are added afterwards, e.g. when LVGL (re)starts. * @param[in] settings the theme settings diff --git a/Modules/lvgl-module/source/grid_navigation.cpp b/Modules/lvgl-module/source/grid_navigation.cpp index ec0136b6b..cd1eba814 100644 --- a/Modules/lvgl-module/source/grid_navigation.cpp +++ b/Modules/lvgl-module/source/grid_navigation.cpp @@ -19,15 +19,115 @@ static void on_child_created(lv_event_t* event) { } } +// Matches which children grid navigation can focus +static bool is_focusable(lv_obj_t* obj) { + return !lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN) && lv_obj_has_flag(obj, static_cast(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE)); +} + +// Finds the next focusable child after (or before) the given index +static lv_obj_t* find_focusable_child(lv_obj_t* container, int32_t index, bool next) { + const auto child_count = static_cast(lv_obj_get_child_count(container)); + for (index += next ? 1 : -1; index >= 0 && index < child_count; index += next ? 1 : -1) { + auto* child = lv_obj_get_child(container, index); + if (is_focusable(child)) { + return child; + } + } + return nullptr; +} + +static int32_t get_center_x(lv_obj_t* obj) { + lv_area_t area; + lv_obj_get_coords(obj, &area); + return area.x1 + lv_area_get_width(&area) / 2; +} + +static int32_t get_center_y(lv_obj_t* obj) { + lv_area_t area; + lv_obj_get_coords(obj, &area); + return area.y1 + lv_area_get_height(&area) / 2; +} + +// Finds the child in the top (or bottom) row that's horizontally closest to the reference child. +// The reference child is returned when it's in that row already. +static lv_obj_t* find_edge_row_child(lv_obj_t* container, bool top, lv_obj_t* reference) { + const uint32_t child_count = lv_obj_get_child_count(container); + bool found = false; + int32_t edge_y = 0; + for (uint32_t i = 0; i < child_count; i++) { + auto* child = lv_obj_get_child(container, static_cast(i)); + if (is_focusable(child)) { + const int32_t y = get_center_y(child); + if (!found || (top ? y < edge_y : y > edge_y)) { + edge_y = y; + found = true; + } + } + } + if (!found) { + return nullptr; + } + if (reference != nullptr && get_center_y(reference) == edge_y) { + return reference; + } + const int32_t reference_x = reference != nullptr ? get_center_x(reference) : 0; + lv_obj_t* closest = nullptr; + int32_t closest_distance = 0; + for (uint32_t i = 0; i < child_count; i++) { + auto* child = lv_obj_get_child(container, static_cast(i)); + if (is_focusable(child) && get_center_y(child) == edge_y) { + const int32_t distance = LV_ABS(get_center_x(child) - reference_x); + if (closest == nullptr || distance < closest_distance) { + closest = child; + closest_distance = distance; + } + } + } + return closest; +} + +// Grid navigation only scrolls the container itself, which isn't scrollable when e.g. it's part of a scrolling page +static void scroll_to_focused_child(lv_obj_t* container) { + lv_obj_t* child = lvgl_grid_navigation_get_focused(container); + if (child != nullptr) { + lv_obj_scroll_to_view_recursive(child, LV_ANIM_ON); + } +} + +static void on_container_key(lv_event_t* event) { + scroll_to_focused_child(lv_event_get_current_target_obj(event)); +} + // Grid navigation shows the focused child as selected whenever the container gets the focus. That's only // right when a key moved the focus there, not when it moved because e.g. the previously focused widget was deleted. static void on_container_focused(lv_event_t* event) { lv_indev_t* indev = lv_indev_active(); const lv_indev_type_t type = indev != nullptr ? lv_indev_get_type(indev) : LV_INDEV_TYPE_NONE; - if (type == LV_INDEV_TYPE_KEYPAD || type == LV_INDEV_TYPE_ENCODER) { + lv_obj_t* container = lv_event_get_current_target_obj(event); + if (type == LV_INDEV_TYPE_KEYPAD) { + // Grid navigation restores the child that was focused last, but keys enter at the side they came from: + // up and down enter at the bottom or top row, the other keys at the last or first child + const uint32_t key = lv_indev_get_key(indev); + lv_obj_t* focused = lvgl_grid_navigation_get_focused(container); + lv_obj_t* child = nullptr; + if (key == LV_KEY_DOWN || key == LV_KEY_UP) { + child = find_edge_row_child(container, key == LV_KEY_DOWN, focused); + } else if (key == LV_KEY_NEXT || key == LV_KEY_RIGHT) { + child = find_focusable_child(container, -1, true); + } else if (key == LV_KEY_PREV || key == LV_KEY_LEFT) { + child = find_focusable_child(container, static_cast(lv_obj_get_child_count(container)), false); + } + if (child != nullptr && child != focused) { + lv_gridnav_set_focused(container, child, LV_ANIM_ON); + } + scroll_to_focused_child(container); return; } - lv_obj_t* child = lvgl_grid_navigation_get_focused(lv_event_get_current_target_obj(event)); + if (type == LV_INDEV_TYPE_ENCODER) { + scroll_to_focused_child(container); + return; + } + lv_obj_t* child = lvgl_grid_navigation_get_focused(container); if (child != nullptr) { lv_obj_remove_state(child, LV_STATE_FOCUS_KEY); } @@ -37,6 +137,7 @@ void lvgl_grid_navigation_add(lv_obj_t* container) { lv_gridnav_add(container, LV_GRIDNAV_CTRL_NONE); // Added after grid navigation's own handler, so it runs after it lv_obj_add_event_cb(container, on_container_focused, LV_EVENT_FOCUSED, nullptr); + lv_obj_add_event_cb(container, on_container_key, LV_EVENT_KEY, nullptr); lv_obj_add_flag(container, GRID_NAVIGATION_FLAG); const uint32_t child_count = lv_obj_get_child_count(container); @@ -62,6 +163,7 @@ void lvgl_grid_navigation_remove(lv_obj_t* container) { lv_gridnav_remove(container); lv_obj_remove_event_cb(container, on_child_created); lv_obj_remove_event_cb(container, on_container_focused); + lv_obj_remove_event_cb(container, on_container_key); lv_obj_remove_flag(container, GRID_NAVIGATION_FLAG); if (group == nullptr) { @@ -80,23 +182,6 @@ void lvgl_grid_navigation_remove(lv_obj_t* container) { } } -// Matches which children grid navigation can focus -static bool is_focusable(lv_obj_t* obj) { - return !lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN) && lv_obj_has_flag(obj, static_cast(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE)); -} - -// Finds the next focusable child after (or before) the given index -static lv_obj_t* find_focusable_child(lv_obj_t* container, int32_t index, bool next) { - const auto child_count = static_cast(lv_obj_get_child_count(container)); - for (index += next ? 1 : -1; index >= 0 && index < child_count; index += next ? 1 : -1) { - auto* child = lv_obj_get_child(container, index); - if (is_focusable(child)) { - return child; - } - } - return nullptr; -} - void lvgl_grid_navigation_step(lv_group_t* group, bool next) { lv_obj_t* focused = lv_group_get_focused(group); if (lvgl_grid_navigation_is_container(focused)) { @@ -105,6 +190,7 @@ void lvgl_grid_navigation_step(lv_group_t* group, bool next) { lv_obj_t* child = find_focusable_child(focused, index, next); if (child != nullptr) { lv_gridnav_set_focused(focused, child, LV_ANIM_ON); + scroll_to_focused_child(focused); return; } } @@ -123,6 +209,7 @@ void lvgl_grid_navigation_step(lv_group_t* group, bool next) { lv_obj_t* child = find_focusable_child(entered, start, next); if (child != nullptr) { lv_gridnav_set_focused(entered, child, LV_ANIM_ON); + scroll_to_focused_child(entered); } } } diff --git a/Modules/lvgl-module/source/icon_names.c b/Modules/lvgl-module/source/icon_names.c index 34b143de0..af585e7fa 100644 --- a/Modules/lvgl-module/source/icon_names.c +++ b/Modules/lvgl-module/source/icon_names.c @@ -24,8 +24,10 @@ const char* const lvgl_icon_shared_names[] = { "download", "forum", "gamepad", + "hard_disk", "help", "hub", + "info", "image", "keyboard_arrow_up", "lightbulb", @@ -33,6 +35,7 @@ const char* const lvgl_icon_shared_names[] = { "lists", "mail", "menu", + "memory", "mop", "more_vert", "music_note", @@ -41,6 +44,7 @@ const char* const lvgl_icon_shared_names[] = { "power_settings_new", "refresh", "search", + "select_window_2", "settings", "toolbar", "navigation", @@ -52,6 +56,7 @@ const char* const lvgl_icon_shared_names[] = { "logo_dev", "camera", "lightstrip", + "phone_android", }; const size_t lvgl_icon_shared_name_count = sizeof(lvgl_icon_shared_names) / sizeof(lvgl_icon_shared_names[0]); diff --git a/Modules/lvgl-module/source/lvgl.c b/Modules/lvgl-module/source/lvgl.c index 0a4784efa..19310983b 100644 --- a/Modules/lvgl-module/source/lvgl.c +++ b/Modules/lvgl-module/source/lvgl.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include enum UiDensity lvgl_get_ui_density(void) { - return TT_LVGL_UI_DENSITY; + return lvgl_theme_is_compact() ? LVGL_UI_DENSITY_COMPACT : LVGL_UI_DENSITY_DEFAULT; } diff --git a/Modules/lvgl-module/source/symbols.c b/Modules/lvgl-module/source/symbols.c index 13352ec16..38adb2012 100644 --- a/Modules/lvgl-module/source/symbols.c +++ b/Modules/lvgl-module/source/symbols.c @@ -81,6 +81,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = { DEFINE_MODULE_SYMBOL(lvgl_theme_get_default_settings), DEFINE_MODULE_SYMBOL(lvgl_theme_set_settings), DEFINE_MODULE_SYMBOL(lvgl_theme_is_mono), + DEFINE_MODULE_SYMBOL(lvgl_theme_is_compact), // lvgl_badge DEFINE_MODULE_SYMBOL(lvgl_badge_create), DEFINE_MODULE_SYMBOL(lvgl_page_indicator_create), diff --git a/Modules/lvgl-module/source/theme.cpp b/Modules/lvgl-module/source/theme.cpp index 80090ffad..e5fc4dc74 100644 --- a/Modules/lvgl-module/source/theme.cpp +++ b/Modules/lvgl-module/source/theme.cpp @@ -29,6 +29,9 @@ void lvgl_theme_get_default_settings(LvglThemeSettings* settings) { settings->color_primary = material_config.color_primary; settings->color_secondary = material_config.color_secondary; settings->color_error = material_config.color_error; + settings->is_compact = TT_LVGL_UI_DENSITY == LVGL_UI_DENSITY_COMPACT; + settings->surface_override = material_config.surface_override; + settings->color_surface = material_config.color_surface; } void lvgl_theme_get_settings(LvglThemeSettings* settings) { @@ -48,6 +51,12 @@ bool lvgl_theme_is_mono() { return active_theme == ActiveTheme::MaterialMono; } +bool lvgl_theme_is_compact() { + LvglThemeSettings settings; + lvgl_theme_get_settings(&settings); + return settings.is_compact; +} + static lv_theme_t* get_active_theme() { switch (active_theme) { case ActiveTheme::Material: @@ -76,7 +85,7 @@ void lvgl_theme_deinit() { lv_theme_t* lvgl_theme_init_for_display(lv_display_t* display, bool isMonoDisplay, bool supportsAnimations) { LvglThemeSettings settings; lvgl_theme_get_settings(&settings); - const bool is_compact = lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT; + const bool is_compact = settings.is_compact; const bool animations_enabled = settings.animations_enabled && supportsAnimations; const ActiveTheme required_theme = (isMonoDisplay || settings.is_mono) ? ActiveTheme::MaterialMono : ActiveTheme::Material; @@ -104,6 +113,8 @@ lv_theme_t* lvgl_theme_init_for_display(lv_display_t* display, bool isMonoDispla config.color_primary = settings.color_primary; config.color_secondary = settings.color_secondary; config.color_error = settings.color_error; + config.surface_override = settings.surface_override; + config.color_surface = settings.color_surface; theme = lv_theme_material_init(display, &config); } active_theme = required_theme; diff --git a/Modules/lvgl-module/source/themes/lv_theme_material.c b/Modules/lvgl-module/source/themes/lv_theme_material.c index 7b4ec65bf..3d1a4cce1 100644 --- a/Modules/lvgl-module/source/themes/lv_theme_material.c +++ b/Modules/lvgl-module/source/themes/lv_theme_material.c @@ -71,14 +71,17 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class; #define COLOR_ON_SECONDARY_CONTAINER (theme->config.is_dark ? lv_color_mix(theme->config.color_secondary, lv_color_white(), LV_OPA_20) : lv_color_mix(theme->config.color_secondary, lv_color_black(), LV_OPA_60)) #define COLOR_ERROR (theme->config.is_dark ? lv_color_mix(theme->config.color_error, lv_color_white(), 140) : theme->config.color_error) #define COLOR_ON_ERROR (theme->config.is_dark ? lv_color_mix(theme->config.color_error, lv_color_black(), LV_OPA_50) : lv_color_white()) -#define COLOR_SURFACE (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_6 : NEUTRAL_98, TINT_NEUTRAL)) +#define DARK_BACKGROUND lv_color_hex(0x21222B) +#define COLOR_SURFACE (theme->config.surface_override ? theme->config.color_surface : theme->config.is_dark ? lv_color_mix(lv_color_hex(0x000000), DARK_BACKGROUND, LV_OPA_20) : lv_color_mix(theme->config.color_primary, NEUTRAL_98, TINT_NEUTRAL)) #define COLOR_ON_SURFACE (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_90 : NEUTRAL_10, TINT_NEUTRAL)) #define COLOR_ON_SURFACE_VARIANT (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_80 : NEUTRAL_30, TINT_NEUTRAL_VARIANT)) -#define COLOR_SURFACE_CONTAINER_LOWEST (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_4 : NEUTRAL_100, TINT_NEUTRAL)) -#define COLOR_SURFACE_CONTAINER_LOW (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_10 : NEUTRAL_96, TINT_NEUTRAL)) -#define COLOR_SURFACE_CONTAINER (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_12 : NEUTRAL_94, TINT_NEUTRAL)) -#define COLOR_SURFACE_CONTAINER_HIGH (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_17 : NEUTRAL_92, TINT_NEUTRAL)) -#define COLOR_SURFACE_CONTAINER_HIGHEST (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_22 : NEUTRAL_90, TINT_NEUTRAL)) +#define SURFACE_LIFT (theme->config.is_dark ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x000000)) +#define SURFACE_SINK (theme->config.is_dark ? lv_color_hex(0x000000) : lv_color_hex(0xFFFFFF)) +#define COLOR_SURFACE_CONTAINER_LOWEST (theme->config.surface_override ? lv_color_mix(SURFACE_SINK, theme->config.color_surface, 64) : theme->config.is_dark ? lv_color_mix(lv_color_hex(0x000000), DARK_BACKGROUND, 89) : lv_color_mix(theme->config.color_primary, NEUTRAL_100, TINT_NEUTRAL)) +#define COLOR_SURFACE_CONTAINER_LOW (theme->config.surface_override ? lv_color_mix(SURFACE_LIFT, theme->config.color_surface, theme->config.is_dark ? 10 : 5) : theme->config.is_dark ? DARK_BACKGROUND : lv_color_mix(theme->config.color_primary, NEUTRAL_96, TINT_NEUTRAL)) +#define COLOR_SURFACE_CONTAINER (theme->config.surface_override ? lv_color_mix(SURFACE_LIFT, theme->config.color_surface, theme->config.is_dark ? 15 : 10) : theme->config.is_dark ? lv_color_mix(lv_color_hex(0xFFFFFF), DARK_BACKGROUND, 8) : lv_color_mix(theme->config.color_primary, NEUTRAL_94, TINT_NEUTRAL)) +#define COLOR_SURFACE_CONTAINER_HIGH (theme->config.surface_override ? lv_color_mix(SURFACE_LIFT, theme->config.color_surface, theme->config.is_dark ? 28 : 15) : theme->config.is_dark ? lv_color_mix(lv_color_hex(0xFFFFFF), DARK_BACKGROUND, 20) : lv_color_mix(theme->config.color_primary, NEUTRAL_92, TINT_NEUTRAL)) +#define COLOR_SURFACE_CONTAINER_HIGHEST (theme->config.surface_override ? lv_color_mix(SURFACE_LIFT, theme->config.color_surface, theme->config.is_dark ? 41 : 20) : theme->config.is_dark ? lv_color_mix(lv_color_hex(0xFFFFFF), DARK_BACKGROUND, 33) : lv_color_mix(theme->config.color_primary, NEUTRAL_90, TINT_NEUTRAL)) #define COLOR_OUTLINE (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_60 : NEUTRAL_50, TINT_NEUTRAL_VARIANT)) #define COLOR_OUTLINE_VARIANT (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_30 : NEUTRAL_80, TINT_NEUTRAL_VARIANT)) #define COLOR_INVERSE_SURFACE (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_90 : NEUTRAL_20, TINT_NEUTRAL)) @@ -140,8 +143,8 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class; #define CHIP_PAD_HOR (theme->config.is_compact ? 3 : SPACE_4) #define CHIP_PAD_VER (theme->config.is_compact ? 2 : LV_DPX_CALC(theme->disp_dpi, 6)) #define PAGE_INDICATOR_COLOR COLOR_ON_SURFACE_VARIANT -#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6)) -#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 3 : LV_DPX_CALC(theme->disp_dpi, 6)) +#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 6 : LV_DPX_CALC(theme->disp_dpi, 8)) +#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6)) #define BADGE_COLOR COLOR_SECONDARY #define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6)) #define SCROLLBAR_COLOR COLOR_OUTLINE @@ -174,8 +177,8 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class; #define ICON_BUTTON_TONAL_COLOR COLOR_SECONDARY_CONTAINER #define ICON_BUTTON_TONAL_ICON_COLOR COLOR_ON_SECONDARY_CONTAINER #define ICON_BUTTON_TONAL_PRESSED_COLOR (lv_color_mix(COLOR_ON_SECONDARY_CONTAINER, COLOR_SECONDARY_CONTAINER, OPACITY_STATE_PRESSED)) -#define APP_BAR_COLOR (lv_color_mix(theme->config.color_primary, NEUTRAL_22, TINT_NEUTRAL)) -#define APP_BAR_CONTENT_COLOR lv_color_white() +#define APP_BAR_COLOR COLOR_SURFACE_CONTAINER_LOW +#define APP_BAR_CONTENT_COLOR COLOR_ON_SURFACE #define APP_BAR_PRESSED_OPACITY (LV_OPA_20) #define APP_BAR_PAD_HOR (theme->config.is_compact ? 0 : SPACE_1) #define APP_BAR_GAP (theme->config.is_compact ? 2 : SPACE_1) @@ -270,11 +273,14 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class; #define LIST_DIVIDER_COLOR COLOR_OUTLINE_VARIANT #define LIST_DIVIDER_WIDTH DENSITY_BORDER #define LIST_SUBHEADER_TEXT_COLOR COLOR_ON_SURFACE_VARIANT -#define TABS_DIVIDER_COLOR COLOR_OUTLINE_VARIANT +#define TABS_COLOR CARD_COLOR +#define TABS_PAD (theme->config.is_compact ? 2 : SPACE_1) +#define TABS_GAP (theme->config.is_compact ? 2 : SPACE_1) #define TAB_LABEL_COLOR COLOR_ON_SURFACE_VARIANT -#define TAB_SELECTED_LABEL_COLOR COLOR_PRIMARY -#define TAB_INDICATOR_COLOR COLOR_PRIMARY -#define TAB_INDICATOR_WIDTH (theme->config.is_compact ? 2 : LV_DPX_CALC(theme->disp_dpi, 3)) +#define TAB_PAD (theme->config.is_compact ? 2 : SPACE_1) +#define TAB_SHAPE SHAPE_CONTROL +#define TAB_SELECTED_COLOR COLOR_SECONDARY_CONTAINER +#define TAB_SELECTED_LABEL_COLOR COLOR_ON_SECONDARY_CONTAINER #define TAB_PAGE_PAD DENSITY_CONTAINER_PAD #define DIALOG_COLOR COLOR_SURFACE_CONTAINER_HIGH #define DIALOG_TEXT_COLOR COLOR_ON_SURFACE_VARIANT @@ -658,6 +664,8 @@ static bool config_equals(const lv_theme_material_config_t * a, const lv_theme_m lv_color_eq(a->color_primary, b->color_primary) && lv_color_eq(a->color_secondary, b->color_secondary) && lv_color_eq(a->color_error, b->color_error) && + a->surface_override == b->surface_override && + lv_color_eq(a->color_surface, b->color_surface) && a->font_small == b->font_small && a->font_normal == b->font_normal && a->font_large == b->font_large; @@ -807,6 +815,7 @@ static void style_init(my_theme_t * theme) lv_style_set_radius(&theme->styles.chip, CHIP_SHAPE); lv_style_set_pad_hor(&theme->styles.chip, CHIP_PAD_HOR); lv_style_set_pad_ver(&theme->styles.chip, CHIP_PAD_VER); + lv_style_set_margin_all(&theme->styles.chip, FOCUS_WIDTH + FOCUS_OFFSET); style_init_reset(&theme->styles.chip_selected); lv_style_set_bg_color(&theme->styles.chip_selected, CHIP_SELECTED_COLOR); @@ -823,6 +832,7 @@ static void style_init(my_theme_t * theme) lv_style_set_pad_hor(&theme->styles.button, BUTTON_PAD_HOR); lv_style_set_pad_ver(&theme->styles.button, BUTTON_PAD_VER); lv_style_set_pad_gap(&theme->styles.button, BUTTON_GAP); + lv_style_set_margin_all(&theme->styles.button, FOCUS_WIDTH + FOCUS_OFFSET); #endif #if LV_USE_BUTTON @@ -923,6 +933,7 @@ static void style_init(my_theme_t * theme) style_init_reset(&theme->styles.switch_track); lv_style_set_width(&theme->styles.switch_track, SWITCH_WIDTH); lv_style_set_height(&theme->styles.switch_track, SWITCH_HEIGHT); + lv_style_set_margin_all(&theme->styles.switch_track, FOCUS_WIDTH + FOCUS_OFFSET); lv_style_set_bg_color(&theme->styles.switch_track, SWITCH_TRACK_COLOR); lv_style_set_bg_opa(&theme->styles.switch_track, LV_OPA_COVER); lv_style_set_border_color(&theme->styles.switch_track, SWITCH_TRACK_BORDER_COLOR); @@ -990,6 +1001,7 @@ static void style_init(my_theme_t * theme) style_init_reset(&theme->styles.sliderbox); lv_style_set_pad_column(&theme->styles.sliderbox, SLIDERBOX_GAP); + lv_style_set_pad_all(&theme->styles.sliderbox, FOCUS_WIDTH + FOCUS_OFFSET); #if LV_USE_BAR || LV_USE_SLIDER style_init_reset(&theme->styles.slider_track_size); @@ -1222,25 +1234,27 @@ static void style_init(my_theme_t * theme) #if LV_USE_TABVIEW style_init_reset(&theme->styles.tabs); - lv_style_set_bg_opa(&theme->styles.tabs, LV_OPA_TRANSP); - lv_style_set_border_color(&theme->styles.tabs, TABS_DIVIDER_COLOR); - lv_style_set_border_width(&theme->styles.tabs, DENSITY_BORDER); - lv_style_set_border_side(&theme->styles.tabs, LV_BORDER_SIDE_BOTTOM); + lv_style_set_bg_color(&theme->styles.tabs, TABS_COLOR); + lv_style_set_bg_opa(&theme->styles.tabs, LV_OPA_COVER); + lv_style_set_border_width(&theme->styles.tabs, 0); + lv_style_set_radius(&theme->styles.tabs, CARD_SHAPE); + lv_style_set_pad_all(&theme->styles.tabs, TABS_PAD); + lv_style_set_pad_gap(&theme->styles.tabs, TABS_GAP); #endif #if LV_USE_BUTTON && LV_USE_TABVIEW style_init_reset(&theme->styles.tab); lv_style_set_bg_opa(&theme->styles.tab, LV_OPA_TRANSP); lv_style_set_text_color(&theme->styles.tab, TAB_LABEL_COLOR); - lv_style_set_radius(&theme->styles.tab, 0); + lv_style_set_radius(&theme->styles.tab, TAB_SHAPE); + lv_style_set_pad_all(&theme->styles.tab, TAB_PAD); #endif #if LV_USE_BUTTON && LV_USE_TABVIEW style_init_reset(&theme->styles.tab_selected); + lv_style_set_bg_color(&theme->styles.tab_selected, TAB_SELECTED_COLOR); + lv_style_set_bg_opa(&theme->styles.tab_selected, LV_OPA_COVER); lv_style_set_text_color(&theme->styles.tab_selected, TAB_SELECTED_LABEL_COLOR); - lv_style_set_border_color(&theme->styles.tab_selected, TAB_INDICATOR_COLOR); - lv_style_set_border_width(&theme->styles.tab_selected, TAB_INDICATOR_WIDTH); - lv_style_set_border_side(&theme->styles.tab_selected, LV_BORDER_SIDE_BOTTOM); #endif #if LV_USE_TABVIEW @@ -1492,7 +1506,9 @@ void lv_theme_material_config_init(lv_theme_material_config_t * config) config->animations_enabled = true; config->color_primary = lv_color_hex(0x00BBFF); config->color_secondary = lv_color_hex(0xEE81C3); - config->color_error = lv_color_hex(0xB3261E); + config->color_error = lv_color_hex(0xEF6C00); + config->surface_override = false; + config->color_surface = lv_color_hex(0x000000); config->font_small = LV_FONT_DEFAULT; config->font_normal = LV_FONT_DEFAULT; config->font_large = LV_FONT_DEFAULT; @@ -1841,6 +1857,8 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj) lv_obj_add_style(obj, &theme->styles.page_indicator_label, 0); return; } + /* label */ + lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY); return; } #endif diff --git a/Modules/lvgl-module/source/themes/lv_theme_material.h b/Modules/lvgl-module/source/themes/lv_theme_material.h index 42aa69f78..182d2bbcf 100644 --- a/Modules/lvgl-module/source/themes/lv_theme_material.h +++ b/Modules/lvgl-module/source/themes/lv_theme_material.h @@ -16,6 +16,8 @@ typedef struct { lv_color_t color_primary; lv_color_t color_secondary; lv_color_t color_error; + bool surface_override; + lv_color_t color_surface; const lv_font_t * font_small; const lv_font_t * font_normal; const lv_font_t * font_large; diff --git a/Modules/lvgl-module/source/themes/lv_theme_material_mono.c b/Modules/lvgl-module/source/themes/lv_theme_material_mono.c index fcd53c2ce..0f333560a 100644 --- a/Modules/lvgl-module/source/themes/lv_theme_material_mono.c +++ b/Modules/lvgl-module/source/themes/lv_theme_material_mono.c @@ -90,8 +90,8 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class; #define CHIP_PAD_HOR (theme->config.is_compact ? 3 : SPACE_4) #define CHIP_PAD_VER (theme->config.is_compact ? 2 : SPACE_2) #define PAGE_INDICATOR_COLOR COLOR_ON_SURFACE -#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6)) -#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 3 : LV_DPX_CALC(theme->disp_dpi, 6)) +#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 6 : LV_DPX_CALC(theme->disp_dpi, 8)) +#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6)) #define BADGE_COLOR COLOR_ON_SURFACE #define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6)) #define SCROLLBAR_COLOR COLOR_ON_SURFACE @@ -114,7 +114,6 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class; #define ICON_BUTTON_TONAL_ICON_COLOR COLOR_ON_SURFACE #define ICON_BUTTON_TONAL_BORDER_COLOR COLOR_OUTLINE #define APP_BAR_TITLE_COLOR COLOR_ON_SURFACE -#define APP_BAR_DIVIDER_COLOR COLOR_OUTLINE #define APP_BAR_PAD_HOR (theme->config.is_compact ? 0 : SPACE_1) #define APP_BAR_GAP (theme->config.is_compact ? 2 : SPACE_1) #define APP_BAR_FOCUS_WIDTH FOCUS_INSET_WIDTH @@ -181,9 +180,10 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class; #define LIST_ITEM_PAD_VER DENSITY_LIST_ITEM_PAD_VER #define LIST_ITEM_GAP (theme->config.is_compact ? 3 : SPACE_4) #define LIST_DIVIDER_COLOR COLOR_OUTLINE -#define TABS_DIVIDER_COLOR COLOR_OUTLINE -#define TAB_INDICATOR_COLOR COLOR_PRIMARY -#define TAB_INDICATOR_WIDTH (theme->config.is_compact ? LINE_MEDIUM : LINE_THICK) +#define TABS_PAD (theme->config.is_compact ? 2 : SPACE_1) +#define TABS_GAP (theme->config.is_compact ? 2 : SPACE_1) +#define TAB_PAD (theme->config.is_compact ? 2 : SPACE_1) +#define TAB_SHAPE SHAPE_CONTROL #define TAB_PAGE_PAD DENSITY_CONTAINER_PAD #define DIALOG_COLOR COLOR_SURFACE #define DIALOG_BORDER_COLOR COLOR_OUTLINE @@ -327,9 +327,6 @@ typedef struct { #if LV_USE_BUTTON && LV_USE_TABVIEW lv_style_t tab; #endif -#if LV_USE_BUTTON && LV_USE_TABVIEW - lv_style_t tab_selected; -#endif #if LV_USE_TABVIEW lv_style_t tab_page; #endif @@ -598,6 +595,7 @@ static void style_init(my_theme_t * theme) lv_style_set_radius(&theme->styles.chip, CHIP_SHAPE); lv_style_set_pad_hor(&theme->styles.chip, CHIP_PAD_HOR); lv_style_set_pad_ver(&theme->styles.chip, CHIP_PAD_VER); + lv_style_set_margin_all(&theme->styles.chip, FOCUS_WIDTH + FOCUS_OFFSET); #if LV_USE_BUTTON || LV_USE_MSGBOX style_init_reset(&theme->styles.button); @@ -610,6 +608,7 @@ static void style_init(my_theme_t * theme) lv_style_set_pad_hor(&theme->styles.button, BUTTON_PAD_HOR); lv_style_set_pad_ver(&theme->styles.button, BUTTON_PAD_VER); lv_style_set_pad_gap(&theme->styles.button, BUTTON_GAP); + lv_style_set_margin_all(&theme->styles.button, FOCUS_WIDTH + FOCUS_OFFSET); #endif style_init_reset(&theme->styles.icon_button); @@ -646,9 +645,7 @@ static void style_init(my_theme_t * theme) lv_style_set_bg_opa(&theme->styles.app_bar, LV_OPA_TRANSP); lv_style_set_text_color(&theme->styles.app_bar, APP_BAR_TITLE_COLOR); lv_style_set_radius(&theme->styles.app_bar, 0); - lv_style_set_border_color(&theme->styles.app_bar, APP_BAR_DIVIDER_COLOR); - lv_style_set_border_width(&theme->styles.app_bar, BORDER); - lv_style_set_border_side(&theme->styles.app_bar, LV_BORDER_SIDE_BOTTOM); + lv_style_set_border_width(&theme->styles.app_bar, 0); lv_style_set_pad_hor(&theme->styles.app_bar, APP_BAR_PAD_HOR); lv_style_set_pad_ver(&theme->styles.app_bar, 0); lv_style_set_pad_gap(&theme->styles.app_bar, APP_BAR_GAP); @@ -666,6 +663,7 @@ static void style_init(my_theme_t * theme) style_init_reset(&theme->styles.switch_track); lv_style_set_width(&theme->styles.switch_track, SWITCH_WIDTH); lv_style_set_height(&theme->styles.switch_track, SWITCH_HEIGHT); + lv_style_set_margin_all(&theme->styles.switch_track, FOCUS_WIDTH + FOCUS_OFFSET); lv_style_set_bg_color(&theme->styles.switch_track, SWITCH_TRACK_COLOR); lv_style_set_bg_opa(&theme->styles.switch_track, LV_OPA_COVER); lv_style_set_border_color(&theme->styles.switch_track, SWITCH_TRACK_BORDER_COLOR); @@ -721,6 +719,7 @@ static void style_init(my_theme_t * theme) style_init_reset(&theme->styles.sliderbox); lv_style_set_pad_column(&theme->styles.sliderbox, SLIDERBOX_GAP); + lv_style_set_pad_all(&theme->styles.sliderbox, FOCUS_WIDTH + FOCUS_OFFSET); #if LV_USE_BAR || LV_USE_SLIDER style_init_reset(&theme->styles.slider_track_size); @@ -897,9 +896,9 @@ static void style_init(my_theme_t * theme) #if LV_USE_TABVIEW style_init_reset(&theme->styles.tabs); lv_style_set_bg_opa(&theme->styles.tabs, LV_OPA_TRANSP); - lv_style_set_border_color(&theme->styles.tabs, TABS_DIVIDER_COLOR); - lv_style_set_border_width(&theme->styles.tabs, BORDER); - lv_style_set_border_side(&theme->styles.tabs, LV_BORDER_SIDE_BOTTOM); + lv_style_set_border_width(&theme->styles.tabs, 0); + lv_style_set_pad_all(&theme->styles.tabs, TABS_PAD); + lv_style_set_pad_gap(&theme->styles.tabs, TABS_GAP); #endif #if LV_USE_BUTTON && LV_USE_TABVIEW @@ -907,14 +906,8 @@ static void style_init(my_theme_t * theme) lv_style_set_bg_opa(&theme->styles.tab, LV_OPA_TRANSP); lv_style_set_text_color(&theme->styles.tab, COLOR_ON_SURFACE); lv_style_set_border_width(&theme->styles.tab, 0); - lv_style_set_radius(&theme->styles.tab, 0); -#endif - -#if LV_USE_BUTTON && LV_USE_TABVIEW - style_init_reset(&theme->styles.tab_selected); - lv_style_set_border_color(&theme->styles.tab_selected, TAB_INDICATOR_COLOR); - lv_style_set_border_width(&theme->styles.tab_selected, TAB_INDICATOR_WIDTH); - lv_style_set_border_side(&theme->styles.tab_selected, LV_BORDER_SIDE_BOTTOM); + lv_style_set_radius(&theme->styles.tab, TAB_SHAPE); + lv_style_set_pad_all(&theme->styles.tab, TAB_PAD); #endif #if LV_USE_TABVIEW @@ -1306,7 +1299,7 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj) if(lv_obj_check_type(lss_ancestor(obj, 2), &lv_tabview_class) && lss_is_child(lss_ancestor(obj, 2), 0, parent)) { lv_obj_add_style(obj, &theme->styles.tab, 0); lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_PRESSED); - lv_obj_add_style(obj, &theme->styles.tab_selected, LV_STATE_CHECKED); + lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_CHECKED); lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY); lv_obj_add_style(obj, &theme->styles.edit_ring, LV_STATE_EDITED); return; @@ -1440,6 +1433,8 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj) lv_obj_add_style(obj, &theme->styles.page_indicator_label, 0); return; } + /* label */ + lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY); return; } #endif diff --git a/Modules/lvgl-module/source/widgets/sliderbox.cpp b/Modules/lvgl-module/source/widgets/sliderbox.cpp index ec4301431..c9b61a945 100644 --- a/Modules/lvgl-module/source/widgets/sliderbox.cpp +++ b/Modules/lvgl-module/source/widgets/sliderbox.cpp @@ -38,7 +38,6 @@ static void sliderbox_constructor(const lv_obj_class_t* classPointer, lv_obj_t* lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE); 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_border_width(obj, 0, LV_STATE_DEFAULT); lv_obj_set_style_bg_opa(obj, 0, LV_STATE_DEFAULT); } diff --git a/Modules/lvgl-module/source/widgets/toolbar.cpp b/Modules/lvgl-module/source/widgets/toolbar.cpp index 4c69bd7b1..3ed6c2478 100644 --- a/Modules/lvgl-module/source/widgets/toolbar.cpp +++ b/Modules/lvgl-module/source/widgets/toolbar.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -16,16 +17,16 @@ constexpr auto* TAG = "lvgl_toolbar"; -static uint32_t getToolbarHeight(UiDensity uiDensity) { - if (uiDensity == LVGL_UI_DENSITY_COMPACT) { +static uint32_t getToolbarHeight(bool isCompact) { + if (isCompact) { return lvgl_get_text_font_height(FONT_SIZE_DEFAULT) * 1.6f; } else { return lvgl_get_text_font_height(FONT_SIZE_LARGE) * 2.2f; } } -static const _lv_font_t* getToolbarFont(UiDensity uiDensity) { - if (uiDensity == LVGL_UI_DENSITY_COMPACT) { +static const _lv_font_t* getToolbarFont(bool isCompact) { + if (isCompact) { return lvgl_get_text_font(FONT_SIZE_DEFAULT); } else { return lvgl_get_text_font(FONT_SIZE_LARGE); @@ -33,9 +34,9 @@ static const _lv_font_t* getToolbarFont(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); - auto padding = (uiDensity != LVGL_UI_DENSITY_COMPACT) ? (uint32_t)(toolbar_height * 0.2f) : 8; +static uint32_t getActionButtonSize(bool isCompact) { + auto toolbar_height = getToolbarHeight(isCompact); + auto padding = !isCompact ? (uint32_t)(toolbar_height * 0.2f) : 8; return toolbar_height - padding; } @@ -84,18 +85,18 @@ 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); +static lv_obj_t* create_action_button(lv_obj_t* parent, bool is_compact) { + auto button_size = getActionButtonSize(is_compact); 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(); + const bool is_compact = lvgl_theme_is_compact(); lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_toolbar_class, parent); lv_obj_class_init_obj(obj); - lv_obj_set_height(obj, getToolbarHeight(ui_density)); + lv_obj_set_height(obj, getToolbarHeight(is_compact)); auto* toolbar = reinterpret_cast(obj); toolbar->nav_action_callback = nullptr; @@ -109,12 +110,12 @@ lv_obj_t* lvgl_toolbar_create(lv_obj_t* parent, const char* title) { // The arrow keys move between the close button and the actions, and up and down leave the toolbar lvgl_grid_navigation_add(obj); - toolbar->close_button = create_action_button(obj, ui_density); + toolbar->close_button = create_action_button(obj, is_compact); toolbar->close_button_image = lv_image_create(toolbar->close_button); lv_obj_align(toolbar->close_button_image, LV_ALIGN_CENTER, 0, 0); toolbar->title_label = lv_label_create(obj); - lv_obj_set_style_text_font(toolbar->title_label, getToolbarFont(ui_density), LV_STATE_DEFAULT); + lv_obj_set_style_text_font(toolbar->title_label, getToolbarFont(is_compact), 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_flex_grow(toolbar->title_label, 1); @@ -155,7 +156,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++; - lv_obj_t* action_button = create_action_button(obj, lvgl_get_ui_density()); + lv_obj_t* action_button = create_action_button(obj, lvgl_theme_is_compact()); lv_obj_add_event_cb(action_button, callback, LV_EVENT_SHORT_CLICKED, user_data); lv_obj_t* button_content; if (isImage) { diff --git a/Modules/lvgl-module/themes/material.lss b/Modules/lvgl-module/themes/material.lss index 6da95531b..fe6ba42ee 100644 --- a/Modules/lvgl-module/themes/material.lss +++ b/Modules/lvgl-module/themes/material.lss @@ -14,7 +14,10 @@ animationsEnabled: bool = true; colorPrimary: color = #00bbff; colorSecondary: color = #ee81c3; - colorError: color = #B3261E; + colorError: color = #EF6C00; + // Replaces the surface (screen background) colour + surfaceOverride: bool = false; + colorSurface: color = #000000; fontSmall: font = raw("LV_FONT_DEFAULT"); fontNormal: font = raw("LV_FONT_DEFAULT"); fontLarge: font = raw("LV_FONT_DEFAULT"); @@ -108,15 +111,20 @@ @color-error: if($isDark, mix($colorError, white, 55%), $colorError); @color-on-error: if($isDark, mix($colorError, black, 50%), white); -// Surfaces: neutral tones tinted with the primary color -@color-surface: mix($colorPrimary, if($isDark, @neutral-6, @neutral-98), @tint-neutral); +// Surfaces: neutral tones tinted with the primary color. +// In dark mode, they're derived from the screen background colour (the low container tone, which apps and the window use). +@dark-background: #21222b; +@color-surface: if($surfaceOverride, $colorSurface, if($isDark, mix(#000000, @dark-background, 20%), mix($colorPrimary, @neutral-98, @tint-neutral))); @color-on-surface: mix($colorPrimary, if($isDark, @neutral-90, @neutral-10), @tint-neutral); @color-on-surface-variant: mix($colorPrimary, if($isDark, @neutral-80, @neutral-30), @tint-neutral-variant); -@color-surface-container-lowest: mix($colorPrimary, if($isDark, @neutral-4, @neutral-100), @tint-neutral); -@color-surface-container-low: mix($colorPrimary, if($isDark, @neutral-10, @neutral-96), @tint-neutral); -@color-surface-container: mix($colorPrimary, if($isDark, @neutral-12, @neutral-94), @tint-neutral); -@color-surface-container-high: mix($colorPrimary, if($isDark, @neutral-17, @neutral-92), @tint-neutral); -@color-surface-container-highest: mix($colorPrimary, if($isDark, @neutral-22, @neutral-90), @tint-neutral); +// With a surface override, the containers are the surface lifted towards the text colour, in the same steps as the neutral tones +@surface-lift: if($isDark, #FFFFFF, #000000); +@surface-sink: if($isDark, #000000, #FFFFFF); +@color-surface-container-lowest: if($surfaceOverride, mix(@surface-sink, $colorSurface, 25%), if($isDark, mix(#000000, @dark-background, 35%), mix($colorPrimary, @neutral-100, @tint-neutral))); +@color-surface-container-low: if($surfaceOverride, mix(@surface-lift, $colorSurface, if($isDark, 4%, 2%)), if($isDark, @dark-background, mix($colorPrimary, @neutral-96, @tint-neutral))); +@color-surface-container: if($surfaceOverride, mix(@surface-lift, $colorSurface, if($isDark, 6%, 4%)), if($isDark, mix(#FFFFFF, @dark-background, 3%), mix($colorPrimary, @neutral-94, @tint-neutral))); +@color-surface-container-high: if($surfaceOverride, mix(@surface-lift, $colorSurface, if($isDark, 11%, 6%)), if($isDark, mix(#FFFFFF, @dark-background, 8%), mix($colorPrimary, @neutral-92, @tint-neutral))); +@color-surface-container-highest: if($surfaceOverride, mix(@surface-lift, $colorSurface, if($isDark, 16%, 8%)), if($isDark, mix(#FFFFFF, @dark-background, 13%), mix($colorPrimary, @neutral-90, @tint-neutral))); @color-outline: mix($colorPrimary, if($isDark, @neutral-60, @neutral-50), @tint-neutral-variant); @color-outline-variant: mix($colorPrimary, if($isDark, @neutral-30, @neutral-80), @tint-neutral-variant); @color-inverse-surface: mix($colorPrimary, if($isDark, @neutral-90, @neutral-20), @tint-neutral); @@ -203,8 +211,8 @@ // Page indicator (lvgl_page_indicator_create): a dot per page, the current page's dot is filled @page-indicator-color: @color-on-surface-variant; -@page-indicator-dot-size: if($isCompact, 4, 6dp); -@page-indicator-gap: if($isCompact, 3, 6dp); +@page-indicator-dot-size: if($isCompact, 6, 8dp); +@page-indicator-gap: if($isCompact, 4, 6dp); // Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite @badge-color: @color-secondary; @@ -250,10 +258,9 @@ @icon-button-tonal-icon-color: @color-on-secondary-container; @icon-button-tonal-pressed-color: mix(@color-on-secondary-container, @color-secondary-container, @opacity-state-pressed); -// Toolbar: Material 2 top app bar, filled with a dark neutral tone for contrast with the content. -// The tone is the same in light and dark mode. The statusbar uses a darker neutral tone with the same tint. -@app-bar-color: mix($colorPrimary, @neutral-22, @tint-neutral); -@app-bar-content-color: white; +// Toolbar: Material 3 top app bar, in the same colour as the app content and the statusbar (which shows the window's container colour) +@app-bar-color: @color-surface-container-low; +@app-bar-content-color: @color-on-surface; @app-bar-pressed-opacity: 20%; @app-bar-pad-hor: if($isCompact, 0, @space-1); @app-bar-gap: if($isCompact, 2, @space-1); @@ -373,12 +380,15 @@ @list-divider-width: @density-border; @list-subheader-text-color: @color-on-surface-variant; -// Tabs: Material 3 primary tabs -@tabs-divider-color: @color-outline-variant; +// Tabs: the tab bar is a Material 3 navigation bar with a pill-shaped active indicator +@tabs-color: @card-color; +@tabs-pad: if($isCompact, 2, @space-1); +@tabs-gap: if($isCompact, 2, @space-1); @tab-label-color: @color-on-surface-variant; -@tab-selected-label-color: @color-primary; -@tab-indicator-color: @color-primary; -@tab-indicator-width: if($isCompact, 2, 3dp); +@tab-pad: if($isCompact, 2, @space-1); +@tab-shape: @shape-control; +@tab-selected-color: @color-secondary-container; +@tab-selected-label-color: @color-on-secondary-container; @tab-page-pad: @density-container-pad; // Dialog (message box) @@ -613,6 +623,8 @@ chip { radius: @chip-shape; padHor: @chip-pad-hor; padVer: @chip-pad-ver; + // Room for the focus ring, which the parent would clip + marginAll: @focus-width + @focus-offset; } chip_selected { @@ -630,6 +642,8 @@ button { padHor: @button-pad-hor; padVer: @button-pad-ver; padGap: @button-gap; + // Room for the focus ring, which the parent would clip + marginAll: @focus-width + @focus-offset; } button_pressed { @@ -733,6 +747,8 @@ app_bar_field { switch_track { width: @switch-width; height: @switch-height; + // Room for the focus ring, which the parent would clip + marginAll: @focus-width + @focus-offset; bgColor: @switch-track-color; bgOpa: cover; borderColor: @switch-track-border-color; @@ -792,6 +808,8 @@ checkbox_box_selected { sliderbox { padColumn: @sliderbox-gap; + // Room for the focus ring of the step buttons, which the sliderbox would clip + padAll: @focus-width + @focus-offset; } slider_track_size { @@ -996,23 +1014,25 @@ list_subheader { } tabs { - bgOpa: transp; - borderColor: @tabs-divider-color; - borderWidth: @density-border; - borderSide: bottom; + bgColor: @tabs-color; + bgOpa: cover; + borderWidth: 0; + radius: @card-shape; + padAll: @tabs-pad; + padGap: @tabs-gap; } tab { bgOpa: transp; textColor: @tab-label-color; - radius: 0; + radius: @tab-shape; + padAll: @tab-pad; } tab_selected { + bgColor: @tab-selected-color; + bgOpa: cover; textColor: @tab-selected-label-color; - borderColor: @tab-indicator-color; - borderWidth: @tab-indicator-width; - borderSide: bottom; } tab_page { @@ -1414,6 +1434,11 @@ apply(page_indicator > label) { page_indicator_label: default; } +// Labels can be selectable, e.g. rows that keys move through +apply(label) { + focus_ring: state.focusKey; +} + apply(page_indicator_dot) { page_indicator_dot: default; page_indicator_dot_current: state.checked; diff --git a/Modules/lvgl-module/themes/material_mono.lss b/Modules/lvgl-module/themes/material_mono.lss index 8869e5972..85cd84159 100644 --- a/Modules/lvgl-module/themes/material_mono.lss +++ b/Modules/lvgl-module/themes/material_mono.lss @@ -146,8 +146,8 @@ // Page indicator (lvgl_page_indicator_create): a dot per page, the current page's dot is filled @page-indicator-color: @color-on-surface; -@page-indicator-dot-size: if($isCompact, 4, 6dp); -@page-indicator-gap: if($isCompact, 3, 6dp); +@page-indicator-dot-size: if($isCompact, 6, 8dp); +@page-indicator-gap: if($isCompact, 4, 6dp); // Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite @badge-color: @color-on-surface; @@ -180,7 +180,6 @@ // Toolbar: top app bar with a divider, which shows the background like the statusbar does @app-bar-title-color: @color-on-surface; -@app-bar-divider-color: @color-outline; @app-bar-pad-hor: if($isCompact, 0, @space-1); @app-bar-gap: if($isCompact, 2, @space-1); // The focus ring is drawn inside toolbar buttons, as there's little room around them @@ -268,10 +267,11 @@ @list-item-gap: if($isCompact, 3, @space-4); @list-divider-color: @color-outline; -// Tabs -@tabs-divider-color: @color-outline; -@tab-indicator-color: @color-primary; -@tab-indicator-width: if($isCompact, @line-medium, @line-thick); +// Tabs: the tab bar is a navigation bar, the selected tab is an inverted pill +@tabs-pad: if($isCompact, 2, @space-1); +@tabs-gap: if($isCompact, 2, @space-1); +@tab-pad: if($isCompact, 2, @space-1); +@tab-shape: @shape-control; @tab-page-pad: @density-container-pad; // Dialog @@ -441,6 +441,8 @@ chip { radius: @chip-shape; padHor: @chip-pad-hor; padVer: @chip-pad-ver; + // Room for the focus ring, which the parent would clip + marginAll: @focus-width + @focus-offset; } button { @@ -453,6 +455,8 @@ button { padHor: @button-pad-hor; padVer: @button-pad-ver; padGap: @button-gap; + // Room for the focus ring, which the parent would clip + marginAll: @focus-width + @focus-offset; } icon_button { @@ -494,9 +498,7 @@ app_bar { bgOpa: transp; textColor: @app-bar-title-color; radius: 0; - borderColor: @app-bar-divider-color; - borderWidth: @border; - borderSide: bottom; + borderWidth: 0; padHor: @app-bar-pad-hor; padVer: 0; padGap: @app-bar-gap; @@ -516,6 +518,8 @@ app_bar_focus_ring { switch_track { width: @switch-width; height: @switch-height; + // Room for the focus ring, which the parent would clip + marginAll: @focus-width + @focus-offset; bgColor: @switch-track-color; bgOpa: cover; borderColor: @switch-track-border-color; @@ -565,6 +569,8 @@ checkbox_box_selected { sliderbox { padColumn: @sliderbox-gap; + // Room for the focus ring of the step buttons, which the sliderbox would clip + padAll: @focus-width + @focus-offset; } slider_track_size { @@ -723,22 +729,17 @@ list_subheader { tabs { bgOpa: transp; - borderColor: @tabs-divider-color; - borderWidth: @border; - borderSide: bottom; + borderWidth: 0; + padAll: @tabs-pad; + padGap: @tabs-gap; } tab { bgOpa: transp; textColor: @color-on-surface; borderWidth: 0; - radius: 0; -} - -tab_selected { - borderColor: @tab-indicator-color; - borderWidth: @tab-indicator-width; - borderSide: bottom; + radius: @tab-shape; + padAll: @tab-pad; } tab_page { @@ -978,7 +979,7 @@ apply(obj) { apply(tabview > *[0] > button) { tab: default; inverted: state.pressed; - tab_selected: state.checked; + inverted: state.checked; focus_ring: state.focusKey; edit_ring: state.edited; } @@ -1077,6 +1078,11 @@ apply(page_indicator > label) { page_indicator_label: default; } +// Labels can be selectable, e.g. rows that keys move through +apply(label) { + focus_ring: state.focusKey; +} + apply(page_indicator_dot) { page_indicator_dot: default; page_indicator_dot_current: state.checked; diff --git a/Tactility/Include/Tactility/settings/AppearanceSettings.h b/Tactility/Include/Tactility/settings/AppearanceSettings.h index 5e39c246a..5da8a4ade 100644 --- a/Tactility/Include/Tactility/settings/AppearanceSettings.h +++ b/Tactility/Include/Tactility/settings/AppearanceSettings.h @@ -13,6 +13,13 @@ enum class ThemeMode { Dark }; +enum class DensityMode { + /** Regular or compact, as configured for the device */ + DeviceDefault, + Regular, + Compact +}; + struct AppearanceSettings { /** Default text font size in pixels, or 0 for the device's default size */ uint16_t fontSize = 0; @@ -23,15 +30,28 @@ struct AppearanceSettings { ThemeMode themeMode = ThemeMode::DeviceDefault; /** Use the monochrome theme, also on colour displays */ bool monoTheme = false; + DensityMode densityMode = DensityMode::DeviceDefault; bool animationsEnabled = true; /** Theme colours as 0xRRGGBB, or empty for the theme's default colour */ std::optional primaryColor; std::optional secondaryColor; std::optional errorColor; + /** Surface (screen background) colour as 0xRRGGBB, or empty for the theme's own surface. See getSurfaceColor(). */ + std::optional surfaceColor; + /** How strongly surfaceColor shows, from 0 (low) to SURFACE_TINT_LEVEL_COUNT - 1 (high) */ + uint8_t surfaceTintLevel = 1; bool operator==(const AppearanceSettings&) const = default; }; +constexpr uint8_t SURFACE_TINT_LEVEL_COUNT = 3; + +/** + * Mixes the surface colour into black (dark themes) or white (light themes), so the theme's text stays readable on it. + * @return the colour that the theme uses as its surface, as 0xRRGGBB + */ +uint32_t getSurfaceColor(uint32_t color, uint8_t tintLevel, bool isDark); + bool load(AppearanceSettings& settings); AppearanceSettings loadOrGetDefault(); diff --git a/Tactility/Private/Tactility/app/AppGrid.h b/Tactility/Private/Tactility/app/AppGrid.h index 36da50549..9f67adbf5 100644 --- a/Tactility/Private/Tactility/app/AppGrid.h +++ b/Tactility/Private/Tactility/app/AppGrid.h @@ -48,9 +48,12 @@ class AppGrid final { Secondary }; - /** Sets the colour of the tile icons. Call before creating the widgets. */ + /** Sets the colour of the tile icons. Applies from the next (re)population. */ void setIconColor(IconColor color) { iconColor = color; } + /** Shows the first page from the next (re)population. */ + void showFirstPage() { page = 0; } + /** Swiping left or right over the grid goes to the next or previous page. Call before creating the widgets. */ void setSwipeNavigation(bool enabled) { swipeNavigation = enabled; } @@ -89,8 +92,8 @@ class AppGrid final { lv_obj_t* prevButton = nullptr; lv_obj_t* nextButton = nullptr; lv_obj_t* pageIndicator = nullptr; - lv_obj_t* pageButtons = nullptr; - lv_obj_t* barButtons = nullptr; + lv_obj_t* bottomBar = nullptr; + lv_obj_t* barSpacer = nullptr; void createGrid(lv_obj_t* parent); void goToPreviousPage(); @@ -105,6 +108,7 @@ class AppGrid final { static void onPrevPressed(lv_event_t* e); static void onNextPressed(lv_event_t* e); static void onGridGesture(lv_event_t* e); + static void onBottomBarFocused(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); diff --git a/Tactility/Private/Tactility/app/btmanage/View.h b/Tactility/Private/Tactility/app/btmanage/View.h index 799704b01..a9887900d 100644 --- a/Tactility/Private/Tactility/app/btmanage/View.h +++ b/Tactility/Private/Tactility/app/btmanage/View.h @@ -26,7 +26,7 @@ class View final { void updateScanning(); void updatePeerList(); - void createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired, size_t index); + void createPeerListItem(lv_obj_t* list, const bluetooth::PeerRecord& record, bool isPaired, size_t index); static void onConnect(lv_event_t* event); diff --git a/Tactility/Private/Tactility/app/i2cscanner/I2cHelpers.h b/Tactility/Private/Tactility/app/i2cscanner/I2cHelpers.h index 21df444af..3bdeef7f7 100644 --- a/Tactility/Private/Tactility/app/i2cscanner/I2cHelpers.h +++ b/Tactility/Private/Tactility/app/i2cscanner/I2cHelpers.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include struct Device; @@ -9,7 +10,8 @@ namespace tt::app::i2cscanner { std::string getAddressText(uint8_t address); -std::string getPortNamesForDropdown(); +/** The names of the ready I2C controllers, in the order of getActivePortAtIndex() */ +std::vector getPortNames(); bool getActivePortAtIndex(int32_t index, struct Device** out); diff --git a/Tactility/Private/Tactility/app/launcher/LauncherMode.h b/Tactility/Private/Tactility/app/launcher/LauncherMode.h new file mode 100644 index 000000000..df6ae7c1a --- /dev/null +++ b/Tactility/Private/Tactility/app/launcher/LauncherMode.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include + +namespace tt::app::launcher { + +/** The content of the launcher grid and its tile interactions */ +struct LauncherMode { + /** Represents this mode on the button that switches to it */ + const char* buttonIcon; + /** The tile icon colour for themes that aren't monochrome */ + AppGrid::IconColor iconColor; + /** Returns the items in display order */ + std::vector (*collect)(); + /** Optional */ + void (*onLongPressed)(AppGrid& grid, const ::AppManifest& manifest); + /** Optional */ + void (*onKey)(AppGrid& grid, const ::AppManifest& manifest, uint32_t key); +}; + +extern const LauncherMode APPS_MODE; +extern const LauncherMode SETTINGS_MODE; + +} diff --git a/Tactility/Private/Tactility/app/wifimanage/View.h b/Tactility/Private/Tactility/app/wifimanage/View.h index f00379168..80bcce5cf 100644 --- a/Tactility/Private/Tactility/app/wifimanage/View.h +++ b/Tactility/Private/Tactility/app/wifimanage/View.h @@ -25,7 +25,7 @@ class View final { void updateScanning(); void updateNetworkList(); void updateConnectToHidden(); - void createSsidListItem(const WifiApRecord& record, bool isConnecting, size_t index); + void createSsidListItem(lv_obj_t* list, const WifiApRecord& record, bool isConnecting, size_t index); static void showDetails(lv_event_t* event); static void connect(lv_event_t* event); diff --git a/Tactility/Source/InitApps.cpp b/Tactility/Source/InitApps.cpp index 53c100bd7..b4235889c 100644 --- a/Tactility/Source/InitApps.cpp +++ b/Tactility/Source/InitApps.cpp @@ -61,7 +61,6 @@ namespace app { namespace power { extern const ::AppManifest manifest; } namespace poweroff { extern const ::AppManifest manifest; } namespace selectiondialog { extern const ::AppManifest manifest; } - namespace settings { extern const ::AppManifest manifest; } namespace setup { extern const ::AppManifest manifest; } namespace shell { extern const ::AppManifest manifest; extern const ::AppManifest sh_manifest; } namespace systeminfo { extern const ::AppManifest manifest; } @@ -131,7 +130,6 @@ static void registerInternalApps() { if (device_exists_of_type(&POWER_SUPPLY_TYPE)) { app_manager_add(&app::poweroff::manifest); } - app_manager_add(&app::settings::manifest); app_manager_add(&app::selectiondialog::manifest); app_manager_add(&app::setup::manifest); app_manager_add(&app::shell::manifest); diff --git a/Tactility/Source/app/AppGrid.cpp b/Tactility/Source/app/AppGrid.cpp index 2d7a0fb77..5e0c8bd0b 100644 --- a/Tactility/Source/app/AppGrid.cpp +++ b/Tactility/Source/app/AppGrid.cpp @@ -56,43 +56,39 @@ const AppGridItem* getTileItem(lv_event_t* e) { } -lv_obj_t* createBarSection(lv_obj_t* parent, lv_flex_align_t align) { - auto* section = lv_obj_create(parent); - lv_obj_set_size(section, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(section, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(section, 0, LV_STATE_DEFAULT); - lv_obj_set_flex_flow(section, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(section, align, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_remove_flag(section, LV_OBJ_FLAG_SCROLLABLE); - return section; -} - -// The width that a section's visible children need, including the gaps between them -int32_t getContentWidth(lv_obj_t* section) { +// The width of the visible buttons on one side of the bar's spacer, including the gaps between them +int32_t getSideWidth(lv_obj_t* bar, bool rightSide) { int32_t width = 0; uint32_t visible = 0; - const uint32_t count = lv_obj_get_child_count(section); + bool afterSpacer = false; + const uint32_t count = lv_obj_get_child_count(bar); for (uint32_t i = 0; i < count; i++) { - lv_obj_t* child = lv_obj_get_child(section, static_cast(i)); - if (!lv_obj_has_flag(child, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_t* child = lv_obj_get_child(bar, static_cast(i)); + if (lv_obj_get_style_flex_grow(child, LV_PART_MAIN) > 0) { + afterSpacer = true; + } else if (afterSpacer == rightSide && !lv_obj_has_flag_any(child, static_cast(LV_OBJ_FLAG_HIDDEN | LV_OBJ_FLAG_IGNORE_LAYOUT))) { width += lv_obj_get_width(child); visible++; } } - return visible > 1 ? width + static_cast(visible - 1) * lv_obj_get_style_pad_column(section, LV_PART_MAIN) : width; + return visible > 1 ? width + static_cast(visible - 1) * lv_obj_get_style_pad_column(bar, LV_PART_MAIN) : width; } -// The page indicator gets the space between the side sections, which grow equally to keep it centered +// The page indicator is centered on the bar and gets the space that the buttons on the wider side leave on both sides void updatePageIndicatorWidth(void* data) { auto* bottom_bar = static_cast(data); - auto* left_section = lv_obj_get_child(bottom_bar, 0); - auto* indicator_section = lv_obj_get_child(bottom_bar, 1); - auto* page_buttons = lv_obj_get_child(bottom_bar, 2); - auto* indicator = lv_obj_get_child(indicator_section, 0); + lv_obj_t* indicator = nullptr; + const uint32_t count = lv_obj_get_child_count(bottom_bar); + for (uint32_t i = 0; i < count && indicator == nullptr; i++) { + lv_obj_t* child = lv_obj_get_child(bottom_bar, static_cast(i)); + if (lv_obj_has_flag(child, LV_OBJ_FLAG_IGNORE_LAYOUT)) { + indicator = child; + } + } if (indicator == nullptr) { return; } - const int32_t side_width = std::max(getContentWidth(left_section), getContentWidth(page_buttons)); + const int32_t side_width = std::max(getSideWidth(bottom_bar, false), getSideWidth(bottom_bar, true)); const int32_t gaps = 2 * lv_obj_get_style_pad_column(bottom_bar, LV_PART_MAIN); const int32_t available = lv_obj_get_content_width(bottom_bar) - 2 * side_width - gaps; lv_obj_set_style_max_width(indicator, std::max(0, available), LV_STATE_DEFAULT); @@ -230,7 +226,8 @@ void AppGrid::populate() { pageCount = page_count; lv_obj_set_flag(pageIndicator, LV_OBJ_FLAG_HIDDEN, page_count <= 1); - lv_obj_set_flag(pageButtons, 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); lvgl_page_indicator_set_page_count(pageIndicator, page_count); lvgl_page_indicator_set_page(pageIndicator, page); @@ -317,31 +314,53 @@ void AppGrid::createGrid(lv_obj_t* parent) { } } +// Keys enter the bar on the previous page button, or on the first button when there's only one page +void AppGrid::onBottomBarFocused(lv_event_t* e) { + const auto* self = static_cast(lv_event_get_user_data(e)); + lv_obj_t* current = lvgl_grid_navigation_get_focused(self->bottomBar); + lv_obj_t* target = lv_obj_has_flag(self->prevButton, LV_OBJ_FLAG_HIDDEN) ? lv_obj_get_child(self->bottomBar, 0) : self->prevButton; + if (current == nullptr || target == current || target == self->barSpacer) { + return; + } + const bool showSelection = lv_obj_has_state(current, LV_STATE_FOCUS_KEY); + lv_gridnav_set_focused(self->bottomBar, target, LV_ANIM_OFF); + if (!showSelection) { + lv_obj_remove_state(target, LV_STATE_FOCUS_KEY); + } +} + void AppGrid::createWidgetsWithBottomBar(lv_obj_t* parent) { - // The side sections grow equally, so the page indicator stays centered - auto* bottom_bar = createBarSection(parent, LV_FLEX_ALIGN_START); - lv_obj_set_width(bottom_bar, LV_PCT(100)); - lvgl_obj_add_edge_padding(bottom_bar); - barButtons = createBarSection(bottom_bar, LV_FLEX_ALIGN_START); - lv_obj_set_flex_grow(barButtons, 1); - auto* indicator_section = createBarSection(bottom_bar, LV_FLEX_ALIGN_CENTER); - pageButtons = createBarSection(bottom_bar, LV_FLEX_ALIGN_END); - lv_obj_set_flex_grow(pageButtons, 1); - lv_obj_add_event_cb(bottom_bar, onBottomBarSizeChanged, LV_EVENT_SIZE_CHANGED, nullptr); - lv_obj_add_event_cb(bottom_bar, onBottomBarDeleted, LV_EVENT_DELETE, nullptr); + bottomBar = lv_obj_create(parent); + lv_obj_set_size(bottomBar, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(bottomBar, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(bottomBar, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(bottomBar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bottomBar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(bottomBar, LV_OBJ_FLAG_SCROLLABLE); + lvgl_obj_add_edge_padding(bottomBar); + lv_obj_add_event_cb(bottomBar, onBottomBarSizeChanged, LV_EVENT_SIZE_CHANGED, nullptr); + lv_obj_add_event_cb(bottomBar, onBottomBarDeleted, LV_EVENT_DELETE, nullptr); createGrid(parent); - lv_obj_move_foreground(bottom_bar); - - pageIndicator = lvgl_page_indicator_create(indicator_section); - prevButton = lvgl_icon_button_create(pageButtons); + lv_obj_move_foreground(bottomBar); + + // The bar buttons go before the spacer, which pushes the page buttons to the right + barSpacer = lv_obj_create(bottomBar); + lv_obj_set_size(barSpacer, 0, 0); + lv_obj_set_flex_grow(barSpacer, 1); + lv_obj_remove_flag(barSpacer, LV_OBJ_FLAG_CLICKABLE); + prevButton = lvgl_icon_button_create(bottomBar); lv_label_set_text(lv_label_create(prevButton), "<"); lv_obj_add_event_cb(prevButton, onPrevPressed, LV_EVENT_SHORT_CLICKED, this); - nextButton = lvgl_icon_button_create(pageButtons); + nextButton = lvgl_icon_button_create(bottomBar); lv_label_set_text(lv_label_create(nextButton), ">"); lv_obj_add_event_cb(nextButton, onNextPressed, LV_EVENT_SHORT_CLICKED, this); - // The arrow keys move between the page buttons - lvgl_grid_navigation_add(pageButtons); + pageIndicator = lvgl_page_indicator_create(bottomBar); + lv_obj_add_flag(pageIndicator, LV_OBJ_FLAG_IGNORE_LAYOUT); + lv_obj_align(pageIndicator, LV_ALIGN_CENTER, 0, 0); + // The arrow keys move between all buttons of the bar as one row + lvgl_grid_navigation_add(bottomBar); + lv_obj_add_event_cb(bottomBar, onBottomBarFocused, LV_EVENT_FOCUSED, this); // Resolves the grid's flex_grow height, which the page size is derived from. lv_obj_update_layout(parent); @@ -356,7 +375,8 @@ void AppGrid::createWidgetsWithBottomBar(lv_obj_t* parent) { } lv_obj_t* AppGrid::addBarButton(const char* icon, lv_event_cb_t onClicked, void* userData) { - auto* button = lvgl_icon_button_create(barButtons); + auto* button = lvgl_icon_button_create(bottomBar); + lv_obj_move_to_index(button, lv_obj_get_index(barSpacer)); auto* label = lv_label_create(button); lv_obj_set_style_text_font(label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT); lv_label_set_text(label, icon); diff --git a/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp b/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp index d1513ecfd..9bcca9944 100644 --- a/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp +++ b/Tactility/Source/app/appearancesettings/AppearanceSettings.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -74,8 +75,11 @@ constexpr NamedColor PALETTE_COLORS[] = { { "Grey", LV_PALETTE_GREY }, }; -constexpr size_t COLOR_COUNT = 3; -constexpr const char* COLOR_TITLES[COLOR_COUNT] = { "Primary color", "Secondary color", "Error color" }; +constexpr size_t COLOR_COUNT = 4; +constexpr size_t SURFACE_COLOR_INDEX = 2; +constexpr const char* COLOR_TITLES[COLOR_COUNT] = { "Primary color", "Secondary color", "Surface color", "Error color" }; +// In tint level order (see settings::appearance::getSurfaceColor()) +constexpr const char* SURFACE_TINT_NAMES[settings::appearance::SURFACE_TINT_LEVEL_COUNT] = { "Low", "Medium", "High" }; struct ColorRowWidgets { lv_obj_t* row = nullptr; @@ -118,7 +122,11 @@ struct Context { AppStream selectStream {}; uint8_t selectBuffer[256] {}; + /** The scroll position of the content, kept while e.g. a selection dialog covers the window. Accessed with the LVGL lock held. */ + int32_t contentScrollY = 0; + // Valid while the window is shown + lv_obj_t* content = nullptr; lv_obj_t* applyButton = nullptr; lv_obj_t* spinner = nullptr; FontRowWidgets regularRow; @@ -126,9 +134,17 @@ struct Context { lv_obj_t* errorLabel = nullptr; lv_obj_t* lightChip = nullptr; lv_obj_t* darkChip = nullptr; + lv_obj_t* regularDensityChip = nullptr; + lv_obj_t* compactDensityChip = nullptr; lv_obj_t* regularThemeChip = nullptr; lv_obj_t* monoThemeChip = nullptr; + // Shown for the regular theme: the "Theme Colors" title and card + lv_obj_t* colorsTitle = nullptr; + lv_obj_t* colorsCard = nullptr; ColorRowWidgets colorRows[COLOR_COUNT]; + /** Shown when a surface colour is set */ + lv_obj_t* surfaceTintRow = nullptr; + lv_obj_t* surfaceTintChips[settings::appearance::SURFACE_TINT_LEVEL_COUNT] = {}; /** The display can only show the mono theme */ bool isMonoDisplay = false; }; @@ -177,6 +193,23 @@ bool isDeviceDefaultDark() { return defaults.is_dark; } +bool isDeviceDefaultCompact() { + LvglThemeSettings defaults; + lvgl_theme_get_default_settings(&defaults); + return defaults.is_compact; +} + +bool isCompact(const settings::appearance::AppearanceSettings& settings) { + switch (settings.densityMode) { + case settings::appearance::DensityMode::Regular: + return false; + case settings::appearance::DensityMode::Compact: + return true; + default: + return isDeviceDefaultCompact(); + } +} + bool isDark(const settings::appearance::AppearanceSettings& settings) { switch (settings.themeMode) { case settings::appearance::ThemeMode::Light: @@ -197,22 +230,39 @@ void setChecked(lv_obj_t* object, bool checked) { } void updateColorRow(Context* ctx, size_t index); +uint32_t getDefaultColor(size_t index); + +void updateSurfaceTintRow(Context* ctx) { + if (ctx->surfaceTintRow == nullptr) { + return; + } + const auto& settings = ctx->pendingSettings; + setHidden(ctx->surfaceTintRow, !settings.surfaceColor.has_value()); + for (uint8_t i = 0; i < settings::appearance::SURFACE_TINT_LEVEL_COUNT; i++) { + setChecked(ctx->surfaceTintChips[i], settings.surfaceTintLevel == i); + } +} void updateThemeWidgets(Context* ctx) { const bool dark = isDark(ctx->pendingSettings); setChecked(ctx->lightChip, !dark); setChecked(ctx->darkChip, dark); + const bool compact = isCompact(ctx->pendingSettings); + setChecked(ctx->regularDensityChip, !compact); + setChecked(ctx->compactDensityChip, compact); if (ctx->regularThemeChip != nullptr) { setChecked(ctx->regularThemeChip, !ctx->pendingSettings.monoTheme); setChecked(ctx->monoThemeChip, ctx->pendingSettings.monoTheme); } // The mono theme doesn't use colours - for (size_t i = 0; i < COLOR_COUNT; i++) { - if (ctx->colorRows[i].row != nullptr) { - setHidden(ctx->colorRows[i].row, ctx->pendingSettings.monoTheme); + if (ctx->colorsCard != nullptr) { + setHidden(ctx->colorsTitle, ctx->pendingSettings.monoTheme); + setHidden(ctx->colorsCard, ctx->pendingSettings.monoTheme); + for (size_t i = 0; i < COLOR_COUNT; i++) { updateColorRow(ctx, i); } } + updateSurfaceTintRow(ctx); } /** Updates the widgets to the pending settings. Requires the LVGL lock. */ @@ -247,6 +297,24 @@ void onDarkPressed(lv_event_t* event) { setDark(static_cast(lv_event_get_user_data(event)), true); } +void setCompact(Context* ctx, bool compact) { + // The device default is kept when it matches, so it doesn't count as a change + if (compact == isDeviceDefaultCompact()) { + ctx->pendingSettings.densityMode = settings::appearance::DensityMode::DeviceDefault; + } else { + ctx->pendingSettings.densityMode = compact ? settings::appearance::DensityMode::Compact : settings::appearance::DensityMode::Regular; + } + updateWidgets(ctx); +} + +void onRegularDensityPressed(lv_event_t* event) { + setCompact(static_cast(lv_event_get_user_data(event)), false); +} + +void onCompactDensityPressed(lv_event_t* event) { + setCompact(static_cast(lv_event_get_user_data(event)), true); +} + void onRegularThemePressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); ctx->pendingSettings.monoTheme = false; @@ -259,10 +327,16 @@ void onMonoThemePressed(lv_event_t* event) { updateWidgets(ctx); } +void onSurfaceTintPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->pendingSettings.surfaceTintLevel = static_cast(reinterpret_cast(lv_obj_get_user_data(lv_event_get_target_obj(event)))); + updateWidgets(ctx); +} + void onAnimationsChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - auto* checkbox = static_cast(lv_event_get_target(event)); - ctx->pendingSettings.animationsEnabled = lv_obj_has_state(checkbox, LV_STATE_CHECKED); + auto* animations_switch = static_cast(lv_event_get_target(event)); + ctx->pendingSettings.animationsEnabled = lv_obj_has_state(animations_switch, LV_STATE_CHECKED); updateWidgets(ctx); } @@ -278,22 +352,35 @@ std::optional& getColor(settings::appearance::AppearanceSettings& sett return settings.primaryColor; case 1: return settings.secondaryColor; + case SURFACE_COLOR_INDEX: + return settings.surfaceColor; default: return settings.errorColor; } } +/** The surface has no default colour of its own: the theme derives it */ uint32_t getDefaultColor(size_t index) { LvglThemeSettings defaults; lvgl_theme_get_default_settings(&defaults); - const lv_color_t colors[COLOR_COUNT] = { defaults.color_primary, defaults.color_secondary, defaults.color_error }; - return lv_color_to_u32(colors[index]) & 0xFFFFFF; + const lv_color_t color = index == 0 ? defaults.color_primary : (index == 1 ? defaults.color_secondary : defaults.color_error); + return lv_color_to_u32(color) & 0xFFFFFF; } void updateColorRow(Context* ctx, size_t index) { const auto& widgets = ctx->colorRows[index]; const auto& color = getColor(ctx->pendingSettings, index); - lv_obj_set_style_bg_color(widgets.swatch, lv_color_hex(color.value_or(getDefaultColor(index))), LV_STATE_DEFAULT); + uint32_t shown; + if (color.has_value()) { + // The picked colour: the surface's tinted version of it is often too dark (or light) to recognise + shown = *color; + } else if (index != SURFACE_COLOR_INDEX) { + shown = getDefaultColor(index); + } else { + // The theme's own near-black or near-white surface + shown = isDark(ctx->pendingSettings) ? 0x131313 : 0xFAFAFA; + } + lv_obj_set_style_bg_color(widgets.swatch, lv_color_hex(shown), LV_STATE_DEFAULT); } void onColorButtonPressed(lv_event_t* event) { @@ -372,9 +459,6 @@ lv_obj_t* createRow(lv_obj_t* parent) { lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(row, lv_obj_get_style_pad_column(parent, LV_PART_MAIN), LV_STATE_DEFAULT); - // Room for the focus outline of the buttons, which is clipped to the row in compact mode - lv_obj_set_style_pad_ver(row, 3, LV_STATE_DEFAULT); - lv_obj_set_style_pad_hor(row, 3, LV_STATE_DEFAULT); lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); return row; } @@ -390,8 +474,10 @@ lv_obj_t* createSection(lv_obj_t* parent, const char* title) { return card; } +/** A row with a title. Its other items move to the next line when they don't fit next to the title. */ lv_obj_t* createLabeledRow(lv_obj_t* parent, const char* text) { auto* row = createRow(parent); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW_WRAP); auto* label = lv_label_create(row); lv_label_set_text(label, text); lv_obj_set_flex_grow(label, 1); @@ -448,14 +534,34 @@ void createThemeCard(lv_obj_t* parent, Context* ctx) { ctx->monoThemeChip = createChip(style_row, "Mono", onMonoThemePressed, ctx); } - auto* animations_checkbox = lv_checkbox_create(card); - lv_checkbox_set_text(animations_checkbox, "Animations"); - setChecked(animations_checkbox, ctx->pendingSettings.animationsEnabled); - lv_obj_add_event_cb(animations_checkbox, onAnimationsChanged, LV_EVENT_VALUE_CHANGED, ctx); + auto* density_row = createLabeledRow(card, "Density"); + ctx->regularDensityChip = createChip(density_row, "Regular", onRegularDensityPressed, ctx); + ctx->compactDensityChip = createChip(density_row, "Compact", onCompactDensityPressed, ctx); - if (!ctx->isMonoDisplay) { - for (size_t i = 0; i < COLOR_COUNT; i++) { - ctx->colorRows[i] = createColorRow(card, i, ctx); + auto* animations_row = createLabeledRow(card, "Animations"); + auto* animations_switch = lv_switch_create(animations_row); + setChecked(animations_switch, ctx->pendingSettings.animationsEnabled); + lv_obj_add_event_cb(animations_switch, onAnimationsChanged, LV_EVENT_VALUE_CHANGED, ctx); +} + +void createColorsCard(lv_obj_t* parent, Context* ctx) { + ctx->colorsTitle = lv_label_create(parent); + lv_label_set_text(ctx->colorsTitle, "Theme Colors"); + + 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); + ctx->colorsCard = card; + + for (size_t i = 0; i < COLOR_COUNT; i++) { + ctx->colorRows[i] = createColorRow(card, i, ctx); + if (i == SURFACE_COLOR_INDEX) { + // How strongly the surface colour shows: it's mixed into black (dark) or white (light) + ctx->surfaceTintRow = createLabeledRow(card, "Surface tint"); + for (uint8_t level = 0; level < settings::appearance::SURFACE_TINT_LEVEL_COUNT; level++) { + ctx->surfaceTintChips[level] = createChip(ctx->surfaceTintRow, SURFACE_TINT_NAMES[level], onSurfaceTintPressed, ctx); + lv_obj_set_user_data(ctx->surfaceTintChips[level], reinterpret_cast(static_cast(level))); + } } } } @@ -470,21 +576,30 @@ lv_obj_t* createButton(lv_obj_t* parent, const char* text, lv_event_cb_t callbac FontRowWidgets createFontRow(lv_obj_t* parent, const char* title, lv_event_cb_t onSelect, lv_event_cb_t onDefault, Context* ctx) { FontRowWidgets widgets; - // "Title [Default]" - auto* title_row = createRow(parent); - auto* label = lv_label_create(title_row); + // "Title [Default] [Select]" + // "file.ttf" + auto* row = createRow(parent); + + // The title with the file name below it, which take the width that the buttons leave. Texts that don't fit scroll. + auto* texts = lv_obj_create(row); + lv_obj_remove_style_all(texts); + lv_obj_set_height(texts, LV_SIZE_CONTENT); + lv_obj_set_flex_grow(texts, 1); + lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN); + lv_obj_remove_flag(texts, LV_OBJ_FLAG_SCROLLABLE); + + auto* label = lv_label_create(texts); + lv_obj_set_width(label, LV_PCT(100)); + lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); lv_label_set_text(label, title); - lv_obj_set_flex_grow(label, 1); - lv_obj_set_style_pad_ver(label, 0, LV_STATE_DEFAULT); - widgets.defaultButton = createButton(title_row, "Default", onDefault, ctx); - // " file.ttf [Select]" - auto* file_row = createRow(parent); - widgets.fileLabel = lv_label_create(file_row); - lv_obj_set_flex_grow(widgets.fileLabel, 1); - lv_label_set_long_mode(widgets.fileLabel, LV_LABEL_LONG_MODE_DOTS); - lv_obj_set_style_pad_left(widgets.fileLabel, 12, LV_STATE_DEFAULT); - createButton(file_row, "Select", onSelect, ctx); + widgets.fileLabel = lv_label_create(texts); + lv_obj_set_width(widgets.fileLabel, LV_PCT(100)); + lv_label_set_long_mode(widgets.fileLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); + lv_obj_set_style_text_font(widgets.fileLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT); + + widgets.defaultButton = createButton(row, "Default", onDefault, ctx); + createButton(row, "Select", onSelect, ctx); widgets.warningLabel = lv_label_create(parent); lv_obj_set_width(widgets.warningLabel, LV_PCT(100)); @@ -504,6 +619,7 @@ void createWidgets(lv_obj_t* parent, void* userData) { ctx->applyButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_OK, onApplyPressed, ctx); auto* content = lv_obj_create(parent); + ctx->content = content; lv_obj_set_width(content, LV_PCT(100)); lv_obj_set_flex_grow(content, 1); lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); @@ -512,6 +628,10 @@ void createWidgets(lv_obj_t* parent, void* userData) { const auto color_format = lv_display_get_color_format(lv_obj_get_display(parent)); ctx->isMonoDisplay = color_format == LV_COLOR_FORMAT_I1 || color_format == LV_COLOR_FORMAT_L8; createThemeCard(content, ctx); + // Monochrome and greyscale displays always use the mono theme, which doesn't use colours + if (!ctx->isMonoDisplay) { + createColorsCard(content, ctx); + } auto* fonts_card = createSection(content, "Fonts"); auto* font_size_row = createRow(fonts_card); @@ -543,7 +663,7 @@ void createWidgets(lv_obj_t* parent, void* userData) { createStepButton(font_size_row, LV_SYMBOL_PLUS, onFontSizeIncrementPressed, font_size_spinbox); ctx->regularRow = createFontRow(fonts_card, "Regular font", onSelectRegularPressed, onDefaultRegularPressed, ctx); - ctx->monoRow = createFontRow(fonts_card, "Mono font", onSelectMonoPressed, onDefaultMonoPressed, ctx); + ctx->monoRow = createFontRow(fonts_card, "Monospace font", onSelectMonoPressed, onDefaultMonoPressed, ctx); ctx->errorLabel = lv_label_create(fonts_card); lv_obj_set_width(ctx->errorLabel, LV_PCT(100)); @@ -552,10 +672,17 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_text_color(ctx->errorLabel, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT); updateWidgets(ctx); + + // The window is rebuilt when it's shown again, e.g. after a selection dialog + lv_obj_update_layout(content); + lv_obj_scroll_to_y(content, ctx->contentScrollY, LV_ANIM_OFF); } void destroyWidgets(void* userData) { auto* ctx = static_cast(userData); + // The LVGL lock is already held, so the content can still be read + ctx->contentScrollY = lv_obj_get_scroll_y(ctx->content); + ctx->content = nullptr; ctx->applyButton = nullptr; ctx->spinner = nullptr; ctx->regularRow = {}; @@ -563,11 +690,19 @@ void destroyWidgets(void* userData) { ctx->errorLabel = nullptr; ctx->lightChip = nullptr; ctx->darkChip = nullptr; + ctx->regularDensityChip = nullptr; + ctx->compactDensityChip = nullptr; ctx->regularThemeChip = nullptr; ctx->monoThemeChip = nullptr; + ctx->colorsTitle = nullptr; + ctx->colorsCard = nullptr; for (auto& row : ctx->colorRows) { row = {}; } + ctx->surfaceTintRow = nullptr; + for (auto& chip : ctx->surfaceTintChips) { + chip = nullptr; + } } int32_t applyThreadMain(void* context) { diff --git a/Tactility/Source/app/apphub/AppHubApp.cpp b/Tactility/Source/app/apphub/AppHubApp.cpp index 64a71ef6a..8cfb94667 100644 --- a/Tactility/Source/app/apphub/AppHubApp.cpp +++ b/Tactility/Source/app/apphub/AppHubApp.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -94,12 +95,19 @@ void onRefreshPressed(lv_event_t* e) { requestRefresh(ctx); } +lv_obj_t* createCard(lv_obj_t* parent) { + auto* card = lvgl_card_create(parent); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + return card; +} + void showRefreshFailedError(Context* ctx, const char* message) { lv_obj_clean(ctx->contentWrapper); - auto* label = lv_label_create(ctx->contentWrapper); + auto* label = lv_label_create(createCard(ctx->contentWrapper)); + lv_obj_set_width(label, LV_PCT(100)); + lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_WRAP); lv_label_set_text(label, message); - lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); lv_obj_remove_flag(ctx->refreshButton, LV_OBJ_FLAG_HIDDEN); } @@ -129,9 +137,17 @@ void showApps(Context* ctx) { return left.appName < right.appName; }); - auto* list = lv_list_create(ctx->contentWrapper); + auto* card = createCard(ctx->contentWrapper); + lv_obj_set_style_pad_all(card, 0, LV_STATE_DEFAULT); + // The list items' pressed and focused backgrounds follow the card's rounded corners + lv_obj_set_style_clip_corner(card, true, LV_STATE_DEFAULT); + + // The card provides the background + auto* list = lv_list_create(card); lv_obj_set_style_pad_all(list, 0, LV_STATE_DEFAULT); lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT); for (int i = 0; i < ctx->entries.size(); i++) { auto& entry = ctx->entries[i]; LOG_I(TAG, "Adding %s", entry.appName.c_str()); @@ -248,10 +264,9 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_add_flag(ctx->refreshButton, LV_OBJ_FLAG_HIDDEN); ctx->contentWrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(ctx->contentWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_width(ctx->contentWrapper, LV_PCT(100)); lv_obj_set_flex_grow(ctx->contentWrapper, 1); - lv_obj_set_style_pad_all(ctx->contentWrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_ver(ctx->contentWrapper, 0, LV_STATE_DEFAULT); ctx->restoreScrollOnNextShow = true; if (ctx->needsInitialRefresh) { diff --git a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp index 2f93c5dac..83b801bc7 100644 --- a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp +++ b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp @@ -226,6 +226,7 @@ void createWidgets(lv_obj_t* parent, void* userData) { // The global toolbar nav callback only knows how to stop old-model apps. lvgl_toolbar_set_nav_action(ctx->toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_grow(wrapper, 1); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); diff --git a/Tactility/Source/app/apppackagelist/AppPackageList.cpp b/Tactility/Source/app/apppackagelist/AppPackageList.cpp index b48d1eb0f..b419be739 100644 --- a/Tactility/Source/app/apppackagelist/AppPackageList.cpp +++ b/Tactility/Source/app/apppackagelist/AppPackageList.cpp @@ -15,6 +15,7 @@ #include +#include #include #include #include @@ -73,9 +74,14 @@ void createWidgets(lv_obj_t* parent, void* userData) { // The global toolbar nav callback only knows how to stop old-model apps. lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); - lv_obj_t* list = lv_list_create(parent); - lv_obj_set_width(list, LV_PCT(100)); - lv_obj_set_flex_grow(list, 1); + auto* content = lv_obj_create(parent); + lv_obj_set_width(content, LV_PCT(100)); + lv_obj_set_flex_grow(content, 1); + lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_border_width(content, 0, LV_STATE_DEFAULT); + + auto* card = lvgl_card_create(content); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); // createWidgets() can rerun for this same Context (window rebuild-on-remove). ctx->packages.clear(); @@ -84,25 +90,24 @@ void createWidgets(lv_obj_t* parent, void* userData) { return strcmp(left.id, right.id) < 0; }); - for (const auto& package : ctx->packages) { - createPackageWidget(&package, list); + if (ctx->packages.empty()) { + auto* no_apps_label = lv_label_create(card); + lv_label_set_text(no_apps_label, "No apps installed."); + return; } - if (ctx->packages.empty()) { - // lv_obj_align() is ignored for children of a flex-managed parent, so the empty-state - // label needs its own flex-growing wrapper to center within; the (empty) list is hidden - // rather than deleted so the wrapper can just take its place in the flex flow. - lv_obj_add_flag(list, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_flex_grow(list, 0); - - auto* empty_wrapper = lv_obj_create(parent); - lv_obj_set_width(empty_wrapper, LV_PCT(100)); - lv_obj_set_flex_grow(empty_wrapper, 1); - lv_obj_set_flex_align(empty_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_border_width(empty_wrapper, 0, LV_STATE_DEFAULT); - - auto* no_apps_label = lv_label_create(empty_wrapper); - lv_label_set_text(no_apps_label, "No apps installed"); + // The list items' pressed and focused backgrounds follow the card's rounded corners + lv_obj_set_style_pad_all(card, 0, LV_STATE_DEFAULT); + lv_obj_set_style_clip_corner(card, true, LV_STATE_DEFAULT); + + // The card provides the background + lv_obj_t* list = lv_list_create(card); + lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT); + + for (const auto& package : ctx->packages) { + createPackageWidget(&package, list); } } diff --git a/Tactility/Source/app/apwebserver/ApWebServer.cpp b/Tactility/Source/app/apwebserver/ApWebServer.cpp index d489f77cd..04bf2731d 100644 --- a/Tactility/Source/app/apwebserver/ApWebServer.cpp +++ b/Tactility/Source/app/apwebserver/ApWebServer.cpp @@ -51,6 +51,7 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); lv_obj_t* wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_style_pad_all(wrapper, 0, LV_PART_MAIN); lv_obj_set_style_pad_row(wrapper, 4, LV_PART_MAIN); diff --git a/Tactility/Source/app/audiosettings/AudioSettings.cpp b/Tactility/Source/app/audiosettings/AudioSettings.cpp index bcc61daf6..4fea6a1a5 100644 --- a/Tactility/Source/app/audiosettings/AudioSettings.cpp +++ b/Tactility/Source/app/audiosettings/AudioSettings.cpp @@ -88,6 +88,9 @@ 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); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); return row; } @@ -102,10 +105,9 @@ Section createSection(lv_obj_t* parent, const char* title, bool enabled, lv_even auto* title_label = lv_label_create(header); lv_label_set_text(title_label, title); - lv_obj_align(title_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(title_label, 1); auto* sw = lv_switch_create(header); - lv_obj_align(sw, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_set_state(sw, LV_STATE_CHECKED, enabled); lv_obj_add_event_cb(sw, onEnabledChanged, LV_EVENT_VALUE_CHANGED, userData); @@ -122,10 +124,9 @@ lv_obj_t* createSwitchRow(lv_obj_t* parent, const char* label, lv_event_cb_t cb, auto* row_label = lv_label_create(row); lv_label_set_text(row_label, label); - lv_obj_align(row_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(row_label, 1); auto* sw = lv_switch_create(row); - lv_obj_align(sw, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(sw, cb, LV_EVENT_VALUE_CHANGED, userData); return sw; @@ -134,13 +135,14 @@ 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 = createRow(parent); + // The label gets the width that the slider leaves, and scrolls when its text doesn't fit auto* row_label = lv_label_create(row); lv_label_set_text(row_label, label); - lv_obj_align(row_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(row_label, 1); + lv_label_set_long_mode(row_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); auto* sliderBox = lvgl_sliderbox_create(row, 0, 100, 10, initialValue); - lv_obj_set_width(sliderBox, LV_PCT(50)); - lv_obj_align(sliderBox, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_set_width(sliderBox, LV_PCT(75)); lvgl_sliderbox_add_value_changed_cb(sliderBox, cb, userData); return sliderBox; @@ -185,6 +187,7 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); diff --git a/Tactility/Source/app/btmanage/View.cpp b/Tactility/Source/app/btmanage/View.cpp index b089f1750..4fb46401f 100644 --- a/Tactility/Source/app/btmanage/View.cpp +++ b/Tactility/Source/app/btmanage/View.cpp @@ -12,6 +12,7 @@ #include #include +#include #include namespace tt::app::btmanage { @@ -98,7 +99,26 @@ static uint8_t mapRssiToPercentage(int8_t rssi) { return static_cast((float)(90 - abs_rssi) / 60.f * 100.f); } -void View::createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired, size_t index) { +/** Creates a title with a card below it, and returns the list in the card */ +static lv_obj_t* createSection(lv_obj_t* parent, const char* title) { + auto* label = lv_label_create(parent); + lv_label_set_text(label, title); + + auto* card = lvgl_card_create(parent); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(card, 0, LV_STATE_DEFAULT); + // The list items' pressed and focused backgrounds follow the card's rounded corners + lv_obj_set_style_clip_corner(card, true, LV_STATE_DEFAULT); + + // The card provides the background + auto* list = lv_list_create(card); + lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT); + return list; +} + +void View::createPeerListItem(lv_obj_t* list, const bluetooth::PeerRecord& record, bool isPaired, size_t index) { const auto percentage = mapRssiToPercentage(record.rssi); const auto label = record.name.empty() ? std::format("Unknown ({:02x}{:02x}{:02x}{:02x}{:02x}{:02x}) {}%", @@ -107,7 +127,7 @@ void View::createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired percentage) : std::format("{} {}%", record.name, percentage); - auto* button = lv_list_add_button(peers_list, nullptr, label.c_str()); + auto* button = lv_list_add_button(list, nullptr, label.c_str()); auto* item_data = new PeerListItemData { context, state, bindings, index, isPaired }; lv_obj_set_user_data(button, item_data); @@ -165,22 +185,20 @@ void View::updatePeerList() { // 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); + auto* enable_on_boot_wrapper = lv_obj_create(peers_list); 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); + lv_obj_set_style_bg_opa(enable_on_boot_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(enable_on_boot_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(enable_on_boot_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(enable_on_boot_wrapper, LV_OBJ_FLAG_SCROLLABLE); auto* enable_label = lv_label_create(enable_on_boot_wrapper); lv_label_set_text(enable_label, "Enable on boot"); - lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(enable_label, 1); enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper); - lv_obj_align(enable_on_boot_switch, LV_ALIGN_RIGHT_MID, 0, 0); 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); @@ -191,22 +209,21 @@ void View::updatePeerList() { // Paired peers section auto paired = state->getPairedPeers(); if (!paired.empty()) { - lv_list_add_text(peers_list, "Paired"); + auto* paired_section = createSection(peers_list, "Paired"); for (size_t i = 0; i < paired.size(); ++i) { - createPeerListItem(paired[i], true, i); + createPeerListItem(paired_section, paired[i], true, i); } } // Scan results section auto scan_results = state->getScanResults(); - lv_list_add_text(peers_list, "Available"); + auto* available_section = createSection(peers_list, "Available"); if (!scan_results.empty()) { for (size_t i = 0; i < scan_results.size(); ++i) { - createPeerListItem(scan_results[i], false, i); + createPeerListItem(available_section, scan_results[i], false, i); } } else if (!state->isScanning()) { - auto* no_devices_label = lv_label_create(peers_list); - lv_label_set_text(no_devices_label, "No devices found."); + lv_list_add_text(available_section, "No devices found."); } // Never hide peers_list: it always contains the "Enable on boot" row. // While scanning with no results the spinner in the toolbar provides feedback. @@ -241,9 +258,11 @@ void View::init(void* newContext, lv_obj_t* parent) { lv_obj_add_event_cb(enable_switch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, context); // Peer list - peers_list = lv_list_create(parent); + peers_list = lv_obj_create(parent); lv_obj_set_flex_grow(peers_list, 1); lv_obj_set_width(peers_list, LV_PCT(100)); + lv_obj_set_flex_flow(peers_list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_border_width(peers_list, 0, LV_STATE_DEFAULT); } void View::update() { diff --git a/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp b/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp index 22ce7cdb8..1dcca7bae 100644 --- a/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp +++ b/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp @@ -154,17 +154,19 @@ void createWidgets(lv_obj_t* parent, void* userData) { // Auto-connect toggle row auto* auto_connect_wrapper = lv_obj_create(wrapper); lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(auto_connect_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(auto_connect_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(auto_connect_wrapper, LV_OBJ_FLAG_SCROLLABLE); lvgl::obj_set_style_bg_invisible(auto_connect_wrapper); lv_obj_set_style_pad_all(auto_connect_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(auto_connect_wrapper, 0, LV_STATE_DEFAULT); auto* auto_connect_label = lv_label_create(auto_connect_wrapper); lv_label_set_text(auto_connect_label, "Auto-connect"); - lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(auto_connect_label, 1); auto* auto_connect_switch = lv_switch_create(auto_connect_wrapper); lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx); - lv_obj_align(auto_connect_switch, LV_ALIGN_RIGHT_MID, 0, 0); if (deviceLoaded && device.autoConnect) { lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED); diff --git a/Tactility/Source/app/chat/ChatView.cpp b/Tactility/Source/app/chat/ChatView.cpp index 5fc778a75..0eb6f35e8 100644 --- a/Tactility/Source/app/chat/ChatView.cpp +++ b/Tactility/Source/app/chat/ChatView.cpp @@ -43,10 +43,12 @@ void ChatView::createInputBar(lv_obj_t* parent) { inputWrapper = lv_obj_create(parent); auto* wrapper = inputWrapper; lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(wrapper, 0, 0); lv_obj_set_style_pad_column(wrapper, 4, 0); lv_obj_set_style_border_opa(wrapper, 0, LV_STATE_DEFAULT); + lv_obj_remove_flag(wrapper, LV_OBJ_FLAG_SCROLLABLE); inputField = lv_textarea_create(wrapper); lv_obj_set_flex_grow(inputField, 1); @@ -55,8 +57,6 @@ void ChatView::createInputBar(lv_obj_t* parent) { lv_textarea_set_max_length(inputField, MAX_MESSAGE_LEN); auto* sendBtn = lv_button_create(wrapper); - lv_obj_set_style_margin_all(sendBtn, 0, LV_STATE_DEFAULT); - lv_obj_set_style_margin_top(sendBtn, 2, LV_STATE_DEFAULT); lv_obj_add_event_cb(sendBtn, onSendClicked, LV_EVENT_CLICKED, this); auto* btnLabel = lv_label_create(sendBtn); @@ -66,6 +66,7 @@ void ChatView::createInputBar(lv_obj_t* parent) { void ChatView::createSettingsPanel(lv_obj_t* parent) { settingsPanel = lv_obj_create(parent); + lv_obj_set_style_border_width(settingsPanel, 0, LV_STATE_DEFAULT); lv_obj_set_width(settingsPanel, LV_PCT(100)); lv_obj_set_flex_grow(settingsPanel, 1); lv_obj_set_flex_flow(settingsPanel, LV_FLEX_FLOW_COLUMN); @@ -95,6 +96,7 @@ void ChatView::createSettingsPanel(lv_obj_t* parent) { // Buttons auto* btnRow = lv_obj_create(settingsPanel); lv_obj_set_flex_flow(btnRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(btnRow, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_size(btnRow, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(btnRow, 0, 0); lv_obj_set_style_pad_column(btnRow, 8, 0); @@ -113,6 +115,7 @@ void ChatView::createSettingsPanel(lv_obj_t* parent) { void ChatView::createChannelPanel(lv_obj_t* parent) { channelPanel = lv_obj_create(parent); + lv_obj_set_style_border_width(channelPanel, 0, LV_STATE_DEFAULT); lv_obj_set_width(channelPanel, LV_PCT(100)); lv_obj_set_flex_grow(channelPanel, 1); lv_obj_set_flex_flow(channelPanel, LV_FLEX_FLOW_COLUMN); @@ -130,6 +133,7 @@ void ChatView::createChannelPanel(lv_obj_t* parent) { auto* btnRow = lv_obj_create(channelPanel); lv_obj_set_flex_flow(btnRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(btnRow, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_size(btnRow, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(btnRow, 0, 0); lv_obj_set_style_pad_column(btnRow, 8, 0); diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index 04af25fb0..836bcb53a 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -19,6 +19,8 @@ #include #include +#include +#include #include #include @@ -132,37 +134,47 @@ void createWidgets(lv_obj_t* parent, void* userData) { // Enable on boot - lv_obj_t* enable_wrapper = lv_obj_create(content_wrapper); + auto* enable_card = lvgl_card_create(content_wrapper); + lv_obj_set_size(enable_card, LV_PCT(100), LV_SIZE_CONTENT); + + lv_obj_t* enable_wrapper = lv_obj_create(enable_card); lv_obj_set_size(enable_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(enable_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(enable_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(enable_wrapper, LV_OBJ_FLAG_SCROLLABLE); lvgl::obj_set_style_bg_invisible(enable_wrapper); lv_obj_set_style_border_width(enable_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_all(enable_wrapper, 0, LV_STATE_DEFAULT); lv_obj_t* enable_label = lv_label_create(enable_wrapper); lv_label_set_text(enable_label, "Enable on boot"); - lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(enable_label, 1); ctx->enableOnBootSwitch = lv_switch_create(enable_wrapper); lv_obj_add_event_cb(ctx->enableOnBootSwitch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, ctx); - lv_obj_align(ctx->enableOnBootSwitch, LV_ALIGN_RIGHT_MID, 0, 0); if (service::development::shouldEnableOnBoot()) { lv_obj_add_state(ctx->enableOnBootSwitch, LV_STATE_CHECKED); } else { lv_obj_remove_state(ctx->enableOnBootSwitch, LV_STATE_CHECKED); } - // Status + // Status and warning - ctx->statusLabel = lv_label_create(content_wrapper); + auto* status_card = lvgl_card_create(content_wrapper); + lv_obj_set_size(status_card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(status_card, LV_FLEX_FLOW_COLUMN); - // Warning + ctx->statusLabel = lv_label_create(status_card); - auto warning_label = lv_label_create(content_wrapper); + auto warning_label = lv_label_create(status_card); lv_label_set_text(warning_label, "This feature is experimental and uses an unsecured http connection."); lv_obj_set_width(warning_label, LV_PCT(100)); lv_label_set_long_mode(warning_label, LV_LABEL_LONG_WRAP); - if (lv_display_get_color_format(lv_obj_get_display(parent)) != LV_COLOR_FORMAT_L8) { - lv_obj_set_style_text_color(warning_label, lv_color_make(0xff, 0xff, 0x00), LV_STATE_DEFAULT); + // The monochrome theme has no error colour + if (!lvgl_theme_is_mono()) { + LvglThemeSettings theme_settings; + lvgl_theme_get_settings(&theme_settings); + lv_obj_set_style_text_color(warning_label, theme_settings.color_error, LV_STATE_DEFAULT); } updateViewState(ctx); diff --git a/Tactility/Source/app/display/Display.cpp b/Tactility/Source/app/display/Display.cpp index 2423fdc31..c11b3ed22 100644 --- a/Tactility/Source/app/display/Display.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -8,6 +8,7 @@ #include #include +#include #ifdef ESP_PLATFORM #include #endif @@ -15,15 +16,25 @@ #include #include +#include #include #include #include +#include +#include +#include +#include +#include +#include #include #include +#include +#include + #ifdef ESP_PLATFORM #include #endif @@ -36,16 +47,48 @@ constexpr auto* TAG = "Display"; namespace { +struct TimeoutOption { + uint32_t ms; + const char* name; +}; + +constexpr TimeoutOption TIMEOUT_OPTIONS[] = { + { 15000, "15 seconds" }, + { 30000, "30 seconds" }, + { 60000, "1 minute" }, + { 120000, "2 minutes" }, + { 300000, "5 minutes" }, + { 0, "Never" }, +}; + +// The icon is an upright phone with its bar at the bottom, rotated clockwise (0.1 degree units) to the screen's shape. +// In settings::display::Orientation order: Landscape, Portrait, LandscapeFlipped, PortraitFlipped +constexpr int32_t ORIENTATION_ICON_ROTATIONS[] = { 2700, 0, 900, 1800 }; + +// In settings::display::ScreensaverType order +constexpr const char* SCREENSAVER_NAMES[] = { "None", "Bouncing Balls", "Mystify", "Matrix Rain", "StackChan" }; + struct Context { uint32_t appInstanceId; settings::display::DisplaySettings displaySettings; bool displaySettingsUpdated = false; + TaskEventGroup* eventGroup = nullptr; + uint32_t selectTimeoutBit = 0; + uint32_t selectScreensaverBit = 0; + uint32_t timeoutDialogId = 0; + uint32_t screensaverDialogId = 0; + // Widgets, valid while the window is shown lv_obj_t* timeoutSwitch = nullptr; - lv_obj_t* timeoutDropdown = nullptr; - lv_obj_t* screensaverDropdown = nullptr; - lv_obj_t* orientationDropdown = nullptr; + // Shown when the auto screen off is enabled: the timeout and screensaver + lv_obj_t* timeoutCard = nullptr; + lv_obj_t* timeoutValueLabel = nullptr; + lv_obj_t* screensaverValueLabel = nullptr; + // Shown when auto-rotate is off or unavailable: the orientation label and chips + lv_obj_t* orientationGroup = nullptr; + lv_obj_t* orientationChips = nullptr; lv_obj_t* autoRotateSwitch = nullptr; - lv_obj_t* mountRotationDropdown = nullptr; + // Shown when auto-rotate is on: the sensor mounting label and chips + lv_obj_t* mountRotationGroup = nullptr; }; @@ -69,29 +112,67 @@ void onBackPressed(lv_event_t* event) { app_event_emit_close(ctx->appInstanceId); } +// The slider shows a percentage of the backlight's brightness range +int32_t brightnessToPercent(Device* backlight, int32_t brightness) { + const int32_t min = backlight_get_min_brightness(backlight); + const int32_t range = backlight_get_max_brightness(backlight) - min; + return range > 0 ? ((brightness - min) * 100 + range / 2) / range : 100; +} + +int32_t percentToBrightness(Device* backlight, int32_t percent) { + const int32_t min = backlight_get_min_brightness(backlight); + const int32_t range = backlight_get_max_brightness(backlight) - min; + const int32_t brightness = min + (percent * range + 50) / 100; + // A brightness of 0 turns the backlight off, so 0% is the dimmest setting that is still on + return brightness < 1 ? 1 : brightness; +} + void onBacklightSliderEvent(lv_event_t* event) { - auto* slider = static_cast(lv_event_get_target(event)); + auto* slider_box = lv_event_get_current_target_obj(event); auto* ctx = static_cast(lv_event_get_user_data(event)); auto* backlight = getBacklightDevice(); assert(backlight != nullptr); - int32_t slider_value = lv_slider_get_value(slider); - ctx->displaySettings.backlightDuty = static_cast(slider_value); + const int32_t percent = lvgl_sliderbox_get_value(slider_box); + ctx->displaySettings.backlightDuty = static_cast(percentToBrightness(backlight, percent)); ctx->displaySettingsUpdated = true; backlight_set_brightness(backlight, ctx->displaySettings.backlightDuty); device_put(backlight); } -void onOrientationSet(lv_event_t* event) { +void forEachChild(lv_obj_t* parent, auto&& action) { + const uint32_t count = lv_obj_get_child_count(parent); + for (uint32_t i = 0; i < count; i++) { + action(lv_obj_get_child(parent, static_cast(i)), i); + } +} + +void setCheckedChip(lv_obj_t* chips, settings::display::Orientation orientation) { + forEachChild(chips, [orientation](lv_obj_t* chip, uint32_t index) { + lv_obj_set_state(chip, LV_STATE_CHECKED, index == static_cast(orientation)); + }); +} + +/** Shows the manual orientation or the sensor mounting, depending on whether auto-rotate is on */ +void updateOrientationVisibility(Context* ctx) { + const bool auto_rotate = ctx->autoRotateSwitch != nullptr && ctx->displaySettings.autoRotateEnabled; + lv_obj_set_flag(ctx->orientationGroup, LV_OBJ_FLAG_HIDDEN, auto_rotate); + if (ctx->mountRotationGroup != nullptr) { + lv_obj_set_flag(ctx->mountRotationGroup, LV_OBJ_FLAG_HIDDEN, !auto_rotate); + } +} + +void onOrientationChipPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - auto* dropdown = static_cast(lv_event_get_target(event)); - uint32_t selected_index = lv_dropdown_get_selected(dropdown); - LOG_I(TAG, "Selected %u", (unsigned)selected_index); + auto* chip = lv_event_get_target_obj(event); + const auto selected_index = lv_obj_get_index(chip); + LOG_I(TAG, "Selected %d", (int)selected_index); auto selected_orientation = static_cast(selected_index); if (selected_orientation != ctx->displaySettings.orientation) { ctx->displaySettings.orientation = selected_orientation; ctx->displaySettingsUpdated = true; lv_display_set_rotation(lv_display_get_default(), settings::display::toLvglDisplayRotation(selected_orientation)); + setCheckedChip(ctx->orientationChips, selected_orientation); } } @@ -101,28 +182,63 @@ void onAutoRotateSwitch(lv_event_t* event) { bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED); ctx->displaySettings.autoRotateEnabled = enabled; ctx->displaySettingsUpdated = true; - if (ctx->mountRotationDropdown) { - if (enabled) { - lv_obj_clear_state(ctx->mountRotationDropdown, LV_STATE_DISABLED); - } else { - lv_obj_add_state(ctx->mountRotationDropdown, LV_STATE_DISABLED); - } - } - if (ctx->orientationDropdown) { - if (enabled) { - lv_obj_add_state(ctx->orientationDropdown, LV_STATE_DISABLED); - } else { - lv_obj_clear_state(ctx->orientationDropdown, LV_STATE_DISABLED); - } - } + updateOrientationVisibility(ctx); } -void onMountRotationSet(lv_event_t* event) { +void onMountRotationChipPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - auto* dropdown = static_cast(lv_event_get_target(event)); - uint32_t selected_index = lv_dropdown_get_selected(dropdown); - ctx->displaySettings.autoRotateMountRotation = static_cast(selected_index); + auto* chip = lv_event_get_target_obj(event); + const auto orientation = static_cast(lv_obj_get_index(chip)); + ctx->displaySettings.autoRotateMountRotation = orientation; ctx->displaySettingsUpdated = true; + setCheckedChip(lv_obj_get_parent(chip), orientation); +} + +/** Creates a transparent column in a card with a title, which centers the title and the content below it */ +lv_obj_t* createCardGroup(lv_obj_t* card, const char* title) { + auto* group = lv_obj_create(card); + lv_obj_set_size(group, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(group, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(group, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_opa(group, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(group, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(group, 0, LV_STATE_DEFAULT); + lv_obj_remove_flag(group, LV_OBJ_FLAG_SCROLLABLE); + + auto* label = lv_label_create(group); + lv_label_set_text(label, title); + return group; +} + +/** + * Creates a row of chips with a phone icon per orientation, in settings::display::Orientation order. + * The row is centered in its parent while the chips fit, and scrolls when they don't. + */ +lv_obj_t* createOrientationChips(lv_obj_t* parent, settings::display::Orientation selected, lv_event_cb_t onPressed, Context* ctx) { + auto* chips = lv_obj_create(parent); + lv_obj_set_size(chips, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_max_width(chips, LV_PCT(100), LV_STATE_DEFAULT); + lv_obj_set_flex_flow(chips, LV_FLEX_FLOW_ROW); + lv_obj_set_scroll_dir(chips, LV_DIR_HOR); + lv_obj_set_style_bg_opa(chips, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(chips, 0, LV_STATE_DEFAULT); + // The chips' margins leave room for their focus rings and space them apart + lv_obj_set_style_pad_all(chips, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(chips, 0, LV_STATE_DEFAULT); + + for (int32_t i = 0; i < 4; i++) { + auto* chip = lvgl_chip_create(chips); + lv_obj_add_event_cb(chip, onPressed, LV_EVENT_SHORT_CLICKED, ctx); + auto* icon = lv_label_create(chip); + lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT); + lv_label_set_text(icon, LVGL_ICON_SHARED_PHONE_ANDROID); + lv_obj_set_style_transform_pivot_x(icon, LV_PCT(50), LV_STATE_DEFAULT); + lv_obj_set_style_transform_pivot_y(icon, LV_PCT(50), LV_STATE_DEFAULT); + lv_obj_set_style_transform_rotation(icon, ORIENTATION_ICON_ROTATIONS[i], LV_STATE_DEFAULT); + } + lvgl_grid_navigation_add(chips); + setCheckedChip(chips, selected); + return chips; } void onTimeoutSwitch(lv_event_t* event) { @@ -131,52 +247,106 @@ void onTimeoutSwitch(lv_event_t* event) { bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED); ctx->displaySettings.backlightTimeoutEnabled = enabled; ctx->displaySettingsUpdated = true; - if (ctx->timeoutDropdown) { - if (enabled) { - lv_obj_clear_state(ctx->timeoutDropdown, LV_STATE_DISABLED); - if (ctx->screensaverDropdown) { - lv_obj_clear_state(ctx->screensaverDropdown, LV_STATE_DISABLED); - } - } else { - lv_obj_add_state(ctx->timeoutDropdown, LV_STATE_DISABLED); - if (ctx->screensaverDropdown) { - lv_obj_add_state(ctx->screensaverDropdown, LV_STATE_DISABLED); - } + if (ctx->timeoutCard) { + lv_obj_set_flag(ctx->timeoutCard, LV_OBJ_FLAG_HIDDEN, !enabled); + } +} + +const char* getTimeoutName(uint32_t ms) { + for (const auto& option : TIMEOUT_OPTIONS) { + if (option.ms == ms) { + return option.name; } } + return "Custom"; } -void onTimeoutChanged(lv_event_t* event) { - auto* ctx = static_cast(lv_event_get_user_data(event)); - auto* dropdown = static_cast(lv_event_get_target(event)); - uint32_t idx = lv_dropdown_get_selected(dropdown); - // Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=Never - static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 0}; - if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) { - ctx->displaySettings.backlightTimeoutMs = values_ms[idx]; - ctx->displaySettingsUpdated = true; +const char* getScreensaverName(settings::display::ScreensaverType type) { + const auto index = static_cast(type); + return index < std::size(SCREENSAVER_NAMES) ? SCREENSAVER_NAMES[index] : ""; +} + +/** Requires the LVGL lock */ +void updateTimeoutLabels(Context* ctx) { + if (ctx->timeoutValueLabel != nullptr) { + lv_label_set_text(ctx->timeoutValueLabel, getTimeoutName(ctx->displaySettings.backlightTimeoutMs)); + lv_label_set_text(ctx->screensaverValueLabel, getScreensaverName(ctx->displaySettings.screensaverType)); } } -void onScreensaverChanged(lv_event_t* event) { +void onChangeTimeoutPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + task_event_group_signal(ctx->eventGroup, ctx->selectTimeoutBit); +} + +void onChangeScreensaverPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - auto* dropdown = static_cast(lv_event_get_target(event)); - uint32_t idx = lv_dropdown_get_selected(dropdown); - // Validate index bounds before casting to enum - if (idx >= static_cast(settings::display::ScreensaverType::Count)) { + task_event_group_signal(ctx->eventGroup, ctx->selectScreensaverBit); +} + +void startTimeoutSelection(Context* ctx) { + if (ctx->timeoutDialogId != 0) { + return; + } + std::vector items; + for (const auto& option : TIMEOUT_OPTIONS) { + items.emplace_back(option.name); + } + ctx->timeoutDialogId = selectiondialog::start(ctx->appInstanceId, "Timeout", items); +} + +void startScreensaverSelection(Context* ctx) { + if (ctx->screensaverDialogId != 0) { return; } - auto selected_type = static_cast(idx); - if (selected_type != ctx->displaySettings.screensaverType) { - ctx->displaySettings.screensaverType = selected_type; + const std::vector items(std::begin(SCREENSAVER_NAMES), std::end(SCREENSAVER_NAMES)); + ctx->screensaverDialogId = selectiondialog::start(ctx->appInstanceId, "Screensaver", items); +} + +// Other results than an index mean that the dialog was dismissed +void onTimeoutSelectionResult(Context* ctx, int32_t selected) { + ctx->timeoutDialogId = 0; + if (selected >= 0 && selected < static_cast(std::size(TIMEOUT_OPTIONS))) { + ctx->displaySettings.backlightTimeoutMs = TIMEOUT_OPTIONS[selected].ms; + ctx->displaySettingsUpdated = true; + } +} + +void onScreensaverSelectionResult(Context* ctx, int32_t selected) { + ctx->screensaverDialogId = 0; + if (selected >= 0 && selected < static_cast(settings::display::ScreensaverType::Count)) { + ctx->displaySettings.screensaverType = static_cast(selected); ctx->displaySettingsUpdated = true; } } +/** A row in a card: "Title Value [Change]" */ +lv_obj_t* createValueRow(lv_obj_t* card, const char* title, lv_event_cb_t onChange, Context* ctx) { + auto* row = lv_obj_create(card); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT); + // The card provides the background + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + + auto* title_label = lv_label_create(row); + lv_label_set_text(title_label, title); + lv_obj_set_flex_grow(title_label, 1); + + auto* value_label = lv_label_create(row); + + auto* button = lv_button_create(row); + lv_label_set_text(lv_label_create(button), "Change"); + lv_obj_add_event_cb(button, onChange, LV_EVENT_SHORT_CLICKED, ctx); + return value_label; +} + void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); - ctx->displaySettings = settings::display::loadOrGetDefault(); bool has_imu = device_exists_of_type(&IMU_TYPE); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); @@ -189,6 +359,7 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); @@ -200,22 +371,16 @@ void createWidgets(lv_obj_t* parent, void* userData) { if (backlight != nullptr) { bool is_on_off_brightness = backlight_get_min_brightness(backlight) == 0 && backlight_get_max_brightness(backlight) == 1; if (!is_on_off_brightness) { - auto* brightness_wrapper = lv_obj_create(main_wrapper); - 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); - - auto* brightness_label = lv_label_create(brightness_wrapper); + auto* brightness_label = lv_label_create(main_wrapper); lv_label_set_text(brightness_label, "Brightness"); - lv_obj_align(brightness_label, LV_ALIGN_LEFT_MID, 0, 0); - auto* brightness_slider = lv_slider_create(brightness_wrapper); - lv_obj_set_width(brightness_slider, LV_PCT(50)); - lv_obj_align(brightness_slider, LV_ALIGN_RIGHT_MID, 0, 0); - lv_slider_set_range(brightness_slider, backlight_get_min_brightness(backlight), backlight_get_max_brightness(backlight)); - lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, ctx); + auto* brightness_card = lvgl_card_create(main_wrapper); + lv_obj_set_size(brightness_card, LV_PCT(100), LV_SIZE_CONTENT); - lv_slider_set_value(brightness_slider, ctx->displaySettings.backlightDuty, LV_ANIM_OFF); + const int32_t percent = brightnessToPercent(backlight, ctx->displaySettings.backlightDuty); + auto* brightness_slider = lvgl_sliderbox_create(brightness_card, 0, 100, 10, percent); + lv_obj_set_width(brightness_slider, LV_PCT(100)); + lvgl_sliderbox_add_value_changed_cb(brightness_slider, onBacklightSliderEvent, ctx); } // Only compared against nullptr below, never dereferenced again, so releasing it here is safe. device_put(backlight); @@ -223,65 +388,41 @@ void createWidgets(lv_obj_t* parent, void* userData) { // Orientation - auto* orientation_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(orientation_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(orientation_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(orientation_wrapper, 0, LV_STATE_DEFAULT); - - auto* orientation_label = lv_label_create(orientation_wrapper); - lv_label_set_text(orientation_label, "Orientation"); - lv_obj_align(orientation_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->orientationDropdown = lv_dropdown_create(orientation_wrapper); - // Note: order correlates with settings::display::Orientation item order - lv_dropdown_set_options(ctx->orientationDropdown, "Landscape\nPortrait Right\nLandscape Flipped\nPortrait Left"); - lv_obj_align(ctx->orientationDropdown, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(ctx->orientationDropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, ctx); - // Set the dropdown to match current orientation enum - lv_dropdown_set_selected(ctx->orientationDropdown, static_cast(ctx->displaySettings.orientation)); - if (has_imu && ctx->displaySettings.autoRotateEnabled) { - lv_obj_add_state(ctx->orientationDropdown, LV_STATE_DISABLED); - } - - // Auto-rotate (IMU-driven) - + // Auto-rotate (IMU-driven), which decides what the card below shows if (has_imu) { - auto* auto_rotate_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(auto_rotate_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(auto_rotate_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(auto_rotate_wrapper, 0, LV_STATE_DEFAULT); - - auto* auto_rotate_label = lv_label_create(auto_rotate_wrapper); + auto* auto_rotate_row = lv_obj_create(main_wrapper); + lv_obj_set_size(auto_rotate_row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(auto_rotate_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(auto_rotate_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_border_width(auto_rotate_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(auto_rotate_row, 0, LV_STATE_DEFAULT); + lv_obj_remove_flag(auto_rotate_row, LV_OBJ_FLAG_SCROLLABLE); + + auto* auto_rotate_label = lv_label_create(auto_rotate_row); lv_label_set_text(auto_rotate_label, "Auto-rotate"); - lv_obj_align(auto_rotate_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(auto_rotate_label, 1); - ctx->autoRotateSwitch = lv_switch_create(auto_rotate_wrapper); - if (ctx->displaySettings.autoRotateEnabled) { - lv_obj_add_state(ctx->autoRotateSwitch, LV_STATE_CHECKED); - } - lv_obj_align(ctx->autoRotateSwitch, LV_ALIGN_RIGHT_MID, 0, 0); + ctx->autoRotateSwitch = lv_switch_create(auto_rotate_row); + lv_obj_set_state(ctx->autoRotateSwitch, LV_STATE_CHECKED, ctx->displaySettings.autoRotateEnabled); lv_obj_add_event_cb(ctx->autoRotateSwitch, onAutoRotateSwitch, LV_EVENT_VALUE_CHANGED, ctx); + } - auto* mount_rotation_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(mount_rotation_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(mount_rotation_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(mount_rotation_wrapper, 0, LV_STATE_DEFAULT); - - auto* mount_rotation_label = lv_label_create(mount_rotation_wrapper); - lv_label_set_text(mount_rotation_label, "Sensor mounting"); - lv_obj_align(mount_rotation_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->mountRotationDropdown = lv_dropdown_create(mount_rotation_wrapper); - // Note: order correlates with settings::display::Orientation item order - lv_dropdown_set_options(ctx->mountRotationDropdown, "Landscape\nPortrait Right\nLandscape Flipped\nPortrait Left"); - lv_obj_align(ctx->mountRotationDropdown, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(ctx->mountRotationDropdown, onMountRotationSet, LV_EVENT_VALUE_CHANGED, ctx); - lv_dropdown_set_selected(ctx->mountRotationDropdown, static_cast(ctx->displaySettings.autoRotateMountRotation)); - if (!ctx->displaySettings.autoRotateEnabled) { - lv_obj_add_state(ctx->mountRotationDropdown, LV_STATE_DISABLED); - } + // The card shows the manual orientation, or the sensor mounting when auto-rotate is on + auto* orientation_card = lvgl_card_create(main_wrapper); + lv_obj_set_size(orientation_card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(orientation_card, LV_FLEX_FLOW_COLUMN); + + ctx->orientationGroup = createCardGroup(orientation_card, "Orientation"); + ctx->orientationChips = createOrientationChips(ctx->orientationGroup, ctx->displaySettings.orientation, onOrientationChipPressed, ctx); + + // Auto-rotate needs to know how the sensor is mounted relative to the display + if (has_imu) { + ctx->mountRotationGroup = createCardGroup(orientation_card, "Sensor mounting"); + createOrientationChips(ctx->mountRotationGroup, ctx->displaySettings.autoRotateMountRotation, onMountRotationChipPressed, ctx); } + updateOrientationVisibility(ctx); + // Screen timeout // Note: DisplayIdleService doesn't act on these settings for kernel-driver displays yet // (it only looks up the deprecated tt::hal::display::DisplayDevice), so these currently @@ -290,74 +431,46 @@ void createWidgets(lv_obj_t* parent, void* userData) { if (backlight != nullptr) { auto* timeout_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(timeout_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(timeout_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(timeout_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_all(timeout_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(timeout_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_remove_flag(timeout_wrapper, LV_OBJ_FLAG_SCROLLABLE); auto* timeout_label = lv_label_create(timeout_wrapper); lv_label_set_text(timeout_label, "Auto screen off"); - lv_obj_align(timeout_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(timeout_label, 1); ctx->timeoutSwitch = lv_switch_create(timeout_wrapper); if (ctx->displaySettings.backlightTimeoutEnabled) { lv_obj_add_state(ctx->timeoutSwitch, LV_STATE_CHECKED); } - lv_obj_align(ctx->timeoutSwitch, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->timeoutSwitch, onTimeoutSwitch, LV_EVENT_VALUE_CHANGED, ctx); - auto* timeout_select_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(timeout_select_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(timeout_select_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(timeout_select_wrapper, 0, LV_STATE_DEFAULT); - - auto* timeout_value_label = lv_label_create(timeout_select_wrapper); - lv_label_set_text(timeout_value_label, "Timeout"); - lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->timeoutDropdown = lv_dropdown_create(timeout_select_wrapper); - lv_dropdown_set_options(ctx->timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\nNever"); - lv_obj_align(ctx->timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(ctx->timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, ctx); - // Initialize dropdown selection from settings - uint32_t ms = ctx->displaySettings.backlightTimeoutMs; - uint32_t idx = 2; // default 1 minute - if (ms == 15000) idx = 0; - else if (ms == 30000) - idx = 1; - else if (ms == 60000) - idx = 2; - else if (ms == 120000) - idx = 3; - else if (ms == 300000) - idx = 4; - else if (ms == 0) - idx = 5; - lv_dropdown_set_selected(ctx->timeoutDropdown, idx); - if (!ctx->displaySettings.backlightTimeoutEnabled) { - lv_obj_add_state(ctx->timeoutDropdown, LV_STATE_DISABLED); - } + auto* timeout_card = lvgl_card_create(main_wrapper); + lv_obj_set_size(timeout_card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(timeout_card, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flag(timeout_card, LV_OBJ_FLAG_HIDDEN, !ctx->displaySettings.backlightTimeoutEnabled); + ctx->timeoutCard = timeout_card; - // Screensaver type - auto* screensaver_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(screensaver_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(screensaver_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(screensaver_wrapper, 0, LV_STATE_DEFAULT); - - auto* screensaver_label = lv_label_create(screensaver_wrapper); - lv_label_set_text(screensaver_label, "Screensaver"); - lv_obj_align(screensaver_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->screensaverDropdown = lv_dropdown_create(screensaver_wrapper); - // Note: order correlates with settings::display::ScreensaverType enum order - lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan"); - lv_obj_align(ctx->screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(ctx->screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, ctx); - lv_dropdown_set_selected(ctx->screensaverDropdown, static_cast(ctx->displaySettings.screensaverType)); - if (!ctx->displaySettings.backlightTimeoutEnabled) { - lv_obj_add_state(ctx->screensaverDropdown, LV_STATE_DISABLED); - } + ctx->timeoutValueLabel = createValueRow(timeout_card, "Timeout", onChangeTimeoutPressed, ctx); + ctx->screensaverValueLabel = createValueRow(timeout_card, "Screensaver", onChangeScreensaverPressed, ctx); + updateTimeoutLabels(ctx); } } +void destroyWidgets(void* userData) { + auto* ctx = static_cast(userData); + ctx->timeoutSwitch = nullptr; + ctx->timeoutCard = nullptr; + ctx->timeoutValueLabel = nullptr; + ctx->screensaverValueLabel = nullptr; + ctx->orientationGroup = nullptr; + ctx->orientationChips = nullptr; + ctx->autoRotateSwitch = nullptr; + ctx->mountRotationGroup = nullptr; +} + // Mirrors the old onHide() behaviour: persist the settings (regardless of whether the app is // giving up its thread for a save/resume cycle, or closing for good) whenever they changed. void persistIfUpdated(Context& ctx) { @@ -386,18 +499,31 @@ int32_t appMain(int argc, char* argv[]) { uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + // Loaded once: the window is rebuilt from it, e.g. after a dialog closed + ctx.displaySettings = settings::display::loadOrGetDefault(); TaskEventGroup event_group {}; task_event_group_construct(&event_group); + ctx.eventGroup = &event_group; + check(task_event_group_claim_bit(&event_group, &ctx.selectTimeoutBit) == ERROR_NONE); + check(task_event_group_claim_bit(&event_group, &ctx.selectScreensaverBit) == ERROR_NONE); AppEventSubscription sub {}; check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); + WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { - task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + uint32_t flags = 0; + task_event_group_wait_any(&event_group, &flags, portMAX_DELAY); + + if (flags & ctx.selectTimeoutBit) { + startTimeoutSelection(&ctx); + } + if (flags & ctx.selectScreensaverBit) { + startScreensaverSelection(&ctx); + } AppEvent event {}; while (app_event_poll(&sub, &event) == ERROR_NONE) { @@ -406,6 +532,17 @@ int32_t appMain(int argc, char* argv[]) { persistIfUpdated(ctx); shouldClose = true; break; + case APP_EVENT_RESULT: + if (event.result.launch_id == ctx.timeoutDialogId) { + onTimeoutSelectionResult(&ctx, event.result.result); + } else if (event.result.launch_id == ctx.screensaverDialogId) { + onScreensaverSelectionResult(&ctx, event.result.result); + } + lvgl_lock(); + updateTimeoutLabels(&ctx); + lvgl_unlock(); + app_manager_stop(event.result.launch_id); + break; default: break; } diff --git a/Tactility/Source/app/grovesettings/GroveSettings.cpp b/Tactility/Source/app/grovesettings/GroveSettings.cpp index 3ddf5a15e..4a69dc11c 100644 --- a/Tactility/Source/app/grovesettings/GroveSettings.cpp +++ b/Tactility/Source/app/grovesettings/GroveSettings.cpp @@ -13,6 +13,9 @@ #include +#include +#include +#include #include namespace tt::app::grovesettings { @@ -36,11 +39,35 @@ void collectDevices(Context* ctx) { }); } -void onModeChanged(lv_event_t* e) { +struct ModeOption { + GroveMode mode; + const char* name; +}; + +constexpr ModeOption MODE_OPTIONS[] = { + { GROVE_MODE_DISABLED, "Disabled" }, + { GROVE_MODE_I2C, "I2C" }, + { GROVE_MODE_UART, "UART" }, +}; + +GroveMode getChipMode(lv_obj_t* chip) { + return static_cast(reinterpret_cast(lv_obj_get_user_data(chip))); +} + +void updateChips(lv_obj_t* chipsRow, GroveMode mode) { + const uint32_t count = lv_obj_get_child_count(chipsRow); + for (uint32_t i = 0; i < count; i++) { + auto* chip = lv_obj_get_child(chipsRow, static_cast(i)); + lv_obj_set_state(chip, LV_STATE_CHECKED, getChipMode(chip) == mode); + } +} + +void onModeChipPressed(lv_event_t* e) { auto* device = static_cast<::Device*>(lv_event_get_user_data(e)); - auto* dropdown = static_cast(lv_event_get_target(e)); - auto mode = static_cast(lv_dropdown_get_selected(dropdown)); - grove_set_mode(device, mode); + auto* chip = lv_event_get_target_obj(e); + if (grove_set_mode(device, getChipMode(chip)) == ERROR_NONE) { + updateChips(lv_obj_get_parent(chip), getChipMode(chip)); + } } void onBackPressed(lv_event_t* event) { @@ -60,29 +87,44 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); for (auto* device : ctx->devices) { - auto* row = lv_obj_create(main_wrapper); - 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* label = lv_label_create(row); + auto* label = lv_label_create(main_wrapper); lv_label_set_text(label, device->name); - lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0); - auto* dropdown = lv_dropdown_create(row); - lv_dropdown_set_options(dropdown, "Disabled\nUART\nI2C"); - lv_obj_align(dropdown, LV_ALIGN_RIGHT_MID, 0, 0); + // The chips are centered in the card while they fit, and scroll when they don't + auto* card = lvgl_card_create(main_wrapper); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_left(label, lv_obj_get_style_pad_left(card, LV_PART_MAIN), LV_STATE_DEFAULT); + + auto* chips_row = lv_obj_create(card); + lv_obj_set_size(chips_row, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_max_width(chips_row, LV_PCT(100), LV_STATE_DEFAULT); + lv_obj_set_flex_flow(chips_row, LV_FLEX_FLOW_ROW); + lv_obj_set_scroll_dir(chips_row, LV_DIR_HOR); + lv_obj_set_style_bg_opa(chips_row, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(chips_row, 0, LV_STATE_DEFAULT); + // The chips' margins leave room for their focus rings and space them apart + lv_obj_set_style_pad_all(chips_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(chips_row, 0, LV_STATE_DEFAULT); + + for (const auto& option : MODE_OPTIONS) { + auto* chip = lvgl_chip_create(chips_row); + lv_label_set_text(lv_label_create(chip), option.name); + lv_obj_set_user_data(chip, reinterpret_cast(static_cast(option.mode))); + lv_obj_add_event_cb(chip, onModeChipPressed, LV_EVENT_SHORT_CLICKED, device); + } + lvgl_grid_navigation_add(chips_row); GroveMode current = GROVE_MODE_DISABLED; grove_get_mode(device, ¤t); - lv_dropdown_set_selected(dropdown, static_cast(current)); - - lv_obj_add_event_cb(dropdown, onModeChanged, LV_EVENT_VALUE_CHANGED, device); + updateChips(chips_row, current); } } diff --git a/Tactility/Source/app/i2cscanner/I2cHelpers.cpp b/Tactility/Source/app/i2cscanner/I2cHelpers.cpp index 0329074a4..2d1953cd8 100644 --- a/Tactility/Source/app/i2cscanner/I2cHelpers.cpp +++ b/Tactility/Source/app/i2cscanner/I2cHelpers.cpp @@ -1,7 +1,5 @@ #include "Tactility/app/i2cscanner/I2cHelpers.h" -#include - #include #include @@ -18,7 +16,7 @@ std::string getAddressText(uint8_t address) { return stream.str(); } -std::string getPortNamesForDropdown() { +std::vector getPortNames() { std::vector config_names; device_for_each_of_type(&I2C_CONTROLLER_TYPE, &config_names, [](auto* device, auto* context) { auto* names = static_cast*>(context); @@ -27,7 +25,7 @@ std::string getPortNamesForDropdown() { } return true; }); - return string::join(config_names, "\n"); + return config_names; } bool getActivePortAtIndex(int32_t index, struct Device** out) { diff --git a/Tactility/Source/app/i2cscanner/I2cScanner.cpp b/Tactility/Source/app/i2cscanner/I2cScanner.cpp index 07fafc736..95f50f5f9 100644 --- a/Tactility/Source/app/i2cscanner/I2cScanner.cpp +++ b/Tactility/Source/app/i2cscanner/I2cScanner.cpp @@ -24,7 +24,10 @@ #include #include +#include #include +#include +#include #include namespace tt::app::i2cscanner { @@ -35,9 +38,6 @@ namespace { constexpr auto* TAG = "I2cScanner"; -constexpr auto* START_SCAN_TEXT = "Scan"; -constexpr auto* STOP_SCAN_TEXT = "Stop scan"; - struct Context { uint32_t appInstanceId; @@ -47,10 +47,14 @@ struct Context { // State ScanState scanState = ScanStateInitial; struct Device* portDevice = nullptr; + int32_t selectedBus = 0; std::vector scannedAddresses; - // Widgets - lv_obj_t* scanButtonLabelWidget = nullptr; - lv_obj_t* portDropdownWidget = nullptr; + /** Row and column sizes of the results grid, which must outlive the grid layout */ + std::vector gridRows; + std::vector gridColumns; + // Widgets: the refresh button and chips only exist when there are I2C interfaces + lv_obj_t* refreshButton = nullptr; + lv_obj_t* chipsRow = nullptr; lv_obj_t* scanListWidget = nullptr; }; @@ -94,6 +98,37 @@ int32_t getLastBusIndex() { return index; } +/** The names of the devicetree devices on the bus at the given address, separated by commas */ +std::string getDeviceNames(struct Device* port, uint8_t address) { + struct Search { + uint8_t address; + std::string names; + } search = { address, "" }; + device_for_each_child(port, &search, [](struct Device* child, void* context) { + auto* search = static_cast(context); + if (child->address == search->address) { + if (!search->names.empty()) { + search->names += ", "; + } + search->names += child->name; + } + return true; + }); + return search.names; +} + +// A disturbed probe can report a device that isn't there, while a real device acknowledges every probe +constexpr int PROBE_COUNT = 3; + +bool hasDeviceAt(struct Device* port, uint8_t address) { + for (int i = 0; i < PROBE_COUNT; i++) { + if (i2c_controller_has_device_at_address(port, address, 10 / portTICK_PERIOD_MS) != ERROR_NONE) { + return false; + } + } + return true; +} + bool getPort(Context* ctx, struct Device** outPort) { if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) { *outPort = ctx->portDevice; @@ -128,25 +163,54 @@ bool shouldStopScanTimer(Context* ctx) { void updateViews(Context* ctx) { if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) { - if (ctx->scanState == ScanStateScanning) { - lv_label_set_text(ctx->scanButtonLabelWidget, STOP_SCAN_TEXT); - lv_obj_remove_flag(ctx->portDropdownWidget, LV_OBJ_FLAG_CLICKABLE); - } else { - lv_label_set_text(ctx->scanButtonLabelWidget, START_SCAN_TEXT); - lv_obj_add_flag(ctx->portDropdownWidget, LV_OBJ_FLAG_CLICKABLE); + if (ctx->scanListWidget == nullptr) { + ctx->mutex.unlock(); + return; + } + + // The scan reads the port once, so the bus can't change and the scan can't restart while it runs + const bool scanning = ctx->scanState == ScanStateScanning; + lv_obj_set_state(ctx->refreshButton, LV_STATE_DISABLED, scanning); + if (ctx->chipsRow != nullptr) { + const uint32_t chip_count = lv_obj_get_child_count(ctx->chipsRow); + for (uint32_t i = 0; i < chip_count; i++) { + auto* chip = lv_obj_get_child(ctx->chipsRow, static_cast(i)); + lv_obj_set_state(chip, LV_STATE_CHECKED, static_cast(i) == ctx->selectedBus); + lv_obj_set_state(chip, LV_STATE_DISABLED, scanning); + } } lv_obj_clean(ctx->scanListWidget); if (ctx->scanState == ScanStateStopped) { lv_obj_remove_flag(ctx->scanListWidget, LV_OBJ_FLAG_HIDDEN); - if (!ctx->scannedAddresses.empty()) { - for (auto address: ctx->scannedAddresses) { - std::string address_text = getAddressText(address); - lv_list_add_text(ctx->scanListWidget, address_text.c_str()); - } + // A grid with the title in the first row, then an address and the devicetree device names per row + const size_t count = ctx->scannedAddresses.size(); + ctx->gridColumns = { LV_GRID_CONTENT, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST }; + ctx->gridRows.assign(count + 1, LV_GRID_CONTENT); + ctx->gridRows.push_back(LV_GRID_TEMPLATE_LAST); + lv_obj_set_grid_dsc_array(ctx->scanListWidget, ctx->gridColumns.data(), ctx->gridRows.data()); + + auto* title = lv_label_create(ctx->scanListWidget); + lv_obj_set_grid_cell(title, LV_GRID_ALIGN_START, 0, 2, LV_GRID_ALIGN_CENTER, 0, 1); + if (count == 0) { + lv_label_set_text(title, "No devices found"); } else { - lv_list_add_text(ctx->scanListWidget, "No devices found"); + lv_label_set_text(title, std::format("{} {} found", count, count == 1 ? "device" : "devices").c_str()); + } + for (size_t i = 0; i < count; i++) { + const uint8_t address = ctx->scannedAddresses[i]; + const auto row = static_cast(i + 1); + auto* address_label = lv_label_create(ctx->scanListWidget); + lv_label_set_text(address_label, getAddressText(address).c_str()); + // Keys and encoders move through the addresses, which scrolls the results into view + lv_obj_add_flag(address_label, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_grid_cell(address_label, LV_GRID_ALIGN_START, 0, 1, LV_GRID_ALIGN_START, row, 1); + + auto* names_label = lv_label_create(ctx->scanListWidget); + lv_label_set_long_mode(names_label, LV_LABEL_LONG_MODE_WRAP); + lv_label_set_text(names_label, getDeviceNames(ctx->portDevice, address).c_str()); + lv_obj_set_grid_cell(names_label, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_START, row, 1); } } else { lv_obj_add_flag(ctx->scanListWidget, LV_OBJ_FLAG_HIDDEN); @@ -194,7 +258,7 @@ void onScanTimer(Context* ctx) { } for (uint8_t address = 1; address < 128; ++address) { - if (i2c_controller_has_device_at_address(safe_port, address, 10 / portTICK_PERIOD_MS) == ERROR_NONE) { + if (hasDeviceAt(safe_port, address)) { LOG_I(TAG, "Found device at address 0x%02X", address); if (!shouldStopScanTimer(ctx)) { addAddressToList(ctx, address); @@ -269,6 +333,7 @@ void selectBus(Context* ctx, int32_t selected) { if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) { ctx->scannedAddresses.clear(); ctx->portDevice = found_device; + ctx->selectedBus = selected; ctx->scanState = ScanStateInitial; ctx->mutex.unlock(); } @@ -288,20 +353,15 @@ void onBackPressed(lv_event_t* event) { app_event_emit_close(ctx->appInstanceId); } -void onSelectBus(lv_event_t* event) { +void onChipPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - auto* dropdown = static_cast(lv_event_get_target(event)); - uint32_t selected = lv_dropdown_get_selected(dropdown); - selectBus(ctx, selected); + auto* chip = lv_event_get_target_obj(event); + selectBus(ctx, static_cast(reinterpret_cast(lv_obj_get_user_data(chip)))); } -void onPressScan(lv_event_t* event) { +void onPressRefresh(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - if (ctx->scanState == ScanStateScanning) { - stopScanning(ctx); - } else { - startScanning(ctx); - } + startScanning(ctx); updateViews(ctx); } @@ -318,50 +378,56 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); - auto* wrapper = lv_obj_create(main_wrapper); - lv_obj_set_width(wrapper, LV_PCT(100)); - lv_obj_set_height(wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_border_width(wrapper, 0, 0); - // Ensures showing selection state for keyboard/encoder devices - lv_obj_set_style_pad_all(wrapper, 6, LV_ALIGN_DEFAULT); - - auto* scan_button = lv_button_create(wrapper); - lv_obj_set_width(scan_button, LV_PCT(48)); - lv_obj_align(scan_button, LV_ALIGN_TOP_LEFT, 0, 1); // Shift 1 pixel to align with selection box - lv_obj_add_event_cb(scan_button, onPressScan, LV_EVENT_SHORT_CLICKED, ctx); - auto* scan_button_label = lv_label_create(scan_button); - lv_obj_align(scan_button_label, LV_ALIGN_CENTER, 0, 0); - lv_label_set_text(scan_button_label, START_SCAN_TEXT); - ctx->scanButtonLabelWidget = scan_button_label; - - auto* port_dropdown = lv_dropdown_create(wrapper); - std::string dropdown_items = getPortNamesForDropdown(); - lv_dropdown_set_options(port_dropdown, dropdown_items.c_str()); - lv_obj_set_width(port_dropdown, LV_PCT(48)); - lv_obj_align(port_dropdown, LV_ALIGN_TOP_RIGHT, 0, 0); - lv_obj_add_event_cb(port_dropdown, onSelectBus, LV_EVENT_VALUE_CHANGED, ctx); - auto selected_bus = getLastBusIndex(); - lv_dropdown_set_selected(port_dropdown, selected_bus); - ctx->portDropdownWidget = port_dropdown; - - auto* scan_list = lv_list_create(main_wrapper); - lv_obj_set_style_margin_top(scan_list, 8, 0); - lv_obj_set_width(scan_list, LV_PCT(100)); - lv_obj_set_height(scan_list, LV_SIZE_CONTENT); + const std::vector port_names = getPortNames(); + if (port_names.empty()) { + lv_obj_set_flex_align(main_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + auto* label = lv_label_create(main_wrapper); + lv_obj_set_width(label, LV_PCT(100)); + lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_WRAP); + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT); + lv_label_set_text(label, "I2C interface not available"); + return; + } + + ctx->refreshButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_REFRESH, onPressRefresh, ctx); + + if (port_names.size() > 1) { + // Centered while the chips fit, and scrollable when they don't + lv_obj_set_flex_align(main_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); + auto* chips_row = lv_obj_create(main_wrapper); + lv_obj_set_size(chips_row, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_max_width(chips_row, LV_PCT(100), LV_STATE_DEFAULT); + lv_obj_set_flex_flow(chips_row, LV_FLEX_FLOW_ROW); + lv_obj_set_scroll_dir(chips_row, LV_DIR_HOR); + lv_obj_set_style_border_width(chips_row, 0, LV_STATE_DEFAULT); + // The chips' margins leave room for their focus rings and space them apart + lv_obj_set_style_pad_all(chips_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(chips_row, 0, LV_STATE_DEFAULT); + for (size_t i = 0; i < port_names.size(); i++) { + auto* chip = lvgl_chip_create(chips_row); + lv_label_set_text(lv_label_create(chip), port_names[i].c_str()); + lv_obj_set_user_data(chip, reinterpret_cast(static_cast(i))); + lv_obj_add_event_cb(chip, onChipPressed, LV_EVENT_SHORT_CLICKED, ctx); + } + lvgl_grid_navigation_add(chips_row); + ctx->chipsRow = chips_row; + } + + auto* scan_list = lvgl_card_create(main_wrapper); + lv_obj_set_size(scan_list, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_layout(scan_list, LV_LAYOUT_GRID); + lvgl_grid_navigation_add(scan_list); lv_obj_add_flag(scan_list, LV_OBJ_FLAG_HIDDEN); ctx->scanListWidget = scan_list; struct Device* dummy; - if (getActivePortAtIndex(selected_bus, &dummy)) { - selectBus(ctx, selected_bus); - } else if (getActivePortAtIndex(0, &dummy)) { - lv_dropdown_set_selected(port_dropdown, 0); - selectBus(ctx, 0); - } + const int32_t last_bus = getLastBusIndex(); + selectBus(ctx, getActivePortAtIndex(last_bus, &dummy) ? last_bus : 0); } // Mirrors the old model's onHide(): stop any in-flight scan before this app's task exits diff --git a/Tactility/Source/app/keyboard/KeyboardSettings.cpp b/Tactility/Source/app/keyboard/KeyboardSettings.cpp index dc31e0b3b..49e9d65be 100644 --- a/Tactility/Source/app/keyboard/KeyboardSettings.cpp +++ b/Tactility/Source/app/keyboard/KeyboardSettings.cpp @@ -122,6 +122,7 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); @@ -129,15 +130,17 @@ void createWidgets(lv_obj_t* parent, void* userData) { // Keyboard backlight toggle auto* bl_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(bl_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(bl_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bl_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(bl_wrapper, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_pad_all(bl_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(bl_wrapper, 0, LV_STATE_DEFAULT); auto* bl_label = lv_label_create(bl_wrapper); lv_label_set_text(bl_label, "Keyboard backlight"); - lv_obj_align(bl_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(bl_label, 1); ctx->switchBacklight = lv_switch_create(bl_wrapper); if (ctx->kbSettings.backlightEnabled) lv_obj_add_state(ctx->switchBacklight, LV_STATE_CHECKED); - lv_obj_align(ctx->switchBacklight, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->switchBacklight, onBacklightSwitch, LV_EVENT_VALUE_CHANGED, ctx); // Brightness slider @@ -160,15 +163,17 @@ void createWidgets(lv_obj_t* parent, void* userData) { // Backlight timeout enable auto* to_enable_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(to_enable_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(to_enable_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(to_enable_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(to_enable_wrapper, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_pad_all(to_enable_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(to_enable_wrapper, 0, LV_STATE_DEFAULT); auto* to_enable_label = lv_label_create(to_enable_wrapper); lv_label_set_text(to_enable_label, "Auto backlight off"); - lv_obj_align(to_enable_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(to_enable_label, 1); ctx->switchTimeoutEnable = lv_switch_create(to_enable_wrapper); if (ctx->kbSettings.backlightTimeoutEnabled) lv_obj_add_state(ctx->switchTimeoutEnable, LV_STATE_CHECKED); - lv_obj_align(ctx->switchTimeoutEnable, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->switchTimeoutEnable, onTimeoutEnableSwitch, LV_EVENT_VALUE_CHANGED, ctx); auto* timeout_select_wrapper = lv_obj_create(main_wrapper); diff --git a/Tactility/Source/app/launcher/AppsMode.cpp b/Tactility/Source/app/launcher/AppsMode.cpp new file mode 100644 index 000000000..8808f7f5f --- /dev/null +++ b/Tactility/Source/app/launcher/AppsMode.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace tt::app::launcher { + +namespace { + +constexpr auto* TAG = "Launcher"; + +const Favourites favourites; + +struct IconEntry { + const char* id; + const char* icon; +}; + +constexpr IconEntry ICONS[] = { + {"tactility.apphub", LVGL_ICON_SHARED_DOWNLOAD}, + {"tactility.camera", LVGL_ICON_SHARED_CAMERA}, + {"tactility.chat", LVGL_ICON_SHARED_FORUM}, + {"tactility.files", LVGL_ICON_SHARED_FOLDER}, + {"tactility.i2cscanner", LVGL_ICON_SHARED_CABLE}, + {"tactility.notes", LVGL_ICON_SHARED_EDIT_NOTE}, + {"tactility.screenshot", LVGL_ICON_SHARED_IMAGE}, + {"tactility.systeminfo", LVGL_ICON_SHARED_DEVICES}, + {"tactility.terminal", LVGL_ICON_SHARED_TERMINAL}, + {"tactility.webserversettings", LVGL_ICON_SHARED_CLOUD}, +}; + +// Hidden apps that are still shown in the launcher +constexpr const char* SHOWN_HIDDEN_APP_IDS[] = { + "tactility.files", +}; + +const char* appIcon(const ::AppManifest* manifest) { + for (const auto& entry : ICONS) { + if (!strcmp(manifest->id, entry.id)) return entry.icon; + } + return LVGL_ICON_SHARED_DEPLOYED_CODE; +} + +bool isShown(const ::AppManifest& manifest) { + if (manifest.category != APP_CATEGORY_USER && manifest.category != APP_CATEGORY_SYSTEM) { + return false; + } + if ((manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) { + return true; + } + return std::ranges::any_of(SHOWN_HIDDEN_APP_IDS, [&](const char* id) { return strcmp(manifest.id, id) == 0; }); +} + +void collectManifest(const ::AppManifest* manifest, void* context) { + auto* manifests = static_cast*>(context); + manifests->push_back(*manifest); +} + +// Apps from installed packages this device can't run (wrong device, too little RAM) +void collectIncompatibleAppIds(const AppPackage* package, void* context) { + if (app_package_manifest_is_compatible(&package->package)) { + return; + } + auto* ids = static_cast*>(context); + for (size_t i = 0; i < package->app_id_count; i++) { + ids->emplace_back(package->app_ids[i]); + } +} + +std::vector collectItems() { + const std::vector favouriteIds = favourites.load(); + + std::vector<::AppManifest> collected; + app_manager_for_each_manifest(collectManifest, &collected); + std::vector incompatibleIds; + app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds); + std::erase_if(collected, [&](const ::AppManifest& manifest) { + return !isShown(manifest) || std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end(); + }); + + std::vector items; + items.reserve(collected.size()); + for (const auto& manifest : collected) { + items.push_back({ manifest, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) }); + } + std::ranges::sort(items, [](const AppGridItem& a, const AppGridItem& b) { + if (a.highlighted != b.highlighted) { + return a.highlighted; + } + return strcmp(a.manifest.name, b.manifest.name) < 0; + }); + return items; +} + +void toggleFavourite(AppGrid& grid, const char* appId, bool keepSelection) { + if (!favourites.toggle(appId)) { + LOG_E(TAG, "Failed to save favourites"); + } + grid.requestRepopulate(keepSelection); +} + +void onLongPressed(AppGrid& grid, const ::AppManifest& manifest) { + toggleFavourite(grid, manifest.id, false); +} + +void onKey(AppGrid& grid, const ::AppManifest& manifest, uint32_t key) { + if (key == 'f' || key == 'F') { + toggleFavourite(grid, manifest.id, true); + } +} + +} // namespace + +const LauncherMode APPS_MODE = { + .buttonIcon = LVGL_ICON_SHARED_APPS, + .iconColor = AppGrid::IconColor::Primary, + .collect = collectItems, + .onLongPressed = onLongPressed, + .onKey = onKey +}; + +} diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 636f3c442..687136437 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -1,13 +1,10 @@ #include #include #include -#include #include #include -#include #include -#include #include #include @@ -22,7 +19,7 @@ #include #include -#include +#include #include #include #include @@ -33,69 +30,27 @@ constexpr auto* TAG = "Launcher"; namespace { -struct IconEntry { - const char* id; - const char* icon; -}; - -constexpr IconEntry ICONS[] = { - {"tactility.apphub", LVGL_ICON_SHARED_DOWNLOAD}, - {"tactility.camera", LVGL_ICON_SHARED_CAMERA}, - {"tactility.chat", LVGL_ICON_SHARED_FORUM}, - {"tactility.files", LVGL_ICON_SHARED_FOLDER}, - {"tactility.i2cscanner", LVGL_ICON_SHARED_CABLE}, - {"tactility.notes", LVGL_ICON_SHARED_EDIT_NOTE}, - {"tactility.screenshot", LVGL_ICON_SHARED_IMAGE}, - {"tactility.systeminfo", LVGL_ICON_SHARED_DEVICES}, - {"tactility.terminal", LVGL_ICON_SHARED_TERMINAL}, - {"tactility.webserversettings", LVGL_ICON_SHARED_CLOUD}, -}; +std::vector 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); -// Hidden apps that are still shown in the launcher -constexpr const char* SHOWN_HIDDEN_APP_IDS[] = { - "tactility.files", +enum class Mode { + Apps, + Settings }; -const char* appIcon(const ::AppManifest* manifest) { - for (const auto& entry : ICONS) { - if (!strcmp(manifest->id, entry.id)) return entry.icon; - } - return LVGL_ICON_SHARED_DEPLOYED_CODE; -} - -bool isShown(const ::AppManifest& manifest) { - if (manifest.category != APP_CATEGORY_USER && manifest.category != APP_CATEGORY_SYSTEM) { - return false; - } - if ((manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) { - return true; - } - return std::ranges::any_of(SHOWN_HIDDEN_APP_IDS, [&](const char* id) { return strcmp(manifest.id, id) == 0; }); +const LauncherMode& getLauncherMode(Mode mode) { + return mode == Mode::Settings ? SETTINGS_MODE : APPS_MODE; } -void collectManifest(const ::AppManifest* manifest, void* context) { - auto* manifests = static_cast*>(context); - manifests->push_back(*manifest); -} - -// Apps from installed packages this device can't run (wrong device, too little RAM) -void collectIncompatibleAppIds(const AppPackage* package, void* context) { - if (app_package_manifest_is_compatible(&package->package)) { - return; - } - auto* ids = static_cast*>(context); - for (size_t i = 0; i < package->app_id_count; i++) { - ids->emplace_back(package->app_ids[i]); - } +Mode getOtherMode(Mode mode) { + return mode == Mode::Settings ? Mode::Apps : Mode::Settings; } -std::vector collectItems(void* userData); -void onAppClicked(const ::AppManifest& manifest, void* userData); -void onAppLongPressed(const ::AppManifest& manifest, void* userData); -void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData); - struct Context { - Favourites favourites; + Mode mode = Mode::Apps; + lv_obj_t* modeButtonIcon = nullptr; AppGrid grid { AppGrid::Callbacks { .collect = collectItems, .onClicked = onAppClicked, @@ -106,29 +61,7 @@ struct Context { }; std::vector collectItems(void* userData) { - const auto* ctx = static_cast(userData); - const std::vector favouriteIds = ctx->favourites.load(); - - std::vector<::AppManifest> collected; - app_manager_for_each_manifest(collectManifest, &collected); - std::vector incompatibleIds; - app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds); - std::erase_if(collected, [&](const ::AppManifest& manifest) { - return !isShown(manifest) || std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end(); - }); - - std::vector items; - items.reserve(collected.size()); - for (const auto& manifest : collected) { - items.push_back({ manifest, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) }); - } - std::ranges::sort(items, [](const AppGridItem& a, const AppGridItem& b) { - if (a.highlighted != b.highlighted) { - return a.highlighted; - } - return strcmp(a.manifest.name, b.manifest.name) < 0; - }); - return items; + return getLauncherMode(static_cast(userData)->mode).collect(); } void startApp(const char* appId) { @@ -143,23 +76,38 @@ void onAppClicked(const ::AppManifest& manifest, void*) { startApp(manifest.id); } -void toggleFavourite(Context* ctx, const char* appId, bool keepSelection) { - if (!ctx->favourites.toggle(appId)) { - LOG_E(TAG, "Failed to save favourites"); +void onAppLongPressed(const ::AppManifest& manifest, void* userData) { + auto* ctx = static_cast(userData); + const LauncherMode& mode = getLauncherMode(ctx->mode); + if (mode.onLongPressed != nullptr) { + mode.onLongPressed(ctx->grid, manifest); } - ctx->grid.requestRepopulate(keepSelection); } -void onAppLongPressed(const ::AppManifest& manifest, void* userData) { - toggleFavourite(static_cast(userData), manifest.id, false); +void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData) { + auto* ctx = static_cast(userData); + const LauncherMode& mode = getLauncherMode(ctx->mode); + if (mode.onKey != nullptr) { + mode.onKey(ctx->grid, manifest, key); + } } -void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData) { - if (key == 'f' || key == 'F') { - toggleFavourite(static_cast(userData), manifest.id, true); +void applyIconColor(Context* ctx) { + // The monochrome theme's primary color is black, also on a black background + if (!lvgl_theme_is_mono()) { + ctx->grid.setIconColor(getLauncherMode(ctx->mode).iconColor); } } +void onModeButtonPressed(lv_event_t* e) { + auto* ctx = static_cast(lv_event_get_user_data(e)); + ctx->mode = getOtherMode(ctx->mode); + applyIconColor(ctx); + ctx->grid.showFirstPage(); + ctx->grid.requestRepopulate(false); + lv_label_set_text(ctx->modeButtonIcon, getLauncherMode(getOtherMode(ctx->mode)).buttonIcon); +} + void onShortcutPressed(lv_event_t* e) { startApp(static_cast(lv_event_get_user_data(e))); } @@ -188,13 +136,11 @@ 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); - // The monochrome theme's primary color is black, also on a black background - if (!lvgl_theme_is_mono()) { - ctx->grid.setIconColor(AppGrid::IconColor::Primary); - } + applyIconColor(ctx); ctx->grid.setSwipeNavigation(true); ctx->grid.createWidgetsWithBottomBar(parent); - ctx->grid.addBarButton(LVGL_ICON_SHARED_SETTINGS, onShortcutPressed, const_cast("tactility.settings")); + lv_obj_t* modeButton = ctx->grid.addBarButton(getLauncherMode(getOtherMode(ctx->mode)).buttonIcon, onModeButtonPressed, ctx); + ctx->modeButtonIcon = lv_obj_get_child(modeButton, 0); if (isAppRegistered("tactility.poweroff") && supportsPowerOff()) { ctx->grid.addBarButton(LVGL_ICON_SHARED_POWER_SETTINGS_NEW, onShortcutPressed, const_cast("tactility.poweroff")); } diff --git a/Tactility/Source/app/launcher/SettingsMode.cpp b/Tactility/Source/app/launcher/SettingsMode.cpp new file mode 100644 index 000000000..b8d95eb6a --- /dev/null +++ b/Tactility/Source/app/launcher/SettingsMode.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include + +#include + +#include +#include + +namespace tt::app::launcher { + +namespace { + +struct IconEntry { + const char* id; + const char* icon; +}; + +constexpr IconEntry ICONS[] = { + {"tactility.appearance", LVGL_ICON_SHARED_PALETTE}, + {"tactility.apppackagelist", LVGL_ICON_SHARED_DEPLOYED_CODE}, + {"tactility.audiosettings", LVGL_ICON_SHARED_MUSIC_NOTE}, + {"tactility.btmanage", LVGL_ICON_SHARED_BLUETOOTH}, + {"tactility.development", LVGL_ICON_SHARED_LOGO_DEV}, + {"tactility.display", LVGL_ICON_SHARED_DISPLAY_SETTINGS}, + {"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION}, + {"tactility.grovesettings", LVGL_ICON_SHARED_CABLE}, + {"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT}, + {"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP}, + {"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE}, + {"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW}, + {"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH}, + {"tactility.touchcalibration", LVGL_ICON_SHARED_CIRCLE}, + {"tactility.trackballsettings", LVGL_ICON_SHARED_DEVICES}, + {"tactility.usbsettings", LVGL_ICON_SHARED_USB}, + {"tactility.wifimanage", LVGL_ICON_SHARED_WIFI}, +}; + +const char* appIcon(const ::AppManifest* manifest) { + for (const auto& entry : ICONS) { + if (!strcmp(manifest->id, entry.id)) return entry.icon; + } + return LVGL_ICON_SHARED_DEPLOYED_CODE; +} + +void collectManifest(const ::AppManifest* manifest, void* context) { + auto* manifests = static_cast*>(context); + manifests->push_back(*manifest); +} + +std::vector collectItems() { + 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 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; +} + +} // namespace + +const LauncherMode SETTINGS_MODE = { + .buttonIcon = LVGL_ICON_SHARED_SETTINGS, + .iconColor = AppGrid::IconColor::Secondary, + .collect = collectItems, + .onLongPressed = nullptr, + .onKey = nullptr +}; + +} diff --git a/Tactility/Source/app/ledstripsettings/LedStripSettings.cpp b/Tactility/Source/app/ledstripsettings/LedStripSettings.cpp index 9f3bbb308..890a9a761 100644 --- a/Tactility/Source/app/ledstripsettings/LedStripSettings.cpp +++ b/Tactility/Source/app/ledstripsettings/LedStripSettings.cpp @@ -284,9 +284,12 @@ lv_obj_t* createRow(lv_obj_t* parent, const char* title) { lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(row, 2, LV_STATE_DEFAULT); lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); auto* label = lv_label_create(row); lv_label_set_text(label, title); - lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(label, 1); return row; } @@ -331,6 +334,7 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(ctx->toolbar, LV_SYMBOL_CLOSE, onNavigationPressed, ctx); ctx->mainPanel = lv_obj_create(parent); + lv_obj_set_style_border_width(ctx->mainPanel, 0, LV_STATE_DEFAULT); lv_obj_set_width(ctx->mainPanel, LV_PCT(100)); lv_obj_set_flex_grow(ctx->mainPanel, 1); lv_obj_set_flex_flow(ctx->mainPanel, LV_FLEX_FLOW_COLUMN); @@ -338,6 +342,7 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_add_flag(ctx->mainPanel, LV_OBJ_FLAG_SCROLLABLE); ctx->customPanel = lv_obj_create(parent); + lv_obj_set_style_border_width(ctx->customPanel, 0, LV_STATE_DEFAULT); lv_obj_set_width(ctx->customPanel, LV_PCT(100)); lv_obj_set_flex_grow(ctx->customPanel, 1); lv_obj_set_flex_flow(ctx->customPanel, LV_FLEX_FLOW_COLUMN); @@ -346,13 +351,11 @@ void createWidgets(lv_obj_t* parent, void* userData) { auto* outputRow = createRow(ctx->mainPanel, "Output"); auto* outputSwitch = lv_switch_create(outputRow); if (ctx->settings.enabled) lv_obj_add_state(outputSwitch, LV_STATE_CHECKED); - lv_obj_align(outputSwitch, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(outputSwitch, onOutputChanged, LV_EVENT_VALUE_CHANGED, ctx); auto* modeRow = createRow(ctx->mainPanel, "Mode"); auto* modeButton = lv_button_create(modeRow); lv_obj_set_width(modeButton, LV_PCT(62)); - lv_obj_align(modeButton, LV_ALIGN_RIGHT_MID, 0, 0); ctx->modeValue = lv_label_create(modeButton); lv_label_set_text(ctx->modeValue, patternName(ctx->settings.pattern)); lv_obj_center(ctx->modeValue); @@ -361,7 +364,6 @@ void createWidgets(lv_obj_t* parent, void* userData) { auto* primaryRow = createRow(ctx->mainPanel, "Color"); auto* primaryButton = lv_button_create(primaryRow); lv_obj_set_width(primaryButton, LV_PCT(62)); - lv_obj_align(primaryButton, LV_ALIGN_RIGHT_MID, 0, 0); ctx->primaryValue = lv_label_create(primaryButton); lv_label_set_text(ctx->primaryValue, presetName(ctx->settings.primaryPreset)); lv_obj_center(ctx->primaryValue); @@ -373,7 +375,6 @@ void createWidgets(lv_obj_t* parent, void* userData) { ctx->secondaryRow = createRow(ctx->mainPanel, "Second color"); auto* secondaryButton = lv_button_create(ctx->secondaryRow); lv_obj_set_width(secondaryButton, LV_PCT(62)); - lv_obj_align(secondaryButton, LV_ALIGN_RIGHT_MID, 0, 0); ctx->secondaryValue = lv_label_create(secondaryButton); lv_label_set_text(ctx->secondaryValue, presetName(ctx->settings.secondaryPreset)); lv_obj_center(ctx->secondaryValue); @@ -385,12 +386,12 @@ void createWidgets(lv_obj_t* parent, void* userData) { auto* brightnessRow = createRow(ctx->mainPanel, "Brightness"); ctx->brightnessValue = lv_label_create(brightnessRow); lv_label_set_text_fmt(ctx->brightnessValue, "%u%%", (static_cast(ctx->settings.brightness) * 100 + 127) / 255); - lv_obj_align(ctx->brightnessValue, LV_ALIGN_RIGHT_MID, 0, 0); ctx->brightnessSlider = lvgl_sliderbox_create(ctx->mainPanel, 0, UINT8_MAX, 17, ctx->settings.brightness); lv_obj_set_width(ctx->brightnessSlider, LV_PCT(100)); lvgl_sliderbox_add_value_changed_cb(ctx->brightnessSlider, onBrightnessChanged, ctx); ctx->choicePanel = lv_obj_create(parent); + lv_obj_set_style_border_width(ctx->choicePanel, 0, LV_STATE_DEFAULT); lv_obj_set_width(ctx->choicePanel, LV_PCT(100)); lv_obj_set_flex_grow(ctx->choicePanel, 1); lv_obj_set_flex_flow(ctx->choicePanel, LV_FLEX_FLOW_COLUMN); diff --git a/Tactility/Source/app/localesettings/LocaleSettings.cpp b/Tactility/Source/app/localesettings/LocaleSettings.cpp index 73ea77013..d68b62c13 100644 --- a/Tactility/Source/app/localesettings/LocaleSettings.cpp +++ b/Tactility/Source/app/localesettings/LocaleSettings.cpp @@ -1,8 +1,8 @@ #include #include -#include #include +#include #include #include @@ -14,11 +14,17 @@ #include #include +#include +#include #include #include +#include +#include #include +#include +#include namespace tt::app::localesettings { @@ -38,14 +44,19 @@ struct Context { uint32_t appInstanceId; tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH); RecursiveMutex mutex; - lv_obj_t* languageDropdown = nullptr; + TaskEventGroup* eventGroup = nullptr; + uint32_t selectLanguageBit = 0; + uint32_t languageSelectLaunchId = 0; + // Valid while the window is shown + lv_obj_t* languageLabel = nullptr; + lv_obj_t* languageValueLabel = nullptr; bool settingsUpdated = false; std::map languageMap; }; -std::string getLanguageOptions(Context* ctx) { +std::vector getLanguageNames(Context* ctx) { std::vector items; for (int i = 0; i < static_cast(settings::Language::count); i++) { switch (static_cast(i)) { @@ -68,25 +79,44 @@ std::string getLanguageOptions(Context* ctx) { break; } } - return string::join(items, "\n"); + return items; } +/** Requires the LVGL lock */ void updateViews(Context* ctx) { - ctx->textResources.load(); - - std::string language_options = getLanguageOptions(ctx); - lv_dropdown_set_options(ctx->languageDropdown, language_options.c_str()); - lv_dropdown_set_selected(ctx->languageDropdown, static_cast(settings::getLanguage())); + if (ctx->languageLabel == nullptr) { + return; + } + lv_label_set_text(ctx->languageLabel, ctx->textResources[i18n::Text::LANGUAGE].c_str()); + const auto names = getLanguageNames(ctx); + const auto index = static_cast(settings::getLanguage()); + lv_label_set_text(ctx->languageValueLabel, index < names.size() ? names[index].c_str() : ""); } -void onLanguageSet(lv_event_t* event) { +void onSelectLanguagePressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - auto* dropdown = static_cast(lv_event_get_target(event)); - auto index = lv_dropdown_get_selected(dropdown); - auto language = static_cast(index); - settings::setLanguage(language); + task_event_group_signal(ctx->eventGroup, ctx->selectLanguageBit); +} - updateViews(ctx); +void startLanguageSelection(Context* ctx) { + if (ctx->languageSelectLaunchId != 0) { + return; + } + ctx->languageSelectLaunchId = selectiondialog::start(ctx->appInstanceId, ctx->textResources[i18n::Text::LANGUAGE], getLanguageNames(ctx)); +} + +void onLanguageSelectionResult(Context* ctx, const AppEvent& event) { + ctx->languageSelectLaunchId = 0; + // Other results mean that the dialog was dismissed + const int32_t selected = event.result.result; + if (selected >= 0 && selected < static_cast(settings::Language::count)) { + settings::setLanguage(static_cast(selected)); + ctx->textResources.load(); + lvgl_lock(); + updateViews(ctx); + lvgl_unlock(); + } + app_manager_stop(event.result.launch_id); } // Preserved from the pre-conversion code as-is: declared but never wired to any widget there @@ -114,29 +144,57 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); - // Language - - auto* language_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_width(language_wrapper, LV_PCT(100)); - lv_obj_set_height(language_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(language_wrapper, 8, 0); - lv_obj_set_style_border_width(language_wrapper, 0, 0); - - auto* languageLabel = lv_label_create(language_wrapper); - lv_label_set_text(languageLabel, ctx->textResources[i18n::Text::LANGUAGE].c_str()); - lv_obj_align(languageLabel, LV_ALIGN_LEFT_MID, 4, 0); - - ctx->languageDropdown = lv_dropdown_create(language_wrapper); - lv_obj_set_width(ctx->languageDropdown, 150); - lv_obj_align(ctx->languageDropdown, LV_ALIGN_RIGHT_MID, 0, 0); - std::string language_options = getLanguageOptions(ctx); - lv_dropdown_set_options(ctx->languageDropdown, language_options.c_str()); - lv_dropdown_set_selected(ctx->languageDropdown, static_cast(settings::getLanguage())); - lv_obj_add_event_cb(ctx->languageDropdown, onLanguageSet, LV_EVENT_VALUE_CHANGED, ctx); + // "Language English [Select]" + auto* card = lvgl_card_create(main_wrapper); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + + // Transparent, so the card provides the background + auto* language_row = lv_obj_create(card); + lv_obj_set_size(language_row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(language_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(language_row, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(language_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(language_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_border_width(language_row, 0, LV_STATE_DEFAULT); + lv_obj_remove_flag(language_row, LV_OBJ_FLAG_SCROLLABLE); + + // The title with the language below it, which take the width that the button leaves. Texts that don't fit scroll. + auto* texts = lv_obj_create(language_row); + lv_obj_set_height(texts, LV_SIZE_CONTENT); + lv_obj_set_flex_grow(texts, 1); + lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(texts, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(texts, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(texts, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(texts, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(texts, LV_OBJ_FLAG_SCROLLABLE); + + ctx->languageLabel = lv_label_create(texts); + lv_obj_set_width(ctx->languageLabel, LV_PCT(100)); + lv_label_set_long_mode(ctx->languageLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); + + ctx->languageValueLabel = lv_label_create(texts); + lv_obj_set_width(ctx->languageValueLabel, LV_PCT(100)); + lv_label_set_long_mode(ctx->languageValueLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); + lv_obj_set_style_text_font(ctx->languageValueLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT); + + auto* select_button = lv_button_create(language_row); + lv_label_set_text(lv_label_create(select_button), "Change"); + lv_obj_add_event_cb(select_button, onSelectLanguagePressed, LV_EVENT_SHORT_CLICKED, ctx); + + updateViews(ctx); +} + +void destroyWidgets(void* userData) { + auto* ctx = static_cast(userData); + ctx->languageLabel = nullptr; + ctx->languageValueLabel = nullptr; } int32_t appMain(int argc, char* argv[]) { @@ -146,26 +204,31 @@ int32_t appMain(int argc, char* argv[]) { TaskEventGroup event_group {}; task_event_group_construct(&event_group); + ctx.eventGroup = &event_group; + check(task_event_group_claim_bit(&event_group, &ctx.selectLanguageBit) == ERROR_NONE); AppEventSubscription sub {}; check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); + WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { - task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + uint32_t flags = 0; + task_event_group_wait_any(&event_group, &flags, portMAX_DELAY); + + if (flags & ctx.selectLanguageBit) { + startLanguageSelection(&ctx); + } AppEvent event {}; while (app_event_poll(&sub, &event) == ERROR_NONE) { - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } else if (event.type == APP_EVENT_RESULT && event.result.launch_id == ctx.languageSelectLaunchId) { + onLanguageSelectionResult(&ctx, event); } - if (shouldClose) break; } } diff --git a/Tactility/Source/app/power/Power.cpp b/Tactility/Source/app/power/Power.cpp index 1b6662e60..11b6ec5d1 100644 --- a/Tactility/Source/app/power/Power.cpp +++ b/Tactility/Source/app/power/Power.cpp @@ -181,18 +181,20 @@ void createWidgets(lv_obj_t* parent, void* userData) { 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_flex_flow(switch_container, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(switch_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(switch_container, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_pad_all(switch_container, 0, 0); lv_obj_set_style_pad_gap(switch_container, 0, 0); lvgl::obj_set_style_bg_invisible(switch_container); lv_obj_t* label = lv_label_create(switch_container); lv_label_set_text(label, "Charging enabled"); - lv_obj_set_align(label, LV_ALIGN_LEFT_MID); + lv_obj_set_flex_grow(label, 1); lv_obj_t* enable_switch = lv_switch_create(switch_container); lv_obj_set_user_data(enable_switch, device); lv_obj_add_event_cb(enable_switch, onPowerEnabledChanged, LV_EVENT_VALUE_CHANGED, ctx); - lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID); lv_obj_set_state(enable_switch, LV_STATE_CHECKED, power_supply_is_allowed_to_charge(device)); entry.enableSwitch = enable_switch; } @@ -201,18 +203,20 @@ void createWidgets(lv_obj_t* parent, void* userData) { 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_flex_flow(qc_container, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(qc_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(qc_container, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_pad_all(qc_container, 0, 0); lv_obj_set_style_pad_gap(qc_container, 0, 0); lvgl::obj_set_style_bg_invisible(qc_container); lv_obj_t* label = lv_label_create(qc_container); lv_label_set_text(label, "Quick charge"); - lv_obj_set_align(label, LV_ALIGN_LEFT_MID); + lv_obj_set_flex_grow(label, 1); lv_obj_t* qc_switch = lv_switch_create(qc_container); lv_obj_set_user_data(qc_switch, device); lv_obj_add_event_cb(qc_switch, onQuickChargeChanged, LV_EVENT_VALUE_CHANGED, ctx); - lv_obj_set_align(qc_switch, LV_ALIGN_RIGHT_MID); lv_obj_set_state(qc_switch, LV_STATE_CHECKED, power_supply_is_quick_charge_enabled(device)); entry.quickChargeSwitch = qc_switch; } diff --git a/Tactility/Source/app/screenshot/Screenshot.cpp b/Tactility/Source/app/screenshot/Screenshot.cpp index d60304623..5abc219b1 100644 --- a/Tactility/Source/app/screenshot/Screenshot.cpp +++ b/Tactility/Source/app/screenshot/Screenshot.cpp @@ -21,6 +21,9 @@ #include #include +#include +#include +#include #include namespace tt::app::screenshot { @@ -31,12 +34,29 @@ extern const ::AppManifest manifest; namespace { +enum class CaptureMode { + Timer, + AppStart +}; + +struct ModeOption { + CaptureMode mode; + const char* name; +}; + +constexpr ModeOption MODE_OPTIONS[] = { + { CaptureMode::Timer, "Timer" }, + { CaptureMode::AppStart, "App start" }, +}; + struct Context { uint32_t appInstanceId; - lv_obj_t* modeDropdown = nullptr; + CaptureMode mode = CaptureMode::Timer; + lv_obj_t* modeChips = nullptr; lv_obj_t* pathTextArea = nullptr; lv_obj_t* startStopButtonLabel = nullptr; - lv_obj_t* timerWrapper = nullptr; + // Shown in timer mode + lv_obj_t* delayRow = nullptr; lv_obj_t* delayTextArea = nullptr; std::unique_ptr updateTimer; }; @@ -56,12 +76,11 @@ void updateScreenshotMode(Context* ctx) { lv_label_set_text(label, "Start"); } - uint32_t selected = lv_dropdown_get_selected(ctx->modeDropdown); - if (selected == 0) { // Timer - lv_obj_remove_flag(ctx->timerWrapper, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(ctx->timerWrapper, LV_OBJ_FLAG_HIDDEN); + const uint32_t chip_count = lv_obj_get_child_count(ctx->modeChips); + for (uint32_t i = 0; i < chip_count; i++) { + lv_obj_set_state(lv_obj_get_child(ctx->modeChips, static_cast(i)), LV_STATE_CHECKED, MODE_OPTIONS[i].mode == ctx->mode); } + lv_obj_set_flag(ctx->delayRow, LV_OBJ_FLAG_HIDDEN, ctx->mode != CaptureMode::Timer); } void onBackPressed(lv_event_t* event) { @@ -85,7 +104,6 @@ void onStartPressed(lv_event_t* event) { return; } - uint32_t selected = lv_dropdown_get_selected(ctx->modeDropdown); const char* path = lv_textarea_get_text(ctx->pathTextArea); error_t result = directory_make(path, true); @@ -94,7 +112,7 @@ void onStartPressed(lv_event_t* event) { return; } - if (selected == 0) { + if (ctx->mode == CaptureMode::Timer) { LOG_I(TAG, "Start timed screenshots"); const char* delay_text = lv_textarea_get_text(ctx->delayTextArea); int delay = atoi(delay_text); @@ -111,59 +129,62 @@ void onStartPressed(lv_event_t* event) { updateScreenshotMode(ctx); } -void onModeSet(lv_event_t* event) { +void onModeChipPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); + ctx->mode = MODE_OPTIONS[lv_obj_get_index(lv_event_get_target_obj(event))].mode; updateScreenshotMode(ctx); } -void createModeSettingWidgets(Context* ctx, lv_obj_t* parent) { - auto service = service::screenshot::optScreenshotService(); - if (service == nullptr) { - LOG_E(TAG, "Service not found/running"); - return; - } +/** A transparent row in a card: "Title [content]" */ +lv_obj_t* createRow(lv_obj_t* card, const char* title) { + auto* row = lv_obj_create(card); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_label_set_text(lv_label_create(row), title); + return row; +} - auto* mode_wrapper = lv_obj_create(parent); - lv_obj_set_size(mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(mode_wrapper, 0, 0); - lv_obj_set_style_border_width(mode_wrapper, 0, 0); - - auto* mode_label = lv_label_create(mode_wrapper); - lv_label_set_text(mode_label, "Mode:"); - lv_obj_align(mode_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->modeDropdown = lv_dropdown_create(mode_wrapper); - lv_dropdown_set_options(ctx->modeDropdown, "Timer\nApp start"); - lv_obj_align_to(ctx->modeDropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0); - lv_obj_add_event_cb(ctx->modeDropdown, onModeSet, LV_EVENT_VALUE_CHANGED, ctx); - service::screenshot::Mode mode = service->getMode(); - if (mode == service::screenshot::Mode::Apps) { - lv_dropdown_set_selected(ctx->modeDropdown, 1); +void createModeWidgets(Context* ctx, lv_obj_t* parent) { + auto* title = lv_label_create(parent); + lv_label_set_text(title, "Mode"); + lv_obj_set_width(title, LV_PCT(100)); + + // The chips are centered in the card while they fit, and scroll when they don't + auto* card = lvgl_card_create(parent); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + ctx->modeChips = lv_obj_create(card); + lv_obj_set_size(ctx->modeChips, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_max_width(ctx->modeChips, LV_PCT(100), LV_STATE_DEFAULT); + lv_obj_set_flex_flow(ctx->modeChips, LV_FLEX_FLOW_ROW); + lv_obj_set_scroll_dir(ctx->modeChips, LV_DIR_HOR); + lv_obj_set_style_bg_opa(ctx->modeChips, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(ctx->modeChips, 0, LV_STATE_DEFAULT); + // The chips' margins leave room for their focus rings and space them apart + lv_obj_set_style_pad_all(ctx->modeChips, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(ctx->modeChips, 0, LV_STATE_DEFAULT); + for (const auto& option : MODE_OPTIONS) { + auto* chip = lvgl_chip_create(ctx->modeChips); + lv_label_set_text(lv_label_create(chip), option.name); + lv_obj_add_event_cb(chip, onModeChipPressed, LV_EVENT_SHORT_CLICKED, ctx); } - - auto* button = lv_button_create(mode_wrapper); - lv_obj_align(button, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(button, onStartPressed, LV_EVENT_SHORT_CLICKED, ctx); - ctx->startStopButtonLabel = lv_label_create(button); - lv_obj_align(ctx->startStopButtonLabel, LV_ALIGN_CENTER, 0, 0); + lvgl_grid_navigation_add(ctx->modeChips); } -void createFilePathWidgets(Context* ctx, lv_obj_t* parent) { - auto* path_wrapper = lv_obj_create(parent); - lv_obj_set_size(path_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(path_wrapper, 0, 0); - lv_obj_set_style_border_width(path_wrapper, 0, 0); - lv_obj_set_flex_flow(path_wrapper, LV_FLEX_FLOW_ROW); - - auto* label_wrapper = lv_obj_create(path_wrapper); - lv_obj_set_style_border_width(label_wrapper, 0, 0); - lv_obj_set_style_pad_all(label_wrapper, 0, 0); - lv_obj_set_size(label_wrapper, 44, 36); - auto* path_label = lv_label_create(label_wrapper); - lv_label_set_text(path_label, "Path:"); - lv_obj_align(path_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->pathTextArea = lv_textarea_create(path_wrapper); +void createSettingsWidgets(Context* ctx, lv_obj_t* parent) { + 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); + + auto* path_row = createRow(card, "Path"); + ctx->pathTextArea = lv_textarea_create(path_row); lv_textarea_set_one_line(ctx->pathTextArea, true); lv_obj_set_flex_grow(ctx->pathTextArea, 1); char data_path[FILE_MAX_PATH_STRING_LENGTH]; @@ -173,41 +194,20 @@ void createFilePathWidgets(Context* ctx, lv_obj_t* parent) { } else { lv_textarea_set_text(ctx->pathTextArea, "Error: no data path"); } -} -void createTimerSettingsWidgets(Context* ctx, lv_obj_t* parent) { - ctx->timerWrapper = lv_obj_create(parent); - lv_obj_set_size(ctx->timerWrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(ctx->timerWrapper, 0, 0); - lv_obj_set_style_border_width(ctx->timerWrapper, 0, 0); - - auto* delay_wrapper = lv_obj_create(ctx->timerWrapper); - lv_obj_set_size(delay_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(delay_wrapper, 0, 0); - lv_obj_set_style_border_width(delay_wrapper, 0, 0); - lv_obj_set_flex_flow(delay_wrapper, LV_FLEX_FLOW_ROW); - - auto* delay_label_wrapper = lv_obj_create(delay_wrapper); - lv_obj_set_style_border_width(delay_label_wrapper, 0, 0); - lv_obj_set_style_pad_all(delay_label_wrapper, 0, 0); - lv_obj_set_size(delay_label_wrapper, 44, 36); - auto* delay_label = lv_label_create(delay_label_wrapper); - lv_label_set_text(delay_label, "Delay:"); - lv_obj_align(delay_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->delayTextArea = lv_textarea_create(delay_wrapper); + ctx->delayRow = createRow(card, "Delay"); + ctx->delayTextArea = lv_textarea_create(ctx->delayRow); lv_textarea_set_one_line(ctx->delayTextArea, true); lv_textarea_set_accepted_chars(ctx->delayTextArea, "0123456789"); lv_textarea_set_text(ctx->delayTextArea, "10"); lv_obj_set_flex_grow(ctx->delayTextArea, 1); + lv_label_set_text(lv_label_create(ctx->delayRow), "seconds"); +} - auto* delay_unit_label_wrapper = lv_obj_create(delay_wrapper); - lv_obj_set_style_border_width(delay_unit_label_wrapper, 0, 0); - lv_obj_set_style_pad_all(delay_unit_label_wrapper, 0, 0); - lv_obj_set_size(delay_unit_label_wrapper, LV_SIZE_CONTENT, 36); - auto* delay_unit_label = lv_label_create(delay_unit_label_wrapper); - lv_obj_align(delay_unit_label, LV_ALIGN_LEFT_MID, 0, 0); - lv_label_set_text(delay_unit_label, "seconds"); +void createStartStopButton(Context* ctx, lv_obj_t* parent) { + auto* button = lv_button_create(parent); + lv_obj_add_event_cb(button, onStartPressed, LV_EVENT_SHORT_CLICKED, ctx); + ctx->startStopButtonLabel = lv_label_create(button); } void createWidgets(lv_obj_t* parent, void* userData) { @@ -230,10 +230,17 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_flex_grow(wrapper, 1); lv_obj_set_style_border_width(wrapper, 0, 0); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + // The cards stretch, the start/stop button is centered + lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START); + + auto service = service::screenshot::optScreenshotService(); + if (service != nullptr && service->getMode() == service::screenshot::Mode::Apps) { + ctx->mode = CaptureMode::AppStart; + } - createModeSettingWidgets(ctx, wrapper); - createFilePathWidgets(ctx, wrapper); - createTimerSettingsWidgets(ctx, wrapper); + createModeWidgets(ctx, wrapper); + createSettingsWidgets(ctx, wrapper); + createStartStopButton(ctx, wrapper); updateScreenshotMode(ctx); diff --git a/Tactility/Source/app/settings/Settings.cpp b/Tactility/Source/app/settings/Settings.cpp deleted file mode 100644 index 4e8883c7e..000000000 --- a/Tactility/Source/app/settings/Settings.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include - -#include - -#include -#include -#include - -namespace tt::app::settings { - -namespace { - -struct IconEntry { - const char* id; - const char* icon; -}; - -constexpr IconEntry ICONS[] = { - {"tactility.appearance", LVGL_ICON_SHARED_PALETTE}, - {"tactility.apppackagelist", LVGL_ICON_SHARED_DEPLOYED_CODE}, - {"tactility.audiosettings", LVGL_ICON_SHARED_MUSIC_NOTE}, - {"tactility.btmanage", LVGL_ICON_SHARED_BLUETOOTH}, - {"tactility.development", LVGL_ICON_SHARED_LOGO_DEV}, - {"tactility.display", LVGL_ICON_SHARED_DISPLAY_SETTINGS}, - {"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION}, - {"tactility.grovesettings", LVGL_ICON_SHARED_CABLE}, - {"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT}, - {"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP}, - {"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE}, - {"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW}, - {"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH}, - {"tactility.touchcalibration", LVGL_ICON_SHARED_CIRCLE}, - {"tactility.trackballsettings", LVGL_ICON_SHARED_DEVICES}, - {"tactility.usbsettings", LVGL_ICON_SHARED_USB}, - {"tactility.wifimanage", LVGL_ICON_SHARED_WIFI}, -}; - -const char* appIcon(const ::AppManifest* manifest) { - for (const auto& entry : ICONS) { - if (!strcmp(manifest->id, entry.id)) return entry.icon; - } - return LVGL_ICON_SHARED_DEPLOYED_CODE; -} - -void onAppClicked(const ::AppManifest& manifest, void*) { - // Fire-and-forget top-level navigation, same as the Launcher's own app-launch buttons. - uint32_t instanceId = 0; - // Re-resolved by id against the live ledger, not the (possibly stale) cached manifest above: - // fails gracefully if the app was uninstalled since this button was built, instead of handing - // 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) { - app_start_with_context(&context, &instanceId); - } -} - -void collectManifest(const ::AppManifest* manifest, void* context) { - auto* manifests = static_cast*>(context); - manifests->push_back(*manifest); -} - -std::vector 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 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(lv_event_get_user_data(event)); - app_event_emit_close(ctx->appInstanceId); -} - -void createWidgets(lv_obj_t* parent, void* userData) { - auto* ctx = static_cast(userData); - - lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); - - if (!lvgl_theme_is_mono()) { - ctx->grid.setIconColor(AppGrid::IconColor::Secondary); - } - ctx->grid.setSwipeNavigation(true); - ctx->grid.createWidgetsWithBottomBar(parent); - ctx->grid.addBarButton(LVGL_ICON_SHARED_CLOSE, onBackPressed, ctx); -} - -int32_t appMain(int argc, char* argv[]) { - uint32_t appInstanceId = app_scheduler_current_app_id(); - Context ctx { appInstanceId }; - - TaskEventGroup event_group {}; - task_event_group_construct(&event_group); - - AppEventSubscription sub {}; - check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - - WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); - - while (true) { - task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); - - bool shouldClose = false; - AppEvent event {}; - while (app_event_poll(&sub, &event) == ERROR_NONE) { - if (event.type == APP_EVENT_CLOSE) { - shouldClose = true; - break; - } - } - if (shouldClose) break; - } - - window_manager_remove(window); - check(app_event_unsubscribe(&sub) == ERROR_NONE); - task_event_group_destruct(&event_group); - return 0; -} - -} // namespace - -extern const ::AppManifest manifest = { - .id = "tactility.settings", - .name = "Settings", - .category = APP_CATEGORY_SYSTEM, - .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast(appMain) }, - .flags = APP_MANIFEST_FLAG_HIDDEN, - .stack = { .depth = 5120, .desired_memory_capability = 0 }, -}; - -} // namespace diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index cf1c545bb..925292252 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -1,8 +1,11 @@ -#include +#include "lvgl/icons/shared.h" +#include "lvgl/theme.h" + + #include "tactility/time.h" +#include #include -#include #include #include @@ -14,11 +17,14 @@ #include #include -#include +#include #include #include +#include +#include #include +#include #include #include @@ -197,40 +203,81 @@ const char* getTaskState(const TaskStatus_t& task) { } } -void clearContainer(lv_obj_t* container) { - lv_obj_clean(container); -} - -void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task) { - auto* label = lv_label_create(parent); - const char* name = (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName; - lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task)); +const char* getTaskName(const TaskStatus_t& task) { + return (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName; } -void updateRtosTasks(lv_obj_t* parent) { - clearContainer(parent); +/** + * Updates the rows in place, so the list keeps its height and scroll position, and the selected task stays selected. + * @param[in] parent the list + * @param[in,out] taskNames the task name of each row + */ +void updateRtosTasks(lv_obj_t* parent, std::vector& taskNames) { + lv_obj_t* selected = lvgl_grid_navigation_get_focused(parent); + const int32_t selected_index = selected != nullptr ? lv_obj_get_index(selected) : -1; + const std::string selected_name = selected_index >= 0 && selected_index < static_cast(taskNames.size()) ? taskNames[selected_index] : ""; + const bool show_selection = selected != nullptr && lv_obj_has_state(selected, LV_STATE_FOCUS_KEY); UBaseType_t count = uxTaskGetNumberOfTasks(); auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); if (!tasks) { + lv_obj_clean(parent); + taskNames.clear(); auto* error_label = lv_label_create(parent); lv_label_set_text(error_label, "Failed to allocate memory for task list"); return; } uint32_t totalRuntime = 0; UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime); + std::sort(tasks, tasks + actual, [](const TaskStatus_t& a, const TaskStatus_t& b) { + return strcasecmp(getTaskName(a), getTaskName(b)) < 0; + }); - for (int i = 0; i < actual; ++i) { - addRtosTask(parent, tasks[i]); + taskNames.clear(); + for (UBaseType_t i = 0; i < actual; ++i) { + auto* row = i < lv_obj_get_child_count(parent) ? lv_obj_get_child(parent, static_cast(i)) : lv_label_create(parent); + lv_label_set_text_fmt(row, "%s (%s)", getTaskName(tasks[i]), getTaskState(tasks[i])); + // Keys and encoders move through the tasks, which scrolls the list + lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); + taskNames.emplace_back(getTaskName(tasks[i])); } - free(tasks); + while (lv_obj_get_child_count(parent) > actual) { + lv_obj_delete(lv_obj_get_child(parent, -1)); + } + + if (selected_index < 0 || taskNames.empty()) { + return; + } + // Follows the task, or keeps the position when the task is gone + auto it = std::ranges::find(taskNames, selected_name); + const auto index = it != taskNames.end() ? static_cast(it - taskNames.begin()) : std::min(selected_index, static_cast(taskNames.size()) - 1); + // Also re-applied when the row didn't change: deleting rows resets grid navigation's selection + auto* row = lv_obj_get_child(parent, index); + lv_gridnav_set_focused(parent, row, LV_ANIM_OFF); + const bool list_focused = lv_obj_get_group(parent) != nullptr && lv_group_get_focused(lv_obj_get_group(parent)) == parent; + if (!show_selection) { + lv_obj_remove_state(row, LV_STATE_FOCUS_KEY); + } + if (list_focused) { + lv_obj_scroll_to_view_recursive(row, LV_ANIM_OFF); + } else { + lv_obj_remove_state(row, LV_STATE_FOCUSED); + } } #endif lv_obj_t* createTab(lv_obj_t* tabview, const char* name) { auto* tab = lv_tabview_add_tab(tabview, name); + auto* tab_bar = lv_tabview_get_tab_bar(tabview); + auto* tab_button = lv_obj_get_child(tab_bar, static_cast(lv_obj_get_child_count(tab_bar)) - 1); + auto* shared_icon_font = lvgl_theme_is_compact() ? lvgl_get_shared_icon_default_font() : lvgl_get_shared_icon_large_font(); + lv_obj_set_style_text_font(tab_button, shared_icon_font, LV_STATE_DEFAULT); + // Square buttons, so the theme's round selection indicator fits the icon + const int32_t button_size = lv_font_get_line_height(shared_icon_font) + 2 * lv_obj_get_style_pad_top(tab_button, LV_PART_MAIN); + lv_obj_set_flex_grow(tab_button, 0); + lv_obj_set_size(tab_button, button_size, button_size); lv_obj_set_flex_flow(tab, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(tab, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(tab, 0, LV_STATE_DEFAULT); @@ -250,6 +297,8 @@ struct Context { MemoryBarWidgets systemStorageBar; lv_obj_t* tasksContainer = nullptr; + /** The task name of each row in tasksContainer */ + std::vector taskNames; lv_obj_t* psramContainer = nullptr; bool hasExternalMem = false; @@ -293,7 +342,7 @@ void updateStorage(Context* ctx) { void updateTasks(Context* ctx) { #if configUSE_TRACE_FACILITY if (ctx->tasksContainer) { - updateRtosTasks(ctx->tasksContainer); // Tasks tab: show state + updateRtosTasks(ctx->tasksContainer, ctx->taskNames); // Tasks tab: show state } #endif } @@ -303,6 +352,24 @@ void onBackPressed(lv_event_t* event) { app_event_emit_close(ctx->appInstanceId); } +#if configUSE_TRACE_FACILITY +void onTabChanged(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + auto* tabview = lv_event_get_target_obj(event); + auto* active_tab = lv_obj_get_child(lv_tabview_get_content(tabview), static_cast(lv_tabview_get_tab_active(tabview))); + const bool tasks_shown = active_tab == lv_obj_get_parent(ctx->tasksContainer); + auto* group = lv_group_get_default(); + if (group == nullptr) { + return; + } + if (tasks_shown && lv_obj_get_group(ctx->tasksContainer) == nullptr) { + lv_group_add_obj(group, ctx->tasksContainer); + } else if (!tasks_shown && lv_obj_get_group(ctx->tasksContainer) != nullptr) { + lv_group_remove_obj(ctx->tasksContainer); + } +} +#endif + void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); @@ -319,17 +386,25 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_grow(wrapper, 1); lv_obj_set_style_pad_all(wrapper, 0, LV_STATE_DEFAULT); + // Space between the screen edges and the rounded tab bar and content + const int32_t edge_space = lvgl_theme_is_compact() ? 2 : LV_DPX(8); + lv_obj_set_style_pad_hor(wrapper, edge_space, LV_STATE_DEFAULT); + lv_obj_set_style_pad_bottom(wrapper, edge_space, LV_STATE_DEFAULT); auto* tabview = lv_tabview_create(wrapper); lv_tabview_set_tab_bar_position(tabview, LV_DIR_LEFT); - auto tab_bar_width = 6 * lvgl_get_text_font_height(FONT_SIZE_DEFAULT); - lv_tabview_set_tab_bar_size(tabview, tab_bar_width); // Create tabs - auto* memory_tab = createTab(tabview, "Memory"); - auto* storage_tab = createTab(tabview, "Storage"); - auto* tasks_tab = createTab(tabview, "Tasks"); - auto* about_tab = createTab(tabview, "About"); + auto* memory_tab = createTab(tabview, LVGL_ICON_SHARED_MEMORY); + auto* storage_tab = createTab(tabview, LVGL_ICON_SHARED_HARD_DISK); + auto* tasks_tab = createTab(tabview, LVGL_ICON_SHARED_SELECT_WINDOW_2); + auto* about_tab = createTab(tabview, LVGL_ICON_SHARED_INFO); + + // As wide as the buttons, which are spread over the bar's height + auto* tab_bar = lv_tabview_get_tab_bar(tabview); + lv_obj_set_width(tab_bar, LV_SIZE_CONTENT); + lv_obj_set_style_pad_hor(tab_bar, lvgl_theme_is_compact() ? 2 : LV_DPX(12), LV_STATE_DEFAULT); + lv_obj_set_flex_align(tab_bar, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); // Memory tab content ctx->internalMemBar = createMemoryBar(memory_tab, "Internal"); @@ -370,6 +445,10 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_set_style_border_width(ctx->tasksContainer, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(ctx->tasksContainer, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_bg_opa(ctx->tasksContainer, 0, LV_STATE_DEFAULT); + lvgl_grid_navigation_add(ctx->tasksContainer); + // Only the shown tab's list can be focused, as focusing another tab's content would scroll to it + lv_group_remove_obj(ctx->tasksContainer); + lv_obj_add_event_cb(tabview, onTabChanged, LV_EVENT_VALUE_CHANGED, ctx); #endif // Build info diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index ce7f6941d..4b88cfa2e 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -12,11 +13,17 @@ #include #include +#include #include +#include #include +#include #include +#include +#include + namespace tt::app::timedatesettings { constexpr auto* TAG = "TimeDate"; @@ -25,13 +32,31 @@ extern const ::AppManifest manifest; namespace { +constexpr const char* DATE_FORMATS[] = { "MM/DD/YYYY", "DD/MM/YYYY", "YYYY-MM-DD", "YYYY/MM/DD" }; + struct Context { uint32_t appInstanceId; - lv_obj_t* timeZoneLabel = nullptr; - lv_obj_t* dateFormatDropdown = nullptr; + TaskEventGroup* eventGroup = nullptr; + uint32_t selectDateFormatBit = 0; uint32_t pendingTimeZoneDialogId = 0; + uint32_t dateFormatDialogId = 0; + // Valid while the window is shown + lv_obj_t* timeZoneLabel = nullptr; + lv_obj_t* dateFormatLabel = nullptr; }; +std::string getDateFormat() { + settings::SystemSettings sysSettings; + if (settings::loadSystemSettings(sysSettings)) { + for (const auto* format : DATE_FORMATS) { + if (sysSettings.dateFormat == format) { + return format; + } + } + } + return DATE_FORMATS[0]; +} + void onBackPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); @@ -49,20 +74,65 @@ void onTimeZonePressed(lv_event_t* event) { ctx->pendingTimeZoneDialogId = timezone::start(ctx->appInstanceId, true); } -void onDateFormatChanged(lv_event_t* event) { - auto* dropdown = static_cast(lv_event_get_target(event)); - auto index = lv_dropdown_get_selected(dropdown); +void onSelectDateFormatPressed(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + task_event_group_signal(ctx->eventGroup, ctx->selectDateFormatBit); +} - const char* dateFormats[] = {"MM/DD/YYYY", "DD/MM/YYYY", "YYYY-MM-DD", "YYYY/MM/DD"}; - std::string selected_format = dateFormats[index]; +void startDateFormatSelection(Context* ctx) { + if (ctx->dateFormatDialogId != 0) { + return; + } + const std::vector items(std::begin(DATE_FORMATS), std::end(DATE_FORMATS)); + ctx->dateFormatDialogId = selectiondialog::start(ctx->appInstanceId, "Date format", items); +} - settings::SystemSettings sysSettings; - if (settings::loadSystemSettings(sysSettings)) { - sysSettings.dateFormat = selected_format; - settings::saveSystemSettings(sysSettings); +void onDateFormatSelectionResult(Context* ctx, const AppEvent& event) { + ctx->dateFormatDialogId = 0; + // Other results mean that the dialog was dismissed + const int32_t selected = event.result.result; + if (selected >= 0 && selected < static_cast(std::size(DATE_FORMATS))) { + settings::SystemSettings sysSettings; + if (settings::loadSystemSettings(sysSettings)) { + sysSettings.dateFormat = DATE_FORMATS[selected]; + settings::saveSystemSettings(sysSettings); + } + lvgl_lock(); + if (ctx->dateFormatLabel != nullptr) { + lv_label_set_text(ctx->dateFormatLabel, getDateFormat().c_str()); + } + lvgl_unlock(); } } +/** + * Creates a title with the setting's value below it, which take the width that the row's button leaves. + * Texts that don't fit scroll. + * @return the value label + */ +lv_obj_t* createTitleAndValue(lv_obj_t* row, const char* title) { + auto* texts = lv_obj_create(row); + lv_obj_set_height(texts, LV_SIZE_CONTENT); + lv_obj_set_flex_grow(texts, 1); + lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(texts, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_row(texts, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(texts, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(texts, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(texts, LV_OBJ_FLAG_SCROLLABLE); + + auto* title_label = lv_label_create(texts); + lv_obj_set_width(title_label, LV_PCT(100)); + lv_label_set_long_mode(title_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); + lv_label_set_text(title_label, title); + + auto* value_label = lv_label_create(texts); + lv_obj_set_width(value_label, LV_PCT(100)); + lv_obj_set_style_text_font(value_label, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT); + lv_label_set_long_mode(value_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR); + return value_label; +} + void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); @@ -73,24 +143,32 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); + // The rows are transparent, so the card provides the background + auto* card = lvgl_card_create(main_wrapper); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + // 24-hour format toggle - auto* time_format_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_width(time_format_wrapper, LV_PCT(100)); - lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(time_format_wrapper, 8, 0); - lv_obj_set_style_border_width(time_format_wrapper, 0, 0); + auto* time_format_row = lv_obj_create(card); + lv_obj_set_size(time_format_row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(time_format_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(time_format_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_border_width(time_format_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(time_format_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(time_format_row, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(time_format_row, LV_OBJ_FLAG_SCROLLABLE); - auto* time_24h_label = lv_label_create(time_format_wrapper); + auto* time_24h_label = lv_label_create(time_format_row); lv_label_set_text(time_24h_label, "24-hour format"); - lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 4, 0); + lv_obj_set_flex_grow(time_24h_label, 1); - auto* time_24h_switch = lv_switch_create(time_format_wrapper); - lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0); + auto* time_24h_switch = lv_switch_create(time_format_row); lv_obj_add_event_cb(time_24h_switch, onTimeFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr); if (settings::isTimeFormat24Hour()) { lv_obj_add_state(time_24h_switch, LV_STATE_CHECKED); @@ -98,57 +176,51 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED); } - // Date format dropdown - - auto* date_format_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_width(date_format_wrapper, LV_PCT(100)); - lv_obj_set_height(date_format_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(date_format_wrapper, 8, 0); - lv_obj_set_style_border_width(date_format_wrapper, 0, 0); + // "Date format MM/DD/YYYY [Select]" - auto* date_format_label = lv_label_create(date_format_wrapper); - lv_label_set_text(date_format_label, "Date format"); - lv_obj_align(date_format_label, LV_ALIGN_LEFT_MID, 4, 0); - - ctx->dateFormatDropdown = lv_dropdown_create(date_format_wrapper); - lv_obj_set_width(ctx->dateFormatDropdown, 150); - lv_obj_align(ctx->dateFormatDropdown, LV_ALIGN_RIGHT_MID, 0, 0); - lv_dropdown_set_options(ctx->dateFormatDropdown, "MM/DD/YYYY\nDD/MM/YYYY\nYYYY-MM-DD\nYYYY/MM/DD"); - - settings::SystemSettings sysSettings; - if (settings::loadSystemSettings(sysSettings)) { - int index = 0; - if (sysSettings.dateFormat == "DD/MM/YYYY") index = 1; - else if (sysSettings.dateFormat == "YYYY-MM-DD") index = 2; - else if (sysSettings.dateFormat == "YYYY/MM/DD") index = 3; - lv_dropdown_set_selected(ctx->dateFormatDropdown, index); - } - lv_obj_add_event_cb(ctx->dateFormatDropdown, onDateFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr); + auto* date_format_row = lv_obj_create(card); + lv_obj_set_size(date_format_row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(date_format_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(date_format_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_border_width(date_format_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(date_format_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(date_format_row, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(date_format_row, LV_OBJ_FLAG_SCROLLABLE); - // Timezone selector + ctx->dateFormatLabel = createTitleAndValue(date_format_row, "Date format"); + lv_label_set_text(ctx->dateFormatLabel, getDateFormat().c_str()); - auto* timezone_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_width(timezone_wrapper, LV_PCT(100)); - lv_obj_set_height(timezone_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(timezone_wrapper, 8, 0); - lv_obj_set_style_border_width(timezone_wrapper, 0, 0); + auto* date_format_button = lv_button_create(date_format_row); + lv_label_set_text(lv_label_create(date_format_button), "Change"); + lv_obj_add_event_cb(date_format_button, onSelectDateFormatPressed, LV_EVENT_SHORT_CLICKED, ctx); - auto* timezone_label = lv_label_create(timezone_wrapper); - lv_label_set_text(timezone_label, "Timezone"); - lv_obj_align(timezone_label, LV_ALIGN_LEFT_MID, 4, 0); + // "Timezone Europe/Brussels [Change]" - auto* timezone_button = lv_button_create(timezone_wrapper); - lv_obj_set_width(timezone_button, 150); - lv_obj_align(timezone_button, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(timezone_button, onTimeZonePressed, LV_EVENT_SHORT_CLICKED, ctx); + auto* timezone_row = lv_obj_create(card); + lv_obj_set_size(timezone_row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(timezone_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(timezone_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_border_width(timezone_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_all(timezone_row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(timezone_row, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(timezone_row, LV_OBJ_FLAG_SCROLLABLE); - ctx->timeZoneLabel = lv_label_create(timezone_button); + ctx->timeZoneLabel = createTitleAndValue(timezone_row, "Timezone"); std::string timeZoneName = settings::getTimeZoneName(); if (timeZoneName.empty()) { timeZoneName = "not set"; } - lv_obj_center(ctx->timeZoneLabel); lv_label_set_text(ctx->timeZoneLabel, timeZoneName.c_str()); + + auto* timezone_button = lv_button_create(timezone_row); + lv_label_set_text(lv_label_create(timezone_button), "Change"); + lv_obj_add_event_cb(timezone_button, onTimeZonePressed, LV_EVENT_SHORT_CLICKED, ctx); +} + +void destroyWidgets(void* userData) { + auto* ctx = static_cast(userData); + ctx->timeZoneLabel = nullptr; + ctx->dateFormatLabel = nullptr; } int32_t appMain(int argc, char* argv[]) { @@ -158,15 +230,22 @@ int32_t appMain(int argc, char* argv[]) { TaskEventGroup event_group {}; task_event_group_construct(&event_group); + ctx.eventGroup = &event_group; + check(task_event_group_claim_bit(&event_group, &ctx.selectDateFormatBit) == ERROR_NONE); AppEventSubscription sub {}; check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); + WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { - task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + uint32_t flags = 0; + task_event_group_wait_any(&event_group, &flags, portMAX_DELAY); + + if (flags & ctx.selectDateFormatBit) { + startDateFormatSelection(&ctx); + } AppEvent event {}; while (app_event_poll(&sub, &event) == ERROR_NONE) { @@ -175,14 +254,18 @@ int32_t appMain(int argc, char* argv[]) { shouldClose = true; break; case APP_EVENT_RESULT: - if (event.result.launch_id == ctx.pendingTimeZoneDialogId) { + if (event.result.launch_id == ctx.dateFormatDialogId) { + onDateFormatSelectionResult(&ctx, event); + } else if (event.result.launch_id == ctx.pendingTimeZoneDialogId) { ctx.pendingTimeZoneDialogId = 0; if (event.result.result == 0 /* Ok */) { const auto name = timezone::getLastName(); LOG_I(TAG, "Result name=%s code=%s", name.c_str(), timezone::getLastCode().c_str()); if (!name.empty()) { lvgl_lock(); - lv_label_set_text(ctx.timeZoneLabel, name.c_str()); + if (ctx.timeZoneLabel != nullptr) { + lv_label_set_text(ctx.timeZoneLabel, name.c_str()); + } lvgl_unlock(); } } diff --git a/Tactility/Source/app/trackball/TrackballSettings.cpp b/Tactility/Source/app/trackball/TrackballSettings.cpp index e5d2249a2..49e068842 100644 --- a/Tactility/Source/app/trackball/TrackballSettings.cpp +++ b/Tactility/Source/app/trackball/TrackballSettings.cpp @@ -3,6 +3,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -25,14 +29,16 @@ extern const ::AppManifest manifest; constexpr auto* TAG = "TrackballSettings"; -// Convert mode to dropdown index (dropdown order: Keys=0, Pointer=1) -static uint32_t modeToDropdownIndex(LvglTrackballMode mode) { - switch (mode) { - case LVGL_TRACKBALL_MODE_KEYS: return 0; - case LVGL_TRACKBALL_MODE_POINTER: return 1; - } - return 0; // default to Keys -} +struct ModeOption { + LvglTrackballMode mode; + const char* name; +}; + +// The order of the mode chips +constexpr ModeOption MODE_OPTIONS[] = { + { LVGL_TRACKBALL_MODE_KEYS, "Keys" }, + { LVGL_TRACKBALL_MODE_POINTER, "Pointer" }, +}; static lv_indev_t* findFirstTrackballIndev() { lv_indev_t* indev = lv_indev_get_next(nullptr); @@ -64,8 +70,14 @@ struct Context { // to whatever is already attached. lv_indev_t* trackballIndev = nullptr; lv_obj_t* switchTrackball = nullptr; - lv_obj_t* trackballModeDropdown = nullptr; + // Shown while the trackball is enabled + lv_obj_t* settingsCard = nullptr; + lv_obj_t* modeChips = nullptr; + // Shown in keys mode + lv_obj_t* keySensitivityGroup = nullptr; lv_obj_t* keySensitivitySlider = nullptr; + // Shown in pointer mode + lv_obj_t* pointerSensitivityGroup = nullptr; lv_obj_t* pointerSensitivitySlider = nullptr; }; @@ -87,47 +99,37 @@ void applyLive(Context* ctx) { lvgl_unlock(); } +/** Shows the settings of the enabled trackball and its mode */ +void updateWidgets(Context* ctx) { + lv_obj_set_flag(ctx->settingsCard, LV_OBJ_FLAG_HIDDEN, !ctx->tbSettings.enabled); + const uint32_t chip_count = lv_obj_get_child_count(ctx->modeChips); + for (uint32_t i = 0; i < chip_count; i++) { + lv_obj_set_state(lv_obj_get_child(ctx->modeChips, static_cast(i)), LV_STATE_CHECKED, MODE_OPTIONS[i].mode == ctx->tbSettings.mode); + } + lv_obj_set_flag(ctx->keySensitivityGroup, LV_OBJ_FLAG_HIDDEN, ctx->tbSettings.mode != LVGL_TRACKBALL_MODE_KEYS); + lv_obj_set_flag(ctx->pointerSensitivityGroup, LV_OBJ_FLAG_HIDDEN, ctx->tbSettings.mode != LVGL_TRACKBALL_MODE_POINTER); +} + void onTrackballSwitch(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); - bool enabled = lv_obj_has_state(ctx->switchTrackball, LV_STATE_CHECKED); - ctx->tbSettings.enabled = enabled; + ctx->tbSettings.enabled = lv_obj_has_state(ctx->switchTrackball, LV_STATE_CHECKED); ctx->updated = true; applyLive(ctx); - - // Enable/disable controls based on trackball state - if (enabled) { - if (ctx->trackballModeDropdown) lv_obj_clear_state(ctx->trackballModeDropdown, LV_STATE_DISABLED); - if (ctx->keySensitivitySlider) lv_obj_clear_state(ctx->keySensitivitySlider, LV_STATE_DISABLED); - if (ctx->pointerSensitivitySlider) lv_obj_clear_state(ctx->pointerSensitivitySlider, LV_STATE_DISABLED); - } else { - if (ctx->trackballModeDropdown) lv_obj_add_state(ctx->trackballModeDropdown, LV_STATE_DISABLED); - if (ctx->keySensitivitySlider) lv_obj_add_state(ctx->keySensitivitySlider, LV_STATE_DISABLED); - if (ctx->pointerSensitivitySlider) lv_obj_add_state(ctx->pointerSensitivitySlider, LV_STATE_DISABLED); - } + updateWidgets(ctx); } -void onTrackballModeChanged(lv_event_t* e) { +void onModeChipPressed(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); - uint32_t selected = lv_dropdown_get_selected(ctx->trackballModeDropdown); - - // Validate selection matches expected enum values (dropdown order: Keys=0, Pointer=1) - LvglTrackballMode mode; - switch (selected) { - case 0: mode = LVGL_TRACKBALL_MODE_KEYS; break; - case 1: mode = LVGL_TRACKBALL_MODE_POINTER; break; - default: return; // Invalid selection, ignore - } - - ctx->tbSettings.mode = mode; + ctx->tbSettings.mode = MODE_OPTIONS[lv_obj_get_index(lv_event_get_target_obj(e))].mode; ctx->updated = true; - // Apply mode change immediately applyLive(ctx); + updateWidgets(ctx); } void onKeySensitivityChanged(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); - int32_t value = lv_slider_get_value(ctx->keySensitivitySlider); + int32_t value = lvgl_sliderbox_get_value(ctx->keySensitivitySlider); ctx->tbSettings.key_sensitivity = static_cast(value); ctx->updated = true; @@ -137,7 +139,7 @@ void onKeySensitivityChanged(lv_event_t* e) { void onPointerSensitivityChanged(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); - int32_t value = lv_slider_get_value(ctx->pointerSensitivitySlider); + int32_t value = lvgl_sliderbox_get_value(ctx->pointerSensitivitySlider); ctx->tbSettings.pointer_sensitivity = static_cast(value); ctx->updated = true; @@ -145,6 +147,24 @@ void onPointerSensitivityChanged(lv_event_t* e) { applyLive(ctx); } +/** A transparent column with a title and a speed slider */ +lv_obj_t* createSpeedGroup(lv_obj_t* parent, const char* title, int32_t value, lv_event_cb_t onChanged, Context* ctx, lv_obj_t** outSlider) { + auto* group = lv_obj_create(parent); + lv_obj_set_size(group, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(group, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(group, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(group, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(group, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(group, LV_OBJ_FLAG_SCROLLABLE); + + lv_label_set_text(lv_label_create(group), title); + + *outSlider = lvgl_sliderbox_create(group, 1, 10, 1, value); + lv_obj_set_width(*outSlider, LV_PCT(100)); + lvgl_sliderbox_add_value_changed_cb(*outSlider, onChanged, ctx); + return group; +} + void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); @@ -161,12 +181,13 @@ void createWidgets(lv_obj_t* parent, void* userData) { if (ctx->trackballIndev == nullptr) { auto* wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_grow(wrapper, 1); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - auto* label = lv_label_create(wrapper); - lv_label_set_text(label, "No trackball device found"); + auto* card = lvgl_card_create(wrapper); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + lv_label_set_text(lv_label_create(card), "No trackball device found."); return; } @@ -180,72 +201,39 @@ void createWidgets(lv_obj_t* parent, void* userData) { if (ctx->tbSettings.enabled) lv_obj_add_state(ctx->switchTrackball, LV_STATE_CHECKED); auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); - // Trackball mode dropdown - auto* tb_mode_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(tb_mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(tb_mode_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(tb_mode_wrapper, 0, LV_STATE_DEFAULT); - - auto* tb_mode_label = lv_label_create(tb_mode_wrapper); - lv_label_set_text(tb_mode_label, "Mode"); - lv_obj_align(tb_mode_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->trackballModeDropdown = lv_dropdown_create(tb_mode_wrapper); - lv_dropdown_set_options(ctx->trackballModeDropdown, "Keys\nPointer"); - lv_obj_align(ctx->trackballModeDropdown, LV_ALIGN_RIGHT_MID, 0, 0); - lv_dropdown_set_selected(ctx->trackballModeDropdown, modeToDropdownIndex(ctx->tbSettings.mode)); - lv_obj_add_event_cb(ctx->trackballModeDropdown, onTrackballModeChanged, LV_EVENT_VALUE_CHANGED, ctx); - - // Disable dropdown if trackball is disabled - if (!ctx->tbSettings.enabled) { - lv_obj_add_state(ctx->trackballModeDropdown, LV_STATE_DISABLED); + ctx->settingsCard = lvgl_card_create(main_wrapper); + lv_obj_set_size(ctx->settingsCard, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(ctx->settingsCard, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(ctx->settingsCard, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + // Mode: the chips are centered while they fit, and scroll when they don't + ctx->modeChips = lv_obj_create(ctx->settingsCard); + lv_obj_set_size(ctx->modeChips, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_max_width(ctx->modeChips, LV_PCT(100), LV_STATE_DEFAULT); + lv_obj_set_flex_flow(ctx->modeChips, LV_FLEX_FLOW_ROW); + lv_obj_set_scroll_dir(ctx->modeChips, LV_DIR_HOR); + lv_obj_set_style_bg_opa(ctx->modeChips, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(ctx->modeChips, 0, LV_STATE_DEFAULT); + // The chips' margins leave room for their focus rings and space them apart + lv_obj_set_style_pad_all(ctx->modeChips, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(ctx->modeChips, 0, LV_STATE_DEFAULT); + for (const auto& option : MODE_OPTIONS) { + auto* chip = lvgl_chip_create(ctx->modeChips); + lv_label_set_text(lv_label_create(chip), option.name); + lv_obj_add_event_cb(chip, onModeChipPressed, LV_EVENT_SHORT_CLICKED, ctx); } + lvgl_grid_navigation_add(ctx->modeChips); - // Key sensitivity slider - auto* key_sens_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(key_sens_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_hor(key_sens_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(key_sens_wrapper, 0, LV_STATE_DEFAULT); - - auto* key_sens_label = lv_label_create(key_sens_wrapper); - lv_label_set_text(key_sens_label, "Key Speed"); - lv_obj_align(key_sens_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->keySensitivitySlider = lv_slider_create(key_sens_wrapper); - lv_slider_set_range(ctx->keySensitivitySlider, 1, 10); - lv_slider_set_value(ctx->keySensitivitySlider, ctx->tbSettings.key_sensitivity, LV_ANIM_OFF); - lv_obj_set_width(ctx->keySensitivitySlider, LV_PCT(50)); - lv_obj_align(ctx->keySensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(ctx->keySensitivitySlider, onKeySensitivityChanged, LV_EVENT_VALUE_CHANGED, ctx); - - if (!ctx->tbSettings.enabled) { - lv_obj_add_state(ctx->keySensitivitySlider, LV_STATE_DISABLED); - } + // The speed of the selected mode + ctx->keySensitivityGroup = createSpeedGroup(ctx->settingsCard, "Key speed", ctx->tbSettings.key_sensitivity, onKeySensitivityChanged, ctx, &ctx->keySensitivitySlider); + ctx->pointerSensitivityGroup = createSpeedGroup(ctx->settingsCard, "Pointer speed", ctx->tbSettings.pointer_sensitivity, onPointerSensitivityChanged, ctx, &ctx->pointerSensitivitySlider); - // Pointer sensitivity slider - auto* ptr_sens_wrapper = lv_obj_create(main_wrapper); - 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); - - auto* ptr_sens_label = lv_label_create(ptr_sens_wrapper); - lv_label_set_text(ptr_sens_label, "Pointer Speed"); - lv_obj_align(ptr_sens_label, LV_ALIGN_LEFT_MID, 0, 0); - - ctx->pointerSensitivitySlider = lv_slider_create(ptr_sens_wrapper); - lv_slider_set_range(ctx->pointerSensitivitySlider, 1, 10); - lv_slider_set_value(ctx->pointerSensitivitySlider, ctx->tbSettings.pointer_sensitivity, LV_ANIM_OFF); - lv_obj_set_width(ctx->pointerSensitivitySlider, LV_PCT(50)); - lv_obj_align(ctx->pointerSensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(ctx->pointerSensitivitySlider, onPointerSensitivityChanged, LV_EVENT_VALUE_CHANGED, ctx); - - if (!ctx->tbSettings.enabled) { - lv_obj_add_state(ctx->pointerSensitivitySlider, LV_STATE_DISABLED); - } + updateWidgets(ctx); } // Mirrors the old onHide() behaviour: persist the settings (regardless of whether the app is diff --git a/Tactility/Source/app/webserversettings/WebServerSettings.cpp b/Tactility/Source/app/webserversettings/WebServerSettings.cpp index 461de78f0..e7fec918f 100644 --- a/Tactility/Source/app/webserversettings/WebServerSettings.cpp +++ b/Tactility/Source/app/webserversettings/WebServerSettings.cpp @@ -16,6 +16,9 @@ #include #include +#include +#include +#include #include namespace tt::app::webserversettings { @@ -33,7 +36,15 @@ struct Context { settings::webserver::WebServerSettings originalSettings; bool updated = false; bool wifiSettingsChanged = false; - lv_obj_t* dropdownWifiMode = nullptr; + lv_obj_t* wifiModeChips = nullptr; + // Shown in access point mode + lv_obj_t* apGroup = nullptr; + // Shown when the access point isn't an open network + lv_obj_t* apPasswordRow = nullptr; + // Shown when authentication is required + lv_obj_t* authCard = nullptr; + // Shown when the web server is enabled + lv_obj_t* urlCard = nullptr; lv_obj_t* textAreaApPassword = nullptr; lv_obj_t* switchApOpenNetwork = nullptr; lv_obj_t* switchWebServerEnabled = nullptr; @@ -53,10 +64,34 @@ void onBackPressed(lv_event_t* event) { app_event_emit_close(ctx->appInstanceId); } -void onWifiModeChanged(lv_event_t* e) { +/** Shows the widgets of the enabled features. Requires the LVGL lock. */ +void updateVisibility(Context* ctx, bool accessPoint, bool openNetwork, bool authEnabled, bool serverEnabled) { + const uint32_t chip_count = lv_obj_get_child_count(ctx->wifiModeChips); + for (uint32_t i = 0; i < chip_count; i++) { + lv_obj_set_state(lv_obj_get_child(ctx->wifiModeChips, static_cast(i)), LV_STATE_CHECKED, (i == 1) == accessPoint); + } + lv_obj_set_flag(ctx->apGroup, LV_OBJ_FLAG_HIDDEN, !accessPoint); + lv_obj_set_flag(ctx->apPasswordRow, LV_OBJ_FLAG_HIDDEN, openNetwork); + lv_obj_set_flag(ctx->authCard, LV_OBJ_FLAG_HIDDEN, !authEnabled); + lv_obj_set_flag(ctx->urlCard, LV_OBJ_FLAG_HIDDEN, !serverEnabled); +} + +void updateVisibility(Context* ctx) { + updateVisibility(ctx, + lv_obj_has_state(lv_obj_get_child(ctx->wifiModeChips, 1), LV_STATE_CHECKED), + lv_obj_has_state(ctx->switchApOpenNetwork, LV_STATE_CHECKED), + lv_obj_has_state(ctx->switchWebServerAuthEnabled, LV_STATE_CHECKED), + lv_obj_has_state(ctx->switchWebServerEnabled, LV_STATE_CHECKED)); +} + +void onWifiModeChipPressed(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); - auto* dropdown = static_cast(lv_event_get_target(e)); - auto index = lv_dropdown_get_selected(dropdown); + // Chip 0 is station mode, chip 1 is access point mode + const uint32_t index = lv_obj_get_index(lv_event_get_target_obj(e)); + updateVisibility(ctx, index == 1, + lv_obj_has_state(ctx->switchApOpenNetwork, LV_STATE_CHECKED), + lv_obj_has_state(ctx->switchWebServerAuthEnabled, LV_STATE_CHECKED), + lv_obj_has_state(ctx->switchWebServerEnabled, LV_STATE_CHECKED)); getMainDispatcher().dispatch([ctx, index] { ctx->wsSettings.wifiMode = static_cast(index); ctx->updated = true; @@ -70,6 +105,7 @@ void onWifiModeChanged(lv_event_t* e) { void onWebServerEnabledSwitch(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); bool enabled = lv_obj_has_state(ctx->switchWebServerEnabled, LV_STATE_CHECKED); + updateVisibility(ctx); getMainDispatcher().dispatch([ctx, enabled] { ctx->wsSettings.webServerEnabled = enabled; ctx->updated = true; @@ -91,22 +127,7 @@ void onWebServerEnabledSwitch(lv_event_t* e) { void onWebServerAuthEnabledSwitch(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); bool enabled = lv_obj_has_state(ctx->switchWebServerAuthEnabled, LV_STATE_CHECKED); - - if (ctx->textAreaWebServerUsername && ctx->textAreaWebServerPassword) { - if (enabled) { - lv_obj_remove_state(ctx->textAreaWebServerUsername, LV_STATE_DISABLED); - lv_obj_add_flag(ctx->textAreaWebServerUsername, LV_OBJ_FLAG_CLICKABLE); - - lv_obj_remove_state(ctx->textAreaWebServerPassword, LV_STATE_DISABLED); - lv_obj_add_flag(ctx->textAreaWebServerPassword, LV_OBJ_FLAG_CLICKABLE); - } else { - lv_obj_add_state(ctx->textAreaWebServerUsername, LV_STATE_DISABLED); - lv_obj_remove_flag(ctx->textAreaWebServerUsername, LV_OBJ_FLAG_CLICKABLE); - - lv_obj_add_state(ctx->textAreaWebServerPassword, LV_STATE_DISABLED); - lv_obj_remove_flag(ctx->textAreaWebServerPassword, LV_OBJ_FLAG_CLICKABLE); - } - } + updateVisibility(ctx); getMainDispatcher().dispatch([ctx, enabled] { ctx->wsSettings.webServerAuthEnabled = enabled; @@ -132,16 +153,7 @@ void onApPasswordChanged(lv_event_t* e) { void onApOpenNetworkSwitch(lv_event_t* e) { auto* ctx = static_cast(lv_event_get_user_data(e)); bool openNetwork = lv_obj_has_state(ctx->switchApOpenNetwork, LV_STATE_CHECKED); - - if (ctx->textAreaApPassword) { - if (openNetwork) { - lv_obj_add_state(ctx->textAreaApPassword, LV_STATE_DISABLED); - lv_obj_remove_flag(ctx->textAreaApPassword, LV_OBJ_FLAG_CLICKABLE); - } else { - lv_obj_remove_state(ctx->textAreaApPassword, LV_STATE_DISABLED); - lv_obj_add_flag(ctx->textAreaApPassword, LV_OBJ_FLAG_CLICKABLE); - } - } + updateVisibility(ctx); getMainDispatcher().dispatch([ctx, openNetwork] { ctx->wsSettings.apOpenNetwork = openNetwork; @@ -187,6 +199,51 @@ void updateUrlDisplay(Context* ctx) { lv_label_set_text(ctx->labelUrlValue, url.c_str()); } +lv_obj_t* createCard(lv_obj_t* parent) { + 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; +} + +/** A transparent column of rows, to show or hide them together */ +lv_obj_t* createGroup(lv_obj_t* parent) { + auto* group = lv_obj_create(parent); + lv_obj_set_size(group, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(group, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(group, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(group, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(group, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(group, LV_OBJ_FLAG_SCROLLABLE); + return group; +} + +/** A transparent row: "Title [content]" */ +lv_obj_t* createRow(lv_obj_t* parent, const char* title) { + auto* row = lv_obj_create(parent); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + auto* label = lv_label_create(row); + lv_label_set_text(label, title); + lv_obj_set_flex_grow(label, 1); + return row; +} + +lv_obj_t* createTextArea(lv_obj_t* row, const char* text, uint32_t maxLength, bool password) { + auto* text_area = lv_textarea_create(row); + lv_obj_set_width(text_area, LV_PCT(50)); + lv_textarea_set_one_line(text_area, true); + lv_textarea_set_max_length(text_area, maxLength); + lv_textarea_set_password_mode(text_area, password); + lv_textarea_set_text(text_area, text); + return text_area; +} + void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); @@ -205,144 +262,80 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_add_event_cb(ctx->switchWebServerEnabled, onWebServerEnabledSwitch, LV_EVENT_VALUE_CHANGED, ctx); auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); - // WiFi Mode dropdown - auto* wifi_mode_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(wifi_mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(wifi_mode_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(wifi_mode_wrapper, 0, LV_STATE_DEFAULT); - auto* wifi_mode_label = lv_label_create(wifi_mode_wrapper); - lv_label_set_text(wifi_mode_label, "WiFi Mode"); - lv_obj_align(wifi_mode_label, LV_ALIGN_LEFT_MID, 0, 0); - ctx->dropdownWifiMode = lv_dropdown_create(wifi_mode_wrapper); - lv_obj_align(ctx->dropdownWifiMode, LV_ALIGN_RIGHT_MID, 0, 0); - lv_dropdown_set_options(ctx->dropdownWifiMode, "Station\nAccess Point"); - lv_dropdown_set_selected(ctx->dropdownWifiMode, static_cast(ctx->wsSettings.wifiMode)); - lv_obj_add_event_cb(ctx->dropdownWifiMode, onWifiModeChanged, LV_EVENT_VALUE_CHANGED, ctx); - - // AP Open Network toggle - auto* ap_open_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(ap_open_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(ap_open_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ap_open_wrapper, 0, LV_STATE_DEFAULT); - auto* ap_open_label = lv_label_create(ap_open_wrapper); - lv_label_set_text(ap_open_label, "AP Open Network"); - lv_obj_align(ap_open_label, LV_ALIGN_LEFT_MID, 0, 0); - ctx->switchApOpenNetwork = lv_switch_create(ap_open_wrapper); + // Address, while the web server is enabled + ctx->urlCard = createCard(main_wrapper); + ctx->labelUrl = lv_label_create(ctx->urlCard); + lv_label_set_text(ctx->labelUrl, "Web server address"); + ctx->labelUrlValue = lv_label_create(ctx->urlCard); + updateUrlDisplay(ctx); + + // WiFi mode, with the access point settings in access point mode + lv_label_set_text(lv_label_create(main_wrapper), "WiFi mode"); + auto* wifi_card = createCard(main_wrapper); + lv_obj_set_flex_align(wifi_card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + // The chips are centered while they fit, and scroll when they don't + ctx->wifiModeChips = lv_obj_create(wifi_card); + lv_obj_set_size(ctx->wifiModeChips, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_style_max_width(ctx->wifiModeChips, LV_PCT(100), LV_STATE_DEFAULT); + lv_obj_set_flex_flow(ctx->wifiModeChips, LV_FLEX_FLOW_ROW); + lv_obj_set_scroll_dir(ctx->wifiModeChips, LV_DIR_HOR); + lv_obj_set_style_bg_opa(ctx->wifiModeChips, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(ctx->wifiModeChips, 0, LV_STATE_DEFAULT); + // The chips' margins leave room for their focus rings and space them apart + lv_obj_set_style_pad_all(ctx->wifiModeChips, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_column(ctx->wifiModeChips, 0, LV_STATE_DEFAULT); + // In settings::webserver::WiFiMode order + for (const char* name : { "Station", "Access point" }) { + auto* chip = lvgl_chip_create(ctx->wifiModeChips); + lv_label_set_text(lv_label_create(chip), name); + lv_obj_add_event_cb(chip, onWifiModeChipPressed, LV_EVENT_SHORT_CLICKED, ctx); + } + lvgl_grid_navigation_add(ctx->wifiModeChips); + + ctx->apGroup = createGroup(wifi_card); + auto* ap_open_row = createRow(ctx->apGroup, "Open network"); + ctx->switchApOpenNetwork = lv_switch_create(ap_open_row); if (ctx->wsSettings.apOpenNetwork) lv_obj_add_state(ctx->switchApOpenNetwork, LV_STATE_CHECKED); - lv_obj_align(ctx->switchApOpenNetwork, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->switchApOpenNetwork, onApOpenNetworkSwitch, LV_EVENT_VALUE_CHANGED, ctx); - // AP Password - auto* ap_pass_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(ap_pass_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(ap_pass_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ap_pass_wrapper, 0, LV_STATE_DEFAULT); - auto* ap_pass_label = lv_label_create(ap_pass_wrapper); - lv_label_set_text(ap_pass_label, "AP Password"); - lv_obj_align(ap_pass_label, LV_ALIGN_LEFT_MID, 0, 0); - ctx->textAreaApPassword = lv_textarea_create(ap_pass_wrapper); - lv_obj_set_width(ctx->textAreaApPassword, 120); - lv_obj_align(ctx->textAreaApPassword, LV_ALIGN_RIGHT_MID, 0, 0); - lv_textarea_set_one_line(ctx->textAreaApPassword, true); - lv_textarea_set_max_length(ctx->textAreaApPassword, 64); - lv_textarea_set_password_mode(ctx->textAreaApPassword, true); - lv_textarea_set_text(ctx->textAreaApPassword, ctx->wsSettings.apPassword.c_str()); + ctx->apPasswordRow = createRow(ctx->apGroup, "Password"); + ctx->textAreaApPassword = createTextArea(ctx->apPasswordRow, ctx->wsSettings.apPassword.c_str(), 64, true); lv_obj_add_event_cb(ctx->textAreaApPassword, onApPasswordChanged, LV_EVENT_VALUE_CHANGED, ctx); - // Disable password field if open network is enabled - if (ctx->wsSettings.apOpenNetwork) { - lv_obj_add_state(ctx->textAreaApPassword, LV_STATE_DISABLED); - lv_obj_remove_flag(ctx->textAreaApPassword, LV_OBJ_FLAG_CLICKABLE); - } - // Web Server Authentication Enable toggle - auto* ws_auth_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(ws_auth_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(ws_auth_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ws_auth_wrapper, 0, LV_STATE_DEFAULT); - auto* ws_auth_label = lv_label_create(ws_auth_wrapper); - lv_label_set_text(ws_auth_label, "Require Authentication"); - lv_obj_align(ws_auth_label, LV_ALIGN_LEFT_MID, 0, 0); - ctx->switchWebServerAuthEnabled = lv_switch_create(ws_auth_wrapper); + // Authentication, with the credentials while it's required + auto* auth_row = createRow(main_wrapper, "Require authentication"); + ctx->switchWebServerAuthEnabled = lv_switch_create(auth_row); if (ctx->wsSettings.webServerAuthEnabled) lv_obj_add_state(ctx->switchWebServerAuthEnabled, LV_STATE_CHECKED); - lv_obj_align(ctx->switchWebServerAuthEnabled, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->switchWebServerAuthEnabled, onWebServerAuthEnabledSwitch, LV_EVENT_VALUE_CHANGED, ctx); - // WebServer Username - auto* ws_user_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(ws_user_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(ws_user_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ws_user_wrapper, 0, LV_STATE_DEFAULT); - auto* ws_user_label = lv_label_create(ws_user_wrapper); - lv_label_set_text(ws_user_label, "Username"); - lv_obj_align(ws_user_label, LV_ALIGN_LEFT_MID, 0, 0); - ctx->textAreaWebServerUsername = lv_textarea_create(ws_user_wrapper); - if (!ctx->wsSettings.webServerAuthEnabled) { - lv_obj_add_state(ctx->textAreaWebServerUsername, LV_STATE_DISABLED); - lv_obj_remove_flag(ctx->textAreaWebServerUsername, LV_OBJ_FLAG_CLICKABLE); - } - lv_obj_set_width(ctx->textAreaWebServerUsername, 120); - lv_obj_align(ctx->textAreaWebServerUsername, LV_ALIGN_RIGHT_MID, 0, 0); - lv_textarea_set_one_line(ctx->textAreaWebServerUsername, true); - lv_textarea_set_max_length(ctx->textAreaWebServerUsername, 32); - lv_textarea_set_text(ctx->textAreaWebServerUsername, ctx->wsSettings.webServerUsername.c_str()); + ctx->authCard = createCard(main_wrapper); + auto* user_row = createRow(ctx->authCard, "Username"); + ctx->textAreaWebServerUsername = createTextArea(user_row, ctx->wsSettings.webServerUsername.c_str(), 32, false); lv_obj_add_event_cb(ctx->textAreaWebServerUsername, onCredentialChanged, LV_EVENT_VALUE_CHANGED, ctx); - - // WebServer Password - auto* ws_pass_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(ws_pass_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(ws_pass_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ws_pass_wrapper, 0, LV_STATE_DEFAULT); - auto* ws_pass_label = lv_label_create(ws_pass_wrapper); - lv_label_set_text(ws_pass_label, "Password"); - lv_obj_align(ws_pass_label, LV_ALIGN_LEFT_MID, 0, 0); - ctx->textAreaWebServerPassword = lv_textarea_create(ws_pass_wrapper); - if (!ctx->wsSettings.webServerAuthEnabled) { - lv_obj_add_state(ctx->textAreaWebServerPassword, LV_STATE_DISABLED); - lv_obj_remove_flag(ctx->textAreaWebServerPassword, LV_OBJ_FLAG_CLICKABLE); - } - lv_obj_set_width(ctx->textAreaWebServerPassword, 120); - lv_obj_align(ctx->textAreaWebServerPassword, LV_ALIGN_RIGHT_MID, 0, 0); - lv_textarea_set_one_line(ctx->textAreaWebServerPassword, true); - lv_textarea_set_max_length(ctx->textAreaWebServerPassword, 64); - lv_textarea_set_password_mode(ctx->textAreaWebServerPassword, true); - lv_textarea_set_text(ctx->textAreaWebServerPassword, ctx->wsSettings.webServerPassword.c_str()); + auto* password_row = createRow(ctx->authCard, "Password"); + ctx->textAreaWebServerPassword = createTextArea(password_row, ctx->wsSettings.webServerPassword.c_str(), 64, true); lv_obj_add_event_cb(ctx->textAreaWebServerPassword, onCredentialChanged, LV_EVENT_VALUE_CHANGED, ctx); - // URL Display - auto* url_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(url_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(url_wrapper, 10, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(url_wrapper, 1, LV_STATE_DEFAULT); - lv_obj_set_flex_flow(url_wrapper, LV_FLEX_FLOW_COLUMN); - lv_obj_set_style_flex_cross_place(url_wrapper, LV_FLEX_ALIGN_START, 0); - - ctx->labelUrl = lv_label_create(url_wrapper); - lv_label_set_text(ctx->labelUrl, "Web Server URL:"); - - ctx->labelUrlValue = lv_label_create(url_wrapper); - if (lv_display_get_color_format(lv_obj_get_display(parent)) == LV_COLOR_FORMAT_L8) { - lv_obj_set_style_text_color(ctx->labelUrlValue, lv_theme_get_color_secondary(ctx->labelUrlValue), LV_PART_MAIN); - } else { - lv_obj_set_style_text_color(ctx->labelUrlValue, lv_palette_main(LV_PALETTE_BLUE), 0); - } - - updateUrlDisplay(ctx); - // Info text auto* info_label = lv_label_create(main_wrapper); lv_label_set_long_mode(info_label, LV_LABEL_LONG_WRAP); - lv_obj_set_width(info_label, LV_PCT(95)); - if (lv_display_get_color_format(lv_obj_get_display(parent)) != LV_COLOR_FORMAT_L8) { - lv_obj_set_style_text_color(info_label, lv_palette_main(LV_PALETTE_GREY), 0); - } + lv_obj_set_width(info_label, LV_PCT(100)); lv_label_set_text(info_label, "WiFi Station credentials are managed separately.\n" "Use the WiFi menu to connect to networks.\n\n" "AP mode uses the password configured above."); + + updateVisibility(ctx, + ctx->wsSettings.wifiMode == settings::webserver::WiFiMode::AccessPoint, + ctx->wsSettings.apOpenNetwork, + ctx->wsSettings.webServerAuthEnabled, + ctx->wsSettings.webServerEnabled); } int32_t appMain(int argc, char* argv[]) { diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 11b1197c4..786cc7e5f 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -255,6 +255,9 @@ void createWidgets(lv_obj_t* parent, void* userData) { ctx->autoConnectWrapper = lv_obj_create(wrapper); lv_obj_set_size(ctx->autoConnectWrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(ctx->autoConnectWrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(ctx->autoConnectWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_SCROLLABLE); lvgl::obj_set_style_bg_invisible(ctx->autoConnectWrapper); lv_obj_set_style_pad_all(ctx->autoConnectWrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(ctx->autoConnectWrapper, 0, LV_STATE_DEFAULT); @@ -262,11 +265,10 @@ void createWidgets(lv_obj_t* parent, void* userData) { auto* auto_connect_label = lv_label_create(ctx->autoConnectWrapper); lv_label_set_text(auto_connect_label, "Auto-connect"); - lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(auto_connect_label, 1); ctx->autoConnectSwitch = lv_switch_create(ctx->autoConnectWrapper); lv_obj_add_event_cb(ctx->autoConnectSwitch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx); - lv_obj_align(ctx->autoConnectSwitch, LV_ALIGN_RIGHT_MID, 0, 0); } int32_t appMain(int argc, char* argv[]) { diff --git a/Tactility/Source/app/wificonnect/WifiConnect.cpp b/Tactility/Source/app/wificonnect/WifiConnect.cpp index f0961b2c3..0380cd791 100644 --- a/Tactility/Source/app/wificonnect/WifiConnect.cpp +++ b/Tactility/Source/app/wificonnect/WifiConnect.cpp @@ -13,6 +13,8 @@ #include #include +#include +#include #include #include @@ -49,7 +51,7 @@ struct Context { lv_obj_t* password_textarea = nullptr; lv_obj_t* password_error = nullptr; lv_obj_t* connect_button = nullptr; - lv_obj_t* remember_switch = nullptr; + lv_obj_t* remember_checkbox = nullptr; lv_obj_t* connecting_spinner = nullptr; lv_obj_t* connection_error = nullptr; @@ -114,13 +116,13 @@ void setLoading(Context* ctx, bool loading) { lv_obj_remove_flag(ctx->connecting_spinner, LV_OBJ_FLAG_HIDDEN); lv_obj_add_state(ctx->password_textarea, LV_STATE_DISABLED); lv_obj_add_state(ctx->ssid_textarea, LV_STATE_DISABLED); - lv_obj_add_state(ctx->remember_switch, LV_STATE_DISABLED); + lv_obj_add_state(ctx->remember_checkbox, LV_STATE_DISABLED); } else { lv_obj_remove_flag(ctx->connect_button, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(ctx->connecting_spinner, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_state(ctx->password_textarea, LV_STATE_DISABLED); lv_obj_remove_state(ctx->ssid_textarea, LV_STATE_DISABLED); - lv_obj_remove_state(ctx->remember_switch, LV_STATE_DISABLED); + lv_obj_remove_state(ctx->remember_checkbox, LV_STATE_DISABLED); } } @@ -161,7 +163,7 @@ void onConnectPressed(lv_event_t* event) { return; } - bool store = lv_obj_get_state(ctx->remember_switch) & LV_STATE_CHECKED; + bool store = lv_obj_get_state(ctx->remember_checkbox) & LV_STATE_CHECKED; setLoading(ctx, true); @@ -198,26 +200,23 @@ void createBottomButtons(Context* ctx, lv_obj_t* parent) { lv_obj_set_width(button_container, LV_PCT(100)); lv_obj_set_height(button_container, LV_SIZE_CONTENT); lv_obj_set_style_pad_all(button_container, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_gap(button_container, 0, LV_STATE_DEFAULT); + lv_obj_set_style_pad_gap(button_container, 4, LV_STATE_DEFAULT); lv_obj_set_style_border_width(button_container, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(button_container, LV_OPA_TRANSP, LV_STATE_DEFAULT); + // "[x] Remember [Connect]", with everything centered vertically + lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(button_container, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - ctx->remember_switch = lv_switch_create(button_container); - lv_obj_add_state(ctx->remember_switch, LV_STATE_CHECKED); - lv_obj_align(ctx->remember_switch, LV_ALIGN_LEFT_MID, 0, 0); - - auto* remember_label = lv_label_create(button_container); - lv_label_set_text(remember_label, "Remember"); - lv_obj_align(remember_label, LV_ALIGN_CENTER, 0, 0); - lv_obj_align_to(remember_label, ctx->remember_switch, LV_ALIGN_OUT_RIGHT_MID, 4, 0); + ctx->remember_checkbox = lv_checkbox_create(button_container); + lv_checkbox_set_text(ctx->remember_checkbox, "Remember"); + lv_obj_add_state(ctx->remember_checkbox, LV_STATE_CHECKED); ctx->connecting_spinner = lvgl_spinner_create(button_container); - lv_obj_align(ctx->connecting_spinner, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_flag(ctx->connecting_spinner, LV_OBJ_FLAG_HIDDEN); ctx->connect_button = lv_btn_create(button_container); auto* connect_label = lv_label_create(ctx->connect_button); lv_label_set_text(connect_label, "Connect"); - lv_obj_align(ctx->connect_button, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(ctx->connect_button, onConnectPressed, LV_EVENT_SHORT_CLICKED, ctx); } @@ -230,11 +229,42 @@ void destroyWidgets(void* userData) { ctx->password_textarea = nullptr; ctx->password_error = nullptr; ctx->connect_button = nullptr; - ctx->remember_switch = nullptr; + ctx->remember_checkbox = nullptr; ctx->connecting_spinner = nullptr; ctx->connection_error = nullptr; } +/** A transparent row: "Title [content]" */ +lv_obj_t* createRow(lv_obj_t* parent, const char* title) { + auto* row = lv_obj_create(parent); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT); + lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + auto* label = lv_label_create(row); + lv_label_set_text(label, title); + lv_obj_set_flex_grow(label, 1); + return row; +} + +/** A hidden label in the theme's error colour */ +lv_obj_t* createErrorLabel(lv_obj_t* parent) { + auto* label = lv_label_create(parent); + lv_obj_set_width(label, LV_PCT(100)); + lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_WRAP); + // The monochrome theme has no error colour + if (!lvgl_theme_is_mono()) { + LvglThemeSettings theme_settings; + lvgl_theme_get_settings(&theme_settings); + lv_obj_set_style_text_color(label, theme_settings.color_error, LV_STATE_DEFAULT); + } + lv_obj_add_flag(label, LV_OBJ_FLAG_HIDDEN); + return label; +} + // TODO: Standardize dialogs void createWidgets(lv_obj_t* parent, void* userData) { auto* ctx = static_cast(userData); @@ -247,76 +277,39 @@ void createWidgets(lv_obj_t* parent, void* userData) { lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx); auto* wrapper = lv_obj_create(parent); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_grow(wrapper, 1); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); + auto* 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); + // SSID - auto* ssid_wrapper = lv_obj_create(wrapper); - lv_obj_set_width(ssid_wrapper, LV_PCT(100)); - lv_obj_set_height(ssid_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(ssid_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_gap(ssid_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ssid_wrapper, 0, LV_STATE_DEFAULT); - - auto* ssid_label_wrapper = lv_obj_create(ssid_wrapper); - lv_obj_set_width(ssid_label_wrapper, LV_PCT(50)); - lv_obj_set_height(ssid_label_wrapper, LV_SIZE_CONTENT); - lv_obj_align(ssid_label_wrapper, LV_ALIGN_LEFT_MID, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(ssid_label_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(ssid_label_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(ssid_label_wrapper, 0, LV_STATE_DEFAULT); - - auto* ssid_label = lv_label_create(ssid_label_wrapper); - lv_label_set_text(ssid_label, "Network:"); - - ctx->ssid_textarea = lv_textarea_create(ssid_wrapper); + auto* ssid_row = createRow(card, "Network"); + ctx->ssid_textarea = lv_textarea_create(ssid_row); lv_textarea_set_one_line(ctx->ssid_textarea, true); - lv_obj_align(ctx->ssid_textarea, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_set_width(ctx->ssid_textarea, LV_PCT(50)); + lv_obj_set_width(ctx->ssid_textarea, LV_PCT(60)); - ctx->ssid_error = lv_label_create(wrapper); - lv_obj_set_style_text_color(ctx->ssid_error, lv_color_make(255, 50, 50), LV_STATE_DEFAULT); - lv_obj_add_flag(ctx->ssid_error, LV_OBJ_FLAG_HIDDEN); + ctx->ssid_error = createErrorLabel(card); // Password - auto* password_wrapper = lv_obj_create(wrapper); - lv_obj_set_width(password_wrapper, LV_PCT(100)); - lv_obj_set_height(password_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(password_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_gap(password_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(password_wrapper, 0, LV_STATE_DEFAULT); - - auto* password_label_wrapper = lv_obj_create(password_wrapper); - lv_obj_set_width(password_label_wrapper, LV_PCT(50)); - lv_obj_set_height(password_label_wrapper, LV_SIZE_CONTENT); - lv_obj_align_to(password_label_wrapper, password_wrapper, LV_ALIGN_LEFT_MID, 0, 0); - lv_obj_set_style_border_width(password_label_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_left(password_label_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_pad_right(password_label_wrapper, 0, LV_STATE_DEFAULT); - - auto* password_label = lv_label_create(password_label_wrapper); - lv_label_set_text(password_label, "Password:"); - - ctx->password_textarea = lv_textarea_create(password_wrapper); + auto* password_row = createRow(card, "Password"); + ctx->password_textarea = lv_textarea_create(password_row); lv_textarea_set_one_line(ctx->password_textarea, true); lv_textarea_set_password_mode(ctx->password_textarea, true); - lv_obj_align(ctx->password_textarea, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_set_width(ctx->password_textarea, LV_PCT(50)); + lv_obj_set_width(ctx->password_textarea, LV_PCT(60)); - ctx->password_error = lv_label_create(wrapper); - lv_obj_set_style_text_color(ctx->password_error, lv_color_make(255, 50, 50), LV_STATE_DEFAULT); - lv_obj_add_flag(ctx->password_error, LV_OBJ_FLAG_HIDDEN); + ctx->password_error = createErrorLabel(card); // Connection error - ctx->connection_error = lv_label_create(wrapper); - lv_obj_set_style_text_color(ctx->connection_error, lv_color_make(255, 50, 50), LV_STATE_DEFAULT); - lv_obj_add_flag(ctx->connection_error, LV_OBJ_FLAG_HIDDEN); + ctx->connection_error = createErrorLabel(card); // Bottom buttons - createBottomButtons(ctx, wrapper); + createBottomButtons(ctx, card); // Init from app parameters if (!ctx->initialSsid.empty()) { diff --git a/Tactility/Source/app/wifimanage/View.cpp b/Tactility/Source/app/wifimanage/View.cpp index ff51025d5..9ff8953af 100644 --- a/Tactility/Source/app/wifimanage/View.cpp +++ b/Tactility/Source/app/wifimanage/View.cpp @@ -13,6 +13,7 @@ #include #include +#include #include @@ -107,15 +108,34 @@ void View::showDetails(lv_event_t* event) { } } -void View::createSsidListItem(const WifiApRecord& record, bool isConnecting, size_t index) { +/** Creates a title with a card below it, and returns the list in the card */ +static lv_obj_t* createSection(lv_obj_t* parent, const char* title) { + auto* label = lv_label_create(parent); + lv_label_set_text(label, title); + + auto* card = lvgl_card_create(parent); + lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(card, 0, LV_STATE_DEFAULT); + // The list items' pressed and focused backgrounds follow the card's rounded corners + lv_obj_set_style_clip_corner(card, true, LV_STATE_DEFAULT); + + // The card provides the background + auto* list = lv_list_create(card); + lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT); + return list; +} + +void View::createSsidListItem(lv_obj_t* list, const WifiApRecord& record, bool isConnecting, size_t index) { if (isConnecting) { - auto* button = lv_list_add_button(networks_list, LV_SYMBOL_WIFI, record.ssid); + auto* button = lv_list_add_button(list, LV_SYMBOL_WIFI, record.ssid); lv_obj_add_event_cb(button, showDetails, LV_EVENT_SHORT_CLICKED, this); } else { const std::string auth_info = (record.authentication_type == WIFI_AUTHENTICATION_TYPE_OPEN) ? "(open) " : " "; const auto percentage = mapRssiToPercentage(record.rssi); const auto label = std::format("{} {}{}%", std::string(record.ssid), auth_info, percentage); - auto* button = lv_list_add_button(networks_list, nullptr, label.c_str()); + auto* button = lv_list_add_button(list, nullptr, label.c_str()); lv_obj_set_user_data(button, reinterpret_cast(index)); if (wifi_settings_contains(record.ssid)) { lv_obj_add_event_cb(button, showDetails, LV_EVENT_SHORT_CLICKED, this); @@ -142,22 +162,20 @@ void View::updateNetworkList() { // 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(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); + auto* enable_on_boot_wrapper = lv_obj_create(networks_list); 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); + lv_obj_set_style_bg_opa(enable_on_boot_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(enable_on_boot_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(enable_on_boot_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_remove_flag(enable_on_boot_wrapper, LV_OBJ_FLAG_SCROLLABLE); auto* enable_label = lv_label_create(enable_on_boot_wrapper); lv_label_set_text(enable_label, "Enable on boot"); - lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_flex_grow(enable_label, 1); enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper); - lv_obj_align(enable_on_boot_switch, LV_ALIGN_RIGHT_MID, 0, 0); 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); @@ -180,15 +198,14 @@ void View::updateNetworkList() { for (int i = 0; i < ap_records.size(); ++i) { auto& record = ap_records[i]; if (record.ssid == connection_target) { - lv_list_add_text(networks_list, "Connected"); - createSsidListItem(record, false, i); + createSsidListItem(createSection(networks_list, "Connected"), record, false, i); added_connected = true; break; } } } - lv_list_add_text(networks_list, "Other networks"); + auto* networks_section = createSection(networks_list, "Networks"); std::set used_ssids; if (!ap_records.empty()) { for (int i = 0; i < ap_records.size(); ++i) { @@ -200,7 +217,7 @@ void View::updateNetworkList() { !connection_target.empty(); bool skip = connection_target_match && added_connected; if (!skip) { - createSsidListItem(record, is_connecting, i); + createSsidListItem(networks_section, record, is_connecting, i); } used_ssids.insert(record.ssid); } @@ -211,8 +228,7 @@ void View::updateNetworkList() { lv_obj_add_flag(networks_list, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_clear_flag(networks_list, LV_OBJ_FLAG_HIDDEN); - lv_obj_t* label = lv_label_create(networks_list); - lv_label_set_text(label, "No networks found."); + lv_list_add_text(networks_section, "No networks found."); } connect_to_hidden = lv_button_create(networks_list); @@ -296,9 +312,11 @@ void View::init(uint32_t newAppInstanceId, lv_obj_t* parent) { // Networks - networks_list = lv_list_create(parent); + networks_list = lv_obj_create(parent); lv_obj_set_flex_grow(networks_list, 1); lv_obj_set_width(networks_list, LV_PCT(100)); + lv_obj_set_flex_flow(networks_list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_border_width(networks_list, 0, LV_STATE_DEFAULT); } void View::update() { diff --git a/Tactility/Source/lvgl/Statusbar.cpp b/Tactility/Source/lvgl/Statusbar.cpp index 9d00a2e49..97554dbf0 100644 --- a/Tactility/Source/lvgl/Statusbar.cpp +++ b/Tactility/Source/lvgl/Statusbar.cpp @@ -197,8 +197,8 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) { lv_obj_set_style_pad_hor(obj, 2, LV_STATE_DEFAULT); lv_obj_center(obj); auto icon_size = lvgl_get_statusbar_icon_font_height(); - auto ui_density = lvgl_get_ui_density(); - auto icon_padding = (ui_density != LVGL_UI_DENSITY_COMPACT) ? static_cast(icon_size * 0.2f) : 2; + const bool is_compact = lvgl_theme_is_compact(); + auto icon_padding = !is_compact ? static_cast(icon_size * 0.2f) : 2; lv_display_t* display = lv_obj_get_display(obj); DisplayShape shape; @@ -252,7 +252,7 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) { // The monochrome theme's focus ring doesn't contrast with the statusbar, so the icons get their own const bool own_focus_ring = lvgl_theme_is_mono(); - const int32_t focus_width = (ui_density != LVGL_UI_DENSITY_COMPACT) ? 2 : 1; + const int32_t focus_width = !is_compact ? 2 : 1; const lv_color_t focus_color = lv_obj_get_style_text_color(obj, LV_PART_MAIN); statusbar_data.mutex.lock(MAX_TICKS); diff --git a/Tactility/Source/lvgl/Theme.cpp b/Tactility/Source/lvgl/Theme.cpp index 103b5096c..39fb57b08 100644 --- a/Tactility/Source/lvgl/Theme.cpp +++ b/Tactility/Source/lvgl/Theme.cpp @@ -11,6 +11,9 @@ void configureTheme(const settings::appearance::AppearanceSettings& settings) { theme_settings.is_dark = settings.themeMode == settings::appearance::ThemeMode::Dark; } theme_settings.is_mono = settings.monoTheme; + if (settings.densityMode != settings::appearance::DensityMode::DeviceDefault) { + theme_settings.is_compact = settings.densityMode == settings::appearance::DensityMode::Compact; + } theme_settings.animations_enabled = settings.animationsEnabled; if (settings.primaryColor.has_value()) { theme_settings.color_primary = lv_color_hex(*settings.primaryColor); @@ -21,6 +24,10 @@ void configureTheme(const settings::appearance::AppearanceSettings& settings) { if (settings.errorColor.has_value()) { theme_settings.color_error = lv_color_hex(*settings.errorColor); } + if (settings.surfaceColor.has_value()) { + theme_settings.surface_override = true; + theme_settings.color_surface = lv_color_hex(settings::appearance::getSurfaceColor(*settings.surfaceColor, settings.surfaceTintLevel, theme_settings.is_dark)); + } lvgl_theme_set_settings(&theme_settings); } diff --git a/Tactility/Source/settings/AppearanceSettings.cpp b/Tactility/Source/settings/AppearanceSettings.cpp index 0f751a275..d9fc43e4f 100644 --- a/Tactility/Source/settings/AppearanceSettings.cpp +++ b/Tactility/Source/settings/AppearanceSettings.cpp @@ -18,10 +18,31 @@ constexpr auto* SETTINGS_KEY_REGULAR_FONT_PATH = "regularFontPath"; constexpr auto* SETTINGS_KEY_MONO_FONT_PATH = "monoFontPath"; constexpr auto* SETTINGS_KEY_THEME_MODE = "themeMode"; constexpr auto* SETTINGS_KEY_MONO_THEME = "monoTheme"; +constexpr auto* SETTINGS_KEY_DENSITY = "density"; constexpr auto* SETTINGS_KEY_ANIMATIONS = "animations"; constexpr auto* SETTINGS_KEY_PRIMARY_COLOR = "primaryColor"; constexpr auto* SETTINGS_KEY_SECONDARY_COLOR = "secondaryColor"; constexpr auto* SETTINGS_KEY_ERROR_COLOR = "errorColor"; +constexpr auto* SETTINGS_KEY_SURFACE_COLOR = "surfaceColor"; +constexpr auto* SETTINGS_KEY_SURFACE_TINT_LEVEL = "surfaceTintLevel"; + +// The share of the surface colour per tint level, in percent +constexpr uint32_t SURFACE_TINT_PERCENTAGES[SURFACE_TINT_LEVEL_COUNT] = { 15, 25, 40 }; + +static uint32_t mixColor(uint32_t color, uint32_t base, uint32_t colorPercentage) { + uint32_t result = 0; + for (int shift = 0; shift <= 16; shift += 8) { + const uint32_t channel = (((color >> shift) & 0xFF) * colorPercentage + ((base >> shift) & 0xFF) * (100 - colorPercentage) + 50) / 100; + result |= channel << shift; + } + return result; +} + +uint32_t getSurfaceColor(uint32_t color, uint8_t tintLevel, bool isDark) { + const uint32_t base = isDark ? 0x000000 : 0xFFFFFF; + const uint8_t level = tintLevel < SURFACE_TINT_LEVEL_COUNT ? tintLevel : SURFACE_TINT_LEVEL_COUNT - 1; + return mixColor(color, base, SURFACE_TINT_PERCENTAGES[level]); +} static std::optional parseColor(const std::map& map, const char* key) { const auto entry = map.find(key); @@ -60,6 +81,17 @@ static const char* toString(ThemeMode mode) { } } +static const char* toString(DensityMode mode) { + switch (mode) { + case DensityMode::Regular: + return "regular"; + case DensityMode::Compact: + return "compact"; + default: + return "default"; + } +} + static std::string getSettingsFilePath() { char path[256]; if (app_paths_get_user_data_path("tactility.appearance", "appearance.properties", path, sizeof(path)) != ERROR_NONE) { @@ -99,12 +131,28 @@ bool load(AppearanceSettings& settings) { if (const auto entry = map.find(SETTINGS_KEY_MONO_THEME); entry != map.end()) { settings.monoTheme = entry->second == "true"; } + if (const auto entry = map.find(SETTINGS_KEY_DENSITY); entry != map.end()) { + if (entry->second == "regular") { + settings.densityMode = DensityMode::Regular; + } else if (entry->second == "compact") { + settings.densityMode = DensityMode::Compact; + } + } if (const auto entry = map.find(SETTINGS_KEY_ANIMATIONS); entry != map.end()) { settings.animationsEnabled = entry->second != "false"; } settings.primaryColor = parseColor(map, SETTINGS_KEY_PRIMARY_COLOR); settings.secondaryColor = parseColor(map, SETTINGS_KEY_SECONDARY_COLOR); settings.errorColor = parseColor(map, SETTINGS_KEY_ERROR_COLOR); + settings.surfaceColor = parseColor(map, SETTINGS_KEY_SURFACE_COLOR); + if (const auto entry = map.find(SETTINGS_KEY_SURFACE_TINT_LEVEL); entry != map.end()) { + const std::string& text = entry->second; + uint32_t level = 0; + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), level); + if (error == std::errc {} && end == text.data() + text.size() && level < SURFACE_TINT_LEVEL_COUNT) { + settings.surfaceTintLevel = static_cast(level); + } + } return true; } @@ -127,10 +175,13 @@ bool save(const AppearanceSettings& settings) { map[SETTINGS_KEY_MONO_FONT_PATH] = settings.monoFontPath; map[SETTINGS_KEY_THEME_MODE] = toString(settings.themeMode); map[SETTINGS_KEY_MONO_THEME] = settings.monoTheme ? "true" : "false"; + map[SETTINGS_KEY_DENSITY] = toString(settings.densityMode); map[SETTINGS_KEY_ANIMATIONS] = settings.animationsEnabled ? "true" : "false"; putColor(map, SETTINGS_KEY_PRIMARY_COLOR, settings.primaryColor); putColor(map, SETTINGS_KEY_SECONDARY_COLOR, settings.secondaryColor); putColor(map, SETTINGS_KEY_ERROR_COLOR, settings.errorColor); + putColor(map, SETTINGS_KEY_SURFACE_COLOR, settings.surfaceColor); + map[SETTINGS_KEY_SURFACE_TINT_LEVEL] = std::to_string(settings.surfaceTintLevel); return file::savePropertiesFile(settings_path, map); }