mirror of
https://github.com/google/pebble.git
synced 2025-05-06 18:01:41 -04:00
1320 lines
47 KiB
C
1320 lines
47 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.
|
|
*/
|
|
|
|
//! Overview:
|
|
//! - Summary of text layout and rendering:
|
|
//! - A line iterator is created to iterate over the lines in a text-box
|
|
//! - The line iterator creates a word iterator to advance through the text
|
|
//! - The word iterator creates a character iterator to advance through
|
|
//! codepoints. This allows reserved codepoints to be used for in-line text
|
|
//! formatting.
|
|
//! - The character iterator uses a UTF-8 iterator to advance through the
|
|
//! UTF-8 encoded unicode codepoints.
|
|
|
|
#include "text.h"
|
|
#include "text_layout_private.h"
|
|
|
|
#include "graphics.h"
|
|
#include "graphics_private.h"
|
|
#include "gtypes.h"
|
|
#include "text_render.h"
|
|
#include "text_resources.h"
|
|
#include "utf8.h"
|
|
|
|
#include "applib/fonts/codepoint.h"
|
|
#include "applib/fonts/fonts.h"
|
|
#include "kernel/ui/kernel_ui.h"
|
|
#include "process_state/app_state/app_state.h"
|
|
#include "applib/applib_malloc.auto.h"
|
|
#include "process_state/app_state/app_state.h"
|
|
#include "system/logging.h"
|
|
#include "system/passert.h"
|
|
#include "util/hash.h"
|
|
#include "util/iterator.h"
|
|
#include "util/math.h"
|
|
|
|
#include "process_management/process_manager.h"
|
|
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <limits.h>
|
|
|
|
static bool prv_char_iter_next_start_of_word(Iterator* char_iter);
|
|
|
|
// PBL-23045 Eventually remove perimeter debugging
|
|
void graphics_text_perimeter_debugging_enable(bool enable) {
|
|
app_state_set_text_perimeter_debugging_enabled(enable);
|
|
}
|
|
|
|
// [CTX] processing individual codepoints doesn't work for contextual writing systems.
|
|
static int8_t prv_codepoint_get_horizontal_advance(FontCache* const font_cache,
|
|
const GFont font,
|
|
const Codepoint codepoint) {
|
|
PBL_ASSERTN(font_cache);
|
|
int8_t horiz_advance = 0;
|
|
if (!codepoint_is_zero_width(codepoint)) {
|
|
horiz_advance = text_resources_get_glyph_horiz_advance(font_cache, codepoint, font);
|
|
}
|
|
return MAX(horiz_advance, 0);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Init functions
|
|
|
|
//! @note can be init to a null-termination character
|
|
void char_iter_init(Iterator* char_iter, CharIterState* char_iter_state, const TextBoxParams* const text_box_params, utf8_t* start) {
|
|
Iterator* utf8_iter = &char_iter_state->utf8_iter;
|
|
Utf8IterState* utf8_iter_state = (Utf8IterState*) &char_iter_state->utf8_iter_state;
|
|
|
|
utf8_iter_init(utf8_iter, utf8_iter_state, text_box_params->utf8_bounds, start);
|
|
|
|
char_iter_state->text_box_params = text_box_params;
|
|
|
|
iter_init(char_iter, (IteratorCallback) char_iter_next, char_iter_prev, (IteratorState) char_iter_state);
|
|
}
|
|
|
|
typedef enum {
|
|
WordStateStart,
|
|
WordStateIdeograph,
|
|
WordStateGrowing,
|
|
WordStateJoining,
|
|
WordStateEnd,
|
|
} WordState;
|
|
|
|
WordState word_state_update(WordState state, Codepoint codepoint) {
|
|
WordState new_state = state;
|
|
|
|
switch (state) {
|
|
case WordStateStart:
|
|
if (codepoint == NEWLINE_CODEPOINT) {
|
|
new_state = WordStateEnd;
|
|
} else if (codepoint_is_ideograph(codepoint)) {
|
|
new_state = WordStateIdeograph;
|
|
} else {
|
|
new_state = WordStateGrowing;
|
|
}
|
|
break;
|
|
case WordStateIdeograph:
|
|
if (codepoint == WORD_JOINER_CODEPOINT) {
|
|
new_state = WordStateJoining;
|
|
} else {
|
|
new_state = WordStateEnd;
|
|
}
|
|
case WordStateGrowing:
|
|
if (codepoint == WORD_JOINER_CODEPOINT) {
|
|
new_state = WordStateJoining;
|
|
} else if (codepoint_is_ideograph(codepoint) || codepoint_is_end_of_word(codepoint)) {
|
|
new_state = WordStateEnd;
|
|
} else {
|
|
new_state = WordStateGrowing;
|
|
}
|
|
break;
|
|
case WordStateJoining:
|
|
if (codepoint == NEWLINE_CODEPOINT) {
|
|
new_state = WordStateEnd;
|
|
} else if (codepoint_is_ideograph(codepoint)) {
|
|
new_state = WordStateIdeograph;
|
|
} else if (codepoint == WORD_JOINER_CODEPOINT) {
|
|
new_state = WordStateJoining;
|
|
} else {
|
|
new_state = WordStateGrowing;
|
|
}
|
|
break;
|
|
case WordStateEnd:
|
|
new_state = WordStateEnd;
|
|
break;
|
|
}
|
|
|
|
return new_state;
|
|
}
|
|
|
|
//! @return true if init to new word, false otherwise (ie end of text)
|
|
//! @note assumes 'start' is not NULL, but does not assume 'start' is valid start of word
|
|
bool word_init(GContext* ctx, Word* word, const TextBoxParams* const text_box_params, utf8_t* start) {
|
|
word->width_px = 0;
|
|
|
|
if (*start == NULL_CODEPOINT) {
|
|
word->start = start;
|
|
word->end = start;
|
|
return false;
|
|
}
|
|
|
|
// Set up iterator
|
|
Iterator char_iter;
|
|
CharIterState char_iter_state;
|
|
char_iter_init(&char_iter, &char_iter_state, text_box_params, start);
|
|
Utf8IterState* utf8_iter_state = (Utf8IterState*) &char_iter_state.utf8_iter_state;
|
|
|
|
bool success = prv_char_iter_next_start_of_word(&char_iter);
|
|
if (!success) {
|
|
// We couldn't find the next start of the word, just initialize to nothing
|
|
word->start = start;
|
|
word->end = start;
|
|
return false;
|
|
}
|
|
|
|
// Init the word & state
|
|
word->start = utf8_iter_state->current;
|
|
WordState state = WordStateStart;
|
|
state = word_state_update(state, utf8_iter_state->codepoint);
|
|
|
|
do {
|
|
if (state == WordStateGrowing || state == WordStateIdeograph) {
|
|
word->width_px += prv_codepoint_get_horizontal_advance(&ctx->font_cache,
|
|
text_box_params->font, utf8_iter_state->codepoint);
|
|
}
|
|
|
|
iter_next(&char_iter);
|
|
state = word_state_update(state, utf8_iter_state->codepoint);
|
|
} while (state != WordStateEnd);
|
|
|
|
word->end = utf8_iter_state->current;
|
|
|
|
return true;
|
|
}
|
|
|
|
void word_iter_init(Iterator* word_iter, WordIterState* word_iter_state, GContext* ctx,
|
|
const TextBoxParams* const text_box_params, utf8_t* start) {
|
|
*word_iter_state = (WordIterState) {
|
|
.ctx = ctx,
|
|
.text_box_params = text_box_params
|
|
};
|
|
|
|
word_init(ctx, &word_iter_state->current, text_box_params, start);
|
|
|
|
iter_init(word_iter, (IteratorCallback) word_iter_next, NULL, (IteratorState) word_iter_state);
|
|
}
|
|
|
|
void line_iter_init(Iterator* line_iter, LineIterState* line_iter_state, GContext* ctx) {
|
|
*line_iter_state = (LineIterState) {
|
|
.ctx = ctx,
|
|
.current = &ctx->text_draw_state.line
|
|
};
|
|
|
|
WordIterState* word_iter_state = &line_iter_state->word_iter_state;
|
|
word_iter_init(&line_iter_state->word_iter, word_iter_state, ctx,
|
|
&ctx->text_draw_state.text_box, ctx->text_draw_state.text_box.utf8_bounds->start);
|
|
|
|
iter_init(line_iter, (IteratorCallback) line_iter_next, NULL, (IteratorState) line_iter_state);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Private helper functions
|
|
|
|
static int16_t prv_get_line_height(const TextBoxParams *text_box_params) {
|
|
return fonts_get_font_height(text_box_params->font) + text_box_params->line_spacing_delta;
|
|
}
|
|
|
|
static int16_t prv_layout_get_line_spacing_delta(GTextLayoutCacheRef layout) {
|
|
if (process_manager_compiled_with_legacy2_sdk()) {
|
|
return 0;
|
|
}
|
|
|
|
return (layout ? ((TextLayoutExtended *)layout)->line_spacing_delta: 0);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Iterator advance functions
|
|
|
|
//! Advance the char iterator to the start of the next word. Used by word_init
|
|
//! to find the start of the next word.
|
|
//! @return is_success
|
|
static bool prv_char_iter_next_start_of_word(Iterator* char_iter) {
|
|
CharIterState* char_iter_state = (CharIterState*) char_iter->state;
|
|
Utf8IterState* utf8_iter_state = (Utf8IterState*) &char_iter_state->utf8_iter_state;
|
|
|
|
// the first codepoint could be invalid, iter_next takes care of the others
|
|
Codepoint codepoint = utf8_iter_state->codepoint;
|
|
if (codepoint_should_skip(codepoint) || codepoint_is_formatting_indicator(codepoint)) {
|
|
if (!iter_next(char_iter)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
while (codepoint_is_zero_width(utf8_iter_state->codepoint)) {
|
|
if (utf8_iter_state->codepoint == 0) {
|
|
PBL_ASSERTN(utf8_iter_state->current == utf8_iter_state->bounds->end);
|
|
return false;
|
|
}
|
|
|
|
if (!iter_next(char_iter)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool prv_line_iter_is_vertical_overflow(const LineIterState* const line_iter_state,
|
|
const TextBoxParams* const text_box_params) {
|
|
int16_t next_line_y_extent;
|
|
// Normally, we lay out the text one line below the regular cutoff so that it may be rendered,
|
|
// albeit clipped. But, if we're rendering in truncation mode (e.g. GTextOverflowModeFill or
|
|
// GTextOverflowModeTrailingEllipsis), we can immediately cut the text off below the box height
|
|
// if we're not rendering the first line.
|
|
// - This, because the user does not expect to see more text drawn below, after the '...'.
|
|
// - The first-line exception means that text, and therefore the telltale
|
|
// ellipsis, will always be visisble.
|
|
if ((text_box_params->overflow_mode == GTextOverflowModeTrailingEllipsis ||
|
|
text_box_params->overflow_mode == GTextOverflowModeFill) &&
|
|
line_iter_state->current->origin.y != text_box_params->box.origin.y) {
|
|
// We're in a truncation mode AND not on the first line.
|
|
// So, include the full height of the current line in next_line_y_extent, so text will stop
|
|
// being layed out immediately after it exceeds the height of the container.
|
|
next_line_y_extent = line_iter_state->current->origin.y + prv_get_line_height(text_box_params);
|
|
} else {
|
|
// We're either in a non-truncating mode, or on the first line of a truncating mode.
|
|
// So, only include the extent of the previous line in next_line_y_extent (making it more of
|
|
// a "last_line_y_extent").
|
|
// Putting aside the misleading variable name, this will cause us to lay out one more line than
|
|
// will completely fit in the container - so that it may still be displayed, even if partially
|
|
// or completely clipped.
|
|
next_line_y_extent = line_iter_state->current->origin.y;
|
|
}
|
|
return (next_line_y_extent > (text_box_params->box.origin.y + text_box_params->box.size.h));
|
|
}
|
|
|
|
//! @return is_advanced
|
|
bool line_iter_next(IteratorState state) {
|
|
LineIterState* line_iter_state = (LineIterState*) state;
|
|
const TextBoxParams* const text_box_params = &line_iter_state->ctx->text_draw_state.text_box;
|
|
|
|
if (prv_line_iter_is_vertical_overflow(line_iter_state, text_box_params)) {
|
|
return false;
|
|
}
|
|
|
|
line_iter_state->current->origin.x = text_box_params->box.origin.x;
|
|
line_iter_state->current->origin.y += prv_get_line_height(text_box_params);
|
|
line_iter_state->current->width_px = 0; // needs to be reset per line
|
|
line_iter_state->current->max_width_px = text_box_params->box.size.w;
|
|
line_iter_state->current->suffix_codepoint = 0;
|
|
line_iter_state->current->start = NULL;
|
|
|
|
return true;
|
|
}
|
|
|
|
//! @return is_advanced
|
|
bool word_iter_next(IteratorState state) {
|
|
WordIterState* word_iter_state = (WordIterState*) state;
|
|
|
|
Word* current_word = &word_iter_state->current;
|
|
const TextBoxParams* const text_box_params = word_iter_state->text_box_params;
|
|
GContext* ctx = word_iter_state->ctx;
|
|
|
|
if (*current_word->end == NULL_CODEPOINT) {
|
|
return false;
|
|
}
|
|
|
|
return word_init(ctx, current_word, text_box_params, current_word->end);
|
|
}
|
|
|
|
//! @return is_advanced
|
|
bool char_iter_next(IteratorState state) {
|
|
CharIterState* char_iter_state = (CharIterState*) state;
|
|
|
|
Codepoint codepoint;
|
|
Iterator* utf8_iter = &char_iter_state->utf8_iter;
|
|
Utf8IterState* utf8_iter_state = &char_iter_state->utf8_iter_state;
|
|
|
|
while (true) {
|
|
if (utf8_iter_state->current >= utf8_iter_state->bounds->end) {
|
|
// EOS while searching for valid codepoint
|
|
return false;
|
|
}
|
|
|
|
bool is_utf8_advanced = iter_next(utf8_iter);
|
|
codepoint = utf8_iter_state->codepoint;
|
|
|
|
if (!is_utf8_advanced) {
|
|
return is_utf8_advanced;
|
|
}
|
|
|
|
PBL_ASSERTN(codepoint != 0);
|
|
|
|
if (codepoint_is_formatting_indicator(codepoint)) {
|
|
continue;
|
|
}
|
|
|
|
if (codepoint_should_skip(codepoint)) {
|
|
continue;
|
|
};
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool char_iter_prev(IteratorState state) {
|
|
CharIterState* char_iter_state = (CharIterState*) state;
|
|
|
|
Codepoint codepoint;
|
|
Iterator* utf8_iter = &char_iter_state->utf8_iter;
|
|
Utf8IterState* utf8_iter_state = &char_iter_state->utf8_iter_state;
|
|
|
|
while (true) {
|
|
if (utf8_iter_state->current <= utf8_iter_state->bounds->start) {
|
|
// EOS while searching for valid codepoint
|
|
return false;
|
|
}
|
|
|
|
bool is_utf8_advanced = iter_prev(utf8_iter);
|
|
codepoint = utf8_iter_state->codepoint;
|
|
|
|
if (!is_utf8_advanced) {
|
|
return is_utf8_advanced;
|
|
}
|
|
|
|
PBL_ASSERTN(codepoint != 0);
|
|
|
|
if (codepoint_is_formatting_indicator(codepoint)) {
|
|
continue;
|
|
}
|
|
|
|
if (codepoint_should_skip(codepoint)) {
|
|
continue;
|
|
};
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Helper functions
|
|
|
|
//! Trim given codepoint from the start of the word
|
|
//! Used to remove whitespace and newlines
|
|
//! @return is_trimmed
|
|
bool word_trim_preceeding_codepoint(GContext* ctx, Word* word, const Codepoint codepoint,
|
|
const TextBoxParams* const text_box_params) {
|
|
Iterator char_iter;
|
|
CharIterState char_iter_state;
|
|
char_iter_init(&char_iter, &char_iter_state, text_box_params, word->start);
|
|
|
|
Utf8IterState* utf8_iter_state = &char_iter_state.utf8_iter_state;
|
|
|
|
if (utf8_iter_state->codepoint != codepoint) {
|
|
return false;
|
|
}
|
|
|
|
bool is_advanced = iter_next(&char_iter);
|
|
|
|
if (!is_advanced) {
|
|
PBL_ASSERTN(*word->end == NULL_CODEPOINT);
|
|
word->start = NULL;
|
|
return false;
|
|
}
|
|
|
|
if (word->end == char_iter_state.utf8_iter_state.current) {
|
|
// Word has been completely trimmed; init a new word
|
|
bool is_end_of_text = (word->end == NULL_CODEPOINT ||
|
|
char_iter_state.utf8_iter_state.current >= text_box_params->utf8_bounds->end);
|
|
|
|
if (!is_end_of_text) {
|
|
word_init(ctx, word, text_box_params, word->end);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Trim
|
|
int advance = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
|
|
text_box_params->font, codepoint);
|
|
PBL_ASSERTN(advance <= word->width_px); // Negative-length word not allowed
|
|
|
|
word->width_px -= advance;
|
|
word->start = utf8_iter_state->current;
|
|
return true;
|
|
}
|
|
|
|
// [INTL] whitespace is more than just the space character.
|
|
void word_trim_preceeding_whitespace(GContext* ctx, Word* word, const TextBoxParams* const text_box_params) {
|
|
while (word_trim_preceeding_codepoint(ctx, word, SPACE_CODEPOINT, text_box_params));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Walk Line
|
|
|
|
typedef void (*CharVisitorCallback)(GContext* ctx, const TextBoxParams* const text_box_params,
|
|
Line* line, GRect cursor, const Codepoint codepoint);
|
|
|
|
void render_chars_char_visitor_cb(GContext* ctx, const TextBoxParams* const text_box_params,
|
|
Line* line, GRect cursor, const Codepoint codepoint) {
|
|
if (codepoint_is_zero_width(codepoint)) {
|
|
return;
|
|
}
|
|
|
|
render_glyph(ctx, codepoint, text_box_params->font, cursor);
|
|
}
|
|
|
|
void update_dimensions_char_visitor_cb(GContext* ctx, const TextBoxParams* const text_box_params,
|
|
Line* line, GRect cursor, const Codepoint codepoint) {
|
|
(void) ctx;
|
|
PBL_ASSERT(cursor.origin.x >= line->origin.x, "Text cursor x=<%u> ahead of line origin x=<%u>",
|
|
cursor.origin.x, line->origin.x);
|
|
|
|
const int glyph_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
|
|
text_box_params->font, codepoint);
|
|
|
|
line->width_px = (cursor.origin.x + glyph_width_px) - line->origin.x;
|
|
|
|
PBL_ASSERT(line->width_px <= text_box_params->box.size.w,
|
|
"Line <%p>: max extent=<%u> exceeds text_box_params width=<%u>",
|
|
line, line->width_px + line->origin.x, text_box_params->box.size.w);
|
|
}
|
|
|
|
//! Call char_visitor_cb on each character in the line
|
|
//! Used to update line dimensions and render characters
|
|
//! Traverse until end of line->width_px if rendering chars, else text_box_params width
|
|
//! if updating line dimensions
|
|
//! @return utf8_t* pointer to last visited character
|
|
utf8_t* walk_line(GContext* ctx, Line* line, const TextBoxParams* const text_box_params,
|
|
CharVisitorCallback char_visitor_cb) {
|
|
PBL_ASSERTN(char_visitor_cb);
|
|
|
|
// We used to check that the line height was <= the container height here - no longer required,
|
|
// as the vertical overflow is handled during layout.
|
|
|
|
int available_horiz_px;
|
|
if (char_visitor_cb == update_dimensions_char_visitor_cb) {
|
|
// Line dimensions not yet set; use all available line space
|
|
available_horiz_px = line->max_width_px;
|
|
} else {
|
|
available_horiz_px = line->width_px;
|
|
}
|
|
|
|
PBL_ASSERT(line->width_px <= text_box_params->box.size.w,
|
|
"Line <%p>: max extent=<%u> exceeds text_box_params width=<%u>", line,
|
|
line->width_px + line->origin.x, text_box_params->box.size.w);
|
|
|
|
int suffix_width_px = 0;
|
|
|
|
if (line->suffix_codepoint) {
|
|
suffix_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
|
|
text_box_params->font, line->suffix_codepoint);
|
|
}
|
|
|
|
if (available_horiz_px < suffix_width_px) {
|
|
return NULL;
|
|
}
|
|
|
|
// Set up iterator
|
|
Iterator char_iter;
|
|
CharIterState char_iter_state;
|
|
char_iter_init(&char_iter, &char_iter_state, text_box_params, line->start);
|
|
Utf8IterState* utf8_iter_state = (Utf8IterState*) &char_iter_state.utf8_iter_state;
|
|
|
|
bool is_newline_as_space = text_box_params->overflow_mode == GTextOverflowModeFill;
|
|
Codepoint current_codepoint = utf8_iter_state->codepoint;
|
|
if (current_codepoint == NEWLINE_CODEPOINT) {
|
|
if (is_newline_as_space) {
|
|
current_codepoint = SPACE_CODEPOINT;
|
|
} else {
|
|
return utf8_iter_state->current;
|
|
}
|
|
}
|
|
|
|
int walked_width_px = 0;
|
|
int next_glyph_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
|
|
text_box_params->font, current_codepoint);
|
|
|
|
utf8_t* last_visited_char = NULL;
|
|
|
|
while (walked_width_px + next_glyph_width_px + suffix_width_px <= available_horiz_px) {
|
|
GRect cursor = {
|
|
.origin = line->origin,
|
|
.size.w = next_glyph_width_px,
|
|
.size.h = fonts_get_font_height(text_box_params->font)
|
|
};
|
|
cursor.origin.x += walked_width_px;
|
|
|
|
char_visitor_cb(ctx, text_box_params, line, cursor, current_codepoint);
|
|
|
|
walked_width_px += next_glyph_width_px;
|
|
|
|
last_visited_char = utf8_iter_state->current;
|
|
|
|
if (!iter_next(&char_iter)) {
|
|
break;
|
|
}
|
|
|
|
current_codepoint = utf8_iter_state->codepoint;
|
|
if (current_codepoint == NEWLINE_CODEPOINT) {
|
|
if (is_newline_as_space) {
|
|
current_codepoint = SPACE_CODEPOINT;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
next_glyph_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
|
|
text_box_params->font, current_codepoint);
|
|
}
|
|
|
|
// Trim trailing whitespace
|
|
if (last_visited_char) {
|
|
while ((current_codepoint == NEWLINE_CODEPOINT || current_codepoint == SPACE_CODEPOINT)) {
|
|
// Newlines should not adjust the width
|
|
if (current_codepoint == NEWLINE_CODEPOINT) {
|
|
next_glyph_width_px = 0;
|
|
} else {
|
|
next_glyph_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
|
|
text_box_params->font,
|
|
current_codepoint);
|
|
}
|
|
|
|
// Safety check
|
|
if (walked_width_px < next_glyph_width_px) {
|
|
break;
|
|
}
|
|
walked_width_px -= next_glyph_width_px;
|
|
|
|
if (!iter_prev(&char_iter)) {
|
|
break;
|
|
}
|
|
current_codepoint = utf8_iter_state->codepoint;
|
|
}
|
|
}
|
|
|
|
if (line->suffix_codepoint) {
|
|
GRect cursor = {
|
|
.origin = line->origin,
|
|
.size.w = next_glyph_width_px,
|
|
.size.h = fonts_get_font_height(text_box_params->font)
|
|
};
|
|
cursor.origin.x += walked_width_px;
|
|
if (char_visitor_cb) {
|
|
char_visitor_cb(ctx, text_box_params, line, cursor, line->suffix_codepoint);
|
|
}
|
|
}
|
|
|
|
return last_visited_char;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Walk Lines
|
|
|
|
void set_ellipsis_on_overflow_last_line_cb(GContext* ctx, Line* line,
|
|
const TextBoxParams* const text_box_params,
|
|
const bool is_text_remaining) {
|
|
// Only set a trailing ellipsis if there is text remaining
|
|
if (!is_text_remaining) {
|
|
return;
|
|
}
|
|
|
|
// Check if outputting two lines extend beyond the text box height - then display the ellipsis
|
|
// on the current line
|
|
bool is_last_line = ((line->origin.y + (2 * prv_get_line_height(text_box_params))) >
|
|
(text_box_params->box.origin.y + text_box_params->box.size.h));
|
|
// Check if this is the last line
|
|
if (!is_last_line) {
|
|
return;
|
|
}
|
|
|
|
line->suffix_codepoint = ELLIPSIS_CODEPOINT;
|
|
|
|
// update the line dimensions
|
|
walk_line(ctx, line, text_box_params, update_dimensions_char_visitor_cb);
|
|
}
|
|
|
|
void render_all_render_line_cb(GContext* ctx, Line* line, const TextBoxParams* const text_box_params) {
|
|
walk_line(ctx, line, text_box_params, (CharVisitorCallback) render_chars_char_visitor_cb);
|
|
}
|
|
|
|
void update_all_layout_update_cb(TextLayout* layout, Line* line,
|
|
const TextBoxParams* const text_box_params) {
|
|
PBL_ASSERTN(line);
|
|
if (layout) {
|
|
layout->max_used_size.h = (line->origin.y - layout->box.origin.y) + line->height_px +
|
|
text_box_params->line_spacing_delta;
|
|
layout->max_used_size.w = MAX(line->width_px, layout->max_used_size.w);
|
|
}
|
|
}
|
|
|
|
//! @return is_overflow
|
|
bool is_clip_box_overflow_top_stop_condition_cb(GContext* ctx, Line* line,
|
|
const TextBoxParams* const text_box_params) {
|
|
int next_line_max_y = line->origin.y;
|
|
int clip_box_min_y = ctx->draw_state.clip_box.origin.y;
|
|
return (next_line_max_y < clip_box_min_y);
|
|
}
|
|
|
|
//! @return is_overflow
|
|
bool is_clip_box_overflow_bottom_stop_condition_cb(GContext* ctx, Line* line,
|
|
const TextBoxParams* const text_box_params) {
|
|
int next_line_min_y = line->origin.y + line->height_px + text_box_params->line_spacing_delta;
|
|
int clip_box_max_y = ctx->draw_state.clip_box.origin.y + ctx->draw_state.clip_box.size.h;
|
|
return (next_line_min_y > clip_box_max_y);
|
|
}
|
|
|
|
//! @return is_overflow
|
|
bool is_clip_box_overflow_stop_condition_cb(GContext* ctx, Line* line,
|
|
const TextBoxParams* const text_box_params) {
|
|
return (is_clip_box_overflow_bottom_stop_condition_cb(ctx, line, text_box_params) ||
|
|
is_clip_box_overflow_top_stop_condition_cb(ctx, line, text_box_params));
|
|
}
|
|
|
|
#define TEXT_LINE_BASE_LINE(line) ((line)->height_px)
|
|
#define TEXT_LINE_CAP_LINE(line) ((line)->height_px * 1 / 2)
|
|
// Based on Gothic fonts, DESCENDER is approx 1/5 of height (ascender + descender)
|
|
// Gothic 24 Bold ascent = 840, descent 168
|
|
// Gothic 18 Bold ascent = 840, descent 168
|
|
// Gothic 14 ascent = 864, descent 144
|
|
// Bitham ascent = 800, descend = 200
|
|
// DroidSerif Bold ascent = 1638, descent = 410
|
|
#define TEXT_LINE_DESCENDER_LINE(line) DIVIDE_CEIL((line)->height_px, 5) // 1/5th rounded up
|
|
|
|
T_STATIC NOINLINE MOCKABLE void prv_debug_perimeter(GContext *ctx, const GRangeHorizontal *h_range,
|
|
const Line *line) {
|
|
// PBL-23045 Eventually remove perimeter debugging
|
|
// Draw a red horizontal line to show the range of the current lines perimeter
|
|
if (app_state_get_text_perimeter_debugging_enabled()) {
|
|
#if !defined(UNITTEST) && !defined(PLATFORM_TINTIN)
|
|
const Fixed_S16_3 fixed_x1 = (Fixed_S16_3) {
|
|
.integer = h_range->origin_x,
|
|
};
|
|
const Fixed_S16_3 fixed_x2 = (Fixed_S16_3) {
|
|
.integer = h_range->origin_x + h_range->size_w,
|
|
};
|
|
graphics_private_draw_horizontal_line_prepared(ctx, &ctx->dest_bitmap,
|
|
&ctx->dest_bitmap.bounds,
|
|
line->origin.y + TEXT_LINE_CAP_LINE(line),
|
|
fixed_x1, fixed_x2, GColorRed);
|
|
graphics_private_draw_horizontal_line_prepared(ctx, &ctx->dest_bitmap,
|
|
&ctx->dest_bitmap.bounds,
|
|
line->origin.y + TEXT_LINE_BASE_LINE(line),
|
|
fixed_x1, fixed_x2, GColorRed);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
typedef struct {
|
|
int16_t origin_x;
|
|
int16_t width_px;
|
|
} OrphanLineState;
|
|
|
|
static OrphanLineState prv_capture_orphan_state(Line const* line) {
|
|
return (OrphanLineState) {
|
|
.origin_x = line->origin.x,
|
|
.width_px = line->width_px,
|
|
};
|
|
}
|
|
|
|
static void prv_apply_orphan_state(const OrphanLineState *state, Line *line) {
|
|
line->origin.x = state->origin_x;
|
|
line->width_px = state->width_px;
|
|
}
|
|
|
|
//! Iterate over lines in the text box
|
|
static inline void prv_walk_lines_down(Iterator* const line_iter, TextLayout* const layout,
|
|
WalkLinesCallbacks* const callbacks) {
|
|
LineIterState* line_iter_state = (LineIterState*) line_iter->state;
|
|
GContext* ctx = line_iter_state->ctx;
|
|
const GSize ctx_size = graphics_context_get_framebuffer_size(ctx);
|
|
const TextBoxParams* const text_box_params = &ctx->text_draw_state.text_box;
|
|
Line* line = line_iter_state->current;
|
|
|
|
const TextLayoutFlowData *flow_data = graphics_text_layout_get_flow_data(layout);
|
|
const bool uses_paging = flow_data->paging.page_on_screen.size_h != 0;
|
|
const bool uses_perimeter = flow_data->perimeter.impl != NULL;
|
|
const GPoint perimeter_paging_offset =
|
|
uses_paging ? gpoint_sub(flow_data->paging.origin_on_screen, line->origin) : GPointZero;
|
|
Word prev_line_word = WORD_EMPTY;
|
|
while (!prv_line_iter_is_vertical_overflow(line_iter_state, text_box_params)) {
|
|
GPoint line_in_perimeter_space = gpoint_add(line->origin, perimeter_paging_offset);
|
|
|
|
if (uses_paging) {
|
|
const int16_t page_max_y = flow_data->paging.page_on_screen.origin_y +
|
|
flow_data->paging.page_on_screen.size_h;
|
|
|
|
// TODO: optimize
|
|
while (line_in_perimeter_space.y < flow_data->paging.page_on_screen.origin_y) {
|
|
line_in_perimeter_space.y += flow_data->paging.page_on_screen.size_h;
|
|
}
|
|
while (line_in_perimeter_space.y >= page_max_y) {
|
|
line_in_perimeter_space.y -= flow_data->paging.page_on_screen.size_h;
|
|
}
|
|
|
|
const int16_t distance_to_page_end = page_max_y - line_in_perimeter_space.y;
|
|
|
|
if (distance_to_page_end < line->height_px + TEXT_LINE_DESCENDER_LINE(line)) {
|
|
// If this line would exceed the page_height, shift the line origin to the next page
|
|
line->origin.y += distance_to_page_end;
|
|
continue; // skip rendering this round, bypasses iter_next (no reset necessary)
|
|
}
|
|
}
|
|
|
|
// PBL-23045 Eventually remove perimeter debugging
|
|
GRangeHorizontal debug_perimeter_horizontal_range = {};
|
|
|
|
// If we are restricting the perimeter of the draw box, restrict per line region here
|
|
if (uses_perimeter) {
|
|
GRangeHorizontal text_horizontal_range = {.origin_x = line_in_perimeter_space.x,
|
|
.size_w = line->max_width_px};
|
|
const GRangeVertical vertical_range = {
|
|
.origin_y = line_in_perimeter_space.y + TEXT_LINE_CAP_LINE(line),
|
|
.size_h = TEXT_LINE_BASE_LINE(line) - TEXT_LINE_CAP_LINE(line)
|
|
};
|
|
GRangeHorizontal perimeter_horizontal_range =
|
|
flow_data->perimeter.impl->callback(flow_data->perimeter.impl, &ctx_size, vertical_range,
|
|
flow_data->perimeter.inset);
|
|
|
|
prv_debug_perimeter(ctx, &perimeter_horizontal_range, line);
|
|
|
|
// protect against range expanding: clip perimeter to the original text range
|
|
grange_clip((GRange*)&perimeter_horizontal_range, (GRange*)&text_horizontal_range);
|
|
text_horizontal_range = perimeter_horizontal_range;
|
|
|
|
// convert range back to screen space
|
|
text_horizontal_range.origin_x -= perimeter_paging_offset.x;
|
|
|
|
// Update line parameters for restricted horizontal range
|
|
line->origin.x = text_horizontal_range.origin_x;
|
|
line->max_width_px = text_horizontal_range.size_w;
|
|
}
|
|
|
|
// reference into the iterator's current word to easily access this attribute here and
|
|
// later without the complicated cast
|
|
Word *const current_word_ref = &(((WordIterState*)line_iter_state->word_iter.state)->current);
|
|
// state that needs to be captured so we can restore it in case of an orphan
|
|
const Word word_before_rendering = *current_word_ref;
|
|
const OrphanLineState orphan_state = prv_capture_orphan_state(line);
|
|
|
|
// When repeating text to prevent orhpans we could run into the situation where repeating text
|
|
// pushes down the remaining text far enough so it ends up on yet another page. This would
|
|
// enter an infinite loop.
|
|
// To avoid that, we only apply this strategy, when it's "safe" to do so (in theory, there's
|
|
// still the propability to run into this scenario if the perimeter isn't vertically symmetric).
|
|
// The chosen number should be large enough for the previous line, the orphan line plus some
|
|
// buffer.
|
|
const int num_safe_lines = 3;
|
|
const bool page_contains_enough_lines =
|
|
(flow_data->paging.page_on_screen.size_h >= num_safe_lines * line->height_px);
|
|
bool avoiding_orphans = uses_paging && ctx->draw_state.avoid_text_orphans &&
|
|
page_contains_enough_lines;
|
|
|
|
render_line: {} // this {} is just an empty statement that both C and our linter accepts
|
|
const bool is_text_remaining = line_add_words(
|
|
line, &line_iter_state->word_iter, callbacks->last_line_cb);
|
|
// NOTE: Account for descender - assume descender is no more than half the line height
|
|
const int16_t line_spacing_delta = prv_layout_get_line_spacing_delta(layout);
|
|
const int32_t line_max_y = line->origin.y + line->height_px +
|
|
TEXT_LINE_DESCENDER_LINE(line) + line_spacing_delta;
|
|
const int32_t clip_box_min_y = ctx->draw_state.clip_box.origin.y;
|
|
|
|
if (line_max_y > clip_box_min_y) {
|
|
if (avoiding_orphans) {
|
|
const bool line_is_first_line_page =
|
|
(line_in_perimeter_space.y == flow_data->paging.page_on_screen.origin_y);
|
|
const bool is_orphan =
|
|
(line_is_first_line_page && prev_line_word.start && !is_text_remaining);
|
|
|
|
if (is_orphan) {
|
|
*current_word_ref = prev_line_word;
|
|
prv_apply_orphan_state(&orphan_state, line);
|
|
avoiding_orphans = false; // prevent infinte loops
|
|
goto render_line;
|
|
}
|
|
}
|
|
if (callbacks->render_line_cb) {
|
|
callbacks->render_line_cb(ctx, line, text_box_params);
|
|
}
|
|
}
|
|
prev_line_word = word_before_rendering;
|
|
|
|
if (callbacks->layout_update_cb) {
|
|
callbacks->layout_update_cb(layout, line, text_box_params);
|
|
}
|
|
|
|
if (callbacks->stop_condition_cb) {
|
|
if (callbacks->stop_condition_cb(ctx, line, text_box_params)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!is_text_remaining) {
|
|
break;
|
|
}
|
|
|
|
// Shouldn't have rendered the line if there was insufficient space
|
|
PBL_ASSERTN(iter_next(line_iter));
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////
|
|
// Text layout
|
|
|
|
//! @return is_success
|
|
bool line_add_word(GContext* ctx, Line* line, Word* word, const TextBoxParams* const text_box_params) {
|
|
// Horizontal overflow
|
|
if (line->width_px > line->max_width_px) {
|
|
return false;
|
|
}
|
|
|
|
// Don't set the line height if there is a vertical overflow
|
|
const int line_height = fonts_get_font_height(text_box_params->font);
|
|
|
|
// We used to re-check for vertical overflow here
|
|
// but this is protected by a call to prv_line_iter_is_vertical_overflow,
|
|
// which will handle the truncation/clipping logic.
|
|
|
|
PBL_ASSERTN(word->start);
|
|
|
|
bool is_newline_first_codepoint = (*word->start == NEWLINE_CODEPOINT);
|
|
|
|
line->height_px = line_height;
|
|
|
|
if (is_newline_first_codepoint) {
|
|
// This trims off leading \n's from word. If we reach the end of the text while doing this, it sets
|
|
// word->start to NULL.
|
|
word_trim_preceeding_codepoint(ctx, word, NEWLINE_CODEPOINT, text_box_params);
|
|
if (text_box_params->overflow_mode != GTextOverflowModeFill) {
|
|
return false;
|
|
}
|
|
// If there is word text left (we have \n's at the end of the text), we're done
|
|
if (word->start == NULL) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool is_overflow = (line->width_px + word->width_px > line->max_width_px);
|
|
bool is_start_of_line = (line->width_px == 0);
|
|
bool should_hyphenate = (is_overflow && is_start_of_line);
|
|
|
|
if (is_start_of_line) {
|
|
line->start = word->start;
|
|
}
|
|
|
|
if (should_hyphenate) {
|
|
// Set suffix character
|
|
// [CJK] - when breaking a Katakana word, you probably don't want to add a hyphen. And to
|
|
// a Japanese user, a hyphen with Katakana looks like a long (chou-on) sound mark.
|
|
line->suffix_codepoint = HYPHEN_CODEPOINT;
|
|
utf8_t* last_visited = walk_line(ctx, line, text_box_params,
|
|
(CharVisitorCallback) update_dimensions_char_visitor_cb);
|
|
last_visited = (last_visited == NULL) ? (word->start) : last_visited;
|
|
|
|
// Trim the word
|
|
int suffix_width_px = prv_codepoint_get_horizontal_advance(&ctx->font_cache,
|
|
text_box_params->font, HYPHEN_CODEPOINT);
|
|
int truncated_word_length_px = (line->width_px - suffix_width_px);
|
|
PBL_ASSERTN(word->width_px >= truncated_word_length_px);
|
|
word->width_px -= truncated_word_length_px;
|
|
word->start = utf8_get_next(last_visited);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!is_overflow) {
|
|
// Add entire word
|
|
PBL_ASSERTN(line->suffix_codepoint == 0);
|
|
line->width_px += word->width_px;
|
|
return true;
|
|
}
|
|
|
|
// Word-wrap
|
|
word_trim_preceeding_whitespace(ctx, word, text_box_params);
|
|
return false;
|
|
}
|
|
|
|
static void prv_line_justify(Line* line, const TextBoxParams* const text_box_params) {
|
|
PBL_ASSERTN(line->max_width_px >= line->width_px);
|
|
|
|
int horiz_px_remaining = (line->max_width_px - line->width_px);
|
|
|
|
// [RTL] in addition to left, right and center alignment, you want a "primary"
|
|
// alignment that is left for LTR writing systems, and right for RTL.
|
|
switch (text_box_params->alignment) {
|
|
case GTextAlignmentCenter:
|
|
line->origin.x = line->origin.x + (horiz_px_remaining / 2);
|
|
break;
|
|
case GTextAlignmentRight:
|
|
line->origin.x = line->origin.x + horiz_px_remaining;
|
|
break;
|
|
case GTextAlignmentLeft:
|
|
break;
|
|
}
|
|
}
|
|
|
|
//! @return is_text_remaining
|
|
bool line_add_words(Line* line, Iterator* word_iter, LastLineCallback last_line_cb) {
|
|
|
|
WordIterState* word_iter_state = (WordIterState*) word_iter->state;
|
|
|
|
line->start = word_iter_state->current.start;
|
|
|
|
bool is_text_remaining = (line->start != NULL);
|
|
|
|
// PBL-22083 : max_width_px == 0 eats a character that should appear on next line
|
|
while (is_text_remaining && line->max_width_px > 0) {
|
|
Word next_word = word_iter_state->current;
|
|
|
|
bool is_added = line_add_word(word_iter_state->ctx, line, &next_word,
|
|
word_iter_state->text_box_params);
|
|
|
|
if (!is_added) {
|
|
word_iter_state->current = next_word;
|
|
// Check if word was trimmed until the null termination
|
|
if (next_word.start == NULL) {
|
|
is_text_remaining = false;
|
|
} else {
|
|
is_text_remaining = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
is_text_remaining = iter_next(word_iter);
|
|
}
|
|
|
|
if (last_line_cb) {
|
|
last_line_cb(word_iter_state->ctx, line, word_iter_state->text_box_params, is_text_remaining);
|
|
}
|
|
|
|
prv_line_justify(line, word_iter_state->text_box_params);
|
|
|
|
return is_text_remaining;
|
|
}
|
|
|
|
static bool prv_text_layout_is_fresh(TextLayout* layout, GFont const font, const GRect box,
|
|
const GTextOverflowMode overflow_mode,
|
|
const GTextAlignment alignment, Codepoint text_hash) {
|
|
PBL_ASSERTN(layout);
|
|
|
|
if (text_hash != layout->hash) {
|
|
return false;
|
|
}
|
|
|
|
if (!grect_equal(&box, &layout->box)) {
|
|
return false;
|
|
}
|
|
|
|
if (overflow_mode != layout->overflow_mode) {
|
|
return false;
|
|
}
|
|
|
|
if (alignment != layout->alignment) {
|
|
return false;
|
|
}
|
|
|
|
if (font != layout->font) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static inline void prv_text_walk_lines(GContext* ctx, TextLayout* const layout,
|
|
WalkLinesCallbacks* callbacks) {
|
|
|
|
TextBoxParams *text_box = &ctx->text_draw_state.text_box;
|
|
|
|
if (grect_is_empty(&text_box->box)) {
|
|
return;
|
|
}
|
|
|
|
const Utf8Bounds *utf8_bounds = text_box->utf8_bounds;
|
|
|
|
bool is_string_empty = (utf8_bounds->start == utf8_bounds->end);
|
|
if (is_string_empty) {
|
|
return;
|
|
}
|
|
|
|
const GTextOverflowMode overflow_mode = text_box->overflow_mode;
|
|
bool is_ellipsis_on_overflow = (overflow_mode == GTextOverflowModeTrailingEllipsis ||
|
|
overflow_mode == GTextOverflowModeFill);
|
|
if (is_ellipsis_on_overflow) {
|
|
callbacks->last_line_cb = set_ellipsis_on_overflow_last_line_cb;
|
|
} else {
|
|
callbacks->last_line_cb = NULL;
|
|
}
|
|
|
|
ctx->text_draw_state.line = (Line) {
|
|
.start = utf8_bounds->start,
|
|
// set initial bounding values for line
|
|
.origin = text_box->box.origin, //<! Needs to be in global co-ords!
|
|
.max_width_px = text_box->box.size.w,
|
|
.height_px = fonts_get_font_height(text_box->font)
|
|
};
|
|
|
|
Iterator line_iter;
|
|
line_iter_init(&line_iter, &ctx->text_draw_state.line_iter_state, ctx);
|
|
|
|
prv_walk_lines_down(&line_iter, layout, callbacks);
|
|
}
|
|
|
|
static void prv_graphics_text_layout_update(GContext* ctx, const char* text, GFont const font,
|
|
const GRect box, const GTextOverflowMode overflow_mode,
|
|
const GTextAlignment alignment,
|
|
TextLayout* const layout) {
|
|
PBL_ASSERTN(layout);
|
|
|
|
bool success = false;
|
|
const Utf8Bounds utf8_bounds = utf8_get_bounds(&success, text);
|
|
if (!success) {
|
|
layout->max_used_size = GSizeZero;
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "Invalid UTF8");
|
|
return;
|
|
}
|
|
|
|
int str_len_bytes = (utf8_bounds.end - utf8_bounds.start);
|
|
Codepoint text_hash = hash((const uint8_t*) utf8_bounds.start, str_len_bytes);
|
|
|
|
if (prv_text_layout_is_fresh(layout, font, box, overflow_mode, alignment, text_hash)) {
|
|
return;
|
|
}
|
|
|
|
layout->max_used_size = GSizeZero;
|
|
layout->hash = text_hash;
|
|
layout->box = box;
|
|
layout->overflow_mode = overflow_mode;
|
|
layout->alignment = alignment;
|
|
layout->font = font;
|
|
|
|
WalkLinesCallbacks callbacks = {
|
|
.layout_update_cb = update_all_layout_update_cb
|
|
};
|
|
|
|
int16_t line_spacing_delta = prv_layout_get_line_spacing_delta(layout);
|
|
ctx->text_draw_state.text_box = (TextBoxParams) {
|
|
.utf8_bounds = &utf8_bounds,
|
|
.box = box,
|
|
.font = font,
|
|
.overflow_mode = overflow_mode,
|
|
.alignment = alignment,
|
|
.line_spacing_delta = line_spacing_delta,
|
|
};
|
|
|
|
prv_text_walk_lines(ctx, layout, &callbacks);
|
|
}
|
|
|
|
// helper macro to avoid source code duplication
|
|
// we call this instead of a true function to keep the stack as low as possible as this is
|
|
// on a critical path.
|
|
#define APP_TEXT_GET_CONTENT_SIZE(text, font, box, overflow_mode, alignment, text_attributes) \
|
|
do { \
|
|
GContext* ctx = app_state_get_graphics_context(); \
|
|
return graphics_text_layout_get_max_used_size( \
|
|
ctx, text, font, box, overflow_mode, alignment, text_attributes); \
|
|
} while (0)
|
|
|
|
GSize app_graphics_text_layout_get_content_size_with_attributes(
|
|
const char *text, GFont const font, const GRect box, const GTextOverflowMode overflow_mode,
|
|
const GTextAlignment alignment, GTextAttributes *text_attributes) {
|
|
APP_TEXT_GET_CONTENT_SIZE(text, font, box, overflow_mode, alignment, text_attributes);
|
|
}
|
|
|
|
|
|
GSize app_graphics_text_layout_get_content_size(const char *text, GFont const font, const GRect box,
|
|
const GTextOverflowMode overflow_mode,
|
|
const GTextAlignment alignment) {
|
|
APP_TEXT_GET_CONTENT_SIZE(text, font, box, overflow_mode, alignment, NULL);
|
|
}
|
|
|
|
uint16_t graphics_text_layout_get_text_height(GContext *ctx, const char *text, GFont const font,
|
|
uint16_t bounds_width,
|
|
const GTextOverflowMode overflow_mode,
|
|
const GTextAlignment alignment) {
|
|
const int16_t LAYOUT_HEIGHT_IGNORE = SHRT_MAX;
|
|
GRect box = {
|
|
.origin = (GPoint) { .x = 0, .y = 0 },
|
|
.size = (GSize) { .w = bounds_width, .h = LAYOUT_HEIGHT_IGNORE }
|
|
};
|
|
GSize size = graphics_text_layout_get_max_used_size(ctx, text, font,
|
|
box, overflow_mode, alignment, NULL);
|
|
return size.h;
|
|
}
|
|
|
|
GSize graphics_text_layout_get_max_used_size(GContext *ctx, const char *text, GFont const font,
|
|
const GRect box, const GTextOverflowMode overflow_mode,
|
|
const GTextAlignment alignment,
|
|
GTextLayoutCacheRef const layout) {
|
|
TextLayoutExtended stack_layout = { 0 }; // Default use extended layout
|
|
TextLayout* text_layout = layout ? (TextLayout*) layout : (TextLayout*) &stack_layout;
|
|
prv_graphics_text_layout_update(ctx, text, font, box, overflow_mode, alignment, text_layout);
|
|
return text_layout->max_used_size;
|
|
}
|
|
|
|
void graphics_draw_text(GContext* ctx, const char* text, GFont const font,
|
|
GRect box, const GTextOverflowMode overflow_mode,
|
|
const GTextAlignment alignment, GTextLayoutCacheRef const layout) {
|
|
if (ctx->lock) {
|
|
return;
|
|
}
|
|
|
|
bool success = false;
|
|
const Utf8Bounds utf8_bounds = utf8_get_bounds(&success, text);
|
|
if (!success) {
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "Invalid UTF8");
|
|
return;
|
|
}
|
|
|
|
GRect global_box = grect_to_global_coordinates(box, ctx);
|
|
|
|
GRect temp_box = global_box;
|
|
grect_clip(&temp_box, &ctx->draw_state.clip_box);
|
|
if (temp_box.size.h <= 0) {
|
|
// the text is not ever going to make it on screen. Bail early.
|
|
return;
|
|
}
|
|
|
|
|
|
if (layout) {
|
|
layout->box.origin = global_box.origin;
|
|
}
|
|
|
|
WalkLinesCallbacks callbacks = {
|
|
.render_line_cb = render_all_render_line_cb,
|
|
.layout_update_cb = update_all_layout_update_cb,
|
|
.stop_condition_cb = is_clip_box_overflow_bottom_stop_condition_cb
|
|
};
|
|
|
|
int16_t line_spacing_delta = prv_layout_get_line_spacing_delta(layout);
|
|
ctx->text_draw_state.text_box = (TextBoxParams) {
|
|
.utf8_bounds = &utf8_bounds,
|
|
.box = global_box,
|
|
.font = font,
|
|
.overflow_mode = overflow_mode,
|
|
.alignment = alignment,
|
|
.line_spacing_delta = line_spacing_delta,
|
|
};
|
|
|
|
prv_text_walk_lines(ctx, layout, &callbacks);
|
|
}
|
|
|
|
void graphics_text_layout_cache_init(GTextLayoutCacheRef* layout) {
|
|
if (process_manager_compiled_with_legacy2_sdk()) {
|
|
*layout = applib_type_malloc(TextLayout);
|
|
*((TextLayout*) *layout) = (TextLayout) { 0 };
|
|
} else {
|
|
*layout = applib_type_malloc(TextLayoutExtended);
|
|
*((TextLayoutExtended*) *layout) = (TextLayoutExtended) { 0 };
|
|
}
|
|
}
|
|
|
|
void graphics_text_layout_cache_deinit(GTextLayoutCacheRef* layout) {
|
|
TextLayout* text_layout = (TextLayout*) *layout;
|
|
applib_free(text_layout);
|
|
*layout = NULL;
|
|
}
|
|
|
|
GTextAttributes *graphics_text_attributes_create(void) {
|
|
GTextAttributes *result;
|
|
graphics_text_layout_cache_init(&result);
|
|
return result;
|
|
}
|
|
|
|
void graphics_text_attributes_destroy(GTextAttributes *text_attributes) {
|
|
if (!text_attributes) {
|
|
return;
|
|
}
|
|
|
|
graphics_text_layout_cache_deinit(&text_attributes);
|
|
}
|
|
|
|
|
|
static TextLayoutExtended* prv_get_writable_extended_layout(GTextLayoutCacheRef layout) {
|
|
PBL_ASSERTN(!process_manager_compiled_with_legacy2_sdk()); // should not get here if 2.X
|
|
PBL_ASSERTN(layout);
|
|
// Invalidate the hash to ensure the layout gets updated when prv_graphics_text_layout_update is
|
|
// called on the layout
|
|
layout->hash = 0;
|
|
return (TextLayoutExtended *)layout;
|
|
}
|
|
|
|
static TextLayoutExtended* prv_get_readable_extended_layout(GTextLayoutCacheRef layout) {
|
|
if (!layout || process_manager_compiled_with_legacy2_sdk()) {
|
|
return NULL;
|
|
}
|
|
return (TextLayoutExtended*)layout;
|
|
}
|
|
|
|
|
|
void graphics_text_layout_set_line_spacing_delta(GTextLayoutCacheRef layout, int16_t delta) {
|
|
TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
|
|
if (extended) {
|
|
extended->line_spacing_delta = delta;
|
|
}
|
|
}
|
|
|
|
int16_t graphics_text_layout_get_line_spacing_delta(const GTextLayoutCacheRef layout) {
|
|
return prv_layout_get_line_spacing_delta(layout);
|
|
}
|
|
|
|
void graphics_text_attributes_restore_default_text_flow(GTextLayoutCacheRef layout) {
|
|
TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
|
|
if (!extended) {
|
|
return;
|
|
}
|
|
extended->flow_data.perimeter.impl = NULL;
|
|
}
|
|
|
|
// this way, we don't need to pull in all the dependencies when doing unit-tests
|
|
// if you want to test this aspect, just define the symbol below in your test_*.c file
|
|
#if !defined(UNITTEST)
|
|
#define USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT
|
|
#endif
|
|
|
|
void graphics_text_attributes_enable_screen_text_flow(GTextLayoutCacheRef layout, uint8_t inset) {
|
|
TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
|
|
if (!extended) {
|
|
return;
|
|
}
|
|
|
|
#if defined(USE_DISPLAY_PERIMETER_ON_FONT_LAYOUT)
|
|
// on rectangular screens, we can just leave the perimeter blank when we don't need an inset
|
|
const GPerimeter *shortcut_perimeter = PBL_IF_ROUND_ELSE(g_perimeter_for_display, NULL);
|
|
const GPerimeter *perimeter = inset > 0 ? g_perimeter_for_display : shortcut_perimeter;
|
|
#else
|
|
const GPerimeter *perimeter = NULL;
|
|
#endif
|
|
|
|
extended->flow_data.perimeter = (TextLayoutFlowDataPerimeter) {
|
|
.impl = perimeter,
|
|
.inset = inset,
|
|
};
|
|
}
|
|
|
|
void graphics_text_attributes_restore_default_paging(GTextLayoutCacheRef layout) {
|
|
TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
|
|
if (!extended) {
|
|
return;
|
|
}
|
|
extended->flow_data.paging.page_on_screen.size_h = 0;
|
|
}
|
|
|
|
void graphics_text_attributes_enable_paging(
|
|
GTextLayoutCacheRef layout, GPoint content_origin_on_screen, GRect paging_on_screen) {
|
|
TextLayoutExtended *extended = prv_get_writable_extended_layout(layout);
|
|
if (extended) {
|
|
extended->flow_data.paging = (TextLayoutFlowDataPaging) {
|
|
.origin_on_screen = content_origin_on_screen,
|
|
.page_on_screen.origin_y = paging_on_screen.origin.y,
|
|
.page_on_screen.size_h = paging_on_screen.size.h,
|
|
};
|
|
}
|
|
}
|
|
|
|
const TextLayoutFlowData *graphics_text_layout_get_flow_data(GTextLayoutCacheRef layout) {
|
|
TextLayoutExtended *extended_layout = prv_get_readable_extended_layout(layout);
|
|
if (extended_layout) {
|
|
return &extended_layout->flow_data;
|
|
} else {
|
|
static const TextLayoutFlowData s_default_data = {
|
|
// yes, this is basically just an empty struct but I want to be explicit here:
|
|
.perimeter.impl = NULL, // no perimeter/inset configured
|
|
.paging.page_on_screen.size_h = 0, // no paging or origin
|
|
};
|
|
return &s_default_data;
|
|
}
|
|
}
|