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 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-18 20:08:26 +02:00
parent e928ee4404
commit b149428dcb
10 changed files with 86 additions and 25 deletions
+11 -9
View File
@@ -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; }
+33 -3
View File
@@ -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.x<a.x1 || slider_press.x>a.x2 || slider_press.y<a.y1 || slider_press.y>a.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);
+3 -1
View File
@@ -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;
+8 -4
View File
@@ -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);
+8 -4
View File
@@ -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);
+3 -1
View File
@@ -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;
+10
View File
@@ -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.
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
+8 -1
View File
@@ -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
}