forked from coredevices/PebbleOS
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsleep_summary_card.c
More file actions
307 lines (249 loc) · 12.4 KB
/
Copy pathsleep_summary_card.c
File metadata and controls
307 lines (249 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */
#include "sleep_summary_card.h"
#include "sleep_summary_card_segments.h"
#include "sleep_detail_card.h"
#include "progress.h"
#include "ui.h"
#include "pbl/services/activity/health_util.h"
#include "applib/pbl_std/pbl_std.h"
#include "applib/ui/kino/kino_layer.h"
#include "applib/ui/text_layer.h"
#include "board/display.h"
#include "kernel/pbl_malloc.h"
#include "resource/resource_ids.auto.h"
#include "pbl/services/i18n/i18n.h"
#include "system/logging.h"
#include "util/size.h"
#include "util/string.h"
// Compile-time display offset calculations
#define HEALTH_X_OFFSET ((DISP_COLS - LEGACY_2X_DISP_COLS) / 2)
#define HEALTH_Y_OFFSET ((DISP_ROWS - LEGACY_2X_DISP_ROWS) / 2)
typedef struct HealthSleepSummaryCardData {
HealthData *health_data;
HealthProgressBar progress_bar;
KinoReel *icon;
GFont number_font;
GFont unit_font;
GFont typical_font;
GFont em_dash_font;
} HealthSleepSummaryCardData;
#define PROGRESS_CURRENT_COLOR (PBL_IF_COLOR_ELSE(GColorVividCerulean, GColorDarkGray))
#define PROGRESS_SECONDARY_COLOR (PBL_IF_COLOR_ELSE(GColorVeryLightBlue, GColorBlack))
#define PROGRESS_TYPICAL_COLOR (PBL_IF_COLOR_ELSE(GColorYellow, GColorBlack))
#define PROGRESS_BACKGROUND_COLOR (PBL_IF_COLOR_ELSE(GColorDarkGray, GColorClear))
#define PROGRESS_OUTLINE_COLOR (PBL_IF_COLOR_ELSE(GColorClear, GColorBlack))
#define CURRENT_TEXT_COLOR (PBL_IF_COLOR_ELSE(GColorVividCerulean, GColorBlack))
#define TYPICAL_TEXT_COLOR (PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite))
#define NO_DATA_TEXT_COLOR (PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack))
#define CARD_BACKGROUND_COLOR (PBL_IF_COLOR_ELSE(GColorOxfordBlue, GColorWhite))
#define TWELVE_HOURS (SECONDS_PER_HOUR * 12)
static void prv_render_sleep_sessions(GContext *ctx, HealthSleepSummaryCardData *data) {
const int num_sessions = health_data_sleep_get_num_sessions(data->health_data);
ActivitySession *sessions = health_data_sleep_get_sessions(data->health_data);
for (int i = 0; i < num_sessions; i++) {
ActivitySession *session = &sessions[i];
GColor fill_color = GColorClear;
if (session->type == ActivitySessionType_Sleep) {
fill_color = PROGRESS_CURRENT_COLOR;
} else if (session->type == ActivitySessionType_RestfulSleep) {
fill_color = PROGRESS_SECONDARY_COLOR;
}
if (gcolor_equal(fill_color, GColorClear)) {
continue;
}
struct tm local_tm;
localtime_r(&session->start_utc, &local_tm);
const int session_start_24h = (local_tm.tm_sec +
local_tm.tm_min * SECONDS_PER_MINUTE +
local_tm.tm_hour * SECONDS_PER_HOUR);
const int session_end_24h = session_start_24h + (session->length_min * SECONDS_PER_MINUTE);
const int session_start_12h = session_start_24h % TWELVE_HOURS;
const int session_end_12h = session_end_24h % TWELVE_HOURS;
const int start = (session_start_12h * HEALTH_PROGRESS_BAR_MAX_VALUE / TWELVE_HOURS);
const int end = (session_end_12h * HEALTH_PROGRESS_BAR_MAX_VALUE / TWELVE_HOURS);
health_progress_bar_fill(ctx, &data->progress_bar, fill_color, start, end);
}
}
static void prv_render_typical_markers(GContext *ctx, HealthSleepSummaryCardData *data) {
// Some time fuzz is applied to a couple values to ensure that typical fill touches the sleep
// sessions (needed because of how our fill algorithms work)
const int sleep_start_24h = health_data_sleep_get_start_time(data->health_data);
const int sleep_end_24h = health_data_sleep_get_end_time(data->health_data);
if (sleep_start_24h || sleep_end_24h) {
#if PBL_COLOR
const int time_fuzz = (2 * SECONDS_PER_MINUTE);
const int sleep_start_12h = (sleep_start_24h) % TWELVE_HOURS;
const int sleep_end_12h = (sleep_end_24h - time_fuzz) % TWELVE_HOURS;
const int sleep_start = (sleep_start_12h * HEALTH_PROGRESS_BAR_MAX_VALUE / TWELVE_HOURS);
const int sleep_end = (sleep_end_12h * HEALTH_PROGRESS_BAR_MAX_VALUE / TWELVE_HOURS);
#endif
const int typical_sleep_start_24h = health_data_sleep_get_typical_start_time(data->health_data);
const int typical_sleep_start_12h = typical_sleep_start_24h % TWELVE_HOURS;
const int typical_sleep_end_24h = health_data_sleep_get_typical_end_time(data->health_data);
const int typical_sleep_end_12h = typical_sleep_end_24h % TWELVE_HOURS;
const int typical_start =
(typical_sleep_start_12h * HEALTH_PROGRESS_BAR_MAX_VALUE / TWELVE_HOURS);
const int typical_end =
(typical_sleep_end_12h * HEALTH_PROGRESS_BAR_MAX_VALUE / TWELVE_HOURS);
#if PBL_COLOR
const bool fell_asleep_late = (typical_sleep_start_24h < sleep_start_24h);
if (fell_asleep_late) {
health_progress_bar_fill(ctx, &data->progress_bar, PROGRESS_TYPICAL_COLOR,
typical_start, sleep_start);
} else {
health_progress_bar_mark(ctx, &data->progress_bar, PROGRESS_TYPICAL_COLOR, typical_start);
}
const bool woke_up_early = (typical_sleep_end_24h > sleep_end_24h);
if (woke_up_early) {
health_progress_bar_fill(ctx, &data->progress_bar, PROGRESS_TYPICAL_COLOR,
sleep_end, typical_end);
} else {
health_progress_bar_mark(ctx, &data->progress_bar, PROGRESS_TYPICAL_COLOR, typical_end);
}
#else
health_progress_bar_mark(ctx, &data->progress_bar, PROGRESS_TYPICAL_COLOR, typical_start);
health_progress_bar_mark(ctx, &data->progress_bar, PROGRESS_TYPICAL_COLOR, typical_end);
#endif
}
}
static void prv_render_progress_bar(GContext *ctx, Layer *base_layer) {
HealthSleepSummaryCardData *data = layer_get_data(base_layer);
// Renders the background
health_progress_bar_fill(ctx, &data->progress_bar, PROGRESS_BACKGROUND_COLOR,
0, HEALTH_PROGRESS_BAR_MAX_VALUE);
prv_render_sleep_sessions(ctx, data);
prv_render_typical_markers(ctx, data);
// This is required to get the rounded corners on the outside of the rectangle
graphics_context_set_stroke_width(ctx, 2);
graphics_context_set_stroke_color(ctx, CARD_BACKGROUND_COLOR);
graphics_draw_round_rect(ctx, &s_sleep_summary_masking_rect, 5);
// This needs to be done after drawing the progress bars or else the progress fill
// overlaps the outline and things look weird
health_progress_bar_outline(ctx, &data->progress_bar, PROGRESS_OUTLINE_COLOR);
}
static void prv_render_icon(GContext *ctx, Layer *base_layer) {
HealthSleepSummaryCardData *data = layer_get_data(base_layer);
const int y = PBL_IF_RECT_ELSE(PBL_IF_BW_ELSE(37, 32), 39) + HEALTH_Y_OFFSET;
const int x_center_offset = 17;
kino_reel_draw(data->icon, ctx, GPoint(base_layer->bounds.size.w / 2 - x_center_offset, y));
}
static void prv_render_current_sleep_text(GContext *ctx, Layer *base_layer) {
HealthSleepSummaryCardData *data = layer_get_data(base_layer);
// 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);
if (current_sleep) {
// Draw the hours slept
GTextNodeHorizontal *horiz_container = graphics_text_node_create_horizontal(MAX_TEXT_NODES);
GTextNodeContainer *container = &horiz_container->container;
horiz_container->horizontal_alignment = GTextAlignmentCenter;
health_util_duration_to_hours_and_minutes_text_node(current_sleep, base_layer,
data->number_font,
data->unit_font,
CURRENT_TEXT_COLOR, container);
graphics_text_node_draw(&container->node, ctx, &rect, NULL, NULL);
graphics_text_node_destroy(&container->node);
} else {
char buffer[16];
const GFont font = data->em_dash_font;
snprintf(buffer, sizeof(buffer), EM_DASH);
graphics_context_set_text_color(ctx, CURRENT_TEXT_COLOR);
graphics_draw_text(ctx, buffer, font, rect, GTextOverflowModeFill, GTextAlignmentCenter, NULL);
}
}
static void prv_render_typical_sleep_text(GContext *ctx, Layer *base_layer) {
HealthSleepSummaryCardData *data = layer_get_data(base_layer);
const int typical_sleep = health_data_sleep_get_cur_wday_average(data->health_data);
char sleep_text[32];
if (typical_sleep) {
health_util_format_hours_and_minutes(sleep_text, sizeof(sleep_text), typical_sleep, base_layer);
} else {
snprintf(sleep_text, sizeof(sleep_text), EM_DASH);
}
health_ui_render_typical_text_box(ctx, base_layer, sleep_text);
}
static void prv_render_no_sleep_data_text(GContext *ctx, Layer *base_layer) {
HealthSleepSummaryCardData *data = layer_get_data(base_layer);
const int y = PBL_IF_RECT_ELSE(91, 100) + HEALTH_Y_OFFSET;
const GRect rect = GRect(0, y, base_layer->bounds.size.w, 60);
const char *text = i18n_get("No sleep data,\nwear your watch\nto sleep", base_layer);
graphics_context_set_text_color(ctx, NO_DATA_TEXT_COLOR);
graphics_draw_text(ctx, text, data->typical_font,
rect, GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
}
static bool prv_has_sleep_data(HealthData *health_data) {
// daily weekly stats doesn't include the first index so we check that separately
return health_data_current_sleep_get(health_data) ||
health_data_sleep_get_monthly_average(health_data) > 0;
}
static void prv_base_layer_update_proc(Layer *base_layer, GContext *ctx) {
HealthSleepSummaryCardData *data = layer_get_data(base_layer);
prv_render_icon(ctx, base_layer);
prv_render_progress_bar(ctx, base_layer);
if (!prv_has_sleep_data(data->health_data)) {
prv_render_no_sleep_data_text(ctx, base_layer);
return;
}
prv_render_current_sleep_text(ctx, base_layer);
prv_render_typical_sleep_text(ctx, base_layer);
}
static void prv_sleep_detail_card_unload_callback(Window *window) {
health_sleep_detail_card_destroy(window);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// API Functions
//
Layer *health_sleep_summary_card_create(HealthData *health_data) {
// create base layer
Layer *base_layer = layer_create_with_data(GRectZero, sizeof(HealthSleepSummaryCardData));
HealthSleepSummaryCardData *health_sleep_summary_card_data = layer_get_data(base_layer);
layer_set_update_proc(base_layer, prv_base_layer_update_proc);
// set health data
*health_sleep_summary_card_data = (HealthSleepSummaryCardData) {
.icon = kino_reel_create_with_resource(RESOURCE_ID_HEALTH_APP_SLEEP),
.progress_bar = {
.num_segments = ARRAY_LENGTH(s_sleep_summary_progress_segments),
.segments = s_sleep_summary_progress_segments,
},
.health_data = health_data,
#if DISP_ROWS > LEGACY_2X_DISP_ROWS
.number_font = fonts_get_system_font(FONT_KEY_LECO_32_BOLD_NUMBERS),
.unit_font = fonts_get_system_font(FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM),
#else
.number_font = fonts_get_system_font(FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM),
.unit_font = fonts_get_system_font(FONT_KEY_LECO_20_BOLD_NUMBERS),
#endif
.typical_font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD),
.em_dash_font = fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD),
};
return base_layer;
}
void health_sleep_summary_card_select_click_handler(Layer *layer) {
HealthSleepSummaryCardData *health_sleep_summary_card_data = layer_get_data(layer);
HealthData *health_data = health_sleep_summary_card_data->health_data;
if (prv_has_sleep_data(health_data)) {
Window *window = health_sleep_detail_card_create(health_data);
window_set_window_handlers(window, &(WindowHandlers) {
.unload = prv_sleep_detail_card_unload_callback,
});
app_window_stack_push(window, true);
}
}
void health_sleep_summary_card_destroy(Layer *base_layer) {
HealthSleepSummaryCardData *data = layer_get_data(base_layer);
i18n_free_all(base_layer);
kino_reel_destroy(data->icon);
layer_destroy(base_layer);
}
GColor health_sleep_summary_card_get_bg_color(Layer *layer) {
return CARD_BACKGROUND_COLOR;
}
bool health_sleep_summary_show_select_indicator(Layer *layer) {
HealthSleepSummaryCardData *health_sleep_summary_card_data = layer_get_data(layer);
return prv_has_sleep_data(health_sleep_summary_card_data->health_data);
}