// 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 #include #include #include #include #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; }