Compare commits

...
Author SHA1 Message Date
kichikuouandClaude Opus 4.8 dac0345e02 [NOT FOR MERGE] Add opt-in virtual mouse pointer for touch devices
Provide a trackpad-style on-screen cursor for touch environments (mainly
Android), where touching the screen previously jumped the pointer to the
touched location (absolute) and SDL hardware cursors are invisible.

When enabled, a finger drag moves an arrow cursor relatively, a tap
left-clicks (held briefly so polling games detect it), two fingers
right-click, and a stationary long press starts a left-button drag. Real
mouse motion is ignored (including the spurious startup (0,0) event), and
program-driven cursor moves are followed via the internal pointer location
instead of warping the OS cursor.

The feature is disabled by default and enabled via the -virtualpointer
command-line option, the virtualpointer profile setting, or the "Virtual
mouse pointer" toggle in the Android launcher (persisted in
SharedPreferences and passed to the engine as -virtualpointer).

The gesture handling lives in event.c and the cursor rendering in the new
platform-independent virtual_pointer module, so it works on any touch
target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 20:29:45 +09:00
15 changed files with 359 additions and 1 deletions
@@ -61,9 +61,13 @@ class GameActivity : SDLActivity() {
}
override fun getArguments(): Array<String> {
return arrayOf(
val args = mutableListOf(
"-gamedir", intent.getStringExtra(EXTRA_GAME_ROOT)!!,
"-savedir", intent.getStringExtra(EXTRA_SAVE_DIRECTORY)!!)
val prefs = getSharedPreferences(Launcher.PREFS_NAME, MODE_PRIVATE)
if (prefs.getBoolean(Launcher.PREF_VIRTUAL_POINTER, false))
args.add("-virtualpointer")
return args.toTypedArray()
}
override fun setTitle(title: CharSequence?) {
@@ -42,6 +42,8 @@ interface LauncherObserver {
class Launcher private constructor(private val rootDir: File) {
companion object {
const val SAVE_DIR = "save"
const val PREFS_NAME = "settings"
const val PREF_VIRTUAL_POINTER = "virtual_pointer"
const val TITLE_FILE = "title.txt"
const val GAMEDIR_FILE = "game_directory.txt"
const val PLAYLIST_FILE = "playlist.txt"
@@ -92,6 +92,9 @@ class LauncherActivity : Activity(), LauncherObserver {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.launcher_menu, menu)
val prefs = getSharedPreferences(Launcher.PREFS_NAME, MODE_PRIVATE)
menu.findItem(R.id.virtual_pointer).isChecked =
prefs.getBoolean(Launcher.PREF_VIRTUAL_POINTER, false)
return true
}
@@ -143,6 +146,13 @@ class LauncherActivity : Activity(), LauncherObserver {
startActivity(intent)
true
}
R.id.virtual_pointer -> {
val enabled = !item.isChecked
item.isChecked = enabled
getSharedPreferences(Launcher.PREFS_NAME, MODE_PRIVATE).edit()
.putBoolean(Launcher.PREF_VIRTUAL_POINTER, enabled).apply()
true
}
else -> super.onOptionsItemSelected(item)
}
}
@@ -12,6 +12,10 @@
<item
android:id="@+id/clear_savedata"
android:title="@string/action_clear_save_data" />
<item
android:id="@+id/virtual_pointer"
android:checkable="true"
android:title="@string/action_virtual_pointer" />
<item
android:id="@+id/help"
android:title="@string/action_help" />
@@ -7,6 +7,7 @@
<string name="action_install_from_zip">ZIPからインストール</string>
<string name="action_licenses">オープンソースライセンス</string>
<string name="action_clear_save_data">セーブデータをクリア</string>
<string name="action_virtual_pointer">仮想マウスポインタ</string>
<string name="cancel">キャンセル</string>
<string name="cannot_find_ald">System 3.x のファイル (*.ald) が見つかりません。</string>
<string name="choose_a_file">ファイルを選択</string>
@@ -6,6 +6,7 @@
<string name="action_import_save_data">Import Save Files</string>
<string name="action_install_from_zip">Install from ZIP</string>
<string name="action_licenses">Open source licenses</string>
<string name="action_virtual_pointer">Virtual mouse pointer</string>
<string name="cancel">Cancel</string>
<string name="cannot_find_ald">Cannot find System 3.x game files (*.ald).</string>
<string name="choose_a_file">Choose a file</string>
+1
View File
@@ -91,6 +91,7 @@ target_sources(xsystem35 PRIVATE
system.c
texthook.c
variable.c
virtual_pointer.c
vsp.c
xsystem35.c
)
+1
View File
@@ -131,6 +131,7 @@ struct _ags {
bool enable_zb;
bool noantialias; /* antialias を使用しない */
bool noimagecursor; /* リソースファイルのカーソルを読みこまない */
bool virtualpointer; /* enable the virtual mouse pointer (trackpad-style touch) */
};
typedef struct _ags ags_t;
+29
View File
@@ -0,0 +1,29 @@
/* XPM */
static const char *virtual_cursor[] = {
/* width height num_colors chars_per_pixel */
"16 19 3 1",
/* colors */
"X c #000000",
". c #ffffff",
" c None",
/* pixels */
"X ",
"XX ",
"X.X ",
"X..X ",
"X...X ",
"X....X ",
"X.....X ",
"X......X ",
"X.......X ",
"X........X ",
"X.....XXXXX ",
"X..X..X ",
"X.X X..X ",
"XX X..X ",
"X X..X ",
" X..X ",
" X..X ",
" X..X ",
" XX "
};
+154
View File
@@ -20,6 +20,7 @@
#include "config.h"
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
@@ -40,6 +41,7 @@
#include "input.h"
#include "msgskip.h"
#include "hacks.h"
#include "virtual_pointer.h"
static void get_event(void);
static void keyEventProsess(SDL_KeyboardEvent *e, bool pressed);
@@ -277,6 +279,13 @@ void send_agsevent(enum agsevent_type type, int code) {
}
void event_set_mouse_location(int x, int y) {
if (vp_is_enabled()) {
// In virtual pointer mode there is no visible OS cursor to warp; move
// the internal location instead so the on-screen virtual cursor follows
// (including each step of the eased path in ags_setCursorLocation()).
event_set_mouse_internal_location(x, y);
return;
}
// scale mouse x and y
float scalex, scaley;
SDL_RenderGetScale(gfx_renderer, &scalex, &scaley);
@@ -306,9 +315,16 @@ void event_set_mouse_location(int x, int y) {
void event_set_mouse_internal_location(int x, int y) {
mousex = x;
mousey = y;
if (vp_is_enabled())
gfx_dirty = true; // repaint so the virtual cursor follows
send_agsevent(AGSEVENT_MOUSE_MOTION, 0);
}
void event_get_pointer_pos(int *x, int *y) {
*x = mousex;
*y = mousey;
}
// Stores a deferred touch event (valid if .timestamp != 0) to add a delay
// between mouse pointer movement and mouse button state change caused by a
// touch event. This prevents the game from processing a button down event
@@ -352,6 +368,127 @@ static void rance4v2_hack(void) {
cancel_yield();
}
/*
* Virtual mouse pointer (trackpad-style touch input).
* Active only when nact->ags.virtualpointer is set. Reuses the pointer state
* (mousex/mousey/mouseb, RawKeyInfo) so all game queries are unaffected.
*/
#define VP_SENSITIVITY 1.5f // finger-to-cursor movement multiplier
#define VP_TAP_MAX_MOVE 0.02f // max normalized finger travel for a tap
#define VP_TAP_MAX_TIME 300 // max touch duration (ms) for a tap
#define VP_LONGPRESS_TIME 500 // hold time (ms) to start a drag
#define VP_CLICK_HOLD_TIME 100 // how long (ms) a tap holds the left button
static bool vp_primary_down; // a finger is tracked as the pointer
static SDL_FingerID vp_primary_id;
static uint32_t vp_down_time;
static float vp_moved_dist; // accumulated normalized finger travel
static bool vp_moved; // travel exceeded the tap threshold
static bool vp_longpress; // left button held due to a long press (drag)
static bool vp_two_finger; // right button held due to a 2nd finger
static bool vp_click_release_pending;
static uint32_t vp_click_release_time;
static bool vp_centered; // cursor has been placed at the center
static void vp_press(int button) {
mouseb |= 1 << button;
RawKeyInfo[mouse_to_rawkey(button)] = true;
send_agsevent(AGSEVENT_BUTTON_PRESS, mouse_to_agsevent(button));
}
static void vp_release(int button) {
mouseb &= ~(1 << button);
RawKeyInfo[mouse_to_rawkey(button)] = false;
send_agsevent(AGSEVENT_BUTTON_RELEASE, mouse_to_agsevent(button));
}
static void vp_move(float dnx, float dny) {
mousex += (int)lroundf(dnx * view_w * VP_SENSITIVITY);
mousey += (int)lroundf(dny * view_h * VP_SENSITIVITY);
// Keep the cursor inside the view.
if (mousex < 0) mousex = 0;
else if (mousex >= view_w) mousex = view_w - 1;
if (mousey < 0) mousey = 0;
else if (mousey >= view_h) mousey = view_h - 1;
send_agsevent(AGSEVENT_MOUSE_MOTION, 0);
gfx_dirty = true;
}
static void vp_finger_down(SDL_TouchFingerEvent *t) {
if (SDL_GetNumTouchFingers(t->touchId) >= 2) {
// A second finger means a right click at the current position.
if (!vp_two_finger) {
vp_two_finger = true;
vp_press(SDL_BUTTON_RIGHT);
}
return;
}
// First finger: start tracking. The cursor moves only on drag.
vp_primary_down = true;
vp_primary_id = t->fingerId;
vp_down_time = SDL_GetTicks();
vp_moved_dist = 0.0f;
vp_moved = false;
}
static void vp_finger_motion(SDL_TouchFingerEvent *t) {
if (vp_two_finger || !vp_primary_down || t->fingerId != vp_primary_id)
return;
vp_moved_dist += hypotf(t->dx, t->dy);
if (vp_moved_dist > VP_TAP_MAX_MOVE)
vp_moved = true;
vp_move(t->dx, t->dy);
}
static void vp_finger_up(SDL_TouchFingerEvent *t) {
if (vp_two_finger) {
// Release the right button once every finger is lifted.
if (SDL_GetNumTouchFingers(t->touchId) == 0) {
vp_two_finger = false;
vp_primary_down = false;
vp_release(SDL_BUTTON_RIGHT);
}
return;
}
if (!vp_primary_down || t->fingerId != vp_primary_id)
return;
vp_primary_down = false;
if (vp_longpress) {
vp_longpress = false;
vp_release(SDL_BUTTON_LEFT); // end drag
} else if (!vp_moved && SDL_GetTicks() - vp_down_time < VP_TAP_MAX_TIME) {
// Tap: left click. Hold the button briefly so polling games detect it.
vp_press(SDL_BUTTON_LEFT);
vp_click_release_pending = true;
vp_click_release_time = SDL_GetTicks() + VP_CLICK_HOLD_TIME;
}
}
// Time-driven virtual pointer transitions, polled each event pump.
static void vp_tick(void) {
if (!vp_is_enabled())
return;
uint32_t now = SDL_GetTicks();
// Place the cursor at the center once the view size is known.
if (!vp_centered && view_w > 0 && view_h > 0) {
mousex = view_w / 2;
mousey = view_h / 2;
vp_centered = true;
gfx_dirty = true;
}
// A stationary long press starts a left-button drag.
if (vp_primary_down && !vp_moved && !vp_longpress && !vp_two_finger &&
now - vp_down_time >= VP_LONGPRESS_TIME) {
vp_longpress = true;
vp_press(SDL_BUTTON_LEFT);
}
// Release the briefly-held tap click.
if (vp_click_release_pending && now >= vp_click_release_time) {
vp_click_release_pending = false;
vp_release(SDL_BUTTON_LEFT);
}
}
void event_handle_event(SDL_Event *e) {
if (event_custom_handler && event_custom_handler(e))
return;
@@ -396,6 +533,10 @@ void event_handle_event(SDL_Event *e) {
#endif
break;
case SDL_MOUSEMOTION:
// Ignore real mouse motion while the virtual pointer is active. This
// also discards the spurious (0,0) motion seen right after startup.
if (vp_is_enabled())
break;
event_set_mouse_internal_location(e->motion.x, e->motion.y);
#ifdef _WIN32
win_menu_onMouseMotion(e->motion.x, e->motion.y);
@@ -429,6 +570,10 @@ void event_handle_event(SDL_Event *e) {
break;
case SDL_FINGERDOWN:
if (vp_is_enabled()) {
vp_finger_down(&e->tfinger);
break;
}
if (SDL_GetNumTouchFingers(e->tfinger.touchId) >= 2) {
mouseb &= ~(1 << SDL_BUTTON_LEFT);
mouseb |= 1 << SDL_BUTTON_RIGHT;
@@ -455,6 +600,10 @@ void event_handle_event(SDL_Event *e) {
break;
case SDL_FINGERUP:
if (vp_is_enabled()) {
vp_finger_up(&e->tfinger);
break;
}
if (SDL_GetNumTouchFingers(e->tfinger.touchId) == 0) {
int ags_button = (mouseb & 1 << SDL_BUTTON_LEFT) ? AGSEVENT_BUTTON_LEFT : AGSEVENT_BUTTON_RIGHT;
mousex = e->tfinger.x * view_w;
@@ -465,6 +614,10 @@ void event_handle_event(SDL_Event *e) {
break;
case SDL_FINGERMOTION:
if (vp_is_enabled()) {
vp_finger_motion(&e->tfinger);
break;
}
mousex = e->tfinger.x * view_w;
mousey = e->tfinger.y * view_h;
send_agsevent(AGSEVENT_MOUSE_MOTION, 0);
@@ -554,6 +707,7 @@ static void get_event(void) {
enum scheduler_event scheduler_event = SCHEDULER_EVENT_INPUT_CHECK_MISS;
fire_deferred_touch_event();
vp_tick();
SDL_Event e;
while (SDL_PollEvent(&e)) {
+1
View File
@@ -31,6 +31,7 @@ void event_remove(void);
void event_set_joy_device_index(int index);
void event_set_mouse_location(int x, int y);
void event_set_mouse_internal_location(int x, int y);
void event_get_pointer_pos(int *x, int *y);
int event_get_key(void);
int event_get_mouse(SDL_Point *p);
void event_get_wheel(int *forward, int *back);
+2
View File
@@ -41,6 +41,7 @@
#include "image.h"
#include "nact.h"
#include "debugger.h"
#include "virtual_pointer.h"
static void gfx_pal_check(void) {
if (nact->ags.pal_changed) {
@@ -60,6 +61,7 @@ void gfx_updateScreen(void) {
return;
SDL_RenderClear(gfx_renderer);
SDL_RenderCopy(gfx_renderer, gfx_texture, NULL, NULL);
vp_draw(gfx_renderer);
SDL_RenderPresent(gfx_renderer);
gfx_dirty = false;
}
+105
View File
@@ -0,0 +1,105 @@
/*
* virtual_pointer.c Trackpad-style virtual mouse pointer for touch devices
*
* Copyright (C) 2026 <KichikuouChrome@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "portab.h"
#include "system.h"
#include "nact.h"
#include "event.h"
#include "virtual_pointer.h"
#include "bitmaps/virtual_cursor.xpm"
bool vp_is_enabled(void) {
return nact->ags.virtualpointer;
}
// Builds an ARGB surface from the (simple, 1 char/pixel) XPM array above.
static SDL_Surface *create_cursor_surface(void) {
int w, h, ncolors, cpp;
if (sscanf(virtual_cursor[0], "%d %d %d %d", &w, &h, &ncolors, &cpp) != 4 || cpp != 1) {
WARNING("virtual_pointer: unexpected cursor bitmap header");
return NULL;
}
// Map each character to an ARGB8888 value.
Uint32 colormap[256] = {0};
for (int i = 0; i < ncolors; i++) {
const char *line = virtual_cursor[1 + i];
unsigned char ch = (unsigned char)line[0];
// Format: "<ch> c <value>", value is "#rrggbb" or "None".
const char *val = line + 4;
if (val[0] == '#') {
unsigned int rgb = (unsigned int)strtoul(val + 1, NULL, 16);
colormap[ch] = 0xff000000u | rgb; // opaque
} else {
colormap[ch] = 0; // transparent
}
}
SDL_Surface *s = SDL_CreateRGBSurfaceWithFormat(0, w, h, 32, SDL_PIXELFORMAT_ARGB8888);
if (!s)
return NULL;
const char **pixels = &virtual_cursor[1 + ncolors];
for (int y = 0; y < h; y++) {
Uint32 *row = (Uint32 *)((Uint8 *)s->pixels + y * s->pitch);
const char *src = pixels[y];
for (int x = 0; x < w; x++)
row[x] = colormap[(unsigned char)src[x]];
}
return s;
}
// Lazily-created cursor texture. The hotspot (arrow tip) is at its top-left
// corner, so it is drawn with its origin at the pointer position.
static SDL_Texture *cursor_texture;
static int cursor_w, cursor_h;
void vp_draw(SDL_Renderer *renderer) {
if (!vp_is_enabled())
return;
if (!cursor_texture) {
SDL_Surface *s = create_cursor_surface();
if (!s)
return;
cursor_w = s->w;
cursor_h = s->h;
cursor_texture = SDL_CreateTextureFromSurface(renderer, s);
SDL_FreeSurface(s);
if (!cursor_texture)
return;
SDL_SetTextureBlendMode(cursor_texture, SDL_BLENDMODE_BLEND);
}
int x, y;
event_get_pointer_pos(&x, &y);
// The renderer's logical size is set to the game view, so drawing in view
// coordinates positions the cursor exactly at the game's mouse coordinates.
SDL_Rect dst = { x, y, cursor_w, cursor_h };
SDL_RenderCopy(renderer, cursor_texture, NULL, &dst);
}
+37
View File
@@ -0,0 +1,37 @@
/*
* virtual_pointer.h Trackpad-style virtual mouse pointer for touch devices
*
* Copyright (C) 2026 <KichikuouChrome@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#ifndef __VIRTUAL_POINTER_H__
#define __VIRTUAL_POINTER_H__
#include <stdbool.h>
#include <SDL.h>
/* Returns true if the virtual mouse pointer feature is enabled. */
bool vp_is_enabled(void);
/*
* Draws the virtual cursor on top of the presented frame. No-op when the
* feature is disabled. Called from gfx_updateScreen() between the game image
* RenderCopy and RenderPresent.
*/
void vp_draw(SDL_Renderer *renderer);
#endif /* __VIRTUAL_POINTER_H__ */
+6
View File
@@ -145,6 +145,7 @@ static void sys35_usage(bool verbose) {
puts(" -fullscreen : start with fullscreen");
puts(" -integerscale : use integer scaling when resizing");
puts(" -noimagecursor : disable image cursor");
puts(" -virtualpointer : enable the virtual mouse pointer (for touch)");
puts(" -version : show version");
puts(" -h : show this message");
puts(" --help : show this message");
@@ -286,6 +287,8 @@ static void sys35_ParseOption(int *argc, char **argv) {
}
} else if (0 == strcmp(argv[i], "-noimagecursor")) {
nact->ags.noimagecursor = true;
} else if (0 == strcmp(argv[i], "-virtualpointer")) {
nact->ags.virtualpointer = true;
} else if (0 == strcmp(argv[i], "-debuglv")) {
if (argv[i + 1] != NULL) {
sys_set_debug_level(argv[i + 1][0] - '0');
@@ -380,6 +383,9 @@ static void check_profile() {
/* disable image cursor */
get_boolean_profile("no_imagecursor", &nact->ags.noimagecursor);
/* enable the virtual mouse pointer */
get_boolean_profile("virtualpointer", &nact->ags.virtualpointer);
/* enable integer scaling */
get_boolean_profile("integerscale", &integer_scaling);