pebble/tests/fw/ui/test_menu_layer_system_cells.c
2025-01-27 11:38:16 -08:00

417 lines
16 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 "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 <stdio.h>
// 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));
}