mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
span refinements
This commit is contained in:
@@ -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
|
||||
if regions:
|
||||
crop_x, crop_y, crop_x2, crop_y2 = crop.corners
|
||||
render_lines = self._assemble_chops(chops[crop_y:crop_y2])
|
||||
|
||||
if regions:
|
||||
|
||||
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
|
||||
if update_region:
|
||||
add_region(update_region)
|
||||
|
||||
# print(regions)
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user