extend trips to line API

This commit is contained in:
Will McGugan
2022-12-29 10:13:37 +00:00
parent cce244ddd0
commit 51c7fef2e1
10 changed files with 76 additions and 57 deletions

View File

@@ -26,7 +26,6 @@ from . import errors
from ._cells import cell_len from ._cells import cell_len
from ._loop import loop_last from ._loop import loop_last
from .strip import Strip from .strip import Strip
from ._types import Strips
from ._typing import TypeAlias from ._typing import TypeAlias
from .geometry import NULL_OFFSET, Offset, Region, Size from .geometry import NULL_OFFSET, Offset, Region, Size
@@ -67,7 +66,7 @@ CompositorMap: TypeAlias = "dict[Widget, MapGeometry]"
class LayoutUpdate: class LayoutUpdate:
"""A renderable containing the result of a render for a given region.""" """A renderable containing the result of a render for a given region."""
def __init__(self, strips: Strips, region: Region) -> None: def __init__(self, strips: list[Strip], region: Region) -> None:
self.strips = strips self.strips = strips
self.region = region self.region = region
@@ -634,7 +633,7 @@ class Compositor:
def _get_renders( def _get_renders(
self, crop: Region | None = None self, crop: Region | None = None
) -> Iterable[tuple[Region, Region, Strips]]: ) -> Iterable[tuple[Region, Region, list[Strip]]]:
"""Get rendered widgets (lists of segments) in the composition. """Get rendered widgets (lists of segments) in the composition.
Returns: Returns:

View File

@@ -10,7 +10,6 @@ from rich.segment import Segment
from rich.style import Style from rich.style import Style
from ._cells import cell_len from ._cells import cell_len
from ._types import Strips
from .css.types import AlignHorizontal, AlignVertical from .css.types import AlignHorizontal, AlignVertical
from .geometry import Size from .geometry import Size

View File

@@ -11,7 +11,6 @@ from ._border import get_box, render_row
from ._filter import LineFilter from ._filter import LineFilter
from ._opacity import _apply_opacity from ._opacity import _apply_opacity
from ._segment_tools import line_crop, line_pad, line_trim from ._segment_tools import line_crop, line_pad, line_trim
from ._types import Strips
from ._typing import TypeAlias from ._typing import TypeAlias
from .color import Color from .color import Color
from .geometry import Region, Size, Spacing from .geometry import Region, Size, Spacing

View File

@@ -29,6 +29,5 @@ class EventTarget(Protocol):
... ...
Strips = List["Strip"]
SegmentLines = List[List["Segment"]] SegmentLines = List[List["Segment"]]
CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]] CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]]

View File

@@ -42,6 +42,32 @@ class Strip:
yield self._segments yield self._segments
yield self.cell_length yield self.cell_length
@classmethod
def blank(cls, cell_length: int, style: Style | None) -> Strip:
"""Create a blank strip.
Args:
cell_length (int): Desired cell length.
style (Style | None): Style of blank.
Returns:
Strip: New strip.
"""
return cls([Segment(" " * cell_length, style)], cell_length)
@classmethod
def from_lines(cls, lines: list[list[Segment]], cell_length: int) -> list[Strip]:
"""Convert lines (lists of segments) to a list of Strips.
Args:
lines (list[list[Segment]]): List of lines, where a line is a list of segments.
cell_length (int): Cell length of lines (must be same).
Returns:
list[Strip]: List of strips.
"""
return [cls(segments, cell_length) for segments in lines]
@property @property
def cell_length(self) -> int: def cell_length(self) -> int:
"""Get the number of cells required to render this object.""" """Get the number of cells required to render this object."""
@@ -194,6 +220,8 @@ class Strip:
Returns: Returns:
Strip: A new Strip. Strip: A new Strip.
""" """
if start == 0 and end == self.cell_length:
return self
cache_key = (start, end) cache_key = (start, end)
cached = self._crop_cache.get(cache_key) cached = self._crop_cache.get(cache_key)
if cached is not None: if cached is not None:

View File

