mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
extend trips to line API
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user