Frames: the border is one colour again, so drop the corner light

Every framed box was post-processed on the way to the screen: the top-left
corner blended toward white and the highlight decayed along the top and left
edges, so a box read as lit from that corner. It reads as a gradient laid over
the chrome rather than as the box being a box. The borders draw flat now.

The pass goes with it — ui/tui/glow.py, and the three composers that ran it
(base, startup, dialog), each of which also carried the platform gate that kept
it off the 16-slot console where it could only ever hand back the same lines.
Nothing else called it, and its memo went with the module. Text colour is
untouched: the pass only ever recoloured box-drawing glyphs, and titles and
subtitles were already skipped.

The four glow tests become one that pins the outcome instead of the mechanism —
every box glyph in a composed frame, nested panels and dialogs included, carries
the border's own colour and no other. Verified against the old pass: with it
back, that set holds sixty-five.

Platform.effects keeps its other two claims (the header blink, the braille
spinner); it just no longer mentions a pass that no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-08-26 01:18:29 -04:00
1 parent ad2b852018
commit c66e2c86b6
5 files changed
+35 -448

No files matched your search

+4 -4
View File
@@ -85,10 +85,10 @@ class Platform:
and quantizes the two scales that would otherwise spend a gradient — the
per-node key hue (:func:`~meshterm.ui.theme.node_style`) and the heard-age heat
(:func:`~meshterm.ui.widgets._recency_style`). Wired in P3.
effects: Whether animated/decorative rendering runs at all: corner glow, the
header battery blink, the braille spinner. ``False`` drops to static chrome
and a ``LINE`` spinner fallback — cheaper, and the glow already no-ops on a
non-truecolor theme, so this just makes that explicit. Wired in P2/P3.
effects: Whether animated/decorative rendering runs at all: the header battery
blink, the braille spinner. ``False`` drops to static chrome and a ``LINE``
spinner fallback, which is cheaper on a console where a repaint is dear.
Wired in P2/P3.
tick_s: The :class:`~prompt_toolkit.application.Application` refresh interval — the
*idle* repaint cadence only, since every screen pushes its own repaint through
``TuiSession.invalidate`` when its data actually moves. Composing one frame was
+4 -24
View File
@@ -18,7 +18,6 @@ from rich.text import Text
from ...platforms import Platform, get_platform, on_platform
from ..logo import load_logo, logo_width
from ..theme import fold_text, hint_style, title_style
from .glow import apply_corner_glow
from .render import crop_cells, render_lines, render_to_ansi
from .screen import Screen
@@ -194,10 +193,7 @@ def _title_bar(screen: Screen, cols: int, more_above: bool, more_below: bool) ->
real border draws in — rather than :func:`~meshterm.ui.theme.hint_style`'s muted
variant, which is for auxiliary text riding *alongside* a border (a footer hint, the
subtitle's own "more" label below), not the border's own glyphs. The ``↑↓`` clip
arrows keep that muted hint style, matching a bordered panel's own subtitle. There
is no corner to light the way :func:`~meshterm.ui.tui.glow.apply_corner_glow` lights
a real frame's top-left — that pass needs a truecolor blend this platform's 16-slot
palette can't render, so the rule is uniformly bold rather than fading from one.
arrows keep that muted hint style, matching a bordered panel's own subtitle.
"""
border = "accent"
tail = ""
@@ -296,13 +292,7 @@ def compose_base(
+ visible
+ render_lines(footer_row(), cols, no_wrap=True)
)
# The glow pass lights the outer frame *and* any tool panels nested in the body. It only
# ever recolours truecolor foregrounds (see glow._advance_fg), so on a platform without
# effects — which is also a platform without truecolor — it would scan every line of
# every frame and hand back the identical list. Skipping it outright is the same picture
# for none of the work. Read live rather than bound at import: set_platform() runs in the
# CLI callback, long after this module is imported (see meshterm.platforms).
lines = header_lines + (apply_corner_glow(body) if platform.effects else body)
lines = header_lines + body
# Guarantee we never exceed the terminal height (pt would otherwise clip unpredictably).
if len(lines) > rows:
lines = lines[:rows]
@@ -435,14 +425,7 @@ def compose_startup(screen: Screen, cols: int, rows: int) -> str:
padding=(vpad, 1),
width=inner_w + 4,
)
# Glow before centering, while the box still starts at column 0 of its own lines. Gated
# on the platform exactly as compose_base gates it: the pass only ever recolours
# truecolor foregrounds, so on the 16-slot console it walked every line of the splash
# to hand back the identical list, on every tick.
panel_lines = render_lines(panel, inner_w + 4)
if get_platform().effects:
panel_lines = apply_corner_glow(panel_lines)
panel_lines = _center(panel_lines, cols)
panel_lines = _center(render_lines(panel, inner_w + 4), cols)
# A small muted line (e.g. a copyright notice) sits immediately under the logo, its right
# edge hung off the logo's right edge so the two read as one signed block.
@@ -535,10 +518,7 @@ def compose_dialog(screen: Screen, cols: int, rows: int) -> str:
padding=(vpad, 1),
width=max_w,
)
dialog_lines = render_lines(panel, max_w)
if get_platform().effects:
dialog_lines = apply_corner_glow(dialog_lines)
out = "\n".join(dialog_lines)
out = "\n".join(render_lines(panel, max_w))
_DIALOG_CACHE[key] = out
if len(_DIALOG_CACHE) > _DIALOG_CACHE_MAX:
_DIALOG_CACHE.popitem(last=False)
-331
View File
@@ -1,331 +0,0 @@
"""A corner-light pass for framed boxes: brighten each frame's top-left, fading to normal.
Every panel the TUI draws — the full-screen base frame, floating dialogs, the startup
splash box, and the content-sized tool panels nested *inside* screen bodies (Status, Link
budget, Mesh, ...) — is bordered with Rich's box glyphs. This module post-processes
composed ANSI lines and gives each such frame a subtle lighting treatment: the top-left
corner is lifted toward white, and the highlight decays along the top edge (rightward) and
the left edge (downward) until it meets the border's own, untouched colour. The right and
bottom edges stay as drawn, so the box reads as lit from the top-left.
The pass is generic on purpose. It does not know which panels exist or what colour each
border uses: it *finds* frames by their corner glyphs and reads every border cell's current
colour from the ANSI stream itself, blending that toward white. A muted box therefore gets
a muted glow, a warn box a warm one — and nested panels are lit individually, each from its
own corner — with no per-callsite wiring. Composers apply it exactly once, after layout
(see :func:`~meshterm.ui.tui.frame.compose_base` and friends), so the effect never
compounds.
Because it runs on every repaint, the pass works on the ANSI strings directly rather than
round-tripping through Rich (parse → restyle → re-render costs ~10ms a frame; string
surgery is well under 1ms). The render console emits truecolor SGR sequences for every
themed colour, so a tiny state machine tracking ``38;2;r;g;b`` foregrounds is enough to
know each border cell's colour; the recolour splices in an override before the glyph and
restores the tracked colour after it, leaving bold/dim attributes and everything else in
the stream untouched. Title and subtitle text embedded in a border row is skipped (only
box-drawing glyphs are recoloured).
"""
from __future__ import annotations
import re
from collections import OrderedDict
from itertools import accumulate
from rich.cells import cell_len
#: Glyphs that open a frame's top edge. Rich's panels use the ROUNDED box (``╭``) — or its
#: SQUARE substitute (``┌``) on legacy Windows consoles; the heavy/double corners are
#: accepted too so a restyled box would still glow.
_TOP_LEFT = frozenset("╭┌┏╔")
#: Glyphs that close a top edge. The nearest one right of a top-left corner on the same row
#: marks the frame's width, which normalises the horizontal fade.
_TOP_RIGHT = frozenset("╮┐┓╗")
#: Glyphs that close a left edge. Finding one directly below a top-left corner marks the
#: frame's height, which normalises the vertical fade.
_BOTTOM_LEFT = frozenset("╰└┗╚")
#: Border glyphs a top edge is drawn with. Anything else on the row (a title, its
#: surrounding spaces) is left untouched, so only the frame itself is recoloured.
_HORIZONTAL = frozenset("─━═")
#: Border glyphs a left edge is drawn with, walked downward from each top-left corner.
_VERTICAL = frozenset("│┃║")
#: How far the corner cell itself is lifted toward pure white (0 = untouched, 1 = white).
#: The fade multiplies this by a per-cell weight, so every other border cell gets less.
_CORNER_LIFT = 0.8
#: Weights below this leave the cell alone — the blend would be invisible, and skipping it
#: keeps far border cells byte-identical to their un-glowed rendering.
_MIN_WEIGHT = 0.02
#: One SGR escape sequence (the only kind the render console emits); group 1 is its
#: semicolon-separated parameter list.
_SGR = re.compile(r"\x1b\[([0-9;]*)m")
#: Finds top-left corner glyphs in a plain (escape-free) row at C speed.
_CORNER = re.compile(f"[{''.join(_TOP_LEFT)}]")
def _strip(line: str) -> str:
"""Return the line's plain text with all SGR escape sequences removed."""
return _SGR.sub("", line)
def _weight(distance: int, extent: int) -> float:
"""Return the highlight weight for a border cell ``distance`` cells from the corner.
The fade is quadratic — ``(1 - t)²`` over the edge's ``extent`` — so the glow
concentrates near the corner and melts into the border's normal colour well before the
far end, rather than tinting the whole edge half-bright.
Args:
distance: Cells travelled along the edge from the top-left corner (0 = the corner).
extent: The edge's full length in cells; the weight reaches 0 here.
Returns:
A weight in [0, 1] to scale :data:`_CORNER_LIFT` by.
"""
if extent <= 0:
return 0.0
t = min(1.0, distance / extent)
return (1.0 - t) ** 2
def _lift(rgb: tuple[int, int, int], amount: float) -> tuple[int, int, int]:
"""Blend an RGB colour toward white by ``amount`` (0 = unchanged, 1 = pure white)."""
r, g, b = rgb
return (
round(r + (255 - r) * amount),
round(g + (255 - g) * amount),
round(b + (255 - b) * amount),
)
def _columns(plain: str) -> list[int]:
"""Return each character's display column (a cumulative cell-width prefix sum).
Border glyphs are single-cell, but body text earlier in a row may hold double-width
characters; walking columns rather than indices keeps the vertical edge walk aligned
under its corner regardless.
"""
return [0, *accumulate(cell_len(ch) for ch in plain)]
def _index_at_column(columns: list[int], column: int) -> int | None:
"""Return the index of the character at display ``column``, or ``None`` if none lands there.
A wide character straddling the column (so nothing *starts* there) returns ``None``,
which simply ends the edge walk — a frame's border cells always start on their column.
"""
for idx in range(len(columns) - 1):
if columns[idx] == column:
return idx
if columns[idx] > column:
return None
return None
def _advance_fg(fg: tuple[int, int, int] | None, params: str) -> tuple[int, int, int] | None:
"""Track the foreground colour across one SGR sequence's parameters.
Only truecolor foregrounds (``38;2;r;g;b``) are resolved — the render console emits
every themed colour that way. A reset (``0`` or empty), a default (``39``), or any
other foreground form yields ``None``, and cells whose colour is unknown are simply not
glowed rather than guessed at.
Args:
fg: The foreground in effect before this sequence, or ``None`` if unknown/default.
params: The sequence's raw parameter list (the text between ``ESC[`` and ``m``).
Returns:
The foreground in effect after the sequence.
"""
parts = params.split(";") if params else [""]
i = 0
while i < len(parts):
code = parts[i] or "0"
if code == "0" or code == "39":
fg = None
elif code == "38":
if parts[i + 1 : i + 2] == ["2"] and i + 4 < len(parts):
try:
fg = (int(parts[i + 2]), int(parts[i + 3]), int(parts[i + 4]))
except ValueError: # pragma: no cover - malformed sequence
fg = None
i += 5
continue
fg = None # an 8-bit (38;5;n) or malformed foreground: unknown, skip its cells
i += 2 if parts[i + 1 : i + 2] == ["5"] else 1
elif code == "48":
# A background colour: skip its components so they aren't misread as codes.
if parts[i + 1 : i + 2] == ["2"]:
i += 5
elif parts[i + 1 : i + 2] == ["5"]:
i += 2
elif code.isdigit() and (30 <= int(code) <= 37 or 90 <= int(code) <= 97):
fg = None # a named 16-colour foreground; the themed borders never use these
i += 1
return fg
def _recolor_line(line: str, targets: list[tuple[int, float]]) -> str:
"""Splice colour overrides into one ANSI line at the given plain-text indices.
Walks the line's SGR sequences to track the active foreground, and wraps each target
character in a lifted-colour override followed by a restore of the tracked colour.
Targets falling where the foreground is unknown are left untouched.
Args:
line: The original ANSI line.
targets: ``(plain_index, weight)`` pairs, the border cells to brighten.
Returns:
The line with the highlight spliced in.
"""
wanted = dict(sorted(targets))
out: list[str] = []
fg: tuple[int, int, int] | None = None
plain_pos = 0
cursor = 0
def emit(text: str) -> None:
"""Append a raw text run, recolouring any target characters inside it."""
nonlocal plain_pos
start = plain_pos
plain_pos += len(text)
if fg is None:
out.append(text)
return
base = 0
for idx, weight in wanted.items():
rel = idx - start
if rel < base or rel >= len(text):
continue
r, g, b = _lift(fg, _CORNER_LIFT * weight)
out.append(text[base:rel])
out.append(f"\x1b[38;2;{r};{g};{b}m{text[rel]}\x1b[38;2;{fg[0]};{fg[1]};{fg[2]}m")
base = rel + 1
out.append(text[base:])
for match in _SGR.finditer(line):
emit(line[cursor : match.start()])
out.append(match.group(0))
fg = _advance_fg(fg, match.group(1))
cursor = match.end()
emit(line[cursor:])
return "".join(out)
class _Glower:
"""One glow pass over a block of composed lines (find frames → splice highlights)."""
def __init__(self, lines: list[str]) -> None:
self.lines = lines
self.plains = [_strip(line) for line in lines]
#: Per-row display-column prefix sums, computed lazily — only walked rows need one.
self._column_cache: dict[int, list[int]] = {}
#: The border cells to brighten: row → ``(plain_index, weight)`` pairs.
self.targets: dict[int, list[tuple[int, float]]] = {}
def _cols(self, row: int) -> list[int]:
"""Return (and cache) the column prefix sums for ``row``."""
cols = self._column_cache.get(row)
if cols is None:
cols = self._column_cache[row] = _columns(self.plains[row])
return cols
def apply(self) -> list[str]:
"""Find every top-left corner and light its frame; return the updated lines."""
for row, plain in enumerate(self.plains):
for match in _CORNER.finditer(plain):
self._glow_frame(row, match.start())
if not self.targets:
return self.lines
out = list(self.lines)
for row, targets in self.targets.items():
out[row] = _recolor_line(self.lines[row], targets)
return out
def _glow_frame(self, row: int, corner: int) -> None:
"""Light one frame from its top-left corner at ``(row, corner)``."""
plain = self.plains[row]
columns = self._cols(row)
# The matching ╮ bounds the top edge; a clipped frame falls back to the row's end.
closing = next((j for j in range(corner + 1, len(plain)) if plain[j] in _TOP_RIGHT), None)
end = closing if closing is not None else len(plain)
extent = max(1, columns[end] - columns[corner])
for idx in range(corner, end):
ch = plain[idx]
if idx != corner and ch not in _HORIZONTAL:
continue # title text (and its padding) keeps its own style
self._mark(row, idx, _weight(columns[idx] - columns[corner], extent))
# Walk the left edge downward until the ╰ that closes it (or the block's clipped
# end), then fade over that height. The bottom corner lands at weight 0 by design.
column = columns[corner]
edge: list[tuple[int, int]] = []
closed = False
for below in range(row + 1, len(self.plains)):
idx = _index_at_column(self._cols(below), column)
if idx is None:
break
ch = self.plains[below][idx]
if ch in _BOTTOM_LEFT:
closed = True
break
if ch not in _VERTICAL:
break
edge.append((below, idx))
height = len(edge) + 1 if closed else len(edge) + 2
for depth, (below, idx) in enumerate(edge, start=1):
self._mark(below, idx, _weight(depth, height))
def _mark(self, row: int, idx: int, weight: float) -> None:
"""Record one border cell to brighten (skipping invisibly small weights)."""
if weight < _MIN_WEIGHT:
return
self.targets.setdefault(row, []).append((idx, weight))
#: Memoized glow output per input block. The pass is pure — the highlight is derived from
#: the lines alone — and the compositors call it with byte-identical input on every idle
#: repaint (the 1 Hz header tick recomposes an unchanged screen), so remembering a handful
#: of recent blocks turns the ~3 ms scan-and-splice into a dict hit. A few entries cover
#: the layers alive in one frame (the base panel and each stacked dialog).
_GLOW_CACHE: "OrderedDict[tuple[str, ...], list[str]]" = OrderedDict()
#: Blocks the memo keeps: the base frame plus the deepest realistic dialog stack.
_GLOW_CACHE_MAX = 12
def apply_corner_glow(lines: list[str]) -> list[str]:
"""Light every frame in ``lines`` from its top-left corner.
Scans the composed ANSI lines for box top-left corners, then blends each frame's border
colour toward white — strongest at the corner, fading quadratically along the top and
left edges to the border's normal colour. Body text, titles, and the right/bottom edges
are untouched. Rows without recoloured cells are returned byte-identical.
Args:
lines: Composed ANSI lines (one per terminal row, no newlines).
Returns:
The lines with the corner highlight applied. The result is memoized per input
block (see :data:`_GLOW_CACHE`) — callers treat it as read-only, which they all
do (every compositor only joins or concatenates it onward).
"""
key = tuple(lines)
cached = _GLOW_CACHE.get(key)
if cached is not None:
_GLOW_CACHE.move_to_end(key)
return cached
out = _Glower(lines).apply()
_GLOW_CACHE[key] = out
if len(_GLOW_CACHE) > _GLOW_CACHE_MAX:
_GLOW_CACHE.popitem(last=False)
return out
+3 -45
View File
@@ -3,8 +3,9 @@
These are behaviour tests, not benchmarks — a wall-clock assertion would be flaky on CI and
would say nothing about *why* a regression happened. Each test instead pins the structural
property the optimisation rests on: the database opens in the mode that makes small writes
cheap, the frame skips the passes that cannot change its pixels, and the startup import graph
stays clear of the three subtrees that were dragging a fifth of a second each onto every run.
cheap, the per-frame scans that cannot change a picture are skipped, and the startup import
graph stays clear of the three subtrees that were dragging a fifth of a second each onto every
run.
The numbers behind these choices were measured on the PicoCalc (Luckfox Lyra, ~1 GHz
Cortex-A7, SD card) and are recorded in the plan's Measurements appendix.
@@ -18,12 +19,9 @@ from pathlib import Path
from prompt_toolkit.data_structures import Size
from prompt_toolkit.output import DummyOutput
from rich.text import Text
from meshterm.persistence import db
from meshterm.platforms import PICOCALC, REGULAR, set_platform
from meshterm.ui.tui.frame import compose_base
from meshterm.ui.tui.screen import Screen
from meshterm.ui.tui.session import _has_wide_glyph
from meshterm.ui.tui.spinner import Spinner
@@ -133,46 +131,6 @@ def test_widgets_does_not_drag_the_spatial_modules() -> None:
# --- per-frame work the platform can skip -------------------------------------------
class _Body(Screen):
"""A minimal screen, just enough for ``compose_base`` to frame something."""
title = "Perf"
def render_body(self, width: int) -> list[str]: # noqa: D102 - inherited docstring
return ["body"]
def _glow_calls(cols: int = 60, rows: int = 20) -> int:
"""Compose one frame, counting how many times the corner-glow pass ran."""
import meshterm.ui.tui.frame as frame_mod
original = frame_mod.apply_corner_glow
calls = 0
def counting(lines: list[str]) -> list[str]:
nonlocal calls
calls += 1
return original(lines)
frame_mod.apply_corner_glow = counting
try:
compose_base(Text("hdr"), _Body(), "Esc back", cols, rows)
finally:
frame_mod.apply_corner_glow = original
return calls
def test_glow_runs_on_regular() -> None:
"""The desktop frame keeps its lit corners."""
assert _glow_calls() == 1
def test_glow_is_skipped_where_effects_are_off() -> None:
"""The pass only recolours truecolor foregrounds, so on PicoCalc it is pure scan cost."""
set_platform(PICOCALC)
assert _glow_calls() == 0
def test_wide_glyph_scan_short_circuits_without_emoji() -> None:
"""A no-emoji console font has no wide glyph, so the per-frame scan can be skipped."""
waving = "hi \U0001f44b"
+24 -44
View File
@@ -22,8 +22,7 @@ from meshterm import copyright_notice
from tests.conftest import plain as _plain
from meshterm.ui.menus import section_heading
from meshterm.ui.pathline import CRACK_TAIL, PathHop, PathLine
from meshterm.ui.tui import frame, glow
from meshterm.ui.tui.glow import apply_corner_glow
from meshterm.ui.tui import frame
from meshterm.ui.tui.progress import ProgressScreen
from meshterm.ui.tui.prompt import (
AutocompleteScreen,
@@ -870,7 +869,7 @@ def test_compose_startup_box_is_horizontally_centered() -> None:
assert box_lines and all(ln.startswith(" ") for ln in box_lines) # centered inset
# --- glow --------------------------------------------------------------------
# --- frames ------------------------------------------------------------------
def _cell_color(line: str, idx: int) -> tuple[int, int, int]:
@@ -892,54 +891,35 @@ def _find(plain: str, glyphs: frozenset[str]) -> int:
return next(i for i, ch in enumerate(plain) if ch in glyphs)
def test_corner_glow_brightens_top_left_and_fades_along_top_edge() -> None:
"""The ╭ corner is lifted toward white and the glow decays rightward to the base color."""
lines = apply_corner_glow(render_lines(Panel(Text("x"), border_style="accent", width=60), 60))
plain = Text.from_ansi(lines[0]).plain
corner = sum(_cell_color(lines[0], _find(plain, glow._TOP_LEFT)))
middle = sum(_cell_color(lines[0], len(plain) // 2))
far = sum(_cell_color(lines[0], _find(plain, glow._TOP_RIGHT) - 1))
assert far == sum(_ACCENT) # the glow has fully melted into the border color
assert corner > middle > far # brightest at the corner, fading rightward
_CORNERS = frozenset("╭┌╮┐╰└╯┘")
_EDGES = frozenset("─│")
def test_corner_glow_fades_down_left_edge_only() -> None:
"""The left border fades downward; the right edge and bottom corner stay untouched."""
body = Text("\n".join("row" for _ in range(8)))
lines = apply_corner_glow(render_lines(Panel(body, border_style="accent", width=30), 30))
upper = sum(_cell_color(lines[1], 0))
lower = sum(_cell_color(lines[7], 0))
assert upper > lower >= sum(_ACCENT) # vertical fade toward the base color
right = Text.from_ansi(lines[1]).plain.rindex("│")
assert _cell_color(lines[1], right) == _ACCENT # right edge keeps the plain border
bottom = Text.from_ansi(lines[-1]).plain
assert _cell_color(lines[-1], _find(bottom, glow._BOTTOM_LEFT)) == _ACCENT # weight is 0 here
def _border_colours(lines: list[str]) -> set[tuple[int, int, int]]:
"""Every distinct colour the box-drawing glyphs in ``lines`` are painted in."""
return {
_cell_color(line, i)
for line in lines
for i, ch in enumerate(Text.from_ansi(line).plain)
if ch in _CORNERS or ch in _EDGES
}
def test_corner_glow_leaves_titles_and_text_alone() -> None:
"""Only border glyphs are recoloured: the title keeps its style, the text its content."""
panel = Panel(Text("body"), title="Hello", border_style="accent", width=40)
raw = render_lines(panel, 40)
lines = apply_corner_glow(raw)
assert [Text.from_ansi(ln).plain for ln in lines] == [Text.from_ansi(ln).plain for ln in raw]
idx = Text.from_ansi(raw[0]).plain.index("H")
assert _cell_color(lines[0], idx) == _cell_color(raw[0], idx) # title untouched
def test_a_frame_draws_in_one_colour() -> None:
"""A border is its own colour the whole way round — no corner lit, no edge fading.
def test_corner_glow_lights_nested_panels_from_their_own_corners() -> None:
"""A tool panel nested inside a screen body glows too, blended from its own border color."""
inner = Panel(Text("body"), border_style="muted", width=20)
The frames were lit from the top-left for a while: the corner blended toward white and
the highlight decayed along the top and left edges. It read as a gradient laid over the
chrome rather than as the box being a box, so the pass is gone and the border draws flat.
Nested panels are checked with the outer frame, because the pass lit those too.
"""
inner = Panel(Text("body"), border_style="accent", width=20)
screen = ScrollScreen(Group(Text("above"), inner), title="outer")
lines = frame.compose_base(Text("h"), screen, "hint", 60, 20).split("\n")
row, plain = next(
(i, p)
for i, p in ((i, Text.from_ansi(ln).plain) for i, ln in enumerate(lines))
# The inner box's top edge: a corner glyph on a row already inside the outer border.
if any(ch in glow._TOP_LEFT for ch in p) and p.lstrip()[0] in glow._VERTICAL
)
muted = (148, 163, 184) # #94a3b8
corner = _cell_color(lines[row], _find(plain, glow._TOP_LEFT))
assert corner != muted and all(c > b for c, b in zip(corner, muted)) # lifted toward white
assert _border_colours(lines) == {_ACCENT}, "the base frame draws in more than one colour"
dialog = frame.compose_dialog(ScrollScreen(Text("body"), title="d"), 80, 20)
assert _border_colours(dialog.split("\n")) == {_ACCENT}, "so does a dialog's"
# --- prompts -----------------------------------------------------------------