mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #246 from Textualize/tab-underline
Underline bar renderable
This commit is contained in:
0
src/textual/renderables/__init__.py
Normal file
0
src/textual/renderables/__init__.py
Normal file
122
src/textual/renderables/underline_bar.py
Normal file
122
src/textual/renderables/underline_bar.py
Normal 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)
|
||||
0
tests/renderables/__init__.py
Normal file
0
tests/renderables/__init__.py
Normal file
126
tests/renderables/test_underline_bar.py
Normal file
126
tests/renderables/test_underline_bar.py
Normal 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
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