classic-lite

Minimalist rewrite of Łukasz Zalewski's "classic" pebble watchface
git clone https://git.instinctive.eu/classic-lite.git
Log | Files | Refs | README | LICENSE

classic-lite.c (26775B)


      1 /*
      2  * Copyright (c) 2015, Natacha Porté
      3  * Face design by Łukasz Zalewski
      4  *
      5  * Permission to use, copy, modify, and distribute this software for any
      6  * purpose with or without fee is hereby granted, provided that the above
      7  * copyright notice and this permission notice appear in all copies.
      8  *
      9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     16  */
     17 
     18 #include <pebble.h>
     19 
     20 #define CACHE_BACKGROUND
     21 
     22 /**********************
     23  * CONFIGURABLE STATE *
     24  **********************/
     25 
     26 static GColor background_color;
     27 static GColor battery_color;
     28 static GColor battery_color2;
     29 static GColor bluetooth_color;
     30 static GColor hour_hand_color;
     31 static GColor minute_hand_color;
     32 static GColor pin_color;
     33 static GColor hour_mark_color;
     34 static GColor inner_rectangle_color;
     35 static GColor minute_mark_color;
     36 static GColor text_color;
     37 static unsigned text_font = 0;
     38 static char text_format[32] = "%a %d";
     39 static bool bluetooth_vibration = true;
     40 static uint8_t show_battery_icon_below = 100;
     41 #define PERSIST_BUFFER_SIZE 47
     42 #define TEXT_FONT_NUMBER 4
     43 
     44 static const char *const text_fonts[] = {
     45 	FONT_KEY_GOTHIC_14,
     46 	FONT_KEY_GOTHIC_18,
     47 	FONT_KEY_GOTHIC_24,
     48 	FONT_KEY_GOTHIC_28,
     49 };
     50 
     51 static const uint16_t text_offsets[] = {
     52 	36 + PBL_IF_RECT_ELSE(5, 15),
     53 	36 + PBL_IF_RECT_ELSE(2, 12),
     54 	36 + PBL_IF_RECT_ELSE(-1, 8),
     55 	36 + PBL_IF_RECT_ELSE(-2, 7),
     56 };
     57 
     58 static const uint16_t text_heights[] = {
     59 	17,
     60 	22,
     61 	29,
     62 	33,
     63 };
     64 
     65 #ifdef PBL_SDK_3
     66 #define IS_VISIBLE(color) ((color).argb != background_color.argb)
     67 #define IS_EQUAL(color1, color2) ((color1).argb == (color2).argb)
     68 #define READ_COLOR(color, byte) do { (color).argb = (byte); } while (0)
     69 #define SAVE_COLOR(byte, color) do { (byte) = (color).argb; } while (0)
     70 #elif PBL_SDK_2
     71 #define IS_VISIBLE(color) ((color) != background_color)
     72 #define IS_EQUAL(color1, color2) ((color1) == (color2))
     73 #define READ_COLOR(color, byte) do { (color) = (byte); } while (0)
     74 #define SAVE_COLOR(byte, color) do { (byte) = (color); } while (0)
     75 #endif
     76 
     77 static void
     78 read_config(void) {
     79 	uint8_t buffer[PERSIST_BUFFER_SIZE + 1];
     80 	int i;
     81 
     82 	i = persist_read_data(1, buffer, sizeof buffer);
     83 
     84 	if (i == E_DOES_NOT_EXIST) return;
     85 
     86 	if (i < 1) {
     87 		APP_LOG(APP_LOG_LEVEL_ERROR,
     88 		    "invalid persist buffer size %d", i);
     89 		return;
     90 	}
     91 
     92 	if (buffer[0] < 1) {
     93 		APP_LOG(APP_LOG_LEVEL_ERROR,
     94 		    "invalid configuration version %u", (unsigned)(buffer[0]));
     95 		return;
     96 	}
     97 
     98 	if (buffer[0] > 3) {
     99 		APP_LOG(APP_LOG_LEVEL_WARNING,
    100 		    "loading data from future version %u, "
    101 		    "data will be lost on the next write",
    102 		    (unsigned)(buffer[0]));
    103 		return;
    104 	}
    105 
    106 	if (i < 43) {
    107 		APP_LOG(APP_LOG_LEVEL_ERROR,
    108 		    "truncated persistent buffer size at %d, aborting", i);
    109 		return;
    110 	}
    111 
    112 	READ_COLOR(background_color,      buffer[1]);
    113 	READ_COLOR(battery_color,         buffer[2]);
    114 	READ_COLOR(bluetooth_color,       buffer[3]);
    115 	READ_COLOR(hour_hand_color,       buffer[4]);
    116 	READ_COLOR(hour_mark_color,       buffer[5]);
    117 	READ_COLOR(inner_rectangle_color, buffer[6]);
    118 	READ_COLOR(minute_mark_color,     buffer[7]);
    119 	READ_COLOR(text_color,            buffer[8]);
    120 
    121 	bluetooth_vibration = (buffer[9] != 0);
    122 	show_battery_icon_below = buffer[10];
    123 
    124 	memcpy(text_format, buffer + 11, sizeof text_format);
    125 
    126 	battery_color2 = battery_color;
    127 	pin_color = minute_hand_color = hour_hand_color;
    128 
    129 	if (buffer[0] < 2) return;
    130 
    131 	if (i < 45) {
    132 		APP_LOG(APP_LOG_LEVEL_ERROR,
    133 		    "truncated persistent buffer (size %d), using only v1",
    134 		    i);
    135 		return;
    136 	}
    137 
    138 	READ_COLOR(battery_color2, buffer[43]);
    139 	text_font = buffer[44];
    140 
    141 	if (buffer[0] < 3) return;
    142 
    143 	if (i < 47) {
    144 		APP_LOG(APP_LOG_LEVEL_ERROR,
    145 		    "truncated persistent buffer (size %d), using only v2",
    146 		    i);
    147 		return;
    148 	}
    149 
    150 	READ_COLOR(minute_hand_color, buffer[45]);
    151 	READ_COLOR(pin_color, buffer[46]);
    152 }
    153 
    154 static void
    155 write_config(void) {
    156 	uint8_t buffer[PERSIST_BUFFER_SIZE];
    157 	int i;
    158 
    159 	buffer[0] = 3;
    160 	SAVE_COLOR(buffer[1], background_color);
    161 	SAVE_COLOR(buffer[2], battery_color);
    162 	SAVE_COLOR(buffer[3], bluetooth_color);
    163 	SAVE_COLOR(buffer[4], hour_hand_color);
    164 	SAVE_COLOR(buffer[45], minute_hand_color);
    165 	SAVE_COLOR(buffer[46], pin_color);
    166 	SAVE_COLOR(buffer[5], hour_mark_color);
    167 	SAVE_COLOR(buffer[6], inner_rectangle_color);
    168 	SAVE_COLOR(buffer[7], minute_mark_color);
    169 	SAVE_COLOR(buffer[8], text_color);
    170 
    171 	buffer[9] = bluetooth_vibration ? 1 : 0;
    172 	buffer[10] = show_battery_icon_below;
    173 
    174 	memcpy(buffer + 11, text_format, sizeof text_format);
    175 
    176 	SAVE_COLOR(buffer[43], battery_color2);
    177 	buffer[44] = text_font;
    178 
    179 	i = persist_write_data(1, buffer, sizeof buffer);
    180 
    181 	if (i < 0 || (size_t)i != sizeof buffer) {
    182 		APP_LOG(APP_LOG_LEVEL_ERROR,
    183 		    "error while writing to persistent storage (%d)", i);
    184 	}
    185 }
    186 
    187 static GColor
    188 color_from_tuple(Tuple *tuple) {
    189 	uint32_t value = 0;
    190 
    191 	if (!tuple) return GColorClear;
    192 
    193 	switch (tuple->type) {
    194 	    case TUPLE_UINT:
    195 		switch (tuple->length) {
    196 		    case 1:
    197 			value = tuple->value->uint8;
    198 			break;
    199 		    case 2:
    200 			value = tuple->value->uint16;
    201 			break;
    202 		    case 4:
    203 			value = tuple->value->uint32;
    204 			break;
    205 		    default:
    206 			APP_LOG(APP_LOG_LEVEL_ERROR,
    207 			    "bad uint length %u in color",
    208 			    (unsigned)tuple->length);
    209 			return GColorClear;
    210 		}
    211 		break;
    212 
    213 	    case TUPLE_INT:
    214 		switch (tuple->length) {
    215 		    case 1:
    216 			if (tuple->value->int8 >= 0)
    217 				value = tuple->value->int8;
    218 			break;
    219 		    case 2:
    220 			if (tuple->value->int16 >= 0)
    221 				value = tuple->value->int16;
    222 			break;
    223 		    case 4:
    224 			if (tuple->value->int32 >= 0)
    225 				value = tuple->value->int32;
    226 			break;
    227 		    default:
    228 			APP_LOG(APP_LOG_LEVEL_ERROR,
    229 			    "bad int length %u in color",
    230 			    (unsigned)tuple->length);
    231 			return GColorClear;
    232 		}
    233 		break;
    234 
    235 	    default:
    236 		APP_LOG(APP_LOG_LEVEL_ERROR,
    237 		    "bad type %d for color",
    238 		    (int)tuple->type);
    239 		return GColorClear;
    240 	}
    241 
    242 #ifdef PBL_SDK_3
    243 	return GColorFromHEX(value);
    244 #elif PBL_SDK_2
    245 	return (value & 0x808080) ? GColorWhite : GColorBlack;
    246 #endif
    247 }
    248 
    249 /*****************
    250  * HELPER MACROS *
    251  *****************/
    252 
    253 #define QUAD_PATH_POINTS(up, left, down, right) {\
    254 	4, \
    255 	(GPoint []) { \
    256 		{ 0, (up) }, \
    257 		{ (left), 0 }, \
    258 		{ 0, (down) }, \
    259 		{ (right), 0 } } }
    260 
    261 #ifdef PBL_SDK_3
    262 #define INSET_RECT(output, input, amount) do { \
    263 	(output) = grect_inset((input), GEdgeInsets((amount))); \
    264 	} while(0)
    265 #else
    266 #define INSET_RECT(output, input, amount) do { \
    267 	(output).origin.x = (input).origin.x + (amount); \
    268 	(output).origin.y = (input).origin.y + (amount); \
    269 	(output).size.w = (input).size.w - 2 * (amount); \
    270 	(output).size.h = (input).size.h - 2 * (amount); \
    271 	} while(0)
    272 #endif
    273 
    274 /**********************
    275  * DISPLAY PRIMITIVES *
    276  **********************/
    277 
    278 #ifdef PBL_RECT
    279 static const GPathInfo minute_hand_path_points
    280 	= QUAD_PATH_POINTS(15, 6, -72, -6);
    281 static const GPathInfo hour_hand_path_points
    282 	= QUAD_PATH_POINTS(15, 7, -50, -7);
    283 #else
    284 static const GPathInfo minute_hand_path_points
    285 	= QUAD_PATH_POINTS(17, 7, -83, -7);
    286 static const GPathInfo hour_hand_path_points
    287 	= QUAD_PATH_POINTS(17, 8, -58, -8);
    288 #endif
    289 static const GPathInfo bluetooth_logo_points = { 7, (GPoint[]) {
    290 	{ -3, -3 },
    291 	{  3,  3 },
    292 	{  0,  6 },
    293 	{  0, -6 },
    294 	{  3, -3 },
    295 	{ -3,  3 },
    296 	{  0,  0 } } };
    297 static const GPathInfo bluetooth_frame_points = { 3, (GPoint[]) {
    298 	{ -13,   9 },
    299 	{   0, -14 },
    300 	{  13,   9 } } };
    301 
    302 static struct tm tm_now;
    303 static bool bluetooth_connected = 0;
    304 static Window *window;
    305 static Layer *background_layer;
    306 static Layer *hand_layer;
    307 static Layer *icon_layer;
    308 static TextLayer *text_layer;
    309 static GPath *bluetooth_frame;
    310 static GPath *bluetooth_logo;
    311 static GPath *hour_hand_path;
    312 static GPath *minute_hand_path;
    313 static GPoint center;
    314 static char text_buffer[64];
    315 static uint8_t current_battery = 100;
    316 #define has_battery (current_battery > show_battery_icon_below)
    317 #define ICON_LAYER_SET_HIDDEN  do { \
    318 	layer_set_hidden(icon_layer, \
    319 	    (bluetooth_connected || !IS_VISIBLE(bluetooth_color)) \
    320 	    && (has_battery \
    321 	     || !(IS_VISIBLE(battery_color) || IS_VISIBLE(battery_color2)))); \
    322 	} while (0)
    323 
    324 #ifdef CACHE_BACKGROUND
    325 #define SCREEN_BUFFER_SIZE \
    326     PBL_IF_RECT_ELSE(PBL_IF_COLOR_ELSE(144 * 168, 144 * 168 / 8), 25868)
    327 
    328 static uint8_t background_cache[SCREEN_BUFFER_SIZE];
    329 static bool use_background_cache = false;
    330 
    331 static size_t
    332 gbitmap_get_data_size(GBitmap *bitmap) {
    333 	GRect bounds;
    334 	if (!bitmap) return 0;
    335 
    336 	bounds = gbitmap_get_bounds(bitmap);
    337 #ifdef PBL_SDK_3
    338 	GBitmapDataRowInfo row_info;
    339 	switch (gbitmap_get_format(bitmap)) {
    340 	    case GBitmapFormat1Bit:
    341 		return bounds.size.w * bounds.size.h / 8;
    342 	    case GBitmapFormat8Bit:
    343 		return bounds.size.w * bounds.size.h;
    344 	    case GBitmapFormat8BitCircular:
    345 		row_info = gbitmap_get_data_row_info(bitmap, bounds.size.h - 1);
    346 		return (row_info.data + row_info.max_x + 1)
    347 		    - gbitmap_get_data(bitmap);
    348 	    default:
    349 		return 0;
    350 	}
    351 #else
    352 	return bounds.size.w * bounds.size.h / 8;
    353 #endif
    354 }
    355 
    356 static bool
    357 save_frame_buffer(GContext *ctx) {
    358 	GBitmap *frame_buffer = graphics_capture_frame_buffer(ctx);
    359 
    360 	if (!frame_buffer) {
    361 		APP_LOG(APP_LOG_LEVEL_WARNING,
    362 		    "Unable to capture frame buffer for saving");
    363 		return false;
    364 	}
    365 
    366 	if (gbitmap_get_data_size(frame_buffer) != SCREEN_BUFFER_SIZE) {
    367 		APP_LOG(APP_LOG_LEVEL_WARNING,
    368 		    "Unexpected frame buffer size %u, expected %u",
    369 		    gbitmap_get_data_size(frame_buffer), SCREEN_BUFFER_SIZE);
    370 		graphics_release_frame_buffer(ctx, frame_buffer);
    371 		return false;
    372 	}
    373 
    374 	memcpy(background_cache, gbitmap_get_data(frame_buffer),
    375 	    sizeof background_cache);
    376 	use_background_cache = true;
    377 	graphics_release_frame_buffer(ctx, frame_buffer);
    378 	return true;
    379 }
    380 
    381 static bool
    382 restore_frame_buffer(GContext *ctx) {
    383 	GBitmap *frame_buffer = graphics_capture_frame_buffer(ctx);
    384 
    385 	if (!frame_buffer) {
    386 		APP_LOG(APP_LOG_LEVEL_WARNING,
    387 		    "Unable to capture frame buffer for restore");
    388 		return false;
    389 	}
    390 
    391 	if (gbitmap_get_data_size(frame_buffer) != SCREEN_BUFFER_SIZE) {
    392 		APP_LOG(APP_LOG_LEVEL_WARNING,
    393 		    "Unexpected frame buffer size %u, expected %u",
    394 		    gbitmap_get_data_size(frame_buffer), SCREEN_BUFFER_SIZE);
    395 		graphics_release_frame_buffer(ctx, frame_buffer);
    396 		return false;
    397 	}
    398 
    399 	memcpy(gbitmap_get_data(frame_buffer), background_cache,
    400 	    sizeof background_cache);
    401 	graphics_release_frame_buffer(ctx, frame_buffer);
    402 	return true;
    403 }
    404 #endif
    405 
    406 #ifdef PBL_RECT
    407 static void
    408 point_at_angle(GRect *rect, int32_t angle, GPoint *output, int *horizontal) {
    409 	int32_t sin_value = sin_lookup(angle);
    410 	int32_t cos_value = cos_lookup(angle);
    411 	int32_t abs_sin = abs(sin_value);
    412 	int32_t abs_cos = abs(cos_value);
    413 	int32_t width = rect->size.w - 1;
    414 	int32_t height = rect->size.h - 1;
    415 
    416 	*horizontal = (height * abs_sin < width * abs_cos);
    417 
    418 	if (*horizontal) {
    419 		output->x = (height * sin_value / abs_cos + width) / 2;
    420 		output->y = (cos_value > 0) ? 0 : height;
    421 	}
    422 	else {
    423 		output->x = (sin_value > 0) ? width : 0;
    424 		output->y = (height - width * cos_value / abs_sin) / 2;
    425 	}
    426 	output->x += rect->origin.x;
    427 	output->y += rect->origin.y;
    428 }
    429 
    430 static void
    431 background_layer_draw(Layer *layer, GContext *ctx) {
    432 	int horiz, i;
    433 	GRect bounds = layer_get_bounds(layer);
    434 	GRect rect, rect2;
    435 	GPoint pt1, pt2;
    436 
    437 	(void)layer;
    438 
    439 #ifdef CACHE_BACKGROUND
    440 	if (use_background_cache && restore_frame_buffer(ctx)) return;
    441 #endif
    442 
    443 	if (IS_VISIBLE(minute_mark_color)) {
    444 		graphics_context_set_stroke_color(ctx, minute_mark_color);
    445 		INSET_RECT(rect, bounds, 5);
    446 		for (i = 0; i < 60; i += 1) {
    447 			point_at_angle(&rect, TRIG_MAX_ANGLE * i / 60,
    448 			    &pt1, &horiz);
    449 			pt2.x = pt1.x + horiz;
    450 			pt2.y = pt1.y + (1 - horiz);
    451 			graphics_draw_line(ctx, pt1, pt2);
    452 		}
    453 	}
    454 
    455 	if (IS_VISIBLE(hour_mark_color)) {
    456 #ifndef PBL_BW
    457 		graphics_context_set_stroke_width(ctx, 3);
    458 #endif
    459 
    460 		graphics_context_set_stroke_color(ctx, hour_mark_color);
    461 		INSET_RECT(rect,  bounds, 11);
    462 		INSET_RECT(rect2, bounds, 22);
    463 		for (i = 0; i < 12; i += 1) {
    464 			point_at_angle(&rect, TRIG_MAX_ANGLE * i / 12,
    465 			    &pt1, &horiz);
    466 			point_at_angle(&rect2, TRIG_MAX_ANGLE * i / 12,
    467 			    &pt2, &horiz);
    468 
    469 			graphics_draw_line(ctx, pt1, pt2);
    470 
    471 #ifdef PBL_BW
    472 			pt1.x += horiz;        pt2.x += horiz;
    473 			pt1.y += 1 - horiz;    pt2.y += 1 - horiz;
    474 			graphics_draw_line(ctx, pt1, pt2);
    475 
    476 			pt1.x -= 2 * horiz;        pt2.x -= 2 * horiz;
    477 			pt1.y -= 2 * (1 - horiz);  pt2.y -= 2 * (1 - horiz);
    478 			graphics_draw_line(ctx, pt1, pt2);
    479 #endif
    480 		}
    481 
    482 #ifndef PBL_BW
    483 		graphics_context_set_stroke_width(ctx, 1);
    484 #endif
    485 	}
    486 
    487 	if (IS_VISIBLE(inner_rectangle_color)) {
    488 		INSET_RECT(rect, bounds, 35);
    489 		graphics_context_set_stroke_color(ctx, inner_rectangle_color);
    490 #ifdef PBL_BW
    491 		pt1.y = rect.origin.y;
    492 		pt2.y = rect.origin.y + rect.size.h - 1;
    493 		for (i = rect.origin.x +2; i < rect.origin.x + rect.size.w; i += 3) {
    494 			pt1.x = pt2.x = i;
    495 			graphics_draw_pixel(ctx, pt1);
    496 			graphics_draw_pixel(ctx, pt2);
    497 		}
    498 
    499 		pt1.x = rect.origin.x;
    500 		pt2.x = rect.origin.x + rect.size.w - 1;
    501 		for (i = rect.origin.y +2;
    502 		    i < rect.origin.y + rect.size.h;
    503 		    i += 3) {
    504 			pt1.y = pt2.y = i;
    505 			graphics_draw_pixel(ctx, pt1);
    506 			graphics_draw_pixel(ctx, pt2);
    507 		}
    508 #else
    509 		graphics_draw_rect(ctx, rect);
    510 #endif
    511 	}
    512 
    513 #ifdef CACHE_BACKGROUND
    514 	save_frame_buffer(ctx);
    515 #endif
    516 }
    517 #else
    518 static GPoint
    519 point_at_angle(GRect bounds, int32_t angle) {
    520 	GPoint ret;
    521 	uint32_t width = bounds.size.w;
    522 	uint32_t height = bounds.size.h;
    523 	ret.x = bounds.origin.x
    524 	    + (TRIG_MAX_RATIO * (width + 1) + width * sin_lookup(angle))
    525 	    / (2 * TRIG_MAX_RATIO);
    526 	ret.y = bounds.origin.y
    527 	    + (TRIG_MAX_RATIO * (height + 1) - height * cos_lookup(angle))
    528 	    / (2 * TRIG_MAX_RATIO);
    529 	return ret;
    530 }
    531 
    532 static void
    533 background_layer_draw(Layer *layer, GContext *ctx) {
    534 	const GRect bounds = layer_get_bounds(layer);
    535 	const GPoint center = grect_center_point(&bounds);
    536 	const int32_t radius = (bounds.size.w + bounds.size.h) / 4;
    537 	const int32_t angle_delta = TRIG_MAX_ANGLE / (6 * radius);
    538 	int32_t angle, x, y;
    539 	GRect rect;
    540 	GPoint pt1, pt2;
    541 	int i;
    542 
    543 	(void)layer;
    544 
    545 #ifdef CACHE_BACKGROUND
    546 	if (use_background_cache && restore_frame_buffer(ctx)) return;
    547 #endif
    548 
    549 	if (IS_VISIBLE(minute_mark_color)) {
    550 		graphics_context_set_stroke_color(ctx, minute_mark_color);
    551 		INSET_RECT(rect, bounds, 5);
    552 		for (i = 0; i < 60; i += 1) {
    553 			angle = TRIG_MAX_ANGLE * i / 60;
    554 			graphics_draw_line(ctx,
    555 			    point_at_angle(rect, angle - angle_delta),
    556 			    point_at_angle(rect, angle + angle_delta));
    557 		}
    558 	}
    559 
    560 	if (IS_VISIBLE(hour_mark_color)) {
    561 		graphics_context_set_stroke_width(ctx, 3);
    562 		graphics_context_set_stroke_color(ctx, hour_mark_color);
    563 		for (i = 0; i < 12; i += 1) {
    564 			angle = TRIG_MAX_ANGLE * i / 12;
    565 			x = sin_lookup(angle);
    566 			y = -cos_lookup(angle);
    567 			pt1.x = center.x + (radius - 11) * x / TRIG_MAX_RATIO;
    568 			pt1.y = center.y + (radius - 11) * y / TRIG_MAX_RATIO;
    569 			pt2.x = center.x + (radius - 22) * x / TRIG_MAX_RATIO;
    570 			pt2.y = center.y + (radius - 22) * y / TRIG_MAX_RATIO;
    571 			graphics_draw_line(ctx, pt1, pt2);
    572 		}
    573 	}
    574 
    575 	if (IS_VISIBLE(inner_rectangle_color)) {
    576 		graphics_context_set_stroke_width(ctx, 1);
    577 		graphics_context_set_stroke_color(ctx, inner_rectangle_color);
    578 		graphics_draw_circle(ctx, center, radius - 35);
    579 	}
    580 
    581 #ifdef CACHE_BACKGROUND
    582 	save_frame_buffer(ctx);
    583 #endif
    584 }
    585 #endif
    586 
    587 static void
    588 hand_layer_draw(Layer *layer, GContext *ctx) {
    589 	(void)layer;
    590 
    591 	graphics_context_set_fill_color(ctx, hour_hand_color);
    592 	graphics_context_set_stroke_color(ctx, background_color);
    593 
    594 	gpath_rotate_to(minute_hand_path, TRIG_MAX_ANGLE * tm_now.tm_min / 60);
    595 	gpath_draw_filled(ctx, minute_hand_path);
    596 	gpath_draw_outline(ctx, minute_hand_path);
    597 
    598 	graphics_context_set_fill_color(ctx, minute_hand_color);
    599 
    600 	gpath_rotate_to(hour_hand_path,
    601 	    TRIG_MAX_ANGLE * (tm_now.tm_hour * 60 + tm_now.tm_min) / 720);
    602 	gpath_draw_filled(ctx, hour_hand_path);
    603 	gpath_draw_outline(ctx, hour_hand_path);
    604 
    605 #ifdef CACHE_BACKGROUND
    606 	if (!use_background_cache) return;
    607 #endif
    608 
    609 	graphics_context_set_fill_color(ctx, background_color);
    610 	graphics_fill_circle(ctx, center, 2);
    611 	graphics_context_set_fill_color(ctx, pin_color);
    612 	graphics_fill_circle(ctx, center, 1);
    613 }
    614 
    615 static void
    616 icon_layer_draw(Layer *layer, GContext *ctx) {
    617 	GRect bounds = layer_get_bounds(layer);
    618 	GPoint center = grect_center_point(&bounds);
    619 	GPoint pt;
    620 
    621 	if (!bluetooth_connected && IS_VISIBLE(bluetooth_color)) {
    622 		pt.x = center.x;
    623 		pt.y = center.y + (has_battery ? +1 : -2);
    624 		gpath_move_to(bluetooth_frame, pt);
    625 		gpath_move_to(bluetooth_logo, pt);
    626 		graphics_context_set_stroke_color(ctx, bluetooth_color);
    627 		gpath_draw_outline(ctx, bluetooth_frame);
    628 #ifdef PBL_SDK_3
    629 		gpath_draw_outline_open(ctx, bluetooth_logo);
    630 #else
    631 		gpath_draw_outline(ctx, bluetooth_logo);
    632 #endif
    633 	}
    634 
    635 	if (!has_battery
    636 	    && (IS_VISIBLE(battery_color) || IS_VISIBLE(battery_color2))) {
    637 		pt.x = center.x - 11;
    638 		pt.y = center.y
    639 		    + (bluetooth_connected ? 0 : PBL_IF_RECT_ELSE(9, 11));
    640 		graphics_context_set_fill_color(ctx, battery_color);
    641 		graphics_fill_rect(ctx,
    642 		    GRect(pt.x, pt.y, 22, 7),
    643 		    0, GCornerNone);
    644 		graphics_fill_rect(ctx,
    645 		    GRect(pt.x + 22, pt.y + 2, 2, 3),
    646 		    0, GCornerNone);
    647 		if (!IS_EQUAL(battery_color2, battery_color)) {
    648 			graphics_context_set_fill_color(ctx, battery_color2);
    649 			graphics_fill_rect(ctx,
    650 			    GRect(pt.x + 5, pt.y + 1, 4, 5),
    651 			    0, GCornerNone);
    652 			graphics_fill_rect(ctx,
    653 			    GRect(pt.x + 13, pt.y + 1, 4, 5),
    654 			    0, GCornerNone);
    655 		}
    656 		graphics_context_set_fill_color(ctx, background_color);
    657 		if (current_battery < 100)
    658 			graphics_fill_rect(ctx,
    659 			    GRect(pt.x + 1 + current_battery / 5, pt.y + 1,
    660 			    20 - current_battery / 5, 5),
    661 			    0, GCornerNone);
    662 	}
    663 }
    664 
    665 static void
    666 update_text_layer(struct tm *time) {
    667 	strftime(text_buffer, sizeof text_buffer, text_format, time);
    668 	text_layer_set_text(text_layer, text_buffer);
    669 }
    670 
    671 static void
    672 update_text_font(unsigned new_text_font) {
    673 	Layer *window_layer = window_get_root_layer(window);
    674 	GRect bounds = layer_get_bounds(window_layer);
    675 
    676 	if (new_text_font >= TEXT_FONT_NUMBER) return;
    677 
    678 	if (text_layer) {
    679 		if (new_text_font == text_font) return;
    680 		text_layer_destroy(text_layer);
    681 	}
    682 
    683 	text_font = new_text_font;
    684 
    685 	text_layer = text_layer_create(GRect(
    686 	    bounds.origin.x,
    687 	    bounds.origin.y + text_offsets[text_font],
    688 	    bounds.size.w,
    689 	    text_heights[text_font]));
    690 	text_layer_set_text_color(text_layer, text_color);
    691 	text_layer_set_background_color(text_layer, GColorClear);
    692 	text_layer_set_text_alignment(text_layer, GTextAlignmentCenter);
    693 	text_layer_set_font(text_layer,
    694 	    fonts_get_system_font(text_fonts[text_font]));
    695 	layer_set_hidden(text_layer_get_layer(text_layer),
    696 	    !text_format[0] || !IS_VISIBLE(text_color));
    697 	layer_insert_below_sibling(text_layer_get_layer(text_layer),
    698 	    icon_layer);
    699 }
    700 
    701 /********************
    702  * SERVICE HANDLERS *
    703  ********************/
    704 
    705 static void
    706 battery_handler(BatteryChargeState charge) {
    707 	if (current_battery == charge.charge_percent) return;
    708 	current_battery = charge.charge_percent;
    709 	ICON_LAYER_SET_HIDDEN;
    710 	if (!has_battery) layer_mark_dirty(icon_layer);
    711 }
    712 
    713 static void
    714 bluetooth_handler(bool connected) {
    715 	bluetooth_connected = connected;
    716 	ICON_LAYER_SET_HIDDEN;
    717 	layer_mark_dirty(icon_layer);
    718 
    719 	if (bluetooth_vibration && !connected) vibes_long_pulse();
    720 }
    721 
    722 static void
    723 inbox_received_handler(DictionaryIterator *iterator, void *context) {
    724 	Tuple *tuple;
    725 
    726 	(void)context;
    727 
    728 	for (tuple = dict_read_first(iterator);
    729 	    tuple;
    730 	    tuple = dict_read_next(iterator)) {
    731 		switch (tuple->key) {
    732 		    case 1:
    733 			background_color = color_from_tuple(tuple);
    734 			window_set_background_color(window, background_color);
    735 			break;
    736 		    case 2:
    737 			battery_color = color_from_tuple(tuple);
    738 			layer_mark_dirty(icon_layer);
    739 			break;
    740 		    case 3:
    741 			bluetooth_color = color_from_tuple(tuple);
    742 			layer_mark_dirty(icon_layer);
    743 			break;
    744 		    case 4:
    745 			hour_hand_color = color_from_tuple(tuple);
    746 			pin_color = minute_hand_color = hour_hand_color;
    747 			layer_mark_dirty(hand_layer);
    748 			break;
    749 		    case 5:
    750 			hour_mark_color = color_from_tuple(tuple);
    751 			layer_mark_dirty(background_layer);
    752 			break;
    753 		    case 6:
    754 			inner_rectangle_color = color_from_tuple(tuple);
    755 			layer_mark_dirty(background_layer);
    756 			break;
    757 		    case 7:
    758 			minute_mark_color = color_from_tuple(tuple);
    759 			layer_mark_dirty(background_layer);
    760 			break;
    761 		    case 8:
    762 			text_color = color_from_tuple(tuple);
    763 			text_layer_set_text_color(text_layer, text_color);
    764 			break;
    765 		    case 9:
    766 			battery_color2 = color_from_tuple(tuple);
    767 			layer_mark_dirty(icon_layer);
    768 			break;
    769 		    case 10:
    770 			if (tuple->type != TUPLE_INT
    771 			    && tuple->type != TUPLE_UINT)
    772 				APP_LOG(APP_LOG_LEVEL_ERROR,
    773 				    "bad type %d for text_font entry",
    774 				    (int)tuple->type);
    775 			else if (tuple->value->uint8 == 0
    776 			    || tuple->value->uint8 > TEXT_FONT_NUMBER)
    777 				APP_LOG(APP_LOG_LEVEL_ERROR,
    778 				    "bad value %u for text_font entry",
    779 				    (unsigned)tuple->value->uint8);
    780 			else
    781 				update_text_font(tuple->value->uint8 - 1);
    782 				update_text_layer(&tm_now);
    783 			break;
    784 		    case 11:
    785 			if (tuple->type == TUPLE_CSTRING) {
    786 				strncpy(text_format, tuple->value->cstring,
    787 				   sizeof text_format);
    788 				update_text_layer(&tm_now);
    789 			} else
    790 				APP_LOG(APP_LOG_LEVEL_ERROR,
    791 				    "bad type %d for text_format entry",
    792 				    (int)tuple->type);
    793 			break;
    794 		    case 12:
    795 			if (tuple->type == TUPLE_INT
    796 			    || tuple->type == TUPLE_UINT)
    797 				bluetooth_vibration
    798 				    = tuple->value->data[0] != 0;
    799 			else
    800 				APP_LOG(APP_LOG_LEVEL_ERROR,
    801 				    "bad type %d for bluetooth_vibration entry",
    802 				    (int)tuple->type);
    803 			break;
    804 		    case 13:
    805 			if (tuple->type == TUPLE_INT)
    806 				show_battery_icon_below
    807 				    = (tuple->value->int8 < 0) ? 0
    808 				    : tuple->value->int8;
    809 			else if (tuple->type == TUPLE_UINT)
    810 				show_battery_icon_below = tuple->value->uint8;
    811 			else
    812 				APP_LOG(APP_LOG_LEVEL_ERROR, "bad type %d for "
    813 				    "show_battery_icon_below entry",
    814 				    (int)tuple->type);
    815 			break;
    816 		    case 20:
    817 			hour_hand_color = color_from_tuple(tuple);
    818 			layer_mark_dirty(hand_layer);
    819 			break;
    820 		    case 21:
    821 			minute_hand_color = color_from_tuple(tuple);
    822 			layer_mark_dirty(hand_layer);
    823 			break;
    824 		    case 22:
    825 			pin_color = color_from_tuple(tuple);
    826 			layer_mark_dirty(hand_layer);
    827 			break;
    828 		    default:
    829 			APP_LOG(APP_LOG_LEVEL_ERROR,
    830 			    "unknown configuration key %lu",
    831 			    (unsigned long)tuple->key);
    832 		}
    833 	}
    834 
    835 #ifdef CACHE_BACKGROUND
    836 	use_background_cache = !IS_VISIBLE(inner_rectangle_color)
    837 	    && !IS_VISIBLE(hour_mark_color)
    838 	    && !IS_VISIBLE(minute_mark_color);
    839 	layer_set_hidden(background_layer, use_background_cache);
    840 #else
    841 	layer_set_hidden(background_layer,
    842 	    !IS_VISIBLE(inner_rectangle_color)
    843 	    && !IS_VISIBLE(hour_mark_color)
    844 	    && !IS_VISIBLE(minute_mark_color));
    845 #endif
    846 
    847 	ICON_LAYER_SET_HIDDEN;
    848 
    849 	layer_set_hidden(text_layer_get_layer(text_layer),
    850 	    !text_format[0] || !IS_VISIBLE(text_color));
    851 
    852 	write_config();
    853 }
    854 
    855 static void
    856 tick_handler(struct tm* tick_time, TimeUnits units_changed) {
    857 	if (tm_now.tm_mday != tick_time->tm_mday) update_text_layer(tick_time);
    858 	tm_now = *tick_time;
    859 	layer_mark_dirty(hand_layer);
    860 }
    861 
    862 /***********************************
    863  * INITIALIZATION AND FINALIZATION *
    864  ***********************************/
    865 
    866 static void
    867 window_load(Window *window) {
    868 	Layer *window_layer = window_get_root_layer(window);
    869 	GRect bounds = layer_get_bounds(window_layer);
    870 	window_set_background_color(window, background_color);
    871 
    872 	center = grect_center_point(&bounds);
    873 	gpath_move_to(hour_hand_path, center);
    874 	gpath_move_to(minute_hand_path, center);
    875 
    876 	background_layer = layer_create(bounds);
    877 	layer_set_update_proc(background_layer, &background_layer_draw);
    878 #ifdef CACHE_BACKGROUND
    879 	use_background_cache = !IS_VISIBLE(inner_rectangle_color)
    880 	    && !IS_VISIBLE(hour_mark_color)
    881 	    && !IS_VISIBLE(minute_mark_color);
    882 	layer_set_hidden(background_layer, use_background_cache);
    883 #else
    884 	layer_set_hidden(background_layer,
    885 	    !IS_VISIBLE(inner_rectangle_color)
    886 	    && !IS_VISIBLE(hour_mark_color)
    887 	    && !IS_VISIBLE(minute_mark_color));
    888 #endif
    889 	layer_add_child(window_layer, background_layer);
    890 
    891 	icon_layer = layer_create(GRect(bounds.origin.x
    892 	    + (bounds.size.w - 33) / 2, PBL_IF_RECT_ELSE(97, 105), 33, 36));
    893 	layer_set_update_proc(icon_layer, &icon_layer_draw);
    894 	ICON_LAYER_SET_HIDDEN;
    895 	layer_add_child(window_layer, icon_layer);
    896 
    897 	update_text_font(text_font);
    898 	update_text_layer(&tm_now);
    899 
    900 	hand_layer = layer_create(bounds);
    901 	layer_set_update_proc(hand_layer, &hand_layer_draw);
    902 	layer_add_child(window_layer, hand_layer);
    903 }
    904 
    905 static void
    906 window_unload(Window *window) {
    907 	layer_destroy(background_layer);
    908 	layer_destroy(hand_layer);
    909 	layer_destroy(icon_layer);
    910 	text_layer_destroy(text_layer);
    911 }
    912 
    913 static void
    914 init(void) {
    915 	time_t current_time = time(0);
    916 	tm_now = *localtime(&current_time);
    917 
    918 	background_color = GColorWhite;
    919 	bluetooth_color = GColorBlack;
    920 	hour_hand_color = GColorBlack;
    921 	minute_hand_color = GColorBlack;
    922 	pin_color = GColorBlack;
    923 	hour_mark_color = GColorBlack;
    924 	minute_mark_color = GColorBlack;
    925 	text_color = GColorBlack;
    926 
    927 #ifdef PBL_BW
    928 	battery_color = GColorBlack;
    929 	inner_rectangle_color = GColorBlack;
    930 #else
    931 	battery_color = GColorDarkGray;
    932 	inner_rectangle_color = GColorLightGray;
    933 #endif
    934 	battery_color2 = battery_color;
    935 
    936 	bluetooth_connected = connection_service_peek_pebble_app_connection();
    937 	current_battery = battery_state_service_peek().charge_percent;
    938 	read_config();
    939 
    940 	window = window_create();
    941 	window_set_window_handlers(window, (WindowHandlers) {
    942 		.load = window_load,
    943 		.unload = window_unload,
    944 	});
    945 
    946 	bluetooth_frame = gpath_create(&bluetooth_frame_points);
    947 	bluetooth_logo = gpath_create(&bluetooth_logo_points);
    948 	hour_hand_path = gpath_create(&hour_hand_path_points);
    949 	minute_hand_path = gpath_create(&minute_hand_path_points);
    950 
    951 	battery_state_service_subscribe(&battery_handler);
    952 	connection_service_subscribe(((ConnectionHandlers){
    953 	    .pebble_app_connection_handler = &bluetooth_handler,
    954 	    .pebblekit_connection_handler = 0}));
    955 	tick_timer_service_subscribe(MINUTE_UNIT, &tick_handler);
    956 	window_stack_push(window, true);
    957 
    958 	app_message_register_inbox_received(inbox_received_handler);
    959 	app_message_open(1024, 0);
    960 }
    961 
    962 static void
    963 deinit(void) {
    964 	battery_state_service_unsubscribe();
    965 	connection_service_unsubscribe();
    966 	tick_timer_service_unsubscribe();
    967 	gpath_destroy(bluetooth_frame);
    968 	gpath_destroy(bluetooth_logo);
    969 	gpath_destroy(hour_hand_path);
    970 	gpath_destroy(minute_hand_path);
    971 	window_destroy(window);
    972 }
    973 
    974 int
    975 main(void) {
    976 	init();
    977 	app_event_loop();
    978 	deinit();
    979 }