fix: redesign X3's UTC offset picker so it is easier to use (#2205)

## Summary

* **What is the goal of this PR?**
Improve the X3 clock UTC offset picker so it is clearer which part of
the offset will change when the user presses up/down.

* **What changes are included?**
Replaces the single underlined `UTC +/- H:MM` string with separate
editable fields for the sign, hour, and minutes.
The editable fields are spaced out, outlined, and the currently selected
field is highlighted with a light gray background.

## Additional Context

* This is a render-only UI change in `ClockOffsetActivity`; it does not
change how UTC offsets are stored, clamped, saved, or applied.
* The selected field still cycles with Confirm, and up/down still
adjusts the active sign/hour/minute value.

Screenshot of **current design** showing the sign (+/-) highlighted:
<img width="264" height="396" alt="old utc offset"
src="https://github.com/user-attachments/assets/c88d69ea-089c-47da-b5b3-ed0ebda5b112"
/>


Screenshot of **new design** showing the sign (+/-) highlighted:
<img width="264" height="396" alt="utc offset fix"
src="https://github.com/user-attachments/assets/f51da8b3-9aab-4f48-a9d1-21c828c01499"
/>

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES >**_
This commit is contained in:
Julia
2026-05-29 19:37:38 -04:00
committed by GitHub
parent 174a24ba42
commit 66d9e4403a
+42 -59
View File
@@ -4,6 +4,7 @@
#include <HalClock.h>
#include <I18n.h>
#include <algorithm>
#include <cstdio>
#include "CrossPointSettings.h"
@@ -146,72 +147,54 @@ void ClockOffsetActivity::render(RenderLock&&) {
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLOCK_UTC_OFFSET));
// Build the offset string. Use a generous font and centre it.
char offsetBuf[16];
snprintf(offsetBuf, sizeof(offsetBuf), "UTC %c %d:%02d", sign == 1 ? '-' : '+', hours,
minutesQuarter * MINUTES_PER_QUARTER);
const int centreY = pageHeight / 2 - 40;
renderer.drawCenteredText(UI_12_FONT_ID, centreY, offsetBuf, true, EpdFontFamily::BOLD);
auto widthOf = [&](const char* s) { return renderer.getTextWidth(UI_12_FONT_ID, s, EpdFontFamily::BOLD); };
constexpr int fieldPaddingX = 6;
constexpr int labelGap = 16;
constexpr int fieldGap = 12;
constexpr int colonGap = 5;
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
const int fieldHeight = lineHeight + 2;
// Underline / caret under the active field. Compute positions by measuring substrings of the
// formatted string so the caret follows the font glyph widths exactly.
// Field substrings:
// "UTC " -> prefix
// "{+/-}" -> sign
// " "
// "{hours}" -> hours
// ":"
// "{mm}" -> minutes
auto widthOf = [&](const char* s) { return renderer.getTextWidth(UI_12_FONT_ID, s); };
const int totalWidth = widthOf(offsetBuf);
const int leftEdge = (pageWidth - totalWidth) / 2;
// Locate each field by reformatting prefixes.
char prefixSign[16];
snprintf(prefixSign, sizeof(prefixSign), "UTC ");
const int signX = leftEdge + widthOf(prefixSign);
char prefixHours[16];
snprintf(prefixHours, sizeof(prefixHours), "UTC %c ", sign == 1 ? '-' : '+');
const int hoursX = leftEdge + widthOf(prefixHours);
char prefixMinutes[16];
snprintf(prefixMinutes, sizeof(prefixMinutes), "UTC %c %d:", sign == 1 ? '-' : '+', hours);
const int minutesX = leftEdge + widthOf(prefixMinutes);
// Width of each field substring for the caret span.
const int signW = widthOf(sign == 1 ? "-" : "+");
char signStr[2] = {sign == 1 ? '-' : '+', '\0'};
char hoursStr[8];
snprintf(hoursStr, sizeof(hoursStr), "%d", hours);
const int hoursW = widthOf(hoursStr);
char minutesStr[8];
snprintf(minutesStr, sizeof(minutesStr), "%02d", minutesQuarter * MINUTES_PER_QUARTER);
const int minutesW = widthOf(minutesStr);
int caretX = 0;
int caretW = 0;
switch (activeField) {
case FIELD_SIGN:
caretX = signX;
caretW = signW;
break;
case FIELD_HOURS:
caretX = hoursX;
caretW = hoursW;
break;
case FIELD_MINUTES:
caretX = minutesX;
caretW = minutesW;
break;
default:
break;
}
// Caret drawn as a short bar below the active field.
const int caretY = centreY + 10;
for (int dy = 0; dy < 2; dy++) {
renderer.drawLine(caretX, caretY + dy, caretX + caretW, caretY + dy);
}
const int labelWidth = widthOf("UTC");
const int signBoxW = std::max(widthOf("+"), widthOf("-")) + fieldPaddingX * 2;
const int hoursBoxW = std::max(widthOf("14"), widthOf("12")) + fieldPaddingX * 2;
const int colonWidth = widthOf(":");
const int minutesBoxW = std::max({widthOf("00"), widthOf("15"), widthOf("30"), widthOf("45")}) + fieldPaddingX * 2;
const int totalWidth =
labelWidth + labelGap + signBoxW + fieldGap + hoursBoxW + colonGap + colonWidth + colonGap + minutesBoxW;
int x = (pageWidth - totalWidth) / 2;
renderer.drawText(UI_12_FONT_ID, x, centreY, "UTC", true, EpdFontFamily::BOLD);
x += labelWidth + labelGap;
auto drawField = [&](const char* text, const int boxX, const int boxWidth, const Field field) {
const bool selected = activeField == field;
renderer.fillRectDither(boxX, centreY, boxWidth, fieldHeight, selected ? Color::LightGray : Color::White);
renderer.drawRect(boxX, centreY, boxWidth, fieldHeight, true);
if (selected) {
renderer.drawRect(boxX + 1, centreY + 1, boxWidth - 2, fieldHeight - 2, true);
}
const int textX = boxX + (boxWidth - widthOf(text)) / 2;
renderer.drawText(UI_12_FONT_ID, textX, centreY, text, true, EpdFontFamily::BOLD);
};
drawField(signStr, x, signBoxW, FIELD_SIGN);
x += signBoxW + fieldGap;
drawField(hoursStr, x, hoursBoxW, FIELD_HOURS);
x += hoursBoxW + colonGap;
renderer.drawText(UI_12_FONT_ID, x, centreY, ":", true, EpdFontFamily::BOLD);
x += colonWidth + colonGap;
drawField(minutesStr, x, minutesBoxW, FIELD_MINUTES);
// Live preview of the resulting wall-clock time, so users can verify against a watch.
if (halClock.isAvailable()) {