mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-09-25 13:13:37 +00:00
339 lines
16 KiB
C++
339 lines
16 KiB
C++
#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/widgets/top_bar_power_presenter.h"
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace gps::ui::runtime
|
|
{
|
|
const shell::Host* test_map_host = nullptr;
|
|
MapTarget test_map_target;
|
|
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; }
|
|
} // 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;
|
|
unsigned pending_reads = 0;
|
|
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 saved_index = SIZE_MAX;
|
|
size_t last_open = 0;
|
|
uint64_t generation = 1;
|
|
void snapshot(ui::geocaching::Section section, ui::geocaching::Snapshot& out) override
|
|
{
|
|
out = {};
|
|
out.generation = generation;
|
|
out.count = 20;
|
|
out.can_refresh = true;
|
|
if (section == ui::geocaching::Section::Published)
|
|
{
|
|
out.count = has_draft ? 1 : 0;
|
|
out.can_create = true;
|
|
}
|
|
std::snprintf(out.status.data(), out.status.size(), "20 shared caches");
|
|
}
|
|
bool item(ui::geocaching::Section section, size_t index, uint64_t expected, ui::geocaching::Item& out) override
|
|
{
|
|
++reads;
|
|
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.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.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();
|
|
}
|
|
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();
|
|
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); });
|
|
const auto profile = width == 480 ? ui::page_profile::make_pager_profile() : ui::page_profile::make_tdeck_profile();
|
|
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 (!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 < 2 || visible > 4 || source.reads != visible) return 4;
|
|
if (!save(std::string(argv[3]) + "-list.ppm", screen)) return 5;
|
|
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 (!save(std::string(argv[3]) + "-detail.ppm", screen)) return 11;
|
|
auto* details_footer = lv_obj_get_child(root, 4);
|
|
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(list, 0)), "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);
|
|
lv_obj_send_event(lv_obj_get_child(footer, 3), LV_EVENT_CLICKED, nullptr);
|
|
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);
|
|
lv_textarea_set_text(name_field, "林间宝藏");
|
|
lv_textarea_set_text(lv_obj_get_child(list, 3), "30.5");
|
|
lv_textarea_set_text(lv_obj_get_child(list, 5), "120.5");
|
|
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;
|
|
source.pending_reads = 2;
|
|
lv_obj_send_event(lv_obj_get_child(list, 0), LV_EVENT_CLICKED, nullptr);
|
|
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;
|
|
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;
|
|
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;
|
|
}
|