ML-186: improve presto redraw performance with partial updates
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
id: ML-186
|
||||||
|
title: Improve Presto display redraw performance
|
||||||
|
status: Done
|
||||||
|
assignee:
|
||||||
|
- Codex
|
||||||
|
created_date: "2026-05-16 20:14"
|
||||||
|
updated_date: "2026-05-16 20:22"
|
||||||
|
labels:
|
||||||
|
- presto
|
||||||
|
- performance
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- "https://github.com/pimoroni/presto/issues/56"
|
||||||
|
documentation:
|
||||||
|
- "https://github.com/pimoroni/presto/blob/main/docs/presto.md"
|
||||||
|
modified_files:
|
||||||
|
- presto/main.py
|
||||||
|
- presto/tests/test_screens.py
|
||||||
|
priority: medium
|
||||||
|
ordinal: 22000
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
|
||||||
|
Use Pimoroni Presto partial display updates for bounded redraws in the MicroPython app where doing so preserves existing behavior. The current app performs full-screen updates for all draws; the desired outcome is to reduce display transfer work for high-frequency or small-area updates without changing visible UI behavior or making physical-device claims without on-device verification.
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
|
||||||
|
- [x] #1 Bounded UI redraws use partial display updates when the Presto firmware supports them and safely fall back to full updates otherwise.
|
||||||
|
- [x] #2 Scrollable fixed-header views continue to render correctly during drag and release redraws.
|
||||||
|
- [x] #3 Search input and scrobble feedback preserve existing visible behavior while avoiding unnecessary full-screen display transfers where practical.
|
||||||
|
- [x] #4 Headless smoke tests pass, and any unverified physical-device behavior is clearly not claimed.
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
<!-- SECTION:PLAN:BEGIN -->
|
||||||
|
|
||||||
|
1. Add safe display update helpers in `presto/main.py`: a full update wrapper, a partial update wrapper with fallback, and named rectangles for the scroll viewport/search input/scrobble button.
|
||||||
|
2. Keep full-screen updates for full state transitions and broad redraws, but use partial updates for bounded areas where the buffer content can be refreshed independently.
|
||||||
|
3. For fixed-header scrollable views, add a bounded redraw mode that clears/redraws only the viewport below the header and calls `partial_update` for that region during drag redraws.
|
||||||
|
4. For small feedback updates, redraw only the search input field after query text changes and redraw only the scrobble button area when its state changes.
|
||||||
|
5. Add focused tests around fallback/partial-update behavior if the emulator can observe it, then run the Presto smoke test suite.
|
||||||
|
<!-- SECTION:PLAN:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
|
||||||
|
Implemented safe partial display update helpers with full-update fallback, viewport-only redraws for scrollable fixed-header views, query-field partial redraws, and scrobble-button partial redraws. After user correction, kept the detail cover layout at `DETAIL_COVER_SIZE = px(200)` and updated tests/docs that incorrectly described the cover as 460px.
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
|
||||||
|
Implemented bounded display update support for the Presto MicroPython app. Full-screen transitions still use full updates, while scroll redraws now refresh only the content viewport, search typing refreshes only the query field row, and scrobble loading/done feedback refreshes only the visible button region. Added tests for partial-update dispatch, fallback behavior, scroll viewport updates, search field updates, and scrobble button updates. Kept the detail cover layout at `DETAIL_COVER_SIZE = px(200)` and updated stale tests/docs that incorrectly described the detail cover as 460px.
|
||||||
|
|
||||||
|
Verification: `python3 -c "import py_compile; py_compile.compile('main.py', cfile='/tmp/main.pyc', doraise=True)"`; `python3 -m py_compile tests/test_screens.py`; `mise run test` (14 passed). Physical Presto behavior was not verified on-device.
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
+159
-49
@@ -115,6 +115,10 @@ 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 = TEXT_H
|
DAY_COUNT_TEXT_H = TEXT_H
|
||||||
|
SCROLL_VIEWPORT_X = 0
|
||||||
|
SCROLL_VIEWPORT_Y = DAY_HEADER_Y + DAY_HEADER_H
|
||||||
|
SCROLL_VIEWPORT_W = WIDTH
|
||||||
|
SCROLL_VIEWPORT_H = HEIGHT - SCROLL_VIEWPORT_Y
|
||||||
|
|
||||||
# Detail view
|
# Detail view
|
||||||
DETAIL_COVER_SIZE = px(200)
|
DETAIL_COVER_SIZE = px(200)
|
||||||
@@ -202,6 +206,12 @@ KB_KEY_W = (WIDTH - 2 * KB_MARGIN - 9 * KB_KEY_GAP) // 10
|
|||||||
KB_INPUT_Y = px(42)
|
KB_INPUT_Y = px(42)
|
||||||
KB_INPUT_H = px(28)
|
KB_INPUT_H = px(28)
|
||||||
KB_INPUT_MARGIN = px(40)
|
KB_INPUT_MARGIN = px(40)
|
||||||
|
KB_INPUT_X = KB_INPUT_MARGIN
|
||||||
|
KB_INPUT_W = WIDTH - (2 * KB_INPUT_MARGIN)
|
||||||
|
KB_INPUT_UPDATE_X = 0
|
||||||
|
KB_INPUT_UPDATE_Y = KB_INPUT_Y
|
||||||
|
KB_INPUT_UPDATE_W = WIDTH
|
||||||
|
KB_INPUT_UPDATE_H = KB_INPUT_H
|
||||||
KB_ROWS_START_Y = KB_INPUT_Y + KB_INPUT_H + px(12)
|
KB_ROWS_START_Y = KB_INPUT_Y + KB_INPUT_H + px(12)
|
||||||
|
|
||||||
ALPHA_KEYS = [
|
ALPHA_KEYS = [
|
||||||
@@ -353,6 +363,44 @@ def _init_pens():
|
|||||||
_pen_scrobble_done_text = display.create_pen(*SCROBBLE_DONE_TEXT)
|
_pen_scrobble_done_text = display.create_pen(*SCROBBLE_DONE_TEXT)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# HELPER: DISPLAY UPDATES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def _display_update():
|
||||||
|
"""Push the full display buffer to the screen."""
|
||||||
|
presto.update()
|
||||||
|
|
||||||
|
|
||||||
|
def _partial_display_update(x, y, w, h):
|
||||||
|
"""Push a bounded display region, falling back to a full update."""
|
||||||
|
if w <= 0 or h <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
presto.partial_update(x, y, w, h)
|
||||||
|
except Exception:
|
||||||
|
_display_update()
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_region(x, y, w, h):
|
||||||
|
"""Clear a bounded display region to the app background."""
|
||||||
|
display.set_pen(_pen_bg)
|
||||||
|
display.rectangle(x, y, w, h)
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_scroll_viewport():
|
||||||
|
"""Clear the scrollable content area below the fixed header."""
|
||||||
|
_clear_region(SCROLL_VIEWPORT_X, SCROLL_VIEWPORT_Y,
|
||||||
|
SCROLL_VIEWPORT_W, SCROLL_VIEWPORT_H)
|
||||||
|
|
||||||
|
|
||||||
|
def _partial_scroll_viewport_update():
|
||||||
|
"""Push the scrollable content area below the fixed header."""
|
||||||
|
_partial_display_update(SCROLL_VIEWPORT_X, SCROLL_VIEWPORT_Y,
|
||||||
|
SCROLL_VIEWPORT_W, SCROLL_VIEWPORT_H)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# HELPER: TEXT WRAPPING
|
# HELPER: TEXT WRAPPING
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -927,7 +975,7 @@ def draw_status(message):
|
|||||||
x = (WIDTH - w) // 2
|
x = (WIDTH - w) // 2
|
||||||
display.text(line, x, start_y + i * line_h, scale=TEXT_SCALE)
|
display.text(line, x, start_y + i * line_h, scale=TEXT_SCALE)
|
||||||
|
|
||||||
presto.update()
|
_display_update()
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -943,7 +991,7 @@ def draw_month_view(app):
|
|||||||
_draw_month_header(app)
|
_draw_month_header(app)
|
||||||
_draw_day_labels()
|
_draw_day_labels()
|
||||||
_draw_day_grid(app)
|
_draw_day_grid(app)
|
||||||
presto.update()
|
_display_update()
|
||||||
|
|
||||||
|
|
||||||
HOME_BTN_W = px(30)
|
HOME_BTN_W = px(30)
|
||||||
@@ -1118,7 +1166,7 @@ def draw_home_screen(app):
|
|||||||
_draw_home_button(HOME_BUTTON1_Y, "Search Collection")
|
_draw_home_button(HOME_BUTTON1_Y, "Search Collection")
|
||||||
_draw_home_button(HOME_BUTTON2_Y, "Today's Records")
|
_draw_home_button(HOME_BUTTON2_Y, "Today's Records")
|
||||||
|
|
||||||
presto.update()
|
_display_update()
|
||||||
|
|
||||||
|
|
||||||
def _draw_home_button(y, label):
|
def _draw_home_button(y, label):
|
||||||
@@ -1143,24 +1191,35 @@ def draw_search_input(app):
|
|||||||
display.clear()
|
display.clear()
|
||||||
|
|
||||||
_draw_search_input_header()
|
_draw_search_input_header()
|
||||||
|
_draw_search_query_field(app)
|
||||||
|
_draw_keyboard(app)
|
||||||
|
_display_update()
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_search_query_field(app):
|
||||||
|
"""Draw the search query field without touching the keyboard."""
|
||||||
|
_clear_region(KB_INPUT_UPDATE_X, KB_INPUT_UPDATE_Y,
|
||||||
|
KB_INPUT_UPDATE_W, KB_INPUT_UPDATE_H)
|
||||||
|
|
||||||
# Query text field
|
|
||||||
display.set_pen(_pen_cell_bg)
|
display.set_pen(_pen_cell_bg)
|
||||||
display.rectangle(KB_INPUT_MARGIN, KB_INPUT_Y,
|
display.rectangle(KB_INPUT_X, KB_INPUT_Y, KB_INPUT_W, KB_INPUT_H)
|
||||||
WIDTH - 2 * KB_INPUT_MARGIN, KB_INPUT_H)
|
|
||||||
display.set_pen(_pen_header_text)
|
display.set_pen(_pen_header_text)
|
||||||
display.set_font("bitmap8")
|
display.set_font("bitmap8")
|
||||||
# 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.text(
|
||||||
display_q,
|
display_q,
|
||||||
KB_INPUT_MARGIN + px(8),
|
KB_INPUT_X + px(8),
|
||||||
KB_INPUT_Y + px(12),
|
KB_INPUT_Y + px(12),
|
||||||
scale=TEXT_SCALE
|
scale=TEXT_SCALE
|
||||||
)
|
)
|
||||||
|
|
||||||
_draw_keyboard(app)
|
|
||||||
presto.update()
|
def _redraw_search_query_field(app):
|
||||||
|
"""Redraw and push only the search query field row."""
|
||||||
|
_draw_search_query_field(app)
|
||||||
|
_partial_display_update(KB_INPUT_UPDATE_X, KB_INPUT_UPDATE_Y,
|
||||||
|
KB_INPUT_UPDATE_W, KB_INPUT_UPDATE_H)
|
||||||
|
|
||||||
|
|
||||||
def _draw_search_input_header():
|
def _draw_search_input_header():
|
||||||
@@ -1272,7 +1331,7 @@ def _flash_key(kx, ky, kw, kh, label):
|
|||||||
_draw_key(kx, ky, label, kw, kh, text_pen=_pen_today_text)
|
_draw_key(kx, ky, label, kw, kh, text_pen=_pen_today_text)
|
||||||
display.set_pen(_pen_arrow)
|
display.set_pen(_pen_arrow)
|
||||||
display.rectangle(kx, ky, kw, kh)
|
display.rectangle(kx, ky, kw, kh)
|
||||||
presto.update()
|
_partial_display_update(kx, ky, kw, kh)
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
|
||||||
@@ -1280,27 +1339,39 @@ def _flash_key(kx, ky, kw, kh, label):
|
|||||||
# DRAWING: SEARCH RESULTS
|
# DRAWING: SEARCH RESULTS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def draw_search_results(app):
|
def draw_search_results(app, partial=False):
|
||||||
"""Render the search results view, reusing record-list rendering."""
|
"""Render the search results view, reusing record-list rendering."""
|
||||||
display.set_pen(_pen_bg)
|
if partial:
|
||||||
display.clear()
|
_clear_scroll_viewport()
|
||||||
|
else:
|
||||||
|
display.set_pen(_pen_bg)
|
||||||
|
display.clear()
|
||||||
|
|
||||||
_draw_search_results_header(app)
|
_draw_search_results_header(app)
|
||||||
|
|
||||||
if app.search.error:
|
if app.search.error:
|
||||||
_draw_search_error()
|
_draw_search_error()
|
||||||
presto.update()
|
if partial:
|
||||||
|
_partial_scroll_viewport_update()
|
||||||
|
else:
|
||||||
|
_display_update()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not app.search.results:
|
if not app.search.results:
|
||||||
_draw_search_empty()
|
_draw_search_empty()
|
||||||
presto.update()
|
if partial:
|
||||||
|
_partial_scroll_viewport_update()
|
||||||
|
else:
|
||||||
|
_display_update()
|
||||||
return
|
return
|
||||||
|
|
||||||
_set_clip_below_header()
|
_set_clip_below_header()
|
||||||
_draw_search_record_list(app)
|
_draw_search_record_list(app)
|
||||||
_remove_clip()
|
_remove_clip()
|
||||||
presto.update()
|
if partial:
|
||||||
|
_partial_scroll_viewport_update()
|
||||||
|
else:
|
||||||
|
_display_update()
|
||||||
|
|
||||||
|
|
||||||
def _draw_search_results_header(app):
|
def _draw_search_results_header(app):
|
||||||
@@ -1402,27 +1473,39 @@ def _draw_search_error():
|
|||||||
# DRAWING: DAY VIEW (records list)
|
# DRAWING: DAY VIEW (records list)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def draw_day_view(app):
|
def draw_day_view(app, partial=False):
|
||||||
"""Render the day view showing records for the selected date."""
|
"""Render the day view showing records for the selected date."""
|
||||||
display.set_pen(_pen_bg)
|
if partial:
|
||||||
display.clear()
|
_clear_scroll_viewport()
|
||||||
|
else:
|
||||||
|
display.set_pen(_pen_bg)
|
||||||
|
display.clear()
|
||||||
|
|
||||||
_draw_day_header(app)
|
_draw_day_header(app)
|
||||||
|
|
||||||
if app.day.error:
|
if app.day.error:
|
||||||
_draw_day_error()
|
_draw_day_error()
|
||||||
presto.update()
|
if partial:
|
||||||
|
_partial_scroll_viewport_update()
|
||||||
|
else:
|
||||||
|
_display_update()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not app.day.records:
|
if not app.day.records:
|
||||||
_draw_day_empty()
|
_draw_day_empty()
|
||||||
presto.update()
|
if partial:
|
||||||
|
_partial_scroll_viewport_update()
|
||||||
|
else:
|
||||||
|
_display_update()
|
||||||
return
|
return
|
||||||
|
|
||||||
_set_clip_below_header()
|
_set_clip_below_header()
|
||||||
_draw_record_list(app)
|
_draw_record_list(app)
|
||||||
_remove_clip()
|
_remove_clip()
|
||||||
presto.update()
|
if partial:
|
||||||
|
_partial_scroll_viewport_update()
|
||||||
|
else:
|
||||||
|
_display_update()
|
||||||
|
|
||||||
|
|
||||||
def _draw_day_header(app):
|
def _draw_day_header(app):
|
||||||
@@ -1862,11 +1945,14 @@ def _remove_clip():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def draw_record_detail(app):
|
def draw_record_detail(app, partial=False):
|
||||||
"""Render the individual record detail view. Layout order:
|
"""Render the individual record detail view. Layout order:
|
||||||
title, artists, large cover, genres, metadata, purchased at."""
|
title, artists, large cover, genres, metadata, purchased at."""
|
||||||
display.set_pen(_pen_bg)
|
if partial:
|
||||||
display.clear()
|
_clear_scroll_viewport()
|
||||||
|
else:
|
||||||
|
display.set_pen(_pen_bg)
|
||||||
|
display.clear()
|
||||||
|
|
||||||
if app.detail.source_screen == STATE_SEARCH_RESULTS:
|
if app.detail.source_screen == STATE_SEARCH_RESULTS:
|
||||||
rec = app.search.results[app.detail.selected_index]
|
rec = app.search.results[app.detail.selected_index]
|
||||||
@@ -1886,7 +1972,8 @@ def draw_record_detail(app):
|
|||||||
|
|
||||||
offset = app.detail.scroll_offset
|
offset = app.detail.scroll_offset
|
||||||
|
|
||||||
_draw_detail_header()
|
if not partial:
|
||||||
|
_draw_detail_header()
|
||||||
|
|
||||||
# Clip scrolling content below the header
|
# Clip scrolling content below the header
|
||||||
_set_clip_below_header()
|
_set_clip_below_header()
|
||||||
@@ -1907,12 +1994,7 @@ def draw_record_detail(app):
|
|||||||
|
|
||||||
# Scrobble button (at bottom of content, only if eligible)
|
# Scrobble button (at bottom of content, only if eligible)
|
||||||
if rec.get("selected_release_id"):
|
if rec.get("selected_release_id"):
|
||||||
# Compute button Y from measured content height (y doesn't track
|
button_y = _detail_scrobble_button_y(app)
|
||||||
# the bottom because _draw_detail_info_below_cover's return is
|
|
||||||
# discarded — use detail content height which already includes
|
|
||||||
# the button height and gap from Step 4).
|
|
||||||
button_y = (DETAIL_COVER_Y - offset
|
|
||||||
+ app.detail.content_height - SCROBBLE_BUTTON_H)
|
|
||||||
# Only draw if the button is at least partially visible
|
# Only draw if the button is at least partially visible
|
||||||
if (button_y + SCROBBLE_BUTTON_H > DAY_HEADER_Y + DAY_HEADER_H
|
if (button_y + SCROBBLE_BUTTON_H > DAY_HEADER_Y + DAY_HEADER_H
|
||||||
and button_y < HEIGHT):
|
and button_y < HEIGHT):
|
||||||
@@ -1931,7 +2013,16 @@ def draw_record_detail(app):
|
|||||||
|
|
||||||
_remove_clip()
|
_remove_clip()
|
||||||
|
|
||||||
presto.update()
|
if partial:
|
||||||
|
_partial_scroll_viewport_update()
|
||||||
|
else:
|
||||||
|
_display_update()
|
||||||
|
|
||||||
|
|
||||||
|
def _detail_scrobble_button_y(app):
|
||||||
|
"""Return the scrobble button Y position for the current detail offset."""
|
||||||
|
return (DETAIL_COVER_Y - app.detail.scroll_offset
|
||||||
|
+ app.detail.content_height - SCROBBLE_BUTTON_H)
|
||||||
|
|
||||||
|
|
||||||
def _draw_detail_header():
|
def _draw_detail_header():
|
||||||
@@ -2141,6 +2232,28 @@ def _draw_scrobble_button(app, rec, y):
|
|||||||
_draw_centered_text(label, bx, y, SCROBBLE_BUTTON_W, SCROBBLE_BUTTON_H, TEXT_H)
|
_draw_centered_text(label, bx, y, SCROBBLE_BUTTON_W, SCROBBLE_BUTTON_H, TEXT_H)
|
||||||
|
|
||||||
|
|
||||||
|
def _redraw_scrobble_button(app, rec):
|
||||||
|
"""Redraw and push only the visible scrobble button region."""
|
||||||
|
if not rec.get("selected_release_id"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if app.detail.content_height == 0:
|
||||||
|
_measure_detail_content(app, rec)
|
||||||
|
|
||||||
|
button_y = _detail_scrobble_button_y(app)
|
||||||
|
update_y = max(button_y, SCROLL_VIEWPORT_Y)
|
||||||
|
update_bottom = min(button_y + SCROBBLE_BUTTON_H, HEIGHT)
|
||||||
|
if update_bottom <= update_y:
|
||||||
|
return
|
||||||
|
|
||||||
|
bx = (WIDTH - SCROBBLE_BUTTON_W) // 2
|
||||||
|
_set_clip_below_header()
|
||||||
|
_draw_scrobble_button(app, rec, button_y)
|
||||||
|
_remove_clip()
|
||||||
|
_partial_display_update(bx, update_y, SCROBBLE_BUTTON_W,
|
||||||
|
update_bottom - update_y)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TOUCH HANDLING
|
# TOUCH HANDLING
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -2268,11 +2381,8 @@ def _scrobble_button_hit_test(app, x, y, rec):
|
|||||||
if y <= DAY_HEADER_Y + DAY_HEADER_H:
|
if y <= DAY_HEADER_Y + DAY_HEADER_H:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Compute button Y position — must match the formula used in
|
# Detail content height already includes the button gap and height.
|
||||||
# draw_record_detail(). Detail content height already
|
button_y = _detail_scrobble_button_y(app)
|
||||||
# includes SCROBBLE_BUTTON_GAP + SCROBBLE_BUTTON_H from _measure_detail_content.
|
|
||||||
button_y = (DETAIL_COVER_Y - app.detail.scroll_offset
|
|
||||||
+ app.detail.content_height - SCROBBLE_BUTTON_H)
|
|
||||||
bx = (WIDTH - SCROBBLE_BUTTON_W) // 2
|
bx = (WIDTH - SCROBBLE_BUTTON_W) // 2
|
||||||
|
|
||||||
return (bx <= x <= bx + SCROBBLE_BUTTON_W and
|
return (bx <= x <= bx + SCROBBLE_BUTTON_W and
|
||||||
@@ -2290,7 +2400,7 @@ def handle_scrobble_touch(app, rec):
|
|||||||
|
|
||||||
# Set loading state and redraw immediately so the user sees "..."
|
# Set loading state and redraw immediately so the user sees "..."
|
||||||
app.detail.scrobble_state = "loading"
|
app.detail.scrobble_state = "loading"
|
||||||
draw_record_detail(app)
|
_redraw_scrobble_button(app, rec)
|
||||||
|
|
||||||
# Make the POST request. urequests has limited timeout support on
|
# Make the POST request. urequests has limited timeout support on
|
||||||
# MicroPython; if WiFi drops mid-request, the default timeout may
|
# MicroPython; if WiFi drops mid-request, the default timeout may
|
||||||
@@ -2307,7 +2417,7 @@ def handle_scrobble_touch(app, rec):
|
|||||||
app.detail.scrobble_state = "idle"
|
app.detail.scrobble_state = "idle"
|
||||||
|
|
||||||
gc.collect()
|
gc.collect()
|
||||||
draw_record_detail(app)
|
_redraw_scrobble_button(app, rec)
|
||||||
|
|
||||||
|
|
||||||
def handle_home_touch(app, x, y):
|
def handle_home_touch(app, x, y):
|
||||||
@@ -2348,12 +2458,12 @@ def handle_search_input_touch(app, x, y):
|
|||||||
if action == "space":
|
if action == "space":
|
||||||
if len(app.search.query) < 50:
|
if len(app.search.query) < 50:
|
||||||
app.search.query += " "
|
app.search.query += " "
|
||||||
draw_search_input(app)
|
_redraw_search_query_field(app)
|
||||||
return
|
return
|
||||||
|
|
||||||
if action == "backspace":
|
if action == "backspace":
|
||||||
app.search.query = app.search.query[:-1]
|
app.search.query = app.search.query[:-1]
|
||||||
draw_search_input(app)
|
_redraw_search_query_field(app)
|
||||||
return
|
return
|
||||||
|
|
||||||
if action == "ok":
|
if action == "ok":
|
||||||
@@ -2370,7 +2480,7 @@ def handle_search_input_touch(app, x, y):
|
|||||||
# Character key
|
# Character key
|
||||||
if len(app.search.query) < 50 and len(action) == 1:
|
if len(app.search.query) < 50 and len(action) == 1:
|
||||||
app.search.query += action
|
app.search.query += action
|
||||||
draw_search_input(app)
|
_redraw_search_query_field(app)
|
||||||
|
|
||||||
|
|
||||||
def handle_search_results_touch(app, x, y):
|
def handle_search_results_touch(app, x, y):
|
||||||
@@ -2618,7 +2728,7 @@ def main():
|
|||||||
lambda: app.search.scroll_offset,
|
lambda: app.search.scroll_offset,
|
||||||
lambda value: setattr(app.search, "scroll_offset", value),
|
lambda value: setattr(app.search, "scroll_offset", value),
|
||||||
lambda: _search_max_scroll_offset(app),
|
lambda: _search_max_scroll_offset(app),
|
||||||
lambda: draw_search_results(app),
|
lambda: draw_search_results(app, partial=True),
|
||||||
True
|
True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2644,7 +2754,7 @@ def main():
|
|||||||
lambda: app.day.scroll_offset,
|
lambda: app.day.scroll_offset,
|
||||||
lambda value: setattr(app.day, "scroll_offset", value),
|
lambda value: setattr(app.day, "scroll_offset", value),
|
||||||
lambda: _max_scroll_offset(app),
|
lambda: _max_scroll_offset(app),
|
||||||
lambda: draw_day_view(app),
|
lambda: draw_day_view(app, partial=True),
|
||||||
True
|
True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2672,7 +2782,7 @@ def main():
|
|||||||
lambda: app.detail.scroll_offset,
|
lambda: app.detail.scroll_offset,
|
||||||
lambda value: setattr(app.detail, "scroll_offset", value),
|
lambda value: setattr(app.detail, "scroll_offset", value),
|
||||||
lambda: _max_detail_scroll_offset(app),
|
lambda: _max_detail_scroll_offset(app),
|
||||||
lambda: draw_record_detail(app),
|
lambda: draw_record_detail(app, partial=True),
|
||||||
True
|
True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,136 @@ class TestSmokeScreens:
|
|||||||
assert main_module._record_thumbnail_url(rec).endswith("/small-0.jpg")
|
assert main_module._record_thumbnail_url(rec).endswith("/small-0.jpg")
|
||||||
assert main_module._record_detail_cover_url(rec).endswith("/medium-0.jpg")
|
assert main_module._record_detail_cover_url(rec).endswith("/medium-0.jpg")
|
||||||
|
|
||||||
|
def test_partial_update_uses_region_when_available(self, main_module, monkeypatch):
|
||||||
|
"""Bounded display updates call the Presto partial update API."""
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_module.presto,
|
||||||
|
"partial_update",
|
||||||
|
lambda *args: calls.append(("partial", args))
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_module.presto,
|
||||||
|
"update",
|
||||||
|
lambda: calls.append(("full", ()))
|
||||||
|
)
|
||||||
|
|
||||||
|
main_module._partial_display_update(1, 2, 3, 4)
|
||||||
|
|
||||||
|
assert calls == [("partial", (1, 2, 3, 4))]
|
||||||
|
|
||||||
|
def test_partial_update_falls_back_to_full_update(self, main_module, monkeypatch):
|
||||||
|
"""Bounded display updates fall back when partial update fails."""
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def _raise_partial(*_args):
|
||||||
|
raise RuntimeError("partial update unavailable")
|
||||||
|
|
||||||
|
monkeypatch.setattr(main_module.presto, "partial_update", _raise_partial)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_module.presto,
|
||||||
|
"update",
|
||||||
|
lambda: calls.append(("full", ()))
|
||||||
|
)
|
||||||
|
|
||||||
|
main_module._partial_display_update(1, 2, 3, 4)
|
||||||
|
|
||||||
|
assert calls == [("full", ())]
|
||||||
|
|
||||||
|
def test_day_view_partial_redraw_updates_scroll_viewport(
|
||||||
|
self, main_module, monkeypatch
|
||||||
|
):
|
||||||
|
"""Day-list bounded redraws update only the scroll viewport."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
app.screen = main_module.STATE_DAY
|
||||||
|
app.view_year = 2026
|
||||||
|
app.view_month = 5
|
||||||
|
app.day.selected_day = 15
|
||||||
|
|
||||||
|
recs = [make_mock_record(i) for i in range(3)]
|
||||||
|
app.day.records = recs
|
||||||
|
app.day.content_height = main_module.prepare_record_list(app, recs)
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_module.presto,
|
||||||
|
"partial_update",
|
||||||
|
lambda *args: calls.append(args)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(main_module.presto, "update", lambda: calls.append("full"))
|
||||||
|
|
||||||
|
main_module.draw_day_view(app, partial=True)
|
||||||
|
|
||||||
|
assert calls == [(
|
||||||
|
main_module.SCROLL_VIEWPORT_X,
|
||||||
|
main_module.SCROLL_VIEWPORT_Y,
|
||||||
|
main_module.SCROLL_VIEWPORT_W,
|
||||||
|
main_module.SCROLL_VIEWPORT_H,
|
||||||
|
)]
|
||||||
|
|
||||||
|
def test_search_query_field_partial_redraw(self, main_module, monkeypatch):
|
||||||
|
"""Typing in search redraws only the query field row."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
app.screen = main_module.STATE_SEARCH_INPUT
|
||||||
|
app.search.query = "blue"
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_module.presto,
|
||||||
|
"partial_update",
|
||||||
|
lambda *args: calls.append(args)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(main_module.presto, "update", lambda: calls.append("full"))
|
||||||
|
|
||||||
|
main_module._redraw_search_query_field(app)
|
||||||
|
|
||||||
|
assert calls == [(
|
||||||
|
main_module.KB_INPUT_UPDATE_X,
|
||||||
|
main_module.KB_INPUT_UPDATE_Y,
|
||||||
|
main_module.KB_INPUT_UPDATE_W,
|
||||||
|
main_module.KB_INPUT_UPDATE_H,
|
||||||
|
)]
|
||||||
|
|
||||||
|
def test_scrobble_button_partial_redraw(self, main_module, monkeypatch):
|
||||||
|
"""Scrobble feedback redraws only the visible button region."""
|
||||||
|
app = self._make_app(main_module)
|
||||||
|
app.screen = main_module.STATE_RECORD
|
||||||
|
app.day.selected_day = 15
|
||||||
|
|
||||||
|
rec = make_mock_record(0)
|
||||||
|
app.day.records = [rec]
|
||||||
|
app.detail.selected_index = 0
|
||||||
|
app.detail.source_screen = main_module.STATE_DAY
|
||||||
|
main_module._measure_detail_content(app, rec)
|
||||||
|
app.detail.scroll_offset = main_module._max_detail_scroll_offset(app)
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
main_module.presto,
|
||||||
|
"partial_update",
|
||||||
|
lambda *args: calls.append(args)
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(main_module.presto, "update", lambda: calls.append("full"))
|
||||||
|
|
||||||
|
app.detail.scrobble_state = "loading"
|
||||||
|
main_module._redraw_scrobble_button(app, rec)
|
||||||
|
|
||||||
|
button_x = (main_module.WIDTH - main_module.SCROBBLE_BUTTON_W) // 2
|
||||||
|
button_y = main_module._detail_scrobble_button_y(app)
|
||||||
|
update_y = max(button_y, main_module.SCROLL_VIEWPORT_Y)
|
||||||
|
update_h = min(
|
||||||
|
button_y + main_module.SCROBBLE_BUTTON_H,
|
||||||
|
main_module.HEIGHT
|
||||||
|
) - update_y
|
||||||
|
|
||||||
|
assert calls == [(
|
||||||
|
button_x,
|
||||||
|
update_y,
|
||||||
|
main_module.SCROBBLE_BUTTON_W,
|
||||||
|
update_h,
|
||||||
|
)]
|
||||||
|
|
||||||
def test_search_input_renders(self, main_module):
|
def test_search_input_renders(self, main_module):
|
||||||
"""Search input screen with on-screen keyboard."""
|
"""Search input screen with on-screen keyboard."""
|
||||||
app = self._make_app(main_module)
|
app = self._make_app(main_module)
|
||||||
|
|||||||
Reference in New Issue
Block a user