Add opacity support

This commit is contained in:
Darren Burns
2022-08-31 15:01:50 +01:00
parent 9f3ca0829f
commit 367b3287bf
7 changed files with 59 additions and 19 deletions

30
src/textual/_opacity.py Normal file
View File

@@ -0,0 +1,30 @@
from typing import Iterable
from rich.segment import Segment
from rich.style import Style
from textual.color import Color
from textual.renderables._blend_colors import blend_colors
def _apply_widget_opacity(
segments: Iterable[Segment],
base_background: Color,
opacity: float,
) -> Iterable[Segment]:
_Segment = Segment
for segment in segments:
text, style, _ = segment
if not style:
yield segment
continue
color = style.color
bgcolor = style.bgcolor
if color and color.triplet and bgcolor and bgcolor.triplet:
blended_foreground = blend_colors(color, base_background, ratio=opacity)
blended_background = blend_colors(bgcolor, base_background, ratio=opacity)
blended_style = Style(color=blended_foreground, bgcolor=blended_background)
yield _Segment(text, style + blended_style)
else:
yield segment

View File

@@ -7,11 +7,12 @@ from rich.segment import Segment
from rich.style import Style from rich.style import Style
from ._border import get_box, render_row from ._border import get_box, render_row
from ._opacity import _apply_widget_opacity
from ._segment_tools import line_crop, line_pad, line_trim from ._segment_tools import line_crop, line_pad, line_trim
from ._types import Lines from ._types import Lines
from .color import Color from .color import Color
from .geometry import Region, Size, Spacing from .geometry import Region, Size, Spacing
from .renderables.opacity import Opacity from .renderables.text_opacity import TextOpacity
from .renderables.tint import Tint from .renderables.tint import Tint
if sys.version_info >= (3, 10): if sys.version_info >= (3, 10):
@@ -238,9 +239,14 @@ class StylesCache:
list[Segment]: New list of segments list[Segment]: New list of segments
""" """
if styles.text_opacity != 1.0: if styles.text_opacity != 1.0:
segments = Opacity.process_segments(segments, styles.text_opacity) segments = TextOpacity.process_segments(segments, styles.text_opacity)
if styles.tint.a: if styles.tint.a:
segments = Tint.process_segments(segments, styles.tint) segments = Tint.process_segments(segments, styles.tint)
if styles.opacity != 1.0:
segments = _apply_widget_opacity(
segments, base_background, styles.opacity
)
return segments if isinstance(segments, list) else list(segments) return segments if isinstance(segments, list) else list(segments)
line: Iterable[Segment] line: Iterable[Segment]

View File

@@ -87,6 +87,7 @@ class RulesMap(TypedDict, total=False):
background: Color background: Color
text_style: Style text_style: Style
opacity: float
text_opacity: float text_opacity: float
padding: Spacing padding: Spacing
@@ -184,6 +185,7 @@ class StylesBase(ABC):
"max_height", "max_height",
"color", "color",
"background", "background",
"opacity",
"text_opacity", "text_opacity",
"tint", "tint",
"scrollbar_color", "scrollbar_color",
@@ -204,6 +206,7 @@ class StylesBase(ABC):
background = ColorProperty(Color(0, 0, 0, 0), background=True) background = ColorProperty(Color(0, 0, 0, 0), background=True)
text_style = StyleFlagsProperty() text_style = StyleFlagsProperty()
opacity = FractionalProperty()
text_opacity = FractionalProperty() text_opacity = FractionalProperty()
padding = SpacingProperty() padding = SpacingProperty()

View File

@@ -31,7 +31,7 @@ def _get_blended_style_cached(
) )
class Opacity: class TextOpacity:
"""Blend foreground in to background.""" """Blend foreground in to background."""
def __init__(self, renderable: RenderableType, opacity: float = 1.0) -> None: def __init__(self, renderable: RenderableType, opacity: float = 1.0) -> None:
@@ -96,7 +96,7 @@ if __name__ == "__main__":
) )
console.print(panel) console.print(panel)
opacity_panel = Opacity(panel, opacity=0.5) opacity_panel = TextOpacity(panel, opacity=0.5)
console.print(opacity_panel) console.print(opacity_panel)
def frange(start, end, step): def frange(start, end, step):

View File

@@ -1266,6 +1266,7 @@ class Widget(DOMNode):
width, height = self.size width, height = self.size
renderable = self.render() renderable = self.render()
renderable = self.post_render(renderable) renderable = self.post_render(renderable)
renderable = self.apply_opacity(renderable)
options = self._console.options.update_dimensions(width, height).update( options = self._console.options.update_dimensions(width, height).update(
highlight=False highlight=False
) )

