mirror of
https://github.com/google/pebble.git
synced 2026-02-22 04:56:50 -05:00
Import of the watch repository from Pebble
This commit is contained in:
661
src/fw/apps/watch/kickstart/kickstart.c
Normal file
661
src/fw/apps/watch/kickstart/kickstart.c
Normal file
@@ -0,0 +1,661 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "kickstart.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/graphics/text.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/ui.h"
|
||||
#include "apps/system_apps/timeline/text_node.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "applib/pbl_std/pbl_std.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/clock.h"
|
||||
#include "services/normal/activity/health_util.h"
|
||||
#include "util/size.h"
|
||||
#include "util/string.h"
|
||||
#include "util/time/time.h"
|
||||
#include "util/trig.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define ROBERT_SCREEN_RES (PBL_DISPLAY_WIDTH == 200 && PBL_DISPLAY_HEIGHT == 228)
|
||||
#define SNOWY_SCREEN_RES (PBL_DISPLAY_WIDTH == 144 && PBL_DISPLAY_HEIGHT == 168)
|
||||
#define SPALDING_SCREEN_RES (PBL_DISPLAY_WIDTH == 180 && PBL_DISPLAY_HEIGHT == 180)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// UI Utils
|
||||
|
||||
#if UNITTEST
|
||||
static int16_t s_unobstructed_area_height = 0;
|
||||
|
||||
T_STATIC void prv_set_unobstructed_area_height(int16_t height) {
|
||||
s_unobstructed_area_height = height;
|
||||
}
|
||||
#endif
|
||||
|
||||
#define MULT_X(a, b) (b ? (1000 * a / b) : 0)
|
||||
#define DIV_X(a) (a / 1000)
|
||||
|
||||
static GPoint prv_steps_to_point(int32_t cur, int32_t total, GRect frame) {
|
||||
#if PBL_RECT
|
||||
/* e 0 b
|
||||
* ---------
|
||||
* | |
|
||||
* | |
|
||||
* | |
|
||||
* | |
|
||||
* | |
|
||||
* ---------
|
||||
* d c
|
||||
*/
|
||||
|
||||
const int32_t top_right = frame.size.w / 2;
|
||||
const int32_t bot_right = frame.size.h + top_right;
|
||||
const int32_t bot_left = frame.size.w + bot_right;
|
||||
const int32_t top_left = frame.size.h + bot_left;
|
||||
const int32_t rect_perimeter = top_left + top_right;
|
||||
|
||||
// limits calculated from length along perimeter starting from '0'
|
||||
const int32_t limit_b = total * top_right / rect_perimeter;
|
||||
const int32_t limit_c = total * bot_right / rect_perimeter;
|
||||
const int32_t limit_d = total * bot_left / rect_perimeter;
|
||||
const int32_t limit_e = total * top_left / rect_perimeter;
|
||||
|
||||
if (cur <= limit_b) {
|
||||
// zone 0 - b
|
||||
return GPoint(frame.origin.x + DIV_X(frame.size.w * (500 + (MULT_X(cur, limit_b) / 2))),
|
||||
frame.origin.y);
|
||||
} else if (cur <= limit_c) {
|
||||
// zone b - c
|
||||
return GPoint(frame.origin.x + frame.size.w,
|
||||
frame.origin.y +
|
||||
DIV_X(frame.size.h * MULT_X((cur - limit_b), (limit_c - limit_b))));
|
||||
} else if (cur <= limit_d) {
|
||||
// zone c - d
|
||||
return GPoint(frame.origin.x +
|
||||
DIV_X(frame.size.w * (1000 - MULT_X((cur - limit_c), (limit_d - limit_c)))),
|
||||
frame.origin.y + frame.size.h);
|
||||
} else if (cur <= limit_e) {
|
||||
// zone d - e
|
||||
return GPoint(frame.origin.x,
|
||||
frame.origin.y +
|
||||
DIV_X(frame.size.h * (1000 - MULT_X((cur - limit_d), (limit_e - limit_d)))));
|
||||
} else {
|
||||
// zone e - 0
|
||||
return GPoint(frame.origin.x +
|
||||
DIV_X(frame.size.w / 2 * MULT_X((cur - limit_e), (total - limit_e))),
|
||||
frame.origin.y);
|
||||
}
|
||||
#elif PBL_ROUND
|
||||
// Simply a calculated point on the circumference
|
||||
const int32_t angle = DIV_X(360 * MULT_X(cur, total));
|
||||
return gpoint_from_polar(frame, GOvalScaleModeFitCircle, DEG_TO_TRIGANGLE(angle));
|
||||
#endif
|
||||
}
|
||||
|
||||
#if PBL_RECT
|
||||
static GPoint prv_inset_point(GRect *frame, GPoint outer_point, int32_t inset_amount) {
|
||||
// Insets the given point by the specified amount
|
||||
return (GPoint) {
|
||||
.x = MAX(inset_amount - 1, MIN(outer_point.x, frame->size.w - inset_amount)),
|
||||
.y = MAX(inset_amount - 1, MIN(outer_point.y, frame->size.h - inset_amount))
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// UI Drawing
|
||||
|
||||
static void prv_draw_outer_ring(GContext *ctx, int32_t current, int32_t total,
|
||||
int32_t fill_thickness, GRect frame, GColor color) {
|
||||
graphics_context_set_fill_color(ctx, color);
|
||||
|
||||
const GRect outer_bounds = grect_inset(frame, GEdgeInsets(-1));
|
||||
|
||||
#if PBL_RECT
|
||||
GPoint start_outer_point = prv_steps_to_point(0, total, outer_bounds);
|
||||
GPoint start_inner_point = prv_inset_point(&frame, start_outer_point, fill_thickness);
|
||||
GPoint end_outer_point = prv_steps_to_point(current, total, outer_bounds);
|
||||
GPoint end_inner_point = prv_inset_point(&frame, end_outer_point, fill_thickness);
|
||||
|
||||
#if PBL_BW
|
||||
// Make sure we draw something if we have any steps
|
||||
if ((start_outer_point.y == end_outer_point.y) && (end_outer_point.x > start_outer_point.x) &&
|
||||
(end_outer_point.x - start_outer_point.x < 3)) {
|
||||
end_outer_point.x = start_outer_point.x + 3;
|
||||
end_inner_point.x = start_inner_point.x + 3;
|
||||
}
|
||||
#endif
|
||||
|
||||
const int32_t max_points = 20;
|
||||
|
||||
GPath path = (GPath) {
|
||||
.points = app_zalloc_check(sizeof(GPoint) * max_points),
|
||||
.num_points = 0,
|
||||
};
|
||||
|
||||
const int32_t top_right = frame.size.w / 2;
|
||||
const int32_t bot_right = frame.size.h + top_right;
|
||||
const int32_t bot_left = frame.size.w + bot_right;
|
||||
const int32_t top_left = frame.size.h + bot_left;
|
||||
const int32_t rect_perimeter = top_left + top_right;
|
||||
|
||||
const int32_t corners[] = {0,
|
||||
total * top_right / rect_perimeter,
|
||||
total * bot_right / rect_perimeter,
|
||||
total * bot_left / rect_perimeter,
|
||||
total * top_left / rect_perimeter,
|
||||
total};
|
||||
|
||||
// start the path with start_outer_point
|
||||
path.points[path.num_points++] = start_outer_point;
|
||||
// loop through and add all the corners b/w start and end
|
||||
for (uint16_t i = 0; i < ARRAY_LENGTH(corners); i++) {
|
||||
if (corners[i] > 0 && corners[i] < current) {
|
||||
path.points[path.num_points++] = prv_steps_to_point(corners[i], total, outer_bounds);
|
||||
}
|
||||
}
|
||||
// add end outer and inner points
|
||||
path.points[path.num_points++] = end_outer_point;
|
||||
path.points[path.num_points++] = end_inner_point;
|
||||
// loop though backwards and add all the corners b/w end and start
|
||||
for (int i = ARRAY_LENGTH(corners) - 1; i >= 0; i--) {
|
||||
if (corners[i] > 0 && corners[i] < current) {
|
||||
path.points[path.num_points++] = prv_inset_point(
|
||||
&frame, prv_steps_to_point(corners[i], total, outer_bounds), fill_thickness);
|
||||
}
|
||||
}
|
||||
// add start_inner_point
|
||||
path.points[path.num_points++] = start_inner_point;
|
||||
|
||||
gpath_draw_filled(ctx, &path);
|
||||
|
||||
#if PBL_COLOR
|
||||
graphics_context_set_stroke_color(ctx, color);
|
||||
gpath_draw_outline(ctx, &path);
|
||||
#else
|
||||
graphics_context_set_stroke_color(ctx, GColorWhite);
|
||||
GRect inner_bounds = grect_inset(outer_bounds, GEdgeInsets(fill_thickness));
|
||||
graphics_draw_rect(ctx, &inner_bounds);
|
||||
inner_bounds = grect_inset(inner_bounds, GEdgeInsets(-1));
|
||||
graphics_draw_rect(ctx, &inner_bounds);
|
||||
#endif
|
||||
|
||||
app_free(path.points);
|
||||
|
||||
#elif PBL_ROUND
|
||||
const int32_t degree = total ? (360 * current / total) : 0;
|
||||
const int32_t to_angle = DEG_TO_TRIGANGLE(degree);
|
||||
graphics_fill_radial(ctx, outer_bounds, GOvalScaleModeFitCircle, fill_thickness, 0, to_angle);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if PBL_ROUND
|
||||
static void prv_draw_outer_dots(GContext *ctx, GRect bounds) {
|
||||
const GRect inset_bounds = grect_inset(bounds, GEdgeInsets(6));
|
||||
|
||||
// outer dots placed along inside circumference
|
||||
const int num_dots = 12;
|
||||
for (int i = 0; i < num_dots; i++) {
|
||||
GPoint pos = gpoint_from_polar(inset_bounds, GOvalScaleModeFitCircle,
|
||||
DEG_TO_TRIGANGLE(i * 360 / num_dots));
|
||||
|
||||
const int dot_radius = 2;
|
||||
graphics_context_set_fill_color(ctx, GColorDarkGray);
|
||||
graphics_fill_circle(ctx, pos, dot_radius);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static void prv_draw_goal_line(GContext *ctx, int32_t current_progress, int32_t total_progress,
|
||||
int32_t line_length, int32_t line_width, GRect frame, GColor color) {
|
||||
const GPoint line_outer_point = prv_steps_to_point(current_progress, total_progress, frame);
|
||||
|
||||
#if PBL_RECT
|
||||
const GPoint line_inner_point = prv_inset_point(&frame, line_outer_point, line_length);
|
||||
#elif PBL_ROUND
|
||||
const GRect inner_bounds = grect_inset(frame, GEdgeInsets(line_length));
|
||||
const GPoint line_inner_point = prv_steps_to_point(current_progress,
|
||||
total_progress, inner_bounds);
|
||||
#endif
|
||||
|
||||
graphics_context_set_stroke_color(ctx, color);
|
||||
graphics_context_set_stroke_width(ctx, line_width);
|
||||
graphics_draw_line(ctx, line_inner_point, line_outer_point);
|
||||
}
|
||||
|
||||
#if ROBERT_SCREEN_RES
|
||||
static void prv_draw_seperator(GContext *ctx, GRect bounds, GColor color) {
|
||||
bounds.origin.y += 111; // top offset
|
||||
|
||||
GPoint p1 = bounds.origin;
|
||||
GPoint p2 = p1;
|
||||
p2.x += bounds.size.w;
|
||||
|
||||
graphics_context_set_stroke_color(ctx, color);
|
||||
graphics_context_set_stroke_width(ctx, 1);
|
||||
graphics_draw_line(ctx, p1, p2);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void prv_draw_steps_and_shoe(GContext *ctx, const char *steps_buffer, GFont font,
|
||||
GRect bounds, GColor color, GBitmap *shoe_icon,
|
||||
bool screen_is_obstructed, bool has_bpm) {
|
||||
#if PBL_BW
|
||||
bounds.origin.y += screen_is_obstructed ? (has_bpm ? 74 : 66) : (has_bpm ? 114 : 96);
|
||||
#elif ROBERT_SCREEN_RES
|
||||
bounds.origin.y += screen_is_obstructed ? 113 : 158;
|
||||
#elif SNOWY_SCREEN_RES
|
||||
if (screen_is_obstructed) {
|
||||
bounds = grect_inset(bounds, GEdgeInsets(0, 20));
|
||||
}
|
||||
#endif
|
||||
|
||||
GRect icon_bounds = gbitmap_get_bounds(shoe_icon);
|
||||
icon_bounds.origin = bounds.origin;
|
||||
#if PBL_BW
|
||||
icon_bounds.origin.x += 23; // icon left offset
|
||||
icon_bounds.origin.y += 9; // icon top offset
|
||||
#elif ROBERT_SCREEN_RES
|
||||
icon_bounds.origin.y += (46 - icon_bounds.size.h); // icon top offest
|
||||
#elif SNOWY_SCREEN_RES
|
||||
icon_bounds.origin.x = screen_is_obstructed ? bounds.origin.x // icon_left offset
|
||||
: (bounds.size.w / 2) - (icon_bounds.size.w / 2);
|
||||
icon_bounds.origin.y += screen_is_obstructed ? 84 : 22; // icon top offset
|
||||
#elif SPALDING_SCREEN_RES
|
||||
icon_bounds.origin.x = (bounds.size.w / 2) - (icon_bounds.size.w / 2);
|
||||
icon_bounds.origin.y += 27; // icon top offset
|
||||
#endif
|
||||
|
||||
graphics_context_set_compositing_mode(ctx, GCompOpSet);
|
||||
graphics_draw_bitmap_in_rect(ctx, shoe_icon, &icon_bounds);
|
||||
|
||||
#if PBL_BW
|
||||
const GTextAlignment alignment = GTextAlignmentLeft;
|
||||
bounds.origin.x += 62; // steps text left offset
|
||||
#elif ROBERT_SCREEN_RES
|
||||
const GTextAlignment alignment = GTextAlignmentRight;
|
||||
#elif SNOWY_SCREEN_RES
|
||||
const GTextAlignment alignment = screen_is_obstructed ? GTextAlignmentRight: GTextAlignmentCenter;
|
||||
bounds.origin.y += screen_is_obstructed ? 65 : 108; // steps text top offset
|
||||
#elif SPALDING_SCREEN_RES
|
||||
const GTextAlignment alignment = GTextAlignmentCenter;
|
||||
bounds.origin.y += 113; // steps text top offset
|
||||
#endif
|
||||
|
||||
graphics_context_set_text_color(ctx, color);
|
||||
graphics_draw_text(ctx, steps_buffer, font, bounds, GTextOverflowModeFill, alignment, NULL);
|
||||
}
|
||||
|
||||
static void prv_draw_time(GContext *ctx, GFont time_font, GFont am_pm_font, GRect bounds,
|
||||
bool screen_is_obstructed, bool has_bpm) {
|
||||
GTextNodeHorizontal *horiz_container = graphics_text_node_create_horizontal(MAX_TEXT_NODES);
|
||||
GTextNodeContainer *container = &horiz_container->container;
|
||||
horiz_container->horizontal_alignment = GTextAlignmentCenter;
|
||||
|
||||
char time_buffer[8];
|
||||
char am_pm_buffer[4];
|
||||
|
||||
const time_t now = rtc_get_time();
|
||||
/// Current time in 24 or 12 hour
|
||||
const char *time_fmt = clock_is_24h_style() ? "%R" : "%l:%M";
|
||||
strftime(time_buffer, sizeof(time_buffer), time_fmt, pbl_override_localtime(&now));
|
||||
health_util_create_text_node_with_text(
|
||||
string_strip_leading_whitespace(time_buffer),
|
||||
time_font, GColorWhite, container);
|
||||
|
||||
if (!clock_is_24h_style()) {
|
||||
/// AM/PM for the current time
|
||||
strftime(am_pm_buffer, sizeof(am_pm_buffer), "%p", pbl_override_localtime(&now));
|
||||
health_util_create_text_node_with_text(
|
||||
am_pm_buffer, am_pm_font, GColorWhite, container);
|
||||
}
|
||||
|
||||
#if PBL_BW
|
||||
bounds.origin.y = screen_is_obstructed ? (has_bpm ? 13 : 23) : (has_bpm ? 36 : 53);
|
||||
#elif ROBERT_SCREEN_RES
|
||||
bounds.origin.y = screen_is_obstructed ? -12 : 6;
|
||||
#elif SNOWY_SCREEN_RES
|
||||
bounds.origin.y = screen_is_obstructed ? 4 : 47;
|
||||
#elif SPALDING_SCREEN_RES
|
||||
bounds.origin.y = 50;
|
||||
#endif
|
||||
|
||||
graphics_text_node_draw(&container->node, ctx, &bounds, NULL, NULL);
|
||||
graphics_text_node_destroy(&container->node);
|
||||
}
|
||||
|
||||
#if PBL_BW || ROBERT_SCREEN_RES
|
||||
static void prv_draw_bpm(GContext *ctx, int32_t current_bpm, GFont font, GBitmap *heart_icon,
|
||||
GRect bounds, bool screen_is_obstructed, void *i18n_owner) {
|
||||
#if PBL_BW
|
||||
bounds.origin.y += screen_is_obstructed ? 52 : 89;
|
||||
#elif ROBERT_SCREEN_RES
|
||||
bounds.origin.y += screen_is_obstructed ? 80 : 123;
|
||||
#endif
|
||||
|
||||
GRect icon_bounds = gbitmap_get_bounds(heart_icon);
|
||||
icon_bounds.origin = bounds.origin;
|
||||
#if PBL_BW
|
||||
icon_bounds.origin.x += 20; // icon left offset
|
||||
#endif
|
||||
|
||||
graphics_context_set_compositing_mode(ctx, GCompOpSet);
|
||||
graphics_draw_bitmap_in_rect(ctx, heart_icon, &icon_bounds);
|
||||
|
||||
char bpm_text[16];
|
||||
snprintf(bpm_text, sizeof(bpm_text), i18n_get("%d BPM", i18n_owner), current_bpm);
|
||||
|
||||
#if PBL_BW
|
||||
bounds.origin.x += 62; // bpm text left offset
|
||||
#endif
|
||||
bounds.origin.y -= PBL_IF_BW_ELSE(5, 8); // bpm text top offset
|
||||
|
||||
const GTextAlignment alignment = PBL_IF_BW_ELSE(GTextAlignmentLeft, GTextAlignmentRight);
|
||||
|
||||
graphics_context_set_text_color(ctx, PBL_IF_COLOR_ELSE(GColorRed, GColorWhite));
|
||||
graphics_draw_text(ctx, bpm_text, font, bounds, GTextOverflowModeFill, alignment, NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Update Proc
|
||||
|
||||
static void prv_base_layer_update_proc(Layer *layer, GContext *ctx) {
|
||||
KickstartData *data = window_get_user_data(layer_get_window(layer));
|
||||
GRect bounds = layer->bounds;
|
||||
|
||||
GRect unobstructed_bounds;
|
||||
layer_get_unobstructed_bounds(layer, &unobstructed_bounds);
|
||||
|
||||
#if UNITTEST
|
||||
unobstructed_bounds.size.h = bounds.size.h - s_unobstructed_area_height;
|
||||
#endif
|
||||
|
||||
const bool screen_is_obstructed = (unobstructed_bounds.size.h != bounds.size.h);
|
||||
|
||||
bounds.size.h = unobstructed_bounds.size.h;
|
||||
|
||||
#if SNOWY_SCREEN_RES
|
||||
const int16_t fill_thickness = screen_is_obstructed ? 10 : 11;
|
||||
#elif ROBERT_SCREEN_RES
|
||||
const int16_t fill_thickness = screen_is_obstructed ? 5 : 13;
|
||||
#elif SPALDING_SCREEN_RES
|
||||
const int16_t fill_thickness = (bounds.size.h - grect_inset(bounds, GEdgeInsets(15)).size.h) / 2;
|
||||
#endif
|
||||
|
||||
#if PBL_COLOR
|
||||
const bool has_passed_goal = (data->current_steps > data->typical_steps);
|
||||
const GColor fill_color = has_passed_goal ? GColorJaegerGreen : GColorVividCerulean;
|
||||
const GColor text_color = has_passed_goal ? GColorJaegerGreen : GColorVividCerulean;
|
||||
#if SNOWY_SCREEN_RES
|
||||
GBitmap *shoe =
|
||||
has_passed_goal ? (screen_is_obstructed ? &data->shoe_green_small : &data->shoe_green)
|
||||
: (screen_is_obstructed ? &data->shoe_blue_small : &data->shoe_blue);
|
||||
#else
|
||||
GBitmap *shoe = has_passed_goal ? &data->shoe_green : &data->shoe_blue;
|
||||
#endif // SNOWY_SCREEN_RES
|
||||
#else
|
||||
const GColor fill_color = GColorDarkGray;
|
||||
const GColor text_color = GColorWhite;
|
||||
GBitmap *shoe = &data->shoe;
|
||||
#endif // PBL_COLOR
|
||||
|
||||
#if PBL_ROUND
|
||||
prv_draw_outer_dots(ctx, bounds);
|
||||
#endif
|
||||
|
||||
// draw outer ring
|
||||
prv_draw_outer_ring(ctx, data->current_steps, data->daily_steps_avg,
|
||||
fill_thickness, bounds, fill_color);
|
||||
|
||||
const int goal_line_length = PBL_IF_COLOR_ELSE(fill_thickness + 3, 12);
|
||||
const int goal_line_width = 4;
|
||||
|
||||
// draw yellow goal line
|
||||
prv_draw_goal_line(ctx, data->typical_steps, MAX(data->daily_steps_avg, data->typical_steps),
|
||||
goal_line_length, goal_line_width, bounds, GColorYellow);
|
||||
|
||||
const bool has_bpm = (data->current_bpm > 0);
|
||||
|
||||
// draw time
|
||||
prv_draw_time(ctx, data->time_font, PBL_IF_COLOR_ELSE(data->am_pm_font, data->time_font),
|
||||
bounds, screen_is_obstructed, has_bpm);
|
||||
|
||||
#if ROBERT_SCREEN_RES
|
||||
bounds = grect_inset(bounds, GEdgeInsets(0, 25));
|
||||
|
||||
// draw deperator
|
||||
if (!screen_is_obstructed) {
|
||||
prv_draw_seperator(ctx, bounds, GColorWhite);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if PBL_BW || ROBERT_SCREEN_RES
|
||||
// draw bpm and heart
|
||||
if (has_bpm) {
|
||||
prv_draw_bpm(ctx, data->current_bpm, data->steps_font, &data->heart_icon, bounds,
|
||||
screen_is_obstructed, data);
|
||||
}
|
||||
#endif
|
||||
|
||||
// draw steps and shoe
|
||||
prv_draw_steps_and_shoe(ctx, data->steps_buffer, data->steps_font, bounds, text_color, shoe,
|
||||
screen_is_obstructed, has_bpm);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Data
|
||||
|
||||
static void prv_update_steps_buffer(KickstartData *data) {
|
||||
const int thousands = data->current_steps / 1000;
|
||||
const int hundreds = data->current_steps % 1000;
|
||||
if (thousands) {
|
||||
/// Step count greater than 1000 with a thousands seperator
|
||||
snprintf(data->steps_buffer, sizeof(data->steps_buffer), i18n_get("%d,%03d", data),
|
||||
thousands, hundreds);
|
||||
} else {
|
||||
/// Step count less than 1000
|
||||
snprintf(data->steps_buffer, sizeof(data->steps_buffer), i18n_get("%d", data),
|
||||
hundreds);
|
||||
}
|
||||
layer_mark_dirty(&data->base_layer);
|
||||
}
|
||||
|
||||
static void prv_update_current_steps(KickstartData *data) {
|
||||
data->current_steps = health_service_sum_today(HealthMetricStepCount);
|
||||
prv_update_steps_buffer(data);
|
||||
}
|
||||
|
||||
static void prv_update_typical_steps(KickstartData *data) {
|
||||
data->typical_steps = health_service_sum_averaged(HealthMetricStepCount,
|
||||
time_start_of_today(),
|
||||
rtc_get_time(),
|
||||
HealthServiceTimeScopeWeekly);
|
||||
}
|
||||
|
||||
static void prv_update_daily_steps_avg(KickstartData *data) {
|
||||
data->daily_steps_avg = health_service_sum_averaged(HealthMetricStepCount,
|
||||
time_start_of_today(),
|
||||
time_start_of_today() + SECONDS_PER_DAY,
|
||||
HealthServiceTimeScopeWeekly);
|
||||
}
|
||||
|
||||
static void prv_update_hrm_bpm(KickstartData *data) {
|
||||
data->current_bpm = health_service_peek_current_value(HealthMetricHeartRateBPM);
|
||||
}
|
||||
|
||||
static void prv_normalize_data(KickstartData *data) {
|
||||
// If the user's daily avg steps are very low (QA or a brand new pebble user), bump the value
|
||||
// to a slightly more reasonable number.
|
||||
// This fixes an integer rounding problem when the value is very small (PBL-43717)
|
||||
const int min_daily_steps_avg = 100;
|
||||
data->daily_steps_avg = MAX(data->daily_steps_avg, min_daily_steps_avg);
|
||||
|
||||
// increase daily avg 5% more than current steps if current steps is more than 95% of daily avg
|
||||
if (data->current_steps >= (data->daily_steps_avg * 95 / 100)) {
|
||||
data->daily_steps_avg = data->current_steps * 105 / 100;
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_update_data(KickstartData *data) {
|
||||
prv_update_current_steps(data);
|
||||
prv_update_typical_steps(data);
|
||||
prv_update_daily_steps_avg(data);
|
||||
prv_update_hrm_bpm(data);
|
||||
prv_normalize_data(data);
|
||||
layer_mark_dirty(&data->base_layer);
|
||||
}
|
||||
|
||||
#if UNITTEST
|
||||
T_STATIC void prv_set_data(KickstartData *data, int32_t current_steps,
|
||||
int32_t typical_steps, int32_t daily_steps_avg, int32_t current_bpm) {
|
||||
data->current_steps = current_steps;
|
||||
data->typical_steps = typical_steps;
|
||||
data->daily_steps_avg = daily_steps_avg;
|
||||
data->current_bpm = current_bpm;
|
||||
prv_normalize_data(data);
|
||||
}
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Handlers
|
||||
|
||||
static void prv_health_service_events_handler(HealthEventType event, void *context) {
|
||||
if (event == HealthEventMovementUpdate) {
|
||||
prv_update_current_steps(context);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_tick_handler(struct tm *tick_time, TimeUnits changed) {
|
||||
KickstartData *data = app_state_get_user_data();
|
||||
prv_update_data(data);
|
||||
}
|
||||
|
||||
T_STATIC void prv_window_load_handler(Window *window) {
|
||||
KickstartData *data = window_get_user_data(window);
|
||||
|
||||
// load resources
|
||||
#if PBL_BW
|
||||
gbitmap_init_with_resource(&data->shoe, RESOURCE_ID_STRIDE_SHOE);
|
||||
gbitmap_init_with_resource(&data->heart_icon, RESOURCE_ID_WORKOUT_APP_HEART);
|
||||
data->steps_font = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD);
|
||||
data->time_font = fonts_get_system_font(FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM);
|
||||
#else
|
||||
gbitmap_init_with_resource(&data->shoe_blue, RESOURCE_ID_STRIDE_SHOE_BLUE);
|
||||
gbitmap_init_with_resource(&data->shoe_green, RESOURCE_ID_STRIDE_SHOE_GREEN);
|
||||
#if ROBERT_SCREEN_RES
|
||||
gbitmap_init_with_resource(&data->heart_icon, RESOURCE_ID_STRIDE_HEART);
|
||||
data->steps_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_46_NUMBERS_AM_PM);
|
||||
data->time_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_88_NUMBERS_AM_PM);
|
||||
data->am_pm_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_88_THIN_NUMBERS_AM_PM);
|
||||
#elif SNOWY_SCREEN_RES || SPALDING_SCREEN_RES
|
||||
#if PBL_RECT
|
||||
gbitmap_init_with_resource(&data->shoe_blue_small, RESOURCE_ID_STRIDE_SHOE_BLUE_SMALL);
|
||||
gbitmap_init_with_resource(&data->shoe_green_small, RESOURCE_ID_STRIDE_SHOE_GREEN_SMALL);
|
||||
#endif // PBL_RECT
|
||||
data->steps_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_36_NUMBERS_AM_PM);
|
||||
data->time_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_60_NUMBERS_AM_PM);
|
||||
data->am_pm_font = fonts_get_system_font(FONT_KEY_AGENCY_FB_60_THIN_NUMBERS_AM_PM);
|
||||
#else
|
||||
#error "Undefined screen size"
|
||||
#endif // ROBERT_SCREEN_RES
|
||||
#endif // PBL_BW
|
||||
|
||||
Layer *window_layer = window_get_root_layer(window);
|
||||
|
||||
// set window background
|
||||
window_set_background_color(window, GColorBlack);
|
||||
|
||||
// set up the base layer
|
||||
layer_init(&data->base_layer, &window_layer->bounds);
|
||||
layer_set_update_proc(&data->base_layer, prv_base_layer_update_proc);
|
||||
layer_add_child(window_layer, &data->base_layer);
|
||||
|
||||
// update steps and time
|
||||
prv_update_steps_buffer(data);
|
||||
|
||||
// subscribe to health service
|
||||
health_service_events_subscribe(prv_health_service_events_handler, data);
|
||||
|
||||
// subscribe to tick timer for minute ticks
|
||||
tick_timer_service_subscribe(MINUTE_UNIT, prv_tick_handler);
|
||||
}
|
||||
|
||||
T_STATIC void prv_window_unload_handler(Window *window) {
|
||||
KickstartData *data = window_get_user_data(window);
|
||||
|
||||
// unsubscribe from service events
|
||||
health_service_events_unsubscribe();
|
||||
tick_timer_service_unsubscribe();
|
||||
|
||||
// deinit everything
|
||||
#if PBL_BW
|
||||
gbitmap_deinit(&data->shoe);
|
||||
#else
|
||||
gbitmap_deinit(&data->shoe_blue);
|
||||
gbitmap_deinit(&data->shoe_green);
|
||||
#endif
|
||||
#if PBL_COLOR && SNOWY_SCREEN_RES
|
||||
gbitmap_deinit(&data->shoe_blue_small);
|
||||
gbitmap_deinit(&data->shoe_green_small);
|
||||
#endif
|
||||
gbitmap_deinit(&data->heart_icon);
|
||||
layer_deinit(&data->base_layer);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// App Main
|
||||
|
||||
static void prv_main(void) {
|
||||
KickstartData *data = app_zalloc_check(sizeof(KickstartData));
|
||||
app_state_set_user_data(data);
|
||||
|
||||
prv_update_data(data);
|
||||
|
||||
window_init(&data->window, WINDOW_NAME("Kickstart"));
|
||||
window_set_user_data(&data->window, data);
|
||||
window_set_window_handlers(&data->window, &(WindowHandlers) {
|
||||
.load = prv_window_load_handler,
|
||||
.unload = prv_window_unload_handler,
|
||||
});
|
||||
app_window_stack_push(&data->window, true);
|
||||
|
||||
app_event_loop();
|
||||
|
||||
window_deinit(&data->window);
|
||||
i18n_free_all(data);
|
||||
app_free(data);
|
||||
}
|
||||
|
||||
const PebbleProcessMd* kickstart_get_app_info() {
|
||||
static const PebbleProcessMdSystem s_app_md = {
|
||||
.common = {
|
||||
// UUID: 3af858c3-16cb-4561-91e7-f1ad2df8725f
|
||||
.uuid = {0x3a, 0xf8, 0x58, 0xc3, 0x16, 0xcb, 0x45, 0x61,
|
||||
0x91, 0xe7, 0xf1, 0xad, 0x2d, 0xf8, 0x72, 0x5f},
|
||||
.main_func = prv_main,
|
||||
.process_type = ProcessTypeWatchface,
|
||||
},
|
||||
.icon_resource_id = RESOURCE_ID_MENU_ICON_KICKSTART_WATCH,
|
||||
.name = "Kickstart"
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_md;
|
||||
}
|
||||
51
src/fw/apps/watch/kickstart/kickstart.h
Normal file
51
src/fw/apps/watch/kickstart/kickstart.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/ui/ui.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
typedef struct KickstartData {
|
||||
Window window;
|
||||
Layer base_layer;
|
||||
|
||||
int32_t current_steps;
|
||||
int32_t typical_steps;
|
||||
int32_t daily_steps_avg;
|
||||
int32_t current_bpm;
|
||||
|
||||
#if PBL_BW
|
||||
GBitmap shoe;
|
||||
#else
|
||||
GBitmap shoe_blue;
|
||||
GBitmap shoe_green;
|
||||
#endif
|
||||
#if PBL_COLOR && PBL_DISPLAY_WIDTH == 144 && PBL_DISPLAY_HEIGHT == 168
|
||||
GBitmap shoe_blue_small;
|
||||
GBitmap shoe_green_small;
|
||||
#endif
|
||||
GBitmap heart_icon;
|
||||
|
||||
GFont steps_font;
|
||||
GFont time_font;
|
||||
GFont am_pm_font;
|
||||
|
||||
bool screen_is_obstructed;
|
||||
char steps_buffer[8];
|
||||
} KickstartData;
|
||||
|
||||
const PebbleProcessMd* kickstart_get_app_info();
|
||||
140
src/fw/apps/watch/low_power/low_power_face.c
Normal file
140
src/fw/apps/watch/low_power/low_power_face.c
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "low_power_face.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/graphics/gdraw_command_image.h"
|
||||
#include "applib/graphics/text.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/kino/kino_layer.h"
|
||||
#include "applib/ui/ui.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/battery/battery_monitor.h"
|
||||
#include "services/common/clock.h"
|
||||
#include "util/time/time.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
Window low_power_window;
|
||||
TextLayer low_power_time_layer;
|
||||
KinoLayer low_power_kino_layer;
|
||||
char time_text[6];
|
||||
} LowPowerFaceData;
|
||||
|
||||
static LowPowerFaceData *s_low_power_data;
|
||||
|
||||
static void prv_minute_tick(struct tm *tick_time, TimeUnits units_changed) {
|
||||
char *time_format;
|
||||
|
||||
if (clock_is_24h_style()) {
|
||||
time_format = "%R";
|
||||
} else {
|
||||
time_format = "%I:%M";
|
||||
}
|
||||
|
||||
strftime(s_low_power_data->time_text, sizeof(s_low_power_data->time_text),
|
||||
time_format, tick_time);
|
||||
|
||||
// Remove leading zero from hour in case of 12h mode
|
||||
if (!clock_is_24h_style() && (s_low_power_data->time_text[0] == '0')) {
|
||||
text_layer_set_text(&s_low_power_data->low_power_time_layer,
|
||||
s_low_power_data->time_text+1);
|
||||
} else {
|
||||
text_layer_set_text(&s_low_power_data->low_power_time_layer,
|
||||
s_low_power_data->time_text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void deinit(void) {
|
||||
tick_timer_service_unsubscribe();
|
||||
kino_layer_deinit(&s_low_power_data->low_power_kino_layer);
|
||||
|
||||
app_free(s_low_power_data);
|
||||
}
|
||||
|
||||
static void init(void) {
|
||||
s_low_power_data = app_malloc_check(sizeof(*s_low_power_data));
|
||||
|
||||
window_init(&s_low_power_data->low_power_window, "Low Power");
|
||||
window_set_background_color(&s_low_power_data->low_power_window, GColorLightGray);
|
||||
app_window_stack_push(&s_low_power_data->low_power_window, true /* Animated */);
|
||||
|
||||
const GFont text_font = fonts_get_system_font(FONT_KEY_LECO_42_NUMBERS);
|
||||
const GTextAlignment text_alignment = GTextAlignmentCenter;
|
||||
const unsigned int font_height = fonts_get_font_height(text_font);
|
||||
const GTextOverflowMode text_overflow_mode = GTextOverflowModeTrailingEllipsis;
|
||||
const GSize text_size = app_graphics_text_layout_get_content_size("00:00", text_font,
|
||||
s_low_power_data->low_power_window.layer.bounds, text_alignment,
|
||||
text_overflow_mode);
|
||||
const int text_pos_y_adjust = -9; // small vertical adjustment to match design specification
|
||||
const int text_pos_y = (DISP_ROWS / 2) - (font_height / 2) + text_pos_y_adjust;
|
||||
const GRect text_container_rect = GRect(0, text_pos_y, DISP_COLS, font_height);
|
||||
GRect text_frame = (GRect) { .size = text_size };
|
||||
grect_align(&text_frame, &text_container_rect, GAlignTop, false);
|
||||
|
||||
kino_layer_init(&s_low_power_data->low_power_kino_layer,
|
||||
&s_low_power_data->low_power_window.layer.bounds);
|
||||
kino_layer_set_reel_with_resource(&s_low_power_data->low_power_kino_layer,
|
||||
RESOURCE_ID_BATTERY_NEEDS_CHARGING);
|
||||
kino_layer_set_alignment(&s_low_power_data->low_power_kino_layer, GAlignBottom);
|
||||
// TODO PBL-30180: Design needs to revise icon so it doesn't have a rounded cap at the bottom
|
||||
s_low_power_data->low_power_kino_layer.layer.frame.origin.y += 2;
|
||||
layer_add_child(&s_low_power_data->low_power_window.layer,
|
||||
&s_low_power_data->low_power_kino_layer.layer);
|
||||
|
||||
text_layer_init_with_parameters(&s_low_power_data->low_power_time_layer,
|
||||
&s_low_power_data->low_power_window.layer.frame, NULL, text_font,
|
||||
GColorBlack, GColorClear, text_alignment, text_overflow_mode);
|
||||
layer_set_frame(&s_low_power_data->low_power_time_layer.layer, &text_frame);
|
||||
layer_add_child(&s_low_power_data->low_power_window.layer,
|
||||
&s_low_power_data->low_power_time_layer.layer);
|
||||
|
||||
// Because of the delay before the tick timer service first calls prv_minute_tick,
|
||||
// we call it ourselves to update the time right away
|
||||
struct tm current_time;
|
||||
clock_get_time_tm(¤t_time);
|
||||
prv_minute_tick(¤t_time, HOUR_UNIT);
|
||||
|
||||
tick_timer_service_subscribe(MINUTE_UNIT, prv_minute_tick);
|
||||
}
|
||||
|
||||
static void low_power_main(void) {
|
||||
init();
|
||||
|
||||
app_event_loop();
|
||||
|
||||
deinit();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* low_power_face_get_app_info() {
|
||||
static const PebbleProcessMdSystem s_app_md = {
|
||||
.common = {
|
||||
// UUID: e9475244-5bbe-4e0f-a637-a218af4c3110
|
||||
.uuid = {0xe9, 0x47, 0x52, 0x44, 0x5b, 0xbe, 0x4e, 0x0f, 0xa6, 0x37, 0xa2, 0x18, 0xaf, 0x4c, 0x31, 0x10},
|
||||
.main_func = low_power_main,
|
||||
.process_type = ProcessTypeWatchface,
|
||||
.visibility = ProcessVisibilityHidden,
|
||||
},
|
||||
.name = "Watch Only"
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_md;
|
||||
}
|
||||
21
src/fw/apps/watch/low_power/low_power_face.h
Normal file
21
src/fw/apps/watch/low_power/low_power_face.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* low_power_face_get_app_info();
|
||||
31
src/fw/apps/watch/tictoc/default/tictoc_default.c
Normal file
31
src/fw/apps/watch/tictoc/default/tictoc_default.c
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/rockyjs/rocky.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
|
||||
void tictoc_main(void) {
|
||||
// Push a window so we don't exit
|
||||
Window *window = window_create();
|
||||
app_window_stack_push(window, false/*animated*/);
|
||||
#if CAPABILITY_HAS_JAVASCRIPT
|
||||
rocky_event_loop_with_system_resource(RESOURCE_ID_JS_TICTOC);
|
||||
#else
|
||||
app_event_loop();
|
||||
#endif
|
||||
}
|
||||
258
src/fw/apps/watch/tictoc/spalding/tictoc_spalding.c
Normal file
258
src/fw/apps/watch/tictoc/spalding/tictoc_spalding.c
Normal file
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "watch_model.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/fonts/fonts.h"
|
||||
#include "applib/graphics/gpath.h"
|
||||
#include "applib/graphics/graphics_circle.h"
|
||||
#include "applib/graphics/text.h"
|
||||
#include "util/trig.h"
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/ui.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/clock.h"
|
||||
#include "util/time/time.h"
|
||||
|
||||
typedef struct {
|
||||
Window window;
|
||||
GFont text_font;
|
||||
ClockModel clock_model;
|
||||
GBitmap *bg_bitmap;
|
||||
GPath *hour_path;
|
||||
GPath *minute_path;
|
||||
} MultiWatchData;
|
||||
|
||||
static const GPathInfo HOUR_PATH_INFO = {
|
||||
.num_points = 9,
|
||||
.points = (GPoint[]) {
|
||||
{-5, 10}, {-2, 10}, {-2, 15}, {2, 15}, {2, 10}, {5, 10}, {5, -51}, {0, -56}, {-5, -51},
|
||||
},
|
||||
};
|
||||
|
||||
static const GPathInfo MINUTE_PATH_INFO = {
|
||||
.num_points = 5,
|
||||
.points = (GPoint[]) {
|
||||
{-5, 10}, {5, 10}, {5, -61}, {0, -66}, {-5, -61},
|
||||
},
|
||||
};
|
||||
|
||||
void watch_model_handle_change(ClockModel *model) {
|
||||
MultiWatchData *data = app_state_get_user_data();
|
||||
data->clock_model = *model;
|
||||
layer_mark_dirty(window_get_root_layer(&data->window));
|
||||
}
|
||||
|
||||
static GPointPrecise prv_gpoint_from_polar(const GPointPrecise *center, uint32_t distance,
|
||||
int32_t angle) {
|
||||
return gpoint_from_polar_precise(center, distance << GPOINT_PRECISE_PRECISION, angle);
|
||||
}
|
||||
|
||||
static void prv_graphics_draw_centered_text(GContext *ctx, const GSize *max_size,
|
||||
const GPoint *center, const GFont font,
|
||||
const GColor color, const char *text) {
|
||||
GSize text_size = app_graphics_text_layout_get_content_size(
|
||||
text, font, (GRect) { .size = *max_size }, GTextOverflowModeFill, GTextAlignmentCenter);
|
||||
GPoint text_center = *center;
|
||||
text_center.x -= text_size.w / 2 + 1;
|
||||
text_center.y -= text_size.h * 2 / 3;
|
||||
graphics_context_set_text_color(ctx, color);
|
||||
graphics_draw_text(ctx, text, font, (GRect) { .origin = text_center, .size = text_size },
|
||||
GTextOverflowModeFill, GTextAlignmentCenter, NULL);
|
||||
}
|
||||
|
||||
static void prv_draw_watch_hand_rounded(GContext *ctx, ClockHand *hand, GPointPrecise center) {
|
||||
GPointPrecise watch_hand_end = prv_gpoint_from_polar(¢er, hand->length, hand->angle);
|
||||
if (hand->style == CLOCK_HAND_STYLE_ROUNDED_WITH_HIGHLIGHT) {
|
||||
graphics_context_set_stroke_color(ctx, GColorWhite);
|
||||
graphics_line_draw_precise_stroked_aa(ctx, center, watch_hand_end, hand->thickness + 2);
|
||||
}
|
||||
graphics_context_set_stroke_color(ctx, hand->color);
|
||||
graphics_line_draw_precise_stroked_aa(ctx, center, watch_hand_end, hand->thickness);
|
||||
}
|
||||
|
||||
static void prv_draw_watch_hand_pointed(GContext *ctx, ClockHand *hand, GPoint center,
|
||||
GPath *path) {
|
||||
graphics_context_set_fill_color(ctx, hand->color);
|
||||
gpath_rotate_to(path, hand->angle);
|
||||
gpath_move_to(path, center);
|
||||
gpath_draw_filled(ctx, path);
|
||||
}
|
||||
|
||||
static void prv_draw_watch_hand(GContext *ctx, ClockHand *hand, GPointPrecise center, GPath *path) {
|
||||
switch (hand->style) {
|
||||
case CLOCK_HAND_STYLE_POINTED:
|
||||
prv_draw_watch_hand_pointed(ctx, hand, GPointFromGPointPrecise(center), path);
|
||||
case CLOCK_HAND_STYLE_ROUNDED:
|
||||
case CLOCK_HAND_STYLE_ROUNDED_WITH_HIGHLIGHT:
|
||||
default:
|
||||
prv_draw_watch_hand_rounded(ctx, hand, center);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static GPointPrecise prv_get_clock_center_point(ClockLocation location, const GRect *bounds) {
|
||||
GPoint imprecise_center_point = {0};
|
||||
switch (location) {
|
||||
case CLOCK_LOCATION_TOP:
|
||||
imprecise_center_point = (GPoint) {
|
||||
.x = bounds->size.w / 2,
|
||||
.y = bounds->size.h / 4,
|
||||
};
|
||||
case CLOCK_LOCATION_RIGHT:
|
||||
imprecise_center_point = (GPoint) {
|
||||
.x = bounds->size.w * 3 / 4 - 5,
|
||||
.y = bounds->size.h / 2,
|
||||
};
|
||||
case CLOCK_LOCATION_BOTTOM:
|
||||
imprecise_center_point = (GPoint) {
|
||||
.x = bounds->size.w / 2,
|
||||
.y = bounds->size.h * 3 / 4 + 6,
|
||||
};
|
||||
case CLOCK_LOCATION_LEFT:
|
||||
imprecise_center_point = (GPoint) {
|
||||
.x = bounds->size.w / 4 + 4,
|
||||
.y = bounds->size.h / 2,
|
||||
};
|
||||
default:
|
||||
// aiming for width / 2 - 0.5 to get the true center
|
||||
return (GPointPrecise) {
|
||||
.x = { .integer = bounds->size.w / 2 - 1, .fraction = 3 },
|
||||
.y = { .integer = bounds->size.h / 2 - 1, .fraction = 3 }
|
||||
};
|
||||
}
|
||||
return GPointPreciseFromGPoint(imprecise_center_point);
|
||||
}
|
||||
|
||||
static void prv_draw_clock_face(GContext *ctx, ClockFace *face) {
|
||||
MultiWatchData *data = app_state_get_user_data();
|
||||
const GRect *bounds = &window_get_root_layer(&data->window)->bounds;
|
||||
const GPointPrecise center = prv_get_clock_center_point(face->location, bounds);
|
||||
|
||||
// Draw hands.
|
||||
// TODO: Need to do something about the static GPaths used for watchands. This is very inflexible.
|
||||
prv_draw_watch_hand(ctx, &face->hour_hand, center, data->hour_path);
|
||||
prv_draw_watch_hand(ctx, &face->minute_hand, center, data->minute_path);
|
||||
|
||||
// Draw bob.
|
||||
GRect bob_rect = (GRect) {
|
||||
.size = GSize(face->bob_radius * 2, face->bob_radius * 2)
|
||||
};
|
||||
GRect bob_center_rect = (GRect) {
|
||||
.size = GSize(face->bob_center_radius * 2, face->bob_center_radius * 2)
|
||||
};
|
||||
grect_align(&bob_rect, bounds, GAlignCenter, false /* clips */);
|
||||
grect_align(&bob_center_rect, bounds, GAlignCenter, false /* clips */);
|
||||
graphics_context_set_fill_color(ctx, face->bob_color);
|
||||
graphics_fill_oval(ctx, bob_rect, GOvalScaleModeFitCircle);
|
||||
graphics_context_set_fill_color(ctx, face->bob_center_color);
|
||||
graphics_fill_oval(ctx, bob_center_rect, GOvalScaleModeFitCircle);
|
||||
}
|
||||
|
||||
static void prv_draw_non_local_clock(GContext *ctx, NonLocalClockFace *clock) {
|
||||
// TODO: The non-local clock text is currently baked into the background image.
|
||||
prv_draw_clock_face(ctx, &clock->face);
|
||||
}
|
||||
|
||||
|
||||
static void prv_update_proc(Layer *layer, GContext *ctx) {
|
||||
MultiWatchData *data = app_state_get_user_data();
|
||||
|
||||
const GRect *bounds = &layer->bounds;
|
||||
|
||||
// Background.
|
||||
graphics_draw_bitmap_in_rect(ctx, data->bg_bitmap, bounds);
|
||||
|
||||
ClockModel *clock_model = &data->clock_model;
|
||||
|
||||
// Watch text. TODO: Locate the text properly, rather than hard-coding.
|
||||
if (clock_model->text.location == CLOCK_TEXT_LOCATION_BOTTOM) {
|
||||
const GPoint point = (GPoint) { 90, 140 };
|
||||
prv_graphics_draw_centered_text(ctx, &bounds->size, &point, data->text_font,
|
||||
clock_model->text.color, clock_model->text.buffer);
|
||||
} else if (clock_model->text.location == CLOCK_TEXT_LOCATION_LEFT) {
|
||||
const GRect box = (GRect) { .origin = GPoint(25, 78), .size = bounds->size };
|
||||
graphics_draw_text(ctx, clock_model->text.buffer, data->text_font, box,
|
||||
GTextOverflowModeFill, GTextAlignmentLeft, NULL);
|
||||
}
|
||||
|
||||
// Draw the clocks.
|
||||
for (uint32_t i = 0; i < clock_model->num_non_local_clocks; ++i) {
|
||||
prv_draw_non_local_clock(ctx, &clock_model->non_local_clock[i]);
|
||||
}
|
||||
prv_draw_clock_face(ctx, &clock_model->local_clock);
|
||||
}
|
||||
|
||||
static void prv_window_load(Window *window) {
|
||||
MultiWatchData *data = app_state_get_user_data();
|
||||
|
||||
layer_set_update_proc(window_get_root_layer(window), prv_update_proc);
|
||||
|
||||
watch_model_init();
|
||||
|
||||
data->hour_path = gpath_create(&HOUR_PATH_INFO);
|
||||
data->minute_path = gpath_create(&MINUTE_PATH_INFO);
|
||||
data->bg_bitmap = gbitmap_create_with_resource(data->clock_model.bg_bitmap_id);
|
||||
}
|
||||
|
||||
static void prv_window_unload(Window *window) {
|
||||
MultiWatchData *data = app_state_get_user_data();
|
||||
gpath_destroy(data->hour_path);
|
||||
gpath_destroy(data->minute_path);
|
||||
gbitmap_destroy(data->bg_bitmap);
|
||||
}
|
||||
|
||||
static void prv_app_did_focus(bool did_focus) {
|
||||
if (!did_focus) {
|
||||
return;
|
||||
}
|
||||
app_focus_service_unsubscribe();
|
||||
watch_model_start_intro();
|
||||
}
|
||||
|
||||
static void prv_init(void) {
|
||||
MultiWatchData *data = app_zalloc_check(sizeof(*data));
|
||||
app_state_set_user_data(data);
|
||||
|
||||
data->text_font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);
|
||||
|
||||
window_init(&data->window, "TicToc");
|
||||
window_set_window_handlers(&data->window, &(WindowHandlers) {
|
||||
.load = prv_window_load,
|
||||
.unload = prv_window_unload,
|
||||
});
|
||||
const bool animated = true;
|
||||
app_window_stack_push(&data->window, animated);
|
||||
|
||||
app_focus_service_subscribe_handlers((AppFocusHandlers) {
|
||||
.did_focus = prv_app_did_focus,
|
||||
});
|
||||
}
|
||||
|
||||
static void prv_deinit(void) {
|
||||
MultiWatchData *data = app_state_get_user_data();
|
||||
window_destroy(&data->window);
|
||||
watch_model_cleanup();
|
||||
}
|
||||
|
||||
void tictoc_main(void) {
|
||||
prv_init();
|
||||
app_event_loop();
|
||||
prv_deinit();
|
||||
}
|
||||
212
src/fw/apps/watch/tictoc/spalding/watch_model.c
Normal file
212
src/fw/apps/watch/tictoc/spalding/watch_model.c
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "watch_model.h"
|
||||
|
||||
#include "util/trig.h"
|
||||
#include "applib/app_watch_info.h"
|
||||
#include "applib/pbl_std/pbl_std.h"
|
||||
#include "applib/tick_timer_service.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "util/time/time.h"
|
||||
|
||||
#include <ctype.h>
|
||||
|
||||
static void prv_calculate_hand_angles(struct tm *tick_time, int32_t *hour_angle,
|
||||
int32_t *minute_angle) {
|
||||
*hour_angle = (tick_time->tm_hour % 12) * TRIG_MAX_ANGLE / 12
|
||||
+ tick_time->tm_min * TRIG_MAX_ANGLE / 60 / 12;
|
||||
*minute_angle = tick_time->tm_min * TRIG_MAX_ANGLE / 60;
|
||||
}
|
||||
|
||||
static ClockFace prv_local_clock_face_default(struct tm *tick_time) {
|
||||
int32_t hour_angle, minute_angle;
|
||||
prv_calculate_hand_angles(tick_time, &hour_angle, &minute_angle);
|
||||
|
||||
// TODO: Don't return by value. This thing is massive.
|
||||
return (ClockFace) {
|
||||
.hour_hand = {
|
||||
.angle = hour_angle,
|
||||
.backwards_extension = LOCAL_HOUR_HAND_BACK_EXT_DEFAULT,
|
||||
.color = LOCAL_HOUR_HAND_COLOR_DEFAULT,
|
||||
.length = LOCAL_HOUR_HAND_LENGTH_DEFAULT,
|
||||
.style = CLOCK_HAND_STYLE_ROUNDED,
|
||||
.thickness = LOCAL_HOUR_HAND_THICKNESS_DEFAULT,
|
||||
},
|
||||
.minute_hand = {
|
||||
.angle = minute_angle,
|
||||
.backwards_extension = LOCAL_MINUTE_HAND_BACK_EXT_DEFAULT,
|
||||
.color = LOCAL_MINUTE_HAND_COLOR_DEFAULT,
|
||||
.length = LOCAL_MINUTE_HAND_LENGTH_DEFAULT,
|
||||
.style = CLOCK_HAND_STYLE_ROUNDED,
|
||||
.thickness = LOCAL_MINUTE_HAND_THICKNESS_DEFAULT,
|
||||
},
|
||||
.bob_radius = LOCAL_BOB_RADIUS_DEFAULT,
|
||||
.bob_color = LOCAL_BOB_COLOR_DEFAULT,
|
||||
.location = CLOCK_LOCATION_CENTER,
|
||||
};
|
||||
}
|
||||
|
||||
static NonLocalClockFace prv_configure_non_local_clock_face(int32_t utc_offset, const char *text,
|
||||
GColor text_color, GColor hand_color,
|
||||
ClockTextLocation location) {
|
||||
time_t t = rtc_get_time();
|
||||
struct tm* tick_time = pbl_override_gmtime(&t);
|
||||
tick_time->tm_hour += utc_offset; // TODO check if this works properly
|
||||
int32_t hour_angle, minute_angle;
|
||||
prv_calculate_hand_angles(tick_time, &hour_angle, &minute_angle);
|
||||
|
||||
// TODO: Don't return by value. This thing is massive.
|
||||
NonLocalClockFace non_local_clock = (NonLocalClockFace) {
|
||||
.face = {
|
||||
.hour_hand = {
|
||||
.length = NON_LOCAL_HOUR_HAND_LENGTH_DEFAULT,
|
||||
.thickness = NON_LOCAL_HOUR_HAND_WIDTH_DEFAULT,
|
||||
.backwards_extension = 0,
|
||||
.angle = hour_angle,
|
||||
.color = hand_color,
|
||||
.style = CLOCK_HAND_STYLE_ROUNDED,
|
||||
},
|
||||
.minute_hand = {
|
||||
.length = NON_LOCAL_MINUTE_HAND_LENGTH_DEFAULT,
|
||||
.thickness = NON_LOCAL_MINUTE_HAND_WIDTH_DEFAULT,
|
||||
.backwards_extension = 0,
|
||||
.angle = minute_angle,
|
||||
.color = hand_color,
|
||||
.style = CLOCK_HAND_STYLE_ROUNDED,
|
||||
},
|
||||
.location = location,
|
||||
},
|
||||
.text_color = text_color,
|
||||
};
|
||||
strncpy(non_local_clock.buffer, text, sizeof(non_local_clock.buffer));
|
||||
return non_local_clock;
|
||||
}
|
||||
|
||||
// Configure the text displayed on the clock.
|
||||
static ClockText prv_configure_clock_text(ClockTextType type, ClockTextLocation location,
|
||||
GColor color, struct tm *tick_time) {
|
||||
ClockText text = (ClockText) {
|
||||
.location = location,
|
||||
.color = color,
|
||||
};
|
||||
|
||||
if (type == CLOCK_TEXT_TYPE_DATE) {
|
||||
strftime(text.buffer, sizeof(text.buffer), "%a %d", tick_time);
|
||||
} else if (type == CLOCK_TEXT_TYPE_TIME) {
|
||||
strftime(text.buffer, sizeof(text.buffer), "$l:%M%P", tick_time);
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < sizeof(text.buffer); i++) {
|
||||
text.buffer[i] = toupper((unsigned char)text.buffer[i]);
|
||||
}
|
||||
|
||||
// TODO: Don't return a struct
|
||||
return text;
|
||||
}
|
||||
|
||||
static ClockModel prv_clock_model_default(struct tm *tick_time) {
|
||||
// Create a generic model and configure a default clock.
|
||||
ClockModel model;
|
||||
model.local_clock = prv_local_clock_face_default(tick_time);
|
||||
|
||||
// Add watch-specific details.
|
||||
const WatchInfoColor watch_color = sys_watch_info_get_color();
|
||||
switch (watch_color) {
|
||||
case WATCH_INFO_COLOR_TIME_ROUND_BLACK_14:
|
||||
model.local_clock.minute_hand.color = GColorBlue;
|
||||
model.text = prv_configure_clock_text(CLOCK_TEXT_TYPE_DATE, CLOCK_TEXT_LOCATION_LEFT,
|
||||
GColorWhite, tick_time);
|
||||
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_14MM_BLACK_RED;
|
||||
break;
|
||||
case WATCH_INFO_COLOR_TIME_ROUND_BLACK_20:
|
||||
model.num_non_local_clocks = 2;
|
||||
model.non_local_clock[0] = prv_configure_non_local_clock_face(-7, "LA", GColorDarkGray,
|
||||
GColorWhite,
|
||||
CLOCK_LOCATION_LEFT);
|
||||
model.non_local_clock[1] = prv_configure_non_local_clock_face(2, "PAR", GColorDarkGray,
|
||||
GColorWhite,
|
||||
CLOCK_LOCATION_RIGHT);
|
||||
model.text = prv_configure_clock_text(CLOCK_TEXT_TYPE_DATE, CLOCK_TEXT_LOCATION_BOTTOM,
|
||||
GColorWhite, tick_time);
|
||||
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_20MM_BLACK;
|
||||
break;
|
||||
case WATCH_INFO_COLOR_TIME_ROUND_SILVER_14:
|
||||
model.local_clock.hour_hand.style = CLOCK_HAND_STYLE_POINTED;
|
||||
model.local_clock.hour_hand.color = GColorBlack;
|
||||
model.local_clock.minute_hand.style = CLOCK_HAND_STYLE_POINTED;
|
||||
model.local_clock.minute_hand.color = GColorCadetBlue;
|
||||
model.text = prv_configure_clock_text(CLOCK_TEXT_TYPE_DATE, CLOCK_TEXT_LOCATION_BOTTOM,
|
||||
GColorDarkGray, tick_time);
|
||||
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_14MM_SILVER;
|
||||
break;
|
||||
case WATCH_INFO_COLOR_TIME_ROUND_SILVER_20:
|
||||
model.local_clock.hour_hand.style = CLOCK_HAND_STYLE_POINTED;
|
||||
model.local_clock.minute_hand.style = CLOCK_HAND_STYLE_POINTED;
|
||||
model.local_clock.minute_hand.color = GColorRed;
|
||||
model.local_clock.bob_color = GColorBlack;
|
||||
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_20MM_SILVER_BROWN;
|
||||
break;
|
||||
case WATCH_INFO_COLOR_TIME_ROUND_ROSE_GOLD_14:
|
||||
default:
|
||||
model.local_clock.bob_center_color = GColorOrange;
|
||||
model.local_clock.minute_hand.color = GColorWhite;
|
||||
model.local_clock.minute_hand.thickness = 2;
|
||||
model.local_clock.minute_hand.length = 54;
|
||||
model.local_clock.hour_hand.color = GColorBlack;
|
||||
model.local_clock.hour_hand.thickness = 8;
|
||||
model.local_clock.hour_hand.length = 39;
|
||||
model.local_clock.bob_radius = 7;
|
||||
model.local_clock.bob_center_radius = 3;
|
||||
model.local_clock.bob_color = GColorWhite;
|
||||
model.bg_bitmap_id = RESOURCE_ID_MULTIWATCH_BACKGROUND_14MM_ROSE_GOLD;
|
||||
break;
|
||||
}
|
||||
|
||||
// disable timezones until they can be configured by the user
|
||||
model.num_non_local_clocks = 0;
|
||||
|
||||
// TODO: Don't return a struct here
|
||||
return model;
|
||||
}
|
||||
|
||||
static void prv_handle_time_update(struct tm *tick_time, TimeUnits units_changed) {
|
||||
ClockModel model = prv_clock_model_default(tick_time);
|
||||
watch_model_handle_change(&model);
|
||||
}
|
||||
|
||||
void watch_model_cleanup() {
|
||||
tick_timer_service_unsubscribe();
|
||||
}
|
||||
|
||||
static void prv_intro_animation_finished(Animation *animation) {
|
||||
const time_t t = rtc_get_time();
|
||||
prv_handle_time_update(pbl_override_localtime(&t), (TimeUnits)0xff);
|
||||
|
||||
tick_timer_service_subscribe(MINUTE_UNIT, prv_handle_time_update);
|
||||
}
|
||||
|
||||
void watch_model_start_intro() {
|
||||
prv_intro_animation_finished(NULL);
|
||||
}
|
||||
|
||||
void watch_model_init(void) {
|
||||
const time_t t = rtc_get_time();
|
||||
struct tm *tick_time = pbl_override_localtime(&t);
|
||||
ClockModel model = prv_clock_model_default(tick_time);
|
||||
watch_model_handle_change(&model);
|
||||
}
|
||||
119
src/fw/apps/watch/tictoc/spalding/watch_model.h
Normal file
119
src/fw/apps/watch/tictoc/spalding/watch_model.h
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/graphics/gtypes.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#define LOCAL_HOUR_HAND_LENGTH_DEFAULT 51
|
||||
#define LOCAL_HOUR_HAND_THICKNESS_DEFAULT 6
|
||||
#define LOCAL_HOUR_HAND_COLOR_DEFAULT GColorWhite
|
||||
#define LOCAL_HOUR_HAND_BACK_EXT_DEFAULT 0
|
||||
|
||||
#define LOCAL_MINUTE_HAND_LENGTH_DEFAULT 58
|
||||
#define LOCAL_MINUTE_HAND_THICKNESS_DEFAULT 6
|
||||
#define LOCAL_MINUTE_HAND_COLOR_DEFAULT GColorWhite
|
||||
#define LOCAL_MINUTE_HAND_BACK_EXT_DEFAULT 0
|
||||
|
||||
#define LOCAL_BOB_RADIUS_DEFAULT 6
|
||||
#define LOCAL_BOB_COLOR_DEFAULT GColorRed
|
||||
|
||||
#define NON_LOCAL_HOUR_HAND_LENGTH_DEFAULT 11
|
||||
#define NON_LOCAL_HOUR_HAND_WIDTH_DEFAULT 3
|
||||
|
||||
#define NON_LOCAL_MINUTE_HAND_LENGTH_DEFAULT 21
|
||||
#define NON_LOCAL_MINUTE_HAND_WIDTH_DEFAULT 3
|
||||
|
||||
#define NUM_NON_LOCAL_CLOCKS 3
|
||||
|
||||
#define GLANCE_TIME_OUT_MS 8000
|
||||
|
||||
typedef enum {
|
||||
CLOCK_TEXT_TYPE_NONE = 0,
|
||||
CLOCK_TEXT_TYPE_TIME,
|
||||
CLOCK_TEXT_TYPE_DATE,
|
||||
} ClockTextType;
|
||||
|
||||
typedef enum {
|
||||
CLOCK_TEXT_LOCATION_NONE = 0,
|
||||
CLOCK_TEXT_LOCATION_BOTTOM,
|
||||
CLOCK_TEXT_LOCATION_LEFT,
|
||||
} ClockTextLocation;
|
||||
|
||||
typedef enum {
|
||||
CLOCK_HAND_STYLE_ROUNDED = 0,
|
||||
CLOCK_HAND_STYLE_ROUNDED_WITH_HIGHLIGHT,
|
||||
CLOCK_HAND_STYLE_POINTED,
|
||||
} ClockHandStyle;
|
||||
|
||||
typedef enum {
|
||||
CLOCK_LOCATION_CENTER,
|
||||
CLOCK_LOCATION_LEFT,
|
||||
CLOCK_LOCATION_BOTTOM,
|
||||
CLOCK_LOCATION_RIGHT,
|
||||
CLOCK_LOCATION_TOP,
|
||||
} ClockLocation;
|
||||
|
||||
typedef struct {
|
||||
uint16_t length;
|
||||
uint16_t thickness;
|
||||
uint16_t backwards_extension;
|
||||
int32_t angle;
|
||||
GColor color;
|
||||
ClockHandStyle style;
|
||||
} ClockHand;
|
||||
|
||||
typedef struct {
|
||||
ClockHand hour_hand;
|
||||
ClockHand minute_hand;
|
||||
uint16_t bob_radius;
|
||||
uint16_t bob_center_radius;
|
||||
GColor bob_color;
|
||||
GColor bob_center_color;
|
||||
ClockLocation location;
|
||||
} ClockFace;
|
||||
|
||||
typedef struct {
|
||||
ClockFace face;
|
||||
char buffer[4];
|
||||
int32_t utc_offest;
|
||||
GColor text_color;
|
||||
} NonLocalClockFace;
|
||||
|
||||
typedef struct {
|
||||
ClockTextType type;
|
||||
ClockTextLocation location;
|
||||
char buffer[10]; // FIXME magic number
|
||||
GColor color;
|
||||
} ClockText;
|
||||
|
||||
typedef struct {
|
||||
ClockFace local_clock;
|
||||
uint32_t num_non_local_clocks;
|
||||
NonLocalClockFace non_local_clock[NUM_NON_LOCAL_CLOCKS];
|
||||
ClockText text;
|
||||
uint32_t bg_bitmap_id;
|
||||
} ClockModel;
|
||||
|
||||
void watch_model_init(void);
|
||||
|
||||
void watch_model_handle_change(ClockModel *model);
|
||||
|
||||
void watch_model_start_intro(void);
|
||||
|
||||
void watch_model_cleanup(void);
|
||||
37
src/fw/apps/watch/tictoc/tictoc.c
Normal file
37
src/fw/apps/watch/tictoc/tictoc.c
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "tictoc.h"
|
||||
|
||||
#include "resource/resource_ids.auto.h"
|
||||
|
||||
const PebbleProcessMd* tictoc_get_app_info(void) {
|
||||
static const PebbleProcessMdSystem s_app_md = {
|
||||
.common = {
|
||||
// UUID: 8f3c8686-31a1-4f5f-91f5-01600c9bdc59
|
||||
.uuid = { 0x8f, 0x3c, 0x86, 0x86, 0x31, 0xa1, 0x4f, 0x5f,
|
||||
0x91, 0xf5, 0x01, 0x60, 0x0c, 0x9b, 0xdc, 0x59 },
|
||||
.main_func = tictoc_main,
|
||||
.process_type = ProcessTypeWatchface,
|
||||
#if CAPABILITY_HAS_JAVASCRIPT && !defined(PLATFORM_SPALDING)
|
||||
.is_rocky_app = true,
|
||||
#endif
|
||||
},
|
||||
.icon_resource_id = RESOURCE_ID_MENU_ICON_TICTOC_WATCH,
|
||||
.name = "TicToc",
|
||||
};
|
||||
return (const PebbleProcessMd*) &s_app_md;
|
||||
}
|
||||
23
src/fw/apps/watch/tictoc/tictoc.h
Normal file
23
src/fw/apps/watch/tictoc/tictoc.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
void tictoc_main(void);
|
||||
|
||||
const PebbleProcessMd* tictoc_get_app_info(void);
|
||||
Reference in New Issue
Block a user