@@ -155,7 +155,7 @@ class RenderCache(NamedTuple):
"""Stores results of a previous render.""" """Stores results of a previous render."""
size: Size size: Size
lines: list[list[Segment]] lines: list[Strip]
class WidgetError(Exception): class WidgetError(Exception):
@@ -2097,11 +2097,11 @@ class Widget(DOMNode):
align_vertical, align_vertical,
) )
) )
strips = [Strip(line, width) for line in lines]
self._render_cache = RenderCache(self.size, lines) self._render_cache = RenderCache(self.size, strips)
self._dirty_regions.clear() self._dirty_regions.clear()
def render_line(self, y: int) -> list[Segment]: def render_line(self, y: int) -> Strip:
"""Render a line of content. """Render a line of content.
Args: Args:
@@ -2115,7 +2115,7 @@ class Widget(DOMNode):
try: try:
line = self._render_cache.lines[y] line = self._render_cache.lines[y]
except IndexError: except IndexError:
line = [Segment(" " * self.size.width, self.rich_style)] line = Strip.blank(self.size.width, self.rich_style)
return line return line
def render_lines(self, crop: Region) -> list[Strip]: def render_lines(self, crop: Region) -> list[Strip]:

View File

@@ -19,6 +19,7 @@ from ..geometry import Region, Size, Spacing, clamp
from ..reactive import Reactive from ..reactive import Reactive
from ..render import measure from ..render import measure
from ..scroll_view import ScrollView from ..scroll_view import ScrollView
from ..strip import Strip
from .._typing import Literal from .._typing import Literal
CursorType = Literal["cell", "row", "column"] CursorType = Literal["cell", "row", "column"]
@@ -214,9 +215,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
tuple[int, int, Style, bool, bool], SegmentLines tuple[int, int, Style, bool, bool], SegmentLines
] ]
self._cell_render_cache = LRUCache(10000) self._cell_render_cache = LRUCache(10000)
self._line_cache: LRUCache[ self._line_cache: LRUCache[tuple[int, int, int, int, int, int, Style], Strip]
tuple[int, int, int, int, int, int, Style], list[Segment]
]
self._line_cache = LRUCache(1000) self._line_cache = LRUCache(1000)
self._line_no = 0 self._line_no = 0
@@ -567,9 +566,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
raise LookupError("Y coord {y!r} is greater than total height") raise LookupError("Y coord {y!r} is greater than total height")
return self._y_offsets[y] return self._y_offsets[y]
def _render_line( def _render_line(self, y: int, x1: int, x2: int, base_style: Style) -> Strip:
self, y: int, x1: int, x2: int, base_style: Style
) -> list[Segment]:
"""Render a line in to a list of segments. """Render a line in to a list of segments.
Args: Args:
@@ -587,7 +584,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
try: try:
row_index, line_no = self._get_offsets(y) row_index, line_no = self._get_offsets(y)
except LookupError: except LookupError:
return [Segment(" " * width, base_style)] return Strip.blank(width, base_style)
cursor_column = ( cursor_column = (
self.cursor_column self.cursor_column
if (self.show_cursor and self.cursor_row == row_index) if (self.show_cursor and self.cursor_row == row_index)
@@ -617,10 +614,11 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
segments = Segment.adjust_line_length(segments, width, style=base_style) segments = Segment.adjust_line_length(segments, width, style=base_style)
simplified_segments = list(Segment.simplify(segments)) simplified_segments = list(Segment.simplify(segments))
self._line_cache[cache_key] = simplified_segments strip = Strip(simplified_segments, width)
return segments self._line_cache[cache_key] = strip
return strip
def render_line(self, y: int) -> list[Segment]: def render_line(self, y: int) -> Strip:
width, height = self.size width, height = self.size
scroll_x, scroll_y = self.scroll_offset scroll_x, scroll_y = self.scroll_offset
fixed_top_row_count = sum( fixed_top_row_count = sum(

View File

@@ -15,7 +15,7 @@ from ..geometry import Size, Region
from ..scroll_view import ScrollView from ..scroll_view import ScrollView
from .._cache import LRUCache from .._cache import LRUCache
from .._segment_tools import line_crop from .._segment_tools import line_crop
from .._types import Strips from ..strip import Strip
class TextLog(ScrollView, can_focus=True): class TextLog(ScrollView, can_focus=True):
@@ -48,8 +48,8 @@ class TextLog(ScrollView, can_focus=True):
super().__init__(name=name, id=id, classes=classes) super().__init__(name=name, id=id, classes=classes)
self.max_lines = max_lines self.max_lines = max_lines
self._start_line: int = 0 self._start_line: int = 0
self.lines: list[list[Segment]] = [] self.lines: list[Strip] = []
self._line_cache: LRUCache[tuple[int, int, int, int], list[Segment]] self._line_cache: LRUCache[tuple[int, int, int, int], Strip]
self._line_cache = LRUCache(1024) self._line_cache = LRUCache(1024)
self.max_width: int = 0 self.max_width: int = 0
self.min_width = min_width self.min_width = min_width
@@ -120,7 +120,8 @@ class TextLog(ScrollView, can_focus=True):
self.max_width, self.max_width,
max(sum(segment.cell_length for segment in _line) for _line in lines), max(sum(segment.cell_length for segment in _line) for _line in lines),
) )
self.lines.extend(lines) strips = Strip.from_lines(lines, render_width)
self.lines.extend(strips)
if self.max_lines is not None and len(self.lines) > self.max_lines: if self.max_lines is not None and len(self.lines) > self.max_lines:
self._start_line += len(self.lines) - self.max_lines self._start_line += len(self.lines) - self.max_lines
@@ -138,13 +139,13 @@ class TextLog(ScrollView, can_focus=True):
self.virtual_size = Size(self.max_width, len(self.lines)) self.virtual_size = Size(self.max_width, len(self.lines))
self.refresh() self.refresh()
def render_line(self, y: int) -> list[Segment]: def render_line(self, y: int) -> Strip:
scroll_x, scroll_y = self.scroll_offset scroll_x, scroll_y = self.scroll_offset
line = self._render_line(scroll_y + y, scroll_x, self.size.width) line = self._render_line(scroll_y + y, scroll_x, self.size.width)
line = list(Segment.apply_style(line, self.rich_style)) strip = Strip(Segment.apply_style(line, self.rich_style), self.size.width)
return line return strip
def render_lines(self, crop: Region) -> Strips: def render_lines(self, crop: Region) -> list[Strip]:
"""Render the widget in to lines. """Render the widget in to lines.
Args: Args:
@@ -156,19 +157,20 @@ class TextLog(ScrollView, can_focus=True):
lines = self._styles_cache.render_widget(self, crop) lines = self._styles_cache.render_widget(self, crop)
return lines return lines
def _render_line(self, y: int, scroll_x: int, width: int) -> list[Segment]: def _render_line(self, y: int, scroll_x: int, width: int) -> Strip:
if y >= len(self.lines): if y >= len(self.lines):
return [Segment(" " * width, self.rich_style)] return Strip.blank(width, self.rich_style)
key = (y + self._start_line, scroll_x, width, self.max_width) key = (y + self._start_line, scroll_x, width, self.max_width)
if key in self._line_cache: if key in self._line_cache:
return self._line_cache[key] return self._line_cache[key]
line = self.lines[y] line = (
line = Segment.adjust_line_length( self.lines[y]
line, max(self.max_width, width), self.rich_style .adjust_cell_length(max(self.max_width, width), self.rich_style)
.crop(scroll_x, scroll_x + width)
) )
line = line_crop(line, scroll_x, scroll_x + width, self.max_width)
self._line_cache[key] = line self._line_cache[key] = line
return line return line

View File

@@ -5,22 +5,21 @@ from typing import ClassVar, Generic, NewType, TypeVar
import rich.repr import rich.repr
from rich.segment import Segment from rich.segment import Segment
from rich.style import Style, NULL_STYLE from rich.style import NULL_STYLE, Style
from rich.text import Text, TextType from rich.text import Text, TextType
from .. import events
from ..binding import Binding
from ..geometry import clamp, Region, Size
from .._loop import loop_last
from .._cache import LRUCache from .._cache import LRUCache
from ..message import Message from .._loop import loop_last
from ..reactive import reactive, var
from .._segment_tools import line_crop, line_pad from .._segment_tools import line_crop, line_pad
from .._types import MessageTarget from .._types import MessageTarget
from .._typing import TypeAlias from .._typing import TypeAlias
from ..binding import Binding
from ..geometry import Region, Size, clamp
from ..message import Message
from ..reactive import reactive, var
from ..scroll_view import ScrollView from ..scroll_view import ScrollView
from ..strip import Strip
from .. import events
NodeID = NewType("NodeID", int) NodeID = NewType("NodeID", int)
TreeDataType = TypeVar("TreeDataType") TreeDataType = TypeVar("TreeDataType")
@@ -365,7 +364,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
self._current_id = 0 self._current_id = 0
self.root = self._add_node(None, text_label, data) self.root = self._add_node(None, text_label, data)
self._line_cache: LRUCache[LineCacheKey, list[Segment]] = LRUCache(1024) self._line_cache: LRUCache[LineCacheKey, Strip] = LRUCache(1024)
self._tree_lines_cached: list[_TreeLine] | None = None self._tree_lines_cached: list[_TreeLine] | None = None
self._cursor_node: TreeNode[TreeDataType] | None = None self._cursor_node: TreeNode[TreeDataType] | None = None
@@ -666,7 +665,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
self.cursor_line = -1 self.cursor_line = -1
self.refresh() self.refresh()
def render_line(self, y: int) -> list[Segment]: def render_line(self, y: int) -> Strip:
width = self.size.width width = self.size.width
scroll_x, scroll_y = self.scroll_offset scroll_x, scroll_y = self.scroll_offset
style = self.rich_style style = self.rich_style
@@ -677,14 +676,12 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
style, style,
) )
def _render_line( def _render_line(self, y: int, x1: int, x2: int, base_style: Style) -> Strip:
self, y: int, x1: int, x2: int, base_style: Style
) -> list[Segment]:
tree_lines = self._tree_lines tree_lines = self._tree_lines
width = self.size.width width = self.size.width
if y >= len(tree_lines): if y >= len(tree_lines):
return [Segment(" " * width, base_style)] return Strip.blank(width, base_style)
line = tree_lines[y] line = tree_lines[y]
@@ -699,7 +696,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
tuple(node._updates for node in line.path), tuple(node._updates for node in line.path),
) )
if cache_key in self._line_cache: if cache_key in self._line_cache:
segments = self._line_cache[cache_key] strip = self._line_cache[cache_key]
else: else:
base_guide_style = self.get_component_rich_style( base_guide_style = self.get_component_rich_style(
"tree--guides", partial=True "tree--guides", partial=True
@@ -785,11 +782,10 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
segments = list(guides.render(self.app.console)) segments = list(guides.render(self.app.console))
pad_width = max(self.virtual_size.width, width) pad_width = max(self.virtual_size.width, width)
segments = line_pad(segments, 0, pad_width - guides.cell_len, line_style) segments = line_pad(segments, 0, pad_width - guides.cell_len, line_style)
self._line_cache[cache_key] = segments strip = self._line_cache[cache_key] = Strip(segments)
segments = line_crop(segments, x1, x2, Segment.get_line_length(segments)) strip = strip.crop(x1, x2)
return strip
return segments
def _on_resize(self, event: events.Resize) -> None: def _on_resize(self, event: events.Resize) -> None:
self._line_cache.grow(event.size.height) self._line_cache.grow(event.size.height)

View File

@@ -4,14 +4,13 @@ from rich.segment import Segment
from rich.style import Style from rich.style import Style
from textual._styles_cache import StylesCache from textual._styles_cache import StylesCache
from textual._types import Strips
from textual.color import Color from textual.color import Color
from textual.css.styles import Styles from textual.css.styles import Styles
from textual.geometry import Region, Size from textual.geometry import Region, Size
from textual.strip import Strip from textual.strip import Strip
def _extract_content(lines: Strips): def _extract_content(lines: list[list[Segment]]):
"""Extract the text content from lines.""" """Extract the text content from lines."""
content = ["".join(segment.text for segment in line) for line in lines] content = ["".join(segment.text for segment in line) for line in lines]
return content return content