Skip to content

Commit 1a6ce09

Browse files
f4mrfauxclaude
andcommitted
feat: Add clean S-Pen implementation
- Implement S-Pen stylus support for Android - Add stylus tool type detection and hover handling - Support for barrel button and pointer interaction modes - Clean implementation without cheat autoload contamination 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent fbe7e12 commit 1a6ce09

File tree

1 file changed

+106
-50
lines changed

1 file changed

+106
-50
lines changed

input/drivers/android_input.c

Lines changed: 106 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -905,9 +905,32 @@ static INLINE void android_input_poll_event_type_motion(
905905
/* Optional: Update cursor position for hover if settings allow */
906906
if (settings->bools.input_stylus_hover_moves_pointer && action == AMOTION_EVENT_ACTION_HOVER_MOVE)
907907
{
908+
struct video_viewport vp = {0};
909+
910+
/* Update mouse deltas for mouse-like cursor behavior */
908911
android_mouse_calculate_deltas(android, event, motion_ptr, source);
912+
913+
/* Also update absolute pointer coordinates for RETRO_DEVICE_POINTER */
914+
video_driver_translate_coord_viewport_confined_wrap(
915+
&vp, x, y,
916+
&android->pointer[motion_ptr].confined_x,
917+
&android->pointer[motion_ptr].confined_y,
918+
&android->pointer[motion_ptr].full_x,
919+
&android->pointer[motion_ptr].full_y);
920+
921+
video_driver_translate_coord_viewport_wrap(
922+
&vp, x, y,
923+
&android->pointer[motion_ptr].x,
924+
&android->pointer[motion_ptr].y,
925+
&android->pointer[motion_ptr].full_x,
926+
&android->pointer[motion_ptr].full_y);
927+
928+
/* Ensure pointer_count covers this motion_ptr */
929+
if (android->pointer_count < (int)motion_ptr + 1)
930+
android->pointer_count = (int)motion_ptr + 1;
931+
909932
#ifdef DEBUG_ANDROID_INPUT
910-
RARCH_LOG("[RA Input] Stylus hover cursor update (user enabled)\n");
933+
RARCH_LOG("[RA Input] Stylus hover cursor update (user enabled) - mouse + pointer coords\n");
911934
#endif
912935
}
913936

@@ -1058,12 +1081,12 @@ static INLINE void android_input_poll_event_type_motion(
10581081
pressure, distance);
10591082
#endif
10601083

1061-
/* STYLUS AS POINTER: Route to RETRO_DEVICE_POINTER for menu interaction
1062-
* Handle DOWN/MOVE => active pointer; UP => release pointer
1063-
* Only activate pointer when stylus_pressed (respects contact setting) */
1084+
/* STYLUS POINTER: Mimic native touchscreen behavior for menu consistency
1085+
* Native touchscreen ONLY updates coordinates on contact, NOT during hover
1086+
* This prevents menu jumping between hover and tap states */
10641087
if (action == AMOTION_EVENT_ACTION_DOWN || action == AMOTION_EVENT_ACTION_MOVE)
10651088
{
1066-
if (stylus_pressed) /* Only when contact detected */
1089+
if (stylus_pressed) /* ONLY update coordinates on actual contact, like native touchscreen */
10671090
{
10681091
struct video_viewport vp = {0};
10691092

@@ -1082,33 +1105,71 @@ static INLINE void android_input_poll_event_type_motion(
10821105
&android->pointer[motion_ptr].full_x,
10831106
&android->pointer[motion_ptr].full_y);
10841107

1085-
/* Ensure pointer_count covers this motion_ptr */
1086-
if (android->pointer_count < (int)motion_ptr + 1)
1087-
android->pointer_count = (int)motion_ptr + 1;
1108+
/* S-Pen Menu Coordination: Activate mouse mode for menu consistency */
1109+
if (!android->mouse_activated)
1110+
{
1111+
RARCH_LOG("[Android Input] S-Pen activated menu mouse mode.\n");
1112+
android->mouse_activated = true;
1113+
}
1114+
1115+
/* Update mouse coordinates only on contact - matches native touchscreen behavior */
1116+
android->mouse_x_viewport = android->pointer[motion_ptr].x;
1117+
android->mouse_y_viewport = android->pointer[motion_ptr].y;
1118+
1119+
/* S-Pen Virtual Pointer System for libretro cores:
1120+
* Index 0 = tip pointer (when tip pressed)
1121+
* Index 1 = virtual barrel pointer (when barrel pressed, mirrors tip XY)
1122+
* This allows cores to detect barrel via pointer_count > 1 or checking index 1 */
1123+
1124+
android->pointer_count = 0; /* Reset count, will increment based on active states */
1125+
1126+
if (tip_down) {
1127+
/* Set up tip pointer at index 0 */
1128+
android->pointer[0] = android->pointer[motion_ptr]; /* Copy translated coordinates */
1129+
android->pointer_count = 1;
1130+
}
1131+
1132+
if (side_primary) {
1133+
/* Set up virtual barrel pointer at index 1, mirroring tip coordinates */
1134+
android->pointer[1] = android->pointer[motion_ptr]; /* Same coordinates as tip */
1135+
android->pointer_count = tip_down ? 2 : 1; /* Increment if tip also active */
1136+
}
1137+
1138+
/* Update mouse button states for menu interaction */
1139+
android->mouse_l = tip_down; /* Left click when tip touches */
1140+
android->mouse_r = side_primary; /* Right click on barrel button */
10881141

10891142
#ifdef DEBUG_ANDROID_INPUT
10901143
if (action == AMOTION_EVENT_ACTION_DOWN)
1091-
RARCH_LOG("[Stylus] POINTER DOWN @ (%.1f, %.1f) idx=%zu cnt=%d\n",
1092-
x, y, motion_ptr, android->pointer_count);
1144+
RARCH_LOG("[Stylus] POINTER DOWN @ (%.1f, %.1f) tip=%d barrel=%d cnt=%d\n",
1145+
x, y, tip_down, side_primary, android->pointer_count);
1146+
#endif
1147+
}
1148+
else
1149+
{
1150+
/* Hovering: Like native touchscreen, DON'T update coordinates during hover
1151+
* This prevents menu jumping and matches user expectations */
1152+
android->pointer_count = 0;
1153+
android->mouse_l = false;
1154+
android->mouse_r = false;
1155+
1156+
#ifdef DEBUG_ANDROID_INPUT
1157+
RARCH_LOG("[Stylus] HOVER (no coord update) @ (%.1f, %.1f) p=%.3f d=%.3f\n",
1158+
x, y, pressure, distance);
10931159
#endif
10941160
}
10951161
return; /* Early return - no shared processing */
10961162
}
10971163

10981164
if (action == AMOTION_EVENT_ACTION_UP)
10991165
{
1100-
/* Compact/release pointer array like the touch path */
1101-
if (motion_ptr < MAX_TOUCH - 1)
1102-
{
1103-
memmove(android->pointer + motion_ptr,
1104-
android->pointer + motion_ptr + 1,
1105-
(MAX_TOUCH - motion_ptr - 1) * sizeof(android->pointer[0]));
1106-
}
1107-
if (android->pointer_count > 0)
1108-
android->pointer_count--;
1166+
/* S-Pen UP: Clear all virtual pointers and button states */
1167+
android->pointer_count = 0;
1168+
android->mouse_l = false;
1169+
android->mouse_r = false;
11091170

11101171
#ifdef DEBUG_ANDROID_INPUT
1111-
RARCH_LOG("[Stylus] POINTER UP idx=%zu cnt=%d\n", motion_ptr, android->pointer_count);
1172+
RARCH_LOG("[Stylus] POINTER UP - cleared all virtual pointers\n");
11121173
#endif
11131174
return; /* Early return - no shared processing */
11141175
}
@@ -1152,28 +1213,24 @@ static INLINE void android_input_poll_event_type_motion(
11521213
* and mouse deltas and don't process as touchscreen event.
11531214
* NOTE: AINPUT_SOURCE_* defines have multiple bits set so do full check
11541215
* Stylus events are now handled above in the toolType-first section */
1155-
if ( (source & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE
1216+
if (((source & AINPUT_SOURCE_MOUSE) == AINPUT_SOURCE_MOUSE
11561217
|| (source & AINPUT_SOURCE_MOUSE_RELATIVE) == AINPUT_SOURCE_MOUSE_RELATIVE)
1218+
&& !is_stylus)
11571219
{
1158-
/* Only handle regular mouse if not currently using stylus */
1159-
if (!is_stylus)
1220+
if (!android->mouse_activated)
11601221
{
1161-
if (!android->mouse_activated)
1162-
{
1163-
#ifdef DEBUG_ANDROID_INPUT
1164-
RARCH_LOG("[Android Input] Mouse activated.\n");
1165-
#endif
1166-
android->mouse_activated = true;
1167-
}
1168-
/* getButtonState requires API level 14 */
1169-
if (p_AMotionEvent_getButtonState)
1170-
{
1171-
int btn = (int)AMotionEvent_getButtonState(event);
1222+
RARCH_LOG("[Android Input] Mouse activated.\n");
1223+
android->mouse_activated = true;
1224+
}
1225+
/* getButtonState requires API level 14 */
1226+
if (p_AMotionEvent_getButtonState)
1227+
{
1228+
int btn = (int)AMotionEvent_getButtonState(event);
11721229

1173-
/* Regular mouse button mapping (stylus events handled above) */
1174-
android->mouse_l = (btn & AMOTION_EVENT_BUTTON_PRIMARY);
1175-
android->mouse_r = (btn & AMOTION_EVENT_BUTTON_SECONDARY);
1176-
android->mouse_m = (btn & AMOTION_EVENT_BUTTON_TERTIARY);
1230+
/* Regular mouse button mapping (stylus events handled above) */
1231+
android->mouse_l = (btn & AMOTION_EVENT_BUTTON_PRIMARY);
1232+
android->mouse_r = (btn & AMOTION_EVENT_BUTTON_SECONDARY);
1233+
android->mouse_m = (btn & AMOTION_EVENT_BUTTON_TERTIARY);
11771234

11781235
btn = (int)AMotionEvent_getAxisValue(event,
11791236
AMOTION_EVENT_AXIS_VSCROLL, motion_ptr);
@@ -1182,19 +1239,18 @@ static INLINE void android_input_poll_event_type_motion(
11821239
android->mouse_wu = btn;
11831240
else if (btn < 0)
11841241
android->mouse_wd = btn;
1185-
}
1186-
else
1187-
{
1188-
/* If getButtonState is not available
1189-
* then treat all MotionEvent.ACTION_DOWN as left button presses */
1190-
if (action == AMOTION_EVENT_ACTION_DOWN)
1191-
android->mouse_l = 1;
1192-
if (action == AMOTION_EVENT_ACTION_UP)
1193-
android->mouse_l = 0;
1194-
}
1195-
1196-
android_mouse_calculate_deltas(android,event,motion_ptr,source);
11971242
}
1243+
else
1244+
{
1245+
/* If getButtonState is not available
1246+
* then treat all MotionEvent.ACTION_DOWN as left button presses */
1247+
if (action == AMOTION_EVENT_ACTION_DOWN)
1248+
android->mouse_l = 1;
1249+
if (action == AMOTION_EVENT_ACTION_UP)
1250+
android->mouse_l = 0;
1251+
}
1252+
1253+
android_mouse_calculate_deltas(android,event,motion_ptr,source);
11981254
/* If stylus is active, don't interfere with its mouse state */
11991255
return;
12001256
}

0 commit comments

Comments
 (0)