Files
ReSkate/Launcher/Test/gamepad_input_tests.cpp
T
NorthDroplet 21d5a0ba4b Launcher: the left stick scrolls under the focus or the pointer
ImGui 1.91 moves the focus with the D-pad only; its left stick scrolls the
focused window, which under a pointer is usually the page itself. The stick
also handed the screen from the pointer to the focus, so scrolling flipped
between the two.

The launcher now scrolls itself: the nearest scrollable panel around the
focus while the focus is shown, else the panel under the pointer or the
mouse (as wheel notches). Scrolling no longer switches between them.
2026-10-05 15:26:41 +00:00

292 lines
12 KiB
C++

// Controller input in the launcher: focus, back, the trackpad pointer and its click,
// checked through real ImGui frames.
#include "Launcher/gamepad_input.h"
#include <imgui.h>
#include <imgui_internal.h>
#include <iostream>
#include <optional>
#include <string>
using dingosdk::launcher_gui::PadFeed;
using dingosdk::launcher_gui::PadState;
namespace {
int failures = 0;
void check(bool condition, const char* what) {
if (condition) return;
std::cerr << "FAIL: " << what << '\n';
++failures;
}
constexpr std::uint16_t dpad_down = 0x0002, dpad_right = 0x0008, right_thumb = 0x0080, button_a = 0x1000, button_b = 0x2000;
struct Seen {
std::string pressed; // the button that reported a press this frame
bool escape{};
bool menu_open{};
float list_scroll{};
};
// One launcher frame: the pad goes in before NewFrame, as gui.cpp does it.
// Two buttons stacked at the top left, and a menu popup opened on request.
Seen frame(PadFeed& feed, const PadState& pad, bool open_menu = false, bool panels = false, bool list = false) {
auto& io = ImGui::GetIO();
io.DisplaySize = ImVec2(800, 600);
io.DeltaTime = 1.0f / 60.0f;
feed.update(io, pad, 1.0f);
ImGui::NewFrame();
Seen seen;
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(ImVec2(800, 600));
ImGui::Begin("##root", nullptr, ImGuiWindowFlags_NoDecoration);
ImGui::SetCursorScreenPos(ImVec2(100, 100));
if (ImGui::InvisibleButton("first", ImVec2(200, 50), ImGuiButtonFlags_EnableNav)) seen.pressed = "first";
ImGui::SetCursorScreenPos(ImVec2(100, 200));
if (ImGui::InvisibleButton("second", ImVec2(200, 50), ImGuiButtonFlags_EnableNav)) seen.pressed = "second";
if (panels) {
// A page's rail and content, side by side, as Settings and Mods lay them out.
ImGui::SetCursorScreenPos(ImVec2(400, 200));
ImGui::BeginChild("##rail", ImVec2(150, 200), ImGuiChildFlags_NavFlattened);
if (ImGui::Button("tab", ImVec2(120, 40))) seen.pressed = "tab";
ImGui::EndChild();
ImGui::SetCursorScreenPos(ImVec2(580, 200));
ImGui::BeginChild("##content", ImVec2(200, 200), ImGuiChildFlags_NavFlattened);
if (ImGui::Button("option", ImVec2(120, 40))) seen.pressed = "option";
ImGui::EndChild();
}
if (list) {
// A long list in a panel, like GET MODS.
ImGui::SetCursorScreenPos(ImVec2(400, 100));
ImGui::BeginChild("##list", ImVec2(300, 200), ImGuiChildFlags_NavFlattened);
for (int row = 0; row < 30; ++row) {
ImGui::PushID(row);
if (ImGui::Selectable("##row", false, 0, ImVec2(280, 40))) seen.pressed = "row " + std::to_string(row);
ImGui::PopID();
}
seen.list_scroll = ImGui::GetScrollY();
ImGui::EndChild();
}
if (open_menu) ImGui::OpenPopup("##menu");
if (ImGui::BeginPopup("##menu")) {
seen.menu_open = true;
ImGui::MenuItem("Details");
ImGui::EndPopup();
}
seen.escape = ImGui::IsKeyPressed(ImGuiKey_Escape, false);
ImGui::End();
ImGui::Render();
return seen;
}
PadState pad(std::uint16_t buttons = 0, std::int16_t right_x = 0, std::int16_t right_y = 0, std::int16_t left_y = 0) {
PadState state;
state.left_y = left_y;
state.connected = true;
state.buttons = buttons;
state.right_x = right_x;
state.right_y = right_y;
return state;
}
// Presses and lets go, returning what the frames saw.
std::string press(PadFeed& feed, std::uint16_t button) {
std::string pressed = frame(feed, pad(button)).pressed;
const auto released = frame(feed, pad()).pressed;
return pressed.empty() ? released : pressed;
}
void fresh() {
if (ImGui::GetCurrentContext()) ImGui::DestroyContext();
ImGui::CreateContext();
auto& io = ImGui::GetIO();
io.IniFilename = nullptr;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
io.Fonts->AddFontDefault();
io.Fonts->Build();
}
} // namespace
int main() {
// ------------------------------------------------ focus
{
fresh();
PadFeed feed;
frame(feed, pad());
frame(feed, pad());
press(feed, dpad_down);
check(ImGui::GetIO().NavVisible, "The D-pad shows the focus");
// The first move lands on the first item; the next one goes down.
press(feed, dpad_down);
check(press(feed, button_a) == "second", "A presses the item the D-pad moved to");
check(!feed.pointing() && !ImGui::GetIO().MouseDrawCursor, "No pointer is drawn while the focus is used");
}
{
fresh();
PadFeed feed;
frame(feed, pad());
frame(feed, pad(button_a));
check(ImGui::IsKeyDown(ImGuiKey_GamepadFaceDown), "A (Cross) is the gamepad's activate button");
PadState gone; // unplugged with A held
frame(feed, gone);
frame(feed, gone);
check(!ImGui::IsKeyDown(ImGuiKey_GamepadFaceDown), "An unplugged pad lets go of A");
check(!(ImGui::GetIO().BackendFlags & ImGuiBackendFlags_HasGamepad), "An unplugged pad is no gamepad");
}
// ------------------------------------------------ back
{
fresh();
PadFeed feed;
frame(feed, pad());
frame(feed, pad());
frame(feed, pad(button_b));
check(ImGui::IsKeyPressed(ImGuiKey_Escape, false), "B is Escape with nothing open");
frame(feed, pad());
check(!ImGui::IsKeyDown(ImGuiKey_Escape), "Letting go of B lets go of Escape");
}
{
fresh();
PadFeed feed;
frame(feed, pad());
check(frame(feed, pad(), true).menu_open, "The menu opens");
frame(feed, pad());
const auto with_b = frame(feed, pad(button_b));
const auto after = frame(feed, pad());
check(!after.menu_open, "B closes an open menu");
check(!with_b.escape && !after.escape, "Closing the menu is not also Escape, which would leave the page");
}
{
fresh();
PadFeed feed;
frame(feed, pad());
ImGui::GetIO().AddKeyEvent(ImGuiKey_Escape, true); // the keyboard's Escape, held
frame(feed, pad());
frame(feed, pad());
check(ImGui::IsKeyDown(ImGuiKey_Escape), "An idle pad does not let go of the keyboard's Escape");
}
{
fresh();
PadFeed feed;
for (int i = 0; i < 2; ++i) frame(feed, pad(), false, true);
const auto tap = [&](std::uint16_t button) { frame(feed, pad(button), false, true); frame(feed, pad(), false, true); };
tap(dpad_down); // onto "first"
tap(dpad_down); // "second", level with the panels
tap(dpad_right); // the rail
tap(dpad_right); // the content
std::string pressed = frame(feed, pad(button_a), false, true).pressed;
const auto released = frame(feed, pad(), false, true).pressed;
check((pressed.empty() ? released : pressed) == "option",
"The D-pad crosses from a page's rail into its content");
}
// ------------------------------------------------ scrolling
{
fresh();
PadFeed feed;
const auto tap = [&](std::uint16_t button) { frame(feed, pad(button), false, false, true); frame(feed, pad(), false, false, true); };
for (int i = 0; i < 2; ++i) frame(feed, pad(), false, false, true);
tap(dpad_right); // from nothing onto the list's first row
tap(dpad_right);
float scroll{};
for (int i = 0; i < 30; ++i) scroll = frame(feed, pad(0, 0, 0, -32767), false, false, true).list_scroll;
scroll = frame(feed, pad(), false, false, true).list_scroll;
check(scroll > 100, "The left stick scrolls the panel holding the focus");
check(!feed.pointing() && GImGui->NavCursorVisible && GImGui->NavHighlightItemUnderNav,
"Scrolling stays with the focus, not the mouse");
for (int i = 0; i < 60; ++i) frame(feed, pad(0, 0, 0, 32767), false, false, true);
check(frame(feed, pad(), false, false, true).list_scroll == 0, "Pushing up scrolls back to the top");
}
{
fresh();
PadFeed feed;
ImGui::GetIO().AddMousePosEvent(500, 200); // over the list
for (int i = 0; i < 2; ++i) frame(feed, pad(), false, false, true);
frame(feed, pad(0, 0, 0, 0), false, false, true);
frame(feed, pad(right_thumb), false, false, true); // the pointer shows, without a click
frame(feed, pad(), false, false, true);
float scroll{};
for (int i = 0; i < 30; ++i) scroll = frame(feed, pad(0, 0, 0, -32767), false, false, true).list_scroll;
scroll = frame(feed, pad(), false, false, true).list_scroll;
check(scroll > 100, "The left stick scrolls the panel under the pointer");
check(feed.pointing(), "Scrolling keeps the pointer");
}
{
fresh();
PadFeed feed;
ImGui::GetIO().AddMousePosEvent(500, 200); // the mouse resting over the list
for (int i = 0; i < 2; ++i) frame(feed, pad(), false, false, true);
float scroll{};
for (int i = 0; i < 30; ++i) scroll = frame(feed, pad(0, 0, 0, -32767), false, false, true).list_scroll;
scroll = frame(feed, pad(), false, false, true).list_scroll;
check(scroll > 100, "The left stick scrolls the panel under the mouse");
}
// ------------------------------------------------ trackpad pointer
{
fresh();
PadFeed feed;
auto& io = ImGui::GetIO();
io.AddMousePosEvent(400, 300);
feed.mouse_moved(ImVec2(400, 300));
frame(feed, pad());
frame(feed, pad(0, 3000, 0));
check(!feed.pointing() && io.MousePos.x == 400, "A thumb resting near the middle does not move the pointer");
frame(feed, pad(0, 16000, 0));
frame(feed, pad());
check(feed.pointing() && io.MouseDrawCursor, "Tilting takes the pointer and draws it");
check(io.MousePos.x > 400 && io.MousePos.y == 300, "Tilting right moves the pointer right, from the mouse's place");
for (int i = 0; i < 120; ++i) frame(feed, pad(0, 32767, 32767));
frame(feed, pad());
check(io.MousePos.x == 799 && io.MousePos.y == 0, "The pointer stays inside the window");
// The system cursor stays where the mouse left it, and is reported every frame.
io.AddMousePosEvent(400, 300);
frame(feed, pad());
frame(feed, pad());
check(io.MousePos.x == 799, "The system cursor's old place does not take the pointer back");
feed.mouse_moved(ImVec2(400, 300)); // the same place, repeated by Windows
check(feed.pointing(), "A mouse move repeated in the same place keeps the pointer");
feed.mouse_moved(ImVec2(50, 60)); // the mouse, or a finger on the screen
io.AddMousePosEvent(50, 60);
frame(feed, pad());
frame(feed, pad());
check(!feed.pointing() && !io.MouseDrawCursor, "The mouse takes the pointer back");
check(io.MousePos.x == 50, "The mouse's place wins once it moved");
}
{
fresh();
PadFeed feed;
ImGui::GetIO().AddMousePosEvent(150, 225); // over "second"
frame(feed, pad());
frame(feed, pad());
std::string pressed = frame(feed, pad(right_thumb)).pressed;
pressed += frame(feed, pad()).pressed;
check(feed.pointing() && pressed.empty() && !ImGui::GetIO().MouseDown[0],
"The first trackpad click (R3) only shows the pointer");
pressed = frame(feed, pad(right_thumb)).pressed;
check(ImGui::GetIO().MouseDown[0], "The next click holds the left button");
pressed += frame(feed, pad()).pressed;
check(pressed == "second", "The trackpad's click presses what the pointer is over");
check(!ImGui::GetIO().MouseDown[0], "Letting go of R3 lets go of the left button");
press(feed, dpad_down);
check(!feed.pointing() && ImGui::GetIO().NavVisible, "The D-pad hands the screen back to the focus");
}
ImGui::DestroyContext();
if (failures) {
std::cerr << failures << " gamepad input check(s) failed\n";
return 1;
}
std::cout << "Gamepad input checks passed.\n";
return 0;
}