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

431 lines
19 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 "util/iterator.h"
#include "applib/graphics/utf8.h"
#include "applib/graphics/text.h"
#include "applib/graphics/text_layout_private.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/framebuffer.h"
#include "clar.h"
///////////////////////////////////////////////////////////
// Stubs
#include "stubs_logging.h"
#include "stubs_passert.h"
#include "stubs_hexdump.h"
#include "stubs_heap.h"
#include "stubs_pebble_tasks.h"
#include "stubs_pbl_malloc.h"
#include "stubs_applib_resource.h"
#include "stubs_app_state.h"
#include "stubs_fonts.h"
#include "stubs_text_resources.h"
#include "stubs_text_render.h"
#include "stubs_reboot_reason.h"
#include "stubs_resources.h"
#include "stubs_syscalls.h"
#include "stubs_compiled_with_legacy2_sdk.h"
#if SCREEN_COLOR_DEPTH_BITS == 8
#define FONT_LINE_DELTA 2
#else
#define FONT_LINE_DELTA 0
#endif
///////////////////////////////////////////////////////////
// Tests
// NOTE: Font height is set to be 10 in stubs_fonts.h
void test_text_layout__ellipsis_overflow(void) {
GContext gcontext;
FrameBuffer *fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) { DISP_COLS, DISP_ROWS });
graphics_context_init(&gcontext, fb, GContextInitializationMode_App);
framebuffer_clear(fb);
GFont font = (GFont) { 0 };
GRect box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 20 * HORIZ_ADVANCE_PX + 1, 13 } };
TextLayoutExtended layout = (TextLayoutExtended) {
.hash = 0,
.box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 20 * HORIZ_ADVANCE_PX + 1, 13 } },
.font = (GFont) { 0 },
.overflow_mode = GTextOverflowModeWordWrap,
.alignment = GTextAlignmentLeft,
.max_used_size = (GSize) { 0, 0 }
};
layout.box = box;
graphics_draw_text(&gcontext,
"Twitter\n@pebble is talking about a lot of really really cool important stuff.\n",
font, box,
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 8 * HORIZ_ADVANCE_PX);
graphics_draw_text(&gcontext, "Twitter\n\n\n\n\n\n\n\n", font, box,
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 8 * HORIZ_ADVANCE_PX);
graphics_draw_text(&gcontext, "Twitter \n \n \n\n \n \n \n\n ", font, box,
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 8 * HORIZ_ADVANCE_PX);
}
void test_text_layout__cache_vert_overflow(void) {
GContext gcontext = (GContext) { };
GFont font = (GFont) { 0 };
GRect box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 4 * HORIZ_ADVANCE_PX + 1, 2 * FONT_HEIGHT + 1 } };
TextLayoutExtended layout = (TextLayoutExtended) {
.hash = 0,
.box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 7 * HORIZ_ADVANCE_PX + 1, FONT_HEIGHT - 1 } },
.font = (GFont) { 0 },
.overflow_mode = GTextOverflowModeWordWrap,
.alignment = GTextAlignmentLeft,
.max_used_size = (GSize) { 0, 0 }
};
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper", font, box, GTextOverflowModeFill, GTextAlignmentLeft, (void*)&layout);
cl_assert(layout.box.size.w == box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX);
cl_assert_equal_i(layout.max_used_size.h, 2 * FONT_HEIGHT); // 2 lines - all that will completely fit in the box ("Jr\nWho-")
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
cl_assert(layout.box.size.w == box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX);
cl_assert_equal_i(layout.max_used_size.h, 3 * FONT_HEIGHT); // 3 lines - one line extra being layed out so that it will clip ("Jr\nWho-\npper")
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper 123", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX);
cl_assert_equal_i(layout.max_used_size.h, 3 * FONT_HEIGHT); // 3 lines - but not 4, since the fourth has no chance of appearing ("Jr\nWho-\npper")
}
void test_text_layout__cache_vert_overflow_first_line(void) {
GContext gcontext = (GContext) { };
GFont font = (GFont) { 0 };
GRect box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 5 * HORIZ_ADVANCE_PX + 1, 7 } };
TextLayoutExtended layout = (TextLayoutExtended) {
.hash = 0,
.box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 7 * HORIZ_ADVANCE_PX + 1, FONT_HEIGHT - 1 } },
.font = (GFont) { 0 },
.overflow_mode = GTextOverflowModeWordWrap,
.alignment = GTextAlignmentLeft,
.max_used_size = (GSize) { 0, 0 }
};
// In all cases, the first line should be layed out (not truncated)
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper", font, box, GTextOverflowModeFill, GTextAlignmentLeft, (void*)&layout);
cl_assert(layout.box.size.w == box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 5 * HORIZ_ADVANCE_PX); // "JR..."
cl_assert_equal_i(layout.max_used_size.h, FONT_HEIGHT);
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper", font, box, GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert(layout.box.size.w == box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 5 * HORIZ_ADVANCE_PX); // "JR..."
cl_assert_equal_i(layout.max_used_size.h, FONT_HEIGHT);
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.max_used_size.w, 2 * HORIZ_ADVANCE_PX); // "JR\nWhopper"
cl_assert_equal_i(layout.max_used_size.h, FONT_HEIGHT);
}
void test_text_layout__cache_vert_overflow_with_newline(void) {
GContext gcontext = (GContext) { };
GFont font = (GFont) { 0 };
GRect box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 5 * HORIZ_ADVANCE_PX + 1, 2 * FONT_HEIGHT + 1 } };
TextLayoutExtended layout = (TextLayoutExtended) {
.hash = 0,
.box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 7 * HORIZ_ADVANCE_PX + 1, FONT_HEIGHT - 1 } },
.font = (GFont) { 0 },
.overflow_mode = GTextOverflowModeWordWrap,
.alignment = GTextAlignmentLeft,
.max_used_size = (GSize) { 0, 0 }
};
graphics_text_layout_get_max_used_size(&gcontext, "JR\n\nWhop", font, box, GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 2 * HORIZ_ADVANCE_PX); // only the JR, since Whop is not being layed out
cl_assert_equal_i(layout.max_used_size.h, 2 * FONT_HEIGHT); // Nothing - save for the first line - will be rendered below the box
graphics_text_layout_get_max_used_size(&gcontext, "JR\n\nWhop", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX); // Includes Whop - as it may be partially rendered at the bottom of the box
cl_assert_equal_i(layout.max_used_size.h, 3 * FONT_HEIGHT); // The blank line before Whop is still being layed out, however, so it is still included in the height
graphics_text_layout_get_max_used_size(&gcontext, "JR\n\n\nWhop", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 2 * HORIZ_ADVANCE_PX); // Back to only JR - as the line being layed out from y=20-30px is empty (and the line from 30-40, Whop, is truncated as it can never appear)
cl_assert_equal_i(layout.max_used_size.h, 3 * FONT_HEIGHT); // Same as above - the blank line is still layed out
graphics_text_layout_get_max_used_size(&gcontext, "JR\n\n\nWhop", font, box, GTextOverflowModeFill, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX); // Fill replaces \n's with spaces, so we will always fill the full horizontal width ("JR Whop" wraps to "JR\nWhop")
cl_assert_equal_i(layout.max_used_size.h, 2 * FONT_HEIGHT); // Same behaviour as TrailingEllipsis in this regard
}
void test_text_layout__pathological_1(void) {
GContext gcontext;
FrameBuffer *fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) { DISP_COLS, DISP_ROWS });
GFont font = (GFont) { 0 };
GRect box = (GRect) { (GPoint) { 0, 0 }, (GSize) {40, 250 * FONT_HEIGHT} };
graphics_context_init(&gcontext, fb, GContextInitializationMode_App);
framebuffer_clear(fb);
graphics_draw_text(&gcontext, "\n", font, box,
GTextOverflowModeFill, GTextAlignmentLeft, NULL);
graphics_draw_text(&gcontext, "\n\n", font, box,
GTextOverflowModeFill, GTextAlignmentLeft, NULL);
graphics_draw_text(&gcontext, "\1\n", font, box,
GTextOverflowModeFill, GTextAlignmentLeft, NULL);
graphics_draw_text(&gcontext, "", font, box,
GTextOverflowModeFill, GTextAlignmentLeft, NULL);
}
void test_text_layout__max_used_size(void) {
char *empty_string = "";
char *singleton = "A";
char *doubleton = "AA";
GFont font = (GFont){ 0 };
GRect box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 3 * HORIZ_ADVANCE_PX + 1, FONT_HEIGHT + 1 } };
TextLayoutExtended layout = (TextLayoutExtended) { };
GContext gcontext = (GContext) { };
layout.hash = 0;
layout.box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 7 * HORIZ_ADVANCE_PX + 1, FONT_HEIGHT - 1} };
layout.font = (GFont) { 0 };
layout.overflow_mode = GTextOverflowModeWordWrap;
layout.alignment = GTextAlignmentLeft;
layout.max_used_size = (GSize) { 0, 0 };
// Ensure that the empty string properly resets our sized boundaries
graphics_text_layout_get_max_used_size(&gcontext, empty_string, font, box,
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.h, 0);
cl_assert_equal_i(layout.max_used_size.w, 0);
graphics_text_layout_get_max_used_size(&gcontext, singleton, font, box,
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 1 * HORIZ_ADVANCE_PX);
cl_assert_equal_i(layout.max_used_size.h, FONT_HEIGHT);
// Ensure that the empty string properly resets our sized boundaries
graphics_text_layout_get_max_used_size(&gcontext, empty_string, font, box,
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.h, 0);
cl_assert_equal_i(layout.max_used_size.w, 0);
graphics_text_layout_get_max_used_size(&gcontext, doubleton, font, box,
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 2 * HORIZ_ADVANCE_PX);
cl_assert_equal_i(layout.max_used_size.h, FONT_HEIGHT);
}
void test_text_layout__disable_paging(void) {
TextLayoutExtended l = {.flow_data.paging.page_on_screen.size_h = 123};
graphics_text_attributes_restore_default_paging((GTextLayoutCacheRef) &l);
cl_assert_equal_i(l.flow_data.paging.page_on_screen.size_h, 0);
}
void test_text_layout__enable_paging(void) {
TextLayoutExtended l = {};
graphics_text_attributes_enable_paging((GTextLayoutCacheRef) &l, GPoint(1, 2), GRect(3, 4, 5, 6));
cl_assert_equal_i(l.flow_data.paging.origin_on_screen.x, 1);
cl_assert_equal_i(l.flow_data.paging.origin_on_screen.y, 2);
cl_assert_equal_i(l.flow_data.paging.page_on_screen.origin_y, 4);
cl_assert_equal_i(l.flow_data.paging.page_on_screen.size_h, 6);
}
void test_text_layout__disable_text_flow(void) {
TextLayoutExtended l = {.flow_data.perimeter.impl = (const GPerimeter *)(1234)};
graphics_text_attributes_restore_default_text_flow((GTextLayoutCacheRef) &l);
cl_assert_equal_p(l.flow_data.perimeter.impl, NULL);
}
// just a fake value to have something to compare against
const GPerimeter * const g_perimeter_for_display = (const GPerimeter *) &g_perimeter_for_display;
void test_text_layout__enable_text_flow(void) {
TextLayoutExtended l = {};
graphics_text_attributes_enable_screen_text_flow((GTextLayoutCacheRef) &l, 123);
cl_assert_equal_p(l.flow_data.perimeter.impl, g_perimeter_for_display);
cl_assert_equal_i(l.flow_data.perimeter.inset, 123);
}
void test_text_layout__create_destroy(void) {
GTextAttributes *attributes = graphics_text_attributes_create();
cl_assert_equal_p(attributes->font, NULL);
cl_assert_equal_i(attributes->hash, 0);
graphics_text_attributes_destroy(attributes);
}
void test_text_layout__get_default_flow_data(void) {
const TextLayoutFlowData *data1 = graphics_text_layout_get_flow_data(NULL);
cl_assert(data1 != NULL);
cl_assert_equal_p(data1->perimeter.impl, NULL);
cl_assert_equal_i(data1->paging.page_on_screen.size_h, 0);
// change SP so that we can make sure that graphics_text_layout_get_flow_data doesn't rely on it
uint8_t change_stack[data1->paging.page_on_screen.size_h + 500];
memset(change_stack, 0xff, 500);
const TextLayoutFlowData *data2 = graphics_text_layout_get_flow_data(NULL);
cl_assert_equal_p(data1, data2);
// values are still 0
cl_assert_equal_p(data2->perimeter.impl, NULL);
cl_assert_equal_i(data2->paging.page_on_screen.size_h, 0);
}
#include "applib/legacy2/ui/text_layer_legacy2.h"
void test_text_layout__delta(void) {
GContext gcontext = (GContext) { };
GFont font = (GFont) { 0 };
GRect box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 4 * HORIZ_ADVANCE_PX + 1, 2 * (FONT_HEIGHT + FONT_LINE_DELTA) + 1 } };
TextLayoutExtended layout = (TextLayoutExtended) {
.hash = 0,
.box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 7 * HORIZ_ADVANCE_PX + 1, FONT_HEIGHT - 1 } },
.font = (GFont) { 0 },
.overflow_mode = GTextOverflowModeWordWrap,
.alignment = GTextAlignmentLeft,
.max_used_size = (GSize) { 0, 0 }
};
if (!process_manager_compiled_with_legacy2_sdk()) {
graphics_text_layout_set_line_spacing_delta((void*)&layout, FONT_LINE_DELTA);
}
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper", font, box, GTextOverflowModeFill, GTextAlignmentLeft, (void*)&layout);
cl_assert(layout.box.size.w == box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX);
// 2 lines - all that will completely fit in the box ("Jr\nWho-")
cl_assert_equal_i(layout.max_used_size.h, 2 * (FONT_HEIGHT + FONT_LINE_DELTA));
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
cl_assert(layout.box.size.w == box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX);
// 3 lines - one line extra being layed out so that it will clip ("Jr\nWho-\npper")
cl_assert_equal_i(layout.max_used_size.h, 3 * (FONT_HEIGHT + FONT_LINE_DELTA));
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper 123", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX);
// 3 lines - but not 4, since the fourth has no chance of appearing ("Jr\nWho-\npper\n 123")
cl_assert_equal_i(layout.max_used_size.h, 3 * (FONT_HEIGHT + FONT_LINE_DELTA));
// Update line spacing and ensure the text layout gets updated
if (!process_manager_compiled_with_legacy2_sdk()) {
graphics_text_layout_set_line_spacing_delta((void*)&layout, FONT_LINE_DELTA - 1);
cl_assert_equal_i(graphics_text_layout_get_line_spacing_delta((void*)&layout), (FONT_LINE_DELTA - 1));
cl_assert_equal_i(layout.max_used_size.h, 3 * (FONT_HEIGHT + FONT_LINE_DELTA));
cl_assert_equal_i(layout.hash, 0);
}
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper 123", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
cl_assert(layout.hash != 0);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX);
if (!process_manager_compiled_with_legacy2_sdk()) {
// 3 lines - but not 4, since the fourth has no chance of appearing ("Jr\nWho-\npper\n 123")
cl_assert_equal_i(layout.max_used_size.h, 3 * (FONT_HEIGHT + (FONT_LINE_DELTA - 1)));
} else {
cl_assert_equal_i(layout.max_used_size.h, 3 * FONT_HEIGHT);
}
if (!process_manager_compiled_with_legacy2_sdk()) {
// Test negative spacing
graphics_text_layout_set_line_spacing_delta((void*)&layout, (-FONT_HEIGHT));
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper 123", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.max_used_size.w, 4 * HORIZ_ADVANCE_PX);
// 4 lines - all four show up but all overlapped so 0 height is returned ("Jr\nWho-\npper\n 123")
cl_assert_equal_i(layout.max_used_size.h, 0);
graphics_text_layout_set_line_spacing_delta((void*)&layout, (1 - FONT_HEIGHT));
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper 123", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
// 4 lines - all four show up but 1 pixel height per line is returned ("Jr\nWho-\npper\n 123")
cl_assert_equal_i(layout.max_used_size.h, 4);
graphics_text_layout_set_line_spacing_delta((void*)&layout, (-4 * FONT_HEIGHT));
graphics_text_layout_get_max_used_size(&gcontext, "JR Whopper 123", font, box, GTextOverflowModeWordWrap, GTextAlignmentLeft, (void*)&layout);
// 4 lines spaced out at 10-40 = -30 pixels each ("Jr\nWho-\npper\n 123")
cl_assert_equal_i(layout.max_used_size.h, -120);
}
}
void test_text_layout__special_codepoints(void) {
GContext gcontext;
FrameBuffer *fb = malloc(sizeof(FrameBuffer));
framebuffer_init(fb, &(GSize) { DISP_COLS, DISP_ROWS });
graphics_context_init(&gcontext, fb, GContextInitializationMode_App);
framebuffer_clear(fb);
GFont font = (GFont) { 0 };
GRect box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 20 * HORIZ_ADVANCE_PX + 1, 13 } };
TextLayoutExtended layout = (TextLayoutExtended) {
.hash = 0,
.box = (GRect) { (GPoint) { 0, 0 }, (GSize) { 20 * HORIZ_ADVANCE_PX + 1, 13 } },
.font = (GFont) { 0 },
.overflow_mode = GTextOverflowModeWordWrap,
.alignment = GTextAlignmentLeft,
.max_used_size = (GSize) { 0, 0 }
};
layout.box = box;
graphics_draw_text(&gcontext,
"\xE2\x80\x8F" // Left-To-Right mark
"\xEF\xB8\x8E" // Variation Selector 1
"\xF0\x9F\x8F\xBB", // White skin tone codepoint
font, box,
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, (void*)&layout);
cl_assert_equal_i(layout.box.size.w, box.size.w);
cl_assert_equal_i(layout.max_used_size.w, 0 * HORIZ_ADVANCE_PX);
}