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 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]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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):
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user