ML-183: scale Presto full-res layout

This commit is contained in:
Claudio Ortolina
2026-05-15 08:22:27 +01:00
parent f6c0e9f045
commit 7338825606
4 changed files with 302 additions and 194 deletions
@@ -0,0 +1,65 @@
---
id: ML-183
title: Scale Presto full-resolution layout
status: Done
assignee:
- codex
created_date: "2026-05-15 07:01"
updated_date: "2026-05-15 07:19"
labels:
- presto
dependencies: []
modified_files:
- presto/main.py
- presto/poc.py
- presto/README.md
priority: medium
ordinal: 17000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Adjust the Presto MicroPython app after enabling full-resolution 480x480 mode so controls, spacing, and bitmap text render at approximately the same physical size they had in the default doubled-pixel mode.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All hardcoded layout measurements in the Presto app are reviewed and scaled appropriately for full-resolution coordinates.
- [x] #2 Bitmap font scale values are reviewed and adjusted so visible text is not unexpectedly small after full_res=True.
- [x] #3 The app source passes the documented Python syntax check without writing **pycache**.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Scale fixed pixel constants in `presto/main.py` for full-resolution coordinates, using 2x values as the baseline and small fit adjustments where a strict double would push content offscreen.
2. Replace bitmap font scale usages in `presto/main.py` with named full-resolution scale constants so text returns to its prior physical size.
3. Apply the same full-resolution scaling to the small `presto/poc.py` proof-of-concept constants since it was also moved to `full_res=True`.
4. Re-scan for stale unscaled literals that affect layout or font rendering, then run the documented `py_compile` syntax check without creating `__pycache__`.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Scaled main Presto layout constants and bitmap font scale values for full-resolution coordinates. Calendar and keyboard use small fit adjustments from strict 2x values to avoid bottom overflow at 480x480. Row thumbnails now prefer the 150px mini cover source so 80px full-resolution row images do not render from a 40px micro image.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Scaled the Presto full-resolution UI so the move to `Presto(full_res=True)` keeps roughly the same physical sizing as the previous default doubled-pixel mode. `main.py` now uses named scale constants and a `px()` helper for layout measurements, font scales, row heights, scroll indicators, touch drag thresholds, headers, buttons, detail spacing, and keyboard geometry. Calendar cells and keyboard key height use small fit adjustments so their bottom rows stay within 480x480.
`poc.py` was scaled with the same 2x coordinate approach. The README thumbnail note was updated because row thumbnails now prefer `mini_cover_url`, avoiding 40px micro images inside the larger full-resolution row thumbnail box.
Verified with the documented `py_compile` syntax check for `main.py` and `poc.py`; no `__pycache__` output was written.
<!-- SECTION:FINAL_SUMMARY:END -->
+1 -1
View File
@@ -173,7 +173,7 @@ Response format:
} }
``` ```
The Presto client prefers `micro_cover_url` for the 40×40 row thumbnail, then falls back to `mini_cover_url` and `thumb_url`. The Presto client prefers `mini_cover_url` for the 80×80 full-resolution row thumbnail, then falls back to `micro_cover_url` and `thumb_url`.
The detail view prefers `thumb_url` for the full-size cover, falling back to `mini_cover_url` and `micro_cover_url`. The detail view prefers `thumb_url` for the full-size cover, falling back to `mini_cover_url` and `micro_cover_url`.
## License ## License
+213 -178
View File
@@ -59,34 +59,52 @@ display = presto.display
WIDTH, HEIGHT = display.get_bounds() WIDTH, HEIGHT = display.get_bounds()
touch = presto.touch touch = presto.touch
UI_SCALE = 2
TEXT_SCALE = UI_SCALE
TITLE_TEXT_SCALE = UI_SCALE
PLACEHOLDER_TEXT_SCALE = UI_SCALE * 2
def px(value):
"""Scale a low-resolution layout measurement to full-resolution pixels."""
return value * UI_SCALE
TEXT_H = px(8)
TEXT_LINE_H = px(12)
TITLE_LINE_H = px(14)
# Calendar layout (computed from display bounds) # Calendar layout (computed from display bounds)
CELL_GAP = 3 CELL_GAP = px(3)
CELLS_PER_ROW = 7 CELLS_PER_ROW = 7
MAX_ROWS = 6 MAX_ROWS = 6
# Header # Header
HEADER_Y = 0 HEADER_Y = 0
HEADER_H = 31 HEADER_H = px(31)
HEADER_TEXT_H = 14 HEADER_TEXT_H = px(14)
HEADER_SIDE_MARGIN = 8 HEADER_SIDE_MARGIN = px(8)
HEADER_BUTTON_W = 32 HEADER_BUTTON_W = px(32)
HEADER_BUTTON_H = 25 HEADER_BUTTON_H = px(25)
HEADER_BUTTON_Y = HEADER_Y + (HEADER_H - HEADER_BUTTON_H) // 2 HEADER_BUTTON_Y = HEADER_Y + (HEADER_H - HEADER_BUTTON_H) // 2
ARROW_W = HEADER_BUTTON_W ARROW_W = HEADER_BUTTON_W
ARROW_H = HEADER_BUTTON_H ARROW_H = HEADER_BUTTON_H
# Day-of-week labels # Day-of-week labels
DOW_Y = HEADER_Y + HEADER_H + 2 DOW_Y = HEADER_Y + HEADER_H + px(2)
DOW_H = 16 DOW_H = px(16)
# Day cells: fit 7 across with comfortable side margins
CELL_SIZE = (WIDTH - 16 - (CELLS_PER_ROW - 1) * CELL_GAP) // CELLS_PER_ROW
if CELL_SIZE > 78:
CELL_SIZE = 78
GRID_LEFT = (WIDTH - (CELLS_PER_ROW * CELL_SIZE + (CELLS_PER_ROW - 1) * CELL_GAP)) // 2
# Grid starts after day-of-week labels with a small gap # Grid starts after day-of-week labels with a small gap
CALENDAR_TOP = DOW_Y + DOW_H + 6 CALENDAR_TOP = DOW_Y + DOW_H + px(6)
# Day cells: fit 7 across with comfortable side margins
CELL_SIZE_BY_WIDTH = (WIDTH - px(16) - (CELLS_PER_ROW - 1) * CELL_GAP) // CELLS_PER_ROW
CELL_SIZE_BY_HEIGHT = (
HEIGHT - CALENDAR_TOP - px(8) - (MAX_ROWS - 1) * CELL_GAP
) // MAX_ROWS
CELL_SIZE = min(CELL_SIZE_BY_WIDTH, CELL_SIZE_BY_HEIGHT, px(78))
GRID_LEFT = (WIDTH - (CELLS_PER_ROW * CELL_SIZE + (CELLS_PER_ROW - 1) * CELL_GAP)) // 2
# Day view (proportional to cell size) # Day view (proportional to cell size)
BACK_X = HEADER_SIDE_MARGIN BACK_X = HEADER_SIDE_MARGIN
@@ -96,28 +114,28 @@ BACK_H = HEADER_BUTTON_H
DAY_HEADER_Y = HEADER_Y DAY_HEADER_Y = HEADER_Y
DAY_HEADER_H = HEADER_H DAY_HEADER_H = HEADER_H
DAY_HEADER_TEXT_H = HEADER_TEXT_H DAY_HEADER_TEXT_H = HEADER_TEXT_H
DAY_COUNT_TEXT_H = 8 DAY_COUNT_TEXT_H = TEXT_H
# Detail view # Detail view
DETAIL_COVER_SIZE = 150 DETAIL_COVER_SIZE = px(150)
DETAIL_COVER_X = (WIDTH - DETAIL_COVER_SIZE) // 2 DETAIL_COVER_X = (WIDTH - DETAIL_COVER_SIZE) // 2
DETAIL_COVER_Y = DAY_HEADER_Y + DAY_HEADER_H + 12 DETAIL_COVER_Y = DAY_HEADER_Y + DAY_HEADER_H + px(12)
DETAIL_INFO_GAP = 4 DETAIL_INFO_GAP = px(4)
DETAIL_TEXT_X = 40 DETAIL_TEXT_X = px(40)
DETAIL_TEXT_W = WIDTH - (2 * DETAIL_TEXT_X) DETAIL_TEXT_W = WIDTH - (2 * DETAIL_TEXT_X)
# Scrobble button (detail view) # Scrobble button (detail view)
SCROBBLE_BUTTON_W = 200 SCROBBLE_BUTTON_W = px(200)
SCROBBLE_BUTTON_H = 40 SCROBBLE_BUTTON_H = px(40)
SCROBBLE_BUTTON_GAP = 12 # vertical gap before button SCROBBLE_BUTTON_GAP = px(12) # vertical gap before button
RECORD_START_Y = DAY_HEADER_Y + DAY_HEADER_H + 8 RECORD_START_Y = DAY_HEADER_Y + DAY_HEADER_H + px(8)
THUMB_SIZE = 40 THUMB_SIZE = px(40)
THUMB_MARGIN = 8 THUMB_MARGIN = px(8)
ROW_PAD_Y = 8 ROW_PAD_Y = px(8)
ROW_SEPARATOR_H = 1 ROW_SEPARATOR_H = px(1)
TEXT_X = THUMB_MARGIN + THUMB_SIZE + 12 TEXT_X = THUMB_MARGIN + THUMB_SIZE + px(12)
TEXT_W = WIDTH - TEXT_X - 12 TEXT_W = WIDTH - TEXT_X - px(12)
# Colors (RGB tuples, converted to pens at runtime) # Colors (RGB tuples, converted to pens at runtime)
BG = (24, 24, 27) BG = (24, 24, 27)
@@ -152,7 +170,7 @@ MONTH_NAMES = [
# Touch debounce (milliseconds) # Touch debounce (milliseconds)
DEBOUNCE_MS = 300 DEBOUNCE_MS = 300
DRAG_REDRAW_MS = 40 DRAG_REDRAW_MS = 40
DRAG_REDRAW_PX = 8 DRAG_REDRAW_PX = px(8)
TOUCH_RELEASE_TIMEOUT_MS = 3_000 TOUCH_RELEASE_TIMEOUT_MS = 3_000
# WiFi connection timeout (seconds) # WiFi connection timeout (seconds)
@@ -167,24 +185,24 @@ DISPLAY_FADE_STEPS = 20
# Home screen # Home screen
HOME_TITLE = "Music Library" HOME_TITLE = "Music Library"
HOME_TITLE_Y = 60 HOME_TITLE_Y = px(60)
HOME_BUTTON_W = WIDTH - 80 HOME_BUTTON_W = WIDTH - px(80)
HOME_BUTTON_H = 50 HOME_BUTTON_H = px(50)
HOME_BUTTON_X = 40 HOME_BUTTON_X = px(40)
HOME_BUTTON1_Y = 120 HOME_BUTTON1_Y = px(120)
HOME_BUTTON2_Y = HOME_BUTTON1_Y + HOME_BUTTON_H + 10 HOME_BUTTON2_Y = HOME_BUTTON1_Y + HOME_BUTTON_H + px(10)
HOME_BTN_BG = (55, 65, 81) # Slate blue — distinct from BG and CELL_BG HOME_BTN_BG = (55, 65, 81) # Slate blue — distinct from BG and CELL_BG
# On-screen keyboard # On-screen keyboard
KB_MARGIN = 20 KB_MARGIN = px(20)
KB_KEY_GAP = 3 KB_KEY_GAP = px(3)
KB_KEY_H = 38 KB_KEY_H = px(36)
KB_KEY_W = (WIDTH - 2 * KB_MARGIN - 9 * KB_KEY_GAP) // 10 KB_KEY_W = (WIDTH - 2 * KB_MARGIN - 9 * KB_KEY_GAP) // 10
KB_INPUT_Y = 42 KB_INPUT_Y = px(42)
KB_INPUT_H = 28 KB_INPUT_H = px(28)
KB_INPUT_MARGIN = 40 KB_INPUT_MARGIN = px(40)
KB_ROWS_START_Y = KB_INPUT_Y + KB_INPUT_H + 12 KB_ROWS_START_Y = KB_INPUT_Y + KB_INPUT_H + px(12)
ALPHA_KEYS = [ ALPHA_KEYS = [
list("qwertyuiop"), list("qwertyuiop"),
@@ -339,7 +357,7 @@ def _init_pens():
# HELPER: TEXT WRAPPING # HELPER: TEXT WRAPPING
# ============================================================================ # ============================================================================
def draw_wrapped(text, x, y, max_width, pen, scale=1): def draw_wrapped(text, x, y, max_width, pen, scale=TEXT_SCALE):
"""Draw text, wrapping at word boundaries to fit within max_width pixels. """Draw text, wrapping at word boundaries to fit within max_width pixels.
PicoGraphics doesn't support automatic wrapping, so we manually break PicoGraphics doesn't support automatic wrapping, so we manually break
@@ -347,7 +365,7 @@ def draw_wrapped(text, x, y, max_width, pen, scale=1):
""" """
lines = _wrapped_lines(text, max_width, scale=scale) lines = _wrapped_lines(text, max_width, scale=scale)
line_height = 8 * scale + 4 # bitmap8 is 8px tall, plus spacing line_height = _text_line_height(scale)
cy = y cy = y
display.set_pen(pen) display.set_pen(pen)
for line in lines: for line in lines:
@@ -357,9 +375,9 @@ def draw_wrapped(text, x, y, max_width, pen, scale=1):
return cy return cy
def draw_wrapped_lines(lines, x, y, pen, scale=1): def draw_wrapped_lines(lines, x, y, pen, scale=TEXT_SCALE):
"""Draw pre-wrapped text lines and return the Y position after them.""" """Draw pre-wrapped text lines and return the Y position after them."""
line_height = 8 * scale + 4 line_height = _text_line_height(scale)
cy = y cy = y
display.set_pen(pen) display.set_pen(pen)
for line in lines: for line in lines:
@@ -369,6 +387,11 @@ def draw_wrapped_lines(lines, x, y, pen, scale=1):
return cy return cy
def _text_line_height(scale=TEXT_SCALE):
"""Return a bitmap8 line height including scaled inter-line spacing."""
return 8 * scale + px(4)
def display_text(text): def display_text(text):
"""Replace characters not available in the bitmap font.""" """Replace characters not available in the bitmap font."""
replacements = ( replacements = (
@@ -404,12 +427,12 @@ def display_text(text):
return rendered return rendered
def _wrapped_line_count(text, max_width, scale=1): def _wrapped_line_count(text, max_width, scale=TEXT_SCALE):
"""Return how many wrapped lines the text will occupy.""" """Return how many wrapped lines the text will occupy."""
return max(1, len(_wrapped_lines(text, max_width, scale=scale))) return max(1, len(_wrapped_lines(text, max_width, scale=scale)))
def _wrapped_lines(text, max_width, scale=1): def _wrapped_lines(text, max_width, scale=TEXT_SCALE):
"""Return wrapped lines for text using the display font metrics.""" """Return wrapped lines for text using the display font metrics."""
words = str(text).split() words = str(text).split()
lines = [] lines = []
@@ -912,9 +935,9 @@ def _draw_placeholder(x, y, w, h):
display.set_pen(_pen_placeholder) display.set_pen(_pen_placeholder)
display.rectangle(x, y, w, h) display.rectangle(x, y, w, h)
display.set_pen(_pen_dim_text) display.set_pen(_pen_dim_text)
cx = x + w // 2 - display.measure_text("?", scale=2) // 2 cx = x + w // 2 - display.measure_text("?", scale=PLACEHOLDER_TEXT_SCALE) // 2
cy = y + h // 2 - 10 cy = y + h // 2 - px(10)
display.text("?", cx, cy, scale=2) display.text("?", cx, cy, scale=PLACEHOLDER_TEXT_SCALE)
# ============================================================================ # ============================================================================
@@ -930,14 +953,14 @@ def draw_status(message):
display.set_font("bitmap8") display.set_font("bitmap8")
lines = message.split("\n") lines = message.split("\n")
line_h = 20 line_h = px(20)
total_h = len(lines) * line_h total_h = len(lines) * line_h
start_y = (HEIGHT - total_h) // 2 start_y = (HEIGHT - total_h) // 2
for i, line in enumerate(lines): for i, line in enumerate(lines):
w = display.measure_text(line, scale=1) w = display.measure_text(line, scale=TEXT_SCALE)
x = (WIDTH - w) // 2 x = (WIDTH - w) // 2
display.text(line, x, start_y + i * line_h, scale=1) display.text(line, x, start_y + i * line_h, scale=TEXT_SCALE)
presto.update() presto.update()
@@ -958,8 +981,8 @@ def draw_month_view(app):
presto.update() presto.update()
HOME_BTN_W = 30 HOME_BTN_W = px(30)
HOME_BTN_H = 25 HOME_BTN_H = px(25)
HOME_BTN_Y = HEADER_Y + (HEADER_H - HOME_BTN_H) // 2 HOME_BTN_Y = HEADER_Y + (HEADER_H - HOME_BTN_H) // 2
@@ -973,14 +996,14 @@ def _draw_month_header(app):
display.set_pen(_pen_header_text) display.set_pen(_pen_header_text)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
title = "{} {}".format(MONTH_NAMES[app.view_month - 1], app.view_year) title = "{} {}".format(MONTH_NAMES[app.view_month - 1], app.view_year)
tw = display.measure_text(title, scale=1) tw = display.measure_text(title, scale=TEXT_SCALE)
# Left boundary: after Home + left-arrow; right boundary: before right-arrow # Left boundary: after Home + left-arrow; right boundary: before right-arrow
title_left = HEADER_SIDE_MARGIN + HOME_BTN_W + 4 + ARROW_W title_left = HEADER_SIDE_MARGIN + HOME_BTN_W + px(4) + ARROW_W
title_right = WIDTH - ARROW_W - HEADER_SIDE_MARGIN title_right = WIDTH - ARROW_W - HEADER_SIDE_MARGIN
title_area = title_right - title_left title_area = title_right - title_left
tx = title_left + (title_area - tw) // 2 tx = title_left + (title_area - tw) // 2
ty = HEADER_Y + (HEADER_H - HEADER_TEXT_H) // 2 ty = HEADER_Y + (HEADER_H - HEADER_TEXT_H) // 2
display.text(title, tx, ty, scale=1) display.text(title, tx, ty, scale=TEXT_SCALE)
# Home button (leftmost) # Home button (leftmost)
hx = HEADER_SIDE_MARGIN hx = HEADER_SIDE_MARGIN
@@ -989,16 +1012,16 @@ def _draw_month_header(app):
display.rectangle(hx, hy, HOME_BTN_W, HOME_BTN_H) display.rectangle(hx, hy, HOME_BTN_W, HOME_BTN_H)
display.set_pen(_pen_back) display.set_pen(_pen_back)
display.set_font("bitmap8") display.set_font("bitmap8")
_draw_centered_text("H", hx, hy, HOME_BTN_W, HOME_BTN_H, 8) _draw_centered_text("H", hx, hy, HOME_BTN_W, HOME_BTN_H, TEXT_H)
# Left arrow (shifted right to make room for Home) # Left arrow (shifted right to make room for Home)
lx = hx + HOME_BTN_W + 4 lx = hx + HOME_BTN_W + px(4)
ly = HEADER_BUTTON_Y ly = HEADER_BUTTON_Y
display.set_pen(_pen_placeholder) display.set_pen(_pen_placeholder)
display.rectangle(lx, ly, ARROW_W, ARROW_H) display.rectangle(lx, ly, ARROW_W, ARROW_H)
display.set_pen(_pen_back) display.set_pen(_pen_back)
display.set_font("bitmap8") display.set_font("bitmap8")
_draw_centered_text("<", lx, ly, ARROW_W, ARROW_H, 8) _draw_centered_text("<", lx, ly, ARROW_W, ARROW_H, TEXT_H)
# Right arrow # Right arrow
rx = WIDTH - ARROW_W - HEADER_SIDE_MARGIN rx = WIDTH - ARROW_W - HEADER_SIDE_MARGIN
@@ -1007,10 +1030,10 @@ def _draw_month_header(app):
display.rectangle(rx, ry, ARROW_W, ARROW_H) display.rectangle(rx, ry, ARROW_W, ARROW_H)
display.set_pen(_pen_back) display.set_pen(_pen_back)
display.set_font("bitmap8") display.set_font("bitmap8")
_draw_centered_text(">", rx, ry, ARROW_W, ARROW_H, 8) _draw_centered_text(">", rx, ry, ARROW_W, ARROW_H, TEXT_H)
def _draw_centered_text(text, x, y, w, h, text_h, scale=1): def _draw_centered_text(text, x, y, w, h, text_h, scale=TEXT_SCALE):
"""Draw text centered inside a rectangular area.""" """Draw text centered inside a rectangular area."""
tw = display.measure_text(text, scale=scale) tw = display.measure_text(text, scale=scale)
tx = x + (w - tw) // 2 tx = x + (w - tw) // 2
@@ -1024,8 +1047,8 @@ def _draw_day_labels():
display.set_font("bitmap8") display.set_font("bitmap8")
for i, name in enumerate(DAY_NAMES): for i, name in enumerate(DAY_NAMES):
cx = GRID_LEFT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE // 2 cx = GRID_LEFT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE // 2
tw = display.measure_text(name, scale=1) tw = display.measure_text(name, scale=TEXT_SCALE)
display.text(name, cx - tw // 2, DOW_Y, scale=1) display.text(name, cx - tw // 2, DOW_Y, scale=TEXT_SCALE)
def _draw_day_grid(app): def _draw_day_grid(app):
@@ -1107,8 +1130,8 @@ def _draw_day_grid(app):
def _draw_centered_num(num, cx, cy): def _draw_centered_num(num, cx, cy):
"""Draw a number centered at (cx, cy).""" """Draw a number centered at (cx, cy)."""
text = str(num) text = str(num)
tw = display.measure_text(text, scale=1) tw = display.measure_text(text, scale=TEXT_SCALE)
display.text(text, cx - tw // 2, cy - 5, scale=1) display.text(text, cx - tw // 2, cy - px(5), scale=TEXT_SCALE)
# ============================================================================ # ============================================================================
@@ -1123,8 +1146,8 @@ def draw_home_screen(app):
# Title # Title
display.set_pen(_pen_title) display.set_pen(_pen_title)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
tw = display.measure_text(HOME_TITLE, scale=1) tw = display.measure_text(HOME_TITLE, scale=TEXT_SCALE)
display.text(HOME_TITLE, (WIDTH - tw) // 2, HOME_TITLE_Y, scale=1) display.text(HOME_TITLE, (WIDTH - tw) // 2, HOME_TITLE_Y, scale=TEXT_SCALE)
# Buttons # Buttons
_draw_home_button(HOME_BUTTON1_Y, "Search Collection") _draw_home_button(HOME_BUTTON1_Y, "Search Collection")
@@ -1139,10 +1162,10 @@ def _draw_home_button(y, label):
display.rectangle(HOME_BUTTON_X, y, HOME_BUTTON_W, HOME_BUTTON_H) display.rectangle(HOME_BUTTON_X, y, HOME_BUTTON_W, HOME_BUTTON_H)
display.set_pen(_pen_today_text) display.set_pen(_pen_today_text)
display.set_font("bitmap8") display.set_font("bitmap8")
tw = display.measure_text(label, scale=1) tw = display.measure_text(label, scale=TEXT_SCALE)
tx = HOME_BUTTON_X + (HOME_BUTTON_W - tw) // 2 tx = HOME_BUTTON_X + (HOME_BUTTON_W - tw) // 2
ty = y + (HOME_BUTTON_H - 8) // 2 ty = y + (HOME_BUTTON_H - TEXT_H) // 2
display.text(label, tx, ty, scale=1) display.text(label, tx, ty, scale=TEXT_SCALE)
# ============================================================================ # ============================================================================
@@ -1164,7 +1187,12 @@ def draw_search_input(app):
display.set_font("bitmap8") display.set_font("bitmap8")
# Show cursor as underscore if query is empty # Show cursor as underscore if query is empty
display_q = app.search.query if app.search.query else "_" display_q = app.search.query if app.search.query else "_"
display.text(display_q, KB_INPUT_MARGIN + 8, KB_INPUT_Y + 12, scale=1) display.text(
display_q,
KB_INPUT_MARGIN + px(8),
KB_INPUT_Y + px(12),
scale=TEXT_SCALE
)
_draw_keyboard(app) _draw_keyboard(app)
presto.update() presto.update()
@@ -1187,9 +1215,9 @@ def _draw_search_input_header():
display.set_pen(_pen_header_text) display.set_pen(_pen_header_text)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
title = "Search" title = "Search"
tw = display.measure_text(title, scale=1) tw = display.measure_text(title, scale=TEXT_SCALE)
display.text(title, (WIDTH - tw) // 2, display.text(title, (WIDTH - tw) // 2,
DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2, scale=1) DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2, scale=TEXT_SCALE)
def _draw_keyboard(app): def _draw_keyboard(app):
@@ -1228,10 +1256,10 @@ def _draw_key(x, y, label, w, h, text_pen=None):
display.rectangle(x, y, w, h) display.rectangle(x, y, w, h)
pen = text_pen if text_pen is not None else _pen_normal_text pen = text_pen if text_pen is not None else _pen_normal_text
display.set_pen(pen) display.set_pen(pen)
tw = display.measure_text(label, scale=1) tw = display.measure_text(label, scale=TEXT_SCALE)
tx = x + (w - tw) // 2 tx = x + (w - tw) // 2
ty = y + (h - 8) // 2 ty = y + (h - TEXT_H) // 2
display.text(label, tx, ty, scale=1) display.text(label, tx, ty, scale=TEXT_SCALE)
def _keyboard_hit_test(app, x, y): def _keyboard_hit_test(app, x, y):
@@ -1326,17 +1354,17 @@ def _draw_search_results_header(app):
display.set_pen(_pen_header_text) display.set_pen(_pen_header_text)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
display_label = "Search: " + app.search.query display_label = "Search: " + app.search.query
max_w = WIDTH - (BACK_X + BACK_W + 8) - 8 max_w = WIDTH - (BACK_X + BACK_W + px(8)) - px(8)
if display.measure_text(display_label, scale=1) > max_w: if display.measure_text(display_label, scale=TEXT_SCALE) > max_w:
suffix = "..." suffix = "..."
display_q = app.search.query display_q = app.search.query
while (display.measure_text("Search: " + display_q + suffix, scale=1) > max_w while (display.measure_text("Search: " + display_q + suffix, scale=TEXT_SCALE) > max_w
and len(display_q) > 0): and len(display_q) > 0):
display_q = display_q[:-1] display_q = display_q[:-1]
display_label = "Search: " + (display_q + suffix if display_q else suffix) display_label = "Search: " + (display_q + suffix if display_q else suffix)
tw = display.measure_text(display_label, scale=1) tw = display.measure_text(display_label, scale=TEXT_SCALE)
display.text(display_label, (WIDTH - tw) // 2, display.text(display_label, (WIDTH - tw) // 2,
DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2, scale=1) DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2, scale=TEXT_SCALE)
def _draw_search_record_list(app): def _draw_search_record_list(app):
@@ -1348,18 +1376,18 @@ def _draw_search_record_list(app):
if app.search.scroll_offset > 0: if app.search.scroll_offset > 0:
display.set_pen(_pen_arrow) display.set_pen(_pen_arrow)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
display.text("^", WIDTH // 2 - 8, RECORD_START_Y - 28, scale=1) display.text("^", WIDTH // 2 - px(8), RECORD_START_Y - px(28), scale=TEXT_SCALE)
cy = RECORD_START_Y - app.search.scroll_offset cy = RECORD_START_Y - app.search.scroll_offset
for rec in app.search.results: for rec in app.search.results:
row_h = rec.get("_row_height", THUMB_SIZE + 8) row_h = rec.get("_row_height", THUMB_SIZE + px(8))
row_bottom = cy + row_h row_bottom = cy + row_h
if row_bottom <= RECORD_START_Y: if row_bottom <= RECORD_START_Y:
cy = row_bottom cy = row_bottom
continue continue
if cy >= HEIGHT - 28: if cy >= HEIGHT - px(28):
break break
_draw_record_row(app, rec, cy) _draw_record_row(app, rec, cy)
@@ -1369,12 +1397,12 @@ def _draw_search_record_list(app):
if app.search.scroll_offset < max_offset: if app.search.scroll_offset < max_offset:
display.set_pen(_pen_arrow) display.set_pen(_pen_arrow)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
display.text("v", WIDTH // 2 - 8, HEIGHT - 24, scale=1) display.text("v", WIDTH // 2 - px(8), HEIGHT - px(24), scale=TEXT_SCALE)
def _search_max_scroll_offset(app): def _search_max_scroll_offset(app):
"""Return the maximum pixel scroll offset for search results.""" """Return the maximum pixel scroll offset for search results."""
viewport_h = HEIGHT - RECORD_START_Y - 28 viewport_h = HEIGHT - RECORD_START_Y - px(28)
return max(0, app.search.content_height - viewport_h) return max(0, app.search.content_height - viewport_h)
@@ -1383,8 +1411,8 @@ def _draw_search_empty():
display.set_pen(_pen_dim_text) display.set_pen(_pen_dim_text)
display.set_font("bitmap8") display.set_font("bitmap8")
msg = "No records found" msg = "No records found"
mw = display.measure_text(msg, scale=1) mw = display.measure_text(msg, scale=TEXT_SCALE)
display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 10, scale=1) display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - px(10), scale=TEXT_SCALE)
def _draw_search_error(): def _draw_search_error():
@@ -1392,13 +1420,13 @@ def _draw_search_error():
display.set_pen(_pen_error) display.set_pen(_pen_error)
display.set_font("bitmap8") display.set_font("bitmap8")
msg = "Could not reach server" msg = "Could not reach server"
mw = display.measure_text(msg, scale=1) mw = display.measure_text(msg, scale=TEXT_SCALE)
display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 20, scale=1) display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - px(20), scale=TEXT_SCALE)
display.set_pen(_pen_dim_text) display.set_pen(_pen_dim_text)
hint = "Tap to retry" hint = "Tap to retry"
hw = display.measure_text(hint, scale=1) hw = display.measure_text(hint, scale=TEXT_SCALE)
display.text(hint, (WIDTH - hw) // 2, HEIGHT // 2 + 10, scale=1) display.text(hint, (WIDTH - hw) // 2, HEIGHT // 2 + px(10), scale=TEXT_SCALE)
# ============================================================================ # ============================================================================
@@ -1447,12 +1475,12 @@ def _draw_day_header(app):
) )
display.set_pen(_pen_header_text) display.set_pen(_pen_header_text)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
tw = display.measure_text(date_str, scale=1) tw = display.measure_text(date_str, scale=TEXT_SCALE)
display.text( display.text(
date_str, date_str,
(WIDTH - tw) // 2, (WIDTH - tw) // 2,
DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2, DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2,
scale=1 scale=TEXT_SCALE
) )
# Record count # Record count
@@ -1460,12 +1488,12 @@ def _draw_day_header(app):
count_str = "{} record{}".format(record_count, "s" if record_count != 1 else "") count_str = "{} record{}".format(record_count, "s" if record_count != 1 else "")
display.set_pen(_pen_dim_text) display.set_pen(_pen_dim_text)
display.set_font("bitmap8") display.set_font("bitmap8")
cw = display.measure_text(count_str, scale=1) cw = display.measure_text(count_str, scale=TEXT_SCALE)
display.text( display.text(
count_str, count_str,
WIDTH - cw - 11, WIDTH - cw - px(11),
DAY_HEADER_Y + (DAY_HEADER_H - DAY_COUNT_TEXT_H) // 2, DAY_HEADER_Y + (DAY_HEADER_H - DAY_COUNT_TEXT_H) // 2,
scale=1 scale=TEXT_SCALE
) )
@@ -1474,13 +1502,13 @@ def _draw_day_error():
display.set_pen(_pen_error) display.set_pen(_pen_error)
display.set_font("bitmap8") display.set_font("bitmap8")
msg = "Could not reach server" msg = "Could not reach server"
mw = display.measure_text(msg, scale=1) mw = display.measure_text(msg, scale=TEXT_SCALE)
display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 20, scale=1) display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - px(20), scale=TEXT_SCALE)
display.set_pen(_pen_dim_text) display.set_pen(_pen_dim_text)
hint = "Tap < Back to return" hint = "Tap < Back to return"
hw = display.measure_text(hint, scale=1) hw = display.measure_text(hint, scale=TEXT_SCALE)
display.text(hint, (WIDTH - hw) // 2, HEIGHT // 2 + 10, scale=1) display.text(hint, (WIDTH - hw) // 2, HEIGHT // 2 + px(10), scale=TEXT_SCALE)
def _draw_day_empty(): def _draw_day_empty():
@@ -1488,8 +1516,8 @@ def _draw_day_empty():
display.set_pen(_pen_dim_text) display.set_pen(_pen_dim_text)
display.set_font("bitmap8") display.set_font("bitmap8")
msg = "No records on this day" msg = "No records on this day"
mw = display.measure_text(msg, scale=1) mw = display.measure_text(msg, scale=TEXT_SCALE)
display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 10, scale=1) display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - px(10), scale=TEXT_SCALE)
def _draw_record_list(app): def _draw_record_list(app):
@@ -1502,19 +1530,19 @@ def _draw_record_list(app):
if app.day.scroll_offset > 0: if app.day.scroll_offset > 0:
display.set_pen(_pen_arrow) display.set_pen(_pen_arrow)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
display.text("^", WIDTH // 2 - 8, RECORD_START_Y - 28, scale=1) display.text("^", WIDTH // 2 - px(8), RECORD_START_Y - px(28), scale=TEXT_SCALE)
cy = RECORD_START_Y - app.day.scroll_offset cy = RECORD_START_Y - app.day.scroll_offset
for rec in app.day.records: for rec in app.day.records:
row_h = rec.get("_row_height", THUMB_SIZE + 8) row_h = rec.get("_row_height", THUMB_SIZE + px(8))
row_bottom = cy + row_h row_bottom = cy + row_h
if row_bottom <= RECORD_START_Y: if row_bottom <= RECORD_START_Y:
cy = row_bottom cy = row_bottom
continue continue
if cy >= HEIGHT - 28: if cy >= HEIGHT - px(28):
break break
_draw_record_row(app, rec, cy) _draw_record_row(app, rec, cy)
@@ -1524,11 +1552,11 @@ def _draw_record_list(app):
if app.day.scroll_offset < max_offset: if app.day.scroll_offset < max_offset:
display.set_pen(_pen_arrow) display.set_pen(_pen_arrow)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
display.text("v", WIDTH // 2 - 8, HEIGHT - 24, scale=1) display.text("v", WIDTH // 2 - px(8), HEIGHT - px(24), scale=TEXT_SCALE)
def _max_scroll_offset(app): def _max_scroll_offset(app):
"""Return the maximum pixel scroll offset for the current records.""" """Return the maximum pixel scroll offset for the current records."""
viewport_h = HEIGHT - RECORD_START_Y - 28 viewport_h = HEIGHT - RECORD_START_Y - px(28)
return max(0, app.day.content_height - viewport_h) return max(0, app.day.content_height - viewport_h)
@@ -1542,7 +1570,7 @@ def prepare_record_list(app, recs):
content_height = 0 content_height = 0
for rec in recs: for rec in recs:
_prepare_record_for_display(rec) _prepare_record_for_display(rec)
content_height += rec.get("_row_height", THUMB_SIZE + 8) content_height += rec.get("_row_height", THUMB_SIZE + px(8))
preload_record_thumbnails(app, recs) preload_record_thumbnails(app, recs)
gc.collect() gc.collect()
@@ -1559,12 +1587,12 @@ def _prepare_record_for_display(rec):
rec["_display_title"] = title rec["_display_title"] = title
rec["_display_artists"] = artist_str rec["_display_artists"] = artist_str
rec["_display_meta"] = meta_text rec["_display_meta"] = meta_text
rec["_display_title_lines"] = _wrapped_lines(title, TEXT_W, scale=1) rec["_display_title_lines"] = _wrapped_lines(title, TEXT_W, scale=TEXT_SCALE)
rec["_display_artist_lines"] = ( rec["_display_artist_lines"] = (
_wrapped_lines(artist_str, TEXT_W, scale=1) if artist_str else [] _wrapped_lines(artist_str, TEXT_W, scale=TEXT_SCALE) if artist_str else []
) )
rec["_display_meta_lines"] = ( rec["_display_meta_lines"] = (
_wrapped_lines(meta_text, TEXT_W, scale=1) if meta_text else [] _wrapped_lines(meta_text, TEXT_W, scale=TEXT_SCALE) if meta_text else []
) )
rec["_thumb_url"] = _record_thumbnail_url(rec) rec["_thumb_url"] = _record_thumbnail_url(rec)
rec["_row_height"] = _record_row_height(rec) rec["_row_height"] = _record_row_height(rec)
@@ -1583,22 +1611,22 @@ def _record_row_height(rec):
title_lines = rec.get("_display_title_lines", None) title_lines = rec.get("_display_title_lines", None)
if title_lines is None: if title_lines is None:
title = rec.get("_display_title", display_text(rec.get("title", "Unknown Title"))) title = rec.get("_display_title", display_text(rec.get("title", "Unknown Title")))
title_lines = _wrapped_lines(title, TEXT_W, scale=1) title_lines = _wrapped_lines(title, TEXT_W, scale=TEXT_SCALE)
ty = max(1, len(title_lines)) * 12 ty = max(1, len(title_lines)) * TEXT_LINE_H
artist_lines = rec.get("_display_artist_lines", None) artist_lines = rec.get("_display_artist_lines", None)
if artist_lines is None: if artist_lines is None:
artist_str = rec.get("_display_artists", "") artist_str = rec.get("_display_artists", "")
artist_lines = _wrapped_lines(artist_str, TEXT_W, scale=1) if artist_str else [] artist_lines = _wrapped_lines(artist_str, TEXT_W, scale=TEXT_SCALE) if artist_str else []
if artist_lines: if artist_lines:
ty += 2 + len(artist_lines) * 12 ty += px(2) + len(artist_lines) * TEXT_LINE_H
meta_lines = rec.get("_display_meta_lines", None) meta_lines = rec.get("_display_meta_lines", None)
if meta_lines is None: if meta_lines is None:
meta_text = rec.get("_display_meta", _record_meta_text(rec)) meta_text = rec.get("_display_meta", _record_meta_text(rec))
meta_lines = _wrapped_lines(meta_text, TEXT_W, scale=1) if meta_text else [] meta_lines = _wrapped_lines(meta_text, TEXT_W, scale=TEXT_SCALE) if meta_text else []
if meta_lines: if meta_lines:
ty += 2 + len(meta_lines) * 12 ty += px(2) + len(meta_lines) * TEXT_LINE_H
content_h = max(THUMB_SIZE, ty) + ROW_PAD_Y * 2 + ROW_SEPARATOR_H content_h = max(THUMB_SIZE, ty) + ROW_PAD_Y * 2 + ROW_SEPARATOR_H
return max(min_h, content_h) return max(min_h, content_h)
@@ -1648,8 +1676,8 @@ def _record_thumbnail_data(app, rec):
def _record_thumbnail_url(rec): def _record_thumbnail_url(rec):
"""Return the preferred cover URL for a record row.""" """Return the preferred cover URL for a record row."""
return ( return (
rec.get("micro_cover_url", "") rec.get("mini_cover_url", "")
or rec.get("mini_cover_url", "") or rec.get("micro_cover_url", "")
or rec.get("thumb_url", "") or rec.get("thumb_url", "")
) )
@@ -1678,26 +1706,26 @@ def _draw_record_row(app, rec, y):
title_lines = rec.get("_display_title_lines", None) title_lines = rec.get("_display_title_lines", None)
if title_lines is None: if title_lines is None:
title = rec.get("_display_title", display_text(rec.get("title", "Unknown Title"))) title = rec.get("_display_title", display_text(rec.get("title", "Unknown Title")))
ty = draw_wrapped(title, TEXT_X, ty, TEXT_W, _pen_title, scale=1) ty = draw_wrapped(title, TEXT_X, ty, TEXT_W, _pen_title, scale=TEXT_SCALE)
else: else:
ty = draw_wrapped_lines(title_lines, TEXT_X, ty, _pen_title, scale=1) ty = draw_wrapped_lines(title_lines, TEXT_X, ty, _pen_title, scale=TEXT_SCALE)
# Artists # Artists
artist_lines = rec.get("_display_artist_lines", None) artist_lines = rec.get("_display_artist_lines", None)
if artist_lines is None: if artist_lines is None:
artist_str = rec.get("_display_artists", "") artist_str = rec.get("_display_artists", "")
artist_lines = _wrapped_lines(artist_str, TEXT_W, scale=1) if artist_str else [] artist_lines = _wrapped_lines(artist_str, TEXT_W, scale=TEXT_SCALE) if artist_str else []
if artist_lines: if artist_lines:
ty += 2 ty += px(2)
ty = draw_wrapped_lines(artist_lines, TEXT_X, ty, _pen_artist, scale=1) ty = draw_wrapped_lines(artist_lines, TEXT_X, ty, _pen_artist, scale=TEXT_SCALE)
meta_lines = rec.get("_display_meta_lines", None) meta_lines = rec.get("_display_meta_lines", None)
if meta_lines is None: if meta_lines is None:
meta_text = rec.get("_display_meta", _record_meta_text(rec)) meta_text = rec.get("_display_meta", _record_meta_text(rec))
meta_lines = _wrapped_lines(meta_text, TEXT_W, scale=1) if meta_text else [] meta_lines = _wrapped_lines(meta_text, TEXT_W, scale=TEXT_SCALE) if meta_text else []
if meta_lines: if meta_lines:
ty += 2 ty += px(2)
ty = draw_wrapped_lines(meta_lines, TEXT_X, ty, _pen_dim_text, scale=1) ty = draw_wrapped_lines(meta_lines, TEXT_X, ty, _pen_dim_text, scale=TEXT_SCALE)
# Separator line at the computed bottom # Separator line at the computed bottom
display.set_pen(_pen_cell_other) display.set_pen(_pen_cell_other)
@@ -1721,16 +1749,23 @@ def _max_detail_scroll_offset(app):
return max(0, content_bottom - HEIGHT) return max(0, content_bottom - HEIGHT)
def _measure_detail_text_height(text, font="bitmap8", scale=1, max_w=None): def _measure_detail_text_height(text, font="bitmap8", scale=TEXT_SCALE, max_w=None):
"""Return the pixel height that text would occupy when drawn wrapped.""" """Return the pixel height that text would occupy when drawn wrapped."""
if max_w is None: if max_w is None:
max_w = DETAIL_TEXT_W max_w = DETAIL_TEXT_W
lines = _wrapped_lines(text, max_w, scale=scale) lines = _wrapped_lines(text, max_w, scale=scale)
line_height = 8 * scale + 4 line_height = _line_height_for_font(font, scale)
return len(lines) * line_height return len(lines) * line_height
def _line_metrics(lines, font="bitmap8", scale=1): def _line_height_for_font(font, scale=TEXT_SCALE):
"""Return scaled line height for the bitmap font used by a detail section."""
if font == "bitmap14_outline":
return 14 * scale
return _text_line_height(scale)
def _line_metrics(lines, font="bitmap8", scale=TEXT_SCALE):
"""Pair pre-wrapped text lines with their measured widths.""" """Pair pre-wrapped text lines with their measured widths."""
display.set_font(font) display.set_font(font)
metrics = [] metrics = []
@@ -1747,12 +1782,12 @@ def _measure_detail_content(app, rec):
title = rec.get("_display_title", title = rec.get("_display_title",
display_text(rec.get("title", "Unknown Title"))) display_text(rec.get("title", "Unknown Title")))
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
title_lines = _wrapped_lines(title, DETAIL_TEXT_W, scale=1) title_lines = _wrapped_lines(title, DETAIL_TEXT_W, scale=TEXT_SCALE)
rec["_detail_title_lines"] = _line_metrics( rec["_detail_title_lines"] = _line_metrics(
title_lines, font="bitmap14_outline", scale=1 title_lines, font="bitmap14_outline", scale=TEXT_SCALE
) )
h += len(title_lines) * 12 h += len(title_lines) * TITLE_LINE_H
h += 6 h += px(6)
# Artists (above cover) # Artists (above cover)
artist_str = rec.get("_display_artists", "") artist_str = rec.get("_display_artists", "")
@@ -1761,10 +1796,10 @@ def _measure_detail_content(app, rec):
artist_str = display_text(", ".join(artists)) if artists else "" artist_str = display_text(", ".join(artists)) if artists else ""
if artist_str: if artist_str:
display.set_font("bitmap8") display.set_font("bitmap8")
artist_lines = _wrapped_lines(artist_str, DETAIL_TEXT_W, scale=1) artist_lines = _wrapped_lines(artist_str, DETAIL_TEXT_W, scale=TEXT_SCALE)
rec["_detail_artist_lines"] = _line_metrics(artist_lines, scale=1) rec["_detail_artist_lines"] = _line_metrics(artist_lines, scale=TEXT_SCALE)
h += len(artist_lines) * 12 h += len(artist_lines) * TEXT_LINE_H
h += 4 h += px(4)
else: else:
rec["_detail_artist_lines"] = [] rec["_detail_artist_lines"] = []
@@ -1779,14 +1814,14 @@ def _measure_detail_content(app, rec):
if genres: if genres:
genre_str = display_text(", ".join(genres)) genre_str = display_text(", ".join(genres))
display.set_font("bitmap8") display.set_font("bitmap8")
genre_lines = _wrapped_lines(genre_str, DETAIL_TEXT_W, scale=1) genre_lines = _wrapped_lines(genre_str, DETAIL_TEXT_W, scale=TEXT_SCALE)
rec["_detail_genre_lines"] = _line_metrics(genre_lines, scale=1) rec["_detail_genre_lines"] = _line_metrics(genre_lines, scale=TEXT_SCALE)
h += len(genre_lines) * 12 h += len(genre_lines) * TEXT_LINE_H
h += 4 h += px(4)
else: else:
rec["_detail_genre_lines"] = [] rec["_detail_genre_lines"] = []
h += 6 h += px(6)
# Metadata line: record_type | format | year # Metadata line: record_type | format | year
meta_parts = [] meta_parts = []
@@ -1803,10 +1838,10 @@ def _measure_detail_content(app, rec):
if meta_parts: if meta_parts:
meta_str = " | ".join(meta_parts) meta_str = " | ".join(meta_parts)
display.set_font("bitmap8") display.set_font("bitmap8")
meta_lines = _wrapped_lines(meta_str, DETAIL_TEXT_W, scale=1) meta_lines = _wrapped_lines(meta_str, DETAIL_TEXT_W, scale=TEXT_SCALE)
rec["_detail_meta_lines"] = _line_metrics(meta_lines, scale=1) rec["_detail_meta_lines"] = _line_metrics(meta_lines, scale=TEXT_SCALE)
h += len(meta_lines) * 12 h += len(meta_lines) * TEXT_LINE_H
h += 4 h += px(4)
else: else:
rec["_detail_meta_lines"] = [] rec["_detail_meta_lines"] = []
@@ -1815,9 +1850,9 @@ def _measure_detail_content(app, rec):
if purchased_at: if purchased_at:
purch_str = "Purchased: " + display_text(str(purchased_at)[:10]) purch_str = "Purchased: " + display_text(str(purchased_at)[:10])
display.set_font("bitmap8") display.set_font("bitmap8")
purchased_lines = _wrapped_lines(purch_str, DETAIL_TEXT_W, scale=1) purchased_lines = _wrapped_lines(purch_str, DETAIL_TEXT_W, scale=TEXT_SCALE)
rec["_detail_purchased_lines"] = _line_metrics(purchased_lines, scale=1) rec["_detail_purchased_lines"] = _line_metrics(purchased_lines, scale=TEXT_SCALE)
h += len(purchased_lines) * 12 h += len(purchased_lines) * TEXT_LINE_H
else: else:
rec["_detail_purchased_lines"] = [] rec["_detail_purchased_lines"] = []
@@ -1906,10 +1941,10 @@ def draw_record_detail(app):
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
if offset > 0: if offset > 0:
display.set_pen(_pen_arrow) display.set_pen(_pen_arrow)
display.text("^", WIDTH // 2 - 8, DAY_HEADER_H + 4, scale=1) display.text("^", WIDTH // 2 - px(8), DAY_HEADER_H + px(4), scale=TEXT_SCALE)
if offset < max_off: if offset < max_off:
display.set_pen(_pen_arrow) display.set_pen(_pen_arrow)
display.text("v", WIDTH // 2 - 8, HEIGHT - 24, scale=1) display.text("v", WIDTH // 2 - px(8), HEIGHT - px(24), scale=TEXT_SCALE)
_remove_clip() _remove_clip()
@@ -1933,12 +1968,12 @@ def _draw_detail_header():
display.set_pen(_pen_header_text) display.set_pen(_pen_header_text)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
label = "Record" label = "Record"
tw = display.measure_text(label, scale=1) tw = display.measure_text(label, scale=TEXT_SCALE)
display.text( display.text(
label, label,
(WIDTH - tw) // 2, (WIDTH - tw) // 2,
DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2, DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2,
scale=1 scale=TEXT_SCALE
) )
@@ -1950,11 +1985,11 @@ def _draw_detail_title_artists(rec, y):
title = rec.get("_display_title", title = rec.get("_display_title",
display_text(rec.get("title", "Unknown Title"))) display_text(rec.get("title", "Unknown Title")))
y = _draw_detail_text_line(title, y, _pen_title, y = _draw_detail_text_line(title, y, _pen_title,
font="bitmap14_outline", scale=1) font="bitmap14_outline", scale=TEXT_SCALE)
else: else:
y = _draw_detail_text_lines(title_lines, y, _pen_title, y = _draw_detail_text_lines(title_lines, y, _pen_title,
font="bitmap14_outline", scale=1) font="bitmap14_outline", scale=TEXT_SCALE)
y += 6 y += px(6)
# Artists # Artists
artist_lines = rec.get("_detail_artist_lines", None) artist_lines = rec.get("_detail_artist_lines", None)
@@ -1963,10 +1998,10 @@ def _draw_detail_title_artists(rec, y):
if not artist_str: if not artist_str:
artists = rec.get("artists", []) artists = rec.get("artists", [])
artist_str = display_text(", ".join(artists)) if artists else "" artist_str = display_text(", ".join(artists)) if artists else ""
artist_lines = _wrapped_lines(artist_str, DETAIL_TEXT_W, scale=1) if artist_str else [] artist_lines = _wrapped_lines(artist_str, DETAIL_TEXT_W, scale=TEXT_SCALE) if artist_str else []
if artist_lines: if artist_lines:
y = _draw_detail_text_lines(artist_lines, y, _pen_artist) y = _draw_detail_text_lines(artist_lines, y, _pen_artist)
y += 4 y += px(4)
return y return y
@@ -2020,14 +2055,14 @@ def _draw_detail_info_below_cover(rec, y):
genres = rec.get("genres", []) genres = rec.get("genres", [])
if genres: if genres:
genre_str = display_text(", ".join(genres)) genre_str = display_text(", ".join(genres))
genre_lines = _wrapped_lines(genre_str, DETAIL_TEXT_W, scale=1) genre_lines = _wrapped_lines(genre_str, DETAIL_TEXT_W, scale=TEXT_SCALE)
else: else:
genre_lines = [] genre_lines = []
if genre_lines: if genre_lines:
y = _draw_detail_text_lines(genre_lines, y, _pen_dim_text) y = _draw_detail_text_lines(genre_lines, y, _pen_dim_text)
y += 4 y += px(4)
y += 6 y += px(6)
# Metadata: record_type | format | year # Metadata: record_type | format | year
meta_lines = rec.get("_detail_meta_lines", None) meta_lines = rec.get("_detail_meta_lines", None)
@@ -2042,11 +2077,11 @@ def _draw_detail_info_below_cover(rec, y):
release_date = rec.get("release_date", "") release_date = rec.get("release_date", "")
if release_date: if release_date:
meta_parts.append(str(release_date)[:4]) meta_parts.append(str(release_date)[:4])
meta_lines = _wrapped_lines(" | ".join(meta_parts), DETAIL_TEXT_W, scale=1) if meta_parts else [] meta_lines = _wrapped_lines(" | ".join(meta_parts), DETAIL_TEXT_W, scale=TEXT_SCALE) if meta_parts else []
if meta_lines: if meta_lines:
y = _draw_detail_text_lines(meta_lines, y, _pen_dim_text) y = _draw_detail_text_lines(meta_lines, y, _pen_dim_text)
y += 4 y += px(4)
# Purchased at # Purchased at
purchased_lines = rec.get("_detail_purchased_lines", None) purchased_lines = rec.get("_detail_purchased_lines", None)
@@ -2054,7 +2089,7 @@ def _draw_detail_info_below_cover(rec, y):
purchased_at = rec.get("purchased_at", "") purchased_at = rec.get("purchased_at", "")
if purchased_at: if purchased_at:
purch_str = "Purchased: " + display_text(str(purchased_at)[:10]) purch_str = "Purchased: " + display_text(str(purchased_at)[:10])
purchased_lines = _wrapped_lines(purch_str, DETAIL_TEXT_W, scale=1) purchased_lines = _wrapped_lines(purch_str, DETAIL_TEXT_W, scale=TEXT_SCALE)
else: else:
purchased_lines = [] purchased_lines = []
if purchased_lines: if purchased_lines:
@@ -2063,7 +2098,7 @@ def _draw_detail_info_below_cover(rec, y):
return y return y
def _draw_detail_text_line(text, y, pen, font="bitmap8", scale=1): def _draw_detail_text_line(text, y, pen, font="bitmap8", scale=TEXT_SCALE):
"""Draw text centered horizontally at y, wrapping if needed. """Draw text centered horizontally at y, wrapping if needed.
Returns the y-position after drawing (including wrapped lines).""" Returns the y-position after drawing (including wrapped lines)."""
max_w = DETAIL_TEXT_W max_w = DETAIL_TEXT_W
@@ -2071,7 +2106,7 @@ def _draw_detail_text_line(text, y, pen, font="bitmap8", scale=1):
display.set_font(font) display.set_font(font)
lines = _wrapped_lines(text, max_w, scale=scale) lines = _wrapped_lines(text, max_w, scale=scale)
line_height = 8 * scale + 4 line_height = _line_height_for_font(font, scale)
cy = y cy = y
for line in lines: for line in lines:
tw = display.measure_text(line, scale=scale) tw = display.measure_text(line, scale=scale)
@@ -2081,12 +2116,12 @@ def _draw_detail_text_line(text, y, pen, font="bitmap8", scale=1):
return cy return cy
def _draw_detail_text_lines(lines, y, pen, font="bitmap8", scale=1): def _draw_detail_text_lines(lines, y, pen, font="bitmap8", scale=TEXT_SCALE):
"""Draw pre-wrapped detail text centered horizontally.""" """Draw pre-wrapped detail text centered horizontally."""
display.set_pen(pen) display.set_pen(pen)
display.set_font(font) display.set_font(font)
line_height = 8 * scale + 4 line_height = _line_height_for_font(font, scale)
cy = y cy = y
for item in lines: for item in lines:
if isinstance(item, tuple): if isinstance(item, tuple):
@@ -2124,7 +2159,7 @@ def _draw_scrobble_button(app, rec, y):
display.set_pen(text_pen) display.set_pen(text_pen)
display.set_font("bitmap8") display.set_font("bitmap8")
_draw_centered_text(label, bx, y, SCROBBLE_BUTTON_W, SCROBBLE_BUTTON_H, 8) _draw_centered_text(label, bx, y, SCROBBLE_BUTTON_W, SCROBBLE_BUTTON_H, TEXT_H)
# ============================================================================ # ============================================================================
@@ -2236,7 +2271,7 @@ def touch_to_record_index(app, y, recs=None, offset=None):
cy = RECORD_START_Y - offset cy = RECORD_START_Y - offset
for i, rec in enumerate(recs): for i, rec in enumerate(recs):
row_h = rec.get("_row_height", THUMB_SIZE + 8) row_h = rec.get("_row_height", THUMB_SIZE + px(8))
if cy <= y < cy + row_h: if cy <= y < cy + row_h:
return i return i
cy += row_h cy += row_h
@@ -2397,7 +2432,7 @@ def handle_month_touch(app, x, y):
return return
# Check left arrow (shifted right to make room for Home) # Check left arrow (shifted right to make room for Home)
lx = HEADER_SIDE_MARGIN + HOME_BTN_W + 4 lx = HEADER_SIDE_MARGIN + HOME_BTN_W + px(4)
ly = HEADER_BUTTON_Y ly = HEADER_BUTTON_Y
if lx <= x <= lx + ARROW_W and ly <= y <= ly + ARROW_H: if lx <= x <= lx + ARROW_W and ly <= y <= ly + ARROW_H:
app.view_year, app.view_month = previous_month(app.view_year, app.view_month) app.view_year, app.view_month = previous_month(app.view_year, app.view_month)
+23 -15
View File
@@ -23,6 +23,14 @@ touch = presto.touch
WIDTH, HEIGHT = display.get_bounds() WIDTH, HEIGHT = display.get_bounds()
UI_SCALE = 2
TEXT_SCALE = UI_SCALE
def px(value):
return value * UI_SCALE
# ============================================================================ # ============================================================================
# LAYOUT AND TIMING # LAYOUT AND TIMING
# ============================================================================ # ============================================================================
@@ -35,16 +43,16 @@ WIFI_CONNECT_TIMEOUT_MS = 10_000
WIFI_CONNECT_POLL_MS = 250 WIFI_CONNECT_POLL_MS = 250
FLASH_MS = 180 FLASH_MS = 180
SCREEN_PADDING = 28 SCREEN_PADDING = px(28)
TITLE_Y = 28 TITLE_Y = px(28)
LABEL_X = SCREEN_PADDING LABEL_X = SCREEN_PADDING
VALUE_X = 150 VALUE_X = px(150)
ROW_1_Y = 90 ROW_1_Y = px(90)
ROW_2_Y = 130 ROW_2_Y = px(130)
ROW_3_Y = 170 ROW_3_Y = px(170)
ROW_4_Y = 210 ROW_4_Y = px(210)
FOOTER_Y = HEIGHT - 50 FOOTER_Y = HEIGHT - px(50)
FLASH_RADIUS = 14 FLASH_RADIUS = px(14)
# ============================================================================ # ============================================================================
@@ -396,7 +404,7 @@ def draw_header():
display.set_pen(_pen_title) display.set_pen(_pen_title)
display.set_font("bitmap14_outline") display.set_font("bitmap14_outline")
text = "Elm-style Presto POC" text = "Elm-style Presto POC"
draw_centered_text(text, TITLE_Y, scale=1) draw_centered_text(text, TITLE_Y, scale=TEXT_SCALE)
def draw_status_rows(model): def draw_status_rows(model):
@@ -408,14 +416,14 @@ def draw_status_rows(model):
def draw_row(label, value, y, value_pen): def draw_row(label, value, y, value_pen):
display.set_pen(_pen_panel) display.set_pen(_pen_panel)
display.rectangle(SCREEN_PADDING, y - 12, WIDTH - 2 * SCREEN_PADDING, 22) display.rectangle(SCREEN_PADDING, y - px(12), WIDTH - 2 * SCREEN_PADDING, px(22))
display.set_font("bitmap8") display.set_font("bitmap8")
display.set_pen(_pen_label) display.set_pen(_pen_label)
display.text(label, LABEL_X, y, scale=1) display.text(label, LABEL_X, y, scale=TEXT_SCALE)
display.set_pen(value_pen) display.set_pen(value_pen)
display.text(value, VALUE_X, y, scale=1) display.text(value, VALUE_X, y, scale=TEXT_SCALE)
def draw_flash(model): def draw_flash(model):
@@ -437,10 +445,10 @@ def draw_flash(model):
def draw_footer(): def draw_footer():
display.set_pen(_pen_dim) display.set_pen(_pen_dim)
display.set_font("bitmap8") display.set_font("bitmap8")
draw_centered_text("Tap anywhere", FOOTER_Y, scale=1) draw_centered_text("Tap anywhere", FOOTER_Y, scale=TEXT_SCALE)
def draw_centered_text(text, y, scale=1): def draw_centered_text(text, y, scale=TEXT_SCALE):
width = display.measure_text(text, scale=scale) width = display.measure_text(text, scale=scale)
display.text(text, (WIDTH - width) // 2, y, scale=scale) display.text(text, (WIDTH - width) // 2, y, scale=scale)