Porting Raylib's Gamepad Display example to Rust

Sebastian Lague's most recent video mentioned Raylib and it perked my ears up because LCOLONQ has also mentioned this library on his stream before. I'd opened up the example page when he did and happened to notice a useful looking snippet: the gamepad display. This example is very similar to what I used to use a website for when playing Celeste in order to show the audience that yes, I was pressing the buttons I said I was and wasn't just raging because of skill issues and it really was my gamepad's dead zones screwing me over.

Opening a heavy browser tab to get that display was never my favorite though, and so it made me curious. Could I use Raylib to make a gamepad tracker and have it take up less of my resources so my PC could run the games better?

Installing Raylib

The first issue I had to tackle was the fact that I don't have Raylib on my machine. So I was reading the README and following along with the windows instructions as best I could. They have a few options, and after skimming each I decided to go with the "Manual Setup with W64Devkit" option.

Why? Because I liked the idea of being able to build the source and then use the library. With the IRC client Halloy, I built it from source and have been very happy with the ability to look at its code or tweak things as needed. So I figure, why not, this would be great for that too! So, I popped over to skeeto's repo to pick up the dev kit and downloaded the EXE and ran it. 1

This hooked me up with a shell and I downloaded the raylib repository and went over to the raylib/src/ directory and after a minute or so, it popped out a happy success message letting me know it was done.

~/Documents/Code/OpenSource/raylib/src $ make
ar rcs ../src/libraylib.a rcore.o rshapes.o rtextures.o rtext.o utils.o rglfw.o rmodels.o raudio.o
"raylib static library generated (libraylib.a) in ../src!"
~/Documents/Code/OpenSource/raylib/src $

Great! That was surprisingly easy (I always expect the worse when building any C, C++, or C# project on Windows) So I read the instructions on how to build the examples and of course, seeing as how I only wanted the gamepad sample, I figured I'd just do the instructions on building just that.

gcc core_basic_window.c -lraylib -lgdi32 -lwinmm

And when I tried it?

~/Documents/Code/OpenSource/raylib/src $ cd ../examples/core/
~/Documents/Code/OpenSource/raylib/examples/core $ gcc core_input_gamepad.c -lraylib -lgdi32 -lwinmm
core_input_gamepad.c:22:10: fatal error: raylib.h: No such file or directory
22 | #include "raylib.h"
    |          ^~~~~~~~~~
compilation terminated.

Erm. Well. Okay. It's been a long time since I wrote some C code that needed linking, I think the last time was when I wrote an http server in C in 2014. So you'll have to forgive me that it took a bit to remember how to use link flags. MySQL kind of handled a lot of that hard work when I did that server and it's been over a decade since I had to do this. That being the case, I backed up, and tried out the other option in the README file: building ALL the examples at once:

mingw32-make PLATFORM=PLATFORM_DESKTOP

This worked! And so, I know that raylib is in the right place for the makefile, but just not for if I want to try to compile things from the core folder without specifying where the library lives. Giving up on compiling just the one example for now, I checked to see if the EXE worked and

Wonderful. But, looking at the link errors, it made me think that unless I figured out how to properly set things up, and read that really long Makefile in the examples folder to figure out exactly how it was referencing the raylib library I had built myself... I was going to have trouble making my own little program and customizing it however I wanted.

At which point, I asked myself the question that everyone seems to be asking now-a-days. "Can I just port this to rust?"

Installing the rust crate for raylib

I asked LCOLONQ's discord/irc channel that question. I think the closet I've ever come to foreign function interfaces was at my first internship when I had to setup some kind of machine learning related C++ code to be called from a Java application in a sort of R&D situation. I also found a new job somewhat close to that time, and so I never really got too far there.

Luckily for me though, someone has already done the actual hard part here. There's a raylib crate! and it's setup to provide a rust-like interface to the library, as well as offering an escape hatch to call the methods directly if needed. Awesome!

cargo new gamepad
cargo add raylib

Cargo setup a hellow world for me, downloaded the raylib rust crate for me, and then I happily ran build to verify things were good and

cargo build
 > thread 'main' panicked at .cargo\registry\src\index.crates.io-1949cf8c6b5b557f\bindgen-0.70.1\lib.rs:622:27: 
   Unable to find libclang: "couldn't find any valid shared libraries matching: ['clang.dll', 'libclang.dll'], 

things were not good.

Apparently, I can't escape the "you don't have the right library on your system" problem even with rust. That said, this is notably different since it's not a static library I'm missing, but rather a compiler. The raylib crate relies on bindgen, and binggen requires clang. Thankfully, we can use chocolately for that. and installing chocolately is one cmd.exe command away!

@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "[System.Net.ServicePointManager]::SecurityProtocol = 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))" && SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"

And when that nightmare of a thing that I'm going to trust is right2 finished. I went ahead and ran the much simpler and easy to ready:

choco install llvm

Then, tried one more time.

$ cargo build
    Compiling raylib-sys v5.5.1
    Compiling raylib v5.5.1
    Compiling gamepad v0.1.0 (Code\Personal\tools\gamepad)
        Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.90s

Beautiful!

Porting the Example

With the ability to compile a program including the raylib library in it. I went ahead and swapped the autogenerated "Hello world!" test from cargo init for the test example from the raylib documentation:

use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Hello, World")
        .build();
    
    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);
        
        d.clear_background(Color::WHITE);
        d.draw_text("Hello, world!", 12, 12, 20, Color::BLACK);
    }
}

