Files
Jean-Pierre MartineauandClaude Opus 5.5 3e251a9707 Docstrings and comments of meshterm/emulator are in ASD-STE100
The same rewrite as for the core package. The physical screen of a
handheld is now the display, and the computer is the handheld. The
comment on FONT_ENV now says that it is the environment variable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 03:51:55 -04:00

166 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: Apache-2.0
"""Cells to pixels: the display that the emulator shows, drawn from a :class:`~.vt.Terminal`.
The Cardputer Zero's display is 320×170. Its grid of 53 columns of 6 pixels and 14 rows of
12 pixels leaves a two-pixel margin at the right and at the bottom. This module divides
the margin equally, so that the grid is at the centre. A Linux console (the PicoCalc's)
starts its grid at the top-left corner of the display instead. It also draws bold as
bright: a dim palette colour becomes the matching bright colour, in the same glyph.
``top_left`` and ``bold_is_bright`` copy these two behaviours.
The pixels are packed directly into the byte layout that the destination wants: 16-bit
RGB565 for the Cardputer's framebuffer, 24-bit RGB for a desktop window. Thus nothing
converts a full frame on its way out.
Only the rows that the terminal reports as dirty are drawn again. A glyph row is drawn by
lookup, not pixel by pixel: each triple of (6-bit row pattern, foreground, background) is
packed one time and used again. This is what makes a full paint affordable on a 1 GHz
core.
"""
from __future__ import annotations
from collections.abc import Callable
from .font import CELL_H, CELL_W, Font
from .vt import BOLD, DIM, HIDDEN, REVERSE, RGB, STRIKE, UNDERLINE, Terminal
#: The size of the Cardputer Zero's display, in pixels.
PANEL_W = 320
PANEL_H = 170
#: A packer: one colour to the bytes of the destination, for one pixel.
Packer = Callable[[RGB], bytes]
def rgb565(colour: RGB) -> bytes:
"""``colour`` as one little-endian RGB565 pixel (the format of the Cardputer's framebuffer)."""
r, g, b = colour
value = (r >> 3) << 11 | (g >> 2) << 5 | b >> 3
return value.to_bytes(2, "little")
def rgb888(colour: RGB) -> bytes:
"""``colour`` as one 24-bit RGB pixel (the format of a desktop window)."""
return bytes(colour)
def _blend(a: RGB, b: RGB) -> RGB:
"""Halfway from ``a`` to ``b``: how the ink of a dim cell is drawn."""
return ((a[0] + b[0]) // 2, (a[1] + b[1]) // 2, (a[2] + b[2]) // 2)
class Raster:
"""The pixels of the display, kept in step with a terminal.
Args:
terminal: The grid to draw.
font: The cell font.
pack: How one pixel is laid out in :attr:`pixels`.
default_fg: The ink of a cell whose style names none.
default_bg: The paper of a cell whose style names none.
width: The width of the display in pixels.
height: The height of the display in pixels.
top_left: Start the grid at the top-left corner of the display, as a Linux
console does, instead of at the centre.
bold_is_bright: Draw bold as the Linux console does. A foreground from the eight
dim slots of the palette takes the matching bright colour (slot ``n + 8``),
and the glyph stays regular, because a console font has no bold face.
"""
def __init__(
self,
terminal: Terminal,
font: Font,
*,
pack: Packer = rgb888,
default_fg: RGB = (170, 170, 170),
default_bg: RGB = (0, 0, 0),
width: int = PANEL_W,
height: int = PANEL_H,
top_left: bool = False,
bold_is_bright: bool = False,
) -> None:
"""Lay out the grid on the display, and fill it with the default paper."""
self.terminal = terminal
self.font = font
self.pack = pack
self.default_fg = default_fg
self.default_bg = default_bg
self.width = width
self.height = height
self.bpp = len(pack((0, 0, 0)))
self.stride = width * self.bpp
self.left = 0 if top_left else (width - terminal.cols * CELL_W) // 2
self.top = 0 if top_left else (height - terminal.rows * CELL_H) // 2
palette = terminal.palette
self._bright = {palette[n]: palette[n + 8] for n in range(8)} if bold_is_bright else None
self.pixels = bytearray(pack(default_bg) * (width * height))
self._spans: dict[tuple[int, RGB, RGB], bytes] = {}
def _span(self, bits: int, fg: RGB, bg: RGB) -> bytes:
"""The six pixels of one glyph row, packed (cached by pattern and colours)."""
key = (bits, fg, bg)
span = self._spans.get(key)
if span is None:
ink, paper = self.pack(fg), self.pack(bg)
span = b"".join(ink if bits & (0x80 >> x) else paper for x in range(CELL_W))
if len(self._spans) > 65536:
self._spans.clear()
self._spans[key] = span
return span
def update(self) -> list[tuple[int, int]]:
"""Draw again the rows that the terminal reports as changed.
Returns:
The bands of pixel rows ``(first_y, last_y_exclusive)`` that changed, in
order. They are for a destination that writes only the parts that changed.
"""
rows = sorted(self.terminal.take_dirty())
for row in rows:
self._draw_row(row)
bands: list[tuple[int, int]] = []
for row in rows:
y0 = self.top + row * CELL_H
if bands and bands[-1][1] == y0:
bands[-1] = (bands[-1][0], y0 + CELL_H)
else:
bands.append((y0, y0 + CELL_H))
return bands
def redraw(self) -> None:
"""Mark each row as dirty, and draw the whole display again."""
self.terminal.take_dirty()
for row in range(self.terminal.rows):
self._draw_row(row)
def _draw_row(self, row: int) -> None:
cells = self.terminal.screen[row]
y0 = self.top + row * CELL_H
stride, bpp = self.stride, self.bpp
pixels = self.pixels
for col, (char, style) in enumerate(cells):
fg = style.fg or self.default_fg
bg = style.bg or self.default_bg
flags = style.flags
bold = bool(flags & BOLD)
if bold and self._bright is not None:
fg, bold = self._bright.get(fg, fg), False
if flags & REVERSE:
fg, bg = bg, fg
if flags & DIM:
fg = _blend(fg, bg)
if flags & HIDDEN or char == "":
glyph = bytes(CELL_H)
else:
glyph = self.font.glyph(char, bold=bold)
if flags & UNDERLINE:
glyph = glyph[: CELL_H - 2] + b"\xfc" + glyph[CELL_H - 1 :]
if flags & STRIKE:
glyph = glyph[:6] + b"\xfc" + glyph[7:]
x0 = (self.left + col * CELL_W) * bpp
for line in range(CELL_H):
at = (y0 + line) * stride + x0
pixels[at : at + CELL_W * bpp] = self._span(glyph[line], fg, bg)