11/* SPDX-License-Identifier: GPL-3.0-or-later
22 * Copyright (C) 2026 Mateusz Szuła
33 *
4- * src/platform/android/touch_overlay.c — Android on-screen touch overlay
5- * (see include/wacki/platform/android_touch.h).
4+ * src/platform/android/touch_overlay.c — Android on-screen touch overlay +
5+ * canvas touch mapping (see include/wacki/platform/android_touch.h).
66 *
7- * Fills the letterbox pillarbox bars with a virtual joystick (left, drives the
8- * cursor) + left/right click buttons (right), semi-transparent. The GAME AREA
9- * is NOT handled here — SDL's built-in touch→mouse synthesis maps those touches
10- * through the renderer's real present transform (exact on every device). This
11- * module only claims the control zones in the bars and, while a finger is on a
12- * control, tells the SDL layer to suppress the synth there
13- * (wacki_overlay_owns_touch) so a bar touch doesn't also drag/click the cursor .
7+ * Owns ALL touch on Android. Measured on-device (BlueStacks): the app's touch
8+ * surface is the GAME WINDOW itself — the emulator draws the letterbox bars but
9+ * they're outside the touch area, so a touch normalizes to the canvas. Hence
10+ * logical = nx·640 / ny·480 (NOT SDL's window-letterbox transform, which is
11+ * what made clicks drift). Game-area touch → cursor + tap-click; the joystick +
12+ * buttons (hugging the canvas edges, the only touchable spot — the bars can't
13+ * host controls) drive the cursor / latches .
1414 *
15- * Geometry is in WINDOW pixels (where SDL_FINGER* events are normalized);
16- * drawing happens after the logical-size letterbox is disabled, in renderer-
17- * OUTPUT pixels, scaled by output/window. */
15+ * NOTE: this assumes touch is normalized to the canvas, which holds on the SDL
16+ * Android surface here. On a device where the surface includes the letterbox
17+ * bars the canvas-edge controls + mapping would need the window transform — see
18+ * git history for the SDL_RenderWindowToLogical variant. */
1819
1920#include "wacki.h" /* g_mouse_x/y, g_lmb_clicked, g_rmb_clicked, WACKI_SCREEN_* */
2021#include "wacki/platform/android_touch.h"
2122
2223#include <SDL.h>
2324#include <math.h>
2425
25- /* ---- tunables ------------------------------------------------ ---- */
26- #define BAR_MIN_PX 96 /* hide controls if a side bar is narrower */
27- #define STICK_BAR_FRAC 0.42f
28- #define STICK_H_FRAC 0.17f
29- #define STICK_CY_FRAC 0.60f
30- #define KNOB_FRAC 0.50f
31- #define LMB_BAR_FRAC 0.42f
32- #define LMB_H_FRAC 0.16f
33- #define LMB_CY_FRAC 0.62f
34- #define RMB_FRAC 0.64f
35- #define STICK_DEADZONE 0.18f
36- #define STICK_SPEED 7.0f /* cursor px/frame at full deflection */
37-
38- /* overlay paint (RGBA, low alpha = subtle over the black bars) */
39- #define A_STICK_BASE 40
40- #define A_STICK_KNOB 95
41- #define A_LMB 64
42- #define A_RMB 48
43-
44- /* ---- geometry (WINDOW pixels), cached on draw ------------------- */
45- static int s_win_w = 0 , s_win_h = 0 ;
46- static float s_kx = 1.0f , s_ky = 1.0f ; /* window→output draw scale */
26+ /* ---- control geometry, in LOGICAL (640×480) px — edge-hugging ---- */
27+ #define STICK_CX 48
28+ #define STICK_CY 240
29+ #define STICK_R 44
30+ #define KNOB_R 22
31+ #define LMB_CX 592
32+ #define LMB_CY 280
33+ #define LMB_R 42
34+ #define RMB_CX 592
35+ #define RMB_CY 190
36+ #define RMB_R 27
37+
38+ #define STICK_DEADZONE 0.18f
39+ #define STICK_SPEED 6.0f
40+ #define TAP_MS 350u
41+ #define TAP_SLOP 10 /* logical px */
42+
43+ #define A_STICK_BASE 46
44+ #define A_STICK_KNOB 100
45+ #define A_LMB 70
46+ #define A_RMB 54
47+
4748static int s_have_geom = 0 ;
48- static int s_gx0 , s_gw ; /* canvas rect (window px) */
49- static int s_controls_on = 0 ;
50- static int s_stick_cx , s_stick_cy , s_stick_r ;
51- static int s_lmb_cx , s_lmb_cy , s_lmb_r ;
52- static int s_rmb_cx , s_rmb_cy , s_rmb_r ;
53-
54- /* ---- virtual stick + per-finger state --------------------------- */
55- static float s_def_x = 0.0f , s_def_y = 0.0f ; /* deflection -1..1 */
49+ static float s_def_x = 0.0f , s_def_y = 0.0f ;
5650static int s_stick_on = 0 ;
57- static float s_cur_x = 0.0f , s_cur_y = 0.0f ; /* sub-pixel cursor mirror */
58- static int s_control_fingers = 0 ; /* fingers on a control zone */
59- static int s_mark_px = -1 , s_mark_py = -1 ; /* DEBUG: last touch (window px) */
51+ static float s_cur_x = 0.0f , s_cur_y = 0.0f ;
6052
61- enum { ROLE_NONE = 0 , ROLE_STICK , ROLE_LMB , ROLE_RMB };
62- typedef struct { SDL_FingerID id ; int used ; int role ; } Finger ;
53+ enum { ROLE_NONE = 0 , ROLE_STICK , ROLE_LMB , ROLE_RMB , ROLE_GAME };
54+ typedef struct {
55+ SDL_FingerID id ; int used ; int role ;
56+ uint32_t t0 ; int sx , sy , moved ; /* ROLE_GAME tap detection */
57+ } Finger ;
6358#define MAX_FINGERS 8
6459static Finger s_fingers [MAX_FINGERS ];
6560
66- /* ---- helpers ----------------------------------------------------- */
6761static int clampi (int v , int lo , int hi ){ return v < lo ? lo : (v > hi ? hi : v ); }
6862
6963static Finger * finger_get (SDL_FingerID id , int create )
@@ -75,62 +69,35 @@ static Finger *finger_get(SDL_FingerID id, int create)
7569 }
7670 if (create && free_slot ) {
7771 free_slot -> used = 1 ; free_slot -> id = id ; free_slot -> role = ROLE_NONE ;
72+ free_slot -> moved = 0 ;
7873 return free_slot ;
7974 }
8075 return NULL ;
8176}
8277
83- static int in_circle (int px , int py , int cx , int cy , int r )
78+ static int in_circle (int x , int y , int cx , int cy , int r )
8479{
85- long dx = px - cx , dy = py - cy ;
80+ long dx = x - cx , dy = y - cy ;
8681 return dx * dx + dy * dy <= (long )r * r ;
8782}
8883
89- static void recompute ( SDL_Renderer * ren , int ww , int wh , int ow , int oh )
84+ static void to_logical ( float nx , float ny , int * lx , int * ly )
9085{
91- s_win_w = ww ; s_win_h = wh ;
92- s_kx = ww > 0 ? (float )ow / ww : 1.0f ;
93- s_ky = wh > 0 ? (float )oh / wh : 1.0f ;
94-
95- /* Canvas rect in WINDOW px, from SDL's present transform. */
96- int ax = 0 , ay = 0 , bx = 0 , by = 0 ;
97- SDL_RenderLogicalToWindow (ren , 0.0f , 0.0f , & ax , & ay );
98- SDL_RenderLogicalToWindow (ren , (float )WACKI_SCREEN_W , (float )WACKI_SCREEN_H , & bx , & by );
99- s_gx0 = ax ; s_gw = bx - ax ;
100-
101- int left_bar = s_gx0 ;
102- int right_bar = ww - (s_gx0 + s_gw );
103- s_controls_on = (left_bar >= BAR_MIN_PX && right_bar >= BAR_MIN_PX );
104-
105- s_stick_r = (int )((left_bar * STICK_BAR_FRAC < wh * STICK_H_FRAC )
106- ? left_bar * STICK_BAR_FRAC : wh * STICK_H_FRAC );
107- s_stick_cx = left_bar / 2 ;
108- s_stick_cy = (int )(wh * STICK_CY_FRAC );
109-
110- int rcx = s_gx0 + s_gw + right_bar / 2 ;
111- int lr = (int )((right_bar * LMB_BAR_FRAC < wh * LMB_H_FRAC )
112- ? right_bar * LMB_BAR_FRAC : wh * LMB_H_FRAC );
113- s_lmb_r = lr ;
114- s_lmb_cx = rcx ;
115- s_lmb_cy = (int )(wh * LMB_CY_FRAC );
116- s_rmb_r = (int )(lr * RMB_FRAC );
117- s_rmb_cx = rcx ;
118- s_rmb_cy = s_lmb_cy - s_lmb_r - s_rmb_r - (int )(wh * 0.03f );
119-
120- s_have_geom = 1 ;
86+ int x = (int )(nx * WACKI_SCREEN_W ), y = (int )(ny * WACKI_SCREEN_H );
87+ * lx = clampi (x , 0 , WACKI_SCREEN_W - 1 );
88+ * ly = clampi (y , 0 , WACKI_SCREEN_H - 1 );
12189}
12290
123- static void stick_set (int px , int py )
91+ static void stick_set (int lx , int ly )
12492{
125- if (s_stick_r <= 0 ) return ;
126- float dx = (float )(px - s_stick_cx ) / s_stick_r ;
127- float dy = (float )(py - s_stick_cy ) / s_stick_r ;
93+ float dx = (float )(lx - STICK_CX ) / STICK_R ;
94+ float dy = (float )(ly - STICK_CY ) / STICK_R ;
12895 float m = sqrtf (dx * dx + dy * dy );
12996 if (m > 1.0f ) { dx /= m ; dy /= m ; }
13097 s_def_x = dx ; s_def_y = dy ;
13198}
13299
133- /* ---- drawing ------------------------------------------------- --- */
100+ /* ---- drawing (logical coords; logical-size letterbox stays active) --- */
134101static void fill_circle (SDL_Renderer * ren , int cx , int cy , int r , Uint8 A )
135102{
136103 if (r <= 0 ) return ;
@@ -140,97 +107,78 @@ static void fill_circle(SDL_Renderer *ren, int cx, int cy, int r, Uint8 A)
140107 SDL_RenderDrawLine (ren , cx - dx , cy + dy , cx + dx , cy + dy );
141108 }
142109}
143- static void draw_control (SDL_Renderer * ren , int cx , int cy , int r , Uint8 A )
144- {
145- fill_circle (ren , (int )(cx * s_kx ), (int )(cy * s_ky ), (int )(r * s_kx ), A );
146- }
147110
148111void wacki_overlay_draw (SDL_Renderer * ren )
149112{
150113 if (!ren ) return ;
151- int ow = 0 , oh = 0 , ww = 0 , wh = 0 ;
152- SDL_GetRendererOutputSize (ren , & ow , & oh );
153- SDL_Window * win = SDL_RenderGetWindow (ren );
154- if (win ) SDL_GetWindowSize (win , & ww , & wh );
155- if (ww <= 0 || wh <= 0 ) { ww = ow ; wh = oh ; }
156- if (ow <= 0 || oh <= 0 ) return ;
157- recompute (ren , ww , wh , ow , oh ); /* logical size still active here */
158- if (!s_controls_on ) return ;
159-
160- int lw = 0 , lh = 0 ;
161- SDL_RenderGetLogicalSize (ren , & lw , & lh );
162- SDL_RenderSetLogicalSize (ren , 0 , 0 );
114+ s_have_geom = 1 ;
115+
163116 SDL_BlendMode prev_bm ;
164117 SDL_GetRenderDrawBlendMode (ren , & prev_bm );
165118 SDL_SetRenderDrawBlendMode (ren , SDL_BLENDMODE_BLEND );
166119 Uint8 pr , pg , pb , pa ;
167120 SDL_GetRenderDrawColor (ren , & pr , & pg , & pb , & pa );
168121
169- draw_control (ren , s_stick_cx , s_stick_cy , s_stick_r , A_STICK_BASE );
170- int kx = s_stick_cx + (int )(s_def_x * s_stick_r );
171- int ky = s_stick_cy + (int )(s_def_y * s_stick_r );
172- draw_control (ren , kx , ky , (int )(s_stick_r * KNOB_FRAC ), A_STICK_KNOB );
173- draw_control (ren , s_lmb_cx , s_lmb_cy , s_lmb_r , A_LMB );
174- draw_control (ren , s_rmb_cx , s_rmb_cy , s_rmb_r , A_RMB );
175-
176- /* DEBUG: red cross at where the APP receives the last touch (output px).
177- * Compare to where you physically clicked: if they differ, the platform is
178- * remapping the touch (e.g. emulator squeezing the bars into the canvas). */
179- if (s_mark_px >= 0 ) {
180- int mx = (int )(s_mark_px * s_kx ), my = (int )(s_mark_py * s_ky );
181- SDL_SetRenderDrawColor (ren , 255 , 0 , 0 , 235 );
182- for (int t = -2 ; t <= 2 ; ++ t ) {
183- SDL_RenderDrawLine (ren , mx + t , 0 , mx + t , oh );
184- SDL_RenderDrawLine (ren , 0 , my + t , ow , my + t );
185- }
186- }
122+ fill_circle (ren , STICK_CX , STICK_CY , STICK_R , A_STICK_BASE );
123+ fill_circle (ren , STICK_CX + (int )(s_def_x * STICK_R ),
124+ STICK_CY + (int )(s_def_y * STICK_R ), KNOB_R , A_STICK_KNOB );
125+ fill_circle (ren , LMB_CX , LMB_CY , LMB_R , A_LMB );
126+ fill_circle (ren , RMB_CX , RMB_CY , RMB_R , A_RMB );
187127
188128 SDL_SetRenderDrawColor (ren , pr , pg , pb , pa );
189129 SDL_SetRenderDrawBlendMode (ren , prev_bm );
190- SDL_RenderSetLogicalSize (ren , lw , lh );
191130}
192131
193- /* ---- input: control zones only (game area = SDL synth) ---------- */
194- int wacki_overlay_owns_touch (void ) { return s_control_fingers > 0 ; }
132+ /* ---- input (we own all touch; synth is disabled) ------ ---------- */
133+ int wacki_overlay_owns_touch (void ) { return 1 ; }
195134
196135void wacki_overlay_finger_down (SDL_FingerID id , float nx , float ny )
197136{
198- if (!s_have_geom || ! s_controls_on ) return ; /* no bars → synth handles all */
199- int px = ( int )( nx * s_win_w ), py = ( int )( ny * s_win_h ) ;
200- s_mark_px = px ; s_mark_py = py ; /* DEBUG: where the app got the touch */
137+ if (!s_have_geom ) return ;
138+ int lx , ly ;
139+ to_logical ( nx , ny , & lx , & ly );
201140 Finger * f = finger_get (id , 1 );
202141 if (!f ) return ;
203142
204- if (in_circle (px , py , s_stick_cx , s_stick_cy , s_stick_r )) {
205- f -> role = ROLE_STICK ; ++ s_control_fingers ;
143+ if (in_circle (lx , ly , STICK_CX , STICK_CY , STICK_R )) {
144+ f -> role = ROLE_STICK ;
206145 if (!s_stick_on ) { s_cur_x = g_mouse_x ; s_cur_y = g_mouse_y ; }
207146 s_stick_on = 1 ;
208- stick_set (px , py );
209- } else if (in_circle (px , py , s_lmb_cx , s_lmb_cy , s_lmb_r )) {
210- f -> role = ROLE_LMB ; ++ s_control_fingers ; g_lmb_clicked = 1 ;
211- } else if (in_circle (px , py , s_rmb_cx , s_rmb_cy , s_rmb_r )) {
212- f -> role = ROLE_RMB ; ++ s_control_fingers ; g_rmb_clicked = 1 ;
147+ stick_set (lx , ly );
148+ } else if (in_circle (lx , ly , LMB_CX , LMB_CY , LMB_R )) {
149+ f -> role = ROLE_LMB ; g_lmb_clicked = 1 ;
150+ } else if (in_circle (lx , ly , RMB_CX , RMB_CY , RMB_R )) {
151+ f -> role = ROLE_RMB ; g_rmb_clicked = 1 ;
152+ } else {
153+ /* game canvas: move the cursor under the finger, arm a tap. */
154+ f -> role = ROLE_GAME ; f -> t0 = SDL_GetTicks ();
155+ f -> sx = lx ; f -> sy = ly ; f -> moved = 0 ;
156+ g_mouse_x = (int16_t )lx ; g_mouse_y = (int16_t )ly ;
213157 }
214- /* else: game-area touch → role stays NONE, not counted; SDL synth handles it. */
215158}
216159
217160void wacki_overlay_finger_motion (SDL_FingerID id , float nx , float ny )
218161{
219- if (!s_have_geom ) return ;
220162 Finger * f = finger_get (id , 0 );
221- if (f && f -> role == ROLE_STICK )
222- stick_set ((int )(nx * s_win_w ), (int )(ny * s_win_h ));
163+ if (!f ) return ;
164+ int lx , ly ;
165+ to_logical (nx , ny , & lx , & ly );
166+ if (f -> role == ROLE_STICK ) {
167+ stick_set (lx , ly );
168+ } else if (f -> role == ROLE_GAME ) {
169+ if (abs (lx - f -> sx ) > TAP_SLOP || abs (ly - f -> sy ) > TAP_SLOP ) f -> moved = 1 ;
170+ g_mouse_x = (int16_t )lx ; g_mouse_y = (int16_t )ly ;
171+ }
223172}
224173
225174void wacki_overlay_finger_up (SDL_FingerID id , float nx , float ny )
226175{
227176 (void )nx ; (void )ny ;
228177 Finger * f = finger_get (id , 0 );
229178 if (!f ) return ;
230- if (f -> role != ROLE_NONE ) {
231- if (f -> role == ROLE_STICK ) { s_stick_on = 0 ; s_def_x = s_def_y = 0.0f ; }
232- if (s_control_fingers > 0 ) -- s_control_fingers ;
233- }
179+ if (f -> role == ROLE_STICK ) { s_stick_on = 0 ; s_def_x = s_def_y = 0.0f ; }
180+ else if (f -> role == ROLE_GAME && !f -> moved && (SDL_GetTicks () - f -> t0 ) <= TAP_MS )
181+ g_lmb_clicked = 1 ;
234182 f -> used = 0 ;
235183}
236184
0 commit comments