Skip to content

Commit

Permalink
Implement a Progress Bar widget. (#2333)
Browse files Browse the repository at this point in the history
* First prototype of PB.

* Repurpose UnderlineBar.

* Factor out 'Bar' widget.

* Revert "Factor out 'Bar' widget."

This reverts commit 0bb4871.

* Add Bar widget.

* Cap progress at 100%.

* Add skeleton for the ETA label.

[skip ci]

* Add ETA display.

* Improve docstrings.

* Directly compute percentage.

* Watch percentage changes directly.

[skip ci]

* Documentation.

* Make reactive percentage private.

Instead, we create a public read-only percentage property.

* Update griffe to fix documentation issue.

Related issues: #1572, mkdocstrings/griffe#128.
Related PRs: mkdocstrings/griffe#135.

* Add example and docs.

* Address review feedback.

[skip ci]

* More documentation.

* Add tests.

* Changelog.

* More tests.

* Fix/fake tests.

* Final tweaks.
  • Loading branch information
rodrigogiraoserrao authored Apr 26, 2023
1 parent ee0d407 commit 4148b1d
Show file tree
Hide file tree
Showing 20 changed files with 2,242 additions and 262 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- `ScrollView` now inherits from `ScrollableContainer` rather than `Widget` https://github.com/Textualize/textual/issues/2332
- Containers no longer inherit any bindings from `Widget` https://github.com/Textualize/textual/issues/2331
- Added `ScrollableContainer`; a container class that binds the common navigation keys to scroll actions (see also above breaking change) https://github.com/Textualize/textual/issues/2332
- Added `ProgressBar` widget https://github.com/Textualize/textual/pull/2333

### Fixed

Expand Down
22 changes: 22 additions & 0 deletions docs/examples/widgets/progress_bar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Container {
overflow: hidden hidden;
height: auto;
}

Center {
margin-top: 1;
margin-bottom: 1;
layout: horizontal;
}

ProgressBar {
padding-left: 3;
}

Input {
width: 16;
}

VerticalScroll {
height: auto;
}
40 changes: 40 additions & 0 deletions docs/examples/widgets/progress_bar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from textual.app import App, ComposeResult
from textual.containers import Center, VerticalScroll
from textual.widgets import Button, Header, Input, Label, ProgressBar


class FundingProgressApp(App[None]):
CSS_PATH = "progress_bar.css"

TITLE = "Funding tracking"

def compose(self) -> ComposeResult:
yield Header()
with Center():
yield Label("Funding: ")
yield ProgressBar(total=100, show_eta=False) # (1)!
with Center():
yield Input(placeholder="$$$")
yield Button("Donate")

yield VerticalScroll(id="history")

def on_button_pressed(self) -> None:
self.add_donation()

def on_input_submitted(self) -> None:
self.add_donation()

def add_donation(self) -> None:
text_value = self.query_one(Input).value
try:
value = int(text_value)
except ValueError:
return
self.query_one(ProgressBar).advance(value)
self.query_one(VerticalScroll).mount(Label(f"Donation for ${value} received!"))
self.query_one(Input).value = ""


if __name__ == "__main__":
FundingProgressApp().run()
34 changes: 34 additions & 0 deletions docs/examples/widgets/progress_bar_isolated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from textual.app import App, ComposeResult
from textual.containers import Center, Middle
from textual.timer import Timer
from textual.widgets import Footer, ProgressBar


class IndeterminateProgressBar(App[None]):
BINDINGS = [("s", "start", "Start")]

progress_timer: Timer
"""Timer to simulate progress happening."""

def compose(self) -> ComposeResult:
with Center():
with Middle():
yield ProgressBar()
yield Footer()

def on_mount(self) -> None:
"""Set up a timer to simulate progess happening."""
self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)

def make_progress(self) -> None:
"""Called automatically to advance the progress bar."""
self.query_one(ProgressBar).advance(1)

def action_start(self) -> None:
"""Start the progress tracking."""
self.query_one(ProgressBar).update(total=100)
self.progress_timer.resume()


if __name__ == "__main__":
IndeterminateProgressBar().run()
46 changes: 46 additions & 0 deletions docs/examples/widgets/progress_bar_isolated_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from textual.app import App, ComposeResult
from textual.containers import Center, Middle
from textual.timer import Timer
from textual.widgets import Footer, ProgressBar


