diff --git a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp index c709ea80..805a423a 100644 --- a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.cpp @@ -399,6 +399,21 @@ void NomadNetScreen::status_timer_cb(lv_timer_t* timer){ screen->_status_timer=nullptr; screen->apply_browser_layout(false); } +void NomadNetScreen::clear_status(){ + cancel_status_timer(); + if(_status)lv_label_set_text(_status,""); + apply_browser_layout(false); +} +void NomadNetScreen::set_partial_activity(bool active){ + if(!_reload_button)return; + lv_obj_set_style_bg_color(_reload_button, + active?Theme::primary():Theme::surfaceContainer(),LV_STATE_DEFAULT); + lv_obj_set_style_bg_color(_reload_button, + active?Theme::primary():Theme::primaryPressed(),LV_STATE_FOCUSED); + if(lv_obj_t* icon=lv_obj_get_child(_reload_button,0)) + lv_obj_set_style_text_color(icon,active?Theme::warning():Theme::textPrimary(),0); + lv_obj_invalidate(_reload_button); +} void NomadNetScreen::set_status(const char* value){ cancel_status_timer(); const char* status=value?value:""; diff --git a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.h b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.h index c28271f4..6e9df4ba 100644 --- a/lib/tdeck_ui/UI/LXMF/NomadNetScreen.h +++ b/lib/tdeck_ui/UI/LXMF/NomadNetScreen.h @@ -34,6 +34,8 @@ public: bool set_local_address(const std::string& address); std::string address() const; void set_status(const char* status); + void clear_status(); + void set_partial_activity(bool active); bool set_page(const NomadNet::Document& document); bool prepare_submission(uint16_t link_id, uint32_t generation, std::string& target, diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index 4b9d902c..f40dc827 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -2174,7 +2174,10 @@ bool UIManager::nomad_refresh_path_after_link_failure() { _nomad_state = NomadState::PATH; _nomad_deadline_ms = millis() + NomadNet::RequestPolicy::PATH_WAIT_MS; LVGL_LOCK(); - _nomadnet_screen->set_status("Refreshing stale path..."); + if (_nomad_partial_controller.active()) + _nomadnet_screen->set_partial_activity(true); + else + _nomadnet_screen->set_status("Refreshing stale path..."); return true; } @@ -2182,6 +2185,10 @@ uint32_t UIManager::nomad_advance_navigation_generation() { _nomad_partial_scheduler.cancel(_nomad_navigation_generation); _nomad_partial_controller.cancel(); _nomad_partial_request = NomadNet::PartialRequest{}; + { + LVGL_LOCK(); + _nomadnet_screen->set_partial_activity(false); + } ++_nomad_navigation_generation; if (_nomad_navigation_generation == 0) ++_nomad_navigation_generation; return _nomad_navigation_generation; @@ -2388,8 +2395,8 @@ bool UIManager::nomad_schedule_partial_ids(const std::string& address, start = end + 1; } LVGL_LOCK(); - _nomadnet_screen->set_status(matched - ? "Dynamic content queued" : "No matching dynamic content"); + if (!matched) + _nomadnet_screen->set_status("No matching dynamic content"); return matched; } @@ -2453,6 +2460,10 @@ void UIManager::nomad_poll_partials(uint32_t now_ms) { _nomad_partial_request = request; _nomad_partial_url = std::move(target); _nomad_state = NomadState::PARTIAL_PENDING; + { + LVGL_LOCK(); + _nomadnet_screen->set_partial_activity(true); + } nomad_begin_partial_transport(); } @@ -2488,10 +2499,6 @@ void UIManager::nomad_begin_partial_transport() { _nomad_request_policy.reset(); _nomad_destination_hash = Bytes(); _nomad_destination_hash.assignHex(_nomad_partial_url.destination_hex.c_str()); - { - LVGL_LOCK(); - _nomadnet_screen->set_status("Discovering dynamic-content path..."); - } if (Transport::has_path(_nomad_destination_hash)) nomad_start_link(); else { Transport::request_path(_nomad_destination_hash); @@ -2536,7 +2543,9 @@ void UIManager::nomad_release_partial(bool success, bool deferred, const char* s _nomad_partial_request = NomadNet::PartialRequest{}; _nomad_partial_url = NomadNet::Url{}; LVGL_LOCK(); - _nomadnet_screen->set_status(status); + _nomadnet_screen->set_partial_activity(false); + if (success) _nomadnet_screen->clear_status(); + else if (status) _nomadnet_screen->set_status(status); } void UIManager::nomad_begin_live_transport() { @@ -2639,7 +2648,10 @@ void UIManager::nomad_start_link() { _nomad_state = NomadState::LINK; _nomad_deadline_ms = millis() + NomadNet::RequestPolicy::LINK_WAIT_MS; LVGL_LOCK(); - _nomadnet_screen->set_status("Establishing encrypted link..."); + if (_nomad_partial_controller.active()) + _nomadnet_screen->set_partial_activity(true); + else + _nomadnet_screen->set_status("Establishing encrypted link..."); } void UIManager::nomad_identify_link_if_configured() { @@ -2727,8 +2739,9 @@ void UIManager::nomad_send_request() { _nomad_state = NomadState::REQUEST; _nomad_deadline_ms = millis() + 30000; LVGL_LOCK(); - _nomadnet_screen->set_status(_nomad_partial_controller.active() - ? "Updating dynamic content..." : "Requesting page..."); + if (_nomad_partial_controller.active()) + _nomadnet_screen->set_partial_activity(true); + else _nomadnet_screen->set_status("Requesting page..."); } bool UIManager::nomad_apply_page_bytes(const uint8_t* data, std::size_t size, bool cached) { @@ -2904,11 +2917,10 @@ void UIManager::nomad_update() { } break; case NomadNet::AsyncMailbox::Kind::PROGRESS: - if (_nomad_state == NomadState::REQUEST) { + if (_nomad_state == NomadState::REQUEST && + !_nomad_partial_controller.active()) { LVGL_LOCK(); - if (_nomad_partial_controller.active()) - _nomadnet_screen->set_status("Receiving dynamic content..."); - else _nomadnet_screen->set_status("Receiving page..."); + _nomadnet_screen->set_status("Receiving page..."); } break; case NomadNet::AsyncMailbox::Kind::OVERSIZED: @@ -2936,6 +2948,7 @@ void UIManager::nomad_update() { _nomad_partial_controller.cancel(); nomad_stop_transport(); LVGL_LOCK(); + _nomadnet_screen->set_partial_activity(false); _nomadnet_screen->set_status("Discarded stale dynamic content"); break; } @@ -2987,7 +3000,7 @@ void UIManager::nomad_update() { : "Dynamic content exceeds page limits"); break; } - nomad_finish_partial(true, "Dynamic content updated"); + nomad_finish_partial(true, nullptr); break; } NomadNet::Document document; diff --git a/tests/native/nomadnet_lvgl_acceptance/acceptance.cpp b/tests/native/nomadnet_lvgl_acceptance/acceptance.cpp index 7e81637a..b69db15c 100644 --- a/tests/native/nomadnet_lvgl_acceptance/acceptance.cpp +++ b/tests/native/nomadnet_lvgl_acceptance/acceptance.cpp @@ -199,7 +199,8 @@ int main() { bool ready = false, cancel = false, enter = false, escape = false, focus_restore = false; bool table_pixels = false, form_pixels = false, focus_pixels = false, glyph_pixels = false; bool background_pixels = false, teardown = false, cached_status_transient = false; - bool cached_status_oom_collapses = false; + bool cached_status_oom_collapses = false, partial_activity_no_layout = false; + bool partial_failure_visible_during_retry = false; bool partial_replace = false, partial_forms = false, partial_empty = false; bool partial_link_focus = false, partial_focus_fallback = false; bool partial_scroll_anchor = false; @@ -239,8 +240,46 @@ int main() { fit_tier = fit.valid && fit.tier == UI::LXMF::NomadNet::TableLayoutTier::FIT; fit_columns = fit.columns == 2 && fit.cards == 0 && fit.x >= 0 && fit.width > 0 && fit.x + fit.width <= 304 && fit.y >= 0 && fit.height > 0; + std::string activity_source; + for (int i = 0; i < 40; ++i) + activity_source += "Activity row " + std::to_string(i) + "\n\n"; + assert(screen.set_page(parser.parse(activity_source))); + screen.clear_status(); + assert(screen.scroll_to_logical(80, LV_ANIM_OFF)); + lv_obj_update_layout(screen._screen); + const int16_t full_content_height = lv_obj_get_height(screen._content); + const int32_t scroll_before_activity = screen._logical_scroll; + const int32_t widget_scroll_before_activity = lv_obj_get_scroll_y(screen._content); + lv_obj_add_state(screen._reload_button, LV_STATE_FOCUSED); + lv_obj_t* reload_icon = lv_obj_get_child(screen._reload_button, 0); + assert(reload_icon); + const lv_color_t focused_idle = lv_obj_get_style_text_color( + reload_icon, LV_PART_MAIN); + screen.set_partial_activity(true); + lv_obj_update_layout(screen._screen); + const lv_color_t focused_active = lv_obj_get_style_text_color( + reload_icon, LV_PART_MAIN); + screen.set_partial_activity(false); + lv_obj_clear_state(screen._reload_button, LV_STATE_FOCUSED); + lv_obj_update_layout(screen._screen); + partial_activity_no_layout = + lv_obj_has_flag(screen._status, LV_OBJ_FLAG_HIDDEN) && + lv_obj_get_height(screen._content) == full_content_height && + screen._logical_scroll == scroll_before_activity && + lv_obj_get_scroll_y(screen._content) == widget_scroll_before_activity && + lv_color_to32(focused_active) != lv_color_to32(focused_idle); - auto reflow_doc = parser.parse( + screen.set_status("Dynamic refresh failed"); + lv_obj_update_layout(screen._screen); + const int16_t failure_content_height = lv_obj_get_height(screen._content); + screen.set_partial_activity(true); + lv_obj_update_layout(screen._screen); + partial_failure_visible_during_retry = + !lv_obj_has_flag(screen._status, LV_OBJ_FLAG_HIDDEN) && + lv_obj_get_height(screen._content) == failure_content_height; + screen.set_partial_activity(false); + screen.clear_status(); + const auto reflow_doc = parser.parse( "`tc304\nFirst very wide heading|Second very wide heading|Third very wide heading\n" "---|---|---\nA long value that cannot fit beside peers|" "Another long value that wraps repeatedly|Final long value\n`t"); @@ -541,12 +580,13 @@ int main() { "LVGL ACCEPT 320x240 fit_tier=%d fit_columns=%d reflow_tier=%d reflow_cards=%d " "eight_column_tier=%d eight_column_preserved=%d eight_column_pixels=%d table_link_focus=%d eight_column_objects=%d " "focus_events=%d edge_scroll=%d ready=%d cancel=%d enter=%d escape=%d focus_restore=%d " - "teardown=%d cached_status_transient=%d cached_status_oom_collapses=%d stale_group=%d background_pixels=%d table_pixels=%d form_pixels=%d " + "teardown=%d cached_status_transient=%d cached_status_oom_collapses=%d partial_activity_no_layout=%d partial_failure_visible_during_retry=%d stale_group=%d background_pixels=%d table_pixels=%d form_pixels=%d " "focus_pixels=%d glyph_pixels=%d partial_replace=%d partial_forms=%d partial_link_focus=%d partial_focus_fallback=%d partial_scroll_anchor=%d partial_second_scroll_rollback=%d partial_region_top_fallback=%d partial_empty=%d exact_fonts=1 objects=%u\n", fit_tier, fit_columns, reflow_tier, reflow_cards, eight_column_tier, eight_column_preserved, eight_column_pixels, table_link_focus, eight_column_objects, focus_events, edge_scroll, ready, cancel, enter, escape, focus_restore, teardown, cached_status_transient, - cached_status_oom_collapses, 0, background_pixels, + cached_status_oom_collapses, partial_activity_no_layout, + partial_failure_visible_during_retry, 0, background_pixels, table_pixels, form_pixels, focus_pixels, glyph_pixels, partial_replace, partial_forms, partial_link_focus, partial_focus_fallback, partial_scroll_anchor, partial_second_scroll_rollback, @@ -555,7 +595,8 @@ int main() { eight_column_preserved && eight_column_pixels && table_link_focus && eight_column_objects && focus_events && edge_scroll && ready && cancel && enter && escape && focus_restore && teardown && cached_status_transient && - cached_status_oom_collapses && background_pixels && + cached_status_oom_collapses && partial_activity_no_layout && + partial_failure_visible_during_retry && background_pixels && table_pixels && form_pixels && focus_pixels && glyph_pixels && partial_replace && partial_forms && partial_link_focus && partial_focus_fallback && partial_scroll_anchor && partial_second_scroll_rollback && diff --git a/tests/native/test_app_launcher_nomadnet.py b/tests/native/test_app_launcher_nomadnet.py index 268e8fb9..0ca1c00f 100644 --- a/tests/native/test_app_launcher_nomadnet.py +++ b/tests/native/test_app_launcher_nomadnet.py @@ -482,6 +482,33 @@ def test_nomadnet_page_body_uses_one_compact_custom_viewport(): assert "lv_group_set_editing(group,false)" in screen +def test_partial_success_uses_top_chrome_activity_without_status_banner(): + manager = (INCLUDE / "UIManager.cpp").read_text() + screen = (INCLUDE / "NomadNetScreen.cpp").read_text() + assert '"Dynamic content updated"' not in manager + assert "nomad_finish_partial(true, nullptr);" in manager + assert "_nomadnet_screen->set_partial_activity(true);" in manager + assert "_nomadnet_screen->set_partial_activity(false);" in manager + assert "void NomadNetScreen::set_partial_activity(bool active)" in screen + activity = screen[screen.index("void NomadNetScreen::set_partial_activity(bool active)"): + screen.index("void NomadNetScreen::set_status(")] + assert "apply_browser_layout" not in activity + assert "lv_timer" not in activity + assert "LV_STATE_FOCUSED" in activity + assert "_reload_button" in activity + release = manager[manager.index("void UIManager::nomad_release_partial("): + manager.index("void UIManager::nomad_begin_live_transport()")] + assert "if (success) _nomadnet_screen->clear_status();" in release + start_link = manager[manager.index("void UIManager::nomad_start_link()"): + manager.index("void UIManager::nomad_identify_link_if_configured()")] + assert "if (_nomad_partial_controller.active())" in start_link + assert "else\n _nomadnet_screen->set_status(\"Establishing encrypted link...\");" in start_link + refresh_path = manager[manager.index("bool UIManager::nomad_refresh_path_after_link_failure()"): + manager.index("uint32_t UIManager::nomad_advance_navigation_generation()")] + assert "if (_nomad_partial_controller.active())" in refresh_path + assert "else\n _nomadnet_screen->set_status(\"Refreshing stale path...\");" in refresh_path + + def test_successful_page_application_releases_normalized_response(): manager = (INCLUDE / "UIManager.cpp").read_text() response = manager[manager.index("case NomadNet::AsyncMailbox::Kind::RESPONSE:"): diff --git a/tests/native/test_nomadnet_lvgl_acceptance.py b/tests/native/test_nomadnet_lvgl_acceptance.py index 18c2215a..1a756afa 100644 --- a/tests/native/test_nomadnet_lvgl_acceptance.py +++ b/tests/native/test_nomadnet_lvgl_acceptance.py @@ -41,7 +41,7 @@ def test_actual_nomadnet_screen_320x240_acceptance(tmp_path): assert "eight_column_tier=1 eight_column_preserved=1 eight_column_pixels=1 table_link_focus=1 eight_column_objects=1" in output assert "focus_events=1 edge_scroll=1" in output assert "ready=1 cancel=1 enter=1 escape=1 focus_restore=1" in output - assert "teardown=1 cached_status_transient=1 cached_status_oom_collapses=1 stale_group=0" in output + assert "teardown=1 cached_status_transient=1 cached_status_oom_collapses=1 partial_activity_no_layout=1 partial_failure_visible_during_retry=1 stale_group=0" in output assert "background_pixels=1 table_pixels=1 form_pixels=1 focus_pixels=1 glyph_pixels=1" in output assert "partial_focus_fallback=1 partial_scroll_anchor=1" in output assert "partial_second_scroll_rollback=1" in output