diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index d343c0436..a10a6f157 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -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. diff --git a/src/engine/client/input.cpp b/src/engine/client/input.cpp index 528fc4e82..dd5fe2ba6 100644 --- a/src/engine/client/input.cpp +++ b/src/engine/client/input.cpp @@ -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 &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 diff --git a/src/engine/client/input.h b/src/engine/client/input.h index 5ff04b06c..e53ec3b4c 100644 --- a/src/engine/client/input.h +++ b/src/engine/client/input.h @@ -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 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 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 &TouchFingerStates() const override; const char *GetClipboardText() override; void SetClipboardText(const char *pText) override; diff --git a/src/engine/input.h b/src/engine/input.h index fec0b71c2..efaec84f2 100644 --- a/src/engine/input.h +++ b/src/engine/input.h @@ -6,9 +6,11 @@ #include "kernel.h" #include +#include #include #include +#include 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 &TouchFingerStates() const = 0; + // clipboard virtual const char *GetClipboardText() = 0; virtual void SetClipboardText(const char *pText) = 0; diff --git a/src/game/client/components/console.cpp b/src/game/client/components/console.cpp index b962b8a3c..d0a3097bf 100644 --- a/src/game/client/components/console.cpp +++ b/src/game/client/components/console.cpp @@ -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; diff --git a/src/game/client/components/console.h b/src/game/client/components/console.h index 3e92ad195..bdfbf4a62 100644 --- a/src/game/client/components/console.h +++ b/src/game/client/components/console.h @@ -10,6 +10,7 @@ #include #include +#include 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; diff --git a/src/game/client/ui.cpp b/src/game/client/ui.cpp index e29646fa3..16830838c 100644 --- a/src/game/client/ui.cpp +++ b/src/game/client/ui.cpp @@ -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); + } + } + + // 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; + } + } } - const CUIRect *pScreen = Screen(); - m_MousePos = m_UpdatedMousePos * vec2(pScreen->w / Graphics()->WindowWidth(), pScreen->h / Graphics()->WindowHeight()); + 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 &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; diff --git a/src/game/client/ui.h b/src/game/client/ui.h index 7ee062aea..6cc7129f2 100644 --- a/src/game/client/ui.h +++ b/src/game/client/ui.h @@ -320,6 +320,26 @@ public: */ typedef std::function 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); diff --git a/src/game/client/ui_scrollregion.cpp b/src/game/client/ui_scrollregion.cpp index a4d8abe6f..8c0cdba6c 100644 --- a/src/game/client/ui_scrollregion.cpp +++ b/src/game/client/ui_scrollregion.cpp @@ -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()) diff --git a/src/game/client/ui_scrollregion.h b/src/game/client/ui_scrollregion.h index 203356c4c..0094b66e2 100644 --- a/src/game/client/ui_scrollregion.h +++ b/src/game/client/ui_scrollregion.h @@ -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;