diff --git a/CHANGELOG.md b/CHANGELOG.md index d69a02dee..69653cba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/styles/border.md b/docs/styles/border.md index 2e370fc16..5e9322909 100644 --- a/docs/styles/border.md +++ b/docs/styles/border.md @@ -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: [<border>] [<color>]; +border: [<border>] [<color>] [<percentage>]; -border-top: [<border>] [<color>]; -border-right: [<border>] [<color>]; -border-bottom: [<border>] [<color>]; -border-left: [<border>] [<color>]; +border-top: [<border>] [<color>] [<percentage>]; +border-right: [<border>] [<color> [<percentage>]]; +border-bottom: [<border>] [<color> [<percentage>]]; +border-left: [<border>] [<color> [<percentage>]]; --8<-- "docs/snippets/syntax_block_end.md" -The style `border` accepts an optional [``](../../css_types/border) that sets the visual style of the widget border and an optional [``](../../css_types/color) to set the color of the border. +The style `border` accepts an optional [``](../../css_types/border) that sets the visual style of the widget border, an optional [``](../../css_types/color) to set the color of the border, and an optional [``](../../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 `` is specified but `` is not, it defaults to `"solid"`. If `` is specified but ``is not, it defaults to green (RGB color `"#00FF00"`). +If `` 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 diff --git a/src/textual/color.py b/src/textual/color.py index 663fff2cf..a5bde511a 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -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: diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 26b67b07c..b98cd8c04 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -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 diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index ac2a53ff3..4c6089d71 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -158,6 +158,169 @@ ''' # --- +# name: test_border_alpha + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BorderAlphaApp + + + + + + + + + + ──────────────────────────────────────────────────────╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + Widget#b00Widget#b01Widget#b10Widget#b11 + + + + ──────────────────────────────────────────────────────╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + ──────────────────────────────────────────────────────+------------------+ + Widget#b02Widget#b03Widget#b12|Widget#b13| + || + || + || + ──────────────────────────────────────────────────────+------------------+ + ──────────────────────────────────────────────────────╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + Widget#b20Widget#b21Widget#b30Widget#b31 + + + + ──────────────────────────────────────────────────────╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + ──────────────────────────────────────────────────────+------------------+ + Widget#b22Widget#b23Widget#b32|Widget#b33| + || + || + || + ──────────────────────────────────────────────────────+------------------+ + + + + + ''' +# --- # name: test_buttons_render ''' diff --git a/tests/snapshot_tests/snapshot_apps/border_alpha.py b/tests/snapshot_tests/snapshot_apps/border_alpha.py new file mode 100644 index 000000000..9b1b02125 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/border_alpha.py @@ -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() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index cae6b0c53..5c07ff8f5 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -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"]