/* SPDX-FileCopyrightText: 2024 Google LLC */ /* SPDX-License-Identifier: Apache-2.0 */ #include "neat_bar.h" #include #define MAX(a, b) (((a) > (b)) ? (a) : (b)) #define ABS(a) (((a) < 0) ? -(a) : (a)) #if PBL_DISPLAY_HEIGHT >= 260 && PBL_ROUND #define DEFAULT_SPREAD 41 #define SELECT_Y 118 // #elif PBL_DISPLAY_HEIGHT >= 228 && PBL_RECT #define DEFAULT_SPREAD 60 #define SELECT_Y 102 // #elif PBL_DISPLAY_HEIGHT >= 180 && PBL_ROUND #define DEFAULT_SPREAD 28 #define SELECT_Y 78 // #else // all 144x168 watches #define DEFAULT_SPREAD 51 #define SELECT_Y 72 #endif #if PBL_ROUND #define USE_ROUND_BAR_BY_DEFAULT true #define DEFAULT_ANIM_DISTANCE 4 #define DEFAULT_ICONS_X_OFFSET -1 #else #define USE_ROUND_BAR_BY_DEFAULT false #define DEFAULT_ANIM_DISTANCE 5 #define DEFAULT_ICONS_X_OFFSET 1 #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 total_y_offset = neat_bar->main_grect.origin.y - neat_bar->icons_y_offset; int8_t spread = neat_bar->icons_spread; uint16_t width = neat_bar->main_grect.size.w; switch (button_index) { case BUTTON_ID_UP: return GRect(neat_bar->icons_x_offset, SELECT_Y - spread - total_y_offset, width, 25); case BUTTON_ID_SELECT: return GRect(neat_bar->icons_x_offset, SELECT_Y - total_y_offset, width, 25); case BUTTON_ID_DOWN: return GRect(neat_bar->icons_x_offset, SELECT_Y + spread - total_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; } 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) { if (s_current_neat_bar->bar_use_round_style) { const uint32_t action_bar_circle_diameter = PBL_DISPLAY_HEIGHT * 19 / 9; GRect neat_bar_circle_frame = (GRect){.size = GSize(action_bar_circle_diameter, action_bar_circle_diameter)}; GRect bounds = layer_get_bounds(layer); grect_align(&neat_bar_circle_frame, &bounds, GAlignLeft, false); graphics_context_set_fill_color(ctx, s_current_neat_bar->background_color); const int32_t inset_thickness = MAX(ABS(neat_bar_circle_frame.size.h), ABS(neat_bar_circle_frame.size.w)); graphics_fill_radial(ctx, neat_bar_circle_frame, GOvalScaleModeFitCircle, inset_thickness, 0, TRIG_MAX_ANGLE); } else { graphics_context_set_antialiased(ctx, false); graphics_context_set_fill_color(ctx, s_current_neat_bar->background_color); graphics_fill_rect(ctx, layer_get_bounds(layer), s_current_neat_bar->rect_bar_corner_radius, s_current_neat_bar->bar_x_offset > 0 ? GCornersAll : GCornersLeft); } } 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; if (neat_bar->animations_enabled) { 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 total_y_offset = neat_bar->main_grect.origin.y - neat_bar->icons_y_offset; int8_t spread = neat_bar->icons_spread; if (neat_bar->up_layer) { layer_set_frame(bitmap_layer_get_layer(neat_bar->up_layer), GRect(neat_bar->icons_x_offset, SELECT_Y - spread - total_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(neat_bar->icons_x_offset, SELECT_Y - total_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(neat_bar->icons_x_offset, SELECT_Y + spread - total_y_offset, neat_bar->main_grect.size.w, 25)); } } 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); } // default: true void neat_bar_layer_set_animations_enabled(NeatBarLayer *neat_bar, bool enabled) { if (enabled == neat_bar->animations_enabled) { return; } neat_bar->animations_enabled = enabled; } // 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: 5 (rect), 4 (round) 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: 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: false (rect), true (round) void neat_bar_layer_set_bar_style(NeatBarLayer *neat_bar, bool use_round_style) { if (use_round_style == neat_bar->bar_use_round_style) { return; } neat_bar->bar_use_round_style = use_round_style; layer_mark_dirty(neat_bar->main_layer); } // default: 0 void neat_bar_layer_set_rect_bar_floating(NeatBarLayer *neat_bar, uint16_t x_offset) { if (x_offset == neat_bar->bar_x_offset) { return; } neat_bar->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: 0 void neat_bar_layer_set_rect_bar_corner_radius(NeatBarLayer *neat_bar, uint16_t corner_radius) { if (corner_radius == neat_bar->rect_bar_corner_radius) { return; } neat_bar->rect_bar_corner_radius = corner_radius; layer_mark_dirty(neat_bar->main_layer); } // default: ACTION_BAR_WIDTH void neat_bar_layer_set_bar_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->bar_x_offset > 0) { uint16_t x = neat_bar->bar_x_offset; neat_bar->bar_x_offset = 0; // force update neat_bar_layer_set_rect_bar_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_bar_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_bar_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: 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: DEFAULT_SPREAD void neat_bar_layer_set_icons_spread(NeatBarLayer *neat_bar, int8_t spread) { if (spread == neat_bar->icons_spread) { return; } neat_bar->icons_spread = spread; s_recenter_icons(neat_bar); } // default: 1 (rect), -1 (round) void neat_bar_layer_set_icons_x_offset(NeatBarLayer *neat_bar, int8_t x_offset) { if (x_offset == neat_bar->icons_x_offset) { return; } neat_bar->icons_x_offset = x_offset; s_recenter_icons(neat_bar); } // default: 0 void neat_bar_layer_set_icons_y_offset(NeatBarLayer *neat_bar, int8_t y_offset) { if (y_offset == neat_bar->icons_y_offset) { return; } neat_bar->icons_y_offset = y_offset; 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; switch (button_id) { case BUTTON_ID_UP: current_bitmap_layer = neat_bar->up_layer; break; case BUTTON_ID_DOWN: current_bitmap_layer = neat_bar->down_layer; break; default: current_bitmap_layer = neat_bar->select_layer; } if (!current_bitmap_layer) { int16_t total_y_offset = neat_bar->main_grect.origin.y - neat_bar->icons_y_offset; int16_t adjusted_y = (button_id == BUTTON_ID_UP) ? SELECT_Y - neat_bar->icons_spread - total_y_offset : (button_id == BUTTON_ID_DOWN) ? SELECT_Y + neat_bar->icons_spread - total_y_offset : SELECT_Y - total_y_offset; current_bitmap_layer = bitmap_layer_create(GRect(neat_bar->icons_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)); // layerage 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; // stock cfg/plumbing neat_bar->click_config_provider = NULL; neat_bar->background_color = GColorBlack; neat_bar->context = NULL; // neat_bar exclusives :D neat_bar->bar_use_round_style = USE_ROUND_BAR_BY_DEFAULT; neat_bar->rect_bar_corner_radius = 0; neat_bar->bar_x_offset = 0; neat_bar->icons_spread = DEFAULT_SPREAD; neat_bar->icons_x_offset = DEFAULT_ICONS_X_OFFSET; neat_bar->icons_y_offset = 0; neat_bar->animations_enabled = true; neat_bar->animations_move_on_y_axis = false; neat_bar->animations_move_distance = DEFAULT_ANIM_DISTANCE; neat_bar->animations_duration = 144; // state tracking neat_bar->up_pressed = false; neat_bar->select_pressed = false; neat_bar->down_pressed = false; 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); }