Merge pull request #246 from Textualize/tab-underline

Underline bar renderable
This commit is contained in:
Will McGugan
2022-01-31 14:22:16 +00:00
committed by GitHub
5 changed files with 272 additions and 0 deletions

View File

View File

@@ -0,0 +1,122 @@
from __future__ import annotations
from rich.console import ConsoleOptions, Console, RenderResult
from rich.segment import Segment
from rich.style import StyleType
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_style (StyleType): The style of the highlighted range of the bar.
background_style (StyleType): The style of the non-highlighted range(s) of the 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_style: StyleType = "magenta",
background_style: StyleType = "grey37",
width: int | None = None,
) -> None:
self.highlight_range = highlight_range
self.highlight_style = highlight_style
self.background_style = background_style
self.width = width
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
highlight_style = console.get_style(self.highlight_style)
background_style = console.get_style(self.background_style)
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 or end < 0 or start > end:
yield Segment(bar * width, style=background_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=background_style)
if not half_start and start > 0:
yield Segment(half_bar_right, style=background_style)
# The highlighted portion
bar_width = int(end) - int(start)
if half_start:
yield Segment(half_bar_left + bar * (bar_width - 1), style=highlight_style)
else:
yield Segment(bar * bar_width, style=highlight_style)
if half_end:
yield Segment(half_bar_right, style=highlight_style)
# The non-highlighted tail
if not half_end and end - width != 0:
yield Segment(half_bar_left, style=background_style)
yield Segment(bar * (int(width) - int(end) - 1), style=background_style)
if __name__ == "__main__":
import random
from time import sleep
from rich.color import ANSI_COLOR_NAMES
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.1
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_style=color,
width=20,
),
f" {range}",
)
from rich.live import Live
bar = UnderlineBar(width=80, highlight_range=(0, 4.5))
with Live(bar, refresh_per_second=60) as live:
while True:
bar.highlight_range = (
bar.highlight_range[0] + 0.1,
bar.highlight_range[1] + 0.1,
)
sleep(0.005)

View File

View File

@@ -0,0 +1,126 @@
from tests.utilities.render import render
from textual.renderables.underline_bar import UnderlineBar
MAGENTA = "\x1b[35m"
GREY = "\x1b[38;5;59m"
STOP = "\x1b[0m"
GREEN = "\x1b[32m"
RED = "\x1b[31m"
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}"
)
def test_highlight_full_range_out_of_bounds_end():
bar = UnderlineBar(highlight_range=(9, 10), width=6)
assert render(bar) == f"{GREY}━━━━━━{STOP}"
def test_highlight_full_range_out_of_bounds_start():
bar = UnderlineBar(highlight_range=(-5, -2), width=6)
assert render(bar) == f"{GREY}━━━━━━{STOP}"
def test_custom_styles():
bar = UnderlineBar(
highlight_range=(2, 4),
highlight_style="red",
background_style="green",
width=6
)
assert render(bar) == (
f"{GREEN}{STOP}"
f"{GREEN}{STOP}"
f"{RED}━━{STOP}"
f"{GREEN}{STOP}"
f"{GREEN}{STOP}"
)

24
tests/utilities/render.py Normal file
View 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