This is surprisingly easy to read and simple example. Normally getting a window up and running in windows is a lot of work. It makes me a bit sad that I didn't see this library when I was working on that sentiment vtuber project a while ago. Anyway, the most important question is if this actually runs though.

Perfect.

Now we can being porting the bits of the actual example c code over for fun. Let's start with something simple first. The window size and that config flag. The C code looks like this:

// Initialization
//--------------------------------------------------------------------------------------
const int screenWidth = 800;
const int screenHeight = 450;

SetConfigFlags(FLAG_MSAA_4X_HINT);  // Set MSAA 4X hint before windows creation

InitWindow(screenWidth, screenHeight, "raylib [core] example - gamepad input");

I moved the consts up to the top level of the program, since I don't think it's worth it to declare them only within the main function. But, what type are they? I mean, they're ints in C, but did the rust interface change anything around to make them unsigned (a negative screen height or width doesn't make sense to me after all).

const SCREEN_WIDTH: i32 = 800;
const SCREEN_HEIGHT: i32 = 450;
...
fn main() {
    let (mut rl, thread) = raylib::init()
        .size(SCREEN_WIDTH, SCREEN_HEIGHT)
        .title("Game Pad output")

After the compiler yelled at me for passing in u32, (it makes sense, an int in c is signed by default after all) I then had to turn to the mysterious MSAA 4X hint. I don't... know what that means. I mean, it has to happen before the window exists. So it's got something to do with the window itself, maybe graphical. Searching for "msaa" in the rust crate turns up it's documentation

Hints that 4x MSAA (anti-aliasing) should be enabled. The system’s graphics drivers may override this setting.

Ah, anti-aliasing! And this is a method on the RaylibBuilder which is what this ::init() method has handed to us, so it's just another simple line on our code so far!

let (mut rl, thread) = raylib::init()
    .size(SCREEN_WIDTH, SCREEN_HEIGHT)
    .title("Game Pad output")
    .msaa_4x()
    .build();

Well that was easy! And hey, the compiler is still happy with me too. Cool. Next up in the C code is something slightly more interesting:

Texture2D texPs3Pad = LoadTexture("resources/ps3.png");
Texture2D texXboxPad = LoadTexture("resources/xbox.png");

And, this being C, if you load up something into memory, you've got to remove it from memory too. At the bottom of the Raylib example is

// De-Initialization
//--------------------------------------------------------------------------------------
UnloadTexture(texPs3Pad);
UnloadTexture(texXboxPad);

CloseWindow();        // Close window and OpenGL context

So, I did a quick look for the word "texture" in the crate docs and spotted the load_texture method of the RaylibHandle. Raylib handle... that rl variable from the hello world feels sort of like that. Looking at the return type for the build method (RaylibHandle, RaylibThread) confirms this. And so, porting the c code to load the resources is easy!

let xbox_texture = rl.load_texture(&thread, "resources/xbox.png")
    .expect("Cannot run program if gamepad texture (xbox) missing");
let ps3_texture  = rl.load_texture(&thread, "resources/ps3.png")
    .expect("Cannot run program if gamepad texture (ps3) missing");

The load_texture method returns a Result, if I can't load up the background file for the gamepad there's no point in running the program. So I just let it crash. I probably don't need to port the PS3 related resource since my gamepad is an xbox style one, but I'm just following along for now and that's not too much more to type. Running the code with these two lines added in let's me confirm that I've got the files in the right place:

Looks like putting my resources folder next to src was the right place. And the logging in raylib is really nice. I wasn't drawing the textures yet since we haven't ported that part of the code yet, but I can clearly see the log telling me that it found and loaded up the image succesfully. Awesome.

Now, what about UnloadTexture(...)? Well, there is an unload_texture method on the handle we can use. But, if I try to pass in xbox_texture to it

error[E0308]: mismatched types
    --> gamepad\src\main.rs:49:32
    |
49  |     rl.unload_texture(&thread, xbox_texture);
    |        --------------          ^^^^^^^^^^^^ expected `WeakTexture2D`, found `Texture2D`
    |        |
    |        arguments to this method are incorrect

Blindly following this, I thought to myself: "well, how do I get a weak texture then"? A quick search in the create found make_weak defined on the texture itself. Sweet! Well that's easy, let's just call that and

call to unsafe function `Texture2D::make_weak` is unsafe and requires unsafe block
    --> gamepad\src\main.rs:49:32
    |
 49 |     rl.unload_texture(&thread, xbox_texture.make_weak());
    |                                ^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe function
    |
    = note: consult the function's documentation for information on how to avoid undefined behavior

Wait, wait, wait. Unsafe? Let's back up and think for a second on the question I should have asked when I saw the other error. What the heck is a weak reference in the first place? The doc says a few things in the conversion methods on the type. Specifically mentioning that you have to manually deal with the memory yourself if you're using it.

So... that would imply then that with a non weak reference, I don't have to deal with unloading it. Right? I went about trying to confirm this and popped open the hood on the source code of the Texture2D struct and what did I find?

make_thin_wrapper!(
    /// Texture, tex data stored in GPU memory (VRAM)
    Texture2D,
    ffi::Texture2D,
    ffi::UnloadTexture
);

Macros! This macro is doing... something. It's making a thin wrapper I guess based on the name. So, what's that exactly? Well, looking at that it's full of stuff I don't understand and makes my eyes go crosseyed:

macro_rules! make_thin_wrapper {
    ($(#[$attrs:meta])* $name:ident, $t:ty, $dropfunc:expr) => {
        make_thin_wrapper!($(#[$attrs])* $name, $t, $dropfunc, true);
    };
    ($(#[$attrs:meta])* $name:ident, $t:ty, $dropfunc:expr, false) => {
        $(#[$attrs])*
        #[repr(transparent)]
        #[derive(Debug)]
        pub struct $name(pub(crate) $t);

        impl_wrapper!($name, $t, $dropfunc, 0);
        gen_from_raw_wrapper!($name, $t, $dropfunc, 0);
    };
    ($(#[$attrs:meta])* $name:ident, $t:ty, $dropfunc:expr, true) => {
        $(#[$attrs])*
        #[repr(transparent)]
        #[derive(Debug)]
        pub struct $name(pub(crate) $t);

        impl_wrapper!($name, $t, $dropfunc, 0);
        deref_impl_wrapper!($name, $t, $dropfunc, 0);
        gen_from_raw_wrapper!($name, $t, $dropfunc, 0);
    };
}

But, what I can tell just based on the structure of this and the names, is that that third argument is a drop function expression. Which implies to me that that previous callsite we saw is giving ffi::UnloadTexture as the function to become the Drop method for the implementation of this thin wrapper around the Texture2D struct. That means that the unload is called when the variable is dropped, confirming that we don't need to call that ourselves and we can let the lifetime of the variable deal with the clean up. This is confirmed by the logging as well:

With that potential memory leak behind us, I can bring over a bit more from the C code.

// Set axis deadzones
const float leftStickDeadzoneX = 0.1f;
const float leftStickDeadzoneY = 0.1f;
const float rightStickDeadzoneX = 0.1f;
const float rightStickDeadzoneY = 0.1f;
const float leftTriggerDeadzone = -0.9f;
const float rightTriggerDeadzone = -0.9f;

SetTargetFPS(60);               // Set our game to run at 60 frames-per-second

More constants which aren't worth mentioning, and one call to configure the FPS. As before, I did a search for the phrase "fps" and turned up set_target_fps on the raylib handle, which makes for a pretty analogous port:

rl.set_target_fps(TARGET_FPS);

That's it for the configuration, now we get to the slightly interactive part. In the C code, within the main loop, we need to make sure the gamepad is connected.

int gamepad = 0; // which gamepad to display
// Main game loop
while (!WindowShouldClose())    // Detect
{
    BeginDrawing();

        ClearBackground(RAYWHITE);

        if (IsKeyPressed(KEY_LEFT) && gamepad > 0) gamepad--;
        if (IsKeyPressed(KEY_RIGHT)) gamepad++;

        if (IsGamepadAvailable(gamepad))
        {
            ...
        }
        else
        {
            DrawText(TextFormat("GP%d: NOT DETECTED", gamepad), 10, 10, 10, GRAY);
            DrawTexture(texXboxPad, 0, 0, LIGHTGRAY);
        }
    
    EndDrawing();
}

I'm omitting a lot, but my first question was if that gamepad variable is somehow being changed by anything besides the arrow keys. I have a vague recollection of some DirectX code I wrote from a book before where one passed in a reference and then out popped the ID of something into the variable you gave to the method. I don't think that's what's going on here though. But most likely this more relates to how many game pads are connected?

This gamepad-- and gamepad++ on the arrow keys is swapping the number around, but what is it. Digging deeper into raylib's source I can see

// Check if a gamepad is available
bool IsGamepadAvailable(int gamepad)
{
    bool result = false;

    if ((gamepad < MAX_GAMEPADS) && CORE.Input.Gamepad.ready[gamepad]) result = true;

    return result;
}

So it's an index. Into an array where the max gamepads is the limit. And that would be 4. Okay.

So, I suppose that deep within the bowels of the library, within its event loop, we're listening to game pads connecting. If we've got multiple pads plugged in (up to 4) then we'll potentially have those to loop through, and that way this little demo can you show which is which and what each one is doing. Neat. Mystery solved, but also, I own one gamepad. I don't care about the others, so I think we can ditch the key presses here unless I'm worried that somehow my gamepad might be registered as number 2.

I doubt it. So, movin along then. Let's just make it easy to tell that the program has detected the pad or not within the rust code.

// Raylib supports up to 4 gamepads, if you just have one connected, it's ID 0.
let gamepad_to_display = 0;

while !rl.window_should_close() {
    let mut d = rl.begin_drawing(&thread);

    if d.is_gamepad_available(gamepad_to_display) {
        d.draw_text(&format!("Gamepad: {gamepad_to_display}"), 60, 60, 20, Color::BLACK);
    } else {
        d.draw_text(&format!("No Gamepad: {gamepad_to_display}"), 60, 60, 20, Color::BLACK);
    }

    d.clear_background(Color::WHITE);
    d.draw_text("Hello, world!", 12, 12, 20, Color::BLACK);
}

Since is_gamepad_available is declared on the RaylibHandle, I initially attempted to use rl.is_gamepad_available and, well:

error[E0502]: cannot borrow `rl` as immutable because it is also borrowed as mutable
--> gamepad\src\main.rs:37:12
    |
35  |         let mut d = rl.begin_drawing(&thread);
    |                     -- mutable borrow occurs here
36  |
37  |         if rl.is_gamepad_available(gamepad_to_display) {
    |            ^^ immutable borrow occurs here
    ...
40  |             d.draw_text(&format!("No Gamepad: {gamepad_to_display}"), 6, 6, 20, Color::BLACK);
    |             - mutable borrow later used here

That netted me an error because it looks like once we start drawing, we've borrowed the handle and now need to use the d which allows for the underlying drawing contexts to change and mutate. The documentation for the method states

Setup canvas (framebuffer) to start drawing. Prefer using the closure version, RaylibHandle::draw. This version returns a handle that calls raylib_sys::EndDrawing at the end of the scope and is provided as a fallback incase you run into issues with closures(such as lifetime or performance reasons)

Well. Preference or not, I'm working with the example they gave us so I'm going to continue using this bit for now. If I can move to the closure version later, then maybe I will if it makes sense to. But then again, the docstring seems to imply that it's more efficient to not use closures here? So maybe I'll just back away slowly and leave it alone.

Running the code and connecting, then disconnecting my gamepad shows the code is working:

And so we can move on to grabbing the inputs for the various sticks. The C code is straightforward, we need to grab both axes of each stick, and then the two triggers also have a float value since one can press them gently. So all of these values are between -1 to 1. Even the triggers. They range from -1 to 1, where 0 is actually the midpoint in the squeeze on the trigger.

// Get axis values
float leftStickX = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_X);
float leftStickY = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_Y);
float rightStickX = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_RIGHT_X);
float rightStickY = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_RIGHT_Y);
float leftTrigger = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_TRIGGER);
float rightTrigger = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_RIGHT_TRIGGER);

The rust binding is kind to us, we've got an enumeration to use for the buttons and as it seems to be the case with most of these functions, we want to call these through the library handle:

let left_stick_x = rl.get_gamepad_axis_movement(gamepad_to_display, GamepadAxis::GAMEPAD_AXIS_LEFT_X);
let left_stick_y = rl.get_gamepad_axis_movement(gamepad_to_display, GamepadAxis::GAMEPAD_AXIS_LEFT_Y);
let right_stick_x = rl.get_gamepad_axis_movement(gamepad_to_display, GamepadAxis::GAMEPAD_AXIS_RIGHT_X);
let right_stick_y = rl.get_gamepad_axis_movement(gamepad_to_display, GamepadAxis::GAMEPAD_AXIS_RIGHT_Y);
let left_trigger = rl.get_gamepad_axis_movement(gamepad_to_display, GamepadAxis::GAMEPAD_AXIS_LEFT_TRIGGER);
let right_trigger = rl.get_gamepad_axis_movement(gamepad_to_display, GamepadAxis::GAMEPAD_AXIS_RIGHT_TRIGGER);

So, we can call these before the call to begin_drawing if we want to use the immutable reference to the handle, and it doesn't really seem to do any harm if we want to do so after. But, that just feels wrong to me. For some reason, I have this sneaking suspicion that doing any additional computations in the middle of telling the computer to draw something is probably a silly idea.

What's also a silly idea is expecting these sticks and triggers to return a perfect 1 or -1. There's a reason we've got constants for dead zones. The C code clamps the values to 0 or -1 depending on if its a stick or trigger and if it's hit the range we setup constants for:

// Calculate deadzones
if (leftStickX > -leftStickDeadzoneX && leftStickX < leftStickDeadzoneX) leftStickX = 0.0f;
if (leftStickY > -leftStickDeadzoneY && leftStickY < leftStickDeadzoneY) leftStickY = 0.0f;
if (rightStickX > -rightStickDeadzoneX && rightStickX < rightStickDeadzoneX) rightStickX = 0.0f;
if (rightStickY > -rightStickDeadzoneY && rightStickY < rightStickDeadzoneY) rightStickY = 0.0f;
if (leftTrigger < leftTriggerDeadzone) leftTrigger = -1.0f;
if (rightTrigger < rightTriggerDeadzone) rightTrigger = -1.0f;

This code can be brought over pretty much verbatim, provided we update all the variabels we just declared to be mut

// Then filter out noise via deadzones
if -DEADZONE_LEFT_STICK_X < left_stick_x && left_stick_x < DEADZONE_LEFT_STICK_X {
    left_stick_x = 0.0;
}
if -DEADZONE_LEFT_STICK_Y < left_stick_y && left_stick_y < DEADZONE_LEFT_STICK_Y {
    left_stick_y = 0.0;
}
if -DEADZONE_RIGHT_STICK_X < right_stick_x && right_stick_x < DEADZONE_RIGHT_STICK_X {
    right_stick_x = 0.0;
}
if -DEADZONE_RIGHT_STICK_Y < right_stick_y && right_stick_y < DEADZONE_RIGHT_STICK_Y {
    right_stick_y = 0.0;
}
if left_trigger < DEADZONE_LEFT_TRIGGER {
    left_trigger = -1.0;
}
if right_trigger < DEADZONE_RIGHT_TRIGGER {
    right_trigger = -1.0;
}

Though, given the way rust doesn't use parenthesis around conditions makes this feel a bit more vertically bulky than the C code. Ah well. I'm sure that somewhere out there, someone is screaming at the screen to me, but I can't hear you. Though if you know how to write this sort of inverse clamp with a built in method let me know. I did go look at float's std library page to see if I could spot anything useful and failed.

Putting aside code aesthetics, rather than doing the equavalent of println to the window with text. Let's Go ahead and jump ahead to grab the stick rendering code from the C rather than proceed to the button input fetching:

if (TextFindIndex(TextToLower(GetGamepadName(gamepad)), XBOX_ALIAS_1) > -1 || TextFindIndex(TextToLower(GetGamepadName(gamepad)), XBOX_ALIAS_2) > -1)
{
    DrawTexture(texXboxPad, 0, 0, DARKGRAY);
    // Draw axis: left joystick
    Color leftGamepadColor = BLACK;
    if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_THUMB)) leftGamepadColor = RED;
    DrawCircle(259, 152, 39, BLACK);
    DrawCircle(259, 152, 34, LIGHTGRAY);
    DrawCircle(259 + (int)(leftStickX*20),
            152 + (int)(leftStickY*20), 25, leftGamepadColor);

    // Draw axis: right joystick
    Color rightGamepadColor = BLACK;
    if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_THUMB)) rightGamepadColor = RED;
    DrawCircle(461, 237, 38, BLACK);
    DrawCircle(461, 237, 33, LIGHTGRAY);
    DrawCircle(461 + (int)(rightStickX*20),
            237 + (int)(rightStickY*20), 25, rightGamepadColor);

    // Draw axis: left-right triggers
    DrawRectangle(170, 30, 15, 70, GRAY);
    DrawRectangle(604, 30, 15, 70, GRAY);
    DrawRectangle(170, 30, 15, (int)(((1 + leftTrigger)/2)*70), RED);
    DrawRectangle(604, 30, 15, (int)(((1 + rightTrigger)/2)*70), RED);

