Skip to content

Commit e093a45

Browse files
aveaoclaude
andcommitted
health: split typical pill into bin-time + daily-total on color
The activity-summary "typical" pill now exposes both the bin-aware "by now" step count and the full-day weekday historical average, laid out side by side on color displays (DISP_COLS > 144). Header line stays "TYPICAL <day>"; below it a two-column row shows the bin time on the left and the static TOTAL label on the right, each underlined, with the corresponding value beneath. BW boards stay two-line: TYPICAL <day> over the daily total alone, since they don't have the room for the split. Renderer-side changes: - Add daily_total and bin_minute arguments to health_ui_render_typical_text_box. Sleep card passes -1/-1 to keep its single-value behavior; activity card passes the bin-aware step count as value_text plus the new numbers. Empty state still collapses to the em-dash via -1/-1. - On color with both numbers present, the pill grows to 53px tall to fit the three rows. On BW (or color sleep), the pill keeps its original 35/36px height. Data-layer change: re-add health_data_steps_get_current_average_minute returning the minute-of-day of the latest completed step-average bin. The activity card caches it on each layer update and passes it through to the renderer. The lazy-refresh helper that backs both health_data_steps_get_current_average and the new accessor is factored out as prv_refresh_current_step_average. Test fixture: pin RTC to Saturday 16:19:35 UTC and 24h clock format so the rendered "16:15" bin time is deterministic in goldens. Each test case seeds step_average_last_updated_time = 975 to match. Regenerate activity-summary goldens on obelix/gabbro. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Ave Özkal <git@ave.zone>
1 parent d2944fc commit e093a45

35 files changed

Lines changed: 148 additions & 12 deletions

File tree

src/fw/apps/system/health/activity_summary_card.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ typedef struct HealthActivitySummaryCardData {
3636
KinoReel *icon;
3737
int32_t current_steps;
3838
int32_t typical_steps;
39+
int32_t typical_steps_bin_minute;
3940
int32_t daily_average_steps;
4041
} HealthActivitySummaryCardData;
4142

@@ -126,14 +127,19 @@ static void prv_render_typical_steps(GContext *ctx, Layer *base_layer) {
126127
snprintf(steps_buffer, sizeof(steps_buffer), EM_DASH);
127128
}
128129

129-
health_ui_render_typical_text_box(ctx, base_layer, steps_buffer);
130+
// Only enable the split layout when the bin-aware count is known.
131+
const int32_t daily_total = (data->typical_steps > 0) ? data->daily_average_steps : -1;
132+
const int32_t bin_minute = (data->typical_steps > 0) ? data->typical_steps_bin_minute : -1;
133+
health_ui_render_typical_text_box(ctx, base_layer, steps_buffer, daily_total, bin_minute);
130134
}
131135

