adds Strip primitive

This commit is contained in:
Will McGugan
2022-12-26 18:06:35 +00:00
parent 87329b6d07
commit 6f82ad9c4a
11 changed files with 2294 additions and 2127 deletions

View File

@@ -26,10 +26,12 @@ from rich.style import Style
from . import errors from . import errors
from ._cells import cell_len from ._cells import cell_len
from ._loop import loop_last from ._loop import loop_last
from ._types import Lines 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
if TYPE_CHECKING: if TYPE_CHECKING:
from .widget import Widget from .widget import Widget
@@ -66,8 +68,8 @@ 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, lines: Lines, region: Region) -> None: def __init__(self, strips: Strips, region: Region) -> None:
self.lines = lines self.strips = strips
self.region = region self.region = region
def __rich_console__( def __rich_console__(
@@ -76,7 +78,7 @@ class LayoutUpdate:
x = self.region.x x = self.region.x
new_line = Segment.line() new_line = Segment.line()
move_to = Control.move_to move_to = Control.move_to
for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)): for last, (y, line) in loop_last(enumerate(self.strips, self.region.y)):
yield move_to(x, y) yield move_to(x, y)
yield from line yield from line
if not last: if not last:
@@ -92,7 +94,7 @@ class ChopsUpdate:
def __init__( def __init__(
self, self,
chops: list[dict[int, list[Segment] | None]], chops: list[dict[int, Strip | None]],
spans: list[tuple[int, int, int]], spans: list[tuple[int, int, int]],
chop_ends: list[list[int]], chop_ends: list[list[int]],
) -> None: ) -> None:
@@ -121,9 +123,9 @@ class ChopsUpdate:
for y, x1, x2 in self.spans: for y, x1, x2 in self.spans:
line = chops[y] line = chops[y]
ends = chop_ends[y] ends = chop_ends[y]
for end, (x, segments) in zip(ends, line.items()): for end, (x, strip) in zip(ends, line.items()):
# TODO: crop to x extents # TODO: crop to x extents
if segments is None: if strip is None:
continue continue
if x > x2 or end <= x1: if x > x2 or end <= x1:
@@ -131,10 +133,10 @@ class ChopsUpdate:
if x2 > x >= x1 and end <= x2: if x2 > x >= x1 and end <= x2:
yield move_to(x, y) yield move_to(x, y)
yield from segments yield from strip
continue continue
iter_segments = iter(segments) iter_segments = iter(strip)
if x < x1: if x < x1:
for segment in iter_segments: for segment in iter_segments:
next_x = x + _cell_len(segment.text) next_x = x + _cell_len(segment.text)
@@ -635,7 +637,7 @@ class Compositor:
def _get_renders( def _get_renders(
self, crop: Region | None = None self, crop: Region | None = None
) -> Iterable[tuple[Region, Region, Lines]]: ) -> Iterable[tuple[Region, Region, Strips]]:
"""Get rendered widgets (lists of segments) in the composition. """Get rendered widgets (lists of segments) in the composition.
Returns: Returns:
@@ -685,19 +687,21 @@ class Compositor:
_Region(delta_x, delta_y, new_width, new_height) _Region(delta_x, delta_y, new_width, new_height)
) )
@classmethod # @classmethod
def _assemble_chops( # def _assemble_chops(cls, chops: list[dict[int, Strip | None]]) -> list[Strip]:
cls, chops: list[dict[int, list[Segment] | None]] # """Combine chops in to lines."""
) -> list[list[Segment]]:
"""Combine chops in to lines."""
from_iterable = chain.from_iterable
segment_lines: list[list[Segment]] = [
list(from_iterable(line for line in bucket.values() if line is not None))
for bucket in chops
]
return segment_lines
def render(self, full: bool = False) -> RenderableType | None: # [Strip.join(strips) for strips in chops]
# from_iterable = chain.from_iterable
# segment_lines: list[list[Segment]] = [
# list(from_iterable(strip for strip in bucket.values() if strip is not None))
# for bucket in chops
# ]
# return segment_lines
def render(self, full: bool = False) -> RenderableType:
"""Render a layout. """Render a layout.
Returns: Returns:
@@ -728,8 +732,6 @@ class Compositor:
else: else:
return None return None
divide = Segment.divide
# Maps each cut on to a list of segments # Maps each cut on to a list of segments
cuts = self.cuts cuts = self.cuts
@@ -738,19 +740,19 @@ class Compositor:
"Callable[[list[int]], dict[int, list[Segment] | None]]", dict.fromkeys "Callable[[list[int]], dict[int, list[Segment] | None]]", dict.fromkeys
) )
# A mapping of cut index to a list of segments for each line # A mapping of cut index to a list of segments for each line
chops: list[dict[int, list[Segment] | None]] chops: list[dict[int, Strip | None]]
chops = [fromkeys(cut_set[:-1]) for cut_set in cuts] chops = [fromkeys(cut_set[:-1]) for cut_set in cuts]
cut_segments: Iterable[list[Segment]] cut_strips: Iterable[Strip]
# Go through all the renders in reverse order and fill buckets with no render # Go through all the renders in reverse order and fill buckets with no render
renders = self._get_renders(crop) renders = self._get_renders(crop)
intersection = Region.intersection intersection = Region.intersection
for region, clip, lines in renders: for region, clip, strips in renders:
render_region = intersection(region, clip) render_region = intersection(region, clip)
for y, line in zip(render_region.line_range, lines): for y, strip in zip(render_region.line_range, strips):
if not is_rendered_line(y): if not is_rendered_line(y):
continue continue
@@ -763,20 +765,20 @@ class Compositor:
] ]
if len(final_cuts) <= 2: if len(final_cuts) <= 2:
# Two cuts, which means the entire line # Two cuts, which means the entire line
cut_segments = [line] cut_strips = [strip]
else: else:
render_x = render_region.x render_x = render_region.x
relative_cuts = [cut - render_x for cut in final_cuts[1:]] relative_cuts = [cut - render_x for cut in final_cuts[1:]]
cut_segments = divide(line, relative_cuts) cut_strips = list(strip.divide(relative_cuts))
# Since we are painting front to back, the first segments for a cut "wins" # Since we are painting front to back, the first segments for a cut "wins"
for cut, segments in zip(final_cuts, cut_segments): for cut, segments in zip(final_cuts, cut_strips):
if chops_line[cut] is None: if chops_line[cut] is None:
chops_line[cut] = segments chops_line[cut] = segments
if full: if full:
render_lines = self._assemble_chops(chops) render_strips = [Strip.join(chop.values()) for chop in chops]
return LayoutUpdate(render_lines, screen_region) return LayoutUpdate(render_strips, screen_region)
else: else:
chop_ends = [cut_set[1:] for cut_set in cuts] chop_ends = [cut_set[1:] for cut_set in cuts]
return ChopsUpdate(chops, spans, chop_ends) return ChopsUpdate(chops, spans, chop_ends)