class IndeterminateProgressBar(App[None]):
BINDINGS = [("s", "start", "Start")]

progress_timer: Timer
"""Timer to simulate progress happening."""

def compose(self) -> ComposeResult:
with Center():
with Middle():
yield ProgressBar()
yield Footer()

def on_mount(self) -> None:
"""Set up a timer to simulate progess happening."""
self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)

def make_progress(self) -> None:
"""Called automatically to advance the progress bar."""
self.query_one(ProgressBar).advance(1)

def action_start(self) -> None:
"""Start the progress tracking."""
self.query_one(ProgressBar).update(total=100)
self.progress_timer.resume()

def key_f(self) -> None:
# Freeze time for the indeterminate progress bar.
self.query_one(ProgressBar).query_one("#bar")._get_elapsed_time = lambda: 5

def key_t(self) -> None:
# Freeze time to show always the same ETA.
self.query_one(ProgressBar).query_one("#eta")._get_elapsed_time = lambda: 3.9
self.query_one(ProgressBar).update(total=100, progress=39)

def key_u(self) -> None:
self.query_one(ProgressBar).update(total=100, progress=100)


if __name__ == "__main__":
IndeterminateProgressBar().run()
22 changes: 22 additions & 0 deletions docs/examples/widgets/progress_bar_styled.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Bar > .bar--indeterminate {
color: $primary;
background: $secondary;
}

Bar > .bar--bar {
color: $primary;
background: $primary 30%;
}

Bar > .bar--complete {
color: $error;
}

PercentageStatus {
text-style: reverse;
color: $secondary;
}

ETAStatus {
text-style: underline;
}
35 changes: 35 additions & 0 deletions docs/examples/widgets/progress_bar_styled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from textual.app import App, ComposeResult
from textual.containers import Center, Middle
from textual.timer import Timer
from textual.widgets import Footer, ProgressBar


class StyledProgressBar(App[None]):
BINDINGS = [("s", "start", "Start")]
CSS_PATH = "progress_bar_styled.css"

progress_timer: Timer
"""Timer to simulate progress happening."""

def compose(self) -> ComposeResult:
with Center():
with Middle():
yield ProgressBar()
yield Footer()

def on_mount(self) -> None:
"""Set up a timer to simulate progess happening."""
self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)

def make_progress(self) -> None:
"""Called automatically to advance the progress bar."""
self.query_one(ProgressBar).advance(1)

def action_start(self) -> None:
"""Start the progress tracking."""
self.query_one(ProgressBar).update(total=100)
self.progress_timer.resume()


if __name__ == "__main__":
StyledProgressBar().run()
47 changes: 47 additions & 0 deletions docs/examples/widgets/progress_bar_styled_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from textual.app import App, ComposeResult
from textual.containers import Center, Middle
from textual.timer import Timer
from textual.widgets import Footer, ProgressBar


class StyledProgressBar(App[None]):
BINDINGS = [("s", "start", "Start")]
CSS_PATH = "progress_bar_styled.css"

progress_timer: Timer
"""Timer to simulate progress happening."""

def compose(self) -> ComposeResult:
with Center():
with Middle():
yield ProgressBar()
yield Footer()

def on_mount(self) -> None:
"""Set up a timer to simulate progess happening."""
self.progress_timer = self.set_interval(1 / 10, self.make_progress, pause=True)

def make_progress(self) -> None:
"""Called automatically to advance the progress bar."""
self.query_one(ProgressBar).advance(1)

def action_start(self) -> None:
"""Start the progress tracking."""
self.query_one(ProgressBar).update(total=100)
self.progress_timer.resume()

def key_f(self) -> None:
# Freeze time for the indeterminate progress bar.
self.query_one(ProgressBar).query_one("#bar")._get_elapsed_time = lambda: 5

def key_t(self) -> None:
# Freeze time to show always the same ETA.
self.query_one(ProgressBar).query_one("#eta")._get_elapsed_time = lambda: 3.9
self.query_one(ProgressBar).update(total=100, progress=39)

def key_u(self) -> None:
self.query_one(ProgressBar).update(total=100, progress=100)


if __name__ == "__main__":
StyledProgressBar().run()
Loading

0 comments on commit 4148b1d

Please sign in to comment.