From 3ac0790413b686d62ff8e3731ee32cf8b00e1e9b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 14 Oct 2024 14:13:10 +0100 Subject: [PATCH] background tint style --- CHANGELOG.md | 4 ++ docs/examples/styles/background_tint.py | 24 +++++++ docs/examples/styles/background_tint.tcss | 9 +++ docs/styles/background.md | 1 + docs/styles/background_tint.md | 77 +++++++++++++++++++++++ mkdocs-nav.yml | 1 + src/textual/css/_styles_builder.py | 1 + src/textual/css/styles.py | 10 +++ src/textual/dom.py | 16 +++-- tests/snapshot_tests/test_snapshots.py | 25 ++++++++ 10 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 docs/examples/styles/background_tint.py create mode 100644 docs/examples/styles/background_tint.tcss create mode 100644 docs/styles/background_tint.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 33fa73d917..053852360e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed `RadioSet` not being scrollable https://github.com/Textualize/textual/issues/5100 +### Added + +- Added `background-tint` CSS rule + ## [0.83.0] - 2024-10-10 ### Added diff --git a/docs/examples/styles/background_tint.py b/docs/examples/styles/background_tint.py new file mode 100644 index 0000000000..a85540c15e --- /dev/null +++ b/docs/examples/styles/background_tint.py @@ -0,0 +1,24 @@ +from textual.app import App, ComposeResult +from textual.containers import Vertical +from textual.widgets import Label + + +class BackgroundTintApp(App): + CSS_PATH = "background_tint.tcss" + + def compose(self) -> ComposeResult: + with Vertical(id="tint1"): + yield Label("0%") + with Vertical(id="tint2"): + yield Label("25%") + with Vertical(id="tint3"): + yield Label("50%") + with Vertical(id="tint4"): + yield Label("75%") + with Vertical(id="tint5"): + yield Label("100%") + + +if __name__ == "__main__": + app = BackgroundTintApp() + app.run() diff --git a/docs/examples/styles/background_tint.tcss b/docs/examples/styles/background_tint.tcss new file mode 100644 index 0000000000..276ef44ead --- /dev/null +++ b/docs/examples/styles/background_tint.tcss @@ -0,0 +1,9 @@ +Vertical { + background: $panel; + color: auto 90%; +} +#tint1 { background-tint: $foreground 0%; } +#tint2 { background-tint: $foreground 25%; } +#tint3 { background-tint: $foreground 50%; } +#tint4 { background-tint: $foreground 75% } +#tint5 { background-tint: $foreground 100% } diff --git a/docs/styles/background.md b/docs/styles/background.md index 420d08eae8..c1c98622b1 100644 --- a/docs/styles/background.md +++ b/docs/styles/background.md @@ -90,4 +90,5 @@ widget.styles.background = Color(120, 60, 100) ## See also + - [`background-tint`](./background_tint.md) to blend a color with the background. - [`color`](./color.md) to set the color of text in a widget. diff --git a/docs/styles/background_tint.md b/docs/styles/background_tint.md new file mode 100644 index 0000000000..80da057cf3 --- /dev/null +++ b/docs/styles/background_tint.md @@ -0,0 +1,77 @@ +# Background-tint + +The `background-tint` style modifies the background color by tinting (blending) it with a new color. + +This style is typically used to subtly change the background of a widget for emphasis. +For instance the following would make a focused widget have a slightly lighter background. + +```css +MyWidget:focus { + background-tint: white 10% +} +``` + +The background tint color should typically have less than 100% alpha, in order to modify the background color. +If the alpha component is 100% then the tint color will replace the background color entirely. + +## Syntax + +--8<-- "docs/snippets/syntax_block_start.md" +background-tint: <color> [<percentage>]; +--8<-- "docs/snippets/syntax_block_end.md" + +The `background-tint` style requires a [``](../css_types/color.md) optionally followed by [``](../css_types/percentage.md) to specify the color's opacity (clamped between `0%` and `100%`). + +## Examples + +### Basic usage + +This example shows background tint applied with alpha from 0 to 100%. + +=== "Output" + + ```{.textual path="docs/examples/styles/background_tint.py"} + ``` + +=== "background_tint.py" + + ```python + --8<-- "docs/examples/styles/background_tint.py" + ``` + +=== "background.tcss" + + ```css hl_lines="9 13 17" + --8<-- "docs/examples/styles/background_tint.tcss" + ``` + + +## CSS + +```css +/* 10% backgrouhnd tint */ +background-tint: blue 10%; + + +/* 20% RGB color */ +background: rgb(100, 120, 200, 0.2); + +``` + +## Python + +You can use the same syntax as CSS, or explicitly set a `Color` object for finer-grained control. + +```python +# Set 20% blue background tint +widget.styles.background = "blue 20%" + +from textual.color import Color +# Set with a color object +widget.styles.background_tint = Color(120, 60, 100, 0.5) +``` + +## See also + + - [`background`](./background.md) to set the background color of a widget. + - [`color`](./color.md) to set the color of text in a widget. diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index 53b75f0391..232e7b23ac 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -75,6 +75,7 @@ nav: - "styles/index.md" - "styles/align.md" - "styles/background.md" + - "styles/background_tint.md" - "styles/border.md" - "styles/border_subtitle_align.md" - "styles/border_subtitle_background.md" diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 6d1baf45e8..8bbb26767a 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -670,6 +670,7 @@ def process_color(self, name: str, tokens: list[Token]) -> None: process_tint = process_color process_background = process_color + process_background_tint = process_color process_scrollbar_color = process_color process_scrollbar_color_hover = process_color process_scrollbar_color_active = process_color diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 4e00d74d23..e2353b9568 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -91,6 +91,8 @@ class RulesMap(TypedDict, total=False): background: Color text_style: Style + background_tint: Color + opacity: float text_opacity: float @@ -215,6 +217,7 @@ class StylesBase: "auto_color", "color", "background", + "background_tint", "opacity", "text_opacity", "tint", @@ -285,6 +288,11 @@ class StylesBase: Supports `Color` objects but also strings e.g. "red" or "#ff0000" You can also specify an opacity after a color e.g. "blue 10%" """ + background_tint = ColorProperty(Color(0, 0, 0, 0)) + """Set a color to tint (blend) with the background. + Supports `Color` objects but also strings e.g. "red" or "#ff0000" + You can also specify an opacity after a color e.g. "blue 10%" + """ text_style = StyleFlagsProperty() """Set the text style of the widget using Rich StyleFlags. e.g. `"bold underline"` or `"b u strikethrough"`. @@ -1011,6 +1019,8 @@ def append_declaration(name: str, value: str) -> None: append_declaration("color", self.color.hex) if "background" in rules: append_declaration("background", self.background.hex) + if "background_tint" in rules: + append_declaration("background-tint", self.background_tint.hex) if "text_style" in rules: append_declaration("text-style", str(get_rule("text_style"))) if "tint" in rules: diff --git a/src/textual/dom.py b/src/textual/dom.py index 8cc4e4437b..4608a3681b 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -1026,8 +1026,12 @@ def rich_style(self) -> Style: has_rule = styles.has_rule opacity *= styles.opacity if has_rule("background"): - text_background = background + styles.background - background += styles.background.multiply_alpha(opacity) + text_background = ( + background + styles.background + styles.background_tint + ) + background += ( + styles.background + styles.background_tint + ).multiply_alpha(opacity) else: text_background = background if has_rule("color"): @@ -1115,7 +1119,7 @@ def background_colors(self) -> tuple[Color, Color]: for node in reversed(self.ancestors_with_self): styles = node.styles base_background = background - background += styles.background + background += styles.background + styles.background_tint return (base_background, background) @property @@ -1131,7 +1135,9 @@ def _opacity_background_colors(self) -> tuple[Color, Color]: styles = node.styles base_background = background opacity *= styles.opacity - background += styles.background.multiply_alpha(opacity) + background += (styles.background + styles.background_tint).multiply_alpha( + opacity + ) return (base_background, background) @property @@ -1146,7 +1152,7 @@ def colors(self) -> tuple[Color, Color, Color, Color]: for node in reversed(self.ancestors_with_self): styles = node.styles base_background = background - background += styles.background + background += styles.background + styles.background_tint if styles.has_rule("color"): base_color = color if styles.auto_color: diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 7f7d4708ac..dca33964cf 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -2311,3 +2311,28 @@ def compose(self) -> ComposeResult: yield Footer() # Not allowed assert snap_compare(MaximizeApp(), press=["m"]) + + +def test_background_tint(snap_compare): + class BackgroundTintApp(App): + CSS = """ + Vertical { + background: $panel; + } + #tint1 { background-tint: $foreground 0%; } + #tint2 { background-tint: $foreground 33%; } + #tint3 { background-tint: $foreground 66%; } + #tint4 { background-tint: $foreground 100% } + """ + + def compose(self) -> ComposeResult: + with Vertical(id="tint1"): + yield Label("0%") + with Vertical(id="tint2"): + yield Label("33%") + with Vertical(id="tint3"): + yield Label("66%") + with Vertical(id="tint4"): + yield Label("100%") + + assert snap_compare(BackgroundTintApp())