From b6ce952716fd7fc02d5f4310bbf0584ca7231ff9 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 7 Apr 2022 10:46:33 +0100 Subject: [PATCH] optimized line_crop --- src/textual/_compositor.py | 16 +++++----- src/textual/_lines.py | 17 ----------- src/textual/_segment_tools.py | 49 +++++++++++++++++++++++++++++++ tests/test_segment_tools.py | 55 +++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 26 deletions(-) delete mode 100644 src/textual/_lines.py create mode 100644 src/textual/_segment_tools.py create mode 100644 tests/test_segment_tools.py diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 5b4e68776..7ffd3b534 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -28,6 +28,7 @@ from .geometry import Region, Offset, Size from ._loop import loop_last +from ._segment_tools import line_crop from ._types import Lines from .widget import Widget @@ -412,7 +413,6 @@ class Compositor: else: widget_regions = [] - divide = Segment.divide intersection = Region.intersection overlaps = Region.overlaps @@ -425,9 +425,9 @@ class Compositor: new_x, new_y, new_width, new_height = intersection(region, clip) delta_x = new_x - region.x delta_y = new_y - region.y - splits = [delta_x, delta_x + new_width] + crop_x = delta_x + new_width lines = widget.get_render_lines(delta_y, delta_y + new_height) - lines = [list(divide(line, splits))[1] for line in lines] + lines = [line_crop(line, delta_x, crop_x) for line in lines] yield region, clip, lines @classmethod @@ -509,13 +509,11 @@ class Compositor: crop_x, crop_y, crop_x2, crop_y2 = crop_region.corners render_lines = self._assemble_chops(chops[crop_y:crop_y2]) - def width_view(line: list[Segment]) -> list[Segment]: - div_lines = list(divide(line, [crop_x, crop_x2])) - line = div_lines[1] if len(div_lines) > 1 else div_lines[0] - return line - if crop is not None and (crop_x, crop_x2) != (0, width): - render_lines = [width_view(line) if line else line for line in render_lines] + render_lines = [ + line_crop(line, crop_x, crop_x2) if line else line + for line in render_lines + ] return SegmentLines(render_lines, new_lines=True) diff --git a/src/textual/_lines.py b/src/textual/_lines.py deleted file mode 100644 index 9dfca984f..000000000 --- a/src/textual/_lines.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from rich.segment import Segment - -from .geometry import Region -from ._types import Lines - - -def crop_lines(lines: Lines, clip: Region) -> Lines: - lines = lines[clip.y : clip.y + clip.height] - - def width_view(line: list[Segment]) -> list[Segment]: - _, line = Segment.divide(line, [clip.x, clip.x + clip.width]) - return line - - cropped_lines = [width_view(line) for line in lines] - return cropped_lines diff --git a/src/textual/_segment_tools.py b/src/textual/_segment_tools.py new file mode 100644 index 000000000..15a9f25fd --- /dev/null +++ b/src/textual/_segment_tools.py @@ -0,0 +1,49 @@ +""" +Tools for processing Segments, or lists of Segments. +""" + +from __future__ import annotations + +from rich.segment import Segment + + +def line_crop(segments: list[Segment], start: int, end: int) -> list[Segment]: + """Crops a list of segments between two cell offsets. + + Args: + segments (list[Segment]): A list of Segments for a line. + start (int): Start offset + end (int): End offset + + Returns: + list[Segment]: A new shorter list of segments + """ + # This is essentially a specialized version of Segment.divide + # The following line has equivalent functionality (but a little slower) + # return list(Segment.divide(segments, [start, end]))[1] + pos = 0 + output_segments: list[Segment] = [] + add_segment = output_segments.append + iter_segments = iter(segments) + segment: Segment | None = None + for segment in iter_segments: + end_pos = pos + segment.cell_length + if end_pos > start: + segment = segment.split_cells(start - pos)[-1] + break + pos = end_pos + else: + return [] + + pos = start + while segment is not None: + end_pos = pos + segment.cell_length + 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 output_segments diff --git a/tests/test_segment_tools.py b/tests/test_segment_tools.py new file mode 100644 index 000000000..6c29ea613 --- /dev/null +++ b/tests/test_segment_tools.py @@ -0,0 +1,55 @@ +from rich.segment import Segment +from rich.style import Style + + +from textual._segment_tools import line_crop + + +def test_line_crop(): + bold = Style(bold=True) + italic = Style(italic=True) + segments = [ + Segment("Hello", bold), + Segment(" World!", italic), + ] + + assert line_crop(segments, 1, 2) == [Segment("e", bold)] + assert line_crop(segments, 4, 20) == [ + Segment("o", bold), + Segment(" World!", italic), + ] + + +def test_line_crop_emoji(): + bold = Style(bold=True) + italic = Style(italic=True) + segments = [ + Segment("Hello", bold), + Segment("💩💩💩", italic), + ] + assert line_crop(segments, 8, 11) == [Segment(" 💩", italic)] + assert line_crop(segments, 9, 11) == [Segment("💩", italic)] + + +def test_line_crop_edge(): + segments = [Segment("foo"), Segment("bar"), Segment("baz")] + assert line_crop(segments, 2, 9) == [Segment("o"), Segment("bar"), Segment("baz")] + assert line_crop(segments, 3, 9) == [Segment("bar"), Segment("baz")] + assert line_crop(segments, 4, 9) == [Segment("ar"), Segment("baz")] + assert line_crop(segments, 4, 8) == [Segment("ar"), Segment("ba")] + + +def test_line_crop_edge_2(): + segments = [ + Segment("╭─"), + Segment( + "────── Placeholder ───────", + ), + Segment( + "─╮", + ), + ] + result = line_crop(segments, 30, 60) + expected = [] + print(repr(result)) + assert result == expected