/* * 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); } }