/* * 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 "applib/graphics/framebuffer.h" #include "applib/graphics/graphics.h" #include "applib/ui/menu_layer.h" #include "applib/ui/status_bar_layer.h" #include "resource/resource.h" #include "resource/resource_ids.auto.h" #include "util/graphics.h" #include "util/size.h" #include "clar.h" #include // Fakes ///////////////////// #include "fake_resource_syscalls.h" #include "fake_spi_flash.h" #include "fixtures/load_test_resources.h" static bool s_cell_is_highlighted = false; // TODO PBL-23041: When round MenuLayer animations are enabled, we need a "is_selected" function bool menu_cell_layer_is_highlighted(const Layer *cell_layer) { return s_cell_is_highlighted; } // Stubs ///////////////////// #include "stubs_analytics.h" #include "stubs_app_state.h" #include "stubs_bootbits.h" #include "stubs_compiled_with_legacy2_sdk.h" #include "stubs_content_indicator.h" #include "stubs_heap.h" #include "stubs_logging.h" #include "stubs_memory_layout.h" #include "stubs_mutex.h" #include "stubs_passert.h" #include "stubs_pbl_malloc.h" #include "stubs_pebble_tasks.h" #include "stubs_print.h" #include "stubs_process_manager.h" #include "stubs_prompt.h" #include "stubs_serial.h" #include "stubs_shell_prefs.h" #include "stubs_sleep.h" #include "stubs_syscalls.h" #include "stubs_task_watchdog.h" #include "stubs_ui_window.h" #include "stubs_unobstructed_area.h" void window_long_click_subscribe(ButtonId button_id, uint16_t delay_ms, ClickHandler down_handler, ClickHandler up_handler) {} void window_set_click_config_provider_with_context(Window *window, ClickConfigProvider click_config_provider, void *context) {} void window_set_click_context(ButtonId button_id, void *context) {} void window_single_click_subscribe(ButtonId button_id, ClickHandler handler) {} void window_single_repeating_click_subscribe(ButtonId button_id, uint16_t repeat_interval_ms, ClickHandler handler) {} // Helper Functions ///////////////////// #include "../graphics/test_graphics.h" #include "../graphics/util.h" // Setup and Teardown //////////////////////////////////// static FrameBuffer *fb; static GContext s_ctx; static GBitmap *s_dest_bitmap; static GBitmap s_tictoc_icon_bitmap; static GBitmap s_smart_alarm_icon_bitmap; static void prv_initialize_icons(void) { gbitmap_init_with_resource(&s_tictoc_icon_bitmap, RESOURCE_ID_MENU_ICON_TICTOC_WATCH); gbitmap_init_with_resource(&s_smart_alarm_icon_bitmap, RESOURCE_ID_SMART_ALARM_ICON_BLACK); } void test_menu_layer_system_cells__initialize(void) { process_manager_set_compiled_with_legacy2_sdk(false); fb = malloc(sizeof(FrameBuffer)); framebuffer_init(fb, &(GSize) {DISP_COLS, DISP_ROWS}); test_graphics_context_init(&s_ctx, fb); framebuffer_clear(fb); // Setup resources fake_spi_flash_init(0, 0x1000000); pfs_init(false); pfs_format(true /* write erase headers */); load_resource_fixture_in_flash(RESOURCES_FIXTURE_PATH, SYSTEM_RESOURCES_FIXTURE_NAME, false /* is_next */); resource_init(); prv_initialize_icons(); } void test_menu_layer_system_cells__cleanup(void) { free(fb); fb = NULL; gbitmap_deinit(&s_tictoc_icon_bitmap); gbitmap_deinit(&s_smart_alarm_icon_bitmap); gbitmap_destroy(s_dest_bitmap); s_dest_bitmap = NULL; } // Helpers ////////////////////// // This struct represents the data for a column of the grid of our resulting test image typedef struct { const char *title_font; const char *subtitle_font; const char *value_font; } MenuLayerSystemCellTestColumnData; static const MenuLayerSystemCellTestColumnData s_menu_system_basic_cell_test_column_data[] = { { NULL, NULL, NULL }, // Use the default fonts }; static const MenuLayerSystemCellTestColumnData s_menu_system_cell_layer_test_column_data[] = { { FONT_KEY_GOTHIC_24_BOLD, FONT_KEY_GOTHIC_14, FONT_KEY_GOTHIC_24_BOLD }, { FONT_KEY_GOTHIC_14, FONT_KEY_GOTHIC_24_BOLD, FONT_KEY_GOTHIC_14 }, }; // This struct represents the data for a row of the grid of our resulting test image typedef struct { char *title; char *subtitle; char *value; GBitmap *icon; MenuCellLayerIconAlign icon_align; bool icon_subbitmap; GBoxModel *icon_box_model; int horizontal_inset; bool icon_form_fit; } MenuLayerSystemCellTestRowData; #define DEFAULT_ICON_ALIGN PBL_IF_RECT_ELSE(MenuCellLayerIconAlign_Left, MenuCellLayerIconAlign_Top) static const MenuLayerSystemCellTestRowData s_menu_system_cell_test_row_data[] = { {"Star Wars", NULL, NULL, NULL, DEFAULT_ICON_ALIGN}, {"The Lord of the Rings", "The Fellowship of the Ring", NULL, NULL, DEFAULT_ICON_ALIGN}, {"The Lord of the Rings", NULL, NULL, &s_tictoc_icon_bitmap, DEFAULT_ICON_ALIGN}, {"The Matrix", "Revolutions", NULL, &s_tictoc_icon_bitmap, DEFAULT_ICON_ALIGN}, {"8:00 AM", "Weekdays", "OFF", NULL, DEFAULT_ICON_ALIGN}, {"8:00 AM", "Weekdays", NULL, &s_tictoc_icon_bitmap, MenuCellLayerIconAlign_Right}, {"8:00 AM", "Weekdays", "OFF", &s_smart_alarm_icon_bitmap, MenuCellLayerIconAlign_TopLeft, false, &(GBoxModel){ .offset = { 0, 5 }, .margin = { 6, 0 }}, PBL_IF_ROUND_ELSE(-6, 0), true}, {"The Lord of the Rings", NULL, NULL, &s_tictoc_icon_bitmap, DEFAULT_ICON_ALIGN, true}, {"The Matrix", "Revolutions", NULL, &s_tictoc_icon_bitmap, DEFAULT_ICON_ALIGN, true}, }; // We render all of the row data's for each of the following heights static const int16_t s_menu_system_cell_test_row_heights[] = { #if PBL_ROUND MENU_CELL_ROUND_UNFOCUSED_SHORT_CELL_HEIGHT, MENU_CELL_ROUND_UNFOCUSED_TALL_CELL_HEIGHT, #endif 0, //!< Will be interpreted as "use menu_cell_basic_cell_height()" MENU_CELL_ROUND_FOCUSED_SHORT_CELL_HEIGHT, MENU_CELL_ROUND_FOCUSED_TALL_CELL_HEIGHT, }; #define GRID_CELL_PADDING 5 static int16_t prv_get_row_height_for_index(int i) { return (s_menu_system_cell_test_row_heights[i] == 0) ? menu_cell_basic_cell_height() : s_menu_system_cell_test_row_heights[i]; } static int16_t prv_calculate_overall_grid_height(void) { int16_t sum = 0; const unsigned int num_distinct_rows = ARRAY_LENGTH(s_menu_system_cell_test_row_data); const unsigned int num_row_heights = ARRAY_LENGTH(s_menu_system_cell_test_row_heights); for (int i = 0; i < num_row_heights; i++) { sum += GRID_CELL_PADDING + prv_get_row_height_for_index(i); } return (sum * num_distinct_rows) + GRID_CELL_PADDING; } typedef enum MenuCellType { MenuCellType_Basic, MenuCellType_BasicCustom, MenuCellType_CellLayer, } MenuCellType; //! Allows testing other cell drawing functions using MenuCellLayerConfig static void prv_menu_cell_draw_dispatch(GContext *ctx, Layer *cell_layer, MenuCellType cell_type, const MenuCellLayerConfig *config, bool icon_subbitmap) { GRect old_icon_bounds = GRectZero; if (config->icon) { old_icon_bounds = config->icon->bounds; if (icon_subbitmap) { gpoint_add_eq(&config->icon->bounds.origin, GPoint(4, 4)); config->icon->bounds.size.w -= 8; config->icon->bounds.size.h -= 8; } } switch (cell_type) { case MenuCellType_Basic: // These should be ignored, we want to make sure they are! menu_cell_basic_draw(ctx, cell_layer, config->title, config->subtitle, config->icon); break; case MenuCellType_BasicCustom: menu_cell_basic_draw_custom( ctx, cell_layer, config->title_font, config->title, config->value_font, config->value, config->subtitle_font, config->subtitle, config->icon, (config->icon_align == MenuCellLayerIconAlign_Right), config->overflow_mode); break; case MenuCellType_CellLayer: menu_cell_layer_draw(ctx, cell_layer, config); break; } if (config->icon) { config->icon->bounds = old_icon_bounds; } } static void prv_draw_cell(MenuCellType cell_type, const GRect *cell_bounds, const MenuLayerSystemCellTestRowData *row_data, const MenuLayerSystemCellTestColumnData *column_data, bool is_selected) { s_ctx.draw_state.drawing_box.origin = cell_bounds->origin; const GRect cell_frame = GRect(0, 0, cell_bounds->size.w, cell_bounds->size.h); const GColor background_color = is_selected ? GColorCobaltBlue : GColorWhite; graphics_context_set_fill_color(&s_ctx, background_color); graphics_fill_rect(&s_ctx, &cell_frame); const GColor foreground_color = is_selected ? GColorWhite : GColorBlack; graphics_context_set_text_color(&s_ctx, foreground_color); graphics_context_set_tint_color(&s_ctx, foreground_color); graphics_context_set_stroke_color(&s_ctx, foreground_color); Layer cell_layer; layer_init(&cell_layer, &cell_frame); cell_layer.is_highlighted = is_selected; s_cell_is_highlighted = is_selected; const MenuCellLayerConfig config = { .title_font = column_data->title_font ? fonts_get_system_font(column_data->title_font) : NULL, .subtitle_font = column_data->subtitle_font ? fonts_get_system_font(column_data->subtitle_font) : NULL, .value_font = column_data->value_font ? fonts_get_system_font(column_data->value_font) : NULL, .title = row_data->title, .subtitle = row_data->subtitle, .value = row_data->value, .icon = row_data->icon, .icon_align = row_data->icon_align, .icon_box_model = row_data->icon_box_model, .icon_form_fit = row_data->icon_form_fit, .horizontal_inset = row_data->horizontal_inset, .overflow_mode = GTextOverflowModeFill, }; prv_menu_cell_draw_dispatch(&s_ctx, &cell_layer, cell_type, &config, row_data->icon_subbitmap); } void prv_prepare_canvas_and_render_cells(MenuCellType cell_type, int16_t cell_width, const MenuLayerSystemCellTestColumnData *columns, unsigned int num_columns, bool is_legacy2) { gbitmap_destroy(s_dest_bitmap); const unsigned int num_row_heights = ARRAY_LENGTH(s_menu_system_cell_test_row_heights); const unsigned int num_rows = ARRAY_LENGTH(s_menu_system_cell_test_row_data); // Multiply num_columns * 2 to account for drawing both focused and unfocused side-by-side const int num_columns_accounting_for_focused_unfocused = num_columns * 2; const int16_t bitmap_width = (cell_width * num_columns_accounting_for_focused_unfocused) + (GRID_CELL_PADDING * (num_columns_accounting_for_focused_unfocused + 1)); const int16_t bitmap_height = prv_calculate_overall_grid_height(); const GSize bitmap_size = GSize(bitmap_width, bitmap_height); s_dest_bitmap = gbitmap_create_blank(bitmap_size, PBL_IF_COLOR_ELSE(GBitmapFormat8Bit, GBitmapFormat1Bit)); s_ctx.dest_bitmap = *s_dest_bitmap; s_ctx.draw_state.clip_box.size = bitmap_size; s_ctx.draw_state.drawing_box.size = bitmap_size; // Fill the bitmap with pink so it's easier to see errors memset(s_dest_bitmap->addr, GColorShockingPinkARGB8, s_dest_bitmap->row_size_bytes * s_dest_bitmap->bounds.size.h); process_manager_set_compiled_with_legacy2_sdk(is_legacy2); int16_t y_offset = 0; for (unsigned int row_index = 0; row_index < num_rows; row_index++) { const MenuLayerSystemCellTestRowData row_data = s_menu_system_cell_test_row_data[row_index]; for (unsigned int row_height_index = 0; row_height_index < num_row_heights; row_height_index++) { y_offset += GRID_CELL_PADDING; const int16_t row_height = prv_get_row_height_for_index(row_height_index); for (unsigned int column_index = 0; column_index < num_columns; column_index++) { const MenuLayerSystemCellTestColumnData column_data = columns[column_index]; int16_t x_offset = GRID_CELL_PADDING + column_index * ((GRID_CELL_PADDING + cell_width) * 2); GRect cell_bounds = GRect(x_offset, y_offset, cell_width, row_height); prv_draw_cell(cell_type, &cell_bounds, &row_data, &column_data, true); cell_bounds.origin.x += cell_width + GRID_CELL_PADDING; prv_draw_cell(cell_type, &cell_bounds, &row_data, &column_data, false); } y_offset += row_height; } } process_manager_set_compiled_with_legacy2_sdk(false); } // Tests ////////////////////// void test_menu_layer_system_cells__basic_cell_width_144_legacy2(void) { #if PLATFORM_TINTIN || PLATFORM_SILK // NOTE: The generated bitmap will look really funky because it's rendering 8bit gbitmaps as // 1bit due to the legacy2 check in gbitmap_get_format. This is normal and expected. const int16_t cell_width = 144; prv_prepare_canvas_and_render_cells( MenuCellType_Basic, cell_width, s_menu_system_basic_cell_test_column_data, ARRAY_LENGTH(s_menu_system_basic_cell_test_column_data), /* is_legacy2 */ true); cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE)); #endif } void test_menu_layer_system_cells__basic_cell_width_144(void) { const int16_t cell_width = 144; prv_prepare_canvas_and_render_cells( MenuCellType_Basic, cell_width, s_menu_system_basic_cell_test_column_data, ARRAY_LENGTH(s_menu_system_basic_cell_test_column_data), /* is_legacy2 */ false); cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE)); } void test_menu_layer_system_cells__basic_custom_cell_width_144(void) { const int16_t cell_width = 144; prv_prepare_canvas_and_render_cells( MenuCellType_BasicCustom, cell_width, s_menu_system_cell_layer_test_column_data, ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data), /* is_legacy2 */ false); cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE)); } void test_menu_layer_system_cells__cell_width_32(void) { #if PBL_ROUND const int16_t cell_width = 32; prv_prepare_canvas_and_render_cells( MenuCellType_CellLayer, cell_width, s_menu_system_cell_layer_test_column_data, ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data), /* is_legacy2 */ false); cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE)); #endif } void test_menu_layer_system_cells__cell_width_100(void) { const int16_t cell_width = 100; prv_prepare_canvas_and_render_cells( MenuCellType_CellLayer, cell_width, s_menu_system_cell_layer_test_column_data, ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data), /* is_legacy2 */ false); cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE)); } void test_menu_layer_system_cells__cell_width_144(void) { const int16_t cell_width = 144; prv_prepare_canvas_and_render_cells( MenuCellType_CellLayer, cell_width, s_menu_system_cell_layer_test_column_data, ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data), /* is_legacy2 */ false); cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE)); } void test_menu_layer_system_cells__cell_width_180(void) { const int16_t cell_width = 180; prv_prepare_canvas_and_render_cells( MenuCellType_CellLayer, cell_width, s_menu_system_cell_layer_test_column_data, ARRAY_LENGTH(s_menu_system_cell_layer_test_column_data), /* is_legacy2 */ false); cl_check(gbitmap_pbi_eq(s_dest_bitmap, TEST_PBI_FILE)); }