Files
ppsspp/UI/GamepadEmu.cpp
T
Henrik RydgårdandClaude Opus 5.5 f364f297f1 Touch controls: Use the full width when the side insets are only for a top corner
On the iPhone Duo's cover screen, the camera sits in a box in the top right
corner, which gives a side inset down the whole right edge in portrait. The
controls are far below it, so they lose width for nothing. iOS 27.1 reports
the box as an occlusion region, and from that we report how far down the side
insets are needed (SYSPROP_DISPLAY_SIDE_INSETS_BOTTOM). When that's within the
top third, the game screen and the touch layout editor skip the side insets.
The rest of the UI, including the FPS counter, still keeps them.

The regions can show up after the size and insets have settled, so the insets
are now recomputed on every layout, and a screen recreates its views when its
layout mode changes, since the touch controls are placed for the bounds they
were created in.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 09:39:49 +01:00

1338 lines
45 KiB
C++

// Copyright (c) 2012- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include "Common/Data/Color/RGBAUtil.h"
#include "Common/Data/Text/I18n.h"
#include "Common/System/Display.h"
#include "Common/System/System.h"
#include "Common/Render/TextureAtlas.h"
#include "Common/Math/math_util.h"
#include "Common/UI/Context.h"
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Core/Config.h"
#include "Core/Core.h"
#include "Core/System.h"
#include "Core/HLE/sceCtrl.h"
#include "Core/ControlMapper.h"
#include "UI/GamepadEmu.h"
const float TOUCH_SCALE_FACTOR = 1.5f;
static uint32_t usedPointerMask = 0;
static uint32_t analogPointerMask = 0;
static float g_gamepadOpacity;
static double g_lastTouch;
MultiTouchButton *primaryButton[TOUCH_MAX_POINTERS]{};
std::set<int> g_activeGesturePointers;
void GamepadUpdateOpacity(float force) {
if (force >= 0.0f) {
g_gamepadOpacity = force;
return;
}
if (coreState == CORE_RUNTIME_ERROR || coreState == CORE_POWERDOWN || coreState == CORE_STEPPING_GE) {
// No need to show the controls.
g_gamepadOpacity = 0.0f;
return;
}
float fadeAfterSeconds = g_Config.iTouchButtonHideSeconds;
float fadeTransitionSeconds = std::min(fadeAfterSeconds, 0.5f);
float opacity = g_Config.iTouchButtonOpacity / 100.0f;
float multiplier = 1.0f;
float secondsWithoutTouch = time_now_d() - g_lastTouch;
if (secondsWithoutTouch >= fadeAfterSeconds && fadeAfterSeconds > 0.0f) {
if (secondsWithoutTouch >= fadeAfterSeconds + fadeTransitionSeconds) {
multiplier = 0.0f;
} else {
float secondsIntoFade = secondsWithoutTouch - fadeAfterSeconds;
multiplier = 1.0f - (secondsIntoFade / fadeTransitionSeconds);
}
}
g_gamepadOpacity = opacity * multiplier;
}
void GamepadResetTouch() {
g_lastTouch = 0.0f;
}
void GamepadTouch() {
g_lastTouch = time_now_d();
}
float GamepadGetOpacity() {
return g_gamepadOpacity;
}
static u32 GetButtonColor() {
return g_Config.iTouchButtonStyle != 0 ? 0xFFFFFF : 0xc0b080;
}
GamepadComponent::GamepadComponent(std::string_view key, UI::LayoutParams *layoutParams) : UI::View(layoutParams), key_(key) {}
static void rotateTouchHelper(float &dx, float &dy) {
// rotates dx and dy depending on rotation count
// modified from controlmapper.cpp
const DisplayLayoutConfig &layout = g_Config.GetDisplayLayoutConfig(g_display.GetDeviceOrientation());
if (!layout.bRotateControlsWithScreen) {
return;
}
int rotations = 0;
switch (layout.iInternalScreenRotation) {
case ROTATION_LOCKED_HORIZONTAL180: rotations = 2; break;
case ROTATION_LOCKED_VERTICAL: rotations = 3; break;
case ROTATION_LOCKED_VERTICAL180: rotations = 1; break;
}
for (int i = 0; i < rotations; i ++) {
float tempVal = dx;
dx = - dy;
dy = tempVal;
}
}
std::string GamepadComponent::DescribeText() const {
return key_;
}
void MultiTouchButton::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(bgImg_);
if (image) {
w = image->w * scale_;
h = image->h * scale_;
} else {
w = 0.0f;
h = 0.0f;
}
}
bool MultiTouchButton::CanGlide() const {
return g_Config.bTouchGliding;
}
bool MultiTouchButton::Touch(const TouchInput &input) {
// input.id comes from platform-specific touch/pointer IDs. Most of our
// platforms try to remap to [0, TOUCH_MAX_POINTERS), and all will be updated
// to do that in the future.
if (input.id < 0 || input.id >= TOUCH_MAX_POINTERS) {
return false;
}
bool retval = GamepadComponent::Touch(input);
if ((input.flags & TouchInputFlags::DOWN) && bounds_.Contains(input.x, input.y)) {
pointerDownMask_ |= 1 << input.id;
usedPointerMask |= 1 << input.id;
if (CanGlide() && !primaryButton[input.id])
primaryButton[input.id] = this;
}
if (input.flags & TouchInputFlags::MOVE) {
if (!(input.flags & TouchInputFlags::MOUSE) || input.buttons) {
bool ignorePress = false;
if (g_activeGesturePointers.count(input.id)) {
// Don't start a button touch if dragging and this pointer is already being used for a gesture.
ignorePress = true;
}
if (bounds_.Contains(input.x, input.y) && !(analogPointerMask & (1 << input.id))) {
if (!ignorePress) {
if (CanGlide() && !primaryButton[input.id]) {
primaryButton[input.id] = this;
}
pointerDownMask_ |= 1 << input.id;
}
} else if (primaryButton[input.id] != this) {
pointerDownMask_ &= ~(1 << input.id);
}
}
}
if (input.flags & TouchInputFlags::UP) {
pointerDownMask_ &= ~(1 << input.id);
usedPointerMask &= ~(1 << input.id);
primaryButton[input.id] = nullptr;
}
if (input.flags & TouchInputFlags::RELEASE_ALL) {
pointerDownMask_ = 0;
usedPointerMask = 0;
}
return retval;
}
void MultiTouchButton::Draw(UIContext &dc) {
float opacity = std::max(g_gamepadOpacity, minimumAlpha_);
if (opacity <= 0.0f)
return;
float scale = scale_;
if (IsDownVisually()) {
if (g_Config.iTouchButtonStyle == 2) {
opacity *= 1.35f;
} else {
scale *= TOUCH_SCALE_FACTOR;
opacity *= 1.15f;
}
}
uint32_t colorBg = colorAlpha(GetButtonColor(), opacity);
uint32_t downBg = colorAlpha(0xFFFFFF, opacity * 0.5f);
uint32_t color = colorAlpha(0xFFFFFF, opacity);
if (IsDownVisually() && g_Config.iTouchButtonStyle == 2) {
if (bgImg_ != bgDownImg_)
dc.Draw()->DrawImageRotated(bgDownImg_, bounds_.centerX(), bounds_.centerY(), scale, bgAngle_ * (M_PI * 2 / 360.0f), downBg, flipImageH_);
}
dc.Draw()->DrawImageRotated(bgImg_, bounds_.centerX(), bounds_.centerY(), scale, bgAngle_ * (M_PI * 2 / 360.0f), colorBg, flipImageH_);
float x = bounds_.centerX();
float y = bounds_.centerY();
// Hack round the fact that the center of the rectangular picture the triangle is contained in
// is not at the "weight center" of the triangle. Same for fast-forward.
if (img_ == ImageID("I_TRIANGLE"))
y -= 2.8f * scale;
if (img_ == ImageID("I_FAST_FORWARD_LINE"))
x += 2.0f * scale;
dc.Draw()->DrawImageRotated(img_, x, y, scale, angle_ * (M_PI * 2 / 360.0f), color);
}
bool BoolButton::Touch(const TouchInput &input) {
bool lastDown = pointerDownMask_ != 0;
bool retval = MultiTouchButton::Touch(input);
bool down = pointerDownMask_ != 0;
if (down != lastDown) {
*value_ = down;
UI::EventParams params{ this };
params.a = down;
OnChange.Trigger(params);
}
return retval;
}
bool PSPButton::Touch(const TouchInput &input) {
bool lastDown = pointerDownMask_ != 0;
bool retval = MultiTouchButton::Touch(input);
bool down = pointerDownMask_ != 0;
if (down && !lastDown) {
if (g_Config.bHapticFeedback) {
System_Vibrate(HAPTIC_VIRTUAL_KEY);
}
__CtrlUpdateButtons(pspButtonBit_, 0);
} else if (lastDown && !down) {
__CtrlUpdateButtons(0, pspButtonBit_);
}
return retval;
}
bool CustomButton::IsDownVisually() const {
return (toggle_ && on_) || (!toggle_ && pointerDownMask_ != 0);
}
bool CustomButton::IsDownByTouch() const {
// This check is due to a stupid mistake, see the header.
#ifdef MOBILE_DEVICE
constexpr int bitNumber = 36;
#else
constexpr int bitNumber = 38;
#endif
if (pspButtonBit_ & (1ULL << bitNumber)) {
return false;
}
// VIRTKEY_TOGGLE_TOUCH_CONTROLS from g_customKeyList
return !toggle_ && pointerDownMask_ != 0;
}
void CustomButton::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
MultiTouchButton::GetContentDimensions(dc, w, h);
if (invertedContentDimension_) {
float tmp = w;
w = h;
h = tmp;
}
}
bool CustomButton::Touch(const TouchInput &input) {
using namespace CustomKeyData;
bool lastDown = pointerDownMask_ != 0;
bool retval = MultiTouchButton::Touch(input);
bool down = pointerDownMask_ != 0;
if (down && !lastDown) {
if (g_Config.bHapticFeedback)
System_Vibrate(HAPTIC_VIRTUAL_KEY);
if (!repeat_) {
for (int i = 0; i < ARRAY_SIZE(g_customKeyList); i++) {
if (pspButtonBit_ & (1ULL << i)) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, g_customKeyList[i].c, (on_ && toggle_) ? KeyInputFlags::UP : KeyInputFlags::DOWN);
}
}
}
on_ = toggle_ ? !on_ : true;
} else if (!toggle_ && lastDown && !down) {
if (!repeat_) {
for (int i = 0; i < ARRAY_SIZE(g_customKeyList); i++) {
if (pspButtonBit_ & (1ULL << i)) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, g_customKeyList[i].c, KeyInputFlags::UP);
}
}
}
on_ = false;
}
return retval;
}
void CustomButton::Update() {
MultiTouchButton::Update();
using namespace CustomKeyData;
if (repeat_) {
// Give the game some time to process the input, frame based so it's faster when fast-forwarding.
static constexpr int DOWN_FRAME = 5;
if (pressedFrames_ == 2*DOWN_FRAME) {
pressedFrames_ = 0;
} else if (pressedFrames_ == DOWN_FRAME) {
for (int i = 0; i < ARRAY_SIZE(g_customKeyList); i++) {
if (pspButtonBit_ & (1ULL << i)) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, g_customKeyList[i].c, KeyInputFlags::UP);
}
}
} else if (on_ && pressedFrames_ == 0) {
for (int i = 0; i < ARRAY_SIZE(g_customKeyList); i++) {
if (pspButtonBit_ & (1ULL << i)) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, g_customKeyList[i].c, KeyInputFlags::DOWN);
}
}
pressedFrames_ = 1;
}
if (pressedFrames_ > 0)
pressedFrames_++;
}
}
bool PSPButton::IsDownVisually() const {
return (__CtrlPeekButtonsVisual() & pspButtonBit_) != 0;
}
PSPDpad::PSPDpad(ImageID arrowIndex, std::string_view key, ImageID arrowDownIndex, ImageID overlayIndex, float scale, float spacing, UI::LayoutParams *layoutParams)
: GamepadComponent(key, layoutParams), arrowIndex_(arrowIndex), arrowDownIndex_(arrowDownIndex), overlayIndex_(overlayIndex),
scale_(scale), spacing_(spacing), dragPointerId_(-1), down_(0) {
}
void PSPDpad::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(arrowIndex_);
if (image) {
w = 2.0f * D_pad_Radius * spacing_ + image->w * scale_;
h = w;
} else {
w = 0.0f;
h = 0.0f;
}
}
bool PSPDpad::Touch(const TouchInput &input) {
// See the comment in MultiTouchButton::Touch().
if (input.id < 0 || input.id >= TOUCH_MAX_POINTERS) {
return false;
}
bool retval = GamepadComponent::Touch(input);
if (input.flags & TouchInputFlags::DOWN) {
if (dragPointerId_ == -1 && bounds_.Contains(input.x, input.y)) {
dragPointerId_ = input.id;
usedPointerMask |= 1 << input.id;
ProcessTouch(input.x, input.y, true, false);
}
}
if (input.flags & TouchInputFlags::MOVE) {
if (!(input.flags & TouchInputFlags::MOUSE) || input.buttons) {
bool ignorePress = false;
if (g_activeGesturePointers.count(input.id)) {
// Don't start a dpad touch if dragging and this pointer is already being used for a gesture.
ignorePress = true;
}
if (dragPointerId_ == -1 && bounds_.Contains(input.x, input.y) && !(analogPointerMask & (1 << input.id))) {
dragPointerId_ = input.id;
}
if (input.id == dragPointerId_) {
ProcessTouch(input.x, input.y, true, ignorePress);
}
}
}
if (input.flags & TouchInputFlags::UP) {
if (input.id == dragPointerId_) {
dragPointerId_ = -1;
usedPointerMask &= ~(1 << input.id);
ProcessTouch(input.x, input.y, false, false);
}
}
return retval;
}
void PSPDpad::ProcessTouch(float x, float y, bool down, bool ignorePress) {
float stick_size = bounds_.w;
float inv_stick_size = 1.0f / stick_size;
const float deadzone = 0.05f;
float dx = (x - bounds_.centerX()) * inv_stick_size;
float dy = (y - bounds_.centerY()) * inv_stick_size;
float rad = sqrtf(dx * dx + dy * dy);
rotateTouchHelper(dx, dy);
if (!g_Config.bStickyTouchDPad && (rad < deadzone || fabs(dx) > 0.5f || fabs(dy) > 0.5))
down = false;
int ctrlMask = 0;
bool fourWay = g_Config.bDisableDpadDiagonals || rad < 0.2f;
if (down) {
if (fourWay) {
int direction = (int)(floorf((atan2f(dy, dx) / (2 * M_PI) * 4) + 0.5f)) & 3;
switch (direction) {
case 0: ctrlMask = CTRL_RIGHT; break;
case 1: ctrlMask = CTRL_DOWN; break;
case 2: ctrlMask = CTRL_LEFT; break;
case 3: ctrlMask = CTRL_UP; break;
}
// 4 way pad
} else {
// 8 way pad
int direction = (int)(floorf((atan2f(dy, dx) / (2 * M_PI) * 8) + 0.5f)) & 7;
switch (direction) {
case 0: ctrlMask = CTRL_RIGHT; break;
case 1: ctrlMask = CTRL_RIGHT | CTRL_DOWN; break;
case 2: ctrlMask = CTRL_DOWN; break;
case 3: ctrlMask = CTRL_DOWN | CTRL_LEFT; break;
case 4: ctrlMask = CTRL_LEFT; break;
case 5: ctrlMask = CTRL_UP | CTRL_LEFT; break;
case 6: ctrlMask = CTRL_UP; break;
case 7: ctrlMask = CTRL_UP | CTRL_RIGHT; break;
}
}
}
int lastDown = down_;
int pressed = ctrlMask & ~lastDown;
int released = (~ctrlMask) & lastDown;
down_ = ctrlMask;
bool vibrate = false;
static const int dir[4] = { CTRL_RIGHT, CTRL_DOWN, CTRL_LEFT, CTRL_UP };
for (int i = 0; i < 4; i++) {
if (!ignorePress && (pressed & dir[i])) {
vibrate = true;
__CtrlUpdateButtons(dir[i], 0);
}
if (released & dir[i]) {
__CtrlUpdateButtons(0, dir[i]);
}
}
if (vibrate && g_Config.bHapticFeedback) {
System_Vibrate(HAPTIC_VIRTUAL_KEY);
}
}
void PSPDpad::Draw(UIContext &dc) {
float opacity = g_gamepadOpacity;
if (opacity <= 0.0f)
return;
// from control mapper.cpp
int rotations = 0;
const DisplayLayoutConfig &layout = g_Config.GetDisplayLayoutConfig(g_display.GetDeviceOrientation());
if (layout.bRotateControlsWithScreen) {
switch (layout.iInternalScreenRotation) {
case ROTATION_LOCKED_HORIZONTAL180: rotations = 2; break;
case ROTATION_LOCKED_VERTICAL: rotations = 3; break;
case ROTATION_LOCKED_VERTICAL180: rotations = 1; break;
}
}
static const float xoff[4] = {1, 0, -1, 0};
static const float yoff[4] = {0, 1, 0, -1};
static const int dir[4] = {CTRL_RIGHT, CTRL_DOWN, CTRL_LEFT, CTRL_UP};
// shitfs right by amount of rotations (accounts for overflow)
int rotatedDir[4];
for (int i = 0; i < 4; i ++) {
rotatedDir[i] = dir[(i+rotations)%4];
}
int buttons = __CtrlPeekButtons();
float r = D_pad_Radius * spacing_;
for (int i = 0; i < 4; i++) {
bool isDown = (buttons & rotatedDir[i]) != 0;
float x = bounds_.centerX() + xoff[i] * r;
float y = bounds_.centerY() + yoff[i] * r;
float x2 = bounds_.centerX() + xoff[i] * (r + 10.f * scale_);
float y2 = bounds_.centerY() + yoff[i] * (r + 10.f * scale_);
float angle = i * (M_PI / 2.0f);
float imgScale = isDown ? scale_ * TOUCH_SCALE_FACTOR : scale_;
float imgOpacity = opacity;
if (isDown && g_Config.iTouchButtonStyle == 2) {
imgScale = scale_;
imgOpacity *= 1.35f;
uint32_t downBg = colorAlpha(0x00FFFFFF, imgOpacity * 0.5f);
if (arrowIndex_ != arrowDownIndex_)
dc.Draw()->DrawImageRotated(arrowDownIndex_, x, y, imgScale, angle + PI, downBg, false);
}
uint32_t colorBg = colorAlpha(GetButtonColor(), imgOpacity);
uint32_t color = colorAlpha(0xFFFFFF, imgOpacity);
dc.Draw()->DrawImageRotated(arrowIndex_, x, y, imgScale, angle + PI, colorBg, false);
if (overlayIndex_.isValid())
dc.Draw()->DrawImageRotated(overlayIndex_, x2, y2, imgScale, angle + PI, color);
}
}
PSPStick::PSPStick(ImageID bgImg, std::string_view key, ImageID stickImg, ImageID stickDownImg, int stick, float scale, UI::LayoutParams *layoutParams)
: GamepadComponent(key, layoutParams), bgImg_(bgImg), stickImageIndex_(stickImg), stickDownImg_(stickDownImg), stick_(stick), scale_(scale) {
stick_size_ = 50;
}
void PSPStick::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
dc.Draw()->GetAtlas()->measureImage(bgImg_, &w, &h);
w *= scale_;
h *= scale_;
}
void PSPStick::Draw(UIContext &dc) {
float opacity = g_gamepadOpacity;
if (opacity <= 0.0f)
return;
if (dragPointerId_ != -1 && g_Config.iTouchButtonStyle == 2) {
opacity *= 1.35f;
}
uint32_t colorBg = colorAlpha(GetButtonColor(), opacity);
uint32_t downBg = colorAlpha(0x00FFFFFF, opacity * 0.5f);
if (centerX_ < 0.0f) {
centerX_ = bounds_.centerX();
centerY_ = bounds_.centerY();
}
float stickX = centerX_;
float stickY = centerY_;
float dx, dy;
__CtrlPeekAnalog(stick_, &dx, &dy);
rotateTouchHelper(dx, dy);
const TouchControlConfig &config = g_Config.GetTouchControlsConfig(g_display.GetDeviceOrientation());
if (!config.bHideStickBackground)
dc.Draw()->DrawImage(bgImg_, stickX, stickY, 1.0f * scale_, colorBg, ALIGN_CENTER);
float headScale = stick_ ? config.fRightStickHeadScale : config.fLeftStickHeadScale;
if (dragPointerId_ != -1 && g_Config.iTouchButtonStyle == 2 && stickDownImg_ != stickImageIndex_)
dc.Draw()->DrawImage(stickDownImg_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * scale_ * headScale, downBg, ALIGN_CENTER);
dc.Draw()->DrawImage(stickImageIndex_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * scale_ * headScale, colorBg, ALIGN_CENTER);
}
bool PSPStick::Touch(const TouchInput &input) {
// See the comment in MultiTouchButton::Touch().
if (input.id < 0 || input.id >= TOUCH_MAX_POINTERS) {
return false;
}
bool retval = GamepadComponent::Touch(input);
if (input.flags & TouchInputFlags::RELEASE_ALL) {
dragPointerId_ = -1;
centerX_ = bounds_.centerX();
centerY_ = bounds_.centerY();
__CtrlSetAnalogXY(stick_, 0.0f, 0.0f);
usedPointerMask = 0;
analogPointerMask = 0;
return retval;
}
if (input.flags & TouchInputFlags::DOWN) {
const TouchControlConfig &config = g_Config.GetTouchControlsConfig(g_display.GetDeviceOrientation());
float fac = 0.5f * (stick_ ? config.fRightStickHeadScale : config.fLeftStickHeadScale)-0.5f;
if (dragPointerId_ == -1 && bounds_.Expand(bounds_.w*fac, bounds_.h*fac).Contains(input.x, input.y)) {
if (g_Config.bAutoCenterTouchAnalog) {
centerX_ = input.x;
centerY_ = input.y;
} else {
centerX_ = bounds_.centerX();
centerY_ = bounds_.centerY();
}
dragPointerId_ = input.id;
usedPointerMask |= 1 << input.id;
analogPointerMask |= 1 << input.id;
ProcessTouch(input.x, input.y, true);
retval = true;
}
}
if (input.flags & TouchInputFlags::MOVE) {
if (input.id == dragPointerId_) {
ProcessTouch(input.x, input.y, true);
retval = true;
}
}
if (input.flags & TouchInputFlags::UP) {
if (input.id == dragPointerId_) {
dragPointerId_ = -1;
centerX_ = bounds_.centerX();
centerY_ = bounds_.centerY();
usedPointerMask &= ~(1 << input.id);
analogPointerMask &= ~(1 << input.id);
ProcessTouch(input.x, input.y, false);
retval = true;
}
}
return retval;
}
void PSPStick::ProcessTouch(float x, float y, bool down) {
if (down && centerX_ >= 0.0f) {
float inv_stick_size = 1.0f / (stick_size_ * scale_);
float dx = (x - centerX_) * inv_stick_size;
float dy = (y - centerY_) * inv_stick_size;
rotateTouchHelper(dx, dy);
// Do not clamp to a circle! The PSP has nearly square range!
// Old code to clamp to a circle
// float len = sqrtf(dx * dx + dy * dy);
// if (len > 1.0f) {
// dx /= len;
// dy /= len;
//}
// Still need to clamp to a square
dx = std::min(1.0f, std::max(-1.0f, dx));
dy = std::min(1.0f, std::max(-1.0f, dy));
__CtrlSetAnalogXY(stick_, dx, -dy);
} else {
__CtrlSetAnalogXY(stick_, 0.0f, 0.0f);
}
}
PSPCustomStick::PSPCustomStick(ImageID bgImg, const char *key, ImageID stickImg, ImageID stickDownImg, int stick, float scale, UI::LayoutParams *layoutParams)
: PSPStick(bgImg, key, stickImg, stickDownImg, stick, scale, layoutParams) {
}
void PSPCustomStick::Draw(UIContext &dc) {
float opacity = g_gamepadOpacity;
if (opacity <= 0.0f)
return;
if (dragPointerId_ != -1 && g_Config.iTouchButtonStyle == 2) {
opacity *= 1.35f;
}
uint32_t colorBg = colorAlpha(GetButtonColor(), opacity);
uint32_t downBg = colorAlpha(0x00FFFFFF, opacity * 0.5f);
if (centerX_ < 0.0f) {
centerX_ = bounds_.centerX();
centerY_ = bounds_.centerY();
}
float stickX = centerX_;
float stickY = centerY_;
float dx, dy;
dx = posX_;
dy = -posY_;
const TouchControlConfig &config = g_Config.GetTouchControlsConfig(g_display.GetDeviceOrientation());
const float headScale = config.fRightStickHeadScale;
if (!config.bHideStickBackground)
dc.Draw()->DrawImage(bgImg_, stickX, stickY, 1.0f * scale_, colorBg, ALIGN_CENTER);
if (dragPointerId_ != -1 && g_Config.iTouchButtonStyle == 2 && stickDownImg_ != stickImageIndex_)
dc.Draw()->DrawImage(stickDownImg_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * scale_ * headScale, downBg, ALIGN_CENTER);
dc.Draw()->DrawImage(stickImageIndex_, stickX + dx * stick_size_ * scale_, stickY - dy * stick_size_ * scale_, 1.0f * scale_ * headScale, colorBg, ALIGN_CENTER);
}
bool PSPCustomStick::Touch(const TouchInput &input) {
if (input.id < 0 || input.id >= TOUCH_MAX_POINTERS) {
return false;
}
bool retval = GamepadComponent::Touch(input);
if (input.flags & TouchInputFlags::RELEASE_ALL) {
dragPointerId_ = -1;
centerX_ = bounds_.centerX();
centerY_ = bounds_.centerY();
posX_ = 0.0f;
posY_ = 0.0f;
usedPointerMask = 0;
analogPointerMask = 0;
return false;
}
if (input.flags & TouchInputFlags::DOWN) {
const TouchControlConfig &config = g_Config.GetTouchControlsConfig(g_display.GetDeviceOrientation());
float fac = 0.5f * config.fRightStickHeadScale - 0.5f;
if (dragPointerId_ == -1 && bounds_.Expand(bounds_.w*fac, bounds_.h*fac).Contains(input.x, input.y)) {
if (g_Config.bAutoCenterTouchAnalog) {
centerX_ = input.x;
centerY_ = input.y;
} else {
centerX_ = bounds_.centerX();
centerY_ = bounds_.centerY();
}
dragPointerId_ = input.id;
usedPointerMask |= 1 << input.id;
analogPointerMask |= 1 << input.id;
ProcessTouch(input.x, input.y, true);
retval = true;
}
}
if (input.flags & TouchInputFlags::MOVE) {
if (input.id == dragPointerId_) {
ProcessTouch(input.x, input.y, true);
retval = true;
}
}
if (input.flags & TouchInputFlags::UP) {
if (input.id == dragPointerId_) {
dragPointerId_ = -1;
centerX_ = bounds_.centerX();
centerY_ = bounds_.centerY();
usedPointerMask &= ~(1 << input.id);
analogPointerMask &= ~(1 << input.id);
ProcessTouch(input.x, input.y, false);
retval = true;
}
}
return retval;
}
void PSPCustomStick::ProcessTouch(float x, float y, bool down) {
static const int buttons[] = {0, CTRL_LTRIGGER, CTRL_RTRIGGER, CTRL_SQUARE, CTRL_TRIANGLE, CTRL_CIRCLE, CTRL_CROSS, CTRL_UP, CTRL_DOWN, CTRL_LEFT, CTRL_RIGHT, CTRL_START, CTRL_SELECT};
static const int analogs[] = {VIRTKEY_AXIS_RIGHT_Y_MAX, VIRTKEY_AXIS_RIGHT_Y_MIN, VIRTKEY_AXIS_RIGHT_X_MIN, VIRTKEY_AXIS_RIGHT_X_MAX, VIRTKEY_AXIS_Y_MAX, VIRTKEY_AXIS_Y_MIN, VIRTKEY_AXIS_X_MIN, VIRTKEY_AXIS_X_MAX};
u32 press = 0;
u32 release = 0;
auto toggle = [&](int config, bool simpleCheck, bool diagCheck = true) {
if (config <= 0 || (size_t)config >= ARRAY_SIZE(buttons))
return;
if (simpleCheck && (!g_Config.bRightAnalogDisableDiagonal || diagCheck))
press |= buttons[config];
else
release |= buttons[config];
};
auto analog = [&](float dx, float dy) {
if (g_Config.bRightAnalogDisableDiagonal) {
if (fabs(dx) > fabs(dy)) {
dy = 0.0f;
} else {
dx = 0.0f;
}
}
auto assign = [&](int config, float value) {
if (config < ARRAY_SIZE(buttons) || config >= ARRAY_SIZE(buttons) + ARRAY_SIZE(analogs)) {
return;
}
switch(analogs[config - ARRAY_SIZE(buttons)]) {
case VIRTKEY_AXIS_Y_MAX:
__CtrlSetAnalogY(0, value);
break;
case VIRTKEY_AXIS_Y_MIN:
__CtrlSetAnalogY(0, value * -1.0f);
break;
case VIRTKEY_AXIS_X_MIN:
__CtrlSetAnalogX(0, value * -1.0f);
break;
case VIRTKEY_AXIS_X_MAX:
__CtrlSetAnalogX(0, value);
break;
case VIRTKEY_AXIS_RIGHT_Y_MAX:
__CtrlSetAnalogY(1, value);
break;
case VIRTKEY_AXIS_RIGHT_Y_MIN:
__CtrlSetAnalogY(1, value * -1.0f);
break;
case VIRTKEY_AXIS_RIGHT_X_MIN:
__CtrlSetAnalogX(1, value * -1.0f);
break;
case VIRTKEY_AXIS_RIGHT_X_MAX:
__CtrlSetAnalogX(1, value);
break;
}
};
// if/when we ever get iLeftAnalog settings, check stick_ for the config to use
// let 0.0f through during centering
if (dy >= 0.0f) {
// down
assign(g_Config.iRightAnalogUp, 0.0f);
assign(g_Config.iRightAnalogDown, dy);
}
if (dy <= 0.0f) {
// up
assign(g_Config.iRightAnalogDown, 0.0f);
assign(g_Config.iRightAnalogUp, dy * -1.0f);
}
if (dx <= 0.0f) {
// left
assign(g_Config.iRightAnalogRight, 0.0f);
assign(g_Config.iRightAnalogLeft, dx * -1.0f);
}
if (dx >= 0.0f) {
// right
assign(g_Config.iRightAnalogLeft, 0.0f);
assign(g_Config.iRightAnalogRight, dx);
}
};
if (down && centerX_ >= 0.0f) {
float inv_stick_size = 1.0f / (stick_size_ * scale_);
float dx = (x - centerX_) * inv_stick_size;
float dy = (y - centerY_) * inv_stick_size;
dx = std::min(1.0f, std::max(-1.0f, dx));
dy = std::min(1.0f, std::max(-1.0f, dy));
toggle(g_Config.iRightAnalogRight, dx > 0.5f, fabs(dx) > fabs(dy));
toggle(g_Config.iRightAnalogLeft, dx < -0.5f, fabs(dx) > fabs(dy));
toggle(g_Config.iRightAnalogUp, dy < -0.5f, fabs(dx) <= fabs(dy));
toggle(g_Config.iRightAnalogDown, dy > 0.5f, fabs(dx) <= fabs(dy));
toggle(g_Config.iRightAnalogPress, true);
analog(dx, dy);
posX_ = dx;
posY_ = dy;
} else {
toggle(g_Config.iRightAnalogRight, false);
toggle(g_Config.iRightAnalogLeft, false);
toggle(g_Config.iRightAnalogUp, false);
toggle(g_Config.iRightAnalogDown, false);
toggle(g_Config.iRightAnalogPress, false);
analog(0.0f, 0.0f);
posX_ = 0.0f;
posY_ = 0.0f;
}
if (release || press) {
__CtrlUpdateButtons(press, release);
}
}
void InitPadLayout(TouchControlConfig *config, DeviceOrientation orientation, float xres, float yres, float globalScale) {
const float scale = globalScale;
const int halfW = xres / 2;
const bool portrait = orientation == DeviceOrientation::Portrait;
const float screenBottom = portrait ? (yres - yres * 0.13f) : yres;
auto initTouchPos = [=](ConfigTouchPos *touch, float x, float y, float extraScale = 1.0f) {
if (touch->x == -1.0f || touch->y == -1.0f) {
touch->x = x / xres;
touch->y = std::max(y, 20.0f * globalScale) / yres;
touch->scale = scale * extraScale;
}
};
// Pause button. Has some special handling, it MUST be visible on some platforms.
float Pause_button_center_X = halfW;
float Pause_button_center_Y = 28.0f;
if (!System_GetPropertyBool(SYSPROP_HAS_BACK_BUTTON)) {
// Make really sure the pause button will be visible. Setting to -1 ensures reinit.
if (config->touchPauseKey.x < 0.0f || config->touchPauseKey.x > 1.0f) {
config->touchPauseKey.x = -1;
}
if (config->touchPauseKey.y < 0.0f || config->touchPauseKey.y > 1.0f) {
config->touchPauseKey.y = -1;
}
config->touchPauseKey.show = true;
}
initTouchPos(&config->touchPauseKey, Pause_button_center_X, Pause_button_center_Y, 0.8f);
// PSP buttons (triangle, circle, square, cross)---------------------
// space between the PSP buttons (triangle, circle, square and cross)
if (config->fActionButtonSpacing < 0) {
config->fActionButtonSpacing = 1.0f;
}
// Position of the circle button (the PSP circle button). It is the farthest to the left
float Action_button_spacing = config->fActionButtonSpacing * baseActionButtonSpacing;
int Action_button_center_X = xres - Action_button_spacing * 2;
int Action_button_center_Y = screenBottom - Action_button_spacing * 2;
if (config->touchRightAnalogStick.show) {
Action_button_center_Y -= 150 * scale;
} else if (portrait) {
Action_button_center_Y -= 120 * scale;
}
initTouchPos(&config->touchActionButtonCenter, Action_button_center_X, Action_button_center_Y);
//D-PAD (up down left right) (aka PSP cross)----------------------------
//radius to the D-pad
// TODO: Make configurable
int D_pad_X = 2.5 * D_pad_Radius * scale;
int D_pad_Y = screenBottom - D_pad_Radius * scale;
if (config->touchAnalogStick.show) {
D_pad_Y -= 200 * scale;
}
initTouchPos(&config->touchDpad, D_pad_X, D_pad_Y);
//analog stick-------------------------------------------------------
//keep the analog stick right below the D pad
int analog_stick_X = D_pad_X;
int analog_stick_Y = screenBottom - 80 * scale;
initTouchPos(&config->touchAnalogStick, analog_stick_X, analog_stick_Y);
//right analog stick-------------------------------------------------
//keep the right analog stick right below the face buttons
int right_analog_stick_X = Action_button_center_X;
int right_analog_stick_Y = screenBottom - 80 * scale;
initTouchPos(&config->touchRightAnalogStick, right_analog_stick_X, right_analog_stick_Y);
//select, start, throttle--------------------------------------------
//space between the bottom keys (space between select, start and un-throttle)
float bottom_key_spacing = 100;
if (g_display.dp_xres < 750) {
bottom_key_spacing *= 0.8f;
}
// On IOS, nudge the bottom button up a little to avoid the task switcher.
// Additionally, in portrait mode on any platform, move the defaults up by quite a bit - much
// more comfortable to reach.
#if PPSSPP_PLATFORM(IOS)
constexpr float bottom_button_Y = 80.0f;
#else
constexpr float bottom_button_Y = 60.0f;
#endif
if (orientation == DeviceOrientation::Portrait) {
int start_key_X = halfW;
int start_key_Y = screenBottom - bottom_button_Y * scale;
initTouchPos(&config->touchStartKey, start_key_X, start_key_Y);
int select_key_X = halfW;
int select_key_Y = screenBottom - bottom_button_Y * scale - 50;
initTouchPos(&config->touchSelectKey, select_key_X, select_key_Y);
int fast_forward_key_X = halfW;
int fast_forward_key_Y = screenBottom - bottom_button_Y * scale - 100;
initTouchPos(&config->touchFastForwardKey, fast_forward_key_X, fast_forward_key_Y);
int home_key_X = halfW;
int home_key_Y = screenBottom - bottom_button_Y * scale - 150;
initTouchPos(&config->touchHomeKey, home_key_X, home_key_Y);
} else {
int start_key_X = halfW + bottom_key_spacing * scale;
int start_key_Y = screenBottom - bottom_button_Y * scale;
initTouchPos(&config->touchStartKey, start_key_X, start_key_Y);
int select_key_X = halfW;
int select_key_Y = screenBottom - bottom_button_Y * scale;
initTouchPos(&config->touchSelectKey, select_key_X, select_key_Y);
// Above select.
int home_key_X = halfW;
int home_key_Y = select_key_Y - 50;
initTouchPos(&config->touchHomeKey, home_key_X, home_key_Y);
int fast_forward_key_X = halfW - bottom_key_spacing * scale;
int fast_forward_key_Y = screenBottom - bottom_button_Y * scale;
initTouchPos(&config->touchFastForwardKey, fast_forward_key_X, fast_forward_key_Y);
}
// L and R------------------------------------------------------------
// Put them above the analog stick / above the buttons to the right.
// The corners were very hard to reach..
int l_key_X = 60 * scale;
int l_key_Y = screenBottom - 380 * scale;
initTouchPos(&config->touchLKey, l_key_X, l_key_Y);
int r_key_X = xres - 60 * scale;
int r_key_Y = l_key_Y;
initTouchPos(&config->touchRKey, r_key_X, r_key_Y);
struct { float x; float y; } customButtonPositions[10] = {
{ 1.2f, 0.5f },
{ 2.2f, 0.5f },
{ 3.2f, 0.5f },
{ 1.2f, 0.333f },
{ 2.2f, 0.333f },
{ -1.2f, 0.5f },
{ -2.2f, 0.5f },
{ -3.2f, 0.5f },
{ -1.2f, 0.333f },
{ -2.2f, 0.333f },
};
for (int i = 0; i < TouchControlConfig::CUSTOM_BUTTON_COUNT; i++) {
const float y_offset = (float)(i / 10) * 0.08333f;
int combo_key_X = halfW + bottom_key_spacing * scale * customButtonPositions[i % 10].x;
int combo_key_Y = screenBottom * (y_offset + customButtonPositions[i % 10].y);
initTouchPos(&config->touchCustom[i], combo_key_X, combo_key_Y);
}
}
ViewLayoutMode TouchControlsLayoutMode() {
// The only control the defaults put in the top third is the centered menu button.
const float sideInsetsBottom = System_GetPropertyFloat(SYSPROP_DISPLAY_SIDE_INSETS_BOTTOM);
if (sideInsetsBottom >= 0.0f && sideInsetsBottom < g_display.dp_yres / 3.0f) {
return ViewLayoutMode::IgnoreSideInsets;
}
return ViewLayoutMode::ApplyInsets;
}
UI::ViewGroup *CreatePadLayout(const TouchControlConfig &config, float xres, float yres, bool *pause, ControlMapper *controlMapper) {
using namespace UI;
return new GamepadEmuView(config, xres, yres, pause, controlMapper, new UI::LayoutParams(UI::FILL_PARENT, UI::FILL_PARENT));
}
GamepadEmuView::GamepadEmuView(const TouchControlConfig &config, float xres, float yres, bool *pause, ControlMapper *controlMapper, UI::LayoutParams *layoutParams)
: UI::AnchorLayout(layoutParams)
{
if (!g_Config.bShowTouchControls) {
return;
}
using namespace UI;
struct ButtonOffset {
float x;
float y;
};
auto buttonLayoutParams = [=](const ConfigTouchPos &touch, ButtonOffset off = { 0, 0 }) {
return new AnchorLayoutParams(touch.x * xres + off.x, touch.y * yres + off.y, NONE, NONE, Centering::Both);
};
// Space between the PSP buttons (triangle, circle, square and cross)
const float actionButtonSpacing = config.fActionButtonSpacing * baseActionButtonSpacing;
// Position of the circle button (the PSP circle button). It is the farthest to the right.
ButtonOffset circleOffset{ actionButtonSpacing, 0.0f };
ButtonOffset crossOffset{ 0.0f, actionButtonSpacing };
ButtonOffset triangleOffset{ 0.0f, -actionButtonSpacing };
ButtonOffset squareOffset{ -actionButtonSpacing, 0.0f };
const int halfW = xres / 2;
const ImageID roundImage = g_Config.iTouchButtonStyle ? ImageID("I_ROUND_LINE") : ImageID("I_ROUND");
const ImageID rectImage = g_Config.iTouchButtonStyle ? ImageID("I_RECT_LINE") : ImageID("I_RECT");
const ImageID shoulderImage = g_Config.iTouchButtonStyle ? ImageID("I_SHOULDER_LINE") : ImageID("I_SHOULDER");
const ImageID stickImage = g_Config.iTouchButtonStyle ? ImageID("I_STICK_LINE") : ImageID("I_STICK");
const ImageID stickBg = g_Config.iTouchButtonStyle ? ImageID("I_STICK_BG_LINE") : ImageID("I_STICK_BG");
auto addPSPButton = [this, buttonLayoutParams](int buttonBit, const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, const ConfigTouchPos &touch, ButtonOffset off = { 0, 0 }) -> PSPButton * {
if (touch.show) {
return Add(new PSPButton(buttonBit, key, bgImg, bgDownImg, img, touch.scale, buttonLayoutParams(touch, off)));
}
return nullptr;
};
auto addBoolButton = [this, buttonLayoutParams](bool *value, const char *key, ImageID bgImg, ImageID bgDownImg, ImageID img, const ConfigTouchPos &touch) -> BoolButton * {
if (touch.show) {
return Add(new BoolButton(value, key, bgImg, bgDownImg, img, touch.scale, buttonLayoutParams(touch)));
}
return nullptr;
};
auto addCustomButton = [this, buttonLayoutParams, controlMapper](const ConfigCustomButton& cfg, const char *key, const ConfigTouchPos &touch) -> CustomButton * {
using namespace CustomKeyData;
if (touch.show) {
_dbg_assert_(cfg.shape < ARRAY_SIZE(customKeyShapes));
_dbg_assert_(cfg.image < ARRAY_SIZE(customKeyImages));
// Note: cfg.shape and cfg.image are bounds-checked elsewhere.
auto aux = Add(new CustomButton(cfg.key, key, cfg.toggle, cfg.repeat, controlMapper,
g_Config.iTouchButtonStyle == 0 ? customKeyShapes[cfg.shape].i : customKeyShapes[cfg.shape].l, customKeyShapes[cfg.shape].i,
customKeyImages[cfg.image].i, touch.scale, customKeyShapes[cfg.shape].d, buttonLayoutParams(touch)));
aux->SetAngle(customKeyImages[cfg.image].r, customKeyShapes[cfg.shape].r);
aux->FlipImageH(customKeyShapes[cfg.shape].f);
return aux;
}
return nullptr;
};
if (config.touchPauseKey.show) {
auto button = addBoolButton(pause, "Pause button", roundImage, ImageID("I_ROUND"), ImageID("I_HAMBURGER"), config.touchPauseKey);
if (button) {
// The user is not allowed to hide this completely on some platforms - it must be findable.
button->SetMinimumAlpha(0.1f);
}
}
// touchActionButtonCenter.show will always be true, since that's the default.
if (config.bShowTouchCircle)
addPSPButton(CTRL_CIRCLE, "Circle button", roundImage, ImageID("I_ROUND"), ImageID("I_CIRCLE"), config.touchActionButtonCenter, circleOffset);
if (config.bShowTouchCross)
addPSPButton(CTRL_CROSS, "Cross button", roundImage, ImageID("I_ROUND"), ImageID("I_CROSS"), config.touchActionButtonCenter, crossOffset);
if (config.bShowTouchTriangle)
addPSPButton(CTRL_TRIANGLE, "Triangle button", roundImage, ImageID("I_ROUND"), ImageID("I_TRIANGLE"), config.touchActionButtonCenter, triangleOffset);
if (config.bShowTouchSquare)
addPSPButton(CTRL_SQUARE, "Square button", roundImage, ImageID("I_ROUND"), ImageID("I_SQUARE"), config.touchActionButtonCenter, squareOffset);
addPSPButton(CTRL_START, "Start button", rectImage, ImageID("I_RECT"), ImageID("I_START"), config.touchStartKey);
addPSPButton(CTRL_SELECT, "Select button", rectImage, ImageID("I_RECT"), ImageID("I_SELECT"), config.touchSelectKey);
addPSPButton(CTRL_HOME, "Home button", rectImage, ImageID("I_RECT"), ImageID("I_HOME"), config.touchHomeKey);
BoolButton *fastForward = addBoolButton(&PSP_CoreParameter().fastForward, "Fast-forward button", rectImage, ImageID("I_RECT"), ImageID("I_FAST_FORWARD_LINE"), config.touchFastForwardKey);
if (fastForward) {
fastForward->OnChange.Add([](UI::EventParams &e) {
if (e.a && coreState == CORE_STEPPING_CPU) {
Core_Resume();
}
});
}
addPSPButton(CTRL_LTRIGGER, "Left shoulder button", shoulderImage, ImageID("I_SHOULDER"), ImageID("I_L"), config.touchLKey);
PSPButton *rTrigger = addPSPButton(CTRL_RTRIGGER, "Right shoulder button", shoulderImage, ImageID("I_SHOULDER"), ImageID("I_R"), config.touchRKey);
if (rTrigger)
rTrigger->FlipImageH(true);
if (config.touchDpad.show) {
const ImageID dirImage = g_Config.iTouchButtonStyle ? ImageID("I_DIR_LINE") : ImageID("I_DIR");
Add(new PSPDpad(dirImage, "D-pad", ImageID("I_DIR"), ImageID("I_ARROW"), config.touchDpad.scale, config.fDpadSpacing, buttonLayoutParams(config.touchDpad)));
}
if (config.touchAnalogStick.show)
Add(new PSPStick(stickBg, "Left analog stick", stickImage, ImageID("I_STICK"), 0, config.touchAnalogStick.scale, buttonLayoutParams(config.touchAnalogStick)));
if (config.touchRightAnalogStick.show) {
if (g_Config.bRightAnalogCustom)
Add(new PSPCustomStick(stickBg, "Right analog stick", stickImage, ImageID("I_STICK"), 1, config.touchRightAnalogStick.scale, buttonLayoutParams(config.touchRightAnalogStick)));
else
Add(new PSPStick(stickBg, "Right analog stick", stickImage, ImageID("I_STICK"), 1, config.touchRightAnalogStick.scale, buttonLayoutParams(config.touchRightAnalogStick)));
}
// Sanitize custom button images, while adding them.
for (int i = 0; i < TouchControlConfig::CUSTOM_BUTTON_COUNT; i++) {
CustomKeyData::Sanitize(g_Config.CustomButton[i]);
char temp[64];
snprintf(temp, sizeof(temp), "Custom %d button", i + 1);
addCustomButton(g_Config.CustomButton[i], temp, config.touchCustom[i]);
}
// Add the two gesture zones.
for (int i = 0; i < 2; i++) {
if (g_Config.gestureControls[i].bGestureControlEnabled || g_Config.gestureControls[i].bAnalogGesture) {
// We have them both cover the whole surface, then limit in the touch handler.
// This is because there's no easy way to do "half the screen" in AnchorLayout.
// We can do more complex layout combinations, but meh.
Add(new GestureGamepad(controlMapper, i, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT, 0.0f, 0.0f, 0.0f, 0.0f)));
}
}
}
void GamepadEmuView::Update() {
AnchorLayout::Update();
GamepadUpdateOpacity();
bool anyDown = false;
for (auto view : views_) {
GamepadComponent *component = dynamic_cast<GamepadComponent *>(view);
if (component && component->IsDownByTouch()) {
// INFO_LOG(Log::System, "GamepadEmuView::Update: component is down by touch: %s", component->DescribeText().c_str());
anyDown = true;
}
}
if (anyDown) {
// INFO_LOG(Log::System, "last touch in update");
g_lastTouch = time_now_d();
}
}
const GestureControlConfig &GestureGamepad::GetZone() {
// TODO: Add support for multiple zones.
return g_Config.gestureControls[zoneIndex_];
}
GestureGamepad::~GestureGamepad() {
if (dragPointerId_ != -1) {
g_activeGesturePointers.erase(dragPointerId_);
}
}
// The gesture config ints (iDoubleTapGesture/iSwipeUp/Down/Left/Right) are
// PER_GAME config values loaded from an ini with no range clamp applied, unlike
// the UI (a PopupMultiChoice) that normally sets them - a hand-edited, corrupted,
// or version-skewed config could contain an out-of-range value, which would
// otherwise index GestureKey::keyList[] out of bounds below. Mirrors the bounds
// check PSPCustomStick::ProcessTouch already does for its own config->buttons[]
// lookup.
static bool ValidGestureKeyConfig(int config) {
return config > 0 && (size_t)config <= ARRAY_SIZE(GestureKey::keyList);
}
bool GestureGamepad::Touch(const TouchInput &input) {
// See the comment in MultiTouchButton::Touch().
if (input.id < 0 || input.id >= TOUCH_MAX_POINTERS) {
return false;
}
const GestureControlConfig &zone = GetZone();
if (usedPointerMask & (1 << input.id)) {
if (input.id == dragPointerId_)
dragPointerId_ = -1;
return false;
}
if (input.flags & TouchInputFlags::RELEASE_ALL) {
dragPointerId_ = -1;
g_activeGesturePointers.clear();
return false;
}
if (input.flags & TouchInputFlags::DOWN) {
// Handle the zones here, easier than with AnchorLayout.
const float minX = zoneIndex_ == 0 ? 0.0f : bounds_.w * 0.5f;
const float maxX = zoneIndex_ == 0 ? bounds_.w * 0.5f : bounds_.w;
if (dragPointerId_ == -1 && input.x >= minX && input.x < maxX) {
dragPointerId_ = input.id;
lastX_ = input.x;
lastY_ = input.y;
downX_ = input.x;
downY_ = input.y;
const float now = time_now_d();
if (now - lastTapRelease_ < 0.3f && !haveDoubleTapped_) {
if (ValidGestureKeyConfig(zone.iDoubleTapGesture))
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[zone.iDoubleTapGesture - 1], KeyInputFlags::DOWN);
haveDoubleTapped_ = true;
}
lastTouchDown_ = now;
}
}
if (input.flags & TouchInputFlags::MOVE) {
if (input.id == dragPointerId_) {
deltaX_ += input.x - lastX_;
deltaY_ += input.y - lastY_;
lastX_ = input.x;
lastY_ = input.y;
const float distance = sqrtf((input.x - downX_) * (input.x - downX_) + (input.y - downY_) * (input.y - downY_));
// TODO: Configurable distance?
if (distance > 50.0f * g_display.dpi_scale_x) {
// The user has dragged some distance from the initial touch. Start ignoring button presses.
g_activeGesturePointers.insert(input.id);
} else {
g_activeGesturePointers.erase(input.id);
}
if (zone.bAnalogGesture) {
const float k = zone.fAnalogGestureSensitivity * 0.02;
float dx = (input.x - downX_) * g_display.dpi_scale_x * k;
float dy = (input.y - downY_) * g_display.dpi_scale_y * k;
dx = std::clamp(dx, -1.0f, 1.0f);
dy = std::clamp(dy, -1.0f, 1.0f);
__CtrlSetAnalogXY(0, dx, -dy);
}
}
}
if (input.flags & TouchInputFlags::UP) {
g_activeGesturePointers.erase(input.id);
if (input.id == dragPointerId_) {
dragPointerId_ = -1;
if (time_now_d() - lastTouchDown_ < 0.3f)
lastTapRelease_ = time_now_d();
if (haveDoubleTapped_) {
if (ValidGestureKeyConfig(zone.iDoubleTapGesture))
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[zone.iDoubleTapGesture - 1], KeyInputFlags::UP);
haveDoubleTapped_ = false;
}
if (zone.bAnalogGesture)
__CtrlSetAnalogXY(0, 0, 0);
}
}
return true;
}
void GestureGamepad::Draw(UIContext &dc) {
float opacity = g_Config.iTouchButtonOpacity / 100.0;
if (opacity <= 0.0f)
return;
uint32_t colorBg = colorAlpha(GetButtonColor(), opacity);
if (GetZone().bAnalogGesture && dragPointerId_ != -1) {
dc.Draw()->DrawImage(ImageID("I_CIRCLE"), downX_, downY_, 0.7f, colorBg, ALIGN_CENTER);
}
}
void GestureGamepad::Update() {
const float th = 1.0f;
float dx = deltaX_ * g_display.dpi_scale_x * GetZone().fSwipeSensitivity;
float dy = deltaY_ * g_display.dpi_scale_y * GetZone().fSwipeSensitivity;
const float smoothing = GetZone().fSwipeSmoothing;
deltaX_ *= smoothing;
deltaY_ *= smoothing;
if (!GetZone().bGestureControlEnabled) {
return;
}
if (ValidGestureKeyConfig(GetZone().iSwipeRight)) {
if (dx > th) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[GetZone().iSwipeRight - 1], KeyInputFlags::DOWN);
swipeRightReleased_ = false;
} else if (!swipeRightReleased_) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[GetZone().iSwipeRight -1], KeyInputFlags::UP);
swipeRightReleased_ = true;
}
}
if (ValidGestureKeyConfig(GetZone().iSwipeLeft)) {
if (dx < -th) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[GetZone().iSwipeLeft - 1], KeyInputFlags::DOWN);
swipeLeftReleased_ = false;
} else if (!swipeLeftReleased_) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[GetZone().iSwipeLeft - 1], KeyInputFlags::UP);
swipeLeftReleased_ = true;
}
}
if (ValidGestureKeyConfig(GetZone().iSwipeUp)) {
if (dy < -th) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[GetZone().iSwipeUp - 1], KeyInputFlags::DOWN);
swipeUpReleased_ = false;
} else if (!swipeUpReleased_) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[GetZone().iSwipeUp - 1], KeyInputFlags::UP);
swipeUpReleased_ = true;
}
}
if (ValidGestureKeyConfig(GetZone().iSwipeDown)) {
if (dy > th) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[GetZone().iSwipeDown - 1], KeyInputFlags::DOWN);
swipeDownReleased_ = false;
} else if (!swipeDownReleased_) {
controlMapper_->PSPKey(DEVICE_ID_TOUCH, GestureKey::keyList[GetZone().iSwipeDown - 1], KeyInputFlags::UP);
swipeDownReleased_ = true;
}
}
}