test nav styling

This commit is contained in:
firestarsdog
2026-07-09 14:14:11 -04:00
parent 57e5d6e0b7
commit 9031dde7ec
@@ -2,6 +2,7 @@ from __future__ import annotations
import json
import math
import re
from pathlib import Path
import pyray as rl
@@ -55,6 +56,59 @@ def _normalize_maneuver_type(maneuver_type: str) -> str:
return normalized.replace(" ", "_")
_STREET_SUFFIXES = {
"street": "St", "avenue": "Ave", "boulevard": "Blvd", "road": "Rd",
"drive": "Dr", "lane": "Ln", "court": "Ct", "place": "Pl",
"highway": "Hwy", "parkway": "Pkwy", "square": "Sq", "terrace": "Ter",
"circle": "Cir", "trail": "Trl", "turnpike": "Tpke", "causeway": "Cswy",
"crossing": "Xing", "loop": "Loop", "way": "Way", "alley": "Aly",
"plaza": "Plz", "point": "Pt",
}
_STREET_PLURALS = {
"streets": "Sts", "avenues": "Aves", "boulevards": "Blvds", "roads": "Rds",
"drives": "Drs", "lanes": "Lns", "courts": "Cts", "places": "Pls",
"highways": "Hwys", "parkways": "Pkwys", "squares": "Sqs", "terraces": "Ters",
"circles": "Cirs", "trails": "Trls", "turnpikes": "Tpkes", "causeways": "Cswys",
"crossings": "Xings", "loops": "Loops", "alleys": "Alys", "plazas": "Plzs",
"points": "Pts",
}
_DIRECTIONS = {
"north": "N", "south": "S", "east": "E", "west": "W",
"northeast": "NE", "northwest": "NW", "southeast": "SE", "southwest": "SW",
}
_STREET_WORD_MAP = {**_STREET_SUFFIXES, **_STREET_PLURALS, **_DIRECTIONS}
_STREET_WORD_RE = re.compile(
r"\b(" + "|".join(sorted(_STREET_WORD_MAP, key=len, reverse=True)) + r")\b\.?",
re.IGNORECASE,
)
_ROUTE_PATTERNS = (
(re.compile(r"\binterstate\s+(\d+)\b", re.IGNORECASE), r"I-\1"),
(re.compile(r"\bstate\s+route\s+(\d+)\b", re.IGNORECASE), r"SR \1"),
(re.compile(r"\bhighway\s+(\d+)\b", re.IGNORECASE), r"Hwy \1"),
(re.compile(r"\broute\s+(\d+)\b", re.IGNORECASE), r"Rte \1"),
)
_LATIN_RE = re.compile(r"[A-Za-z]")
def _abbreviate_street_suffixes(text: str) -> str:
if not text or not _LATIN_RE.search(text):
return text
for pattern, repl in _ROUTE_PATTERNS:
text = pattern.sub(repl, text)
return _STREET_WORD_RE.sub(lambda m: _STREET_WORD_MAP[m.group(1).lower()], text)
def _fit_3dots(text: str, max_width: float, font_size: int, font) -> str:
if measure_text_cached(font, text, font_size).x <= max_width:
return text
shortened = text.rstrip()
candidate = f"{shortened}..."
while shortened and measure_text_cached(font, candidate, font_size).x > max_width:
shortened = shortened[:-1].rstrip()
candidate = f"{shortened}..." if shortened else "..."
return candidate
class NavigationCardRenderer(Widget):
def __init__(self, layout_variant: str = "default"):
"""layout_variant: "default" (tici/tizi) or "mici"."""
@@ -204,7 +258,8 @@ class NavigationCardRenderer(Widget):
self._valid = True
def _wrap_text_lines(self, text: str, max_width: float, font_size: int, max_lines: int = 2) -> tuple[list[str], bool]:
def _wrap_text_lines(self, text: str, max_width: float, font_size: int, max_lines: int = 2, font=None) -> tuple[list[str], bool]:
font = font or self._font_bold
words = text.split()
if not words:
return [], False
@@ -216,7 +271,7 @@ class NavigationCardRenderer(Widget):
while word_index < len(words):
candidate_words = current_words + [words[word_index]]
candidate_line = " ".join(candidate_words)
text_width = measure_text_cached(self._font_bold, candidate_line, font_size).x
text_width = measure_text_cached(font, candidate_line, font_size).x
if text_width <= max_width or not current_words:
current_words = candidate_words
word_index += 1
@@ -233,7 +288,7 @@ class NavigationCardRenderer(Widget):
if truncated and lines:
shortened = lines[-1]
candidate = f"{shortened}"
while shortened and measure_text_cached(self._font_bold, candidate, font_size).x > max_width:
while shortened and measure_text_cached(font, candidate, font_size).x > max_width:
if " " in shortened:
shortened = shortened.rsplit(" ", 1)[0]
else:
@@ -278,6 +333,32 @@ class NavigationCardRenderer(Widget):
line_y = y + index * (font_size + line_gap)
rl.draw_text_ex(self._font_bold, line, rl.Vector2(x, line_y), font_size, 0, rl.WHITE)
def _draw_lines(self, lines: list[str], x: float, y0: float, font_size: int, font, color) -> None:
for index, line in enumerate(lines[:2]):
rl.draw_text_ex(font, line, rl.Vector2(x, y0 + index * font_size), font_size, 0, color)
def _fit_intelligent(self, text, max_width, max_lines, preferred, minimum, font):
text = _abbreviate_street_suffixes(text or "")
if not text:
return [], preferred
one_line_font = minimum
for fs in range(preferred, minimum - 1, -2):
if measure_text_cached(font, text, fs).x <= max_width:
one_line_font = fs
break
for fs in range(preferred, minimum - 1, -2):
lines, truncated = self._wrap_text_lines(text, max_width, fs, max_lines, font)
if not truncated and all(measure_text_cached(font, ln, fs).x <= max_width for ln in lines):
if one_line_font > minimum and len(lines) == 2 and len(lines[1].split()) == 1:
if fs - one_line_font <= 4 or measure_text_cached(font, lines[1], fs).x < 0.35 * max_width:
return [text], one_line_font
return lines, fs
lines, _ = self._wrap_text_lines(text, max_width, minimum, max_lines, font)
return [self._fit_3dots(ln.replace("", "..."), max_width, minimum, font) for ln in lines], minimum
def _render_default(self, rect: rl.Rectangle) -> None: # tici/tizi expanded card
right_margin = 40
card_width = 560
@@ -291,6 +372,8 @@ class NavigationCardRenderer(Widget):
then_icon_size = 72
preferred_font_size = 48
minimum_font_size = 38
secondary_preferred = 26
secondary_minimum = 22
distance_font_size = 32
container = rl.Rectangle(container_x, container_y, card_width, card_height)
@@ -315,28 +398,17 @@ class NavigationCardRenderer(Widget):
text_x = icon_x + icon_size + gap_after_icon
right_gutter = icon_padding + (then_section_width if show_next else 0)
text_area_width = card_width - (text_x - container_x) - right_gutter
title_lines, title_font_size = self._fit_title(self._primary_text, text_area_width, preferred_font_size, minimum_font_size)
title_lines, title_font_size = self._fit_intelligent(self._primary_text, text_area_width, 2, preferred_font_size, minimum_font_size, self._font_bold)
secondary_lines, secondary_font_size = self._fit_intelligent(self._secondary_text or "", text_area_width, 2, secondary_preferred, secondary_minimum, self._font_medium)
secondary_font_size = 26
text_block_h = len(title_lines) * title_font_size
if self._secondary_text:
text_block_h += 8 + secondary_font_size
n_title = len(title_lines)
n_sec = len(secondary_lines)
gap = 8 if n_sec else 0
text_block_h = n_title * title_font_size + gap + n_sec * secondary_font_size
text_block_y = container_y + (card_height - text_block_h) // 2
for index, line in enumerate(title_lines[:2]):
rl.draw_text_ex(self._font_bold, line, rl.Vector2(text_x, text_block_y + index * title_font_size), title_font_size, 0, rl.WHITE)
if self._secondary_text:
secondary_text = self._truncate_text(self._secondary_text, text_area_width, secondary_font_size)
secondary_y = text_block_y + len(title_lines) * title_font_size + 8
rl.draw_text_ex(
self._font_medium,
secondary_text,
rl.Vector2(text_x, secondary_y),
secondary_font_size,
0,
rl.Color(255, 255, 255, 180),
)
self._draw_lines(title_lines, text_x, text_block_y, title_font_size, self._font_bold, rl.WHITE)
self._draw_lines(secondary_lines, text_x, text_block_y + n_title * title_font_size + gap, secondary_font_size, self._font_medium, rl.Color(255, 255, 255, 180))
if not show_next:
return