132136
static void prv_base_layer_update_proc(Layer *base_layer, GContext *ctx) {
133137
HealthActivitySummaryCardData *data = layer_get_data(base_layer);
134138

135139
data->current_steps = health_data_current_steps_get(data->health_data);
136140
data->typical_steps = health_data_steps_get_current_average(data->health_data);
141+
data->typical_steps_bin_minute =
142+
health_data_steps_get_current_average_minute(data->health_data);
137143
data->daily_average_steps = health_data_steps_get_cur_wday_average(data->health_data);
138144

139145
prv_render_icon(ctx, base_layer);

src/fw/apps/system/health/data.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ static int32_t prv_health_data_get_n_average_chunks(HealthData *health_data, int
213213
return total_steps_avg;
214214
}
215215

216-
int32_t health_data_steps_get_current_average(HealthData *health_data) {
216+
static void prv_refresh_current_step_average(HealthData *health_data) {
217217
// get the current minutes into today
218218
time_t utc_sec = rtc_get_time();
219219
struct tm local_tm;
@@ -230,9 +230,18 @@ int32_t health_data_steps_get_current_average(HealthData *health_data) {
230230
health_data->step_average_last_updated_time =
231231
(today_min / k_minutes_per_step_avg) * k_minutes_per_step_avg;
232232
}
233+
}
234+
235+
int32_t health_data_steps_get_current_average(HealthData *health_data) {
236+
prv_refresh_current_step_average(health_data);
233237
return health_data->current_step_average;
234238
}
235239

240+
int32_t health_data_steps_get_current_average_minute(HealthData *health_data) {
241+
prv_refresh_current_step_average(health_data);
242+
return health_data->step_average_last_updated_time;
243+
}
244+
236245
int32_t health_data_steps_get_cur_wday_average(HealthData *health_data) {
237246
return prv_health_data_get_n_average_chunks(health_data, ACTIVITY_NUM_METRIC_AVERAGES);
238247
}

src/fw/apps/system/health/data.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ int32_t health_data_current_calories_get(HealthData *health_data);
9090
//! @return The integer number of steps that should be taken by this time today
9191
int32_t health_data_steps_get_current_average(HealthData *health_data);
9292

93+
//! Get the minute-of-day timestamp of the latest completed step-average bin.
94+
//! Returns a value in [0, MINUTES_PER_DAY) representing the start minute of the
95+
//! current incomplete bin (i.e., the end of the latest completed bin).
96+
//! @param health_data A pointer to the health data to use
97+
//! @return The minute-of-day of the latest completed bin boundary
98+
int32_t health_data_steps_get_current_average_minute(HealthData *health_data);
99+
93100
//! Get the step average for the current day of the week
94101
//! @param health_data A pointer to the health data to use
95102
//! @return An integer value for the number of steps that are typically taken on this week day

src/fw/apps/system/health/sleep_summary_card.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ static void prv_render_typical_sleep_text(GContext *ctx, Layer *base_layer) {
201201
snprintf(sleep_text, sizeof(sleep_text), EM_DASH);
202202
}
203203

204-
health_ui_render_typical_text_box(ctx, base_layer, sleep_text);
204+
health_ui_render_typical_text_box(ctx, base_layer, sleep_text, -1, -1);
205205
}
206206

207207
static void prv_render_no_sleep_data_text(GContext *ctx, Layer *base_layer) {

src/fw/apps/system/health/ui.c

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33

44
#include "ui.h"
55

6+
#include <inttypes.h>
7+
68
#include "applib/pbl_std/pbl_std.h"
79
#include "board/display.h"
10+
#include "pbl/services/clock.h"
811
#include "pbl/services/i18n/i18n.h"
912
#include "util/string.h"
13+
#include "util/time/time.h"
1014

1115
// Compile-time display offset calculations
1216
#define HEALTH_X_OFFSET ((DISP_COLS - LEGACY_2X_DISP_COLS) / 2)
@@ -47,41 +51,116 @@ void health_ui_draw_text_in_box(GContext *ctx, const char *text, const GRect dra
4751
}
4852
}
4953

50-
void health_ui_render_typical_text_box(GContext *ctx, Layer *layer, const char *value_text) {
54+
void health_ui_render_typical_text_box(GContext *ctx, Layer *layer, const char *value_text,
55+
int32_t daily_total, int32_t bin_minute) {
5156
time_t now = rtc_get_time();
5257
struct tm time_tm;
5358
localtime_r(&now, &time_tm);
5459
char weekday[8];
5560
strftime(weekday, sizeof(weekday), "%a", &time_tm);
5661
toupper_str(weekday);
5762

58-
char typical_text[32];
63+
char typical_text[16];
5964
snprintf(typical_text, sizeof(typical_text), i18n_get("TYPICAL %s", layer), weekday);
6065

66+
// Color displays with both bin and daily-total data get the 3-row split
67+
// layout (time + value on the left, TOTAL + daily on the right). Everything
68+
// else (BW, sleep, em-dash fallback) renders a single value below the
69+
// TYPICAL <day> header.
70+
const bool is_split = PBL_IF_COLOR_ELSE(daily_total > 0 && bin_minute >= 0, false);
71+
const int pill_height = is_split ? 53 : PBL_IF_RECT_ELSE(35, 36);
72+
6173
const int y = PBL_IF_RECT_ELSE(PBL_IF_BW_ELSE(122, 120), 125) + HEALTH_Y_OFFSET;
62-
GRect rect = GRect(0, y, layer->bounds.size.w, PBL_IF_RECT_ELSE(35, 36));
74+
GRect rect = GRect(0, y, layer->bounds.size.w, pill_height);
6375
#if PBL_RECT
6476
rect = grect_inset(rect, GEdgeInsets(0, HEALTH_INSET));
6577
#endif
6678

6779
const GColor bg_color = PBL_IF_COLOR_ELSE(GColorYellow, GColorBlack);
6880
const GColor text_color = PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite);
6981
const GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);
82+
const GFont small_font = fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD);
7083

7184
graphics_context_set_fill_color(ctx, bg_color);
7285
graphics_fill_round_rect(ctx, &rect, 3, GCornersAll);
7386

87+
// Compensate for the text renderer's top padding so the header glyphs sit
88+
// just inside the pill's visual top edge.
7489
rect.origin.y -= PBL_IF_RECT_ELSE(3, 2);
75-
// Restrict the rect to draw one line at a time to prevent them from wrapping into each other
76-
rect.size.h = 16;
7790

7891
graphics_context_set_text_color(ctx, text_color);
7992

80-
graphics_draw_text(ctx, typical_text, font, rect,
93+
// Row 1: TYPICAL <day>, full width.
94+
GRect header_rect = rect;
95+
header_rect.size.h = 16;
96+
graphics_draw_text(ctx, typical_text, font, header_rect,
8197
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
8298

83-
rect.origin.y += 16;
99+
if (!is_split) {
100+
GRect value_rect = header_rect;
101+
value_rect.origin.y += 16;
102+
103+
const char *bottom_text = value_text;
104+
char bottom_buf[12];
105+
if (daily_total > 0) {
106+
snprintf(bottom_buf, sizeof(bottom_buf), "%"PRId32, daily_total);
107+
bottom_text = bottom_buf;
108+
}
109+
graphics_draw_text(ctx, bottom_text, font, value_rect,
110+
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
111+
return;
112+
}
84113

85-
graphics_draw_text(ctx, value_text, font, rect,
114+
// 3-row split layout (color activity only).
115+
char time_text[12];
116+
clock_format_time(time_text, sizeof(time_text),
117+
bin_minute / MINUTES_PER_HOUR,
118+
bin_minute % MINUTES_PER_HOUR,
119+
false /* add_space */);
120+
121+
char total_value[12];
122+
snprintf(total_value, sizeof(total_value), "%"PRId32, daily_total);
123+
124+
const char *total_label = i18n_get("TOTAL", layer);
125+
126+
// Row 2: subheaders (TOTAL | time). Reserve room below the header so the
127+
// subheaders aren't crowded against the TYPICAL line.
128+
GRect sub_left = GRect(rect.origin.x, rect.origin.y + 19,
129+
rect.size.w / 2, 14);
130+
GRect sub_right = sub_left;
131+
sub_right.origin.x += sub_left.size.w;
132+
graphics_draw_text(ctx, time_text, small_font, sub_left,
133+
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
134+
graphics_draw_text(ctx, total_label, small_font, sub_right,
135+
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
136+
137+
// Underline each subheader. Each underline tracks the rendered text width.
138+
const int16_t underline_y = sub_left.origin.y + 15;
139+
graphics_context_set_stroke_color(ctx, text_color);
140+
graphics_context_set_stroke_width(ctx, 1);
141+
142+
const GSize time_size = app_graphics_text_layout_get_content_size(
143+
time_text, small_font, sub_left, GTextOverflowModeWordWrap, GTextAlignmentCenter);
144+
const int16_t time_under_x =
145+
sub_left.origin.x + (sub_left.size.w - time_size.w) / 2;
146+
graphics_draw_line(ctx, GPoint(time_under_x, underline_y),
147+
GPoint(time_under_x + time_size.w, underline_y));
148+
149+
const GSize total_size = app_graphics_text_layout_get_content_size(
150+
total_label, small_font, sub_right, GTextOverflowModeWordWrap, GTextAlignmentCenter);
151+
const int16_t total_under_x =
152+
sub_right.origin.x + (sub_right.size.w - total_size.w) / 2;
153+
graphics_draw_line(ctx, GPoint(total_under_x, underline_y),
154+
GPoint(total_under_x + total_size.w, underline_y));
155+
156+
// Row 3: values (value_text | daily_total). Tightened against the subheaders.
157+
GRect val_left = sub_left;
158+
val_left.origin.y += 12;
159+
val_left.size.h = 16;
160+
GRect val_right = val_left;
161+
val_right.origin.x += val_left.size.w;
162+
graphics_draw_text(ctx, value_text, font, val_left,
163+
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
164+
graphics_draw_text(ctx, total_value, font, val_right,
86165
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
87166
}

src/fw/apps/system/health/ui.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,17 @@ void health_ui_draw_text_in_box(GContext *ctx, const char *text, const GRect dra
99
const int16_t y_offset, const GFont small_font, GColor box_color,
1010
GColor text_color);
1111

12-
void health_ui_render_typical_text_box(GContext *ctx, Layer *layer, const char *value_text);
12+
//! Render the "typical" pill used by the health summary cards.
13+
//!
14+
//! @param value_text Pre-formatted value text for the lower line. On color
15+
//! activity the bin-aware step count; on sleep the typical sleep duration.
16+
//! @param daily_total Full-day weekday-average step count, or <= 0 if not
17+
//! applicable. Triggers the split layout on color and replaces value_text
18+
//! on BW.
19+
//! @param bin_minute Minute-of-day of the latest completed step-average bin,
20+
//! or < 0 if not applicable. Together with a positive daily_total enables
21+
//! the split layout on color.
22+
void health_ui_render_typical_text_box(GContext *ctx, Layer *layer,
23+
const char *value_text,
24+
int32_t daily_total,
25+
int32_t bin_minute);

tests/fw/apps/system_apps/health/test_health_activity_summary_card.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,16 @@ GContext *graphics_context_get_current_context(void) {
1919
return &s_ctx;
2020
}
2121

22+
// Override fake_clock's WEAK default so the split layout's bin time renders
23+
// in a stable 24h format.
24+
bool clock_is_24h_style(void) {
25+
return true;
26+
}
27+
2228
void test_health_activity_summary_card__initialize(void) {
29+
// Pin RTC to 2024-01-06 16:19:35 UTC (Saturday 16:19 -> bin minute 975 = 16:15).
30+
rtc_set_time(1704557975);
31+
2332
// Setup graphics context
2433
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
2534
framebuffer_clear(&s_fb);
@@ -79,6 +88,7 @@ void test_health_activity_summary_card__no_current_steps(void) {
7988
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
8089
10, 10, 10, 10, 10, 50}, // 1000
8190
.current_step_average = 750,
91+
.step_average_last_updated_time = 975,
8292
};
8393

8494
prv_create_card_and_render(&health_data);
@@ -99,6 +109,7 @@ void test_health_activity_summary_card__render_current_behind_typical1(void) {
99109
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
100110
10, 10, 10, 10, 10, 50}, // 1000
101111
.current_step_average = 340,
112+
.step_average_last_updated_time = 975,
102113
};
103114

104115
prv_create_card_and_render(&health_data);
@@ -119,6 +130,7 @@ void test_health_activity_summary_card__render_current_behind_typical2(void) {
119130
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
120131
10, 10, 10, 10, 10, 50}, // 1000
121132
.current_step_average = 340,
133+
.step_average_last_updated_time = 975,
122134
};
123135

124136
prv_create_card_and_render(&health_data);
@@ -139,6 +151,7 @@ void test_health_activity_summary_card__render_current_behind_typical3(void) {
139151
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
140152
10, 10, 10, 10, 10, 50}, // 1000
141153
.current_step_average = 555,
154+
.step_average_last_updated_time = 975,
142155
};
143156

144157
prv_create_card_and_render(&health_data);
@@ -159,6 +172,7 @@ void test_health_activity_summary_card__render_current_behind_typical4(void) {
159172
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
160173
10, 10, 10, 10, 10, 50}, // 1000
161174
.current_step_average = 840,
175+
.step_average_last_updated_time = 975,
162176
};
163177

164178
prv_create_card_and_render(&health_data);
@@ -179,6 +193,7 @@ void test_health_activity_summary_card__render_current_behind_typical5(void) {
179193
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
180194
10, 10, 10, 10, 10, 50}, // 1000
181195
.current_step_average = 914,
196+
.step_average_last_updated_time = 975,
182197
};
183198

184199
prv_create_card_and_render(&health_data);
@@ -199,6 +214,7 @@ void test_health_activity_summary_card__render_current_equals_typical(void) {
199214
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
200215
10, 10, 10, 10, 10, 50}, // 1000
201216
.current_step_average = 837,
217+
.step_average_last_updated_time = 975,
202218
};
203219

204220
prv_create_card_and_render(&health_data);
@@ -219,6 +235,7 @@ void test_health_activity_summary_card__render_current_above_typical1(void) {
219235
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
220236
10, 10, 10, 10, 10, 50}, // 1000
221237
.current_step_average = 170,
238+
.step_average_last_updated_time = 975,
222239
};
223240

224241
prv_create_card_and_render(&health_data);
@@ -239,6 +256,7 @@ void test_health_activity_summary_card__render_current_above_typical2(void) {
239256
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
240257
10, 10, 10, 10, 10, 50}, // 1000
241258
.current_step_average = 379,
259+
.step_average_last_updated_time = 975,
242260
};
243261

244262
prv_create_card_and_render(&health_data);
@@ -259,6 +277,7 @@ void test_health_activity_summary_card__render_current_above_typical3(void) {
259277
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
260278
10, 10, 10, 10, 10, 50}, // 1000
261279
.current_step_average = 480,
280+
.step_average_last_updated_time = 975,
262281
};
263282

264283
prv_create_card_and_render(&health_data);
@@ -279,6 +298,7 @@ void test_health_activity_summary_card__render_current_above_typical4(void) {
279298
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
280299
10, 10, 10, 10, 10, 50}, // 1000
281300
.current_step_average = 700,
301+
.step_average_last_updated_time = 975,
282302
};
283303

284304
prv_create_card_and_render(&health_data);
@@ -299,6 +319,7 @@ void test_health_activity_summary_card__render_current_above_typical5(void) {
299319
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
300320
10, 10, 10, 10, 10, 50}, // 1000
301321
.current_step_average = 900,
322+
.step_average_last_updated_time = 975,
302323
};
303324

304325
prv_create_card_and_render(&health_data);
@@ -319,6 +340,7 @@ void test_health_activity_summary_card__render_current_above_expected(void) {
319340
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
320341
10, 10, 10, 10, 10, 50}, // 1000
321342
.current_step_average = 800,
343+
.step_average_last_updated_time = 975,
322344
};
323345

324346
prv_create_card_and_render(&health_data);
261 Bytes
Loading
252 Bytes
Loading
251 Bytes
Loading

0 commit comments

Comments
 (0)