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:
Kovid Goyal 2026-02-24 08:45:21 +05:30
parent 2eb79c0a3a
commit c03e280128
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
3 changed files with 51 additions and 20 deletions

View file

@ -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]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -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

View file

@ -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