Merge pull request #1610 from Textualize/fix-1607

Fix #1607 to allow programmatic style changes
This commit is contained in:
Rodrigo Girão Serrão
2023-01-31 15:13:24 +00:00
committed by GitHub
7 changed files with 353 additions and 18 deletions

View File

@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed ### Fixed
- Fixed stuck screen https://github.com/Textualize/textual/issues/1632 - Fixed stuck screen https://github.com/Textualize/textual/issues/1632
- Fixed programmatic style changes not refreshing children layouts when parent widget did not change size https://github.com/Textualize/textual/issues/1607
- Fixed relative units in `grid-rows` and `grid-columns` being computed with respect to the wrong dimension https://github.com/Textualize/textual/issues/1406 - Fixed relative units in `grid-rows` and `grid-columns` being computed with respect to the wrong dimension https://github.com/Textualize/textual/issues/1406
- Fixed bug with animations that were triggered back to back, where the second one wouldn't start https://github.com/Textualize/textual/issues/1372 - Fixed bug with animations that were triggered back to back, where the second one wouldn't start https://github.com/Textualize/textual/issues/1372
- Fixed bug with animations that were scheduled where all but the first would be skipped https://github.com/Textualize/textual/issues/1372 - Fixed bug with animations that were scheduled where all but the first would be skipped https://github.com/Textualize/textual/issues/1372

View File

@@ -60,9 +60,23 @@ PropertySetType = TypeVar("PropertySetType")
class GenericProperty(Generic[PropertyGetType, PropertySetType]): class GenericProperty(Generic[PropertyGetType, PropertySetType]):
def __init__(self, default: PropertyGetType, layout: bool = False) -> None: """Descriptor that abstracts away common machinery for other style descriptors.
Args:
default: The default value (or a factory thereof) of the property.
layout: Whether to refresh the node layout on value change.
refresh_children: Whether to refresh the node children on value change.
"""
def __init__(
self,
default: PropertyGetType,
layout: bool = False,
refresh_children: bool = False,
) -> None:
self.default = default self.default = default
self.layout = layout self.layout = layout
self.refresh_children = refresh_children
def validate_value(self, value: object) -> PropertyGetType: def validate_value(self, value: object) -> PropertyGetType:
"""Validate the setter value. """Validate the setter value.
@@ -88,11 +102,11 @@ class GenericProperty(Generic[PropertyGetType, PropertySetType]):
_rich_traceback_omit = True _rich_traceback_omit = True
if value is None: if value is None:
obj.clear_rule(self.name) obj.clear_rule(self.name)
obj.refresh(layout=self.layout) obj.refresh(layout=self.layout, children=self.refresh_children)
return return
new_value = self.validate_value(value) new_value = self.validate_value(value)
if obj.set_rule(self.name, new_value): if obj.set_rule(self.name, new_value):
obj.refresh(layout=self.layout) obj.refresh(layout=self.layout, children=self.refresh_children)
class IntegerProperty(GenericProperty[int, int]): class IntegerProperty(GenericProperty[int, int]):
@@ -202,8 +216,16 @@ class ScalarProperty:
class ScalarListProperty: class ScalarListProperty:
def __init__(self, percent_unit: Unit) -> None: """Descriptor for lists of scalars.
Args:
percent_unit: The dimension to which percentage scalars will be relative to.
refresh_children: Whether to refresh the node children on value change.
"""
def __init__(self, percent_unit: Unit, refresh_children: bool = False) -> None:
self.percent_unit = percent_unit self.percent_unit = percent_unit
self.refresh_children = refresh_children
def __set_name__(self, owner: Styles, name: str) -> None: def __set_name__(self, owner: Styles, name: str) -> None:
self.name = name self.name = name
@@ -218,7 +240,7 @@ class ScalarListProperty:
) -> None: ) -> None:
if value is None: if value is None:
obj.clear_rule(self.name) obj.clear_rule(self.name)
obj.refresh(layout=True) obj.refresh(layout=True, children=self.refresh_children)
return return
parse_values: Iterable[str | float] parse_values: Iterable[str | float]
if isinstance(value, str): if isinstance(value, str):
@@ -237,7 +259,7 @@ class ScalarListProperty:
else parse_value else parse_value
) )
if obj.set_rule(self.name, tuple(scalars)): if obj.set_rule(self.name, tuple(scalars)):
obj.refresh(layout=True) obj.refresh(layout=True, children=self.refresh_children)
class BoxProperty: class BoxProperty:
@@ -682,12 +704,25 @@ class OffsetProperty:
class StringEnumProperty: class StringEnumProperty:
"""Descriptor for getting and setting string properties and ensuring that the set """Descriptor for getting and setting string properties and ensuring that the set
value belongs in the set of valid values. value belongs in the set of valid values.
Args:
valid_values: The set of valid values that the descriptor can take.
default: The default value (or a factory thereof) of the property.
layout: Whether to refresh the node layout on value change.
refresh_children: Whether to refresh the node children on value change.
""" """
def __init__(self, valid_values: set[str], default: str, layout=False) -> None: def __init__(
self,
valid_values: set[str],
default: str,
layout: bool = False,
refresh_children: bool = False,
) -> None:
self._valid_values = valid_values self._valid_values = valid_values
self._default = default self._default = default
self._layout = layout self._layout = layout
self._refresh_children = refresh_children
def __set_name__(self, owner: StylesBase, name: str) -> None: def __set_name__(self, owner: StylesBase, name: str) -> None:
self.name = name self.name = name
@@ -721,7 +756,7 @@ class StringEnumProperty:
if value is None: if value is None:
if obj.clear_rule(self.name): if obj.clear_rule(self.name):
self._before_refresh(obj, value) self._before_refresh(obj, value)
obj.refresh(layout=self._layout) obj.refresh(layout=self._layout, children=self._refresh_children)
else: else:
if value not in self._valid_values: if value not in self._valid_values:
raise StyleValueError( raise StyleValueError(
@@ -734,7 +769,7 @@ class StringEnumProperty:
) )
if obj.set_rule(self.name, value): if obj.set_rule(self.name, value):
self._before_refresh(obj, value) self._before_refresh(obj, value)
obj.refresh(layout=self._layout) obj.refresh(layout=self._layout, children=self._refresh_children)
class OverflowProperty(StringEnumProperty): class OverflowProperty(StringEnumProperty):

