From 7424988df0bac861f9511e97c7ccaa8c526f6a4a Mon Sep 17 00:00:00 2001 From: Andrew McOlash Date: Wed, 22 Apr 2026 14:00:09 -0700 Subject: [PATCH 1/6] fw/shell: Add DarkMode preference + stubs Co-Authored-By: Claude Signed-off-by: Andrew McOlash --- src/fw/shell/normal/prefs.c | 32 +++++++++++++++++++++----- src/fw/shell/normal/prefs_values.h.inc | 1 + src/fw/shell/prefs.h | 12 ++++++++++ src/fw/shell/prf/stubs.c | 11 +++++++++ src/fw/shell/sdk/prefs.c | 8 +++++++ tests/stubs/stubs_shell_prefs.h | 8 +++++++ 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/fw/shell/normal/prefs.c b/src/fw/shell/normal/prefs.c index fffd0f331..b63c8810d 100644 --- a/src/fw/shell/normal/prefs.c +++ b/src/fw/shell/normal/prefs.c @@ -101,7 +101,7 @@ static uint32_t s_dynamic_backlight_min_threshold = 0; // default set from board #if CAPABILITY_HAS_ORIENTATION_MANAGER #define PREF_KEY_DISPLAY_ORIENTATION_LEFT_HANDED "displayOrientationLeftHanded" static bool s_display_orientation_left = false; -#endif +#endif #define PREF_KEY_BACKLIGHT_AMBIENT_THRESHOLD "lightAmbientThreshold" static uint32_t s_backlight_ambient_threshold = 0; // default set from board config in shell_prefs_init() @@ -249,11 +249,13 @@ static GColor s_theme_highlight_color = GColorVividCerulean; #define PREF_KEY_MENU_SCROLL_VIBE_BEHAVIOR "menuScrollVibeBehavior" #define PREF_KEY_MUSIC_SHOW_VOLUME_CONTROLS "musicShowVolumeControls" #define PREF_KEY_MUSIC_SHOW_PROGRESS_BAR "musicShowProgressBar" +#define PREF_KEY_DARK_MODE "darkMode" static bool s_menu_scroll_wrap_around = false; static MenuScrollVibeBehavior s_menu_scroll_vibe_behavior = MenuScrollNoVibe; static bool s_music_show_volume_controls = true; static bool s_music_show_progress_bar = true; +static uint8_t s_dark_mode = DarkModeOff; // ============================================================================================ // Handlers for each pref that validate the new setting and store the new value in our globals. @@ -391,13 +393,13 @@ static bool prv_set_s_motion_sensitivity(uint8_t *sensitivity) { return false; } s_motion_sensitivity = *sensitivity; - + // Update accelerometer sensitivity in accel_manager // This applies the setting to the hardware #if CAPABILITY_HAS_ACCEL_SENSITIVITY accel_manager_update_sensitivity(*sensitivity); #endif - + return true; } @@ -745,7 +747,16 @@ static bool prv_set_s_music_show_progress_bar(bool *enabled) { s_music_show_progress_bar = *enabled; return true; } - + +static bool prv_set_s_dark_mode(uint8_t *mode) { + if (*mode >= DarkModeCount) { + s_dark_mode = DarkModeOn; + return false; + } + s_dark_mode = *mode; + return true; +} + // ------------------------------------------------------------------------------------ // Table of all prefs typedef bool (*PrefSetHandler)(const void *value, size_t val_len); @@ -830,10 +841,10 @@ void shell_prefs_init(void) { } settings_file_close(&file); - + // Update the ambient light driver with the loaded threshold value ambient_light_set_dark_threshold(s_backlight_ambient_threshold); - + // Initialize prefs sync (must be after prefs are loaded) prefs_sync_init(); @@ -1854,3 +1865,12 @@ bool shell_prefs_get_music_show_progress_bar(void) { void shell_prefs_set_music_show_progress_bar(bool enable) { prv_pref_set(PREF_KEY_MUSIC_SHOW_PROGRESS_BAR, &enable, sizeof(enable)); } + +DarkMode shell_prefs_get_dark_mode(void) { + return (DarkMode)s_dark_mode; +} + +void shell_prefs_set_dark_mode(DarkMode mode) { + uint8_t val = (uint8_t)mode; + prv_pref_set(PREF_KEY_DARK_MODE, &val, sizeof(val)); +} diff --git a/src/fw/shell/normal/prefs_values.h.inc b/src/fw/shell/normal/prefs_values.h.inc index 2594ba204..21cab7b4d 100644 --- a/src/fw/shell/normal/prefs_values.h.inc +++ b/src/fw/shell/normal/prefs_values.h.inc @@ -70,3 +70,4 @@ PREFS_MACRO(PREF_KEY_MENU_SCROLL_VIBE_BEHAVIOR, s_menu_scroll_vibe_behavior) PREFS_MACRO(PREF_KEY_MUSIC_SHOW_VOLUME_CONTROLS, s_music_show_volume_controls) PREFS_MACRO(PREF_KEY_MUSIC_SHOW_PROGRESS_BAR, s_music_show_progress_bar) + PREFS_MACRO(PREF_KEY_DARK_MODE, s_dark_mode) diff --git a/src/fw/shell/prefs.h b/src/fw/shell/prefs.h index 0bbeb1a44..4825a9c0a 100644 --- a/src/fw/shell/prefs.h +++ b/src/fw/shell/prefs.h @@ -209,3 +209,15 @@ void shell_prefs_set_music_show_volume_controls(bool enable); bool shell_prefs_get_music_show_progress_bar(void); void shell_prefs_set_music_show_progress_bar(bool enable); + +typedef enum DarkMode { + DarkModeOff = 0, + DarkModeOn = 1, +#if CAPABILITY_HAS_AMBIENT_LIGHT_SENSOR + DarkModeAuto = 2, // Follows the ambient light sensor; dark mode when ambient light is low +#endif + DarkModeCount +} DarkMode; + +DarkMode shell_prefs_get_dark_mode(void); +void shell_prefs_set_dark_mode(DarkMode mode); diff --git a/src/fw/shell/prf/stubs.c b/src/fw/shell/prf/stubs.c index 543e80e8b..71b4bf8f4 100644 --- a/src/fw/shell/prf/stubs.c +++ b/src/fw/shell/prf/stubs.c @@ -252,6 +252,17 @@ void shell_prefs_set_language_english(bool english) { void shell_prefs_toggle_language_english(void) { } +GColor shell_prefs_get_theme_highlight_color(void) { + return PBL_IF_COLOR_ELSE(GColorVividCerulean, GColorBlack); +} + +DarkMode shell_prefs_get_dark_mode(void) { + return DarkModeOff; +} + +void shell_prefs_set_dark_mode(DarkMode mode) { +} + FontInfo *fonts_get_system_emoji_font_for_size(unsigned int font_height) { return NULL; } diff --git a/src/fw/shell/sdk/prefs.c b/src/fw/shell/sdk/prefs.c index 3230997cf..610698258 100644 --- a/src/fw/shell/sdk/prefs.c +++ b/src/fw/shell/sdk/prefs.c @@ -294,6 +294,14 @@ void shell_prefs_set_theme_highlight_color(GColor color) { // Not used in SDK shell } +DarkMode shell_prefs_get_dark_mode(void) { + return DarkModeOff; +} + +void shell_prefs_set_dark_mode(DarkMode mode) { + // Not used in SDK shell +} + #if CAPABILITY_HAS_APP_SCALING LegacyAppRenderMode shell_prefs_get_legacy_app_render_mode(void) { return (LegacyAppRenderMode)s_legacy_app_render_mode; diff --git a/tests/stubs/stubs_shell_prefs.h b/tests/stubs/stubs_shell_prefs.h index 368d2e86e..dde8a07cd 100644 --- a/tests/stubs/stubs_shell_prefs.h +++ b/tests/stubs/stubs_shell_prefs.h @@ -74,6 +74,14 @@ void WEAK shell_prefs_set_menu_scroll_wrap_around_enable(bool enable) { s_menu_scroll_enable = enable; } +GColor WEAK shell_prefs_get_theme_highlight_color(void) { + return PBL_IF_COLOR_ELSE(GColorVividCerulean, GColorBlack); +} + +DarkMode WEAK shell_prefs_get_dark_mode(void) { + return DarkModeOff; +} + static MenuScrollVibeBehavior s_menu_scroll_vibe_behavior = MenuScrollNoVibe; MenuScrollVibeBehavior WEAK shell_prefs_get_menu_scroll_vibe_behavior(void) { From 604a6afeda18cf50f0f3e02b29d81345e4f2a965 Mon Sep 17 00:00:00 2001 From: Andrew McOlash Date: Wed, 22 Apr 2026 14:02:14 -0700 Subject: [PATCH 2/6] fw/shell: Add dark mode system_theme functions + stubs Co-Authored-By: Claude Signed-off-by: Andrew McOlash --- src/fw/shell/normal/shell.c | 5 +- src/fw/shell/system_theme.c | 48 +++++++++++++++++++ src/fw/shell/system_theme.h | 12 +++++ .../services/timeline/test_timeline_layouts.c | 1 + tests/stubs/stubs_ambient_light.h | 14 +++--- tests/stubs/stubs_pebble_process_md.h | 7 +-- tests/stubs/stubs_system_theme.h | 15 ++++++ 7 files changed, 91 insertions(+), 11 deletions(-) diff --git a/src/fw/shell/normal/shell.c b/src/fw/shell/normal/shell.c index d18c307bf..171c16d96 100644 --- a/src/fw/shell/normal/shell.c +++ b/src/fw/shell/normal/shell.c @@ -9,14 +9,15 @@ #include "process_management/app_install_types.h" #include "process_management/app_manager.h" #include "pbl/services/compositor/compositor_transitions.h" +#include "shell/prefs.h" -#define WATCHFACE_SHUTTER_COLOR GColorWhite #define HEALTH_SHUTTER_COLOR PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite) #define ACTION_SHUTTER_COLOR PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite) static const CompositorTransition *prv_get_watchface_compositor_animation( CompositorTransitionDirection direction) { - return PBL_IF_RECT_ELSE(compositor_shutter_transition_get(direction, WATCHFACE_SHUTTER_COLOR), + return PBL_IF_RECT_ELSE(compositor_shutter_transition_get(direction, + shell_prefs_get_theme_highlight_color()), compositor_port_hole_transition_app_get(direction)); } diff --git a/src/fw/shell/system_theme.c b/src/fw/shell/system_theme.c index e011d7703..0f0498a64 100644 --- a/src/fw/shell/system_theme.c +++ b/src/fw/shell/system_theme.c @@ -5,13 +5,19 @@ #include "applib/fonts/fonts.h" #include "apps/system/settings/notifications_private.h" +#include "process_management/pebble_process_md.h" #include "process_management/process_manager.h" #include "pbl/services/analytics/analytics.h" #include "shell/prefs.h" +#include "syscall/syscall.h" #include "syscall/syscall_internal.h" #include "system/passert.h" #include "util/size.h" +#if CAPABILITIES_HAS_AMBIENT_LIGHT +#include "drivers/ambient_light.h" +#endif + #include typedef struct SystemThemeTextStyle { @@ -170,6 +176,48 @@ GFont system_theme_get_font_for_default_size(TextStyleFont font) { font)); } +//! Returns true if the current (system) app context should render in dark mode. +static bool prv_is_system_app(void) { + const PebbleTask task = pebble_task_get_current(); + if (task != PebbleTask_App && task != PebbleTask_Worker) { + // Kernel / other tasks are always treated as system + return true; + } + const ProcessAppSDKType sdk_type = + process_metadata_get_app_sdk_type(sys_process_manager_get_current_process_md()); + return sdk_type == ProcessAppSDKType_System; +} + +bool system_theme_is_dark_mode(void) { + // Dark mode is only supported on color platforms, so treat all non-color platforms as light mode + if (PBL_IF_COLOR_ELSE(false, true)) { + return false; + } + if (!prv_is_system_app()) { + return false; + } + switch (shell_prefs_get_dark_mode()) { + case DarkModeOff: + return false; + case DarkModeOn: + return true; + #if CAPABILITY_HAS_AMBIENT_LIGHT_SENSOR + case DarkModeAuto: + return !ambient_light_is_light(); + #endif + default: + return false; + } +} + +GColor system_theme_get_bg_color(void) { + return system_theme_is_dark_mode() ? GColorBlack : GColorWhite; +} + +GColor system_theme_get_fg_color(void) { + return system_theme_is_dark_mode() ? GColorWhite : GColorBlack; +} + static const PreferredContentSize s_platform_default_content_sizes[] = { [PlatformTypeAplite] = PreferredContentSizeMedium, [PlatformTypeBasalt] = PreferredContentSizeMedium, diff --git a/src/fw/shell/system_theme.h b/src/fw/shell/system_theme.h index b7e7a3460..f80614e50 100644 --- a/src/fw/shell/system_theme.h +++ b/src/fw/shell/system_theme.h @@ -4,6 +4,7 @@ #pragma once #include "applib/fonts/fonts.h" +#include "applib/graphics/gtypes.h" #include "applib/platform.h" #include "applib/preferred_content_size.h" @@ -96,3 +97,14 @@ PreferredContentSize system_theme_get_default_content_size_for_runtime_platform( //! platform PreferredContentSize system_theme_convert_host_content_size_to_runtime_platform( PreferredContentSize size); + +//! @return true if the current app context should render in dark mode (system apps only). +bool system_theme_is_dark_mode(void); + +//! @return The background color appropriate for the current theme (black in dark mode on color +//! displays, white otherwise). +GColor system_theme_get_bg_color(void); + +//! @return The foreground color appropriate for the current theme (white in dark mode on color +//! displays, black otherwise). +GColor system_theme_get_fg_color(void); diff --git a/tests/fw/services/timeline/test_timeline_layouts.c b/tests/fw/services/timeline/test_timeline_layouts.c index f266eb089..2d8150777 100644 --- a/tests/fw/services/timeline/test_timeline_layouts.c +++ b/tests/fw/services/timeline/test_timeline_layouts.c @@ -79,6 +79,7 @@ void clock_get_since_time(char *buffer, int buf_size, time_t timestamp) { #include "stubs_property_animation.h" #include "stubs_serial.h" #include "stubs_shell_prefs.h" +#include "stubs_system_theme.h" #include "stubs_sleep.h" #include "stubs_syscalls.h" #include "stubs_task_watchdog.h" diff --git a/tests/stubs/stubs_ambient_light.h b/tests/stubs/stubs_ambient_light.h index c7a6ba1cd..8c531e3ec 100644 --- a/tests/stubs/stubs_ambient_light.h +++ b/tests/stubs/stubs_ambient_light.h @@ -3,18 +3,20 @@ #pragma once -void ambient_light_init(void) { +#include "util/attributes.h" + +void WEAK ambient_light_init(void) { } -uint32_t ambient_light_get_light_level(void) { +uint32_t WEAK ambient_light_get_light_level(void) { return 0; } -void command_als_read(void) { +void WEAK command_als_read(void) { } -uint32_t ambient_light_get_dark_threshold(void) { +uint32_t WEAK ambient_light_get_dark_threshold(void) { return 0; } -void ambient_light_set_dark_threshold(uint32_t new_threshold) { +void WEAK ambient_light_set_dark_threshold(uint32_t new_threshold) { } -bool ambient_light_is_light(void) { +bool WEAK ambient_light_is_light(void) { return false; } diff --git a/tests/stubs/stubs_pebble_process_md.h b/tests/stubs/stubs_pebble_process_md.h index f604b42bc..85a84f65b 100644 --- a/tests/stubs/stubs_pebble_process_md.h +++ b/tests/stubs/stubs_pebble_process_md.h @@ -4,15 +4,16 @@ #pragma once #include "process_management/pebble_process_md.h" +#include "util/attributes.h" -Version process_metadata_get_sdk_version(const PebbleProcessMd *md) { +Version WEAK process_metadata_get_sdk_version(const PebbleProcessMd *md) { return (Version) { PROCESS_INFO_CURRENT_SDK_VERSION_MAJOR, PROCESS_INFO_CURRENT_SDK_VERSION_MINOR }; } -ProcessAppSDKType process_metadata_get_app_sdk_type(const PebbleProcessMd *md) { +ProcessAppSDKType WEAK process_metadata_get_app_sdk_type(const PebbleProcessMd *md) { return 0; } -int process_metadata_get_code_bank_num(const PebbleProcessMd *md) { +int WEAK process_metadata_get_code_bank_num(const PebbleProcessMd *md) { return 0; } diff --git a/tests/stubs/stubs_system_theme.h b/tests/stubs/stubs_system_theme.h index 4283c414a..d16cc2e3e 100644 --- a/tests/stubs/stubs_system_theme.h +++ b/tests/stubs/stubs_system_theme.h @@ -6,6 +6,9 @@ #include "shell/system_theme.h" #include "util/attributes.h" +#include "stubs_ambient_light.h" +#include "stubs_pebble_process_md.h" + #include const char *WEAK system_theme_get_font_key(TextStyleFont font) { @@ -29,3 +32,15 @@ PreferredContentSize WEAK system_theme_convert_host_content_size_to_runtime_plat PreferredContentSize size) { return size; } + +GColor WEAK system_theme_get_bg_color(void) { + return GColorWhite; +} + +GColor WEAK system_theme_get_fg_color(void) { + return GColorBlack; +} + +bool WEAK system_theme_is_dark_mode(void) { + return false; +} \ No newline at end of file From f54cfd447de1455eb269dbff14efc893c2ee806d Mon Sep 17 00:00:00 2001 From: Andrew McOlash Date: Wed, 22 Apr 2026 14:03:58 -0700 Subject: [PATCH 3/6] fw/applib/ui: Theme-aware defaults for applib ui components Co-Authored-By: Claude Signed-off-by: Andrew McOlash --- src/fw/applib/ui/date_selection_window.c | 5 +---- src/fw/applib/ui/menu_layer.c | 8 +++++--- src/fw/applib/ui/number_window.c | 3 ++- src/fw/applib/ui/progress_layer.c | 5 +++-- src/fw/applib/ui/status_bar_layer.c | 5 +++-- src/fw/applib/ui/time_range_selection_window.c | 2 +- src/fw/applib/ui/time_selection_window.c | 5 +---- src/fw/applib/ui/window.c | 7 ++++--- tests/fw/ui/test_menu_layer.c | 4 +++- tests/fw/ui/test_status_bar_layer.c | 1 + tests/fw/ui/test_window_stack.c | 1 + 11 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/fw/applib/ui/date_selection_window.c b/src/fw/applib/ui/date_selection_window.c index 109b0be0a..34d2c6192 100644 --- a/src/fw/applib/ui/date_selection_window.c +++ b/src/fw/applib/ui/date_selection_window.c @@ -138,7 +138,7 @@ static void prv_handle_dec(unsigned index, void *context) { // --------------------------------------------------------------------------- static void prv_text_layer_init(Layer *window_layer, TextLayer *text_layer, const GFont font) { - text_layer_init_with_parameters(text_layer, &GRectZero, NULL, font, GColorBlack, GColorClear, + text_layer_init_with_parameters(text_layer, &GRectZero, NULL, font, system_theme_get_fg_color(), GColorClear, GTextAlignmentCenter, GTextOverflowModeTrailingEllipsis); layer_add_child(window_layer, &text_layer->layer); layer_set_hidden(&text_layer->layer, true); @@ -220,9 +220,6 @@ void date_selection_window_init(DateSelectionWindowData *window, const char *lab // Status bar setup status_bar_layer_init(&window->status_layer); - status_bar_layer_set_colors(&window->status_layer, - PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack), - PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite)); status_bar_layer_set_separator_mode(&window->status_layer, PBL_IF_COLOR_ELSE(OPTION_MENU_STATUS_SEPARATOR_MODE, StatusBarLayerSeparatorModeNone)); diff --git a/src/fw/applib/ui/menu_layer.c b/src/fw/applib/ui/menu_layer.c index f7a311480..c77c4a585 100644 --- a/src/fw/applib/ui/menu_layer.c +++ b/src/fw/applib/ui/menu_layer.c @@ -17,6 +17,7 @@ #include "applib/legacy2/ui/menu_layer_legacy2.h" #include "kernel/pbl_malloc.h" #include "process_management/process_manager.h" +#include "shell/prefs.h" #include "shell/system_theme.h" #include "system/logging.h" #include "system/passert.h" @@ -695,8 +696,9 @@ void menu_layer_init(MenuLayer *menu_layer, const GRect *frame) { scroll_layer_set_shadow_hidden(scroll_layer, true); scroll_layer_set_context(scroll_layer, menu_layer); - menu_layer_set_normal_colors(menu_layer, GColorWhite, GColorBlack); - menu_layer_set_highlight_colors(menu_layer, GColorBlack, GColorWhite); + menu_layer_set_normal_colors(menu_layer, system_theme_get_bg_color(), system_theme_get_fg_color()); + GColor highlight_bg = shell_prefs_get_theme_highlight_color(); + menu_layer_set_highlight_colors(menu_layer, highlight_bg, gcolor_legible_over(highlight_bg)); InverterLayer *inverter = &menu_layer->inverter; inverter_layer_init(inverter, &GRectZero); @@ -1419,7 +1421,7 @@ void menu_layer_set_scroll_vibe_on_blocked(MenuLayer *menu_layer, bool scroll_vi if (!menu_layer) { return; } - + if (scroll_vibe_on_blocked) { menu_layer->scroll_vibe_on_wrap_around = false; } diff --git a/src/fw/applib/ui/number_window.c b/src/fw/applib/ui/number_window.c index 18da3c562..bc5738062 100644 --- a/src/fw/applib/ui/number_window.c +++ b/src/fw/applib/ui/number_window.c @@ -8,6 +8,7 @@ #include "applib/applib_malloc.auto.h" #include "kernel/ui/kernel_ui.h" #include "kernel/ui/system_icons.h" +#include "shell/system_theme.h" #include "util/size.h" #include @@ -94,7 +95,7 @@ void prv_update_proc(Layer *layer, GContext* ctx) { _Static_assert(offsetof(NumberWindow, window) == 0, ""); NumberWindow *nw = (NumberWindow*) layer; - graphics_context_set_text_color(ctx, GColorBlack); + graphics_context_set_text_color(ctx, system_theme_get_fg_color()); GRect frame = prv_get_text_frame(layer); frame.size.h = 54; diff --git a/src/fw/applib/ui/progress_layer.c b/src/fw/applib/ui/progress_layer.c index f0c3ba163..ee04f9554 100644 --- a/src/fw/applib/ui/progress_layer.c +++ b/src/fw/applib/ui/progress_layer.c @@ -7,6 +7,7 @@ #include "applib/graphics/gtypes.h" #include "applib/graphics/graphics.h" #include "applib/ui/layer.h" +#include "shell/system_theme.h" #include "util/math.h" #include @@ -49,8 +50,8 @@ void progress_layer_init(ProgressLayer* progress_layer, const GRect *frame) { layer_init(&progress_layer->layer, frame); progress_layer->layer.update_proc = (LayerUpdateProc) progress_layer_update_proc; - progress_layer->foreground_color = GColorBlack; - progress_layer->background_color = GColorWhite; + progress_layer->foreground_color = system_theme_get_fg_color(); + progress_layer->background_color = system_theme_get_bg_color(); progress_layer->corner_radius = 1; } diff --git a/src/fw/applib/ui/status_bar_layer.c b/src/fw/applib/ui/status_bar_layer.c index 3e17ae8ab..aa8a223bc 100644 --- a/src/fw/applib/ui/status_bar_layer.c +++ b/src/fw/applib/ui/status_bar_layer.c @@ -15,6 +15,7 @@ #include "kernel/ui/kernel_ui.h" #include "process_state/app_state/app_state.h" #include "pbl/services/clock.h" +#include "shell/system_theme.h" #include "syscall/syscall.h" #include "syscall/syscall_internal.h" #include "system/passert.h" @@ -104,8 +105,8 @@ void status_bar_layer_init(StatusBarLayer *status_bar_layer) { event_service_client_subscribe(&(status_bar_layer->tick_event)); status_bar_layer->config = (StatusBarLayerConfig){ - .foreground_color = GColorWhite, - .background_color = GColorBlack, + .foreground_color = system_theme_get_fg_color(), + .background_color = system_theme_get_bg_color(), .separator.mode = StatusBarLayerSeparatorModeNone, }; diff --git a/src/fw/applib/ui/time_range_selection_window.c b/src/fw/applib/ui/time_range_selection_window.c index 85008cda8..abfecf375 100644 --- a/src/fw/applib/ui/time_range_selection_window.c +++ b/src/fw/applib/ui/time_range_selection_window.c @@ -100,7 +100,7 @@ static void prv_text_layer_init(Window *window, TextLayer *text_layer, GRect *re const char *i18n_str) { const GFont subtitle_font = system_theme_get_font_for_default_size(TextStyleFont_Subtitle); text_layer_init_with_parameters(text_layer, rect, i18n_get(i18n_str, window), - subtitle_font, GColorBlack, GColorClear, GTextAlignmentCenter, + subtitle_font, system_theme_get_fg_color(), GColorClear, GTextAlignmentCenter, GTextOverflowModeTrailingEllipsis); layer_add_child(&window->layer, &text_layer->layer); } diff --git a/src/fw/applib/ui/time_selection_window.c b/src/fw/applib/ui/time_selection_window.c index 6d9b6ab8d..cdc89ed4a 100644 --- a/src/fw/applib/ui/time_selection_window.c +++ b/src/fw/applib/ui/time_selection_window.c @@ -216,7 +216,7 @@ void time_selection_window_configure(TimeSelectionWindowData *time_selection_win } static void prv_text_layer_init(Layer *window_layer, TextLayer *text_layer, const GFont font) { - text_layer_init_with_parameters(text_layer, &GRectZero, NULL, font, GColorBlack, GColorClear, + text_layer_init_with_parameters(text_layer, &GRectZero, NULL, font, system_theme_get_fg_color(), GColorClear, GTextAlignmentCenter, GTextOverflowModeTrailingEllipsis); layer_add_child(window_layer, &text_layer->layer); layer_set_hidden(&text_layer->layer, true); @@ -272,9 +272,6 @@ void time_selection_window_init(TimeSelectionWindowData *time_selection_window, // Status setup status_bar_layer_init(&time_selection_window->status_layer); - status_bar_layer_set_colors(&time_selection_window->status_layer, - PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack), - PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite)); status_bar_layer_set_separator_mode(&time_selection_window->status_layer, PBL_IF_COLOR_ELSE(OPTION_MENU_STATUS_SEPARATOR_MODE, StatusBarLayerSeparatorModeNone)); diff --git a/src/fw/applib/ui/window.c b/src/fw/applib/ui/window.c index 7eb483a2b..91ce9b0e1 100644 --- a/src/fw/applib/ui/window.c +++ b/src/fw/applib/ui/window.c @@ -18,6 +18,7 @@ #include "kernel/ui/modals/modal_manager.h" #include "process_management/process_manager.h" #include "process_state/app_state/app_state.h" +#include "shell/system_theme.h" #include "system/logging.h" #include "system/passert.h" #include "syscall/syscall.h" @@ -89,8 +90,8 @@ void prv_render_legacy2_system_status_bar(GContext *ctx, Window *window) { grect_clip(&ctx->draw_state.clip_box, &window->layer.frame); StatusBarLayerConfig config = { - .foreground_color = GColorWhite, - .background_color = GColorBlack, + .foreground_color = system_theme_get_fg_color(), + .background_color = system_theme_get_bg_color(), .mode = StatusBarLayerModeClock, }; GRect frame = window->layer.frame; @@ -185,7 +186,7 @@ void window_init(Window *window, const char* debug_name) { window->is_fullscreen = fullscreen; window->layer.window = window; window->layer.update_proc = window_do_layer_update_proc; - window->background_color = GColorWhite; + window->background_color = system_theme_get_bg_color(); window->in_click_config_provider = false; window->is_waiting_for_click_config = false; window->parent_window_stack = NULL; diff --git a/tests/fw/ui/test_menu_layer.c b/tests/fw/ui/test_menu_layer.c index 1599cc19e..a7c148f06 100644 --- a/tests/fw/ui/test_menu_layer.c +++ b/tests/fw/ui/test_menu_layer.c @@ -17,8 +17,10 @@ #include "stubs_passert.h" #include "stubs_pbl_malloc.h" #include "stubs_pebble_tasks.h" -#include "stubs_ui_window.h" #include "stubs_process_manager.h" +#include "stubs_shell_prefs.h" +#include "stubs_system_theme.h" +#include "stubs_ui_window.h" #include "stubs_unobstructed_area.h" #include "stubs_vibes.h" diff --git a/tests/fw/ui/test_status_bar_layer.c b/tests/fw/ui/test_status_bar_layer.c index 47aaf8b2f..acae9c4cd 100644 --- a/tests/fw/ui/test_status_bar_layer.c +++ b/tests/fw/ui/test_status_bar_layer.c @@ -28,6 +28,7 @@ #include "stubs_process_manager.h" #include "stubs_resources.h" #include "stubs_syscalls.h" +#include "stubs_system_theme.h" #include "stubs_ui_window.h" #include "stubs_unobstructed_area.h" #include "stubs_window_stack.h" diff --git a/tests/fw/ui/test_window_stack.c b/tests/fw/ui/test_window_stack.c index b0d25bf74..e0a7398c3 100644 --- a/tests/fw/ui/test_window_stack.c +++ b/tests/fw/ui/test_window_stack.c @@ -40,6 +40,7 @@ #include "stubs_queue.h" #include "stubs_resources.h" #include "stubs_syscalls.h" +#include "stubs_system_theme.h" #include "stubs_unobstructed_area.h" // Fakes From 59bdd0db4bebc9ed7e81a164825bcc7217d96846 Mon Sep 17 00:00:00 2001 From: Andrew McOlash Date: Wed, 22 Apr 2026 14:06:34 -0700 Subject: [PATCH 4/6] fw/apps/system/settings: Apply dark mode to settings and add toggle Co-Authored-By: Claude Signed-off-by: Andrew McOlash --- .../apps/system/settings/activity_tracker.c | 3 -- src/fw/apps/system/settings/display.c | 22 +++++++++ src/fw/apps/system/settings/option_menu.c | 2 +- .../system/settings/quick_launch_app_menu.c | 2 +- .../system/settings/quick_launch_setup_menu.c | 4 +- src/fw/apps/system/settings/settings.c | 20 ++++---- src/fw/apps/system/settings/system.c | 9 ++-- src/fw/apps/system/settings/themes.c | 46 +++++++++++++++++-- src/fw/apps/system/settings/window.c | 26 +++++++---- 9 files changed, 103 insertions(+), 31 deletions(-) diff --git a/src/fw/apps/system/settings/activity_tracker.c b/src/fw/apps/system/settings/activity_tracker.c index 7b71ffa1c..fc0cdfc51 100644 --- a/src/fw/apps/system/settings/activity_tracker.c +++ b/src/fw/apps/system/settings/activity_tracker.c @@ -240,9 +240,6 @@ static Window *prv_init(void) { option_menu_init(&data->option_menu); // Not using option_menu_configure because prv_reload_menu_data already sets // icons_enabled and chosen row index - option_menu_set_status_colors(&data->option_menu, GColorWhite, GColorBlack); - GColor highlight_bg = shell_prefs_get_theme_highlight_color(); - option_menu_set_highlight_colors(&data->option_menu, highlight_bg, gcolor_legible_over(highlight_bg)); option_menu_set_title(&data->option_menu, i18n_get("Background App", data)); option_menu_set_content_type(&data->option_menu, OptionMenuContentType_SingleLine); option_menu_set_callbacks(&data->option_menu, &option_menu_callbacks, data); diff --git a/src/fw/apps/system/settings/display.c b/src/fw/apps/system/settings/display.c index 8c143f215..7ff0588ff 100644 --- a/src/fw/apps/system/settings/display.c +++ b/src/fw/apps/system/settings/display.c @@ -455,6 +455,9 @@ enum SettingsDisplayItem { #endif #if CAPABILITY_HAS_APP_SCALING SettingsDisplayLegacyAppMode, +#endif +#if !CAPABILITY_HAS_THEMING + SettingsDisplayDarkMode, #endif NumSettingsDisplayItems }; @@ -486,6 +489,11 @@ static void prv_display_select_click_cb(SettingsCallbacks *context, uint16_t row case SettingsDisplayLegacyAppMode: prv_legacy_app_mode_menu_push((SettingsDisplayData*)context); break; +#endif +#if !CAPABILITY_HAS_THEMING + case SettingsDisplayDarkMode: + shell_prefs_set_dark_mode((shell_prefs_get_dark_mode() + 1) % DarkModeCount); + break; #endif default: WTF; @@ -530,6 +538,20 @@ static void prv_display_draw_row_cb(SettingsCallbacks *context, GContext *ctx, subtitle = (shell_prefs_get_legacy_app_render_mode() >= LegacyAppRenderMode_ScalingNearest) ? i18n_noop("Scaled") : i18n_noop("Centered"); break; +#endif +#if !CAPABILITY_HAS_THEMING + case SettingsDisplayDarkMode: { + title = i18n_noop("Dark Mode"); + static const char * const s_dark_mode_labels[] = { + [DarkModeOff] = i18n_noop("Off"), + [DarkModeOn] = i18n_noop("On"), + #if CAPABILITY_HAS_AMBIENT_LIGHT_SENSOR + [DarkModeAuto] = i18n_noop("Auto"), + #endif + }; + subtitle = s_dark_mode_labels[shell_prefs_get_dark_mode()]; + break; + } #endif default: WTF; diff --git a/src/fw/apps/system/settings/option_menu.c b/src/fw/apps/system/settings/option_menu.c index c41d872ae..b357e4484 100644 --- a/src/fw/apps/system/settings/option_menu.c +++ b/src/fw/apps/system/settings/option_menu.c @@ -42,7 +42,7 @@ OptionMenu *settings_option_menu_create( .title = i18n_get(i18n_title_key, option_menu), .content_type = content_type, .choice = choice, - .status_colors = { GColorWhite, GColorBlack }, + .status_colors = { system_theme_get_bg_color(), system_theme_get_fg_color() }, .highlight_colors = { highlight_bg, gcolor_legible_over(highlight_bg) }, .icons_enabled = icons_enabled, }; diff --git a/src/fw/apps/system/settings/quick_launch_app_menu.c b/src/fw/apps/system/settings/quick_launch_app_menu.c index 750f76881..3176e36f4 100644 --- a/src/fw/apps/system/settings/quick_launch_app_menu.c +++ b/src/fw/apps/system/settings/quick_launch_app_menu.c @@ -158,7 +158,7 @@ void quick_launch_app_menu_window_push(ButtonId button, bool is_tap) { const OptionMenuConfig config = { .title = i18n_get(i18n_noop("Quick Launch"), data), .choice = (install_id == INSTALL_ID_INVALID) ? 0 : (app_index + NUM_CUSTOM_CELLS), - .status_colors = { GColorWhite, GColorBlack, }, + .status_colors = { system_theme_get_bg_color(), system_theme_get_fg_color() }, .highlight_colors = { highlight_bg, gcolor_legible_over(highlight_bg) }, .icons_enabled = true, }; diff --git a/src/fw/apps/system/settings/quick_launch_setup_menu.c b/src/fw/apps/system/settings/quick_launch_setup_menu.c index f1b9e04eb..ed0a6e045 100644 --- a/src/fw/apps/system/settings/quick_launch_setup_menu.c +++ b/src/fw/apps/system/settings/quick_launch_setup_menu.c @@ -56,8 +56,8 @@ static void prv_push_first_use_dialog(void) { const char *text = i18n_get("Open favorite apps quickly with a long button press from your " "watchface.", i18n_owner); ExpandableDialog *expandable_dialog = expandable_dialog_create_with_params( - WINDOW_NAME("Quick Launch First Use"), RESOURCE_ID_SUNNY_DAY_TINY, text, GColorBlack, - GColorWhite, NULL, RESOURCE_ID_ACTION_BAR_ICON_CHECK, prv_handle_quick_launch_confirm); + WINDOW_NAME("Quick Launch First Use"), RESOURCE_ID_SUNNY_DAY_TINY, text, system_theme_get_fg_color(), + system_theme_get_bg_color(), NULL, RESOURCE_ID_ACTION_BAR_ICON_CHECK, prv_handle_quick_launch_confirm); expandable_dialog_set_header(expandable_dialog, header); #if PBL_ROUND expandable_dialog_set_header_font(expandable_dialog, diff --git a/src/fw/apps/system/settings/settings.c b/src/fw/apps/system/settings/settings.c index ef41b7eb0..616fb5ca1 100644 --- a/src/fw/apps/system/settings/settings.c +++ b/src/fw/apps/system/settings/settings.c @@ -13,6 +13,7 @@ #include "pbl/services/i18n/i18n.h" #include "system/passert.h" #include "shell/prefs.h" +#include "shell/system_theme.h" #include "util/size.h" #define SETTINGS_CATEGORY_MENU_CELL_UNFOCUSED_ROUND_VERTICAL_PADDING 14 @@ -135,13 +136,6 @@ static void prv_window_load(Window *window) { .select_click = prv_select_callback, .get_separator_height = prv_get_separator_height_callback }); - GColor highlight_bg = shell_prefs_get_theme_highlight_color(); - menu_layer_set_normal_colors(menu_layer, - PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite), - PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack)); - menu_layer_set_highlight_colors(menu_layer, - highlight_bg, - gcolor_legible_over(highlight_bg)); menu_layer_set_click_config_onto_window(menu_layer, &data->window); menu_layer_set_scroll_wrap_around(menu_layer, shell_prefs_get_menu_scroll_wrap_around_enable()); menu_layer_set_scroll_vibe_on_wrap(menu_layer, shell_prefs_get_menu_scroll_vibe_behavior() == MenuScrollVibeOnWrapAround); @@ -150,6 +144,15 @@ static void prv_window_load(Window *window) { layer_add_child(&data->window.layer, menu_layer_get_layer(menu_layer)); } +static void prv_window_appear(Window *window) { + SettingsAppData *data = window_get_user_data(window); + // Refresh background and menu colors in case dark mode changed while away + window_set_background_color(window, system_theme_get_bg_color()); + menu_layer_set_normal_colors(&data->menu_layer, system_theme_get_bg_color(), + system_theme_get_fg_color()); + layer_mark_dirty(menu_layer_get_layer(&data->menu_layer)); +} + static void prv_window_unload(Window *window) { SettingsAppData *data = window_get_user_data(window); @@ -174,9 +177,10 @@ static void handle_init(void) { window_set_user_data(window, data); window_set_window_handlers(window, &(WindowHandlers){ .load = prv_window_load, + .appear = prv_window_appear, .unload = prv_window_unload, }); - window_set_background_color(window, PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite)); + window_set_background_color(window, system_theme_get_bg_color()); app_window_stack_push(window, true); } diff --git a/src/fw/apps/system/settings/system.c b/src/fw/apps/system/settings/system.c index 642c4cd69..bf8a8ab7c 100644 --- a/src/fw/apps/system/settings/system.c +++ b/src/fw/apps/system/settings/system.c @@ -176,7 +176,6 @@ static void prv_init_status_bar(StatusBarLayer *status_layer, Window *window, co status_bar_layer_init(status_layer); status_bar_layer_set_title(status_layer, text, false, false); status_bar_layer_set_separator_mode(status_layer, OPTION_MENU_STATUS_SEPARATOR_MODE); - status_bar_layer_set_colors(status_layer, GColorWhite, GColorBlack); layer_add_child(&window->layer, status_bar_layer_get_layer(status_layer)); } @@ -881,7 +880,9 @@ static void prv_draw_fcc_cell_round( const uint8_t fcc_number_subtitle_height = fonts_get_font_height(fcc_number_subtitle_font); const GTextOverflowMode text_overflow_mode = GTextOverflowModeFill; - graphics_context_set_text_color(ctx, cell_is_highlighted ? GColorWhite : GColorBlack); + graphics_context_set_text_color(ctx, cell_is_highlighted ? + system_theme_get_bg_color() : + system_theme_get_fg_color()); // Calculate the container of the FCC cell content and center it within the cell const int16_t title_and_icon_width = 50; @@ -1324,7 +1325,7 @@ static void prv_kcc_window_load(Window *window) { - title_text_internal_padding; text_layer_init_with_parameters(&data->title_text, &title_text_frame, title, title_text_font, - GColorBlack, GColorClear, GTextAlignmentCenter, + system_theme_get_fg_color(), GColorClear, GTextAlignmentCenter, GTextOverflowModeTrailingEllipsis); layer_add_child(window_layer, text_layer_get_layer(&data->title_text)); @@ -1332,7 +1333,7 @@ static void prv_kcc_window_load(Window *window) { info_text_frame.origin.y = title_text_frame.origin.y + title_text_size.h + vertical_spacing; text_layer_init_with_parameters(&data->info_text, &info_text_frame, prv_get_korea_kcc_id(), info_text_font, - GColorBlack, GColorClear, GTextAlignmentCenter, + system_theme_get_fg_color(), GColorClear, GTextAlignmentCenter, GTextOverflowModeTrailingEllipsis); layer_add_child(window_layer, text_layer_get_layer(&data->info_text)); } diff --git a/src/fw/apps/system/settings/themes.c b/src/fw/apps/system/settings/themes.c index 29e6b7335..7142ff293 100644 --- a/src/fw/apps/system/settings/themes.c +++ b/src/fw/apps/system/settings/themes.c @@ -117,7 +117,7 @@ static OptionMenu *prv_push_color_menu(void) { PBL_LOG_WRN("Invalid menu color, using default"); selected = 0; } - OptionMenu * const option_menu = settings_option_menu_create( + OptionMenu * const option_menu = settings_option_menu_push( title, OptionMenuContentType_SingleLine, selected, &callbacks, ARRAY_LENGTH(s_color_definitions), true /* icons_enabled */, color_names, NULL); @@ -133,12 +133,52 @@ static OptionMenu *prv_push_color_menu(void) { return option_menu; } + +static const char * const s_dark_mode_labels[] = { + i18n_noop("Off"), i18n_noop("On"), i18n_noop("Auto"), +}; + +static void prv_select_click_cb(SettingsCallbacks *context, uint16_t row) { + if (row == 0) shell_prefs_set_dark_mode((shell_prefs_get_dark_mode() + 1) % DarkModeCount); + else prv_push_color_menu(); + settings_menu_reload_data(SettingsMenuItemThemes); + settings_menu_mark_dirty(SettingsMenuItemThemes); +} + +static void prv_draw_row_cb(SettingsCallbacks *context, GContext *ctx, + const Layer *cell_layer, uint16_t row, bool selected) { + const char *title, *subtitle; + if (row == 0) { + title = i18n_noop("Dark Mode"); + subtitle = s_dark_mode_labels[shell_prefs_get_dark_mode()]; + } else { + int idx = prv_color_to_index(shell_prefs_get_theme_highlight_color(), + DEFAULT_THEME_HIGHLIGHT_COLOR); + title = i18n_noop("Accent Color"); + subtitle = s_color_definitions[idx < 0 ? 0 : idx].name; + } + menu_cell_basic_draw(ctx, cell_layer, i18n_get(title, context), + i18n_get(subtitle, context), NULL); +} + +static uint16_t prv_num_rows_cb(SettingsCallbacks *context) { return 2; } +static void prv_deinit_cb(SettingsCallbacks *context) { + i18n_free_all(context); + app_free(context); +} + #endif // CAPABILITY_HAS_THEMING static Window *prv_create_color_menu(void) { #if CAPABILITY_HAS_THEMING - OptionMenu *option_menu = prv_push_color_menu(); - return option_menu ? &option_menu->window : NULL; + SettingsCallbacks *callbacks = app_malloc_check(sizeof(*callbacks)); + *callbacks = (SettingsCallbacks){ + .deinit = prv_deinit_cb, + .draw_row = prv_draw_row_cb, + .select_click = prv_select_click_cb, + .num_rows = prv_num_rows_cb, + }; + return settings_window_create(SettingsMenuItemThemes, callbacks); #else WTF; return NULL; diff --git a/src/fw/apps/system/settings/window.c b/src/fw/apps/system/settings/window.c index 70bf7dc97..3e3a73133 100644 --- a/src/fw/apps/system/settings/window.c +++ b/src/fw/apps/system/settings/window.c @@ -29,6 +29,7 @@ #include "pbl/services/i18n/i18n.h" #include "pbl/services/system_task.h" #include "system/bootbits.h" +#include "shell/system_theme.h" #include "system/passert.h" #include "shell/prefs.h" @@ -62,10 +63,18 @@ typedef struct SettingsData { // Pref change handler /////////////////////// +static void prv_refresh_theme_colors(SettingsData *data) { + const GColor bg = system_theme_get_bg_color(); + const GColor fg = system_theme_get_fg_color(); + window_set_background_color(&data->window, bg); + status_bar_layer_set_colors(&data->status_layer, bg, fg); + menu_layer_set_normal_colors(&data->menu_layer, bg, fg); + layer_mark_dirty(menu_layer_get_layer(&data->menu_layer)); +} + static void prv_pref_change_handler(PebbleEvent *event, void *context) { SettingsData *data = context; - // Refresh the menu when any pref changes - layer_mark_dirty(menu_layer_get_layer(&data->menu_layer)); + prv_refresh_theme_colors(data); } // Filter category helpers @@ -85,8 +94,8 @@ static void prv_set_sub_menu_colors(GContext *ctx, const Layer *cell_layer, bool graphics_context_set_fill_color(ctx, highlight_bg); graphics_context_set_text_color(ctx, gcolor_legible_over(highlight_bg)); } else { - graphics_context_set_fill_color(ctx, GColorWhite); - graphics_context_set_text_color(ctx, GColorBlack); + graphics_context_set_fill_color(ctx, system_theme_get_bg_color()); + graphics_context_set_text_color(ctx, system_theme_get_fg_color()); } graphics_fill_rect(ctx, &cell_layer->bounds); } @@ -183,7 +192,7 @@ static void prv_settings_window_load(Window *window) { ? data->title_override : settings_menu_get_status_name(data->current_category); status_bar_layer_set_title(status_layer, i18n_get(title, data), false, false); - status_bar_layer_set_colors(status_layer, GColorWhite, GColorBlack); + status_bar_layer_set_colors(status_layer, system_theme_get_bg_color(), system_theme_get_fg_color()); status_bar_layer_set_separator_mode(status_layer, OPTION_MENU_STATUS_SEPARATOR_MODE); layer_add_child(&data->window.layer, status_bar_layer_get_layer(status_layer)); @@ -203,9 +212,6 @@ static void prv_settings_window_load(Window *window) { .selection_changed = prv_selection_changed_callback, .selection_will_change = prv_selection_will_change_callback, }); - menu_layer_set_normal_colors(menu_layer, GColorWhite, GColorBlack); - GColor highlight_bg = shell_prefs_get_theme_highlight_color(); - menu_layer_set_highlight_colors(menu_layer, highlight_bg, gcolor_legible_over(highlight_bg)); menu_layer_set_click_config_onto_window(menu_layer, &data->window); menu_layer_set_scroll_wrap_around(menu_layer, shell_prefs_get_menu_scroll_wrap_around_enable()); menu_layer_set_scroll_vibe_on_wrap(menu_layer, shell_prefs_get_menu_scroll_vibe_behavior() == MenuScrollVibeOnWrapAround); @@ -234,6 +240,8 @@ static void prv_settings_window_load(Window *window) { static void prv_settings_window_appear(Window *window) { SettingsData *data = window_get_user_data(window); + // Refresh colors in case the theme changed while this window was hidden + prv_refresh_theme_colors(data); SettingsCallbacks *callbacks = prv_get_current_callbacks(data); if (callbacks->appear) { callbacks->appear(data->callbacks); @@ -312,7 +320,7 @@ void settings_window_destroy(Window *window) { void settings_menu_mark_dirty(SettingsMenuItem category) { SettingsData *data = app_state_get_user_data(); if (data->current_category == category) { - layer_mark_dirty(menu_layer_get_layer(&data->menu_layer)); + prv_refresh_theme_colors(data); } } From 1bf7bbbc0b9c84ec82494698866b08f100c04053 Mon Sep 17 00:00:00 2001 From: Andrew McOlash Date: Wed, 22 Apr 2026 14:08:58 -0700 Subject: [PATCH 5/6] fw/apps/system: Apply dark mode to system apps Signed-off-by: Andrew McOlash --- src/fw/apps/system/alarms/alarms.c | 4 +--- src/fw/apps/system/health/health.c | 5 +++-- src/fw/apps/system/health/hr_detail_card.c | 3 ++- .../launcher/default/app_glance_structured.c | 16 +++++----------- src/fw/apps/system/music.c | 16 ++++++++++------ src/fw/apps/system/notifications.c | 12 ++++++------ src/fw/apps/system/weather/layout.c | 5 +++-- src/fw/apps/system/workout/selection.c | 2 +- 8 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/fw/apps/system/alarms/alarms.c b/src/fw/apps/system/alarms/alarms.c index feedf1790..512abadc7 100644 --- a/src/fw/apps/system/alarms/alarms.c +++ b/src/fw/apps/system/alarms/alarms.c @@ -353,7 +353,7 @@ static void prv_push_alarms_app_opened_dialog(AlarmsAppData *data) { const char *header = i18n_get("Smart Alarm", data); ExpandableDialog *expandable_dialog = expandable_dialog_create_with_params( header, RESOURCE_ID_SMART_ALARM_TINY, first_use_text, - GColorBlack, GColorWhite, NULL, RESOURCE_ID_ACTION_BAR_ICON_CHECK, + system_theme_get_fg_color(), system_theme_get_bg_color(), NULL, RESOURCE_ID_ACTION_BAR_ICON_CHECK, prv_alarms_app_opened_click_handler); expandable_dialog_set_action_bar_background_color(expandable_dialog, ALARMS_APP_HIGHLIGHT_COLOR); @@ -403,8 +403,6 @@ static void prv_handle_init(void) { layer_add_child(&data->window.layer, menu_layer_get_layer(&data->menu_layer)); status_bar_layer_init(&data->status_layer); - status_bar_layer_set_colors(&data->status_layer, PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack), - PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite)); status_bar_layer_set_separator_mode(&data->status_layer, StatusBarLayerSeparatorModeNone); layer_add_child(&data->window.layer, status_bar_layer_get_layer(&data->status_layer)); diff --git a/src/fw/apps/system/health/health.c b/src/fw/apps/system/health/health.c index 200748978..dbc4264f0 100644 --- a/src/fw/apps/system/health/health.c +++ b/src/fw/apps/system/health/health.c @@ -16,6 +16,7 @@ #include "pbl/services/activity/activity_private.h" #include "pbl/services/timeline/timeline.h" #include "resource/resource_ids.auto.h" +#include "shell/system_theme.h" #include "system/logging.h" // Health app versions @@ -84,8 +85,8 @@ static void prv_show_insights_onboarding_dialog(void) { "Insights Onboarding", RESOURCE_ID_HEALTH_ICON_MOON, text, - GColorBlack, - GColorWhite, + system_theme_get_fg_color(), + system_theme_get_bg_color(), NULL, RESOURCE_ID_ACTION_BAR_ICON_CHECK, expandable_dialog_close_cb); diff --git a/src/fw/apps/system/health/hr_detail_card.c b/src/fw/apps/system/health/hr_detail_card.c index 6ef97deeb..4cc4728bf 100644 --- a/src/fw/apps/system/health/hr_detail_card.c +++ b/src/fw/apps/system/health/hr_detail_card.c @@ -9,6 +9,7 @@ #include "pbl/services/i18n/i18n.h" #include "pbl/services/activity/activity.h" #include "pbl/services/activity/health_util.h" +#include "shell/system_theme.h" #include @@ -89,7 +90,7 @@ Window *health_hr_detail_card_create(HealthData *health_data) { .num_headings = card_data->num_headings, .headings = card_data->headings, .weekly_max = max_progress, - .bg_color = GColorWhite, + .bg_color = system_theme_get_bg_color(), .num_zones = card_data->num_zones, .zones = card_data->zones, .data = card_data, diff --git a/src/fw/apps/system/launcher/default/app_glance_structured.c b/src/fw/apps/system/launcher/default/app_glance_structured.c index 29b5aaf16..0154fec8b 100644 --- a/src/fw/apps/system/launcher/default/app_glance_structured.c +++ b/src/fw/apps/system/launcher/default/app_glance_structured.c @@ -13,6 +13,7 @@ #include "resource/resource_ids.auto.h" #include "pbl/services/timeline/attribute.h" #include "shell/prefs.h" +#include "shell/system_theme.h" #include "system/passert.h" #include "util/attributes.h" #include "util/string.h" @@ -87,16 +88,9 @@ static void prv_structured_glance_icon_bitmap_processor_post_func( GColor launcher_app_glance_structured_get_highlight_color( LauncherAppGlanceStructured *structured_glance) { -#if PBL_COLOR - if (structured_glance->glance.is_highlighted) { - GColor highlight_bg = shell_prefs_get_theme_highlight_color(); - return gcolor_legible_over(highlight_bg); - } else { - return GColorBlack; - } -#else - return structured_glance->glance.is_highlighted ? GColorWhite : GColorBlack; -#endif + return structured_glance->glance.is_highlighted ? + gcolor_legible_over(shell_prefs_get_theme_highlight_color()) : + system_theme_get_fg_color(); } static GColor prv_get_icon_tint_color(LauncherAppGlanceStructured *structured_glance) { @@ -362,7 +356,7 @@ static GTextNode *prv_create_structured_glance_title_subtitle_node( GTextNode *title_node = prv_structured_glance_create_title_text_node(structured_glance); // We require a valid title node PBL_ASSERTN(title_node); - + // Push the margin a bit closer #if PBL_DISPLAY_HEIGHT >= 200 && PBL_RECT title_node->margin.h = -3; diff --git a/src/fw/apps/system/music.c b/src/fw/apps/system/music.c index b6a2d8c04..1cd5fdb93 100644 --- a/src/fw/apps/system/music.c +++ b/src/fw/apps/system/music.c @@ -690,7 +690,9 @@ static void prv_no_music_window_click_config(void *context) { static MusicNoMusicWindow *prv_create_no_music_window(void) { MusicNoMusicWindow *window = app_malloc_check(sizeof(MusicNoMusicWindow)); window_init(&window->window, WINDOW_NAME("NoMusicWindow")); - window_set_background_color(&window->window, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite)); + window_set_background_color(&window->window, PBL_IF_COLOR_ELSE( + system_theme_is_dark_mode() ? GColorDarkGray : GColorLightGray, + GColorWhite)); window_set_window_handlers(&window->window, &(WindowHandlers) { .unload = prv_unload_no_music_window }); @@ -713,7 +715,7 @@ static MusicNoMusicWindow *prv_create_no_music_window(void) { &NO_MUSIC_TEXT_RECT, i18n_get("START PLAYBACK\nON YOUR PHONE", window), fonts_get_system_font(config->no_music_font_key), - GColorBlack, GColorClear, GTextAlignmentCenter, + system_theme_get_fg_color(), GColorClear, GTextAlignmentCenter, GTextOverflowModeTrailingEllipsis); layer_add_child(&window->window.layer, &window->bitmap_layer.layer); layer_add_child(&window->window.layer, &window->text_layer.layer); @@ -834,7 +836,7 @@ static void prv_configure_music_text_layer( TextLayer *text_layer, char* text_buffer, const GRect *rect, int16_t y_offset, GTextAlignment align, GFont font) { text_layer_init_with_parameters(text_layer, rect, text_buffer, font, - GColorBlack, GColorClear, align, GTextOverflowModeFill); + system_theme_get_fg_color(), GColorClear, align, GTextOverflowModeFill); layer_set_bounds(&text_layer->layer, &GRect(0, -y_offset, rect->size.w, rect->size.h + y_offset)); } @@ -842,7 +844,9 @@ static void prv_configure_music_text_layer( static void prv_init_ui(Window *window) { MusicAppData *data = window_get_user_data(window); - window_set_background_color(window, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite)); + window_set_background_color(window, PBL_IF_COLOR_ELSE( + system_theme_is_dark_mode() ? GColorDarkGray : GColorLightGray, + GColorWhite)); const GSize WINDOW_SIZE = window->layer.bounds.size; @@ -896,7 +900,7 @@ static void prv_init_ui(Window *window) { progress_layer_init(&data->track_pos_bar, &track_rect); progress_layer_set_background_color(&data->track_pos_bar, - PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite)); + PBL_IF_COLOR_ELSE(system_theme_get_bg_color(), GColorWhite)); progress_layer_set_foreground_color(&data->track_pos_bar, PBL_IF_COLOR_ELSE(GColorRed, GColorBlack)); progress_layer_set_corner_radius(&data->track_pos_bar, config->track_corner_radius); @@ -915,7 +919,7 @@ static void prv_init_ui(Window *window) { WINDOW_SIZE.w); status_layer_frame.size.w = STATUS_BAR_LAYER_WIDTH; layer_set_frame(&status_layer->layer, &status_layer_frame); - status_bar_layer_set_colors(&data->status_layer, GColorClear, GColorBlack); + status_bar_layer_set_colors(&data->status_layer, GColorClear, system_theme_get_fg_color()); layer_add_child(&data->window.layer, &status_layer->layer); music_get_pos(&data->track_pos, &data->track_length); diff --git a/src/fw/apps/system/notifications.c b/src/fw/apps/system/notifications.c index aabdf8fd7..92e914dd3 100644 --- a/src/fw/apps/system/notifications.c +++ b/src/fw/apps/system/notifications.c @@ -711,10 +711,10 @@ static void prv_window_load(Window *window) { .select_click = prv_select_callback, }); - menu_layer_set_normal_colors(menu_layer, GColorWhite, GColorBlack); + menu_layer_set_normal_colors(menu_layer, system_theme_get_bg_color(), system_theme_get_fg_color()); menu_layer_set_highlight_colors(menu_layer, - PBL_IF_COLOR_ELSE(DEFAULT_NOTIFICATION_COLOR, GColorBlack), - GColorWhite); + PBL_IF_COLOR_ELSE(DEFAULT_NOTIFICATION_COLOR, system_theme_get_fg_color()), + system_theme_get_bg_color()); menu_layer_set_click_config_onto_window(menu_layer, window); menu_layer_set_scroll_wrap_around(menu_layer, shell_prefs_get_menu_scroll_wrap_around_enable()); @@ -730,14 +730,14 @@ static void prv_window_load(Window *window) { &GRect(horizontal_margin, window->layer.bounds.size.h / 2 - 15, window->layer.bounds.size.w - horizontal_margin, window->layer.bounds.size.h / 2), - i18n_get("No notifications", data), font, GColorBlack, - GColorWhite, GTextAlignmentCenter, + i18n_get("No notifications", data), font, system_theme_get_fg_color(), + system_theme_get_bg_color(), GTextAlignmentCenter, GTextOverflowModeTrailingEllipsis); layer_add_child(&window->layer, text_layer_get_layer(text_layer)); #if PBL_ROUND GColor bg_color = GColorClear; - GColor fg_color = GColorBlack; + GColor fg_color = system_theme_get_fg_color(); StatusBarLayer *status_bar = &data->status_bar_layer; status_bar_layer_init(status_bar); diff --git a/src/fw/apps/system/weather/layout.c b/src/fw/apps/system/weather/layout.c index 4abe5bddc..712d61ae6 100644 --- a/src/fw/apps/system/weather/layout.c +++ b/src/fw/apps/system/weather/layout.c @@ -27,6 +27,7 @@ #include "pbl/services/timeline/timeline_resources.h" #include "pbl/services/weather/weather_service.h" #include "pbl/services/weather/weather_types.h" +#include "shell/system_theme.h" #include "system/logging.h" #include "system/passert.h" #include "util/size.h" @@ -318,8 +319,8 @@ static void prv_content_indicator_setup_direction(ContentIndicator *content_indi ContentIndicatorDirection direction) { content_indicator_configure_direction(content_indicator, direction, &(ContentIndicatorConfig) { .layer = indicator_layer, - .colors.foreground = GColorBlack, - .colors.background = GColorWhite, + .colors.foreground = system_theme_get_fg_color(), + .colors.background = system_theme_get_bg_color(), }); } diff --git a/src/fw/apps/system/workout/selection.c b/src/fw/apps/system/workout/selection.c index 26978398a..bd69bc56f 100644 --- a/src/fw/apps/system/workout/selection.c +++ b/src/fw/apps/system/workout/selection.c @@ -176,7 +176,7 @@ WorkoutSelectionWindow *workout_selection_push(SelectWorkoutCallback select_work .draw_row = prv_draw_row_callback, .select_click = prv_select_callback, }); - menu_layer_set_normal_colors(menu_layer, GColorWhite, GColorBlack); + menu_layer_set_normal_colors(menu_layer, system_theme_get_bg_color(), system_theme_get_fg_color()); menu_layer_set_highlight_colors(menu_layer, PBL_IF_COLOR_ELSE(GColorYellow, GColorBlack), PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite)); From 3a1ec48d5bd64925ce4a0ba32cfef73fd1d4b698 Mon Sep 17 00:00:00 2001 From: Andrew McOlash Date: Wed, 22 Apr 2026 14:09:21 -0700 Subject: [PATCH 6/6] fw: Apply dark mode to timeline and notifications Co-Authored-By: Claude Signed-off-by: Andrew McOlash --- src/fw/apps/system/timeline/layer.c | 5 +++-- src/fw/apps/system/timeline/peek_layer.c | 8 +++++++- src/fw/apps/system/timeline/peek_layer.h | 3 +++ src/fw/popups/notifications/notification_window.c | 3 ++- src/fw/popups/timeline/peek.c | 8 ++++---- src/fw/services/timeline/notification_layout.c | 2 +- src/fw/services/timeline/swap_layer.c | 9 +++++++-- src/fw/services/timeline/timeline_layout.c | 3 ++- tests/fw/ui/test_jumboji.c | 1 + 9 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/fw/apps/system/timeline/layer.c b/src/fw/apps/system/timeline/layer.c index 1dfbe758c..f186415bc 100644 --- a/src/fw/apps/system/timeline/layer.c +++ b/src/fw/apps/system/timeline/layer.c @@ -613,7 +613,7 @@ static void prv_update_proc(struct Layer *layer, GContext* ctx) { TimelineLayer *timeline_layer = (TimelineLayer *)layer; const GRect *bounds = &layer->bounds; - graphics_context_set_fill_color(ctx, GColorWhite); + graphics_context_set_fill_color(ctx, system_theme_get_bg_color()); graphics_fill_rect(ctx, &(GRect) { .size = bounds->size }); AnimationProgress progress; @@ -621,7 +621,7 @@ static void prv_update_proc(struct Layer *layer, GContext* ctx) { animation_get_progress(timeline_layer->animation, &progress)) { const GPoint offset = { PEEK_ANIMATIONS_SPEED_LINES_OFFSET_X, interpolate_int64_linear(progress, 0, -DISP_ROWS) }; - graphics_context_set_fill_color(ctx, GColorBlack); + graphics_context_set_fill_color(ctx, system_theme_get_fg_color()); peek_animations_draw_timeline_speed_lines(ctx, offset); } @@ -934,6 +934,7 @@ void timeline_layer_init(TimelineLayer *layer, const GRect *frame_ref, }; peek_layer_set_icon(&layer->day_separator, &timeline_res); peek_layer_set_background_color(&layer->day_separator, GColorClear); + peek_layer_set_text_color(&layer->day_separator, system_theme_get_fg_color()); peek_layer_set_dot_diameter(&layer->day_separator, style->day_sep_dot_diameter); layer_set_hidden((Layer *)&layer->day_separator, true); layer_add_child((Layer *)layer, (Layer *)&layer->day_separator); diff --git a/src/fw/apps/system/timeline/peek_layer.c b/src/fw/apps/system/timeline/peek_layer.c index acc9da329..a932ab4da 100644 --- a/src/fw/apps/system/timeline/peek_layer.c +++ b/src/fw/apps/system/timeline/peek_layer.c @@ -46,7 +46,7 @@ static void prv_update_proc(Layer *layer, GContext *ctx) { } if (peek_layer->show_dot) { - graphics_context_set_fill_color(ctx, GColorBlack); + graphics_context_set_fill_color(ctx, system_theme_get_fg_color()); GRect dot_rect = { .size = { peek_layer->dot_diameter, peek_layer->dot_diameter } }; grect_align(&dot_rect, &peek_layer->layer.bounds, GAlignCenter, false /* clip */); graphics_fill_radial(ctx, dot_rect, GOvalScaleModeFitCircle, peek_layer->dot_diameter, 0, @@ -168,6 +168,12 @@ void peek_layer_set_background_color(PeekLayer *peek_layer, GColor color) { peek_layer->bg_color = color; } +void peek_layer_set_text_color(PeekLayer *peek_layer, GColor color) { + text_layer_set_text_color(&peek_layer->number.text_layer, color); + text_layer_set_text_color(&peek_layer->title.text_layer, color); + text_layer_set_text_color(&peek_layer->subtitle.text_layer, color); +} + static bool prv_is_dot_size(GSize size) { return (size.w <= UNFOLD_DOT_SIZE_PX && size.h <= UNFOLD_DOT_SIZE_PX); } diff --git a/src/fw/apps/system/timeline/peek_layer.h b/src/fw/apps/system/timeline/peek_layer.h index af042038a..621300170 100644 --- a/src/fw/apps/system/timeline/peek_layer.h +++ b/src/fw/apps/system/timeline/peek_layer.h @@ -99,6 +99,9 @@ ImmutableAnimation *peek_layer_create_play_section_animation(PeekLayer *peek_lay //! Set the background color of the peek layer. void peek_layer_set_background_color(PeekLayer *peek_layer, GColor color); +//! Set the text color of all peek layer text fields. +void peek_layer_set_text_color(PeekLayer *peek_layer, GColor color); + //! Sets the text of the peek layer text fields. The text is copied over. //! See the individual text field setters for more information about each field. void peek_layer_set_fields(PeekLayer *peek_layer, const char *number, const char *title, diff --git a/src/fw/popups/notifications/notification_window.c b/src/fw/popups/notifications/notification_window.c index e974fa159..95619da59 100644 --- a/src/fw/popups/notifications/notification_window.c +++ b/src/fw/popups/notifications/notification_window.c @@ -51,6 +51,7 @@ #include "pbl/services/timeline/timeline_resources.h" #include "system/logging.h" #include "system/passert.h" +#include "shell/system_theme.h" #include "util/math.h" #include "util/trig.h" @@ -1167,7 +1168,7 @@ static void prv_layout_did_appear_handler(SwapLayer *swap_layer, LayoutLayer *la static void prv_update_colors_handler(SwapLayer *swap_layer, GColor bg_color, bool status_bar_filled, void *context) { NotificationWindowData *data = context; - GColor status_color = (status_bar_filled) ? bg_color : GColorWhite; + GColor status_color = (status_bar_filled) ? bg_color : system_theme_get_bg_color(); // Status bar is clear on round, because the banner is rendered under it status_bar_layer_set_colors(&data->status_layer, PBL_IF_ROUND_ELSE(GColorClear, status_color), gcolor_legible_over(status_color)); diff --git a/src/fw/popups/timeline/peek.c b/src/fw/popups/timeline/peek.c index df154e2c5..89321650d 100644 --- a/src/fw/popups/timeline/peek.c +++ b/src/fw/popups/timeline/peek.c @@ -47,7 +47,7 @@ static void prv_draw_background(GContext *ctx, const GRect *frame_orig, // Fill all the way to the bottom of the screen frame.size.h = DISP_ROWS - frame.origin.y; #endif - const GColor background_color = GColorWhite; + const GColor background_color = system_theme_get_bg_color(); graphics_context_set_fill_color(ctx, background_color); graphics_fill_rect(ctx, &frame); @@ -59,7 +59,7 @@ static void prv_draw_background(GContext *ctx, const GRect *frame_orig, // Draw the top border and concurrent event indicators frame = *frame_orig; - const GColor border_color = GColorBlack; + const GColor border_color = system_theme_is_dark_mode() ? GColorDarkGray : GColorBlack; for (unsigned int i = 0; i <= num_concurrent; i++) { const bool has_content = (i < num_concurrent); for (unsigned int type = 0; type < (has_content ? 2 : 1); type++) { @@ -421,7 +421,7 @@ static void prv_push_timeline_peek(void *unused) { void timeline_peek_init(void) { TimelinePeek *peek = &s_peek; *peek = (TimelinePeek) { -#if CAPABILITY_HAS_TIMELINE_PEEK && !SHELL_SDK && !TARGET_QEMU +#if CAPABILITY_HAS_TIMELINE_PEEK && !SHELL_SDK .enabled = timeline_peek_prefs_get_enabled(), #endif }; @@ -458,7 +458,7 @@ static bool prv_can_animate(void) { void timeline_peek_set_visible(bool visible, bool animated) { TimelinePeek *peek = &s_peek; -#if !SHELL_SDK && !TARGET_QEMU +#if !SHELL_SDK if (!peek->exists) { visible = false; } diff --git a/src/fw/services/timeline/notification_layout.c b/src/fw/services/timeline/notification_layout.c index 882dbe6ae..67a747897 100644 --- a/src/fw/services/timeline/notification_layout.c +++ b/src/fw/services/timeline/notification_layout.c @@ -483,7 +483,7 @@ static NOINLINE void prv_card_render_internal(NotificationLayout *layout, GConte .text_flow = PBL_IF_ROUND_ELSE(true, false), .paging = PBL_IF_ROUND_ELSE(true, false), }; - graphics_context_set_text_color(ctx, GColorBlack); + graphics_context_set_text_color(ctx, system_theme_get_fg_color()); (text_visible ? graphics_text_node_draw : graphics_text_node_get_size)(layout->view_node, ctx, &box, &config, &layout->view_size); diff --git a/src/fw/services/timeline/swap_layer.c b/src/fw/services/timeline/swap_layer.c index 25b2686bd..50480b399 100644 --- a/src/fw/services/timeline/swap_layer.c +++ b/src/fw/services/timeline/swap_layer.c @@ -14,6 +14,7 @@ #include "pbl/services/timeline/layout_layer.h" #include "pbl/services/timeline/notification_layout.h" #include "pbl/services/notifications/alerts_preferences_private.h" +#include "shell/system_theme.h" #include "kernel/ui/kernel_ui.h" #include "process_state/app_state/app_state.h" #include "system/logging.h" @@ -243,7 +244,7 @@ static void prv_arrow_layer_update_proc(Layer *layer, GContext* ctx) { const GRect *layer_bounds = &layer->bounds; #if PBL_RECT - graphics_context_set_fill_color(ctx, GColorWhite); + graphics_context_set_fill_color(ctx, system_theme_get_bg_color()); graphics_fill_rect(ctx, layer_bounds); #endif @@ -261,7 +262,11 @@ static void prv_arrow_layer_update_proc(Layer *layer, GContext* ctx) { #if UNITTEST const GCompOp compositing_mode = GCompOpSet; #else - const GCompOp compositing_mode = PBL_IF_COLOR_ELSE(GCompOpSet, GCompOpAssign); + // Arrow color is inverted in dark mode + const GCompOp compositing_mode = PBL_IF_COLOR_ELSE( + system_theme_is_dark_mode() ? GCompOpTint : GCompOpSet, + GCompOpAssign + ); #endif graphics_context_set_compositing_mode(ctx, compositing_mode); diff --git a/src/fw/services/timeline/timeline_layout.c b/src/fw/services/timeline/timeline_layout.c index cc7bf54e6..80d5787b1 100644 --- a/src/fw/services/timeline/timeline_layout.c +++ b/src/fw/services/timeline/timeline_layout.c @@ -659,7 +659,8 @@ static void prv_render_view(TimelineLayout *layout, GContext *ctx, bool render, GRect box; (is_card ? prv_get_card_view_bounds : prv_get_pin_view_bounds)(layout, &box); graphics_context_set_text_color( - ctx, (is_card ? layout_get_colors((LayoutLayer *)layout)->primary_color : GColorBlack)); + ctx, (is_card ? layout_get_colors((LayoutLayer *)layout)->primary_color : + system_theme_get_fg_color())); static const GRect page_frame_on_screen = { { 0, STATUS_BAR_LAYER_HEIGHT }, { DISP_COLS, DISP_ROWS - STATUS_BAR_LAYER_HEIGHT } }; const GTextNodeDrawConfig config = { diff --git a/tests/fw/ui/test_jumboji.c b/tests/fw/ui/test_jumboji.c index 3858d61b5..844452fbe 100644 --- a/tests/fw/ui/test_jumboji.c +++ b/tests/fw/ui/test_jumboji.c @@ -26,6 +26,7 @@ #include "stubs_pin_db.h" #include "stubs_resources.h" #include "stubs_shell_prefs.h" +#include "stubs_system_theme.h" #include "stubs_text_node.h" #include "stubs_timeline_item.h" #include "stubs_timeline_resources.h"