From b149428dcbb8abd572e6db918d135bbb3668d38b Mon Sep 17 00:00:00 2001 From: Max Gramser Date: Fri, 18 Sep 2026 20:08:26 +0200 Subject: [PATCH] Give a card's small slider the whole card and keep a quick drag on it from turning the page (0.2.81) On a CYD the strip at the bottom of a compact card is 8 px high, so a finger landing a little above it pressed the card: the light's card opened, the light toggled, or the page-swipe gesture turned the page without a log line. The strip's click area now covers its card (slider_zone); a press that never moves forwards the card's tap or hold and shows the card pressed; the CYD's page-swipe handler and the Guition's flick handler leave a captured slider alone and log what they do; a slider commit no longer bounces against its own previous send (TouchGuard::accept_within); the CYD's edge band is 20 px because its resistive panel reports up to the bezel. App 0.2.81, firmware 0.2.68. Co-Authored-By: Claude Fable 5.1 --- components/smart_display/cyd_ui.h | 20 +++++++------ components/smart_display/runtime_tiles.h | 36 ++++++++++++++++++++++-- guition-4848s040.yaml | 4 ++- home-like-2432s028.yaml | 12 +++++--- packages/cyd.yaml | 12 +++++--- packages/guition.yaml | 4 ++- screen_manager/CHANGELOG.md | 10 +++++++ screen_manager/app/core.py | 2 +- screen_manager/config.yaml | 2 +- tests/test_cyd_ui.cpp | 9 +++++- 10 files changed, 86 insertions(+), 25 deletions(-) diff --git a/components/smart_display/cyd_ui.h b/components/smart_display/cyd_ui.h index 1defbfe..475e73a 100644 --- a/components/smart_display/cyd_ui.h +++ b/components/smart_display/cyd_ui.h @@ -73,7 +73,7 @@ class TouchGuard { void consume() { accepted_ = true; moved_ = true; } bool accept(uint32_t now, int tile) { if (moved_) { reject_ = MOVED; return false; } - return accept_slider(now, tile); + return accept_within(now, tile, 600); } // For -/+ keys and the page buttons: every clean tap counts, even the third within a second, // so a setpoint moves several steps in one go and Next, Next, Next reaches page 4 without @@ -87,14 +87,9 @@ class TouchGuard { return true; } // Only for a slider which captured this contact and did not lose the press. - // Consume the gesture so its parent can never also turn it into a tile tap. - bool accept_slider(uint32_t now, int tile) { - if (accepted_) { reject_ = USED; return false; } - if (!long_enough(now, min_press_)) return false; - if (has_previous_ && tile == previous_tile_ && now - previous_ < 600) { reject_ = BOUNCE; return false; } - remember(now, tile); - return true; - } + // Consume the gesture so its parent can never also turn it into a tile tap. A slider sends + // once per contact, on release, so a second drag right after the first is no bounce. + bool accept_slider(uint32_t now, int tile) { return accept_within(now, tile, 0); } // Why the last accept() refused, for the touch log; empty after a success. std::string reason() const { switch (reject_) { @@ -107,6 +102,13 @@ class TouchGuard { } private: static constexpr int SETTLE_SAMPLES = 4; + bool accept_within(uint32_t now, int tile, uint32_t gap) { + if (accepted_) { reject_ = USED; return false; } + if (!long_enough(now, min_press_)) return false; + if (gap && has_previous_ && tile == previous_tile_ && now - previous_ < gap) { reject_ = BOUNCE; return false; } + remember(now, tile); + return true; + } bool long_enough(uint32_t now, uint32_t minimum) { duration_ = now - started_; if (duration_ < minimum) { reject_ = TOO_SHORT; return false; } diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index 4c13bc0..40965dc 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -873,9 +873,25 @@ inline void commit_slider(unsigned i,int raw){ value=std::clamp(t.minimum+std::round(value*(t.maximum-t.minimum)/t.step)*t.step,t.minimum,t.maximum); action(d+".set_value",t.entity,"value",std::to_string(value)); } } +// Where the finger landed on a slider, the value it had, and whether it landed beside the strip (in the tile's lower +// half that the strip claims, slider_zone) and moved: a press beside the strip that never moves is a tap on the tile. +inline lv_point_t slider_press{0,0};inline int slider_press_value=0;inline bool slider_press_beside=false,slider_press_moved=false,slider_press_held=false; +// The card a strip belongs to, for a press beside the strip that turns out to be the card's tap or hold. +inline lv_obj_t *strip_card(lv_obj_t *slider){for(auto &w:widgets)if(w.slider==slider)return w.tile;return nullptr;} inline void slider_event(lv_event_t *e){ auto *slider=lv_event_get_target_obj(e);auto code=lv_event_get_code(e); - if(code==LV_EVENT_PRESSED){captured_slider=slider;slider_changed=false;} + if(code==LV_EVENT_PRESSED){captured_slider=slider;slider_changed=false;slider_press_moved=false;slider_press_beside=false;slider_press_held=false; + slider_press_value=lv_slider_get_value(slider); + if(auto *indev=lv_indev_active()){lv_indev_get_point(indev,&slider_press);lv_area_t a;lv_obj_get_coords(slider,&a);slider_press_beside=slider_press.xa.x2 || slider_press.ya.y2;} + // Beside the strip the card lights up as under a tap, until the finger starts dragging. + if(slider_press_beside)if(auto *card=strip_card(slider))lv_obj_add_state(card,LV_STATE_PRESSED);} + if(code==LV_EVENT_PRESSING && captured_slider==slider && !slider_press_moved){ + if(auto *indev=lv_indev_active()){lv_point_t p;lv_indev_get_point(indev,&p);if(std::abs(p.x-slider_press.x)>=10 || std::abs(p.y-slider_press.y)>=10){slider_press_moved=true; + if(auto *card=strip_card(slider))lv_obj_remove_state(card,LV_STATE_PRESSED);}}} + // A finger resting beside the strip holds the card, as it would without the strip's claim on the card. + if(code==LV_EVENT_LONG_PRESSED && captured_slider==slider && slider_press_beside && !slider_press_moved && !slider_press_held){ + if(auto *card=strip_card(slider)){slider_press_held=true;lv_obj_send_event(card,LV_EVENT_LONG_PRESSED,nullptr);}} + if((code==LV_EVENT_RELEASED || code==LV_EVENT_PRESS_LOST) && slider_press_beside)if(auto *card=strip_card(slider))lv_obj_remove_state(card,LV_STATE_PRESSED); // On release LVGL sets the value once more from the last touch point: log it when that throws a // dragged slider to one of its ends, so a stray touch sample shows up (cyd::release_jump). if(code==LV_EVENT_VALUE_CHANGED && captured_slider==slider){ @@ -895,6 +911,15 @@ inline void slider_event(lv_event_t *e){ // A slider among a card's own parts (the media tile's volume) belongs to the tile the slot shows now. for(auto &w:widgets)if(w.slider==slider || w.control_slider==slider || (w.extra && lv_obj_get_parent(slider)==w.extra)){index=w.index;break;} bool changed=slider_changed;captured_slider=nullptr;slider_changed=false; + // A finger that landed beside a tile's strip and let go without moving tapped the tile (or held it, sent above): + // the value LVGL set from that point on release goes back, and the tile's own rules decide what the tap does. + if(slider_press_beside && !slider_press_moved){ + if(auto *card=strip_card(slider)){ + lv_slider_set_value(slider,slider_press_value,LV_ANIM_OFF); + ESP_LOGI("slider","%s beside the strip of %s: the tile's",slider_press_held?"Hold":"Tap",model.tiles[index].entity.c_str()); + if(!slider_press_held)lv_obj_send_event(card,LV_EVENT_SHORT_CLICKED,nullptr); + return;} + } // A finger let go within the edge band of the glass meant the slider's end (cyd::edge_snap). if(auto *indev=lv_indev_active();indev && lv_obj_get_width(slider)>=lv_obj_get_height(slider)){ lv_point_t p;lv_indev_get_point(indev,&p);lv_area_t a;lv_obj_get_coords(slider,&a); @@ -2247,6 +2272,11 @@ inline int content_width(const Widgets &w) { inline int content_height(const Widgets &w) { return tile_height(w) - lv_obj_get_style_space_top(w.tile, LV_PART_MAIN) - lv_obj_get_style_space_bottom(w.tile, LV_PART_MAIN); } +// A card's strip is thin (8 px on a CYD card), so the whole card belongs to it: a finger that lands above the strip +// still drags it. A press that never moves taps or holds the card instead (slider_event), so nothing is lost. +inline int slider_zone(const Widgets &w, int strip) { + return std::max(8, (int) lv_obj_get_style_space_top(w.tile, LV_PART_MAIN) + content_height(w) - strip); +} inline void bind(size_t index, lv_obj_t *tile, lv_obj_t *title, lv_obj_t *value, lv_obj_t *circle, lv_obj_t *icon) { lv_obj_update_layout(tile); // Compact cards need room for two text lines and a separate dimmer track. @@ -2975,7 +3005,7 @@ inline void render_full(Widgets &w,const Tile &t,bool custom,bool clock,bool sun // The small slider becomes a strip a thumb finds at the bottom; the room above it is the button. hide_extra(w);hide_panel(w); int strip=big?64:40; - lv_obj_set_size(w.slider,content_w,strip); + lv_obj_set_size(w.slider,content_w,strip);lv_obj_set_ext_click_area(w.slider,slider_zone(w,strip)); slider_handle(w.slider,content_w,strip); lv_obj_remove_flag(w.slider,LV_OBJ_FLAG_HIDDEN); if(!lv_obj_has_state(w.slider,LV_STATE_PRESSED) && lv_slider_get_value(w.slider)!=slider_value(t))lv_slider_set_value(w.slider,slider_value(t),LV_ANIM_OFF); @@ -3133,7 +3163,7 @@ inline void render_slot(size_t slot) { lv_obj_set_pos(w.unit,content_w-chevron_w,std::max(0,(header_height-chevron_w)/2));lv_obj_set_size(w.unit,chevron_w,chevron_w); lv_obj_remove_flag(w.unit,LV_OBJ_FLAG_HIDDEN); }else lv_obj_add_flag(w.unit,LV_OBJ_FLAG_HIDDEN); - lv_obj_set_size(w.slider,content_w,slider_height); + lv_obj_set_size(w.slider,content_w,slider_height);lv_obj_set_ext_click_area(w.slider,slider_zone(w,slider_height)); slider_handle(w.slider,content_w,slider_height); if(mini){lv_obj_remove_flag(w.slider,LV_OBJ_FLAG_HIDDEN);if(!lv_obj_has_state(w.slider,LV_STATE_PRESSED) && lv_slider_get_value(w.slider)!=slider_value(t))lv_slider_set_value(w.slider,slider_value(t),LV_ANIM_OFF);} else lv_obj_add_flag(w.slider,LV_OBJ_FLAG_HIDDEN); diff --git a/guition-4848s040.yaml b/guition-4848s040.yaml index 986507c..6c76d4f 100644 --- a/guition-4848s040.yaml +++ b/guition-4848s040.yaml @@ -2,7 +2,7 @@ # Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md. # A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile. substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.67" + SCREEN_FIRMWARE_VERSION: "0.2.68" # 0: no drift limit. A press that drifts is still a tap as long as the finger lets go on the tile # (runtime_tiles::event checks where it let go: LVGL keeps the press on the tile and would click it # anywhere), and a quick flick is LVGL's gesture, never a tap (the home_page gesture handler in on_boot). @@ -236,6 +236,8 @@ esphome: lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *) { if (!runtime_tiles::enabled || id(calibration_active) || !id(active_entity).empty() || settings_screen::visible() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN))) return; + // A quick drag on a slider is that slider's: the press stays with it (firmware 0.2.68+). + if (runtime_tiles::captured_slider) return; cyd::touch_guard.consume(); ESP_LOGI("touch", "flick: no tap"); if (id(display_dimmed)) return; diff --git a/home-like-2432s028.yaml b/home-like-2432s028.yaml index 2df31de..49ae77d 100644 --- a/home-like-2432s028.yaml +++ b/home-like-2432s028.yaml @@ -17,14 +17,15 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.67" + SCREEN_FIRMWARE_VERSION: "0.2.68" # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. TOUCH_MOVE_LIMIT_PX: "56" # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. TOUCH_MIN_PRESS_MS: "60" - # A slider let go this close to the left or right edge of the glass (about 1 cm) meant its end when - # that end lies in the band: a panel loses a finger swiped off the glass before the edge. - SLIDER_EDGE_SNAP_PX: "56" + # A slider let go this close to the left or right edge of the glass meant its end when that end lies in + # the band. The resistive panel reports a finger up to the bezel, so only the strip's last pixels and + # the margin beside them count (a capacitive Guition loses a fast finger 30-50 px before its edge). + SLIDER_EDGE_SNAP_PX: "20" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "cyd-new" DEVICE_FRIENDLY_NAME: "My CYD" @@ -327,8 +328,11 @@ esphome: auto *input=lv_indev_active();if(!input)return; auto direction=lv_indev_get_gesture_dir(input); if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return; + // A quick drag on a slider is that slider's, never a page swipe (firmware 0.2.68+). + if(runtime_tiles::captured_slider){ESP_LOGI("touch","page swipe ignored: a slider is being dragged");return;} cyd::touch_guard.consume(); lv_indev_wait_release(input); + ESP_LOGI("touch","page swipe: page %d -> %d",id(tile_page),id(tile_page)+(direction==LV_DIR_LEFT?1:-1)); id(tile_page)+=direction==LV_DIR_LEFT?1:-1; id(show_tile_page).execute(); }, LV_EVENT_GESTURE, nullptr); diff --git a/packages/cyd.yaml b/packages/cyd.yaml index b2e7598..acaf17b 100644 --- a/packages/cyd.yaml +++ b/packages/cyd.yaml @@ -18,14 +18,15 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.67" + SCREEN_FIRMWARE_VERSION: "0.2.68" # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. TOUCH_MOVE_LIMIT_PX: "56" # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. TOUCH_MIN_PRESS_MS: "60" - # A slider let go this close to the left or right edge of the glass (about 1 cm) meant its end when - # that end lies in the band: a panel loses a finger swiped off the glass before the edge. - SLIDER_EDGE_SNAP_PX: "56" + # A slider let go this close to the left or right edge of the glass meant its end when that end lies in + # the band. The resistive panel reports a finger up to the bezel, so only the strip's last pixels and + # the margin beside them count (a capacitive Guition loses a fast finger 30-50 px before its edge). + SLIDER_EDGE_SNAP_PX: "20" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "cyd-new" DEVICE_FRIENDLY_NAME: "My CYD" @@ -331,8 +332,11 @@ esphome: auto *input=lv_indev_active();if(!input)return; auto direction=lv_indev_get_gesture_dir(input); if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return; + // A quick drag on a slider is that slider's, never a page swipe (firmware 0.2.68+). + if(runtime_tiles::captured_slider){ESP_LOGI("touch","page swipe ignored: a slider is being dragged");return;} cyd::touch_guard.consume(); lv_indev_wait_release(input); + ESP_LOGI("touch","page swipe: page %d -> %d",id(tile_page),id(tile_page)+(direction==LV_DIR_LEFT?1:-1)); id(tile_page)+=direction==LV_DIR_LEFT?1:-1; id(show_tile_page).execute(); }, LV_EVENT_GESTURE, nullptr); diff --git a/packages/guition.yaml b/packages/guition.yaml index d55957b..2916b78 100644 --- a/packages/guition.yaml +++ b/packages/guition.yaml @@ -3,7 +3,7 @@ # Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md. # A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile. substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.67" + SCREEN_FIRMWARE_VERSION: "0.2.68" # 0: no drift limit. A press that drifts is still a tap as long as the finger lets go on the tile # (runtime_tiles::event checks where it let go: LVGL keeps the press on the tile and would click it # anywhere), and a quick flick is LVGL's gesture, never a tap (the home_page gesture handler in on_boot). @@ -240,6 +240,8 @@ esphome: lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *) { if (!runtime_tiles::enabled || id(calibration_active) || !id(active_entity).empty() || settings_screen::visible() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN))) return; + // A quick drag on a slider is that slider's: the press stays with it (firmware 0.2.68+). + if (runtime_tiles::captured_slider) return; cyd::touch_guard.consume(); ESP_LOGI("touch", "flick: no tap"); if (id(display_dimmed)) return; diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 2d5550e..4ffede7 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.2.81 (firmware 0.2.68) + +A card's slider takes the whole card, and a quick drag on it never turns a page. + +- **Dragging anywhere on a card with a small slider drags the slider.** The strip at the bottom of a compact card is 8 px high on a CYD, so a finger that landed a little above it pressed the card instead: the light's card opened, the light toggled, or a quick drag turned the page. The whole card now belongs to the strip, on both boards. A press that never moves is still the card's tap and a finger resting on it is still its hold, with the card lighting up as before; only a moving finger drags. The log says `Tap beside the strip of light.x: the tile's`. +- **A quick drag on a slider is never a page swipe.** The CYD's swipe between pages caught fast drags on sliders and turned the page without a word in the log. The log now says `page swipe: page 0 -> 1` when a swipe turns a page and `page swipe ignored: a slider is being dragged` when a slider had the finger. A swipe that starts on a card with a slider no longer turns the page: start it on another card, between cards, or use the buttons. The Guition's flick rule leaves a slider's drag alone the same way. +- **A second drag right after the first counts.** A slider's send within 600 ms of its previous one was dropped as a bounce, silently. That window is for buttons; a slider sends once per touch, on release. +- **The edge band on a CYD is 20 px** (was 56). Its resistive panel reports a finger up to the bezel, and on a single card's 131 px slider the old band turned a release at 71 % into 100 %. The Guition keeps its centimetre. +- Needs firmware 0.2.68: press **Update** on the screen. Includes everything from 0.2.80. CYD firmware: 1,653,504 bytes, 90.1 % of the update slot (720 bytes more than 0.2.80). + ## 0.2.80 (firmware 0.2.67) A slider swiped off the edge of the glass goes all the way. diff --git a/screen_manager/app/core.py b/screen_manager/app/core.py index 342cadb..e107b48 100644 --- a/screen_manager/app/core.py +++ b/screen_manager/app/core.py @@ -37,7 +37,7 @@ WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] REPO = 'https://github.com/MaxGramser/homeassistant_espscreen' REFS = {'cyd': 'main', 'guition': 'main'} # Firmware shipped with this app release; screens below it get an update offer. -FIRMWARE_VERSION = '0.2.67' +FIRMWARE_VERSION = '0.2.68' # The Auto standby switch a screen offers Home Assistant automations. AUTO_STANDBY_MIN_FIRMWARE = '0.2.41' # The settings page the screen opens itself, and the screen.settings tile that opens it. diff --git a/screen_manager/config.yaml b/screen_manager/config.yaml index 582ed68..684b7a9 100644 --- a/screen_manager/config.yaml +++ b/screen_manager/config.yaml @@ -1,5 +1,5 @@ name: ESP Screen Manager -version: "0.2.80" +version: "0.2.81" slug: esp_screen_manager description: Choose tiles for your CYD and Guition touchscreen, without reflashing. url: https://github.com/MaxGramser/homeassistant_espscreen diff --git a/tests/test_cyd_ui.cpp b/tests/test_cyd_ui.cpp index ec4a453..0f98e00 100644 --- a/tests/test_cyd_ui.cpp +++ b/tests/test_cyd_ui.cpp @@ -55,7 +55,7 @@ int main() { assert(!moving.accept_slider(1202, 202)); // one send per contact moving.begin(1250); assert(!moving.accept_slider(1260, 202)); // noise remains rejected - assert(!moving.accept_slider(1350, 202)); // same-control bounce remains rejected + assert(moving.accept_slider(1350, 202)); // a second drag 150 ms after the first commit counts (0.2.81) // Per-board limits (0.2.23): a centimetre of drift on the Guition is still a tap, and the // reference settles over the first samples, so the landing wobble does not count. cyd::TouchGuard wide; @@ -176,4 +176,11 @@ int main() { assert(cyd::edge_snap(20, 29, 450, 480, 67) == -1); // off the left: the start assert(cyd::edge_snap(20, 258, 452, 480, 67) == 0); // the right column's start is not in the band assert(cyd::edge_snap(437, 29, 450, 480, 0) == 0); // band off + // A second drag on the same slider right after the first counts (a slider sends once per contact, on release); + // a tap on the tile that soon after the slider's commit is still a bounce for the tile's own id only. + cyd::TouchGuard drags; + drags.begin(3000); assert(drags.accept_slider(3200, 205)); + drags.begin(3300); assert(drags.accept_slider(3500, 205)); // 300 ms after the last commit + drags.begin(3550); assert(drags.accept(3650, 105)); // the tile itself: another id + drags.begin(3700); assert(!drags.accept(3800, 105)); // the tile twice within 600 ms }