From db1b784606beb9736a14ef82faf2a9ec7e56d014 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 19 Apr 2023 09:31:59 +0100 Subject: [PATCH] dim filter (#2323) * dim filter * optimization * Remove test code * move functions out of filter * docstring * move function to module scope * docstring * docstrings --- src/textual/filter.py | 151 +++++++++++++++++++++++++++++++------- tests/test_line_filter.py | 18 +++++ 2 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 tests/test_line_filter.py diff --git a/src/textual/filter.py b/src/textual/filter.py index bbf3dbaef..387f49be9 100644 --- a/src/textual/filter.py +++ b/src/textual/filter.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from functools import lru_cache +from rich.color import Color as RichColor from rich.segment import Segment from rich.style import Style @@ -14,39 +15,139 @@ class LineFilter(ABC): @abstractmethod def apply(self, segments: list[Segment]) -> list[Segment]: - """Transform a list of segments.""" + """Transform a list of segments. + + Args: + segments: A list of segments. + + Returns: + A new list of segments. + """ + + +@lru_cache(1024) +def monochrome_style(style: Style) -> Style: + """Convert colors in a style to monochrome. + + Args: + style: A Rich Style. + + Returns: + A new Rich style. + """ + style_color = style.color + style_background = style.bgcolor + color = ( + None + if style_color is None + else Color.from_rich_color(style_color).monochrome.rich_color + ) + background = ( + None + if style_background is None + else Color.from_rich_color(style_background).monochrome.rich_color + ) + return style + Style.from_color(color, background) class Monochrome(LineFilter): """Convert all colors to monochrome.""" def apply(self, segments: list[Segment]) -> list[Segment]: - to_monochrome = self.to_monochrome - _Segment = Segment - return [ - _Segment(text, to_monochrome(style), None) for text, style, _ in segments - ] - - @lru_cache(1024) - def to_monochrome(self, style: Style) -> Style: - """Convert colors in a style to monochrome. + """Transform a list of segments. Args: - style: A Rich Style. + segments: A list of segments. Returns: - A new Rich style. + A new list of segments. """ - style_color = style.color - style_background = style.bgcolor - color = ( - None - if style_color is None - else Color.from_rich_color(style_color).monochrome.rich_color - ) - background = ( - None - if style_background is None - else Color.from_rich_color(style_background).monochrome.rich_color - ) - return style + Style.from_color(color, background) + _monochrome_style = monochrome_style + _Segment = Segment + return [ + _Segment(text, _monochrome_style(style), None) + for text, style, _ in segments + ] + + +NO_DIM = Style(dim=False) +"""A Style to set dim to False.""" + + +@lru_cache(1024) +def dim_color(background: RichColor, color: RichColor, factor: float) -> RichColor: + """Dim a color by blending towards the background + + Args: + background: background color. + color: Foreground color. + factor: Blend factor + + Returns: + New dimmer color. + """ + red1, green1, blue1 = background.triplet + red2, green2, blue2 = color.triplet + + return RichColor.from_rgb( + red1 + (red2 - red1) * factor, + green1 + (green2 - green1) * factor, + blue1 + (blue2 - blue1) * factor, + ) + + +@lru_cache(1024) +def dim_style(style: Style, factor: float) -> Style: + """Replace dim attribute with a dim color. + + Args: + style: Style to dim. + factor: Blend factor. + + Returns: + New dimmed style. + """ + return ( + style + + Style.from_color(dim_color(style.bgcolor, style.color, factor), None) + + NO_DIM + ) + + +# Can be used as a workaround for https://github.com/xtermjs/xterm.js/issues/4161 +class DimFilter(LineFilter): + """Replace dim attributes with modified colors.""" + + def __init__(self, dim_factor: float = 0.5) -> None: + """Initialize the filter. + + Args: + dim_factor: The factor to dim by; 0 is 100% background (i.e. invisible), 1.0 is no change. + """ + self.dim_factor = dim_factor + + def apply(self, segments: list[Segment]) -> list[Segment]: + """Transform a list of segments. + + Args: + segments: A list of segments. + + Returns: + A new list of segments. + """ + _Segment = Segment + _dim_style = dim_style + factor = self.dim_factor + + return [ + ( + _Segment( + segment.text, + _dim_style(segment.style, factor), + None, + ) + if segment.style is not None and segment.style.dim + else segment + ) + for segment in segments + ] diff --git a/tests/test_line_filter.py b/tests/test_line_filter.py new file mode 100644 index 000000000..06fb00ee3 --- /dev/null +++ b/tests/test_line_filter.py @@ -0,0 +1,18 @@ +from rich.segment import Segment +from rich.style import Style + +from textual.filter import DimFilter + + +def test_dim_apply(): + """Check dim filter changes color and resets dim attribute.""" + + dim_filter = DimFilter() + + segments = [Segment("Hello, World!", Style.parse("dim #ffffff on #0000ff"))] + + dimmed_segments = dim_filter.apply(segments) + + expected = [Segment("Hello, World!", Style.parse("not dim #7f7fff on #0000ff"))] + + assert dimmed_segments == expected