From 8f38ea4494a673cf638fd8853005219951d17f99 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 10 May 2022 17:37:27 +0100 Subject: [PATCH] span refinements --- src/textual/_compositor.py | 57 +++++++++-------------- src/textual/screen.py | 8 ++-- src/textual/scrollbar.py | 3 -- src/textual/widget.py | 3 ++ tests/test_compositor_regions_to_spans.py | 30 +++++++----- 5 files changed, 48 insertions(+), 53 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 2a8930982..b9256c09d 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -15,7 +15,7 @@ from __future__ import annotations from operator import attrgetter, itemgetter import sys -from typing import cast, Iterator, Iterable, NamedTuple, TYPE_CHECKING +from typing import Callable, cast, Iterator, Iterable, NamedTuple, TYPE_CHECKING import rich.repr from rich.console import Console, ConsoleOptions, RenderResult, RenderableType @@ -100,12 +100,15 @@ class SpansUpdate: self, console: Console, options: ConsoleOptions ) -> RenderResult: move_to = Control.move_to - for line, offset, segments in self.spans: - yield move_to(offset, line) + new_line = Segment.line() + for last, (y, x, segments) in loop_last(self.spans): + yield move_to(x, y) yield from segments + if not last: + yield new_line def __rich_repr__(self) -> rich.repr.Result: - yield [(y, offset, "...") for y, offset, segments in self.spans] + yield [(y, x, "...") for y, x, _segments in self.spans] @rich.repr.auto(angular=True) @@ -161,7 +164,7 @@ class Compositor: ranges.sort() x1, x2 = ranges[0] for next_x1, next_x2 in ranges[1:]: - if next_x1 <= x2 + 1: + if next_x1 <= x2: if next_x2 > x2: x2 = next_x2 else: @@ -523,6 +526,7 @@ class Compositor: is_rendered_line = {y for y, _, _ in spans}.__contains__ else: crop = screen_region + spans = [] is_rendered_line = lambda y: True _Segment = Segment @@ -566,34 +570,25 @@ class Compositor: if chops_line[cut] is None: chops_line[cut] = segments - # Assemble the cut renders in to lists of segments - crop_x, crop_y, crop_x2, crop_y2 = crop.corners - render_lines = self._assemble_chops(chops[crop_y:crop_y2]) - if regions: - + crop_x, crop_y, crop_x2, crop_y2 = crop.corners + render_lines = self._assemble_chops(chops[crop_y:crop_y2]) render_spans = [ - (y, x1, line_crop(render_lines[y], x1, x2)) for y, x1, x2 in spans + (y, x1, line_crop(render_lines[y - crop_y], x1, x2)) + for y, x1, x2 in spans ] return SpansUpdate(render_spans) else: - return SegmentLines(render_lines, new_lines=True) - - if crop is not None and (crop_x, crop_x2) != (0, width): - 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) + render_lines = self._assemble_chops(chops) + return LayoutUpdate(render_lines, screen_region) def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: yield self.render() - def update_widgets(self, *widgets: Widget) -> RenderableType | None: + def update_widgets(self, widgets: set[Widget]) -> RenderableType | None: """Update a given widget in the composition. Args: @@ -605,21 +600,13 @@ class Compositor: """ regions: list[Region] = [] add_region = regions.append - for widget in widgets: - if widget not in self.regions: - continue + for widget in self.regions.keys() & widgets: region, clip = self.regions[widget] - if not region: - continue update_region = region.intersection(clip) - if not update_region: - continue - add_region(update_region) - - # print(regions) + if update_region: + add_region(update_region) + print("UPDATE_WIDGET") + print(widgets) + print(regions) update = self.render(regions or None) - # print("UPDATE", update) return update - # update = LayoutUpdate(update_lines, total_region) - # # print(widgets, total_region) - # return update diff --git a/src/textual/screen.py b/src/textual/screen.py index ff5413873..2d4b08ccf 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -97,8 +97,8 @@ class Screen(Widget): # Render widgets together if self._dirty_widgets: - self.log(dirty=len(self._dirty_widgets)) - display_update = self._compositor.update_widgets(*self._dirty_widgets) + self.log(dirty=self._dirty_widgets) + display_update = self._compositor.update_widgets(self._dirty_widgets) if display_update is not None: self.app.display(display_update) self._dirty_widgets.clear() @@ -108,6 +108,9 @@ class Screen(Widget): """Refresh the layout (can change size and positions of widgets).""" if not self.size: return + # This paint the entire screen, so replaces the batched dirty widgets + self._update_timer.pause() + self._dirty_widgets.clear() try: hidden, shown, resized = self._compositor.reflow(self, self.size) @@ -138,7 +141,6 @@ class Screen(Widget): self.app.on_exception(error) return self.app.refresh() - self._dirty_widgets.clear() async def handle_update(self, message: messages.Update) -> None: message.stop() diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index 5633f0d0e..9180923b5 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -227,9 +227,6 @@ class ScrollBar(Widget): style=style, ) - async def on_event(self, event) -> None: - await super().on_event(event) - async def on_enter(self, event: events.Enter) -> None: self.mouse_over = True diff --git a/src/textual/widget.py b/src/textual/widget.py index 3be4e77c1..9b8f1ed7c 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -272,6 +272,8 @@ class Widget(DOMNode): self.show_horizontal_scrollbar = show_horizontal self.show_vertical_scrollbar = show_vertical + self.horizontal_scrollbar.display = show_horizontal + self.vertical_scrollbar.display = show_vertical @property def scrollbars_enabled(self) -> tuple[bool, bool]: @@ -666,6 +668,7 @@ class Widget(DOMNode): def watch_mouse_over(self, value: bool) -> None: """Update from CSS if mouse over state changes.""" + return self.app.update_styles() def watch_has_focus(self, value: bool) -> None: diff --git a/tests/test_compositor_regions_to_spans.py b/tests/test_compositor_regions_to_spans.py index 7ddaecedb..31de88ad5 100644 --- a/tests/test_compositor_regions_to_spans.py +++ b/tests/test_compositor_regions_to_spans.py @@ -8,41 +8,47 @@ def test_regions_to_ranges_no_regions(): def test_regions_to_ranges_single_region(): regions = [Region(0, 0, 3, 2)] - assert list(Compositor._regions_to_spans(regions)) == [(0, 0, 2), (1, 0, 2)] + assert list(Compositor._regions_to_spans(regions)) == [ + (0, 0, 3), + (1, 0, 3), + ] def test_regions_to_ranges_partially_overlapping_regions(): regions = [Region(0, 0, 2, 2), Region(1, 1, 2, 2)] assert list(Compositor._regions_to_spans(regions)) == [ - (0, 0, 1), - (1, 0, 2), - (2, 1, 2), + (0, 0, 2), + (1, 0, 3), + (2, 1, 3), ] def test_regions_to_ranges_fully_overlapping_regions(): regions = [Region(1, 1, 3, 3), Region(2, 2, 1, 1), Region(0, 2, 3, 1)] assert list(Compositor._regions_to_spans(regions)) == [ - (1, 1, 3), - (2, 0, 3), - (3, 1, 3), + (1, 1, 4), + (2, 0, 4), + (3, 1, 4), ] def test_regions_to_ranges_disjoint_regions_different_lines(): regions = [Region(0, 0, 2, 1), Region(2, 2, 2, 1)] - assert list(Compositor._regions_to_spans(regions)) == [(0, 0, 1), (2, 2, 3)] + assert list(Compositor._regions_to_spans(regions)) == [(0, 0, 2), (2, 2, 4)] def test_regions_to_ranges_disjoint_regions_same_line(): regions = [Region(0, 0, 1, 2), Region(2, 0, 1, 1)] assert list(Compositor._regions_to_spans(regions)) == [ - (0, 0, 0), - (0, 2, 2), - (1, 0, 0), + (0, 0, 1), + (0, 2, 3), + (1, 0, 1), ] def test_regions_to_ranges_directly_adjacent_ranges_merged(): regions = [Region(0, 0, 1, 2), Region(1, 0, 1, 2)] - assert list(Compositor._regions_to_spans(regions)) == [(0, 0, 1), (1, 0, 1)] + assert list(Compositor._regions_to_spans(regions)) == [ + (0, 0, 2), + (1, 0, 2), + ]