diff options
Diffstat (limited to 'src/ui/touch.c')
-rw-r--r-- | src/ui/touch.c | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/src/ui/touch.c b/src/ui/touch.c new file mode 100644 index 00000000..12cd1745 --- /dev/null +++ b/src/ui/touch.c | |||
@@ -0,0 +1,278 @@ | |||
1 | /* Copyright 2021 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #include "touch.h" | ||
24 | #include "window.h" | ||
25 | #include "app.h" | ||
26 | |||
27 | #include <the_Foundation/array.h> | ||
28 | #include <the_Foundation/math.h> | ||
29 | #include <SDL_timer.h> | ||
30 | |||
31 | iDeclareType(Touch) | ||
32 | iDeclareType(TouchState) | ||
33 | iDeclareType(Momentum) | ||
34 | |||
35 | struct Impl_Touch { | ||
36 | SDL_FingerID id; | ||
37 | iWidget *affinity; /* widget on which the touch started */ | ||
38 | iBool hasMoved; | ||
39 | uint32_t startTime; | ||
40 | iFloat3 startPos; | ||
41 | uint32_t posTime[2]; | ||
42 | iFloat3 pos[2]; | ||
43 | iFloat3 remainder; | ||
44 | }; | ||
45 | |||
46 | iLocalDef void pushPos_Touch_(iTouch *d, const iFloat3 pos, uint32_t time) { | ||
47 | d->posTime[1] = d->posTime[0]; | ||
48 | d->posTime[0] = time; | ||
49 | d->pos[1] = d->pos[0]; | ||
50 | d->pos[0] = pos; | ||
51 | } | ||
52 | |||
53 | struct Impl_Momentum { | ||
54 | iWidget *affinity; | ||
55 | uint32_t releaseTime; | ||
56 | iFloat3 velocity; | ||
57 | iFloat3 accum; | ||
58 | }; | ||
59 | |||
60 | struct Impl_TouchState { | ||
61 | iArray *touches; | ||
62 | iArray *moms; | ||
63 | double lastMomTime; | ||
64 | }; | ||
65 | |||
66 | static iTouchState *touchState_(void) { | ||
67 | static iTouchState state_; | ||
68 | iTouchState *d = &state_; | ||
69 | if (!d->touches) { | ||
70 | d->touches = new_Array(sizeof(iTouch)); | ||
71 | d->moms = new_Array(sizeof(iMomentum)); | ||
72 | d->lastMomTime = SDL_GetTicks(); | ||
73 | } | ||
74 | return d; | ||
75 | } | ||
76 | |||
77 | static iTouch *find_TouchState_(iTouchState *d, SDL_FingerID id) { | ||
78 | iConstForEach(Array, i, d->touches) { | ||
79 | iTouch *touch = (iTouch *) i.value; | ||
80 | if (touch->id == id) { | ||
81 | return touch; | ||
82 | } | ||
83 | } | ||
84 | return NULL; | ||
85 | } | ||
86 | |||
87 | static uint32_t longPressSpanMs_ = 500; | ||
88 | static int tapRadiusPt_ = 15; | ||
89 | |||
90 | static iBool isStationary_Touch_(const iTouch *d) { | ||
91 | return !d->hasMoved && | ||
92 | length_F3(sub_F3(d->pos[0], d->startPos)) < tapRadiusPt_ * get_Window()->pixelRatio; | ||
93 | } | ||
94 | |||
95 | static void dispatchClick_Touch_(const iTouch *d, int button) { | ||
96 | const iFloat3 tapPos = divf_F3(add_F3(d->pos[0], d->startPos), 2); | ||
97 | SDL_MouseButtonEvent btn = { | ||
98 | .type = SDL_MOUSEBUTTONDOWN, | ||
99 | .button = button, | ||
100 | .clicks = 1, | ||
101 | .state = SDL_PRESSED, | ||
102 | .timestamp = SDL_GetTicks(), | ||
103 | .which = SDL_TOUCH_MOUSEID, | ||
104 | .x = x_F3(tapPos), | ||
105 | .y = y_F3(tapPos) | ||
106 | }; | ||
107 | dispatchEvent_Widget(get_Window()->root, (SDL_Event *) &btn); | ||
108 | /* Immediately released, too. */ | ||
109 | btn.type = SDL_MOUSEBUTTONUP; | ||
110 | btn.state = SDL_RELEASED; | ||
111 | btn.timestamp = SDL_GetTicks(); | ||
112 | dispatchEvent_Widget(get_Window()->root, (SDL_Event *) &btn); | ||
113 | } | ||
114 | |||
115 | static void clearWidgetMomentum_TouchState_(iTouchState *d, iWidget *widget) { | ||
116 | if (!widget) return; | ||
117 | iForEach(Array, m, d->moms) { | ||
118 | iMomentum *mom = m.value; | ||
119 | if (mom->affinity == widget) { | ||
120 | remove_ArrayIterator(&m); | ||
121 | } | ||
122 | } | ||
123 | } | ||
124 | |||
125 | static void update_TouchState_(void *ptr) { | ||
126 | iTouchState *d = ptr; | ||
127 | const uint32_t nowTime = SDL_GetTicks(); | ||
128 | /* Check for long presses to simulate right clicks. */ | ||
129 | iForEach(Array, i, d->touches) { | ||
130 | iTouch *touch = i.value; | ||
131 | /* Holding a touch will reset previous momentum for this widget. */ | ||
132 | if (isStationary_Touch_(touch)) { | ||
133 | if (nowTime - touch->startTime > 25) { | ||
134 | clearWidgetMomentum_TouchState_(d, touch->affinity); | ||
135 | } | ||
136 | if (nowTime - touch->startTime >= longPressSpanMs_ && touch->affinity) { | ||
137 | dispatchClick_Touch_(touch, SDL_BUTTON_RIGHT); | ||
138 | remove_ArrayIterator(&i); | ||
139 | } | ||
140 | } | ||
141 | } | ||
142 | /* Update/cancel momentum scrolling. */ { | ||
143 | const float minSpeed = 10.0f; | ||
144 | const float momFriction = 0.975f; | ||
145 | const float stepDurationMs = 1000.0f / 120.0f; | ||
146 | double momAvailMs = nowTime - d->lastMomTime; | ||
147 | int numSteps = iMin((int) (momAvailMs / stepDurationMs), 10); | ||
148 | d->lastMomTime += numSteps * stepDurationMs; | ||
149 | // printf("mom steps:%d\n", numSteps); | ||
150 | iForEach(Array, m, d->moms) { | ||
151 | if (numSteps == 0) break; | ||
152 | iMomentum *mom = m.value; | ||
153 | for (int step = 0; step < numSteps; step++) { | ||
154 | mulvf_F3(&mom->velocity, momFriction); | ||
155 | addv_F3(&mom->accum, mulf_F3(mom->velocity, stepDurationMs / 1000.0f)); | ||
156 | } | ||
157 | const iInt2 pixels = initF3_I2(mom->accum); | ||
158 | if (pixels.x || pixels.y) { | ||
159 | subv_F3(&mom->accum, initI2_F3(pixels)); | ||
160 | dispatchEvent_Widget(mom->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){ | ||
161 | .type = SDL_MOUSEWHEEL, | ||
162 | .timestamp = SDL_GetTicks(), | ||
163 | .which = 0, /* means "precise scrolling" in DocumentWidget */ | ||
164 | .x = pixels.x, | ||
165 | .y = pixels.y | ||
166 | }); | ||
167 | } | ||
168 | //printf("mom vel:%f\n", length_F3(mom->velocity)); | ||
169 | if (length_F3(mom->velocity) < minSpeed) { | ||
170 | remove_ArrayIterator(&m); | ||
171 | } | ||
172 | } | ||
173 | } | ||
174 | /* Keep updating if interaction is still ongoing. */ | ||
175 | if (!isEmpty_Array(d->touches) || !isEmpty_Array(d->moms)) { | ||
176 | addTicker_App(update_TouchState_, ptr); | ||
177 | } | ||
178 | } | ||
179 | |||
180 | iBool processEvent_Touch(const SDL_Event *ev) { | ||
181 | /* We only handle finger events here. */ | ||
182 | if (ev->type != SDL_FINGERDOWN && ev->type != SDL_FINGERMOTION && ev->type != SDL_FINGERUP) { | ||
183 | return iFalse; | ||
184 | } | ||
185 | iTouchState *d = touchState_(); | ||
186 | const SDL_TouchFingerEvent *fing = &ev->tfinger; | ||
187 | iWindow *window = get_Window(); | ||
188 | const iInt2 rootSize = rootSize_Window(window); | ||
189 | const iFloat3 pos = init_F3(fing->x * rootSize.x, fing->y * rootSize.y, 0); | ||
190 | //printf("%2d: %f: touch %f, %f\n", ev->type, z_F3(pos), x_F3(pos), y_F3(pos)); | ||
191 | //fflush(stdout); | ||
192 | const uint32_t nowTime = SDL_GetTicks(); | ||
193 | if (ev->type == SDL_FINGERDOWN) { | ||
194 | /* Register the new touch. */ | ||
195 | iWidget *aff = hitChild_Widget(window->root, init_I2(iRound(x_F3(pos)), iRound(y_F3(pos)))); | ||
196 | pushBack_Array(d->touches, &(iTouch){ | ||
197 | .id = fing->fingerId, | ||
198 | .affinity = aff, | ||
199 | .startTime = nowTime, | ||
200 | .startPos = pos, | ||
201 | .pos = pos | ||
202 | }); | ||
203 | /* Some widgets rely on hover state. */ | ||
204 | dispatchEvent_Widget(window->root, (SDL_Event *) &(SDL_MouseMotionEvent){ | ||
205 | .type = SDL_MOUSEMOTION, | ||
206 | .timestamp = SDL_GetTicks(), | ||
207 | .which = SDL_TOUCH_MOUSEID, | ||
208 | .x = x_F3(pos), | ||
209 | .y = y_F3(pos) | ||
210 | }); | ||
211 | addTicker_App(update_TouchState_, d); | ||
212 | } | ||
213 | else if (ev->type == SDL_FINGERMOTION) { | ||
214 | iTouch *touch = find_TouchState_(d, fing->fingerId); | ||
215 | if (touch && touch->affinity) { | ||
216 | /* TODO: Update touch position. */ | ||
217 | const iFloat3 amount = add_F3(touch->remainder, | ||
218 | divf_F3(mul_F3(init_F3(fing->dx, fing->dy, 0), | ||
219 | init_F3(rootSize.x, rootSize.y, 0)), | ||
220 | window->pixelRatio)); | ||
221 | const iInt2 pixels = init_I2(iRound(x_F3(amount)), iRound(y_F3(amount))); | ||
222 | iFloat3 remainder = sub_F3(amount, initI2_F3(pixels)); | ||
223 | touch->remainder = remainder; | ||
224 | pushPos_Touch_(touch, pos, nowTime); | ||
225 | if (!touch->hasMoved && !isStationary_Touch_(touch)) { | ||
226 | touch->hasMoved = iTrue; | ||
227 | } | ||
228 | if (pixels.x || pixels.y) { | ||
229 | // printf("%p (%s) wy: %f\n", touch->affinity, class_Widget(touch->affinity)->name, | ||
230 | // fing->dy * rootSize.y / window->pixelRatio); | ||
231 | dispatchEvent_Widget(touch->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){ | ||
232 | .type = SDL_MOUSEWHEEL, | ||
233 | .timestamp = SDL_GetTicks(), | ||
234 | .which = 0, /* means "precise scrolling" in DocumentWidget */ | ||
235 | .x = pixels.x, | ||
236 | .y = pixels.y | ||
237 | }); | ||
238 | /* TODO: Keep increasing movement if the direction is the same. */ | ||
239 | clearWidgetMomentum_TouchState_(d, touch->affinity); | ||
240 | } | ||
241 | } | ||
242 | } | ||
243 | else if (ev->type == SDL_FINGERUP) { | ||
244 | iTouch *touch = find_TouchState_(d, fing->fingerId); | ||
245 | iForEach(Array, i, d->touches) { | ||
246 | iTouch *touch = i.value; | ||
247 | if (touch->id != fing->fingerId) { | ||
248 | continue; | ||
249 | } | ||
250 | const uint32_t elapsed = nowTime - touch->posTime[1]; | ||
251 | iFloat3 velocity = zero_F3(); | ||
252 | if (elapsed < 50) { | ||
253 | velocity = divf_F3(sub_F3(pos, touch->pos[1]), (float) elapsed / 1000.0f); | ||
254 | } | ||
255 | pushPos_Touch_(touch, pos, nowTime); | ||
256 | iBool wasUsed = iFalse; | ||
257 | const uint32_t duration = nowTime - touch->startTime; | ||
258 | /* If short and didn't move far, do a tap (left click). */ | ||
259 | if (duration < longPressSpanMs_ && isStationary_Touch_(touch)) { | ||
260 | dispatchClick_Touch_(touch, SDL_BUTTON_LEFT); | ||
261 | } | ||
262 | else if (length_F3(velocity) > 10.0f) { | ||
263 | clearWidgetMomentum_TouchState_(d, touch->affinity); | ||
264 | iMomentum mom = { | ||
265 | .affinity = touch->affinity, | ||
266 | .releaseTime = nowTime, | ||
267 | .velocity = velocity | ||
268 | }; | ||
269 | if (isEmpty_Array(d->moms)) { | ||
270 | d->lastMomTime = nowTime; | ||
271 | } | ||
272 | pushBack_Array(d->moms, &mom); | ||
273 | } | ||
274 | remove_ArrayIterator(&i); | ||
275 | } | ||
276 | } | ||
277 | return iTrue; | ||
278 | } | ||