From b0aff8ecf9355f8081fabba2743e351ab338c029 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 25 May 2026 13:08:27 +0200 Subject: [PATCH] refactor(ui): trail Min-dist on a Config screen, not a popup The Hold-Enter popup was inconsistent with how every other Tool screen (Auto-Advert, Ringtone Editor, etc.) presents its settings. Reshape the entry point: - New V_CONFIG view is the resting screen. Shows "Min dist: ", the stored points count and distance (so the user can see what pressing Enter will resume), and a hint "<>dist [Ent] start/resume". LEFT/RIGHT cycles min-dist; Enter saves any pending change and starts/resumes tracking. Tracks open straight into V_SUMMARY. - The three live views (Summary, Map, List) are only reachable while tracking. Enter on any of them stops and drops back to V_CONFIG; LEFT/RIGHT cycles between the three. - Esc/Hold from any view writes the pending min-dist change (if any) and returns to Tools. Popup state (_opts_open / _opts_dirty / renderOptions) is gone, along with all writes-on-every-cycle. Min-dist now batches in _cfg_dirty and commits once on Start or Back. Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/ui-new/TrailScreen.h | 192 ++++++++++-------- 1 file changed, 112 insertions(+), 80 deletions(-) diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 16a4830b..8252dcf5 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -1,8 +1,7 @@ #pragma once // GPS trail viewer. Tools › Trail. -// Summary view: counters, distance, time, speed. -// Map view: pixel-by-pixel auto-fit of the polyline with start (+) and current (×) markers. -// Phase 3 adds a per-point list view. +// Config view (stopped): shows the Min-dist setting and lets Enter start tracking. +// Active views (running): Summary counters, Map polyline, per-point List. // Included by UITask.cpp after Trail store + ToolsScreen. #include "../Trail.h" @@ -12,116 +11,112 @@ class TrailScreen : public UIScreen { UITask* _task; TrailStore* _store; - enum View { V_SUMMARY = 0, V_MAP = 1, V_LIST = 2, V_COUNT }; - uint8_t _view = V_SUMMARY; + // V_CONFIG is the resting screen — shown whenever the trail is stopped. + // Once tracking starts, the user cycles between V_SUMMARY/V_MAP/V_LIST with + // LEFT/RIGHT. Stop returns to V_CONFIG. + enum View { V_CONFIG = 0, V_SUMMARY = 1, V_MAP = 2, V_LIST = 3 }; + static const uint8_t ACTIVE_VIEW_COUNT = 3; // Summary, Map, List + + uint8_t _view = V_CONFIG; int _summary_scroll = 0; // top visible item index in Summary view int _list_scroll = 0; // top visible row in List view (newest = row 0) - bool _opts_open = false; // tool-local options popup (min-delta) + bool _cfg_dirty = false; // unsaved Min-dist edits; flushed on start or back static const int SUMMARY_ITEM_COUNT = 5; public: TrailScreen(UITask* task, TrailStore* store) : _task(task), _store(store) {} - void enter() { /* nothing to reset; live trail stays */ } + void enter() { + _view = _store->isActive() ? V_SUMMARY : V_CONFIG; + _summary_scroll = 0; + _list_scroll = 0; + _cfg_dirty = false; + } int render(DisplayDriver& display) override { display.setTextSize(1); display.setColor(DisplayDriver::LIGHT); - const char* title = (_view == V_MAP) ? "TRAIL MAP" - : (_view == V_LIST) ? "TRAIL LIST" - : "TRAIL"; + + // Snap _view to whatever the active-state allows. Stopped → always V_CONFIG; + // active and stuck on V_CONFIG → flip to Summary. + if (!_store->isActive() && _view != V_CONFIG) _view = V_CONFIG; + if ( _store->isActive() && _view == V_CONFIG) _view = V_SUMMARY; + + const char* title = (_view == V_MAP) ? "TRAIL MAP" + : (_view == V_LIST) ? "TRAIL LIST" + : "TRAIL"; display.drawTextCentered(display.width() / 2, 0, title); display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); - if (_view == V_MAP) renderMap(display); - else if (_view == V_LIST) renderList(display); - else renderSummary(display); + if (_view == V_CONFIG) renderConfig(display); + else if (_view == V_MAP) renderMap(display); + else if (_view == V_LIST) renderList(display); + else renderSummary(display); - // Bottom hint — current view indicator + control reminders. + // Bottom hint display.setColor(DisplayDriver::LIGHT); int hint_y = display.height() - display.lineStep(); - char hint[28]; - snprintf(hint, sizeof(hint), "<>%d/%d [Ent] %s", - (int)_view + 1, (int)V_COUNT, - _store->isActive() ? "stop" : "start"); display.setCursor(2, hint_y); - display.print(hint); - - // Options popup overlay (Hold Enter to open) — tool-local Min-delta setting. - if (_opts_open) renderOptions(display); + if (_view == V_CONFIG) { + display.print(_store->empty() ? "<>dist [Ent] start" : "<>dist [Ent] resume"); + } else { + char hint[28]; + snprintf(hint, sizeof(hint), "<>%d/%d [Ent] stop", + (int)(_view - V_SUMMARY + 1), (int)ACTIVE_VIEW_COUNT); + display.print(hint); + } return _store->isActive() ? 1000 : 5000; } bool handleInput(char c) override { - // Options popup consumes all input while open. - if (_opts_open) { - if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _opts_open = false; return true; } - if (c == KEY_ENTER) { - NodePrefs* p = _task->getNodePrefs(); - if (p) { - uint8_t idx = p->trail_min_delta_idx; - if (idx >= TrailStore::MIN_DELTA_COUNT) idx = 0; - idx = (idx + 1) % TrailStore::MIN_DELTA_COUNT; - p->trail_min_delta_idx = idx; - the_mesh.savePrefs(); - } - } + if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { + if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; } + _task->gotoToolsScreen(); return true; } - if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; } - if (c == KEY_CONTEXT_MENU) { _opts_open = true; return true; } - if (c == KEY_LEFT || c == KEY_PREV) { _view = (_view + V_COUNT - 1) % V_COUNT; return true; } - if (c == KEY_RIGHT || c == KEY_NEXT) { _view = (_view + 1) % V_COUNT; return true; } - if (_view == V_SUMMARY && c == KEY_UP && _summary_scroll > 0) { _summary_scroll--; return true; } - if (_view == V_SUMMARY && c == KEY_DOWN) { _summary_scroll++; return true; } // clamped on render - if (_view == V_LIST && c == KEY_UP && _list_scroll > 0) { _list_scroll--; return true; } - if (_view == V_LIST && c == KEY_DOWN) { _list_scroll++; return true; } // clamped on render - if (c == KEY_ENTER) { - bool now_active = !_store->isActive(); - _store->setActive(now_active); - if (now_active) { + if (_view == V_CONFIG) { + NodePrefs* p = _task->getNodePrefs(); + if ((c == KEY_LEFT || c == KEY_PREV) && p) { cycleMinDelta(p, -1); return true; } + if ((c == KEY_RIGHT || c == KEY_NEXT) && p) { cycleMinDelta(p, +1); return true; } + if (c == KEY_ENTER) { + if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; } + _store->setActive(true); + _view = V_SUMMARY; _task->showAlert(gpsHasFix() ? "Tracking started" : "Waiting for GPS fix", 1000); - } else { - _task->showAlert("Tracking stopped", 800); + return true; } + return false; + } + + // Active views: Summary / Map / List + if (c == KEY_LEFT || c == KEY_PREV) { + int v = (int)_view - V_SUMMARY; + v = (v + ACTIVE_VIEW_COUNT - 1) % ACTIVE_VIEW_COUNT; + _view = (uint8_t)(V_SUMMARY + v); + return true; + } + if (c == KEY_RIGHT || c == KEY_NEXT) { + int v = (int)_view - V_SUMMARY; + v = (v + 1) % ACTIVE_VIEW_COUNT; + _view = (uint8_t)(V_SUMMARY + v); + return true; + } + if (_view == V_SUMMARY && c == KEY_UP && _summary_scroll > 0) { _summary_scroll--; return true; } + if (_view == V_SUMMARY && c == KEY_DOWN) { _summary_scroll++; return true; } + if (_view == V_LIST && c == KEY_UP && _list_scroll > 0) { _list_scroll--; return true; } + if (_view == V_LIST && c == KEY_DOWN) { _list_scroll++; return true; } + if (c == KEY_ENTER) { + _store->setActive(false); + _view = V_CONFIG; + _task->showAlert("Tracking stopped", 800); return true; } return false; } - // Small modal overlay with the only tool-local option: minimum distance - // between consecutive samples. Pressing Enter cycles through the 4 options - // (5/10/25/100 m) so the user can keep tapping; Esc closes the popup. - void renderOptions(DisplayDriver& display) { - NodePrefs* p = _task->getNodePrefs(); - int step = display.lineStep(); - int bw = display.width() - 16; - int bh = step * 3 + 4; - int bx = (display.width() - bw) / 2; - int by = (display.height() - bh) / 2; - - display.setColor(DisplayDriver::DARK); - display.fillRect(bx, by, bw, bh); - display.setColor(DisplayDriver::LIGHT); - display.drawRect(bx, by, bw, bh); - - display.setCursor(bx + 4, by + 2); - display.print("Trail options"); - display.fillRect(bx, by + step + 1, bw, 1); - - char buf[24]; - snprintf(buf, sizeof(buf), "Min dist: %s", - TrailStore::minDeltaLabel(p ? p->trail_min_delta_idx : 0)); - display.setCursor(bx + 4, by + step + 3); - display.print(buf); - - display.setCursor(bx + 4, by + step * 2 + 3); - display.print("[Ent] cycle [Esc]"); - } - // True when the global SensorManager has a usable GPS fix. static bool gpsHasFix() { #if ENV_INCLUDE_GPS == 1 @@ -133,6 +128,44 @@ public: } private: + void cycleMinDelta(NodePrefs* p, int dir) { + uint8_t idx = p->trail_min_delta_idx; + if (idx >= TrailStore::MIN_DELTA_COUNT) idx = 0; + if (dir > 0) idx = (idx + 1) % TrailStore::MIN_DELTA_COUNT; + else idx = (idx + TrailStore::MIN_DELTA_COUNT - 1) % TrailStore::MIN_DELTA_COUNT; + p->trail_min_delta_idx = idx; + _cfg_dirty = true; + } + + void renderConfig(DisplayDriver& display) { + NodePrefs* p = _task->getNodePrefs(); + const int y0 = display.listStart(); + const int step = display.lineStep(); + + char buf[24]; + snprintf(buf, sizeof(buf), "Min dist: %s", + TrailStore::minDeltaLabel(p ? p->trail_min_delta_idx : 0)); + display.setCursor(2, y0); + display.print(buf); + + // Existing trail summary (if any) so the user sees what Enter will resume. + if (!_store->empty()) { + snprintf(buf, sizeof(buf), "Stored: %d pts", _store->count()); + display.setCursor(2, y0 + step * 2); + display.print(buf); + + uint32_t d = _store->totalDistanceMeters(); + if (d < 1000) snprintf(buf, sizeof(buf), "Dist: %lu m", (unsigned long)d); + else snprintf(buf, sizeof(buf), "Dist: %lu.%02lu km", + (unsigned long)(d / 1000), (unsigned long)((d % 1000) / 10)); + display.setCursor(2, y0 + step * 3); + display.print(buf); + } else { + display.setCursor(2, y0 + step * 2); + display.print("No trail yet."); + } + } + // Format the i-th summary line into buf. Indices match SUMMARY_ITEM_COUNT. void summaryItem(int i, char* buf, size_t n) const { switch (i) { @@ -194,7 +227,6 @@ private: display.print(buf); } - // Scroll arrows on the right edge when there's more above/below. int cw = display.getCharWidth(); if (_summary_scroll > 0) { display.setCursor(display.width() - cw, y0);