fix(frame): start the corner fade at the brightest palette stop
A ladder is a hue walk, not a brightness walk, so painting the hot end backwards put the dim colour at the corner. Sort the ramp by stock brightness and follow the pane height for the bottom-left glow.
This commit is contained in:
1 parent
81116282cb
commit
ec2ca72efb
4 files changed
+117
-39
No files matched your search
@@ -75,6 +75,32 @@ PALETTES: dict[str, tuple[str, ...]] = {
|
||||
"mono": ("bright_black", "white", "bright_white"),
|
||||
}
|
||||
|
||||
#: The stock names from dark to bright. These are xterm's default values in
|
||||
#: Rec. 601 luma order — the closest thing to a stock answer. A theme may
|
||||
#: disagree about the exact numbers, but the ordering is what the frame fades
|
||||
#: need: a ladder is a *hue* walk, so where it ends says nothing about which
|
||||
#: end is brighter.
|
||||
BRIGHTNESS: tuple[str, ...] = (
|
||||
"black",
|
||||
"blue",
|
||||
"red",
|
||||
"bright_red",
|
||||
"magenta",
|
||||
"bright_magenta",
|
||||
"bright_blue",
|
||||
"green",
|
||||
"bright_black",
|
||||
"cyan",
|
||||
"bright_green",
|
||||
"bright_cyan",
|
||||
"yellow",
|
||||
"white",
|
||||
"bright_yellow",
|
||||
"bright_white",
|
||||
)
|
||||
|
||||
_RANK = {name: rank for rank, name in enumerate(BRIGHTNESS)}
|
||||
|
||||
#: Cycle order for the palette key.
|
||||
PALETTE_ORDER: tuple[str, ...] = tuple(PALETTES)
|
||||
|
||||
@@ -86,3 +112,8 @@ def palette_stops(name: str | None) -> tuple[str, ...]:
|
||||
"""The ladder for ``name``, falling back to the default palette."""
|
||||
key = (name or "").strip().lower()
|
||||
return PALETTES.get(key, PALETTES[DEFAULT_PALETTE])
|
||||
|
||||
|
||||
def brightness(colour: str) -> int:
|
||||
"""How bright a stock name is: 0 (black) to 15 (bright white)."""
|
||||
return _RANK.get(colour.strip().lower(), 0)
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
The panes draw their own frames instead of using Textual's ``border`` so the
|
||||
corners can be coloured. Two of them — the top right and the bottom left —
|
||||
run a short fade out of the palette's hot end and back into the border grey;
|
||||
everything else, including the other two corners, is that grey.
|
||||
run a short fade out of the palette's hot end and back into the border grey,
|
||||
brightest colour at the corner; everything else, including the other two
|
||||
corners, is that grey.
|
||||
|
||||
The ladder is whatever palette the app is on (``c`` cycles them), which is why
|
||||
every function here takes ``stops`` rather than reading a module constant.
|
||||
@@ -11,12 +12,14 @@ every function here takes ``stops`` rather than reading a module constant.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cache
|
||||
|
||||
from rich.cells import cell_len
|
||||
from rich.console import Group, RenderableType
|
||||
from rich.text import Text
|
||||
from textual.widget import Widget
|
||||
|
||||
from ..palettes import DEFAULT_PALETTE, PALETTES
|
||||
from ..palettes import DEFAULT_PALETTE, PALETTES, brightness
|
||||
from .common import active_stops
|
||||
|
||||
#: The frame itself: one grey, the same grey as the scrollbar thumb.
|
||||
@@ -26,20 +29,32 @@ EDGE = "bright_black"
|
||||
FADE_CELLS = 4
|
||||
|
||||
|
||||
@cache
|
||||
def fade_ramp(stops: tuple[str, ...]) -> tuple[str, ...]:
|
||||
"""The corner ramp: brightest cell first, border grey past its end.
|
||||
|
||||
The colours come from the palette's hot end, but a ladder is a hue walk,
|
||||
not a brightness walk: ``spectrum`` ends on ``red``, which is dimmer than
|
||||
the ``yellow`` just before it, so painting the ladder backwards put the
|
||||
dim colour at the corner and the glow out on the edge. Sorting the hot
|
||||
end by brightness keeps the corner the brightest cell and dims outwards,
|
||||
whatever theme sits behind the names.
|
||||
"""
|
||||
hot = [name for name in stops if name != EDGE][-FADE_CELLS:]
|
||||
return tuple(sorted(hot, key=brightness, reverse=True))
|
||||
|
||||
|
||||
def corner_fade(distance: int, stops: tuple[str, ...] = ()) -> str:
|
||||
"""Colour ``distance`` cells out from a corner; 0 is the corner glyph.
|
||||
|
||||
The fade is short — four cells — and walks backwards down the palette from
|
||||
its hot end, so the corner glows in the palette's brightest colour and then
|
||||
hands over to the plain border grey.
|
||||
The fade is short — four cells — and runs from the brightest colour the
|
||||
palette's hot end offers back into the plain border grey.
|
||||
"""
|
||||
if not stops:
|
||||
stops = PALETTES[DEFAULT_PALETTE]
|
||||
ramp = fade_ramp(tuple(stops))
|
||||
distance = max(0, distance)
|
||||
if distance >= FADE_CELLS:
|
||||
return EDGE
|
||||
index = len(stops) - 1 - distance
|
||||
return stops[index] if index >= 0 else EDGE
|
||||
return ramp[distance] if distance < len(ramp) else EDGE
|
||||
|
||||
|
||||
def inner_size(width: int, height: int) -> tuple[int, int]:
|
||||
@@ -119,7 +134,6 @@ def wrap(
|
||||
width = max(4, width)
|
||||
height = max(2, height)
|
||||
inner_width, inner_height = inner_size(width, height)
|
||||
rows = len(lines[:inner_height])
|
||||
|
||||
framed: list[Text] = [top(width, title, stops)]
|
||||
for index, line in enumerate(lines[:inner_height]):
|
||||
@@ -129,12 +143,12 @@ def wrap(
|
||||
line = line.copy()
|
||||
line.truncate(inner_width, overflow="ellipsis")
|
||||
row = Text(no_wrap=True)
|
||||
row.append_text(side_row(index, rows, "left", stops))
|
||||
row.append_text(side_row(index, inner_height, "left", stops))
|
||||
row.append_text(line)
|
||||
pad = inner_width - cell_len(line.plain)
|
||||
if pad > 0:
|
||||
row.append(" " * pad)
|
||||
row.append_text(side_row(index, rows, "right", stops))
|
||||
row.append_text(side_row(index, inner_height, "right", stops))
|
||||
framed.append(row)
|
||||
while len(framed) < height - 1:
|
||||
index = len(framed) - 1
|
||||
|
||||
+38
-20
@@ -1,15 +1,17 @@
|
||||
"""The BBS frames: grey edges, corners fading white → grey → cyan."""
|
||||
"""The BBS frames: grey edges, and a short palette glow in two corners."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from beefy_player.palettes import PALETTES
|
||||
from beefy_player.palettes import PALETTES, brightness
|
||||
from beefy_player.widgets import frame
|
||||
|
||||
#: The original ladder, used as a fixed reference in these tests.
|
||||
STOPS = PALETTES["spectrum"]
|
||||
HOT = STOPS[-1] # the corner colour
|
||||
#: Its glow: the brightest cell first, dimmer cells towards the edge.
|
||||
RAMP = frame.fade_ramp(STOPS)
|
||||
HOT = RAMP[0] # the corner colour
|
||||
|
||||
|
||||
def styles_of(line: Text) -> list[tuple[str, str]]:
|
||||
@@ -18,26 +20,29 @@ def styles_of(line: Text) -> list[tuple[str, str]]:
|
||||
]
|
||||
|
||||
|
||||
def test_the_corner_gets_the_brightest_colour_of_the_fade() -> None:
|
||||
ranks = [brightness(colour) for colour in RAMP]
|
||||
assert ranks == sorted(ranks, reverse=True)
|
||||
# spectrum's hot end is red, which is dimmer than the yellow before it:
|
||||
# the corner must still be the bright one, not the ladder's last stop
|
||||
assert STOPS[-1] != HOT
|
||||
assert brightness(HOT) > brightness(STOPS[-1])
|
||||
assert set(RAMP) <= set(STOPS)
|
||||
assert frame.corner_fade(len(RAMP), STOPS) == frame.EDGE
|
||||
|
||||
|
||||
def test_only_the_top_right_corner_fades_on_the_top_edge() -> None:
|
||||
spans = styles_of(frame.top(30, stops=STOPS))
|
||||
assert spans[0] == ("╭", frame.EDGE) # plain grey corner
|
||||
# the fade runs into the top-right corner, hot end last
|
||||
assert [style for _text, style in spans[-4:-1]] == [
|
||||
STOPS[-4],
|
||||
STOPS[-3],
|
||||
STOPS[-2],
|
||||
]
|
||||
# the fade runs into the top-right corner, brightest cell last
|
||||
assert [style for _text, style in spans[-4:-1]] == [RAMP[3], RAMP[2], RAMP[1]]
|
||||
assert spans[-1] == ("╮", f"bold {HOT}")
|
||||
|
||||
|
||||
def test_only_the_bottom_left_corner_fades_on_the_bottom_edge() -> None:
|
||||
spans = styles_of(frame.bottom(30, STOPS))
|
||||
assert spans[0] == ("╰", f"bold {HOT}")
|
||||
assert [style for _text, style in spans[1:4]] == [
|
||||
STOPS[-2],
|
||||
STOPS[-3],
|
||||
STOPS[-4],
|
||||
]
|
||||
assert [style for _text, style in spans[1:4]] == [RAMP[1], RAMP[2], RAMP[3]]
|
||||
assert spans[-1] == ("╯", frame.EDGE) # plain grey corner
|
||||
|
||||
|
||||
@@ -51,15 +56,15 @@ def test_the_sides_glow_towards_their_own_corner_only() -> None:
|
||||
frame.EDGE,
|
||||
frame.EDGE,
|
||||
frame.EDGE,
|
||||
STOPS[-4],
|
||||
STOPS[-3],
|
||||
STOPS[-2],
|
||||
RAMP[3],
|
||||
RAMP[2],
|
||||
RAMP[1],
|
||||
]
|
||||
# right column fades down from the top-right corner
|
||||
assert [frame.side_style(row, 10, "right", STOPS) for row in range(10)] == [
|
||||
STOPS[-2],
|
||||
STOPS[-3],
|
||||
STOPS[-4],
|
||||
RAMP[1],
|
||||
RAMP[2],
|
||||
RAMP[3],
|
||||
frame.EDGE,
|
||||
frame.EDGE,
|
||||
frame.EDGE,
|
||||
@@ -98,3 +103,16 @@ def test_content_wider_than_the_pane_is_trimmed_not_spilled() -> None:
|
||||
assert lines[1].endswith("│")
|
||||
# the original renderable is left untouched
|
||||
assert long_line.plain == "x" * 40
|
||||
|
||||
|
||||
def test_a_short_pane_still_glows_in_its_bottom_left_corner() -> None:
|
||||
"""The fade follows the pane's height, not the number of content rows."""
|
||||
group = frame.wrap([Text("one line")], 20, 6, stops=STOPS)
|
||||
lines = list(group.renderables)
|
||||
# rows 1-4 are the content box; the fade climbs its last three rows
|
||||
assert str(lines[1].spans[0].style) == frame.EDGE
|
||||
assert [str(line.spans[0].style) for line in lines[2:5]] == [
|
||||
RAMP[3],
|
||||
RAMP[2],
|
||||
RAMP[1],
|
||||
]
|
||||
+21
-6
@@ -13,6 +13,7 @@ from beefy_player.palettes import (
|
||||
DEFAULT_PALETTE,
|
||||
PALETTE_ORDER,
|
||||
PALETTES,
|
||||
brightness,
|
||||
palette_stops,
|
||||
)
|
||||
from beefy_player.widgets import frame
|
||||
@@ -133,13 +134,27 @@ async def test_the_spectrum_rows_use_the_active_palette() -> None:
|
||||
assert colours(spectrum._row_palette(12, 0, False)) <= set(PALETTES["neon"])
|
||||
|
||||
|
||||
def test_the_corner_fade_walks_back_down_the_ladder() -> None:
|
||||
def test_the_corner_fade_starts_at_the_brightest_stop() -> None:
|
||||
stops = PALETTES["spectrum"]
|
||||
assert frame.corner_fade(0, stops) == stops[-1]
|
||||
assert frame.corner_fade(1, stops) == stops[-2]
|
||||
assert frame.corner_fade(3, stops) == stops[-4]
|
||||
# the fade is short: past four cells it is just the border grey
|
||||
assert frame.corner_fade(4, stops) == frame.EDGE
|
||||
ramp = frame.fade_ramp(stops)
|
||||
assert frame.corner_fade(0, stops) == ramp[0]
|
||||
assert frame.corner_fade(1, stops) == ramp[1]
|
||||
# the fade is short: past the ramp it is just the border grey
|
||||
assert frame.corner_fade(len(ramp), stops) == frame.EDGE
|
||||
# spectrum's hot end is red, which is dimmer than the yellow before it;
|
||||
# the corner still gets the bright colour
|
||||
assert ramp[0] != stops[-1]
|
||||
assert ramp[-1] == stops[-1]
|
||||
|
||||
|
||||
def test_every_palette_fades_from_bright_to_dim() -> None:
|
||||
"""No palette may put a brighter cell further from the corner."""
|
||||
for name, stops in PALETTES.items():
|
||||
ramp = frame.fade_ramp(stops)
|
||||
assert ramp, name
|
||||
ranks = [brightness(colour) for colour in ramp]
|
||||
assert ranks == sorted(ranks, reverse=True), (name, ramp)
|
||||
assert set(ramp) <= set(stops), name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in new issue
Block a user