mirror of
https://github.com/google/pebble.git
synced 2025-05-24 20:24:53 +00:00
337 lines
14 KiB
C
337 lines
14 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 "option_menu_window.h"
|
|
|
|
#include "applib/applib_malloc.auto.h"
|
|
#include "resource/resource_ids.auto.h"
|
|
#include "shell/system_theme.h"
|
|
#include "system/passert.h"
|
|
|
|
typedef struct OptionMenuStyle {
|
|
#if PBL_RECT
|
|
uint16_t cell_heights[OptionMenuContentTypeCount];
|
|
#endif
|
|
int16_t top_inset;
|
|
int16_t right_icon_spacing;
|
|
int16_t text_inset_single;
|
|
int16_t text_inset_multi;
|
|
int16_t right_text_inset_with_icon;
|
|
} OptionMenuStyle;
|
|
|
|
static const OptionMenuStyle s_style_medium = {
|
|
#if PBL_RECT
|
|
.cell_heights[OptionMenuContentType_DoubleLine] = 56,
|
|
#endif
|
|
.right_icon_spacing = PBL_IF_RECT_ELSE(7, 35),
|
|
};
|
|
|
|
static const OptionMenuStyle s_style_large = {
|
|
#if PBL_RECT
|
|
.cell_heights[OptionMenuContentType_SingleLine] = 46,
|
|
#endif
|
|
.top_inset = 1,
|
|
.right_icon_spacing = PBL_IF_RECT_ELSE(10, 35),
|
|
.text_inset_single = -1,
|
|
.text_inset_multi = -3,
|
|
.right_text_inset_with_icon = 4,
|
|
};
|
|
|
|
static const OptionMenuStyle * const s_styles[NumPreferredContentSizes] = {
|
|
[PreferredContentSizeSmall] = &s_style_medium,
|
|
[PreferredContentSizeMedium] = &s_style_medium,
|
|
[PreferredContentSizeLarge] = &s_style_large,
|
|
[PreferredContentSizeExtraLarge] = &s_style_large,
|
|
};
|
|
|
|
static const OptionMenuStyle *prv_get_style(void) {
|
|
return s_styles[PreferredContentSizeDefault];
|
|
}
|
|
|
|
static uint16_t prv_get_num_rows_callback(MenuLayer *menu_layer, uint16_t section_index,
|
|
void *context) {
|
|
OptionMenu *option_menu = context;
|
|
if (option_menu->callbacks.get_num_rows) {
|
|
return option_menu->callbacks.get_num_rows(option_menu, option_menu->context);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint16_t option_menu_default_cell_height(OptionMenuContentType content_type, bool selected) {
|
|
const OptionMenuStyle * const UNUSED style = prv_get_style();
|
|
const int16_t cell_height =
|
|
PBL_IF_ROUND_ELSE(selected ? MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT :
|
|
MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT,
|
|
style->cell_heights[content_type]);
|
|
return cell_height ?: menu_cell_basic_cell_height();
|
|
}
|
|
|
|
static int16_t prv_get_cell_height_callback(MenuLayer *menu_layer, MenuIndex *cell_index,
|
|
void *context) {
|
|
const bool is_selected = menu_layer_is_index_selected(menu_layer, cell_index);
|
|
OptionMenu *option_menu = context;
|
|
if (option_menu->callbacks.get_cell_height) {
|
|
return option_menu->callbacks.get_cell_height(option_menu, cell_index->row, is_selected,
|
|
option_menu->context);
|
|
} else {
|
|
return option_menu_default_cell_height(option_menu->content_type, is_selected);
|
|
}
|
|
}
|
|
|
|
static int32_t prv_draw_selection_icon(const OptionMenu *option_menu, GContext *ctx,
|
|
const GRect *cell_layer_bounds, bool is_chosen) {
|
|
const int32_t left_icon_spacing = PBL_IF_RECT_ELSE(0, 14);
|
|
const GSize not_chosen_icon_bounds = gbitmap_get_bounds(&option_menu->not_chosen_image).size;
|
|
const GSize chosen_icon_bounds = gbitmap_get_bounds(&option_menu->chosen_image).size;
|
|
PBL_ASSERTN(gsize_equal(¬_chosen_icon_bounds, &chosen_icon_bounds));
|
|
GRect icon_frame = { .size = chosen_icon_bounds };
|
|
grect_align(&icon_frame, cell_layer_bounds, GAlignRight, false);
|
|
|
|
const OptionMenuStyle * const style = prv_get_style();
|
|
icon_frame.origin.x -= style->right_icon_spacing;
|
|
|
|
const GBitmap *const icon =
|
|
is_chosen ? &option_menu->chosen_image : &option_menu->not_chosen_image;
|
|
graphics_context_set_compositing_mode(ctx, GCompOpTint);
|
|
graphics_draw_bitmap_in_rect(ctx, icon, &icon_frame);
|
|
return icon_frame.size.w + left_icon_spacing + style->right_icon_spacing;
|
|
}
|
|
|
|
static void prv_draw_row_callback(GContext *ctx, const Layer *cell_layer, MenuIndex *cell_index,
|
|
void *context) {
|
|
OptionMenu *option_menu = context;
|
|
|
|
const MenuIndex selected = menu_layer_get_selected_index(&option_menu->menu_layer);
|
|
const bool is_selected = (menu_index_compare(&selected, cell_index) == 0);
|
|
|
|
const GRect *cell_layer_bounds = &cell_layer->bounds;
|
|
GRect remaining_rect = *cell_layer_bounds;
|
|
|
|
if (option_menu->icons_enabled) {
|
|
const bool is_chosen = (cell_index->row == option_menu->choice);
|
|
const int32_t left_inset_x = PBL_IF_RECT_ELSE(0, 14);
|
|
const int32_t right_inset_x = prv_draw_selection_icon(option_menu, ctx, &remaining_rect,
|
|
is_chosen);
|
|
remaining_rect = grect_inset(remaining_rect, GEdgeInsets(0, right_inset_x, 0, left_inset_x));
|
|
}
|
|
|
|
#if PBL_ROUND
|
|
if (!is_selected && option_menu->icons_enabled) {
|
|
const int32_t left_text_inset_to_prevent_clipping = 8;
|
|
remaining_rect = grect_inset(remaining_rect,
|
|
GEdgeInsets(0, 0, 0, left_text_inset_to_prevent_clipping));
|
|
}
|
|
#else
|
|
const OptionMenuStyle * const style = prv_get_style();
|
|
const int32_t left_text_inset = menu_cell_basic_horizontal_inset();
|
|
const int32_t right_text_inset = option_menu->icons_enabled ? style->right_text_inset_with_icon :
|
|
left_text_inset;
|
|
remaining_rect = grect_inset(remaining_rect, GEdgeInsets(style->top_inset, right_text_inset, 0,
|
|
left_text_inset));
|
|
#endif
|
|
|
|
if (option_menu->callbacks.draw_row) {
|
|
option_menu->callbacks.draw_row(option_menu, ctx, cell_layer, &remaining_rect, cell_index->row,
|
|
is_selected, option_menu->context);
|
|
}
|
|
|
|
}
|
|
|
|
static void prv_select_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *context) {
|
|
OptionMenu *option_menu = context;
|
|
option_menu->choice = cell_index->row;
|
|
layer_mark_dirty((Layer *)&option_menu->menu_layer);
|
|
if (option_menu->callbacks.select) {
|
|
option_menu->callbacks.select(option_menu, option_menu->choice, option_menu->context);
|
|
}
|
|
}
|
|
|
|
static void prv_window_load(Window *window) {
|
|
OptionMenu *option_menu = window_get_user_data(window);
|
|
|
|
menu_layer_set_callbacks(&option_menu->menu_layer, option_menu, &(MenuLayerCallbacks) {
|
|
.get_cell_height = prv_get_cell_height_callback,
|
|
.get_num_rows = prv_get_num_rows_callback,
|
|
.draw_row = prv_draw_row_callback,
|
|
.select_click = prv_select_callback
|
|
});
|
|
menu_layer_set_click_config_onto_window(&option_menu->menu_layer, window);
|
|
if (option_menu->choice != OPTION_MENU_CHOICE_NONE) {
|
|
menu_layer_set_selected_index(&option_menu->menu_layer, MenuIndex(0, option_menu->choice),
|
|
MenuRowAlignCenter, false);
|
|
}
|
|
layer_add_child(window_get_root_layer(window), menu_layer_get_layer(&option_menu->menu_layer));
|
|
}
|
|
|
|
static void prv_window_unload(Window *window) {
|
|
OptionMenu *option_menu = window_get_user_data(window);
|
|
if (option_menu->callbacks.unload) {
|
|
option_menu->callbacks.unload(option_menu, option_menu->context);
|
|
}
|
|
}
|
|
|
|
void option_menu_set_status_colors(OptionMenu *option_menu, GColor background, GColor foreground) {
|
|
option_menu->status_colors.background = background;
|
|
option_menu->status_colors.foreground = foreground;
|
|
status_bar_layer_set_colors(&option_menu->status_layer,
|
|
option_menu->status_colors.background,
|
|
option_menu->status_colors.foreground);
|
|
}
|
|
|
|
void option_menu_set_normal_colors(OptionMenu *option_menu, GColor background, GColor foreground) {
|
|
option_menu->normal_colors.background = background;
|
|
option_menu->normal_colors.foreground = foreground;
|
|
menu_layer_set_normal_colors(&option_menu->menu_layer,
|
|
option_menu->normal_colors.background,
|
|
option_menu->normal_colors.foreground);
|
|
}
|
|
|
|
void option_menu_set_highlight_colors(OptionMenu *option_menu, GColor background,
|
|
GColor foreground) {
|
|
option_menu->highlight_colors.background = background;
|
|
option_menu->highlight_colors.foreground = foreground;
|
|
menu_layer_set_highlight_colors(&option_menu->menu_layer,
|
|
option_menu->highlight_colors.background,
|
|
option_menu->highlight_colors.foreground);
|
|
}
|
|
|
|
void option_menu_set_callbacks(OptionMenu *option_menu, const OptionMenuCallbacks *callbacks,
|
|
void *context) {
|
|
option_menu->callbacks = *callbacks;
|
|
option_menu->context = context;
|
|
}
|
|
|
|
void option_menu_set_title(OptionMenu *option_menu, const char *title) {
|
|
option_menu->title = title;
|
|
status_bar_layer_set_title(&option_menu->status_layer, title, false, false);
|
|
}
|
|
|
|
void option_menu_set_choice(OptionMenu *option_menu, int choice) {
|
|
option_menu->choice = choice;
|
|
layer_mark_dirty((Layer *)&option_menu->menu_layer);
|
|
}
|
|
|
|
void option_menu_set_content_type(OptionMenu *option_menu, OptionMenuContentType content_type) {
|
|
option_menu->content_type = content_type;
|
|
}
|
|
|
|
void option_menu_reload_data(OptionMenu *option_menu) {
|
|
menu_layer_reload_data(&option_menu->menu_layer);
|
|
}
|
|
|
|
void option_menu_set_icons_enabled(OptionMenu *option_menu, bool icons_enabled) {
|
|
option_menu->icons_enabled = icons_enabled;
|
|
}
|
|
|
|
void option_menu_configure(OptionMenu *option_menu,
|
|
const OptionMenuConfig *config) {
|
|
option_menu_set_title(option_menu, config->title);
|
|
option_menu_set_choice(option_menu, config->choice);
|
|
option_menu_set_content_type(option_menu, config->content_type);
|
|
option_menu_set_status_colors(option_menu, config->status_colors.background,
|
|
config->status_colors.foreground);
|
|
option_menu_set_highlight_colors(option_menu, config->highlight_colors.background,
|
|
config->highlight_colors.foreground);
|
|
option_menu_set_icons_enabled(option_menu, config->icons_enabled);
|
|
}
|
|
|
|
void option_menu_init(OptionMenu *option_menu) {
|
|
*option_menu = (OptionMenu) {
|
|
.choice = OPTION_MENU_CHOICE_NONE,
|
|
.title_font = system_theme_get_font_for_default_size(TextStyleFont_MenuCellTitle),
|
|
};
|
|
|
|
// radio button icons are enabled by default
|
|
option_menu_set_icons_enabled(option_menu, true);
|
|
|
|
GBitmap *chosen_image = &option_menu->chosen_image;
|
|
gbitmap_init_with_resource(chosen_image, RESOURCE_ID_CHECKED_RADIO_BUTTON);
|
|
GBitmap *not_chosen_image = &option_menu->not_chosen_image;
|
|
gbitmap_init_with_resource(not_chosen_image, RESOURCE_ID_UNCHECKED_RADIO_BUTTON);
|
|
|
|
window_init(&option_menu->window, WINDOW_NAME("OptionMenu"));
|
|
window_set_user_data(&option_menu->window, option_menu);
|
|
window_set_window_handlers(&option_menu->window, &(WindowHandlers) {
|
|
.load = prv_window_load,
|
|
.unload = prv_window_unload,
|
|
});
|
|
|
|
StatusBarLayer *status_layer = &option_menu->status_layer;
|
|
status_bar_layer_init(status_layer);
|
|
status_bar_layer_set_separator_mode(status_layer, OPTION_MENU_STATUS_SEPARATOR_MODE);
|
|
layer_add_child(&option_menu->window.layer, &status_layer->layer);
|
|
|
|
MenuLayer *menu_layer = &option_menu->menu_layer;
|
|
GRect bounds = grect_inset(option_menu->window.layer.bounds, (GEdgeInsets) {
|
|
.top = STATUS_BAR_LAYER_HEIGHT,
|
|
.bottom = PBL_IF_RECT_ELSE(0, STATUS_BAR_LAYER_HEIGHT),
|
|
});
|
|
menu_layer_init(menu_layer, &bounds);
|
|
}
|
|
|
|
void option_menu_deinit(OptionMenu *option_menu) {
|
|
menu_layer_deinit(&option_menu->menu_layer);
|
|
status_bar_layer_deinit(&option_menu->status_layer);
|
|
window_deinit(&option_menu->window);
|
|
|
|
gbitmap_deinit(&option_menu->chosen_image);
|
|
gbitmap_deinit(&option_menu->not_chosen_image);
|
|
}
|
|
|
|
OptionMenu *option_menu_create(void) {
|
|
OptionMenu *option_menu = applib_type_malloc(OptionMenu);
|
|
if (!option_menu) {
|
|
return NULL;
|
|
}
|
|
option_menu_init(option_menu);
|
|
return option_menu;
|
|
}
|
|
|
|
void option_menu_destroy(OptionMenu *option_menu) {
|
|
option_menu_deinit(option_menu);
|
|
applib_free(option_menu);
|
|
}
|
|
|
|
void option_menu_system_draw_row(OptionMenu *option_menu, GContext *ctx, const Layer *cell_layer,
|
|
const GRect *cell_frame, const char *title, bool selected,
|
|
void *context) {
|
|
const GTextOverflowMode overflow_mode = GTextOverflowModeTrailingEllipsis;
|
|
// On rectangular, always align to the left. On round, align to the right if we have an icon and
|
|
// otherwise to the center. Icons on the right with text in the center looks very bad and wastes
|
|
// text space.
|
|
const GTextAlignment text_alignment =
|
|
PBL_IF_RECT_ELSE(GTextAlignmentLeft,
|
|
option_menu->icons_enabled ? GTextAlignmentRight : GTextAlignmentCenter);
|
|
GFont const title_font = option_menu->title_font;
|
|
const GSize text_size = graphics_text_layout_get_max_used_size(ctx, title, title_font,
|
|
*cell_frame, overflow_mode,
|
|
text_alignment, NULL);
|
|
GRect text_frame = *cell_frame;
|
|
const int min_text_height = fonts_get_font_height(title_font);
|
|
text_frame.size = text_size;
|
|
const GAlign text_frame_alignment =
|
|
PBL_IF_RECT_ELSE(GAlignLeft, option_menu->icons_enabled ? GAlignRight : GAlignCenter);
|
|
grect_align(&text_frame, cell_frame, text_frame_alignment, true /* clips */);
|
|
const OptionMenuStyle * const style = prv_get_style();
|
|
const int16_t text_inset = (text_size.h > min_text_height) ? style->text_inset_multi :
|
|
style->text_inset_single;
|
|
text_frame = grect_inset(text_frame, GEdgeInsets(0, text_inset));
|
|
text_frame.origin.y -= fonts_get_font_cap_offset(title_font);
|
|
|
|
if (title) {
|
|
graphics_draw_text(ctx, title, title_font, text_frame, overflow_mode, text_alignment, NULL);
|
|
}
|
|
}
|