Skip to content

Commit

Permalink
new loading indicator
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Oct 2, 2024
1 parent ffc8174 commit b5acfef
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 58 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 24 additions & 14 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -715,7 +725,7 @@ def add_widget(
)

map[widget] = _MapGeometry(
region + layout_offset,
(region + layout_offset),
order,
clip,
total_region.size,
Expand All @@ -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,
Expand Down
107 changes: 65 additions & 42 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from typing import (
TYPE_CHECKING,
AsyncGenerator,
Awaitable,
ClassVar,
Collection,
Generator,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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")

Expand Down Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions src/textual/widgets/_loading_indicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit b5acfef

Please sign in to comment.