""" Canvas - 2D surface for rendering. The Canvas represents a full rendered surface that can be larger than the display. The Camera then defines the visible viewport into this canvas. """ from dataclasses import dataclass @dataclass class CanvasRegion: """A rectangular region on the canvas.""" x: int y: int width: int height: int def is_valid(self) -> bool: """Check if region has positive dimensions.""" return self.width > 0 and self.height > 0 def rows(self) -> set[int]: """Return set of row indices in this region.""" return set(range(self.y, self.y + self.height)) class Canvas: """2D canvas for rendering content. The canvas is a 2D grid of cells that can hold text content. It can be larger than the visible viewport (display). Attributes: width: Total width in characters height: Total height in characters """ def __init__(self, width: int = 80, height: int = 24): self.width = width self.height = height self._grid: list[list[str]] = [ [" " for _ in range(width)] for _ in range(height) ] self._dirty_regions: list[CanvasRegion] = [] # Track dirty regions def clear(self) -> None: """Clear the entire canvas.""" self._grid = [[" " for _ in range(self.width)] for _ in range(self.height)] self._dirty_regions = [CanvasRegion(0, 0, self.width, self.height)] def mark_dirty(self, x: int, y: int, width: int, height: int) -> None: """Mark a region as dirty (caller declares what they changed).""" self._dirty_regions.append(CanvasRegion(x, y, width, height)) def get_dirty_regions(self) -> list[CanvasRegion]: """Get all dirty regions and clear the set.""" regions = self._dirty_regions self._dirty_regions = [] return regions def get_dirty_rows(self) -> set[int]: """Get union of all dirty rows.""" rows: set[int] = set() for region in self._dirty_regions: rows.update(region.rows()) return rows def is_dirty(self) -> bool: """Check if any region is dirty.""" return len(self._dirty_regions) > 0 def get_region(self, x: int, y: int, width: int, height: int) -> list[list[str]]: """Get a rectangular region from the canvas. Args: x: Left position y: Top position width: Region width height: Region height Returns: 2D list of characters (height rows, width columns) """ region: list[list[str]] = [] for py in range(y, y + height): row: list[str] = [] for px in range(x, x + width): if 0 <= py < self.height and 0 <= px < self.width: row.append(self._grid[py][px]) else: row.append(" ") region.append(row) return region def get_region_flat(self, x: int, y: int, width: int, height: int) -> list[str]: """Get a rectangular region as flat list of lines. Args: x: Left position y: Top position width: Region width height: Region height Returns: List of strings (one per row) """ region = self.get_region(x, y, width, height) return ["".join(row) for row in region] def put_region(self, x: int, y: int, content: list[list[str]]) -> None: """Put content into a rectangular region on the canvas. Args: x: Left position y: Top position content: 2D list of characters to place """ height = len(content) if content else 0 width = len(content[0]) if height > 0 else 0 for py, row in enumerate(content): for px, char in enumerate(row): canvas_x = x + px canvas_y = y + py if 0 <= canvas_y < self.height and 0 <= canvas_x < self.width: self._grid[canvas_y][canvas_x] = char if width > 0 and height > 0: self.mark_dirty(x, y, width, height) def put_text(self, x: int, y: int, text: str) -> None: """Put a single line of text at position. Args: x: Left position y: Row position text: Text to place """ text_len = len(text) for i, char in enumerate(text): canvas_x = x + i if 0 <= canvas_x < self.width and 0 <= y < self.height: self._grid[y][canvas_x] = char if text_len > 0: self.mark_dirty(x, y, text_len, 1) def fill(self, x: int, y: int, width: int, height: int, char: str = " ") -> None: """Fill a rectangular region with a character. Args: x: Left position y: Top position width: Region width height: Region height char: Character to fill with """ for py in range(y, y + height): for px in range(x, x + width): if 0 <= py < self.height and 0 <= px < self.width: self._grid[py][px] = char if width > 0 and height > 0: self.mark_dirty(x, y, width, height) def resize(self, width: int, height: int) -> None: """Resize the canvas. Args: width: New width height: New height """ if width == self.width and height == self.height: return new_grid: list[list[str]] = [[" " for _ in range(width)] for _ in range(height)] for py in range(min(self.height, height)): for px in range(min(self.width, width)): new_grid[py][px] = self._grid[py][px] self.width = width self.height = height self._grid = new_grid