diff --git a/backlog/tasks/ml-183 - Scale-Presto-full-resolution-layout.md b/backlog/tasks/ml-183 - Scale-Presto-full-resolution-layout.md new file mode 100644 index 00000000..5cc30086 --- /dev/null +++ b/backlog/tasks/ml-183 - Scale-Presto-full-resolution-layout.md @@ -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 + + + +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. + + + +## Acceptance Criteria + + + +- [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**. + + +## Implementation Plan + + + +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__`. + + +## Implementation Notes + + + +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. + + + +## Final Summary + + + +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. + + diff --git a/presto/README.md b/presto/README.md index 01f3f720..c1283d8c 100644 --- a/presto/README.md +++ b/presto/README.md @@ -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`. ## License diff --git a/presto/main.py b/presto/main.py index 5650a922..9d7aa850 100644 --- a/presto/main.py +++ b/presto/main.py @@ -59,34 +59,52 @@ display = presto.display WIDTH, HEIGHT = display.get_bounds() 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) -CELL_GAP = 3 +CELL_GAP = px(3) CELLS_PER_ROW = 7 MAX_ROWS = 6 # Header HEADER_Y = 0 -HEADER_H = 31 -HEADER_TEXT_H = 14 -HEADER_SIDE_MARGIN = 8 -HEADER_BUTTON_W = 32 -HEADER_BUTTON_H = 25 +HEADER_H = px(31) +HEADER_TEXT_H = px(14) +HEADER_SIDE_MARGIN = px(8) +HEADER_BUTTON_W = px(32) +HEADER_BUTTON_H = px(25) HEADER_BUTTON_Y = HEADER_Y + (HEADER_H - HEADER_BUTTON_H) // 2 ARROW_W = HEADER_BUTTON_W ARROW_H = HEADER_BUTTON_H # Day-of-week labels -DOW_Y = HEADER_Y + HEADER_H + 2 -DOW_H = 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 +DOW_Y = HEADER_Y + HEADER_H + px(2) +DOW_H = px(16) # 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) BACK_X = HEADER_SIDE_MARGIN @@ -96,28 +114,28 @@ BACK_H = HEADER_BUTTON_H DAY_HEADER_Y = HEADER_Y DAY_HEADER_H = HEADER_H DAY_HEADER_TEXT_H = HEADER_TEXT_H -DAY_COUNT_TEXT_H = 8 +DAY_COUNT_TEXT_H = TEXT_H # Detail view -DETAIL_COVER_SIZE = 150 +DETAIL_COVER_SIZE = px(150) DETAIL_COVER_X = (WIDTH - DETAIL_COVER_SIZE) // 2 -DETAIL_COVER_Y = DAY_HEADER_Y + DAY_HEADER_H + 12 -DETAIL_INFO_GAP = 4 -DETAIL_TEXT_X = 40 +DETAIL_COVER_Y = DAY_HEADER_Y + DAY_HEADER_H + px(12) +DETAIL_INFO_GAP = px(4) +DETAIL_TEXT_X = px(40) DETAIL_TEXT_W = WIDTH - (2 * DETAIL_TEXT_X) # Scrobble button (detail view) -SCROBBLE_BUTTON_W = 200 -SCROBBLE_BUTTON_H = 40 -SCROBBLE_BUTTON_GAP = 12 # vertical gap before button +SCROBBLE_BUTTON_W = px(200) +SCROBBLE_BUTTON_H = px(40) +SCROBBLE_BUTTON_GAP = px(12) # vertical gap before button -RECORD_START_Y = DAY_HEADER_Y + DAY_HEADER_H + 8 -THUMB_SIZE = 40 -THUMB_MARGIN = 8 -ROW_PAD_Y = 8 -ROW_SEPARATOR_H = 1 -TEXT_X = THUMB_MARGIN + THUMB_SIZE + 12 -TEXT_W = WIDTH - TEXT_X - 12 +RECORD_START_Y = DAY_HEADER_Y + DAY_HEADER_H + px(8) +THUMB_SIZE = px(40) +THUMB_MARGIN = px(8) +ROW_PAD_Y = px(8) +ROW_SEPARATOR_H = px(1) +TEXT_X = THUMB_MARGIN + THUMB_SIZE + px(12) +TEXT_W = WIDTH - TEXT_X - px(12) # Colors (RGB tuples, converted to pens at runtime) BG = (24, 24, 27) @@ -152,7 +170,7 @@ MONTH_NAMES = [ # Touch debounce (milliseconds) DEBOUNCE_MS = 300 DRAG_REDRAW_MS = 40 -DRAG_REDRAW_PX = 8 +DRAG_REDRAW_PX = px(8) TOUCH_RELEASE_TIMEOUT_MS = 3_000 # WiFi connection timeout (seconds) @@ -167,24 +185,24 @@ DISPLAY_FADE_STEPS = 20 # Home screen HOME_TITLE = "Music Library" -HOME_TITLE_Y = 60 -HOME_BUTTON_W = WIDTH - 80 -HOME_BUTTON_H = 50 -HOME_BUTTON_X = 40 -HOME_BUTTON1_Y = 120 -HOME_BUTTON2_Y = HOME_BUTTON1_Y + HOME_BUTTON_H + 10 +HOME_TITLE_Y = px(60) +HOME_BUTTON_W = WIDTH - px(80) +HOME_BUTTON_H = px(50) +HOME_BUTTON_X = px(40) +HOME_BUTTON1_Y = px(120) +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 # On-screen keyboard -KB_MARGIN = 20 -KB_KEY_GAP = 3 -KB_KEY_H = 38 +KB_MARGIN = px(20) +KB_KEY_GAP = px(3) +KB_KEY_H = px(36) KB_KEY_W = (WIDTH - 2 * KB_MARGIN - 9 * KB_KEY_GAP) // 10 -KB_INPUT_Y = 42 -KB_INPUT_H = 28 -KB_INPUT_MARGIN = 40 -KB_ROWS_START_Y = KB_INPUT_Y + KB_INPUT_H + 12 +KB_INPUT_Y = px(42) +KB_INPUT_H = px(28) +KB_INPUT_MARGIN = px(40) +KB_ROWS_START_Y = KB_INPUT_Y + KB_INPUT_H + px(12) ALPHA_KEYS = [ list("qwertyuiop"), @@ -339,7 +357,7 @@ def _init_pens(): # 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. 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) - line_height = 8 * scale + 4 # bitmap8 is 8px tall, plus spacing + line_height = _text_line_height(scale) cy = y display.set_pen(pen) for line in lines: @@ -357,9 +375,9 @@ def draw_wrapped(text, x, y, max_width, pen, scale=1): 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.""" - line_height = 8 * scale + 4 + line_height = _text_line_height(scale) cy = y display.set_pen(pen) for line in lines: @@ -369,6 +387,11 @@ def draw_wrapped_lines(lines, x, y, pen, scale=1): 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): """Replace characters not available in the bitmap font.""" replacements = ( @@ -404,12 +427,12 @@ def display_text(text): 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 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.""" words = str(text).split() lines = [] @@ -912,9 +935,9 @@ def _draw_placeholder(x, y, w, h): display.set_pen(_pen_placeholder) display.rectangle(x, y, w, h) display.set_pen(_pen_dim_text) - cx = x + w // 2 - display.measure_text("?", scale=2) // 2 - cy = y + h // 2 - 10 - display.text("?", cx, cy, scale=2) + cx = x + w // 2 - display.measure_text("?", scale=PLACEHOLDER_TEXT_SCALE) // 2 + cy = y + h // 2 - px(10) + display.text("?", cx, cy, scale=PLACEHOLDER_TEXT_SCALE) # ============================================================================ @@ -930,14 +953,14 @@ def draw_status(message): display.set_font("bitmap8") lines = message.split("\n") - line_h = 20 + line_h = px(20) total_h = len(lines) * line_h start_y = (HEIGHT - total_h) // 2 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 - 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() @@ -958,8 +981,8 @@ def draw_month_view(app): presto.update() -HOME_BTN_W = 30 -HOME_BTN_H = 25 +HOME_BTN_W = px(30) +HOME_BTN_H = px(25) 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_font("bitmap14_outline") 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 - 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_area = title_right - title_left tx = title_left + (title_area - tw) // 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) hx = HEADER_SIDE_MARGIN @@ -989,16 +1012,16 @@ def _draw_month_header(app): display.rectangle(hx, hy, HOME_BTN_W, HOME_BTN_H) display.set_pen(_pen_back) 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) - lx = hx + HOME_BTN_W + 4 + lx = hx + HOME_BTN_W + px(4) ly = HEADER_BUTTON_Y display.set_pen(_pen_placeholder) display.rectangle(lx, ly, ARROW_W, ARROW_H) display.set_pen(_pen_back) 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 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.set_pen(_pen_back) 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.""" tw = display.measure_text(text, scale=scale) tx = x + (w - tw) // 2 @@ -1024,8 +1047,8 @@ def _draw_day_labels(): display.set_font("bitmap8") for i, name in enumerate(DAY_NAMES): cx = GRID_LEFT + i * (CELL_SIZE + CELL_GAP) + CELL_SIZE // 2 - tw = display.measure_text(name, scale=1) - display.text(name, cx - tw // 2, DOW_Y, scale=1) + tw = display.measure_text(name, scale=TEXT_SCALE) + display.text(name, cx - tw // 2, DOW_Y, scale=TEXT_SCALE) def _draw_day_grid(app): @@ -1107,8 +1130,8 @@ def _draw_day_grid(app): def _draw_centered_num(num, cx, cy): """Draw a number centered at (cx, cy).""" text = str(num) - tw = display.measure_text(text, scale=1) - display.text(text, cx - tw // 2, cy - 5, scale=1) + tw = display.measure_text(text, scale=TEXT_SCALE) + display.text(text, cx - tw // 2, cy - px(5), scale=TEXT_SCALE) # ============================================================================ @@ -1123,8 +1146,8 @@ def draw_home_screen(app): # Title display.set_pen(_pen_title) display.set_font("bitmap14_outline") - tw = display.measure_text(HOME_TITLE, scale=1) - display.text(HOME_TITLE, (WIDTH - tw) // 2, HOME_TITLE_Y, scale=1) + tw = display.measure_text(HOME_TITLE, scale=TEXT_SCALE) + display.text(HOME_TITLE, (WIDTH - tw) // 2, HOME_TITLE_Y, scale=TEXT_SCALE) # Buttons _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.set_pen(_pen_today_text) 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 - ty = y + (HOME_BUTTON_H - 8) // 2 - display.text(label, tx, ty, scale=1) + ty = y + (HOME_BUTTON_H - TEXT_H) // 2 + display.text(label, tx, ty, scale=TEXT_SCALE) # ============================================================================ @@ -1164,7 +1187,12 @@ def draw_search_input(app): display.set_font("bitmap8") # Show cursor as underscore if query is empty 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) presto.update() @@ -1187,9 +1215,9 @@ def _draw_search_input_header(): display.set_pen(_pen_header_text) display.set_font("bitmap14_outline") title = "Search" - tw = display.measure_text(title, scale=1) + tw = display.measure_text(title, scale=TEXT_SCALE) 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): @@ -1228,10 +1256,10 @@ def _draw_key(x, y, label, w, h, text_pen=None): display.rectangle(x, y, w, h) pen = text_pen if text_pen is not None else _pen_normal_text display.set_pen(pen) - tw = display.measure_text(label, scale=1) + tw = display.measure_text(label, scale=TEXT_SCALE) tx = x + (w - tw) // 2 - ty = y + (h - 8) // 2 - display.text(label, tx, ty, scale=1) + ty = y + (h - TEXT_H) // 2 + display.text(label, tx, ty, scale=TEXT_SCALE) 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_font("bitmap14_outline") display_label = "Search: " + app.search.query - max_w = WIDTH - (BACK_X + BACK_W + 8) - 8 - if display.measure_text(display_label, scale=1) > max_w: + max_w = WIDTH - (BACK_X + BACK_W + px(8)) - px(8) + if display.measure_text(display_label, scale=TEXT_SCALE) > max_w: suffix = "..." 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): display_q = display_q[:-1] 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, - 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): @@ -1348,18 +1376,18 @@ def _draw_search_record_list(app): if app.search.scroll_offset > 0: display.set_pen(_pen_arrow) 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 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 if row_bottom <= RECORD_START_Y: cy = row_bottom continue - if cy >= HEIGHT - 28: + if cy >= HEIGHT - px(28): break _draw_record_row(app, rec, cy) @@ -1369,12 +1397,12 @@ def _draw_search_record_list(app): if app.search.scroll_offset < max_offset: display.set_pen(_pen_arrow) 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): """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) @@ -1383,8 +1411,8 @@ def _draw_search_empty(): display.set_pen(_pen_dim_text) display.set_font("bitmap8") msg = "No records found" - mw = display.measure_text(msg, scale=1) - display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 10, scale=1) + mw = display.measure_text(msg, scale=TEXT_SCALE) + display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - px(10), scale=TEXT_SCALE) def _draw_search_error(): @@ -1392,13 +1420,13 @@ def _draw_search_error(): display.set_pen(_pen_error) display.set_font("bitmap8") msg = "Could not reach server" - mw = display.measure_text(msg, scale=1) - display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 20, scale=1) + mw = display.measure_text(msg, scale=TEXT_SCALE) + display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - px(20), scale=TEXT_SCALE) display.set_pen(_pen_dim_text) hint = "Tap to retry" - hw = display.measure_text(hint, scale=1) - display.text(hint, (WIDTH - hw) // 2, HEIGHT // 2 + 10, scale=1) + hw = display.measure_text(hint, scale=TEXT_SCALE) + 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_font("bitmap14_outline") - tw = display.measure_text(date_str, scale=1) + tw = display.measure_text(date_str, scale=TEXT_SCALE) display.text( date_str, (WIDTH - tw) // 2, DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2, - scale=1 + scale=TEXT_SCALE ) # Record count @@ -1460,12 +1488,12 @@ def _draw_day_header(app): count_str = "{} record{}".format(record_count, "s" if record_count != 1 else "") display.set_pen(_pen_dim_text) display.set_font("bitmap8") - cw = display.measure_text(count_str, scale=1) + cw = display.measure_text(count_str, scale=TEXT_SCALE) display.text( count_str, - WIDTH - cw - 11, + WIDTH - cw - px(11), 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_font("bitmap8") msg = "Could not reach server" - mw = display.measure_text(msg, scale=1) - display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 20, scale=1) + mw = display.measure_text(msg, scale=TEXT_SCALE) + display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - px(20), scale=TEXT_SCALE) display.set_pen(_pen_dim_text) hint = "Tap < Back to return" - hw = display.measure_text(hint, scale=1) - display.text(hint, (WIDTH - hw) // 2, HEIGHT // 2 + 10, scale=1) + hw = display.measure_text(hint, scale=TEXT_SCALE) + display.text(hint, (WIDTH - hw) // 2, HEIGHT // 2 + px(10), scale=TEXT_SCALE) def _draw_day_empty(): @@ -1488,8 +1516,8 @@ def _draw_day_empty(): display.set_pen(_pen_dim_text) display.set_font("bitmap8") msg = "No records on this day" - mw = display.measure_text(msg, scale=1) - display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - 10, scale=1) + mw = display.measure_text(msg, scale=TEXT_SCALE) + display.text(msg, (WIDTH - mw) // 2, HEIGHT // 2 - px(10), scale=TEXT_SCALE) def _draw_record_list(app): @@ -1502,19 +1530,19 @@ def _draw_record_list(app): if app.day.scroll_offset > 0: display.set_pen(_pen_arrow) 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 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 if row_bottom <= RECORD_START_Y: cy = row_bottom continue - if cy >= HEIGHT - 28: + if cy >= HEIGHT - px(28): break _draw_record_row(app, rec, cy) @@ -1524,11 +1552,11 @@ def _draw_record_list(app): if app.day.scroll_offset < max_offset: display.set_pen(_pen_arrow) 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): """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) @@ -1542,7 +1570,7 @@ def prepare_record_list(app, recs): content_height = 0 for rec in recs: _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) gc.collect() @@ -1559,12 +1587,12 @@ def _prepare_record_for_display(rec): rec["_display_title"] = title rec["_display_artists"] = artist_str 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"] = ( - _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"] = ( - _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["_row_height"] = _record_row_height(rec) @@ -1583,22 +1611,22 @@ def _record_row_height(rec): title_lines = rec.get("_display_title_lines", None) if title_lines is None: title = rec.get("_display_title", display_text(rec.get("title", "Unknown Title"))) - title_lines = _wrapped_lines(title, TEXT_W, scale=1) - ty = max(1, len(title_lines)) * 12 + title_lines = _wrapped_lines(title, TEXT_W, scale=TEXT_SCALE) + ty = max(1, len(title_lines)) * TEXT_LINE_H artist_lines = rec.get("_display_artist_lines", None) if artist_lines is None: 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: - ty += 2 + len(artist_lines) * 12 + ty += px(2) + len(artist_lines) * TEXT_LINE_H meta_lines = rec.get("_display_meta_lines", None) if meta_lines is None: 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: - 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 return max(min_h, content_h) @@ -1648,8 +1676,8 @@ def _record_thumbnail_data(app, rec): def _record_thumbnail_url(rec): """Return the preferred cover URL for a record row.""" return ( - rec.get("micro_cover_url", "") - or rec.get("mini_cover_url", "") + rec.get("mini_cover_url", "") + or rec.get("micro_cover_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) if title_lines is None: 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: - 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 artist_lines = rec.get("_display_artist_lines", None) if artist_lines is None: 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: - ty += 2 - ty = draw_wrapped_lines(artist_lines, TEXT_X, ty, _pen_artist, scale=1) + ty += px(2) + ty = draw_wrapped_lines(artist_lines, TEXT_X, ty, _pen_artist, scale=TEXT_SCALE) meta_lines = rec.get("_display_meta_lines", None) if meta_lines is None: 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: - ty += 2 - ty = draw_wrapped_lines(meta_lines, TEXT_X, ty, _pen_dim_text, scale=1) + ty += px(2) + ty = draw_wrapped_lines(meta_lines, TEXT_X, ty, _pen_dim_text, scale=TEXT_SCALE) # Separator line at the computed bottom display.set_pen(_pen_cell_other) @@ -1721,16 +1749,23 @@ def _max_detail_scroll_offset(app): 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.""" if max_w is None: max_w = DETAIL_TEXT_W 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 -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.""" display.set_font(font) metrics = [] @@ -1747,12 +1782,12 @@ def _measure_detail_content(app, rec): title = rec.get("_display_title", display_text(rec.get("title", "Unknown Title"))) 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( - title_lines, font="bitmap14_outline", scale=1 + title_lines, font="bitmap14_outline", scale=TEXT_SCALE ) - h += len(title_lines) * 12 - h += 6 + h += len(title_lines) * TITLE_LINE_H + h += px(6) # Artists (above cover) 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 "" if artist_str: display.set_font("bitmap8") - artist_lines = _wrapped_lines(artist_str, DETAIL_TEXT_W, scale=1) - rec["_detail_artist_lines"] = _line_metrics(artist_lines, scale=1) - h += len(artist_lines) * 12 - h += 4 + artist_lines = _wrapped_lines(artist_str, DETAIL_TEXT_W, scale=TEXT_SCALE) + rec["_detail_artist_lines"] = _line_metrics(artist_lines, scale=TEXT_SCALE) + h += len(artist_lines) * TEXT_LINE_H + h += px(4) else: rec["_detail_artist_lines"] = [] @@ -1779,14 +1814,14 @@ def _measure_detail_content(app, rec): if genres: genre_str = display_text(", ".join(genres)) display.set_font("bitmap8") - genre_lines = _wrapped_lines(genre_str, DETAIL_TEXT_W, scale=1) - rec["_detail_genre_lines"] = _line_metrics(genre_lines, scale=1) - h += len(genre_lines) * 12 - h += 4 + genre_lines = _wrapped_lines(genre_str, DETAIL_TEXT_W, scale=TEXT_SCALE) + rec["_detail_genre_lines"] = _line_metrics(genre_lines, scale=TEXT_SCALE) + h += len(genre_lines) * TEXT_LINE_H + h += px(4) else: rec["_detail_genre_lines"] = [] - h += 6 + h += px(6) # Metadata line: record_type | format | year meta_parts = [] @@ -1803,10 +1838,10 @@ def _measure_detail_content(app, rec): if meta_parts: meta_str = " | ".join(meta_parts) display.set_font("bitmap8") - meta_lines = _wrapped_lines(meta_str, DETAIL_TEXT_W, scale=1) - rec["_detail_meta_lines"] = _line_metrics(meta_lines, scale=1) - h += len(meta_lines) * 12 - h += 4 + meta_lines = _wrapped_lines(meta_str, DETAIL_TEXT_W, scale=TEXT_SCALE) + rec["_detail_meta_lines"] = _line_metrics(meta_lines, scale=TEXT_SCALE) + h += len(meta_lines) * TEXT_LINE_H + h += px(4) else: rec["_detail_meta_lines"] = [] @@ -1815,9 +1850,9 @@ def _measure_detail_content(app, rec): if purchased_at: purch_str = "Purchased: " + display_text(str(purchased_at)[:10]) display.set_font("bitmap8") - purchased_lines = _wrapped_lines(purch_str, DETAIL_TEXT_W, scale=1) - rec["_detail_purchased_lines"] = _line_metrics(purchased_lines, scale=1) - h += len(purchased_lines) * 12 + purchased_lines = _wrapped_lines(purch_str, DETAIL_TEXT_W, scale=TEXT_SCALE) + rec["_detail_purchased_lines"] = _line_metrics(purchased_lines, scale=TEXT_SCALE) + h += len(purchased_lines) * TEXT_LINE_H else: rec["_detail_purchased_lines"] = [] @@ -1906,10 +1941,10 @@ def draw_record_detail(app): display.set_font("bitmap14_outline") if offset > 0: 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: 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() @@ -1933,12 +1968,12 @@ def _draw_detail_header(): display.set_pen(_pen_header_text) display.set_font("bitmap14_outline") label = "Record" - tw = display.measure_text(label, scale=1) + tw = display.measure_text(label, scale=TEXT_SCALE) display.text( label, (WIDTH - tw) // 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", display_text(rec.get("title", "Unknown Title"))) y = _draw_detail_text_line(title, y, _pen_title, - font="bitmap14_outline", scale=1) + font="bitmap14_outline", scale=TEXT_SCALE) else: y = _draw_detail_text_lines(title_lines, y, _pen_title, - font="bitmap14_outline", scale=1) - y += 6 + font="bitmap14_outline", scale=TEXT_SCALE) + y += px(6) # Artists artist_lines = rec.get("_detail_artist_lines", None) @@ -1963,10 +1998,10 @@ def _draw_detail_title_artists(rec, y): if not artist_str: artists = rec.get("artists", []) 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: y = _draw_detail_text_lines(artist_lines, y, _pen_artist) - y += 4 + y += px(4) return y @@ -2020,14 +2055,14 @@ def _draw_detail_info_below_cover(rec, y): genres = rec.get("genres", []) if 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: genre_lines = [] if genre_lines: 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 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", "") if release_date: 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: y = _draw_detail_text_lines(meta_lines, y, _pen_dim_text) - y += 4 + y += px(4) # Purchased at 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", "") if purchased_at: 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: purchased_lines = [] if purchased_lines: @@ -2063,7 +2098,7 @@ def _draw_detail_info_below_cover(rec, 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. Returns the y-position after drawing (including wrapped lines).""" 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) lines = _wrapped_lines(text, max_w, scale=scale) - line_height = 8 * scale + 4 + line_height = _line_height_for_font(font, scale) cy = y for line in lines: 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 -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.""" display.set_pen(pen) display.set_font(font) - line_height = 8 * scale + 4 + line_height = _line_height_for_font(font, scale) cy = y for item in lines: if isinstance(item, tuple): @@ -2124,7 +2159,7 @@ def _draw_scrobble_button(app, rec, y): display.set_pen(text_pen) 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 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: return i cy += row_h @@ -2397,7 +2432,7 @@ def handle_month_touch(app, x, y): return # 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 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) diff --git a/presto/poc.py b/presto/poc.py index 6e951eb4..5a93853e 100644 --- a/presto/poc.py +++ b/presto/poc.py @@ -23,6 +23,14 @@ touch = presto.touch WIDTH, HEIGHT = display.get_bounds() +UI_SCALE = 2 +TEXT_SCALE = UI_SCALE + + +def px(value): + return value * UI_SCALE + + # ============================================================================ # LAYOUT AND TIMING # ============================================================================ @@ -35,16 +43,16 @@ WIFI_CONNECT_TIMEOUT_MS = 10_000 WIFI_CONNECT_POLL_MS = 250 FLASH_MS = 180 -SCREEN_PADDING = 28 -TITLE_Y = 28 +SCREEN_PADDING = px(28) +TITLE_Y = px(28) LABEL_X = SCREEN_PADDING -VALUE_X = 150 -ROW_1_Y = 90 -ROW_2_Y = 130 -ROW_3_Y = 170 -ROW_4_Y = 210 -FOOTER_Y = HEIGHT - 50 -FLASH_RADIUS = 14 +VALUE_X = px(150) +ROW_1_Y = px(90) +ROW_2_Y = px(130) +ROW_3_Y = px(170) +ROW_4_Y = px(210) +FOOTER_Y = HEIGHT - px(50) +FLASH_RADIUS = px(14) # ============================================================================ @@ -396,7 +404,7 @@ def draw_header(): display.set_pen(_pen_title) display.set_font("bitmap14_outline") 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): @@ -408,14 +416,14 @@ def draw_status_rows(model): def draw_row(label, value, y, value_pen): 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_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.text(value, VALUE_X, y, scale=1) + display.text(value, VALUE_X, y, scale=TEXT_SCALE) def draw_flash(model): @@ -437,10 +445,10 @@ def draw_flash(model): def draw_footer(): display.set_pen(_pen_dim) 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) display.text(text, (WIDTH - width) // 2, y, scale=scale)