mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
23
src/textual/renderables/_blend_colors.py
Normal file
23
src/textual/renderables/_blend_colors.py
Normal 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
|
||||
)
|
||||
91
src/textual/renderables/opacity.py
Normal file
91
src/textual/renderables/opacity.py
Normal 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)
|
||||
@@ -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__":
|
||||
|
||||
50
tests/renderables/test_opacity.py
Normal file
50
tests/renderables/test_opacity.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user