"""Row-based layout for the continuous reader view — the TUI's terminal-grid analogue of the browser's pixel-based getBoundingClientRect() geometry (see blocks.py's module docstring for why blockIndex is exact but innerFraction is only an approximation here). Each block is wrapped independently at a known width, once, into a flat list of pre-rendered Strip objects (one per terminal row) that book_view.py's Line-API widget indexes directly in render_line() — this is what makes very large books (tens of thousands of blocks) open in seconds rather than minutes: Textual only ever renders the rows actually on screen, instead of laying out the whole book up front the way a single giant Static would. """ from __future__ import annotations from dataclasses import dataclass from rich.console import Console from rich.text import Text from textual.strip import Strip _SEPARATOR_ROWS = 1 # one blank row between blocks, matching the old "\n\n".join style @dataclass class BookLayout: starts: list[int] # row where block i begins heights: list[int] # rendered row count for block i row_strips: list[Strip] # one Strip per absolute row, len == total_rows total_rows: int width: int def build_layout(block_texts: list[str], width: int, console: Console) -> BookLayout: width = max(1, width) starts: list[int] = [] heights: list[int] = [] row_strips: list[Strip] = [] blank = Strip.blank(width) row = 0 for text in block_texts: starts.append(row) wrapped_lines = list(Text(text).wrap(console, width)) if text else [] if not wrapped_lines: wrapped_lines = [Text("")] heights.append(len(wrapped_lines)) for line in wrapped_lines: strip = Strip(line.render(console), None).adjust_cell_length(width) row_strips.append(strip) row_strips.append(blank) row += len(wrapped_lines) + _SEPARATOR_ROWS return BookLayout(starts=starts, heights=heights, row_strips=row_strips, total_rows=row, width=width) def get_position_anchor(layout: BookLayout, scroll_y: int) -> tuple[int, float]: """Mirrors app.js's getPositionAnchor(): the last block whose top row is at or above scroll_y, or the first block if none has scrolled that far yet.""" n = len(layout.heights) if n == 0: return 0, 0.0 best_index = 0 found = False for i in range(n): if layout.heights[i] < 1: continue if layout.starts[i] > scroll_y: break best_index = i found = True if not found: best_index = next((i for i in range(n) if layout.heights[i] >= 1), 0) top = layout.starts[best_index] height = max(1, layout.heights[best_index]) inner_fraction = max(0.0, min(1.0, (scroll_y - top) / height)) return best_index, inner_fraction def scroll_y_for_anchor(layout: BookLayout, block_index: int, inner_fraction: float) -> int: """Mirrors app.js's restoreFromAnchor().""" n = len(layout.heights) if n == 0: return 0 idx = max(0, min(block_index, n - 1)) top = layout.starts[idx] height = layout.heights[idx] return top + round(inner_fraction * height)