We have, not only the axis values we just pulled out in use here, but also the color changing based on whether or not the stick is currently clicked in or not. So this gives us an example of how to check for buttons being down as well. How nice! This means I can probably skip the explanations for some of the other bits of this unless anything stands out as notable. But let's dig in and port this over to rust. None of the magic numbers are going to change here So really it's just a matter of finding the right methods on the handle to use.

As I've said earlier, I don't actually care about the PS3. So, that first conditional that's checking that the index is what's expected seems unneccesary to me for my purposes. So let's ditch it, and just assume.

d.draw_texture(&xbox_texture, 0, 0, Color::DARKGRAY);

Drawing the PNG we have from one corner of the box up works just fine since those SCREEN_WIDTH and SCREEN_HEIGHT are actually just the size of the PNG resource we've got. Simple. Tinting it dark grey allows the white game pad of the original image to take on a color that's nice to look at. Right?

Right. That's the one texture. Let's translate the sticks and triggers then, I'll start off with the left stick since the right will just be a mirror of it:

let left_gamepad_color = if rl.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_LEFT_THUMB) {
    Color::RED;
} else {
    Color::BLACK;
};
... after the drawing start ...
d.draw_circle(259, 152, 39.0, Color::BLACK);
d.draw_circle(259, 152, 34.0, Color::LIGHTGRAY);
d.draw_circle(
    259 + (left_stick_x as i32 * 20),
    152 + (left_stick_y as i32 * 20), 
    25.0, left_gamepad_color
);

