mirror of
https://github.com/google/pebble.git
synced 2025-11-20 22:40:57 -05:00
Import of the watch repository from Pebble
This commit is contained in:
396
src/fw/applib/ui/action_bar_layer.c
Normal file
396
src/fw/applib/ui/action_bar_layer.c
Normal file
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
Reference in New Issue
Block a user