From 80075f769aba9bdf4e92a474058caeca29aa221f Mon Sep 17 00:00:00 2001 From: Amolith Date: Sat, 15 Aug 2026 16:07:03 -0600 Subject: [PATCH] watch: Make goal colors more vivid Use distinct saturated colors for the five safety ranges while preserving the day label as a non-color signal. Pair each background with an AAA-contrast foreground across the goal list, goal detail, and reminders. Closes: at://did:plc:q3dqwhaox5kb57hcvg4aq4zc/sh.tangled.repo.issue/3mt5qni777j22 --- .../beehave/beeminder-connection.feature.yaml | 2 ++ mise.toml | 2 +- src/c/beeminder_colors.c | 28 +++++++++++++++---- src/c/beeminder_colors.h | 7 ++++- src/c/goal_detail_window.c | 6 ++-- src/c/goal_list_window.c | 5 ++-- src/c/reminder_window.c | 6 ++-- tests/c/beeminder_colors_test.c | 27 ++++++++++++++++++ tests/c/fakes/pebble.h | 12 ++++++++ 9 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 tests/c/beeminder_colors_test.c diff --git a/features/beehave/beeminder-connection.feature.yaml b/features/beehave/beeminder-connection.feature.yaml index 6adce7f..ccae95d 100644 --- a/features/beehave/beeminder-connection.feature.yaml +++ b/features/beehave/beeminder-connection.feature.yaml @@ -77,6 +77,8 @@ components: 1: Beehave lists goals in Beeminder's ascending urgency-key order without a separate Beehave sort preference. 2: Each goal-list entry identifies the goal by slug and shows its safety buffer, a single scannable phrase containing the relative effective deadline followed by the remaining requirement amount, and the current pledge; exact deadline and goal units appear on goal detail rather than the list. 3: Safety remains understandable without relying on color as the only signal. + 3-1: Goal list, detail, and reminder safety indicators use dark red for 0 days, yellow for 1 day, blue for 2 days, light green for 3–6 days, and vivid green for 7 or more days. + 3-2: Text within each safety indicator has at least 7:1 contrast against its background color. 4: Selecting any goal opens a detail view before offering a goal-specific action. 5: Goal detail shows the slug, display title, safety buffer, pledge, a sentence combining the remaining requirement with goal units and its relative effective deadline, and an exact local date and time phrased as the derail time. 5-1: When goal detail includes an automatic-data indicator and a system-status footer, the requirement, deadline, indicator, and footer remain visually distinct without overlap. diff --git a/mise.toml b/mise.toml index f045265..4b8a62f 100644 --- a/mise.toml +++ b/mise.toml @@ -71,7 +71,7 @@ for test_source in tests/c/*_test.c; do fi cc -std=c11 -Wall -Wextra -Werror -pedantic \ -Itests/c/fakes \ - src/c/appmessage_tuple.c src/c/beeminder_requirement.c src/c/crc32.c \ + src/c/appmessage_tuple.c src/c/beeminder_colors.c src/c/beeminder_requirement.c src/c/crc32.c \ src/c/datapoint_request.c src/c/datapoint_value.c \ src/c/goal_serialization.c src/c/goal_store.c \ src/c/persistence_budget.c \ diff --git a/src/c/beeminder_colors.c b/src/c/beeminder_colors.c index 69fb5a6..b3b8698 100644 --- a/src/c/beeminder_colors.c +++ b/src/c/beeminder_colors.c @@ -1,18 +1,34 @@ #include "beeminder_colors.h" -GColor beeminder_safety_color(int32_t safety_days) { +// beeminder-connection.GOAL_BROWSING.3-1 beeminder-connection.GOAL_BROWSING.3-2 +BeeminderSafetyColors beeminder_safety_colors(int32_t safety_days) { if (safety_days == 0) { - return GColorMelon; + return (BeeminderSafetyColors){ + .background = GColorDarkCandyAppleRed, + .foreground = GColorWhite, + }; } if (safety_days == 1) { - return GColorPastelYellow; + return (BeeminderSafetyColors){ + .background = GColorYellow, + .foreground = GColorBlack, + }; } if (safety_days == 2) { - return GColorBabyBlueEyes; + return (BeeminderSafetyColors){ + .background = GColorPictonBlue, + .foreground = GColorBlack, + }; } if (safety_days < 7) { - return GColorMintGreen; + return (BeeminderSafetyColors){ + .background = GColorMediumAquamarine, + .foreground = GColorBlack, + }; } - return GColorMayGreen; + return (BeeminderSafetyColors){ + .background = GColorGreen, + .foreground = GColorBlack, + }; } diff --git a/src/c/beeminder_colors.h b/src/c/beeminder_colors.h index 582becc..86d5856 100644 --- a/src/c/beeminder_colors.h +++ b/src/c/beeminder_colors.h @@ -2,4 +2,9 @@ #include -GColor beeminder_safety_color(int32_t safety_days); +typedef struct { + GColor background; + GColor foreground; +} BeeminderSafetyColors; + +BeeminderSafetyColors beeminder_safety_colors(int32_t safety_days); diff --git a/src/c/goal_detail_window.c b/src/c/goal_detail_window.c index ff86917..3ae1aed 100644 --- a/src/c/goal_detail_window.c +++ b/src/c/goal_detail_window.c @@ -551,6 +551,7 @@ static void detail_draw(Layer *layer, GContext *context) { int16_t exact_deadline_top; int16_t requirement_deadline_height = 0; const int32_t current_timestamp_seconds = beeminder_display_current_timestamp_seconds(); + const BeeminderSafetyColors safety_colors = beeminder_safety_colors(s_detail_goal.safety_days); // beeminder-connection.GOAL_BROWSING.18 runtime-state-sync.CONFIGURATION.7 beeminder_deadline_exact_format(s_detail_goal.effective_deadline_timestamp_seconds, clock_is_24h_style(), @@ -585,16 +586,17 @@ static void detail_draw(Layer *layer, GContext *context) { graphics_context_set_fill_color(context, GColorWhite); graphics_fill_rect(context, bounds, 0, GCornerNone); - graphics_context_set_fill_color(context, beeminder_safety_color(s_detail_goal.safety_days)); + graphics_context_set_fill_color(context, safety_colors.background); graphics_fill_rect(context, GRect(0, 0, bounds.size.w, DETAIL_SAFETY_HEIGHT), 0, GCornerNone); - graphics_context_set_text_color(context, GColorBlack); + graphics_context_set_text_color(context, safety_colors.foreground); graphics_draw_text(context, safety_text, s_detail_meta_font, GRect(DETAIL_TEXT_MARGIN, 5, content_width / 2, DETAIL_SAFETY_HEIGHT - 5), GTextOverflowModeFill, GTextAlignmentLeft, NULL); graphics_draw_text(context, pledge_text, s_detail_meta_font, GRect(bounds.size.w / 2, 5, bounds.size.w / 2 - DETAIL_TEXT_MARGIN, DETAIL_SAFETY_HEIGHT - 5), GTextOverflowModeFill, GTextAlignmentRight, NULL); + graphics_context_set_text_color(context, GColorBlack); graphics_draw_text(context, s_detail_goal.slug, s_detail_heading_font, GRect(DETAIL_TEXT_MARGIN, 42, content_width, 25), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); diff --git a/src/c/goal_list_window.c b/src/c/goal_list_window.c index ed174ee..ed9da74 100644 --- a/src/c/goal_list_window.c +++ b/src/c/goal_list_window.c @@ -70,6 +70,7 @@ static void goal_menu_draw_row_contents(GContext *ctx, const Layer *cell_layer, int16_t copy_width = pledge_left - GOAL_COPY_PLEDGE_GAP - GOAL_COPY_LEFT; int16_t token_top = (bounds.size.h - GOAL_SAFETY_TOKEN_HEIGHT) / 2; const int32_t current_timestamp_seconds = beeminder_display_current_timestamp_seconds(); + const BeeminderSafetyColors safety_colors = beeminder_safety_colors(goal->safety_days); // beeminder-connection.GOAL_BROWSING.18 if (current_timestamp_seconds >= 0) { @@ -91,7 +92,7 @@ static void goal_menu_draw_row_contents(GContext *ctx, const Layer *cell_layer, graphics_draw_line(ctx, GPoint(0, bounds.size.h - 1), GPoint(bounds.size.w - 1, bounds.size.h - 1)); } - graphics_context_set_fill_color(ctx, beeminder_safety_color(goal->safety_days)); + graphics_context_set_fill_color(ctx, safety_colors.background); graphics_fill_rect( ctx, GRect(GOAL_ROW_PADDING_LEFT + horizontal_offset_px, token_top, GOAL_SAFETY_TOKEN_WIDTH, GOAL_SAFETY_TOKEN_HEIGHT), @@ -103,7 +104,7 @@ static void goal_menu_draw_row_contents(GContext *ctx, const Layer *cell_layer, snprintf(safety_text, sizeof(safety_text), "%ldd", (long)goal->safety_days); } - graphics_context_set_text_color(ctx, GColorBlack); + graphics_context_set_text_color(ctx, safety_colors.foreground); graphics_draw_text(ctx, safety_text, s_goal_meta_font, GRect(GOAL_ROW_PADDING_LEFT + horizontal_offset_px, token_top + 5, GOAL_SAFETY_TOKEN_WIDTH, GOAL_SAFETY_TOKEN_HEIGHT - 5), diff --git a/src/c/reminder_window.c b/src/c/reminder_window.c index 37f8121..003ae94 100644 --- a/src/c/reminder_window.c +++ b/src/c/reminder_window.c @@ -121,6 +121,7 @@ static void content_draw(Layer *layer, GContext *context) { int16_t exact_deadline_height = 0; int16_t exact_deadline_top = REMINDER_REQUIREMENT_TOP; int16_t requirement_deadline_height = 0; + const BeeminderSafetyColors safety_colors = beeminder_safety_colors(safety_days); if (s_presentation == ReminderPresentationComplete) { // watch-reminder-delivery.CACHED_PRESENTATION.5 @@ -162,15 +163,16 @@ static void content_draw(Layer *layer, GContext *context) { graphics_context_set_fill_color(context, GColorWhite); graphics_fill_rect(context, bounds, 0, GCornerNone); - graphics_context_set_fill_color(context, beeminder_safety_color(safety_days)); + graphics_context_set_fill_color(context, safety_colors.background); graphics_fill_rect(context, GRect(0, 0, bounds.size.w, REMINDER_HEADER_HEIGHT), 0, GCornerNone); - graphics_context_set_text_color(context, GColorBlack); + graphics_context_set_text_color(context, safety_colors.foreground); graphics_draw_text(context, safety_days == 0 ? "BEEMERGENCY" : "REMINDER", s_meta_font, GRect(REMINDER_TEXT_MARGIN, 7, content_width - 43, 22), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); graphics_draw_text(context, pledge_text, s_meta_font, GRect(bounds.size.w - 51, 7, 43, 22), GTextOverflowModeFill, GTextAlignmentRight, NULL); + graphics_context_set_text_color(context, GColorBlack); graphics_draw_text(context, goal_label, s_heading_font, GRect(REMINDER_TEXT_MARGIN, 49, content_width, 29), GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL); if (s_presentation == ReminderPresentationComplete) { diff --git a/tests/c/beeminder_colors_test.c b/tests/c/beeminder_colors_test.c new file mode 100644 index 0000000..214c403 --- /dev/null +++ b/tests/c/beeminder_colors_test.c @@ -0,0 +1,27 @@ +#include "../../src/c/beeminder_colors.h" + +#include +#include + +static void assert_colors(int32_t safety_days, GColor background, GColor foreground) { + const BeeminderSafetyColors colors = beeminder_safety_colors(safety_days); + + assert(colors.background.argb == background.argb); + assert(colors.foreground.argb == foreground.argb); +} + +// beeminder-connection.GOAL_BROWSING.3-1 beeminder-connection.GOAL_BROWSING.3-2 +static void test_safety_categories_use_the_selected_contrasting_colors(void) { + assert_colors(0, GColorDarkCandyAppleRed, GColorWhite); + assert_colors(1, GColorYellow, GColorBlack); + assert_colors(2, GColorPictonBlue, GColorBlack); + assert_colors(3, GColorMediumAquamarine, GColorBlack); + assert_colors(6, GColorMediumAquamarine, GColorBlack); + assert_colors(7, GColorGreen, GColorBlack); + assert_colors(INT32_MAX, GColorGreen, GColorBlack); +} + +int main(void) { + test_safety_categories_use_the_selected_contrasting_colors(); + return 0; +} diff --git a/tests/c/fakes/pebble.h b/tests/c/fakes/pebble.h index f490a34..c0ca464 100644 --- a/tests/c/fakes/pebble.h +++ b/tests/c/fakes/pebble.h @@ -8,6 +8,18 @@ typedef int32_t status_t; typedef int32_t WakeupId; +typedef union { + uint8_t argb; +} GColor; + +#define GColorBlack ((GColor){.argb = UINT8_C(0xC0)}) +#define GColorDarkCandyAppleRed ((GColor){.argb = UINT8_C(0xE0)}) +#define GColorGreen ((GColor){.argb = UINT8_C(0xCC)}) +#define GColorMediumAquamarine ((GColor){.argb = UINT8_C(0xDE)}) +#define GColorPictonBlue ((GColor){.argb = UINT8_C(0xDB)}) +#define GColorWhite ((GColor){.argb = UINT8_C(0xFF)}) +#define GColorYellow ((GColor){.argb = UINT8_C(0xFC)}) + #define APP_LOG(...) ((void)0) #define E_RANGE INT32_C(-8) #define PERSIST_DATA_MAX_LENGTH 256 -- knotmirror 0.0.1