Skip to content

Commit

Permalink
Add new color scheme and thickness to progress bar
Browse files Browse the repository at this point in the history
- Rainbow scheme will update bar color based on percentage
starting from .bar--bar.color up to .bar--complete.color in
counterclockwise direction.

- Bar thickness can have 3 styles: thin, normal or thick.
  • Loading branch information
Peter Talaber authored and Peter Talaber committed Oct 9, 2023
1 parent f69ef53 commit aaf0e08
Show file tree
Hide file tree
Showing 11 changed files with 1,204 additions and 31 deletions.
22 changes: 22 additions & 0 deletions docs/examples/widgets/progress_bar_styled_rainbow.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: blue;
background: $primary 30%;
}

Bar > .bar--complete {
color: red;
}

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

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


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

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

def compose(self) -> ComposeResult:
with Center():
with Middle():
yield ProgressBar(color_scheme="rainbow")
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__":
app = StyledExtProgressBar()
app.run()
52 changes: 52 additions & 0 deletions docs/examples/widgets/progress_bar_styled_rainbow_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from textual.app import App, ComposeResult
from textual.containers import Center, Middle
from textual.timer import Timer
from textual.widgets import Footer, ProgressBar


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

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

def compose(self) -> ComposeResult:
with Center():
with Middle():
yield ProgressBar(color_scheme="rainbow")
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_1(self) -> None:
self._action_common_keypress(10)

def key_5(self) -> None:
self._action_common_keypress(50)

def key_9(self) -> None:
self._action_common_keypress(90)

def _action_common_keypress(self, progress: int) -> None:
# Freeze time for the indeterminate progress bar.
self.query_one(ProgressBar).query_one("#eta")._get_elapsed_time = lambda: 0
self.query_one(ProgressBar).update(total=100, progress=progress)
self.progress_timer.pause()

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


