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

847 lines
36 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_menu_layer.h"
#include "action_menu_window_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/fonts/fonts.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/text.h"
#include "applib/ui/animation.h"
#include "applib/ui/menu_layer.h"
#include "applib/ui/property_animation.h"
#include "applib/ui/window_private.h"
#include "kernel/pbl_malloc.h"
#include "kernel/ui/kernel_ui.h"
#include "resource/resource_ids.auto.h"
#include "shell/system_theme.h"
#include "system/passert.h"
#include "util/math.h"
#include <string.h>
#define INDICATOR "»"
static const int VERTICAL_PADDING = PBL_IF_COLOR_ELSE(2, 4);
static const int EXTRA_PADDING_1_BIT = 2;
static const int SHORT_COL_COUNT = 3;
static const int MAX_NUM_VISIBLE_LINES = 2;
static const int SHORT_ITEM_MAX_ROWS_SPALDING = 3;
static GFont prv_get_item_font(void) {
return system_theme_get_font(TextStyleFont_MenuCellTitle);
}
//! Only used on round displays to achieve a fish-eye effect
static GFont prv_get_unfocused_item_font(void) {
return system_theme_get_font(TextStyleFont_Header);
}
static uint16_t prv_get_num_rows(MenuLayer *menu_layer, uint16_t section_index,
void *callback_context) {
ActionMenuLayer *aml = callback_context;
return (uint16_t)(aml->num_items +
(aml->num_short_items + SHORT_COL_COUNT - 1) / SHORT_COL_COUNT);
}
static void prv_cell_column_draw(GContext *ctx, struct Layer const *cell_layer,
ActionMenuLayer *aml, ActionMenuItem *items,
int num_items, int sel_idx) {
const GFont font = aml->layout_cache.font;
const int16_t font_height = fonts_get_font_height(font);
const GRect *layer_bounds = &cell_layer->bounds;
GRect r = *layer_bounds;
#if PBL_ROUND
// more narrow on round
r = grect_inset_internal(r, 25, 0);
// center the columns horizontally if there's only one row
const bool is_single_short_row = aml->num_short_items <= SHORT_COL_COUNT;
r.size.w /= is_single_short_row ? num_items : SHORT_COL_COUNT;
#else
r.size.w /= SHORT_COL_COUNT;
#endif
r.origin.y += (r.size.h - font_height) / 2 - 4;
for (int i = 0; i < num_items; i++) {
if (!items[i].label) {
break;
}
if (sel_idx == i) {
graphics_context_set_text_color(ctx, PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack));
#if SCREEN_COLOR_DEPTH_BITS == 1
// We only want to have a background on non-color platforms, while leaving this in with
// a PBL_IF_COLOR_ELSE makes this a no-op, we'll save some cycles and code space just
// skipping it.
graphics_context_set_fill_color(ctx, GColorWhite);
const int16_t y_offset = 1;
const int16_t padding = r.size.w / 6;
const uint16_t corner_radius = 4;
GRect bg_rect = r;
bg_rect.origin.y = layer_bounds->origin.y;
bg_rect.size.h = layer_bounds->size.h;
bg_rect = grect_inset_internal(bg_rect, padding, y_offset);
graphics_fill_round_rect(ctx, &bg_rect, corner_radius, GCornersAll);
#endif
} else {
graphics_context_set_text_color(ctx, PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite));
}
graphics_draw_text(ctx, items[i].label, font, r, GTextOverflowModeTrailingEllipsis,
GTextAlignmentCenter, NULL);
r.origin.x += r.size.w;
}
}
static const ActionMenuItem *prv_get_item_for_index(ActionMenuLayer *aml, int idx) {
if (!aml->num_items && !aml->num_short_items) {
return NULL;
}
PBL_ASSERTN(idx >= 0);
if (idx < aml->num_items) {
return &aml->items[idx];
} else {
const int short_items_idx = idx - aml->num_items;
PBL_ASSERTN(short_items_idx < aml->num_short_items);
return &aml->short_items[short_items_idx];
}
}
static int16_t prv_get_item_line_height(ActionMenuLayer *aml, int idx) {
const GFont font = aml->layout_cache.font;
const ActionMenuItem *item = prv_get_item_for_index(aml, idx);
GRect box = menu_layer_get_layer(&aml->menu_layer)->bounds;
// In calculating the item line height for round displays, we need to horizontally inset by the
// standard focused cell inset since that's the horizontal inset of the cells where we show
// the vertical scrolling animation of long text cells (where the height is crucial to be correct)
const int inset = PBL_IF_ROUND_ELSE(MENU_CELL_ROUND_FOCUSED_HORIZONTAL_INSET,
menu_cell_basic_horizontal_inset());
// Tintin has a rounded rectangle highlight
box = grect_inset_internal(box, PBL_IF_COLOR_ELSE(inset, 2 * inset), 0);
// We offset the text 5 pixels from the left of the cell. If the indicator is
// present, the indicator also will be offset, so we add 5 pixels more spacing
// between the text and the indicator. This extra padding isn't needed for round.
const int nudge = PBL_IF_ROUND_ELSE(0, menu_cell_basic_horizontal_inset());
GContext *ctx = graphics_context_get_current_context();
// On rectangular displays, if the indicator is present, the indicator also will be offset,
// so we add another nudge between the text and the indicator.
#if PBL_RECT
if (!item->is_leaf) {
const GSize indicator_size = graphics_text_layout_get_max_used_size(ctx, INDICATOR,
font, box,
GTextOverflowModeWordWrap,
GTextAlignmentRight, NULL);
box.size.w -= (indicator_size.w + nudge);
}
#endif
return graphics_text_layout_get_text_height(ctx, item->label, font, box.size.w,
GTextOverflowModeWordWrap, PBL_IF_ROUND_ELSE(GTextAlignmentCenter, GTextAlignmentLeft));
}
// Item Scroll Animation
///////////////////////////////////
static int16_t prv_get_cell_offset(void *subject) {
ActionMenuLayer *aml = subject;
return aml->item_animation.current_offset_y;
}
T_STATIC void prv_set_cell_offset(void *subject, int16_t value) {
ActionMenuLayer *aml = subject;
aml->item_animation.current_offset_y = value;
layer_mark_dirty(&aml->layer);
}
static void prv_cell_animation_stopped_handler(Animation *animation, bool finished, void *context) {
ActionMenuLayer *aml = context;
if (finished) {
prv_set_cell_offset(aml, aml->item_animation.bottom_offset_y);
}
}
static const PropertyAnimationImplementation s_item_animation_implementation = {
.base = {
.update = (AnimationUpdateImplementation)property_animation_update_int16
},
.accessors = {
.setter = { .int16 = prv_set_cell_offset },
.getter = { .int16 = prv_get_cell_offset }
}
};
static void prv_unschedule_item_animation(ActionMenuLayer *aml) {
animation_unschedule(aml->item_animation.animation);
aml->item_animation.animation = NULL;
}
static void prv_animate_cell(ActionMenuLayer *aml, GRect *label_text_frame, bool *draw_top_shading,
bool *draw_bottom_shading) {
// Check to see if this item spans more than max number of visible lines,
// in which case we want to make it scroll.
const int16_t item_height = aml->layout_cache.item_heights[aml->selected_index];
const int16_t line_height = fonts_get_font_height(aml->layout_cache.font);
#if SCREEN_COLOR_DEPTH_BITS == 1
// We need to force it to scroll a little extra for 1 bit
label_text_frame->origin.y -= EXTRA_PADDING_1_BIT;
#endif
// On rect displays, calculate the visible item height based on a desired number of visible lines
// On round displays, use the height of the provided box since it might be inset for the indicator
const int16_t max_visible_item_height = PBL_IF_RECT_ELSE(MAX_NUM_VISIBLE_LINES * line_height,
label_text_frame->size.h);
if (item_height > max_visible_item_height) {
// Compute the limit at which we should bounce back to the top of the layer. Since
// there are at most MAX_NUM_VISIBLE_LINES shown at a given time, we want to stop
// when there are that number of lines in view and no more lines remaining below.
const int16_t max_scroll_distance = item_height - max_visible_item_height;
ActionMenuItemAnimation *item_animation = &aml->item_animation;
if (item_animation->animation == NULL) {
const int16_t DELAY_PER_LINE = 600; /* milliseconds to delay per line */
// Top offset represents when the text has scrolled to its minimum y value so the last line of
// text is visible. Bottom offset represents when the text has scrolled all the way to its
// maximum y so the first line of text is visible.
item_animation->top_offset_y = -max_scroll_distance;
item_animation->bottom_offset_y = 0;
item_animation->current_offset_y = 0;
// Create the animation that will scroll us up in the cell
PropertyAnimation *animation = property_animation_create(&s_item_animation_implementation,
(void *)aml, NULL, &item_animation->top_offset_y);
animation_set_duration((Animation *)animation, DELAY_PER_LINE * (item_height / line_height));
animation_set_curve((Animation *)animation, AnimationCurveLinear);
animation_set_handlers((Animation *)animation, (AnimationHandlers){0}, aml);
// Create the animation that stalls when we have auto-scrolled up completely
PropertyAnimation *s_animation = property_animation_create(&s_item_animation_implementation,
(void *)aml, &item_animation->top_offset_y, &item_animation->top_offset_y);
animation_set_duration((Animation *)s_animation, DELAY_PER_LINE /* ms to wait */);
animation_set_handlers((Animation *)s_animation, (AnimationHandlers){0}, aml);
// Create the reverse animation that takes us from the scrolled up position back down
PropertyAnimation *r_animation = property_animation_create(&s_item_animation_implementation,
(void *)aml, &item_animation->top_offset_y, &item_animation->bottom_offset_y);
animation_set_duration((Animation *)r_animation,
(DELAY_PER_LINE / 4) * (item_height / line_height));
animation_set_curve((Animation *)r_animation, AnimationCurveEaseInOut);
animation_set_handlers((Animation *)r_animation, (AnimationHandlers){0}, aml);
item_animation->animation = animation_sequence_create((Animation *)animation,
(Animation *)s_animation, (Animation *)r_animation);
animation_set_handlers(item_animation->animation,
(AnimationHandlers){ .stopped = prv_cell_animation_stopped_handler },
aml);
animation_set_play_count(item_animation->animation, PLAY_COUNT_INFINITE);
animation_set_delay(item_animation->animation, DELAY_PER_LINE /* ms */);
animation_schedule(item_animation->animation);
}
*draw_top_shading = (item_animation->current_offset_y != item_animation->bottom_offset_y);
*draw_bottom_shading = (item_animation->current_offset_y != item_animation->top_offset_y);
// update the rect height and offset based on the current animation state
label_text_frame->origin.y += item_animation->current_offset_y;
label_text_frame->size.h = item_height;
}
}
// Menu Layer Drawing Routines
///////////////////////////////
static bool prv_should_center(ActionMenuLayer *aml) {
// We only center an ActionMenuLayer's items if the user has specified to
// center the items or there is only one item in the ActionMenuLayer.
if (aml->num_items == 1 || aml->layout_cache.align == ActionMenuAlignCenter) {
return true;
}
return false;
}
static void prv_cell_item_content_draw_rect(GContext *ctx, const Layer *cell_layer,
const ActionMenuLayer *aml, const ActionMenuItem *item,
bool selected, GRect *content_box) {
char *indicator = NULL;
const int16_t horizontal_padding = menu_cell_basic_horizontal_inset();
const GFont font = aml->layout_cache.font;
if (!item->is_leaf) {
// If an item is not a leaf, then there would be an indicator when it is focused. Either
// we draw the indicator or we force the box to be smaller to force the text to render as
// if the indicator was present in case it would line wrap.
if (selected) {
indicator = INDICATOR;
} else {
const GSize indicator_size = graphics_text_layout_get_max_used_size(
ctx, INDICATOR, font, *content_box, GTextOverflowModeWordWrap, GTextAlignmentRight, NULL);
content_box->size.w -= (indicator_size.w + (2 * horizontal_padding));
}
} else {
content_box->size.w -= horizontal_padding;
}
#if SCREEN_COLOR_DEPTH_BITS == 1
// Fill in the background layer. This effectively does nothing on watches where we have the
// ability to draw with color, but on others, it will render a background behind the selected
// cell.
const int x_offset = horizontal_padding;
const int y_padding = EXTRA_PADDING_1_BIT;
const uint16_t corner_radius = 4;
GRect bg_box = grect_inset_internal(cell_layer->bounds, x_offset, 0);
bg_box.size.h -= y_padding;
graphics_fill_round_rect(ctx, &bg_box, corner_radius, GCornersAll);
// We have to adjust the box to compensate for the padding we added. Note that we can't call
// inset as it will discard our offset when it standardizes.
content_box->origin.x += x_offset;
content_box->size.w -= (2 * x_offset);
content_box->size.h -= (2 * y_padding);
#endif
// Cast the cell layer so we can briefly modify its bounds. We do this because we're
// desperate for stack space and we understand the call hierarchy. We'll restore the state below.
Layer *mutable_cell_layer = (Layer *)cell_layer;
const GRect saved_bounds = mutable_cell_layer->bounds;
mutable_cell_layer->bounds = *content_box;
// Draw the menu cell specifying that we're allowing word wrapping
const GTextOverflowMode overflow_mode = GTextOverflowModeWordWrap;
menu_cell_basic_draw_custom(ctx, mutable_cell_layer, font, item->label, font, indicator, font,
NULL, NULL, false, overflow_mode);
// Restore the cell layer's bounds
mutable_cell_layer->bounds = saved_bounds;
}
static void prv_cell_item_content_draw_round(GContext *ctx, const Layer *cell_layer,
const ActionMenuLayer *aml, const ActionMenuItem *item,
bool selected, GRect *content_box) {
const int16_t horizontal_inset = selected ? MENU_CELL_ROUND_FOCUSED_HORIZONTAL_INSET :
MENU_CELL_ROUND_UNFOCUSED_HORIZONTAL_INSET;
*content_box = grect_inset(*content_box, GEdgeInsets(0, horizontal_inset));
// Use a smaller font for the unfocused cells to achieve a fish-eye effect
const GFont font = selected ? aml->layout_cache.font : prv_get_unfocused_item_font();
const GTextOverflowMode overflow_mode = selected ? GTextOverflowModeWordWrap :
GTextOverflowModeTrailingEllipsis;
const GTextAlignment text_alignment = GTextAlignmentCenter;
const GSize text_size = graphics_text_layout_get_max_used_size(ctx, item->label, font,
*content_box, overflow_mode,
text_alignment, NULL);
GRect text_box = (GRect) { .size = text_size };
const GAlign item_label_text_alignment = GAlignCenter;
grect_align(&text_box, content_box, item_label_text_alignment, true /* clip */);
text_box.origin.y -= fonts_get_font_cap_offset(font);
graphics_draw_text(ctx, item->label, font, text_box, overflow_mode, text_alignment, NULL);
}
static int16_t prv_get_indicator_height(const ActionMenuLayer *aml) {
// This magic factor is an approximation of the indicator height in relation to the font line
// height; it Just Works(tm)
return fonts_get_font_height(aml->layout_cache.font) * 40 / 100;
}
static void prv_draw_indicator_round(GContext *ctx, const ActionMenuLayer *aml,
const GRect *label_text_container) {
const int indicator_height = fonts_get_font_height(aml->layout_cache.font);
const int text_height = aml->layout_cache.item_heights[aml->selected_index];
const int content_height = MIN(label_text_container->size.h, text_height + indicator_height);
GRect content_frame = (GRect) {
.size = GSize(label_text_container->size.w, content_height)
};
GRect indicator_frame = (GRect) {
.size = GSize(label_text_container->size.w, indicator_height)
};
grect_align(&content_frame, label_text_container, GAlignCenter, true);
grect_align(&indicator_frame, &content_frame, GAlignBottom, true);
graphics_draw_text(ctx, INDICATOR, aml->layout_cache.font, indicator_frame,
GTextOverflowModeWordWrap, GTextAlignmentCenter, NULL);
}
static void prv_cell_item_draw(GContext *ctx, const Layer *cell_layer,
ActionMenuLayer *aml, const ActionMenuItem *item,
bool selected) {
GRect label_text_container = cell_layer->bounds;
// bottom_inset won't be used on black and white, using UNUSED here quiets the linter
UNUSED int16_t bottom_inset = 0;
#if PBL_ROUND
// On round displays, inset the box from the bottom to account for drawing the indicator at the
// bottom center, and then draw the indicator
const bool selected_with_indicator = (selected && !item->is_leaf);
if (selected_with_indicator) {
prv_draw_indicator_round(ctx, aml, &label_text_container);
const int16_t indicator_text_margin = 7;
bottom_inset = prv_get_indicator_height(aml) + indicator_text_margin;
label_text_container.size.h -= bottom_inset;
}
#endif
GRect label_text_frame = label_text_container;
bool draw_top_shading = false;
bool draw_bottom_shading = false;
// If we are the selected index, check to see if we have started scrolling.
// If we have, use our internal box to draw the layer, otherwise use the
// layer box.
if (selected) {
prv_animate_cell(aml, &label_text_frame, &draw_top_shading, &draw_bottom_shading);
#if !defined(RECOVERY_FW) && SCREEN_COLOR_DEPTH_BITS == 8
// Replace the clip box with a clip box that will render the item in the right place with the
// right size, without menu layer's selection clipping. Menu layer will responsible for cleaning
// up the changes made to this clip box.
ctx->draw_state.clip_box.origin = ctx->draw_state.drawing_box.origin;
ctx->draw_state.clip_box.size = cell_layer->bounds.size;
// We have to update the clip box of the drawing state to account for text padding to
// force it to clip around the shadow.
if (draw_top_shading) {
ctx->draw_state.clip_box.origin.y += VERTICAL_PADDING;
ctx->draw_state.clip_box.size.h -= VERTICAL_PADDING;
}
if (draw_bottom_shading) {
ctx->draw_state.clip_box.size.h -= VERTICAL_PADDING + bottom_inset;
}
// Prevent drawing outside of the context bitmap
grect_clip(&ctx->draw_state.clip_box, &ctx->dest_bitmap.bounds);
#endif
graphics_context_set_text_color(ctx, PBL_IF_COLOR_ELSE(GColorWhite, GColorBlack));
graphics_context_set_fill_color(ctx, PBL_IF_COLOR_ELSE(GColorBlack, GColorWhite));
}
PBL_IF_RECT_ELSE(prv_cell_item_content_draw_rect,
prv_cell_item_content_draw_round)(ctx, cell_layer, aml, item, selected,
&label_text_frame);
#if !defined(RECOVERY_FW) && SCREEN_COLOR_DEPTH_BITS == 8
const int16_t fade_height = 10;
graphics_context_set_compositing_mode(ctx, GCompOpSet);
if (draw_top_shading) {
GRect top_bounds = label_text_container;
top_bounds.origin.y += VERTICAL_PADDING;
top_bounds.size.h = fade_height;
graphics_draw_bitmap_in_rect(ctx, &aml->item_animation.fade_top, &top_bounds);
}
if (draw_bottom_shading) {
GRect bottom_bounds = label_text_container;
bottom_bounds.size.h = fade_height;
bottom_bounds.origin.y = grect_get_max_y(&label_text_container) -
(fade_height + VERTICAL_PADDING);
graphics_draw_bitmap_in_rect(ctx, &aml->item_animation.fade_bottom, &bottom_bounds);
}
#endif
}
static void prv_draw_row(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index,
void *callback_context) {
ActionMenuLayer *aml = callback_context;
if (cell_index->row < aml->num_items) {
const ActionMenuItem *item = prv_get_item_for_index(aml, cell_index->row);
const bool selected = menu_layer_is_index_selected(&aml->menu_layer, cell_index);
prv_cell_item_draw(ctx, cell_layer, aml, item, selected);
} else {
const int base_idx = (cell_index->row - aml->num_items) * SHORT_COL_COUNT;
const int sel_idx = aml->selected_index - (base_idx + aml->num_items);
const int num_items = CLIP(aml->num_short_items - base_idx, 0, SHORT_COL_COUNT);
prv_cell_column_draw(ctx, cell_layer, aml, (ActionMenuItem *)&aml->short_items[base_idx],
num_items, sel_idx);
}
}
static int prv_get_menu_layer_row(ActionMenuLayer *aml, int item_index) {
if (item_index < aml->num_items) {
return item_index;
} else {
return aml->num_items + (item_index - aml->num_items) / SHORT_COL_COUNT;
}
}
T_STATIC void prv_set_selected_index(ActionMenuLayer *aml, int new_selected_index, bool animated) {
new_selected_index = CLIP(new_selected_index, 0, aml->num_items + aml->num_short_items - 1);
if (new_selected_index != aml->selected_index) {
// Unschedule any running item animation but don't NULL the pointer, to prevent another
// animation from being accidentally re-scheduled.
animation_unschedule(aml->item_animation.animation);
}
if (new_selected_index >= aml->num_items) {
// For short columns, aml->selected_index needs to be updated here, because the column index
// will be lost in the menu layer selection changed callback. Otherwise, it will be updated
// in prv_selection_changed_cb() to ensure the correct index is used by the draw functions.
aml->selected_index = new_selected_index;
}
const int menu_layer_index = prv_get_menu_layer_row(aml, new_selected_index);
menu_layer_set_selected_index(&aml->menu_layer, MenuIndex(0, menu_layer_index),
MenuRowAlignCenter, animated);
}
static void prv_scroll_handler(ClickRecognizerRef recognizer, void *context) {
ActionMenuLayer *aml = context;
const bool up = (click_recognizer_get_button_id(recognizer) == BUTTON_ID_UP);
const int new_idx = aml->selected_index + (up ? -1 : 1);
prv_set_selected_index(aml, new_idx, true /* animated */);
}
static void prv_select_handler(ClickRecognizerRef recognizer, void *context) {
ActionMenuLayer *aml = context;
const ActionMenuItem *item = prv_get_item_for_index(aml, aml->selected_index);
if (item && aml->cb) {
aml->cb(item, aml->context);
}
}
static bool prv_aml_is_short(ActionMenuLayer *aml) {
return (aml->num_short_items != 0 || aml->num_items == 0);
}
static int16_t prv_get_cell_padding(ActionMenuLayer *aml) {
const int16_t default_sep_height = 10;
#if PBL_ROUND
// when showing columns, set cells further apart
return prv_aml_is_short(aml) ? default_sep_height : 1;
#elif SCREEN_COLOR_DEPTH_BITS == 1
return default_sep_height;
#else
const int16_t line_height = fonts_get_font_height(aml->layout_cache.font);
const int16_t sep_height = MAX(menu_cell_small_cell_height() - line_height,
default_sep_height) + 1;
return sep_height;
#endif
}
static int16_t prv_get_cell_height_cb(struct MenuLayer *menu_layer,
MenuIndex *cell_index,
void *context) {
ActionMenuLayer *aml = (ActionMenuLayer *)context;
const int16_t line_height = fonts_get_font_height(aml->layout_cache.font);
// If we have short items, just return the line height.
if (prv_aml_is_short(aml)) {
return line_height;
}
#if PBL_ROUND
return menu_layer_is_index_selected(menu_layer, cell_index) ?
MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT :
MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT;
#else
const int16_t max_visible_height = line_height * MAX_NUM_VISIBLE_LINES;
const int16_t actual_height = aml->layout_cache.item_heights[cell_index->row];
return (VERTICAL_PADDING * 2) + MIN(max_visible_height, actual_height);
#endif
}
static int16_t prv_get_separator_height_cb(struct MenuLayer *menu_layer, MenuIndex *cell_index,
void *callback_context) {
// We use the separator to pad the cells (insert spacing), so we compute the height
// needed for each separator here.
ActionMenuLayer *aml = callback_context;
return prv_get_cell_padding(aml);
}
typedef struct ActionMenuSeparatorConfig {
GSize separator;
} ActionMenuSeparatorConfig;
static const ActionMenuSeparatorConfig s_separator_configs[NumPreferredContentSizes] = {
[PreferredContentSizeSmall] = {
.separator = {100, 1},
},
[PreferredContentSizeMedium] = {
.separator = {100, 1},
},
[PreferredContentSizeLarge] = {
.separator = {162, 2},
},
[PreferredContentSizeExtraLarge] = {
.separator = {162, 2},
},
};
static void prv_draw_separator_cb(GContext *ctx, const Layer *cell_layer,
MenuIndex *cell_index, void *callback_context) {
ActionMenuLayer *aml = callback_context;
if (aml->separator_index && cell_index->row == aml->separator_index) {
const PreferredContentSize runtime_platform_default_size =
system_theme_get_default_content_size_for_runtime_platform();
const ActionMenuSeparatorConfig *config = &s_separator_configs[runtime_platform_default_size];
// If this index is the seperator index, we want to draw the separator line
// in the vertical center of the separator
const int16_t nudge_down = PBL_IF_RECT_ELSE(3, 0);
const int16_t nudge_right = menu_cell_basic_horizontal_inset() + 1;
const int16_t separator_width = config->separator.w;
const GRect *cell_layer_bounds = &cell_layer->bounds;
const int16_t offset_x = PBL_IF_RECT_ELSE(nudge_right,
(cell_layer->bounds.size.w - separator_width) / 2);
const int16_t offset_y = (cell_layer_bounds->size.h / 2) + nudge_down;
GPoint separator_start_point = gpoint_add(cell_layer_bounds->origin,
GPoint(offset_x, offset_y));
graphics_context_set_stroke_color(ctx, PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite));
separator_start_point.y += config->separator.h;
for (int i = 0; i < config->separator.h; i++) {
// First point from bottom will be +0, second +1, third +0, etc.
separator_start_point.y--;
separator_start_point.x += i & 1;
graphics_draw_horizontal_line_dotted(ctx, separator_start_point, separator_width);
separator_start_point.x -= i & 1;
}
}
}
static int16_t prv_get_header_height_cb(struct MenuLayer *menu_layer, uint16_t second_index,
void *callback_context) {
ActionMenuLayer *aml = callback_context;
if (!prv_should_center(aml) || prv_aml_is_short(aml) || aml->num_items == 0) {
return 0;
}
const int16_t line_height = fonts_get_font_height(aml->layout_cache.font);
const int16_t padding = prv_get_cell_padding(aml);
const int16_t max_visible_height = line_height * MAX_NUM_VISIBLE_LINES;
const GRect *bounds = &aml->layer.bounds;
int16_t total_h = 0;
for (int16_t idx = 0; idx < aml->num_items; idx++) {
int16_t item_height = aml->layout_cache.item_heights[idx];
total_h += MIN(max_visible_height, item_height);
}
const int16_t header_padding = 6 * aml->num_items;
const int16_t header_height = ((bounds->size.h - total_h) / 2) - padding;
return MAX(header_height - header_padding, 0);
}
static void prv_draw_header_cb(GContext *ctx, const Layer *cell_layer, uint16_t section_index,
void *callback_context) {
// The header here is just being used for padding, so we don't actually need to draw anything.
return;
}
static void prv_selection_changed_cb(struct MenuLayer *menu_layer, MenuIndex new_index,
MenuIndex old_index, void *callback_context) {
ActionMenuLayer *aml = callback_context;
if (new_index.row < aml->num_items) {
// Enable a new item animation to be scheduled
prv_unschedule_item_animation(aml);
aml->selected_index = new_index.row;
}
}
static void prv_changed_proc(Layer *layer) {
ActionMenuLayer *aml = (ActionMenuLayer *)layer;
const GRect *aml_bounds = &layer->bounds;
GRect menu_layer_frame = *aml_bounds;
#if PBL_ROUND
if (prv_aml_is_short(aml)) {
// clip the menu layer to show exactly SHORT_ITEM_MAX_ROWS_SPALDING lines at a time
const int16_t font_height = fonts_get_font_height(aml->layout_cache.font);
const int16_t cell_padding = prv_get_cell_padding(aml);
const int num_visible_rows = MIN(prv_get_num_rows(&aml->menu_layer, 0, aml),
SHORT_ITEM_MAX_ROWS_SPALDING);
menu_layer_frame.size.h = (font_height * num_visible_rows) +
(cell_padding * (num_visible_rows - 1));
grect_align(&menu_layer_frame, aml_bounds, GAlignCenter, true /* clip */);
}
#endif
layer_set_frame(menu_layer_get_layer(&aml->menu_layer), &menu_layer_frame);
}
static void prv_update_proc(Layer *layer, GContext *ctx) {
#if PBL_ROUND
ActionMenuLayer *aml = (ActionMenuLayer *)layer;
const int num_rows = prv_get_num_rows(&aml->menu_layer, 0, aml);
if (prv_aml_is_short(aml) && (num_rows > SHORT_ITEM_MAX_ROWS_SPALDING)) {
// draw some "content indicator" arrows
const GRect *aml_bounds = &layer->bounds;
const GRect *menu_layer_frame = &menu_layer_get_layer(&aml->menu_layer)->frame;
const int16_t arrow_layer_height = (aml_bounds->size.h - menu_layer_frame->size.h) / 2;
const int row = prv_get_menu_layer_row(aml, aml->selected_index);
const GColor bg_color = GColorBlack;
const GColor fg_color = PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite);
GRect arrow_rect = (GRect) { .size = GSize(aml_bounds->size.w, arrow_layer_height) };
if (row >= SHORT_ITEM_MAX_ROWS_SPALDING - 1) {
grect_align(&arrow_rect, aml_bounds, GAlignTop, true /* clip */);
content_indicator_draw_arrow(ctx, &arrow_rect, ContentIndicatorDirectionUp, fg_color,
bg_color, GAlignTop);
}
if (num_rows - row >= SHORT_ITEM_MAX_ROWS_SPALDING) {
grect_align(&arrow_rect, aml_bounds, GAlignBottom, true /* clip */);
content_indicator_draw_arrow(ctx, &arrow_rect, ContentIndicatorDirectionDown, fg_color,
bg_color, GAlignBottom);
}
}
#endif
}
static void prv_update_aml_cache(ActionMenuLayer *aml, int selected_index) {
prv_unschedule_item_animation(aml);
if (aml->layout_cache.item_heights != NULL) {
applib_free(aml->layout_cache.item_heights);
aml->layout_cache.item_heights = NULL;
}
if (aml->num_items > 0) {
// Update the cache of heights. We do this here to avoid recomputing the same
// values repeatedly when we call the menu layer height callback.
aml->layout_cache.item_heights = applib_zalloc(aml->num_items * sizeof(int16_t));
for (int idx = 0; idx < aml->num_items; idx++) {
aml->layout_cache.item_heights[idx] = prv_get_item_line_height(aml, idx);
}
}
#if PBL_ROUND
const bool center_focused = !prv_aml_is_short(aml);
menu_layer_set_center_focused(&aml->menu_layer, center_focused);
#endif
layer_mark_dirty(&aml->layer);
menu_layer_reload_data(&aml->menu_layer);
prv_set_selected_index(aml, selected_index, false /* animated */);
}
// Public API
/////////////////////
void action_menu_layer_click_config_provider(ActionMenuLayer *aml) {
window_single_repeating_click_subscribe(BUTTON_ID_UP, 100, prv_scroll_handler);
window_set_click_context(BUTTON_ID_UP, aml);
window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100, prv_scroll_handler);
window_set_click_context(BUTTON_ID_DOWN, aml);
window_single_click_subscribe(BUTTON_ID_SELECT, prv_select_handler);
window_set_click_context(BUTTON_ID_SELECT, aml);
}
void action_menu_layer_set_callback(ActionMenuLayer *aml,
ActionMenuLayerCallback cb,
void *context) {
aml->cb = cb;
aml->context = context;
}
void action_menu_layer_init(ActionMenuLayer *aml, const GRect *frame) {
layer_init(&aml->layer, frame);
// Since menu_layer_set_callbacks() will call the menu functions, we need to initialize
// the ActionMenuLayer attributes before setting the callbacks onto the menu.
aml->item_animation = (ActionMenuItemAnimation){};
aml->layout_cache = (ActionMenuLayoutCache){
.font = prv_get_item_font()
};
aml->layer.property_changed_proc = prv_changed_proc;
aml->layer.update_proc = prv_update_proc;
menu_layer_init(&aml->menu_layer, &aml->layer.bounds);
menu_layer_set_normal_colors(&aml->menu_layer, GColorBlack,
PBL_IF_COLOR_ELSE(GColorDarkGray, GColorWhite));
#if PBL_ROUND
menu_layer_pad_bottom_enable(&aml->menu_layer, false);
#endif
menu_layer_set_callbacks(&aml->menu_layer, aml, &(MenuLayerCallbacks){
.get_num_rows = prv_get_num_rows,
.draw_row = prv_draw_row,
.get_cell_height = prv_get_cell_height_cb,
.get_separator_height = prv_get_separator_height_cb,
.draw_separator = prv_draw_separator_cb,
.get_header_height = prv_get_header_height_cb,
.draw_header = prv_draw_header_cb,
.selection_changed = prv_selection_changed_cb
});
#if !defined(RECOVERY_FW)
gbitmap_init_with_resource_system(&aml->item_animation.fade_top, SYSTEM_APP,
RESOURCE_ID_ACTION_MENU_FADE_TOP);
gbitmap_init_with_resource_system(&aml->item_animation.fade_bottom, SYSTEM_APP,
RESOURCE_ID_ACTION_MENU_FADE_BOTTOM);
#endif
layer_add_child(&aml->layer, menu_layer_get_layer(&aml->menu_layer));
layer_set_hidden((Layer *)&aml->menu_layer.inverter, true);
aml->menu_layer.selection_animation_disabled = true;
}
void action_menu_layer_deinit(ActionMenuLayer *aml) {
if (aml->layout_cache.item_heights) {
applib_free(aml->layout_cache.item_heights);
}
prv_unschedule_item_animation(aml);
#ifndef RECOVERY_FW
gbitmap_deinit(&aml->item_animation.fade_top);
gbitmap_deinit(&aml->item_animation.fade_bottom);
#endif
menu_layer_deinit(&aml->menu_layer);
}
ActionMenuLayer *action_menu_layer_create(GRect frame) {
ActionMenuLayer *aml = applib_zalloc(sizeof(ActionMenuLayer));
if (!aml) {
return NULL;
}
action_menu_layer_init(aml, &frame);
return aml;
}
void action_menu_layer_destroy(ActionMenuLayer *aml) {
if (!aml) {
return;
}
action_menu_layer_deinit(aml);
applib_free(aml);
}
void action_menu_layer_set_align(ActionMenuLayer *aml, ActionMenuAlign align) {
if (!aml) {
return;
}
aml->layout_cache.align = align;
}
void action_menu_layer_set_items(ActionMenuLayer *aml, const ActionMenuItem* items, int num_items,
unsigned default_selected_item, unsigned separator_index) {
aml->items = items;
aml->num_items = num_items;
aml->separator_index = separator_index;
prv_update_aml_cache(aml, default_selected_item);
}
void action_menu_layer_set_short_items(ActionMenuLayer *aml, const ActionMenuItem* items,
int num_items, unsigned default_selected_item) {
aml->short_items = items;
aml->separator_index = 0;
aml->num_short_items = num_items;
prv_update_aml_cache(aml, default_selected_item);
}