mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 03:16:54 +02:00
Input mapping tabs UI, emulated touchpad, and rumble for non-PS5 controllers (#1541)
* Revamp input mapping dialog with tabs, gamepad remapping and gyro emulation toggle Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Add emulated touchpad (mouse/gamepad) with particle skins; widen input dialog Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Disable mouse touchpad mode when the cursor leaves the window Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Add touchpad hand-cursor toggle; share touchpad options across both tabs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Hide touchpad trail and particles when the visual toggle is off Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Rename input mapping tabs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Rumble non-DualSense gamepads from haptic audio Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix input mapping, touchpad transitions, and rumble fallback --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
|
||||
#include "graphics/host_gpu/graphicContext.h"
|
||||
#include "graphics/presentation/ddsImage.h"
|
||||
#include "graphics/presentation/window/hostInput.h"
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
@@ -149,6 +150,47 @@ bool GetOverlaySnapshot(OverlaySnapshot* snapshot) {
|
||||
}
|
||||
|
||||
constexpr size_t INPUT_QUEUE_CAPACITY = 128;
|
||||
constexpr int MAX_TOUCH_CURSOR_IMAGE_DIMENSION = 2048;
|
||||
constexpr float TOUCH_CURSOR_IMAGE_SIZE = 48.0f;
|
||||
|
||||
enum class TouchParticleSkin : uint8_t { Classic, BlueNeon, Monochrome, Crimson, RedBlue };
|
||||
|
||||
TouchParticleSkin GetTouchParticleSkin() {
|
||||
static const auto skin = [] {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
constexpr std::string_view key = "TouchParticleSkin=";
|
||||
if (std::string_view(value).starts_with(key)) {
|
||||
const auto name = std::string_view(value).substr(key.size());
|
||||
if (name == "BlueNeon") {
|
||||
return TouchParticleSkin::BlueNeon;
|
||||
}
|
||||
if (name == "Monochrome") {
|
||||
return TouchParticleSkin::Monochrome;
|
||||
}
|
||||
if (name == "Crimson") {
|
||||
return TouchParticleSkin::Crimson;
|
||||
}
|
||||
if (name == "RedBlue") {
|
||||
return TouchParticleSkin::RedBlue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return TouchParticleSkin::Classic;
|
||||
}();
|
||||
return skin;
|
||||
}
|
||||
|
||||
bool TouchCursorImageEnabled() {
|
||||
static const bool enabled = [] {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
if (std::string_view(value) == "TouchCursorImage=Off") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
enum class InputKind : uint8_t {
|
||||
Button,
|
||||
@@ -552,9 +594,11 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
|
||||
const auto system = SystemDialog::GetVisualState();
|
||||
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
|
||||
const bool splash = g_splash_active.load(std::memory_order_acquire);
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash,
|
||||
const auto cursor = GetHostInputTouchPadVisualState();
|
||||
return {core.active || dialog.active || system.active || trophy_active || splash ||
|
||||
(cursor.enabled && cursor.cursor_visible),
|
||||
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) +
|
||||
(splash ? 2u : 0u)};
|
||||
(splash ? 2u : 0u) + static_cast<decltype(core.revision)>(cursor.revision)};
|
||||
}
|
||||
|
||||
bool ProcessSystemOverlayInput(const SDL_Event& event) {
|
||||
@@ -694,6 +738,20 @@ void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
|
||||
}
|
||||
|
||||
struct SystemOverlay::Impl {
|
||||
enum class TouchParticleIcon : uint8_t { Square, Triangle, Cross, Circle };
|
||||
|
||||
struct TouchParticle {
|
||||
ImVec2 position;
|
||||
Clock::time_point born;
|
||||
TouchParticleIcon icon;
|
||||
};
|
||||
|
||||
struct TouchTrailSegment {
|
||||
ImVec2 from;
|
||||
ImVec2 to;
|
||||
Clock::time_point born;
|
||||
};
|
||||
|
||||
explicit Impl(GraphicContext& context): graphics(context) {}
|
||||
|
||||
~Impl() {
|
||||
@@ -705,6 +763,12 @@ struct SystemOverlay::Impl {
|
||||
ReleaseVulkan();
|
||||
if (imgui_context != nullptr) {
|
||||
ImGui::SetCurrentContext(imgui_context);
|
||||
if (hand_cursor_image) {
|
||||
ImGui::UnregisterUserTexture(hand_cursor_image.get());
|
||||
}
|
||||
if (drawing_cursor_image) {
|
||||
ImGui::UnregisterUserTexture(drawing_cursor_image.get());
|
||||
}
|
||||
if (trophy_notification_image) {
|
||||
ImGui::UnregisterUserTexture(trophy_notification_image.get());
|
||||
}
|
||||
@@ -735,6 +799,8 @@ struct SystemOverlay::Impl {
|
||||
style.WindowRounding = 10.0f;
|
||||
style.FrameRounding = 6.0f;
|
||||
style.ItemSpacing = {8.0f, 8.0f};
|
||||
hand_cursor_image = LoadTouchCursorImage("hand.png");
|
||||
drawing_cursor_image = LoadTouchCursorImage("drawing.png");
|
||||
}
|
||||
|
||||
void PlayTrophySound() {
|
||||
@@ -775,6 +841,55 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<ImTextureData> LoadTouchCursorImage(std::string_view filename) {
|
||||
const char* base_path = SDL_GetBasePath();
|
||||
if (base_path == nullptr) {
|
||||
LOGF("Could not locate the emulator folder for touchpad cursor images: %s\n",
|
||||
SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
const std::filesystem::path path =
|
||||
std::filesystem::path(base_path) / "assets" / "cursors" / std::string(filename);
|
||||
|
||||
Common::File file;
|
||||
if (!file.Open(path, Common::File::Mode::Read)) {
|
||||
LOGF("Touchpad cursor image not found: %s\n", Common::PathToString(path).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
const auto encoded = file.ReadWholeBuffer();
|
||||
if (encoded.empty() ||
|
||||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
LOGF("Touchpad cursor image is empty or too large: %s\n",
|
||||
Common::PathToString(path).c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
auto* pixels = stbi_load_from_memory(
|
||||
reinterpret_cast<const stbi_uc*>(encoded.data()), static_cast<int>(encoded.size()),
|
||||
&width, &height, nullptr, 4);
|
||||
if (pixels == nullptr || width <= 0 || height <= 0 ||
|
||||
width > MAX_TOUCH_CURSOR_IMAGE_DIMENSION ||
|
||||
height > MAX_TOUCH_CURSOR_IMAGE_DIMENSION) {
|
||||
LOGF("Could not load touchpad cursor image %s: %s\n",
|
||||
Common::PathToString(path).c_str(),
|
||||
pixels == nullptr ? stbi_failure_reason() : "image dimensions exceed 2048x2048");
|
||||
stbi_image_free(pixels);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto texture = std::make_unique<ImTextureData>();
|
||||
texture->Create(ImTextureFormat_RGBA32, width, height);
|
||||
std::memcpy(texture->Pixels, pixels,
|
||||
static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4);
|
||||
texture->UseColors = true;
|
||||
stbi_image_free(pixels);
|
||||
ImGui::RegisterUserTexture(texture.get());
|
||||
LOGF("Loaded touchpad cursor image: %s\n", Common::PathToString(path).c_str());
|
||||
return texture;
|
||||
}
|
||||
|
||||
std::unique_ptr<ImTextureData> LoadTrophyIcon(std::span<const std::byte> encoded) {
|
||||
if (encoded.empty() ||
|
||||
encoded.size() > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
@@ -1191,6 +1306,248 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void EmitTouchParticles(ImVec2 from, ImVec2 to, Clock::time_point now) {
|
||||
const float dx = to.x - from.x;
|
||||
const float dy = to.y - from.y;
|
||||
const float distance = std::hypot(dx, dy);
|
||||
if (distance < 0.01f) {
|
||||
return;
|
||||
}
|
||||
if (distance >= 1.0f) {
|
||||
trail_segments.push_back({from, to, now});
|
||||
if (trail_segments.size() > MAX_TOUCH_TRAIL_SEGMENTS) {
|
||||
trail_segments.erase(trail_segments.begin());
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float PARTICLE_SPACING = 24.0f;
|
||||
float traveled = 0.0f;
|
||||
while (particle_distance + distance - traveled >= PARTICLE_SPACING) {
|
||||
traveled += PARTICLE_SPACING - particle_distance;
|
||||
const float fraction = std::clamp(traveled / distance, 0.0f, 1.0f);
|
||||
particles.push_back(
|
||||
{{from.x + dx * fraction, from.y + dy * fraction}, now, next_particle_icon});
|
||||
next_particle_icon = static_cast<TouchParticleIcon>(
|
||||
(static_cast<uint8_t>(next_particle_icon) + 1) % 4);
|
||||
if (particles.size() > MAX_TOUCH_PARTICLES) {
|
||||
particles.erase(particles.begin());
|
||||
}
|
||||
particle_distance = 0.0f;
|
||||
}
|
||||
particle_distance += distance - traveled;
|
||||
}
|
||||
|
||||
void DrawTouchParticles(ImDrawList* draw, Clock::time_point now) {
|
||||
const auto skin = GetTouchParticleSkin();
|
||||
constexpr float PARTICLE_LIFETIME = 1.0f;
|
||||
constexpr float TRAIL_LIFETIME = 0.35f;
|
||||
std::erase_if(trail_segments, [now](const auto& segment) {
|
||||
return std::chrono::duration<float>(now - segment.born).count() >= TRAIL_LIFETIME;
|
||||
});
|
||||
std::erase_if(particles, [now](const auto& particle) {
|
||||
return std::chrono::duration<float>(now - particle.born).count() >=
|
||||
PARTICLE_LIFETIME;
|
||||
});
|
||||
for (const auto& segment: trail_segments) {
|
||||
const float age =
|
||||
std::chrono::duration<float>(now - segment.born).count() / TRAIL_LIFETIME;
|
||||
const float fade = 1.0f - age;
|
||||
if (skin == TouchParticleSkin::RedBlue) {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(20, 75, 220, static_cast<int>(105.0f * fade)),
|
||||
9.0f * (1.0f - age * 0.5f));
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(255, 45, 65, static_cast<int>(230.0f * fade)),
|
||||
4.0f * (1.0f - age * 0.5f));
|
||||
} else if (skin == TouchParticleSkin::Crimson) {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(110, 0, 8, static_cast<int>(130.0f * fade)),
|
||||
9.0f * (1.0f - age * 0.5f));
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(255, 35, 20, static_cast<int>(235.0f * fade)),
|
||||
4.0f * (1.0f - age * 0.5f));
|
||||
} else {
|
||||
draw->AddLine(segment.from, segment.to,
|
||||
IM_COL32(35, 145, 255, static_cast<int>(180.0f * fade)),
|
||||
5.0f * (1.0f - age * 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& particle: particles) {
|
||||
const float age =
|
||||
std::chrono::duration<float>(now - particle.born).count() / PARTICLE_LIFETIME;
|
||||
const float alpha = 1.0f - age;
|
||||
const float size = 9.0f * (1.0f - age * 0.35f);
|
||||
const float drift = (static_cast<uint8_t>(particle.icon) % 2 == 0 ? -1.0f : 1.0f) *
|
||||
8.0f * age;
|
||||
const ImVec2 center {particle.position.x + drift, particle.position.y - 18.0f * age};
|
||||
const auto faded = [alpha, skin, icon = particle.icon](int r, int g, int b) {
|
||||
if (skin == TouchParticleSkin::BlueNeon) {
|
||||
r = 75;
|
||||
g = 220;
|
||||
b = 255;
|
||||
} else if (skin == TouchParticleSkin::RedBlue) {
|
||||
const bool red = icon == TouchParticleIcon::Square ||
|
||||
icon == TouchParticleIcon::Cross;
|
||||
r = red ? 255 : 75;
|
||||
g = red ? 65 : 175;
|
||||
b = red ? 85 : 255;
|
||||
} else if (skin == TouchParticleSkin::Monochrome) {
|
||||
r = 235;
|
||||
g = 245;
|
||||
b = 255;
|
||||
} else if (skin == TouchParticleSkin::Crimson) {
|
||||
const bool ember = icon == TouchParticleIcon::Triangle ||
|
||||
icon == TouchParticleIcon::Circle;
|
||||
r = 255;
|
||||
g = ember ? 205 : 115;
|
||||
b = ember ? 70 : 15;
|
||||
}
|
||||
return IM_COL32(r, g, b, static_cast<int>(255.0f * alpha));
|
||||
};
|
||||
|
||||
if (skin != TouchParticleSkin::Classic) {
|
||||
const ImU32 glow =
|
||||
skin == TouchParticleSkin::BlueNeon
|
||||
? IM_COL32(0, 115, 255, static_cast<int>(38.0f * alpha))
|
||||
: (skin == TouchParticleSkin::RedBlue
|
||||
? IM_COL32(75, 100, 255, static_cast<int>(55.0f * alpha))
|
||||
: (skin == TouchParticleSkin::Crimson
|
||||
? IM_COL32(255, 70, 0, static_cast<int>(70.0f * alpha))
|
||||
: IM_COL32(115, 165, 255, static_cast<int>(30.0f * alpha))));
|
||||
draw->AddCircleFilled(center, size + 6.0f, glow);
|
||||
}
|
||||
const ImU32 particle_glow =
|
||||
skin == TouchParticleSkin::Crimson
|
||||
? (particle.icon == TouchParticleIcon::Triangle ||
|
||||
particle.icon == TouchParticleIcon::Circle
|
||||
? IM_COL32(255, 195, 30, static_cast<int>(135.0f * alpha))
|
||||
: IM_COL32(255, 95, 0, static_cast<int>(135.0f * alpha)))
|
||||
: (skin == TouchParticleSkin::RedBlue
|
||||
? (particle.icon == TouchParticleIcon::Square ||
|
||||
particle.icon == TouchParticleIcon::Cross
|
||||
? IM_COL32(255, 40, 65, static_cast<int>(105.0f * alpha))
|
||||
: IM_COL32(35, 125, 255, static_cast<int>(105.0f * alpha)))
|
||||
: IM_COL32(35, 145, 255,
|
||||
static_cast<int>((skin == TouchParticleSkin::BlueNeon
|
||||
? 100.0f
|
||||
: 65.0f) *
|
||||
alpha)));
|
||||
draw->AddCircleFilled(center, size + 2.0f, particle_glow);
|
||||
switch (particle.icon) {
|
||||
case TouchParticleIcon::Square:
|
||||
draw->AddRect({center.x - size * 0.65f, center.y - size * 0.65f},
|
||||
{center.x + size * 0.65f, center.y + size * 0.65f},
|
||||
faded(255, 105, 180), 2.0f, 0, 2.2f);
|
||||
break;
|
||||
case TouchParticleIcon::Triangle:
|
||||
draw->AddLine({center.x, center.y - size}, {center.x - size, center.y + size},
|
||||
faded(80, 230, 130), 2.2f);
|
||||
draw->AddLine({center.x - size, center.y + size},
|
||||
{center.x + size, center.y + size}, faded(80, 230, 130), 2.2f);
|
||||
draw->AddLine({center.x + size, center.y + size},
|
||||
{center.x, center.y - size}, faded(80, 230, 130), 2.2f);
|
||||
break;
|
||||
case TouchParticleIcon::Cross:
|
||||
draw->AddLine({center.x - size * 0.75f, center.y - size * 0.75f},
|
||||
{center.x + size * 0.75f, center.y + size * 0.75f},
|
||||
faded(90, 170, 255), 2.5f);
|
||||
draw->AddLine({center.x + size * 0.75f, center.y - size * 0.75f},
|
||||
{center.x - size * 0.75f, center.y + size * 0.75f},
|
||||
faded(90, 170, 255), 2.5f);
|
||||
break;
|
||||
case TouchParticleIcon::Circle:
|
||||
draw->AddCircle(center, size * 0.8f, faded(255, 105, 105), 0, 2.2f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawTouchPadCursor(const HostInputTouchPadVisualState& state) {
|
||||
if (!TouchCursorImageEnabled()) {
|
||||
particles.clear();
|
||||
trail_segments.clear();
|
||||
return;
|
||||
}
|
||||
const auto now = Clock::now();
|
||||
if (state.enabled && state.position_valid) {
|
||||
const ImVec2 target {state.x * extent.width, state.y * extent.height};
|
||||
if (!cursor_initialized || state.gesture != cursor_gesture) {
|
||||
cursor_position = target;
|
||||
cursor_initialized = true;
|
||||
cursor_gesture = state.gesture;
|
||||
particle_distance = 0.0f;
|
||||
} else {
|
||||
const float dt = last_cursor_update == Clock::time_point {}
|
||||
? 1.0f / 60.0f
|
||||
: std::clamp(std::chrono::duration<float>(now -
|
||||
last_cursor_update)
|
||||
.count(),
|
||||
1.0f / 1000.0f, 0.05f);
|
||||
const ImVec2 previous = cursor_position;
|
||||
const float follow = 1.0f - std::exp(-18.0f * dt);
|
||||
cursor_position.x += (target.x - cursor_position.x) * follow;
|
||||
cursor_position.y += (target.y - cursor_position.y) * follow;
|
||||
if (state.drawing) {
|
||||
EmitTouchParticles(previous, cursor_position, now);
|
||||
}
|
||||
}
|
||||
last_cursor_update = now;
|
||||
}
|
||||
auto* draw = ImGui::GetForegroundDrawList();
|
||||
DrawTouchParticles(draw, now);
|
||||
if (!state.enabled || !state.cursor_visible ||
|
||||
!state.position_valid || !cursor_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImVec2 position = cursor_position;
|
||||
const auto& cursor_image =
|
||||
state.drawing ? drawing_cursor_image : hand_cursor_image;
|
||||
if (cursor_image != nullptr) {
|
||||
const float scale = TOUCH_CURSOR_IMAGE_SIZE /
|
||||
static_cast<float>(std::max(cursor_image->Height, 1));
|
||||
const ImVec2 size {static_cast<float>(cursor_image->Width) * scale,
|
||||
static_cast<float>(cursor_image->Height) * scale};
|
||||
draw->AddImage(cursor_image->GetTexRef(), position,
|
||||
{position.x + size.x, position.y + size.y});
|
||||
return;
|
||||
}
|
||||
|
||||
const ImU32 blue = IM_COL32(35, 145, 255, 220);
|
||||
const ImVec2 hand[] = {
|
||||
{position.x, position.y}, {position.x, position.y + 20.0f},
|
||||
{position.x + 4.0f, position.y + 16.0f},
|
||||
{position.x + 9.0f, position.y + 27.0f},
|
||||
{position.x + 13.0f, position.y + 25.0f},
|
||||
{position.x + 10.0f, position.y + 17.0f},
|
||||
{position.x + 18.0f, position.y + 18.0f},
|
||||
{position.x + 20.0f, position.y + 15.0f},
|
||||
{position.x + 19.0f, position.y + 12.0f},
|
||||
{position.x + 12.0f, position.y + 10.0f},
|
||||
{position.x + 12.0f, position.y + 4.0f},
|
||||
{position.x + 10.0f, position.y + 2.0f},
|
||||
{position.x + 8.0f, position.y + 3.0f},
|
||||
{position.x + 8.0f, position.y + 10.0f},
|
||||
{position.x + 6.0f, position.y + 2.0f},
|
||||
{position.x + 4.0f, position.y + 2.0f},
|
||||
{position.x + 3.0f, position.y + 10.0f},
|
||||
{position.x + 2.0f, position.y + 1.0f},
|
||||
};
|
||||
draw->AddConvexPolyFilled(hand, IM_ARRAYSIZE(hand), IM_COL32(250, 250, 250, 255));
|
||||
for (std::size_t i = 0; i < IM_ARRAYSIZE(hand); i++) {
|
||||
draw->AddLine(hand[i], hand[(i + 1) % IM_ARRAYSIZE(hand)],
|
||||
IM_COL32(20, 25, 35, 255), 1.5f);
|
||||
}
|
||||
if (state.drawing) {
|
||||
const ImVec2 pen_start {position.x + 12.0f, position.y + 8.0f};
|
||||
const ImVec2 pen_end {position.x + 23.0f, position.y - 3.0f};
|
||||
draw->AddLine(pen_start, pen_end, IM_COL32(10, 45, 95, 255), 5.0f);
|
||||
draw->AddLine(pen_start, pen_end, blue, 2.5f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void DrawTrophyNotification(vk::Extent2D frame_extent, Clock::time_point now) {
|
||||
std::erase_if(retired_trophy_images, [](const auto& texture) {
|
||||
if (texture->Status != ImTextureStatus_Destroyed) {
|
||||
@@ -1316,7 +1673,8 @@ struct SystemOverlay::Impl {
|
||||
const bool has_overlay = GetOverlaySnapshot(&snapshot);
|
||||
const auto now = Clock::now();
|
||||
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) &&
|
||||
!g_splash_active.load(std::memory_order_acquire)) {
|
||||
!g_splash_active.load(std::memory_order_acquire) &&
|
||||
!GetHostInputTouchPadVisualState().enabled) {
|
||||
return false;
|
||||
}
|
||||
const auto prepared_session = snapshot.session;
|
||||
@@ -1356,6 +1714,7 @@ struct SystemOverlay::Impl {
|
||||
}
|
||||
}
|
||||
DrawTrophyNotification(frame_extent, now);
|
||||
DrawTouchPadCursor(GetHostInputTouchPadVisualState());
|
||||
ImGui::Render();
|
||||
extent = frame_extent;
|
||||
return true;
|
||||
@@ -1410,6 +1769,18 @@ struct SystemOverlay::Impl {
|
||||
ImVec2 right_stick {};
|
||||
OverlaySession session;
|
||||
std::unique_ptr<ImTextureData> trophy_notification_image;
|
||||
std::unique_ptr<ImTextureData> hand_cursor_image;
|
||||
std::unique_ptr<ImTextureData> drawing_cursor_image;
|
||||
bool cursor_initialized = false;
|
||||
ImVec2 cursor_position {};
|
||||
float particle_distance = 0.0f;
|
||||
TouchParticleIcon next_particle_icon = TouchParticleIcon::Square;
|
||||
Clock::time_point last_cursor_update;
|
||||
uint64_t cursor_gesture = 0;
|
||||
std::vector<TouchParticle> particles;
|
||||
std::vector<TouchTrailSegment> trail_segments;
|
||||
static constexpr std::size_t MAX_TOUCH_PARTICLES = 64;
|
||||
static constexpr std::size_t MAX_TOUCH_TRAIL_SEGMENTS = 128;
|
||||
std::optional<TrophyNotification> trophy_notification;
|
||||
Clock::time_point trophy_notification_born;
|
||||
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
@@ -27,6 +29,8 @@ struct ControlInfo {
|
||||
float touch_x = 0.0f;
|
||||
|
||||
Controller::Setting setting {};
|
||||
bool gyro = false;
|
||||
bool touchpad_mode = false;
|
||||
};
|
||||
|
||||
static constexpr std::array CONTROL_INFO = {
|
||||
@@ -61,10 +65,66 @@ static constexpr std::array CONTROL_INFO = {
|
||||
ControlInfo {.name = "VibrationIntensity", .setting = Controller::Setting::VibrationIntensity},
|
||||
ControlInfo {.name = "TriggerEffectIntensity",
|
||||
.setting = Controller::Setting::TriggerEffectIntensity},
|
||||
ControlInfo {.name = "GyroToggle", .gyro = true},
|
||||
ControlInfo {.name = "TouchPadMode", .touchpad_mode = true},
|
||||
};
|
||||
|
||||
constexpr std::size_t INVALID_CONTROL = CONTROL_INFO.size();
|
||||
|
||||
struct PadSource {
|
||||
std::string_view name;
|
||||
int button; // SDL_GamepadButton, or -1 for an analog trigger
|
||||
};
|
||||
|
||||
constexpr std::array PAD_SOURCES = {
|
||||
PadSource {"South", SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
PadSource {"East", SDL_GAMEPAD_BUTTON_EAST},
|
||||
PadSource {"West", SDL_GAMEPAD_BUTTON_WEST},
|
||||
PadSource {"North", SDL_GAMEPAD_BUTTON_NORTH},
|
||||
PadSource {"Back", SDL_GAMEPAD_BUTTON_BACK},
|
||||
PadSource {"Guide", SDL_GAMEPAD_BUTTON_GUIDE},
|
||||
PadSource {"Start", SDL_GAMEPAD_BUTTON_START},
|
||||
PadSource {"LeftStick", SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
PadSource {"RightStick", SDL_GAMEPAD_BUTTON_RIGHT_STICK},
|
||||
PadSource {"LeftShoulder", SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
|
||||
PadSource {"RightShoulder", SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
|
||||
PadSource {"DpadUp", SDL_GAMEPAD_BUTTON_DPAD_UP},
|
||||
PadSource {"DpadDown", SDL_GAMEPAD_BUTTON_DPAD_DOWN},
|
||||
PadSource {"DpadLeft", SDL_GAMEPAD_BUTTON_DPAD_LEFT},
|
||||
PadSource {"DpadRight", SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
|
||||
PadSource {"Misc1", SDL_GAMEPAD_BUTTON_MISC1},
|
||||
PadSource {"RightPaddle1", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
PadSource {"LeftPaddle1", SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
PadSource {"RightPaddle2", SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
PadSource {"LeftPaddle2", SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
PadSource {"Touchpad", SDL_GAMEPAD_BUTTON_TOUCHPAD},
|
||||
PadSource {"LeftTrigger", -1},
|
||||
PadSource {"RightTrigger", -1},
|
||||
};
|
||||
|
||||
constexpr std::size_t LEFT_TRIGGER_SOURCE = PAD_SOURCES.size() - 2;
|
||||
constexpr std::size_t RIGHT_TRIGGER_SOURCE = PAD_SOURCES.size() - 1;
|
||||
constexpr std::size_t INVALID_SOURCE = PAD_SOURCES.size();
|
||||
|
||||
struct PadDefault {
|
||||
std::string_view control;
|
||||
std::string_view source;
|
||||
};
|
||||
|
||||
constexpr std::array PAD_DEFAULTS = {
|
||||
PadDefault {"Cross", "South"}, PadDefault {"Circle", "East"},
|
||||
PadDefault {"Square", "West"}, PadDefault {"Triangle", "North"},
|
||||
PadDefault {"Options", "Start"}, PadDefault {"L3", "LeftStick"},
|
||||
PadDefault {"R3", "RightStick"}, PadDefault {"L1", "LeftShoulder"},
|
||||
PadDefault {"R1", "RightShoulder"}, PadDefault {"L2", "LeftTrigger"},
|
||||
PadDefault {"R2", "RightTrigger"}, PadDefault {"Up", "DpadUp"},
|
||||
PadDefault {"Down", "DpadDown"}, PadDefault {"Left", "DpadLeft"},
|
||||
PadDefault {"Right", "DpadRight"}, PadDefault {"TouchPad", "Touchpad"},
|
||||
PadDefault {"GyroToggle", "Back"},
|
||||
};
|
||||
|
||||
constexpr std::string_view PAD_PREFIX = "Pad:";
|
||||
|
||||
struct Binding {
|
||||
SDL_Keycode key = SDLK_UNKNOWN;
|
||||
uint8_t mouse_button = 0;
|
||||
@@ -74,6 +134,7 @@ struct Binding {
|
||||
constexpr int MOUSE_POLL_INTERVAL_MS = 33;
|
||||
constexpr uint64_t CURSOR_IDLE_HIDE_MS = 2000;
|
||||
constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity=";
|
||||
constexpr std::string_view TOUCH_PARTICLE_SKIN = "TouchParticleSkin=";
|
||||
|
||||
struct MouseJoystickState {
|
||||
bool enabled = false;
|
||||
@@ -85,6 +146,38 @@ MouseJoystickState g_mouse;
|
||||
SDL_Window* g_mouse_window = nullptr;
|
||||
uint64_t g_cursor_hide_at = 0;
|
||||
|
||||
struct MouseTouchPadState {
|
||||
bool enabled = false;
|
||||
bool cursor_visible = true;
|
||||
bool drawing = false;
|
||||
bool position_valid = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t revision = 0;
|
||||
uint64_t gesture = 0;
|
||||
};
|
||||
|
||||
struct GamepadTouchPadState {
|
||||
bool enabled = false;
|
||||
bool drawing = false;
|
||||
int id = -1;
|
||||
int x_axis = 128;
|
||||
int y_axis = 128;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t next_poll = 0;
|
||||
uint64_t last_poll = 0;
|
||||
};
|
||||
|
||||
constexpr int GAMEPAD_TOUCHPAD_POLL_MS = 8;
|
||||
constexpr int GAMEPAD_TOUCHPAD_DEADZONE = 18;
|
||||
constexpr float GAMEPAD_TOUCHPAD_SPEED = 0.8f;
|
||||
|
||||
MouseTouchPadState g_mouse_touchpad;
|
||||
GamepadTouchPadState g_gamepad_touchpad;
|
||||
std::mutex g_touchpad_mutex;
|
||||
std::unordered_map<int, std::array<bool, 2>> g_pad_trigger_pressed;
|
||||
|
||||
std::size_t ControlFromName(std::string_view name) {
|
||||
const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(),
|
||||
[name](const auto& item) { return item.name == name; });
|
||||
@@ -126,6 +219,14 @@ public:
|
||||
InputMap() {
|
||||
for (const auto& value: Config::GetKeymap()) {
|
||||
const std::string_view entry = value;
|
||||
if (entry == "Keyboard=Disabled") {
|
||||
m_custom = true;
|
||||
continue;
|
||||
}
|
||||
if (entry == "Pad=Disabled") {
|
||||
m_custom_pad = true;
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(MOUSE_SENSITIVITY)) {
|
||||
const float sensitivity =
|
||||
std::strtof(value.c_str() + MOUSE_SENSITIVITY.size(), nullptr);
|
||||
@@ -133,6 +234,13 @@ public:
|
||||
std::clamp(std::isfinite(sensitivity) ? sensitivity : 1.0f, 0.1f, 5.0f);
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(TOUCH_PARTICLE_SKIN) || entry.starts_with("TouchCursorImage=")) {
|
||||
continue;
|
||||
}
|
||||
if (entry.starts_with(PAD_PREFIX)) {
|
||||
AddPadEntry(entry.substr(PAD_PREFIX.size()), value);
|
||||
continue;
|
||||
}
|
||||
const auto split = entry.find('=');
|
||||
|
||||
Binding binding;
|
||||
@@ -153,10 +261,25 @@ public:
|
||||
}
|
||||
Add(binding);
|
||||
}
|
||||
if (!m_custom_pad) {
|
||||
for (const auto& item: PAD_DEFAULTS) {
|
||||
AssignPad(ControlFromName(item.control), SourceFromName(item.source));
|
||||
}
|
||||
}
|
||||
}
|
||||
[[nodiscard]] bool Custom() const { return m_size != 0; }
|
||||
[[nodiscard]] bool Custom() const { return m_custom; }
|
||||
[[nodiscard]] float MouseSensitivity() const { return m_mouse_sensitivity; }
|
||||
|
||||
[[nodiscard]] std::size_t PadControl(std::size_t source) const {
|
||||
return source < m_pad_controls.size() ? m_pad_controls[source] : INVALID_CONTROL;
|
||||
}
|
||||
|
||||
static std::size_t SourceFromButton(int button) {
|
||||
const auto item = std::find_if(PAD_SOURCES.begin(), PAD_SOURCES.end(),
|
||||
[button](const auto& s) { return s.button == button; });
|
||||
return static_cast<std::size_t>(std::distance(PAD_SOURCES.begin(), item));
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t FindKey(int key_code) const {
|
||||
key_code = NormalizeKey(static_cast<SDL_Keycode>(key_code));
|
||||
const auto binding =
|
||||
@@ -173,7 +296,39 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static std::size_t SourceFromName(std::string_view name) {
|
||||
const auto item = std::find_if(PAD_SOURCES.begin(), PAD_SOURCES.end(),
|
||||
[name](const auto& s) { return s.name == name; });
|
||||
return static_cast<std::size_t>(std::distance(PAD_SOURCES.begin(), item));
|
||||
}
|
||||
|
||||
void AssignPad(std::size_t control, std::size_t source) {
|
||||
for (auto& existing: m_pad_controls) {
|
||||
if (existing == control) {
|
||||
existing = INVALID_CONTROL;
|
||||
}
|
||||
}
|
||||
m_pad_controls[source] = control;
|
||||
}
|
||||
|
||||
void AddPadEntry(std::string_view entry, const std::string& original) {
|
||||
const auto split = entry.find('=');
|
||||
const auto control = split != std::string_view::npos ? ControlFromName(entry.substr(0, split))
|
||||
: INVALID_CONTROL;
|
||||
const auto source = split != std::string_view::npos ? SourceFromName(entry.substr(split + 1))
|
||||
: INVALID_SOURCE;
|
||||
if (control == INVALID_CONTROL || source == INVALID_SOURCE) {
|
||||
EXIT("Invalid input mapping: %s\n", original.c_str());
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.button == 0 && !info.gyro && info.axis != Controller::Axis::AxisMax) {
|
||||
EXIT("Invalid input mapping: %s\n", original.c_str());
|
||||
}
|
||||
m_custom_pad = true;
|
||||
AssignPad(control, source);
|
||||
}
|
||||
void Add(const Binding& binding) {
|
||||
m_custom = true;
|
||||
for (std::size_t index = 0; index < m_size;) {
|
||||
if (Conflicts(m_bindings[index], binding)) {
|
||||
m_bindings[index] = m_bindings[--m_size];
|
||||
@@ -186,6 +341,13 @@ private:
|
||||
}
|
||||
|
||||
std::array<Binding, CONTROL_INFO.size()> m_bindings {};
|
||||
std::array<std::size_t, PAD_SOURCES.size()> m_pad_controls = [] {
|
||||
std::array<std::size_t, PAD_SOURCES.size()> a {};
|
||||
a.fill(INVALID_CONTROL);
|
||||
return a;
|
||||
}();
|
||||
bool m_custom = false;
|
||||
bool m_custom_pad = false;
|
||||
std::size_t m_size = 0;
|
||||
float m_mouse_sensitivity = 1.0f;
|
||||
};
|
||||
@@ -211,6 +373,39 @@ void SetTouchPad(float x, bool down) {
|
||||
Controller::SetTouchPad(Controller::HOST_INPUT_CONTROLLER_ID, 0, down, x, 0.5f);
|
||||
}
|
||||
|
||||
void SetSwipeTouchPad(bool down, float x, float y) {
|
||||
Controller::SetTouchPad(Controller::HOST_INPUT_CONTROLLER_ID, 1, down, x, y);
|
||||
}
|
||||
|
||||
void RecordMouseTouchPadPosition(float x, float y) {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
if (g_mouse_window != nullptr) {
|
||||
SDL_GetWindowSize(g_mouse_window, &width, &height);
|
||||
}
|
||||
if (width > 0 && height > 0) {
|
||||
x = std::clamp(x / static_cast<float>(width), 0.0f, 1.0f);
|
||||
y = std::clamp(y / static_cast<float>(height), 0.0f, 1.0f);
|
||||
} else {
|
||||
x = 0.5f;
|
||||
y = 0.5f;
|
||||
}
|
||||
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_mouse_touchpad.enabled) {
|
||||
return;
|
||||
}
|
||||
g_mouse_touchpad.x = x;
|
||||
g_mouse_touchpad.y = y;
|
||||
g_mouse_touchpad.position_valid = true;
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
|
||||
bool TouchPadModeActive() {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
return g_mouse_touchpad.enabled || g_gamepad_touchpad.enabled;
|
||||
}
|
||||
|
||||
uint32_t DefaultKeyboardButton(int key_code) {
|
||||
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
|
||||
case SDLK_UP: return Controller::PAD_BUTTON_UP;
|
||||
@@ -270,6 +465,18 @@ void SetControl(std::size_t control, bool down) {
|
||||
SetButton(info.button, down);
|
||||
return;
|
||||
}
|
||||
if (info.gyro) {
|
||||
if (down) {
|
||||
Controller::ToggleGyroEmulation(Controller::HOST_INPUT_CONTROLLER_ID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.touchpad_mode) {
|
||||
if (down) {
|
||||
HostInputToggleTouchPadMode();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.axis == Controller::Axis::AxisMax) {
|
||||
CycleSettingOnPress(info.setting, down);
|
||||
return;
|
||||
@@ -297,6 +504,11 @@ void DefaultKeyboardInput(int key_code, bool down) {
|
||||
|
||||
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
|
||||
case SDLK_BACKSPACE: SetTouchPad(0.25f, down); return;
|
||||
case SDLK_F6:
|
||||
if (down) {
|
||||
HostInputToggleTouchPadMode();
|
||||
}
|
||||
return;
|
||||
case SDLK_TAB: SetTouchPad(0.75f, down); return;
|
||||
case SDLK_1: CycleSettingOnPress(Controller::Setting::SpeakerVolume, down); return;
|
||||
case SDLK_2: CycleSettingOnPress(Controller::Setting::VibrationIntensity, down); return;
|
||||
@@ -399,8 +611,134 @@ bool IsCursorActivity(const SDL_Event& event) {
|
||||
return which != SDL_TOUCH_MOUSEID && which != SDL_PEN_MOUSEID;
|
||||
}
|
||||
|
||||
std::array<int, 2> GamepadLeftStick(int id) {
|
||||
auto* pad = SDL_GetGamepadFromID(id);
|
||||
if (pad == nullptr) {
|
||||
return {128, 128};
|
||||
}
|
||||
const auto value = [pad](SDL_GamepadAxis axis) {
|
||||
return Controller::controller_get_axis(SDL_JOYSTICK_AXIS_MIN, SDL_JOYSTICK_AXIS_MAX,
|
||||
SDL_GetGamepadAxis(pad, axis));
|
||||
};
|
||||
return {value(SDL_GAMEPAD_AXIS_LEFTX), value(SDL_GAMEPAD_AXIS_LEFTY)};
|
||||
}
|
||||
|
||||
void ApplyPadControl(int id, std::size_t control, std::size_t source, bool down) {
|
||||
if (control == INVALID_CONTROL) {
|
||||
return;
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.gyro) {
|
||||
if (down) {
|
||||
Controller::ToggleGyroEmulation(id);
|
||||
}
|
||||
} else if (info.touchpad_mode) {
|
||||
if (down) {
|
||||
HostInputToggleGamepadTouchPad(id);
|
||||
}
|
||||
} else if (info.button == Controller::PAD_BUTTON_CROSS && HostInputGamepadDrawButton(id, down)) {
|
||||
return;
|
||||
} else if (info.button == Controller::PAD_BUTTON_TOUCH_PAD) {
|
||||
// Real touchpads report their own finger position; other inputs emulate a click.
|
||||
if (source != PAD_SOURCES.size() && PAD_SOURCES[source].button != SDL_GAMEPAD_BUTTON_TOUCHPAD) {
|
||||
Controller::SetTouchPad(id, 0, down, info.touch_x != 0.0f ? info.touch_x : 0.5f, 0.5f);
|
||||
}
|
||||
Controller::SetButton(id, Controller::PAD_BUTTON_TOUCH_PAD, down);
|
||||
} else if (info.button == Controller::PAD_BUTTON_L2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerLeft, down ? 255 : 0);
|
||||
} else if (info.button == Controller::PAD_BUTTON_R2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerRight, down ? 255 : 0);
|
||||
} else if (info.button != 0) {
|
||||
Controller::SetButton(id, info.button, down);
|
||||
} else if (info.axis == Controller::Axis::AxisMax) {
|
||||
CycleSettingOnPress(info.setting, down);
|
||||
}
|
||||
}
|
||||
|
||||
int PollGamepadTouchPad(uint64_t now_ms) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
int id = -1;
|
||||
bool drawing = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled) {
|
||||
return -1;
|
||||
}
|
||||
if (now_ms < g_gamepad_touchpad.next_poll) {
|
||||
return static_cast<int>(g_gamepad_touchpad.next_poll - now_ms);
|
||||
}
|
||||
|
||||
const float dt = g_gamepad_touchpad.last_poll == 0 || now_ms <= g_gamepad_touchpad.last_poll
|
||||
? 0.008f
|
||||
: std::min(static_cast<float>(now_ms - g_gamepad_touchpad.last_poll) *
|
||||
0.001f,
|
||||
0.05f);
|
||||
g_gamepad_touchpad.last_poll = now_ms;
|
||||
g_gamepad_touchpad.next_poll = now_ms + GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
const auto velocity = [](int axis) {
|
||||
const int offset = axis - 128;
|
||||
if (std::abs(offset) <= GAMEPAD_TOUCHPAD_DEADZONE) {
|
||||
return 0.0f;
|
||||
}
|
||||
const int adjusted =
|
||||
offset > 0 ? offset - GAMEPAD_TOUCHPAD_DEADZONE : offset + GAMEPAD_TOUCHPAD_DEADZONE;
|
||||
return static_cast<float>(adjusted) / static_cast<float>(127 - GAMEPAD_TOUCHPAD_DEADZONE);
|
||||
};
|
||||
const float old_x = g_gamepad_touchpad.x;
|
||||
const float old_y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad.x = std::clamp(
|
||||
old_x + velocity(g_gamepad_touchpad.x_axis) * GAMEPAD_TOUCHPAD_SPEED * dt, 0.0f, 1.0f);
|
||||
g_gamepad_touchpad.y = std::clamp(
|
||||
old_y + velocity(g_gamepad_touchpad.y_axis) * GAMEPAD_TOUCHPAD_SPEED * dt, 0.0f, 1.0f);
|
||||
if (old_x != g_gamepad_touchpad.x || old_y != g_gamepad_touchpad.y) {
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
id = g_gamepad_touchpad.id;
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
drawing = g_gamepad_touchpad.drawing;
|
||||
}
|
||||
if (drawing) {
|
||||
Controller::SetTouchPad(id, 1, true, x, y);
|
||||
}
|
||||
return GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void HostInputPadButton(int id, int button, bool down) {
|
||||
const auto source = InputMap::SourceFromButton(button);
|
||||
const auto& map = GetInputMap();
|
||||
ApplyPadControl(id, map.PadControl(source), source, down);
|
||||
}
|
||||
|
||||
void HostInputPadTrigger(int id, bool left, int value) {
|
||||
const auto source = left ? LEFT_TRIGGER_SOURCE : RIGHT_TRIGGER_SOURCE;
|
||||
const auto control = GetInputMap().PadControl(source);
|
||||
if (control == INVALID_CONTROL) {
|
||||
return;
|
||||
}
|
||||
const auto& info = CONTROL_INFO[control];
|
||||
if (info.button == Controller::PAD_BUTTON_L2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerLeft, value);
|
||||
} else if (info.button == Controller::PAD_BUTTON_R2) {
|
||||
Controller::SetAxis(id, Controller::Axis::TriggerRight, value);
|
||||
} else {
|
||||
auto& state = g_pad_trigger_pressed[id][left ? 0 : 1];
|
||||
const bool down = state ? value > 32 : value > 96;
|
||||
if (down != state) {
|
||||
state = down;
|
||||
ApplyPadControl(id, control, source, down);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputDisconnectGamepad(int id) {
|
||||
g_pad_trigger_pressed.erase(id);
|
||||
HostInputDisconnectGamepadTouchPad(id);
|
||||
}
|
||||
|
||||
void HostInputInit(SDL_Window* window) {
|
||||
GetInputMap();
|
||||
g_mouse_window = window;
|
||||
@@ -410,6 +748,16 @@ void HostInputInit(SDL_Window* window) {
|
||||
}
|
||||
|
||||
void HostInputShutdown() {
|
||||
g_pad_trigger_pressed.clear();
|
||||
HostInputSetTouchPadMode(false);
|
||||
int gamepad_touch_id = -1;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
gamepad_touch_id = g_gamepad_touchpad.enabled ? g_gamepad_touchpad.id : -1;
|
||||
}
|
||||
if (gamepad_touch_id >= 0) {
|
||||
HostInputDisconnectGamepadTouchPad(gamepad_touch_id);
|
||||
}
|
||||
if (g_mouse.enabled) {
|
||||
SetRelativeMouseMode(false);
|
||||
CenterMouseStick();
|
||||
@@ -438,6 +786,219 @@ void HostInputMouseButton(uint8_t mouse_button, bool down) {
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputMouseTouchPad(bool down, float x, float y) {
|
||||
RecordMouseTouchPadPosition(x, y);
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_mouse_touchpad.enabled) {
|
||||
return;
|
||||
}
|
||||
if (down && !g_mouse_touchpad.drawing) {
|
||||
g_mouse_touchpad.gesture++;
|
||||
}
|
||||
g_mouse_touchpad.drawing = down;
|
||||
g_mouse_touchpad.revision++;
|
||||
x = g_mouse_touchpad.x;
|
||||
y = g_mouse_touchpad.y;
|
||||
}
|
||||
SetSwipeTouchPad(down, x, y);
|
||||
}
|
||||
|
||||
void HostInputMouseMotion(float x, float y) {
|
||||
RecordMouseTouchPadPosition(x, y);
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_mouse_touchpad.enabled && g_mouse_touchpad.drawing) {
|
||||
SetSwipeTouchPad(true, g_mouse_touchpad.x, g_mouse_touchpad.y);
|
||||
}
|
||||
}
|
||||
|
||||
void HostInputSetTouchPadMode(bool enabled) {
|
||||
if (enabled) {
|
||||
int gamepad_id = -1;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_gamepad_touchpad.enabled) {
|
||||
gamepad_id = g_gamepad_touchpad.id;
|
||||
}
|
||||
}
|
||||
if (gamepad_id >= 0) {
|
||||
HostInputDisconnectGamepadTouchPad(gamepad_id);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_mouse_touchpad.enabled == enabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!enabled) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
(void) SDL_GetMouseState(&x, &y);
|
||||
HostInputMouseTouchPad(false, x, y);
|
||||
}
|
||||
bool show_system_cursor = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
g_mouse_touchpad.enabled = enabled;
|
||||
g_mouse_touchpad.cursor_visible = true;
|
||||
g_mouse_touchpad.drawing = false;
|
||||
g_mouse_touchpad.position_valid = false;
|
||||
g_mouse_touchpad.gesture++;
|
||||
g_mouse_touchpad.revision++;
|
||||
show_system_cursor = !enabled && !g_gamepad_touchpad.enabled;
|
||||
}
|
||||
if (enabled) {
|
||||
if (!SDL_HideCursor()) {
|
||||
LOGF("Failed to hide mouse cursor for touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
} else if (show_system_cursor && !Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
LOGF("Mouse touchpad mode: %s\n", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
void HostInputToggleTouchPadMode() {
|
||||
bool enabled = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
enabled = !g_mouse_touchpad.enabled;
|
||||
}
|
||||
HostInputSetTouchPadMode(enabled);
|
||||
}
|
||||
|
||||
void HostInputToggleGamepadTouchPad(int id) {
|
||||
bool mouse_mode = false;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
mouse_mode = g_mouse_touchpad.enabled;
|
||||
}
|
||||
if (mouse_mode) {
|
||||
HostInputSetTouchPadMode(false);
|
||||
}
|
||||
|
||||
const auto stick = GamepadLeftStick(id);
|
||||
bool enabled = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (g_gamepad_touchpad.enabled && g_gamepad_touchpad.id != id) {
|
||||
return;
|
||||
}
|
||||
enabled = !g_gamepad_touchpad.enabled;
|
||||
if (enabled) {
|
||||
g_gamepad_touchpad = {};
|
||||
g_gamepad_touchpad.enabled = true;
|
||||
g_gamepad_touchpad.id = id;
|
||||
g_gamepad_touchpad.x_axis = stick[0];
|
||||
g_gamepad_touchpad.y_axis = stick[1];
|
||||
g_gamepad_touchpad.last_poll = SDL_GetTicks();
|
||||
g_gamepad_touchpad.next_poll = g_gamepad_touchpad.last_poll + GAMEPAD_TOUCHPAD_POLL_MS;
|
||||
g_mouse_touchpad.gesture++;
|
||||
} else {
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad = {};
|
||||
}
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetAxis(id, Controller::Axis::LeftX, enabled ? 128 : stick[0]);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftY, enabled ? 128 : stick[1]);
|
||||
if (enabled) {
|
||||
if (!SDL_HideCursor()) {
|
||||
LOGF("Failed to hide mouse cursor for gamepad touchpad mode: %s\n", SDL_GetError());
|
||||
}
|
||||
} else {
|
||||
Controller::SetTouchPad(id, 1, false, x, y);
|
||||
if (!Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after gamepad touchpad mode: %s\n",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
LOGF("Gamepad touchpad mode: %s\n", enabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
bool HostInputGamepadDrawButton(int id, bool down) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return false;
|
||||
}
|
||||
if (!down && !g_gamepad_touchpad.drawing) {
|
||||
return false;
|
||||
}
|
||||
if (down && !g_gamepad_touchpad.drawing) {
|
||||
g_mouse_touchpad.gesture++;
|
||||
}
|
||||
g_gamepad_touchpad.drawing = down;
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetTouchPad(id, 1, down, x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HostInputGamepadAxis(int id, bool horizontal, int value) {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return false;
|
||||
}
|
||||
(horizontal ? g_gamepad_touchpad.x_axis : g_gamepad_touchpad.y_axis) = std::clamp(value, 0, 255);
|
||||
g_gamepad_touchpad.next_poll = SDL_GetTicks();
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostInputDisconnectGamepadTouchPad(int id) {
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
{
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
if (!g_gamepad_touchpad.enabled || g_gamepad_touchpad.id != id) {
|
||||
return;
|
||||
}
|
||||
x = g_gamepad_touchpad.x;
|
||||
y = g_gamepad_touchpad.y;
|
||||
g_gamepad_touchpad = {};
|
||||
g_mouse_touchpad.revision++;
|
||||
}
|
||||
Controller::SetTouchPad(id, 1, false, x, y);
|
||||
const auto stick = GamepadLeftStick(id);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftX, stick[0]);
|
||||
Controller::SetAxis(id, Controller::Axis::LeftY, stick[1]);
|
||||
if (!TouchPadModeActive() && !Config::HideCursorEnabled() && !SDL_ShowCursor()) {
|
||||
LOGF("Failed to restore mouse cursor after gamepad disconnect: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
HostInputTouchPadVisualState GetHostInputTouchPadVisualState() {
|
||||
std::scoped_lock lock(g_touchpad_mutex);
|
||||
HostInputTouchPadVisualState state {};
|
||||
if (g_gamepad_touchpad.enabled) {
|
||||
state.enabled = true;
|
||||
state.cursor_visible = true;
|
||||
state.drawing = g_gamepad_touchpad.drawing;
|
||||
state.x = g_gamepad_touchpad.x;
|
||||
state.y = g_gamepad_touchpad.y;
|
||||
state.revision = g_mouse_touchpad.revision;
|
||||
state.gesture = g_mouse_touchpad.gesture;
|
||||
state.position_valid = true;
|
||||
return state;
|
||||
}
|
||||
state.enabled = g_mouse_touchpad.enabled;
|
||||
state.cursor_visible = g_mouse_touchpad.cursor_visible;
|
||||
state.drawing = g_mouse_touchpad.drawing;
|
||||
state.x = g_mouse_touchpad.x;
|
||||
state.y = g_mouse_touchpad.y;
|
||||
state.revision = g_mouse_touchpad.revision;
|
||||
state.gesture = g_mouse_touchpad.gesture;
|
||||
state.position_valid = g_mouse_touchpad.position_valid;
|
||||
return state;
|
||||
}
|
||||
|
||||
void HostInputToggleMouseToJoystick() {
|
||||
if (g_mouse.enabled) {
|
||||
SetRelativeMouseMode(false);
|
||||
@@ -467,16 +1028,19 @@ bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms) {
|
||||
timeout = PollMouse(SDL_GetTicks());
|
||||
}
|
||||
|
||||
if (g_cursor_hide_at != 0) {
|
||||
if (g_cursor_hide_at != 0 && !TouchPadModeActive()) {
|
||||
const auto now_ms = SDL_GetTicks();
|
||||
const int cursor_timeout =
|
||||
now_ms < g_cursor_hide_at ? static_cast<int>(g_cursor_hide_at - now_ms) : 0;
|
||||
timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout);
|
||||
}
|
||||
if (const int touch_timeout = PollGamepadTouchPad(SDL_GetTicks()); touch_timeout >= 0) {
|
||||
timeout = timeout < 0 ? touch_timeout : std::min(timeout, touch_timeout);
|
||||
}
|
||||
timeout = timeout < 0 ? max_wait_ms : std::min(timeout, max_wait_ms);
|
||||
const bool has_event = SDL_WaitEventTimeout(event, timeout);
|
||||
|
||||
if (Config::HideCursorEnabled()) {
|
||||
if (Config::HideCursorEnabled() && !TouchPadModeActive()) {
|
||||
const auto now_ms = SDL_GetTicks();
|
||||
if (has_event && !g_mouse.enabled && IsCursorActivity(*event) &&
|
||||
SDL_GetWindowFromEvent(event) == g_mouse_window) {
|
||||
|
||||
@@ -8,10 +8,33 @@ union SDL_Event;
|
||||
|
||||
namespace Libs::Graphics {
|
||||
|
||||
struct HostInputTouchPadVisualState {
|
||||
bool enabled = false;
|
||||
bool cursor_visible = true;
|
||||
bool drawing = false;
|
||||
bool position_valid = false;
|
||||
float x = 0.5f;
|
||||
float y = 0.5f;
|
||||
uint64_t revision = 0;
|
||||
uint64_t gesture = 0;
|
||||
};
|
||||
|
||||
void HostInputInit(SDL_Window* window);
|
||||
void HostInputShutdown();
|
||||
void HostInputKey(int key_code, bool down);
|
||||
void HostInputMouseButton(uint8_t mouse_button, bool down);
|
||||
void HostInputMouseTouchPad(bool down, float x, float y);
|
||||
void HostInputMouseMotion(float x, float y);
|
||||
void HostInputPadButton(int id, int sdl_button, bool down);
|
||||
void HostInputPadTrigger(int id, bool left, int value);
|
||||
void HostInputDisconnectGamepad(int id);
|
||||
void HostInputSetTouchPadMode(bool enabled);
|
||||
void HostInputToggleTouchPadMode();
|
||||
void HostInputToggleGamepadTouchPad(int id);
|
||||
[[nodiscard]] bool HostInputGamepadDrawButton(int id, bool down);
|
||||
[[nodiscard]] bool HostInputGamepadAxis(int id, bool horizontal, int value);
|
||||
void HostInputDisconnectGamepadTouchPad(int id);
|
||||
[[nodiscard]] HostInputTouchPadVisualState GetHostInputTouchPadVisualState();
|
||||
void HostInputToggleMouseToJoystick();
|
||||
[[nodiscard]] bool HostInputWaitEvent(SDL_Event* event, int max_wait_ms);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "graphics/presentation/window.h"
|
||||
#include "graphics/presentation/window.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
@@ -54,25 +54,6 @@ struct EventKeyboard {
|
||||
double timestamp_seconds;
|
||||
};
|
||||
|
||||
static uint32_t ControllerButtonToPadButton(int button) {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH: return Controller::PAD_BUTTON_CROSS;
|
||||
case SDL_GAMEPAD_BUTTON_EAST: return Controller::PAD_BUTTON_CIRCLE;
|
||||
case SDL_GAMEPAD_BUTTON_WEST: return Controller::PAD_BUTTON_SQUARE;
|
||||
case SDL_GAMEPAD_BUTTON_NORTH: return Controller::PAD_BUTTON_TRIANGLE;
|
||||
case SDL_GAMEPAD_BUTTON_START: return Controller::PAD_BUTTON_OPTIONS;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_STICK: return Controller::PAD_BUTTON_L3;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_STICK: return Controller::PAD_BUTTON_R3;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: return Controller::PAD_BUTTON_L1;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return Controller::PAD_BUTTON_R1;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP: return Controller::PAD_BUTTON_UP;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN: return Controller::PAD_BUTTON_DOWN;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT: return Controller::PAD_BUTTON_LEFT;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: return Controller::PAD_BUTTON_RIGHT;
|
||||
case SDL_GAMEPAD_BUTTON_TOUCHPAD: return Controller::PAD_BUTTON_TOUCH_PAD;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static Controller::Axis ControllerAxisFromSdl(int axis_id) {
|
||||
switch (axis_id) {
|
||||
@@ -268,6 +249,15 @@ static void GameEventMouse([[maybe_unused]] const EventMouse& mb) {
|
||||
}
|
||||
|
||||
HostInputMouseButton(mouse_button, mb.down);
|
||||
if (!mb.touch && mb.left) {
|
||||
HostInputMouseTouchPad(mb.down, static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
}
|
||||
} else if (mb.motion && !mb.touch) {
|
||||
if (mb.left) {
|
||||
HostInputMouseTouchPad(true, static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
} else {
|
||||
HostInputMouseMotion(static_cast<float>(mb.x), static_cast<float>(mb.y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +306,7 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
|
||||
}
|
||||
|
||||
if (f.removed) {
|
||||
HostInputDisconnectGamepad(f.id);
|
||||
if (auto* pad = SDL_GetGamepadFromID(f.id); pad != nullptr) {
|
||||
Controller::Disconnect(f.id);
|
||||
SDL_CloseGamepad(pad);
|
||||
@@ -323,30 +314,22 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
|
||||
}
|
||||
|
||||
if (f.down || f.up) {
|
||||
const auto button = ControllerButtonToPadButton(f.button);
|
||||
if (button != 0) {
|
||||
Controller::SetButton(f.id, button, f.down);
|
||||
} else if (f.button == SDL_GAMEPAD_BUTTON_BACK) {
|
||||
// Pads without a touchpad (Switch, Xbox, ...) have no way to click it, which
|
||||
// blocks games that require it. Use their Back/Select button as a centered click.
|
||||
auto* pad = SDL_GetGamepadFromID(f.id);
|
||||
if (pad != nullptr && SDL_GetNumGamepadTouchpads(pad) == 0) {
|
||||
if (f.down) {
|
||||
Controller::SetTouchPad(f.id, 0, true, 0.5f, 0.5f);
|
||||
}
|
||||
Controller::SetButton(f.id, Controller::PAD_BUTTON_TOUCH_PAD, f.down);
|
||||
if (f.up) {
|
||||
Controller::SetTouchPad(f.id, 0, false, 0.5f, 0.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
HostInputPadButton(f.id, f.button, f.down);
|
||||
}
|
||||
|
||||
if (f.axis) {
|
||||
const auto axis = ControllerAxisFromSdl(f.axis_id);
|
||||
if (axis != Controller::Axis::AxisMax) {
|
||||
Controller::SetAxis(f.id, axis,
|
||||
if (f.axis_id == SDL_GAMEPAD_AXIS_LEFT_TRIGGER ||
|
||||
f.axis_id == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
HostInputPadTrigger(f.id, f.axis_id == SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
|
||||
ControllerAxisValueFromSdl(f.axis_id, f.axis_value));
|
||||
} else if (const auto axis = ControllerAxisFromSdl(f.axis_id);
|
||||
axis != Controller::Axis::AxisMax) {
|
||||
const int value = ControllerAxisValueFromSdl(f.axis_id, f.axis_value);
|
||||
if ((f.axis_id == SDL_GAMEPAD_AXIS_LEFTX || f.axis_id == SDL_GAMEPAD_AXIS_LEFTY) &&
|
||||
HostInputGamepadAxis(f.id, f.axis_id == SDL_GAMEPAD_AXIS_LEFTX, value)) {
|
||||
return;
|
||||
}
|
||||
Controller::SetAxis(f.id, axis, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,6 +414,7 @@ void WindowContext::ProcessWindowEvent(const SDL_WindowEvent& event) {
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_MOUSE_LEAVE:
|
||||
LOGF("Mouse left window %" PRIu32 "\n", window_event.windowID);
|
||||
HostInputSetTouchPadMode(false);
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
LOGF("Window %" PRIu32 " gained keyboard focus\n", window_event.windowID);
|
||||
|
||||
Reference in New Issue
Block a user