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