Skip to content

Commit 9999f0a

Browse files
committed
feat: add anti-ghost-touch validation to confirmation slider.
1 parent ee619ab commit 9999f0a

File tree

1 file changed

+118
-27
lines changed
  • core/src/trezor/lvglui/scrs/components

1 file changed

+118
-27
lines changed

core/src/trezor/lvglui/scrs/components/slider.py

Lines changed: 118 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import utime
2+
13
from trezor import motor
24
from trezor.lvglui.i18n import gettext as _, keys as i18n_keys
35

@@ -6,6 +8,13 @@
68

79
MAX_VISIBLE_VALUE = 175
810
MIN_VISIBLE_VALUE = 20
11+
START_VALUE_TOLERANCE = 5
12+
# slider 456px, knob 82px, pad 16px => track ~358px, range 155 units => ~2.3px/unit
13+
MAX_FORWARD_JUMP = 60 # ~138px/frame
14+
MIN_PROGRESS_STEPS = 3 # normal slide produces 15-60 events
15+
MAX_DRAG_DURATION_MS = 3000 # max 3s per drag, blocks slow random touch accumulation
16+
TOUCH_BOUNDS_MARGIN_HORIZONTAL_PX = 12
17+
TOUCH_BOUNDS_MARGIN_VERTICAL_PX = 36
918

1019

1120
class Slider(lv.slider):
@@ -76,6 +85,18 @@ def __init__(self, parent, text, relative_y=-114) -> None:
7685
self.add_event_cb(self.on_event, lv.EVENT.DRAW_PART_BEGIN, None)
7786
self.add_event_cb(self.on_event, lv.EVENT.DRAW_PART_END, None)
7887

88+
self._drag_active = False
89+
self._start_value = MIN_VISIBLE_VALUE
90+
self._last_value = MIN_VISIBLE_VALUE
91+
self._reached_end = False
92+
self._progress_count = 0
93+
self._invalid_jump = False
94+
self._passed = False
95+
self._drag_start_ms = 0
96+
self._blocked_until_release = False
97+
self._touch_point = lv.point_t()
98+
self._slider_area = lv.area_t()
99+
79100
def enable(self, enable: bool = True):
80101
if enable:
81102
self.disable = False
@@ -120,41 +141,111 @@ def change_knob_style(self, level):
120141
lv.PART.KNOB | lv.STATE.DEFAULT,
121142
)
122143