And I should have a left stick...

Ah, wups. I rounded off our float too soon!

d.draw_circle(259, 152, 39.0, Color::BLACK);
d.draw_circle(259, 152, 34.0, Color::LIGHTGRAY);
d.draw_circle(
    259 + (left_stick_x * 20.0) as i32,
    152 + (left_stick_y * 20.0) as i32, 
    25.0, left_gamepad_color
);

Much better. You can see how much more smooth it is. Believe it or not, that janky snapping in the first one is the same sort of movements. The right stick is the same, but slightly different numbers for placement. 259 beomes 461, 152 to 237, 39 to 38, and 34 to 33. The rest stays the same.

The trigger code is nearly the same as the C code, we just have to put .0 next to more things in order for the rust compiler not to get mad at us:

// left and right triggers
d.draw_rectangle(170, 30, 15, 70, Color::GRAY);
d.draw_rectangle(604, 30, 15, 70, Color::GRAY);
d.draw_rectangle(170, 30, 15, ((1.0 + left_trigger) /2.0 * 70.0) as i32, Color::RED);
d.draw_rectangle(604, 30, 15, ((1.0 + right_trigger) /2.0 * 70.0) as i32, Color::RED);

The way this code works is that when the trigger is fully pulled in, the value will be 1. This results in us doing 1 + 1, then dividing that 2 by a 2 to get a 1. Which results in the height parameter becoming the same as the background gray color (70 pixels tall).

