/* SPDX-FileCopyrightText: 2024 Google LLC */ /* SPDX-License-Identifier: Apache-2.0 */ #include "neat_bar.h" #include #if PBL_DISPLAY_WIDTH >= 260 && PBL_ROUND const int8_t ICON_X_OFFSET = -1; const uint16_t UP_Y = 76; const uint16_t SELECT_Y = 118; const uint16_t DOWN_Y = 158; #elif PBL_DISPLAY_WIDTH >= 200 && PBL_RECT const int8_t ICON_X_OFFSET = 0; const uint16_t UP_Y = 41; const uint16_t SELECT_Y = 102; const uint16_t DOWN_Y = 161; #elif PBL_DISPLAY_WIDTH >= 180 && PBL_ROUND const int8_t ICON_X_OFFSET = -1; const uint16_t UP_Y = 49; const uint16_t SELECT_Y = 78; const uint16_t DOWN_Y = 105; #else // all 144x168 watches const int8_t ICON_X_OFFSET = 0; const uint16_t UP_Y = 26; const uint16_t SELECT_Y = 72; const uint16_t DOWN_Y = 117; #endif #if PBL_ROUND #define ANIM_DISTANCE 4 #else #define ANIM_DISTANCE 5 #endif static const GRect s_base_grect_rect = GRect(PBL_DISPLAY_WIDTH - ACTION_BAR_WIDTH, 0, ACTION_BAR_WIDTH, PBL_DISPLAY_HEIGHT); static NeatBarLayer *s_current_neat_bar = NULL; static GRect s_get_home_frame(NeatBarLayer *neat_bar, uint8_t button_index) { int16_t y_offset = neat_bar->main_grect.origin.y; int8_t spread = neat_bar->spread; uint16_t width = neat_bar->main_grect.size.w; switch (button_index) { case BUTTON_ID_UP: return GRect(ICON_X_OFFSET, UP_Y - spread - y_offset, width, 25); case BUTTON_ID_SELECT: return GRect(ICON_X_OFFSET, SELECT_Y - y_offset, width, 25); case BUTTON_ID_DOWN: return GRect(ICON_X_OFFSET, DOWN_Y + spread - y_offset, width, 25); default: return GRectZero; } } // Performs a press or release animation for a single button, anchored to its // home position. Duplicate events (press when already pressed, release when // already released) are ignored so overlapping animations cannot accumulate // drift. static void s_animate_icon(NeatBarLayer *neat_bar, uint8_t button_index, bool is_press) { BitmapLayer *bitmap_layer; bool *pressed; switch (button_index) { case BUTTON_ID_UP: bitmap_layer = neat_bar->up_layer; pressed = &neat_bar->up_pressed; break; case BUTTON_ID_SELECT: bitmap_layer = neat_bar->select_layer; pressed = &neat_bar->select_pressed; break; case BUTTON_ID_DOWN: bitmap_layer = neat_bar->down_layer; pressed = &neat_bar->down_pressed; break; default: return; } if (!bitmap_layer) return; if (is_press) { if (*pressed) return; *pressed = true; } else { if (!*pressed) return; *pressed = false; } // legacy animation: no icon movement, just invert the background third if (neat_bar->animations_legacy_pressed_color) { layer_mark_dirty(neat_bar->main_layer); return; } Layer *layer = bitmap_layer_get_layer(bitmap_layer); GRect home = s_get_home_frame(neat_bar, button_index); // off = home shifted by -distance (the "pressed" position) GRect off = home; int16_t distance = neat_bar->animations_move_distance; if (neat_bar->animations_move_on_y_axis) { off.origin.y -= distance; } else { off.origin.x -= distance; } GRect from = is_press ? home : off; GRect to = is_press ? off : home; PropertyAnimation *prop_anim = property_animation_create_layer_frame(layer, &from, &to); Animation *anim = property_animation_get_animation(prop_anim); animation_set_duration(anim, neat_bar->animations_duration); animation_schedule(anim); } static void s_icon_down_handler(NeatBarLayer *neat_bar, uint8_t button_index) { s_animate_icon(neat_bar, button_index, true); } static void s_icon_up_handler(NeatBarLayer *neat_bar, uint8_t button_index) { s_animate_icon(neat_bar, button_index, false); } static void s_up_down_handler(ClickRecognizerRef recognizer, void *context) { s_icon_down_handler(context, BUTTON_ID_UP); } static void s_up_up_handler(ClickRecognizerRef recognizer, void *context) { s_icon_up_handler(context, BUTTON_ID_UP); } static void s_down_down_handler(ClickRecognizerRef recognizer, void *context) { s_icon_down_handler(context, BUTTON_ID_DOWN); } static void s_down_up_handler(ClickRecognizerRef recognizer, void *context) { s_icon_up_handler(context, BUTTON_ID_DOWN); } static void s_select_down_handler(ClickRecognizerRef recognizer, void *context) { s_icon_down_handler(context, BUTTON_ID_SELECT); } static void s_select_up_handler(ClickRecognizerRef recognizer, void *context) { s_icon_up_handler(context, BUTTON_ID_SELECT); } static void s_main_layer_update_proc(Layer *layer, GContext *ctx) { NeatBarLayer *neat_bar = s_current_neat_bar; GRect bounds = layer_get_bounds(layer); graphics_context_set_antialiased(ctx, false); graphics_context_set_fill_color(ctx, neat_bar->background_color); graphics_fill_rect(ctx, bounds, neat_bar->corner_radius, neat_bar->x_offset > 0 ? GCornersAll : GCornersLeft); // Legacy animation: invert the background third for any pressed button if (neat_bar->animations_legacy_pressed_color) { int16_t third_height = bounds.size.h / 3; graphics_context_set_fill_color(ctx, *neat_bar->animations_legacy_pressed_color); if (neat_bar->up_pressed) { graphics_fill_rect(ctx, GRect(1, 1, bounds.size.w - 2, third_height - 2), 0, GCornerNone); } if (neat_bar->select_pressed) { graphics_fill_rect(ctx, GRect(1, third_height + 1, bounds.size.w - 2, third_height - 2), 0, GCornerNone); } if (neat_bar->down_pressed) { graphics_fill_rect(ctx, GRect(1, third_height * 2 + 1, bounds.size.w - 2, bounds.size.h - third_height * 2 - 2), 0, GCornerNone); } } } static void s_base_click_config_provider(void *config_context) { NeatBarLayer *neat_bar = config_context; void *context = neat_bar->context ? neat_bar->context : neat_bar; window_raw_click_subscribe(BUTTON_ID_UP, s_up_down_handler, s_up_up_handler, neat_bar); window_raw_click_subscribe(BUTTON_ID_DOWN, s_down_down_handler, s_down_up_handler, neat_bar); window_raw_click_subscribe(BUTTON_ID_SELECT, s_select_down_handler, s_select_up_handler, neat_bar); window_set_click_context(BUTTON_ID_UP, context); window_set_click_context(BUTTON_ID_DOWN, context); window_set_click_context(BUTTON_ID_SELECT, context); if (neat_bar->click_config_provider) { neat_bar->click_config_provider(context); } } inline static void s_update_click_config_provider(NeatBarLayer *neat_bar) { if (neat_bar->window) { window_set_click_config_provider_with_context(neat_bar->window, s_base_click_config_provider, neat_bar); } } static void s_recenter_icons(NeatBarLayer *neat_bar) { int16_t y_offset = neat_bar->main_grect.origin.y; int8_t spread = neat_bar->spread; if (neat_bar->up_layer) { layer_set_frame(bitmap_layer_get_layer(neat_bar->up_layer), GRect(ICON_X_OFFSET, UP_Y - spread - y_offset, neat_bar->main_grect.size.w, 25)); } if (neat_bar->select_layer) { layer_set_frame(bitmap_layer_get_layer(neat_bar->select_layer), GRect(ICON_X_OFFSET, SELECT_Y - y_offset, neat_bar->main_grect.size.w, 25)); } if (neat_bar->down_layer) { layer_set_frame(bitmap_layer_get_layer(neat_bar->down_layer), GRect(ICON_X_OFFSET, DOWN_Y + spread - y_offset, neat_bar->main_grect.size.w, 25)); } } // match stock rectangular aesthetic (except for icon x offset) void neat_bar_layer_set_theme_default_rect(NeatBarLayer *neat_bar) { neat_bar_layer_set_floating(neat_bar, false); neat_bar->main_grect = s_base_grect_rect; layer_set_frame(neat_bar->main_layer, neat_bar->main_grect); neat_bar_layer_set_corner_radius(neat_bar, 0); s_recenter_icons(neat_bar); } // match SDK 2.X aesthetic void neat_bar_layer_set_theme_legacy_rect(NeatBarLayer *neat_bar) { neat_bar_layer_set_floating(neat_bar, false); neat_bar->main_grect = GRect(s_base_grect_rect.origin.x, 4, s_base_grect_rect.size.w, s_base_grect_rect.size.h - 8); layer_set_frame(neat_bar->main_layer, neat_bar->main_grect); neat_bar_layer_set_corner_radius(neat_bar, 3); s_recenter_icons(neat_bar); } void neat_bar_layer_add_to_window(NeatBarLayer *neat_bar, Window *window) { layer_add_child(window_get_root_layer(window), neat_bar->main_layer); neat_bar->window = window; s_update_click_config_provider(neat_bar); } void neat_bar_layer_set_click_config_provider(NeatBarLayer *neat_bar, ClickConfigProvider click_config_provider) { neat_bar->click_config_provider = click_config_provider; s_update_click_config_provider(neat_bar); } void neat_bar_layer_set_animations_move_distance(NeatBarLayer *neat_bar, int8_t distance) { if (distance == neat_bar->animations_move_distance) { return; } neat_bar->animations_move_distance = distance; } // default: false void neat_bar_layer_set_animations_move_axis(NeatBarLayer *neat_bar, bool use_y_axis) { if (use_y_axis == neat_bar->animations_move_on_y_axis) { return; } neat_bar->animations_move_on_y_axis = use_y_axis; } // default: 144 void neat_bar_layer_set_animations_duration(NeatBarLayer *neat_bar, uint8_t duration_ms) { if (duration_ms == neat_bar->animations_duration) { return; } neat_bar->animations_duration = duration_ms; } // default: 0 void neat_bar_layer_set_floating(NeatBarLayer *neat_bar, uint16_t x_offset) { if (x_offset == neat_bar->x_offset) { return; } neat_bar->x_offset = x_offset; neat_bar->main_grect.origin.x = PBL_DISPLAY_WIDTH - neat_bar->main_grect.size.w - x_offset; layer_set_frame(neat_bar->main_layer, neat_bar->main_grect); layer_mark_dirty(neat_bar->main_layer); } // default: ACTION_BAR_WIDTH void neat_bar_layer_set_width(NeatBarLayer *neat_bar, uint16_t width) { if (width == neat_bar->main_grect.size.w) { return; } neat_bar->main_grect.origin.x = PBL_DISPLAY_WIDTH - width; neat_bar->main_grect.size.w = width; layer_set_frame(neat_bar->main_layer, neat_bar->main_grect); if (neat_bar->x_offset > 0) { uint16_t x = neat_bar->x_offset; neat_bar->x_offset = 0; // force update neat_bar_layer_set_floating(neat_bar, x); } else { layer_mark_dirty(neat_bar->main_layer); } s_recenter_icons(neat_bar); } // default: PBL_DISPLAY_HEIGHT void neat_bar_layer_set_height(NeatBarLayer *neat_bar, uint16_t height) { if (height == neat_bar->main_grect.size.h) { return; } neat_bar->main_grect.size.h = height; layer_set_frame(neat_bar->main_layer, neat_bar->main_grect); layer_mark_dirty(neat_bar->main_layer); } // default: 0 void neat_bar_layer_set_y_offset(NeatBarLayer *neat_bar, uint16_t y_offset) { if (y_offset == neat_bar->main_grect.origin.y) { return; } neat_bar->main_grect.origin.y = y_offset; layer_set_frame(neat_bar->main_layer, neat_bar->main_grect); layer_mark_dirty(neat_bar->main_layer); s_recenter_icons(neat_bar); } // default: 0 void neat_bar_layer_set_corner_radius(NeatBarLayer *neat_bar, uint16_t corner_radius) { if (corner_radius == neat_bar->corner_radius) { return; } neat_bar->corner_radius = corner_radius; layer_mark_dirty(neat_bar->main_layer); } // default: GColorBlack void neat_bar_layer_set_background_color(NeatBarLayer *neat_bar, GColor background_color) { if (gcolor_equal(background_color, neat_bar->background_color)) { return; } neat_bar->background_color = background_color; layer_mark_dirty(neat_bar->main_layer); } // default: NULL (null disables legacy animations) void neat_bar_layer_set_animations_type(NeatBarLayer *neat_bar, GColor8 *pressed_color) { if (pressed_color->argb == neat_bar->animations_legacy_pressed_color->argb) { return; } neat_bar->animations_legacy_pressed_color = pressed_color; layer_mark_dirty(neat_bar->main_layer); } // default: 0 void neat_bar_layer_set_spread(NeatBarLayer *neat_bar, int8_t spread) { if (spread == neat_bar->spread) { return; } neat_bar->spread = spread; s_recenter_icons(neat_bar); } // constraints: must be <=25px in height (width is only constrained to the width of the bar) void neat_bar_layer_set_icon(NeatBarLayer *neat_bar, ButtonId button_id, GBitmap *icon) { BitmapLayer *current_bitmap_layer; uint16_t y_pos; switch (button_id) { case BUTTON_ID_UP: current_bitmap_layer = neat_bar->up_layer; y_pos = UP_Y; break; case BUTTON_ID_DOWN: current_bitmap_layer = neat_bar->down_layer; y_pos = DOWN_Y; break; default: current_bitmap_layer = neat_bar->select_layer; y_pos = SELECT_Y; } if (!current_bitmap_layer) { int16_t adjusted_y = (button_id == BUTTON_ID_UP) ? y_pos - neat_bar->spread : (button_id == BUTTON_ID_DOWN) ? y_pos + neat_bar->spread : y_pos; current_bitmap_layer = bitmap_layer_create(GRect(ICON_X_OFFSET, adjusted_y, neat_bar->main_grect.size.w, 25)); layer_add_child(neat_bar->main_layer, bitmap_layer_get_layer(current_bitmap_layer)); switch (button_id) { case BUTTON_ID_UP: neat_bar->up_layer = current_bitmap_layer; break; case BUTTON_ID_DOWN: neat_bar->down_layer = current_bitmap_layer; break; default: neat_bar->select_layer = current_bitmap_layer; break; } } bitmap_layer_set_alignment(current_bitmap_layer, GAlignCenter); bitmap_layer_set_bitmap(current_bitmap_layer, icon); #if PBL_COLOR bitmap_layer_set_compositing_mode(current_bitmap_layer, GCompOpSet); #endif } NeatBarLayer *neat_bar_layer_create() { NeatBarLayer *neat_bar = malloc(sizeof(NeatBarLayer)); neat_bar->main_grect = s_base_grect_rect; neat_bar->main_layer = layer_create(s_base_grect_rect); neat_bar->up_layer = NULL; neat_bar->down_layer = NULL; neat_bar->select_layer = NULL; neat_bar->click_config_provider = NULL; neat_bar->background_color = GColorBlack; neat_bar->corner_radius = 0; neat_bar->x_offset = 0; neat_bar->spread = 0; neat_bar->animations_duration = 144; neat_bar->animations_move_on_y_axis = false; neat_bar->animations_move_distance = ANIM_DISTANCE; neat_bar->animations_legacy_pressed_color = NULL; neat_bar->up_pressed = false; neat_bar->select_pressed = false; neat_bar->down_pressed = false; neat_bar->context = NULL; layer_set_update_proc(neat_bar->main_layer, s_main_layer_update_proc); s_current_neat_bar = neat_bar; return neat_bar; } void neat_bar_layer_destroy(NeatBarLayer *neat_bar) { if (neat_bar == NULL) { return; } bitmap_layer_destroy(neat_bar->down_layer); bitmap_layer_destroy(neat_bar->select_layer); bitmap_layer_destroy(neat_bar->up_layer); layer_destroy(neat_bar->main_layer); if (s_current_neat_bar == neat_bar) { s_current_neat_bar = NULL; } free(neat_bar); }