From c03e280128049816d1000e5c67647eae2ef5e22e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 24 Feb 2026 08:45:21 +0530 Subject: [PATCH] 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 --- docs/changelog.rst | 3 ++ docs/text-sizing-protocol.rst | 11 ++++--- kitty/fonts.c | 57 +++++++++++++++++++++++++---------- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 045d0fa21..1f29112ab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/text-sizing-protocol.rst b/docs/text-sizing-protocol.rst index 98f9fa65f..10568d576 100644 --- a/docs/text-sizing-protocol.rst +++ b/docs/text-sizing-protocol.rst @@ -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 diff --git a/kitty/fonts.c b/kitty/fonts.c index 5269fb882..523dd241e 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -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