From b5acfef6279d7a87e8d138de637136ec2eb8d38a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 2 Oct 2024 14:32:07 +0100 Subject: [PATCH] new loading indicator --- CHANGELOG.md | 1 + src/textual/_compositor.py | 38 +++++--- src/textual/widget.py | 107 +++++++++++++--------- src/textual/widgets/_loading_indicator.py | 5 +- 4 files changed, 93 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab342eef44..1c7e14437a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed issue with screen not updating when auto_refresh was enabled https://github.com/Textualize/textual/pull/5063 +- Fixed issues regarding loading indicator https://github.com/Textualize/textual/issues/3935 ### Added diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 2b09180db1..7a3f0c8fe7 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -664,6 +664,17 @@ def add_widget( get_layer_index = layers_to_index.get + if widget._cover_widget: + map[widget._cover_widget] = _MapGeometry( + region.shrink(widget.styles.gutter), + order, + clip, + region.size, + container_size, + virtual_region, + dock_gutter, + ) + # Add all the widgets for sub_region, _, sub_widget, z, fixed, overlay in reversed( placements @@ -681,18 +692,17 @@ def add_widget( widget_region = self._constrain( sub_widget.styles, widget_region, no_clip ) - - add_widget( - sub_widget, - sub_region, - widget_region, - ((1, 0, 0),) if overlay else widget_order, - layer_order, - no_clip if overlay else sub_clip, - visible, - arrange_result.scroll_spacing, - ) - + if widget._cover_widget is None: + add_widget( + sub_widget, + sub_region, + widget_region, + ((1, 0, 0),) if overlay else widget_order, + layer_order, + no_clip if overlay else sub_clip, + visible, + arrange_result.scroll_spacing, + ) layer_order -= 1 if visible: @@ -715,7 +725,7 @@ def add_widget( ) map[widget] = _MapGeometry( - region + layout_offset, + (region + layout_offset), order, clip, total_region.size, @@ -737,7 +747,7 @@ def add_widget( if styles.constrain != "none": widget_region = self._constrain(styles, widget_region, no_clip) - map[widget] = _MapGeometry( + map[widget._render_widget] = _MapGeometry( widget_region, order, clip, diff --git a/src/textual/widget.py b/src/textual/widget.py index e55b937da7..e1673dcdf3 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -14,7 +14,6 @@ from typing import ( TYPE_CHECKING, AsyncGenerator, - Awaitable, ClassVar, Collection, Generator, @@ -58,7 +57,6 @@ from textual._styles_cache import StylesCache from textual._types import AnimationLevel from textual.actions import SkipAction -from textual.await_complete import AwaitComplete from textual.await_remove import AwaitRemove from textual.box_model import BoxModel from textual.cache import FIFOCache @@ -333,6 +331,38 @@ class Widget(DOMNode): loading: Reactive[bool] = Reactive(False) """If set to `True` this widget will temporarily be replaced with a loading indicator.""" + virtual_size: Reactive[Size] = Reactive(Size(0, 0), layout=True) + """The virtual (scrollable) [size][textual.geometry.Size] of the widget.""" + + has_focus: Reactive[bool] = Reactive(False, repaint=False) + """Does this widget have focus? Read only.""" + + mouse_hover: Reactive[bool] = Reactive(False, repaint=False) + """Is the mouse over this widget? Read only.""" + + scroll_x: Reactive[float] = Reactive(0.0, repaint=False, layout=False) + """The scroll position on the X axis.""" + + scroll_y: Reactive[float] = Reactive(0.0, repaint=False, layout=False) + """The scroll position on the Y axis.""" + + scroll_target_x = Reactive(0.0, repaint=False) + """Scroll target destination, X coord.""" + + scroll_target_y = Reactive(0.0, repaint=False) + """Scroll target destination, Y coord.""" + + show_vertical_scrollbar: Reactive[bool] = Reactive(False, layout=True) + """Show a vertical scrollbar?""" + + show_horizontal_scrollbar: Reactive[bool] = Reactive(False, layout=True) + """Show a horizontal scrollbar?""" + + border_title: str | Text | None = _BorderTitle() # type: ignore + """A title to show in the top border (if there is one).""" + border_subtitle: str | Text | None = _BorderTitle() # type: ignore + """A title to show in the bottom border (if there is one).""" + # Default sort order, incremented by constructor _sort_order: ClassVar[int] = 0 @@ -430,38 +460,8 @@ def __init__( """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.""" - - has_focus: Reactive[bool] = Reactive(False, repaint=False) - """Does this widget have focus? Read only.""" - - mouse_hover: Reactive[bool] = Reactive(False, repaint=False) - """Is the mouse over this widget? Read only.""" - - scroll_x: Reactive[float] = Reactive(0.0, repaint=False, layout=False) - """The scroll position on the X axis.""" - - scroll_y: Reactive[float] = Reactive(0.0, repaint=False, layout=False) - """The scroll position on the Y axis.""" - - scroll_target_x = Reactive(0.0, repaint=False) - """Scroll target destination, X coord.""" - - scroll_target_y = Reactive(0.0, repaint=False) - """Scroll target destination, Y coord.""" - - show_vertical_scrollbar: Reactive[bool] = Reactive(False, layout=True) - """Show a vertical scrollbar?""" - - show_horizontal_scrollbar: Reactive[bool] = Reactive(False, layout=True) - """Show a horizontal scrollbar?""" - - border_title: str | Text | None = _BorderTitle() # type: ignore - """A title to show in the top border (if there is one).""" - border_subtitle: str | Text | None = _BorderTitle() # type: ignore - """A title to show in the bottom border (if there is one).""" + self._cover_widget: Widget | None = None + """Widget to render over this widget (used by loading indicator).""" @property def is_mounted(self) -> bool: @@ -587,6 +587,31 @@ def is_maximized(self) -> bool: except NoScreen: return False + @property + def _render_widget(self) -> Widget: + return self._cover_widget if self._cover_widget is not None else self + + def _cover(self, widget: Widget) -> None: + """Set a widget used to replace the visuals of this widget (used for loading indicator). + + Args: + widget: A newly constructed, but unmounted widget. + """ + self._uncover() + self._cover_widget = widget + widget._parent = self + widget._start_messages() + widget._post_register(self.app) + self.app.stylesheet.apply(widget) + self.refresh(layout=True) + + def _uncover(self) -> None: + """Remove any widget, previously set via [`_cover`][textual.widget.Widget._cover].""" + if self._cover_widget is not None: + self._cover_widget.remove() + self._cover_widget = None + self.refresh(layout=True) + 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. @@ -716,7 +741,7 @@ def get_loading_widget(self) -> Widget: loading_widget = self.app.get_loading_widget() return loading_widget - def set_loading(self, loading: bool) -> Awaitable: + def set_loading(self, loading: bool) -> None: """Set or reset the loading state of this widget. A widget in a loading state will display a LoadingIndicator that obscures the widget. @@ -728,19 +753,16 @@ def set_loading(self, loading: bool) -> Awaitable: An optional awaitable. """ LOADING_INDICATOR_CLASS = "-textual-loading-indicator" - LOADING_INDICATOR_QUERY = f".{LOADING_INDICATOR_CLASS}" - remove_indicator = self.query_children(LOADING_INDICATOR_QUERY).remove() if loading: loading_indicator = self.get_loading_widget() loading_indicator.add_class(LOADING_INDICATOR_CLASS) - await_mount = self.mount(loading_indicator) - return AwaitComplete(remove_indicator, await_mount).call_next(self) + self._cover(loading_indicator) else: - return remove_indicator + self._uncover() - async def _watch_loading(self, loading: bool) -> None: + def _watch_loading(self, loading: bool) -> None: """Called when the 'loading' reactive is changed.""" - await self.set_loading(loading) + self.set_loading(loading) ExpectType = TypeVar("ExpectType", bound="Widget") @@ -3993,6 +4015,7 @@ def _on_scroll_to_region(self, message: messages.ScrollToRegion) -> None: self.scroll_to_region(message.region, animate=True) def _on_unmount(self) -> None: + self._uncover() self.workers.cancel_node(self) def action_scroll_home(self) -> None: diff --git a/src/textual/widgets/_loading_indicator.py b/src/textual/widgets/_loading_indicator.py index 6e93a7b94a..fdc31793c0 100644 --- a/src/textual/widgets/_loading_indicator.py +++ b/src/textual/widgets/_loading_indicator.py @@ -18,11 +18,12 @@ class LoadingIndicator(Widget): DEFAULT_CSS = """ LoadingIndicator { - width: 100%; - height: 100%; + # width: 100%; + # height: 100%; min-height: 1; content-align: center middle; color: $accent; + text-style: not reverse; } LoadingIndicator.-textual-loading-indicator { layer: _loading;