[presto] Improve touch responsiveness and visual layout
This commit is contained in:
+275
-94
@@ -27,7 +27,6 @@ Setup:
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
import network
|
import network
|
||||||
import ntptime
|
import ntptime
|
||||||
@@ -66,9 +65,13 @@ MAX_ROWS = 6
|
|||||||
# Header
|
# Header
|
||||||
HEADER_Y = 0
|
HEADER_Y = 0
|
||||||
HEADER_H = 46
|
HEADER_H = 46
|
||||||
MONTH_Y = HEADER_Y + 6
|
HEADER_TEXT_H = 14
|
||||||
ARROW_W = 48
|
HEADER_SIDE_MARGIN = 8
|
||||||
ARROW_H = 34
|
HEADER_BUTTON_W = 44
|
||||||
|
HEADER_BUTTON_H = 32
|
||||||
|
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
|
# Day-of-week labels
|
||||||
DOW_Y = HEADER_Y + HEADER_H + 2
|
DOW_Y = HEADER_Y + HEADER_H + 2
|
||||||
@@ -84,12 +87,16 @@ GRID_LEFT = (WIDTH - (CELLS_PER_ROW * CELL_SIZE + (CELLS_PER_ROW - 1) * CELL_GAP
|
|||||||
CALENDAR_TOP = DOW_Y + DOW_H + 6
|
CALENDAR_TOP = DOW_Y + DOW_H + 6
|
||||||
|
|
||||||
# Day view (proportional to cell size)
|
# Day view (proportional to cell size)
|
||||||
BACK_X = 8
|
BACK_X = HEADER_SIDE_MARGIN
|
||||||
BACK_Y = 8
|
BACK_Y = HEADER_BUTTON_Y
|
||||||
BACK_W = 44
|
BACK_W = HEADER_BUTTON_W
|
||||||
BACK_H = 32
|
BACK_H = HEADER_BUTTON_H
|
||||||
RECORD_START_Y = BACK_Y + BACK_H + 12
|
DAY_HEADER_Y = HEADER_Y
|
||||||
THUMB_SIZE = 75
|
DAY_HEADER_H = HEADER_H
|
||||||
|
DAY_HEADER_TEXT_H = HEADER_TEXT_H
|
||||||
|
DAY_COUNT_TEXT_H = 8
|
||||||
|
RECORD_START_Y = DAY_HEADER_Y + DAY_HEADER_H + 8
|
||||||
|
THUMB_SIZE = 40
|
||||||
THUMB_MARGIN = 8
|
THUMB_MARGIN = 8
|
||||||
TEXT_X = THUMB_MARGIN + THUMB_SIZE + 12
|
TEXT_X = THUMB_MARGIN + THUMB_SIZE + 12
|
||||||
TEXT_W = WIDTH - TEXT_X - 12
|
TEXT_W = WIDTH - TEXT_X - 12
|
||||||
@@ -149,8 +156,9 @@ selected_day = 0 # 1-based day of month
|
|||||||
records = [] # List of record dicts from API
|
records = [] # List of record dicts from API
|
||||||
records_error = False # True if API call failed
|
records_error = False # True if API call failed
|
||||||
# Scroll state
|
# Scroll state
|
||||||
scroll_offset = 0 # Scroll position for day view
|
scroll_offset = 0 # Pixel scroll position for day view
|
||||||
_visible_count = 1 # How many records fit on screen (set during draw)
|
_visible_count = 1 # How many records fit on screen (set during draw)
|
||||||
|
_dragging = False # True while fast scroll redraws are active
|
||||||
|
|
||||||
# Touch debounce
|
# Touch debounce
|
||||||
_last_touch = 0
|
_last_touch = 0
|
||||||
@@ -246,6 +254,32 @@ def draw_wrapped(text, x, y, max_width, pen, scale=1):
|
|||||||
return cy
|
return cy
|
||||||
|
|
||||||
|
|
||||||
|
def _wrapped_line_count(text, max_width, scale=1):
|
||||||
|
"""Return how many wrapped lines the text will occupy."""
|
||||||
|
words = text.split(" ")
|
||||||
|
lines = 0
|
||||||
|
current_line = ""
|
||||||
|
|
||||||
|
for word in words:
|
||||||
|
test_line = current_line + (" " if current_line else "") + word
|
||||||
|
w = display.measure_text(test_line, scale=scale)
|
||||||
|
if w <= max_width:
|
||||||
|
current_line = test_line
|
||||||
|
else:
|
||||||
|
if current_line:
|
||||||
|
lines += 1
|
||||||
|
if display.measure_text(word, scale=scale) > max_width:
|
||||||
|
lines += 1
|
||||||
|
current_line = ""
|
||||||
|
else:
|
||||||
|
current_line = word
|
||||||
|
|
||||||
|
if current_line:
|
||||||
|
lines += 1
|
||||||
|
|
||||||
|
return max(1, lines)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# HELPER: DATE / CALENDAR MATH
|
# HELPER: DATE / CALENDAR MATH
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -425,6 +459,15 @@ def fetch_thumbnail(url):
|
|||||||
# JPEG RENDERING
|
# JPEG RENDERING
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
def _jpeg_scale_options():
|
||||||
|
"""Return JPEG scale flags paired with their output-size divisors."""
|
||||||
|
full = getattr(_jpegdec_lib, "JPEG_SCALE_FULL", 0)
|
||||||
|
half = getattr(_jpegdec_lib, "JPEG_SCALE_HALF", 1)
|
||||||
|
quarter = getattr(_jpegdec_lib, "JPEG_SCALE_QUARTER", 2)
|
||||||
|
eighth = getattr(_jpegdec_lib, "JPEG_SCALE_EIGHTH", 3)
|
||||||
|
return ((full, 1), (half, 2), (quarter, 4), (eighth, 8))
|
||||||
|
|
||||||
|
|
||||||
def draw_jpeg(data, x, y, max_w, max_h):
|
def draw_jpeg(data, x, y, max_w, max_h):
|
||||||
"""Decode and draw a JPEG image, scaled to fit within max_w x max_h
|
"""Decode and draw a JPEG image, scaled to fit within max_w x max_h
|
||||||
and centered in the bounding box.
|
and centered in the bounding box.
|
||||||
@@ -449,16 +492,18 @@ def draw_jpeg(data, x, y, max_w, max_h):
|
|||||||
try:
|
try:
|
||||||
img_h = jpeg.get_height()
|
img_h = jpeg.get_height()
|
||||||
img_w = jpeg.get_width()
|
img_w = jpeg.get_width()
|
||||||
# Try scales from largest to smallest; use first that fits
|
# Try scales from largest to smallest; use the first that fits.
|
||||||
# (0=full, 2=half, 4=quarter, 8=eighth)
|
scale = None
|
||||||
for s in (2, 4, 8):
|
divisor = 1
|
||||||
if img_w // s <= max_w and img_h // s <= max_h:
|
for scale_flag, scale_divisor in _jpeg_scale_options():
|
||||||
scale = s
|
if img_w // scale_divisor <= max_w and img_h // scale_divisor <= max_h:
|
||||||
|
scale = scale_flag
|
||||||
|
divisor = scale_divisor
|
||||||
break
|
break
|
||||||
else:
|
if scale is None:
|
||||||
scale = 0
|
scale, divisor = _jpeg_scale_options()[-1]
|
||||||
sw = img_w // max(1, scale)
|
sw = img_w // divisor
|
||||||
sh = img_h // max(1, scale)
|
sh = img_h // divisor
|
||||||
ox = x + (max_w - sw) // 2
|
ox = x + (max_w - sw) // 2
|
||||||
oy = y + (max_h - sh) // 2
|
oy = y + (max_h - sh) // 2
|
||||||
jpeg.decode(ox, oy, scale)
|
jpeg.decode(ox, oy, scale)
|
||||||
@@ -467,10 +512,7 @@ def draw_jpeg(data, x, y, max_w, max_h):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback: decode at quarter scale (works for most cover art)
|
# Fallback: decode at quarter scale (works for most cover art)
|
||||||
jpeg.decode(x, y, 4)
|
jpeg.decode(x, y, getattr(_jpegdec_lib, "JPEG_SCALE_QUARTER", 2))
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -559,25 +601,34 @@ def _draw_month_header():
|
|||||||
title = "{} {}".format(MONTH_NAMES[view_month - 1], view_year)
|
title = "{} {}".format(MONTH_NAMES[view_month - 1], view_year)
|
||||||
tw = display.measure_text(title, scale=1)
|
tw = display.measure_text(title, scale=1)
|
||||||
tx = (WIDTH - tw) // 2
|
tx = (WIDTH - tw) // 2
|
||||||
ty = MONTH_Y
|
ty = HEADER_Y + (HEADER_H - HEADER_TEXT_H) // 2
|
||||||
display.text(title, tx, ty, scale=1)
|
display.text(title, tx, ty, scale=1)
|
||||||
|
|
||||||
# Left arrow
|
# Left arrow
|
||||||
lx = 12
|
lx = HEADER_SIDE_MARGIN
|
||||||
ly = HEADER_Y + (HEADER_H - ARROW_H) // 2
|
ly = HEADER_BUTTON_Y
|
||||||
display.set_pen(_pen_cell_bg) # Match header bg
|
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_arrow)
|
display.set_pen(_pen_back)
|
||||||
display.set_font("bitmap14_outline")
|
display.set_font("bitmap8")
|
||||||
display.text("<", lx + 14, ly + 8, scale=1)
|
_draw_centered_text("<", lx, ly, ARROW_W, ARROW_H, 8)
|
||||||
|
|
||||||
# Right arrow
|
# Right arrow
|
||||||
rx = WIDTH - ARROW_W - 12
|
rx = WIDTH - ARROW_W - HEADER_SIDE_MARGIN
|
||||||
ry = HEADER_Y + (HEADER_H - ARROW_H) // 2
|
ry = HEADER_BUTTON_Y
|
||||||
display.set_pen(_pen_cell_bg)
|
display.set_pen(_pen_placeholder)
|
||||||
display.rectangle(rx, ry, ARROW_W, ARROW_H)
|
display.rectangle(rx, ry, ARROW_W, ARROW_H)
|
||||||
display.set_pen(_pen_arrow)
|
display.set_pen(_pen_back)
|
||||||
display.text(">", rx + 12, ry + 8, scale=1)
|
display.set_font("bitmap8")
|
||||||
|
_draw_centered_text(">", rx, ry, ARROW_W, ARROW_H, 8)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_centered_text(text, x, y, w, h, text_h, scale=1):
|
||||||
|
"""Draw text centered inside a rectangular area."""
|
||||||
|
tw = display.measure_text(text, scale=scale)
|
||||||
|
tx = x + (w - tw) // 2
|
||||||
|
ty = y + (h - text_h) // 2
|
||||||
|
display.text(text, tx, ty, scale=scale)
|
||||||
|
|
||||||
|
|
||||||
def _draw_day_labels():
|
def _draw_day_labels():
|
||||||
@@ -682,19 +733,20 @@ def draw_day_view():
|
|||||||
display.set_pen(_pen_bg)
|
display.set_pen(_pen_bg)
|
||||||
display.clear()
|
display.clear()
|
||||||
|
|
||||||
_draw_day_header()
|
|
||||||
|
|
||||||
if records_error:
|
if records_error:
|
||||||
_draw_day_error()
|
_draw_day_error()
|
||||||
|
_draw_day_header()
|
||||||
presto.update()
|
presto.update()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not records:
|
if not records:
|
||||||
_draw_day_empty()
|
_draw_day_empty()
|
||||||
|
_draw_day_header()
|
||||||
presto.update()
|
presto.update()
|
||||||
return
|
return
|
||||||
|
|
||||||
_draw_record_list()
|
_draw_record_list()
|
||||||
|
_draw_day_header()
|
||||||
presto.update()
|
presto.update()
|
||||||
|
|
||||||
|
|
||||||
@@ -702,7 +754,7 @@ def _draw_day_header():
|
|||||||
"""Draw the header with formatted date and back button."""
|
"""Draw the header with formatted date and back button."""
|
||||||
# Background bar
|
# Background bar
|
||||||
display.set_pen(_pen_cell_bg)
|
display.set_pen(_pen_cell_bg)
|
||||||
display.rectangle(0, BACK_Y - 4, WIDTH, BACK_H + 8)
|
display.rectangle(0, DAY_HEADER_Y, WIDTH, DAY_HEADER_H)
|
||||||
|
|
||||||
# Back button
|
# Back button
|
||||||
bx, by = BACK_X, BACK_Y
|
bx, by = BACK_X, BACK_Y
|
||||||
@@ -710,7 +762,7 @@ def _draw_day_header():
|
|||||||
display.rectangle(bx, by, BACK_W, BACK_H)
|
display.rectangle(bx, by, BACK_W, BACK_H)
|
||||||
display.set_pen(_pen_back)
|
display.set_pen(_pen_back)
|
||||||
display.set_font("bitmap8")
|
display.set_font("bitmap8")
|
||||||
display.text("< Back", bx + 10, by + 14, scale=1)
|
_draw_centered_text("<", bx, by, BACK_W, BACK_H, DAY_COUNT_TEXT_H)
|
||||||
|
|
||||||
# Formatted date
|
# Formatted date
|
||||||
date_str = "{} {}, {}".format(
|
date_str = "{} {}, {}".format(
|
||||||
@@ -719,15 +771,24 @@ def _draw_day_header():
|
|||||||
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=1)
|
||||||
display.text(date_str, (WIDTH - tw) // 2, BACK_Y + 8, scale=1)
|
display.text(
|
||||||
|
date_str,
|
||||||
|
(WIDTH - tw) // 2,
|
||||||
|
DAY_HEADER_Y + (DAY_HEADER_H - DAY_HEADER_TEXT_H) // 2,
|
||||||
|
scale=1
|
||||||
|
)
|
||||||
|
|
||||||
# Record count
|
# Record count
|
||||||
count_str = "{} record{}".format(len(records), "s" if len(records) != 1 else "")
|
count_str = "{} record{}".format(len(records), "s" if len(records) != 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=1)
|
||||||
# display.text(count_str, WIDTH - cw - 16, BACK_Y + BACK_H - 4, scale=1)
|
display.text(
|
||||||
display.text(count_str, WIDTH - cw - 16, BACK_Y + 14, scale=1)
|
count_str,
|
||||||
|
WIDTH - cw - 16,
|
||||||
|
DAY_HEADER_Y + (DAY_HEADER_H - DAY_COUNT_TEXT_H) // 2,
|
||||||
|
scale=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _draw_day_error():
|
def _draw_day_error():
|
||||||
@@ -756,8 +817,10 @@ def _draw_day_empty():
|
|||||||
def _draw_record_list():
|
def _draw_record_list():
|
||||||
"""Draw the scrollable list of record rows with cover thumbnails.
|
"""Draw the scrollable list of record rows with cover thumbnails.
|
||||||
Uses dynamic row heights so wrapped text doesn't break layout."""
|
Uses dynamic row heights so wrapped text doesn't break layout."""
|
||||||
start_idx = scroll_offset
|
global scroll_offset, _visible_count
|
||||||
total = len(records)
|
|
||||||
|
max_offset = _max_scroll_offset()
|
||||||
|
scroll_offset = min(max(0, scroll_offset), max_offset)
|
||||||
|
|
||||||
# Up arrow if scrolled down
|
# Up arrow if scrolled down
|
||||||
if scroll_offset > 0:
|
if scroll_offset > 0:
|
||||||
@@ -765,29 +828,83 @@ def _draw_record_list():
|
|||||||
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 - 8, RECORD_START_Y - 28, scale=1)
|
||||||
|
|
||||||
# Draw rows with dynamic heights
|
cy = RECORD_START_Y - scroll_offset
|
||||||
cy = RECORD_START_Y
|
|
||||||
visible_count = 0
|
visible_count = 0
|
||||||
for i in range(start_idx, total):
|
|
||||||
rec = records[i]
|
for rec in records:
|
||||||
row_bottom = _draw_record_row(rec, cy)
|
row_h = _record_row_height(rec)
|
||||||
# Stop if the row would overflow the display
|
row_bottom = cy + row_h
|
||||||
if row_bottom > HEIGHT - 28:
|
|
||||||
|
if row_bottom <= RECORD_START_Y:
|
||||||
|
cy = row_bottom
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cy >= HEIGHT - 28:
|
||||||
break
|
break
|
||||||
cy = row_bottom
|
|
||||||
|
_draw_record_row(rec, cy)
|
||||||
visible_count += 1
|
visible_count += 1
|
||||||
|
cy = row_bottom
|
||||||
|
|
||||||
# Down arrow if more records below
|
# Down arrow if more records below
|
||||||
if start_idx + visible_count < total:
|
if 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, cy + 4, scale=1)
|
display.text("v", WIDTH // 2 - 8, HEIGHT - 24, scale=1)
|
||||||
|
|
||||||
# Store how many fit for scroll logic
|
# Store how many fit for scroll logic
|
||||||
global _visible_count
|
|
||||||
_visible_count = max(1, visible_count)
|
_visible_count = max(1, visible_count)
|
||||||
|
|
||||||
|
|
||||||
|
def _max_scroll_offset():
|
||||||
|
"""Return the maximum pixel scroll offset for the current records."""
|
||||||
|
viewport_h = HEIGHT - RECORD_START_Y - 28
|
||||||
|
content_h = 0
|
||||||
|
|
||||||
|
display.set_font("bitmap8")
|
||||||
|
for rec in records:
|
||||||
|
content_h += _record_row_height(rec)
|
||||||
|
|
||||||
|
return max(0, content_h - viewport_h)
|
||||||
|
|
||||||
|
|
||||||
|
def _record_row_height(rec):
|
||||||
|
"""Calculate a record row height without drawing it."""
|
||||||
|
display.set_font("bitmap8")
|
||||||
|
min_h = THUMB_SIZE + 8
|
||||||
|
title = rec.get("title", "Unknown Title")
|
||||||
|
ty = _wrapped_line_count(title, TEXT_W, scale=1) * 12
|
||||||
|
|
||||||
|
artists = rec.get("artists", [])
|
||||||
|
if artists:
|
||||||
|
ty += 2 + _wrapped_line_count(", ".join(artists), TEXT_W, scale=1) * 12
|
||||||
|
|
||||||
|
content_h = max(THUMB_SIZE, ty) + 10
|
||||||
|
return max(min_h, content_h)
|
||||||
|
|
||||||
|
|
||||||
|
def _record_thumbnail_data(rec):
|
||||||
|
"""Fetch thumbnail bytes once per record and reuse them on redraw."""
|
||||||
|
if rec.get("_thumb_failed", False):
|
||||||
|
return None
|
||||||
|
|
||||||
|
cached = rec.get("_thumb_data", None)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
thumb_url = rec.get("mini_cover_url", "") or rec.get("thumb_url", "")
|
||||||
|
if not thumb_url:
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = fetch_thumbnail(thumb_url)
|
||||||
|
if data is None:
|
||||||
|
rec["_thumb_failed"] = True
|
||||||
|
else:
|
||||||
|
rec["_thumb_data"] = data
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def _draw_record_row(rec, y):
|
def _draw_record_row(rec, y):
|
||||||
"""Draw a single record row. Returns the Y position after the row
|
"""Draw a single record row. Returns the Y position after the row
|
||||||
(including separator), so the next row can be positioned correctly
|
(including separator), so the next row can be positioned correctly
|
||||||
@@ -795,14 +912,16 @@ def _draw_record_row(rec, y):
|
|||||||
min_h = THUMB_SIZE + 8 # Minimum row height (thumbnail + padding)
|
min_h = THUMB_SIZE + 8 # Minimum row height (thumbnail + padding)
|
||||||
|
|
||||||
# Fetch and draw thumbnail (top-aligned in row)
|
# Fetch and draw thumbnail (top-aligned in row)
|
||||||
thumb_url = rec.get("mini_cover_url", "")
|
|
||||||
thumb_y = y + 4
|
thumb_y = y + 4
|
||||||
if thumb_url:
|
if _dragging:
|
||||||
jpeg_data = fetch_thumbnail(thumb_url)
|
|
||||||
draw_jpeg(jpeg_data, THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE)
|
|
||||||
gc.collect()
|
|
||||||
else:
|
|
||||||
_draw_placeholder(THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE)
|
_draw_placeholder(THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE)
|
||||||
|
else:
|
||||||
|
jpeg_data = _record_thumbnail_data(rec)
|
||||||
|
if jpeg_data is not None:
|
||||||
|
draw_jpeg(jpeg_data, THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE)
|
||||||
|
gc.collect()
|
||||||
|
else:
|
||||||
|
_draw_placeholder(THUMB_MARGIN, thumb_y, THUMB_SIZE, THUMB_SIZE)
|
||||||
|
|
||||||
# Title (starts at same height as thumbnail)
|
# Title (starts at same height as thumbnail)
|
||||||
title = rec.get("title", "Unknown Title")
|
title = rec.get("title", "Unknown Title")
|
||||||
@@ -831,6 +950,62 @@ def _draw_record_row(rec, y):
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TOUCH HANDLING
|
# TOUCH HANDLING
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
def read_touch():
|
||||||
|
"""Return the current touch as (x, y), or None when not pressed."""
|
||||||
|
try:
|
||||||
|
touch.poll()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if touch.state:
|
||||||
|
return int(touch.x), int(touch.y)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for method_name in ("read", "get_touch"):
|
||||||
|
method = getattr(touch, method_name, None)
|
||||||
|
if method is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
point = method()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
normalised = _normalise_touch(point)
|
||||||
|
if normalised is not None:
|
||||||
|
return normalised
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalise_touch(point):
|
||||||
|
"""Convert common touch return shapes to (x, y)."""
|
||||||
|
if not point:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(point, (tuple, list)):
|
||||||
|
if len(point) == 2:
|
||||||
|
return int(point[0]), int(point[1])
|
||||||
|
if len(point) >= 3 and point[0]:
|
||||||
|
return int(point[1]), int(point[2])
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(point, "state") and not point.state:
|
||||||
|
return None
|
||||||
|
return int(point.x), int(point.y)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_touch_release():
|
||||||
|
"""Block until the active touch is released."""
|
||||||
|
while read_touch() is not None:
|
||||||
|
time.sleep(0.02)
|
||||||
|
|
||||||
|
|
||||||
def touch_to_calendar_cell(x, y):
|
def touch_to_calendar_cell(x, y):
|
||||||
"""Map a touch (x, y) to (row, col) in the calendar grid, or None
|
"""Map a touch (x, y) to (row, col) in the calendar grid, or None
|
||||||
if the touch is outside the grid."""
|
if the touch is outside the grid."""
|
||||||
@@ -866,14 +1041,14 @@ def handle_month_touch(x, y):
|
|||||||
global view_year, view_month, selected_day, state, records, records_error, scroll_offset
|
global view_year, view_month, selected_day, state, records, records_error, scroll_offset
|
||||||
|
|
||||||
# Check left arrow
|
# Check left arrow
|
||||||
lx, ly = 12, HEADER_Y + (HEADER_H - ARROW_H) // 2
|
lx, ly = HEADER_SIDE_MARGIN, 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:
|
||||||
view_year, view_month = previous_month(view_year, view_month)
|
view_year, view_month = previous_month(view_year, view_month)
|
||||||
draw_month_view()
|
draw_month_view()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check right arrow
|
# Check right arrow
|
||||||
rx, ry = WIDTH - ARROW_W - 12, HEADER_Y + (HEADER_H - ARROW_H) // 2
|
rx, ry = WIDTH - ARROW_W - HEADER_SIDE_MARGIN, HEADER_BUTTON_Y
|
||||||
if rx <= x <= rx + ARROW_W and ry <= y <= ry + ARROW_H:
|
if rx <= x <= rx + ARROW_W and ry <= y <= ry + ARROW_H:
|
||||||
view_year, view_month = next_month(view_year, view_month)
|
view_year, view_month = next_month(view_year, view_month)
|
||||||
draw_month_view()
|
draw_month_view()
|
||||||
@@ -942,6 +1117,7 @@ def main():
|
|||||||
main event loop."""
|
main event loop."""
|
||||||
global state, view_year, view_month, _last_touch
|
global state, view_year, view_month, _last_touch
|
||||||
global selected_day, records, records_error, scroll_offset
|
global selected_day, records, records_error, scroll_offset
|
||||||
|
global _dragging
|
||||||
|
|
||||||
# -- Init display --
|
# -- Init display --
|
||||||
init_display()
|
init_display()
|
||||||
@@ -959,14 +1135,14 @@ def main():
|
|||||||
sync_time()
|
sync_time()
|
||||||
set_today()
|
set_today()
|
||||||
|
|
||||||
# Start at current month and today's day view
|
# Start on today's records.
|
||||||
view_year = today_year
|
view_year = today_year
|
||||||
view_month = today_month
|
view_month = today_month
|
||||||
selected_day = today_day
|
selected_day = today_day
|
||||||
state = STATE_DAY
|
state = STATE_DAY
|
||||||
records_error = False
|
records_error = False
|
||||||
|
scroll_offset = 0
|
||||||
|
|
||||||
# Fetch today's records
|
|
||||||
draw_status("Loading records for\n{} {}, {}...".format(
|
draw_status("Loading records for\n{} {}, {}...".format(
|
||||||
MONTH_NAMES[view_month - 1], today_day, view_year
|
MONTH_NAMES[view_month - 1], today_day, view_year
|
||||||
))
|
))
|
||||||
@@ -974,58 +1150,64 @@ def main():
|
|||||||
records = recs
|
records = recs
|
||||||
records_error = had_error
|
records_error = had_error
|
||||||
|
|
||||||
# Enter main loop
|
|
||||||
draw_day_view()
|
draw_day_view()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
touch.poll()
|
touch_point = read_touch()
|
||||||
|
if touch_point is None:
|
||||||
if not touch.state:
|
|
||||||
time.sleep(0.03)
|
time.sleep(0.03)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
x, y = int(touch.x), int(touch.y)
|
x, y = touch_point
|
||||||
now = time.ticks_ms()
|
now = time.ticks_ms()
|
||||||
if time.ticks_diff(now, _last_touch) < DEBOUNCE_MS:
|
if time.ticks_diff(now, _last_touch) < DEBOUNCE_MS:
|
||||||
|
time.sleep(0.02)
|
||||||
continue
|
continue
|
||||||
_last_touch = now
|
_last_touch = now
|
||||||
|
|
||||||
if state == STATE_MONTH:
|
if state == STATE_MONTH:
|
||||||
handle_month_touch(x, y)
|
handle_month_touch(x, y)
|
||||||
# Wait for release to avoid re-triggering
|
wait_for_touch_release()
|
||||||
while touch.state:
|
|
||||||
touch.poll()
|
|
||||||
time.sleep(0.02)
|
|
||||||
|
|
||||||
elif state == STATE_DAY:
|
elif state == STATE_DAY:
|
||||||
# Check back button immediately (no drag needed)
|
# Check back button immediately (no drag needed)
|
||||||
if BACK_X <= x <= BACK_X + BACK_W and BACK_Y <= y <= BACK_Y + BACK_H:
|
if BACK_X <= x <= BACK_X + BACK_W and BACK_Y <= y <= BACK_Y + BACK_H:
|
||||||
state = STATE_MONTH
|
state = STATE_MONTH
|
||||||
draw_month_view()
|
draw_month_view()
|
||||||
while touch.state:
|
wait_for_touch_release()
|
||||||
touch.poll()
|
|
||||||
time.sleep(0.02)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Track vertical drag for scrolling
|
# Track vertical drag for scrolling
|
||||||
drag_start_y = y
|
drag_start_y = y
|
||||||
|
last_drag_y = y
|
||||||
|
pending_delta = 0
|
||||||
dragged = False
|
dragged = False
|
||||||
while touch.state:
|
_dragging = False
|
||||||
touch.poll()
|
while True:
|
||||||
time.sleep(0.02)
|
touch_point = read_touch()
|
||||||
if touch.state:
|
if touch_point is None:
|
||||||
dy = drag_start_y - int(touch.y)
|
break
|
||||||
if abs(dy) > 5:
|
|
||||||
dragged = True
|
current_y = touch_point[1]
|
||||||
# Every ~50px of drag = one record scroll
|
if abs(current_y - drag_start_y) > 5:
|
||||||
if abs(dy) >= 50:
|
dragged = True
|
||||||
total = len(records)
|
|
||||||
if dy > 0:
|
pending_delta += last_drag_y - current_y
|
||||||
scroll_offset = min(total - _visible_count, scroll_offset + 1)
|
if abs(pending_delta) >= 3:
|
||||||
else:
|
max_offset = _max_scroll_offset()
|
||||||
scroll_offset = max(0, scroll_offset - 1)
|
new_offset = min(max(0, scroll_offset + pending_delta), max_offset)
|
||||||
|
if new_offset != scroll_offset:
|
||||||
|
scroll_offset = new_offset
|
||||||
|
_dragging = True
|
||||||
draw_day_view()
|
draw_day_view()
|
||||||
drag_start_y = int(touch.y)
|
pending_delta = 0
|
||||||
|
|
||||||
|
last_drag_y = current_y
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
if _dragging:
|
||||||
|
_dragging = False
|
||||||
|
draw_day_view()
|
||||||
|
|
||||||
if not dragged:
|
if not dragged:
|
||||||
# It was a tap — handle normally
|
# It was a tap — handle normally
|
||||||
@@ -1037,4 +1219,3 @@ def main():
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user