compositor and cache optimizations

This commit is contained in:
Will McGugan
2022-06-08 11:46:53 +01:00
parent 7914ab9c83
commit 0ff8c7e47c
11 changed files with 107 additions and 43 deletions

21
examples/pride.py Normal file
View File

@@ -0,0 +1,21 @@
from textual.app import App
from textual.widgets import Static
class PrideApp(App):
"""Displays a pride flag."""
COLORS = ["red", "orange", "yellow", "green", "blue", "purple"]
def compose(self):
for color in self.COLORS:
stripe = Static()
stripe.styles.height = "1fr"
stripe.styles.background = color
yield stripe
app = PrideApp()
if __name__ == "__main__":
app.run()

32
poetry.lock generated
View File

@@ -345,7 +345,7 @@ mkdocs = ">=1.1"
[[package]] [[package]]
name = "mkdocs-material" name = "mkdocs-material"
version = "8.3.2" version = "8.3.3"
description = "Documentation that simply works" description = "Documentation that simply works"
category = "dev" category = "dev"
optional = false optional = false
@@ -533,7 +533,7 @@ python-versions = ">=3.6"
[[package]] [[package]]
name = "pymdown-extensions" name = "pymdown-extensions"
version = "9.4" version = "9.5"
description = "Extension pack for Python Markdown." description = "Extension pack for Python Markdown."
category = "dev" category = "dev"
optional = false optional = false
@@ -658,21 +658,16 @@ version = "12.4.4"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main" category = "main"
optional = false optional = false
python-versions = "^3.6.3" python-versions = ">=3.6.3,<4.0.0"
develop = true
[package.dependencies] [package.dependencies]
commonmark = "^0.9.0" commonmark = ">=0.9.0,<0.10.0"
pygments = "^2.6.0" pygments = ">=2.6.0,<3.0.0"
typing-extensions = {version = ">=4.0.0, <5.0", markers = "python_version < \"3.9\""} typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""}
[package.extras] [package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[package.source]
type = "directory"
url = "../rich"
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@@ -782,7 +777,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.7" python-versions = "^3.7"
content-hash = "4aeef009c7c1f6a34d0dd1c3c16647b53a339f5963dcbe31eef43bb9dac270ab" content-hash = "378a60041202d505cba26ea7084886fe1b01090a0035253b100593b559aed090"
[metadata.files] [metadata.files]
aiohttp = [ aiohttp = [
@@ -1127,8 +1122,8 @@ mkdocs-autorefs = [
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
] ]
mkdocs-material = [ mkdocs-material = [
{file = "mkdocs-material-8.3.2.tar.gz", hash = "sha256:d203ba166063ccd0ff83893a54f46d98e2fb3961babd7c0176cb3f95ac62df20"}, {file = "mkdocs-material-8.3.3.tar.gz", hash = "sha256:3dd30af894f6d5da3d8a2f8ffc04c90c4d0f1be013e654ec45f608373c131542"},
{file = "mkdocs_material-8.3.2-py2.py3-none-any.whl", hash = "sha256:57d910ccb6d98d0d1281cbb34a9c8ae6d820b2f5caad1d1246d8ed49eec10a50"}, {file = "mkdocs_material-8.3.3-py2.py3-none-any.whl", hash = "sha256:4f9564af58f9c96f25c263cb705a40a82c833cb10c2626d6db6ddadedaa5b6c3"},
] ]
mkdocs-material-extensions = [ mkdocs-material-extensions = [
{file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"},
@@ -1319,8 +1314,8 @@ pygments = [
{file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
] ]
pymdown-extensions = [ pymdown-extensions = [
{file = "pymdown_extensions-9.4-py3-none-any.whl", hash = "sha256:5b7432456bf555ce2b0ab3c2439401084cda8110f24f6b3ecef952b8313dfa1b"}, {file = "pymdown_extensions-9.5-py3-none-any.whl", hash = "sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4"},
{file = "pymdown_extensions-9.4.tar.gz", hash = "sha256:1baa22a60550f731630474cad28feb0405c8101f1a7ddc3ec0ed86ee510bcc43"}, {file = "pymdown_extensions-9.5.tar.gz", hash = "sha256:3ef2d998c0d5fa7eb09291926d90d69391283561cf6306f85cd588a5eb5befa0"},
] ]
pyparsing = [ pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
@@ -1386,7 +1381,10 @@ pyyaml-env-tag = [
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
] ]
rich = [] rich = [
{file = "rich-12.4.4-py3-none-any.whl", hash = "sha256:d2bbd99c320a2532ac71ff6a3164867884357da3e3301f0240090c5d2fdac7ec"},
{file = "rich-12.4.4.tar.gz", hash = "sha256:4c586de507202505346f3e32d1363eb9ed6932f0c2f63184dea88983ff4971e2"},
]
six = [ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},

View File

@@ -22,8 +22,8 @@ textual = "textual.cli.cli:run"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.7" python = "^3.7"
#rich = "^12.4.3" rich = "^12.4.3"
rich = {path="../rich", develop=true} #rich = {path="../rich", develop=true}
click = "8.1.2" click = "8.1.2"
importlib-metadata = "^4.11.3" importlib-metadata = "^4.11.3"
typing-extensions = { version = "^4.0.0", python = "<3.8" } typing-extensions = { version = "^4.0.0", python = "<3.8" }

View File

@@ -76,8 +76,8 @@ App > Screen {
Tweet { Tweet {
height: 12; height:12;
width: 80; width: 100%;
margin: 1 3; margin: 1 3;
background: $panel; background: $panel;
@@ -94,18 +94,18 @@ Tweet {
.scrollable { .scrollable {
width: 80;
overflow-y: scroll; overflow-y: scroll;
max-width:80; margin: 1 2;
height: 20; height: 20;
align-horizontal: center; align-horizontal: center;
layout: vertical; layout: vertical;
} }
.code { .code {
height: 34; height: 34;
width: 100%;
} }
@@ -189,7 +189,7 @@ OptionItem:hover {
} }
Error { Error {
width: 80; width: 100%;
height:3; height:3;
background: $error; background: $error;
color: $text-error; color: $text-error;
@@ -202,7 +202,7 @@ Error {
} }
Warning { Warning {
width: 80; width: 100%;
height:3; height:3;
background: $warning; background: $warning;
color: $text-warning-fade-1; color: $text-warning-fade-1;
@@ -214,7 +214,7 @@ Warning {
} }
Success { Success {
width: 80; width: 100%;
height:3; height:3;
box-sizing: border-box; box-sizing: border-box;
background: $success-lighten-3; background: $success-lighten-3;

View File

@@ -117,6 +117,11 @@ class BasicApp(App, css_path="basic.css"):
Warning(), Warning(),
Tweet(TweetBody(), classes="scroll-horizontal"), Tweet(TweetBody(), classes="scroll-horizontal"),
Success(), Success(),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
), ),
footer=Widget(), footer=Widget(),
sidebar=Widget( sidebar=Widget(
@@ -154,3 +159,15 @@ app = BasicApp()
if __name__ == "__main__": if __name__ == "__main__":
app.run() app.run()
from textual.geometry import Region
from textual.color import Color
print(Region.intersection.cache_info())
print(Region.overlaps.cache_info())
print(Region.union.cache_info())
print(Region.split_vertical.cache_info())
print(Region.__contains__.cache_info())
from textual.css.scalar import Scalar
print(Scalar.resolve_dimension.cache_info())

View File

@@ -68,7 +68,7 @@ class MapGeometry(NamedTuple):
CompositorMap: TypeAlias = "dict[Widget, MapGeometry]" CompositorMap: TypeAlias = "dict[Widget, MapGeometry]"
@rich.repr.auto @rich.repr.auto(angular=True)
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."""
@@ -89,11 +89,7 @@ class LayoutUpdate:
yield new_line yield new_line
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
x, y, width, height = self.region yield self.region
yield "x", x
yield "y", y
yield "width", width
yield "height", height
@rich.repr.auto(angular=True) @rich.repr.auto(angular=True)
@@ -558,7 +554,10 @@ class Compositor:
delta_y = new_y - region.y delta_y = new_y - region.y
crop_x = delta_x + new_width crop_x = delta_x + new_width
lines = widget.get_render_lines(delta_y, delta_y + new_height) lines = widget.get_render_lines(delta_y, delta_y + new_height)
lines = [line_crop(line, delta_x, crop_x) for line in lines] if (delta_x, crop_x) != (0, region.width):
lines = [
line_crop(line, delta_x, crop_x, region.width) for line in lines
]
yield region, clip, lines yield region, clip, lines
@classmethod @classmethod
@@ -600,7 +599,7 @@ class Compositor:
is_rendered_line = lambda y: True is_rendered_line = lambda y: True
elif update_regions: elif update_regions:
# Create a crop regions that surrounds all updates # Create a crop regions that surrounds all updates
crop = Region.from_union(list(update_regions)).intersection(screen_region) crop = Region.from_union(update_regions).intersection(screen_region)
spans = list(self._regions_to_spans(update_regions)) spans = list(self._regions_to_spans(update_regions))
is_rendered_line = {y for y, _, _ in spans}.__contains__ is_rendered_line = {y for y, _, _ in spans}.__contains__
else: else:

View File

@@ -7,14 +7,16 @@ from __future__ import annotations
from rich.segment import Segment from rich.segment import Segment
def line_crop(segments: list[Segment], start: int, end: int) -> list[Segment]: def line_crop(
segments: list[Segment], start: int, end: int, total: int
) -> list[Segment]:
"""Crops a list of segments between two cell offsets. """Crops a list of segments between two cell offsets.
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
end (int): End offset end (int): End offset
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
""" """
@@ -35,6 +37,13 @@ def line_crop(segments: list[Segment], start: int, end: int) -> list[Segment]:
else: else:
return [] return []
if end >= total:
# 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 output_segments
pos = start pos = start
while segment is not None: while segment is not None:
end_pos = pos + segment.cell_length end_pos = pos + segment.cell_length

View File

@@ -231,7 +231,6 @@ class Color(NamedTuple):
r, g, b, _ = self r, g, b, _ = self
return Color(r, g, b, alpha) return Color(r, g, b, alpha)
@lru_cache(maxsize=2048)
def blend(self, destination: Color, factor: float) -> Color: def blend(self, destination: Color, factor: float) -> Color:
"""Generate a new color between two colors. """Generate a new color between two colors.
@@ -418,6 +417,7 @@ class ColorPair(NamedTuple):
) )
@lru_cache(maxsize=1024)
def rgb_to_lab(rgb: Color) -> Lab: def rgb_to_lab(rgb: Color) -> Lab:
"""Convert an RGB color to the CIE-L*ab format. """Convert an RGB color to the CIE-L*ab format.
@@ -444,6 +444,7 @@ def rgb_to_lab(rgb: Color) -> Lab:
return Lab(116 * y - 16, 500 * (x - y), 200 * (y - z)) return Lab(116 * y - 16, 500 * (x - y), 200 * (y - z))
@lru_cache(maxsize=1024)
def lab_to_rgb(lab: Lab) -> Color: def lab_to_rgb(lab: Lab) -> Color:
"""Convert a CIE-L*ab color to RGB. """Convert a CIE-L*ab color to RGB.

View File

@@ -223,6 +223,14 @@ class Scalar(NamedTuple):
@classmethod @classmethod
def from_number(cls, value: float) -> Scalar: def from_number(cls, value: float) -> Scalar:
"""Create a scalar with cells unit.
Args:
value (float): A number of cells.
Returns:
Scalar: New Scalar.
"""
return cls(float(value), Unit.CELLS, Unit.WIDTH) return cls(float(value), Unit.CELLS, Unit.WIDTH)
@classmethod @classmethod

View File

@@ -6,9 +6,13 @@ Functions and classes to manage terminal geometry (anything involving coordinate
from __future__ import annotations from __future__ import annotations
from typing import Any, cast, NamedTuple, Sequence, Tuple, Union, TypeVar from functools import lru_cache
SpacingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]] from typing import Any, cast, Collection, NamedTuple, Tuple, TypeAlias, Union, TypeVar
SpacingDimensions: TypeAlias = Union[
int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]
]
T = TypeVar("T", int, float) T = TypeVar("T", int, float)
@@ -182,7 +186,7 @@ class Region(NamedTuple):
height: int = 0 height: int = 0
@classmethod @classmethod
def from_union(cls, regions: Sequence[Region]) -> Region: def from_union(cls, regions: Collection[Region]) -> Region:
"""Create a Region from the union of other regions. """Create a Region from the union of other regions.
Args: Args:
@@ -356,6 +360,7 @@ class Region(NamedTuple):
height + expand_height * 2, height + expand_height * 2,
) )
@lru_cache(maxsize=1024)
def overlaps(self, other: Region) -> bool: def overlaps(self, other: Region) -> bool:
"""Check if another region overlaps this region. """Check if another region overlaps this region.
@@ -433,6 +438,7 @@ class Region(NamedTuple):
self_x, self_y, width, height = self self_x, self_y, width, height = self
return Region(self_x + x, self_y + y, width, height) return Region(self_x + x, self_y + y, width, height)
@lru_cache(maxsize=4096)
def __contains__(self, other: Any) -> bool: def __contains__(self, other: Any) -> bool:
"""Check if a point is in this region.""" """Check if a point is in this region."""
if isinstance(other, Region): if isinstance(other, Region):
@@ -483,6 +489,7 @@ class Region(NamedTuple):
height=max(0, height - top - bottom), height=max(0, height - top - bottom),
) )
@lru_cache(maxsize=4096)
def intersection(self, region: Region) -> Region: def intersection(self, region: Region) -> Region:
"""Get the overlapping portion of the two regions. """Get the overlapping portion of the two regions.
@@ -507,6 +514,7 @@ class Region(NamedTuple):
return Region(rx1, ry1, rx2 - rx1, ry2 - ry1) return Region(rx1, ry1, rx2 - rx1, ry2 - ry1)
@lru_cache(maxsize=4096)
def union(self, region: Region) -> Region: def union(self, region: Region) -> Region:
"""Get a new region that contains both regions. """Get a new region that contains both regions.
@@ -524,6 +532,7 @@ class Region(NamedTuple):
) )
return union_region return union_region
@lru_cache(maxsize=1024)
def split(self, cut_x: int, cut_y: int) -> tuple[Region, Region, Region, Region]: def split(self, cut_x: int, cut_y: int) -> tuple[Region, Region, Region, Region]:
"""Split a region in to 4 from given x and y offsets (cuts). """Split a region in to 4 from given x and y offsets (cuts).
@@ -563,6 +572,7 @@ class Region(NamedTuple):
_Region(x + cut_x, y + cut_y, width - cut_x, height - cut_y), _Region(x + cut_x, y + cut_y, width - cut_x, height - cut_y),
) )
@lru_cache(maxsize=1024)
def split_vertical(self, cut: int) -> tuple[Region, Region]: def split_vertical(self, cut: int) -> tuple[Region, Region]:
"""Split a region in to two, from a given x offset. """Split a region in to two, from a given x offset.
@@ -591,6 +601,7 @@ class Region(NamedTuple):
Region(x + cut, y, width - cut, height), Region(x + cut, y, width - cut, height),
) )
@lru_cache(maxsize=1024)
def split_horizontal(self, cut: int) -> tuple[Region, Region]: def split_horizontal(self, cut: int) -> tuple[Region, Region]:
"""Split a region in to two, from a given x offset. """Split a region in to two, from a given x offset.

View File

@@ -31,7 +31,7 @@ class VerticalLayout(Layout):
box_models = [ box_models = [
widget.get_box_model(size, parent_size, fraction_unit) widget.get_box_model(size, parent_size, fraction_unit)
for widget in cast("list[Widget]", parent.children) for widget in parent.children
] ]
margins = [ margins = [