diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a25a18fd..c04c4250c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [7.0.3] - 2026-01-10 + +### Fixed + +- Fixed performance issue with large scrollable containers https://github.com/Textualize/textual/pull/6317 + ## [7.0.2] - 2026-01-09 ### Fixed @@ -3284,6 +3290,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[7.0.3]: https://github.com/Textualize/textual/compare/v7.0.2...v7.0.3 [7.0.2]: https://github.com/Textualize/textual/compare/v7.0.1...v7.0.2 [7.0.1]: https://github.com/Textualize/textual/compare/v7.0.0...v7.0.1 [7.0.0]: https://github.com/Textualize/textual/compare/v6.11.0...v7.0.0 diff --git a/pyproject.toml b/pyproject.toml index 17206a8953..dc28faf4ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "7.0.2" +version = "7.0.3" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/" diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index 7d97cda9f0..34387071f4 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -723,9 +723,15 @@ def update_nodes(self, nodes: Iterable[DOMNode], animate: bool = False) -> None: for node in nodes: apply(node, animate=animate, cache=cache) if isinstance(node, Widget) and node.is_scrollable: - if node.show_vertical_scrollbar: + show_vertical_scrollbar = ( + node.show_vertical_scrollbar and node.scrollbar_size_vertical + ) + show_horizontal_scrollbar = ( + node.show_horizontal_scrollbar and node.scrollbar_size_horizontal + ) + if show_vertical_scrollbar: apply(node.vertical_scrollbar, cache=cache) - if node.show_horizontal_scrollbar: + if show_horizontal_scrollbar: apply(node.horizontal_scrollbar, cache=cache) - if node.show_horizontal_scrollbar and node.show_vertical_scrollbar: + if show_horizontal_scrollbar and show_vertical_scrollbar: apply(node.scrollbar_corner, cache=cache) diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index ef039148c5..41c90d59cb 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -258,7 +258,7 @@ def __init__( self.thickness = thickness self.grabbed_position: float = 0 super().__init__(name=name) - self.auto_links = False + self.set_reactive(ScrollBar.auto_links, False) window_virtual_size: Reactive[int] = Reactive(100) window_size: Reactive[int] = Reactive(0) @@ -274,6 +274,10 @@ def __rich_repr__(self) -> rich.repr.Result: if self.thickness > 1: yield "thickness", self.thickness + def validate_position(self, position: float) -> float: + """Position has a granulatory of 1/8 of a cell.""" + return int(position * 8) / 8 + def render(self) -> RenderableType: assert self.parent is not None styles = self.parent.styles diff --git a/src/textual/visual.py b/src/textual/visual.py index 0cb4d2de72..4f9f6ea5d7 100644 --- a/src/textual/visual.py +++ b/src/textual/visual.py @@ -235,7 +235,10 @@ def to_strips( selection_style, ), ) - strips = [strip._apply_link_style(widget.link_style) for strip in strips] + if widget.auto_links and not widget.is_container: + # TODO: This is suprisingly expensive (why?) + link_style = widget.link_style + strips = [strip._apply_link_style(link_style) for strip in strips] if height is None: height = len(strips) diff --git a/src/textual/widget.py b/src/textual/widget.py index 38cfb6ae5f..94b254b520 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -349,7 +349,7 @@ class Widget(DOMNode): loading: Reactive[bool] = Reactive(False) """If set to `True` this widget will temporarily be replaced with a loading indicator.""" - virtual_size: Reactive[Size] = Reactive(Size(0, 0), layout=True) + virtual_size = Reactive(Size(0, 0), layout=True) """The virtual (scrollable) [size][textual.geometry.Size] of the widget.""" has_focus: Reactive[bool] = Reactive(False, repaint=False) @@ -1903,12 +1903,14 @@ def watch_hover_style( self.highlight_link_id = hover_style.link_id def watch_scroll_x(self, old_value: float, new_value: float) -> None: - self.horizontal_scrollbar.position = new_value + if self.show_horizontal_scrollbar: + self.horizontal_scrollbar.position = new_value if round(old_value) != round(new_value): self._refresh_scroll() def watch_scroll_y(self, old_value: float, new_value: float) -> None: - self.vertical_scrollbar.position = new_value + if self.show_vertical_scrollbar: + self.vertical_scrollbar.position = new_value if self._anchored and self._anchor_released: self._check_anchor() if round(old_value) != round(new_value): @@ -2644,8 +2646,6 @@ def _set_dirty(self, *regions: Region) -> None: self._dirty_regions.clear() self._repaint_regions.clear() self._styles_cache.clear() - self._styles_cache.set_dirty(self.size.region) - outer_size = self.outer_size self._dirty_regions.add(outer_size.region) if outer_size: @@ -4092,13 +4092,13 @@ def _scroll_update(self, virtual_size: Size) -> None: self._refresh_scrollbars() width, height = self.container_size - if self.show_vertical_scrollbar: + if self.show_vertical_scrollbar and self.styles.scrollbar_size_vertical: self.vertical_scrollbar.window_virtual_size = virtual_size.height self.vertical_scrollbar.window_size = ( height - self.scrollbar_size_horizontal ) self.vertical_scrollbar.refresh() - if self.show_horizontal_scrollbar: + if self.show_horizontal_scrollbar and self.styles.scrollbar_size_horizontal: self.horizontal_scrollbar.window_virtual_size = virtual_size.width self.horizontal_scrollbar.window_size = width - self.scrollbar_size_vertical self.horizontal_scrollbar.refresh() diff --git a/src/textual/widgets/_footer.py b/src/textual/widgets/_footer.py index 14aa56cb81..4bbbfd88d6 100644 --- a/src/textual/widgets/_footer.py +++ b/src/textual/widgets/_footer.py @@ -96,7 +96,7 @@ def __init__( if disabled: classes += " -disabled" super().__init__(classes=classes) - self.shrink = False + self.set_reactive(Widget.shrink, False) if tooltip: self.tooltip = tooltip @@ -260,7 +260,8 @@ def __init__( disabled=disabled, ) self.set_reactive(Footer.show_command_palette, show_command_palette) - self.compact = compact + self.set_reactive(Footer.compact, compact) + self.set_class(compact, "-compact", update=False) def compose(self) -> ComposeResult: if not self._bindings_ready: diff --git a/src/textual/widgets/_static.py b/src/textual/widgets/_static.py index cd2f443ab2..389171400e 100644 --- a/src/textual/widgets/_static.py +++ b/src/textual/widgets/_static.py @@ -44,8 +44,8 @@ def __init__( super().__init__( name=name, id=id, classes=classes, disabled=disabled, markup=markup ) - self.expand = expand - self.shrink = shrink + self.set_reactive(Widget.expand, expand) + self.set_reactive(Widget.shrink, shrink) self.__content = content self.__visual: Visual | None = None