diff --git a/CHANGELOG.md b/CHANGELOG.md index 74703fded5..8b9b74fe17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed stuck screen https://github.com/Textualize/textual/issues/1632 - Fixed relative units in `grid-rows` and `grid-columns` being computed with respect to the wrong dimension https://github.com/Textualize/textual/issues/1406 +- Programmatically setting `overflow_x`/`overflow_y` refreshes the layout correctly https://github.com/Textualize/textual/issues/1616 - Fixed double-paste into `Input` https://github.com/Textualize/textual/issues/1657 ## [0.10.1] - 2023-01-20 diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 6ec181b5cc..ca17095bc0 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -704,6 +704,9 @@ def __get__(self, obj: StylesBase, objtype: type[StylesBase] | None = None) -> s """ return obj.get_rule(self.name, self._default) + def _before_refresh(self, obj: StylesBase, value: str | None) -> None: + """Do any housekeeping before asking for a layout refresh after a value change.""" + def __set__(self, obj: StylesBase, value: str | None = None): """Set the string property and ensure it is in the set of allowed values. @@ -717,6 +720,7 @@ def __set__(self, obj: StylesBase, value: str | None = None): _rich_traceback_omit = True if value is None: if obj.clear_rule(self.name): + self._before_refresh(obj, value) obj.refresh(layout=self._layout) else: if value not in self._valid_values: @@ -729,9 +733,20 @@ def __set__(self, obj: StylesBase, value: str | None = None): ), ) if obj.set_rule(self.name, value): + self._before_refresh(obj, value) obj.refresh(layout=self._layout) +class OverflowProperty(StringEnumProperty): + """Descriptor for overflow styles that forces widgets to refresh scrollbars.""" + + def _before_refresh(self, obj: StylesBase, value: str | None) -> None: + from ..widget import Widget # Avoid circular import + + if isinstance(obj.node, Widget): + obj.node._refresh_scrollbars() + + class NameProperty: """Descriptor for getting and setting name properties.""" diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 1100b7320a..cd4ff3af3d 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -26,6 +26,7 @@ NameListProperty, NameProperty, OffsetProperty, + OverflowProperty, ScalarListProperty, ScalarProperty, SpacingProperty, @@ -246,8 +247,8 @@ class StylesBase(ABC): dock = DockProperty() - overflow_x = StringEnumProperty(VALID_OVERFLOW, "hidden") - overflow_y = StringEnumProperty(VALID_OVERFLOW, "hidden") + overflow_x = OverflowProperty(VALID_OVERFLOW, "hidden") + overflow_y = OverflowProperty(VALID_OVERFLOW, "hidden") layer = NameProperty() layers = NameListProperty() diff --git a/src/textual/widget.py b/src/textual/widget.py index e6c9c26266..97142df50e 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1,7 +1,6 @@ from __future__ import annotations from asyncio import Lock, wait -from asyncio import Lock, create_task, wait from collections import Counter from fractions import Fraction from itertools import islice @@ -922,7 +921,7 @@ def _refresh_scrollbars(self) -> None: show_horizontal = self.show_horizontal_scrollbar if overflow_x == "hidden": show_horizontal = False - if overflow_x == "scroll": + elif overflow_x == "scroll": show_horizontal = True elif overflow_x == "auto": show_horizontal = self.virtual_size.width > width @@ -1974,13 +1973,14 @@ def _get_scrollable_region(self, region: Region) -> Region: """ show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled - scrollbar_size_horizontal = self.styles.scrollbar_size_horizontal - scrollbar_size_vertical = self.styles.scrollbar_size_vertical + styles = self.styles + scrollbar_size_horizontal = styles.scrollbar_size_horizontal + scrollbar_size_vertical = styles.scrollbar_size_vertical - if self.styles.scrollbar_gutter == "stable": + if styles.scrollbar_gutter == "stable": # Let's _always_ reserve some space, whether the scrollbar is actually displayed or not: show_vertical_scrollbar = True - scrollbar_size_vertical = self.styles.scrollbar_size_vertical + scrollbar_size_vertical = styles.scrollbar_size_vertical if show_horizontal_scrollbar and show_vertical_scrollbar: (region, _, _, _) = region.split( diff --git a/tests/test_overflow_change.py b/tests/test_overflow_change.py new file mode 100644 index 0000000000..1ebd39765c --- /dev/null +++ b/tests/test_overflow_change.py @@ -0,0 +1,37 @@ +"""Regression test for #1616 https://github.com/Textualize/textual/issues/1616""" +import pytest + + +from textual.app import App +from textual.containers import Vertical + + +async def test_overflow_change_updates_virtual_size_appropriately(): + class MyApp(App): + def compose(self): + yield Vertical() + + app = MyApp() + + async with app.run_test() as pilot: + vertical = app.query_one(Vertical) + + height = vertical.virtual_size.height + + vertical.styles.overflow_x = "scroll" + await pilot.pause() # Let changes propagate. + assert vertical.virtual_size.height < height + + vertical.styles.overflow_x = "hidden" + await pilot.pause() + assert vertical.virtual_size.height == height + + width = vertical.virtual_size.width + + vertical.styles.overflow_y = "scroll" + await pilot.pause() + assert vertical.virtual_size.width < width + + vertical.styles.overflow_y = "hidden" + await pilot.pause() + assert vertical.virtual_size.width == width