"""The message strip the project dialogs report success and failure in.
A dialog's message is whatever the engine had to say: a validation error naming
the setting and its suggestion, an exporter refusing a bundle and explaining
what to do about it. Those run to several hundred characters, and a plain
:class:`Label` draws one long line straight out of its box and off the screen,
so the actionable end of the sentence is exactly the part that never arrives.
Wrapping to the strip's own width and eliding the middle keeps both the opening
words and the instruction at the end on screen.
"""
from simvx.core import Label
from simvx.core.text.measure import LOGICAL_EM, text_minimum_width
__all__ = ["ERROR_COLOUR", "OK_COLOUR", "WARNING_COLOUR", "message_lines", "set_message"]
#: Message colours, shared so the dialogs read the same way as each other.
OK_COLOUR = (0.45, 0.85, 0.45, 1.0)
ERROR_COLOUR = (0.9, 0.4, 0.4, 1.0)
WARNING_COLOUR = (0.95, 0.75, 0.30, 1.0)
#: Line pitch Label draws multi-line text on, as a multiple of the font size.
_LINE_PITCH = 1.2
_ELLIPSIS = "..."
def _fits(text: str, width: float, font_size: float) -> bool:
return text_minimum_width(text, font_size / LOGICAL_EM) <= width
def _split_word(word: str, width: float, font_size: float) -> list[str]:
"""Break a single unbreakable run (a long path, a URL) across lines."""
parts: list[str] = []
current = ""
for char in word:
if current and not _fits(current + char, width, font_size):
parts.append(current)
current = char
else:
current += char
if current:
parts.append(current)
return parts
[docs]
def message_lines(text: str, width: float, font_size: float) -> list[str]:
"""Wrap *text* into lines that each measure at most *width* pixels."""
if width <= 0:
return text.split("\n")
lines: list[str] = []
for paragraph in text.split("\n"):
current = ""
for word in paragraph.split(" "):
candidate = f"{current} {word}" if current else word
if _fits(candidate, width, font_size):
current = candidate
continue
if current:
lines.append(current)
if _fits(word, width, font_size):
current = word
else:
pieces = _split_word(word, width, font_size)
lines.extend(pieces[:-1])
current = pieces[-1] if pieces else ""
lines.append(current)
return lines
def _elide(lines: list[str], max_lines: int) -> list[str]:
"""Trim *lines* to *max_lines*, keeping the opening and the final line."""
if len(lines) <= max_lines:
return lines
if max_lines <= 1:
return [f"{lines[0].rstrip()} {_ELLIPSIS}"]
if max_lines == 2:
return [lines[0], f"{_ELLIPSIS} {lines[-1]}"]
return [*lines[: max_lines - 2], _ELLIPSIS, lines[-1]]
[docs]
def set_message(label: Label, text: str, colour: tuple[float, float, float, float]) -> None:
"""Show *text* in *label*, wrapped to the strip and clipped to its height."""
label.text_colour = colour
label.vertical_alignment = "top"
if not text:
label.text = ""
return
lines = message_lines(text, label.size.x, label.font_size)
max_lines = max(1, int(label.size.y // (label.font_size * _LINE_PITCH)))
label.text = "\n".join(_elide(lines, max_lines))