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

360 lines
13 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_window.h"
#include "action_menu_window_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/ui/status_bar_layer.h"
#include "applib/ui/window.h"
#include "applib/ui/window_stack.h"
#include "process_state/app_state/app_state.h"
#include "services/normal/timeline/timeline.h"
#include "services/common/i18n/i18n.h"
#define ACTION_MENU_DEFAULT_BACKGROUND_COLOR GColorWhite
static const int IN_OUT_ANIMATION_DURATION = 200;
static void prv_invoke_will_close(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
if (data->config.will_close) {
data->config.will_close(action_menu,
data->performed_item,
data->config.context);
}
}
static void prv_invoke_did_close(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
if (data->config.did_close) {
data->config.did_close(action_menu,
data->performed_item,
data->config.context);
}
}
static void prv_action_window_push(WindowStack *window_stack, ActionMenu *action_menu,
bool animated) {
window_stack_push(window_stack, &action_menu->window, animated);
}
static void prv_action_window_pop(bool animated, ActionMenu *action_menu) {
prv_invoke_will_close(action_menu);
window_stack_remove(&action_menu->window, animated);
}
static void prv_action_window_insert_below(ActionMenu *action_menu, Window *window) {
window_stack_insert_next(action_menu->window.parent_window_stack, window);
}
static void prv_remove_window(Window *window) {
window_stack_remove(window, false /* animated */);
}
static void prv_view_model_did_change(ActionMenuData *data) {
ActionMenuViewModel *vm = &data->view_model;
const ActionMenuLevel *cur_level = vm->cur_level;
GRect frame = grect_inset(data->action_menu.window.layer.frame, vm->menu_insets);
layer_set_frame(&data->action_menu_layer.layer, &frame);
if (cur_level->display_mode == ActionMenuLevelDisplayModeThin) {
action_menu_layer_set_items(&data->action_menu_layer, NULL, 0, 0, 0);
action_menu_layer_set_short_items(&data->action_menu_layer,
cur_level->items,
cur_level->num_items,
cur_level->default_selected_item);
} else {
action_menu_layer_set_short_items(&data->action_menu_layer, NULL, 0, 0);
action_menu_layer_set_items(&data->action_menu_layer, cur_level->items, cur_level->num_items,
cur_level->default_selected_item, cur_level->separator_index);
}
crumbs_layer_set_level(&data->crumbs_layer, vm->num_dots);
}
static void prv_next_level_anim_stopped(Animation *anim, bool finished, void *context) {
// update the view model
AnimationContext *anim_ctx = (AnimationContext*) context;
ActionMenuData *data = window_get_user_data((Window*) anim_ctx->window);
if (!data || !finished) {
// We could have gotten cleaned up in the middle of an animation, bail
applib_free(anim_ctx);
return;
}
if (data->view_model.cur_level->parent_level == anim_ctx->next_level) {
--data->view_model.num_dots;
} else {
++data->view_model.num_dots;
}
data->view_model.cur_level = anim_ctx->next_level;
// update the view
prv_view_model_did_change(data);
applib_free(anim_ctx);
}
static GEdgeInsets prv_action_menu_insets(Window *window) {
const int crumbs_width = crumbs_layer_width();
return (GEdgeInsets) {
.top = PBL_IF_RECT_ELSE(0, STATUS_BAR_LAYER_HEIGHT),
.right = PBL_IF_RECT_ELSE(0, crumbs_width),
.bottom = PBL_IF_RECT_ELSE(0, STATUS_BAR_LAYER_HEIGHT),
.left = crumbs_width,
};
}
static Animation* prv_create_content_in_animation(ActionMenuData *data,
const ActionMenuLevel *level) {
// animate the ease in of the new level
const GRect window_frame = data->action_menu.window.layer.frame;
const GEdgeInsets insets = prv_action_menu_insets(&data->action_menu.window);
GRect stop = grect_inset(window_frame, insets);
GRect start = stop;
start.origin.x -= crumbs_layer_width();
PropertyAnimation *prop_anim =
property_animation_create_layer_frame((Layer *)&data->action_menu_layer,
&start,
&stop);
Animation *content_in = property_animation_get_animation(prop_anim);
animation_set_duration(content_in, IN_OUT_ANIMATION_DURATION);
#if !defined(PLATFORM_TINTIN)
// animate the dots
Animation *crumbs_anim = crumbs_layer_get_animation(&data->crumbs_layer);
animation_set_duration(crumbs_anim, IN_OUT_ANIMATION_DURATION);
// combine the two
Animation *spawn_anim = animation_spawn_create(content_in, crumbs_anim, NULL);
return spawn_anim;
#else
return content_in;
#endif
}
static Animation* prv_create_content_out_animation(ActionMenuData *data,
const ActionMenuLevel *level) {
// animate the ease out of the current level
GRect *start = &data->action_menu_layer.layer.frame;
GRect stop = *start;
stop.origin.x = crumbs_layer_width() - start->size.w;
PropertyAnimation *prop_anim =
property_animation_create_layer_frame((Layer *)&data->action_menu_layer, start, &stop);
Animation *content_out = property_animation_get_animation(prop_anim);
animation_set_duration(content_out, IN_OUT_ANIMATION_DURATION);
AnimationHandlers anim_handlers = {
.started = NULL,
.stopped = prv_next_level_anim_stopped,
};
AnimationContext *anim_ctx = applib_type_malloc(AnimationContext);
*anim_ctx = (AnimationContext) {
.window = &data->action_menu.window,
.next_level = level,
};
animation_set_handlers(content_out, anim_handlers, anim_ctx);
return content_out;
}
static void prv_set_level(ActionMenuData *data, const ActionMenuLevel *level) {
if (animation_is_scheduled(data->level_change_anim)) {
// We are already animating.
return;
}
Animation *content_out = prv_create_content_out_animation(data, level);
Animation *content_in = prv_create_content_in_animation(data, level);
data->level_change_anim = animation_sequence_create(content_out, content_in, NULL);
animation_schedule(data->level_change_anim);
}
static void prv_action_callback(const ActionMenuItem *item, void *context) {
ActionMenu *action_menu = context;
ActionMenuData *data = window_get_user_data(&action_menu->window);
if (item->is_leaf && item->perform_action) {
item->perform_action(action_menu, item, data->config.context);
data->performed_item = item;
if (!data->frozen) {
prv_action_window_pop(true /*animated*/, action_menu);
}
} else if (item->next_level) {
prv_set_level(data, item->next_level);
}
}
static void prv_back_click_handler(ClickRecognizerRef recognizer, void *context) {
ActionMenuData *data = context;
if (animation_is_scheduled(data->level_change_anim)) {
animation_set_elapsed(data->level_change_anim,
animation_get_duration(data->level_change_anim, true, true));
}
ActionMenuLevel *parent_level = data->view_model.cur_level->parent_level;
if (parent_level) {
prv_set_level(data, parent_level);
} else {
prv_action_window_pop(true, &data->action_menu);
}
}
static void prv_click_config_provider(void *context) {
ActionMenuData *data = context;
action_menu_layer_click_config_provider(&data->action_menu_layer);
window_single_click_subscribe(BUTTON_ID_BACK, prv_back_click_handler);
window_set_click_context(BUTTON_ID_BACK, data);
}
static void prv_action_window_load(Window *window) {
ActionMenuData *data = window_get_user_data(window);
// Init action menu layer
ActionMenuLayer *action_menu_layer = &data->action_menu_layer;
action_menu_layer_init(action_menu_layer, &GRectZero);
action_menu_layer_set_callback(action_menu_layer, prv_action_callback, (void *)window);
action_menu_layer_set_align(action_menu_layer, data->config.align);
// Init crumbs layer
CrumbsLayer *crumbs_layer = &data->crumbs_layer;
GRect frame = window_get_root_layer(window)->frame;
#if PBL_RECT
// on round display, the layer fills a full circle
// here (on rect) it's just a small vertical stripe on the left
frame.size.w = crumbs_layer_width();
#endif
crumbs_layer_init(&data->crumbs_layer, &frame, data->config.colors.background,
data->config.colors.foreground);
// Add them to the tree
layer_add_child(window_get_root_layer(window), (Layer *)action_menu_layer);
layer_add_child(window_get_root_layer(window), (Layer *)crumbs_layer);
// Click config
window_set_click_config_provider_with_context(window, prv_click_config_provider, data);
// Init the view model
data->view_model = (ActionMenuViewModel) {
.cur_level = data->config.root_level,
.menu_insets = prv_action_menu_insets(window),
.num_dots = 1,
};
prv_view_model_did_change(data);
}
static void prv_action_window_unload(Window *window) {
ActionMenuData *data = window_get_user_data(window);
// call did close callback so user can cleanup
prv_invoke_did_close((ActionMenu *)window);
// cleanup
animation_unschedule(data->level_change_anim);
action_menu_layer_deinit(&data->action_menu_layer);
crumbs_layer_deinit(&data->crumbs_layer);
applib_free(data);
}
static void prv_dummy_click_config(void *data) {
}
ActionMenuLevel *action_menu_get_root_level(ActionMenu *action_menu) {
if (!action_menu) return NULL;
ActionMenuData *data = window_get_user_data(&action_menu->window);
return (ActionMenuLevel *)data->config.root_level;
}
void *action_menu_get_context(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
return data->config.context;
}
void action_menu_freeze(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
window_set_click_config_provider(&action_menu->window, prv_dummy_click_config);
data->frozen = true;
}
void action_menu_unfreeze(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
window_set_click_config_provider_with_context(&action_menu->window,
prv_click_config_provider, data);
data->frozen = false;
}
bool action_menu_is_frozen(ActionMenu *action_menu) {
ActionMenuData *data = window_get_user_data(&action_menu->window);
return data->frozen;
}
void action_menu_close(ActionMenu *action_menu, bool animated) {
prv_action_window_pop(animated, action_menu);
}
void action_menu_set_result_window(ActionMenu *action_menu, Window *result_window) {
if (!action_menu) return;
// remove existing result window
ActionMenuData *data = window_get_user_data(&action_menu->window);
if (data->result_window) {
prv_remove_window(data->result_window);
}
// insert new result window
if (result_window) {
prv_action_window_insert_below(action_menu, result_window);
}
data->result_window = result_window;
}
void action_menu_set_align(ActionMenuConfig *config, ActionMenuAlign align) {
if (!config) {
return;
}
config->align = align;
}
ActionMenu *action_menu_open(WindowStack *window_stack, ActionMenuConfig *config) {
ActionMenuData *data = applib_type_zalloc(ActionMenuData);
data->config = *config;
#if SCREEN_COLOR_DEPTH_BITS == 8
// Apply defaults if client didn't assign foreground/background colors
if (gcolor_is_invisible(data->config.colors.background)) {
data->config.colors.background = ACTION_MENU_DEFAULT_BACKGROUND_COLOR;
}
if (gcolor_is_invisible((data->config.colors.foreground))) {
data->config.colors.foreground = gcolor_legible_over(data->config.colors.background);
}
#else
data->config.colors.background = GColorLightGray;
data->config.colors.foreground = GColorBlack;
#endif
Window *window = &data->action_menu.window;
window_init(window, WINDOW_NAME("Action Menu"));
window_set_user_data(window, data);
window_set_fullscreen(window, true);
window_set_background_color(window, GColorBlack);
window_set_window_handlers(window, &(WindowHandlers) {
.load = prv_action_window_load,
.unload = prv_action_window_unload,
});
prv_action_window_push(window_stack, &data->action_menu, true /* animated */);
return &data->action_menu;
}
ActionMenu *app_action_menu_open(ActionMenuConfig *config) {
return action_menu_open(app_state_get_window_stack(), config);
}