mirror of
https://github.com/google/pebble.git
synced 2025-05-25 04:34:52 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
337
src/fw/applib/ui/option_menu_window.c
Normal file
337
src/fw/applib/ui/option_menu_window.c
Normal file
|
@ -0,0 +1,337 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue