Browse Source

Process touch events and support inertial scrolling during touch (#795)

Andrew Karpushin 3 months ago
parent
commit
f62ff5a75f

+ 56 - 0
Include/RmlUi/Core/Context.h

@@ -237,6 +237,29 @@ public:
 	/// @note The mouse is considered activated again after the next call to 'ProcessMouseMove()'.
 	/// @note The mouse is considered activated again after the next call to 'ProcessMouseMove()'.
 	bool ProcessMouseLeave();
 	bool ProcessMouseLeave();
 
 
+	/// Process touch movements for this context.
+	/// @param[in] touches List of touches.
+	/// @param[in] key_modifier_state The state of key modifiers (shift, control, caps-lock, etc.) keys; this should be generated by ORing together
+	/// members of the Input::KeyModifier enumeration.
+	/// @return True if no touch points are interacting with any elements in the context, otherwise false.
+	bool ProcessTouchMove(const TouchList& touches, int key_modifier_state);
+	/// Process touch start (press) for this context.
+	/// @param[in] touches List of touches.
+	/// @param[in] key_modifier_state The state of key modifiers (shift, control, caps-lock, etc.) keys; this should be generated by ORing together
+	/// members of the Input::KeyModifier enumeration.
+	/// @return True if no touch points are interacting with any elements in the context, otherwise false.
+	bool ProcessTouchStart(const TouchList& touches, int key_modifier_state);
+	/// Process touch end (release) for this context.
+	/// @param[in] touches List of touches.
+	/// @param[in] key_modifier_state The state of key modifiers (shift, control, caps-lock, etc.) keys; this should be generated by ORing together
+	/// members of the Input::KeyModifier enumeration.
+	/// @return True if no touch points are interacting with any elements in the context, otherwise false.
+	bool ProcessTouchEnd(const TouchList& touches, int key_modifier_state);
+	/// Process touch cancel for this context.
+	/// @param[in] touches List of touches.
+	/// @return True if no touch points are interacting with any elements in the context, otherwise false.
+	bool ProcessTouchCancel(const TouchList& touches);
+
 	/// Returns a hint on whether the mouse is currently interacting with any elements in this context, based on previously submitted
 	/// Returns a hint on whether the mouse is currently interacting with any elements in this context, based on previously submitted
 	/// 'ProcessMouse...()' commands.
 	/// 'ProcessMouse...()' commands.
 	/// @note Interaction is determined irrespective of background and opacity. See the RCSS property 'pointer-events' to disable interaction for
 	/// @note Interaction is determined irrespective of background and opacity. See the RCSS property 'pointer-events' to disable interaction for
@@ -344,6 +367,20 @@ private:
 	Vector2i mouse_position;
 	Vector2i mouse_position;
 	bool mouse_active;
 	bool mouse_active;
 
 
+	// The current state of Touches, required to implement proper inertia scrolling.
+	struct TouchState
+	{
+		bool scrolling_right = false;
+		bool scrolling_down = false;
+		Vector2f start_position;
+		Vector2f last_position;
+		Element* scroll_container = nullptr;
+		double scrolling_last_time = 0;
+		double scrolling_start_time_x = 0;
+		double scrolling_start_time_y = 0;
+	};
+	SmallUnorderedMap<TouchId, TouchState> touch_states;
+
 	// Controller for various scroll behavior modes.
 	// Controller for various scroll behavior modes.
 	UniquePtr<ScrollController> scroll_controller; // [not-null]
 	UniquePtr<ScrollController> scroll_controller; // [not-null]
 
 
@@ -415,6 +452,25 @@ private:
 	// Releases all unloaded documents pending destruction.
 	// Releases all unloaded documents pending destruction.
 	void ReleaseUnloadedDocuments();
 	void ReleaseUnloadedDocuments();
 
 
+	// Helper method to lookup TouchState by touch id.
+	TouchState* LookupTouch(TouchId identifier);
+	/// Process single touch movement for this context.
+	/// @param[in] touch Touch data: identifier and coordinates.
+	/// @return True if touch point is not interacting with any elements in the context, otherwise false.
+	bool ProcessTouchMove(const Touch& touch, int key_modifier_state);
+	/// Process single touch press for this context.
+	/// @param[in] touch Touch data: identifier and coordinates.
+	/// @return True if touch point is not interacting with any elements in the context, otherwise false.
+	bool ProcessTouchStart(const Touch& touch, int key_modifier_state);
+	/// Process single touch release for this context.
+	/// @param[in] touch Touch data: identifier and coordinates.
+	/// @return True if touch point is not interacting with any elements in the context, otherwise false.
+	bool ProcessTouchEnd(const Touch& touch, int key_modifier_state);
+	/// Cancel processing touch for this context.
+	/// @param[in] touch Touch data: identifier and coordinates.
+	/// @return True if touch point is not interacting with any elements in the context, otherwise false.
+	bool ProcessTouchCancel(const Touch& touch);
+
 	// Sends the specified event to all elements in new_items that don't appear in old_items.
 	// Sends the specified event to all elements in new_items that don't appear in old_items.
 	static void SendEvents(const ElementSet& old_items, const ElementSet& new_items, EventId id, const Dictionary& parameters);
 	static void SendEvents(const ElementSet& old_items, const ElementSet& new_items, EventId id, const Dictionary& parameters);
 
 

+ 7 - 0
Include/RmlUi/Core/Types.h

@@ -101,6 +101,7 @@ enum class EventId : uint16_t;
 enum class PropertyId : uint8_t;
 enum class PropertyId : uint8_t;
 enum class MediaQueryId : uint8_t;
 enum class MediaQueryId : uint8_t;
 enum class FamilyId : int;
 enum class FamilyId : int;
+using TouchId = uintptr_t;
 
 
 // Types for external interfaces.
 // Types for external interfaces.
 using FileHandle = uintptr_t;
 using FileHandle = uintptr_t;
@@ -117,6 +118,12 @@ using ElementPtr = UniqueReleaserPtr<Element>;
 using ContextPtr = UniqueReleaserPtr<Context>;
 using ContextPtr = UniqueReleaserPtr<Context>;
 using EventPtr = UniqueReleaserPtr<Event>;
 using EventPtr = UniqueReleaserPtr<Event>;
 
 
+struct Touch {
+	TouchId identifier;
+	Vector2f position;
+};
+using TouchList = Vector<Touch>;
+
 enum class StableVectorIndex : uint32_t { Invalid = uint32_t(-1) };
 enum class StableVectorIndex : uint32_t { Invalid = uint32_t(-1) };
 enum class TextureFileIndex : uint32_t { Invalid = uint32_t(-1) };
 enum class TextureFileIndex : uint32_t { Invalid = uint32_t(-1) };
 
 

+ 199 - 0
Source/Core/Context.cpp

@@ -55,6 +55,10 @@ static constexpr float DOUBLE_CLICK_TIME = 0.5f;    // [s]
 static constexpr float DOUBLE_CLICK_MAX_DIST = 3.f; // [dp]
 static constexpr float DOUBLE_CLICK_MAX_DIST = 3.f; // [dp]
 static constexpr float UNIT_SCROLL_LENGTH = 80.f;   // [dp]
 static constexpr float UNIT_SCROLL_LENGTH = 80.f;   // [dp]
 
 
+// If the user stops scrolling for this amount of time in seconds before touch/click release, don't apply inertia.
+static constexpr float SCROLL_INERTIA_DELAY = 0.1f;
+static constexpr float TOUCH_MOVEMENT_DECAY_RATE = 5.0f;
+
 static void DebugVerifyLocaleSetting()
 static void DebugVerifyLocaleSetting()
 {
 {
 #ifdef RMLUI_DEBUG
 #ifdef RMLUI_DEBUG
@@ -875,6 +879,192 @@ bool Context::IsMouseInteracting() const
 	return (hover && hover != root.get()) || (active && active != root.get()) || scroll_controller->GetMode() == ScrollController::Mode::Autoscroll;
 	return (hover && hover != root.get()) || (active && active != root.get()) || scroll_controller->GetMode() == ScrollController::Mode::Autoscroll;
 }
 }
 
 
+Context::TouchState* Context::LookupTouch(TouchId identifier)
+{
+	auto touch_it = touch_states.find(identifier);
+	return touch_it != touch_states.end() ? &touch_it->second : nullptr;
+}
+
+bool Context::ProcessTouchStart(const TouchList& touches, int key_modifier_state)
+{
+	bool result = true;
+	for (const auto& touch : touches)
+		result &= ProcessTouchStart(touch, key_modifier_state);
+	return result;
+}
+
+bool Context::ProcessTouchMove(const TouchList& touches, int key_modifier_state)
+{
+	bool result = true;
+	for (const auto& touch : touches)
+		result &= ProcessTouchMove(touch, key_modifier_state);
+	return result;
+}
+
+bool Context::ProcessTouchEnd(const TouchList& touches, int key_modifier_state)
+{
+	bool result = true;
+	for (const auto& touch : touches)
+		result &= ProcessTouchEnd(touch, key_modifier_state);
+	return result;
+}
+
+bool Context::ProcessTouchCancel(const TouchList& touches)
+{
+	bool result = true;
+	for (const auto& touch : touches)
+		result &= ProcessTouchCancel(touch);
+	return result;
+}
+
+bool Context::ProcessTouchStart(const Touch& touch, int key_modifier_state)
+{
+	TouchState * state = LookupTouch(touch.identifier);
+	RMLUI_ASSERTMSG(state == nullptr, "Receiving touch start event for an already started touch.");
+	if (!state)
+	{
+		auto it_inserted = touch_states.emplace(touch.identifier, TouchState()).first;
+		state = &it_inserted->second;
+	}
+
+	state->start_position = state->last_position = touch.position;
+	state->scrolling_start_time_x = state->scrolling_start_time_y = 0;
+	state->scrolling_last_time = GetSystemInterface()->GetElapsedTime();
+
+	Element* touch_element = GetElementAtPoint(touch.position);
+	state->scroll_container = touch_element ? touch_element->GetClosestScrollableContainer() : nullptr;
+
+	// reset any scrolling when we touch the element
+	if (state->scroll_container && scroll_controller->GetTarget() == state->scroll_container)
+		scroll_controller->Reset();
+
+	ProcessMouseMove(static_cast<int>(touch.position.x), static_cast<int>(touch.position.y), key_modifier_state);
+
+	// always assume touch press/release events are handled as left mouse button
+	return ProcessMouseButtonDown(0, key_modifier_state);
+}
+
+bool Context::ProcessTouchMove(const Touch& touch, int key_modifier_state)
+{
+	TouchState* state = LookupTouch(touch.identifier);
+	if (!state)
+		return true;
+
+	state->scrolling_last_time = GetSystemInterface()->GetElapsedTime();
+
+	if (state->scroll_container)
+	{
+		if (drag)
+		{
+			// Don't scroll and reset scrolling state when dragging any element (scrollbars and others)
+			state->start_position = state->last_position = touch.position;
+			state->scrolling_start_time_x = state->scrolling_start_time_y = 0;
+		}
+		else
+		{
+			Vector2f delta = touch.position - state->last_position;
+			if (delta.x != 0 || delta.y != 0)
+			{
+				// use instant scrolling when touch is pressed even when default scroll behavior is smooth
+				scroll_controller->InstantScrollOnTarget(state->scroll_container, -delta);
+
+				double current_time = GetSystemInterface()->GetElapsedTime();
+
+				// If the user changes direction, reset the start time and position.
+				bool going_right = (delta.x > 0);
+				if (delta.x != 0 && (going_right != state->scrolling_right ||
+						state->scrolling_start_time_x == 0)) // time set to 0 means no touch move events happened before and direction is unclear
+				{
+					state->start_position.x = touch.position.x;
+					state->scrolling_right = going_right;
+					state->scrolling_start_time_x = state->scrolling_last_time;
+				}
+				else
+				{
+					// move starting position towards end position with a weight of e^-kt to better capture 
+					// and calculate velocity of the very last touch movements before touch release
+					float elapsed_time_x = static_cast<float>(current_time - state->scrolling_start_time_x);
+					float weight = Math::Exp(-elapsed_time_x * TOUCH_MOVEMENT_DECAY_RATE);
+
+					state->start_position.x = touch.position.x - (touch.position.x - state->start_position.x) * weight;
+					state->scrolling_start_time_x = current_time - (current_time - state->scrolling_start_time_x) * weight;
+				}
+
+				bool going_down = (delta.y > 0);
+				if (delta.y != 0 && (going_down != state->scrolling_down ||
+						state->scrolling_start_time_y == 0)) // time set to 0 means no touch move events happened before and direction is unclear
+				{
+					state->start_position.y = touch.position.y;
+					state->scrolling_down = going_down;
+					state->scrolling_start_time_y = state->scrolling_last_time;
+				}
+				else
+				{
+					// move starting position towards end position with a weight of e^-kt to better capture
+					// and calculate velocity of the very last touch movements before touch release
+					float elapsed_time_y = static_cast<float>(current_time - state->scrolling_start_time_y);
+					float weight = Math::Exp(-elapsed_time_y * TOUCH_MOVEMENT_DECAY_RATE);
+
+					state->start_position.y = touch.position.y - (touch.position.y - state->start_position.y) * weight;
+					state->scrolling_start_time_y = current_time - (current_time - state->scrolling_start_time_y) * weight;
+				}	
+			}
+		}
+	}
+
+	state->last_position = touch.position;
+
+	return ProcessMouseMove(static_cast<int>(touch.position.x), static_cast<int>(touch.position.y), key_modifier_state);
+}
+
+bool Context::ProcessTouchEnd(const Touch& touch, int key_modifier_state)
+{
+	TouchState* state = LookupTouch(touch.identifier);
+	if (!state)
+		return true;
+
+	if (state->scroll_container)
+	{
+		double current_time = GetSystemInterface()->GetElapsedTime();
+		double time_since_last_move = current_time - state->scrolling_last_time;
+		if (time_since_last_move < SCROLL_INERTIA_DELAY)
+		{
+			// apply scrolling inertia
+			Vector2f delta = touch.position - state->start_position;
+
+			Vector2f velocity(-delta);
+
+			float elapsed_time_x = static_cast<float>(current_time - state->scrolling_start_time_x);
+			float elapsed_time_y = static_cast<float>(current_time - state->scrolling_start_time_y);
+
+			if (elapsed_time_x > 0)
+				velocity.x /= elapsed_time_x;
+			if (elapsed_time_y > 0)
+				velocity.y /= elapsed_time_y;
+
+			scroll_controller->ApplyScrollInertia(state->scroll_container, velocity);
+		}
+	}
+
+	touch_states.erase(touch.identifier);
+
+	ProcessMouseMove(static_cast<int>(touch.position.x), static_cast<int>(touch.position.y), key_modifier_state);
+
+	// always assume touch press/release events are handled as left mouse button
+	return ProcessMouseButtonUp(0, key_modifier_state);
+}
+
+bool Context::ProcessTouchCancel(const Touch& touch)
+{
+	TouchState* state = LookupTouch(touch.identifier);
+	if (!state)
+		return false;
+
+	touch_states.erase(touch.identifier);
+
+	return ProcessMouseButtonUp(0, 0);
+}
+
 void Context::SetDefaultScrollBehavior(ScrollBehavior scroll_behavior, float speed_factor)
 void Context::SetDefaultScrollBehavior(ScrollBehavior scroll_behavior, float speed_factor)
 {
 {
 	scroll_controller->SetDefaultScrollBehavior(scroll_behavior, speed_factor);
 	scroll_controller->SetDefaultScrollBehavior(scroll_behavior, speed_factor);
@@ -1003,6 +1193,15 @@ void Context::OnElementDetach(Element* element)
 
 
 	if (scroll_controller->GetTarget() == element)
 	if (scroll_controller->GetTarget() == element)
 		scroll_controller->Reset();
 		scroll_controller->Reset();
+
+	// Clear TouchState if we're touching element
+	for (auto touch_it = touch_states.begin(); touch_it != touch_states.end();)
+	{
+		if (touch_it->second.scroll_container == element)
+			touch_it = touch_states.erase(touch_it);
+		else
+			++touch_it;
+	}
 }
 }
 
 
 bool Context::OnFocusChange(Element* new_focus, bool focus_visible)
 bool Context::OnFocusChange(Element* new_focus, bool focus_visible)

+ 57 - 0
Source/Core/ScrollController.cpp

@@ -42,6 +42,9 @@ static constexpr float SMOOTHSCROLL_MAX_VELOCITY = 10'000.f;   // [dp/s]
 static constexpr float SMOOTHSCROLL_VELOCITY_CONSTANT = 800.f; // [dp/s]
 static constexpr float SMOOTHSCROLL_VELOCITY_CONSTANT = 800.f; // [dp/s]
 static constexpr float SMOOTHSCROLL_VELOCITY_SQUARE_FACTOR = 0.05f;
 static constexpr float SMOOTHSCROLL_VELOCITY_SQUARE_FACTOR = 0.05f;
 
 
+// Factor to multiply friction by before applying to velocity.
+static constexpr float INERTIA_FRICTION_FACTOR = 5.0f;
+
 // Clamp the delta time to some reasonable FPS range, to avoid large steps in case of stuttering or freezing.
 // Clamp the delta time to some reasonable FPS range, to avoid large steps in case of stuttering or freezing.
 static constexpr float DELTA_TIME_CLAMP_LOW = 1.f / 500.f; // [s]
 static constexpr float DELTA_TIME_CLAMP_LOW = 1.f / 500.f; // [s]
 static constexpr float DELTA_TIME_CLAMP_HIGH = 1.f / 15.f; // [s]
 static constexpr float DELTA_TIME_CLAMP_HIGH = 1.f / 15.f; // [s]
@@ -123,12 +126,40 @@ void ScrollController::ActivateSmoothscroll(Element* in_target, Vector2f delta_d
 		Reset();
 		Reset();
 }
 }
 
 
