Files
pebble/src/fw/applib/ui/action_bar_layer.c
2025-01-27 11:38:16 -08:00

397 lines
15 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "action_bar_layer.h"
#include "animation.h"
#include "animation_timing.h"
#include "applib/app_timer.h"
#include "applib/applib_malloc.auto.h"
#include "applib/graphics/graphics.h"
#include "applib/ui/window_private.h"
#include "process_management/process_manager.h"
#include "system/passert.h"
#include "util/trig.h"
const int16_t MAX_ICON_HEIGHT = 18;
const int64_t PRESS_ANIMATION_DURATION_MS = 144;
const int64_t ICON_CHANGE_ANIMATION_DURATION_MS = 144;
const int16_t ICON_CHANGE_OFFSET[NUM_ACTION_BAR_ITEMS] = {-5, 0, 5};
const uint32_t MILLISECONDS_PER_FRAME = 1000 / 30;
static int prv_width(void) {
const PlatformType platform = process_manager_current_platform();
return _ACTION_BAR_WIDTH(platform);
}
static int prv_vertical_icon_margin(void) {
const PlatformType platform = process_manager_current_platform();
return PBL_PLATFORM_SWITCH(platform,
/*aplite*/ 24,
/*basalt*/ 24,
/*chalk*/ 53,
/*diorite*/ 24,
/*emery*/ 45);
}
static int prv_press_animation_offset(void) {
const PlatformType platform = process_manager_current_platform();
return PBL_PLATFORM_SWITCH(platform,
/*aplite*/ 5,
/*basalt*/ 5,
/*chalk*/ 4,
/*diorite*/ 5,
/*emery*/ 5);
}
// TODO: Once PBL-16032 is implemented, use that instead.
static int64_t prv_get_precise_time(void) {
time_t seconds;
uint16_t milliseconds;
time_ms(&seconds, &milliseconds);
return (seconds * 1000) + milliseconds;
}
inline static bool action_bar_is_highlighted(ActionBarLayer *action_bar, uint8_t index) {
PBL_ASSERTN(index < NUM_ACTION_BAR_ITEMS);
return (bool) (action_bar->is_highlighted & (1 << index));
}
static void prv_register_redraw_timer(ActionBarLayer *layer);
static void prv_timed_redraw(void *context) {
ActionBarLayer *action_bar = context;
layer_mark_dirty(&action_bar->layer);
action_bar->redraw_timer = NULL;
int64_t now = prv_get_precise_time();
for (int i = 0; i < NUM_ACTION_BAR_ITEMS; ++i) {
if ((action_bar->state_change_times[i] != 0 &&
(now - action_bar->state_change_times[i]) <= PRESS_ANIMATION_DURATION_MS) ||
(action_bar->icon_change_times[i] != 0 &&
(now - action_bar->icon_change_times[i]) <= ICON_CHANGE_ANIMATION_DURATION_MS)) {
prv_register_redraw_timer(action_bar);
return;
}
}
}
static void prv_register_redraw_timer(ActionBarLayer *action_bar) {
if (!action_bar->redraw_timer) {
action_bar->redraw_timer = app_timer_register(MILLISECONDS_PER_FRAME, prv_timed_redraw,
action_bar);
}
}
inline static void action_bar_set_highlighted(ActionBarLayer *action_bar, uint8_t index, bool highlighted) {
PBL_ASSERT(index < NUM_ACTION_BAR_ITEMS, "Index: %"PRIu8, index);
const uint8_t bit = (1 << index);
if (action_bar_is_highlighted(action_bar, index) == highlighted) {
return;
}
if (highlighted) {
action_bar->is_highlighted |= bit;
} else {
action_bar->is_highlighted &= ~bit;
prv_register_redraw_timer(action_bar);
}
action_bar->state_change_times[index] = prv_get_precise_time();
layer_mark_dirty(&action_bar->layer);
}
void action_bar_changed_proc(ActionBarLayer *action_bar, GContext* ctx) {
if (action_bar->layer.window && action_bar->layer.window->on_screen == false) {
// clear first, fixes issue of returning from other page while highlighted
for (int i = 0; i < NUM_ACTION_BAR_ITEMS; i++) {
action_bar_set_highlighted(action_bar, i, false);
}
}
}
static GPoint prv_offset_since_time(int64_t time_ms, int64_t duration_ms,
GPoint max_offset) {
if (time_ms == 0) {
return GPointZero;
}
const int64_t delta_ms = prv_get_precise_time() - time_ms;
if (delta_ms >= duration_ms) {
return GPointZero;
}
const uint32_t normalized_time = (delta_ms * ANIMATION_NORMALIZED_MAX) / duration_ms;
const uint32_t normalized_distance = animation_timing_curve(normalized_time,
AnimationCurveEaseOut);
const GPoint real_offset = GPoint(
max_offset.x - ((normalized_distance * max_offset.x) / ANIMATION_NORMALIZED_MAX),
max_offset.y - ((normalized_distance * max_offset.y) / ANIMATION_NORMALIZED_MAX));
return real_offset;
}
static GPoint prv_get_button_press_offset(ActionBarLayer *action_bar, uint8_t button_index) {
const int16_t animation_offset = prv_press_animation_offset();
const GPoint offset[5] = {
GPointZero,
GPoint(-animation_offset, 0),
GPoint(0, -animation_offset),
GPoint(animation_offset, 0),
GPoint(0, animation_offset),
};
return offset[action_bar->animation[button_index]];
}
static void prv_draw_background_rect(ActionBarLayer *action_bar, GContext *ctx, GColor bg_color) {
graphics_fill_rect(ctx, &action_bar->layer.bounds);
}
void prv_draw_background_round(ActionBarLayer *action_bar, GContext *ctx, GColor bg_color) {
const uint32_t action_bar_circle_diameter = DISP_ROWS * 19 / 9;
GRect action_bar_circle_frame = (GRect) {
.size = GSize(action_bar_circle_diameter, action_bar_circle_diameter)
};
grect_align(&action_bar_circle_frame, &action_bar->layer.bounds, GAlignLeft, false /* clips */);
graphics_fill_oval(ctx, action_bar_circle_frame, GOvalScaleModeFitCircle);
}
void action_bar_update_proc(ActionBarLayer *action_bar, GContext* ctx) {
const GColor bg_color = action_bar->background_color;
if (!gcolor_is_transparent(bg_color)) {
graphics_context_set_fill_color(ctx, bg_color);
PBL_IF_RECT_ELSE(prv_draw_background_rect,
prv_draw_background_round)(action_bar, ctx, bg_color);
}
for (unsigned int index = 0; index < NUM_ACTION_BAR_ITEMS; ++index) {
const GBitmap *icon = action_bar->icons[index];
if (icon) {
GRect rect = GRect(1, 0, prv_width(), MAX_ICON_HEIGHT);
const int button_id = index + 1;
const int vertical_icon_margin = prv_vertical_icon_margin();
switch (button_id) {
case BUTTON_ID_UP:
rect.origin.y = vertical_icon_margin;
break;
case BUTTON_ID_SELECT:
rect.origin.y = (action_bar->layer.bounds.size.h / 2) - (rect.size.h / 2);
break;
case BUTTON_ID_DOWN:
rect.origin.y = action_bar->layer.bounds.size.h - vertical_icon_margin -
rect.size.h;
break;
default:
WTF;
}
// In order to avoid creating relatively heavy animations, we instead just base our drawing
// directly on time. The time is set when the animation should start; we convert the delta
// since then into an offset and apply that to our rendering.
GPoint offset;
if (action_bar_is_highlighted(action_bar, index)) {
offset = prv_get_button_press_offset(action_bar, index);
} else {
const int64_t state_change_time = action_bar->state_change_times[index];
offset = prv_offset_since_time(state_change_time, PRESS_ANIMATION_DURATION_MS,
prv_get_button_press_offset(action_bar, index));
}
const int64_t icon_change_time = action_bar->icon_change_times[index];
offset = gpoint_add(offset, prv_offset_since_time(icon_change_time,
ICON_CHANGE_ANIMATION_DURATION_MS,
GPoint(0, ICON_CHANGE_OFFSET[index])));
GRect icon_rect = icon->bounds;
const bool clip = true;
grect_align(&icon_rect, &rect, GAlignCenter, clip);
#if PBL_ROUND
// Offset needed because the new curvature of the action bar makes the icons look off-center
const int32_t icon_horizontal_offset = -2;
icon_rect.origin.x += icon_horizontal_offset;
#endif
icon_rect.origin.x += offset.x;
icon_rect.origin.y += offset.y;
// We use GCompOpAssign on 1-bit images, because they still support the old operations.
// We use GCompOpSet otherwise to ensure we support transparency.
if (gbitmap_get_format(icon) == GBitmapFormat1Bit) {
graphics_context_set_compositing_mode(ctx, GCompOpAssign);
} else {
graphics_context_set_compositing_mode(ctx, GCompOpSet);
}
graphics_draw_bitmap_in_rect(ctx, (GBitmap*)icon, &icon_rect);
}
}
}
void action_bar_layer_init(ActionBarLayer *action_bar) {
*action_bar = (ActionBarLayer){};
layer_set_clips(&action_bar->layer, true);
action_bar->layer.update_proc = (LayerUpdateProc) action_bar_update_proc;
action_bar->layer.property_changed_proc =
(PropertyChangedProc) action_bar_changed_proc;
action_bar->background_color = GColorBlack;
for (unsigned int i = 0; i < NUM_ACTION_BAR_ITEMS; ++i) {
action_bar->animation[i] = ActionBarLayerIconPressAnimationMoveLeft;
}
}
ActionBarLayer* action_bar_layer_create(void) {
ActionBarLayer* layer = applib_type_malloc(ActionBarLayer);
if (layer) {
action_bar_layer_init(layer);
}
return layer;
}
void action_bar_layer_deinit(ActionBarLayer *action_bar_layer) {
if (action_bar_layer->redraw_timer) {
app_timer_cancel(action_bar_layer->redraw_timer);
}
layer_deinit(&action_bar_layer->layer);
}
void action_bar_layer_destroy(ActionBarLayer *action_bar_layer) {
if (action_bar_layer == NULL) {
return;
}
action_bar_layer_deinit(action_bar_layer);
applib_free(action_bar_layer);
}
Layer* action_bar_layer_get_layer(ActionBarLayer *action_bar_layer) {
return &action_bar_layer->layer;
}
inline static void* action_bar_get_context(ActionBarLayer *action_bar) {
return action_bar->context ? action_bar->context : action_bar;
}
void action_bar_layer_set_context(ActionBarLayer *action_bar, void *context) {
action_bar->context = context;
}
static void action_bar_raw_up_down_handler(ClickRecognizerRef recognizer, ActionBarLayer *action_bar, bool is_highlighted) {
const ButtonId button_id = click_recognizer_get_button_id(recognizer);
const uint8_t index = button_id - 1;
// is_highlighted will cause the icon in the action bar to render normal or inverted:
action_bar_set_highlighted(action_bar, index, is_highlighted);
}
static void action_bar_raw_up_handler(ClickRecognizerRef recognizer, void *context) {
ActionBarLayer *action_bar = (ActionBarLayer *)context;
action_bar_raw_up_down_handler(recognizer, action_bar, false);
}
static void action_bar_raw_down_handler(ClickRecognizerRef recognizer, void *context) {
ActionBarLayer *action_bar = (ActionBarLayer *)context;
action_bar_raw_up_down_handler(recognizer, action_bar, true);
}
static void action_bar_click_config_provider(void *config_context) {
ActionBarLayer *action_bar = config_context;
void *context = action_bar_get_context(action_bar);
// For UP, SELECT and DOWN, setup the raw handler and assign the user specified context:
for (ButtonId button_id = BUTTON_ID_UP; button_id < NUM_BUTTONS; ++button_id) {
window_raw_click_subscribe(button_id, action_bar_raw_down_handler, action_bar_raw_up_handler, action_bar);
window_set_click_context(button_id, context);
}
// If back button is overridden, set context of BACK click recognizer as well:
if (action_bar->window && action_bar->window->overrides_back_button) {
window_set_click_context(BUTTON_ID_BACK, context);
}
if (action_bar->click_config_provider) {
action_bar->click_config_provider(context);
}
}
inline static void action_bar_update_click_config_provider(ActionBarLayer *action_bar) {
if (action_bar->window) {
window_set_click_config_provider_with_context(action_bar->window,
action_bar_click_config_provider, action_bar);
}
}
void action_bar_layer_set_click_config_provider(ActionBarLayer *action_bar, ClickConfigProvider click_config_provider) {
action_bar->click_config_provider = click_config_provider;
action_bar_update_click_config_provider(action_bar);
}
void action_bar_layer_set_icon_animated(ActionBarLayer *action_bar, ButtonId button_id,
const GBitmap *icon, bool animated) {
if (button_id < BUTTON_ID_UP || button_id >= NUM_BUTTONS) {
return;
}
const uint8_t index = button_id - 1;
if (action_bar->icons[index] == icon) {
return;
}
action_bar->icons[index] = icon;
if (animated) {
action_bar->icon_change_times[index] = prv_get_precise_time();
prv_register_redraw_timer(action_bar);
} else {
action_bar->icon_change_times[index] = 0;
}
layer_mark_dirty(&action_bar->layer);
}
void action_bar_layer_set_icon(ActionBarLayer *action_bar, ButtonId button_id,
const GBitmap *icon) {
action_bar_layer_set_icon_animated(action_bar, button_id, icon, false);
}
void action_bar_layer_clear_icon(ActionBarLayer *action_bar, ButtonId button_id) {
action_bar_layer_set_icon(action_bar, button_id, NULL);
}
void action_bar_layer_set_icon_press_animation(ActionBarLayer *action_bar, ButtonId button_id,
ActionBarLayerIconPressAnimation animation) {
if (button_id < BUTTON_ID_UP || button_id >= NUM_BUTTONS) {
return;
}
action_bar->animation[button_id - 1] = animation;
}
void action_bar_layer_add_to_window(ActionBarLayer *action_bar, struct Window *window) {
const GRect *window_bounds = &window->layer.bounds;
const int16_t width = prv_width();
GRect rect = GRect(0, 0, width, window_bounds->size.h);
layer_set_bounds(&action_bar->layer, &rect);
rect.origin.x = window_bounds->size.w - width;
layer_set_frame(&action_bar->layer, &rect);
layer_add_child(&window->layer, &action_bar->layer);
action_bar->window = window;
action_bar_update_click_config_provider(action_bar);
}
void action_bar_layer_remove_from_window(ActionBarLayer *action_bar) {
if (action_bar == NULL || action_bar->window == NULL) {
return;
}
layer_remove_from_parent(&action_bar->layer);
window_set_click_config_provider_with_context(action_bar->window, NULL, NULL);
action_bar->window = NULL;
}
void action_bar_layer_set_background_color(ActionBarLayer *action_bar, GColor background_color) {
if (gcolor_equal(background_color, action_bar->background_color)) {
return;
}
action_bar->background_color = background_color;
layer_mark_dirty(&(action_bar->layer));
}