From e76e42921f1f265b506f52431bb926f35083ea27 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 25 Oct 2024 15:07:34 +0100 Subject: [PATCH] can view fixes --- CHANGELOG.md | 2 ++ src/textual/demo/widgets.py | 12 ++++++------ src/textual/screen.py | 32 ++++++++++++++++++++++++++------ src/textual/widget.py | 33 ++++++++++++++++++++++++++++++--- 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db4fb32bcc..f1583c243b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Grid will now size children to the maximum height of a row https://github.com/Textualize/textual/pull/5113 - Markdown links will be opened with `App.open_url` automatically https://github.com/Textualize/textual/pull/5113 - The universal selector (`*`) will now not match widgets with the class `-textual-system` (scrollbars, notifications etc) https://github.com/Textualize/textual/pull/5113 +- Renamed `Screen.can_view` and `Widget.can_view` to `Screen.can_view_entire` and `Widget.can_view_entire` https://github.com/Textualize/textual/pull/5174 ### Added @@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `textual._loop.loop_from_index` https://github.com/Textualize/textual/pull/5164 - Added `min_color` and `max_color` to Sparklines constructor, which take precedence over CSS https://github.com/Textualize/textual/pull/5174 - Added new demo `python -m textual`, not *quite* finished but better than the old one https://github.com/Textualize/textual/pull/5174 +- Added `Screen.can_view_partial` and `Widget.can_view_partial` https://github.com/Textualize/textual/pull/5174 ### Fixed diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index edb3e1e6ca..7432dbdde8 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -339,25 +339,25 @@ def on_mount(self) -> None: log = self.query_one(Log) rich_log = self.query_one(RichLog) log.write("I am a Log Widget") - rich_log.write("I am a [b]Rich Log Widget") + rich_log.write("I am a Rich Log Widget") self.set_interval(0.25, self.update_log) self.set_interval(1, self.update_rich_log) def update_log(self) -> None: """Update the Log with new content.""" - if not self.screen.can_view(self) or not self.screen.is_active: + log = self.query_one(Log) + if not self.screen.can_view_partial(log) or not self.screen.is_active: return self.log_count += 1 - log = self.query_one(Log) line_no = self.log_count % len(self.TEXT) line = self.TEXT[self.log_count % len(self.TEXT)] log.write_line(f"fear[{line_no}] = {line!r}") def update_rich_log(self) -> None: """Update the Rich Log with content.""" - if not self.screen.can_view(self) or not self.screen.is_active: - return rich_log = self.query_one(RichLog) + if not self.screen.can_view_partial(rich_log) or not self.screen.is_active: + return self.rich_log_count += 1 log_option = self.rich_log_count % 3 if log_option == 0: @@ -425,7 +425,7 @@ def on_mount(self) -> None: def update_sparks(self) -> None: """Update the sparks data.""" - if not self.screen.can_view(self) or not self.screen.is_active: + if not self.screen.can_view_partial(self) or not self.screen.is_active: return self.count += 1 offset = self.count * 40 diff --git a/src/textual/screen.py b/src/textual/screen.py index f60cad4aa9..ab9b187efa 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -896,7 +896,7 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: def scroll_to_center(widget: Widget) -> None: """Scroll to center (after a refresh).""" - if self.focused is widget and not self.can_view(widget): + if self.focused is widget and not self.can_view_entire(widget): self.scroll_to_center(widget, origin_visible=True) self.call_later(scroll_to_center, widget) @@ -1480,24 +1480,44 @@ async def action_dismiss(self, result: ScreenResultType | None = None) -> None: await self._flush_next_callbacks() self.dismiss(result) - def can_view(self, widget: Widget) -> bool: - """Check if a given widget is in the current view (scrollable area). + def can_view_entire(self, widget: Widget) -> bool: + """Check if a given widget is fully within the current screen. Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible. Args: - widget: A widget that is a descendant of self. + widget: A widget. Returns: - True if the entire widget is in view, False if it is partially visible or not in view. + `True` if the entire widget is in view, `False` if it is partially visible or not in view. """ + if widget not in self._compositor.visible_widgets: + return False + # If the widget is one that overlays the screen... + if widget.styles.overlay == "screen": + # ...simply check if it's within the screen's region. + return widget.region in self.region + # Failing that fall back to normal checking. + return super().can_view_entire(widget) + + def can_view_partial(self, widget: Widget) -> bool: + """Check if a given widget is at least partially within the current view. + + Args: + widget: A widget. + + Returns: + `True` if the any part of the widget is in view, `False` if it is completely outside of the screen. + """ + if widget not in self._compositor.visible_widgets: + return False # If the widget is one that overlays the screen... if widget.styles.overlay == "screen": # ...simply check if it's within the screen's region. return widget.region in self.region # Failing that fall back to normal checking. - return super().can_view(widget) + return super().can_view_partial(widget) def validate_title(self, title: Any) -> str | None: """Ensure the title is a string or `None`.""" diff --git a/src/textual/widget.py b/src/textual/widget.py index 53392a69bc..7c71bb885c 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -3303,8 +3303,8 @@ def scroll_to_center( immediate=immediate, ) - def can_view(self, widget: Widget) -> bool: - """Check if a given widget is in the current view (scrollable area). + def can_view_entire(self, widget: Widget) -> bool: + """Check if a given widget is *fully* within the current view (scrollable area). Note: This doesn't necessarily equate to a widget being visible. There are other reasons why a widget may not be visible. @@ -3313,11 +3313,38 @@ def can_view(self, widget: Widget) -> bool: widget: A widget that is a descendant of self. Returns: - True if the entire widget is in view, False if it is partially visible or not in view. + `True` if the entire widget is in view, `False` if it is partially visible or not in view. """ if widget is self: return True + if widget not in self.screen._compositor.visible_widgets: + return False + + region = widget.region + node: Widget = widget + + while isinstance(node.parent, Widget) and node is not self: + if region not in node.parent.scrollable_content_region: + return False + node = node.parent + return True + + def can_view_partial(self, widget: Widget) -> bool: + """Check if a given widget at least partially visible within the current view (scrollable area). + + Args: + widget: A widget that is a descendant of self. + + Returns: + `True` if any part of the widget is visible, `False` if it is outside of the viewable area. + """ + if widget is self: + return True + + if widget not in self.screen._compositor.visible_widgets or not widget.display: + return False + region = widget.region node: Widget = widget