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
|
from operator import attrgetter, itemgetter
|
||||||
import sys
|
import sys
|
||||||
from typing import cast, Iterator, Iterable, NamedTuple, TYPE_CHECKING
|
from typing import Callable, cast, Iterator, Iterable, NamedTuple, TYPE_CHECKING
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||||
@@ -100,12 +100,15 @@ class SpansUpdate:
|
|||||||
self, console: Console, options: ConsoleOptions
|
self, console: Console, options: ConsoleOptions
|
||||||
) -> RenderResult:
|
) -> RenderResult:
|
||||||
move_to = Control.move_to
|
move_to = Control.move_to
|
||||||
for line, offset, segments in self.spans:
|
new_line = Segment.line()
|
||||||
yield move_to(offset, line)
|
for last, (y, x, segments) in loop_last(self.spans):
|
||||||
|
yield move_to(x, y)
|
||||||
yield from segments
|
yield from segments
|
||||||
|
if not last:
|
||||||
|
yield new_line
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
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)
|
@rich.repr.auto(angular=True)
|
||||||
@@ -161,7 +164,7 @@ class Compositor:
|
|||||||
ranges.sort()
|
ranges.sort()
|
||||||
x1, x2 = ranges[0]
|
x1, x2 = ranges[0]
|
||||||
for next_x1, next_x2 in ranges[1:]:
|
for next_x1, next_x2 in ranges[1:]:
|
||||||
if next_x1 <= x2 + 1:
|
if next_x1 <= x2:
|
||||||
if next_x2 > x2:
|
if next_x2 > x2:
|
||||||
x2 = next_x2
|
x2 = next_x2
|
||||||
else:
|
else:
|
||||||
@@ -523,6 +526,7 @@ class Compositor:
|
|||||||
is_rendered_line = {y for y, _, _ in spans}.__contains__
|
is_rendered_line = {y for y, _, _ in spans}.__contains__
|
||||||
else:
|
else:
|
||||||
crop = screen_region
|
crop = screen_region
|
||||||
|
spans = []
|
||||||
is_rendered_line = lambda y: True
|
is_rendered_line = lambda y: True
|
||||||
|
|
||||||
_Segment = Segment
|
_Segment = Segment
|
||||||
@@ -566,34 +570,25 @@ class Compositor:
|
|||||||
if chops_line[cut] is None:
|
if chops_line[cut] is None:
|
||||||
chops_line[cut] = segments
|
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:
|
if regions:
|
||||||
|
crop_x, crop_y, crop_x2, crop_y2 = crop.corners
|
||||||
|
render_lines = self._assemble_chops(chops[crop_y:crop_y2])
|
||||||
render_spans = [
|
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)
|
return SpansUpdate(render_spans)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return SegmentLines(render_lines, new_lines=True)
|
render_lines = self._assemble_chops(chops)
|
||||||
|
return LayoutUpdate(render_lines, screen_region)
|
||||||
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)
|
|
||||||
|
|
||||||
def __rich_console__(
|
def __rich_console__(
|
||||||
self, console: Console, options: ConsoleOptions
|
self, console: Console, options: ConsoleOptions
|
||||||
) -> RenderResult:
|
) -> RenderResult:
|
||||||
yield self.render()
|
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.
|
"""Update a given widget in the composition.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -605,21 +600,13 @@ class Compositor:
|
|||||||
"""
|
"""
|
||||||
regions: list[Region] = []
|
regions: list[Region] = []
|
||||||
add_region = regions.append
|
add_region = regions.append
|
||||||
for widget in widgets:
|
for widget in self.regions.keys() & widgets:
|
||||||
if widget not in self.regions:
|
|
||||||
continue
|
|
||||||
region, clip = self.regions[widget]
|
region, clip = self.regions[widget]
|
||||||
if not region:
|
|
||||||
continue
|
|
||||||
update_region = region.intersection(clip)
|
update_region = region.intersection(clip)
|
||||||
if not update_region:
|
if update_region:
|
||||||
continue
|
add_region(update_region)
|
||||||
add_region(update_region)
|
print("UPDATE_WIDGET")
|
||||||
|
print(widgets)
|
||||||
# print(regions)
|
print(regions)
|
||||||
update = self.render(regions or None)
|
update = self.render(regions or None)
|
||||||
# print("UPDATE", update)
|
|
||||||
return 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
|
# Render widgets together
|
||||||
if self._dirty_widgets:
|
if self._dirty_widgets:
|
||||||
self.log(dirty=len(self._dirty_widgets))
|
self.log(dirty=self._dirty_widgets)
|
||||||
display_update = self._compositor.update_widgets(*self._dirty_widgets)
|
display_update = self._compositor.update_widgets(self._dirty_widgets)
|
||||||
if display_update is not None:
|
if display_update is not None:
|
||||||
self.app.display(display_update)
|
self.app.display(display_update)
|
||||||
self._dirty_widgets.clear()
|
self._dirty_widgets.clear()
|
||||||
@@ -108,6 +108,9 @@ class Screen(Widget):
|
|||||||
"""Refresh the layout (can change size and positions of widgets)."""
|
"""Refresh the layout (can change size and positions of widgets)."""
|
||||||
if not self.size:
|
if not self.size:
|
||||||
return
|
return
|
||||||
|
# This paint the entire screen, so replaces the batched dirty widgets
|
||||||
|
self._update_timer.pause()
|
||||||
|
self._dirty_widgets.clear()
|
||||||
try:
|
try:
|
||||||
hidden, shown, resized = self._compositor.reflow(self, self.size)
|
hidden, shown, resized = self._compositor.reflow(self, self.size)
|
||||||
|
|
||||||
@@ -138,7 +141,6 @@ class Screen(Widget):
|
|||||||
self.app.on_exception(error)
|
self.app.on_exception(error)
|
||||||
return
|
return
|
||||||
self.app.refresh()
|
self.app.refresh()
|
||||||
self._dirty_widgets.clear()
|
|
||||||
|
|
||||||
async def handle_update(self, message: messages.Update) -> None:
|
async def handle_update(self, message: messages.Update) -> None:
|
||||||
message.stop()
|
message.stop()
|
||||||
|
|||||||
@@ -227,9 +227,6 @@ class ScrollBar(Widget):
|
|||||||
style=style,
|
style=style,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_event(self, event) -> None:
|
|
||||||
await super().on_event(event)
|
|
||||||
|
|
||||||
async def on_enter(self, event: events.Enter) -> None:
|
async def on_enter(self, event: events.Enter) -> None:
|
||||||
self.mouse_over = True
|
self.mouse_over = True
|
||||||
|
|
||||||
|
|||||||
@@ -272,6 +272,8 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
self.show_horizontal_scrollbar = show_horizontal
|
self.show_horizontal_scrollbar = show_horizontal
|
||||||
self.show_vertical_scrollbar = show_vertical
|
self.show_vertical_scrollbar = show_vertical
|
||||||
|
self.horizontal_scrollbar.display = show_horizontal
|
||||||
|
self.vertical_scrollbar.display = show_vertical
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scrollbars_enabled(self) -> tuple[bool, bool]:
|
def scrollbars_enabled(self) -> tuple[bool, bool]:
|
||||||
@@ -666,6 +668,7 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
def watch_mouse_over(self, value: bool) -> None:
|
def watch_mouse_over(self, value: bool) -> None:
|
||||||
"""Update from CSS if mouse over state changes."""
|
"""Update from CSS if mouse over state changes."""
|
||||||
|
return
|
||||||
self.app.update_styles()
|
self.app.update_styles()
|
||||||
|
|
||||||
def watch_has_focus(self, value: bool) -> None:
|
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():
|
def test_regions_to_ranges_single_region():
|
||||||
regions = [Region(0, 0, 3, 2)]
|
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():
|
def test_regions_to_ranges_partially_overlapping_regions():
|
||||||
regions = [Region(0, 0, 2, 2), Region(1, 1, 2, 2)]
|
regions = [Region(0, 0, 2, 2), Region(1, 1, 2, 2)]
|
||||||
assert list(Compositor._regions_to_spans(regions)) == [
|
assert list(Compositor._regions_to_spans(regions)) == [
|
||||||
(0, 0, 1),
|
(0, 0, 2),
|
||||||
(1, 0, 2),
|
(1, 0, 3),
|
||||||
(2, 1, 2),
|
(2, 1, 3),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_regions_to_ranges_fully_overlapping_regions():
|
def test_regions_to_ranges_fully_overlapping_regions():
|
||||||
regions = [Region(1, 1, 3, 3), Region(2, 2, 1, 1), Region(0, 2, 3, 1)]
|
regions = [Region(1, 1, 3, 3), Region(2, 2, 1, 1), Region(0, 2, 3, 1)]
|
||||||
assert list(Compositor._regions_to_spans(regions)) == [
|
assert list(Compositor._regions_to_spans(regions)) == [
|
||||||
(1, 1, 3),
|
(1, 1, 4),
|
||||||
(2, 0, 3),
|
(2, 0, 4),
|
||||||
(3, 1, 3),
|
(3, 1, 4),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_regions_to_ranges_disjoint_regions_different_lines():
|
def test_regions_to_ranges_disjoint_regions_different_lines():
|
||||||
regions = [Region(0, 0, 2, 1), Region(2, 2, 2, 1)]
|
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():
|
def test_regions_to_ranges_disjoint_regions_same_line():
|
||||||
regions = [Region(0, 0, 1, 2), Region(2, 0, 1, 1)]
|
regions = [Region(0, 0, 1, 2), Region(2, 0, 1, 1)]
|
||||||
assert list(Compositor._regions_to_spans(regions)) == [
|
assert list(Compositor._regions_to_spans(regions)) == [
|
||||||
(0, 0, 0),
|
(0, 0, 1),
|
||||||
(0, 2, 2),
|
(0, 2, 3),
|
||||||
(1, 0, 0),
|
(1, 0, 1),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_regions_to_ranges_directly_adjacent_ranges_merged():
|
def test_regions_to_ranges_directly_adjacent_ranges_merged():
|
||||||
regions = [Region(0, 0, 1, 2), Region(1, 0, 1, 2)]
|
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