Support touch input in engine, UI and console

Add support for touch input to the engine, UI and console. Ingame touch controls require more discussion and will be delivered separately based on this engine implementation.

Engine
------

The state of all currently pressed touch fingers is aggregated based on the SDL touch events and can be retrieved with the `IInput::TouchFingerStates` function. This design is less complex than an event-based system where the touch events are delivered to the individual client components, as each system would then have to keep track of the finger states individually. However, this means that only one component can handle touch fingers at any given time, which seems like a reasonable assumption for our use cases.

Obsolete code for relative mouse handling on Android is removed. Connecting a mouse to an Android device should now also work as expected, as more recent SDL/Android versions support relative mouse input natively.

User Interface
--------------

Support absolute mouse positioning and clicking in the user interfaces (menus, editor, demo player) with touch presses.

Support right clicking by pressing and holding one finger at roughly the same position for 0.5 seconds.

Support scrolling scroll regions up and down with a two finger swiping gesture. Fast scrolling via a two finger flinging gesture is not yet supported and would be a useful future extension.

The menus and demo player are fully usable with touch inputs. The editor is only fully usable with an external keyboard and/or mouse, as panning the map is not currently possible with only touch inputs, which is also left as a possible future extension.

Console
-------

The touch input logic for the user interface is reused for the console. Thereby, text selection in the console with touch input works, although the text can only be copied by pressing Ctrl+C with an external keyboard at the moment. In the future, we could add buttons to the console to activate the search and copy functionalities with touch inputs.

Support scrolling the console history up and down with a two finger swiping gesture.

The local/remote consoles can currently only be opened with an external keyboard. The ingame touch controls will also include buttons to open the consoles.
This commit is contained in:
Robert Müller 2024-05-21 21:25:35 +02:00
parent 75d2b82ccd
commit 36f19f491e
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);
}
}
// 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<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;