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:
@@ -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
|
|
||||||
if border:
|
|
||||||
from engine.display import render_border
|
|
||||||
|
|
||||||
buffer = render_border(buffer, self.width, self.height, fps, frame_time)
|
|
||||||
|
|
||||||
self._screen.fill((0, 0, 0))
|
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:
|
||||||
|
from engine.display import render_border
|
||||||
|
|
||||||
|
buffer = render_border(buffer, self.width, self.height, fps, frame_time)
|
||||||
|
|
||||||
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))
|
||||||
|
|||||||
Reference in New Issue
Block a user