When the trigger isn't pulled, and we're in the dead zone, we've got a -1, and so we end up with a 0 being multiplied by the rest of the terms, resulting in no height at all. Which shows just the gray background to the user. Neat right?

The only thing left of course are the buttons. This includes start, select, the D-pad, the ABYX, and the shoulder buttons. All of these are straightforward ports from C

// Draw buttons: basic
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_MIDDLE_RIGHT)) DrawCircle(436, 150, 9, RED);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_MIDDLE_LEFT)) DrawCircle(352, 150, 9, RED);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_LEFT)) DrawCircle(501, 151, 15, BLUE);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN)) DrawCircle(536, 187, 15, LIME);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT)) DrawCircle(572, 151, 15, MAROON);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_UP)) DrawCircle(536, 115, 15, GOLD);

// Draw buttons: d-pad
DrawRectangle(317, 202, 19, 71, BLACK);
DrawRectangle(293, 228, 69, 19, BLACK);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_UP)) DrawRectangle(317, 202, 19, 26, RED);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_DOWN)) DrawRectangle(317, 202 + 45, 19, 26, RED);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_LEFT)) DrawRectangle(292, 228, 25, 19, RED);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_RIGHT)) DrawRectangle(292 + 44, 228, 26, 19, RED);

// Draw buttons: left-right back
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_TRIGGER_1)) DrawCircle(259, 61, 20, RED);
if (IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_TRIGGER_1)) DrawCircle(536, 61, 20, RED);
                

