From 67c022595ef387dfdeb8124bb6b0355e06c2e7d8 Mon Sep 17 00:00:00 2001 From: Blaine Booher Date: Sat, 15 Aug 2026 17:36:07 -0500 Subject: [PATCH] FX list: drag along the right edge to scrub the whole list LVGL 8 scrollbars are display-only, and the ~190-row effect list is dozens of flings from end to end. Pressing within 18 px of the list's right edge and dragging now maps the finger's position onto the full scroll range, like dragging a phone's scroll indicator. A press there without movement is still a normal tap, so the gear column keeps working, and the table's release-time cell click is suppressed after a scrub. Co-Authored-By: Claude Fable 5 --- src/ui/tabs.cpp | 59 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/ui/tabs.cpp b/src/ui/tabs.cpp index 1c7f719..6e2aaa1 100644 --- a/src/ui/tabs.cpp +++ b/src/ui/tabs.cpp @@ -2298,8 +2298,66 @@ void openFxControls() { } +// ── Scrub strip ────────────────────────────────────────────────────────────── +// LVGL 8 scrollbars are display-only, and a ~190-row effect list is dozens of +// flings end to end. Pressing in a strip along the right edge and dragging +// maps the finger's position onto the whole scroll range instead, like +// dragging a phone's scroll indicator. A press in the strip without movement +// still behaves as a normal tap so the gear column keeps working. +constexpr lv_coord_t kScrubStripWidth = 18; +constexpr lv_coord_t kScrubMoveThreshold = 4; +lv_obj_t* scrub_target = nullptr; +bool scrub_moved = false; +lv_coord_t scrub_press_y = 0; + +bool scrubConsumedGesture() { return scrub_target && scrub_moved; } + +void onScrubStrip(lv_event_t* event) { + lv_obj_t* obj = lv_event_get_target(event); + lv_indev_t* indev = lv_indev_get_act(); + if (!indev) return; + lv_point_t point; + lv_indev_get_point(indev, &point); + const lv_event_code_t code = lv_event_get_code(event); + lv_area_t area; + lv_obj_get_coords(obj, &area); + if (code == LV_EVENT_PRESSED) { + if (point.x < area.x2 - kScrubStripWidth) return; + scrub_target = obj; + scrub_moved = false; + scrub_press_y = point.y; + // Keep LVGL's own drag-to-scroll out of the way for this press. + lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + } else if (code == LV_EVENT_PRESSING) { + if (scrub_target != obj) return; + if (!scrub_moved && LV_ABS(point.y - scrub_press_y) < kScrubMoveThreshold) return; + scrub_moved = true; + const lv_coord_t height = lv_area_get_height(&area); + if (height <= 0) return; + const lv_coord_t range = lv_obj_get_scroll_top(obj) + lv_obj_get_scroll_bottom(obj); + lv_coord_t offset = point.y - area.y1; + if (offset < 0) offset = 0; + if (offset > height) offset = height; + lv_obj_scroll_to_y(obj, int32_t(range) * offset / height, LV_ANIM_OFF); + } else if (code == LV_EVENT_RELEASED || code == LV_EVENT_PRESS_LOST) { + if (scrub_target != obj) return; + lv_obj_add_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + scrub_target = nullptr; + scrub_moved = false; + } +} + +void attachScrubStrip(lv_obj_t* scrollable) { + lv_obj_add_event_cb(scrollable, onScrubStrip, LV_EVENT_PRESSED, nullptr); + lv_obj_add_event_cb(scrollable, onScrubStrip, LV_EVENT_PRESSING, nullptr); + lv_obj_add_event_cb(scrollable, onScrubStrip, LV_EVENT_RELEASED, nullptr); + lv_obj_add_event_cb(scrollable, onScrubStrip, LV_EVENT_PRESS_LOST, nullptr); +} + void onEffectTableClicked(lv_event_t* event) { if (!wledControlsAvailable()) return; + // The table reports a cell "click" on release even after a scrub drag. + if (scrubConsumedGesture()) return; lv_obj_t* table = lv_event_get_target(event); uint16_t row = LV_TABLE_CELL_NONE; uint16_t col = LV_TABLE_CELL_NONE; @@ -2892,6 +2950,7 @@ void createFxTab(lv_obj_t* tab) { lv_table_set_cell_value(table, i, 1, LV_SYMBOL_SETTINGS); } lv_obj_add_event_cb(table, onEffectTableClicked, LV_EVENT_VALUE_CHANGED, nullptr); + attachScrubStrip(table); lv_obj_add_event_cb(table, onEffectTableDrawPart, LV_EVENT_DRAW_PART_BEGIN, nullptr); revealSelectedEffect(false, false);