Scale box-drawing characters vertically to fill line height
Box-drawing characters (│┃║┌┐└┘├┤etc) are designed to connect between lines but the font's em-box is smaller than our line-height (14px vs 16.8px), creating visible gaps. Solution: Render box-drawing characters as separate text elements with a vertical scale transform of 1.2 (matching line-height) to stretch them to fill the full cell height and connect properly. This fixes disconnected vertical lines and corners in TUI applications.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "textual-webterm"
|
name = "textual-webterm"
|
||||||
version = "0.3.13"
|
version = "0.3.14"
|
||||||
description = "Serve terminal sessions over the web"
|
description = "Serve terminal sessions over the web"
|
||||||
authors = ["Will McGugan <will@textualize.io>"]
|
authors = ["Will McGugan <will@textualize.io>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -50,6 +50,25 @@ FONT_SIZE = 14
|
|||||||
LINE_HEIGHT = 1.2
|
LINE_HEIGHT = 1.2
|
||||||
CHAR_WIDTH = 8 # Width of monospace character at 14px (typically ~0.57 ratio)
|
CHAR_WIDTH = 8 # Width of monospace character at 14px (typically ~0.57 ratio)
|
||||||
|
|
||||||
|
# Box drawing characters that need vertical scaling to fill line height
|
||||||
|
# These are designed to connect between lines but the font's em-box is smaller
|
||||||
|
# than our line height, creating gaps
|
||||||
|
BOX_DRAWING_CHARS = frozenset(
|
||||||
|
# Light and heavy box drawing (U+2500-U+257F)
|
||||||
|
"─━│┃┄┅┆┇┈┉┊┋┌┍┎┏┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋"
|
||||||
|
# Double box drawing
|
||||||
|
"═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬"
|
||||||
|
# Rounded corners
|
||||||
|
"╭╮╯╰"
|
||||||
|
# Light and heavy dashed (U+2571-U+257F)
|
||||||
|
"\u2571\u2572\u2573╴╵╶╷╸╹╺╻╼╽╾╿"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_box_drawing(char: str) -> bool:
|
||||||
|
"""Check if character is a box-drawing character that needs scaling."""
|
||||||
|
return len(char) == 1 and char in BOX_DRAWING_CHARS
|
||||||
|
|
||||||
|
|
||||||
class CharData(TypedDict):
|
class CharData(TypedDict):
|
||||||
"""Character data from pyte screen buffer."""
|
"""Character data from pyte screen buffer."""
|
||||||
@@ -232,7 +251,20 @@ def render_terminal_svg(
|
|||||||
if classes:
|
if classes:
|
||||||
attrs.append(f'class="{" ".join(classes)}"')
|
attrs.append(f'class="{" ".join(classes)}"')
|
||||||
|
|
||||||
row_tspans.append(f'<tspan {" ".join(attrs)}>{_escape_xml(char_data)}</tspan>')
|
# Box-drawing characters need vertical scaling to fill line height
|
||||||
|
# Render them as separate text elements with transform
|
||||||
|
if _is_box_drawing(char_data):
|
||||||
|
# Scale vertically by line_height ratio, anchored at top of cell
|
||||||
|
# The transform scales around (x, rect_y) to stretch the glyph
|
||||||
|
fill_attr = f' fill="{fg}"' if fg != foreground else ""
|
||||||
|
class_attr = f' class="{" ".join(classes)}"' if classes else ""
|
||||||
|
row_bg_rects.append(
|
||||||
|
f'<text x="{x:.1f}" y="{text_y:.1f}" '
|
||||||
|
f'transform="translate(0,{rect_y:.1f}) scale(1,{line_height}) translate(0,{-rect_y:.1f})"'
|
||||||
|
f'{fill_attr}{class_attr}>{_escape_xml(char_data)}</text>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
row_tspans.append(f'<tspan {" ".join(attrs)}>{_escape_xml(char_data)}</tspan>')
|
||||||
|
|
||||||
col += char_cols
|
col += char_cols
|
||||||
|
|
||||||
|
|||||||
@@ -328,6 +328,22 @@ class TestRenderTerminalSvg:
|
|||||||
# Text tspan with yellow
|
# Text tspan with yellow
|
||||||
assert f'fill="{ANSI_COLORS["yellow"]}"' in svg
|
assert f'fill="{ANSI_COLORS["yellow"]}"' in svg
|
||||||
|
|
||||||
|
def test_box_drawing_vertical_scale(self) -> None:
|
||||||
|
"""Box-drawing characters are scaled vertically to fill line height."""
|
||||||
|
buffer = [[self._char("│")]] # Vertical line
|
||||||
|
svg = render_terminal_svg(buffer, width=80, height=24)
|
||||||
|
# Box drawing chars rendered with transform for vertical scaling
|
||||||
|
assert 'scale(1,1.2)' in svg
|
||||||
|
# Should be a separate text element, not a tspan
|
||||||
|
assert '<text x="' in svg
|
||||||
|
|
||||||
|
def test_box_drawing_corners(self) -> None:
|
||||||
|
"""Box-drawing corner characters are scaled."""
|
||||||
|
buffer = [[self._char("┌"), self._char("┐")]]
|
||||||
|
svg = render_terminal_svg(buffer, width=80, height=24)
|
||||||
|
# Both corners should have scale transforms
|
||||||
|
assert svg.count('scale(1,1.2)') == 2
|
||||||
|
|
||||||
def test_unicode_text(self) -> None:
|
def test_unicode_text(self) -> None:
|
||||||
"""Unicode text is preserved."""
|
"""Unicode text is preserved."""
|
||||||
buffer = self._make_buffer(["你好世界"])
|
buffer = self._make_buffer(["你好世界"])
|
||||||
|
|||||||
Reference in New Issue
Block a user