to rust

// select and start
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_MIDDLE_LEFT) {
    d.draw_circle(436, 150, 9.0, Color::RED);
}
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_MIDDLE_RIGHT) {
    d.draw_circle(352, 150, 9.0, Color::RED);
}

// face buttons
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_RIGHT_FACE_LEFT) {
    d.draw_circle(501, 151, 15.0, Color::BLUE);
}
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_RIGHT_FACE_DOWN) {
    d.draw_circle(536, 187, 15.0, Color::LIME);
}
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_RIGHT_FACE_RIGHT) {
    d.draw_circle(572, 151, 15.0, Color::MAROON);
}
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_RIGHT_FACE_UP) {
    d.draw_circle(536, 115, 15.0, Color::GOLD);
}

// Draw buttons: d-pad
d.draw_rectangle(317, 202, 19, 71, Color::BLACK);
d.draw_rectangle(293, 228, 69, 19, Color::BLACK);
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_LEFT_FACE_UP) {
    d.draw_rectangle(317, 202, 19, 26, Color::RED);
}
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_LEFT_FACE_DOWN) {
    d.draw_rectangle(317, 202 + 45, 19, 26, Color::RED);
}
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_LEFT_FACE_LEFT) {
    d.draw_rectangle(292, 228, 25, 19, Color::RED);
}
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_LEFT_FACE_RIGHT) {
    d.draw_rectangle(292 + 44, 228, 26, 19, Color::RED);
}

// Draw buttons: left-right shoulder buttons
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_LEFT_TRIGGER_1) {
    d.draw_circle(239, 82, 20.0, Color::RED);
}
if d.is_gamepad_button_down(gamepad_to_display, GamepadButton::GAMEPAD_BUTTON_RIGHT_TRIGGER_1) {
    d.draw_circle(557, 82, 20.0, Color::RED);
}
                

With the result being exactly what you'd expect:

Wrap up

Now, the reason I'd want this, like I said before. Is to show what inputs I'm sending when I play a game that requires tight timing and skill. Mainly because I've gone through several controllers over the years and they've worn down over time, which sometimes results in me pressing the buttons and it no registering, or worse, pressing on the stick, but the dead zones being miscalibrated from drift over the years and resulting in nonsense happening.

Either way, this sort of thing is handy. Not having to run a tab in a browser to get one of those game input sites to show a nice gamepad is really great. A browser window is going to take up half a gig of memory nowadays if it's doing anything interesting it seems. So something that's lightweight and takes up a miniscule amount of memory and CPU is even better in making sure that I don't get lag when I stream and play.

I wasn't sure if the capturing would work once the gamepad was no longer in focus, so I booted up Celeste to check it out: