Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Documentation.
Browse files Browse the repository at this point in the history
rodrigogiraoserrao committed Apr 24, 2023
1 parent 8f2fae5 commit 35597ef
Showing 1 changed file with 82 additions and 12 deletions.
94 changes: 82 additions & 12 deletions src/textual/widgets/_progress_bar.py
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@

from ..app import ComposeResult, RenderResult
from ..containers import Horizontal
from ..message import Message
from ..reactive import reactive
from ..renderables.bar import RenderableBar
from ..widget import Widget
@@ -33,16 +34,20 @@ class Bar(Widget, can_focus=False):

DEFAULT_CSS = """
Bar {
width: 1fr;
width: 32;
height: 1;
}
Bar > .bar--bar, Bar > .bar--indeterminate {
background: $foreground 10%;
Bar > .bar--bar {
color: $warning;
background: $foreground 10%;
}
Bar > .bar--complete {
Bar > .bar--indeterminate {
color: $error;
background: $foreground 10%;
}
Bar > .bar--complete {
color: $success;
background: $foreground 10%;
}
"""

@@ -56,6 +61,7 @@ class Bar(Widget, can_focus=False):
"""Internal timer used to render the bar when it's in the indeterminate state."""

def watch__completion_percentage(self, percentage: float | None) -> None:
"""Manage the timer that enables the indeterminate bar animation."""
if percentage is not None:
if self._refresh_timer:
self._refresh_timer.stop()
@@ -65,30 +71,34 @@ def watch__completion_percentage(self, percentage: float | None) -> None:
self._refresh_timer = self.set_interval(1 / 60, self.refresh)

def render(self) -> RenderResult:
"""Render the bar with the correct portion filled."""
if self._completion_percentage is None:
return self.render_indeterminate()
else:
width = self.size.width
bar_style = (
self.get_component_rich_style("bar--bar")
if self._completion_percentage < 1
else self.get_component_rich_style("bar--complete")
)
return RenderableBar(
highlight_range=(0, width * self._completion_percentage),
highlight_range=(0, self.size.width * self._completion_percentage),
highlight_style=Style.from_color(bar_style.color),
background_style=Style.from_color(bar_style.bgcolor),
)

def render_indeterminate(self) -> RenderResult:
"""Render a frame of the indeterminate progress bar animation."""
width = self.size.width
highlighted_bar_width = 0.25 * width
# Width used to enable the visual effect of the bar going into the corners.
total_imaginary_width = width + highlighted_bar_width

speed = 30 # Cells per second.
# Compute the position of the bar.
start = (speed * monotonic()) % (2 * total_imaginary_width)
if start > total_imaginary_width:
start = 2 * total_imaginary_width - start
# If the bar is to the right of its width, wrap it back from right to left.
start = 2 * total_imaginary_width - start # = (tiw - (start - tiw))
start -= highlighted_bar_width
end = start + highlighted_bar_width

@@ -142,9 +152,11 @@ def watch__completion_percentage(self, percentage: float | None) -> None:
if percentage is None:
self.renderable = "--:--:--"
else:
# Keep track of the starting time.
if self._start_time is None:
self._start_time = monotonic()

# If we are not notified of progress for some time, periodically refresh
# the countdown so that the ETA is updated either way.
if self._refresh_timer is None:
self._refresh_timer = self.set_interval(1 / 2, self.update_eta)

@@ -154,15 +166,20 @@ def watch__completion_percentage(self, percentage: float | None) -> None:
def update_eta(self) -> None:
"""Update the ETA display."""
percentage = self._completion_percentage
# We display --:--:-- if we haven't started, if we are done,
# or if we don't know when we started keeping track of time.
if not percentage or percentage >= 1 or self._start_time is None:
self.renderable = "--:--:--"
# If we are done, we can delete the timer that periodically refreshes
# the countdown display.
if (
percentage is not None
and percentage >= 1
and self._refresh_timer is not None
):
self._refresh_timer.stop()
self._refresh_timer = None
# Render a countdown timer with hh:mm:ss, unless it's a LONG time.
else:
delta = monotonic() - self._start_time
left = ceil((delta / percentage) * (1 - percentage))
@@ -192,14 +209,50 @@ class ProgressBar(Widget, can_focus=False):

progress: reactive[float] = reactive(0.0)
"""The progress so far, in number of steps."""
total: reactive[float | None] = reactive[Optional[float]](100.0)
total: reactive[float | None] = reactive[Optional[float]](None)
"""The total number of steps associated with this progress bar, when known.
The value `None` will render an indeterminate progress bar.
Once `total` is set to a numerical value, it cannot be set back to `None`
Once `total` is set to a numerical value, it cannot be set back to `None`.
"""
percentage: reactive[float | None] = reactive[Optional[float]](None)
"""The percentage of progress that has been completed."""
_started = False
"""Whether the total was set to a non-`None` value before."""

class Completed(Message):
"""Sent when the progress reaches 100%.
Can be handled using `on_progress_bar_completed` in a subclass of
[`ProgressBar`][textual.widgets.ProgressBar] or in a parent widget in the DOM.
Attributes:
progress_bar: The progress bar that reached 100% completion.
"""

def __init__(self, progress_bar: ProgressBar) -> None:
self.progress_bar = progress_bar
super().__init__()

class Started(Message):
"""Sent when the progress bar starts tracking progress.
This message is sent when the total progress is set to a non-`None` value.
This message may be sent _before_ the progress bar is mounted if the progress
bar is instantiated with a non-`None` value for the total number of steps.
Can be handled using `on_progress_bar_started` in a subclass of
[`ProgressBar`][textual.widgets.ProgressBar] or in a parent widget in the DOM.
Attributes:
progress_bar: The progress bar that started tracking progress.
total: The total number of steps that the bar will track.
"""

def __init__(self, progress_bar: ProgressBar, total: float) -> None:
self.progress_bar = progress_bar
self.total = total
super().__init__()

def __init__(
self,
@@ -245,7 +298,6 @@ def key_space(self):
self.hide_percentage = hide_percentage
self.hide_eta = hide_eta

self.percentage = None # Without this, assigning self.total breaks. 🤔
self.total = total

def compose(self) -> ComposeResult:
@@ -277,13 +329,31 @@ def compose(self) -> ComposeResult:
yield eta_status

def validate_progress(self, progress: float) -> float:
"""Prevent the progress to go over the total number of steps."""
if self.total is not None:
return min(progress, self.total)
return progress

def watch_total(self, total: float | None) -> None:
"""Post the message [Started][textual.widgets.ProgressBar.Started]."""
if total is None:
return
if not self._started:
self.post_message(ProgressBar.Started(self, total))
self._started = True

def compute_percentage(self) -> float | None:
"""Keep the percentage of progress updated automatically.
This compute method will also post the message
[Completed][textual.widgets.ProgressBar.Completed] when the percentage
reaches 1.
"""
if self.total is not None:
return self.progress / self.total
percentage = self.progress / self.total
if percentage >= 1:
self.post_message(ProgressBar.Completed(self))
return percentage
return None

def advance(self, advance: float = 1) -> None:

0 comments on commit 35597ef

Please sign in to comment.