View File

@@ -265,26 +265,36 @@ class StylesBase(ABC):
scrollbar_background_hover = ColorProperty("#444444") scrollbar_background_hover = ColorProperty("#444444")
scrollbar_background_active = ColorProperty("black") scrollbar_background_active = ColorProperty("black")
scrollbar_gutter = StringEnumProperty(VALID_SCROLLBAR_GUTTER, "auto") scrollbar_gutter = StringEnumProperty(
VALID_SCROLLBAR_GUTTER, "auto", layout=True, refresh_children=True
)
scrollbar_size_vertical = IntegerProperty(default=1, layout=True) scrollbar_size_vertical = IntegerProperty(default=1, layout=True)
scrollbar_size_horizontal = IntegerProperty(default=1, layout=True) scrollbar_size_horizontal = IntegerProperty(default=1, layout=True)
align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left") align_horizontal = StringEnumProperty(
align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") VALID_ALIGN_HORIZONTAL, "left", layout=True, refresh_children=True
)
align_vertical = StringEnumProperty(
VALID_ALIGN_VERTICAL, "top", layout=True, refresh_children=True
)
align = AlignProperty() align = AlignProperty()
content_align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left") content_align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
content_align = AlignProperty() content_align = AlignProperty()
grid_rows = ScalarListProperty(percent_unit=Unit.HEIGHT) grid_rows = ScalarListProperty(percent_unit=Unit.HEIGHT, refresh_children=True)
grid_columns = ScalarListProperty(percent_unit=Unit.WIDTH) grid_columns = ScalarListProperty(percent_unit=Unit.WIDTH, refresh_children=True)
grid_size_columns = IntegerProperty(default=1, layout=True) grid_size_columns = IntegerProperty(default=1, layout=True, refresh_children=True)
grid_size_rows = IntegerProperty(default=0, layout=True) grid_size_rows = IntegerProperty(default=0, layout=True, refresh_children=True)
grid_gutter_horizontal = IntegerProperty(default=0, layout=True) grid_gutter_horizontal = IntegerProperty(
grid_gutter_vertical = IntegerProperty(default=0, layout=True) default=0, layout=True, refresh_children=True
)
grid_gutter_vertical = IntegerProperty(
default=0, layout=True, refresh_children=True
)
row_span = IntegerProperty(default=1, layout=True) row_span = IntegerProperty(default=1, layout=True)
column_span = IntegerProperty(default=1, layout=True) column_span = IntegerProperty(default=1, layout=True)

