Merge pull request #8621 from Robyt3/Client-Engine-UI-Touch-Input

Support touch input in engine, UI and console
This commit is contained in:
Dennis Felsing 2024-07-19 22:03:26 +00:00 committed by GitHub
commit cb9521d29f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 326 additions and 43 deletions

View file

@ -4558,6 +4558,10 @@ int main(int argc, const char **argv)
pClient->ShellRegister();
#endif
// Do not automatically translate touch events to mouse events and vice versa.
SDL_SetHint("SDL_TOUCH_MOUSE_EVENTS", "0");
SDL_SetHint("SDL_MOUSE_TOUCH_EVENTS", "0");
#if defined(CONF_PLATFORM_MACOS)
// Hints will not be set if there is an existing override hint or environment variable that takes precedence.
// So this respects cli environment overrides.

View file

@ -263,14 +263,7 @@ bool CInput::MouseRelative(float *pX, float *pY)
return false;
ivec2 Relative;
#if defined(CONF_PLATFORM_ANDROID) // No relative mouse on Android
ivec2 CurrentPos;
SDL_GetMouseState(&CurrentPos.x, &CurrentPos.y);
Relative = CurrentPos - m_LastMousePos;
m_LastMousePos = CurrentPos;
#else
SDL_GetRelativeMouseState(&Relative.x, &Relative.y);
#endif
*pX = Relative.x;
*pY = Relative.y;
@ -287,25 +280,30 @@ void CInput::MouseModeAbsolute()
void CInput::MouseModeRelative()
{
m_InputGrabbed = true;
#if !defined(CONF_PLATFORM_ANDROID) // No relative mouse on Android
SDL_SetRelativeMouseMode(SDL_TRUE);
#endif
Graphics()->SetWindowGrab(true);
// Clear pending relative mouse motion
SDL_GetRelativeMouseState(nullptr, nullptr);
}
void CInput::NativeMousePos(int *pX, int *pY) const
vec2 CInput::NativeMousePos() const
{
SDL_GetMouseState(pX, pY);
ivec2 Position;
SDL_GetMouseState(&Position.x, &Position.y);
return vec2(Position.x, Position.y);
}
bool CInput::NativeMousePressed(int Index)
bool CInput::NativeMousePressed(int Index) const
{
int i = SDL_GetMouseState(nullptr, nullptr);
return (i & SDL_BUTTON(Index)) != 0;
}
const std::vector<IInput::CTouchFingerState> &CInput::TouchFingerStates() const
{
return m_vTouchFingerStates;
}
const char *CInput::GetClipboardText()
{
SDL_free(m_pClipboardText);
@ -353,6 +351,10 @@ void CInput::Clear()
mem_zero(m_aInputState, sizeof(m_aInputState));
mem_zero(m_aInputCount, sizeof(m_aInputCount));
m_vInputEvents.clear();
for(CTouchFingerState &TouchFingerState : m_vTouchFingerStates)
{
TouchFingerState.m_Delta = vec2(0.0f, 0.0f);
}
}
float CInput::GetUpdateTime() const
@ -539,6 +541,39 @@ void CInput::HandleJoystickRemovedEvent(const SDL_JoyDeviceEvent &Event)
}
}
void CInput::HandleTouchDownEvent(const SDL_TouchFingerEvent &Event)
{
CTouchFingerState TouchFingerState;
TouchFingerState.m_Finger.m_DeviceId = Event.touchId;
TouchFingerState.m_Finger.m_FingerId = Event.fingerId;
TouchFingerState.m_Position = vec2(Event.x, Event.y);
TouchFingerState.m_Delta = vec2(Event.dx, Event.dy);
m_vTouchFingerStates.emplace_back(TouchFingerState);
}
void CInput::HandleTouchUpEvent(const SDL_TouchFingerEvent &Event)
{
auto FoundState = std::find_if(m_vTouchFingerStates.begin(), m_vTouchFingerStates.end(), [Event](const CTouchFingerState &State) {
return State.m_Finger.m_DeviceId == Event.touchId && State.m_Finger.m_FingerId == Event.fingerId;
});
if(FoundState != m_vTouchFingerStates.end())
{
m_vTouchFingerStates.erase(FoundState);
}
}
void CInput::HandleTouchMotionEvent(const SDL_TouchFingerEvent &Event)
{
auto FoundState = std::find_if(m_vTouchFingerStates.begin(), m_vTouchFingerStates.end(), [Event](const CTouchFingerState &State) {
return State.m_Finger.m_DeviceId == Event.touchId && State.m_Finger.m_FingerId == Event.fingerId;
});
if(FoundState != m_vTouchFingerStates.end())
{
FoundState->m_Position = vec2(Event.x, Event.y);
FoundState->m_Delta += vec2(Event.dx, Event.dy);
}
}
void CInput::SetCompositionWindowPosition(float X, float Y, float H)
{
SDL_Rect Rect;
@ -745,6 +780,18 @@ int CInput::Update()
Action |= IInput::FLAG_RELEASE;
break;
case SDL_FINGERDOWN:
HandleTouchDownEvent(Event.tfinger);
break;
case SDL_FINGERUP:
HandleTouchUpEvent(Event.tfinger);
break;
case SDL_FINGERMOTION:
HandleTouchMotionEvent(Event.tfinger);
break;
case SDL_WINDOWEVENT:
// Ignore keys following a focus gain as they may be part of global
// shortcuts

View file

@ -60,8 +60,8 @@ private:
IEngineGraphics *m_pGraphics;
IConsole *m_pConsole;
IEngineGraphics *Graphics() { return m_pGraphics; }
IConsole *Console() { return m_pConsole; }
IEngineGraphics *Graphics() const { return m_pGraphics; }
IConsole *Console() const { return m_pConsole; }
// joystick
std::vector<CJoystick> m_vJoysticks;
@ -78,7 +78,6 @@ private:
bool m_MouseFocus;
#if defined(CONF_PLATFORM_ANDROID)
ivec2 m_LastMousePos = ivec2(0, 0); // No relative mouse on Android
int m_NumBackPresses = 0;
bool m_BackButtonReleased = true;
int64_t m_LastBackPress = -1;
@ -102,6 +101,7 @@ private:
uint32_t m_aInputCount[g_MaxKeys];
unsigned char m_aInputState[g_MaxKeys];
uint32_t m_InputCounter;
std::vector<CTouchFingerState> m_vTouchFingerStates;
void UpdateMouseState();
void UpdateJoystickState();
@ -110,6 +110,9 @@ private:
void HandleJoystickHatMotionEvent(const SDL_JoyHatEvent &Event);
void HandleJoystickAddedEvent(const SDL_JoyDeviceEvent &Event);
void HandleJoystickRemovedEvent(const SDL_JoyDeviceEvent &Event);
void HandleTouchDownEvent(const SDL_TouchFingerEvent &Event);
void HandleTouchUpEvent(const SDL_TouchFingerEvent &Event);
void HandleTouchMotionEvent(const SDL_TouchFingerEvent &Event);
char m_aDropFile[IO_MAX_PATH_LENGTH];
@ -142,8 +145,10 @@ public:
bool MouseRelative(float *pX, float *pY) override;
void MouseModeAbsolute() override;
void MouseModeRelative() override;
void NativeMousePos(int *pX, int *pY) const override;
bool NativeMousePressed(int Index) override;
vec2 NativeMousePos() const override;
bool NativeMousePressed(int Index) const override;
const std::vector<CTouchFingerState> &TouchFingerStates() const override;
const char *GetClipboardText() override;
void SetClipboardText(const char *pText) override;

View file

@ -6,9 +6,11 @@
#include "kernel.h"
#include <base/types.h>
#include <base/vmath.h>
#include <cstdint>
#include <functional>
#include <vector>
const int g_MaxKeys = 512;
extern const char g_aaKeyStrings[g_MaxKeys][20];
@ -89,12 +91,62 @@ public:
virtual void SetActiveJoystick(size_t Index) = 0;
// mouse
virtual void NativeMousePos(int *pX, int *pY) const = 0;
virtual bool NativeMousePressed(int Index) = 0;
virtual vec2 NativeMousePos() const = 0;
virtual bool NativeMousePressed(int Index) const = 0;
virtual void MouseModeRelative() = 0;
virtual void MouseModeAbsolute() = 0;
virtual bool MouseRelative(float *pX, float *pY) = 0;
// touch
/**
* Represents a unique finger for a current touch event. If there are multiple touch input devices, they
* are handled transparently like different fingers. The concrete values of the member variables of this
* class are arbitrary based on the touch device driver and should only be used to uniquely identify touch
* fingers. Note that once a finger has been released, the same finger value may also be reused again.
*/
class CTouchFinger
{
friend class CInput;
int64_t m_DeviceId;
int64_t m_FingerId;
public:
bool operator==(const CTouchFinger &Other) const { return m_DeviceId == Other.m_DeviceId && m_FingerId == Other.m_FingerId; }
bool operator!=(const CTouchFinger &Other) const { return !(*this == Other); }
};
/**
* Represents the state of a particular touch finger currently being pressed down on a touch device.
*/
class CTouchFingerState
{
public:
/**
* The unique finger which this state is associated with.
*/
CTouchFinger m_Finger;
/**
* The current position of the finger. The x- and y-components of the position are normalized to the
* range `0.0f`-`1.0f` representing the absolute position of the finger on the current touch device.
*/
vec2 m_Position;
/**
* The current delta of the finger. The x- and y-components of the delta are normalized to the
* range `0.0f`-`1.0f` representing the absolute delta of the finger on the current touch device.
*
* @remark This is reset to zero at the end of each frame.
*/
vec2 m_Delta;
};
/**
* Returns a vector of the states of all touch fingers currently being pressed down on touch devices.
* Note that this only contains fingers which are pressed down, i.e. released fingers are never stored.
* The order of the fingers in this vector is based on the order in which the fingers where pressed.
*
* @return vector of all touch finger states
*/
virtual const std::vector<CTouchFingerState> &TouchFingerStates() const = 0;
// clipboard
virtual const char *GetClipboardText() = 0;
virtual void SetClipboardText(const char *pText) = 0;

View file

@ -1072,24 +1072,48 @@ void CGameConsole::OnRender()
TextRender()->TextEx(&Cursor, aPrompt);
// check if mouse is pressed
if(!pConsole->m_MouseIsPress && Input()->NativeMousePressed(1))
const vec2 WindowSize = vec2(Graphics()->WindowWidth(), Graphics()->WindowHeight());
const vec2 ScreenSize = vec2(Screen.w, Screen.h);
Ui()->UpdateTouchState(m_TouchState);
const auto &&GetMousePosition = [&]() -> vec2 {
if(m_TouchState.m_PrimaryPressed)
{
return m_TouchState.m_PrimaryPosition * ScreenSize;
}
else
{
return Input()->NativeMousePos() / WindowSize * ScreenSize;
}
};
if(!pConsole->m_MouseIsPress && (m_TouchState.m_PrimaryPressed || Input()->NativeMousePressed(1)))
{
pConsole->m_MouseIsPress = true;
ivec2 MousePress;
Input()->NativeMousePos(&MousePress.x, &MousePress.y);
pConsole->m_MousePress.x = (MousePress.x / (float)Graphics()->WindowWidth()) * Screen.w;
pConsole->m_MousePress.y = (MousePress.y / (float)Graphics()->WindowHeight()) * Screen.h;
pConsole->m_MousePress = GetMousePosition();
}
if(pConsole->m_MouseIsPress && !m_TouchState.m_PrimaryPressed && !Input()->NativeMousePressed(1))
{
pConsole->m_MouseIsPress = false;
}
if(pConsole->m_MouseIsPress)
{
ivec2 MouseRelease;
Input()->NativeMousePos(&MouseRelease.x, &MouseRelease.y);
pConsole->m_MouseRelease.x = (MouseRelease.x / (float)Graphics()->WindowWidth()) * Screen.w;
pConsole->m_MouseRelease.y = (MouseRelease.y / (float)Graphics()->WindowHeight()) * Screen.h;
pConsole->m_MouseRelease = GetMousePosition();
}
if(pConsole->m_MouseIsPress && !Input()->NativeMousePressed(1))
const float ScaledRowHeight = RowHeight / ScreenSize.y;
if(absolute(m_TouchState.m_ScrollAmount.y) >= ScaledRowHeight)
{
pConsole->m_MouseIsPress = false;
if(m_TouchState.m_ScrollAmount.y > 0.0f)
{
pConsole->m_BacklogCurLine += pConsole->GetLinesToScroll(-1, 1);
m_TouchState.m_ScrollAmount.y -= ScaledRowHeight;
}
else
{
--pConsole->m_BacklogCurLine;
if(pConsole->m_BacklogCurLine < 0)
pConsole->m_BacklogCurLine = 0;
m_TouchState.m_ScrollAmount.y += ScaledRowHeight;
}
pConsole->m_HasSelection = false;
}
x = Cursor.m_X;

View file

@ -10,6 +10,7 @@
#include <game/client/component.h>
#include <game/client/lineinput.h>
#include <game/client/ui.h>
enum
{
@ -153,6 +154,7 @@ class CGameConsole : public CComponent
float m_StateChangeDuration;
bool m_WantsSelectionCopy = false;
CUi::CTouchState m_TouchState;
static const ColorRGBA ms_SearchHighlightColor;
static const ColorRGBA ms_SearchSelectedColor;

View file

@ -176,24 +176,63 @@ void CUi::OnCursorMove(float X, float Y)
void CUi::Update(vec2 MouseWorldPos)
{
unsigned MouseButtons = 0;
const vec2 WindowSize = vec2(Graphics()->WindowWidth(), Graphics()->WindowHeight());
const CUIRect *pScreen = Screen();
unsigned UpdatedMouseButtonsNext = 0;
if(Enabled())
{
if(Input()->KeyIsPressed(KEY_MOUSE_1))
MouseButtons |= 1;
if(Input()->KeyIsPressed(KEY_MOUSE_2))
MouseButtons |= 2;
if(Input()->KeyIsPressed(KEY_MOUSE_3))
MouseButtons |= 4;
// Update mouse buttons based on mouse keys
for(int MouseKey = KEY_MOUSE_1; MouseKey <= KEY_MOUSE_3; ++MouseKey)
{
if(Input()->KeyIsPressed(MouseKey))
{
m_UpdatedMouseButtons |= 1 << (MouseKey - KEY_MOUSE_1);
}
}
const CUIRect *pScreen = Screen();
m_MousePos = m_UpdatedMousePos * vec2(pScreen->w / Graphics()->WindowWidth(), pScreen->h / Graphics()->WindowHeight());
// Update mouse position and buttons based on touch finger state
UpdateTouchState(m_TouchState);
if(m_TouchState.m_AnyPressed)
{
if(!CheckMouseLock())
{
m_UpdatedMousePos = m_TouchState.m_PrimaryPosition * WindowSize;
m_UpdatedMousePos.x = clamp(m_UpdatedMousePos.x, 0.0f, WindowSize.x - 1.0f);
m_UpdatedMousePos.y = clamp(m_UpdatedMousePos.y, 0.0f, WindowSize.y - 1.0f);
}
m_UpdatedMouseDelta += m_TouchState.m_PrimaryDelta * WindowSize;
// Scroll currently hovered scroll region with touch scroll gesture.
if(m_TouchState.m_ScrollAmount != vec2(0.0f, 0.0f))
{
if(m_pHotScrollRegion != nullptr)
{
m_pHotScrollRegion->ScrollRelativeDirect(-m_TouchState.m_ScrollAmount.y * pScreen->h);
}
m_TouchState.m_ScrollAmount = vec2(0.0f, 0.0f);
}
// We need to delay the click until the next update or it's not possible to use UI
// elements because click and hover would happen at the same time for touch events.
if(m_TouchState.m_PrimaryPressed)
{
UpdatedMouseButtonsNext |= 1;
}
if(m_TouchState.m_SecondaryPressed)
{
UpdatedMouseButtonsNext |= 2;
}
}
}
m_MousePos = m_UpdatedMousePos * vec2(pScreen->w, pScreen->h) / WindowSize;
m_MouseDelta = m_UpdatedMouseDelta;
m_UpdatedMouseDelta = vec2(0.0f, 0.0f);
m_MouseWorldPos = MouseWorldPos;
m_LastMouseButtons = m_MouseButtons;
m_MouseButtons = MouseButtons;
m_MouseButtons = m_UpdatedMouseButtons;
m_UpdatedMouseButtons = UpdatedMouseButtonsNext;
m_pHotItem = m_pBecomingHotItem;
if(m_pActiveItem)
@ -257,6 +296,87 @@ void CUi::ConvertMouseMove(float *pX, float *pY, IInput::ECursorType CursorType)
*pY *= Factor;
}
void CUi::UpdateTouchState(CTouchState &State) const
{
const std::vector<IInput::CTouchFingerState> &vTouchFingerStates = Input()->TouchFingerStates();
// Updated touch position as long as any finger is beinged pressed.
const bool WasAnyPressed = State.m_AnyPressed;
State.m_AnyPressed = !vTouchFingerStates.empty();
if(State.m_AnyPressed)
{
// We always use the position of first finger being pressed down. Multi-touch UI is
// not possible and always choosing the last finger would cause the cursor to briefly
// warp without having any effect if multiple fingers are used.
const IInput::CTouchFingerState &PrimaryTouchFingerState = vTouchFingerStates.front();
State.m_PrimaryPosition = PrimaryTouchFingerState.m_Position;
State.m_PrimaryDelta = PrimaryTouchFingerState.m_Delta;
}
// Update primary (left click) and secondary (right click) action.
if(State.m_SecondaryPressedNext)
{
// The secondary action is delayed by one frame until the primary has been released,
// otherwise most UI elements cannot be activated by the secondary action because they
// never become the hot-item unless all mouse buttons are released for one frame.
State.m_SecondaryPressedNext = false;
State.m_SecondaryPressed = true;
}
else if(vTouchFingerStates.size() != 1)
{
// Consider primary and secondary to be pressed only when exactly one finger is pressed,
// to avoid UI elements and console text selection being activated while scrolling.
State.m_PrimaryPressed = false;
State.m_SecondaryPressed = false;
}
else if(!WasAnyPressed)
{
State.m_PrimaryPressed = true;
State.m_SecondaryActivationTime = Client()->GlobalTime();
State.m_SecondaryActivationDelta = vec2(0.0f, 0.0f);
}
else if(State.m_PrimaryPressed)
{
// Activate secondary by pressing and holding roughly on the same position for some time.
const float SecondaryActivationDelay = 0.5f;
const float SecondaryActivationMaxDistance = 0.001f;
State.m_SecondaryActivationDelta += State.m_PrimaryDelta;
if(Client()->GlobalTime() - State.m_SecondaryActivationTime >= SecondaryActivationDelay &&
length(State.m_SecondaryActivationDelta) <= SecondaryActivationMaxDistance)
{
State.m_PrimaryPressed = false;
State.m_SecondaryPressedNext = true;
}
}
// Handle two fingers being moved roughly in same direction as a scrolling gesture.
if(vTouchFingerStates.size() == 2)
{
const vec2 Delta0 = vTouchFingerStates[0].m_Delta;
const vec2 Delta1 = vTouchFingerStates[1].m_Delta;
const float Similarity = dot(normalize(Delta0), normalize(Delta1));
const float SimilarityThreshold = 0.8f; // How parallel the deltas have to be (1.0f being completely parallel)
if(Similarity > SimilarityThreshold)
{
const float DirectionThreshold = 3.0f; // How much longer the delta of one axis has to be compared to other axis
// Vertical scrolling (y-delta must be larger than x-delta)
if(absolute(Delta0.y) > DirectionThreshold * absolute(Delta0.x) &&
absolute(Delta1.y) > DirectionThreshold * absolute(Delta1.x) &&
Delta0.y * Delta1.y > 0.0f) // Same y direction required
{
// Accumulate average delta of the two fingers
State.m_ScrollAmount.y += (Delta0.y + Delta1.y) / 2.0f;
}
}
}
else
{
// Scrolling gesture should start from zero again if released.
State.m_ScrollAmount = vec2(0.0f, 0.0f);
}
}
bool CUi::ConsumeHotkey(EHotkey Hotkey)
{
const bool Pressed = m_HotkeysPressed & Hotkey;

View file

@ -320,6 +320,26 @@ public:
*/
typedef std::function<void()> FPopupMenuClosedCallback;
/**
* Represents the aggregated state of current touch events to control a user interface.
*/
class CTouchState
{
friend class CUi;
bool m_SecondaryPressedNext = false;
float m_SecondaryActivationTime = 0.0f;
vec2 m_SecondaryActivationDelta = vec2(0.0f, 0.0f);
public:
bool m_AnyPressed = false;
bool m_PrimaryPressed = false;
bool m_SecondaryPressed = false;
vec2 m_PrimaryPosition = vec2(-1.0f, -1.0f);
vec2 m_PrimaryDelta = vec2(0.0f, 0.0f);
vec2 m_ScrollAmount = vec2(0.0f, 0.0f);
};
private:
bool m_Enabled;
@ -327,8 +347,8 @@ private:
const void *m_pActiveItem = nullptr;
const void *m_pLastActiveItem = nullptr; // only used internally to track active CLineInput
const void *m_pBecomingHotItem = nullptr;
const CScrollRegion *m_pHotScrollRegion = nullptr;
const CScrollRegion *m_pBecomingHotScrollRegion = nullptr;
CScrollRegion *m_pHotScrollRegion = nullptr;
CScrollRegion *m_pBecomingHotScrollRegion = nullptr;
bool m_ActiveItemValid = false;
int m_ActiveButtonLogicButton = -1;
@ -360,8 +380,10 @@ private:
vec2 m_MousePos = vec2(0.0f, 0.0f); // in gui space
vec2 m_MouseDelta = vec2(0.0f, 0.0f); // in gui space
vec2 m_MouseWorldPos = vec2(-1.0f, -1.0f); // in world space
unsigned m_UpdatedMouseButtons = 0;
unsigned m_MouseButtons = 0;
unsigned m_LastMouseButtons = 0;
CTouchState m_TouchState;
bool m_MouseSlow = false;
bool m_MouseLock = false;
const void *m_pMouseLockId = nullptr;
@ -491,7 +513,7 @@ public:
}
return false;
}
void SetHotScrollRegion(const CScrollRegion *pId) { m_pBecomingHotScrollRegion = pId; }
void SetHotScrollRegion(CScrollRegion *pId) { m_pBecomingHotScrollRegion = pId; }
const void *HotItem() const { return m_pHotItem; }
const void *NextHotItem() const { return m_pBecomingHotItem; }
const void *ActiveItem() const { return m_pActiveItem; }
@ -512,6 +534,7 @@ public:
bool MouseInsideClip() const { return !IsClipped() || MouseInside(ClipArea()); }
bool MouseHovered(const CUIRect *pRect) const { return MouseInside(pRect) && MouseInsideClip(); }
void ConvertMouseMove(float *pX, float *pY, IInput::ECursorType CursorType) const;
void UpdateTouchState(CTouchState &State) const;
void ResetMouseSlow() { m_MouseSlow = false; }
bool ConsumeHotkey(EHotkey Hotkey);

View file

@ -234,6 +234,11 @@ void CScrollRegion::ScrollRelative(EScrollRelative Direction, float SpeedMultipl
m_ScrollSpeedMultiplier = SpeedMultiplier;
}
void CScrollRegion::ScrollRelativeDirect(float ScrollAmount)
{
m_RequestScrollY = clamp(m_ScrollY + ScrollAmount, 0.0f, m_ContentH - m_ClipRect.h);
}
void CScrollRegion::DoEdgeScrolling()
{
if(!ScrollbarShown())

View file

@ -133,6 +133,7 @@ public:
bool AddRect(const CUIRect &Rect, bool ShouldScrollHere = false); // returns true if the added rect is visible (not clipped)
void ScrollHere(EScrollOption Option = SCROLLHERE_KEEP_IN_VIEW);
void ScrollRelative(EScrollRelative Direction, float SpeedMultiplier = 1.0f);
void ScrollRelativeDirect(float ScrollAmount);
const CUIRect *ClipRect() const { return &m_ClipRect; }
void DoEdgeScrolling();
bool RectClipped(const CUIRect &Rect) const;