Text sizing protocol: Fix alignment/cropping issues when rendering text with a fractional scale
Also add some clarifications to the docs for exactly how alignment operates with fractional scaling. Fixes #9471 Fixes #9537
This commit is contained in:
parent
2eb79c0a3a
commit
c03e280128
3 changed files with 51 additions and 20 deletions
|
|
@ -243,6 +243,9 @@ Detailed list of changes
|
|||
- Wayland: Add support for :code:`titlebar-only` in :opt:`hide_window_decorations`
|
||||
to hide the titlebar while keeping shadows for window resizing. (:pull:`9486`)
|
||||
|
||||
- Text sizing protocol: Fix alignment/cropping issues when rendering text with
|
||||
a fractional scale (:iss:`9471`)
|
||||
|
||||
|
||||
0.45.0 [2025-12-24]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -77,9 +77,9 @@ There are only a handful of metadata keys, defined in the table below:
|
|||
|
||||
"d", "Integer from 0 to 15", "0", "The denominator for the fractional scale. Must be ``> n`` when non-zero."
|
||||
|
||||
"v", "Integer from 0 to 2", "0", "The vertical alignment to use for fractionally scaled text. ``0`` - top, ``1`` - bottom, ``2`` - centered"
|
||||
"v", "Integer from 0 to 2", "0", "The vertical alignment to use for fractionally scaled text (n < d). ``0`` - top, ``1`` - bottom, ``2`` - centered"
|
||||
|
||||
"h", "Integer from 0 to 2", "0", "The horizontal alignment to use for fractionally scaled text. ``0`` - left, ``1`` - right, ``2`` - centered"
|
||||
"h", "Integer from 0 to 2", "0", "The horizontal alignment to use for fractionally scaled text (n < d). ``0`` - left, ``1`` - right, ``2`` - centered"
|
||||
|
||||
|
||||
How it works
|
||||
|
|
@ -160,8 +160,11 @@ The fractional scale **does not** affect the number of cells the text occupies,
|
|||
instead, it just adjusts the rendered font size within those cells.
|
||||
The fraction is specified using an integer numerator and denominator (``n`` and
|
||||
``d``). In addition, by using the ``v`` key one can vertically align the
|
||||
fractionally scaled text at top, bottom or middle. Similarly, the ``h`` key
|
||||
does horizontal alignment — left, right or centered.
|
||||
fractionally scaled render area at top, bottom or middle. Similarly, the ``h`` key
|
||||
does horizontal alignment — left, right or centered. Note that alignment
|
||||
here is not actual text alignment, it refers to how the fractionally scaled
|
||||
render area fits inside the full render area of size ``s * w`` by ``s`` cells.
|
||||
Thus, alignment only applies when ``n < d``.
|
||||
|
||||
When using fractional scaling one often wants to fit more than a single
|
||||
character per cell. To accommodate that, there is the ``w`` key. This specifies
|
||||
|
|
|
|||
|
|
@ -1134,11 +1134,12 @@ render_filled_sprite(pixel *buf, unsigned num_glyphs, FontCellMetrics scaled_met
|
|||
static void
|
||||
apply_horizontal_alignment(pixel *canvas, RunFont rf, bool center_glyph, GlyphRenderInfo ri, unsigned max_render_width, unsigned canvas_height, unsigned num_cells, unsigned num_glyphs, bool was_colored) {
|
||||
int delta = 0;
|
||||
(void)was_colored;
|
||||
#ifdef __APPLE__
|
||||
if (num_cells == 2 && was_colored) center_glyph = true;
|
||||
#else
|
||||
(void)was_colored;
|
||||
#endif
|
||||
if (rf.subscale_n && rf.subscale_d && rf.align.horizontal) {
|
||||
if (rf.subscale_n && rf.subscale_d && rf.align.horizontal && rf.subscale_n < rf.subscale_d) {
|
||||
delta = max_render_width - ri.rendered_width;
|
||||
if (rf.align.horizontal == 2) delta /= 2;
|
||||
} else if (center_glyph && num_glyphs && num_cells > 1 && ri.rendered_width < max_render_width) {
|
||||
|
|
@ -1150,7 +1151,6 @@ apply_horizontal_alignment(pixel *canvas, RunFont rf, bool center_glyph, GlyphRe
|
|||
}
|
||||
|
||||
|
||||
|
||||
static void
|
||||
render_group(
|
||||
FontGroup *fg, unsigned int num_cells, unsigned int num_glyphs, CPUCell *cpu_cells, GPUCell *gpu_cells,
|
||||
|
|
@ -1160,7 +1160,17 @@ render_group(
|
|||
#define sp global_glyph_render_scratch.sprite_positions
|
||||
const FontCellMetrics scaled_metrics = fg->fcm;
|
||||
bool all_rendered = true;
|
||||
unsigned num_scaled_cells = (unsigned)ceil(num_cells / scale); if (!num_scaled_cells) num_scaled_cells = 1u;
|
||||
|
||||
unsigned num_scaled_cells = MAX(1u, (unsigned)ceil(num_cells / scale));
|
||||
const unsigned canvas_width = num_cells * unscaled_metrics.cell_width;
|
||||
const bool rendering_in_smaller_area = rf.subscale_n < rf.subscale_d;
|
||||
if (rendering_in_smaller_area) {
|
||||
// scw might be 1 px less than scaled_metrics.cell_width because of rounding, but it is the correct value
|
||||
// to use to determine the num of scaled cells
|
||||
unsigned scw = (unsigned)(unscaled_metrics.cell_width * scale);
|
||||
num_scaled_cells = unscaled_metrics.cell_width / scw;
|
||||
}
|
||||
unsigned scaled_canvas_width = num_scaled_cells * scaled_metrics.cell_width;
|
||||
Font *font = fg->fonts + rf.font_idx;
|
||||
|
||||
#define failed { \
|
||||
|
|
@ -1171,7 +1181,7 @@ render_group(
|
|||
|
||||
// One can have infinite ligatures with repeated groups of sprites when scaled size is an exact multiple or
|
||||
// divisor of unscaled size but I cant be bothered to implement that.
|
||||
bool is_infinite_ligature = num_cells == num_scaled_cells && num_cells > 9 && num_glyphs == num_cells;
|
||||
const bool is_infinite_ligature = num_cells == num_scaled_cells && num_cells > 9 && num_glyphs == num_cells;
|
||||
for (unsigned i = 0, ligature_index = 0; i < num_cells; i++) {
|
||||
bool is_repeat_sprite = is_infinite_ligature && i > 1 && i + 1 < num_glyphs && glyphs[i] == glyphs[i-1] && glyphs[i] == glyphs[i-2] && glyphs[i] == glyphs[i+1];
|
||||
sp[i] = is_repeat_sprite ? sp[i-1] : sprite_position_for(fg, rf, glyphs, glyph_count, ligature_index++, num_cells);
|
||||
|
|
@ -1184,6 +1194,8 @@ render_group(
|
|||
}
|
||||
|
||||
ensure_canvas_can_fit(fg, MAX(num_cells, num_scaled_cells) + 1, rf.scale);
|
||||
if (rendering_in_smaller_area) ensure_canvas_can_fit(fg, 2 * num_cells + 1, 1); // scratch space
|
||||
pixel *scratch = fg->canvas.buf + canvas_width * unscaled_metrics.cell_height;
|
||||
text_in_cell(cpu_cells, tc, global_glyph_render_scratch.lc);
|
||||
bool is_only_filled_boxes = false;
|
||||
bool was_colored = has_emoji_presentation(cpu_cells, global_glyph_render_scratch.lc);
|
||||
|
|
@ -1192,29 +1204,41 @@ render_group(
|
|||
is_only_filled_boxes = true;
|
||||
for (unsigned i = 1; i < num_glyphs && is_only_filled_boxes; i++) if (global_glyph_render_scratch.glyphs[i] != box_glyph_id) is_only_filled_boxes = false;
|
||||
}
|
||||
/*printf("num_cells: %u num_scaled_cells: %u num_glyphs: %u scale: %f unscaled: %ux%u scaled: %ux%u\n", num_cells, num_scaled_cells, num_glyphs, scale, unscaled_metrics.cell_width, unscaled_metrics.cell_height, scaled_metrics.cell_width, scaled_metrics.cell_height);*/
|
||||
GlyphRenderInfo ri = {0};
|
||||
pixel *canvas = rendering_in_smaller_area ? scratch : fg->canvas.buf;
|
||||
if (is_only_filled_boxes) { // special case rendering of █ for tests
|
||||
render_filled_sprite(fg->canvas.buf, num_glyphs, scaled_metrics, num_scaled_cells);
|
||||
render_filled_sprite(canvas, num_glyphs, scaled_metrics, num_scaled_cells);
|
||||
was_colored = false;
|
||||
ri.canvas_width = num_cells * unscaled_metrics.cell_width; ri.rendered_width = num_glyphs * scaled_metrics.cell_width;
|
||||
/*dump_sprite(fg->canvas.buf, scaled_metrics.cell_width * num_scaled_cells, scaled_metrics.cell_height);*/
|
||||
ri.canvas_width = canvas_width; ri.rendered_width = num_glyphs * scaled_metrics.cell_width;
|
||||
/*dump_sprite(canvas, scaled_metrics.cell_width * num_scaled_cells, scaled_metrics.cell_height);*/
|
||||
} else {
|
||||
render_glyphs_in_cells(font->face, font->bold, font->italic, info, positions, num_glyphs, fg->canvas.buf, scaled_metrics.cell_width, scaled_metrics.cell_height, num_scaled_cells, scaled_metrics.baseline, &was_colored, (FONTS_DATA_HANDLE)fg, &ri);
|
||||
render_glyphs_in_cells(font->face, font->bold, font->italic, info, positions, num_glyphs, canvas, scaled_metrics.cell_width, scaled_metrics.cell_height, num_scaled_cells, scaled_metrics.baseline, &was_colored, (FONTS_DATA_HANDLE)fg, &ri);
|
||||
ri.rendered_width = MIN(ri.rendered_width, ri.canvas_width);
|
||||
}
|
||||
apply_horizontal_alignment(fg->canvas.buf, rf, center_glyph, ri, num_cells * unscaled_metrics.cell_width, scaled_metrics.cell_height, num_scaled_cells, num_glyphs, was_colored);
|
||||
// printf("num_cells: %u num_scaled_cells: %u num_glyphs: %u scale: %f unscaled: %ux%u scaled: %ux%u rendered_width: %d\n", num_cells, num_scaled_cells, num_glyphs, scale, unscaled_metrics.cell_width, unscaled_metrics.cell_height, scaled_metrics.cell_width, scaled_metrics.cell_height, ri.rendered_width);
|
||||
if (rendering_in_smaller_area) { // expand into actual canvas width
|
||||
unsigned stride = MIN(canvas_width, scaled_canvas_width);
|
||||
for (unsigned y = 0; y < scaled_metrics.cell_height; y++) memcpy(
|
||||
fg->canvas.buf + y * canvas_width, canvas + y * scaled_canvas_width, sizeof(pixel) * stride);
|
||||
ri.canvas_width = canvas_width;
|
||||
scaled_canvas_width = canvas_width;
|
||||
canvas = fg->canvas.buf;
|
||||
}
|
||||
apply_horizontal_alignment(
|
||||
canvas, rf, center_glyph, ri, canvas_width,
|
||||
scaled_metrics.cell_height, num_scaled_cells, num_glyphs, was_colored);
|
||||
if (PyErr_Occurred()) PyErr_Print();
|
||||
|
||||
fg->fcm = unscaled_metrics; // needed for current_send_sprite_to_gpu()
|
||||
|
||||
if (num_cells == num_scaled_cells && rf.scale == 1.f) {
|
||||
if (num_cells == num_scaled_cells && scale == 1.f) {
|
||||
Region src = {.bottom=unscaled_metrics.cell_height, .right=unscaled_metrics.cell_width}, dest = src;
|
||||
DecorationMetadata dm = index_for_decorations(fg, rf, src, dest, scaled_metrics);
|
||||
for (unsigned i = 0; i < num_cells; i++) {
|
||||
if (!sp[i]->rendered) {
|
||||
bool is_repeat_sprite = is_infinite_ligature && i > 0 && sp[i]->idx == sp[i-1]->idx;
|
||||
if (!is_repeat_sprite) {
|
||||
pixel *b = num_cells == 1 ? fg->canvas.buf : extract_cell_from_canvas(fg, i, num_cells);
|
||||
pixel *b = num_cells == 1 ? canvas : extract_cell_from_canvas(fg, i, num_cells);
|
||||
sp[i]->idx = current_send_sprite_to_gpu(fg, b, dm, scaled_metrics);
|
||||
if (!sp[i]->idx) failed;
|
||||
} else sp[i]->idx = sp[i-1]->idx;
|
||||
|
|
@ -1226,10 +1250,12 @@ render_group(
|
|||
Region src={.bottom=scaled_metrics.cell_height, .right=scaled_metrics.cell_width * num_scaled_cells}, dest={.right=unscaled_metrics.cell_width};
|
||||
calculate_regions_for_line(rf, unscaled_metrics.cell_height, &src, &dest);
|
||||
DecorationMetadata dm = index_for_decorations(fg, rf, src, dest, scaled_metrics);
|
||||
/*printf("line: %u src -> dest: (%u %u) -> (%u %u)\n", rf.multicell_y, src.top, src.bottom, dest.top, dest.bottom);*/
|
||||
if (rendering_in_smaller_area) src.right = unscaled_metrics.cell_width;
|
||||
// printf("line: %u src -> dest: (%u %u) -> (%u %u)\n", rf.multicell_y, src.top, src.bottom, dest.top, dest.bottom);
|
||||
for (unsigned i = 0; i < num_cells; i++) {
|
||||
if (!sp[i]->rendered) {
|
||||
pixel *b = extract_cell_region(&fg->canvas, i, &src, &dest, scaled_metrics.cell_width * num_scaled_cells, unscaled_metrics);
|
||||
pixel *b = extract_cell_region(
|
||||
&fg->canvas, i, &src, &dest, scaled_canvas_width, unscaled_metrics);
|
||||
/*printf("cell %u src -> dest: (%u %u) -> (%u %u)\n", i, src.left, src.right, dest.left, dest.right);*/
|
||||
sp[i]->idx = current_send_sprite_to_gpu(fg, b, dm, scaled_metrics);
|
||||
if (!sp[i]->idx) failed;
|
||||
|
|
@ -1239,7 +1265,6 @@ render_group(
|
|||
set_cell_sprite(gpu_cells + i, sp[i]);
|
||||
}
|
||||
}
|
||||
|
||||
fg->fcm = scaled_metrics;
|
||||
#undef sp
|
||||
#undef failed
|
||||
|
|
|
|||
Loading…
Reference in a new issue