From f77c7ea9015066a7eb857dd1d4df2f3e48cebf70 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 1 Oct 2026 10:30:27 +0200 Subject: [PATCH] perf(l2): screens lay out in up to half the time: row widths known before the first layout A group row's text column (and an info row's key) grows into what the controls on its right leave, and LVGL lays out children before their parent, so its labels were first measured 1 px wide; LVGL also works out a new label's height at its old width (0) before the real one. Each height that changed then took another pass up the screen: 4-6 passes per screen. rowsFit(), run before a frame's layout (LV_EVENT_REFR_START) and by layoutNow() in place of every explicit lv_obj_update_layout(), gives each new row's growing child the width the flex will give it, worked out from the other children's styles and text, and labelFit() measures wrapping labels at their real width up front. The grow stays, so a control that can't be sized ahead only falls back to the old passes. Layout on the device: Display page 34 -> 24 ms, Bot 33 -> 18, Quick 37 -> 22, Radio 26 -> 18, Diagnostics 20 -> 12. Sim screenshots of 26 screens unchanged. Co-Authored-By: Claude Opus 5.5 --- .../companion_radio/ui-lvgl/ChannelScreen.h | 2 +- .../companion_radio/ui-lvgl/DeviceScreen.h | 2 +- examples/companion_radio/ui-lvgl/HomeScreen.h | 2 +- examples/companion_radio/ui-lvgl/MapAreas.h | 2 +- examples/companion_radio/ui-lvgl/MapScreen.h | 4 +- .../companion_radio/ui-lvgl/QuickScreen.h | 2 +- .../companion_radio/ui-lvgl/RadioScreen.h | 2 +- .../companion_radio/ui-lvgl/RepeaterScreen.h | 2 +- .../companion_radio/ui-lvgl/SoundScreen.h | 2 +- .../companion_radio/ui-lvgl/StorageScreen.h | 2 +- examples/companion_radio/ui-lvgl/UITask.cpp | 159 +++++++++++++++--- examples/companion_radio/ui-lvgl/WifiScreen.h | 6 +- 12 files changed, 154 insertions(+), 33 deletions(-) diff --git a/examples/companion_radio/ui-lvgl/ChannelScreen.h b/examples/companion_radio/ui-lvgl/ChannelScreen.h index 73dcda0c..e978ed93 100644 --- a/examples/companion_radio/ui-lvgl/ChannelScreen.h +++ b/examples/companion_radio/ui-lvgl/ChannelScreen.h @@ -270,7 +270,7 @@ void UITask::channelEditField(lv_obj_t* ta) { lv_obj_remove_flag(s_kb, LV_OBJ_FLAG_HIDDEN); if (_body) lv_obj_set_height(_body, lv_obj_get_height(_body) - lv_obj_get_height(s_kb)); } - lv_obj_update_layout(screen()); + layoutNow(screen()); lv_obj_scroll_to_view(ta, LV_ANIM_OFF); } diff --git a/examples/companion_radio/ui-lvgl/DeviceScreen.h b/examples/companion_radio/ui-lvgl/DeviceScreen.h index 5fde59fd..f25aef6e 100644 --- a/examples/companion_radio/ui-lvgl/DeviceScreen.h +++ b/examples/companion_radio/ui-lvgl/DeviceScreen.h @@ -392,7 +392,7 @@ void UITask::pinRowRefresh() { if (_screen != SCR_SETTINGS_NAV) return; buildSchemaSettings(); fillFlush(); - if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); } + if (_body) { layoutNow(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); } } // ── Accent colour ───────────────────────────────────────────────────────────── diff --git a/examples/companion_radio/ui-lvgl/HomeScreen.h b/examples/companion_radio/ui-lvgl/HomeScreen.h index 0c4b6a54..3cda3b99 100644 --- a/examples/companion_radio/ui-lvgl/HomeScreen.h +++ b/examples/companion_radio/ui-lvgl/HomeScreen.h @@ -616,7 +616,7 @@ void UITask::buildHomeMap(lv_obj_t* box) { lv_obj_align(s_caption, LV_ALIGN_BOTTOM_LEFT, 6, -6); mapview::s_available = lvport::mountStorage() && mapview::s_provider->available(); mapview::s_trails_on = lvport::trailsOn(); - lv_obj_update_layout(s_area); + layoutNow(s_area); homeMapFit(); homeMapLayout(); } diff --git a/examples/companion_radio/ui-lvgl/MapAreas.h b/examples/companion_radio/ui-lvgl/MapAreas.h index c60a1cec..f03ee60a 100644 --- a/examples/companion_radio/ui-lvgl/MapAreas.h +++ b/examples/companion_radio/ui-lvgl/MapAreas.h @@ -418,7 +418,7 @@ void UITask::mapAreaPopup(int idx) { s_del_lbl = actButton(r, LV_SYMBOL_TRASH, "Delete", A_DELETE); // Framed in the map above the sheet: the view is kept to go back to. - lv_obj_update_layout(panel); + layoutNow(panel); int w = _map_area ? lv_obj_get_width(_map_area) : 320, h = _map_area ? lv_obj_get_height(_map_area) : 218; int sheet = lv_obj_get_height(panel) + 8; // its inset int above = h - sheet; diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index 1b125995..42a63335 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -493,7 +493,7 @@ void UITask::buildMap() { lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); if (_map_nav) buildNavControls(body); - lv_obj_update_layout(body); + layoutNow(body); mapview::s_mark_count = 0; // the previous markers went with the previous screen rebuildMapMarkers(); layoutMap(); @@ -691,7 +691,7 @@ void UITask::rebuildMapMarkers() { else rebuildNodeMarkers(); for (int k = mapview::s_mark_count; k < mapview::s_mark_prev; k++) lv_obj_delete(mapview::s_marks[k].obj); mapview::s_mark_prev = 0; - lv_obj_update_layout(_map_marks); // so layoutMap() knows where each dot sits in its marker + layoutNow(_map_marks); // so layoutMap() knows where each dot sits in its marker } void UITask::rebuildNodeMarkers() { diff --git a/examples/companion_radio/ui-lvgl/QuickScreen.h b/examples/companion_radio/ui-lvgl/QuickScreen.h index 18e463cd..ebbd1078 100644 --- a/examples/companion_radio/ui-lvgl/QuickScreen.h +++ b/examples/companion_radio/ui-lvgl/QuickScreen.h @@ -185,7 +185,7 @@ void UITask::quickEditDone(bool ok) { lv_obj_t* body = _body; int32_t y = body ? lv_obj_get_scroll_y(body) : 0; buildQuickMsgs(); - if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } + if (_body) { layoutNow(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } } } diff --git a/examples/companion_radio/ui-lvgl/RadioScreen.h b/examples/companion_radio/ui-lvgl/RadioScreen.h index 9ab2c665..c3c6cb34 100644 --- a/examples/companion_radio/ui-lvgl/RadioScreen.h +++ b/examples/companion_radio/ui-lvgl/RadioScreen.h @@ -139,7 +139,7 @@ void UITask::rebuildRadio() { int32_t y = _body ? lv_obj_get_scroll_y(_body) : 0; buildRadio(); fillFlush(); - if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } + if (_body) { layoutNow(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } } void UITask::radioFreqPopup(bool repeater) { diff --git a/examples/companion_radio/ui-lvgl/RepeaterScreen.h b/examples/companion_radio/ui-lvgl/RepeaterScreen.h index 22692548..02f74519 100644 --- a/examples/companion_radio/ui-lvgl/RepeaterScreen.h +++ b/examples/companion_radio/ui-lvgl/RepeaterScreen.h @@ -114,7 +114,7 @@ void UITask::rebuildRepeater() { int32_t y = _body ? lv_obj_get_scroll_y(_body) : 0; buildRepeater(); fillFlush(); - if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } + if (_body) { layoutNow(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } } void UITask::repeaterSet(int which, int v) { diff --git a/examples/companion_radio/ui-lvgl/SoundScreen.h b/examples/companion_radio/ui-lvgl/SoundScreen.h index 62ea5e9a..02470edc 100644 --- a/examples/companion_radio/ui-lvgl/SoundScreen.h +++ b/examples/companion_radio/ui-lvgl/SoundScreen.h @@ -107,7 +107,7 @@ static void rebuildStrip() { cellText(i); styleCell(i, i == s_sel); } - lv_obj_update_layout(s_strip); + layoutNow(s_strip); if (s_sel < s_mel.len) lv_obj_scroll_to_view(s_cells[s_sel], LV_ANIM_OFF); } diff --git a/examples/companion_radio/ui-lvgl/StorageScreen.h b/examples/companion_radio/ui-lvgl/StorageScreen.h index 86a35883..d778045d 100644 --- a/examples/companion_radio/ui-lvgl/StorageScreen.h +++ b/examples/companion_radio/ui-lvgl/StorageScreen.h @@ -196,7 +196,7 @@ static lv_obj_t* catRow(lv_obj_t* g, int c) { // Segment widths: each category's share of the card, the rest of `used` grey. static void showSegments(uint64_t total, uint64_t used) { if (!s_sd_bar || !total) return; - lv_obj_update_layout(s_sd_bar); + layoutNow(s_sd_bar); int w = lv_obj_get_width(s_sd_bar), x = 0; uint64_t counted = 0; for (int c = 0; c <= C_COUNT; c++) { diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 67a92d96..efe59f2f 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -138,12 +138,18 @@ static void styleOpaque(lv_obj_t* o, uint32_t bg) { lv_obj_set_style_bg_opa(o, LV_OPA_COVER, 0); } +static bool growsWide(lv_obj_t* o) { // a flex grow along a row: the width is the layout's + lv_obj_t* p = lv_obj_get_parent(o); + if (!p || !lv_obj_get_style_flex_grow(o, LV_PART_MAIN) || lv_obj_get_style_layout(p, LV_PART_MAIN) != LV_LAYOUT_FLEX) return false; + lv_flex_flow_t f = lv_obj_get_style_flex_flow(p, LV_PART_MAIN); + return !(f & LV_FLEX_COLUMN); +} // The width of `o`'s content, worked out from the styles -- or -1 where it -// hangs on its content or a flex grow. A new box's own size is only known +// hangs on its content or a row's flex grow. A new box's own size is only known // after a layout pass, and LVGL lays out children before their parent, so // each box sized in % of the one above adds a whole pass over the screen. static int32_t innerW(lv_obj_t* o) { - if (!o || lv_obj_get_style_flex_grow(o, LV_PART_MAIN)) return -1; + if (!o || growsWide(o)) return -1; int32_t w = lv_obj_get_style_width(o, LV_PART_MAIN); if (LV_COORD_IS_PCT(w)) { int32_t pw = innerW(lv_obj_get_parent(o)); @@ -159,10 +165,27 @@ static int32_t innerW(lv_obj_t* o) { if (side & LV_BORDER_SIDE_RIGHT) w -= bw; return w; } +extern "C" bool lv_obj_refr_size(lv_obj_t* obj); // lv_obj_pos.c, in no header +// A wrapping label's size settled now, `w` wide. LVGL's first layout of a +// label works out its height at the width it had before -- 0 for a new one, +// a line per letter -- and only then measures it again at the new width. +static void labelFit(lv_obj_t* l, int32_t w) { + l->coords.x2 = l->coords.x1 + w - 1; + lv_obj_refr_size(l); +} +// A flex row whose one growing child gets its width before the first layout +// (rowsFit(), further down). +static bool s_fit_pending; +static lv_obj_t* s_fit_screen; // newScreen()'s, maybe still fading in: not lv_screen_active() yet +static void fitLater(lv_obj_t* row) { + lv_obj_add_flag(row, LV_OBJ_FLAG_USER_2); + s_fit_pending = true; +} // The parent's full width, in pixels when that's known now (see innerW()). static void fillWidth(lv_obj_t* o) { int32_t w = innerW(lv_obj_get_parent(o)); lv_obj_set_width(o, w >= 0 ? w : LV_PCT(100)); + if (w > 0 && lv_obj_check_type(o, &lv_label_class)) labelFit(o, w); } static lv_obj_t* label(lv_obj_t* parent, const char* text, const lv_font_t* font, uint32_t color) { @@ -361,6 +384,7 @@ namespace freeze { enum PopFit : uint8_t { POP_FIT, POP_FULL, POP_TOP, POP_BOTTOM }; static lv_obj_t* popupOpen(lv_obj_t* parent, PopFit fit, lv_obj_t*& overlay, int32_t inset = 8) { overlay = dimOverlay(parent); + s_fit_pending = true; // rowsFit() 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); @@ -468,6 +492,7 @@ static lv_obj_t* infoRow(lv_obj_t* card, const char* key, const char* value, uin lv_obj_set_width(v, LV_SIZE_CONTENT); lv_obj_set_style_max_width(v, LV_PCT(66), 0); lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0); + fitLater(r); return v; } @@ -569,9 +594,102 @@ static lv_obj_t* groupText(lv_obj_t* row, const char* text, const char* hint, ui noteLabel(t, hint); } lv_obj_set_user_data(row, l); // rowTitle() + fitLater(row); return l; } +// A group row's text column grows into what the controls on its right leave, +// and LVGL lays out children before their parent: its labels were first +// measured 1 px wide (a line per letter), then at the real width, and each +// height that changed took another pass up the screen -- four in all. Before +// a frame's layout, rowsFit() gives each new column the width the flex will: +// its row's minus the controls', worked out from their styles and text. The +// grow stays, so a control this can't size only costs the old passes. +static constexpr lv_obj_flag_t NOT_LAID_OUT = + (lv_obj_flag_t)(LV_OBJ_FLAG_HIDDEN | LV_OBJ_FLAG_FLOATING | LV_OBJ_FLAG_IGNORE_LAYOUT); + +// `o`'s outer width as the layout will make it; -1 when that's unclear. +static int32_t layoutW(lv_obj_t* o) { + int32_t w = lv_obj_get_style_width(o, LV_PART_MAIN); + if (LV_COORD_IS_PX(w)) { + // as it is + } else if (w != LV_SIZE_CONTENT) { + return -1; + } else if (lv_obj_check_type(o, &lv_label_class)) { + lv_obj_refr_size(o); // the label's own measure, cached for the layout + w = lv_obj_get_width(o); + } else if (lv_obj_get_style_layout(o, LV_PART_MAIN) == LV_LAYOUT_FLEX && + lv_obj_get_style_flex_flow(o, LV_PART_MAIN) == LV_FLEX_FLOW_ROW) { + int32_t gap = lv_obj_get_style_pad_column(o, LV_PART_MAIN), n = 0; + w = lv_obj_get_style_pad_left(o, LV_PART_MAIN) + lv_obj_get_style_pad_right(o, LV_PART_MAIN); + for (uint32_t i = 0; i < lv_obj_get_child_count(o); i++) { + lv_obj_t* c = lv_obj_get_child(o, i); + if (lv_obj_has_flag_any(c, NOT_LAID_OUT)) continue; + int32_t cw = layoutW(c); + if (cw < 0) return -1; + w += cw + (n++ ? gap : 0); + } + w = LV_CLAMP(lv_obj_get_style_min_width(o, LV_PART_MAIN), w, lv_obj_get_style_max_width(o, LV_PART_MAIN)); + } else { + return -1; + } + return w + lv_obj_get_style_margin_left(o, LV_PART_MAIN) + lv_obj_get_style_margin_right(o, LV_PART_MAIN); +} + +static void rowFit(lv_obj_t* row) { + lv_obj_refr_size(row); // its own width now: a child's % size is of it + int32_t w = innerW(row), gap = lv_obj_get_style_pad_column(row, LV_PART_MAIN); + if (w < 0) return; + lv_obj_t* grow = nullptr; + for (uint32_t i = 0; i < lv_obj_get_child_count(row); i++) { + lv_obj_t* c = lv_obj_get_child(row, i); + if (lv_obj_has_flag_any(c, NOT_LAID_OUT)) continue; + if (lv_obj_get_style_flex_grow(c, LV_PART_MAIN)) { + if (grow) return; // one grows, or the share isn't worth working out + grow = c; + continue; + } + int32_t cw = layoutW(c); + if (cw < 0) return; + w -= cw + gap; + } + if (!grow || w < 1) return; + lv_obj_set_width(grow, w); + if (lv_obj_check_type(grow, &lv_label_class)) { labelFit(grow, w); return; } + lv_obj_refr_size(grow); + for (uint32_t i = 0; i < lv_obj_get_child_count(grow); i++) { // its labels, 100 % of it + lv_obj_t* l = lv_obj_get_child(grow, i); + if (lv_obj_check_type(l, &lv_label_class)) labelFit(l, w); + } +} + +// ... and every label given a width in pixels, which labelFit() then measures +// at it (the screen titles, a list row's cut-off name ...). +static void rowsFit(lv_obj_t* root) { + lv_obj_tree_walk(root, [](lv_obj_t* o, void*) { + if (lv_obj_has_flag(o, LV_OBJ_FLAG_USER_2)) { + lv_obj_remove_flag(o, LV_OBJ_FLAG_USER_2); + rowFit(o); + } else if (lv_obj_check_type(o, &lv_label_class)) { + int32_t w = lv_obj_get_style_width(o, LV_PART_MAIN); + if (LV_COORD_IS_PX(w) && w > 0 && lv_obj_get_width(o) != w) labelFit(o, w); + } + return LV_OBJ_TREE_WALK_NEXT; + }, nullptr); +} +static void rowsFitPending() { + if (!s_fit_pending) return; + s_fit_pending = false; + if (s_fit_screen && s_fit_screen != lv_screen_active()) rowsFit(s_fit_screen); + rowsFit(lv_screen_active()); + rowsFit(lv_layer_top()); +} +// lv_obj_update_layout() for a screen just built: its rows fitted first. +static void layoutNow(lv_obj_t* o) { + rowsFitPending(); + lv_obj_update_layout(o); +} + // The title label of a settingRow() / listRow(), to recolour or rename it. static lv_obj_t* rowTitle(lv_obj_t* row) { return (lv_obj_t*)lv_obj_get_user_data(row); } @@ -719,7 +837,7 @@ 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); } - if (sel_row) { lv_obj_update_layout(panel); lv_obj_scroll_to_view_recursive(sel_row, LV_ANIM_OFF); } + if (sel_row) { layoutNow(panel); lv_obj_scroll_to_view_recursive(sel_row, LV_ANIM_OFF); } } // A choice: in a group row the value is bare text (accent) and a chevron; @@ -1128,6 +1246,7 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs } theme::setAccent(lvport::loadAccent()); theme::install(lv_display_get_default()); + lv_display_add_event_cb(lv_display_get_default(), [](lv_event_t*) { rowsFitPending(); }, LV_EVENT_REFR_START, NULL); buildStatusBar(); applyDisplayPrefs(); // a slider percentage overrides the level main.cpp set @@ -1417,7 +1536,7 @@ void UITask::loop() { uint32_t b = micros() - t; TM_PRINT("build"); TM_RESET(); uint32_t tl = micros(); - lv_obj_update_layout(lv_screen_active()); // layout apart from drawing + layoutNow(lv_screen_active()); // layout apart from drawing tl = micros() - tl; TM_PRINT("layout"); TM_RESET(); uint32_t fl0 = lvport::s_flush_us, td = micros(); @@ -1777,7 +1896,7 @@ static void bannerShow(const char* icon, const char* title, const char* text) { lv_anim_delete(s_banner, NULL); lv_obj_remove_flag(s_banner, LV_OBJ_FLAG_HIDDEN); if (!shown) { // a new one while it's up just changes the text - lv_obj_update_layout(s_banner); + layoutNow(s_banner); anim::run(s_banner, anim::setTy, -(lv_obj_get_height(s_banner) + theme::STATUS_H), 0, anim::SCREEN_MS); } else { lv_obj_set_style_translate_y(s_banner, 0, 0); @@ -1891,7 +2010,7 @@ void UITask::buildStatusBar() { // for what's refreshed every second: LVGL restyles the box on every call, so // it redraws moved or not -- here it's only moved. static void placeLeftOf(lv_obj_t* o, lv_obj_t* base, int32_t dx) { - lv_obj_update_layout(o); + layoutNow(o); int32_t x = lv_obj_get_x(base) - lv_obj_get_width(o) + dx; int32_t y = lv_obj_get_y(base) + (lv_obj_get_height(base) - lv_obj_get_height(o)) / 2; if (lv_obj_get_style_align(o, LV_PART_MAIN) != LV_ALIGN_TOP_LEFT || lv_obj_get_x(o) != x || lv_obj_get_y(o) != y) @@ -2104,7 +2223,7 @@ void UITask::showToast(const char* text, uint32_t ms) { int32_t y = theme::STATUS_H + 4; if (s_banner && !lv_obj_has_flag(s_banner, LV_OBJ_FLAG_HIDDEN)) y = lv_obj_get_y(s_banner) + lv_obj_get_height(s_banner) + 4; lv_obj_align(_toast, LV_ALIGN_TOP_MID, 0, y); - lv_obj_update_layout(_toast); + layoutNow(_toast); if (!shown) anim::run(_toast, anim::setTy, -(y + lv_obj_get_height(_toast)), 0, anim::POP_MS); // a replaced text just changes else lv_obj_set_style_translate_y(_toast, 0, 0); if (!_toast_timer) _toast_timer = lv_timer_create(toastTimerCb, ms, _toast); @@ -2204,6 +2323,8 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { for (lv_obj_t*& t : _map_tiles) t = nullptr; lv_obj_t* prev = _scr; lv_obj_t* scr = lv_obj_create(NULL); + s_fit_screen = scr; // rowsFit() + s_fit_pending = true; styleOpaque(scr, theme::BG); lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE); @@ -2299,13 +2420,13 @@ void UITask::back() { case SCR_QUICK: // back to Messages & contacts' bottom, where the row is if (_nav_overlay) { navClosePopup(); break; } showSchemaSettings(settings::PG_MESSAGES); - if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); } + if (_body) { layoutNow(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); } break; case SCR_MELODY: // back to the Sound page's bottom, where the melodies are melodySave(); stopMelody(); showSchemaSettings(settings::PG_SOUND); - if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); } + if (_body) { layoutNow(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); } break; case SCR_CLOCK: showHome(); break; case SCR_RADIO: @@ -2321,7 +2442,7 @@ void UITask::back() { case SCR_SCOPES: // back to the Radio screen's bottom, where Scopes is if (_nav_overlay) { navClosePopup(); break; } showRadio(); - if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); } + if (_body) { layoutNow(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); } break; case SCR_CHATS: if (_nav_overlay) navClosePopup(); else showHome(); break; case SCR_NEARBY: @@ -2518,7 +2639,7 @@ void UITask::buildChats() { stylePrimary(nb); if (unreadTotal() > 0) { lv_obj_t* rb = headerButton(_header, LV_SYMBOL_OK " Read all", onMarkAllRead, 0, NULL); - lv_obj_update_layout(_header); + layoutNow(_header); lv_obj_align_to(rb, nb, LV_ALIGN_OUT_LEFT_MID, -6, 0); } } @@ -2627,7 +2748,7 @@ void UITask::chatFold(int which) { lvport::saveChatFold(s_chat_fold); int y = _body ? lv_obj_get_scroll_y(_body) : 0; buildChats(); - if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } + if (_body) { layoutNow(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } } // ── Contact picker (start a DM) ─────────────────────────────────────────────── @@ -2931,7 +3052,7 @@ void UITask::refreshNearbyList() { lv_obj_set_style_pad_top(l, 12, 0); } if (scroll > 0) { - lv_obj_update_layout(_nearby_list); + layoutNow(_nearby_list); lv_obj_scroll_to_y(_nearby_list, scroll, LV_ANIM_OFF); } } @@ -3186,7 +3307,7 @@ void UITask::setKeyboardVisible(bool show) { lv_obj_set_height(_body, lv_display_get_vertical_resolution(NULL) - top); } if (_thread_list) { // keep the newest message in view - lv_obj_update_layout(_thread_list); + layoutNow(_thread_list); lv_obj_scroll_to_y(_thread_list, LV_COORD_MAX, LV_ANIM_OFF); } } @@ -3273,7 +3394,7 @@ void UITask::buildThread() { if (_compose_ta && _share_text[0]) { // shareToMessage(): the text waits in the field lv_textarea_set_text(_compose_ta, _share_text); _share_text[0] = '\0'; - lv_obj_update_layout(screen()); + layoutNow(screen()); setKeyboardVisible(true); } } @@ -3694,7 +3815,7 @@ void UITask::refreshThread() { if (s_older_lbl) { char t[40]; olderText(t, sizeof(t)); setText(s_older_lbl, t); } s_th_rows_n = s_msg_meta_n = s_msg_loc_n = n; if (n > kept) { // new messages: down to them - lv_obj_update_layout(_thread_list); + layoutNow(_thread_list); lv_obj_scroll_to_y(_thread_list, LV_COORD_MAX, LV_ANIM_OFF); } return; @@ -3714,7 +3835,7 @@ void UITask::refreshThread() { s_th_rows_newest = _thread_skip == 0; // a page back is built whole each time if (_thread_skip > 0) pageButton(_thread_list, LV_SYMBOL_DOWN " Newer messages", -1); if (n == 0) label(_thread_list, "No messages yet", THEME_FONT_BODY, theme::TEXT_MUTED); - lv_obj_update_layout(_thread_list); + layoutNow(_thread_list); lv_obj_scroll_to_y(_thread_list, _thread_scroll_top ? 0 : LV_COORD_MAX, LV_ANIM_OFF); _thread_scroll_top = false; } @@ -4025,7 +4146,7 @@ void UITask::setSchemaValue(int idx, int v) { int32_t y = body ? lv_obj_get_scroll_y(body) : 0; buildSchemaSettings(); fillFlush(); - if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } + if (_body) { layoutNow(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } } } @@ -4045,7 +4166,7 @@ void UITask::buildSettings() { // A group at a time (fillStart): the first shows at once. Coming back to // where it was scrolled needs them all first. fillStart(SETTINGS_GROUPS, 1, &UITask::settingsGroup); - if (restore) { fillFlush(); lv_obj_update_layout(body); lv_obj_scroll_to_y(body, s_settings_y, LV_ANIM_OFF); } + if (restore) { fillFlush(); layoutNow(body); lv_obj_scroll_to_y(body, s_settings_y, LV_ANIM_OFF); } } void UITask::settingsGroup(int i) { diff --git a/examples/companion_radio/ui-lvgl/WifiScreen.h b/examples/companion_radio/ui-lvgl/WifiScreen.h index aa72a8ce..98ca4084 100644 --- a/examples/companion_radio/ui-lvgl/WifiScreen.h +++ b/examples/companion_radio/ui-lvgl/WifiScreen.h @@ -147,7 +147,7 @@ void UITask::buildWifi() { static void wifiRebuild(UITask* ui, lv_obj_t*& body) { int32_t y = body ? lv_obj_get_scroll_y(body) : 0; ui->showWifi(); - if (body) { lv_obj_update_layout(body); lv_obj_scroll_to_y(body, y, LV_ANIM_OFF); } + if (body) { layoutNow(body); lv_obj_scroll_to_y(body, y, LV_ANIM_OFF); } } void UITask::wifiEdit(lv_obj_t* ta) { @@ -160,7 +160,7 @@ void UITask::wifiEdit(lv_obj_t* ta) { lv_obj_remove_flag(_wifi_kb, LV_OBJ_FLAG_HIDDEN); if (_body) lv_obj_set_height(_body, lv_obj_get_height(_body) - lv_obj_get_height(_wifi_kb)); } - lv_obj_update_layout(screen()); + layoutNow(screen()); lv_obj_scroll_to_view(ta, LV_ANIM_OFF); } @@ -241,7 +241,7 @@ void UITask::wifiTestShow(const char* msg, const char* ssid, const char* pass) { lv_textarea_set_text(_wifi_ssid, ssid); lv_textarea_set_text(_wifi_pass, pass); } - lv_obj_update_layout(_body); + layoutNow(_body); lv_obj_scroll_to_view(_wifi_status, LV_ANIM_ON); }