Files

746 lines
36 KiB
C++

#include "platform/ui/gps_runtime.h"
#include "platform/ui/map_search_runtime.h"
#include "platform/ui/psram_ui_lease.h"
#include "ui/components/two_pane_styles.h"
#include "ui/page/page_profile.h"
#include "ui/screens/geocaching/geocaching_page_shell.h"
#include "ui/screens/gps/gps_page_runtime.h"
#include "ui/support/card_file_location.h"
#include "ui/widgets/top_bar_power_presenter.h"
#include "ui_presentation/map/map_location_request.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
namespace
{
std::string card_file;
size_t card_file_offset = 0;
bool testCardFileLocations()
{
lv_fs_drv_t driver;
lv_fs_drv_init(&driver);
driver.letter = 'R';
driver.open_cb = [](lv_fs_drv_t*, const char*, lv_fs_mode_t) -> void*
{ card_file_offset = 0; return &card_file; };
driver.close_cb = [](lv_fs_drv_t*, void*)
{ return LV_FS_RES_OK; };
driver.read_cb = [](lv_fs_drv_t*, void*, void* buffer, uint32_t bytes, uint32_t* count)
{
*count = static_cast<uint32_t>(std::min<size_t>(bytes, card_file.size() - card_file_offset));
std::memcpy(buffer, card_file.data() + card_file_offset, *count);
card_file_offset += *count;
return LV_FS_RES_OK;
};
lv_fs_drv_register(&driver);
double latitude = 0, longitude = 0;
card_file = "<kml xmlns=\"http://www.opengis.net/kml/2.2\"><Placemark><LineString><coordinates>120.5,30.5,10 120.6,30.6,20</coordinates></LineString></Placemark></kml>";
if (!ui::support::readCardFileLocation("R:/route.kml", latitude, longitude) || latitude != 30.5 || longitude != 120.5)
{
std::printf("KML first point failed: %.7f %.7f, read=%u\n", latitude, longitude, unsigned(card_file_offset));
return false;
}
card_file = "<gpx><trk><trkseg><trkpt lat=\"30.5\" lon=\"120.5\"><ele>10</ele></trkpt><trkpt lat=\"30.6\" lon=\"120.6\"/></trkseg></trk></gpx>";
if (!ui::support::readCardFileLocation("R:/track.gpx", latitude, longitude) || latitude != 30.5 || longitude != 120.5)
{
std::puts("GPX first point failed");
return false;
}
card_file = "lat,lon,timestamp,satellites\n30.5,120.5,100,7\n30.6,120.6,101,7\n";
if (!ui::support::readCardFileLocation("R:/track.csv", latitude, longitude) || latitude != 30.5 || longitude != 120.5)
{
std::puts("Firmware CSV first point failed");
return false;
}
card_file = "epoch_s,lat,lon,alt_m,speed_mps,course_deg\n100,30.5,120.5,10,1,2\n";
if (!ui::support::readCardFileLocation("R:/track.csv", latitude, longitude) || latitude != 30.5 || longitude != 120.5)
{
std::printf("Desktop CSV first point failed: %.7f %.7f\n", latitude, longitude);
return false;
}
card_file = "<gpx><trk><trkseg/></trk></gpx>";
if (ui::support::readCardFileLocation("R:/empty.gpx", latitude, longitude)) return false;
return true;
}
} // namespace
namespace platform::ui::gps
{
GpsState test_position;
GpsState get_data() { return test_position; }
} // namespace platform::ui::gps
namespace platform::ui::map_search
{
uint32_t test_request = 0;
unsigned test_submissions = 0;
uint32_t submitRegion(double, double)
{
++test_submissions;
return ++test_request;
}
bool pollRegion(uint32_t request, RegionResult& output)
{
if (request != test_request) return false;
output.status = Status::Ready;
std::snprintf(output.path, sizeof(output.path), "Sample country / Woodland district");
return true;
}
} // namespace platform::ui::map_search
namespace gps::ui::runtime
{
const shell::Host* test_map_host = nullptr;
MapTarget test_map_target;
::ui::map::MapLocationRequest* test_location = nullptr;
bool test_map_available = true;
void enter(const shell::Host* host, lv_obj_t*, shell::Projection, ::ui::map::MapLocationRequest* location, ::ui::map::MapTargetRequest*)
{
test_map_host = host;
test_location = location;
location->result.state = test_map_available ? ::ui::map::MapLocationSelectionState::Selecting : ::ui::map::MapLocationSelectionState::Cancelled;
}
bool enter_target(const shell::Host* host, lv_obj_t*, const MapTarget& target)
{
test_map_host = host;
test_map_target = target;
return true;
}
void exit(lv_obj_t*)
{
test_map_host = nullptr;
test_location = nullptr;
}
} // namespace gps::ui::runtime
namespace ui::widgets::top_bar_power
{
void bind(TopBar& bar) { lv_label_set_text(bar.right_label, LV_SYMBOL_BATTERY_FULL " 100%"); }
void unbind(TopBar&) {}
} // namespace ui::widgets::top_bar_power
struct TestSource : ui::geocaching::Source
{
ui::geocaching::DraftInput draft;
std::string draft_name, draft_description, draft_hint;
bool has_draft = false, fail_save = false, snapshot_busy = false;
unsigned deletions = 0;
bool deleteDraft(const std::array<uint8_t, 16>& id, uint64_t version) override
{
if (!has_draft || id != draft.id || version != draft.generation) return false;
++deletions;
if (!fail_save)
{
has_draft = false;
++generation;
}
return true;
}
unsigned pending_reads = 0;
bool detail_ready = false;
unsigned archives = 0;
ui::geocaching::DraftSaveStatus archive_result = ui::geocaching::DraftSaveStatus::Pending;
bool archiveCache(const std::array<uint8_t, 32>&, const std::array<uint8_t, 32>&) override
{
++archives;
return true;
}
ui::geocaching::DraftSaveStatus archiveStatus(const std::array<uint8_t, 32>&) override { return archive_result; }
unsigned detail_closes = 0;
bool readDetail(const std::array<uint8_t, 32>&, const std::array<uint8_t, 32>&,
void (*sink)(const ui::geocaching::DetailView&, void*), void* context) override
{
ui::geocaching::DetailView view;
if (detail_ready)
{
view.status = ui::geocaching::DetailStatus::Ready;
view.description = "A woodland cache beside the old trail.\nFollow the stream to the stone bridge.";
view.hint = "Look beneath the large flat stone.";
view.can_archive = true;
}
sink(view, context);
return true;
}
void closeDetail() override { ++detail_closes; }
unsigned cancelled_reads = 0;
bool fail_read = false;
unsigned publications = 0;
uint8_t current_author = 7;
unsigned reads = 0, opens = 0, refreshes = 0, downloads = 0;
size_t item_count = 20;
size_t saved_index = SIZE_MAX;
size_t last_open = 0;
uint64_t generation = 1;
const char* list_status = "20 shared caches";
bool list_can_refresh = true;
size_t requested_offset = 0, requested_count = 0;
unsigned pending_windows = 0;
ui::geocaching::Section requested_section = ui::geocaching::Section::Discover;
void requestWindow(ui::geocaching::Section section, size_t offset, size_t count) override
{
requested_section = section;
requested_offset = offset;
requested_count = count;
}
void snapshot(ui::geocaching::Section section, ui::geocaching::Snapshot& out) override
{
out = {};
if (snapshot_busy)
{
out.busy = true;
std::snprintf(out.status.data(), out.status.size(), "Updating...");
return;
}
out.generation = generation;
out.count = item_count;
out.can_refresh = list_can_refresh;
if (section == ui::geocaching::Section::Published)
{
out.count = has_draft ? 1 : 0;
out.can_create = true;
}
std::snprintf(out.status.data(), out.status.size(), "%s", list_status);
}
bool item(ui::geocaching::Section section, size_t index, uint64_t expected, ui::geocaching::Item& out) override
{
++reads;
if (!requested_count || requested_count > 4 ||
(section == ui::geocaching::Section::Published && requested_section != section)) return false;
if (pending_windows)
{
--pending_windows;
return false;
}
if (index >= 20 || expected != generation) return false;
out = {};
if (section == ui::geocaching::Section::Published)
{
if (!has_draft || index) return false;
out.is_draft = true;
out.state = draft.state;
out.publication_revision = publications ? 1 : 0;
out.edit_generation = draft.generation;
std::memcpy(out.id.data(), draft.id.data(), 16);
std::snprintf(out.name.data(), out.name.size(), "%s", draft_name.c_str());
return true;
}
out.id[0] = static_cast<uint8_t>(index);
out.has_coordinates = true;
out.latitude_e7 = 305000000;
out.longitude_e7 = 1205000000;
out.downloaded = saved_index == index;
out.can_download = !out.downloaded;
std::snprintf(out.name.data(), out.name.size(), "Cache %02u - woodland trail", unsigned(index + 1));
std::snprintf(out.detail.data(), out.detail.size(), "30.5000000, 120.5000000\nDifficulty 2.0 / Terrain 2.5\nDirectory preview - not yet downloaded");
return true;
}
void refresh(ui::geocaching::Section) override
{
++refreshes;
++generation;
}
void open(const ui::geocaching::Item& item, uint64_t expected) override
{
if (expected == generation)
{
++opens;
last_open = item.id[0];
}
}
bool download(const ui::geocaching::Item& item, uint64_t expected) override
{
if (expected != generation) return false;
++downloads;
saved_index = item.id[0];
++generation;
return true;
}
bool saveDraft(ui::geocaching::DraftInput& input) override
{
if (fail_save) return true;
input.id.fill(0x42);
draft = input;
++draft.generation;
draft_name = input.name;
draft_description = input.description;
draft_hint = input.hint;
draft.name = draft_name;
draft.description = draft_description;
draft.hint = draft_hint;
has_draft = true;
++generation;
return true;
}
ui::geocaching::DraftSaveStatus draftSaveStatus(const std::array<uint8_t, 16>&, uint64_t) override
{
return fail_save ? ui::geocaching::DraftSaveStatus::Failed : ui::geocaching::DraftSaveStatus::Saved;
}
ui::geocaching::DraftReadStatus readDraft(const std::array<uint8_t, 16>& id, void (*sink)(const ui::geocaching::DraftInput&, void*), void* context) override
{
if (pending_reads)
{
--pending_reads;
return ui::geocaching::DraftReadStatus::Pending;
}
if (fail_read) return ui::geocaching::DraftReadStatus::Failed;
if (!has_draft || id != draft.id) return ui::geocaching::DraftReadStatus::Failed;
sink(draft, context);
return ui::geocaching::DraftReadStatus::Ready;
}
void cancelDraftRead(const std::array<uint8_t, 16>& id) override
{
if (id == draft.id) ++cancelled_reads;
}
bool publicationAuthor(const std::array<uint8_t, 16>& id, uint64_t version, std::array<uint8_t, 64>& author, uint32_t* from, uint32_t* to) override
{
if (!has_draft || id != draft.id || version != draft.generation) return false;
author.fill(current_author);
if (from) *from = 0;
if (to) *to = 1;
return true;
}
bool publishDraft(const std::array<uint8_t, 16>& id, uint64_t version, const std::array<uint8_t, 64>& expected, uint32_t revision) override
{
std::array<uint8_t, 64> current;
if (!publicationAuthor(id, version, current, nullptr, nullptr) || current != expected || revision != 1) return false;
++publications;
return true;
}
};
bool save(const std::string& path, lv_obj_t* screen)
{
lv_obj_update_layout(screen);
auto* image = lv_snapshot_take(screen, LV_COLOR_FORMAT_RGB888);
if (!image) return false;
std::ofstream out(path, std::ios::binary);
out << "P6\n"
<< image->header.w << " " << image->header.h << "\n255\n";
for (unsigned y = 0; y < image->header.h; ++y)
for (unsigned x = 0; x < image->header.w; ++x)
{
const auto* pixel = image->data + y * image->header.stride + x * 3;
const char rgb[] = {char(pixel[2]), char(pixel[1]), char(pixel[0])};
out.write(rgb, 3);
}
lv_draw_buf_destroy(image);
return out.good();
}
lv_obj_t* findKeyboard(lv_obj_t* root)
{
if (lv_obj_check_type(root, &lv_buttonmatrix_class)) return root;
for (uint32_t i = 0; i < lv_obj_get_child_count(root); ++i)
if (auto* found = findKeyboard(lv_obj_get_child(root, i))) return found;
return nullptr;
}
int main(int argc, char** argv)
{
if (argc != 4) return 1;
const int width = std::atoi(argv[1]), height = std::atoi(argv[2]);
lv_init();
if (!testCardFileLocations()) return 91;
auto* display = lv_display_create(width, height);
std::vector<uint8_t> pixels(width * 32 * 4);
lv_display_set_color_format(display, LV_COLOR_FORMAT_XRGB8888);
lv_display_set_buffers(display, pixels.data(), nullptr, pixels.size(), LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_flush_cb(display, [](lv_display_t* display, const lv_area_t*, uint8_t*)
{ lv_display_flush_ready(display); });
auto profile = width == 480 ? ui::page_profile::make_pager_profile() : ui::page_profile::make_tdeck_profile();
profile.compact_touch_keyboard = width == 320;
if (profile.compact_touch_keyboard) profile.ime_keyboard_height = 108;
ui::page_profile::set_active_profile(&profile);
auto* screen = lv_screen_active();
auto* old_group = lv_group_create();
lv_group_set_default(old_group);
unsigned exits = 0;
ui::page::Host host{&exits, [](void* context)
{ ++*static_cast<unsigned*>(context); }};
geocaching::ui::shell::enter(&host, screen);
auto* root = lv_obj_get_child(screen, 0);
if (!platform::ui::psramUiRequired()) return 70;
if (!root || !save(std::string(argv[3]) + "-empty.ppm", screen)) return 2;
auto* tabs = lv_obj_get_child(root, 1);
for (unsigned i = 0; i < 3; ++i)
{
auto* tab = lv_obj_get_child(tabs, i);
if (lv_color_to_u32(lv_obj_get_style_bg_color(tab, LV_PART_MAIN)) == lv_color_to_u32(lv_palette_main(LV_PALETTE_BLUE))) return 3;
}
TestSource source;
geocaching::ui::shell::bind(&source);
lv_tick_inc(501);
lv_timer_handler();
auto* list = lv_obj_get_child(root, 3);
const auto visible = lv_obj_get_child_count(list);
if (visible != 1 || source.reads != 1 || source.requested_count != 1) return 4;
if (!save(std::string(argv[3]) + "-list.ppm", screen)) return 5;
auto* first_row = lv_obj_get_child(list, 0);
if (std::strcmp(lv_label_get_text(lv_obj_get_child(first_row, 1)), "My position unknown") ||
!std::strstr(lv_label_get_text(lv_obj_get_child(first_row, 2)), "~ Sample country") ||
platform::ui::map_search::test_submissions != 1) return 88;
platform::ui::gps::test_position.valid = true;
platform::ui::gps::test_position.lat = 30.5005;
platform::ui::gps::test_position.lng = 120.5005;
lv_tick_inc(600);
lv_timer_handler();
if (!std::strstr(lv_label_get_text(lv_obj_get_child(first_row, 1)), "Distance ") ||
!std::strstr(lv_label_get_text(lv_obj_get_child(first_row, 1)), " m") ||
platform::ui::map_search::test_submissions != 1) return 89;
if (!save(std::string(argv[3]) + "-location-card.ppm", screen)) return 90;
// Flipping a large directory hydrates one item and reuses every card widget.
const auto card_children = lv_obj_get_child_count(first_row);
const auto reads_before_flip = source.reads;
auto* navigation = lv_obj_get_child(root, 4);
lv_obj_send_event(lv_obj_get_child(navigation, 3), LV_EVENT_CLICKED, nullptr);
if (source.requested_offset != 1 || source.requested_count != 1 || source.reads != reads_before_flip + 1 ||
lv_obj_get_child(list, 0) != first_row || lv_obj_get_child_count(first_row) != card_children ||
std::strcmp(lv_label_get_text(lv_obj_get_child(first_row, 0)), "Cache 02 - woodland trail")) return 86;
lv_obj_send_event(lv_obj_get_child(navigation, 2), LV_EVENT_CLICKED, nullptr);
source.item_count = 5000;
++source.generation;
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_get_child(list, 0) != first_row || lv_obj_get_child_count(list) != 1 ||
lv_obj_get_child_count(first_row) != card_children || source.requested_count != 1) return 87;
source.item_count = 20;
++source.generation;
lv_tick_inc(600);
lv_timer_handler();
const auto reads_before_busy = source.reads;
source.list_status = "Saving and verifying GPX...";
source.list_can_refresh = false;
lv_tick_inc(600);
lv_timer_handler();
auto* status_footer = lv_obj_get_child(root, 4);
if (lv_obj_get_child(list, 0) != first_row || source.reads != reads_before_busy ||
std::strcmp(lv_label_get_text(lv_obj_get_child(root, 2)), source.list_status) ||
!lv_obj_has_state(lv_obj_get_child(status_footer, 0), LV_STATE_DISABLED)) return 78;
source.list_status = "20 shared caches";
source.list_can_refresh = true;
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_get_child(list, 0) != first_row || source.reads != reads_before_busy ||
lv_obj_has_state(lv_obj_get_child(status_footer, 0), LV_STATE_DISABLED)) return 79;
source.snapshot_busy = true;
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_get_child_count(list) != visible || lv_obj_get_child(list, 0) != first_row ||
source.reads != reads_before_busy || std::strcmp(lv_label_get_text(lv_obj_get_child(root, 2)), "20 shared caches")) return 60;
lv_obj_send_event(lv_obj_get_child(tabs, 1), LV_EVENT_CLICKED, nullptr);
if (lv_obj_get_child_count(list) != 1 ||
std::strcmp(lv_label_get_text(lv_obj_get_child(lv_obj_get_child(list, 0), 0)), "Loading caches...")) return 61;
lv_obj_send_event(lv_obj_get_child(tabs, 0), LV_EVENT_CLICKED, nullptr);
source.snapshot_busy = false;
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_get_child_count(list) != visible) return 62;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
if (source.opens != 1 || source.last_open != 0) return 6;
if (std::strcmp(lv_label_get_text(lv_obj_get_child(lv_obj_get_child(list, 1), 1)), "Loading details...")) return 64;
source.detail_ready = true;
// Detail completion is independent of list generation changes.
lv_tick_inc(600);
lv_timer_handler();
if (!std::strstr(lv_label_get_text(lv_obj_get_child(lv_obj_get_child(list, 1), 1)), "Follow the stream") ||
!std::strstr(lv_label_get_text(lv_obj_get_child(lv_obj_get_child(list, 2), 1)), "large flat stone") ||
lv_obj_has_flag(lv_obj_get_child(list, 2), LV_OBJ_FLAG_HIDDEN)) return 65;
if (!save(std::string(argv[3]) + "-detail.ppm", screen)) return 11;
auto* archive_action = lv_obj_get_child(list, 4);
if (lv_color_to_u32(lv_obj_get_style_bg_color(archive_action, LV_PART_MAIN)) != lv_color_to_u32(lv_color_hex(ui::components::two_pane_styles::kMainPanelBg))) return 85;
if (lv_obj_has_flag(archive_action, LV_OBJ_FLAG_HIDDEN)) return 80;
lv_obj_send_event(archive_action, LV_EVENT_CLICKED, nullptr);
if (source.archives || std::strcmp(lv_label_get_text(lv_obj_get_child(archive_action, 0)), "Confirm public archive")) return 81;
lv_obj_send_event(archive_action, LV_EVENT_CLICKED, nullptr);
if (source.archives != 1 || !lv_obj_has_state(archive_action, LV_STATE_DISABLED)) return 82;
lv_tick_inc(600);
lv_timer_handler();
if (std::strcmp(lv_label_get_text(lv_obj_get_child(archive_action, 0)), "Awaiting directory confirmation...")) return 83;
source.archive_result = ui::geocaching::DraftSaveStatus::Saved;
lv_tick_inc(600);
lv_timer_handler();
if (std::strcmp(lv_label_get_text(lv_obj_get_child(archive_action, 0)), "Archived; directory confirmed")) return 84;
lv_obj_scroll_to_view(lv_obj_get_child(list, 2), LV_ANIM_OFF);
if (!save(std::string(argv[3]) + "-detail-hint.ppm", screen)) return 66;
if (lv_obj_get_scroll_y(list) <= 0) return 67;
auto* details_footer = lv_obj_get_child(root, 4);
const auto detail_reads = source.reads;
source.snapshot_busy = true;
lv_tick_inc(600);
lv_timer_handler();
if (source.reads != detail_reads || lv_obj_has_state(lv_obj_get_child(details_footer, 3), LV_STATE_DISABLED) ||
std::strcmp(lv_label_get_text(lv_obj_get_child(lv_obj_get_child(list, 0), 1)), "Cache 01 - woodland trail")) return 63;
source.snapshot_busy = false;
lv_obj_send_event(lv_obj_get_child(details_footer, 3), LV_EVENT_CLICKED, nullptr);
if (source.downloads != 1 || source.saved_index != 0 ||
lv_obj_has_state(lv_obj_get_child(details_footer, 3), LV_STATE_DISABLED)) return 14;
lv_obj_send_event(lv_obj_get_child(details_footer, 3), LV_EVENT_CLICKED, nullptr);
if (!gps::ui::runtime::test_map_host || !lv_obj_has_flag(root, LV_OBJ_FLAG_HIDDEN) ||
std::strcmp(gps::ui::runtime::test_map_target.name, "Cache 01 - woodland trail")) return 15;
source.saved_index = 1;
++source.generation;
ui::map::MapOverlaySnapshot overlays;
const auto& map_target = gps::ui::runtime::test_map_target;
if (!map_target.append_overlays) return 17;
map_target.append_overlays(map_target.overlay_context, overlays);
if (overlays.item_count != 1 || overlays.items[0].kind != ui::map::MapOverlayKind::Geocache) return 18;
overlays.item_count = ui::map::MapOverlaySnapshot::kMaxItems;
map_target.append_overlays(map_target.overlay_context, overlays);
if (!overlays.truncated || overlays.item_count != ui::map::MapOverlaySnapshot::kMaxItems) return 19;
if (!map_target.select_overlay || overlays.items[0].stable_id != 2) return 20;
map_target.select_overlay(map_target.overlay_context, overlays.items[0].stable_id);
ui::page::request_exit(gps::ui::runtime::test_map_host);
ui::page::request_exit(gps::ui::runtime::test_map_host);
lv_tick_inc(10);
lv_timer_handler();
if (gps::ui::runtime::test_map_host || lv_obj_has_flag(root, LV_OBJ_FLAG_HIDDEN) || exits) return 16;
if (std::strcmp(lv_label_get_text(lv_obj_get_child(lv_obj_get_child(list, 0), 1)), "Cache 02 - woodland trail")) return 21;
lv_obj_send_event(lv_obj_get_child(lv_obj_get_child(root, 0), 0), LV_EVENT_CLICKED, nullptr);
if (exits || lv_obj_get_child_count(list) != visible) return 12;
auto* footer = lv_obj_get_child(root, 4);
source.pending_windows = 1;
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
if (source.requested_offset != visible || source.requested_count != visible ||
!lv_obj_has_state(lv_obj_get_child(footer, 3), LV_STATE_DISABLED)) return 51;
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_get_child_count(list) != visible || source.requested_offset != visible ||
lv_obj_has_state(lv_obj_get_child(footer, 3), LV_STATE_DISABLED)) return 52;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
if (source.opens != 2 || source.last_open != visible) return 7;
lv_obj_send_event(lv_obj_get_child(lv_obj_get_child(root, 0), 0), LV_EVENT_CLICKED, nullptr);
if (exits) return 13;
lv_obj_send_event(lv_obj_get_child(lv_obj_get_child(root, 0), 0), LV_EVENT_CLICKED, nullptr);
if (exits != 1) return 8;
geocaching::ui::shell::exit(nullptr, screen);
if (lv_obj_get_child_count(screen) || lv_group_get_default() != old_group) return 9;
geocaching::ui::shell::enter(&host, screen);
root = lv_obj_get_child(screen, 0);
tabs = lv_obj_get_child(root, 1);
lv_obj_send_event(lv_obj_get_child(tabs, 2), LV_EVENT_CLICKED, nullptr);
footer = lv_obj_get_child(root, 4);
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
list = lv_obj_get_child(root, 3);
auto* name_field = lv_obj_get_child(list, 1);
for (unsigned field = 0; field < 7; ++field)
{
if (field == 1 || field == 2) continue;
auto* input = lv_obj_get_child(list, 2 * field + 1);
const auto layers = lv_obj_get_child_count(lv_layer_top());
lv_obj_send_event(input, LV_EVENT_CLICKED, nullptr);
if (profile.compact_touch_keyboard)
{
if (lv_obj_get_child_count(lv_layer_top()) != layers + 1) return 40;
auto* modal = lv_obj_get_child(lv_layer_top(), -1);
auto* draft = lv_obj_get_child(modal, 1);
if (!lv_obj_check_type(draft, &lv_textarea_class)) return 41;
auto* keyboard = findKeyboard(modal);
if (!keyboard) return 45;
if (field == 0)
{
lv_buttonmatrix_set_selected_button(keyboard, 0);
lv_obj_send_event(keyboard, LV_EVENT_VALUE_CHANGED, nullptr);
if (std::strcmp(lv_textarea_get_text(draft), "q")) return 46;
}
lv_textarea_set_text(draft, field == 0 ? "Touch input" : "1.5");
if (field == 0 && !save(std::string(argv[3]) + "-keyboard.ppm", modal)) return 42;
auto* actions = lv_obj_get_child(modal, 0);
lv_obj_send_event(lv_obj_get_child(actions, 1), LV_EVENT_CLICKED, nullptr);
if (std::strcmp(lv_textarea_get_text(input), field == 0 ? "Touch input" : "1.5") ||
lv_obj_get_child_count(lv_layer_top()) != layers) return 43;
}
else if (lv_obj_get_child_count(lv_layer_top()) != layers) return 44;
}
lv_textarea_set_text(name_field, "林间宝藏");
auto* location_button = lv_obj_get_child(list, 3);
if (!lv_obj_check_type(location_button, &lv_button_class) ||
!lv_obj_check_type(lv_obj_get_child(list, 5), &lv_label_class)) return 53;
lv_obj_send_event(location_button, LV_EVENT_CLICKED, nullptr);
lv_timer_handler();
if (!gps::ui::runtime::test_location || !lv_obj_has_flag(root, LV_OBJ_FLAG_HIDDEN) ||
gps::ui::runtime::test_location->has_initial_viewport) return 54;
gps::ui::runtime::test_location->result = {30.5, 120.5, ui::map::MapLocationSelectionState::Picked};
gps::ui::runtime::test_map_host->request_exit(nullptr);
lv_timer_handler();
if (lv_obj_has_flag(root, LV_OBJ_FLAG_HIDDEN) || std::strcmp(lv_textarea_get_text(name_field), "林间宝藏") ||
!std::strstr(lv_label_get_text(lv_obj_get_child(list, 5)), "30.5000000")) return 55;
// Cancelling a second selection retains the previous position and live text.
lv_obj_send_event(location_button, LV_EVENT_CLICKED, nullptr);
lv_timer_handler();
if (!gps::ui::runtime::test_location || !gps::ui::runtime::test_location->has_initial_viewport ||
gps::ui::runtime::test_location->initial_viewport.center_lon != 120.5) return 56;
gps::ui::runtime::test_location->result = {0, 0, ui::map::MapLocationSelectionState::Cancelled};
gps::ui::runtime::test_map_host->request_exit(nullptr);
lv_timer_handler();
if (!std::strstr(lv_label_get_text(lv_obj_get_child(list, 5)), "120.5000000")) return 57;
gps::ui::runtime::test_map_available = false;
lv_obj_send_event(location_button, LV_EVENT_CLICKED, nullptr);
lv_timer_handler();
if (lv_obj_has_flag(root, LV_OBJ_FLAG_HIDDEN) || std::strcmp(lv_textarea_get_text(name_field), "林间宝藏")) return 58;
gps::ui::runtime::test_map_available = true;
// Date-line coordinates use the protocol's canonical -180 representation.
lv_obj_send_event(location_button, LV_EVENT_CLICKED, nullptr);
lv_timer_handler();
gps::ui::runtime::test_location->result = {0, 180, ui::map::MapLocationSelectionState::Picked};
gps::ui::runtime::test_map_host->request_exit(nullptr);
lv_timer_handler();
if (!std::strstr(lv_label_get_text(lv_obj_get_child(list, 5)), "-180.0000000")) return 59;
lv_obj_send_event(location_button, LV_EVENT_CLICKED, nullptr);
lv_timer_handler();
gps::ui::runtime::test_location->result = {91, 0, ui::map::MapLocationSelectionState::Picked};
gps::ui::runtime::test_map_host->request_exit(nullptr);
lv_timer_handler();
if (!std::strstr(lv_label_get_text(lv_obj_get_child(list, 5)), "-180.0000000")) return 60;
lv_obj_send_event(location_button, LV_EVENT_CLICKED, nullptr);
lv_timer_handler();
gps::ui::runtime::test_location->result = {30.5, 120.5, ui::map::MapLocationSelectionState::Picked};
gps::ui::runtime::test_map_host->request_exit(nullptr);
lv_timer_handler();
const std::string description = std::string(63, 'a') + "宝藏";
lv_textarea_set_text(lv_obj_get_child(list, 7), description.c_str());
if (!save(std::string(argv[3]) + "-editor.ppm", screen)) return 27;
source.fail_save = true;
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_has_state(name_field, LV_STATE_DISABLED) || std::strcmp(lv_textarea_get_text(name_field), "林间宝藏")) return 22;
source.fail_save = false;
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
lv_tick_inc(600);
lv_timer_handler();
if (!source.has_draft || source.draft.latitude_e7 != 305000000 || source.draft.longitude_e7 != 1205000000) return 23;
source.pending_reads = 1;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
if (!lv_obj_has_state(lv_obj_get_child(list, 1), LV_STATE_DISABLED) ||
!lv_obj_has_state(lv_obj_get_child(footer, 3), LV_STATE_DISABLED)) return 33;
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_has_state(lv_obj_get_child(list, 1), LV_STATE_DISABLED)) return 34;
if (std::strcmp(lv_textarea_get_text(lv_obj_get_child(list, 1)), "林间宝藏") ||
description != lv_textarea_get_text(lv_obj_get_child(list, 7))) return 24;
lv_textarea_set_text(lv_obj_get_child(list, 1), "Discard this");
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
if (lv_obj_has_flag(lv_obj_get_child(footer, 2), LV_OBJ_FLAG_HIDDEN)) return 25;
lv_obj_send_event(lv_obj_get_child(footer, 2), LV_EVENT_CLICKED, nullptr);
if (source.draft_name != "林间宝藏") return 26;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
lv_tick_inc(600);
lv_timer_handler();
lv_obj_send_event(lv_obj_get_child(footer, 2), LV_EVENT_CLICKED, nullptr);
if (!std::strstr(lv_label_get_text(lv_obj_get_child(list, 0)), "publicly") || source.publications) return 28;
if (!save(std::string(argv[3]) + "-publish.ppm", screen)) return 29;
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
if (source.publications || lv_obj_has_state(lv_obj_get_child(list, 1), LV_STATE_DISABLED)) return 30;
lv_obj_send_event(lv_obj_get_child(footer, 2), LV_EVENT_CLICKED, nullptr);
++source.current_author;
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
if (source.publications || !std::strstr(lv_label_get_text(lv_obj_get_child(root, 2)), "not started")) return 31;
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
lv_obj_send_event(lv_obj_get_child(footer, 2), LV_EVENT_CLICKED, nullptr);
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
if (source.publications != 1 || lv_obj_get_child_count(list) != 1) return 32;
// Archiving retains the local copy and requires a separate signed publication.
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
lv_tick_inc(600);
lv_timer_handler();
auto* archive = lv_obj_get_child(list, lv_obj_get_child_count(list) - 4);
if (std::strcmp(lv_label_get_text(lv_obj_get_child(archive, 0)), "Archive cache")) return 74;
lv_obj_send_event(archive, LV_EVENT_CLICKED, nullptr);
for (unsigned poll = 0; poll < 3; ++poll)
{
lv_tick_inc(600);
lv_timer_handler();
}
if (!source.has_draft || source.draft.state != 2 || source.publications != 1 ||
!std::strstr(lv_label_get_text(lv_obj_get_child(list, 0)), "Archive this public cache?")) return 75;
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
if (source.publications != 2 || !source.has_draft || lv_obj_get_child_count(list) != 1) return 76;
// Restore the active fixture for the existing edit/delete checks.
source.draft.state = 0;
++source.generation;
lv_tick_inc(600);
lv_timer_handler();
source.pending_reads = 20;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
lv_obj_update_layout(root);
auto* loading_back = lv_obj_get_child(lv_obj_get_child(root, 0), 0);
lv_group_focus_obj(loading_back);
lv_obj_scroll_to_y(list, 24, LV_ANIM_OFF);
const auto loading_scroll = lv_obj_get_scroll_y(list);
for (unsigned poll = 0; poll < 4; ++poll)
{
lv_tick_inc(600);
lv_timer_handler();
lv_obj_update_layout(root);
if (lv_group_get_focused(lv_group_get_default()) != loading_back) return 69;
if (lv_obj_get_scroll_y(list) != loading_scroll)
{
std::fprintf(stderr, "Pending draft read moved scroll: %d -> %d\n", int(loading_scroll), int(lv_obj_get_scroll_y(list)));
return 68;
}
}
source.pending_reads = 1;
lv_obj_send_event(lv_obj_get_child(footer, 0), LV_EVENT_CLICKED, nullptr);
lv_tick_inc(600);
lv_timer_handler();
if (lv_obj_get_child_count(list) != 1 || source.pending_reads != 1 || source.cancelled_reads != 1) return 35;
source.pending_reads = 0;
source.fail_read = true;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
if (lv_obj_get_child_count(list) != 1 || !std::strstr(lv_label_get_text(lv_obj_get_child(root, 2)), "Cannot read")) return 36;
source.fail_read = false;
// Deleting requires confirmation; cancellation and failed writes keep data.
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
auto* remove = lv_obj_get_child(list, lv_obj_get_child_count(list) - 2);
auto* cancel_remove = lv_obj_get_child(list, lv_obj_get_child_count(list) - 1);
lv_obj_send_event(remove, LV_EVENT_CLICKED, nullptr);
if (source.deletions || !source.has_draft || lv_obj_has_flag(cancel_remove, LV_OBJ_FLAG_HIDDEN)) return 39;
lv_obj_send_event(cancel_remove, LV_EVENT_CLICKED, nullptr);
if (source.deletions || !lv_obj_has_flag(cancel_remove, LV_OBJ_FLAG_HIDDEN)) return 40;
source.fail_save = true;
lv_obj_send_event(remove, LV_EVENT_CLICKED, nullptr);
lv_obj_send_event(remove, LV_EVENT_CLICKED, nullptr);
if (source.deletions != 1 || !source.has_draft || !std::strstr(lv_label_get_text(lv_obj_get_child(root, 2)), "Delete failed")) return 41;
source.fail_save = false;
lv_tick_inc(600);
lv_timer_handler();
lv_obj_send_event(remove, LV_EVENT_CLICKED, nullptr);
if (source.deletions != 2 || source.has_draft) return 42;
// Restore the fixture for the remaining lifetime tests.
source.has_draft = true;
++source.generation;
lv_tick_inc(600);
lv_timer_handler();
source.pending_reads = 2;
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
geocaching::ui::shell::bind(nullptr);
if (source.cancelled_reads != 2 || lv_obj_get_child_count(list) != 1) return 37;
geocaching::ui::shell::bind(&source);
source.pending_reads = 2;
lv_tick_inc(600);
lv_timer_handler();
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
geocaching::ui::shell::exit(nullptr, screen);
if (source.cancelled_reads != 3) return 38;
// Warm LVGL style caches before checking repeated page lifetime cleanup.
geocaching::ui::shell::enter(&host, screen);
geocaching::ui::shell::exit(nullptr, screen);
lv_mem_monitor_t baseline{}, after{};
lv_mem_monitor(&baseline);
for (unsigned i = 0; i < 20; ++i)
{
geocaching::ui::shell::enter(&host, screen);
geocaching::ui::shell::exit(nullptr, screen);
}
lv_mem_monitor(&after);
if (after.free_size != baseline.free_size || lv_obj_get_child_count(screen)) return 10;
if (platform::ui::psramUiRequired()) return 71;
geocaching::ui::shell::beginMapOverlays();
if (!platform::ui::psramUiRequired()) return 72;
geocaching::ui::shell::endMapOverlays();
if (platform::ui::psramUiRequired()) return 73;
geocaching::ui::shell::bind(nullptr);
lv_group_delete(old_group);
std::printf("%dx%d: visible=%u, no LVGL growth after 20 enter/exit cycles\n", width, height, unsigned(visible));
return 0;
}