mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Underline bar renderable
This commit is contained in:
@@ -1,95 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
from rich.color import Color, ANSI_COLOR_NAMES
|
||||
from rich.console import ConsoleOptions, Console, RenderResult
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
|
||||
class UnderlineBar:
|
||||
def __init__(
|
||||
self,
|
||||
highlight_range: tuple[float, float] = 0,
|
||||
range_color: Color = Color.parse("yellow"),
|
||||
other_color: Color = Color.parse("default"),
|
||||
background_color: Color = Color.parse("default"),
|
||||
width: int | None = None,
|
||||
) -> None:
|
||||
self.highlight_range = highlight_range
|
||||
self.highlight_style = Style.from_color(
|
||||
color=range_color, bgcolor=background_color
|
||||
)
|
||||
self.other_style = Style.from_color(color=other_color, bgcolor=background_color)
|
||||
self.width = width
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
half_bar_right = "╸"
|
||||
half_bar_left = "╺"
|
||||
bar = "━"
|
||||
width = (self.width or options.max_width) - 1
|
||||
start, end = self.highlight_range
|
||||
|
||||
# Round start and end to nearest half
|
||||
start = round(start * 2) / 2
|
||||
end = round(end * 2) / 2
|
||||
|
||||
half_start = start - int(start) > 0
|
||||
half_end = end - int(end) > 0
|
||||
|
||||
# Initial non-highlighted portion of bar
|
||||
yield Segment(bar * (int(start - 0.5)), style=self.other_style)
|
||||
if not half_start and start > 0:
|
||||
yield Segment(half_bar_right, style=self.other_style)
|
||||
|
||||
# If we have a half bar at start and end, we need 1 less full bar
|
||||
full_bar_width = int(end) - int(start)
|
||||
if half_start and half_end:
|
||||
full_bar_width -= 1
|
||||
|
||||
# The highlighted portion
|
||||
if not half_start:
|
||||
yield Segment(bar * full_bar_width, style=self.highlight_style)
|
||||
else:
|
||||
yield Segment(half_bar_left, style=self.highlight_style)
|
||||
yield Segment(bar * full_bar_width, style=self.highlight_style)
|
||||
if half_end:
|
||||
yield Segment(half_bar_right, style=self.highlight_style)
|
||||
|
||||
# The non-highlighted tail
|
||||
if not half_end and end - width != 1:
|
||||
yield Segment(half_bar_left, style=self.other_style)
|
||||
yield Segment(bar * (int(width) - int(end)), style=self.other_style)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
console = Console()
|
||||
|
||||
def frange(start, end, step):
|
||||
current = start
|
||||
while current < end:
|
||||
yield current
|
||||
current += step
|
||||
|
||||
while current >= 0:
|
||||
yield current
|
||||
current -= step
|
||||
|
||||
start_range = frange(0, 12, 0.5)
|
||||
end_range = frange(6, 18, 0.5)
|
||||
ranges = zip(start_range, end_range)
|
||||
|
||||
for range in ranges:
|
||||
color = random.choice(list(ANSI_COLOR_NAMES.keys()))
|
||||
console.print(
|
||||
UnderlineBar(
|
||||
range,
|
||||
range_color=Color.parse(color),
|
||||
other_color=Color.parse("#4f4f4f"),
|
||||
width=18,
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
112
src/textual/renderables/underline_bar.py
Normal file
112
src/textual/renderables/underline_bar.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
from rich.color import Color, ANSI_COLOR_NAMES
|
||||
from rich.console import ConsoleOptions, Console, RenderResult
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
|
||||
class UnderlineBar:
|
||||
"""Thin horizontal bar with a portion highlighted.
|
||||
|
||||
Args:
|
||||
highlight_range (tuple[float, float]): The range to highlight. Defaults to ``(0, 0)`` (no highlight)
|
||||
highlight_color (Color | str): The color of the highlighted range of the bar.
|
||||
non_highlight_color (Color | str): The color of the non-highlighted range(s) of the bar.
|
||||
background_color (Color | str): The background color of the entire bar.
|
||||
width (int, optional): The width of the bar, or ``None`` to fill available width.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
highlight_range: tuple[float, float] = (0, 0),
|
||||
highlight_color: Color | str = "magenta",
|
||||
non_highlight_color: Color | str = "grey37",
|
||||
background_color: Color | str = "default",
|
||||
width: int | None = None,
|
||||
) -> None:
|
||||
self.highlight_range = highlight_range
|
||||
self.highlight_style = Style(color=highlight_color, bgcolor=background_color)
|
||||
self.non_highlight_style = Style(
|
||||
color=non_highlight_color, bgcolor=background_color
|
||||
)
|
||||
self.width = width
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
half_bar_right = "╸"
|
||||
half_bar_left = "╺"
|
||||
bar = "━"
|
||||
width = self.width or options.max_width
|
||||
start, end = self.highlight_range
|
||||
|
||||
start = max(start, 0)
|
||||
end = min(end, width)
|
||||
|
||||
if start == end == 0:
|
||||
yield Segment(bar * width, style=self.non_highlight_style)
|
||||
return
|
||||
|
||||
# Round start and end to nearest half
|
||||
start = round(start * 2) / 2
|
||||
end = round(end * 2) / 2
|
||||
|
||||
# Check if we start/end on a number that rounds to a .5
|
||||
half_start = start - int(start) > 0
|
||||
half_end = end - int(end) > 0
|
||||
|
||||
# Initial non-highlighted portion of bar
|
||||
yield Segment(bar * (int(start - 0.5)), style=self.non_highlight_style)
|
||||
if not half_start and start > 0:
|
||||
yield Segment(half_bar_right, style=self.non_highlight_style)
|
||||
|
||||
# The highlighted portion
|
||||
bar_width = int(end) - int(start)
|
||||
if half_start:
|
||||
yield Segment(
|
||||
half_bar_left + bar * (bar_width - 1), style=self.highlight_style
|
||||
)
|
||||
else:
|
||||
yield Segment(bar * bar_width, style=self.highlight_style)
|
||||
if half_end:
|
||||
yield Segment(half_bar_right, style=self.highlight_style)
|
||||
|
||||
# The non-highlighted tail
|
||||
if not half_end and end - width != 0:
|
||||
yield Segment(half_bar_left, style=self.non_highlight_style)
|
||||
yield Segment(bar * (int(width) - int(end) - 1), style=self.non_highlight_style)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
console = Console()
|
||||
|
||||
def frange(start, end, step):
|
||||
current = start
|
||||
while current < end:
|
||||
yield current
|
||||
current += step
|
||||
|
||||
while current >= 0:
|
||||
yield current
|
||||
current -= step
|
||||
|
||||
step = 0.5
|
||||
start_range = frange(0.5, 10.5, step)
|
||||
end_range = frange(10, 20, step)
|
||||
ranges = zip(start_range, end_range)
|
||||
|
||||
console.print(UnderlineBar(width=20), f" (.0, .0)")
|
||||
|
||||
for range in ranges:
|
||||
color = random.choice(list(ANSI_COLOR_NAMES.keys()))
|
||||
console.print(
|
||||
UnderlineBar(
|
||||
range,
|
||||
highlight_color=Color.parse(color),
|
||||
width=20,
|
||||
),
|
||||
f" {range}",
|
||||
)
|
||||
0
tests/renderables/__init__.py
Normal file
0
tests/renderables/__init__.py
Normal file
98
tests/renderables/test_underline_bar.py
Normal file
98
tests/renderables/test_underline_bar.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from tests.utilities.render import render
|
||||
from textual.renderables.underline_bar import UnderlineBar
|
||||
|
||||
MAGENTA = "\x1b[35;49m"
|
||||
GREY = "\x1b[38;5;59;49m"
|
||||
STOP = "\x1b[0m"
|
||||
|
||||
|
||||
def test_no_highlight():
|
||||
bar = UnderlineBar(width=6)
|
||||
assert render(bar) == f"{GREY}━━━━━━{STOP}"
|
||||
|
||||
|
||||
def test_highlight_from_zero():
|
||||
bar = UnderlineBar(highlight_range=(0, 2.5), width=6)
|
||||
assert render(bar) == (
|
||||
f"{MAGENTA}━━{STOP}{MAGENTA}╸{STOP}{GREY}━━━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_from_zero_point_five():
|
||||
bar = UnderlineBar(highlight_range=(0.5, 2), width=6)
|
||||
assert render(bar) == (
|
||||
f"{MAGENTA}╺━{STOP}{GREY}╺{STOP}{GREY}━━━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_middle():
|
||||
bar = UnderlineBar(highlight_range=(2, 4), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━{STOP}"
|
||||
f"{GREY}╸{STOP}"
|
||||
f"{MAGENTA}━━{STOP}"
|
||||
f"{GREY}╺{STOP}"
|
||||
f"{GREY}━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_half_start():
|
||||
bar = UnderlineBar(highlight_range=(2.5, 4), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━━{STOP}"
|
||||
f"{MAGENTA}╺━{STOP}"
|
||||
f"{GREY}╺{STOP}"
|
||||
f"{GREY}━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_half_end():
|
||||
bar = UnderlineBar(highlight_range=(2, 4.5), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━{STOP}"
|
||||
f"{GREY}╸{STOP}"
|
||||
f"{MAGENTA}━━{STOP}"
|
||||
f"{MAGENTA}╸{STOP}"
|
||||
f"{GREY}━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_half_start_and_half_end():
|
||||
bar = UnderlineBar(highlight_range=(2.5, 4.5), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━━{STOP}"
|
||||
f"{MAGENTA}╺━{STOP}"
|
||||
f"{MAGENTA}╸{STOP}"
|
||||
f"{GREY}━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_to_near_end():
|
||||
bar = UnderlineBar(highlight_range=(3, 5.5), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━━{STOP}"
|
||||
f"{GREY}╸{STOP}"
|
||||
f"{MAGENTA}━━{STOP}"
|
||||
f"{MAGENTA}╸{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_to_end():
|
||||
bar = UnderlineBar(highlight_range=(3, 6), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_out_of_bounds_start():
|
||||
bar = UnderlineBar(highlight_range=(-2, 3), width=6)
|
||||
assert render(bar) == (
|
||||
f"{MAGENTA}━━━{STOP}{GREY}╺{STOP}{GREY}━━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_out_of_bounds_end():
|
||||
bar = UnderlineBar(highlight_range=(3, 9), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}"
|
||||
)
|
||||
24
tests/utilities/render.py
Normal file
24
tests/utilities/render.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import io
|
||||
import re
|
||||
|
||||
from rich.console import Console, RenderableType
|
||||
|
||||
|
||||
re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b")
|
||||
|
||||
|
||||
def replace_link_ids(render: str) -> str:
|
||||
"""Link IDs have a random ID and system path which is a problem for
|
||||
reproducible tests.
|
||||
|
||||
"""
|
||||
return re_link_ids.sub("id=0;foo\x1b", render)
|
||||
|
||||
|
||||
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())
|
||||
return output
|
||||
Reference in New Issue
Block a user