+void ScrollController::InstantScrollOnTarget(Element* in_target, Vector2f delta_distance)
+{
+	if (!in_target)
+		return;
+
+	// instant scroll element without changing the current target
+
+	Element* safe_target = target;
+
+	target = in_target;
+	PerformScrollOnTarget(delta_distance);
+	target = safe_target;
+}
+
+void ScrollController::ApplyScrollInertia(Element* in_target, const Vector2f& velocity)
+{
+	Reset();
+	if (!in_target || (velocity.x == 0 && velocity.y == 0))
+		return;
+
+	target = in_target;
+	inertia_scroll_velocity = velocity;
+	mode = Mode::Inertia;
+	UpdateTime();
+}
+
 bool ScrollController::Update(Vector2i mouse_position, float dp_ratio)
 bool ScrollController::Update(Vector2i mouse_position, float dp_ratio)
 {
 {
 	if (mode == Mode::Autoscroll)
 	if (mode == Mode::Autoscroll)
 		UpdateAutoscroll(mouse_position, dp_ratio);
 		UpdateAutoscroll(mouse_position, dp_ratio);
 	else if (mode == Mode::Smoothscroll)
 	else if (mode == Mode::Smoothscroll)
 		UpdateSmoothscroll(dp_ratio);
 		UpdateSmoothscroll(dp_ratio);
+	else if (mode == Mode::Inertia)
+		UpdateInertia();
 
 
 	return mode != Mode::None;
 	return mode != Mode::None;
 }
 }
