fix(ui-lvgl): popups no longer shrink when their list is pulled past its end

LVGL 9.2.2 measures a SIZE_CONTENT box from its children as scrolled, so an
elastic pull past the end of a capped popup list shrank it by the overshoot,
again on every pull. lvgl_patches.py fixes calc_content_height in the
downloaded library (a build middleware for PlatformIO, called directly by the
sim's build_wasm.sh).

The five popups that each built the same panel now share popupOpen() (fit to
content / full height / under the status bar).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-28 14:45:19 +02:00
co-authored by Claude Opus 5.5
parent ff6354fd05
commit 9d4f018696
7 changed files with 104 additions and 75 deletions
+1 -14
View File
@@ -710,20 +710,7 @@ void UITask::mapDownloadPopup() {
if (_dl_zmax < _map_z) _dl_zmax = _map_z + 3;
if (_dl_zmax > src_max) _dl_zmax = src_max; // the server has nothing finer
_dl_overlay = dimOverlay(screen());
lv_obj_t* panel = lv_obj_create(_dl_overlay);
anim::popup(_dl_overlay);
lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16, LV_SIZE_CONTENT);
lv_obj_align(panel, LV_ALIGN_CENTER, 0, theme::STATUS_H / 2);
lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0);
lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_radius(panel, theme::RADIUS, 0);
lv_obj_set_style_pad_all(panel, theme::PAD, 0);
lv_obj_set_style_pad_row(panel, 4, 0);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_max_height(panel, lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 12, 0); // scrolls past that
lv_obj_t* panel = popupOpen(screen(), POP_FIT, _dl_overlay);
lv_obj_t* hdr = lv_obj_create(panel);
styleSurface(hdr, theme::BG);
+1 -20
View File
@@ -476,27 +476,8 @@ void UITask::navToNode(const uint8_t* key, int32_t lat, int32_t lon, const char*
lv_obj_t* UITask::navPopupPanel(const char* title, bool full) {
navClosePopup();
_nav_overlay = dimOverlay(screen());
lv_obj_t* panel = lv_obj_create(_nav_overlay);
anim::popup(_nav_overlay);
int w = lv_display_get_horizontal_resolution(NULL) - 16;
if (full) {
lv_obj_set_size(panel, w, lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 12);
lv_obj_set_pos(panel, 8, theme::STATUS_H + 6);
} else { // as tall as its content, scrolling past the screen's height
lv_obj_set_size(panel, w, LV_SIZE_CONTENT);
lv_obj_set_style_max_height(panel, lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 12, 0);
lv_obj_align(panel, LV_ALIGN_CENTER, 0, theme::STATUS_H / 2);
}
lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0);
lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_radius(panel, theme::RADIUS, 0);
lv_obj_set_style_pad_all(panel, theme::PAD, 0);
lv_obj_t* panel = popupOpen(screen(), full ? POP_FULL : POP_FIT, _nav_overlay);
lv_obj_set_style_pad_row(panel, 6, 0);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
if (full) lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE); // its own list scrolls
lv_obj_t* hdr = lv_obj_create(panel);
styleSurface(hdr, theme::BG);
+1 -12
View File
@@ -141,19 +141,8 @@ void UITask::radioFreqPopup(bool repeater) {
using namespace radioview;
if (s_overlay) return;
s_freq_rpt = repeater;
s_overlay = dimOverlay(screen());
lv_obj_t* panel = lv_obj_create(s_overlay);
anim::popup(s_overlay);
lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16, LV_SIZE_CONTENT);
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4);
lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0);
lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_radius(panel, theme::RADIUS, 0);
lv_obj_set_style_pad_all(panel, theme::PAD, 0);
lv_obj_t* panel = popupOpen(screen(), POP_TOP, s_overlay);
lv_obj_set_style_pad_row(panel, 6, 0);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
float lo, hi;
radio_driver.getFreqBounds(lo, hi);
char t[48];
+37 -29
View File
@@ -150,6 +150,41 @@ static lv_obj_t* dimOverlay(lv_obj_t* parent) {
return o;
}
// A popup: the dimmed layer (into `overlay`, to close it by) and its panel --
// the page colour, the accent hairline, a flex column -- rising into view.
// POP_FIT: as tall as its content, centred below the status bar, scrolling
// past that height. POP_FULL: all of that height; its own list scrolls.
// POP_TOP: as tall as its content, under the status bar, clear of a keyboard.
enum PopFit : uint8_t { POP_FIT, POP_FULL, POP_TOP };
static lv_obj_t* popupOpen(lv_obj_t* parent, PopFit fit, lv_obj_t*& overlay, int32_t inset = 8) {
overlay = dimOverlay(parent);
lv_obj_t* panel = lv_obj_create(overlay);
int32_t w = lv_display_get_horizontal_resolution(NULL), h = lv_display_get_vertical_resolution(NULL);
lv_obj_set_width(panel, w - 2 * inset);
if (fit == POP_FULL) {
lv_obj_set_height(panel, h - theme::STATUS_H - 12);
lv_obj_set_pos(panel, inset, theme::STATUS_H + 6);
} else if (fit == POP_TOP) {
lv_obj_set_height(panel, LV_SIZE_CONTENT);
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4);
} else {
lv_obj_set_height(panel, LV_SIZE_CONTENT);
lv_obj_set_style_max_height(panel, h - theme::STATUS_H - 12, 0);
lv_obj_align(panel, LV_ALIGN_CENTER, 0, theme::STATUS_H / 2);
lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_ACTIVE);
}
if (fit != POP_FIT) lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0);
lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_radius(panel, theme::RADIUS, 0);
lv_obj_set_style_pad_all(panel, theme::PAD, 0);
lv_obj_set_style_pad_row(panel, 4, 0);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
anim::popup(overlay);
return panel;
}
// ── Info cards ──
// Label / value rows on a card (Diagnostics, GPS, node detail, About): the
// label small and muted on the left, the value on the right -- wrapping,
@@ -429,23 +464,11 @@ static void pickerOpen(lv_obj_t* c) {
Choice* ch = choiceOf(c);
pickerClose();
s_pick_target = c;
s_pick_overlay = dimOverlay(lv_layer_top());
lv_obj_t* panel = popupOpen(lv_layer_top(), POP_FIT, s_pick_overlay, 24);
lv_obj_add_event_cb(s_pick_overlay, [](lv_event_t* e) { // a tap beside the list: nothing changes
if (lv_event_get_target(e) == lv_event_get_current_target(e)) pickerClose();
}, LV_EVENT_CLICKED, NULL);
lv_obj_t* panel = lv_obj_create(s_pick_overlay);
styleSurface(panel, theme::BG);
lv_obj_set_width(panel, lv_display_get_horizontal_resolution(NULL) - 48);
lv_obj_set_height(panel, LV_SIZE_CONTENT);
lv_obj_set_style_max_height(panel, lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 16, 0);
lv_obj_align(panel, LV_ALIGN_CENTER, 0, theme::STATUS_H / 2);
lv_obj_set_style_radius(panel, theme::RADIUS, 0);
lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_pad_all(panel, 6, 0);
lv_obj_set_style_pad_row(panel, 4, 0);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
lv_obj_set_scrollbar_mode(panel, LV_SCROLLBAR_MODE_ACTIVE);
lv_obj_t* t = label(panel, ch->title, THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_obj_set_style_pad_hor(t, 4, 0);
lv_obj_t* card = group(panel, nullptr);
@@ -459,7 +482,6 @@ static void pickerOpen(lv_obj_t* c) {
if (cur) { label(r, LV_SYMBOL_OK, THEME_FONT_BODY, theme::ACCENT); sel_row = r; }
lv_obj_add_event_cb(r, onPickOption, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
}
anim::popup(s_pick_overlay);
if (sel_row) { lv_obj_update_layout(panel); lv_obj_scroll_to_view_recursive(sel_row, LV_ANIM_OFF); }
}
@@ -1812,21 +1834,7 @@ void UITask::startNearbyScan() {
// Dimmed full-screen overlay (swallows taps) holding a panel with the results.
// A child of the current screen, so it goes away with it.
void UITask::showScanPopup() {
_scan_overlay = dimOverlay(screen());
lv_obj_t* panel = lv_obj_create(_scan_overlay);
anim::popup(_scan_overlay);
lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16,
lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 12);
lv_obj_set_pos(panel, 8, theme::STATUS_H + 6);
lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0);
lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_radius(panel, theme::RADIUS, 0);
lv_obj_set_style_pad_all(panel, theme::PAD, 0);
lv_obj_set_style_pad_row(panel, 4, 0);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* panel = popupOpen(screen(), POP_FULL, _scan_overlay);
lv_obj_t* hdr = lv_obj_create(panel);
styleSurface(hdr, theme::BG);
@@ -0,0 +1,60 @@
# Fixes to LVGL 9.2.2 (lib_deps) that ui-lvgl needs, applied in place to the
# downloaded library -- idempotent, so every build can run it.
#
# PlatformIO: extra_scripts = pre:examples/companion_radio/ui-lvgl/lvgl_patches.py
# (patches as LVGL is compiled, i.e. after lib_deps are fetched)
# The sim: python3 examples/companion_radio/ui-lvgl/lvgl_patches.py <lvgl dir>
#
# Content height counted as scrolled (lv_obj_pos.c, calc_content_height): a
# SIZE_CONTENT box capped by max_height measures its children where the scroll
# has moved them, so an elastic pull past its end shrinks it by the overshoot,
# again on every pull (popups with a list). The function zeroes scroll.y for
# the count, but the children's coords already carry it; it's added back.
import os
import sys
PATCHES = [
("src/core/lv_obj_pos.c", [
(" /*Normal top aligns. */\n"
" child_res_tmp = child->coords.y2 - obj->coords.y1 + 1;\n",
" /*Normal top aligns. */\n"
" child_res_tmp = child->coords.y2 - obj->coords.y1 + 1 + scroll_y_tmp;\n"),
(" else {\n"
" child_res_tmp = child->coords.y2 - obj->coords.y1 + 1;\n"
" }\n",
" else {\n"
" child_res_tmp = child->coords.y2 - obj->coords.y1 + 1 + scroll_y_tmp;\n"
" }\n"),
]),
]
def apply(lvgl_dir):
for rel, subs in PATCHES:
path = os.path.join(lvgl_dir, rel)
with open(path) as f:
src = f.read()
out = src
for old, new in subs:
if new in out:
continue
if out.count(old) != 1:
raise RuntimeError("lvgl_patches: %s changed upstream, re-check the patch" % rel)
out = out.replace(old, new)
if out != src:
with open(path, "w") as f:
f.write(out)
print("lvgl_patches: patched %s" % rel)
if __name__ == "__main__":
apply(sys.argv[1])
else:
Import("env") # noqa: F821 (SCons)
def _patch(env, node):
path = node.srcnode().get_abspath()
apply(path[: path.rindex(os.sep + "src" + os.sep)])
return node
env.AddBuildMiddleware(_patch, "*/lvgl/src/core/lv_obj_pos.c") # noqa: F821
+3
View File
@@ -70,6 +70,9 @@ build_flags =
-D NEARBY_MAX=64
-D HIST_ARCHIVE ; message history also on the SD card (ui-lvgl/HistoryStore.h)
-D GPS_SKYVIEW ; satellites / signal / DOPs for the GPS screen (helpers/sensors/GpsSky.h)
extra_scripts =
${esp32_base.extra_scripts}
pre:examples/companion_radio/ui-lvgl/lvgl_patches.py ; fixes to LVGL itself
build_src_filter = ${Wio_Tracker_L2.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/LGFXDisplay.cpp>
+1
View File
@@ -246,6 +246,7 @@ if [ "$SIM_UI" = "lvgl" ]; then
# would shadow the libc <features.h> the C headers include.
C_FLAGS=(-std=gnu99 "${OPT_FLAGS[@]}" "${DEFINES[@]}" "-I$LVGL_DIR" -Iexamples/companion_radio/ui-lvgl)
LV_OBJ_DIR="$OUT_DIR/obj_lvgl"
python3 examples/companion_radio/ui-lvgl/lvgl_patches.py "$LVGL_DIR" # as the firmware build does
C_SRCS=()
while IFS= read -r f; do C_SRCS+=("$f"); done < <(
find "$LVGL_DIR/src" -name '*.c' | sed "s|^$REPO_ROOT/||" | sort