144+
def _clamp_value(self, value):
145+
if value > MAX_VISIBLE_VALUE:
146+
self.set_value(MAX_VISIBLE_VALUE, lv.ANIM.OFF)
147+
return MAX_VISIBLE_VALUE
148+
if value < MIN_VISIBLE_VALUE:
149+
self.set_value(MIN_VISIBLE_VALUE, lv.ANIM.OFF)
150+
return MIN_VISIBLE_VALUE
151+
return value
152+
153+
def _reset_drag_state(self, value):
154+
self._drag_active = True
155+
self._start_value = value
156+
self._last_value = value
157+
self._reached_end = value >= MAX_VISIBLE_VALUE
158+
self._progress_count = 0
159+
self._invalid_jump = False
160+
self._passed = False
161+
self._drag_start_ms = utime.ticks_ms()
162+
self._blocked_until_release = False
163+
164+
def _can_pass(self, current_value):
165+
return (
166+
self._drag_active
167+
and self._start_value <= MIN_VISIBLE_VALUE + START_VALUE_TOLERANCE
168+
and current_value >= MAX_VISIBLE_VALUE
169+
and self._reached_end
170+
and self._progress_count >= MIN_PROGRESS_STEPS
171+
and not self._invalid_jump
172+
and utime.ticks_diff(utime.ticks_ms(), self._drag_start_ms)
173+
<= MAX_DRAG_DURATION_MS
174+
)
175+
176+
def _abort_drag(self):
177+
self._drag_active = False
178+
self._blocked_until_release = True
179+
self._invalid_jump = True
180+
self._passed = False
181+
self.tips.add_flag(lv.obj.FLAG.HIDDEN)
182+
self.set_value(MIN_VISIBLE_VALUE, lv.ANIM.ON)
183+
indev = lv.indev_get_act()
184+
if indev:
185+
indev.wait_release()
186+
123187
def on_event(self, event):
124188
code = event.code
125-
target = event.get_target()
126-
current_value = target.get_value()
127-
if code == lv.EVENT.PRESSED:
189+
current_value = event.get_target().get_value()
190+
if code == lv.EVENT.PRESSING:
191+
current_value = self._clamp_value(current_value)
192+
if self._blocked_until_release:
193+
return
194+
if not self._drag_active:
195+
return
196+
# timeout: reset slider immediately instead of waiting for RELEASED
197+
if utime.ticks_diff(utime.ticks_ms(), self._drag_start_ms) > MAX_DRAG_DURATION_MS:
198+
self._abort_drag()
199+
return
200+
# check if touch is within slider component bounds (not knob-only hit_test)
201+
indev = lv.indev_get_act()
202+
if indev:
203+
indev.get_point(self._touch_point)
204+
self.get_coords(self._slider_area)
205+
if (
206+
self._touch_point.x < self._slider_area.x1 - TOUCH_BOUNDS_MARGIN_HORIZONTAL_PX
207+
or self._touch_point.x > self._slider_area.x2 + TOUCH_BOUNDS_MARGIN_HORIZONTAL_PX
208+
or self._touch_point.y < self._slider_area.y1 - TOUCH_BOUNDS_MARGIN_VERTICAL_PX
209+
or self._touch_point.y > self._slider_area.y2 + TOUCH_BOUNDS_MARGIN_VERTICAL_PX
210+
):
211+
self._abort_drag()
212+
return
213+
delta = current_value - self._last_value
214+
if delta > 0:
215+
self._progress_count += 1
216+
if delta > MAX_FORWARD_JUMP:
217+
self._invalid_jump = True
218+
self._last_value = current_value
219+
if current_value >= MAX_VISIBLE_VALUE:
220+
self._reached_end = True
221+
elif code == lv.EVENT.PRESSED:
128222
motor.vibrate(motor.WHISPER)
129-
elif code == lv.EVENT.PRESSING:
130-
if current_value > MAX_VISIBLE_VALUE:
131-
self.set_value(MAX_VISIBLE_VALUE, lv.ANIM.OFF)
132-
elif current_value < MIN_VISIBLE_VALUE:
133-
self.set_value(MIN_VISIBLE_VALUE, lv.ANIM.OFF)
223+
current_value = self._clamp_value(current_value)
224+
self._reset_drag_state(current_value)
134225
elif code == lv.EVENT.RELEASED:
135-
if current_value < MAX_VISIBLE_VALUE:
226+
self._blocked_until_release = False
227+
self._passed = self._can_pass(current_value)
228+
if self._passed:
229+
self.tips.clear_flag(lv.obj.FLAG.HIDDEN)
230+
if self.has_flag(lv.obj.FLAG.CLICKABLE):
231+
self.clear_flag(lv.obj.FLAG.CLICKABLE)
232+
motor.vibrate(motor.SUCCESS)
233+
lv.event_send(self, lv.EVENT.READY, None)
234+
else:
136235
motor.vibrate(motor.ERROR)
137236
self.set_value(MIN_VISIBLE_VALUE, lv.ANIM.ON)
237+
self.tips.add_flag(lv.obj.FLAG.HIDDEN)
238+
self._drag_active = False
138239
elif code == lv.EVENT.DRAW_PART_BEGIN:
139240
dsc = lv.obj_draw_part_dsc_t.__cast__(event.get_param())
140-
if dsc.part == lv.PART.KNOB:
141-
if dsc.id == 0:
142-
if current_value < MAX_VISIBLE_VALUE:
143-
# if self.disable:
144-
# dsc.rect_dsc.bg_img_src = (
145-
# Slider.SLIDER_DISABLE_ARROW_IMG_SRC
146-
# )
147-
# else:
148-
dsc.rect_dsc.bg_img_src = self.arrow_img_src
149-
else:
150-
self.tips.clear_flag(lv.obj.FLAG.HIDDEN)
151-
dsc.rect_dsc.bg_img_src = self.done_img_src
152-
if self.has_flag(lv.obj.FLAG.CLICKABLE):
153-
self.clear_flag(lv.obj.FLAG.CLICKABLE)
154-
else:
155-
return
156-
motor.vibrate(motor.SUCCESS)
157-
lv.event_send(self, lv.EVENT.READY, None)
241+
if dsc.part == lv.PART.KNOB and dsc.id == 0:
242+
show_done = self._passed or self._can_pass(current_value)
243+
if show_done:
244+
self.tips.clear_flag(lv.obj.FLAG.HIDDEN)
245+
dsc.rect_dsc.bg_img_src = self.done_img_src
246+
else:
247+
self.tips.add_flag(lv.obj.FLAG.HIDDEN)
248+
dsc.rect_dsc.bg_img_src = self.arrow_img_src
158249
elif code == lv.EVENT.DRAW_PART_END:
159250
dsc = lv.obj_draw_part_dsc_t.__cast__(event.get_param())
160251
if dsc.part == lv.PART.MAIN:

0 commit comments

Comments
 (0)