@@ -190,6 +221,32 @@ void ScrollController::UpdateSmoothscroll(float dp_ratio)
 		Reset();
 		Reset();
 }
 }
 
 
+void ScrollController::UpdateInertia()
+{
+	RMLUI_ASSERT(mode == Mode::Inertia && target);
+
+	if (inertia_scroll_velocity.x == 0.0f && inertia_scroll_velocity.y == 0.0f)
+	{
+		Reset();
+		return;
+	}
+
+	// Apply and dampen inertia.
+
+	float dt = UpdateTime();
+
+	Vector2f scroll_delta = inertia_scroll_velocity * dt;
+	PerformScrollOnTarget(scroll_delta);
+
+	float dampening = 1.0f - INERTIA_FRICTION_FACTOR * dt;
+	inertia_scroll_velocity *= dampening;
+
+	if (std::abs(inertia_scroll_velocity.x) < 30.0f)
+		inertia_scroll_velocity.x = 0.0f;
+	if (std::abs(inertia_scroll_velocity.y) < 30.0f)
+		inertia_scroll_velocity.y = 0.0f;
+}
+
 bool ScrollController::HasSmoothscrollReachedTarget() const
 bool ScrollController::HasSmoothscrollReachedTarget() const
 {
 {
 	constexpr float epsilon = 0.1f;
 	constexpr float epsilon = 0.1f;

+ 9 - 0
Source/Core/ScrollController.h

@@ -47,11 +47,16 @@ public:
 		None,
 		None,
 		Smoothscroll, // Smooth scrolling to target distance.
 		Smoothscroll, // Smooth scrolling to target distance.
 		Autoscroll,   // Scrolling with middle mouse button.
 		Autoscroll,   // Scrolling with middle mouse button.
+		Inertia,      // Applying scrolling inertia when using swipe gesture
 	};
 	};
 
 
 	void ActivateAutoscroll(Element* target, Vector2i start_position);
 	void ActivateAutoscroll(Element* target, Vector2i start_position);
 
 
 	void ActivateSmoothscroll(Element* target, Vector2f delta_distance, ScrollBehavior scroll_behavior);
 	void ActivateSmoothscroll(Element* target, Vector2f delta_distance, ScrollBehavior scroll_behavior);
+	
+	void InstantScrollOnTarget(Element* target, Vector2f delta_distance);
+	
+	void ApplyScrollInertia(Element* target, const Vector2f& velocity);
 
 
 	bool Update(Vector2i mouse_position, float dp_ratio);
 	bool Update(Vector2i mouse_position, float dp_ratio);
 
 
@@ -79,6 +84,8 @@ private:
 
 
 	void UpdateSmoothscroll(float dp_ratio);
 	void UpdateSmoothscroll(float dp_ratio);
 
 
+	void UpdateInertia();
+
 	bool HasSmoothscrollReachedTarget() const;
 	bool HasSmoothscrollReachedTarget() const;
 
 
 	void PerformScrollOnTarget(Vector2f delta_distance);
 	void PerformScrollOnTarget(Vector2f delta_distance);
@@ -97,6 +104,8 @@ private:
 
 
 	Vector2f smoothscroll_target_distance;
 	Vector2f smoothscroll_target_distance;
 	Vector2f smoothscroll_scrolled_distance;
 	Vector2f smoothscroll_scrolled_distance;
+
+	Vector2f inertia_scroll_velocity;
 };
 };
 
 
 } // namespace Rml
 } // namespace Rml