fix(pygame): add fallback border rendering for fonts without box-drawing chars

- Detect if font lacks box-drawing glyphs by testing rendering
- Use pygame.graphics to draw border when text glyphs unavailable
- Adjust content offset to avoid overlapping border
- Ensures border always visible regardless of font support

This improves compatibility across platforms and font configurations.
This commit is contained in:
2026-03-18 12:20:55 -07:00
parent 6d2c5ba304
commit abe49ba7d7

View File

@@ -136,6 +136,21 @@ class PygameDisplay:
else: else:
self._font = pygame.font.SysFont("monospace", self.cell_height - 2) self._font = pygame.font.SysFont("monospace", self.cell_height - 2)
# Check if font supports box-drawing characters; if not, try to find one
self._use_fallback_border = False
if self._font:
try:
# Test rendering some key box-drawing characters
test_chars = ["", "", "", "", "", ""]
for ch in test_chars:
surf = self._font.render(ch, True, (255, 255, 255))
# If surface is empty (width=0 or all black), font lacks glyph
if surf.get_width() == 0:
raise ValueError("Missing glyph")
except Exception:
# Font doesn't support box-drawing, will use line drawing fallback
self._use_fallback_border = True
self._initialized = True self._initialized = True
def show(self, buffer: list[str], border: bool = False) -> None: def show(self, buffer: list[str], border: bool = False) -> None:
@@ -184,14 +199,26 @@ class PygameDisplay:
fps = 1000.0 / avg_ms fps = 1000.0 / avg_ms
frame_time = avg_ms frame_time = avg_ms
# Apply border if requested self._screen.fill((0, 0, 0))
# If border requested but font lacks box-drawing glyphs, use graphical fallback
if border and self._use_fallback_border:
self._draw_fallback_border(fps, frame_time)
# Adjust content area to fit inside border
content_offset_x = self.cell_width
content_offset_y = self.cell_height
self.window_width - 2 * self.cell_width
self.window_height - 2 * self.cell_height
else:
# Normal rendering (with or without text border)
content_offset_x = 0
content_offset_y = 0
if border: if border:
from engine.display import render_border from engine.display import render_border
buffer = render_border(buffer, self.width, self.height, fps, frame_time) buffer = render_border(buffer, self.width, self.height, fps, frame_time)
self._screen.fill((0, 0, 0))
blit_list = [] blit_list = []
for row_idx, line in enumerate(buffer[: self.height]): for row_idx, line in enumerate(buffer[: self.height]):
@@ -199,7 +226,7 @@ class PygameDisplay:
break break
tokens = parse_ansi(line) tokens = parse_ansi(line)
x_pos = 0 x_pos = content_offset_x
for text, fg, bg, _bold in tokens: for text, fg, bg, _bold in tokens:
if not text: if not text:
@@ -219,10 +246,17 @@ class PygameDisplay:
self._glyph_cache[cache_key] = self._font.render(text, True, fg) self._glyph_cache[cache_key] = self._font.render(text, True, fg)
surface = self._glyph_cache[cache_key] surface = self._glyph_cache[cache_key]
blit_list.append((surface, (x_pos, row_idx * self.cell_height))) blit_list.append(
(surface, (x_pos, content_offset_y + row_idx * self.cell_height))
)
x_pos += self._font.size(text)[0] x_pos += self._font.size(text)[0]
self._screen.blits(blit_list) self._screen.blits(blit_list)
# Draw fallback border using graphics if needed
if border and self._use_fallback_border:
self._draw_fallback_border(fps, frame_time)
self._pygame.display.flip() self._pygame.display.flip()
elapsed_ms = (time.perf_counter() - t0) * 1000 elapsed_ms = (time.perf_counter() - t0) * 1000
@@ -231,6 +265,56 @@ class PygameDisplay:
chars_in = sum(len(line) for line in buffer) chars_in = sum(len(line) for line in buffer)
monitor.record_effect("pygame_display", elapsed_ms, chars_in, chars_in) monitor.record_effect("pygame_display", elapsed_ms, chars_in, chars_in)
def _draw_fallback_border(self, fps: float, frame_time: float) -> None:
"""Draw border using pygame graphics primitives instead of text."""
if not self._screen or not self._pygame:
return
# Colors
border_color = (0, 255, 0) # Green (like terminal border)
text_color = (255, 255, 255)
# Calculate dimensions
x1 = 0
y1 = 0
x2 = self.window_width - 1
y2 = self.window_height - 1
# Draw outer rectangle
self._pygame.draw.rect(
self._screen, border_color, (x1, y1, x2 - x1 + 1, y2 - y1 + 1), 1
)
# Draw top border with FPS
if fps > 0:
fps_text = f" FPS:{fps:.0f}"
else:
fps_text = ""
# We need to render this text with a fallback font that has basic ASCII
# Use system font which should have these characters
try:
font = self._font # May not have box chars but should have alphanumeric
text_surf = font.render(fps_text, True, text_color, (0, 0, 0))
text_rect = text_surf.get_rect()
# Position on top border, right-aligned
text_x = x2 - text_rect.width - 5
text_y = y1 + 2
self._screen.blit(text_surf, (text_x, text_y))
except Exception:
pass
# Draw bottom border with frame time
if frame_time > 0:
ft_text = f" {frame_time:.1f}ms"
try:
ft_surf = self._font.render(ft_text, True, text_color, (0, 0, 0))
ft_rect = ft_surf.get_rect()
ft_x = x2 - ft_rect.width - 5
ft_y = y2 - ft_rect.height - 2
self._screen.blit(ft_surf, (ft_x, ft_y))
except Exception:
pass
def clear(self) -> None: def clear(self) -> None:
if self._screen and self._pygame: if self._screen and self._pygame:
self._screen.fill((0, 0, 0)) self._screen.fill((0, 0, 0))