From f44d483ee284439793cb8485ca27513a18208add Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 12 Jan 2023 10:32:29 +0000 Subject: [PATCH 01/21] Always refresh a changed scroll value, even if there's no scrollbar See #1201. See https://github.com/Textualize/textual/issues/1201#issuecomment-1380123660 though -- there has been internal discussion about how there may be a need to even inhibit by-code scrolling (as opposed to by-input) scrolling, in some situations. This would likely require that there's something higher-level, perhaps, that doesn't even attempt to animate `scroll_x` or `scroll_y` in the first place. Consider this a placeholder approach for the moment. --- src/textual/widget.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 7808f1a08..ea904208c 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -805,14 +805,14 @@ class Widget(DOMNode): def watch_scroll_x(self, old_value: float, new_value: float) -> None: if self.show_horizontal_scrollbar: self.horizontal_scrollbar.position = round(new_value) - if round(old_value) != round(new_value): - self._refresh_scroll() + if round(old_value) != round(new_value): + self._refresh_scroll() def watch_scroll_y(self, old_value: float, new_value: float) -> None: if self.show_vertical_scrollbar: self.vertical_scrollbar.position = round(new_value) - if round(old_value) != round(new_value): - self._refresh_scroll() + if round(old_value) != round(new_value): + self._refresh_scroll() def validate_scroll_x(self, value: float) -> float: return clamp(value, 0, self.max_scroll_x) From 7cc17d6da13a3ac6472206bf972805735d9ab9d5 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 12 Jan 2023 11:06:19 +0000 Subject: [PATCH 02/21] Have scroll_x/y watch functions obey relevant allow properties --- src/textual/widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index ea904208c..d5a3c6efd 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -803,13 +803,13 @@ class Widget(DOMNode): self.highlight_link_id = hover_style.link_id def watch_scroll_x(self, old_value: float, new_value: float) -> None: - if self.show_horizontal_scrollbar: + if self.allow_horizontal_scroll: self.horizontal_scrollbar.position = round(new_value) if round(old_value) != round(new_value): self._refresh_scroll() def watch_scroll_y(self, old_value: float, new_value: float) -> None: - if self.show_vertical_scrollbar: + if self.allow_vertical_scroll: self.vertical_scrollbar.position = round(new_value) if round(old_value) != round(new_value): self._refresh_scroll() From 9fee634c9d5a18b2a2e86b734171b05f065a769d Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 12 Jan 2023 13:44:33 +0000 Subject: [PATCH 03/21] Add a new force parameter to all the scroll_ methods See #1201. --- CHANGELOG.md | 1 + src/textual/widget.py | 61 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af9c30012..c0f1d8f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fail-fast and print pretty tracebacks for Widget compose errors https://github.com/Textualize/textual/pull/1505 - Added Widget._refresh_scroll to avoid expensive layout when scrolling https://github.com/Textualize/textual/pull/1524 - `events.Paste` now bubbles https://github.com/Textualize/textual/issues/1434 +- Programmatic calls to scroll now optionally scroll even if overflow styling says otherwise (introduces a new `force` parameter to all the `scroll_*` methods) https://github.com/Textualize/textual/issues/1201 ### Fixed diff --git a/src/textual/widget.py b/src/textual/widget.py index d5a3c6efd..beec21019 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1337,6 +1337,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll to a given (absolute) coordinate, optionally animating. @@ -1348,10 +1349,13 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if the scroll position changed, otherwise False. """ + maybe_scroll_x = x is not None and (self.allow_horizontal_scroll or force) + maybe_scroll_y = y is not None and (self.allow_vertical_scroll or force) scrolled_x = scrolled_y = False if animate: # TODO: configure animation speed @@ -1361,7 +1365,7 @@ class Widget(DOMNode): if easing is None: easing = DEFAULT_SCROLL_EASING - if x is not None: + if maybe_scroll_x: self.scroll_target_x = x if x != self.scroll_x: self.animate( @@ -1372,7 +1376,7 @@ class Widget(DOMNode): easing=easing, ) scrolled_x = True - if y is not None: + if maybe_scroll_y: self.scroll_target_y = y if y != self.scroll_y: self.animate( @@ -1385,11 +1389,11 @@ class Widget(DOMNode): scrolled_y = True else: - if x is not None: + if maybe_scroll_x: scroll_x = self.scroll_x self.scroll_target_x = self.scroll_x = x scrolled_x = scroll_x != self.scroll_x - if y is not None: + if maybe_scroll_y: scroll_y = self.scroll_y self.scroll_target_y = self.scroll_y = y scrolled_y = scroll_y != self.scroll_y @@ -1405,6 +1409,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll relative to current position. @@ -1416,6 +1421,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if the scroll position changed, otherwise False. @@ -1427,6 +1433,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_home( @@ -1436,6 +1443,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll to home position. @@ -1445,6 +1453,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1452,7 +1461,13 @@ class Widget(DOMNode): if speed is None and duration is None: duration = 1.0 return self.scroll_to( - 0, 0, animate=animate, speed=speed, duration=duration, easing=easing + 0, + 0, + animate=animate, + speed=speed, + duration=duration, + easing=easing, + force=force, ) def scroll_end( @@ -1462,6 +1477,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll to the end of the container. @@ -1471,6 +1487,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1485,6 +1502,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_left( @@ -1494,6 +1512,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll one cell left. @@ -1503,6 +1522,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1514,6 +1534,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_right( @@ -1523,6 +1544,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll on cell right. @@ -1532,6 +1554,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1543,6 +1566,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_down( @@ -1552,6 +1576,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll one line down. @@ -1561,6 +1586,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1572,6 +1598,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_up( @@ -1581,6 +1608,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll one line up. @@ -1590,6 +1618,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1601,6 +1630,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_page_up( @@ -1610,6 +1640,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll one page up. @@ -1619,6 +1650,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1630,6 +1662,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_page_down( @@ -1639,6 +1672,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll one page down. @@ -1648,6 +1682,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1659,6 +1694,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_page_left( @@ -1668,6 +1704,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll one page left. @@ -1677,6 +1714,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1690,6 +1728,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_page_right( @@ -1699,6 +1738,7 @@ class Widget(DOMNode): speed: float | None = None, duration: float | None = None, easing: EasingFunction | str | None = None, + force: bool = False, ) -> bool: """Scroll one page right. @@ -1708,6 +1748,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1721,6 +1762,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) def scroll_to_widget( @@ -1732,6 +1774,7 @@ class Widget(DOMNode): duration: float | None = None, easing: EasingFunction | str | None = None, top: bool = False, + force: bool = False, ) -> bool: """Scroll scrolling to bring a widget in to view. @@ -1743,6 +1786,7 @@ class Widget(DOMNode): easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. top (bool, optional): Scroll widget to top of container. Defaults to False. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling has occurred in any descendant, otherwise False. @@ -1762,6 +1806,7 @@ class Widget(DOMNode): duration=duration, top=top, easing=easing, + force=force, ) if scroll_offset: scrolled = True @@ -1790,6 +1835,7 @@ class Widget(DOMNode): duration: float | None = None, easing: EasingFunction | str | None = None, top: bool = False, + force: bool = False, ) -> Offset: """Scrolls a given region in to view, if required. @@ -1805,6 +1851,7 @@ class Widget(DOMNode): easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. top (bool, optional): Scroll region to top of container. Defaults to False. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: Offset: The distance that was scrolled. @@ -1832,6 +1879,7 @@ class Widget(DOMNode): speed=speed, duration=duration, easing=easing, + force=force, ) return delta @@ -1843,6 +1891,7 @@ class Widget(DOMNode): duration: float | None = None, top: bool = False, easing: EasingFunction | str | None = None, + force: bool = False, ) -> None: """Scroll the container to make this widget visible. @@ -1853,6 +1902,7 @@ class Widget(DOMNode): top (bool, optional): Scroll to top of container. Defaults to False. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. + force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. """ parent = self.parent if isinstance(parent, Widget): @@ -1864,6 +1914,7 @@ class Widget(DOMNode): duration=duration, top=top, easing=easing, + force=force, ) def __init_subclass__( From 13e68b66c7fd6aaf630b4abfb928fd77aedc6ef7 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 12 Jan 2023 16:34:36 +0000 Subject: [PATCH 04/21] Tidy up the force parameter in all the docstrings --- src/textual/widget.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index beec21019..90359eaba 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1349,7 +1349,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if the scroll position changed, otherwise False. @@ -1421,7 +1421,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if the scroll position changed, otherwise False. @@ -1453,7 +1453,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1487,7 +1487,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1522,7 +1522,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1554,7 +1554,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1586,7 +1586,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1618,7 +1618,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1650,7 +1650,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1682,7 +1682,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1714,7 +1714,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1748,7 +1748,7 @@ class Widget(DOMNode): duration (float | None, optional): Duration of animation, if animate is True and speed is None. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling was done. @@ -1786,7 +1786,7 @@ class Widget(DOMNode): easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. top (bool, optional): Scroll widget to top of container. Defaults to False. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: bool: True if any scrolling has occurred in any descendant, otherwise False. @@ -1851,7 +1851,7 @@ class Widget(DOMNode): easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. top (bool, optional): Scroll region to top of container. Defaults to False. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. Returns: Offset: The distance that was scrolled. @@ -1902,7 +1902,7 @@ class Widget(DOMNode): top (bool, optional): Scroll to top of container. Defaults to False. easing (EasingFunction | str | None, optional): An easing method for the scrolling animation. Defaults to "None", which will result in Textual choosing the configured default scrolling easing function. - force (bool,optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. + force (bool, optional): Force scrolling even when prohibited by overflow styling. Defaults to `False`. """ parent = self.parent if isinstance(parent, Widget): From 843810ef177bb436fc8161f3250e4715110c50fa Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Sun, 15 Jan 2023 10:35:20 +0000 Subject: [PATCH 05/21] Remove no-scroll guard from scroll watchers --- src/textual/widget.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index 90359eaba..54e543ae5 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -803,14 +803,10 @@ class Widget(DOMNode): self.highlight_link_id = hover_style.link_id def watch_scroll_x(self, old_value: float, new_value: float) -> None: - if self.allow_horizontal_scroll: - self.horizontal_scrollbar.position = round(new_value) if round(old_value) != round(new_value): self._refresh_scroll() def watch_scroll_y(self, old_value: float, new_value: float) -> None: - if self.allow_vertical_scroll: - self.vertical_scrollbar.position = round(new_value) if round(old_value) != round(new_value): self._refresh_scroll() From f9d0796d7749be3a9beb65e50542e28abeb02f90 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 06:54:07 +0000 Subject: [PATCH 06/21] Document the TITLE class attribute See #1564. --- src/textual/app.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/textual/app.py b/src/textual/app.py index b0f2e8c63..21d196ae5 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -240,7 +240,14 @@ class App(Generic[ReturnType], DOMNode): SCREENS: dict[str, Screen | Callable[[], Screen]] = {} _BASE_PATH: str | None = None CSS_PATH: CSSPathType = None + TITLE: str | None = None + """str | None: The default title for the application. + + If set to a string, this sets the default title for the application. See + also the `title` attribute. + """ + SUB_TITLE: str | None = None BINDINGS = [ From 97248d202b6a77fca2c913455558deb95db2cbc7 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 06:56:47 +0000 Subject: [PATCH 07/21] Document the SUB_TITLE class attribute See #1564. --- src/textual/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/app.py b/src/textual/app.py index 21d196ae5..073b43862 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -249,6 +249,11 @@ class App(Generic[ReturnType], DOMNode): """ SUB_TITLE: str | None = None + """str | None: The default sub-title for the application. + + If set to a string, this sets the default sub-title for the application. See + also the `sub_title` attribute. + """ BINDINGS = [ Binding("ctrl+c", "quit", "Quit", show=False, priority=True), From 7f9c109a79f2cad94e1caf84c8e20b70ec093788 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 07:00:18 +0000 Subject: [PATCH 08/21] Document the title reactive instance attribute See #1564. --- src/textual/app.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/textual/app.py b/src/textual/app.py index 073b43862..444fa70f9 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -262,6 +262,13 @@ class App(Generic[ReturnType], DOMNode): ] title: Reactive[str] = Reactive("", compute=False) + """Reactive[str]: The title for the application. + + The initial value in a running application will be that set in `TITLE` + (if one is set). Assign new values to this instance attribute to change + the title. + """ + sub_title: Reactive[str] = Reactive("", compute=False) dark: Reactive[bool] = Reactive(True, compute=False) From f72a4cfa8cb643eb74aa7c797127c7782fadd0b9 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 07:06:33 +0000 Subject: [PATCH 09/21] Document the sub_title reactive instance attribute See #1564. --- src/textual/app.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/textual/app.py b/src/textual/app.py index 444fa70f9..e8e083fc5 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -270,6 +270,13 @@ class App(Generic[ReturnType], DOMNode): """ sub_title: Reactive[str] = Reactive("", compute=False) + """Reactive[str]: The sub-title for the application. + + The initial value in a running application will be that set in `SUB_TITLE` + (if one is set). Assign new values to this instance attribute to change + the sub-title. + """ + dark: Reactive[bool] = Reactive(True, compute=False) def __init__( From cb7adb78bf5685ae3b0f152aeb823d7a770a2fe7 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 10:32:49 +0000 Subject: [PATCH 10/21] Move the title and sub_title docstrings to __ini__ According to the current working of out documentation generation pipeline, this is the way to document a reactive property that is referred to within the owner class' __init__. See #1572 and #1564. --- src/textual/app.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index e8e083fc5..0db8c3b15 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -262,21 +262,7 @@ class App(Generic[ReturnType], DOMNode): ] title: Reactive[str] = Reactive("", compute=False) - """Reactive[str]: The title for the application. - - The initial value in a running application will be that set in `TITLE` - (if one is set). Assign new values to this instance attribute to change - the title. - """ - sub_title: Reactive[str] = Reactive("", compute=False) - """Reactive[str]: The sub-title for the application. - - The initial value in a running application will be that set in `SUB_TITLE` - (if one is set). Assign new values to this instance attribute to change - the sub-title. - """ - dark: Reactive[bool] = Reactive(True, compute=False) def __init__( @@ -329,10 +315,24 @@ class App(Generic[ReturnType], DOMNode): self._animator = Animator(self) self._animate = self._animator.bind(self) self.mouse_position = Offset(0, 0) + self.title = ( self.TITLE if self.TITLE is not None else f"{self.__class__.__name__}" ) + """The title for the application. + + The initial value in a running application will be that set in `TITLE` + (if one is set). Assign new values to this instance attribute to change + the title. + """ + self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else "" + """The sub-title for the application. + + The initial value in a running application will be that set in `SUB_TITLE` + (if one is set). Assign new values to this instance attribute to change + the sub-title. + """ self._logger = Logger(self._log) From 09487a36ed684709413cc09192b4b79e56a9e8d6 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 12:59:23 +0000 Subject: [PATCH 11/21] Remove typing from Binding property docstrings See #1573. --- src/textual/binding.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/textual/binding.py b/src/textual/binding.py index c8e145977..083b1e180 100644 --- a/src/textual/binding.py +++ b/src/textual/binding.py @@ -28,17 +28,17 @@ class Binding: """The configuration of a key binding.""" key: str - """str: Key to bind. This can also be a comma-separated list of keys to map multiple keys to a single action.""" + """Key to bind. This can also be a comma-separated list of keys to map multiple keys to a single action.""" action: str - """str: Action to bind to.""" + """Action to bind to.""" description: str - """str: Description of action.""" + """Description of action.""" show: bool = True - """bool: Show the action in Footer, or False to hide.""" + """Show the action in Footer, or False to hide.""" key_display: str | None = None - """str | None: How the key should be shown in footer.""" + """How the key should be shown in footer.""" priority: bool = False - """bool: Enable priority binding for this key.""" + """Enable priority binding for this key.""" @rich.repr.auto From 2e849022078a296784f4bb5dda2112bd9783cf55 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 14:47:34 +0000 Subject: [PATCH 12/21] Document some of the Button reactive properties --- src/textual/widgets/_button.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 3587d30f2..0d4c1c315 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -195,8 +195,13 @@ class Button(Static, can_focus=True): self.variant = self.validate_variant(variant) label: Reactive[RenderableType] = Reactive("") + """The text label that appears within the button.""" + variant = Reactive.init("default") + """The variant name for the button.""" + disabled = Reactive(False) + """The disabled state of the button; `True` if disabled, `False` if not.""" def __rich_repr__(self) -> rich.repr.Result: yield from super().__rich_repr__() From 3dae3ae5d2c30bcb3f4fb98f7b44815167e9be67 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 14:51:01 +0000 Subject: [PATCH 13/21] Document the reactive properties of the Checkbox --- src/textual/widgets/_checkbox.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_checkbox.py b/src/textual/widgets/_checkbox.py index bf0cdaf2a..9d8718884 100644 --- a/src/textual/widgets/_checkbox.py +++ b/src/textual/widgets/_checkbox.py @@ -57,9 +57,6 @@ class Checkbox(Widget, can_focus=True): "checkbox--switch", } - value = reactive(False, init=False) - slider_pos = reactive(0.0) - def __init__( self, value: bool = False, @@ -84,6 +81,12 @@ class Checkbox(Widget, can_focus=True): self._reactive_value = value self._should_animate = animate + value = reactive(False, init=False) + """The value of the checkbox; `True` for on and `False` for off.""" + + slider_pos = reactive(0.0) + """The position of the slider.""" + def watch_value(self, value: bool) -> None: target_slider_pos = 1.0 if value else 0.0 if self._should_animate: From affcb60d9b0c5c340a0570a40c7791948ecc3a9e Mon Sep 17 00:00:00 2001 From: Galmo13 <96174042+Galmo13@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:07:58 +0100 Subject: [PATCH 14/21] Fixed a typo Fixed a "buy" by a "by". --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 0463bd2eb..4b412e51c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,7 +2,7 @@ This directory contains example Textual applications. -To run them, navigate to the examples directory and enter `python` followed buy the name of the Python file. +To run them, navigate to the examples directory and enter `python` followed by the name of the Python file. ``` cd textual/examples From d5ffc4e0acdbd50e5b9feb969bc8af6cb8da00aa Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 15:32:28 +0000 Subject: [PATCH 15/21] Add a docstring to the __init__of Input --- src/textual/widgets/_input.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index ec68be8cd..4532cd697 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -113,6 +113,17 @@ class Input(Widget, can_focus=True): id: str | None = None, classes: str | None = None, ) -> None: + """Initialise the `Input` widget. + + Args: + value (str | None, optional): An optional default value for the input. + placeholder (str, optional): Optional placeholder text for the input. + highlighter (Highlighter | None, optional): An optional highlighter for the input. + password (bool, optional): Flag to say if the field should obfuscate its content. Default is `False`. + name (str | None, optional): Optional name for the input widget. + id (str | None): Optional ID for the widget. + classes (str | None): Optional initial classes for the widget. + """ super().__init__(name=name, id=id, classes=classes) if value is not None: self.value = value From 00291e7c992188c36a2b1e1657c71f6a8e78fc50 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 15:32:49 +0000 Subject: [PATCH 16/21] Make some of Input's docstrings more property-like --- src/textual/widgets/_input.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 4532cd697..a1abd0454 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -138,7 +138,7 @@ class Input(Widget, can_focus=True): @property def _cursor_offset(self) -> int: - """Get the cell offset of the cursor.""" + """The cell offset of the cursor.""" offset = self._position_to_cell(self.cursor_position) if self._cursor_at_end: offset += 1 @@ -146,7 +146,7 @@ class Input(Widget, can_focus=True): @property def _cursor_at_end(self) -> bool: - """Check if the cursor is at the end""" + """Flag to indicate if the cursor is at the end""" return self.cursor_position >= len(self.value) def validate_cursor_position(self, cursor_position: int) -> int: @@ -181,7 +181,7 @@ class Input(Widget, can_focus=True): @property def cursor_width(self) -> int: - """Get the width of the input (with extra space for cursor at the end).""" + """The width of the input (with extra space for cursor at the end).""" if self.placeholder and not self.value: return cell_len(self.placeholder) return self._position_to_cell(len(self.value)) + 1 From 2b2d6c730c72483a107a07cf489473907d8c59bd Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 16 Jan 2023 15:41:20 +0000 Subject: [PATCH 17/21] Correct over-eager remove of if constructs That's how I'm reading https://github.com/Textualize/textual/pull/1550/files/1b9f51a272bc89d1aaf8b15537a19c276c8897fa#r1070555064 --- src/textual/widget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/textual/widget.py b/src/textual/widget.py index a815cdfd5..58721fea3 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -803,10 +803,12 @@ class Widget(DOMNode): self.highlight_link_id = hover_style.link_id def watch_scroll_x(self, old_value: float, new_value: float) -> None: + self.horizontal_scrollbar.position = round(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 = round(new_value) if round(old_value) != round(new_value): self._refresh_scroll() From a521640dace3e9518d2d90b7dd236ed9503a0288 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 17 Jan 2023 09:55:56 +0000 Subject: [PATCH 18/21] Fixes for textlog issues --- CHANGELOG.md | 2 ++ src/textual/strip.py | 6 ++++-- src/textual/widgets/_text_log.py | 18 ++++++++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39e484376..137f756a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - The styles `scrollbar-background-active` and `scrollbar-color-hover` are no longer ignored https://github.com/Textualize/textual/pull/1480 - The widget `Placeholder` can now have its width set to `auto` https://github.com/Textualize/textual/pull/1508 - Behavior of widget `Input` when rendering after programmatic value change and related scenarios https://github.com/Textualize/textual/issues/1477 https://github.com/Textualize/textual/issues/1443 +- Fixed TextLog wrapping issue https://github.com/Textualize/textual/issues/1554 +- Fixed issue with TextLog not writing anything before layout ## [0.9.1] - 2022-12-30 diff --git a/src/textual/strip.py b/src/textual/strip.py index 9f8575c6d..54a74b4ad 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -57,12 +57,14 @@ class Strip: return cls([Segment(" " * cell_length, style)], cell_length) @classmethod - def from_lines(cls, lines: list[list[Segment]], cell_length: int) -> list[Strip]: + def from_lines( + cls, lines: list[list[Segment]], cell_length: int | None = None + ) -> list[Strip]: """Convert lines (lists of segments) to a list of Strips. Args: lines (list[list[Segment]]): List of lines, where a line is a list of segments. - cell_length (int): Cell length of lines (must be same). + cell_length (int | None): Cell length of lines (must be same). Defaults to None. Returns: list[Strip]: List of strips. diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py index 0bfc8b899..f9903181b 100644 --- a/src/textual/widgets/_text_log.py +++ b/src/textual/widgets/_text_log.py @@ -19,10 +19,10 @@ from ..strip import Strip class TextLog(ScrollView, can_focus=True): - DEFAULT_CSS = """ + DEFAULT_CSS = """ TextLog{ background: $surface; - color: $text; + color: $text; overflow-y: scroll; } """ @@ -103,11 +103,11 @@ class TextLog(ScrollView, can_focus=True): container_width = ( self.scrollable_content_region.width if width is None else width ) - - if expand and render_width < container_width: - render_width = container_width - if shrink and render_width > container_width: - render_width = container_width + if container_width: + if expand and render_width < container_width: + render_width = container_width + if shrink and render_width > container_width: + render_width = container_width segments = self.app.console.render( renderable, render_options.update_width(render_width) @@ -120,7 +120,9 @@ class TextLog(ScrollView, can_focus=True): self.max_width, max(sum(segment.cell_length for segment in _line) for _line in lines), ) - strips = Strip.from_lines(lines, render_width) + strips = Strip.from_lines(lines) + for strip in strips: + strip.adjust_cell_length(render_width) self.lines.extend(strips) if self.max_lines is not None and len(self.lines) > self.max_lines: From e6d89dd839ec18ee15ec92ce004d48d4a5c411d2 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 17 Jan 2023 10:10:16 +0000 Subject: [PATCH 19/21] docstring --- src/textual/strip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/strip.py b/src/textual/strip.py index 54a74b4ad..3af759d4f 100644 --- a/src/textual/strip.py +++ b/src/textual/strip.py @@ -64,7 +64,7 @@ class Strip: Args: lines (list[list[Segment]]): List of lines, where a line is a list of segments. - cell_length (int | None): Cell length of lines (must be same). Defaults to None. + cell_length (int | None): Cell length of lines (must be same) or None if not known. Defaults to None. Returns: list[Strip]: List of strips. From 518c721ecfbcebcf4d917095e88f5f84c23da2ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Tue, 17 Jan 2023 10:44:49 +0000 Subject: [PATCH 20/21] Docstring correction --- src/textual/css/match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/css/match.py b/src/textual/css/match.py index 35a60ea4a..187b5d811 100644 --- a/src/textual/css/match.py +++ b/src/textual/css/match.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: def match(selector_sets: Iterable[SelectorSet], node: DOMNode) -> bool: - """Check if a given selector matches any of the given selector sets. + """Check if a given node matches any of the given selector sets. Args: selector_sets (Iterable[SelectorSet]): Iterable of selector sets. From 2956f1f4d8819a2fe322d018cc6b4707c3cbeb21 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 17 Jan 2023 14:22:25 +0000 Subject: [PATCH 21/21] update change log [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 137f756a2..b2a2c94c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - The widget `Placeholder` can now have its width set to `auto` https://github.com/Textualize/textual/pull/1508 - Behavior of widget `Input` when rendering after programmatic value change and related scenarios https://github.com/Textualize/textual/issues/1477 https://github.com/Textualize/textual/issues/1443 - Fixed TextLog wrapping issue https://github.com/Textualize/textual/issues/1554 -- Fixed issue with TextLog not writing anything before layout +- Fixed issue with TextLog not writing anything before layout https://github.com/Textualize/textual/issues/1498 ## [0.9.1] - 2022-12-30