pebble/src/fw/applib/ui/menu_layer.c
Josh Soref 852098a92e spelling: anyways
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-01-28 21:18:59 -05:00

1308 lines
51 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 "menu_layer.h"
#include "menu_layer_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/preferred_content_size.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/text.h"
#include "util/trig.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/animation_timing.h"
#include "applib/ui/click.h"
#include "applib/ui/window.h"
#include "applib/pbl_std/pbl_std.h"
#include "applib/legacy2/ui/menu_layer_legacy2.h"
#include "kernel/pbl_malloc.h"
#include "process_management/process_manager.h"
#include "shell/system_theme.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#include <string.h>
//! @return True if there was an animation to cancel, false otherwise
static bool prv_cancel_selection_animation(MenuLayer *menu_layer);
//////////////////////
// Menu Layer
//
// NOTES: The MenuLayer is built on top of ScrollLayer. It uses ScrollLayer's scrolling and clipping features.
// Since it easily becomes to costly in terms of RAM to hold a layer for each row in the menu in memory,
// the MenuLayer does not use layers for its rows and headers. When a row is about to be displayed,
// it will call out to the client using a callback to get that row drawn.
// Inside the MenuLayer's update_proc (Layer drawing callback), it will call out to its client for each row
// that needs to be drawn, until all visible rows have been drawn.
static void prv_menu_scroll_offset_changed_handler(ScrollLayer *scroll_layer,
MenuLayer *menu_layer) {
// TODO: we might need to propagate this event down to MenuLayerCallbacks
}
static void prv_menu_select_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
// If the selection animation is running, complete it. Note that 2.x apps don't have a selection
// animation.
if (menu_layer->animation.animation) {
animation_set_elapsed(menu_layer->animation.animation,
animation_get_duration(menu_layer->animation.animation, true, true));
}
// If we're in the middle of scrolling, finish scrolling immediately before handling the select
// click. We do this to make a transition animation have a consistent position to animate from.
// Note that animation_set_elapsed isn't supported on 2.x animations. Just skip this step, as
// no 2.x transitions interact directly with menu layer state.
if (!process_manager_compiled_with_legacy2_sdk() && menu_layer->scroll_layer.animation) {
Animation *scroll_layer_animation =
property_animation_get_animation(menu_layer->scroll_layer.animation);
animation_set_elapsed(scroll_layer_animation,
animation_get_duration(scroll_layer_animation, true, true));
}
// Actually handle the click
if (menu_layer->callbacks.select_click) {
menu_layer->callbacks.select_click(menu_layer, &menu_layer->selection.index,
menu_layer->callback_context);
}
}
static void prv_menu_select_long_click_handler(ClickRecognizerRef recognizer,
MenuLayer *menu_layer) {
if (menu_layer->callbacks.select_long_click) {
menu_layer->callbacks.select_long_click(menu_layer, &menu_layer->selection.index,
menu_layer->callback_context);
}
}
void menu_up_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
const bool up = true;
const bool animated = true;
menu_layer_set_selected_next(menu_layer, up, MenuRowAlignCenter, animated);
}
void menu_down_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
const bool up = false;
const bool animated = true;
menu_layer_set_selected_next(menu_layer, up, MenuRowAlignCenter, animated);
}
static void prv_menu_click_config_provider(MenuLayer *menu_layer) {
// The config that gets passed in, has already the UP and DOWN buttons configured
// we're overriding the default behavior here:
window_single_repeating_click_subscribe(BUTTON_ID_UP, 100 /*ms*/,
(ClickHandler)menu_up_click_handler);
if (menu_layer->callbacks.select_click) {
window_single_click_subscribe(BUTTON_ID_SELECT, (ClickHandler)prv_menu_select_click_handler);
}
if (menu_layer->callbacks.select_long_click) {
window_long_click_subscribe(BUTTON_ID_SELECT, 0,
(ClickHandler)prv_menu_select_long_click_handler, NULL);
}
window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100 /*ms*/,
(ClickHandler)menu_down_click_handler);
}
static inline uint16_t prv_menu_layer_get_num_sections(MenuLayer *menu_layer) {
if (menu_layer->callbacks.get_num_sections) {
return menu_layer->callbacks.get_num_sections(menu_layer, menu_layer->callback_context);
} else {
return 1; // default
}
}
static inline uint16_t prv_menu_layer_get_num_rows(MenuLayer *menu_layer, uint16_t section_index) {
if (section_index == MENU_INDEX_NOT_FOUND) {
return 0;
}
if (menu_layer->callbacks.get_num_rows) {
return menu_layer->callbacks.get_num_rows(menu_layer, section_index,
menu_layer->callback_context);
} else {
return 1; // default
}
}
static inline int16_t prv_menu_layer_get_separator_height(MenuLayer *menu_layer,
MenuIndex *cell_index) {
if (menu_layer->callbacks.get_separator_height) {
return menu_layer->callbacks.get_separator_height(menu_layer, cell_index, menu_layer->callback_context);
} else if (process_manager_compiled_with_legacy2_sdk()) {
return MENU_CELL_LEGACY2_BASIC_SEPARATOR_HEIGHT;
} else {
return MENU_CELL_BASIC_SEPARATOR_HEIGHT;
}
}
static inline int16_t prv_menu_layer_get_header_height(MenuLayer *menu_layer,
uint16_t section_index) {
if (menu_layer->callbacks.get_header_height) {
return menu_layer->callbacks.get_header_height(menu_layer, section_index, menu_layer->callback_context);
} else {
return 0; // default
}
}
static inline int16_t prv_menu_layer_get_cell_height(MenuLayer *menu_layer, MenuIndex
*cell_index, bool provide_correct_selection_index) {
if (menu_layer->callbacks.get_cell_height) {
const MenuIndex prev_selection_index = menu_layer->selection.index;
if (!provide_correct_selection_index) {
menu_layer->selection.index.section = MENU_INDEX_NOT_FOUND;
}
const int16_t result = menu_layer->callbacks.get_cell_height(menu_layer, cell_index,
menu_layer->callback_context);
menu_layer->selection.index = prev_selection_index;
return result;
} else {
return menu_cell_basic_cell_height(); // default
}
}
static inline void prv_menu_layer_draw_separator(MenuLayer *menu_layer, Layer *cell_layer,
MenuCellSpan *cursor, GContext* ctx) {
const int16_t y = cursor->y - cursor->sep;
if (menu_layer->callbacks.draw_separator) {
// Save current drawing state:
GDrawState prev_state = graphics_context_get_drawing_state(ctx);
GRect prev_bounds = cell_layer->bounds;
GRect new_bounds = prev_bounds;
// Translate the drawing_box to the bounds of the layer:
ctx->draw_state.drawing_box.origin.y += y;
ctx->draw_state.drawing_box.size.h = cursor->h;
// Set the height appropriately on the cell layer
new_bounds.size.h = cursor->sep;
layer_set_bounds(cell_layer, &new_bounds);
// Call the client, to ask to draw the separator:
menu_layer->callbacks.draw_separator(ctx, cell_layer, &cursor->index, menu_layer->callback_context);
// Restore current drawing state:
graphics_context_set_drawing_state(ctx, prev_state);
// Restore the layer bounds:
layer_set_bounds(cell_layer, &prev_bounds);
} else {
graphics_fill_rect(
ctx, &GRect(0, y, menu_layer->scroll_layer.layer.bounds.size.w, cursor->sep));
}
}
static void prv_prepare_row(GContext *ctx, MenuLayer *menu_layer,
Layer *cell_layer, bool highlight) {
if (!process_manager_compiled_with_legacy2_sdk()) {
GColor *colors = (highlight) ? menu_layer->highlight_colors : menu_layer->normal_colors;
ctx->draw_state.fill_color = colors[MenuLayerColorBackground];
ctx->draw_state.text_color = colors[MenuLayerColorForeground];
ctx->draw_state.tint_color = colors[MenuLayerColorForeground];
if (!gcolor_is_transparent(ctx->draw_state.fill_color)) {
graphics_fill_rect(ctx, &cell_layer->bounds);
}
}
cell_layer->is_highlighted = highlight;
}
static void prv_prepare_and_draw_row(GContext *ctx, MenuLayer *menu_layer,
Layer *cell_layer, MenuCellSpan *cursor, bool highlight) {
prv_prepare_row(ctx, menu_layer, cell_layer, highlight);
const GRect prev_bounds = cell_layer->bounds;
// in theory, we could decrement the origin by cell_content_origin_offset_y after the call
// in practice once shouldn't trust the draw_row implementation
const int16_t draw_box_origin_y = ctx->draw_state.drawing_box.origin.y;
ctx->draw_state.drawing_box.origin.y += menu_layer->animation.cell_content_origin_offset_y;
// Call the client, to ask to draw the row:
menu_layer->callbacks.draw_row(ctx, cell_layer, &cursor->index, menu_layer->callback_context);
ctx->draw_state.drawing_box.origin.y = draw_box_origin_y;
cell_layer->bounds = prev_bounds;
}
static inline void prv_menu_layer_draw_row(MenuLayer *menu_layer, Layer *cell_layer,
MenuCellSpan *cursor, GContext* ctx) {
if (cursor->h == 0) {
// cell has height 0, no need to draw anything.
return;
}
cell_layer->bounds.size.h = cursor->h;
cell_layer->frame.size.h = cursor->h;
cell_layer->frame.origin.y = cursor->y;
// Save current drawing state:
GDrawState prev_state = graphics_context_get_drawing_state(ctx);
// Translate the drawing_box to the bounds of the layer:
ctx->draw_state.drawing_box.origin.y += cursor->y;
ctx->draw_state.drawing_box.size.h = cursor->h;
// Use the drawing_box as a clipper to force the content to only use
// the space available to it and remove overflow
const GRect *const rect_clipper = (const GRect *const)&ctx->draw_state.drawing_box;
grect_clip((GRect *const)&ctx->draw_state.clip_box, rect_clipper);
const bool fully_covered = grect_equal(&cell_layer->frame, &menu_layer->inverter.layer.frame);
const bool partial = grect_overlaps_grect(&cell_layer->frame, &menu_layer->inverter.layer.frame);
if (fully_covered || !partial) {
prv_prepare_and_draw_row(ctx, menu_layer, cell_layer, cursor, fully_covered);
} else {
// Render the full cell without highlight
prv_prepare_and_draw_row(ctx, menu_layer, cell_layer, cursor, false);
// Set clipper to the inverter layer in clipping box coordinates
GRect selection_clipper;
layer_get_global_frame(&menu_layer->inverter.layer, &selection_clipper);
grect_clip((GRect *const)&ctx->draw_state.clip_box, &selection_clipper);
// Render with highlight
prv_prepare_and_draw_row(ctx, menu_layer, cell_layer, cursor, true);
}
// Restore current drawing state:
graphics_context_set_drawing_state(ctx, prev_state);
}
static inline void prv_menu_layer_draw_section_header(MenuLayer *menu_layer, Layer *cell_layer,
MenuCellSpan *cursor, GContext* ctx) {
cell_layer->bounds.size.h = cursor->h;
cell_layer->frame.size.h = cursor->h;
cell_layer->frame.origin.y = cursor->y;
// Callback to get the shared cell instance filled with data:
// Save current drawing state:
GDrawState prev_state = graphics_context_get_drawing_state(ctx);
// Translate the drawing_box to the bounds of the layer:
ctx->draw_state.drawing_box.origin.y += cursor->y;
ctx->draw_state.drawing_box.size.h = cursor->h;
const GRect *const rect_clipper = (const GRect *const)&ctx->draw_state.drawing_box;
grect_clip((GRect *const)&ctx->draw_state.clip_box, rect_clipper);
prv_prepare_row(ctx, menu_layer, cell_layer, false);
// Call the client, to ask to draw the section:
menu_layer->callbacks.draw_header(ctx, cell_layer, cursor->index.section, menu_layer->callback_context);
// Restore current drawing state:
graphics_context_set_drawing_state(ctx, prev_state);
}
static void prv_menu_layer_render_section_from_iterator(MenuIterator *iterator) {
MenuRenderIterator *it = (MenuRenderIterator*)iterator;
const int16_t top_diff = it->it.cursor.y - it->content_top_y;
const bool is_header_in_frame = (top_diff >= 0 && it->it.cursor.y <= it->content_bottom_y) ||
(it->it.cell_bottom_y >= it->content_top_y && it->it.cell_bottom_y <= it->content_bottom_y);
if (is_header_in_frame) {
// Draw section header:
prv_menu_layer_draw_section_header(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
// Draw the separator on top of the cell:
if (top_diff >= it->it.cursor.sep) {
prv_menu_layer_draw_separator(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
}
}
}
static void prv_menu_layer_render_row_from_iterator(MenuIterator *iterator) {
MenuRenderIterator *it = (MenuRenderIterator*)iterator;
const int16_t iter_y = it->it.cursor.y;
const int16_t top_diff = it->it.cursor.y - it->content_top_y;
const bool is_row_in_frame = (top_diff >= 0 && it->it.cursor.y <= it->content_bottom_y) ||
(it->it.cell_bottom_y >= it->content_top_y && it->it.cell_bottom_y <= it->content_bottom_y);
if (is_row_in_frame) {
it->cursor_in_frame = true;
// Draw the cell
prv_menu_layer_draw_row(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
// Draw the separator on top of the cell
if (top_diff >= it->it.cursor.sep) {
prv_menu_layer_draw_separator(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
}
// Update the cache with the center-most row
it->it.cursor.y = iter_y;
if (false == it->cache_set) {
it->new_cache = it->it.cursor;
it->cache_set = true;
}
} else {
if (it->cursor_in_frame) {
it->it.should_continue = false;
}
}
it->it.cursor.y = iter_y;
}
// NOTE: The following two iteration functions are asymmetrical!
// In other words, even one is going downward and the other upward, there are some subtle
// differences. Most importantly: the downward function calls the row_callback_after_geometry for
// the row the iterator's cursor is currently set to, while the upward function skips over the
// current row.
// Secondly, section_callback is only called when a sections is encountered while walking.
// For example, if the current index is (section: 0, row: 0), the section_callback for section 0
// will only be called when walking upward.
static void prv_menu_layer_walk_downward_from_iterator(MenuIterator *it) {
const uint16_t num_sections = prv_menu_layer_get_num_sections(it->menu_layer);
it->should_continue = true;
for (;;) { // sections
const uint16_t num_rows_in_section = prv_menu_layer_get_num_rows(it->menu_layer,
it->cursor.index.section);
for (;;) { // rows
if (it->cursor.index.row >= num_rows_in_section) {
// Reached last row
break;
}
if (it->row_callback_before_geometry) {
it->row_callback_before_geometry(it);
}
it->cursor.h = prv_menu_layer_get_cell_height(it->menu_layer, &it->cursor.index, true);
it->cell_bottom_y = it->cursor.y + it->cursor.h;
// ROW
if (it->row_callback_after_geometry) {
it->row_callback_after_geometry(it);
}
if (it->should_continue == false) {
return;
}
// Next row:
it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
it->cursor.y = it->cell_bottom_y; // Bottom of previous cell is y of the next cell
// Don't leave space for the seperator for the (non-existent) row after the last row.
// This doesn't impact cell drawing in this loop (this condition will only trip on the last run).
// But, other parts of the system rely on the cursor being set properly at the end of this iteration.
if (it->cursor.index.row < num_rows_in_section - 1 || it->cursor.index.section < num_sections - 1) {
it->cursor.y += it->cursor.sep;
}
++(it->cursor.index.row);
} // for() rows
// Next section:
++(it->cursor.index.section);
if (it->cursor.index.section >= num_sections) {
break;
// Reached last section
}
it->cursor.index.row = 0;
it->cursor.h = prv_menu_layer_get_header_height(it->menu_layer, it->cursor.index.section);
it->cell_bottom_y = it->cursor.y + it->cursor.h;
// SECTION
if (it->cursor.h > 0) {
it->section_callback(it);
it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
it->cursor.y = it->cell_bottom_y + it->cursor.sep;
}
if (it->should_continue == false) {
return;
}
} // for() sections
}
static void prv_menu_layer_walk_upward_from_iterator(MenuIterator *it) {
it->should_continue = true;
for (;;) { // sections
for (;;) { // rows
// Previous row
if (it->cursor.index.row == 0) {
// Reached top-most row in current section
break;
}
--(it->cursor.index.row);
if (it->row_callback_before_geometry) {
it->row_callback_before_geometry(it);
}
// when walking upwards, selected_index isn't set yet here
// hence, the heights are the sizes as they were before the selection changed
it->cursor.h = prv_menu_layer_get_cell_height(it->menu_layer, &it->cursor.index, false);
it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
it->cursor.y -= it->cursor.h + it->cursor.sep;
it->cell_bottom_y = it->cursor.y + it->cursor.h;
// ask for height again, this time with correct selection status
it->cursor.h = prv_menu_layer_get_cell_height(it->menu_layer, &it->cursor.index, true);
// ROW
if (it->row_callback_after_geometry) {
it->row_callback_after_geometry(it);
}
if (it->should_continue == false) {
break;
}
} // for() rows
if (it->cursor.index.row == 0) {
// If top-most row, layout the section header
it->cursor.h = prv_menu_layer_get_header_height(it->menu_layer, it->cursor.index.section);
it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
if (it->cursor.h > 0) {
// Bottom of previous cell is y of the next cell
const int16_t total_height = it->cursor.h + it->cursor.sep;
if (total_height > it->cursor.y) {
// If the total height is greater than the cursor y, don't
// add in space to accommodate the separator as the downwards callback
// will add it for us.
it->cursor.y -= it->cursor.h;
} else {
it->cursor.y -= total_height;
}
it->cell_bottom_y = it->cursor.y + it->cursor.h;
// SECTION
it->section_callback(it);
}
}
if (it->should_continue == false) {
return;
}
// Previous section:
if (it->cursor.index.section == 0) {
// Reached top
break;
}
--(it->cursor.index.section);
// -1 will happen when entering for() rows
it->cursor.index.row = it->menu_layer->callbacks.get_num_rows(it->menu_layer,
it->cursor.index.section, it->menu_layer->callback_context);
} // for() sections
}
static void NOINLINE prv_draw_background(MenuLayer *menu_layer, GContext *ctx,
Layer *bg_layer, bool highlight) {
GDrawState prev_state = graphics_context_get_drawing_state(ctx);
const GRect *bounds = &bg_layer->bounds;
ctx->draw_state.drawing_box.origin.y = bounds->origin.y;
ctx->draw_state.drawing_box.size.h = bounds->size.h;
MenuLayerDrawBackgroundCallback draw_background_cb = menu_layer->callbacks.draw_background;
if (draw_background_cb) {
draw_background_cb(ctx, bg_layer, false, menu_layer->callback_context);
} else if (highlight) {
ctx->draw_state.fill_color = menu_layer->highlight_colors[MenuLayerColorBackground];
graphics_fill_rect(ctx, bounds);
} else {
ctx->draw_state.fill_color = menu_layer->normal_colors[MenuLayerColorBackground];
graphics_fill_rect(ctx, bounds);
}
graphics_context_set_drawing_state(ctx, prev_state);
}
void menu_layer_update_proc(Layer *scroll_content_layer, GContext* ctx) {
MenuLayer *menu_layer = (MenuLayer*)(((uint8_t*)scroll_content_layer) -
offsetof(MenuLayer, scroll_layer.content_sublayer));
const GSize frame_size = menu_layer->scroll_layer.layer.frame.size;
const int16_t content_top_y = -scroll_layer_get_content_offset(&menu_layer->scroll_layer).y;
const int16_t content_bottom_y = content_top_y + frame_size.h;
if (!process_manager_compiled_with_legacy2_sdk()) {
prv_draw_background(menu_layer, ctx, &menu_layer->scroll_layer.layer, false);
}
MenuRenderIterator *render_iter = applib_type_malloc(MenuRenderIterator);
PBL_ASSERTN(render_iter);
if (menu_layer->center_focused) {
// in this mode, the selected row is always the best candidate for the cache
menu_layer->cache.cursor = menu_layer->selection;
}
*render_iter = (MenuRenderIterator) {
.it = {
.menu_layer = menu_layer,
.cursor = menu_layer->cache.cursor,
.row_callback_after_geometry = prv_menu_layer_render_row_from_iterator,
.section_callback = prv_menu_layer_render_section_from_iterator,
},
.ctx = ctx,
.content_top_y = content_top_y,
.content_bottom_y = content_bottom_y,
.cache_set = false,
.cursor_in_frame = false,
.cell_layer = {
.bounds = {
.size = {
.w = frame_size.w,
},
},
.frame = {
.size = {
.w = frame_size.w,
},
},
},
};
layer_add_child(&menu_layer->scroll_layer.content_sublayer, &render_iter->cell_layer);
// Set separator color
graphics_context_set_fill_color(ctx, GColorBlack);
// We're caching the y-coord and index of the one row, as our "anchor" point in the menu.
// We'll be walking downward and upward from that index until the rows fall off the screen.
const int16_t content_center_y = (content_top_y + content_bottom_y) / 2;
if (content_center_y >= menu_layer->cache.cursor.y) {
// Walk downward from cache.cursor, then upward
prv_menu_layer_walk_downward_from_iterator(&render_iter->it);
render_iter->it.cursor = menu_layer->cache.cursor;
prv_menu_layer_walk_upward_from_iterator(&render_iter->it);
} else {
// Walk upward from cache.cursor, then downward
prv_menu_layer_walk_upward_from_iterator(&render_iter->it);
render_iter->it.cursor = menu_layer->cache.cursor;
prv_menu_layer_walk_downward_from_iterator(&render_iter->it);
}
layer_remove_from_parent(&render_iter->cell_layer);
// Assign the new cache:
menu_layer->cache.cursor = render_iter->new_cache;
task_free(render_iter);
}
void menu_layer_init_scroll_layer_callbacks(MenuLayer *menu_layer) {
ScrollLayer *scroll_layer = &menu_layer->scroll_layer;
scroll_layer_set_callbacks(scroll_layer, (ScrollLayerCallbacks) {
.click_config_provider = (ClickConfigProvider)prv_menu_click_config_provider,
.content_offset_changed_handler = (ScrollLayerCallback)prv_menu_scroll_offset_changed_handler,
});
scroll_layer->content_sublayer.update_proc = (LayerUpdateProc)menu_layer_update_proc;
}
static void prv_set_center_focused(MenuLayer *menu_layer, bool center_focused) {
menu_layer->center_focused = center_focused;
scroll_layer_set_clips_content_offset(&menu_layer->scroll_layer, !center_focused);
}
void menu_layer_init(MenuLayer *menu_layer, const GRect *frame) {
*menu_layer = (MenuLayer) {
.pad_bottom = true,
};
ScrollLayer *scroll_layer = &menu_layer->scroll_layer;
scroll_layer_init(scroll_layer, frame);
menu_layer_init_scroll_layer_callbacks(menu_layer);
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);
InverterLayer *inverter = &menu_layer->inverter;
inverter_layer_init(inverter, &GRectZero);
scroll_layer_add_child(scroll_layer, &inverter->layer);
// Hide inverter layer by default for 3.0 apps
layer_set_hidden(inverter_layer_get_layer(&menu_layer->inverter), true);
#if PBL_ROUND
prv_set_center_focused(menu_layer, true);
#endif
}
MenuLayer* menu_layer_create(GRect frame) {
MenuLayer *layer = applib_type_malloc(MenuLayer);
if (layer) {
menu_layer_init(layer, &frame);
}
return layer;
}
void menu_layer_pad_bottom_enable(MenuLayer *menu_layer, bool enable) {
menu_layer->pad_bottom = enable;
}
void menu_layer_deinit(MenuLayer *menu_layer) {
prv_cancel_selection_animation(menu_layer);
layer_deinit(&menu_layer->inverter.layer);
scroll_layer_deinit(&menu_layer->scroll_layer);
}
void menu_layer_destroy(MenuLayer* menu_layer) {
if (menu_layer == NULL) {
return;
}
menu_layer_deinit(menu_layer);
applib_free(menu_layer);
}
Layer* menu_layer_get_layer(const MenuLayer *menu_layer) {
return &((MenuLayer *)menu_layer)->scroll_layer.layer;
}
ScrollLayer* menu_layer_get_scroll_layer(const MenuLayer *menu_layer) {
return &((MenuLayer *)menu_layer)->scroll_layer;
}
typedef struct MenuPrimeCacheIterator {
MenuIterator it;
bool cache_set;
} MenuPrimeCacheIterator;
static void prv_menu_layer_iterator_noop_callback(MenuIterator *it) {
(void)it;
}
static void prv_menu_layer_iterator_prime_cache_callback(MenuIterator *iterator) {
MenuPrimeCacheIterator *it = (MenuPrimeCacheIterator*)iterator;
if (false == it->cache_set) {
// Prime the cursor cache:
it->it.menu_layer->cache.cursor = it->it.cursor;
// Set initial selection too:
it->it.menu_layer->selection = it->it.cursor;
it->cache_set = true;
}
}
//! Calculate the total height of all row cells and section headers,
//! and assign the appropriate content size to the scroll_layer.
//! Also prime the offset cache on the fly.
void menu_layer_update_caches(MenuLayer *menu_layer) {
// Save the currently selected cell index.
MenuIndex selected_index = menu_layer_get_selected_index(menu_layer);
MenuPrimeCacheIterator it = {
.it = {
.menu_layer = menu_layer,
.row_callback_after_geometry = prv_menu_layer_iterator_prime_cache_callback,
.section_callback = prv_menu_layer_iterator_noop_callback,
.should_continue = true,
.cursor = {
// Section header of current section (0) is not part of the walk down, set it "manually"
.y = prv_menu_layer_get_header_height(menu_layer, 0),
.sep = prv_menu_layer_get_separator_height(menu_layer, 0)
},
},
.cache_set = false,
};
if (prv_menu_layer_get_header_height(menu_layer, 0) != 0) {
// We have to add the separator height, as when drawing down -> up, we render the separator
// for the row above before proceeding down. We only render this separator at the top if we
// have headers on the first section.
it.it.cursor.y += it.it.cursor.sep;
}
// handle special case of just one row so that calls for menu_layer_get_selected_index()
// will already answer correctly
if (prv_menu_layer_get_num_sections(menu_layer) == 1 &&
prv_menu_layer_get_num_rows(menu_layer, 0) == 1) {
menu_layer->selection.index = MenuIndex(0, 0);
}
prv_menu_layer_walk_downward_from_iterator(&it.it);
int16_t total_height = it.it.cursor.y;
if (menu_layer->pad_bottom) {
total_height += MENU_LAYER_BOTTOM_PADDING;
}
// Set the content size on the scroll layer, so all the rows will fit onto the content layer:
const GSize frame_size = menu_layer->scroll_layer.layer.frame.size;
scroll_layer_set_content_size(&menu_layer->scroll_layer, GSize(frame_size.w, total_height));
// Set the selected cell again:
const bool animated = false;
menu_layer_set_selected_index(menu_layer, selected_index, MenuRowAlignNone, animated);
}
void menu_layer_set_callbacks(MenuLayer *menu_layer, void *callback_context,
const MenuLayerCallbacks *callbacks) {
if (callbacks) {
menu_layer->callbacks = *callbacks;
PBL_ASSERTN(menu_layer->callbacks.draw_row);
PBL_ASSERTN(menu_layer->callbacks.get_num_rows);
}
menu_layer->callback_context = callback_context;
menu_layer_reload_data(menu_layer);
}
void menu_layer_set_callbacks_by_value(MenuLayer *menu_layer, void *callback_context,
MenuLayerCallbacks callbacks) {
menu_layer_set_callbacks(menu_layer, callback_context, &callbacks);
}
void menu_layer_set_click_config_onto_window(MenuLayer *menu_layer, struct Window *window) {
// Delegate this directly to the scroll layer:
scroll_layer_set_click_config_onto_window(&menu_layer->scroll_layer, window);
}
//! @returns 0 if A and B are equal, 1 if A has a higher section & row combination than B or else -1
int16_t menu_index_compare(const MenuIndex *a, const MenuIndex *b) {
const int16_t max_rows = MAX(a->row, b->row) + 1;
const int32_t a_abs = ((a->section * max_rows) + a->row);
const int32_t b_abs = ((b->section * max_rows) + b->row);
if (a_abs > b_abs) {
return 1;
} else if (a_abs < b_abs) {
return -1;
} else {
return 0;
}
}
static void prv_selection_complete(Animation *animation, bool finished, void *context) {
MenuLayer *menu_layer = (MenuLayer *) context;
menu_layer->animation.animation = NULL;
}
static bool prv_cancel_selection_animation(MenuLayer *menu_layer) {
const bool result = animation_is_scheduled(menu_layer->animation.animation);
if (result) {
animation_unschedule(menu_layer->animation.animation);
}
menu_layer->animation.animation = NULL;
return result;
}
#define TOP_DOWN_PX 7
#define BOTTOM_DOWN_PX 10
static void prv_setup_selection_animation(MenuLayer *menu_layer, bool up) {
// Move selection inverter layer:
const int16_t w = menu_layer->scroll_layer.layer.frame.size.w;
const GSize size = GSize(w, menu_layer->selection.h);
// Step 1. Bring down TOP of cell by TOP_DOWN_PX.
GRect from;
if (menu_layer->animation.animation) {
from = menu_layer->animation.target;
prv_cancel_selection_animation(menu_layer);
} else {
from = menu_layer->inverter.layer.frame;
}
GRect target = (GRect) {
.origin = {
.x = 0,
.y = from.origin.y + ((up) ? 0 : TOP_DOWN_PX),
},
.size = {
.w = size.w,
.h = size.h - TOP_DOWN_PX,
}
};
Animation *a1 = (Animation *) property_animation_create_layer_frame(&menu_layer->inverter.layer,
&from, &target);
animation_set_duration(a1, 100);
animation_set_curve(a1, AnimationCurveEaseOut);
animation_set_auto_destroy(a1, true);
// Step 2. Skip the top of the highlight down to the top of the newly selected cell,
// and have the selection BOTTOM_DOWN_PX below the selected cell.
from.origin.y = menu_layer->selection.y - ((up) ? BOTTOM_DOWN_PX : 0);
from.size.h = size.h + BOTTOM_DOWN_PX;
// Step 3. Bring up the bottom of the highlight to only cover the selected cell.
target.origin.y = menu_layer->selection.y;
target.size = size;
Animation *a2 = (Animation *) property_animation_create_layer_frame(&menu_layer->inverter.layer,
&from, &target);
animation_set_duration(a2, 250);
animation_set_curve(a2, AnimationCurveEaseOut);
animation_set_auto_destroy(a2, true);
Animation *a = animation_sequence_create(a1, a2, NULL);
animation_set_auto_destroy(a, true); // [MJ] false?
animation_set_handlers(a, (AnimationHandlers) { .stopped = prv_selection_complete }, menu_layer);
menu_layer->animation.animation = a;
menu_layer->animation.target = target;
animation_schedule(a);
}
static void prv_menu_layer_update_selection_highlight(MenuLayer *menu_layer, bool up,
bool animated,
bool change_ongoing_animation) {
if (menu_layer->center_focused || menu_layer->selection_animation_disabled) {
// animation on center_focused will not happen by moving the selection
// see prv_schedule_center_focus_animation()
animated = false;
}
Animation *scroll_animation = (Animation *) menu_layer->scroll_layer.animation;
if (change_ongoing_animation && animation_is_scheduled(scroll_animation)) {
animation_unschedule(scroll_animation);
}
if (change_ongoing_animation && animated && !process_manager_compiled_with_legacy2_sdk()) {
prv_setup_selection_animation(menu_layer, up);
} else {
if (change_ongoing_animation) {
prv_cancel_selection_animation(menu_layer);
}
// Move selection inverter layer:
const int16_t w = menu_layer->scroll_layer.layer.frame.size.w;
const GSize size = GSize(w, menu_layer->selection.h);
menu_layer->inverter.layer.bounds = (GRect) {
.origin = { 0, 0 },
.size = size,
};
menu_layer->inverter.layer.frame = (GRect) {
.origin = {
.x = 0,
.y = menu_layer->selection.y,
},
.size = size,
};
layer_mark_dirty(&menu_layer->inverter.layer);
}
}
static MenuRowAlign prv_corrected_scroll_align(MenuLayer *menu_layer, MenuRowAlign align) {
if (menu_layer->center_focused) {
return MenuRowAlignCenter;
}
return align;
}
static void prv_menu_layer_update_selection_scroll_position(MenuLayer *menu_layer,
MenuRowAlign scroll_align,
bool animated) {
scroll_align = prv_corrected_scroll_align(menu_layer, scroll_align);
if (scroll_align != MenuRowAlignNone) {
int16_t y;
const GSize frame_size = menu_layer->scroll_layer.layer.frame.size;
// Scroll to the right position:
switch (scroll_align) {
case MenuRowAlignTop:
y = - menu_layer->selection.y;
break;
case MenuRowAlignBottom:
y = frame_size.h - menu_layer->selection.y - menu_layer->selection.h;
break;
default:
case MenuRowAlignCenter:
y = (frame_size.h / 2) - menu_layer->selection.y - (menu_layer->selection.h / 2);
break;
}
if (menu_layer->center_focused) {
// animation on center_focus will not happen via scrolling
// see prv_schedule_center_focus_animation()
animated = false;
}
// scroll layer will take care of clipping if necessary
scroll_layer_set_content_offset(&menu_layer->scroll_layer, GPoint(0, y), animated);
}
}
typedef struct MenuSelectIndexIterator {
MenuIterator it;
MenuCellSpan selection;
bool did_change_selection:1;
} MenuSelectIndexIterator;
static void prv_menu_layer_iterator_selection_index_callback(MenuIterator *iterator) {
MenuSelectIndexIterator *it = (MenuSelectIndexIterator*)iterator;
if (!menu_index_compare(&it->it.cursor.index, &it->selection.index)) {
it->it.menu_layer->selection = it->it.cursor;
it->it.should_continue = false;
it->did_change_selection = true;
}
}
static void prv_menu_layer_iterator_update_selection(MenuIterator *iterator) {
MenuLayer *menu_layer = iterator->menu_layer;
if (menu_index_compare(&iterator->cursor.index, &menu_layer->selection.index) == 0) {
menu_layer->selection = iterator->cursor;
}
}
static void prv_walk_with_iterator(const int8_t direction, MenuIterator *it) {
MenuLayer *menu_layer = it->menu_layer;
const int16_t prev_selection_height = menu_layer->selection.h;
const MenuIndex prev_selection_index = menu_layer->selection.index;
if (menu_layer->center_focused) {
it->row_callback_before_geometry = it->row_callback_after_geometry;
it->row_callback_after_geometry = prv_menu_layer_iterator_update_selection;
// invalidate current selection while iterating
menu_layer->selection.index.section = MENU_INDEX_NOT_FOUND;
}
if (direction < 0) {
// new index comes before current selection
prv_menu_layer_walk_upward_from_iterator(it);
} else if (direction > 0) {
// new index comes after current selection
prv_menu_layer_walk_downward_from_iterator(it);
}
// potentially restore previous state of selection
if (menu_layer->selection.index.section == MENU_INDEX_NOT_FOUND) {
menu_layer->selection.index = prev_selection_index;
menu_layer->selection.h = prev_selection_height;
}
}
typedef struct {
MenuLayer *menu_layer;
bool up;
} CenterFocusSelectionAnimationState;
static CenterFocusSelectionAnimationState prv_center_focus_animation_state(Animation *animation) {
PropertyAnimation *prop_anim = (PropertyAnimation *)animation;
CenterFocusSelectionAnimationState result = {};
property_animation_get_subject(prop_anim, (void **)&result.menu_layer);
property_animation_to(prop_anim, &result.up, sizeof(result.up), false);
return result;
}
static void prv_center_focus_animation_setup(Animation *animation) {
CenterFocusSelectionAnimationState state = prv_center_focus_animation_state(animation);
state.menu_layer->animation.cell_content_origin_offset_y = 0;
state.menu_layer->animation.selection_extend_top = 0;
state.menu_layer->animation.selection_extend_bottom = 0;
}
static void prv_announce_selection_changed(MenuLayer *menu_layer, MenuIndex prev_index) {
if (!menu_layer->callbacks.selection_changed) {
return;
}
menu_layer->callbacks.selection_changed(menu_layer, menu_layer->selection.index,
prev_index, menu_layer->callback_context);
}
void prv_center_focus_animation_update_impl(Animation *animation,
bool second_half,
AnimationProgress adjusted_progress) {
CenterFocusSelectionAnimationState state = prv_center_focus_animation_state(animation);
// values as seen in the design videos
const int16_t move_in_dist = 16;
const int16_t move_out_dist = 4;
const int16_t abs_content_offset = second_half ?
interpolate_int16(adjusted_progress, move_out_dist, 0) :
interpolate_int16(adjusted_progress, 0, move_in_dist);
const int16_t content_offset = (state.up ? abs_content_offset : -abs_content_offset) / 2;
state.menu_layer->animation.cell_content_origin_offset_y = content_offset;
const bool reached_second_half_before = menu_index_compare(
&state.menu_layer->selection.index, &state.menu_layer->animation.new_selection.index) == 0;
if (second_half) {
if (!reached_second_half_before) {
const MenuIndex prev_index = state.menu_layer->selection.index;
state.menu_layer->selection = state.menu_layer->animation.new_selection;
prv_announce_selection_changed(state.menu_layer, prev_index);
}
// this favors robustness over efficiency - the functions might be called multiple times
// but instead of keeping track (which is more difficult that it seems)
// we simply call them too often
prv_menu_layer_update_selection_scroll_position(state.menu_layer, MenuRowAlignCenter, false);
prv_menu_layer_update_selection_highlight(state.menu_layer, state.up, false, false);
state.menu_layer->inverter.layer.frame.size.h += abs_content_offset;
state.menu_layer->inverter.layer.bounds.size = state.menu_layer->inverter.layer.frame.size;
// when scrolling up, bounce back at the top (otherwise at the bottom)
if (!state.up) {
state.menu_layer->inverter.layer.frame.origin.y -= abs_content_offset;
}
}
layer_mark_dirty(&state.menu_layer->scroll_layer.layer);
}
void prv_center_focus_animation_update_in_and_out(Animation *animation,
const AnimationProgress progress) {
const AnimationProgress half_progress = ANIMATION_NORMALIZED_MAX / 2;
const bool second_half = progress >= half_progress;
const AnimationProgress adjusted_progress = second_half ?
animation_timing_scaled(progress, half_progress, ANIMATION_NORMALIZED_MAX) :
animation_timing_scaled(progress, 0, half_progress);
prv_center_focus_animation_update_impl(animation, second_half, adjusted_progress);
}
void prv_center_focus_animation_update_out_only(Animation *animation,
const AnimationProgress progress) {
// anyways only render the bounce back
prv_center_focus_animation_update_impl(animation, true, progress);
}
static void prv_center_focus_animation_teardown(Animation *animation) {
// usually a "redundant" call. Just in case the animation gets cancelled before finish
prv_center_focus_animation_update_in_and_out(animation, ANIMATION_NORMALIZED_MAX);
}
static void prv_schedule_center_focus_animation(MenuLayer *menu_layer, bool up,
const MenuCellSpan *prev_selection,
bool was_animating) {
// we reconfigure the current index to be the previous index so that all parties in the ongoing
// animation will continue to reply with the proper values with respect to the selection
// half-way through the animation we then switch (back) to the new index
menu_layer->animation.new_selection = menu_layer->selection;
menu_layer->selection = *prev_selection;
// force selection + scrolling to be at the right spot, not animated since the actual animation
// for center_focused is done via rendering offset below
const bool selection_animated = false;
prv_menu_layer_update_selection_highlight(menu_layer, up, selection_animated,
true /* change_ongoing_animation */);
prv_menu_layer_update_selection_scroll_position(menu_layer, MenuRowAlignNone, selection_animated);
static const PropertyAnimationImplementation s_center_focus_selection_animation_in_out_impl = {
.base = {
.setup = prv_center_focus_animation_setup,
.update = prv_center_focus_animation_update_in_and_out,
.teardown = prv_center_focus_animation_teardown,
}
};
static const PropertyAnimationImplementation s_center_focus_selection_animation_out_only_impl = {
.base = {
.setup = prv_center_focus_animation_setup,
.update = prv_center_focus_animation_update_out_only,
.teardown = prv_center_focus_animation_teardown,
}
};
// when we were animating already, use the implementation that's only showing the bounce back
const PropertyAnimationImplementation *impl = was_animating ?
&s_center_focus_selection_animation_out_only_impl :
&s_center_focus_selection_animation_in_out_impl;
PropertyAnimation *const prop_anim = property_animation_create(impl, menu_layer, NULL, NULL);
// we're (ab)using the .to value to store the direction, see prv_center_focus_animation_state()
property_animation_to(prop_anim, &up, sizeof(up), true);
Animation *const anim = property_animation_get_animation(prop_anim);
menu_layer->animation.animation = anim;
// number of frames measured in the video
const uint32_t full_duration_ms = ANIMATION_TARGET_FRAME_INTERVAL_MS * 7;
uint32_t duration = full_duration_ms;
if (was_animating) {
// only show second half of animation if uses presses repetitive
// as it's only the bounce back, then
duration /= 2;
animation_set_delay(anim, duration);
}
animation_set_duration(anim, duration);
animation_set_curve(anim, AnimationCurveEaseInOut);
animation_schedule(anim);
if (was_animating) {
// create visual state that's already reflecting the beginning of the "out" animation
prv_center_focus_animation_update_out_only(anim, 0);
}
}
static void prv_apply_selection_change(MenuLayer *menu_layer, MenuRowAlign scroll_align, bool up,
bool did_change, const MenuCellSpan *prev_selection,
bool was_animating, bool animated) {
if (menu_layer->center_focused && animated) {
prv_schedule_center_focus_animation(menu_layer, up, prev_selection, was_animating);
} else {
prv_menu_layer_update_selection_highlight(menu_layer, up, animated, true);
prv_menu_layer_update_selection_scroll_position(menu_layer, scroll_align, animated);
// only call this here, on animated center focus, the announcement will happen in-between
// as we change the selection index for real
if (did_change) {
prv_announce_selection_changed(menu_layer, prev_selection->index);
}
}
}
typedef struct {
bool was_animating;
MenuCellSpan prev_selection;
} MenuLayerBeforeSelectionChangeState;
static MenuLayerBeforeSelectionChangeState prv_capture_state_and_cancel_center_focus_animation(
MenuLayer *menu_layer) {
// it's critical to cancel the animation for center focus here so that any potential in-between
// selection state will be cleaned up
const bool was_animating = menu_layer->center_focused ?
prv_cancel_selection_animation(menu_layer) :
false;
return (MenuLayerBeforeSelectionChangeState) {
.was_animating = was_animating,
.prev_selection = menu_layer->selection,
};
}
void menu_layer_set_selected_index(MenuLayer *menu_layer, MenuIndex index, MenuRowAlign scroll_align, bool animated) {
const MenuLayerBeforeSelectionChangeState before_state =
prv_capture_state_and_cancel_center_focus_animation(menu_layer);
// Keep the selection within a valid range
const uint16_t num_sections = prv_menu_layer_get_num_sections(menu_layer);
if (index.section >= num_sections) {
index.section = num_sections - 1;
}
// check to make sure this callback has been set, return early if not
if (menu_layer->callbacks.get_num_rows == NULL) {
PBL_LOG(LOG_LEVEL_ERROR, "Please set menu layer callbacks before running menu_layer_set_selected_index.");
return;
}
const uint16_t num_rows = menu_layer->callbacks.get_num_rows(menu_layer, index.section, menu_layer->callback_context);
if (index.row >= num_rows) {
index.row = num_rows - 1;
}
// when called from iteration triggered by menu_layer_set_selected_next() the
// selection.index.section could be MENU_INDEX_NOT_FOUND (a very large value)
// in this case, walk forward from {0, 0| to avoid a very long loop run
const bool is_invalid_section = menu_layer->selection.index.section == MENU_INDEX_NOT_FOUND;
const int16_t comp = is_invalid_section ? 1 :
menu_index_compare(&index, &menu_layer->selection.index);
MenuSelectIndexIterator it = {
.it = {
.menu_layer = menu_layer,
.row_callback_after_geometry = prv_menu_layer_iterator_selection_index_callback,
.section_callback = prv_menu_layer_iterator_noop_callback,
.should_continue = true,
.cursor = is_invalid_section ? (MenuCellSpan){} : menu_layer->selection,
},
.selection = {
.index = index,
},
.did_change_selection = false,
};
prv_walk_with_iterator((int8_t)comp, &it.it);
const bool up = (comp == -1);
prv_apply_selection_change(menu_layer, scroll_align, up, it.did_change_selection,
&before_state.prev_selection, before_state.was_animating, animated);
}
typedef struct MenuSelectNextIterator {
MenuIterator it;
uint8_t count;
bool did_change_selection:1;
} MenuSelectNextIterator;
static void prv_menu_layer_iterator_selection_next_callback(MenuIterator *iterator) {
MenuSelectNextIterator *it = (MenuSelectNextIterator*)iterator;
MenuLayer *menu_layer = it->it.menu_layer;
if (it->count == 1) {
MenuLayerSelectionWillChangeCallback cb = menu_layer->callbacks.selection_will_change;
it->it.should_continue = false;
it->did_change_selection = true;
if (cb) {
MenuIndex new_index = it->it.cursor.index;
cb(menu_layer, &new_index, menu_layer->selection.index, menu_layer->callback_context);
if (menu_index_compare(&new_index, &menu_layer->selection.index) == 0) {
// locked into old index
} else if (menu_index_compare(&new_index, &it->it.cursor.index) == 0) {
// new index is the index we wanted to select
menu_layer->selection = it->it.cursor;
} else {
// when center focused, animation will be scheduled at the very end
// see prv_apply_selection_change()
const bool animated = !menu_layer->center_focused;
// Specified an alternate index
// This is safe since menu_layer_set_selected_index will not trigger the
// SelectionWillChangeCallback again.
menu_layer_set_selected_index(menu_layer, new_index, MenuRowAlignNone, animated);
it->did_change_selection = false;
}
} else {
menu_layer->selection = it->it.cursor;
}
} else {
++it->count;
}
}
void menu_layer_set_selected_next(MenuLayer *menu_layer, bool up,
MenuRowAlign scroll_align, bool animated) {
const MenuLayerBeforeSelectionChangeState before_state =
prv_capture_state_and_cancel_center_focus_animation(menu_layer);
MenuSelectNextIterator it = {
.it = {
.menu_layer = menu_layer,
.row_callback_after_geometry = prv_menu_layer_iterator_selection_next_callback,
.section_callback = prv_menu_layer_iterator_noop_callback,
.should_continue = true,
.cursor = menu_layer->selection,
},
.count = up ? 1 : 0, // see asymmetry note with menu_layer_walk_downward_from_iterator()
.did_change_selection = false,
};
prv_walk_with_iterator((int8_t)(up ? -1 : 1), &it.it);
prv_apply_selection_change(menu_layer, scroll_align, up, it.did_change_selection,
&before_state.prev_selection, before_state.was_animating, animated);
}
MenuIndex menu_layer_get_selected_index(const MenuLayer *menu_layer) {
return menu_layer->selection.index;
}
bool menu_layer_is_index_selected(const MenuLayer *menu_layer, MenuIndex *index) {
MenuIndex selected_index = menu_layer_get_selected_index(menu_layer);
return menu_index_compare(&selected_index, index) == 0;
}
//! indicates that the data behind the menu has changed and needs a re-draw
void menu_layer_reload_data(MenuLayer *menu_layer) {
menu_layer_update_caches(menu_layer);
}
bool menu_cell_layer_is_highlighted(const Layer *cell_layer) {
return cell_layer->is_highlighted;
}
void menu_layer_set_normal_colors(MenuLayer *menu_layer, GColor background, GColor foreground) {
menu_layer->normal_colors[MenuLayerColorBackground] = background;
menu_layer->normal_colors[MenuLayerColorForeground] = foreground;
}
void menu_layer_set_highlight_colors(MenuLayer *menu_layer, GColor background, GColor foreground) {
menu_layer->highlight_colors[MenuLayerColorBackground] = background;
menu_layer->highlight_colors[MenuLayerColorForeground] = foreground;
}
bool menu_layer_get_center_focused(MenuLayer *menu_layer) {
return menu_layer->center_focused;
}
void menu_layer_set_center_focused(MenuLayer *menu_layer, bool center_focused) {
if (!menu_layer) {
return;
}
prv_set_center_focused(menu_layer, center_focused);
menu_layer_update_caches(menu_layer);
}