From 8c7c30e3bb11306fd45e577258587ce48930e473 Mon Sep 17 00:00:00 2001 From: NorthDroplet Date: Mon, 5 Oct 2026 14:38:48 +0000 Subject: [PATCH] Launcher: controller reaches every panel; pad pointer without cursor warps - Settings' and Mods' rails, pages and lists are NavFlattened children, so the D-pad crosses between them instead of treating each as one item that only B could leave. - The pad's pointer no longer moves the system cursor. Under Proton that move came back late as a mouse move and dropped the pointer; now a mouse move counts only when it lands somewhere new. - The first R3 only shows the pointer, so a stray click does not press whatever the mouse last rested on. --- Launcher/Test/gamepad_input_tests.cpp | 84 ++++++++++++++++++--------- Launcher/gamepad_input.cpp | 35 +++++------ Launcher/gamepad_input.h | 16 ++--- Launcher/gui.cpp | 8 +-- Launcher/gui_mods.cpp | 14 +++-- Launcher/gui_mods_browse.cpp | 5 +- Launcher/gui_settings.cpp | 7 ++- 7 files changed, 96 insertions(+), 73 deletions(-) diff --git a/Launcher/Test/gamepad_input_tests.cpp b/Launcher/Test/gamepad_input_tests.cpp index bdc62a5..063f0d6 100644 --- a/Launcher/Test/gamepad_input_tests.cpp +++ b/Launcher/Test/gamepad_input_tests.cpp @@ -31,12 +31,11 @@ struct Seen { // 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, std::optional* warp = nullptr) { +Seen frame(PadFeed& feed, const PadState& pad, bool open_menu = false, bool panels = false) { auto& io = ImGui::GetIO(); io.DisplaySize = ImVec2(800, 600); io.DeltaTime = 1.0f / 60.0f; - const auto moved = feed.update(io, pad, 1.0f); - if (warp) *warp = moved; + feed.update(io, pad, 1.0f); ImGui::NewFrame(); Seen seen; ImGui::SetNextWindowPos(ImVec2(0, 0)); @@ -46,6 +45,17 @@ Seen frame(PadFeed& feed, const PadState& pad, bool open_menu = false, std::opti 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 (open_menu) ImGui::OpenPopup("##menu"); if (ImGui::BeginPopup("##menu")) { seen.menu_open = true; @@ -145,38 +155,55 @@ int main() { check(ImGui::IsKeyDown(ImGuiKey_Escape), "An idle pad does not let go of the keyboard's Escape"); } + { + fresh(); + PadFeed feed; + constexpr std::uint16_t dpad_right = 0x0008; + 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"); + } + // ------------------------------------------------ trackpad pointer { fresh(); PadFeed feed; - ImGui::GetIO().AddMousePosEvent(400, 300); + auto& io = ImGui::GetIO(); + io.AddMousePosEvent(400, 300); + feed.mouse_moved(ImVec2(400, 300)); frame(feed, pad()); - std::optional warp; - frame(feed, pad(0, 3000, 0), false, &warp); - check(!warp && !feed.pointing(), "A thumb resting near the middle does not move the pointer"); + 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), false, &warp); - check(feed.pointing() && ImGui::GetIO().MouseDrawCursor, "Tilting takes the pointer and draws it"); - check(warp && warp->x > 400 && warp->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), false, &warp); - check(warp && warp->x == 799 && warp->y == 0, "The pointer stays inside the window"); + frame(feed, pad(0, 16000, 0)); frame(feed, pad()); - check(ImGui::GetIO().MousePos.x == 799 && ImGui::GetIO().MousePos.y == 0, "ImGui's mouse is where the pad left it"); + 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 reporting a stale place (Proton, gamescope) does not take the pointer back. - ImGui::GetIO().AddMousePosEvent(10, 10); - frame(feed, pad(), false, &warp); + // The system cursor stays where the mouse left it, and is reported every frame. + io.AddMousePosEvent(400, 300); frame(feed, pad()); - check(!warp && ImGui::GetIO().MousePos.x == 799, "Without tilt the pointer stays put"); + 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(799, 0)); // the echo of our own cursor move - check(feed.pointing(), "The echo of our own cursor move keeps the pointer"); - feed.mouse_moved(ImVec2(50, 60)); // the mouse, or a finger on the screen - ImGui::GetIO().AddMousePosEvent(50, 60); + 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() && !ImGui::GetIO().MouseDrawCursor, "The mouse takes the pointer back"); - check(ImGui::GetIO().MousePos.x == 50, "The mouse's place wins once it moved"); + 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(); @@ -185,14 +212,15 @@ int main() { frame(feed, pad()); frame(feed, pad()); std::string pressed = frame(feed, pad(right_thumb)).pressed; - check(ImGui::GetIO().MouseDown[0], "The trackpad's click (R3) holds the left button"); - const auto released = frame(feed, pad()).pressed; - if (pressed.empty()) pressed = released; + 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"); - frame(feed, pad(0, 20000, 0)); - check(feed.pointing(), "The pointer is the pad's"); press(feed, dpad_down); check(!feed.pointing() && ImGui::GetIO().NavVisible, "The D-pad hands the screen back to the focus"); } diff --git a/Launcher/gamepad_input.cpp b/Launcher/gamepad_input.cpp index 8380948..fc167c4 100644 --- a/Launcher/gamepad_input.cpp +++ b/Launcher/gamepad_input.cpp @@ -29,8 +29,7 @@ constexpr float left_dead_zone = 7849.0f / 32767.0f; constexpr float right_dead_zone = 0.2f; // Pointer speed at full tilt, in design pixels a second. constexpr float pointer_speed = 1400.0f; -// A mouse move this close to the pad's pointer is the echo of moving the -// system cursor there; one further away is the mouse or a finger. +// A mouse move this close to the last one is the same place reported again. constexpr float pointer_slack = 2.0f; float axis(std::int16_t value) { return std::clamp(static_cast(value) / 32767.0f, -1.0f, 1.0f); } @@ -52,13 +51,13 @@ bool imgui_cancels() { return top && !(top->Flags & ImGuiWindowFlags_Modal); } -bool moved_away(ImVec2 position, ImVec2 pointer) { - return std::abs(position.x - pointer.x) > pointer_slack || std::abs(position.y - pointer.y) > pointer_slack; +bool moved_away(ImVec2 position, ImVec2 before) { + return std::abs(position.x - before.x) > pointer_slack || std::abs(position.y - before.y) > pointer_slack; } } // namespace -std::optional PadFeed::update(ImGuiIO& io, const PadState& input, float scale) { +void PadFeed::update(ImGuiIO& io, const PadState& input, float scale) { // A pad that went away lets go of everything it held. const PadState pad = input.connected ? input : PadState{}; const auto changed = static_cast(pad.buttons ^ buttons_); @@ -90,12 +89,13 @@ std::optional PadFeed::update(ImGuiIO& io, const PadState& input, float if ((pressed & focus_bits) || std::abs(left_x) > left_dead_zone || std::abs(left_y) > left_dead_zone) pointing_ = false; - bool warp = false; const float x = axis(pad.right_x), y = -axis(pad.right_y); // screen Y grows downward const float tilt = std::min(1.0f, std::sqrt(x * x + y * y)); + // The first R3 only shows the pointer: a stray click must not press + // whatever the mouse last rested on. + const bool click = pointing_ && (pressed & right_thumb); if (!pointing_ && (tilt > right_dead_zone || (pressed & right_thumb))) { pointing_ = true; - warp = true; // From wherever the mouse left it, which may be outside the window. pointer_ = ImGui::IsMousePosValid(&io.MousePos) ? io.MousePos : ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f); @@ -107,31 +107,28 @@ std::optional PadFeed::update(ImGuiIO& io, const PadState& input, float const float step = speed * speed * pointer_speed * scale * io.DeltaTime / tilt; pointer_.x += x * step; pointer_.y += y * step; - warp = true; } if (pointing_) { pointer_.x = std::clamp(pointer_.x, 0.0f, std::max(0.0f, io.DisplaySize.x - 1)); pointer_.y = std::clamp(pointer_.y, 0.0f, std::max(0.0f, io.DisplaySize.y - 1)); - // Every frame, after the backend's own: it may report a system cursor - // that Proton or gamescope did not move along with ours. + // Every frame, after the backend's own report of the system cursor, + // which stays where the mouse left it. io.AddMouseSourceEvent(ImGuiMouseSource_Mouse); io.AddMousePosEvent(pointer_.x, pointer_.y); } - // The release is sent even when the mouse took the pointer meanwhile: the - // press was ours. - if (changed & right_thumb) { + if (click || (clicking_ && (changed & right_thumb))) { + clicking_ = click; io.AddMouseSourceEvent(ImGuiMouseSource_Mouse); - io.AddMouseButtonEvent(ImGuiMouseButton_Left, (pad.buttons & right_thumb) != 0); + io.AddMouseButtonEvent(ImGuiMouseButton_Left, click); } io.MouseDrawCursor = pointing_; - if (warp) return pointer_; - return std::nullopt; } void PadFeed::mouse_moved(ImVec2 position) { - // Our own cursor move comes back as a mouse move too; only one that lands - // somewhere else is the mouse or a finger. - if (pointing_ && moved_away(position, pointer_)) pointing_ = false; + // Windows repeats a mouse move where the mouse already was when a window + // changes under it; only a move to a new place is the mouse or a finger. + if (pointing_ && moved_away(position, mouse_)) pointing_ = false; + mouse_ = position; } } // namespace dingosdk::launcher_gui diff --git a/Launcher/gamepad_input.h b/Launcher/gamepad_input.h index 45904a9..3a9dc33 100644 --- a/Launcher/gamepad_input.h +++ b/Launcher/gamepad_input.h @@ -3,7 +3,6 @@ #include #include -#include // Controller input for the launcher window. Kept free of Windows headers so // the mapping can be tested anywhere; gui_gamepad.cpp reads the pads. @@ -29,15 +28,14 @@ struct PadState { class PadFeed { public: // Call once a frame after the backends' NewFrame and before - // ImGui::NewFrame(), which takes the events in. Returns where the OS - // cursor should move (client area pixels) when the pad moved the pointer. - std::optional update(ImGuiIO& io, const PadState& pad, float scale); + // ImGui::NewFrame(), which takes the events in. + void update(ImGuiIO& io, const PadState& pad, float scale); // A mouse move or a touch the window received (WM_MOUSEMOVE, client area - // pixels). One that is not the echo of our own cursor move takes the - // pointer back from the pad. + // pixels). A move to a new place takes the pointer back from the pad. void mouse_moved(ImVec2 position); - // The right stick owns the pointer; ImGui draws it, since the Steam - // Deck's Game Mode may not show the system cursor. + // The right stick owns the pointer. ImGui draws it and the system cursor + // stays put: under Proton a moved system cursor comes back late as a + // mouse move, which would look like the mouse taking over. bool pointing() const { return pointing_; } private: @@ -45,7 +43,9 @@ private: ImGuiKey back_key_{ImGuiKey_Escape}; bool connected_{}; bool pointing_{}; + bool clicking_{}; // R3 holds the left button ImVec2 pointer_{}; + ImVec2 mouse_{-1, -1}; // where the last mouse move was }; } // namespace dingosdk::launcher_gui diff --git a/Launcher/gui.cpp b/Launcher/gui.cpp index b72f418..f29efac 100644 --- a/Launcher/gui.cpp +++ b/Launcher/gui.cpp @@ -472,13 +472,7 @@ int run(const launcher_app::Session& session, const std::vector& a // A pad pressed while another window has the focus is not meant for us; // it reads as let go, so nothing stays held while we are in the background. const bool foreground = GetForegroundWindow() == window; - if (const auto pointer = pad_feed.update(ImGui::GetIO(), foreground ? read_pad() : PadState{}, g_scale)) { - // The system cursor follows the pad's pointer, so the two agree when the - // mouse or the touch screen is used next. ImGui draws the pointer itself, - // so a cursor the system will not move (Proton, gamescope) costs nothing. - POINT point{static_cast(pointer->x), static_cast(pointer->y)}; - if (ClientToScreen(window, &point)) SetCursorPos(point.x, point.y); - } + pad_feed.update(ImGui::GetIO(), foreground ? read_pad() : PadState{}, g_scale); ImGui::NewFrame(); frame(launcher, fonts, window, ui, mods_panel); ImGui::Render(); diff --git a/Launcher/gui_mods.cpp b/Launcher/gui_mods.cpp index 1e6d8ee..9c26f67 100644 --- a/Launcher/gui_mods.cpp +++ b/Launcher/gui_mods.cpp @@ -571,7 +571,7 @@ void installed_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, co // ------------------------------------------------ the list, and under it how its order works const float hint = ImGui::GetTextLineHeightWithSpacing(); const float body = std::max(S(120), height - (ImGui::GetCursorPosY() - top) - hint); - ImGui::BeginChild("##mod_list", ImVec2(0, body), ImGuiChildFlags_Borders); + ImGui::BeginChild("##mod_list", ImVec2(0, body), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened); const auto note = [](const char* first, const char* second = nullptr) { ImGui::Spacing(); ImGui::Indent(S(14)); @@ -670,7 +670,8 @@ void mod_overview(const Fonts& fonts, ModsPanel& panel, const thunderstore::Inst ImGui::Spacing(); ImGui::BeginChild("##mod_overview_body", - ImVec2(0, std::max(S(80), extent.y - ImGui::GetCursorPosY() - S(24) - ImGui::GetFrameHeight()))); + ImVec2(0, std::max(S(80), extent.y - ImGui::GetCursorPosY() - S(24) - ImGui::GetFrameHeight())), + ImGuiChildFlags_NavFlattened); ImGui::PushTextWrapPos(0); if (!mod.outdated.empty()) ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(color::danger), @@ -850,7 +851,7 @@ void mods_broken_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& const float footer = ImGui::GetFrameHeight() + ImGui::GetTextLineHeight() + S(56); ImGui::BeginChild("##broken_list", ImVec2(0, frame.y - ImGui::GetCursorPosY() - footer), - ImGuiChildFlags_Borders); + ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened); for (const auto& problem : problems) { ImGui::PushFont(fonts.bold); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(color::danger)); @@ -934,7 +935,7 @@ void mods_outdated_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, U const float footer = ImGui::GetFrameHeight() + S(44); ImGui::BeginChild("##outdated_list", ImVec2(0, frame.y - ImGui::GetCursorPosY() - footer), - ImGuiChildFlags_Borders); + ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened); for (const auto* package : pending) { const auto found = installed.find(thunderstore::folder_for(package->full_name)); ImGui::PushFont(fonts.bold); @@ -1013,12 +1014,13 @@ void mods_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui, Mo const float rail_top = top + title_size * 2 + S(16); ImGui::SetCursorPos(ImVec2(rail_x, rail_top)); - ImGui::BeginChild("##rail", ImVec2(rail_width, bottom - rail_top)); + // Flattened, so a controller's D-pad crosses from the rail into the list and back. + ImGui::BeginChild("##rail", ImVec2(rail_width, bottom - rail_top), ImGuiChildFlags_NavFlattened); const bool leave = rail(fonts, panel, window, rail_width, pending, installing); ImGui::EndChild(); ImGui::SetCursorPos(ImVec2(content_x, top)); - ImGui::BeginChild("##content", ImVec2(frame.x - content_x - S(28), bottom - top)); + ImGui::BeginChild("##content", ImVec2(frame.x - content_x - S(28), bottom - top), ImGuiChildFlags_NavFlattened); // With nothing to say, the list runs all the way down to BACK's bottom // edge; a message takes two lines off it until it is gone. const float status = panel.message.empty() ? S(4) : ImGui::GetTextLineHeight() * 2 + S(12); diff --git a/Launcher/gui_mods_browse.cpp b/Launcher/gui_mods_browse.cpp index 4b6afe6..35b17ad 100644 --- a/Launcher/gui_mods_browse.cpp +++ b/Launcher/gui_mods_browse.cpp @@ -474,7 +474,7 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float const auto packages = visible_packages(store, installed); // ------------------------------------------------ the list - ImGui::BeginChild("##store_list", ImVec2(0, body), ImGuiChildFlags_Borders); + ImGui::BeginChild("##store_list", ImVec2(0, body), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened); const auto note = [](const char* text) { ImGui::Spacing(); ImGui::Indent(S(14)); @@ -653,7 +653,8 @@ void package_overview(const Fonts& fonts, ModsPanel& panel, ImVec2 size, bool in ImGui::Spacing(); ImGui::BeginChild("##overview_body", - ImVec2(0, std::max(S(80), extent.y - ImGui::GetCursorPosY() - S(24) - ImGui::GetFrameHeight()))); + ImVec2(0, std::max(S(80), extent.y - ImGui::GetCursorPosY() - S(24) - ImGui::GetFrameHeight())), + ImGuiChildFlags_NavFlattened); ImGui::PushTextWrapPos(0); if (package.deprecated) ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(color::warning), diff --git a/Launcher/gui_settings.cpp b/Launcher/gui_settings.cpp index 394d5eb..ae1ef4c 100644 --- a/Launcher/gui_settings.cpp +++ b/Launcher/gui_settings.cpp @@ -123,7 +123,8 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui // ------------------------------------------------ rail const float rail_top = top + title_size + S(20); ImGui::SetCursorPos(ImVec2(rail_x, rail_top)); - ImGui::BeginChild("##settings_rail", ImVec2(rail_width, bottom - rail_top)); + // Flattened, so a controller's D-pad crosses from the rail into the page and back. + ImGui::BeginChild("##settings_rail", ImVec2(rail_width, bottom - rail_top), ImGuiChildFlags_NavFlattened); static constexpr std::array names{"GAME", "DISPLAY", "KEYS", "ADVANCED"}; ImGui::BeginDisabled(binding); for (int i = 0; i < static_cast(names.size()); ++i) @@ -140,9 +141,9 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui // Kept to a readable column: settings are sentences, not a table. ImGui::SetCursorPos(ImVec2(content_x, top)); ImGui::BeginChild("##settings_content", - ImVec2(std::min(S(760), frame.x - content_x - S(28)), bottom - top)); + ImVec2(std::min(S(760), frame.x - content_x - S(28)), bottom - top), ImGuiChildFlags_NavFlattened); const float footer = ImGui::GetTextLineHeight() + S(16); - ImGui::BeginChild("##settings_page", ImVec2(0, ImGui::GetWindowHeight() - footer)); + ImGui::BeginChild("##settings_page", ImVec2(0, ImGui::GetWindowHeight() - footer), ImGuiChildFlags_NavFlattened); switch (ui.settings_tab) { case 0: { section_caption(fonts, "GAME FOLDER");