Fix cursor/background vertical alignment in SVG screenshots

- Remove dominant-baseline: text-before-edge (has Safari compatibility issues)
- Use separate y positions for rect (top of cell) and text (baseline)
- rect_y = padding + row * line_height (top of cell)
- text_y = rect_y + font_size (alphabetic baseline position)

This ensures background rects and text are properly aligned across all
browsers, fixing the half-line vertical offset on cursor blocks.
This commit is contained in:
GitHub Copilot
2026-01-24 19:55:45 +00:00
parent 1d09ff151f
commit 1f5e5c2c31
5 changed files with 11 additions and 8 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual-webterm"
version = "0.3.11"
version = "0.3.12"
description = "Serve terminal sessions over the web"
authors = ["Will McGugan <will@textualize.io>"]
license = "MIT"
+10 -5
View File
@@ -138,6 +138,9 @@ def render_terminal_svg(
parts.append(f"<title>{_escape_xml(title)}</title>")
# Style definitions
# Note: We use alphabetic baseline (default) and offset text y by font_size
# to align text top with rect top. This is more compatible across browsers
# than dominant-baseline: text-before-edge which has Safari issues.
parts.append("<defs><style>")
parts.append(
f".terminal-bg {{ fill: {background}; }}"
@@ -146,7 +149,6 @@ def render_terminal_svg(
f"font-size: {font_size}px; "
f"fill: {foreground}; "
f"white-space: pre; "
f"dominant-baseline: text-before-edge; "
f"text-rendering: optimizeLegibility; "
f"}}"
f".bold {{ font-weight: bold; }}"
@@ -167,8 +169,11 @@ def render_terminal_svg(
# Render each row - use explicit x position for EACH character
# to ensure pixel-perfect alignment regardless of font metrics
for row_idx, row_data in enumerate(screen_buffer):
# With dominant-baseline: text-before-edge, text top aligns to y
y = 10 + row_idx * actual_line_height
# rect_y is the top of the cell
rect_y = 10 + row_idx * actual_line_height
# text_y is the baseline position (alphabetic baseline = bottom of lowercase letters)
# For most fonts, baseline is roughly at font_size from top of em box
text_y = rect_y + font_size
if not row_data:
continue
@@ -205,7 +210,7 @@ def render_terminal_svg(
if bg != background:
bg_width = char_cols * char_width
row_bg_rects.append(
f'<rect x="{x:.1f}" y="{y:.1f}" '
f'<rect x="{x:.1f}" y="{rect_y:.1f}" '
f'width="{bg_width:.1f}" height="{actual_line_height:.1f}" '
f'fill="{bg}"/>'
)
@@ -234,7 +239,7 @@ def render_terminal_svg(
if row_bg_rects or row_tspans:
parts.extend(row_bg_rects)
if row_tspans:
parts.append(f'<text y="{y:.1f}">')
parts.append(f'<text y="{text_y:.1f}">')
parts.extend(row_tspans)
parts.append("</text>")
-2
View File
@@ -152,8 +152,6 @@ class TestRenderTerminalSvg:
def test_css_properties(self) -> None:
"""SVG includes essential CSS properties for proper rendering."""
svg = render_terminal_svg([], width=80, height=24)
# Check for proper baseline alignment
assert "dominant-baseline: text-before-edge" in svg
# Check for legibility optimization
assert "text-rendering: optimizeLegibility" in svg
# Check for monospace font