Skip to content

Commit 13fb40f

Browse files
committed
fix(android): overlay owns all touch, canvas-normalized mapping
On the SDL Android surface the touch normalizes to the game canvas (the emulator's letterbox bars are outside the touch area), so SDL's built-in window-letterbox touch->mouse synth drifts. Turn synth off and let the overlay map every touch as logical = nx*640 / ny*480: game-area touch -> cursor + tap-click, edge-hugging joystick/LMB/RMB drive cursor + latches. Verified against on-device marker: touch X clamps to 0 across the left bar and only tracks once inside the game window == canvas-normalized.
1 parent 39bdba0 commit 13fb40f

2 files changed

Lines changed: 94 additions & 147 deletions

File tree

src/platform/android/touch_overlay.c

Lines changed: 89 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,63 @@
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+
4748
static 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;
5650
static 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
6459
static Finger s_fingers[MAX_FINGERS];
6560

66-
/* ---- helpers ----------------------------------------------------- */
6761
static int clampi(int v, int lo, int hi){ return v < lo ? lo : (v > hi ? hi : v); }
6862

6963
static 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) --- */
134101
static 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

148111
void 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

196135
void 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

217160
void 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

225174
void 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

src/platform/sdl/platform_sdl.c

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,11 @@ int PlatformInit(int w, int h, const char *title)
127127
#endif
128128

129129
#ifdef __ANDROID__
130-
/* Game-area touches go through SDL's built-in touch→mouse synthesis (it maps
131-
* through the renderer's real present transform, so the cursor lands exactly
132-
* under the finger on every device incl. emulators). The on-screen overlay
133-
* only handles the control zones in the letterbox bars and suppresses the
134-
* stray synth there (wacki_overlay_owns_touch). Must precede SDL_Init. */
135-
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "1");
130+
/* The overlay owns ALL touch (android_touch.c maps it to the canvas — on the
131+
* SDL Android surface the touch normalizes to the game window, so SDL's own
132+
* window-letterbox synth drifts). Turn synth off so touches don't also
133+
* generate a (mis-mapped) mouse event. Must precede SDL_Init. */
134+
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
136135
#endif
137136

138137
/* The SDL subsystems each platform needs come from the video HAL:

0 commit comments

Comments
 (0)