View File

@@ -10,7 +10,7 @@ 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 Lines from ._types import Strips
from .css.types import AlignHorizontal, AlignVertical from .css.types import AlignHorizontal, AlignVertical
from .geometry import Size from .geometry import Size
@@ -22,8 +22,8 @@ def line_crop(
Args: Args:
segments (list[Segment]): A list of Segments for a line. segments (list[Segment]): A list of Segments for a line.
start (int): Start offset start (int): Start offset (cells)
end (int): End offset (exclusive) end (int): End offset (cells, exclusive)
total (int): Total cell length of segments. total (int): Total cell length of segments.
Returns: Returns:
list[Segment]: A new shorter list of segments list[Segment]: A new shorter list of segments
@@ -130,7 +130,7 @@ def line_pad(
def align_lines( def align_lines(
lines: Lines, lines: Strips,
style: Style, style: Style,
size: Size, size: Size,
horizontal: AlignHorizontal, horizontal: AlignHorizontal,
@@ -153,7 +153,7 @@ def align_lines(
width, height = size width, height = size
shape_width, shape_height = Segment.get_shape(lines) shape_width, shape_height = Segment.get_shape(lines)
def blank_lines(count: int) -> Lines: def blank_lines(count: int) -> Strips:
return [[Segment(" " * width, style)]] * count return [[Segment(" " * width, style)]] * count
top_blank_lines = bottom_blank_lines = 0 top_blank_lines = bottom_blank_lines = 0

View File

@@ -11,12 +11,13 @@ 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 Lines 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
from .renderables.text_opacity import TextOpacity from .renderables.text_opacity import TextOpacity
from .renderables.tint import Tint from .renderables.tint import Tint
from .strip import Strip
if TYPE_CHECKING: if TYPE_CHECKING:
from .css.styles import StylesBase from .css.styles import StylesBase
@@ -25,35 +26,6 @@ if TYPE_CHECKING:
RenderLineCallback: TypeAlias = Callable[[int], List[Segment]] RenderLineCallback: TypeAlias = Callable[[int], List[Segment]]
def style_links(
segments: Iterable[Segment], link_id: str, link_style: Style
) -> list[Segment]:
"""Apply a style to the given link id.
Args:
segments (Iterable[Segment]): Segments.
link_id (str): A link id.
link_style (Style): Style to apply.
Returns:
list[Segment]: A list of new segments.
"""
_Segment = Segment
segments = [
_Segment(
text,
(style + link_style if style is not None else None)
if (style and not style._null and style._link_id == link_id)
else style,
control,
)
for text, style, control in segments
]
return segments
@lru_cache(1024 * 8) @lru_cache(1024 * 8)
def make_blank(width, style: Style) -> Segment: def make_blank(width, style: Style) -> Segment:
"""Make a blank segment. """Make a blank segment.
@@ -95,7 +67,7 @@ class StylesCache:
""" """
def __init__(self) -> None: def __init__(self) -> None:
self._cache: dict[int, list[Segment]] = {} self._cache: dict[int, Strip] = {}
self._dirty_lines: set[int] = set() self._dirty_lines: set[int] = set()
self._width = 1 self._width = 1
@@ -123,7 +95,7 @@ class StylesCache:
self._cache.clear() self._cache.clear()
self._dirty_lines.clear() self._dirty_lines.clear()
def render_widget(self, widget: Widget, crop: Region) -> Lines: def render_widget(self, widget: Widget, crop: Region) -> list[Strip]:
"""Render the content for a widget. """Render the content for a widget.
Args: Args:
@@ -135,7 +107,7 @@ class StylesCache:
""" """
base_background, background = widget.background_colors base_background, background = widget.background_colors
styles = widget.styles styles = widget.styles
lines = self.render( strips = self.render(
styles, styles,
widget.region.size, widget.region.size,
base_background, base_background,
@@ -147,7 +119,6 @@ class StylesCache:
filter=widget.app._filter, filter=widget.app._filter,
) )
if widget.auto_links: if widget.auto_links:
_style_links = style_links
hover_style = widget.hover_style hover_style = widget.hover_style
link_hover_style = widget.link_hover_style link_hover_style = widget.link_hover_style
if ( if (
@@ -157,12 +128,12 @@ class StylesCache:
and "@click" in hover_style.meta and "@click" in hover_style.meta
): ):
if link_hover_style: if link_hover_style:
lines = [ strips = [
_style_links(line, hover_style.link_id, link_hover_style) strip.style_links(hover_style.link_id, link_hover_style)
for line in lines for strip in strips
] ]
return lines return strips
def render( def render(
self, self,
@@ -175,7 +146,7 @@ class StylesCache:
padding: Spacing | None = None, padding: Spacing | None = None,
crop: Region | None = None, crop: Region | None = None,
filter: LineFilter | None = None, filter: LineFilter | None = None,
) -> Lines: ) -> list[Strip]:
"""Render a widget content plus CSS styles. """Render a widget content plus CSS styles.
Args: Args:
@@ -202,15 +173,14 @@ class StylesCache:
if width != self._width: if width != self._width:
self.clear() self.clear()
self._width = width self._width = width
lines: Lines = [] strips: list[Strip] = []
add_line = lines.append add_strip = strips.append
simplify = Segment.simplify
is_dirty = self._dirty_lines.__contains__ is_dirty = self._dirty_lines.__contains__
render_line = self.render_line render_line = self.render_line
for y in crop.line_range: for y in crop.line_range:
if is_dirty(y) or y not in self._cache: if is_dirty(y) or y not in self._cache:
line = render_line( strip = render_line(
styles, styles,
y, y,
size, size,
@@ -220,21 +190,19 @@ class StylesCache:
background, background,
render_content_line, render_content_line,
) )
line = list(simplify(line)) self._cache[y] = strip
self._cache[y] = line
else: else:
line = self._cache[y] strip = self._cache[y]
if filter: if filter:
line = filter.filter(line) strip = strip.apply_filter(filter)
add_line(line) add_strip(strip)
self._dirty_lines.difference_update(crop.line_range) self._dirty_lines.difference_update(crop.line_range)
if crop.column_span != (0, width): if crop.column_span != (0, width):
_line_crop = line_crop
x1, x2 = crop.column_span x1, x2 = crop.column_span
lines = [_line_crop(line, x1, x2, width) for line in lines] strips = [strip.crop(x1, x2) for strip in strips]
return lines return strips
def render_line( def render_line(
self, self,
@@ -246,7 +214,7 @@ class StylesCache:
base_background: Color, base_background: Color,
background: Color, background: Color,
render_content_line: RenderLineCallback, render_content_line: RenderLineCallback,
) -> list[Segment]: ) -> Strip:
"""Render a styled line. """Render a styled line.
Args: Args:
@@ -402,4 +370,5 @@ class StylesCache:
else: else:
line = [*line, right] line = [*line, right]
return post(line) strip = Strip(post(line), width)
return strip

View File

@@ -2,10 +2,12 @@ from typing import Awaitable, Callable, List, TYPE_CHECKING, Union
from rich.segment import Segment from rich.segment import Segment
from textual._typing import Protocol from ._typing import Protocol
if TYPE_CHECKING: if TYPE_CHECKING:
from .message import Message from .message import Message
from .strip import Strip
class MessageTarget(Protocol): class MessageTarget(Protocol):
@@ -27,5 +29,5 @@ class EventTarget(Protocol):
... ...
Lines = List[List[Segment]] Strips = List["Strip"]
CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]] CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]]

View File

@@ -30,6 +30,16 @@ class ScrollView(Widget):
"""Not transparent, i.e. renders something.""" """Not transparent, i.e. renders something."""
return False return False
def watch_scroll_x(self, new_value: float) -> None:
if self.show_horizontal_scrollbar:
self.horizontal_scrollbar.position = int(new_value)
self.refresh()
def watch_scroll_y(self, new_value: float) -> None:
if self.show_vertical_scrollbar:
self.vertical_scrollbar.position = int(new_value)
self.refresh()
def on_mount(self): def on_mount(self):
self._refresh_scrollbars() self._refresh_scrollbars()
@@ -68,6 +78,8 @@ class ScrollView(Widget):
virtual_size (Size): New virtual size. virtual_size (Size): New virtual size.
container_size (Size): New container size. container_size (Size): New container size.
""" """
if self._size != size or container_size != container_size:
self.refresh()
if ( if (
self._size != size self._size != size
or virtual_size != self.virtual_size or virtual_size != self.virtual_size
@@ -77,9 +89,7 @@ class ScrollView(Widget):
virtual_size = self.virtual_size virtual_size = self.virtual_size
self._container_size = size - self.styles.gutter.totals self._container_size = size - self.styles.gutter.totals
self._scroll_update(virtual_size) self._scroll_update(virtual_size)
self.scroll_to(self.scroll_x, self.scroll_y, animate=False) self.scroll_to(self.scroll_x, self.scroll_y, animate=False)
self.refresh()
def render(self) -> RenderableType: def render(self) -> RenderableType:
"""Render the scrollable region (if `render_lines` is not implemented). """Render the scrollable region (if `render_lines` is not implemented).

181
src/textual/strip.py Normal file
View File

@@ -0,0 +1,181 @@
from __future__ import annotations
from itertools import chain
from typing import Iterator, Iterable, Sequence
import rich.repr
from rich.cells import cell_len, set_cell_size
from rich.segment import Segment
from rich.style import Style
from ._filter import LineFilter
from ._segment_tools import line_crop
from ._profile import timer
@rich.repr.auto
class Strip:
__slots__ = ["_segments", "_cell_length", "_divide_cache"]
def __init__(
self, segments: Iterable[Segment], cell_length: int | None = None
) -> None:
self._segments = list(segments)
self._cell_length = cell_length
self._divide_cache: dict[tuple[int], list[Strip]] = {}
def __rich_repr__(self) -> rich.repr.Result:
yield self._segments
yield self.cell_length
@property
def cell_length(self) -> int:
"""Get the number of cells required to render this object."""
# Done on demand and cached, as this is an O(n) operation
if self._cell_length is None:
self._cell_length = Segment.get_line_length(self._segments)
return self._cell_length
@classmethod
def join(cls, strips: Iterable[Strip | None]) -> Strip:
segments: list[list[Segment]] = []
add_segments = segments.append
total_cell_length = 0
for strip in strips:
if strip is None:
continue
total_cell_length += strip.cell_length
add_segments(strip._segments)
strip = cls(chain.from_iterable(segments), total_cell_length)
return strip
def __bool__(self) -> bool:
return bool(self._segments)
def __iter__(self) -> Iterator[Segment]:
return iter(self._segments)
def __len__(self) -> int:
return len(self._segments)
def __eq__(self, strip: Strip) -> bool:
return (
self._segments == strip._segments and self.cell_length == strip.cell_length
)
def adjust_line_length(self, cell_length: int, style: Style | None) -> Strip:
new_line: list[Segment]
line = self._segments
current_cell_length = self.cell_length
_Segment = Segment
if current_cell_length < cell_length:
new_line = line + [
_Segment(" " * (cell_length - current_cell_length), style)
]
elif current_cell_length > cell_length:
new_line = []
append = new_line.append
line_length = 0
for segment in line:
segment_length = segment.cell_length
if line_length + segment_length < cell_length:
append(segment)
line_length += segment_length
else:
text, segment_style, _ = segment
text = set_cell_size(text, cell_length - line_length)
append(_Segment(text, segment_style))
break
else:
return self
return Strip(new_line, cell_length)
def simplify(self) -> Strip:
line = Strip(
Segment.simplify(self._segments),
self._cell_length,
)
return line
def apply_filter(self, filter: LineFilter) -> Strip:
return Strip(filter.filter(self._segments), self._cell_length)
def style_links(self, link_id: str, link_style: Style) -> Strip:
_Segment = Segment
if not any(
segment.style._link_id == link_id
for segment in self._segments
if segment.style
):
return self
segments = [
_Segment(
text,
(style + link_style if style is not None else None)
if (style and not style._null and style._link_id == link_id)
else style,
control,
)
for text, style, control in self._segments
]
return Strip(segments, self._cell_length)
def crop(self, start: int, end: int) -> Strip:
_cell_len = cell_len
pos = 0
output_segments: list[Segment] = []
add_segment = output_segments.append
iter_segments = iter(self._segments)
segment: Segment | None = None
for segment in iter_segments:
end_pos = pos + _cell_len(segment.text)
if end_pos > start:
segment = segment.split_cells(start - pos)[1]
break
pos = end_pos
else:
return Strip([], 0)
if end >= self.cell_length:
# The end crop is the end of the segments, so we can collect all remaining segments
if segment:
add_segment(segment)
output_segments.extend(iter_segments)
return Strip(output_segments, self.cell_length - start)
pos = start
while segment is not None:
end_pos = pos + _cell_len(segment.text)
if end_pos < end:
add_segment(segment)
else:
add_segment(segment.split_cells(end - pos)[0])
break
pos = end_pos
segment = next(iter_segments, None)
return Strip(output_segments, end - start)
def divide(self, cuts: Iterable[int]) -> list[Strip]:
pos = 0
cache_key = tuple(cuts)
cached = self._divide_cache.get(cache_key)
if cached is not None:
return cached
strips: list[Strip] = []
add_strip = strips.append
for segments, cut in zip(Segment.divide(self._segments, cuts), cuts):
add_strip(Strip(segments, cut - pos))
pos += cut
self._divide_cache[cache_key] = strips
return strips

View File

@@ -43,7 +43,7 @@ from ._easing import DEFAULT_SCROLL_EASING
from ._layout import Layout from ._layout import Layout
from ._segment_tools import align_lines from ._segment_tools import align_lines
from ._styles_cache import StylesCache from ._styles_cache import StylesCache
from ._types import Lines from ._types import Strips
from .actions import SkipAction from .actions import SkipAction
from .await_remove import AwaitRemove from .await_remove import AwaitRemove
from .binding import Binding from .binding import Binding
@@ -57,6 +57,7 @@ from .message import Message
from .messages import CallbackType from .messages import CallbackType
from .reactive import Reactive from .reactive import Reactive
from .render import measure from .render import measure
from .strip import Strip
from .walk import walk_depth_first from .walk import walk_depth_first
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -156,7 +157,7 @@ class RenderCache(NamedTuple):
"""Stores results of a previous render.""" """Stores results of a previous render."""
size: Size size: Size
lines: Lines lines: Strips
class WidgetError(Exception): class WidgetError(Exception):
@@ -2118,7 +2119,7 @@ class Widget(DOMNode):
line = [Segment(" " * self.size.width, self.rich_style)] line = [Segment(" " * self.size.width, self.rich_style)]
return line return line
def render_lines(self, crop: Region) -> Lines: def render_lines(self, crop: Region) -> list[Strip]:
"""Render the widget in to lines. """Render the widget in to lines.
Args: Args:
@@ -2127,8 +2128,8 @@ class Widget(DOMNode):
Returns: Returns:
Lines: A list of list of segments. Lines: A list of list of segments.
""" """
lines = self._styles_cache.render_widget(self, crop) strips = self._styles_cache.render_widget(self, crop)
return lines return strips
def get_style_at(self, x: int, y: int) -> Style: def get_style_at(self, x: int, y: int) -> Style:
"""Get the Rich style in a widget at a given relative offset. """Get the Rich style in a widget at a given relative offset.

View File

@@ -14,7 +14,7 @@ from rich.text import Text, TextType
from .. import events, messages from .. import events, messages
from .._cache import LRUCache from .._cache import LRUCache
from .._segment_tools import line_crop from .._segment_tools import line_crop
from .._types import Lines from .._types import Strips
from ..geometry import Region, Size, Spacing, clamp from ..geometry import Region, Size, Spacing, clamp
from ..reactive import Reactive from ..reactive import Reactive
from ..render import measure from ..render import measure
@@ -207,10 +207,10 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self.row_count = 0 self.row_count = 0
self._y_offsets: list[tuple[int, int]] = [] self._y_offsets: list[tuple[int, int]] = []
self._row_render_cache: LRUCache[ self._row_render_cache: LRUCache[
tuple[int, int, Style, int, int], tuple[Lines, Lines] tuple[int, int, Style, int, int], tuple[Strips, Strips]
] ]
self._row_render_cache = LRUCache(1000) self._row_render_cache = LRUCache(1000)
self._cell_render_cache: LRUCache[tuple[int, int, Style, bool, bool], Lines] self._cell_render_cache: LRUCache[tuple[int, int, Style, bool, bool], Strips]
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], list[Segment] tuple[int, int, int, int, int, int, Style], list[Segment]
@@ -450,7 +450,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
width: int, width: int,
cursor: bool = False, cursor: bool = False,
hover: bool = False, hover: bool = False,
) -> Lines: ) -> Strips:
"""Render the given cell. """Render the given cell.
Args: Args:
@@ -488,7 +488,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
base_style: Style, base_style: Style,
cursor_column: int = -1, cursor_column: int = -1,
hover_column: int = -1, hover_column: int = -1,
) -> tuple[Lines, Lines]: ) -> tuple[Strips, Strips]:
"""Render a row in to lines for each cell. """Render a row in to lines for each cell.
Args: Args:

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 Lines from .._types import Strips
class TextLog(ScrollView, can_focus=True): class TextLog(ScrollView, can_focus=True):
@@ -143,7 +143,7 @@ class TextLog(ScrollView, can_focus=True):
line = list(Segment.apply_style(line, self.rich_style)) line = list(Segment.apply_style(line, self.rich_style))
return line return line
def render_lines(self, crop: Region) -> Lines: def render_lines(self, crop: Region) -> Strips:
"""Render the widget in to lines. """Render the widget in to lines.
Args: Args:

File diff suppressed because one or more lines are too long

View File

@@ -4,13 +4,14 @@ 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 Lines 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
def _extract_content(lines: Lines): def _extract_content(lines: Strips):
"""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
@@ -44,10 +45,11 @@ def test_no_styles():
) )
style = Style.from_color(bgcolor=Color.parse("green").rich_color) style = Style.from_color(bgcolor=Color.parse("green").rich_color)
expected = [ expected = [
[Segment("foo", style)], Strip([Segment("foo", style)], 3),
[Segment("bar", style)], Strip([Segment("bar", style)], 3),
[Segment("baz", style)], Strip([Segment("baz", style)], 3),
] ]
assert lines == expected assert lines == expected