View File

@@ -14,7 +14,7 @@ from textual import events
from textual._layout_resolve import layout_resolve, Edge from textual._layout_resolve import layout_resolve, Edge
from textual.keys import Keys from textual.keys import Keys
from textual.reactive import Reactive from textual.reactive import Reactive
from textual.renderables.opacity import Opacity from textual.renderables.text_opacity import TextOpacity
from textual.renderables.underline_bar import UnderlineBar from textual.renderables.underline_bar import UnderlineBar
from textual.widget import Widget from textual.widget import Widget
@@ -125,7 +125,7 @@ class TabsRenderable:
style=inactive_tab_style style=inactive_tab_style
+ Style.from_meta({"@click": f"range_clicked('{tab.name}')"}), + Style.from_meta({"@click": f"range_clicked('{tab.name}')"}),
) )
dimmed_tab_content = Opacity( dimmed_tab_content = TextOpacity(
tab_content, opacity=self.inactive_text_opacity tab_content, opacity=self.inactive_text_opacity
) )
segments = console.render(dimmed_tab_content) segments = console.render(dimmed_tab_content)

View File

@@ -2,7 +2,7 @@ import pytest
from rich.text import Text from rich.text import Text
from tests.utilities.render import render from tests.utilities.render import render
from textual.renderables.opacity import Opacity from textual.renderables.text_opacity import TextOpacity
STOP = "\x1b[0m" STOP = "\x1b[0m"
@@ -12,39 +12,39 @@ def text():
return Text("Hello, world!", style="#ff0000 on #00ff00", end="") return Text("Hello, world!", style="#ff0000 on #00ff00", end="")
def test_simple_opacity(text): def test_simple_text_opacity(text):
blended_red_on_green = "\x1b[38;2;127;127;0;48;2;0;255;0m" blended_red_on_green = "\x1b[38;2;127;127;0;48;2;0;255;0m"
assert render(Opacity(text, opacity=.5)) == ( assert render(TextOpacity(text, opacity=.5)) == (
f"{blended_red_on_green}Hello, world!{STOP}" f"{blended_red_on_green}Hello, world!{STOP}"
) )
def test_value_zero_sets_foreground_color_to_background_color(text): def test_value_zero_sets_foreground_color_to_background_color(text):
foreground = background = "0;255;0" foreground = background = "0;255;0"
assert render(Opacity(text, opacity=0)) == ( assert render(TextOpacity(text, opacity=0)) == (
f"\x1b[38;2;{foreground};48;2;{background}mHello, world!{STOP}" f"\x1b[38;2;{foreground};48;2;{background}mHello, world!{STOP}"
) )
def test_opacity_value_of_one_noop(text): def test_text_opacity_value_of_one_noop(text):
assert render(Opacity(text, opacity=1)) == render(text) assert render(TextOpacity(text, opacity=1)) == render(text)
def test_ansi_colors_noop(): def test_ansi_colors_noop():
ansi_colored_text = Text("Hello, world!", style="red on green", end="") ansi_colored_text = Text("Hello, world!", style="red on green", end="")
assert render(Opacity(ansi_colored_text, opacity=.5)) == render(ansi_colored_text) assert render(TextOpacity(ansi_colored_text, opacity=.5)) == render(ansi_colored_text)
def test_opacity_no_style_noop(): def test_text_opacity_no_style_noop():
text_no_style = Text("Hello, world!", end="") text_no_style = Text("Hello, world!", end="")
assert render(Opacity(text_no_style, opacity=.2)) == render(text_no_style) assert render(TextOpacity(text_no_style, opacity=.2)) == render(text_no_style)
def test_opacity_only_fg_noop(): def test_text_opacity_only_fg_noop():
text_only_fg = Text("Hello, world!", style="#ff0000", end="") text_only_fg = Text("Hello, world!", style="#ff0000", end="")
assert render(Opacity(text_only_fg, opacity=.5)) == render(text_only_fg) assert render(TextOpacity(text_only_fg, opacity=.5)) == render(text_only_fg)
def test_opacity_only_bg_noop(): def test_text_opacity_only_bg_noop():
text_only_bg = Text("Hello, world!", style="on #ff0000", end="") text_only_bg = Text("Hello, world!", style="on #ff0000", end="")
assert render(Opacity(text_only_bg, opacity=.5)) == render(text_only_bg) assert render(TextOpacity(text_only_bg, opacity=.5)) == render(text_only_bg)