Refine Usage screen typography and match Claude Code spinner easing

- Drop "Claude" prefix from titles, center all three
- Replace verbose "Current Session" / "Current Week" labels with rounded
  pills above the bar; reset countdown moves below the bar
- Extend bar to fill panel inner width (was 8px short)
- Symmetric vertical pill placement within panel
- Spinner: per-frame hold times + ping-pong indexing, modeled on
  Claude Code's Cavalry triangle oscillator (turn-around frames hold
  ~2x longer)
- Bottom spinner re-centered between weekly panel and screen edge
- screenshot.sh: 480x320 -> 480x480 for the new panel
- CLAUDE.md: document screenshot.sh + temporary boot-screen swap as
  the standard self-QA workflow for UI changes
This commit is contained in:
Hermann Björgvin Haraldsson
2026-05-10 00:38:04 +00:00
parent 38d8894016
commit 8b5878724f
3 changed files with 53 additions and 25 deletions
+44 -23
View File
@@ -71,9 +71,9 @@ static screen_t current_screen = SCREEN_USAGE;
// Animation state
static uint32_t anim_last_ms = 0;
static uint8_t anim_spinner_idx = 0;
static uint8_t anim_phase = 0;
static uint8_t anim_msg_idx = 0;
static uint32_t anim_msg_start = 0;
#define ANIM_SPIN_MS 120
#define ANIM_MSG_MS 4000
static const char* const spinner_frames[] = {
@@ -81,6 +81,14 @@ static const char* const spinner_frames[] = {
"\xE2\x9C\xB6", "\xE2\x9C\xB3", "\xE2\x9C\xA2",
};
#define SPINNER_COUNT 6
#define SPINNER_PHASES (2 * (SPINNER_COUNT - 1)) // 10: ping-pong 0..5..0
// Per-frame hold time. Modeled on Claude Code's spinner (Cavalry triangle
// oscillator, range 0..5, period 5s) — turn-around frames (0 and 5) appear
// once per cycle, middle frames twice, so 0/5 read as held longer.
static const uint16_t spinner_ms[SPINNER_COUNT] = {
260, 130, 130, 130, 130, 260,
};
static const char* const anim_messages[] = {
"Accomplishing", "Elucidating", "Perusing",
@@ -237,6 +245,21 @@ static lv_obj_t* make_hint(lv_obj_t* parent, int x, int y, const char* text) {
return lbl;
}
static lv_obj_t* make_pill(lv_obj_t* parent, const char* text) {
lv_obj_t* lbl = lv_label_create(parent);
lv_label_set_text(lbl, text);
lv_obj_set_style_text_font(lbl, &font_styrene_28, 0);
lv_obj_set_style_text_color(lbl, COL_TEXT, 0);
lv_obj_set_style_bg_color(lbl, COL_BAR_BG, 0);
lv_obj_set_style_bg_opa(lbl, LV_OPA_COVER, 0);
lv_obj_set_style_radius(lbl, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_pad_left(lbl, 18, 0);
lv_obj_set_style_pad_right(lbl, 18, 0);
lv_obj_set_style_pad_top(lbl, 6, 0);
lv_obj_set_style_pad_bottom(lbl, 6, 0);
return lbl;
}
// ---- Battery icon initialization ----
static void init_battery_icons(void) {
init_icon_dsc_rgb565a8(&battery_dscs[0], ICON_BATTERY_W, ICON_BATTERY_H, icon_battery_data);
@@ -262,10 +285,10 @@ static void init_usage_screen(lv_obj_t* scr) {
// Title
lbl_title = lv_label_create(usage_container);
lv_label_set_text(lbl_title, "Claude Usage");
lv_label_set_text(lbl_title, "Usage");
lv_obj_set_style_text_font(lbl_title, &font_tiempos_56, 0);
lv_obj_set_style_text_color(lbl_title, COL_TEXT, 0);
lv_obj_set_pos(lbl_title, 106, TITLE_Y);
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, TITLE_Y);
// Session panel
lv_obj_t* p_session = make_panel(usage_container, MARGIN, CONTENT_Y, CONTENT_W, PANEL_H);
@@ -276,19 +299,18 @@ static void init_usage_screen(lv_obj_t* scr) {
lv_obj_set_style_text_color(lbl_session_pct, COL_TEXT, 0);
lv_obj_set_pos(lbl_session_pct, 0, 0);
bar_session = make_bar(p_session, 0, 56, CONTENT_W - 40, 24);
lbl_session_label = make_pill(p_session, "Current");
// Symmetric: panel-outer-top → pill-top equals pill-bottom → bar-top.
// Pill height = font line_height(30) + pad_top(6) + pad_bottom(6) = 42; panel pad_top = 12; bar at y=56 → y_inner=1.
lv_obj_align(lbl_session_label, LV_ALIGN_TOP_RIGHT, 0, 1);
lbl_session_label = lv_label_create(p_session);
lv_label_set_text(lbl_session_label, "Current Session");
lv_obj_set_style_text_font(lbl_session_label, &font_styrene_28, 0);
lv_obj_set_style_text_color(lbl_session_label, COL_DIM, 0);
lv_obj_set_pos(lbl_session_label, 0, 94);
bar_session = make_bar(p_session, 0, 56, CONTENT_W - 32, 24);
lbl_session_reset = lv_label_create(p_session);
lv_label_set_text(lbl_session_reset, "---");
lv_obj_set_style_text_font(lbl_session_reset, &font_styrene_28, 0);
lv_obj_set_style_text_color(lbl_session_reset, COL_DIM, 0);
lv_obj_align(lbl_session_reset, LV_ALIGN_TOP_RIGHT, 0, 94);
lv_obj_set_pos(lbl_session_reset, 0, 94);
// Weekly panel
int weekly_y = CONTENT_Y + PANEL_H + PANEL_GAP;
@@ -300,26 +322,23 @@ static void init_usage_screen(lv_obj_t* scr) {
lv_obj_set_style_text_color(lbl_weekly_pct, COL_TEXT, 0);
lv_obj_set_pos(lbl_weekly_pct, 0, 0);
bar_weekly = make_bar(p_weekly, 0, 56, CONTENT_W - 40, 24);
lbl_weekly_label = make_pill(p_weekly, "Weekly");
lv_obj_align(lbl_weekly_label, LV_ALIGN_TOP_RIGHT, 0, 1);
lbl_weekly_label = lv_label_create(p_weekly);
lv_label_set_text(lbl_weekly_label, "Current Week");
lv_obj_set_style_text_font(lbl_weekly_label, &font_styrene_28, 0);
lv_obj_set_style_text_color(lbl_weekly_label, COL_DIM, 0);
lv_obj_set_pos(lbl_weekly_label, 0, 94);
bar_weekly = make_bar(p_weekly, 0, 56, CONTENT_W - 32, 24);
lbl_weekly_reset = lv_label_create(p_weekly);
lv_label_set_text(lbl_weekly_reset, "---");
lv_obj_set_style_text_font(lbl_weekly_reset, &font_styrene_28, 0);
lv_obj_set_style_text_color(lbl_weekly_reset, COL_DIM, 0);
lv_obj_align(lbl_weekly_reset, LV_ALIGN_TOP_RIGHT, 0, 94);
lv_obj_set_pos(lbl_weekly_reset, 0, 94);
// Animation
lbl_anim = lv_label_create(usage_container);
lv_label_set_text(lbl_anim, "");
lv_obj_set_style_text_font(lbl_anim, &font_mono_32, 0);
lv_obj_set_style_text_color(lbl_anim, COL_ACCENT, 0);
lv_obj_align(lbl_anim, LV_ALIGN_BOTTOM_MID, 0, -24);
lv_obj_align(lbl_anim, LV_ALIGN_BOTTOM_MID, 0, -15);
}
// ======== Controller Screen (480x480) ========
@@ -340,10 +359,10 @@ static void init_controller_screen(lv_obj_t* scr) {
// Title
lv_obj_t* lbl_ctrl_title = lv_label_create(ctrl_container);
lv_label_set_text(lbl_ctrl_title, "Claude Controller");
lv_label_set_text(lbl_ctrl_title, "Controller");
lv_obj_set_style_text_font(lbl_ctrl_title, &font_tiempos_56, 0);
lv_obj_set_style_text_color(lbl_ctrl_title, COL_TEXT, 0);
lv_obj_set_pos(lbl_ctrl_title, 66, TITLE_Y);
lv_obj_align(lbl_ctrl_title, LV_ALIGN_TOP_MID, 0, TITLE_Y);
// Initialize icon descriptors
init_icon_dsc(&icon_escape_dsc, ICON_ESCAPE_W, ICON_ESCAPE_H, icon_escape_data);
@@ -445,7 +464,7 @@ static void init_bluetooth_screen(lv_obj_t* scr) {
lv_label_set_text(lbl_ble_title, "Bluetooth");
lv_obj_set_style_text_font(lbl_ble_title, &font_tiempos_56, 0);
lv_obj_set_style_text_color(lbl_ble_title, COL_TEXT, 0);
lv_obj_set_pos(lbl_ble_title, 66, TITLE_Y);
lv_obj_align(lbl_ble_title, LV_ALIGN_TOP_MID, 0, TITLE_Y);
// Info panel (taller for 480x480)
lv_obj_t* p_info = make_panel(ble_container, MARGIN, CONTENT_Y, CONTENT_W, 160);
@@ -587,9 +606,11 @@ void ui_tick_anim(void) {
anim_msg_start = now;
}
if (now - anim_last_ms >= ANIM_SPIN_MS) {
if (now - anim_last_ms >= spinner_ms[anim_spinner_idx]) {
anim_last_ms = now;
anim_spinner_idx = (anim_spinner_idx + 1) % SPINNER_COUNT;
anim_phase = (anim_phase + 1) % SPINNER_PHASES;
anim_spinner_idx = (anim_phase < SPINNER_COUNT) ? anim_phase
: (SPINNER_PHASES - anim_phase);
static char buf[80];
snprintf(buf, sizeof(buf), "%s %s\xE2\x80\xA6",