DVX_GUI/src/apps/kpunch/cpanel/cpJoystick.c
2026-10-12 14:19:24 -05:00

737 lines
23 KiB
C

// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// cpJoystick.c -- Control Panel's Game Controllers page (see cpJoystick.h).
//
// The joystick library is looked up by name when the page is built, so
// the Control Panel still works where it is not installed (the page then
// says so). The page holds a handle on the chosen controller: a timer
// redraws the test view from it, and the calibration wizard reads its raw
// values. Every change (calibration, dead zone, game port mode, keyboard
// joystick) applies at once through the library; OK stores the settings
// in DVX.INI, Cancel loads them back from the copy read when the Control
// Panel opened.
#include "cpJoystick.h"
#include "joystick/joystick.h"
#include "dvxDlg.h"
#include "dvxPlat.h"
#include "box/box.h"
#include "button/button.h"
#include "canvas/canvas.h"
#include "dropdown/dropdown.h"
#include "label/label.h"
#include "slider/slider.h"
#include "timer/timer.h"
#include "stb_ds_wrap.h"
#include <dlfcn.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#define CPJ_LABEL_W 90
#define CPJ_GAP 4
#define CPJ_BTN_W 110
#define CPJ_SMALL_BTN_W 70
#define CPJ_TICK_MS 50
#define CPJ_VIEW_W 320
#define CPJ_VIEW_H 128
#define CPJ_BOX 128 // the X / Y square
#define CPJ_DOT_R 3
#define CPJ_BAR_X 144 // axis bars, buttons and hat, right of the square
#define CPJ_BAR_W 140
#define CPJ_BAR_H 10
#define CPJ_ROW_H 18
#define CPJ_SHOWN_AXES 4
#define CPJ_BTN_Y (CPJ_SHOWN_AXES * CPJ_ROW_H + CPJ_GAP)
#define CPJ_BTN_BOX 18
#define CPJ_SHOWN_BTNS 8
#define CPJ_HAT_Y (CPJ_BTN_Y + CPJ_BTN_BOX + CPJ_GAP)
#define CPJ_STATUS_Y (CPJ_HAT_Y + CPJ_ROW_H)
#define CPJ_ZONE_LBL_W 40
#define CPJ_TEXT_BUF 128
#define CPJ_KEY_BUF 32
#define CPJ_KEYS_DLG_W 240
#define CPJ_WIZ_DLG_W 470
#define CPJ_WIZ_CHARS 56 // the wizard's text lines keep this width as their text changes
#define CPJ_SPAN_DIV 4 // each side of an axis's centre must reach centre / 4 away
#define CPJ_AXIS_SPAN ((int64_t)JOY_AXIS_MAX - JOY_AXIS_MIN)
// The joystick library's functions (NULL when it is not installed)
typedef struct {
__typeof__(joyChoiceGet) *choiceGet;
__typeof__(joyChoices) *choices;
__typeof__(joyChoiceSet) *choiceSet;
__typeof__(joyClose) *close;
__typeof__(joyCount) *count;
__typeof__(joyGetDeadZone) *getDeadZone;
__typeof__(joyGetInfo) *getInfo;
__typeof__(joyGetRaw) *getRaw;
__typeof__(joyKbdSlotGet) *slotGet;
__typeof__(joyKbdSlotSet) *slotSet;
__typeof__(joyKeyCount) *keyCount;
__typeof__(joyKeyName) *keyName;
__typeof__(joyOpen) *open;
__typeof__(joyRead) *read;
__typeof__(joyRescan) *rescan;
__typeof__(joySetCalibration) *setCal;
__typeof__(joySetDeadZone) *setDeadZone;
__typeof__(joySettingsLoad) *load;
__typeof__(joySettingsStore) *store;
} JoyFnsT;
typedef struct {
const char *name;
size_t offset;
} JoyFnSymT;
// The calibration wizard's steps
typedef enum {
WizCenterE,
WizEdgesE,
WizButtonsE
} WizStepE;
typedef struct {
WindowT *dlg;
WidgetT *text;
WidgetT *values;
WidgetT *next;
JoyInfoT info;
JoyAxisCalT cal[JOY_MAX_AXES];
WizStepE step;
} CalWizT;
// ============================================================
// Tables
// ============================================================
#define JOY_FN_SYM(field, fn) { DVX_SYM(#fn), offsetof(JoyFnsT, field) }
static const JoyFnSymT sSyms[] = {
JOY_FN_SYM(choiceGet, joyChoiceGet),
JOY_FN_SYM(choices, joyChoices),
JOY_FN_SYM(choiceSet, joyChoiceSet),
JOY_FN_SYM(close, joyClose),
JOY_FN_SYM(count, joyCount),
JOY_FN_SYM(getDeadZone, joyGetDeadZone),
JOY_FN_SYM(getInfo, joyGetInfo),
JOY_FN_SYM(getRaw, joyGetRaw),
JOY_FN_SYM(slotGet, joyKbdSlotGet),
JOY_FN_SYM(slotSet, joyKbdSlotSet),
JOY_FN_SYM(keyCount, joyKeyCount),
JOY_FN_SYM(keyName, joyKeyName),
JOY_FN_SYM(open, joyOpen),
JOY_FN_SYM(read, joyRead),
JOY_FN_SYM(rescan, joyRescan),
JOY_FN_SYM(setCal, joySetCalibration),
JOY_FN_SYM(setDeadZone, joySetDeadZone),
JOY_FN_SYM(load, joySettingsLoad),
JOY_FN_SYM(store, joySettingsStore)
};
static const char *const sAxisNames[CPJ_SHOWN_AXES] = { "X", "Y", "3", "4" };
static const char *const sSlotLabels[JOY_KBD_SLOT_COUNT] = { "Up:", "Down:", "Left:", "Right:", "Button 1:", "Button 2:", "Button 3:", "Button 4:" };
static const char *const sWizText[] = {
"Let the stick rest in the middle, then click Next.",
"Move the stick round all its edges, then click Next.",
"Press each button to test it, then click Finish."
};
// ============================================================
// Module state
// ============================================================
static JoyFnsT sJoy;
static AppContextT *sAc = NULL;
static PrefsHandleT *sPrefs = NULL;
static WidgetT *sDevDrop = NULL;
static WidgetT *sView = NULL;
static WidgetT *sCalBtn = NULL;
static WidgetT *sResetBtn = NULL;
static WidgetT *sZoneSldr = NULL;
static WidgetT *sZoneLbl = NULL;
static WidgetT *sModeDrop = NULL;
static WidgetT *sKbdDrop = NULL;
static WidgetT *sKeyDrops[JOY_KBD_SLOT_COUNT];
static const char **sKeyItems = NULL; // stb_ds array of the key names
static const char *sModeLabels[JOY_GAMEPORT_CHOICE_COUNT];
static const char *sFallbackLabels[JOY_FALLBACK_CHOICE_COUNT];
static int32_t sDevice = -1;
static int32_t sHandle = -1;
static JoyStateT sShown;
static bool sShownValid = false;
static char sDevNames[JOY_MAX_DEVICES][JOY_NAME_MAX];
static const char *sDevLabels[JOY_MAX_DEVICES];
static CalWizT sWiz;
// ============================================================
// Prototypes
// ============================================================
static WidgetT *addRow(WidgetT *parent, const char *label);
static int32_t axisPixel(int32_t value, int32_t size);
static void choiceDrop(WidgetT *drop, JoyChoiceE which, const char **labels);
void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
void cpJoystickFree(void);
void cpJoystickRevert(void);
void cpJoystickStore(PrefsHandleT *prefs);
static void drawView(void);
static bool joyStateEqual(const JoyStateT *a, const JoyStateT *b);
static void onCalibrate(WidgetT *w);
static void onDetect(WidgetT *w);
static void onDevice(WidgetT *w);
static void onFallback(WidgetT *w);
static void onKeys(WidgetT *w);
static bool onKeysOk(WindowT *dlg, void *userCtx);
static void onMode(WidgetT *w);
static void onReset(WidgetT *w);
static void onTick(WidgetT *w);
static void onWizCancel(WidgetT *w);
static void onWizNext(WidgetT *w);
static void onWizTick(WidgetT *w);
static void onZone(WidgetT *w);
static void refreshDevices(void);
static bool resolveLibrary(void);
static void selectDevice(int32_t device);
static void showZone(void);
static bool wizAxis(int32_t axis);
// "Label:" and the row for its controls.
static WidgetT *addRow(WidgetT *parent, const char *label) {
WidgetT *row = wgtHBox(parent);
WidgetT *lbl = wgtLabel(row, label);
row->spacing = wgtPixels(CPJ_GAP);
lbl->prefW = wgtPixels(CPJ_LABEL_W);
return row;
}
// An axis value as a pixel offset 0..size-1.
static int32_t axisPixel(int32_t value, int32_t size) {
return (int32_t)(((int64_t)value - JOY_AXIS_MIN) * (size - 1) / CPJ_AXIS_SPAN);
}
// A listed [joystick] setting's choices in drop, its current one chosen.
static void choiceDrop(WidgetT *drop, JoyChoiceE which, const char **labels) {
int32_t count = 0;
const PrefsChoiceT *choices = sJoy.choices(which, &count);
prefsChoiceLabels(choices, count, labels);
wgtDropdownSetItems(drop, labels, count);
wgtDropdownSetSelected(drop, sJoy.choiceGet(which));
}
void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
sAc = ac;
sPrefs = prefs;
sDevice = -1;
sHandle = -1;
sShownValid = false;
if (!resolveLibrary()) {
wgtLabel(page, "Game controller support is not installed.");
return;
}
WidgetT *row = addRow(page, "Controller:");
sDevDrop = wgtDropdown(row);
sDevDrop->weight = WGT_WEIGHT_FILL;
sDevDrop->onChange = onDevice;
WidgetT *detect = wgtButton(row, "Detect");
detect->onClick = onDetect;
detect->prefW = wgtPixels(CPJ_SMALL_BTN_W);
sView = wgtCanvas(page, CPJ_VIEW_W, CPJ_VIEW_H);
row = wgtHBox(page);
row->spacing = wgtPixels(CPJ_GAP);
sCalBtn = wgtButton(row, "Calibrate...");
sCalBtn->onClick = onCalibrate;
sCalBtn->prefW = wgtPixels(CPJ_BTN_W);
sResetBtn = wgtButton(row, "Automatic");
sResetBtn->onClick = onReset;
sResetBtn->prefW = wgtPixels(CPJ_BTN_W);
wgtLabel(row, "Dead zone:");
sZoneSldr = wgtSlider(row, 0, JOY_DEADZONE_MAX);
sZoneSldr->weight = WGT_WEIGHT_FILL;
sZoneSldr->onChange = onZone;
sZoneLbl = wgtLabel(row, "");
sZoneLbl->prefW = wgtPixels(CPJ_ZONE_LBL_W);
row = addRow(page, "Game port:");
sModeDrop = wgtDropdown(row);
sModeDrop->onChange = onMode;
choiceDrop(sModeDrop, JoyChoiceGamePortE, sModeLabels);
row = addRow(page, "Keyboard:");
sKbdDrop = wgtDropdown(row);
sKbdDrop->onChange = onFallback;
choiceDrop(sKbdDrop, JoyChoiceFallbackE, sFallbackLabels);
WidgetT *keys = wgtButton(row, "Keys...");
keys->onClick = onKeys;
keys->prefW = wgtPixels(CPJ_SMALL_BTN_W);
WidgetT *timer = wgtTimer(page, CPJ_TICK_MS, true);
timer->onChange = onTick;
refreshDevices();
}
void cpJoystickFree(void) {
if (sJoy.close) {
sJoy.close(sHandle);
}
sHandle = -1;
arrfree(sKeyItems);
sKeyItems = NULL;
}
void cpJoystickRevert(void) {
if (sJoy.load) {
sJoy.load(sPrefs);
}
}
void cpJoystickStore(PrefsHandleT *prefs) {
if (sJoy.store) {
sJoy.store(prefs);
}
}
// The test view: the X / Y position in the square, a bar per axis, the
// buttons (lit while down), the hat, and whether it answers.
static void drawView(void) {
const ColorSchemeT *cs = &sAc->colors;
JoyInfoT info;
wgtCanvasClear(sView, cs->windowFace);
wgtCanvasSetPenColor(sView, cs->contentFg);
if (sDevice < 0 || !sJoy.getInfo(sDevice, &info)) {
wgtCanvasDrawText(sView, 0, 0, "No game controllers found.");
return;
}
if (sShownValid && !sShown.connected) {
wgtCanvasDrawText(sView, CPJ_BAR_X, CPJ_STATUS_Y, "Not connected");
}
wgtCanvasSetPenColor(sView, cs->contentBg);
wgtCanvasFillRect(sView, 0, 0, CPJ_BOX, CPJ_BOX);
wgtCanvasSetPenColor(sView, cs->windowShadow);
wgtCanvasDrawRect(sView, 0, 0, CPJ_BOX, CPJ_BOX);
wgtCanvasDrawLine(sView, CPJ_BOX / 2, 0, CPJ_BOX / 2, CPJ_BOX - 1);
wgtCanvasDrawLine(sView, 0, CPJ_BOX / 2, CPJ_BOX - 1, CPJ_BOX / 2);
wgtCanvasSetPenColor(sView, cs->activeTitleBg);
wgtCanvasFillCircle(sView, axisPixel(sShown.axes[0], CPJ_BOX), axisPixel(sShown.axes[1], CPJ_BOX), CPJ_DOT_R);
int32_t barX = CPJ_BAR_X + sAc->font.charWidth * 2;
for (int32_t a = 0; a < info.axisCount && a < CPJ_SHOWN_AXES; a++) {
int32_t y = a * CPJ_ROW_H;
wgtCanvasSetPenColor(sView, cs->contentFg);
wgtCanvasDrawText(sView, CPJ_BAR_X, y, sAxisNames[a]);
wgtCanvasSetPenColor(sView, cs->contentBg);
wgtCanvasFillRect(sView, barX, y + CPJ_GAP, CPJ_BAR_W, CPJ_BAR_H);
if (info.axisPresent & (1U << a)) {
wgtCanvasSetPenColor(sView, cs->activeTitleBg);
wgtCanvasFillRect(sView, barX, y + CPJ_GAP, axisPixel(sShown.axes[a], CPJ_BAR_W) + 1, CPJ_BAR_H);
}
wgtCanvasSetPenColor(sView, cs->windowShadow);
wgtCanvasDrawRect(sView, barX, y + CPJ_GAP, CPJ_BAR_W, CPJ_BAR_H);
}
for (int32_t b = 0; b < info.buttonCount && b < CPJ_SHOWN_BTNS; b++) {
int32_t x = CPJ_BAR_X + b * (CPJ_BTN_BOX + CPJ_GAP);
bool down = (sShown.buttons & (1U << b)) != 0;
char digit[2];
digit[0] = (char)('1' + b);
digit[1] = '\0';
wgtCanvasSetPenColor(sView, down ? cs->activeTitleBg : cs->contentBg);
wgtCanvasFillRect(sView, x, CPJ_BTN_Y, CPJ_BTN_BOX, CPJ_BTN_BOX);
wgtCanvasSetPenColor(sView, cs->windowShadow);
wgtCanvasDrawRect(sView, x, CPJ_BTN_Y, CPJ_BTN_BOX, CPJ_BTN_BOX);
wgtCanvasSetPenColor(sView, down ? cs->activeTitleFg : cs->contentFg);
wgtCanvasDrawText(sView, x + (CPJ_BTN_BOX - sAc->font.charWidth) / 2, CPJ_BTN_Y + (CPJ_BTN_BOX - sAc->font.charHeight) / 2, digit);
}
if (info.hatCount > 0) {
char text[CPJ_TEXT_BUF];
if (sShown.hats[0] == JOY_HAT_CENTERED) {
snprintf(text, sizeof(text), "Hat: centred");
} else {
snprintf(text, sizeof(text), "Hat: %" PRId32 " degrees", sShown.hats[0]);
}
wgtCanvasSetPenColor(sView, cs->contentFg);
wgtCanvasDrawText(sView, CPJ_BAR_X, CPJ_HAT_Y, text);
}
}
// Two states the view draws alike, compared field by field: the struct
// ends in a bool, and memcmp would compare its padding too.
static bool joyStateEqual(const JoyStateT *a, const JoyStateT *b) {
return memcmp(a->axes, b->axes, sizeof(a->axes)) == 0 && memcmp(a->hats, b->hats, sizeof(a->hats)) == 0 && a->buttons == b->buttons && a->connected == b->connected;
}
// The wizard reads raw values through the page's handle, which keeps the
// polling going while the dialog is up.
static void onCalibrate(WidgetT *w) {
(void)w;
if (sDevice < 0 || !sJoy.getInfo(sDevice, &sWiz.info)) {
return;
}
sWiz.dlg = dvxDialogCreate(sAc, "Calibrate", CPJ_WIZ_DLG_W);
if (!sWiz.dlg) {
return;
}
WidgetT *root = sWiz.dlg->widgetRoot;
sWiz.step = WizCenterE;
sWiz.text = wgtLabel(root, sWizText[WizCenterE]);
sWiz.text->minW = wgtChars(CPJ_WIZ_CHARS);
sWiz.values = wgtLabel(root, "");
sWiz.values->minW = wgtChars(CPJ_WIZ_CHARS);
WidgetT *row = wgtHBox(root);
row->align = AlignEndE;
row->spacing = wgtPixels(CPJ_GAP);
sWiz.next = wgtButton(row, "Next");
sWiz.next->onClick = onWizNext;
sWiz.next->prefW = wgtPixels(CPJ_SMALL_BTN_W);
WidgetT *cancel = wgtButton(row, "Cancel");
cancel->onClick = onWizCancel;
cancel->prefW = wgtPixels(CPJ_SMALL_BTN_W);
WidgetT *timer = wgtTimer(root, CPJ_TICK_MS, true);
timer->onChange = onWizTick;
onWizTick(NULL);
dvxDialogRun(sAc, sWiz.dlg);
sWiz.dlg = NULL;
sShownValid = false;
}
// Look for controllers again. The page's own handle is closed first
// (with every handle of the Control Panel);
// another program's handle stops the search.
static void onDetect(WidgetT *w) {
(void)w;
sJoy.close(sHandle);
sHandle = -1;
if (sJoy.rescan() < 0) {
dvxMessageBox(sAc, "Game Controllers", "A program is using a game controller. Close it, then click Detect again.", MB_OK | MB_ICONWARNING);
}
refreshDevices();
}
static void onDevice(WidgetT *w) {
selectDevice(wgtDropdownGetSelected(w));
}
static void onFallback(WidgetT *w) {
sJoy.choiceSet(JoyChoiceFallbackE, wgtDropdownGetSelected(w));
onDetect(w);
}
// One dropdown of key names per keyboard joystick key.
static void onKeys(WidgetT *w) {
(void)w;
if (!sKeyItems) {
for (int32_t i = 0; i < sJoy.keyCount(); i++) {
arrput(sKeyItems, sJoy.keyName(i));
}
}
WindowT *dlg = dvxDialogCreate(sAc, "Keyboard Joystick", CPJ_KEYS_DLG_W);
if (!dlg) {
return;
}
for (int32_t i = 0; i < JOY_KBD_SLOT_COUNT; i++) {
sKeyDrops[i] = wgtDropdown(addRow(dlg->widgetRoot, sSlotLabels[i]));
wgtDropdownSetItems(sKeyDrops[i], sKeyItems, (int32_t)arrlen(sKeyItems));
wgtDropdownSetSelected(sKeyDrops[i], sJoy.slotGet(i));
}
dvxDialogAddOkCancel(dlg, "OK", onKeysOk, NULL);
dvxDialogRun(sAc, dlg);
}
static bool onKeysOk(WindowT *dlg, void *userCtx) {
(void)dlg;
(void)userCtx;
for (int32_t i = 0; i < JOY_KBD_SLOT_COUNT; i++) {
sJoy.slotSet(i, wgtDropdownGetSelected(sKeyDrops[i]));
}
return true;
}
// The game port makes its joysticks anew in the other mode.
static void onMode(WidgetT *w) {
sJoy.choiceSet(JoyChoiceGamePortE, wgtDropdownGetSelected(w));
onDetect(w);
}
// Forget the calibration: every axis ranges automatically again.
static void onReset(WidgetT *w) {
(void)w;
for (int32_t a = 0; a < JOY_MAX_AXES; a++) {
sJoy.setCal(sDevice, a, NULL);
}
}
// Redraw the view when the state changed.
static void onTick(WidgetT *w) {
JoyStateT st;
(void)w;
if (!sJoy.read(sHandle, &st) || (sShownValid && joyStateEqual(&st, &sShown))) {
return;
}
sShown = st;
sShownValid = true;
drawView();
}
static void onWizCancel(WidgetT *w) {
(void)w;
dvxDialogEnd(sWiz.dlg, false);
}
// Centre: every axis's resting value. Edges: the stick must have gone
// well out to both sides of the centre on every axis. Buttons: store.
static void onWizNext(WidgetT *w) {
JoyRawT raw;
(void)w;
switch (sWiz.step) {
case WizCenterE:
sJoy.getRaw(sDevice, &raw);
for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
sWiz.cal[a].min = raw.axes[a];
sWiz.cal[a].center = raw.axes[a];
sWiz.cal[a].max = raw.axes[a];
}
sWiz.step = WizEdgesE;
break;
case WizEdgesE:
for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
int32_t span = sWiz.cal[a].center / CPJ_SPAN_DIV;
if (wizAxis(a) && (sWiz.cal[a].center - sWiz.cal[a].min <= span || sWiz.cal[a].max - sWiz.cal[a].center <= span)) {
wgtSetText(sWiz.text, "Move the stick further, all the way to each edge.");
return;
}
}
sWiz.step = WizButtonsE;
wgtSetText(sWiz.next, "Finish");
break;
case WizButtonsE:
for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
if (wizAxis(a)) {
sJoy.setCal(sDevice, a, &sWiz.cal[a]);
}
}
dvxDialogEnd(sWiz.dlg, true);
return;
}
wgtSetText(sWiz.text, sWizText[sWiz.step]);
}
// The raw values (and, while the edges are being found, their ranges) and
// the buttons held.
static void onWizTick(WidgetT *w) {
JoyRawT raw;
char text[CPJ_TEXT_BUF];
int32_t pos = 0;
(void)w;
if (!sJoy.getRaw(sDevice, &raw)) {
return;
}
for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
if (!wizAxis(a)) {
continue;
}
if (sWiz.step == WizEdgesE) {
JoyAxisCalT *c = &sWiz.cal[a];
c->min = raw.axes[a] < c->min ? raw.axes[a] : c->min;
c->max = raw.axes[a] > c->max ? raw.axes[a] : c->max;
pos = dvxStrAppendf(text, sizeof(text), pos, "%" PRId32 ": %" PRId32 "-%" PRId32 " ", a + 1, c->min, c->max);
} else {
pos = dvxStrAppendf(text, sizeof(text), pos, "%" PRId32 ": %" PRId32 " ", a + 1, raw.axes[a]);
}
}
pos = dvxStrAppendf(text, sizeof(text), pos, " Buttons:");
for (int32_t b = 0; b < sWiz.info.buttonCount; b++) {
pos = dvxStrAppendf(text, sizeof(text), pos, " %c", (raw.buttons & (1U << b)) ? (char)('1' + b) : '-');
}
if (pos >= 0) {
wgtSetText(sWiz.values, text);
}
}
static void onZone(WidgetT *w) {
sJoy.setDeadZone(sDevice, wgtSliderGetValue(w));
showZone();
}
// The controllers found, in the dropdown; the first one chosen.
static void refreshDevices(void) {
JoyInfoT info;
int32_t count = sJoy.count();
for (int32_t i = 0; i < count; i++) {
sJoy.getInfo(i, &info);
snprintf(sDevNames[i], sizeof(sDevNames[i]), "%s", info.name);
sDevLabels[i] = sDevNames[i];
}
wgtDropdownSetItems(sDevDrop, sDevLabels, count);
wgtDropdownSetSelected(sDevDrop, count > 0 ? 0 : -1);
selectDevice(count > 0 ? 0 : -1);
}
static bool resolveLibrary(void) {
for (int32_t i = 0; i < DVX_ARRAY_LEN(sSyms); i++) {
void *fn = dlsym(RTLD_DEFAULT, sSyms[i].name);
if (!fn) {
memset(&sJoy, 0, sizeof(sJoy));
return false;
}
memcpy((char *)&sJoy + sSyms[i].offset, &fn, sizeof(fn));
}
return true;
}
// Watch another controller: a handle on it, its dead zone, and the
// calibration buttons only for one with analog axes.
static void selectDevice(int32_t device) {
JoyInfoT info;
bool analog = device >= 0 && sJoy.getInfo(device, &info) && (info.caps & JOY_CAP_CALIBRATE);
sJoy.close(sHandle);
sDevice = device;
sHandle = device >= 0 ? sJoy.open(sAc, device) : -1;
sShownValid = false;
wgtSetEnabled(sCalBtn, analog);
wgtSetEnabled(sResetBtn, analog);
wgtSetEnabled(sZoneSldr, device >= 0);
wgtSliderSetValue(sZoneSldr, device >= 0 ? sJoy.getDeadZone(device) : 0);
showZone();
memset(&sShown, 0, sizeof(sShown));
for (int32_t i = 0; i < JOY_MAX_HATS; i++) {
sShown.hats[i] = JOY_HAT_CENTERED;
}
drawView();
}
static void showZone(void) {
char text[CPJ_KEY_BUF];
snprintf(text, sizeof(text), "%" PRId32 "%%", wgtSliderGetValue(sZoneSldr));
wgtSetText(sZoneLbl, text);
}
// Axis a of the controller being calibrated answers.
static bool wizAxis(int32_t axis) {
return (sWiz.info.axisPresent & (1U << axis)) != 0;
}