"""Line-API scroll view for the continuous reader — renders only the rows actually visible on screen (via Widget.render_line), reading from the flat Strip list diora_tui.layout.build_layout() precomputed once. This is what keeps very large books responsive: nothing here scales with book size at paint time, only with viewport height. """ from __future__ import annotations from contextlib import contextmanager from typing import Callable from textual.geometry import Size from textual.scroll_view import ScrollView from textual.strip import Strip from .layout import BookLayout class ContinuousBookView(ScrollView): # We always wrap text to fit exactly, so a horizontal scrollbar should # never be needed — and "scroll" (not "auto") for the vertical one keeps # its gutter reserved from the very first (still-empty) layout pass, so # the width we wrap text at later never has to guess whether a scrollbar # will appear and steal columns out from under already-wrapped lines. DEFAULT_CSS = """ ContinuousBookView { overflow-x: hidden; overflow-y: scroll; } """ def __init__(self, book_layout: BookLayout, on_read_scroll: Callable[[], None] | None = None) -> None: super().__init__() self.book_layout = book_layout self.virtual_size = Size(book_layout.width, book_layout.total_rows) # Called when the reader moves the view (keys, wheel, scrollbar), not when the program does # (restoring a position, re-wrapping after a resize, jumping on an offer): only the first # is "arriving at a place by reading" for the reading position (docs/progress-sync.md). self.on_read_scroll = on_read_scroll self._program_scrolls = 0 @contextmanager def program_scroll(self): self._program_scrolls += 1 try: yield finally: self._program_scrolls -= 1 def watch_scroll_y(self, old_value: float, new_value: float) -> None: super().watch_scroll_y(old_value, new_value) if old_value != new_value and not self._program_scrolls and self.on_read_scroll is not None: self.on_read_scroll() def render_line(self, y: int) -> Strip: _scroll_x, scroll_y = self.scroll_offset row = scroll_y + y strips = self.book_layout.row_strips width = self.scrollable_content_region.width if row < 0 or row >= len(strips): return Strip.blank(width, self.rich_style) return strips[row].crop_extend(0, width, self.rich_style)