Skip to content

Commit 559c1c8

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

File tree

1 file changed

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

1 file changed

+125
-27
lines changed

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

Lines changed: 125 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,118 @@ 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 (
198+
utime.ticks_diff(utime.ticks_ms(), self._drag_start_ms)
199+
> MAX_DRAG_DURATION_MS
200+
):
201+
self._abort_drag()
202+
return
203+
# check if touch is within slider component bounds (not knob-only hit_test)
204+
indev = lv.indev_get_act()
205+
if indev:
206+
indev.get_point(self._touch_point)
207+
self.get_coords(self._slider_area)
208+
if (
209+
self._touch_point.x
210+
< self._slider_area.x1 - TOUCH_BOUNDS_MARGIN_HORIZONTAL_PX
211+
or self._touch_point.x
212+
> self._slider_area.x2 + TOUCH_BOUNDS_MARGIN_HORIZONTAL_PX
213+
or self._touch_point.y
214+
< self._slider_area.y1 - TOUCH_BOUNDS_MARGIN_VERTICAL_PX
215+
or self._touch_point.y
216+
> self._slider_area.y2 + TOUCH_BOUNDS_MARGIN_VERTICAL_PX
217+
):
218+
self._abort_drag()
219+
return
220+
delta = current_value - self._last_value
221+
if delta > 0:
222+
self._progress_count += 1
223+
if delta > MAX_FORWARD_JUMP:
224+
self._invalid_jump = True
225+
self._last_value = current_value
226+
if current_value >= MAX_VISIBLE_VALUE:
227+
self._reached_end = True
228+
elif code == lv.EVENT.PRESSED:
128229
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)
230+
current_value = self._clamp_value(current_value)
231+
self._reset_drag_state(current_value)
134232
elif code == lv.EVENT.RELEASED:
135-
if current_value < MAX_VISIBLE_VALUE:
233+
self._blocked_until_release = False
234+
self._passed = self._can_pass(current_value)
235+
if self._passed:
236+
self.tips.clear_flag(lv.obj.FLAG.HIDDEN)
237+
if self.has_flag(lv.obj.FLAG.CLICKABLE):
238+
self.clear_flag(lv.obj.FLAG.CLICKABLE)
239+
motor.vibrate(motor.SUCCESS)
240+
lv.event_send(self, lv.EVENT.READY, None)
241+
else:
136242
motor.vibrate(motor.ERROR)
137243
self.set_value(MIN_VISIBLE_VALUE, lv.ANIM.ON)
244+
self.tips.add_flag(lv.obj.FLAG.HIDDEN)
245+
self._drag_active = False
138246
elif code == lv.EVENT.DRAW_PART_BEGIN:
139247
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)
248+
if dsc.part == lv.PART.KNOB and dsc.id == 0:
249+
show_done = self._passed or self._can_pass(current_value)
250+
if show_done:
251+
self.tips.clear_flag(lv.obj.FLAG.HIDDEN)
252+
dsc.rect_dsc.bg_img_src = self.done_img_src
253+
else:
254+
self.tips.add_flag(lv.obj.FLAG.HIDDEN)
255+
dsc.rect_dsc.bg_img_src = self.arrow_img_src
158256
elif code == lv.EVENT.DRAW_PART_END:
159257
dsc = lv.obj_draw_part_dsc_t.__cast__(event.get_param())
160258
if dsc.part == lv.PART.MAIN:

0 commit comments

Comments
 (0)