mirror of
https://github.com/google/pebble.git
synced 2025-11-19 05:50:54 -05:00
305 lines
13 KiB
C
305 lines
13 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 "text_render.h"
|
|
|
|
#include "gcontext.h"
|
|
#include "graphics.h"
|
|
#include "process_state/app_state/app_state.h"
|
|
#include "system/passert.h"
|
|
#include "text_resources.h"
|
|
#include "util/bitset.h"
|
|
#include "util/math.h"
|
|
|
|
#if !defined(__clang__)
|
|
#pragma GCC optimize ("O2")
|
|
#endif
|
|
|
|
static GRect get_glyph_rect(const GlyphData* glyph) {
|
|
GRect r = {
|
|
.size.w = glyph->header.width_px,
|
|
.size.h = glyph->header.height_px,
|
|
.origin.x = glyph->header.left_offset_px,
|
|
.origin.y = glyph->header.top_offset_px
|
|
};
|
|
|
|
return r;
|
|
}
|
|
|
|
/// This function returns the x coordinate of where to write the contents of a given word (32-bits)
|
|
/// of data from the 1-bit frame buffer into the 8-bit framebuffer
|
|
/// @param dest_bitmap 8-bit destination frame buffer bitmap
|
|
/// @param block_addr source address in 1-bit frame buffer of where the word is being updated
|
|
/// within a given row; assumed to be zero-based
|
|
/// @param y_offset row offset within the source 1-bit frame buffer
|
|
T_STATIC int32_t prv_convert_1bit_addr_to_8bit_x(GBitmap *dest_bitmap, uint32_t *block_addr,
|
|
int32_t y_offset) {
|
|
// Each byte block_addr corresponds to 8 pixels (i.e. 4-bytes in the 8-bit frame buffer).
|
|
// Thus multiply by 8 to get the word offset within the destination 8-bit frame buffer.
|
|
// Also need to account for the fact that the 1-bit frame buffer has 16 bits of unused space
|
|
// on each row (thus 16 bytes need to be subtracted from the destination address since there is
|
|
// no padding on each row of the 8-bit frame buffer.
|
|
const int32_t padding = (32 - (dest_bitmap->bounds.size.w % 32)) % 32;
|
|
// Calculate the overall offset in the 8-bit bitmap
|
|
const int32_t bitmap_offset_8bit = ((uint32_t)block_addr * 8) - (padding * y_offset);
|
|
// Calculate just the offset from the start of the target row in the 8-bit bitmap (i.e. "x")
|
|
return bitmap_offset_8bit - (dest_bitmap->bounds.size.w * y_offset);
|
|
}
|
|
|
|
// PRO TIP: if you have to modify this function, expect to waste the rest of your day on it
|
|
void render_glyph(GContext* const ctx, const uint32_t codepoint, FontInfo* const font,
|
|
const GRect cursor) {
|
|
if (codepoint_is_special(codepoint)) {
|
|
TextRenderState *state = app_state_get_text_render_state();
|
|
if (state->special_codepoint_handler_cb) {
|
|
state->special_codepoint_handler_cb(ctx, codepoint, cursor,
|
|
state->special_codepoint_handler_context);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const GlyphData* glyph = text_resources_get_glyph(&ctx->font_cache, codepoint, font);
|
|
|
|
PBL_ASSERTN(glyph);
|
|
// Bitfiddle the metrics data:
|
|
GRect glyph_metrics = get_glyph_rect(glyph);
|
|
|
|
// Calculate the box that we intend to draw to the screen, in screen coordinates
|
|
GRect glyph_target = {
|
|
.origin = { .x = cursor.origin.x + glyph_metrics.origin.x,
|
|
.y = cursor.origin.y + glyph_metrics.origin.y },
|
|
.size = { .w = glyph_metrics.size.w,
|
|
.h = glyph_metrics.size.h }
|
|
};
|
|
|
|
|
|
// The destination bitmap's x-coordinate and row advance. Used in the loop below.
|
|
GBitmap* dest_bitmap = graphics_context_get_bitmap(ctx);
|
|
const int32_t x = (int32_t)((int16_t)cursor.origin.x + (int16_t)glyph_metrics.origin.x);
|
|
|
|
// Now clip that box against the screen/other UI elements. This rect will be the rect that we
|
|
// actually fill with bits on the screen.
|
|
GRect clipped_glyph_target = glyph_target;
|
|
grect_clip(&clipped_glyph_target, &ctx->draw_state.clip_box);
|
|
|
|
// The number of bits to be clipped off the edges
|
|
const int left_clip = clipped_glyph_target.origin.x - glyph_target.origin.x;
|
|
const int right_clip = MIN(glyph_target.size.w,
|
|
MAX(0, glyph_target.size.w - clipped_glyph_target.size.w - left_clip));
|
|
|
|
#if SCREEN_COLOR_DEPTH_BITS == 8
|
|
// Set base address to 0 for 8-bit as this will be later translated to the destination bitmap
|
|
// address - so do all calculations so everything is offset from 0
|
|
uint32_t * base_addr = 0;
|
|
#else
|
|
uint32_t * base_addr = ((uint32_t*)dest_bitmap->addr);
|
|
#endif
|
|
|
|
const uint32_t * const dest_block_x_begin = base_addr +
|
|
(left_clip ?
|
|
MAX(0, (((x + left_clip + 31)/ 32) - 1)) : (x / 32));
|
|
|
|
if (clipped_glyph_target.size.h == 0 || clipped_glyph_target.size.w == 0) {
|
|
return;
|
|
}
|
|
|
|
#if SCREEN_COLOR_DEPTH_BITS == 8
|
|
// NOTE: Since all calculations are based on 1-bit calculation - use the row size from
|
|
// the 1-bit frame buffer
|
|
const int row_size_bytes = 4 * ((dest_bitmap->bounds.size.w / 32) +
|
|
((dest_bitmap->bounds.size.w % 32) ? 1 : 0));
|
|
#else
|
|
const int row_size_bytes = dest_bitmap->row_size_bytes;
|
|
#endif // SCREEN_COLOR_DEPTH_BITS == 8
|
|
|
|
// Number of blocks (i.e. 32-bit chunks)
|
|
const int dest_row_length = row_size_bytes / 4;
|
|
|
|
// The number of bits between the beginning of dest_block and glyph_block.
|
|
// If x is negative we need to be fancy to get the rounded down remainder. This
|
|
// is the number of bits to the right of the next 32-bit boundary to the left.
|
|
// For example, if x is -5 we want this shift to be 27, since -32 (the nearest
|
|
// boundary) + 27 = -5
|
|
const uint8_t dest_shift_at_line_begin = (x >= 0) ?
|
|
x % 32 :
|
|
(x - ((x / 32) * 32));
|
|
|
|
uint8_t dest_shift = dest_shift_at_line_begin;
|
|
|
|
// The glyph bitmap starts the block after the metrics data:
|
|
uint32_t const* glyph_block = glyph->data;
|
|
|
|
// Set up the first piece of source glyph bitmap:
|
|
int8_t glyph_block_bits_left = 32;
|
|
uint32_t src = *glyph_block;
|
|
|
|
// Use bit-rotate to align to shift the bitmap to align with the destination.
|
|
// The advantage of rotate vs. bitwise shift is that we can use
|
|
// the bits that wrapped around for the next dest_block
|
|
rotl32(src, dest_shift);
|
|
int8_t src_rotated = dest_shift;
|
|
// how many 32-bit blocks do we need to bitblt on each row. If we're not word aligned we'll need to
|
|
// modify an extra partial word, as we'll have an incomplete word on either side of the line segment
|
|
// we're modifying.
|
|
// For 1-bit, each pixel goes into one bit in dest bitmap - so 32 pixels per block
|
|
const uint8_t num_dest_blocks_per_row = (clipped_glyph_target.size.w / 32) +
|
|
(((dest_shift + left_clip) % 32) ? 1 : 0);
|
|
|
|
// Handle clipping at the top of the character. We need to skip a number of bits in our source data.
|
|
const unsigned int bits_to_skip = glyph_metrics.size.w * (clipped_glyph_target.origin.y - glyph_target.origin.y);
|
|
if (bits_to_skip) {
|
|
glyph_block += bits_to_skip / 32;
|
|
src = *glyph_block;
|
|
|
|
// Simulate the rotate that happens at the bottom of the bitblt loop so our source value is set
|
|
// up just as if we actually rendered those first few lines.
|
|
rotl32(src, (dest_shift_at_line_begin + ((0 - ((uint8_t)glyph_metrics.size.w)) % 32) * (clipped_glyph_target.origin.y - glyph_target.origin.y)) % 32);
|
|
src_rotated = (dest_shift_at_line_begin + ((0 - ((uint8_t)glyph_metrics.size.w)) % 32) * (clipped_glyph_target.origin.y - glyph_target.origin.y)) % 32;
|
|
glyph_block_bits_left -= bits_to_skip % 32;
|
|
}
|
|
|
|
for (int dest_y = clipped_glyph_target.origin.y; dest_y != clipped_glyph_target.origin.y + clipped_glyph_target.size.h; ++dest_y) {
|
|
dest_shift = dest_shift_at_line_begin;
|
|
|
|
// Number of bits to render on this line.
|
|
uint8_t glyph_line_bits_left = clipped_glyph_target.size.w;
|
|
|
|
uint32_t *dest_block = (uint32_t *)dest_block_x_begin + (dest_y * dest_row_length);
|
|
const uint32_t *dest_block_end = dest_block + num_dest_blocks_per_row + 1;
|
|
|
|
if (left_clip) {
|
|
const int left_clip_shift = left_clip % 32;
|
|
const int clipped_blocks = left_clip / 32;
|
|
|
|
dest_shift = (dest_shift + left_clip_shift) % 32;
|
|
glyph_block_bits_left -= left_clip_shift;
|
|
|
|
glyph_block += clipped_blocks;
|
|
|
|
if (glyph_block_bits_left <= 0) {
|
|
src = *(++glyph_block);
|
|
glyph_block_bits_left += 32;
|
|
// Need to account for the dest_shift when loading up the new glyph block
|
|
rotl32(src, glyph_block_bits_left + dest_shift);
|
|
src_rotated = glyph_block_bits_left + dest_shift;
|
|
}
|
|
|
|
dest_block += clipped_blocks;
|
|
}
|
|
|
|
while (dest_block != dest_block_end && glyph_line_bits_left) {
|
|
PBL_ASSERT(dest_block < dest_block_end, "DB=<%p> DBE=<%p>", dest_block, dest_block_end);
|
|
PBL_ASSERTN(dest_block >= (uint32_t*) base_addr);
|
|
PBL_ASSERTN(dest_block < (uint32_t*) base_addr + row_size_bytes *
|
|
(dest_bitmap->bounds.origin.y + dest_bitmap->bounds.size.h));
|
|
|
|
// bitblt part of glyph_block:
|
|
const uint8_t number_of_bits = MIN(32 - dest_shift, MIN(glyph_line_bits_left, glyph_block_bits_left));
|
|
const uint32_t mask = (((1 << number_of_bits) - 1) << dest_shift);
|
|
|
|
#if SCREEN_COLOR_DEPTH_BITS == 8
|
|
// dest_block points to the block if the dest image was a 1-bit buffer
|
|
// translate this to an x coordinate in the 8-bit buffer
|
|
const int32_t block_start_x = prv_convert_1bit_addr_to_8bit_x(dest_bitmap, dest_block,
|
|
dest_y);
|
|
const GBitmapDataRowInfo data_row = gbitmap_get_data_row_info(dest_bitmap, dest_y);
|
|
// Only enter the loop if the current block is within the valid data row range
|
|
if (block_start_x + 31 >= data_row.min_x && block_start_x <= data_row.max_x) {
|
|
uint8_t *dest_addr = data_row.data + block_start_x;
|
|
|
|
// For each bit in block, write that bit to the dest_bitmap
|
|
for (unsigned int bitindex = 0; bitindex < 32; bitindex++) {
|
|
const int32_t current_x = block_start_x + bitindex;
|
|
// Stop iteration early if we have reached the end of the data row
|
|
if (current_x > data_row.max_x) {
|
|
break;
|
|
}
|
|
// Skip over pixels outside of the bitmap data's x coordinate range
|
|
if (current_x < data_row.min_x) {
|
|
continue;
|
|
}
|
|
// Find position in dest_bitmap that corresponds to the bit index
|
|
// Write to that position if mask for that bit is 1
|
|
if ((mask & src) & (1 << bitindex)) {
|
|
GColor dest_color;
|
|
if (ctx->draw_state.compositing_mode == GCompOpSet) {
|
|
// Blend (i.e. for transparency) if GCompOpSet
|
|
dest_color = gcolor_alpha_blend(ctx->draw_state.text_color,
|
|
(GColor) {.argb = dest_addr[bitindex]});
|
|
} else {
|
|
dest_color = ctx->draw_state.text_color;
|
|
dest_color.a = 3;
|
|
}
|
|
dest_addr[bitindex] = dest_color.argb;
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
if (gcolor_equal(ctx->draw_state.text_color, GColorBlack)) {
|
|
*(dest_block) &= ~(mask & src);
|
|
} else {
|
|
*(dest_block) |= mask & src;
|
|
}
|
|
#endif
|
|
|
|
dest_shift = (dest_shift + number_of_bits) % 32;
|
|
glyph_block_bits_left -= number_of_bits;
|
|
glyph_line_bits_left -= number_of_bits;
|
|
|
|
if (glyph_block_bits_left <= 0) {
|
|
// We ran out of bits in the current glyph block. Get the next glyph blob:
|
|
src = *(++glyph_block);
|
|
glyph_block_bits_left += 32;
|
|
rotl32(src, dest_shift);
|
|
src_rotated = dest_shift;
|
|
// Continue with this dest_block if there is still space left:
|
|
if (dest_shift) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
++dest_block;
|
|
}
|
|
|
|
dest_shift += right_clip % 32;
|
|
|
|
// emulate having drawn the right clip
|
|
if (glyph_block_bits_left <= right_clip) {
|
|
int jump_words = (right_clip - glyph_block_bits_left) / 32 + 1;
|
|
glyph_block += jump_words;
|
|
src = *glyph_block;
|
|
rotl32(src, src_rotated);
|
|
glyph_block_bits_left += 32 * jump_words;
|
|
}
|
|
glyph_block_bits_left -= right_clip;
|
|
|
|
|
|
// Rotate the bits into the right position for the next row:
|
|
dest_shift = dest_shift_at_line_begin - dest_shift;
|
|
rotl32(src, dest_shift % 32);
|
|
src_rotated = (src_rotated + dest_shift) % 32;
|
|
}
|
|
|
|
graphics_context_mark_dirty_rect(ctx, clipped_glyph_target);
|
|
}
|
|
|
|
|
|
void text_render_set_special_codepoint_cb(SpecialCodepointHandlerCb handler, void *context) {
|
|
TextRenderState *state = app_state_get_text_render_state();
|
|
state->special_codepoint_handler_cb = handler;
|
|
state->special_codepoint_handler_context = context;
|
|
}
|