Border colour percentage (#1954)

* Allow setting an additional alpha on a border

See #1863.

* Update the ChangeLog

* Add snapshot tests for the border alpha value

* Extend the border snapshot tests

While this doesn't test *every* permutation, it covers enough bases that if
something were to change it should catch it.

* Tweak a typo in the border style examples

* Add border transparency percentage to the border docs

* Add a CSS example for using border transparency

* Add Color.multiply_alpha

* Update the CHANGELOG

* Multiply the alpha on a colour rather than replace it

As requested in
https://github.com/Textualize/textual/pull/1954#pullrequestreview-1328170386

(actually required while talking in person with Will, but noted in the
above)

* Multiply the alpha on a border colour rather than replace it

As requested in
https://github.com/Textualize/textual/pull/1954#pullrequestreview-1328170386

(actually requested while talking in person with Will, but noted in the
above)
This commit is contained in:
Dave Pearson
2023-03-07 14:14:17 +00:00
committed by GitHub
parent b9977812f7
commit b7de48cca3
7 changed files with 247 additions and 8 deletions

View File

@@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `radio_set` attribute to `RadioSet` events https://github.com/Textualize/textual/pull/1940
- Added `switch` attribute to `Switch` events https://github.com/Textualize/textual/pull/1940
- Breaking change: Added `toggle_button` attribute to RadioButton and Checkbox events, replaces `input` https://github.com/Textualize/textual/pull/1940
- A percentage alpha can now be applied to a border https://github.com/Textualize/textual/issues/1863
- Added `Color.multiply_alpha`.
### Fixed

View File

@@ -9,15 +9,15 @@ The `border` rule enables the drawing of a box around a widget.
## Syntax
--8<-- "docs/snippets/syntax_block_start.md"
border: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a>];
border: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a>] [<a href="../../css_types/percentage">&lt;percentage&gt;</a>];
border-top: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a>];
border-right: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a>];
border-bottom: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a>];
border-left: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a>];
border-top: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a>] [<a href="../../css_types/percentage">&lt;percentage&gt;</a>];
border-right: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a> [<a href="../../css_types/percentage">&lt;percentage&gt;</a>]];
border-bottom: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a> [<a href="../../css_types/percentage">&lt;percentage&gt;</a>]];
border-left: [<a href="../../css_types/border">&lt;border&gt;</a>] [<a href="../../css_types/color">&lt;color&gt;</a> [<a href="../../css_types/percentage">&lt;percentage&gt;</a>]];
--8<-- "docs/snippets/syntax_block_end.md"
The style `border` accepts an optional [`<border>`](../../css_types/border) that sets the visual style of the widget border and an optional [`<color>`](../../css_types/color) to set the color of the border.
The style `border` accepts an optional [`<border>`](../../css_types/border) that sets the visual style of the widget border, an optional [`<color>`](../../css_types/color) to set the color of the border, and an optional [`<percentage>`](../../css_types/percentage) to specify the color transparency.
Borders may also be set individually for the four edges of a widget with the `border-top`, `border-right`, `border-bottom` and `border-left` rules.
@@ -41,6 +41,7 @@ The CSS snippet above will add a solid green border around `Static` widgets, exc
If `<color>` is specified but `<border>` is not, it defaults to `"solid"`.
If `<border>` is specified but `<color>`is not, it defaults to green (RGB color `"#00FF00"`).
If `<percentage>` is not specified it defaults to `100%`.
## Border command
@@ -126,8 +127,11 @@ This example also shows that a widget cannot contain both a `border` and an `out
/* Set a heavy white border */
border: heavy white;
/* set a red border on the left */
/* Set a red border on the left */
border-left: outer red;
/* Set a rounded orange border, 50% transparency. */
border: round orange 50%;
```
## Python

View File

@@ -341,6 +341,18 @@ class Color(NamedTuple):
r, g, b, _ = self
return Color(r, g, b, alpha)
def multiply_alpha(self, alpha: float) -> Color:
"""Create a new color, multiplying the alpha with a new alpha.
Args:
alpha: The alpha value to multiple by.
Returns:
A new color.
"""
r, g, b, a = self
return Color(r, g, b, a * alpha)
def blend(
self, destination: Color, factor: float, alpha: float | None = None
) -> Color:

View File

@@ -441,6 +441,7 @@ class StylesBuilder:
def _parse_border(self, name: str, tokens: list[Token]) -> BorderValue:
border_type: EdgeType = "solid"
border_color = Color(0, 255, 0)
border_alpha: float | None = None
def border_value_error():
self.error(name, token, border_property_help_text(name, context="css"))
@@ -462,9 +463,18 @@ class StylesBuilder:
except ColorParseError:
border_value_error()
elif token_name == "scalar":
alpha_scalar = Scalar.parse(token.value)
if alpha_scalar.unit != Unit.PERCENT:
self.error(name, token, "alpha must be given as a percentage.")
border_alpha = alpha_scalar.value / 100.0
else:
border_value_error()
if border_alpha is not None:
border_color = border_color.multiply_alpha(border_alpha)
return normalize_border_value((border_type, border_color))
def _process_border_edge(self, edge: str, name: str, tokens: list[Token]) -> None:
@@ -612,7 +622,7 @@ class StylesBuilder:
if color is not None or alpha is not None:
if alpha is not None:
color = (color or Color(255, 255, 255)).with_alpha(alpha)
color = (color or Color(255, 255, 255)).multiply_alpha(alpha)
self.styles._rules[name] = color # type: ignore
process_tint = process_color

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
from textual.app import App, ComposeResult
from textual.containers import Grid
from textual.widget import Widget
class BorderAlphaApp(App[None]):
CSS = """
Grid {
height: 100%;
width: 100%;
grid-size: 2 2;
}
#b00 { border: 0%; }
#b01 { border: 33%; }
#b02 { border: 66%; }
#b03 { border: 100%; }
#b10 { border: solid 0%; }
#b11 { border: dashed 33%; }
#b12 { border: round 66%; }
#b13 { border: ascii 100%; }
#b20 { border: 0% red; }
#b21 { border: 33% orange; }
#b22 { border: 66% green; }
#b23 { border: 100% blue; }
#b30 { border: solid 0% red; }
#b31 { border: dashed 33% orange; }
#b32 { border: round 66% green; }
#b33 { border: ascii 100% blue; }
"""
def compose( self ) -> ComposeResult:
with Grid():
for outer in range(4):
with Grid():
for inner in range(4):
yield Widget(id=f"b{outer}{inner}")
if __name__ == "__main__":
BorderAlphaApp().run()

View File

@@ -240,6 +240,11 @@ def test_label_widths(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "label_widths.py")
def test_border_alpha(snap_compare):
"""Test setting a border alpha."""
assert snap_compare(SNAPSHOT_APPS_DIR / "border_alpha.py")
def test_auto_width_input(snap_compare):
assert snap_compare(
SNAPSHOT_APPS_DIR / "auto_width_input.py", press=["tab", *"Hello"]