Skip to content

Commit c933c8a

Browse files
authored
Merge pull request #2 from nefarius/nicer-visuals
Gamepad rendering improvements and device-specific fixes
2 parents 108012f + 6bc46c5 commit c933c8a

15 files changed

Lines changed: 769 additions & 43 deletions

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ add_executable(MultiPadTester WIN32
1818
source_group("Source Files" FILES ${SOURCE_FILES})
1919
source_group("Header Files" FILES ${HEADER_FILES})
2020

21-
target_include_directories(MultiPadTester PRIVATE src)
21+
target_include_directories(MultiPadTester PRIVATE src res)
2222

2323
target_link_libraries(MultiPadTester PRIVATE
2424
imgui::imgui

assets/CONTROLLER_TEXTURE.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Controller body textures (400×280 recommended for overlay alignment):
2+
3+
xbox_body.png — used for Xbox and other non-Sony devices.
4+
dualsense_body.png — used when a Sony device is detected (DualSense, DualShock, etc.).
5+
6+
Replace these PNGs with your own art to change how each layout looks.

assets/dualsense_body.png

1.72 MB
Loading

assets/xbox_body.png

1.63 MB
Loading

res/app.rc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1+
#include "resource.h"
12
IDI_APPICON ICON "app.ico"
3+
IDR_XBOX_BODY RCDATA "..\\assets\\xbox_body.png"
4+
IDR_DUALSENSE_BODY RCDATA "..\\assets\\dualsense_body.png"

res/resource.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#pragma once
2+
#define IDR_XBOX_BODY 101
3+
#define IDR_DUALSENSE_BODY 102

src/gamepad_renderer.cpp

Lines changed: 160 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,76 @@
66

77
namespace GamepadRenderer {
88

9-
// ── palette ──────────────────────────────────────────────────
9+
// ── layout coordinates (Xbox: stick upper-left, D-pad lower-left; Sony: D-pad upper-left, stick lower-left) ──
10+
11+
struct LayoutCoords {
12+
float dpadX, dpadY;
13+
float faceX, faceY;
14+
float leftStickX, leftStickY;
15+
float rightStickX, rightStickY;
16+
float lbTlX, lbTlY, lbBrX, lbBrY;
17+
float rbTlX, rbTlY, rbBrX, rbBrY;
18+
float ltTlX, ltTlY, ltBrX, ltBrY;
19+
float rtTlX, rtTlY, rtBrX, rtBrY;
20+
float backX, backY, guideX, guideY, startX, startY;
21+
const char* faceLabelA;
22+
const char* faceLabelB;
23+
const char* faceLabelX;
24+
const char* faceLabelY;
25+
const char* backLabel; // Back / Share
26+
const char* guideLabel; // Guide / PS
27+
const char* startLabel; // Start / Options
28+
};
29+
30+
static const LayoutCoords XBOX_LAYOUT = {
31+
160.f, 154.f, // D-pad lower-left
32+
275.f, 85.f, // face buttons
33+
125.f, 85.f, // left stick upper-left
34+
252.f, 158.f, // right stick
35+
72.f, 32.f, 162.f, 50.f, // left bumper
36+
238.f, 32.f, 328.f, 50.f, // right bumper
37+
58.f, 10.f, 88.f, 36.f, // left trigger
38+
312.f, 10.f, 342.f, 36.f, // right trigger
39+
182.f, 88.f, 200.f, 100.f, 218.f, 88.f,
40+
"A", "B", "X", "Y",
41+
"Back", "Guide", "Start"
42+
};
43+
44+
// PlayStation: Circle (A), Cross (B), Square (X), Triangle (Y) — ASCII so default font works
45+
static const char* SONY_FACE_A = "O"; // Circle
46+
static const char* SONY_FACE_B = "X"; // Cross
47+
static const char* SONY_FACE_X = "S"; // Square
48+
static const char* SONY_FACE_Y = "T"; // Triangle
49+
50+
static const LayoutCoords SONY_LAYOUT = {
51+
114.f, 92.5f, // D-pad upper-left (Sony symmetrical)
52+
285.f, 96.f, // face buttons
53+
160.f, 130.f, // left stick lower-left (Sony symmetrical)
54+
240.f, 130.f, // right stick
55+
72.f, 32.f, 162.f, 50.f,
56+
238.f, 32.f, 328.f, 50.f,
57+
58.f, 10.f, 88.f, 36.f,
58+
312.f, 10.f, 342.f, 36.f,
59+
182.f, 88.f, 200.f, 100.f, 218.f, 88.f,
60+
SONY_FACE_A, SONY_FACE_B, SONY_FACE_X, SONY_FACE_Y,
61+
"Share", "PS", "Options"
62+
};
63+
64+
/**
65+
* @brief Selects the layout coordinates for the specified layout type.
66+
*
67+
* @param t Layout type indicating which coordinate set to use (e.g., LayoutType::Sony).
68+
* @return const LayoutCoords& Reference to the corresponding LayoutCoords constant: SONY_LAYOUT when t is LayoutType::Sony, otherwise XBOX_LAYOUT.
69+
*/
70+
static const LayoutCoords& GetLayoutCoords(LayoutType t) {
71+
return t == LayoutType::Sony ? SONY_LAYOUT : XBOX_LAYOUT;
72+
}
73+
74+
/**
75+
* @brief Color used for the "A" face button.
76+
*
77+
* @return ImU32 Packed RGBA color equal to approximately RGB(96, 202, 56) with alpha 255.
78+
*/
1079

1180
static ImU32 ColorA() { return IM_COL32( 96, 202, 56, 255); }
1281
static ImU32 ColorB() { return IM_COL32(228, 56, 56, 255); }
@@ -19,7 +88,16 @@ static ImU32 Body() { return IM_COL32( 50, 50, 55, 255); }
1988
static ImU32 Outline() { return IM_COL32(120, 120, 130, 255); }
2089
static ImU32 BgDim() { return IM_COL32( 35, 35, 38, 180); }
2190

22-
// ── small drawing helpers ────────────────────────────────────
91+
/**
92+
* @brief Draws the controller body and its left/right side panels using the provided layout.
93+
*
94+
* Renders filled, rounded rectangles with outlines to represent the main controller shell
95+
* and the two side panels. Positions, sizes, corner radii and stroke widths are derived
96+
* from the supplied Layout instance and the module's color helpers.
97+
*
98+
* @param dl ImGui draw list used for rendering.
99+
* @param L Layout providing coordinate transforms and scaling for placement and sizes.
100+
*/
23101

24102
static void DrawBody(ImDrawList* dl, const Layout& L) {
25103
ImVec2 tl = L.P(40, 30);
@@ -38,8 +116,20 @@ static void DrawBody(ImDrawList* dl, const Layout& L) {
38116
dl->AddRect(grtl, grbr, Outline(), L.S(20), 0, L.S(2));
39117
}
40118

41-
static void DrawDPad(ImDrawList* dl, const Layout& L, const GamepadState& gs) {
42-
ImVec2 center = L.P(120, 155);
119+
/**
120+
* @brief Renders a D-pad at the given logical center, showing directional presses.
121+
*
122+
* Draws a cross-shaped D-pad centered at (cx, cy) in layout coordinates and highlights
123+
* individual directions when the corresponding D-pad buttons in gs are pressed.
124+
*
125+
* @param dl ImGui draw list used to issue drawing commands.
126+
* @param L Layout helper providing coordinate transforms and scaling.
127+
* @param gs Current gamepad state used to query directional button presses.
128+
* @param cx X coordinate of the D-pad center in logical layout space.
129+
* @param cy Y coordinate of the D-pad center in logical layout space.
130+
*/
131+
static void DrawDPad(ImDrawList* dl, const Layout& L, const GamepadState& gs, float cx, float cy) {
132+
ImVec2 center = L.P(cx, cy);
43133
float arm = L.S(18);
44134
float w = L.S(12);
45135
float rnd = L.S(3);
@@ -74,17 +164,38 @@ static void DrawDPad(ImDrawList* dl, const Layout& L, const GamepadState& gs) {
74164
ImVec2(center.x + arm, center.y + w * 0.5f), Lit(), rnd);
75165
}
76166

77-
static void DrawFaceButtons(ImDrawList* dl, const Layout& L, const GamepadState& gs) {
78-
ImVec2 center = L.P(290, 110);
167+
/**
168+
* @brief Render the four face buttons (A/B/X/Y) with colors, labels, and pressed-state visuals.
169+
*
170+
* Draws four circular face buttons positioned around the given center using the supplied Layout for
171+
* scaling and positioning. Each button is filled with its face color when pressed (or a dim fill when
172+
* not), has a colored outline, and displays the provided label centered on the button. Button press
173+
* state is read from the provided GamepadState.
174+
*
175+
* @param dl ImGui draw list used to issue the rendering commands.
176+
* @param L Layout providing coordinate transforms and scaling.
177+
* @param gs Current gamepad state used to determine which face buttons are pressed.
178+
* @param cx X coordinate (logical) of the face-button group center.
179+
* @param cy Y coordinate (logical) of the face-button group center.
180+
* @param labelA Label text to render on the A button.
181+
* @param labelB Label text to render on the B button.
182+
* @param labelX Label text to render on the X button.
183+
* @param labelY Label text to render on the Y button.
184+
*/
185+
static void DrawFaceButtons(ImDrawList* dl, const Layout& L, const GamepadState& gs,
186+
float cx, float cy,
187+
const char* labelA, const char* labelB,
188+
const char* labelX, const char* labelY) {
189+
ImVec2 center = L.P(cx, cy);
79190
float spread = L.S(18);
80191
float r = L.S(11);
81192

82193
struct FaceBtn { float dx; float dy; Button btn; ImU32 col; const char* label; };
83194
std::array btns = {
84-
FaceBtn{ 0, spread, Button::A, ColorA(), "A" },
85-
FaceBtn{ spread, 0, Button::B, ColorB(), "B" },
86-
FaceBtn{ -spread, 0, Button::X, ColorX(), "X" },
87-
FaceBtn{ 0, -spread, Button::Y, ColorY(), "Y" },
195+
FaceBtn{ 0, spread, Button::A, ColorA(), labelA },
196+
FaceBtn{ spread, 0, Button::B, ColorB(), labelB },
197+
FaceBtn{ -spread, 0, Button::X, ColorX(), labelX },
198+
FaceBtn{ 0, -spread, Button::Y, ColorY(), labelY },
88199
};
89200

90201
for (auto& fb : btns) {
@@ -150,27 +261,37 @@ static void DrawSmallButton(ImDrawList* dl, const Layout& L,
150261
}
151262

152263
/**
153-
* @brief Render a controller panel that visualizes a gamepad's layout and state.
264+
* @brief Render a framed controller panel visualizing a gamepad's layout and current state.
154265
*
155-
* Renders a framed panel at the given position/size containing a header (built from the player
156-
* slot, optional display name, and backend name), a connection-status pill, and a scaled
157-
* representation of the controller showing body, D-pad, face buttons, thumbsticks, bumpers,
158-
* triggers, and small buttons. When the gamepad is not connected, displays a centered
159-
* "No controller detected" message instead of the controls.
266+
* Renders a header containing the player slot, optional display name, and backend name plus a
267+
* connection-status pill, and draws a scaled representation of the controller (body, D‑pad,
268+
* face buttons, thumbsticks, bumpers, triggers, and small buttons) inside the panel. If the
269+
* gamepad is not connected, displays a centered "No controller detected" message instead of
270+
* the controls. If a body texture is provided, it is used for the controller background;
271+
* otherwise the body is drawn procedurally.
160272
*
161-
* @param dl ImGui draw list to use for low-level drawing operations.
273+
* @param dl ImGui draw list used for low-level drawing operations.
162274
* @param panelPos Top-left corner of the panel in screen coordinates.
163275
* @param panelSize Width and height of the panel in screen coordinates.
164276
* @param gs Current gamepad state (buttons, sticks, triggers, connection).
165277
* @param slotIndex Zero-based player slot index used in the header label.
166278
* @param backendName Null-terminated string identifying the input backend (shown in header).
167279
* @param displayName Optional null-terminated display name for the controller; when non-empty
168280
* it is included in the header as "Player N - DisplayName [backend]".
281+
* @param bodyTexture Optional ImGui texture ID to render as the controller body; when null,
282+
* the procedural body is drawn instead.
283+
* @param textureSizeLogical Logical size (width, height) of the provided bodyTexture in the
284+
* same coordinate space used by the renderer.
285+
* @param layoutType LayoutType selecting the coordinate/label set (e.g., Xbox or Sony) used to
286+
* position and label controls within the rendered controller.
169287
*/
170288

171289
void DrawGamepad(ImDrawList* dl, ImVec2 panelPos, ImVec2 panelSize,
172290
const GamepadState& gs, int slotIndex, const char* backendName,
173-
const char* displayName) {
291+
const char* displayName,
292+
ImTextureID bodyTexture,
293+
ImVec2 textureSizeLogical,
294+
LayoutType layoutType) {
174295
dl->AddRectFilled(panelPos,
175296
ImVec2(panelPos.x + panelSize.x, panelPos.y + panelSize.y),
176297
BgDim(), 8.0f);
@@ -228,26 +349,35 @@ void DrawGamepad(ImDrawList* dl, ImVec2 panelPos, ImVec2 panelSize,
228349
return;
229350
}
230351

231-
DrawBody(dl, L);
232-
DrawDPad(dl, L, gs);
233-
DrawFaceButtons(dl, L, gs);
352+
if (bodyTexture != GamepadRenderer::kNoBodyTexture) {
353+
ImVec2 tl = L.P(0.f, 0.f);
354+
ImVec2 br = L.P(textureSizeLogical.x, textureSizeLogical.y);
355+
dl->AddImage(bodyTexture, tl, br, ImVec2(0.f, 0.f), ImVec2(1.f, 1.f));
356+
} else {
357+
DrawBody(dl, L);
358+
}
359+
360+
const LayoutCoords& c = GetLayoutCoords(layoutType);
361+
DrawDPad(dl, L, gs, c.dpadX, c.dpadY);
362+
DrawFaceButtons(dl, L, gs, c.faceX, c.faceY,
363+
c.faceLabelA, c.faceLabelB, c.faceLabelX, c.faceLabelY);
234364

235-
DrawThumbstick(dl, L, L.P(170, 170), gs.leftStickX, gs.leftStickY,
365+
DrawThumbstick(dl, L, L.P(c.leftStickX, c.leftStickY), gs.leftStickX, gs.leftStickY,
236366
gs.IsPressed(Button::LeftThumb));
237-
DrawThumbstick(dl, L, L.P(240, 170), gs.rightStickX, gs.rightStickY,
367+
DrawThumbstick(dl, L, L.P(c.rightStickX, c.rightStickY), gs.rightStickX, gs.rightStickY,
238368
gs.IsPressed(Button::RightThumb));
239369

240-
DrawBumper(dl, L, L.P(70, 30), L.P(160, 48), gs.IsPressed(Button::LeftBumper));
241-
DrawBumper(dl, L, L.P(240, 30), L.P(330, 48), gs.IsPressed(Button::RightBumper));
370+
DrawBumper(dl, L, L.P(c.lbTlX, c.lbTlY), L.P(c.lbBrX, c.lbBrY), gs.IsPressed(Button::LeftBumper));
371+
DrawBumper(dl, L, L.P(c.rbTlX, c.rbTlY), L.P(c.rbBrX, c.rbBrY), gs.IsPressed(Button::RightBumper));
242372

243-
DrawTrigger(dl, L, L.P(50, 2), L.P(80, 28), gs.leftTrigger);
244-
DrawTrigger(dl, L, L.P(320, 2), L.P(350, 28), gs.rightTrigger);
373+
DrawTrigger(dl, L, L.P(c.ltTlX, c.ltTlY), L.P(c.ltBrX, c.ltBrY), gs.leftTrigger);
374+
DrawTrigger(dl, L, L.P(c.rtTlX, c.rtTlY), L.P(c.rtBrX, c.rtBrY), gs.rightTrigger);
245375

246-
DrawSmallButton(dl, L, L.P(175, 100), L.S(28), L.S(12), "Back",
376+
DrawSmallButton(dl, L, L.P(c.backX, c.backY), L.S(28), L.S(12), c.backLabel,
247377
gs.IsPressed(Button::Back));
248-
DrawSmallButton(dl, L, L.P(200, 118), L.S(22), L.S(12), "Guide",
378+
DrawSmallButton(dl, L, L.P(c.guideX, c.guideY), L.S(22), L.S(12), c.guideLabel,
249379
gs.IsPressed(Button::Guide));
250-
DrawSmallButton(dl, L, L.P(225, 100), L.S(28), L.S(12), "Start",
380+
DrawSmallButton(dl, L, L.P(c.startX, c.startY), L.S(28), L.S(12), c.startLabel,
251381
gs.IsPressed(Button::Start));
252382

253383
{

src/gamepad_renderer.h

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,30 @@
2525
*/
2626
namespace GamepadRenderer
2727
{
28+
/**
29+
* @brief Holds an origin and per-axis scale factors for converting logical gamepad coordinates to screen space.
30+
*
31+
* The origin is a screen-space offset applied after scaling. sx and sy are the horizontal and vertical
32+
* scale factors, respectively, used to map logical coordinates into the panel's pixel coordinates.
33+
*/
34+
35+
/**
36+
* @brief Map a logical (x, y) coordinate to a screen-space point using this layout.
37+
* @param x Logical x coordinate.
38+
* @param y Logical y coordinate.
39+
* @returns Screen-space ImVec2 obtained by applying the per-axis scales and then adding the origin offset.
40+
*/
41+
42+
/**
43+
* @brief Scale a scalar value uniformly using the smaller of the two axis scales.
44+
* @param v Value in logical units to scale.
45+
* @returns The value multiplied by min(sx, sy).
46+
*/
47+
enum class LayoutType { Xbox, Sony };
48+
49+
/** Sentinel for "no body texture" (valid for both pointer and integer ImTextureID backends). */
50+
inline constexpr ImTextureID kNoBodyTexture = static_cast<ImTextureID>(0);
51+
2852
struct Layout
2953
{
3054
ImVec2 origin;
@@ -40,5 +64,8 @@
4064

4165
void DrawGamepad(ImDrawList* dl, ImVec2 panelPos, ImVec2 panelSize,
4266
const GamepadState& gs, int slotIndex, const char* backendName,
43-
const char* displayName = nullptr);
67+
const char* displayName = nullptr,
68+
ImTextureID bodyTexture = kNoBodyTexture,
69+
ImVec2 textureSizeLogical = ImVec2(400.f, 280.f),
70+
LayoutType layoutType = LayoutType::Xbox);
4471
} // namespace GamepadRenderer

0 commit comments

Comments
 (0)