Merge pull request #272 from Textualize/render-opacity

Render opacity
This commit is contained in:
Will McGugan
2022-02-10 10:44:08 +00:00
committed by GitHub
5 changed files with 173 additions and 28 deletions

View File

@@ -0,0 +1,23 @@
from rich.color import Color
def blend_colors(color1: Color, color2: Color, ratio: float) -> Color:
"""Given two RGB colors, return a color that sits some distance between
them in RGB color space.
Args:
color1 (Color): The first color.
color2 (Color): The second color.
ratio (float): The ratio of color1 to color2.
Returns:
Color: A Color representing the blending of the two supplied colors.
"""
r1, g1, b1 = color1.triplet
r2, g2, b2 = color2.triplet
dr = r2 - r1
dg = g2 - g1
db = b2 - b1
return Color.from_rgb(
red=r1 + dr * ratio, green=g1 + dg * ratio, blue=b1 + db * ratio
)

View File

@@ -0,0 +1,91 @@
import functools
from rich.color import Color
from rich.console import ConsoleOptions, Console, RenderResult, RenderableType
from rich.segment import Segment
from rich.style import Style
from textual.renderables._blend_colors import blend_colors
class Opacity:
"""Wrap a renderable to blend foreground color into the background color.
Args:
renderable (RenderableType): The RenderableType to manipulate.
opacity (float): The opacity as a float. A value of 1.0 means text is fully visible.
"""
def __init__(self, renderable: RenderableType, opacity: float = 1.0) -> None:
self.renderable = renderable
self.opacity = opacity
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
segments = console.render(self.renderable, options)
opacity = self.opacity
for segment in segments:
style = segment.style
if not style:
yield segment
continue
fg = style.color
bg = style.bgcolor
if fg and fg.triplet and bg and bg.triplet:
yield Segment(
segment.text,
_get_blended_style_cached(
fg_color=fg, bg_color=bg, opacity=opacity
),
segment.control,
)
else:
yield segment
@functools.lru_cache(maxsize=1024)
def _get_blended_style_cached(
fg_color: Color, bg_color: Color, opacity: float
) -> Style:
return Style.from_color(
color=blend_colors(bg_color, fg_color, ratio=opacity),
bgcolor=bg_color,
)
if __name__ == "__main__":
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
from time import sleep
console = Console()
panel = Panel.fit(
Text("Steak: £30", style="#fcffde on #03761e"),
title="Menu",
style="#ffffff on #000000",
)
console.print(panel)
opacity_panel = Opacity(panel, opacity=0.5)
console.print(opacity_panel)
def frange(start, end, step):
current = start
while current < end:
yield current
current += step
while current >= 0:
yield current
current -= step
import itertools
with Live(opacity_panel, refresh_per_second=60) as live:
for value in itertools.cycle(frange(0, 1, 0.05)):
opacity_panel.value = value
sleep(0.05)

View File

@@ -8,6 +8,8 @@ from rich.console import ConsoleOptions, Console, RenderResult
from rich.segment import Segment
from rich.style import Style
from textual.renderables._blend_colors import blend_colors
T = TypeVar("T", int, float)
@@ -62,10 +64,10 @@ class Sparkline:
width = self.width or options.max_width
len_data = len(self.data)
if len_data == 0:
yield Segment("" * width, style=self.min_color)
yield Segment("" * width, self.min_color)
return
if len_data == 1:
yield Segment("" * width, style=self.max_color)
yield Segment("" * width, self.max_color)
return
minimum, maximum = min(self.data), max(self.data)
@@ -83,32 +85,10 @@ class Sparkline:
partition_summary = summary_function(partition)
height_ratio = (partition_summary - minimum) / extent
bar_index = int(height_ratio * (len(self.BARS) - 1))
bar_color = _blend_colors(min_color, max_color, height_ratio)
bar_color = blend_colors(min_color, max_color, height_ratio)
bars_rendered += 1
bucket_index += step
yield Segment(text=self.BARS[bar_index], style=Style.from_color(bar_color))
def _blend_colors(color1: Color, color2: Color, ratio: float) -> Color:
"""Given two RGB colors, return a color that sits some distance between
them in RGB color space.
Args:
color1 (Color): The first color.
color2 (Color): The second color.
ratio (float): The ratio of color1 to color2.
Returns:
Color: A Color representing the blending of the two supplied colors.
"""
r1, g1, b1 = color1.triplet
r2, g2, b2 = color2.triplet
dr = r2 - r1
dg = g2 - g1
db = b2 - b1
return Color.from_rgb(
red=r1 + dr * ratio, green=g1 + dg * ratio, blue=b1 + db * ratio
)
yield Segment(self.BARS[bar_index], Style.from_color(bar_color))
if __name__ == "__main__":

View File

@@ -0,0 +1,50 @@
import pytest
from rich.text import Text
from tests.utilities.render import render
from textual.renderables.opacity import Opacity
STOP = "\x1b[0m"
@pytest.fixture
def text():
return Text("Hello, world!", style="#ff0000 on #00ff00", end="")
def test_simple_opacity(text):
blended_red_on_green = "\x1b[38;2;127;127;0;48;2;0;255;0m"
assert render(Opacity(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)) == (
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_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)
def test_opacity_no_style_noop():
text_no_style = Text("Hello, world!", end="")
assert render(Opacity(text_no_style, opacity=.2)) == render(text_no_style)
def test_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)
def test_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)

View File

@@ -19,6 +19,7 @@ def render(renderable: RenderableType, no_wrap: bool = False) -> str:
console = Console(
width=100, file=io.StringIO(), color_system="truecolor", legacy_windows=False
)
console.print(renderable, no_wrap=no_wrap)
output = replace_link_ids(console.file.getvalue())
with console.capture() as capture:
console.print(renderable, no_wrap=no_wrap, end="")
output = replace_link_ids(capture.get())
return output