Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions src/fw/apps/system/health/activity_summary_card.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
#include "board/display.h"
#include "kernel/pbl_malloc.h"
#include "resource/resource_ids.auto.h"
#include "pbl/services/clock.h"
#include "pbl/services/i18n/i18n.h"
#include "system/logging.h"
#include "util/size.h"
#include "util/string.h"
#include "util/time/time.h"

// Compile-time display offset calculations
#define HEALTH_X_OFFSET ((DISP_COLS - LEGACY_2X_DISP_COLS) / 2)
Expand All @@ -36,6 +38,7 @@ typedef struct HealthActivitySummaryCardData {
KinoReel *icon;
int32_t current_steps;
int32_t typical_steps;
int32_t typical_steps_bin_minute;
int32_t daily_average_steps;
} HealthActivitySummaryCardData;

Expand Down Expand Up @@ -109,7 +112,11 @@ static void prv_render_current_steps(GContext *ctx, Layer *base_layer) {
snprintf(buffer, sizeof(buffer), EM_DASH);
}

const int y = PBL_IF_RECT_ELSE(PBL_IF_BW_ELSE(85, 83), 88) + HEALTH_Y_OFFSET;
// Mirror the pill's downshift at half the offset so the step count and
// pill stay visually balanced. Zero on legacy-sized displays where
// HEALTH_Y_OFFSET itself is 0.
const int y = PBL_IF_RECT_ELSE(PBL_IF_BW_ELSE(85, 83), 88) + HEALTH_Y_OFFSET
+ HEALTH_Y_OFFSET / 6;
graphics_context_set_text_color(ctx, CURRENT_TEXT_COLOR);
graphics_draw_text(ctx, buffer, font,
GRect(0, y, base_layer->bounds.size.w, 40),
Expand All @@ -119,21 +126,45 @@ static void prv_render_current_steps(GContext *ctx, Layer *base_layer) {
static void prv_render_typical_steps(GContext *ctx, Layer *base_layer) {
HealthActivitySummaryCardData *data = layer_get_data(base_layer);

char steps_buffer[12];
if (data->typical_steps) {
snprintf(steps_buffer, sizeof(steps_buffer), "%"PRId32, data->typical_steps);
char daily_buffer[12];
if (data->daily_average_steps > 0) {
snprintf(daily_buffer, sizeof(daily_buffer), "%"PRId32, data->daily_average_steps);
} else {
snprintf(steps_buffer, sizeof(steps_buffer), EM_DASH);
snprintf(daily_buffer, sizeof(daily_buffer), EM_DASH);
}

#if DISP_COLS >= 200
// Wide displays break the typical data into a two-column split: the bin-aware
// count under its bin time on the left, the full-day total under a "TOTAL"
// label on the right. With no typical data to break down, fall through to the
// single daily-total line below.
if (data->typical_steps > 0) {
char steps_buffer[12];
snprintf(steps_buffer, sizeof(steps_buffer), "%"PRId32, data->typical_steps);

char bin_time[12];
clock_format_time(bin_time, sizeof(bin_time),
data->typical_steps_bin_minute / MINUTES_PER_HOUR,
data->typical_steps_bin_minute % MINUTES_PER_HOUR,
false /* add_space */);

health_ui_render_split_typical_text_box(ctx, base_layer, steps_buffer, bin_time,
daily_buffer, i18n_get("TOTAL", base_layer));
return;
}
#endif

health_ui_render_typical_text_box(ctx, base_layer, steps_buffer);
// Narrow displays, and the no-typical-data case on wide displays, show just
// the daily total (em-dash when missing).
health_ui_render_typical_text_box(ctx, base_layer, daily_buffer);
}

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

data->current_steps = health_data_current_steps_get(data->health_data);
data->typical_steps = health_data_steps_get_current_average(data->health_data);
data->typical_steps_bin_minute = health_data_steps_get_current_average_minute(data->health_data);
data->daily_average_steps = health_data_steps_get_cur_wday_average(data->health_data);

prv_render_icon(ctx, base_layer);
Expand Down
4 changes: 4 additions & 0 deletions src/fw/apps/system/health/data.c
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ int32_t health_data_steps_get_current_average(HealthData *health_data) {
return health_data->current_step_average;
}

int32_t health_data_steps_get_current_average_minute(HealthData *health_data) {
return health_data->step_average_last_updated_time;
}

int32_t health_data_steps_get_cur_wday_average(HealthData *health_data) {
return prv_health_data_get_n_average_chunks(health_data, ACTIVITY_NUM_METRIC_AVERAGES);
}
Expand Down
6 changes: 6 additions & 0 deletions src/fw/apps/system/health/data.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ int32_t health_data_current_calories_get(HealthData *health_data);
//! @return The integer number of steps that should be taken by this time today
int32_t health_data_steps_get_current_average(HealthData *health_data);

//! Get the minute-of-day of the latest completed step-average bin. Reflects the
//! value cached by the most recent health_data_steps_get_current_average() call.
//! @param health_data A pointer to the health data to use
//! @return The minute-of-day (0..1439) of the latest completed step-average bin
int32_t health_data_steps_get_current_average_minute(HealthData *health_data);

//! Get the step average for the current day of the week
//! @param health_data A pointer to the health data to use
//! @return An integer value for the number of steps that are typically taken on this week day
Expand Down
6 changes: 5 additions & 1 deletion src/fw/apps/system/health/sleep_summary_card.c
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,11 @@ static void prv_render_icon(GContext *ctx, Layer *base_layer) {
static void prv_render_current_sleep_text(GContext *ctx, Layer *base_layer) {
HealthSleepSummaryCardData *data = layer_get_data(base_layer);

const int y = PBL_IF_RECT_ELSE(PBL_IF_BW_ELSE(85, 83), 88) + HEALTH_Y_OFFSET;
// Mirror the pill's downshift at half the offset so the step count and
// pill stay visually balanced. Zero on legacy-sized displays where
// HEALTH_Y_OFFSET itself is 0.
const int y = PBL_IF_RECT_ELSE(PBL_IF_BW_ELSE(85, 83), 88) + HEALTH_Y_OFFSET
+ HEALTH_Y_OFFSET / 6;
const GRect rect = GRect(0, y, base_layer->bounds.size.w, 40);

const int current_sleep = health_data_current_sleep_get(data->health_data);
Expand Down
87 changes: 79 additions & 8 deletions src/fw/apps/system/health/ui.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@

#include "ui.h"

#include <inttypes.h>

#include "applib/pbl_std/pbl_std.h"
#include "board/display.h"
#include "pbl/services/clock.h"
#include "pbl/services/i18n/i18n.h"
#include "util/string.h"
#include "util/time/time.h"

// Compile-time display offset calculations
#define HEALTH_X_OFFSET ((DISP_COLS - LEGACY_2X_DISP_COLS) / 2)
Expand Down Expand Up @@ -47,7 +51,11 @@ void health_ui_draw_text_in_box(GContext *ctx, const char *text, const GRect dra
}
}

void health_ui_render_typical_text_box(GContext *ctx, Layer *layer, const char *value_text) {
// Draws the rounded pill background and the "TYPICAL <day>" header, common to
// both the single-value and split layouts. Returns the pill rect with origin.y
// already nudged up to compensate for the text renderer's top padding, so
// callers can position their body rows relative to it.
static GRect prv_render_typical_pill(GContext *ctx, Layer *layer, int pill_height) {
time_t now = rtc_get_time();
struct tm time_tm;
localtime_r(&now, &time_tm);
Expand All @@ -58,8 +66,14 @@ void health_ui_render_typical_text_box(GContext *ctx, Layer *layer, const char *
char typical_text[32];
snprintf(typical_text, sizeof(typical_text), i18n_get("TYPICAL %s", layer), weekday);

const int y = PBL_IF_RECT_ELSE(PBL_IF_BW_ELSE(122, 120), 125) + HEALTH_Y_OFFSET;
GRect rect = GRect(0, y, layer->bounds.size.w, PBL_IF_RECT_ELSE(35, 36));
// Push the pill lower by a third of the display's vertical health-offset.
// Scales to the slack available — zero on legacy-sized displays (asterix,
// flint), a few px on taller displays (obelix, getafix). Applies to both
// activity and sleep cards on those taller displays; the extra breathing
// room reads better on both.
const int y = PBL_IF_RECT_ELSE(PBL_IF_BW_ELSE(122, 120), 125) + HEALTH_Y_OFFSET
+ HEALTH_Y_OFFSET / 3;
GRect rect = GRect(0, y, layer->bounds.size.w, pill_height);
#if PBL_RECT
rect = grect_inset(rect, GEdgeInsets(0, HEALTH_INSET));
#endif
Expand All @@ -71,17 +85,74 @@ void health_ui_render_typical_text_box(GContext *ctx, Layer *layer, const char *
graphics_context_set_fill_color(ctx, bg_color);
graphics_fill_round_rect(ctx, &rect, 3, GCornersAll);

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

graphics_context_set_text_color(ctx, text_color);

graphics_draw_text(ctx, typical_text, font, rect,
// Row 1: TYPICAL <day>, full width.
GRect header_rect = rect;
header_rect.size.h = 16;
graphics_draw_text(ctx, typical_text, font, header_rect,
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);

rect.origin.y += 16;
return rect;
}

void health_ui_render_typical_text_box(GContext *ctx, Layer *layer, const char *value_text) {
GRect rect = prv_render_typical_pill(ctx, layer, PBL_IF_RECT_ELSE(35, 36));

graphics_draw_text(ctx, value_text, font, rect,
const GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);

// Row 2: the single value line, full width below the header.
GRect value_rect = rect;
value_rect.size.h = 16;
value_rect.origin.y += 16;
graphics_draw_text(ctx, value_text, font, value_rect,
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
}

void health_ui_render_split_typical_text_box(GContext *ctx, Layer *layer,
const char *left_value, const char *left_label,
const char *right_value, const char *right_label) {
GRect rect = prv_render_typical_pill(ctx, layer, 53);

const GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);
const GFont small_font = fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD);

// On round we narrow the column band so the two halves sit closer to the
// pill's center instead of marooned at the round display's edges.
const int16_t col_band_inset = PBL_IF_RECT_ELSE(0, 40);
const int16_t col_w = (rect.size.w - 2 * col_band_inset) / 2;

// Row 2: values, numbers on top.
GRect val_left = GRect(rect.origin.x + col_band_inset, rect.origin.y + 18,
col_w, 16);
GRect val_right = val_left;
val_right.origin.x += val_left.size.w;
graphics_draw_text(ctx, left_value, font, val_left,
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
graphics_draw_text(ctx, right_value, font, val_right,
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);

// Row 3: labels below the numbers, in black.
const GColor label_color = GColorArmyGreen;
GRect sub_left = val_left;
sub_left.origin.y += 18;
sub_left.size.h = 14;
GRect sub_right = sub_left;
sub_right.origin.x += sub_left.size.w;
graphics_context_set_text_color(ctx, label_color);
graphics_draw_text(ctx, left_label, small_font, sub_left,
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
graphics_draw_text(ctx, right_label, small_font, sub_right,
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);

// Vertical divider between the two columns, 1px wide black.
graphics_context_set_stroke_color(ctx, label_color);
graphics_context_set_stroke_width(ctx, 1);
const int16_t divider_x = rect.origin.x + rect.size.w / 2;
graphics_draw_line(ctx, GPoint(divider_x, val_left.origin.y + 6),
GPoint(divider_x, sub_left.origin.y + sub_left.size.h));
}
21 changes: 20 additions & 1 deletion src/fw/apps/system/health/ui.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,23 @@ void health_ui_draw_text_in_box(GContext *ctx, const char *text, const GRect dra
const int16_t y_offset, const GFont small_font, GColor box_color,
GColor text_color);

void health_ui_render_typical_text_box(GContext *ctx, Layer *layer, const char *value_text);
//! Render the "TYPICAL <day>" pill with a single value line below the header.
//! Used on narrow displays and on the sleep card.
//!
//! @param value_text Pre-formatted value text for the lower line (e.g. the
//! daily-total step count, or the typical sleep duration).
void health_ui_render_typical_text_box(GContext *ctx, Layer *layer,
const char *value_text);

//! Render the "TYPICAL <day>" pill as a two-column split: each column shows a
//! value on top and a label beneath, separated by a vertical divider. Used on
//! wide (>= 200px) displays for the activity card. The caller supplies all four
//! strings so the renderer stays generic (not steps-specific).
//!
//! @param left_value Pre-formatted value for the left column (top).
//! @param left_label Label for the left column (bottom).
//! @param right_value Pre-formatted value for the right column (top).
//! @param right_label Label for the right column (bottom).
void health_ui_render_split_typical_text_box(GContext *ctx, Layer *layer,
const char *left_value, const char *left_label,
const char *right_value, const char *right_label);
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ GContext *graphics_context_get_current_context(void) {
return &s_ctx;
}

// Override fake_clock's WEAK default so the split layout's bin time renders
// in a stable 24h format.
bool clock_is_24h_style(void) {
return true;
}

void test_health_activity_summary_card__initialize(void) {
// Pin RTC to 2024-01-06 16:19:35 UTC (Saturday 16:19 -> bin minute 975 = 16:15).
rtc_set_time(1704557975);

// Setup graphics context
framebuffer_init(&s_fb, &(GSize) {DISP_COLS, DISP_ROWS});
framebuffer_clear(&s_fb);
Expand Down Expand Up @@ -79,6 +88,7 @@ void test_health_activity_summary_card__no_current_steps(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 750,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -99,6 +109,7 @@ void test_health_activity_summary_card__render_current_behind_typical1(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 340,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -119,6 +130,7 @@ void test_health_activity_summary_card__render_current_behind_typical2(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 340,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -139,6 +151,7 @@ void test_health_activity_summary_card__render_current_behind_typical3(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 555,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -159,6 +172,7 @@ void test_health_activity_summary_card__render_current_behind_typical4(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 840,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -179,6 +193,7 @@ void test_health_activity_summary_card__render_current_behind_typical5(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 914,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -199,6 +214,7 @@ void test_health_activity_summary_card__render_current_equals_typical(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 837,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -219,6 +235,7 @@ void test_health_activity_summary_card__render_current_above_typical1(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 170,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -239,6 +256,7 @@ void test_health_activity_summary_card__render_current_above_typical2(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 379,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -259,6 +277,7 @@ void test_health_activity_summary_card__render_current_above_typical3(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 480,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -279,6 +298,7 @@ void test_health_activity_summary_card__render_current_above_typical4(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 700,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -299,6 +319,7 @@ void test_health_activity_summary_card__render_current_above_typical5(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 900,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand All @@ -319,6 +340,7 @@ void test_health_activity_summary_card__render_current_above_expected(void) {
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 10, 50}, // 1000
.current_step_average = 800,
.step_average_last_updated_time = 975,
};

prv_create_card_and_render(&health_data);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading