diff --git a/sandbox/will/basic.py b/sandbox/will/basic.py index 3e5f02732..a1a9fc2bb 100644 --- a/sandbox/will/basic.py +++ b/sandbox/will/basic.py @@ -115,7 +115,7 @@ class BasicApp(App, css_path="basic.css"): Static(Syntax(CODE, "python"), classes="code"), classes="scrollable", ), - table, + # table, Error(), Tweet(TweetBody(), classes="scrollbar-size-custom"), Warning(), diff --git a/src/textual/_segment_tools.py b/src/textual/_segment_tools.py index 22f1cfee6..a9bccb699 100644 --- a/src/textual/_segment_tools.py +++ b/src/textual/_segment_tools.py @@ -35,7 +35,7 @@ def line_crop( for segment in iter_segments: end_pos = pos + _cell_len(segment.text) if end_pos > start: - segment = segment.split_cells(start - pos)[-1] + segment = segment.split_cells(start - pos)[1] break pos = end_pos else: @@ -86,5 +86,5 @@ def line_trim(segments: list[Segment], start: bool, end: bool) -> list[Segment]: if last_segment.text: segments[-1] = last_segment else: - segments.pop(-1) + segments.pop() return segments diff --git a/src/textual/_styles_render.py b/src/textual/_styles_render.py index 1803d419b..bdc437b76 100644 --- a/src/textual/_styles_render.py +++ b/src/textual/_styles_render.py @@ -11,11 +11,11 @@ from .css.types import EdgeType from ._segment_tools import line_crop from ._types import Lines from .geometry import Region, Size -from .widget import Widget if TYPE_CHECKING: from .css.styles import RenderStyles + from .widget import Widget NORMALIZE_BORDER: dict[EdgeType, EdgeType] = {"none": "", "hidden": ""} @@ -27,8 +27,14 @@ class StylesRenderer: self._cache: dict[int, list[Segment]] = {} self._dirty_lines: set[int] = set() - def invalidate(self, region: Region) -> None: - self._dirty_lines.update(region.y_range) + def set_dirty(self, *regions: Region) -> None: + if regions: + for region in regions: + self._dirty_lines.update(region.y_range) + else: + self._dirty_lines.clear() + for y in self._widget.size.lines: + self._dirty_lines.add(y) def render(self, region: Region) -> Lines: @@ -50,12 +56,14 @@ class StylesRenderer: width, height = size lines: Lines = [] add_line = lines.append + simplify = Segment.simplify is_dirty = self._dirty_lines.__contains__ render_line = self.render_line for y in region.y_range: if is_dirty(y) or y not in self._cache: line = render_line(styles, y, size, base_background, background) + line = list(simplify(line)) self._cache[y] = line else: line = self._cache[y] @@ -64,13 +72,13 @@ class StylesRenderer: if region.x_extents != (0, width): _line_crop = line_crop - x1, x2 = region.x_range + x1, x2 = region.x_extents lines = [_line_crop(line, x1, x2, width) for line in lines] return lines - def render_content_line(self, y: int, width: int) -> list[Segment]: - return [Segment((str(y) * width)[:width])] + def render_content_line(self, y: int) -> list[Segment]: + return self._widget.render_line(y) def render_line( self, @@ -149,11 +157,9 @@ class StylesRenderer: return [Segment(" " * width, background_style)] # Apply background style - line = list( - Segment.apply_style( - self.render_content_line(y - gutter.top, content_width), inner_style - ) - ) + line = self.render_content_line(y - gutter.top) + if inner_style: + line = list(Segment.apply_style(line, inner_style)) # Add padding if pad_left and pad_right: @@ -184,7 +190,7 @@ class StylesRenderer: from_color(border_left_color.rich_color), ) _, (_, _, right), _ = get_box( - border_left, + border_right, inner_style, outer_style, from_color(border_right_color.rich_color), @@ -195,7 +201,7 @@ class StylesRenderer: elif border_left: return [left, *line] - return [right, *line] + return [*line, right] if __name__ == "__main__": diff --git a/src/textual/geometry.py b/src/textual/geometry.py index a3d611a15..79ca4dd27 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -139,6 +139,10 @@ class Size(NamedTuple): width, height = self return Region(0, 0, width, height) + @property + def lines(self) -> list[int]: + return list(range(self.height)) + def __add__(self, other: object) -> Size: if isinstance(other, tuple): width, height = self diff --git a/src/textual/widget.py b/src/textual/widget.py index fefe087c8..e9d44ffaf 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -17,6 +17,7 @@ from rich.align import Align from rich.console import Console, RenderableType from rich.measure import Measurement from rich.padding import Padding +from rich.segment import Segment from rich.style import Style from . import errors @@ -25,6 +26,7 @@ from ._animator import BoundAnimator from ._border import Border from .box_model import BoxModel, get_box_model from ._context import active_app +from ._styles_render import StylesRenderer from ._types import Lines from .dom import DOMNode from ._layout import ArrangeResult @@ -118,6 +120,8 @@ class Widget(DOMNode): self._arrangement: ArrangeResult | None = None self._arrangement_cache_key: tuple[int, Size] = (-1, Size()) + self._styles_renderer = StylesRenderer(self) + super().__init__(name=name, id=id, classes=classes) self.add_children(*children) @@ -833,9 +837,18 @@ class Widget(DOMNode): """ renderable = self.render() - renderable = self._style_renderable(renderable) + styles = self.styles + content_align = (styles.content_align_horizontal, styles.content_align_vertical) + if content_align != ("left", "top"): + horizontal, vertical = content_align + renderable = Align(renderable, horizontal, vertical=vertical) + return renderable + @property + def content_size(self) -> Size: + return self._size - self.styles.gutter.totals + @property def size(self) -> Size: return self._size @@ -955,13 +968,13 @@ class Widget(DOMNode): def _render_lines(self) -> None: """Render all lines.""" - width, height = self.size + width, height = self.content_size renderable = self.render_styled() options = self.console.options.update_dimensions(width, height).update( highlight=False ) - lines = self.console.render_lines(renderable, options) - self._render_cache = RenderCache(self.size, lines) + lines = self.console.render_lines(renderable, options, style=self.rich_style) + self._render_cache = RenderCache(self.content_size, lines) self._dirty_regions.clear() def _crop_lines(self, lines: Lines, x1, x2) -> Lines: @@ -971,6 +984,10 @@ class Widget(DOMNode): lines = [_line_crop(line, x1, x2, width) for line in lines] return lines + def render_line(self, y) -> list[Segment]: + line = self._render_cache.lines[y] + return line + def render_lines(self, crop: Region) -> Lines: """Render the widget in to lines. @@ -981,8 +998,12 @@ class Widget(DOMNode): Lines: A list of list of segments """ if self._dirty_regions: + self._styles_renderer.set_dirty(*self._dirty_regions) self._render_lines() + lines = self._styles_renderer.render(crop) + return lines + x1, y1, x2, y2 = crop.corners lines = self._render_cache.lines[y1:y2] lines = self._crop_lines(lines, x1, x2)