From 7b093429a22a7f9ba128cb658d33cd2f5dbcd403 Mon Sep 17 00:00:00 2001 From: NorthDroplet Date: Wed, 7 Oct 2026 20:57:08 +0000 Subject: [PATCH] Launcher: controller starts on PLAY and the open tab; rows and side moves in lists - The focus starts on PLAY when the launcher opens with a pad connected, and on the open tab (GAME, MY MODS) when the pad opens Settings or the Mod manager. - List rows act as one item: up and down go from row to row in one press, and a row's buttons take the focus only from the row (D-pad right). - D-pad left and right between the rail and a list: ImGui looks to the side only along a thin band through the focus and missed items level with the gap between two rows, so a side move that finds nothing looks again across the focus's full height. - The left stick scrolls the panel under the pointer directly instead of through wheel events, which queued behind the pointer's moves. - On a Steam Deck (SteamDeck=1) the right stick gets a trackpad's feel: small swipes move the pointer, and the glide after the thumb lifts is cut. --- Launcher/Test/gamepad_input_tests.cpp | 112 +++++++++++++++++++++++ Launcher/gamepad_input.cpp | 122 ++++++++++++++++++++------ Launcher/gamepad_input.h | 24 +++++ Launcher/gui.cpp | 6 +- Launcher/gui_home.cpp | 5 +- Launcher/gui_mods.cpp | 6 ++ Launcher/gui_mods_browse.cpp | 3 + Launcher/gui_settings.cpp | 5 +- 8 files changed, 255 insertions(+), 28 deletions(-) diff --git a/Launcher/Test/gamepad_input_tests.cpp b/Launcher/Test/gamepad_input_tests.cpp index 736e0fa4..37df5304 100644 --- a/Launcher/Test/gamepad_input_tests.cpp +++ b/Launcher/Test/gamepad_input_tests.cpp @@ -22,6 +22,7 @@ void check(bool condition, const char* what) { ++failures; } +constexpr std::uint16_t dpad_up = 0x0001, dpad_left = 0x0004; constexpr std::uint16_t dpad_down = 0x0002, dpad_right = 0x0008, right_thumb = 0x0080, button_a = 0x1000, button_b = 0x2000; struct Seen { @@ -39,6 +40,7 @@ Seen frame(PadFeed& feed, const PadState& pad, bool open_menu = false, bool pane io.DeltaTime = 1.0f / 60.0f; feed.update(io, pad, 1.0f); ImGui::NewFrame(); + feed.after_new_frame(); Seen seen; ImGui::SetNextWindowPos(ImVec2(0, 0)); ImGui::SetNextWindowSize(ImVec2(800, 600)); @@ -99,6 +101,56 @@ std::string press(PadFeed& feed, std::uint16_t button) { return pressed.empty() ? released : pressed; } +// The Mod manager's layout: a rail of tabs beside a list whose rows carry +// buttons, the rail's tiles level with the gaps between rows. +std::string mods_page(PadFeed& feed, const PadState& state) { + auto& io = ImGui::GetIO(); + io.DisplaySize = ImVec2(900, 700); + io.DeltaTime = 1.0f / 60.0f; + feed.update(io, state, 1.0f); + ImGui::NewFrame(); + feed.after_new_frame(); + std::string pressed; + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(900, 700)); + ImGui::Begin("##page", nullptr, ImGuiWindowFlags_NoDecoration); + ImGui::SetCursorPos(ImVec2(20, 100)); + ImGui::BeginChild("##rail", ImVec2(200, 400), ImGuiChildFlags_NavFlattened); + if (ImGui::Button("MY MODS", ImVec2(150, 40))) pressed = "MY MODS"; + dingosdk::launcher_gui::default_focus(); + if (ImGui::Button("GET MODS", ImVec2(150, 40))) pressed = "GET MODS"; + ImGui::EndChild(); + ImGui::SetCursorPos(ImVec2(260, 100)); + ImGui::BeginChild("##content", ImVec2(600, 500), ImGuiChildFlags_NavFlattened); + for (int row = 0; row < 12; ++row) { + ImGui::PushID(row); + dingosdk::launcher_gui::begin_row(); + const ImVec2 start = ImGui::GetCursorScreenPos(); + if (ImGui::Selectable("##row", false, ImGuiSelectableFlags_AllowOverlap, ImVec2(560, 60))) + pressed = "row " + std::to_string(row); + const ImVec2 next = ImGui::GetCursorScreenPos(); + dingosdk::launcher_gui::row_buttons(); + ImGui::SetCursorScreenPos(ImVec2(start.x + 380, start.y + 14)); + if (ImGui::Button("INSTALL", ImVec2(100, 32))) pressed = "install " + std::to_string(row); + dingosdk::launcher_gui::end_row(); + ImGui::SetCursorScreenPos(next); + ImGui::PopID(); + } + ImGui::EndChild(); + ImGui::End(); + ImGui::Render(); + return pressed; +} + +// Taps a D-pad direction on the Mod manager, then A: what A pressed is where the focus went. +std::string mods_tap(PadFeed& feed, std::uint16_t button) { + mods_page(feed, pad(button)); + for (int i = 0; i < 3; ++i) mods_page(feed, pad()); + std::string pressed = mods_page(feed, pad(button_a)); + pressed += mods_page(feed, pad()); + return pressed; +} + void fresh() { if (ImGui::GetCurrentContext()) ImGui::DestroyContext(); ImGui::CreateContext(); @@ -138,6 +190,33 @@ int main() { check(!(ImGui::GetIO().BackendFlags & ImGuiBackendFlags_HasGamepad), "An unplugged pad is no gamepad"); } + // ------------------------------------------------ the Mod manager + { + fresh(); + PadFeed feed; + mods_page(feed, pad()); + mods_page(feed, pad()); + check(GImGui->NavCursorVisible && mods_tap(feed, 0) == "MY MODS", + "With a pad connected the page opens with the focus on its tab"); + check(mods_tap(feed, dpad_down) == "GET MODS", "Down goes to the next tab"); + const auto row = mods_tap(feed, dpad_right); + check(row.rfind("row ", 0) == 0, "Right goes from a tab level with a gap between rows into the list"); + check(mods_tap(feed, dpad_left) == "GET MODS", "Left goes from the list back to the tabs"); + mods_tap(feed, dpad_right); + const int first = std::stoi(mods_tap(feed, dpad_down).substr(4)); + check(mods_tap(feed, dpad_down) == "row " + std::to_string(first + 1), "Down goes to the next row in one press"); + check(mods_tap(feed, dpad_up) == "row " + std::to_string(first), "Up goes to the row above in one press"); + check(mods_tap(feed, dpad_right) == "install " + std::to_string(first), "Right goes to the row's own button"); + check(mods_tap(feed, dpad_up) == "row " + std::to_string(first - 1), "Up from a row's button goes to the row above"); + } + { + fresh(); + PadFeed feed; + mods_page(feed, PadState{}); + mods_page(feed, PadState{}); + check(!GImGui->NavCursorVisible, "Without a pad nothing shows a focus"); + } + // ------------------------------------------------ back { fresh(); @@ -227,6 +306,39 @@ int main() { check(scroll > 100, "The left stick scrolls the panel under the mouse"); } + // ------------------------------------------------ the Steam Deck's trackpad + { + fresh(); + PadFeed stick; + PadFeed trackpad(true); + ImGui::GetIO().AddMousePosEvent(400, 300); + frame(trackpad, pad()); + for (int i = 0; i < 10; ++i) frame(trackpad, pad(0, 3300, 0)); // a slow swipe + frame(trackpad, pad()); + check(ImGui::GetIO().MousePos.x > 405, "A slow swipe on the trackpad moves the pointer"); + fresh(); + ImGui::GetIO().AddMousePosEvent(400, 300); + frame(stick, pad()); + for (int i = 0; i < 10; ++i) frame(stick, pad(0, 3300, 0)); + frame(stick, pad()); + check(!stick.pointing() && ImGui::GetIO().MousePos.x == 400, "The same small tilt of a thumbstick does not"); + + fresh(); + PadFeed glide(true); + ImGui::GetIO().AddMousePosEvent(100, 300); + frame(glide, pad()); + for (int i = 0; i < 5; ++i) frame(glide, pad(0, 20000, 0)); // the swipe + float tilt = 20000; + float stopped = 0; + for (int i = 0; i < 30; ++i) { // the thumb lifted: the tilt only fades + tilt *= 0.9f; + frame(glide, pad(0, static_cast(tilt), 0)); + if (i == 6) stopped = ImGui::GetIO().MousePos.x; + } + frame(glide, pad()); + check(stopped > 100 && ImGui::GetIO().MousePos.x == stopped, "The trackpad's glide after the thumb lifts does not carry the pointer on"); + } + // ------------------------------------------------ trackpad pointer { fresh(); diff --git a/Launcher/gamepad_input.cpp b/Launcher/gamepad_input.cpp index 0e551cb8..f6df1d11 100644 --- a/Launcher/gamepad_input.cpp +++ b/Launcher/gamepad_input.cpp @@ -24,15 +24,25 @@ constexpr std::uint16_t focus_bits = dpad_up | dpad_down | dpad_left | dpad_righ // XInput's left stick dead zone. constexpr float left_dead_zone = 7849.0f / 32767.0f; -// Larger on the right: a thumb resting on the Deck's trackpad is never quite -// centred, and the pointer must not creep under it. -constexpr float right_dead_zone = 0.2f; -// Pointer speed at full tilt, in design pixels a second. -constexpr float pointer_speed = 1400.0f; -// Scrolling at full tilt: design pixels a second for the focused panel, and -// mouse wheel notches a second (ImGui scrolls five lines a notch) under a pointer. +// Scrolling at full tilt, in design pixels a second. constexpr float scroll_speed = 1200.0f; -constexpr float wheel_speed = 16.0f; + +// How the right stick drives the pointer. +struct PointerFeel { + float dead_zone; + float speed; // design pixels a second at full tilt + bool squared; // a small tilt is slow, for placing the pointer precisely + bool cut_glide; // stop once the tilt only fades +}; +// A thumbstick sets a speed. A resting thumb is never quite centred, so the +// dead zone is large. +constexpr PointerFeel stick_feel{0.2f, 1400.0f, true, false}; +// The Steam Deck's trackpad, in Steam's gamepad layouts, tilts the stick by how +// fast the thumb swipes and keeps gliding after it lifts, fading out. Small +// swipes must move the pointer, and the glide must not carry it on. +constexpr PointerFeel trackpad_feel{0.06f, 2600.0f, false, true}; +// Frames of a steadily fading tilt that make it a glide rather than the thumb. +constexpr int glide_frames = 4; // A mouse move this close to the last one is the same place reported again. constexpr float pointer_slack = 2.0f; @@ -41,20 +51,17 @@ float axis(std::int16_t value) { return std::clamp(static_cast(value) / 3 // The left stick scrolls whatever the user is looking at: the panel holding // the pad's focus while the focus is shown, else the one under the pointer. // ImGui's own stick scrolling only ever moves the focused window, which under a -// pointer is usually the page itself, with nothing to scroll. -void scroll(ImGuiIO& io, float tilt, float scale) { +// pointer is usually the page itself, with nothing to scroll. It scrolls the +// panel directly: wheel events would queue behind the pointer's moves and lag. +void scroll(const ImGuiIO& io, float tilt, float scale) { const float amount = std::copysign(std::clamp((std::abs(tilt) - left_dead_zone) / (1 - left_dead_zone), 0.0f, 1.0f), tilt); if (amount == 0.0f) return; const ImGuiContext& g = *GImGui; - if (g.NavCursorVisible && g.NavHighlightItemUnderNav) { - // The nearest panel around the focus that can scroll. - ImGuiWindow* window = g.NavWindow; - while (window && window->ScrollMax.y <= 0 && (window->Flags & ImGuiWindowFlags_ChildWindow)) window = window->ParentWindow; - if (window && window->ScrollMax.y > 0) - ImGui::SetScrollY(window, window->Scroll.y - amount * scroll_speed * scale * io.DeltaTime); - return; - } - io.AddMouseWheelEvent(0, amount * wheel_speed * io.DeltaTime); + ImGuiWindow* window = g.NavCursorVisible && g.NavHighlightItemUnderNav ? g.NavWindow : g.HoveredWindow; + // The nearest panel around it that can scroll. + while (window && window->ScrollMax.y <= 0 && (window->Flags & ImGuiWindowFlags_ChildWindow)) window = window->ParentWindow; + if (window && window->ScrollMax.y > 0) + ImGui::SetScrollY(window, window->Scroll.y - amount * scroll_speed * scale * io.DeltaTime); } // B goes to ImGui's own cancel while ImGui has something to cancel: a combo or @@ -99,22 +106,28 @@ void PadFeed::update(ImGuiIO& io, const PadState& input, float scale) { // a pointer moves, so the pointer goes away instead. Scrolling keeps both. if (pressed & focus_bits) pointing_ = false; + const PointerFeel& feel = trackpad_ ? trackpad_feel : stick_feel; 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)); + if (feel.cut_glide) { + // A glide only ever fades; the thumb speeds up again or stops. + if (tilt <= feel.dead_zone || tilt > last_tilt_ + 0.01f) fading_ = 0; + else if (tilt < last_tilt_) ++fading_; + last_tilt_ = tilt; + } + const bool steering = tilt > feel.dead_zone && fading_ < glide_frames; // 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))) { + if (!pointing_ && (steering || (pressed & right_thumb))) { pointing_ = 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); } - if (pointing_ && tilt > right_dead_zone) { - // Squared, so a small tilt places the pointer precisely and a full one - // crosses the window in about a second. - const float speed = (tilt - right_dead_zone) / (1 - right_dead_zone); - const float step = speed * speed * pointer_speed * scale * io.DeltaTime / tilt; + if (pointing_ && steering) { + const float speed = (tilt - feel.dead_zone) / (1 - feel.dead_zone); + const float step = (feel.squared ? speed * speed : speed) * feel.speed * scale * io.DeltaTime / tilt; pointer_.x += x * step; pointer_.y += y * step; } @@ -134,6 +147,65 @@ void PadFeed::update(ImGuiIO& io, const PadState& input, float scale) { io.MouseDrawCursor = pointing_; } +void PadFeed::after_new_frame() { + ImGuiContext& g = *GImGui; + // ImGui looks to the side only along a thin band through the middle of the + // focus, so it misses an item level with the gap between two list rows: the + // rail's tiles beside a list. A side move that found nothing looks again + // across the focus's full height. + if (side_dir_ != ImGuiDir_None && g.NavId == side_from_ && g.NavWindow && !g.NavMoveSubmitted) { + const ImGuiDir dir = side_dir_; + side_dir_ = ImGuiDir_None; + ImGui::NavMoveRequestSubmit(dir, dir, ImGuiNavMoveFlags_None, + ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY); + g.NavScoringRect = ImGui::WindowRectRelToAbs(g.NavWindow, g.NavWindow->NavRectRel[g.NavLayer]); + last_nav_id_ = g.NavId; + return; + } + side_dir_ = ImGuiDir_None; + if (g.NavMoveSubmitted && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right)) { + side_dir_ = g.NavMoveDir; + // ImGui clears the focus for the move when the item is wider than its + // panel; the move then lands on that same item, which is no move either. + side_from_ = g.NavId ? g.NavId : last_nav_id_; + } + last_nav_id_ = g.NavId; +} + +namespace { +bool g_row_focused{}; +} // namespace + +void default_focus() { + ImGui::SetItemDefaultFocus(); + ImGuiContext& g = *GImGui; + // With a pad there, the focus starts on it straight away as the window + // appears: when the launcher starts, and when the pad opened this page. + // (Not whenever nothing has the focus: ImGui clears it for a frame while + // the focus moves off a row wider than its panel.) + const bool pad = (g.IO.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool pad_led = g.NavCursorVisible && g.NavHighlightItemUnderNav; + if (!pad || !ImGui::IsWindowAppearing() || !(pad_led || g.FrameCount <= 2)) return; + ImGui::SetFocusID(g.LastItemData.ID, g.CurrentWindow); + g.NavCursorVisible = true; + g.NavHighlightItemUnderNav = true; +} + +void begin_row() { + const ImGuiID scope = ImGui::GetID("##row_focus"); + g_row_focused = GImGui->NavFocusScopeId == scope; + ImGui::PushFocusScope(scope); +} + +void row_buttons() { + ImGui::PushItemFlag(ImGuiItemFlags_NoNav, !g_row_focused); +} + +void end_row() { + ImGui::PopItemFlag(); + ImGui::PopFocusScope(); +} + void PadFeed::mouse_moved(ImVec2 position) { // 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. diff --git a/Launcher/gamepad_input.h b/Launcher/gamepad_input.h index ea1d7ac2..5bffc92a 100644 --- a/Launcher/gamepad_input.h +++ b/Launcher/gamepad_input.h @@ -28,9 +28,14 @@ struct PadState { // click is R3, so this is what makes the trackpad a mouse in the launcher. class PadFeed { public: + // `trackpad`: the right stick is a Steam Deck's trackpad, not a thumbstick. + explicit PadFeed(bool trackpad = false) : trackpad_(trackpad) {} // Call once a frame after the backends' NewFrame and before // ImGui::NewFrame(), which takes the events in. void update(ImGuiIO& io, const PadState& pad, float scale); + // Call right after ImGui::NewFrame(): looks again for a D-pad left or right + // that ImGui found nothing for. + void after_new_frame(); // A mouse move or a touch the window received (WM_MOUSEMOVE, client area // pixels). A move to a new place takes the pointer back from the pad. void mouse_moved(ImVec2 position); @@ -40,7 +45,13 @@ public: bool pointing() const { return pointing_; } private: + bool trackpad_{}; std::uint16_t buttons_{}; + float last_tilt_{}; + int fading_{}; // frames the right stick's tilt only faded + ImGuiDir side_dir_{ImGuiDir_None}; // a D-pad left or right ImGui looked for last frame + ImGuiID side_from_{}; + ImGuiID last_nav_id_{}; ImGuiKey back_key_{ImGuiKey_Escape}; bool pointing_{}; bool clicking_{}; // R3 holds the left button @@ -48,4 +59,17 @@ private: ImVec2 mouse_{-1, -1}; // where the last mouse move was }; +// Makes the item just submitted the one a controller starts on in its window. +// With a pad connected, the focus moves there at once when nothing has it yet +// or when the pad opened the page. +void default_focus(); + +// A list row a controller treats as one item: up and down go from row to row +// in one press, and the buttons on a row take the focus only from the row +// itself (D-pad right). begin_row() before the row's list_row, row_buttons() +// after it, end_row() after the row's last button. +void begin_row(); +void row_buttons(); +void end_row(); + } // namespace dingosdk::launcher_gui diff --git a/Launcher/gui.cpp b/Launcher/gui.cpp index 5b82c64d..ba7ef22c 100644 --- a/Launcher/gui.cpp +++ b/Launcher/gui.cpp @@ -435,7 +435,10 @@ int run(const launcher_app::Session& session, const std::vector& a const auto ui_storage = std::make_unique(); auto& ui = *ui_storage; ModsPanel mods_panel; - PadFeed pad_feed; + // The Steam Deck's right trackpad reaches the launcher as the right stick + // (Steam sets SteamDeck=1 for what it starts there), which wants a + // trackpad's feel rather than a thumbstick's. + PadFeed pad_feed(GetEnvironmentVariableW(L"SteamDeck", nullptr, 0) > 0); g_pad_feed = &pad_feed; bool running = true; HANDLE game{}; @@ -490,6 +493,7 @@ int run(const launcher_app::Session& session, const std::vector& a const bool foreground = GetForegroundWindow() == window; pad_feed.update(ImGui::GetIO(), foreground ? read_pad() : PadState{}, g_scale); ImGui::NewFrame(); + pad_feed.after_new_frame(); frame(launcher, fonts, window, ui, mods_panel); // While the pad moves the focus, the mouse cursor (the Deck's trackpad in its // desktop layout) hides; the next mouse move hides the focus and brings it back. diff --git a/Launcher/gui_home.cpp b/Launcher/gui_home.cpp index f12306b2..40223c16 100644 --- a/Launcher/gui_home.cpp +++ b/Launcher/gui_home.cpp @@ -1,6 +1,7 @@ #include "gui_internal.h" -#include "mod_manager.h" +#include "mod_manager.h" + #include "problem.h" #include @@ -463,6 +464,8 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane ? (ui.mods_pending == 1 ? std::string("1 MOD UPDATE") : std::format("{} MOD UPDATES", ui.mods_pending)) : std::string(); if (action_tile(draw, fonts, primary, primary_size, label, detail, enabled && !modal, secondary, play_mark)) act(); + // A controller starts on PLAY. + if (!modal) default_focus(); if (settings_tile(draw, fonts, settings, ImVec2(column, S(84)), !modal)) ui.settings = true; if (time - ui.mods_checked > 5 && !ui.mods) { std::size_t enabled_mods = 0, installed = 0; diff --git a/Launcher/gui_mods.cpp b/Launcher/gui_mods.cpp index 9c26f679..7b4f40bf 100644 --- a/Launcher/gui_mods.cpp +++ b/Launcher/gui_mods.cpp @@ -129,10 +129,13 @@ bool rail(const Fonts& fonts, ModsPanel& panel, HWND window, float width, : pending.size() == 1 ? std::string("1 update on Thunderstore") : std::format("{} updates on Thunderstore", pending.size()))) panel.tab = 0; + // A controller starts on the open tab's tile. + if (panel.tab == 0) default_focus(); if (nav_tile(fonts, width, "GET MODS", panel.tab == 1, store.loaded ? std::to_string(store.packages.size()) : std::string("..."), false, store.loaded ? std::string() : std::string("Loading the Thunderstore listing"))) panel.tab = 1; + if (panel.tab == 1) default_focus(); ImGui::Dummy(ImVec2(0, S(8))); if (!pending.empty()) { @@ -316,8 +319,10 @@ void installed_row(const Fonts& fonts, ModsPanel& panel, const thunderstore::Ins const ImVec2 start = ImGui::GetCursorScreenPos(); const float width = ImGui::GetContentRegionAvail().x; const bool ticked = panel.marked.contains(mod.name); + begin_row(); const bool pressed = list_row("##row", width, tall, ticked); bool menu = ImGui::IsItemClicked(ImGuiMouseButton_Right); + row_buttons(); if (pressed) { // Ctrl and Shift pick, as they do in a file list; a plain click opens the mod. if (ImGui::GetIO().KeyShift) mark(panel, view, position, true, true); @@ -399,6 +404,7 @@ void installed_row(const Fonts& fonts, ModsPanel& panel, const thunderstore::Ins if (const auto detail = summary(panel, mod); !detail.empty()) draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(text_x, start.y + S(37)), color::muted, detail.c_str(), nullptr, 0, &clip); + end_row(); if (menu) ImGui::OpenPopup("##menu"); if (ImGui::BeginPopup("##menu")) { diff --git a/Launcher/gui_mods_browse.cpp b/Launcher/gui_mods_browse.cpp index 35b17ad7..544db04b 100644 --- a/Launcher/gui_mods_browse.cpp +++ b/Launcher/gui_mods_browse.cpp @@ -518,10 +518,12 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float const ImVec2 start = ImGui::GetCursorScreenPos(); const float width = ImGui::GetContentRegionAvail().x; // The row itself opens the overview; the widgets on it keep their clicks. + begin_row(); if (list_row("##row", width, tall, ticked)) { store.selected = package.full_name; store.overview = false; } + row_buttons(); const float right = start.x + width; const float text_x = start.x + S(82); mod_icon(panel, &package, ImVec2(start.x + S(14), start.y + S(11)), S(56)); @@ -572,6 +574,7 @@ void browse_page(Launcher& launcher, const Fonts& fonts, ModsPanel& panel, float std::format("v{}{} / {} downloads / {}", version.number, version.file_size ? " / " + size_text(version.file_size) : std::string(), count_text(package.downloads), date_text(package.date_updated)).c_str()); + end_row(); ImGui::PopID(); }); ImGui::EndDisabled(); diff --git a/Launcher/gui_settings.cpp b/Launcher/gui_settings.cpp index ae1ef4c3..8db3361c 100644 --- a/Launcher/gui_settings.cpp +++ b/Launcher/gui_settings.cpp @@ -127,9 +127,12 @@ void settings_window(Launcher& launcher, const Fonts& fonts, ImVec2 size, Ui& ui 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) + for (int i = 0; i < static_cast(names.size()); ++i) { if (nav_tile(fonts, rail_width, names[static_cast(i)], ui.settings_tab == i)) ui.settings_tab = i; + // A controller starts on the open tab's tile. + if (ui.settings_tab == i) default_focus(); + } ImGui::SetCursorPosY(ImGui::GetWindowHeight() - S(38)); push_primary_button(); if (ImGui::Button("\xe2\x86\x90 BACK", ImVec2(-1, S(34)))) open = false;