Skip to content

Commit

Permalink
Merge pull request #4530 from Textualize/anchor
Browse files Browse the repository at this point in the history
Add anchor method
  • Loading branch information
willmcgugan authored May 20, 2024
2 parents 3b09172 + 50a3eca commit 964d988
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 70 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +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/).

## [0.62.0] - Unrelease
## [0.62.0] - 2024-05-20

### Added

- Added `start` and `end` properties to Markdown Navigator
- Added `Widget.anchor`, `Widget.clear_anchor`, and `Widget.is_anchored` https://github.com/Textualize/textual/pull/4530

## [0.61.1] - 2024-05-19

Expand Down Expand Up @@ -1978,6 +1979,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

[0.62.0]: https://github.com/Textualize/textual/compare/v0.61.1...v0.62.0
[0.61.1]: https://github.com/Textualize/textual/compare/v0.61.0...v0.61.1
[0.61.0]: https://github.com/Textualize/textual/compare/v0.60.1...v0.61.0
[0.60.1]: https://github.com/Textualize/textual/compare/v0.60.0...v0.60.1
Expand Down
2 changes: 2 additions & 0 deletions examples/markdown.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from pathlib import Path
from sys import argv

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "0.61.1"
version = "0.62.0"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
Expand Down
61 changes: 61 additions & 0 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,10 @@ def __init__(
might result in a race condition.
This can be fixed by adding `async with widget.lock:` around the method calls.
"""
self._anchored: Widget | None = None
"""An anchored child widget, or `None` if no child is anchored."""
self._anchor_animate: bool = False
"""Flag to enable animation when scrolling anchored widgets."""

virtual_size: Reactive[Size] = Reactive(Size(0, 0), layout=True)
"""The virtual (scrollable) [size][textual.geometry.Size] of the widget."""
Expand Down Expand Up @@ -515,6 +519,40 @@ def opacity(self) -> float:
break
return opacity

@property
def is_anchored(self) -> bool:
"""Is this widget anchored?"""
return self._parent is not None and self._parent is self

def anchor(self, *, animate: bool = False) -> None:
"""Anchor the widget, which scrolls it into view (like [scroll_visible][textual.widget.Widget.scroll_visible]),
but also keeps it in view if the widget's size changes, or the size of its container changes.
!!! note
Anchored widgets will be un-anchored if the users scrolls the container.
Args:
animate: `True` if the scroll should animate, or `False` if it shouldn't.
"""
if self._parent is not None and isinstance(self._parent, Widget):
self._parent._anchored = self
self._parent._anchor_animate = animate
self.check_idle()

def clear_anchor(self) -> None:
"""Stop anchoring this widget (a no-op if this widget is not anchored)."""
if (
self._parent is not None
and isinstance(self._parent, Widget)
and self._parent._anchored is self
):
self._parent._anchored = None

def _clear_anchor(self) -> None:
"""Clear an anchored child."""
self._anchored = None

def _check_disabled(self) -> bool:
"""Check if the widget is disabled either explicitly by setting `disabled`,
or implicitly by setting `loading`.
Expand Down Expand Up @@ -3178,6 +3216,7 @@ def _size_updated(
Returns:
True if anything changed, or False if nothing changed.
"""

if (
self._size != size
or self.virtual_size != virtual_size
Expand Down Expand Up @@ -3502,6 +3541,11 @@ async def _on_idle(self, event: events.Idle) -> None:
"""
self._check_refresh()

if self.is_anchored:
self.scroll_visible(animate=self._anchor_animate)
if self._anchored:
self._anchored.scroll_visible(animate=self._anchor_animate)

def _check_refresh(self) -> None:
"""Check if a refresh was requested."""
if self._parent is not None and not self._closing:
Expand Down Expand Up @@ -3702,45 +3746,54 @@ def _on_blur(self, event: events.Blur) -> None:
def _on_mouse_scroll_down(self, event: events.MouseScrollDown) -> None:
if event.ctrl or event.shift:
if self.allow_horizontal_scroll:
self._clear_anchor()
if self._scroll_right_for_pointer(animate=False):
event.stop()
else:
if self.allow_vertical_scroll:
self._clear_anchor()
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:
self._clear_anchor()
if self._scroll_left_for_pointer(animate=False):
event.stop()
else:
if self.allow_vertical_scroll:
self._clear_anchor()
if self._scroll_up_for_pointer(animate=False):
event.stop()

def _on_scroll_to(self, message: ScrollTo) -> None:
if self._allow_scroll:
self._clear_anchor()
self.scroll_to(message.x, message.y, animate=message.animate, duration=0.1)
message.stop()

def _on_scroll_up(self, event: ScrollUp) -> None:
if self.allow_vertical_scroll:
self._clear_anchor()
self.scroll_page_up()
event.stop()

def _on_scroll_down(self, event: ScrollDown) -> None:
if self.allow_vertical_scroll:
self._clear_anchor()
self.scroll_page_down()
event.stop()

def _on_scroll_left(self, event: ScrollLeft) -> None:
if self.allow_horizontal_scroll:
self._clear_anchor()
self.scroll_page_left()
event.stop()

def _on_scroll_right(self, event: ScrollRight) -> None:
if self.allow_horizontal_scroll:
self._clear_anchor()
self.scroll_page_right()
event.stop()

Expand All @@ -3767,41 +3820,49 @@ def _on_unmount(self) -> None:
def action_scroll_home(self) -> None:
if not self._allow_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_home()

def action_scroll_end(self) -> None:
if not self._allow_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_end()

def action_scroll_left(self) -> None:
if not self.allow_horizontal_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_left()

def action_scroll_right(self) -> None:
if not self.allow_horizontal_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_right()

def action_scroll_up(self) -> None:
if not self.allow_vertical_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_up()

def action_scroll_down(self) -> None:
if not self.allow_vertical_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_down()

def action_page_down(self) -> None:
if not self.allow_vertical_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_page_down()

def action_page_up(self) -> None:
if not self.allow_vertical_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_page_up()

def notify(
Expand Down
138 changes: 70 additions & 68 deletions tests/snapshot_tests/__snapshots__/test_snapshots.ambr

Large diffs are not rendered by default.

0 comments on commit 964d988

Please sign in to comment.