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]]
name = "mkdocs-material"
version = "8.3.2"
version = "8.3.3"
description = "Documentation that simply works"
category = "dev"
optional = false
@@ -533,7 +533,7 @@ python-versions = ">=3.6"
[[package]]
name = "pymdown-extensions"
version = "9.4"
version = "9.5"
description = "Extension pack for Python Markdown."
category = "dev"
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"
category = "main"
optional = false
python-versions = "^3.6.3"
develop = true
python-versions = ">=3.6.3,<4.0.0"
[package.dependencies]
commonmark = "^0.9.0"
pygments = "^2.6.0"
typing-extensions = {version = ">=4.0.0, <5.0", markers = "python_version < \"3.9\""}
commonmark = ">=0.9.0,<0.10.0"
pygments = ">=2.6.0,<3.0.0"
typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""}
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[package.source]
type = "directory"
url = "../rich"
[[package]]
name = "six"
version = "1.16.0"
@@ -782,7 +777,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "4aeef009c7c1f6a34d0dd1c3c16647b53a339f5963dcbe31eef43bb9dac270ab"
content-hash = "378a60041202d505cba26ea7084886fe1b01090a0035253b100593b559aed090"
[metadata.files]
aiohttp = [
@@ -1127,8 +1122,8 @@ mkdocs-autorefs = [
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
]
mkdocs-material = [
{file = "mkdocs-material-8.3.2.tar.gz", hash = "sha256:d203ba166063ccd0ff83893a54f46d98e2fb3961babd7c0176cb3f95ac62df20"},
{file = "mkdocs_material-8.3.2-py2.py3-none-any.whl", hash = "sha256:57d910ccb6d98d0d1281cbb34a9c8ae6d820b2f5caad1d1246d8ed49eec10a50"},
{file = "mkdocs-material-8.3.3.tar.gz", hash = "sha256:3dd30af894f6d5da3d8a2f8ffc04c90c4d0f1be013e654ec45f608373c131542"},
{file = "mkdocs_material-8.3.3-py2.py3-none-any.whl", hash = "sha256:4f9564af58f9c96f25c263cb705a40a82c833cb10c2626d6db6ddadedaa5b6c3"},
]
mkdocs-material-extensions = [
{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"},
]
pymdown-extensions = [
{file = "pymdown_extensions-9.4-py3-none-any.whl", hash = "sha256:5b7432456bf555ce2b0ab3c2439401084cda8110f24f6b3ecef952b8313dfa1b"},
{file = "pymdown_extensions-9.4.tar.gz", hash = "sha256:1baa22a60550f731630474cad28feb0405c8101f1a7ddc3ec0ed86ee510bcc43"},
{file = "pymdown_extensions-9.5-py3-none-any.whl", hash = "sha256:ec141c0f4983755349f0c8710416348d1a13753976c028186ed14f190c8061c4"},
{file = "pymdown_extensions-9.5.tar.gz", hash = "sha256:3ef2d998c0d5fa7eb09291926d90d69391283561cf6306f85cd588a5eb5befa0"},
]
pyparsing = [
{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.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 = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},

View File

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

View File

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

View File

@@ -117,6 +117,11 @@ class BasicApp(App, css_path="basic.css"):
Warning(),
Tweet(TweetBody(), classes="scroll-horizontal"),
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(),
sidebar=Widget(
@@ -154,3 +159,15 @@ app = BasicApp()
if __name__ == "__main__":
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]"
@rich.repr.auto
@rich.repr.auto(angular=True)
class LayoutUpdate:
"""A renderable containing the result of a render for a given region."""
@@ -89,11 +89,7 @@ class LayoutUpdate:
yield new_line
def __rich_repr__(self) -> rich.repr.Result:
x, y, width, height = self.region
yield "x", x
yield "y", y
yield "width", width
yield "height", height
yield self.region
@rich.repr.auto(angular=True)
@@ -558,7 +554,10 @@ class Compositor:
delta_y = new_y - region.y
crop_x = delta_x + new_width
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
@classmethod
@@ -600,7 +599,7 @@ class Compositor:
is_rendered_line = lambda y: True
elif update_regions:
# 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))
is_rendered_line = {y for y, _, _ in spans}.__contains__
else:

View File

@@ -7,14 +7,16 @@ from __future__ import annotations
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.
Args:
segments (list[Segment]): A list of Segments for a line.
start (int): Start offset
end (int): End offset
total (int): Total cell length of segments.
Returns:
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:
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
while segment is not None:
end_pos = pos + segment.cell_length

View File

@@ -231,7 +231,6 @@ class Color(NamedTuple):
r, g, b, _ = self
return Color(r, g, b, alpha)
@lru_cache(maxsize=2048)
def blend(self, destination: Color, factor: float) -> Color:
"""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:
"""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))
@lru_cache(maxsize=1024)
def lab_to_rgb(lab: Lab) -> Color:
"""Convert a CIE-L*ab color to RGB.

View File

@@ -223,6 +223,14 @@ class Scalar(NamedTuple):
@classmethod
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)
@classmethod

View File

@@ -6,9 +6,13 @@ Functions and classes to manage terminal geometry (anything involving coordinate
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)
@@ -182,7 +186,7 @@ class Region(NamedTuple):
height: int = 0
@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.
Args:
@@ -356,6 +360,7 @@ class Region(NamedTuple):
height + expand_height * 2,
)
@lru_cache(maxsize=1024)
def overlaps(self, other: Region) -> bool:
"""Check if another region overlaps this region.
@@ -433,6 +438,7 @@ class Region(NamedTuple):
self_x, self_y, width, height = self
return Region(self_x + x, self_y + y, width, height)
@lru_cache(maxsize=4096)
def __contains__(self, other: Any) -> bool:
"""Check if a point is in this region."""
if isinstance(other, Region):
@@ -483,6 +489,7 @@ class Region(NamedTuple):
height=max(0, height - top - bottom),
)
@lru_cache(maxsize=4096)
def intersection(self, region: Region) -> Region:
"""Get the overlapping portion of the two regions.
@@ -507,6 +514,7 @@ class Region(NamedTuple):
return Region(rx1, ry1, rx2 - rx1, ry2 - ry1)
@lru_cache(maxsize=4096)
def union(self, region: Region) -> Region:
"""Get a new region that contains both regions.
@@ -524,6 +532,7 @@ class Region(NamedTuple):
)
return union_region
@lru_cache(maxsize=1024)
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).
@@ -563,6 +572,7 @@ class Region(NamedTuple):
_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]:
"""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),
)
@lru_cache(maxsize=1024)
def split_horizontal(self, cut: int) -> tuple[Region, Region]:
"""Split a region in to two, from a given x offset.

View File

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