mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into widget-doc-sweep
This commit is contained in:
@@ -13,6 +13,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added `RadioButton` https://github.com/Textualize/textual/pull/1872
|
||||
- Added `RadioSet` https://github.com/Textualize/textual/pull/1872
|
||||
|
||||
### Changed
|
||||
|
||||
- Widget scrolling methods (such as `Widget.scroll_home` and `Widget.scroll_end`) now perform the scroll after the next refresh https://github.com/Textualize/textual/issues/1774
|
||||
|
||||
### Fixed
|
||||
|
||||
- Scrolling with cursor keys now moves just one cell https://github.com/Textualize/textual/issues/1897
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix exceptions in watch methods being hidden on startup https://github.com/Textualize/textual/issues/1886
|
||||
|
||||
4
Makefile
4
Makefile
@@ -76,3 +76,7 @@ setup:
|
||||
.PHONY: update
|
||||
update:
|
||||
poetry update
|
||||
|
||||
.PHONY: install-pre-commit
|
||||
install-pre-commit:
|
||||
$(run) pre-commit install
|
||||
|
||||
@@ -300,6 +300,12 @@ So, thanks to this bit of code in my `Activity` widget...
|
||||
self.save_activity_list()
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
The code above used `emit_no_wait`. Since this blog post was first
|
||||
published that method has been removed from Textual. You should use
|
||||
[`post_message_no_wait` or `post_message`](/guide/events/#sending-messages) instead now.
|
||||
|
||||
### Pain points
|
||||
|
||||
On top of the issues of getting to know terminal-based-CSS that I mentioned
|
||||
|
||||
@@ -157,10 +157,14 @@ The chapter on [Textual CSS](CSS.md) describes how to use CSS in detail. For now
|
||||
|
||||
The following example enables loading of CSS by adding a `CSS_PATH` class variable:
|
||||
|
||||
```python title="question02.py" hl_lines="6"
|
||||
```python title="question02.py" hl_lines="6 9"
|
||||
--8<-- "docs/examples/app/question02.py"
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
We also added an `id` to the `Label`, because we want to style it in the CSS.
|
||||
|
||||
If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references `"question01.css"` in the same directory as the Python code. Here is that CSS file:
|
||||
|
||||
```sass title="question02.css"
|
||||
|
||||
@@ -1422,7 +1422,7 @@ class Widget(DOMNode):
|
||||
self._repaint_regions.clear()
|
||||
return regions
|
||||
|
||||
def scroll_to(
|
||||
def _scroll_to(
|
||||
self,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
@@ -1497,6 +1497,43 @@ class Widget(DOMNode):
|
||||
|
||||
return scrolled_x or scrolled_y
|
||||
|
||||
def scroll_to(
|
||||
self,
|
||||
x: float | None = None,
|
||||
y: float | None = None,
|
||||
*,
|
||||
animate: bool = True,
|
||||
speed: float | None = None,
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
"""Scroll to a given (absolute) coordinate, optionally animating.
|
||||
|
||||
Args:
|
||||
x: X coordinate (column) to scroll to, or None for no change. Defaults to None.
|
||||
y: Y coordinate (row) to scroll to, or None for no change. Defaults to None.
|
||||
animate: Animate to new scroll position. Defaults to True.
|
||||
speed: Speed of scroll if animate is True. Or None to use duration.
|
||||
duration: Duration of animation, if animate is True and speed is None.
|
||||
easing: An easing method for the scrolling animation. Defaults to "None",
|
||||
which will result in Textual choosing the default scrolling easing function.
|
||||
force: Force scrolling even when prohibited by overflow styling. Defaults to `False`.
|
||||
|
||||
Note:
|
||||
The call to scroll is made after the next refresh.
|
||||
"""
|
||||
self.call_after_refresh(
|
||||
self._scroll_to,
|
||||
x,
|
||||
y,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
force=force,
|
||||
)
|
||||
|
||||
def scroll_relative(
|
||||
self,
|
||||
x: float | None = None,
|
||||
@@ -1507,7 +1544,7 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll relative to current position.
|
||||
|
||||
Args:
|
||||
@@ -1518,11 +1555,8 @@ class Widget(DOMNode):
|
||||
duration: Duration of animation, if animate is `True` and speed is `None`.
|
||||
easing: An easing method for the scrolling animation.
|
||||
force: Force scrolling even when prohibited by overflow styling.
|
||||
|
||||
Returns:
|
||||
`True` if the scroll position changed, otherwise `False`.
|
||||
"""
|
||||
return self.scroll_to(
|
||||
self.scroll_to(
|
||||
None if x is None else (self.scroll_x + x),
|
||||
None if y is None else (self.scroll_y + y),
|
||||
animate=animate,
|
||||
@@ -1540,7 +1574,7 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll to home position.
|
||||
|
||||
Args:
|
||||
@@ -1549,13 +1583,10 @@ class Widget(DOMNode):
|
||||
duration: Duration of animation, if animate is `True` and speed is `None`.
|
||||
easing: An easing method for the scrolling animation.
|
||||
force: Force scrolling even when prohibited by overflow styling.
|
||||
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
"""
|
||||
if speed is None and duration is None:
|
||||
duration = 1.0
|
||||
return self.scroll_to(
|
||||
self.scroll_to(
|
||||
0,
|
||||
0,
|
||||
animate=animate,
|
||||
@@ -1573,7 +1604,7 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll to the end of the container.
|
||||
|
||||
Args:
|
||||
@@ -1582,21 +1613,29 @@ class Widget(DOMNode):
|
||||
duration: Duration of animation, if animate is `True` and speed is `None`.
|
||||
easing: An easing method for the scrolling animation.
|
||||
force: Force scrolling even when prohibited by overflow styling.
|
||||
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
"""
|
||||
if speed is None and duration is None:
|
||||
duration = 1.0
|
||||
return self.scroll_to(
|
||||
0,
|
||||
self.max_scroll_y,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
force=force,
|
||||
)
|
||||
|
||||
# In most cases we'd call self.scroll_to and let it handle the call
|
||||
# to do things after a refresh, but here we need the refresh to
|
||||
# happen first so that we can get the new self.max_scroll_y (that
|
||||
# is, we need the layout to work out and then figure out how big
|
||||
# things are). Because of this we'll create a closure over the call
|
||||
# here and make our own call to call_after_refresh.
|
||||
def _lazily_scroll_end() -> None:
|
||||
"""Scroll to the end of the widget."""
|
||||
self._scroll_to(
|
||||
0,
|
||||
self.max_scroll_y,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
force=force,
|
||||
)
|
||||
|
||||
self.call_after_refresh(_lazily_scroll_end)
|
||||
|
||||
def scroll_left(
|
||||
self,
|
||||
@@ -1606,9 +1645,37 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll one cell left.
|
||||
|
||||
Args:
|
||||
animate: Animate scroll. Defaults to True.
|
||||
speed: Speed of scroll if animate is True. Or None to use duration.
|
||||
duration: Duration of animation, if animate is True and speed is None.
|
||||
easing: An easing method for the scrolling animation. Defaults to "None",
|
||||
which will result in Textual choosing the configured default scrolling easing function.
|
||||
force: Force scrolling even when prohibited by overflow styling. Defaults to `False`.
|
||||
"""
|
||||
self.scroll_to(
|
||||
x=self.scroll_target_x - 1,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
force=force,
|
||||
)
|
||||
|
||||
def _scroll_left_for_pointer(
|
||||
self,
|
||||
*,
|
||||
animate: bool = True,
|
||||
speed: float | None = None,
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
"""Scroll left one position, taking scroll sensitivity into account.
|
||||
|
||||
Args:
|
||||
animate: Animate scroll. Defaults to True.
|
||||
speed: Speed of scroll if animate is True. Or None to use duration.
|
||||
@@ -1620,8 +1687,11 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
|
||||
Note:
|
||||
How much is scrolled is controlled by
|
||||
[App.scroll_sensitivity_x][textual.app.App.scroll_sensitivity_x].
|
||||
"""
|
||||
return self.scroll_to(
|
||||
return self._scroll_to(
|
||||
x=self.scroll_target_x - self.app.scroll_sensitivity_x,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
@@ -1638,8 +1708,36 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
"""Scroll one cell right.
|
||||
|
||||
Args:
|
||||
animate: Animate scroll. Defaults to True.
|
||||
speed: Speed of scroll if animate is True. Or None to use duration.
|
||||
duration: Duration of animation, if animate is True and speed is None.
|
||||
easing: An easing method for the scrolling animation. Defaults to "None",
|
||||
which will result in Textual choosing the configured default scrolling easing function.
|
||||
force: Force scrolling even when prohibited by overflow styling. Defaults to `False`.
|
||||
"""
|
||||
self.scroll_to(
|
||||
x=self.scroll_target_x + 1,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
force=force,
|
||||
)
|
||||
|
||||
def _scroll_right_for_pointer(
|
||||
self,
|
||||
*,
|
||||
animate: bool = True,
|
||||
speed: float | None = None,
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
"""Scroll on cell right.
|
||||
"""Scroll right one position, taking scroll sensitivity into account.
|
||||
|
||||
Args:
|
||||
animate: Animate scroll. Defaults to True.
|
||||
@@ -1652,8 +1750,11 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
|
||||
Note:
|
||||
How much is scrolled is controlled by
|
||||
[App.scroll_sensitivity_x][textual.app.App.scroll_sensitivity_x].
|
||||
"""
|
||||
return self.scroll_to(
|
||||
return self._scroll_to(
|
||||
x=self.scroll_target_x + self.app.scroll_sensitivity_x,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
@@ -1670,9 +1771,37 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll one line down.
|
||||
|
||||
Args:
|
||||
animate: Animate scroll. Defaults to True.
|
||||
speed: Speed of scroll if animate is True. Or None to use duration.
|
||||
duration: Duration of animation, if animate is True and speed is None.
|
||||
easing: An easing method for the scrolling animation. Defaults to "None",
|
||||
which will result in Textual choosing the configured default scrolling easing function.
|
||||
force: Force scrolling even when prohibited by overflow styling. Defaults to `False`.
|
||||
"""
|
||||
self.scroll_to(
|
||||
y=self.scroll_target_y + 1,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
force=force,
|
||||
)
|
||||
|
||||
def _scroll_down_for_pointer(
|
||||
self,
|
||||
*,
|
||||
animate: bool = True,
|
||||
speed: float | None = None,
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
"""Scroll down one position, taking scroll sensitivity into account.
|
||||
|
||||
Args:
|
||||
animate: Animate scroll. Defaults to True.
|
||||
speed: Speed of scroll if animate is True. Or None to use duration.
|
||||
@@ -1684,8 +1813,11 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
|
||||
Note:
|
||||
How much is scrolled is controlled by
|
||||
[App.scroll_sensitivity_y][textual.app.App.scroll_sensitivity_y].
|
||||
"""
|
||||
return self.scroll_to(
|
||||
return self._scroll_to(
|
||||
y=self.scroll_target_y + self.app.scroll_sensitivity_y,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
@@ -1702,9 +1834,37 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll one line up.
|
||||
|
||||
Args:
|
||||
animate: Animate scroll. Defaults to True.
|
||||
speed: Speed of scroll if animate is True. Or None to use duration.
|
||||
duration: Duration of animation, if animate is True and speed is None.
|
||||
easing: An easing method for the scrolling animation. Defaults to "None",
|
||||
which will result in Textual choosing the configured default scrolling easing function.
|
||||
force: Force scrolling even when prohibited by overflow styling. Defaults to `False`.
|
||||
"""
|
||||
self.scroll_to(
|
||||
y=self.scroll_target_y - 1,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
force=force,
|
||||
)
|
||||
|
||||
def _scroll_up_for_pointer(
|
||||
self,
|
||||
*,
|
||||
animate: bool = True,
|
||||
speed: float | None = None,
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
"""Scroll up one position, taking scroll sensitivity into account.
|
||||
|
||||
Args:
|
||||
animate: Animate scroll. Defaults to True.
|
||||
speed: Speed of scroll if animate is True. Or None to use duration.
|
||||
@@ -1716,9 +1876,12 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
|
||||
Note:
|
||||
How much is scrolled is controlled by
|
||||
[App.scroll_sensitivity_y][textual.app.App.scroll_sensitivity_y].
|
||||
"""
|
||||
return self.scroll_to(
|
||||
y=self.scroll_target_y - +self.app.scroll_sensitivity_y,
|
||||
return self._scroll_to(
|
||||
y=self.scroll_target_y - self.app.scroll_sensitivity_y,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
duration=duration,
|
||||
@@ -1734,7 +1897,7 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll one page up.
|
||||
|
||||
Args:
|
||||
@@ -1744,12 +1907,8 @@ class Widget(DOMNode):
|
||||
easing: An easing method for the scrolling animation. Defaults to "None",
|
||||
which will result in Textual choosing the configured default scrolling easing function.
|
||||
force: Force scrolling even when prohibited by overflow styling. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(
|
||||
self.scroll_to(
|
||||
y=self.scroll_y - self.container_size.height,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
@@ -1766,7 +1925,7 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll one page down.
|
||||
|
||||
Args:
|
||||
@@ -1776,12 +1935,8 @@ class Widget(DOMNode):
|
||||
easing: An easing method for the scrolling animation. Defaults to "None",
|
||||
which will result in Textual choosing the configured default scrolling easing function.
|
||||
force: Force scrolling even when prohibited by overflow styling. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
|
||||
"""
|
||||
return self.scroll_to(
|
||||
self.scroll_to(
|
||||
y=self.scroll_y + self.container_size.height,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
@@ -1798,7 +1953,7 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll one page left.
|
||||
|
||||
Args:
|
||||
@@ -1808,14 +1963,10 @@ class Widget(DOMNode):
|
||||
easing: An easing method for the scrolling animation. Defaults to "None",
|
||||
which will result in Textual choosing the configured default scrolling easing function.
|
||||
force: Force scrolling even when prohibited by overflow styling. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
|
||||
"""
|
||||
if speed is None and duration is None:
|
||||
duration = 0.3
|
||||
return self.scroll_to(
|
||||
self.scroll_to(
|
||||
x=self.scroll_x - self.container_size.width,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
@@ -1832,7 +1983,7 @@ class Widget(DOMNode):
|
||||
duration: float | None = None,
|
||||
easing: EasingFunction | str | None = None,
|
||||
force: bool = False,
|
||||
) -> bool:
|
||||
) -> None:
|
||||
"""Scroll one page right.
|
||||
|
||||
Args:
|
||||
@@ -1842,14 +1993,10 @@ class Widget(DOMNode):
|
||||
easing: An easing method for the scrolling animation. Defaults to "None",
|
||||
which will result in Textual choosing the configured default scrolling easing function.
|
||||
force: Force scrolling even when prohibited by overflow styling. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
True if any scrolling was done.
|
||||
|
||||
"""
|
||||
if speed is None and duration is None:
|
||||
duration = 0.3
|
||||
return self.scroll_to(
|
||||
self.scroll_to(
|
||||
x=self.scroll_x + self.container_size.width,
|
||||
animate=animate,
|
||||
speed=speed,
|
||||
@@ -2578,21 +2725,21 @@ class Widget(DOMNode):
|
||||
def _on_mouse_scroll_down(self, event: events.MouseScrollDown) -> None:
|
||||
if event.ctrl or event.shift:
|
||||
if self.allow_horizontal_scroll:
|
||||
if self.scroll_right(animate=False):
|
||||
if self._scroll_right_for_pointer(animate=False):
|
||||
event.stop()
|
||||
else:
|
||||
if self.allow_vertical_scroll:
|
||||
if self.scroll_down(animate=False):
|
||||
if self._scroll_down_for_pointer(animate=False):
|
||||
event.stop()
|
||||
|
||||
def _on_mouse_scroll_up(self, event: events.MouseScrollUp) -> None:
|
||||
if event.ctrl or event.shift:
|
||||
if self.allow_horizontal_scroll:
|
||||
if self.scroll_left(animate=False):
|
||||
if self._scroll_left_for_pointer(animate=False):
|
||||
event.stop()
|
||||
else:
|
||||
if self.allow_vertical_scroll:
|
||||
if self.scroll_up(animate=False):
|
||||
if self._scroll_up_for_pointer(animate=False):
|
||||
event.stop()
|
||||
|
||||
def _on_scroll_to(self, message: ScrollTo) -> None:
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user