class StyledExtProgressBar(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(thickness=2)
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__":
app = StyledExtProgressBar()
app.run()
53 changes: 53 additions & 0 deletions docs/examples/widgets/progress_bar_styled_thickness_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from textual.app import App, ComposeResult
from textual.containers import Center, Middle
from textual.timer import Timer
from textual.widgets import Footer, ProgressBar


class StyledExtProgressBar(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_0(self) -> None:
self._action_common_keypress(0)

def key_1(self) -> None:
self._action_common_keypress(1)

def key_2(self) -> None:
self._action_common_keypress(2)

def _action_common_keypress(self, thickness: int) -> None:
# Freeze time for the indeterminate progress bar.
self.query_one(ProgressBar).query_one("#eta")._get_elapsed_time = lambda: 0
self.query_one(ProgressBar).query_one("#bar").thickness = thickness
self.query_one(ProgressBar).update(total=100, progress=50)
self.progress_timer.pause()

if __name__ == "__main__":
app = StyledExtProgressBar()
app.run()
71 changes: 67 additions & 4 deletions docs/widgets/progress_bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ It shows the progress bar in:

=== "39% done"

```{.textual path="docs/examples/widgets/progress_bar_isolated_.py" press="t"}
```{.textual path="docs/examples/widgets/progress_bar_styled_.py" press="t"}
```

=== "Completed"
Expand Down Expand Up @@ -104,16 +104,79 @@ Refer to the [section below](#styling-the-progress-bar) for more information.
--8<-- "docs/examples/widgets/progress_bar_styled.tcss"
```

### Rainbow Color Schemes

The rainbow color scheme gradually changes the color of the bar as it progresses.
The initial color is defined by the component class `bar--bar` and the final color by the component class `bar--complete`.
(The gradient is computed in the HSL color space in the counterclockwise direction.)

=== "Rainbow at 10%"

```{.textual path="docs/examples/widgets/progress_bar_styled_rainbow_.py" press="1"}
```

=== "Rainbow at 50%"

```{.textual path="docs/examples/widgets/progress_bar_styled_rainbow_.py" press="5"}
```

=== "Rainbow at 90%"

```{.textual path="docs/examples/widgets/progress_bar_styled_rainbow_.py" press="9"}
```
=== "progress_bar_styled_rainbow.py"

```python
--8<-- "docs/examples/widgets/progress_bar_styled_rainbow.py"
```

=== "progress_bar_styled.css"

```sass
--8<-- "docs/examples/widgets/progress_bar_styled_rainbow.css"
```

### Progress Bar Thickness Styling

This shows a progress bar with custom bar thickness: thin, normal or thick.

=== "Thin"

```{.textual path="docs/examples/widgets/progress_bar_styled_thickness_.py" press="0"}
```

=== "Normal"

```{.textual path="docs/examples/widgets/progress_bar_styled_thickness_.py" press="1"}
```

=== "Thick"

```{.textual path="docs/examples/widgets/progress_bar_styled_thickness_.py" press="2"}
```
=== "progress_bar_styled_thickness.py"

```python
--8<-- "docs/examples/widgets/progress_bar_styled_thickness.py"
```

=== "progress_bar_styled.css"

```sass
--8<-- "docs/examples/widgets/progress_bar_styled.css"
```

## Styling the Progress Bar

The progress bar is composed of three sub-widgets that can be styled independently:

| Widget name | ID | Description |
| ------------------ | ------------- | ---------------------------------------------------------------- |
| `Bar` | `#bar` | The bar that visually represents the progress made. |
| `Bar` | `#bar` | Bar that visually represents the progress made. |
| `PercentageStatus` | `#percentage` | [Label](./label.md) that shows the percentage of completion. |
| `ETAStatus` | `#eta` | [Label](./label.md) that shows the estimated time to completion. |


### Bar Component Classes

::: textual.widgets._progress_bar.Bar.COMPONENT_CLASSES
Expand All @@ -125,9 +188,9 @@ The progress bar is composed of three sub-widgets that can be styled independent

| Name | Type | Default | Description |
| ------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------- |
| `percentage` | `float | None` | The read-only percentage of progress that has been made. This is `None` if the `total` hasn't been set. |
| `percentage` | `float` | `None` | The read-only percentage of progress that has been made. This is `None` if the `total` hasn't been set. |
| `progress` | `float` | `0` | The number of steps of progress already made. |
| `total` | `float | None` | The total number of steps that we are keeping track of. |
| `total` | `float` | `None` | The total number of steps that we are keeping track of. |

## Messages

Expand Down
26 changes: 9 additions & 17 deletions src/textual/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,9 +393,9 @@ def hsl_blend(
This method calculates a new color on a gradient using the HSL color space.
The position on the gradient is given by `factor`, which is a float between -1 and 1, where 0 is the original color, and 1 or -1 is the `destination` color.
A negative `factor` affects the direction of the hue angle, a positive number is in the clockwise direction, a negative number is in the counter-clockwise direction.
For lightess and saturation, only the absolute value of `factor` is used.
A value of `gradient` between the two extremes produces a color somewhere between the two end points.
The sign of `factor` affects the direction of the hue angle, a positive number is in the clockwise direction, a negative number is in the counter-clockwise direction.
For lightness and saturation, only the absolute value of `factor` is used.
A value of `factor` between the two extremes produces a color somewhere between the two end points.
Args:
destination: Another color.
Expand All @@ -405,10 +405,10 @@ def hsl_blend(
Returns:
A new color.
"""
abs_factor = factor if factor >= 0 else -1.0 * factor
abs_factor = abs(factor)
if factor == 0:
return self
elif factor >= 1 or factor <= -1:
elif abs_factor >= 1:
return destination

hsl_1 = self.hsl
Expand All @@ -421,19 +421,11 @@ def hsl_blend(
else:
new_alpha = alpha

# When the factor is > 0, hue is clockwise, otherwise it is counter-clockwise.
if factor > 0:
if hsl_1.h <= hsl_2.h:
new_h = hsl_1.h + (hsl_2.h - hsl_1.h) * abs_factor
else:
new_h = hsl_1.h + (hsl_2.h + 1.0 - hsl_1.h) * abs_factor
new_h = new_h - 1.0 if new_h >= 1.0 else new_h
sign = 1 if factor > 0 else -1
if (sign * hsl_1.h) <= (sign * hsl_2.h):
new_h = hsl_1.h + (hsl_2.h - hsl_1.h) * abs_factor
else:
if hsl_1.h >= hsl_2.h:
new_h = hsl_1.h + (hsl_2.h - hsl_1.h) * abs_factor
else:
new_h = (hsl_1.h + (hsl_2.h - 1.0 - hsl_1.h) * abs_factor)
new_h = new_h + 1.0 if new_h < 0.0 else new_h
new_h = (hsl_1.h + (hsl_2.h + sign - hsl_1.h) * abs_factor) % 1

new_s = hsl_1.s + (hsl_2.s - hsl_1.s) * abs_factor
new_l = hsl_1.l + (hsl_2.l - hsl_1.l) * abs_factor
Expand Down
Loading

0 comments on commit aaf0e08

Please sign in to comment.