Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Build and Release TouchJoy

on:
push:
branches: [ "master", "main" ]
pull_request:
branches: [ "master", "main" ]
workflow_dispatch:

jobs:
build-windows:
runs-on: windows-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Configure CMake
run: cmake -B build -S .

- name: Compile Project
run: cmake --build build --config Release

- name: Package Artifacts
shell: bash
run: |
mkdir -p TouchJoy-Release
cp build/Release/TouchJoy.exe TouchJoy-Release/
cp data/* TouchJoy-Release/
mv TouchJoy-Release/sample.ini TouchJoy-Release/config.ini

- name: Upload Downloadable Artifact
uses: actions/upload-artifact@v4
with:
name: TouchJoy-Windows
path: TouchJoy-Release/
41 changes: 41 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
cmake_minimum_required(VERSION 3.15)
project(TouchJoy C CXX)

set(CMAKE_C_STANDARD 99)

# Fetch ViGEmClient automatically for Xbox 360 controller emulation
include(FetchContent)
FetchContent_Declare(
ViGEmClient
GIT_REPOSITORY https://github.com/nefarius/ViGEmClient.git
GIT_TAG master
)
FetchContent_MakeAvailable(ViGEmClient)

add_compile_definitions(
_CRT_SECURE_NO_WARNINGS
GB_INI_MAX_SECTION_LENGTH=16
GB_INI_MAX_NAME_LENGTH=16
)

# 1. Main Windowed Application
add_executable(TouchJoy WIN32
src/gamepad.c
src/gamepad_window.c
src/libs.c
src/main.c
src/utils.c
)
# Link ViGEm, Windows APIs, and Shell32 (for the browser prompt)
target_link_libraries(TouchJoy PRIVATE ViGEmClient setupapi.lib winmm.lib shell32.lib)

# 2. Test Console Application
add_executable(TouchJoyTest
src/gamepad.c
src/gamepad_window.c
src/libs.c
src/main.c
src/utils.c
)
target_compile_definitions(TouchJoyTest PRIVATE _TEST)
target_link_libraries(TouchJoyTest PRIVATE ViGEmClient setupapi.lib winmm.lib shell32.lib)
43 changes: 25 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
TouchJoy - On screen gamepad for touch-screen Windows devices
TouchJoy - On-screen gamepad for touch-screen Windows devices
=============================================================

## What is it?

This program helps you create an on-screen gamepad so that you can game on a touch-enabled Windows device:
This program creates a highly customizable on-screen virtual gamepad so you can play modern PC games on a touch-enabled Windows device (like a Microsoft Surface, ROG Ally, or Windows tablet).

![screenshot](data/screenshot.jpg)

The layout is fully customizable using a simple [ini file](data/sample.ini).
The file is also automatically reloaded whenever it is saved so that you can tweak and adjust it quickly.
Unlike legacy mappers that just send fake keyboard presses, **TouchJoy natively emulates an Xbox 360 Controller**. This means it is instantly recognized by modern games via XInput with full analog joystick and trigger support.

## Why?
The layout is fully customizable using a simple `config.ini` file. The file is automatically reloaded whenever it is saved, so you can tweak and adjust your layout in real-time without restarting the app.

I want to play Windows games lying down and there are not many good on-screen gamepad applications for Windows.
It is also a chance to learn how far I can go with only C99 and as few external libraries as possible.
## Prerequisites

## How to build?
Because TouchJoy emulates a physical hardware controller, it requires the **ViGEmBus** kernel-mode driver to be installed on your system.

You need [GENie](https://github.com/bkaradzic/genie) and Visual Studio 2013+ since the project is written in C99.
* If you do not have it installed, TouchJoy will automatically prompt you on startup and open your browser to the [Official ViGEmBus GitHub Releases](https://github.com/nefarius/ViGEmBus/releases/latest) page so you can download it safely.

`vs2013.bat` can be used to generate a solution for Visual Studio 2013.
## How to install and use?

## How to use?
**The easy way:**
1. Go to the **Actions** or **Releases** tab on this GitHub repository.
2. Download the latest `TouchJoy-Windows.zip` artifact.
3. Extract the folder anywhere on your PC.
4. Double click `TouchJoy.exe`.
5. To change your layout or map different buttons, open `config.ini` in any text editor. (For now, the code and `config.ini` itself are the best manuals).

For now, the code and [this sample](data/sample.ini) are the only manuals.
I'm terribly sorry.
## How to build from source?

## External libraries
The project has been modernized to use **CMake**. It requires Visual Studio 2019/2022 (with the Desktop C++ workload) or any modern C99-compliant compiler.

* [stb_image](https://github.com/nothings/stb): image loading
* [gb_ini](https://github.com/gingerBill/gb): ini parsing
* [utest](https://github.com/evolutional/utest): unit testing
```bash
# Clone the repository
git clone [https://github.com/YOUR-USERNAME/TouchJoy.git](https://github.com/YOUR-USERNAME/TouchJoy.git)
cd TouchJoy

Special thanks to Sean Barrett for providing an useful [list](https://github.com/nothings/stb/blob/master/docs/other_libs.md) of single-header libraries.
# Configure the build system (this will automatically fetch ViGEmClient)
cmake -B build -S .

# Compile the project
cmake --build build --config Release
60 changes: 0 additions & 60 deletions genie.lua

This file was deleted.

1 change: 0 additions & 1 deletion sample.bat

This file was deleted.

118 changes: 71 additions & 47 deletions src/gamepad_window.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
#define VC_EXTRALEAN
#include <Windows.h>
#include <windowsx.h>
#include <string.h> // Required for _stricmp

#include <ViGEm/Client.h>

#include "utils.h"

#define MOUSEEVENTF_FROMTOUCH 0xFF515700
#define BUTTON(HWND, VAR) \
Button* VAR = (Button*)GetWindowLongPtr(HWND, GWLP_USERDATA);

// EXTERN VIGEM STATE
extern PVIGEM_CLIENT g_client;
extern PVIGEM_TARGET g_pad;
extern XUSB_REPORT g_report;

typedef enum
{
TOUCH_DOWN,
Expand Down Expand Up @@ -53,6 +61,54 @@ void HandleKeyButton(Button* button, bool down)
SendInput(1, &input, sizeof(INPUT));
}

// --- NEW XBOX BUTTON MAPPING ---
USHORT GetXboxButtonMask(const char* name)
{
if (_stricmp(name, "a") == 0) return XUSB_GAMEPAD_A;
if (_stricmp(name, "b") == 0) return XUSB_GAMEPAD_B;
if (_stricmp(name, "x") == 0) return XUSB_GAMEPAD_X;
if (_stricmp(name, "y") == 0) return XUSB_GAMEPAD_Y;
if (_stricmp(name, "lb") == 0 || _stricmp(name, "l") == 0) return XUSB_GAMEPAD_LEFT_SHOULDER;
if (_stricmp(name, "rb") == 0 || _stricmp(name, "r") == 0) return XUSB_GAMEPAD_RIGHT_SHOULDER;
if (_stricmp(name, "start") == 0) return XUSB_GAMEPAD_START;
if (_stricmp(name, "select") == 0 || _stricmp(name, "back") == 0) return XUSB_GAMEPAD_BACK;
if (_stricmp(name, "up") == 0) return XUSB_GAMEPAD_DPAD_UP;
if (_stricmp(name, "down") == 0) return XUSB_GAMEPAD_DPAD_DOWN;
if (_stricmp(name, "left") == 0) return XUSB_GAMEPAD_DPAD_LEFT;
if (_stricmp(name, "right") == 0) return XUSB_GAMEPAD_DPAD_RIGHT;
if (_stricmp(name, "ls") == 0 || _stricmp(name, "l3") == 0) return XUSB_GAMEPAD_LEFT_THUMB;
if (_stricmp(name, "rs") == 0 || _stricmp(name, "r3") == 0) return XUSB_GAMEPAD_RIGHT_THUMB;
return 0;
}

void HandleXboxButton(Button* button, bool down)
{
// Handle Analog Triggers (LT / RT)
if (_stricmp(button->name, "lt") == 0) {
g_report.bLeftTrigger = down ? 255 : 0;
}
else if (_stricmp(button->name, "rt") == 0) {
g_report.bRightTrigger = down ? 255 : 0;
}
else {
// Handle Digital Buttons
USHORT mask = GetXboxButtonMask(button->name);
if (mask != 0) {
if (down) g_report.wButtons |= mask;
else g_report.wButtons &= ~mask;
}
else {
// Fallback: If it's not a standard Xbox name, send a normal keyboard key
HandleKeyButton(button, down);
return;
}
}

// Push the updated state to the virtual controller
vigem_target_x360_update(g_client, g_pad, g_report);
}
// -------------------------------

void HandleQuitButton(Button* button, bool down)
{
UNUSED(button);
Expand All @@ -64,17 +120,11 @@ void HandleWheelButton(Button* button, bool down)
{
if (!down) { return; }

// Scrolling is a bit tricky.
// First we need to move the mouse to the target area.
// Then we simulate a scroll event.
INPUT inputs[2];

// Move the mouse to a point slightly above and to the left of the button's
// top left corner
inputs[0].type = INPUT_MOUSE;
int x = GetButtonX(button) - 5;
int y = GetButtonY(button) - 5;
// Windows uses a weird coordinate system for mouse: [0, 65535]
int absX = (int)((float)x / (float)GetSystemMetrics(SM_CXSCREEN) * 65535.f);
int absY = (int)((float)y / (float)GetSystemMetrics(SM_CYSCREEN) * 65535.f);
inputs[0].mi.dx = absX;
Expand All @@ -84,7 +134,6 @@ void HandleWheelButton(Button* button, bool down)
inputs[0].mi.time = 0;
inputs[0].mi.dwExtraInfo = 0;

// Scroll
inputs[1].type = INPUT_MOUSE;
inputs[1].mi.dx = 0;
inputs[1].mi.dy = 0;
Expand All @@ -98,58 +147,33 @@ void HandleWheelButton(Button* button, bool down)

void HandleStickButton(Button* button, TouchEvent event, int touchX, int touchY)
{
float joyX, joyY;
float joyX = 0.0f;
float joyY = 0.0f;

if (event == TOUCH_UP)
{
// If the touch is released, the stick moves to its center position
joyX = 0.f;
joyY = 0.f;
}
else
if (event != TOUCH_UP)
{
// In other cases, use the real touch position to calculate stick
// position
joyX = (float)touchX / (float)button->width * 2.0f - 1.0f;
joyY = (float)touchY / (float)button->height * 2.0f - 1.0f;

joyX = (float)touchX / (float)button->width * 2.f - 1.f;
joyY = (float)touchY / (float)button->height * 2.f - 1.f;
if (joyX < -1.0f) joyX = -1.0f;
if (joyX > 1.0f) joyX = 1.0f;
if (joyY < -1.0f) joyY = -1.0f;
if (joyY > 1.0f) joyY = 1.0f;
}

bool newStates[4];
float threshold = button->extras.stick.threshold;
newStates[STICK_UP] = joyY < -threshold;
newStates[STICK_DOWN] = joyY > threshold;
newStates[STICK_LEFT] = joyX < -threshold;
newStates[STICK_RIGHT] = joyX > threshold;

INPUT inputs[4];
int numInputs = 0;

for (int i = 0; i < 4; ++i)
{
if (newStates[i] != button->extras.stick.states[i])
{
INPUT* input = &inputs[numInputs++];
input->type = INPUT_KEYBOARD;
input->ki.wVk = button->extras.stick.codes[i];
input->ki.dwFlags = newStates[i] ? 0 : KEYEVENTF_KEYUP;
input->ki.wScan = 0;
input->ki.time = 0;
input->ki.dwExtraInfo = 0;
}

button->extras.stick.states[i] = newStates[i];
}
g_report.sThumbLX = (SHORT)(joyX * 32767.0f);
g_report.sThumbLY = (SHORT)(joyY * -32767.0f);

SendInput(numInputs, inputs, sizeof(INPUT));
vigem_target_x360_update(g_client, g_pad, g_report);
}

void HandleUpDown(Button* button, bool down)
{
switch (button->type)
{
case BTN_KEY:
HandleKeyButton(button, down);
// Route through our new Xbox Button handler!
HandleXboxButton(button, down);
break;
case BTN_WHEEL:
HandleWheelButton(button, down);
Expand All @@ -163,7 +187,7 @@ void HandleUpDown(Button* button, bool down)
LRESULT CALLBACK OnTouch(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
BUTTON(hWnd, button);
TOUCHINPUT touch; //Only handle the first touch on any button
TOUCHINPUT touch;

if (GetTouchInputInfo((HTOUCHINPUT)lParam, 1, &touch, sizeof(TOUCHINPUT)))
{
Expand Down
Loading