View File

@@ -0,0 +1,95 @@
import pytest
from textual.app import App
from textual.containers import Grid
from textual.screen import Screen
from textual.widgets import Label
@pytest.mark.parametrize(
"style, value",
[
("grid_size_rows", 3),
("grid_size_columns", 3),
("grid_gutter_vertical", 4),
("grid_gutter_horizontal", 4),
("grid_rows", "1fr 3fr"),
("grid_columns", "1fr 3fr"),
],
)
async def test_programmatic_style_change_updates_children(style: str, value: object):
"""Regression test for #1607 https://github.com/Textualize/textual/issues/1607
Some programmatic style changes to a widget were not updating the layout of the
children widgets, which seemed to be happening when the style change did not affect
the size of the widget but did affect the layout of the children.
This test, in particular, checks the attributes that _should_ affect the size of the
children widgets.
"""
class MyApp(App[None]):
CSS = """
Grid { grid-size: 2 2; }
Label { width: 100%; height: 100%; }
"""
def compose(self):
yield Grid(
Label("one"),
Label("two"),
Label("three"),
Label("four"),
)
app = MyApp()
async with app.run_test() as pilot:
sizes = [(lbl.size.width, lbl.size.height) for lbl in app.screen.query(Label)]
setattr(app.query_one(Grid).styles, style, value)
await pilot.pause()
assert sizes != [
(lbl.size.width, lbl.size.height) for lbl in app.screen.query(Label)
]
@pytest.mark.parametrize(
"style, value",
[
("align_horizontal", "right"),
("align_vertical", "bottom"),
("align", ("right", "bottom")),
],
)
async def test_programmatic_align_change_updates_children_position(
style: str, value: str
):
"""Regression test for #1607 for the align(_xxx) styles.
See https://github.com/Textualize/textual/issues/1607.
"""
class MyApp(App[None]):
CSS = "Grid { grid-size: 2 2; }"
def compose(self):
yield Grid(
Label("one"),
Label("two"),
Label("three"),
Label("four"),
)
app = MyApp()
async with app.run_test() as pilot:
offsets = [(lbl.region.x, lbl.region.y) for lbl in app.screen.query(Label)]
setattr(app.query_one(Grid).styles, style, value)
await pilot.pause()
assert offsets != [
(lbl.region.x, lbl.region.y) for lbl in app.screen.query(Label)
]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
from textual.app import App
from textual.containers import Grid
from textual.widgets import Label
class ProgrammaticScrollbarGutterChange(App[None]):
CSS = """
Grid { grid-size: 2 2; scrollbar-size: 5 5; }
Label { width: 100%; height: 100%; background: red; }
"""
def compose(self):
yield Grid(
Label("one"),
Label("two"),
Label("three"),
Label("four"),
)
def on_key(self, event):
if event.key == "s":
self.query_one(Grid).styles.scrollbar_gutter = "stable"
app = ProgrammaticScrollbarGutterChange()
if __name__ == "__main__":
app().run()

View File

@@ -179,6 +179,16 @@ def test_nested_auto_heights(snap_compare):
assert snap_compare("snapshot_apps/nested_auto_heights.py", press=["1", "2", "_"]) assert snap_compare("snapshot_apps/nested_auto_heights.py", press=["1", "2", "_"])
def test_programmatic_scrollbar_gutter_change(snap_compare):
"""Regression test for #1607 https://github.com/Textualize/textual/issues/1607
See also tests/css/test_programmatic_style_changes.py for other related regression tests.
"""
assert snap_compare(
"snapshot_apps/programmatic_scrollbar_gutter_change.py", press=["s"]
)
# --- Other --- # --- Other ---