mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Added tint and opacity
This commit is contained in:
@@ -15,9 +15,9 @@
|
|||||||
scrollbar-size-vertical: 2;
|
scrollbar-size-vertical: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* *:hover {
|
*:hover {
|
||||||
tint: red 30%;
|
/* tint: 30% red; */
|
||||||
} */
|
}
|
||||||
|
|
||||||
App > Screen {
|
App > Screen {
|
||||||
layout: dock;
|
layout: dock;
|
||||||
|
|||||||
@@ -657,13 +657,13 @@ class Compositor:
|
|||||||
for region, clip, lines in renders:
|
for region, clip, lines in renders:
|
||||||
render_region = intersection(region, clip)
|
render_region = intersection(region, clip)
|
||||||
|
|
||||||
for y, line in zip(render_region.y_range, lines):
|
for y, line in zip(render_region.line_range, lines):
|
||||||
if not is_rendered_line(y):
|
if not is_rendered_line(y):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
chops_line = chops[y]
|
chops_line = chops[y]
|
||||||
|
|
||||||
first_cut, last_cut = render_region.x_extents
|
first_cut, last_cut = render_region.column_span
|
||||||
cuts_line = cuts[y]
|
cuts_line = cuts[y]
|
||||||
final_cuts = [
|
final_cuts = [
|
||||||
cut for cut in cuts_line if (last_cut >= cut >= first_cut)
|
cut for cut in cuts_line if (last_cut >= cut >= first_cut)
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
from typing import TYPE_CHECKING
|
from typing import Iterable, TYPE_CHECKING
|
||||||
|
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
|
|
||||||
from ._border import get_box
|
from ._border import get_box
|
||||||
from .color import Color
|
from .color import Color
|
||||||
from .css.types import EdgeType
|
from .css.types import EdgeType
|
||||||
|
from .renderables.opacity import Opacity
|
||||||
|
from .renderables.tint import Tint
|
||||||
from ._segment_tools import line_crop
|
from ._segment_tools import line_crop
|
||||||
from ._types import Lines
|
from ._types import Lines
|
||||||
from .geometry import Region, Size
|
from .geometry import Region, Size
|
||||||
@@ -22,19 +24,21 @@ NORMALIZE_BORDER: dict[EdgeType, EdgeType] = {"none": "", "hidden": ""}
|
|||||||
|
|
||||||
|
|
||||||
class StylesRenderer:
|
class StylesRenderer:
|
||||||
|
"""Responsible for rendering CSS Styles and keeping a cached of rendered lines."""
|
||||||
|
|
||||||
def __init__(self, widget: Widget) -> None:
|
def __init__(self, widget: Widget) -> None:
|
||||||
self._widget = widget
|
self._widget = widget
|
||||||
self._cache: dict[int, list[Segment]] = {}
|
self._cache: dict[int, list[Segment]] = {}
|
||||||
self._dirty_lines: set[int] = set()
|
self._dirty_lines: set[int] = set()
|
||||||
|
|
||||||
def set_dirty(self, *regions: Region) -> None:
|
def set_dirty(self, *regions: Region) -> None:
|
||||||
|
"""Add a dirty region, or set the entire widget as dirty."""
|
||||||
if regions:
|
if regions:
|
||||||
for region in regions:
|
for region in regions:
|
||||||
self._dirty_lines.update(region.y_range)
|
self._dirty_lines.update(region.line_range)
|
||||||
else:
|
else:
|
||||||
self._dirty_lines.clear()
|
self._dirty_lines.clear()
|
||||||
for y in self._widget.size.lines:
|
self._dirty_lines.update(self._widget.size.lines_range)
|
||||||
self._dirty_lines.add(y)
|
|
||||||
|
|
||||||
def render(self, region: Region) -> Lines:
|
def render(self, region: Region) -> Lines:
|
||||||
|
|
||||||
@@ -60,7 +64,7 @@ class StylesRenderer:
|
|||||||
|
|
||||||
is_dirty = self._dirty_lines.__contains__
|
is_dirty = self._dirty_lines.__contains__
|
||||||
render_line = self.render_line
|
render_line = self.render_line
|
||||||
for y in region.y_range:
|
for y in region.line_range:
|
||||||
if is_dirty(y) or y not in self._cache:
|
if is_dirty(y) or y not in self._cache:
|
||||||
line = render_line(styles, y, size, base_background, background)
|
line = render_line(styles, y, size, base_background, background)
|
||||||
line = list(simplify(line))
|
line = list(simplify(line))
|
||||||
@@ -68,11 +72,11 @@ class StylesRenderer:
|
|||||||
else:
|
else:
|
||||||
line = self._cache[y]
|
line = self._cache[y]
|
||||||
add_line(line)
|
add_line(line)
|
||||||
self._dirty_lines.difference_update(region.y_range)
|
self._dirty_lines.difference_update(region.line_range)
|
||||||
|
|
||||||
if region.x_extents != (0, width):
|
if region.column_span != (0, width):
|
||||||
_line_crop = line_crop
|
_line_crop = line_crop
|
||||||
x1, x2 = region.x_extents
|
x1, x2 = region.column_span
|
||||||
lines = [_line_crop(line, x1, x2, width) for line in lines]
|
lines = [_line_crop(line, x1, x2, width) for line in lines]
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
@@ -109,9 +113,17 @@ class StylesRenderer:
|
|||||||
|
|
||||||
from_color = Style.from_color
|
from_color = Style.from_color
|
||||||
|
|
||||||
inner_style = from_color(bgcolor=background.rich_color)
|
rich_style = styles.rich_style
|
||||||
|
inner_style = from_color(bgcolor=background.rich_color) + rich_style
|
||||||
outer_style = from_color(bgcolor=base_background.rich_color)
|
outer_style = from_color(bgcolor=base_background.rich_color)
|
||||||
|
|
||||||
|
def post(segments: Iterable[Segment]) -> list[Segment]:
|
||||||
|
if styles.opacity != 1.0:
|
||||||
|
segments = Opacity.process_segments(segments, styles.opacity)
|
||||||
|
if styles.tint.a:
|
||||||
|
segments = Tint.process_segments(segments, styles.tint)
|
||||||
|
return segments if isinstance(segments, list) else list(segments)
|
||||||
|
|
||||||
# Draw top or bottom borders
|
# Draw top or bottom borders
|
||||||
if border_top and y in (0, height - 1):
|
if border_top and y in (0, height - 1):
|
||||||
|
|
||||||
@@ -120,22 +132,24 @@ class StylesRenderer:
|
|||||||
border_top if y == 0 else border_bottom,
|
border_top if y == 0 else border_bottom,
|
||||||
inner_style,
|
inner_style,
|
||||||
outer_style,
|
outer_style,
|
||||||
Style.from_color(color=border_color.rich_color),
|
from_color(color=border_color.rich_color),
|
||||||
)
|
)
|
||||||
box1, box2, box3 = box_segments[0 if y == 0 else 2]
|
box1, box2, box3 = box_segments[0 if y == 0 else 2]
|
||||||
|
|
||||||
if border_left and border_right:
|
if border_left and border_right:
|
||||||
return [box1, Segment(box2.text * (width - 2), box2.style), box3]
|
return post([box1, Segment(box2.text * (width - 2), box2.style), box3])
|
||||||
elif border_left:
|
elif border_left:
|
||||||
return [box1, Segment(box2.text * (width - 1), box2.style)]
|
return post([box1, Segment(box2.text * (width - 1), box2.style)])
|
||||||
elif border_right:
|
elif border_right:
|
||||||
return [Segment(box2.text * (width - 1), box2.style), box3]
|
return post([Segment(box2.text * (width - 1), box2.style), box3])
|
||||||
else:
|
else:
|
||||||
return [Segment(box2.text * width, box2.style)]
|
return post([Segment(box2.text * width, box2.style)])
|
||||||
|
|
||||||
# Draw padding
|
# Draw padding
|
||||||
if (pad_top and y < gutter.top) or (pad_bottom and y >= height - gutter.bottom):
|
if (pad_top and y < gutter.top) or (pad_bottom and y >= height - gutter.bottom):
|
||||||
background_style = from_color(bgcolor=background.rich_color)
|
background_style = from_color(
|
||||||
|
color=rich_style.color, bgcolor=background.rich_color
|
||||||
|
)
|
||||||
_, (left, _, _), _ = get_box(
|
_, (left, _, _), _ = get_box(
|
||||||
border_left,
|
border_left,
|
||||||
inner_style,
|
inner_style,
|
||||||
@@ -146,15 +160,15 @@ class StylesRenderer:
|
|||||||
border_right,
|
border_right,
|
||||||
inner_style,
|
inner_style,
|
||||||
outer_style,
|
outer_style,
|
||||||
Style.from_color(color=border_right_color.rich_color),
|
from_color(color=border_right_color.rich_color),
|
||||||
)
|
)
|
||||||
if border_left and border_right:
|
if border_left and border_right:
|
||||||
return [left, Segment(" " * (width - 2), background_style), right]
|
return post([left, Segment(" " * (width - 2), background_style), right])
|
||||||
if border_left:
|
if border_left:
|
||||||
return [left, Segment(" " * (width - 1), background_style)]
|
return post([left, Segment(" " * (width - 1), background_style)])
|
||||||
if border_right:
|
if border_right:
|
||||||
return [Segment(" " * (width - 1), background_style), right]
|
return post([Segment(" " * (width - 1), background_style), right])
|
||||||
return [Segment(" " * width, background_style)]
|
return post([Segment(" " * width, background_style)])
|
||||||
|
|
||||||
# Apply background style
|
# Apply background style
|
||||||
line = self.render_content_line(y - gutter.top)
|
line = self.render_content_line(y - gutter.top)
|
||||||
@@ -180,7 +194,7 @@ class StylesRenderer:
|
|||||||
]
|
]
|
||||||
|
|
||||||
if not border_left and not border_right:
|
if not border_left and not border_right:
|
||||||
return line
|
return post(line)
|
||||||
|
|
||||||
# Add left / right border
|
# Add left / right border
|
||||||
_, (left, _, _), _ = get_box(
|
_, (left, _, _), _ = get_box(
|
||||||
@@ -197,11 +211,11 @@ class StylesRenderer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if border_left and border_right:
|
if border_left and border_right:
|
||||||
return [left, *line, right]
|
return post([left, *line, right])
|
||||||
elif border_left:
|
elif border_left:
|
||||||
return [left, *line]
|
return post([left, *line])
|
||||||
|
|
||||||
return [*line, right]
|
return post([*line, right])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ class StylesBase(ABC):
|
|||||||
"max_height",
|
"max_height",
|
||||||
"color",
|
"color",
|
||||||
"background",
|
"background",
|
||||||
|
"opacity",
|
||||||
"tint",
|
"tint",
|
||||||
"scrollbar_color",
|
"scrollbar_color",
|
||||||
"scrollbar_color_hover",
|
"scrollbar_color_hover",
|
||||||
|
|||||||
@@ -140,8 +140,8 @@ class Size(NamedTuple):
|
|||||||
return Region(0, 0, width, height)
|
return Region(0, 0, width, height)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lines(self) -> list[int]:
|
def lines_range(self) -> range:
|
||||||
return list(range(self.height))
|
return range(self.height)
|
||||||
|
|
||||||
def __add__(self, other: object) -> Size:
|
def __add__(self, other: object) -> Size:
|
||||||
if isinstance(other, tuple):
|
if isinstance(other, tuple):
|
||||||
@@ -310,21 +310,21 @@ class Region(NamedTuple):
|
|||||||
return bool(self.width and self.height)
|
return bool(self.width and self.height)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def x_extents(self) -> tuple[int, int]:
|
def column_span(self) -> tuple[int, int]:
|
||||||
"""Get the starting and ending x coord.
|
"""Get the start and end column (x coord).
|
||||||
|
|
||||||
The end value is non inclusive.
|
The end value is exclusive.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple[int, int]: Pair of x coordinates (row numbers).
|
tuple[int, int]: Pair of x coordinates (column numbers).
|
||||||
"""
|
"""
|
||||||
return (self.x, self.x + self.width)
|
return (self.x, self.x + self.width)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def y_extents(self) -> tuple[int, int]:
|
def line_span(self) -> tuple[int, int]:
|
||||||
"""Get the starting and ending x coord.
|
"""Get the star and end line number (y coord).
|
||||||
|
|
||||||
The end value is non inclusive.
|
The end value is exclusive.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple[int, int]: Pair of y coordinates (line numbers).
|
tuple[int, int]: Pair of y coordinates (line numbers).
|
||||||
@@ -385,12 +385,12 @@ class Region(NamedTuple):
|
|||||||
return x, y, x + width, y + height
|
return x, y, x + width, y + height
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def x_range(self) -> range:
|
def column_range(self) -> range:
|
||||||
"""A range object for X coordinates."""
|
"""A range object for X coordinates."""
|
||||||
return range(self.x, self.x + self.width)
|
return range(self.x, self.x + self.width)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def y_range(self) -> range:
|
def line_range(self) -> range:
|
||||||
"""A range object for Y coordinates."""
|
"""A range object for Y coordinates."""
|
||||||
return range(self.y, self.y + self.height)
|
return range(self.y, self.y + self.height)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import functools
|
import functools
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.console import ConsoleOptions, Console, RenderResult, RenderableType
|
from rich.console import ConsoleOptions, Console, RenderResult, RenderableType
|
||||||
@@ -8,51 +9,63 @@ from rich.style import Style
|
|||||||
from textual.renderables._blend_colors import blend_colors
|
from textual.renderables._blend_colors import blend_colors
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1024)
|
||||||
|
def _get_blended_style_cached(
|
||||||
|
bg_color: Color, fg_color: Color, opacity: float
|
||||||
|
) -> Style:
|
||||||
|
return Style.from_color(
|
||||||
|
color=blend_colors(bg_color, fg_color, ratio=opacity),
|
||||||
|
bgcolor=bg_color,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Opacity:
|
class Opacity:
|
||||||
|
"""Blend foreground in to background."""
|
||||||
|
|
||||||
|
def __init__(self, renderable: RenderableType, opacity: float = 1.0) -> None:
|
||||||
"""Wrap a renderable to blend foreground color into the background color.
|
"""Wrap a renderable to blend foreground color into the background color.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
renderable (RenderableType): The RenderableType to manipulate.
|
renderable (RenderableType): The RenderableType to manipulate.
|
||||||
opacity (float): The opacity as a float. A value of 1.0 means text is fully visible.
|
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.renderable = renderable
|
||||||
self.opacity = opacity
|
self.opacity = opacity
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def process_segments(
|
||||||
|
cls, segments: Iterable[Segment], opacity: float
|
||||||
|
) -> Iterable[Segment]:
|
||||||
|
"""Apply opacity to segments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
segments (Iterable[Segment]): Incoming segments.
|
||||||
|
opacity (float): Opacity to apply.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Iterable[Segment]: Segments with applied opacity.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_Segment = Segment
|
||||||
|
for segment in segments:
|
||||||
|
text, style, control = segment
|
||||||
|
if not style:
|
||||||
|
yield segment
|
||||||
|
continue
|
||||||
|
|
||||||
|
color = style.color
|
||||||
|
bgcolor = style.bgcolor
|
||||||
|
if color and color.triplet and bgcolor and bgcolor.triplet:
|
||||||
|
color_style = _get_blended_style_cached(bgcolor, color, opacity)
|
||||||
|
yield _Segment(text, style + color_style)
|
||||||
|
else:
|
||||||
|
yield segment
|
||||||
|
|
||||||
def __rich_console__(
|
def __rich_console__(
|
||||||
self, console: Console, options: ConsoleOptions
|
self, console: Console, options: ConsoleOptions
|
||||||
) -> RenderResult:
|
) -> RenderResult:
|
||||||
segments = console.render(self.renderable, options)
|
segments = console.render(self.renderable, options)
|
||||||
opacity = self.opacity
|
return self.process_segments(segments, 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:
|
|
||||||
color_style = _get_blended_style_cached(
|
|
||||||
fg_color=fg, bg_color=bg, opacity=opacity
|
|
||||||
)
|
|
||||||
yield Segment(
|
|
||||||
segment.text,
|
|
||||||
style + color_style,
|
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
from rich.console import ConsoleOptions, Console, RenderResult, RenderableType
|
from rich.console import ConsoleOptions, Console, RenderResult, RenderableType
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
@@ -12,7 +13,7 @@ class Tint:
|
|||||||
"""Applies a color on top of an existing renderable."""
|
"""Applies a color on top of an existing renderable."""
|
||||||
|
|
||||||
def __init__(self, renderable: RenderableType, color: Color) -> None:
|
def __init__(self, renderable: RenderableType, color: Color) -> None:
|
||||||
"""_summary_
|
"""Wrap a renderable to apply a tint color.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
renderable (RenderableType): A renderable.
|
renderable (RenderableType): A renderable.
|
||||||
@@ -21,20 +22,29 @@ class Tint:
|
|||||||
self.renderable = renderable
|
self.renderable = renderable
|
||||||
self.color = color
|
self.color = color
|
||||||
|
|
||||||
def __rich_console__(
|
@classmethod
|
||||||
self, console: Console, options: ConsoleOptions
|
def process_segments(
|
||||||
) -> RenderResult:
|
cls, segments: Iterable[Segment], color: Color
|
||||||
segments = console.render(self.renderable, options)
|
) -> Iterable[Segment]:
|
||||||
|
"""Apply tint to segments.
|
||||||
|
|
||||||
color = self.color
|
Args:
|
||||||
|
segments (Iterable[Segment]): Incoming segments.
|
||||||
|
color (Color): Color of tint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Iterable[Segment]: Segments with applied tint.
|
||||||
|
|
||||||
|
"""
|
||||||
from_rich_color = Color.from_rich_color
|
from_rich_color = Color.from_rich_color
|
||||||
style_from_color = Style.from_color
|
style_from_color = Style.from_color
|
||||||
|
_Segment = Segment
|
||||||
for segment in segments:
|
for segment in segments:
|
||||||
text, style, control = segment
|
text, style, control = segment
|
||||||
if control or style is None:
|
if control or style is None:
|
||||||
yield segment
|
yield segment
|
||||||
else:
|
else:
|
||||||
yield Segment(
|
yield _Segment(
|
||||||
text,
|
text,
|
||||||
(
|
(
|
||||||
style
|
style
|
||||||
@@ -45,3 +55,10 @@ class Tint:
|
|||||||
),
|
),
|
||||||
control,
|
control,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __rich_console__(
|
||||||
|
self, console: Console, options: ConsoleOptions
|
||||||
|
) -> RenderResult:
|
||||||
|
segments = console.render(self.renderable, options)
|
||||||
|
color = self.color
|
||||||
|
return self.process_segments(segments, color)
|
||||||
|
|||||||
@@ -279,11 +279,11 @@ def test_size_sub():
|
|||||||
|
|
||||||
|
|
||||||
def test_region_x_extents():
|
def test_region_x_extents():
|
||||||
assert Region(5, 10, 20, 30).x_extents == (5, 25)
|
assert Region(5, 10, 20, 30).column_span == (5, 25)
|
||||||
|
|
||||||
|
|
||||||
def test_region_y_extents():
|
def test_region_y_extents():
|
||||||
assert Region(5, 10, 20, 30).y_extents == (10, 40)
|
assert Region(5, 10, 20, 30).line_span == (10, 40)
|
||||||
|
|
||||||
|
|
||||||
def test_region_x_max():
|
def test_region_x_max():
|
||||||
@@ -294,12 +294,12 @@ def test_region_y_max():
|
|||||||
assert Region(5, 10, 20, 30).bottom == 40
|
assert Region(5, 10, 20, 30).bottom == 40
|
||||||
|
|
||||||
|
|
||||||
def test_region_x_range():
|
def test_region_columns_range():
|
||||||
assert Region(5, 10, 20, 30).x_range == range(5, 25)
|
assert Region(5, 10, 20, 30).column_range == range(5, 25)
|
||||||
|
|
||||||
|
|
||||||
def test_region_y_range():
|
def test_region_lines_range():
|
||||||
assert Region(5, 10, 20, 30).y_range == range(10, 40)
|
assert Region(5, 10, 20, 30).line_range == range(10, 40)
|
||||||
|
|
||||||
|
|
||||||
def test_region_reset_offset():
|
def test_region_reset_offset():
|
||||||
|
|||||||
Reference in New Issue
Block a user