mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Add opacity support
This commit is contained in:
30
src/textual/_opacity.py
Normal file
30
src/textual/_opacity.py
Normal 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
|
||||
@@ -7,11 +7,12 @@ from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
from ._border import get_box, render_row
|
||||
from ._opacity import _apply_widget_opacity
|
||||
from ._segment_tools import line_crop, line_pad, line_trim
|
||||
from ._types import Lines
|
||||
from .color import Color
|
||||
from .geometry import Region, Size, Spacing
|
||||
from .renderables.opacity import Opacity
|
||||
from .renderables.text_opacity import TextOpacity
|
||||
from .renderables.tint import Tint
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
@@ -238,9 +239,14 @@ class StylesCache:
|
||||
list[Segment]: New list of segments
|
||||
"""
|
||||
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:
|
||||
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)
|
||||
|
||||
line: Iterable[Segment]
|
||||
|
||||
@@ -87,6 +87,7 @@ class RulesMap(TypedDict, total=False):
|
||||
background: Color
|
||||
text_style: Style
|
||||
|
||||
opacity: float
|
||||
text_opacity: float
|
||||
|
||||
padding: Spacing
|
||||
@@ -184,6 +185,7 @@ class StylesBase(ABC):
|
||||
"max_height",
|
||||
"color",
|
||||
"background",
|
||||
"opacity",
|
||||
"text_opacity",
|
||||
"tint",
|
||||
"scrollbar_color",
|
||||
@@ -204,6 +206,7 @@ class StylesBase(ABC):
|
||||
background = ColorProperty(Color(0, 0, 0, 0), background=True)
|
||||
text_style = StyleFlagsProperty()
|
||||
|
||||
opacity = FractionalProperty()
|
||||
text_opacity = FractionalProperty()
|
||||
|
||||
padding = SpacingProperty()
|
||||
|
||||
@@ -31,7 +31,7 @@ def _get_blended_style_cached(
|
||||
)
|
||||
|
||||
|
||||
class Opacity:
|
||||
class TextOpacity:
|
||||
"""Blend foreground in to background."""
|
||||
|
||||
def __init__(self, renderable: RenderableType, opacity: float = 1.0) -> None:
|
||||
@@ -96,7 +96,7 @@ if __name__ == "__main__":
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
opacity_panel = Opacity(panel, opacity=0.5)
|
||||
opacity_panel = TextOpacity(panel, opacity=0.5)
|
||||
console.print(opacity_panel)
|
||||
|
||||
def frange(start, end, step):
|
||||
@@ -1266,6 +1266,7 @@ class Widget(DOMNode):
|
||||
width, height = self.size
|
||||
renderable = self.render()
|
||||
renderable = self.post_render(renderable)
|
||||
renderable = self.apply_opacity(renderable)
|
||||
options = self._console.options.update_dimensions(width, height).update(
|
||||
highlight=False
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ from textual import events
|
||||
from textual._layout_resolve import layout_resolve, Edge
|
||||
from textual.keys import Keys
|
||||
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.widget import Widget
|
||||
|
||||
@@ -125,7 +125,7 @@ class TabsRenderable:
|
||||
style=inactive_tab_style
|
||||
+ Style.from_meta({"@click": f"range_clicked('{tab.name}')"}),
|
||||
)
|
||||
dimmed_tab_content = Opacity(
|
||||
dimmed_tab_content = TextOpacity(
|
||||
tab_content, opacity=self.inactive_text_opacity
|
||||
)
|
||||
segments = console.render(dimmed_tab_content)
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from rich.text import Text
|
||||
|
||||
from tests.utilities.render import render
|
||||
from textual.renderables.opacity import Opacity
|
||||
from textual.renderables.text_opacity import TextOpacity
|
||||
|
||||
STOP = "\x1b[0m"
|
||||
|
||||
@@ -12,39 +12,39 @@ def text():
|
||||
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"
|
||||
assert render(Opacity(text, opacity=.5)) == (
|
||||
assert render(TextOpacity(text, opacity=.5)) == (
|
||||
f"{blended_red_on_green}Hello, world!{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_value_zero_sets_foreground_color_to_background_color(text):
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
def test_opacity_value_of_one_noop(text):
|
||||
assert render(Opacity(text, opacity=1)) == render(text)
|
||||
def test_text_opacity_value_of_one_noop(text):
|
||||
assert render(TextOpacity(text, opacity=1)) == render(text)
|
||||
|
||||
|
||||
def test_ansi_colors_noop():
|
||||
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="")
|
||||
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="")
|
||||
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="")
|
||||
assert render(Opacity(text_only_bg, opacity=.5)) == render(text_only_bg)
|
||||
assert render(TextOpacity(text_only_bg, opacity=.5)) == render(text_only_bg)
|
||||
Reference in New Issue
Block a user