Michael Ragazzon 2 лет назад
Родитель
Сommit
fad11329d6

+ 2 - 0
CMake/FileList.cmake

@@ -85,6 +85,7 @@ set(Core_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserString.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserString.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserTransform.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserTransform.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyShorthandDefinition.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyShorthandDefinition.h
+    ${PROJECT_SOURCE_DIR}/Source/Core/ScrollController.h
     ${PROJECT_SOURCE_DIR}/Source/Core/StreamFile.h
     ${PROJECT_SOURCE_DIR}/Source/Core/StreamFile.h
     ${PROJECT_SOURCE_DIR}/Source/Core/StyleSheetFactory.h
     ${PROJECT_SOURCE_DIR}/Source/Core/StyleSheetFactory.h
     ${PROJECT_SOURCE_DIR}/Source/Core/StyleSheetNode.h
     ${PROJECT_SOURCE_DIR}/Source/Core/StyleSheetNode.h
@@ -359,6 +360,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserTransform.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertyParserTransform.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertySpecification.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/PropertySpecification.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/RenderInterface.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/RenderInterface.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/ScrollController.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Spritesheet.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Spritesheet.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Stream.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Stream.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/StreamFile.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/StreamFile.cpp

+ 4 - 1
Include/RmlUi/Core/ComputedValues.h

@@ -154,7 +154,7 @@ namespace Style {
 
 
 			flex_basis_type(LengthPercentageAuto::Auto), row_gap_type(LengthPercentage::Length), column_gap_type(LengthPercentage::Length),
 			flex_basis_type(LengthPercentageAuto::Auto), row_gap_type(LengthPercentage::Length), column_gap_type(LengthPercentage::Length),
 
 
-			vertical_align_type(VerticalAlign::Baseline), drag(Drag::None), tab_index(TabIndex::None)
+			vertical_align_type(VerticalAlign::Baseline), drag(Drag::None), tab_index(TabIndex::None), overscroll_behavior(OverscrollBehavior::Auto)
 		{}
 		{}
 
 
 		LengthPercentage::Type min_width_type : 1, max_width_type : 1;
 		LengthPercentage::Type min_width_type : 1, max_width_type : 1;
@@ -169,6 +169,7 @@ namespace Style {
 		VerticalAlign::Type vertical_align_type : 4;
 		VerticalAlign::Type vertical_align_type : 4;
 		Drag drag : 3;
 		Drag drag : 3;
 		TabIndex tab_index : 1;
 		TabIndex tab_index : 1;
+		OverscrollBehavior overscroll_behavior : 1;
 
 
 		Clip clip;
 		Clip clip;
 
 
@@ -286,6 +287,7 @@ namespace Style {
 		Colourb           image_color()                const { return rare.image_color; }
 		Colourb           image_color()                const { return rare.image_color; }
 		LengthPercentage  row_gap()                    const { return LengthPercentage(rare.row_gap_type, rare.row_gap); }
 		LengthPercentage  row_gap()                    const { return LengthPercentage(rare.row_gap_type, rare.row_gap); }
 		LengthPercentage  column_gap()                 const { return LengthPercentage(rare.column_gap_type, rare.column_gap); }
 		LengthPercentage  column_gap()                 const { return LengthPercentage(rare.column_gap_type, rare.column_gap); }
+		OverscrollBehavior overscroll_behavior()       const { return rare.overscroll_behavior; }
 		float             scrollbar_margin()           const { return rare.scrollbar_margin; }
 		float             scrollbar_margin()           const { return rare.scrollbar_margin; }
 		
 		
 		// -- Assignment --
 		// -- Assignment --
@@ -362,6 +364,7 @@ namespace Style {
 		void drag                      (Drag value)              { rare.drag                       = value; }
 		void drag                      (Drag value)              { rare.drag                       = value; }
 		void tab_index                 (TabIndex value)          { rare.tab_index                  = value; }
 		void tab_index                 (TabIndex value)          { rare.tab_index                  = value; }
 		void image_color               (Colourb value)           { rare.image_color                = value; }
 		void image_color               (Colourb value)           { rare.image_color                = value; }
+		void overscroll_behavior       (OverscrollBehavior value){ rare.overscroll_behavior        = value; }
 		void scrollbar_margin          (float value)             { rare.scrollbar_margin           = value; }
 		void scrollbar_margin          (float value)             { rare.scrollbar_margin           = value; }
 
 
 		// clang-format on
 		// clang-format on

+ 6 - 14
Include/RmlUi/Core/Context.h

@@ -45,6 +45,7 @@ class RenderInterface;
 class DataModel;
 class DataModel;
 class DataModelConstructor;
 class DataModelConstructor;
 class DataTypeRegister;
 class DataTypeRegister;
+class ScrollController;
 enum class EventId : uint16_t;
 enum class EventId : uint16_t;
 
 
 /**
 /**
@@ -318,12 +319,8 @@ private:
 	Vector2i mouse_position;
 	Vector2i mouse_position;
 	bool mouse_active;
 	bool mouse_active;
 
 
-	// Autoscroll state (scrolling with middle mouse button). Autoscroll enabled when target is non-null.
-	Element* autoscroll_target;
-	Vector2i autoscroll_start_position;
-	Vector2f autoscroll_accumulated_length;
-	double autoscroll_previous_update_time;
-	bool autoscroll_holding;
+	// Controller for various scroll behavior modes.
+	UniquePtr<ScrollController> scroll_controller; // [not-null]
 
 
 	// Enables cursor handling.
 	// Enables cursor handling.
 	bool enable_cursor;
 	bool enable_cursor;
@@ -374,17 +371,12 @@ private:
 	// Releases the drag clone, if one exists.
 	// Releases the drag clone, if one exists.
 	void ReleaseDragClone();
 	void ReleaseDragClone();
 
 
+	// Scroll the target by the given amount, using smooth scrolling.
+	void PerformSmoothscrollOnTarget(Element* target, Vector2f delta_offset);
+
 	// Returns the data model with the provided name, or nullptr if it does not exist.
 	// Returns the data model with the provided name, or nullptr if it does not exist.
 	DataModel* GetDataModelPtr(const String& name) const;
 	DataModel* GetDataModelPtr(const String& name) const;
 
 
-	// Returns the scrolling cursor based on scroll direction.
-	String GetScrollCursor() const;
-
-	// Update autoscroll state (scrolling with middle mouse button), and submit scroll events as necessary.
-	void UpdateAutoscroll();
-	// Reset autoscroll state, disabling the mode.
-	void ResetAutoscroll();
-
 	// Builds the parameters for a generic key event.
 	// Builds the parameters for a generic key event.
 	void GenerateKeyEventParameters(Dictionary& parameters, Input::KeyIdentifier key_identifier);
 	void GenerateKeyEventParameters(Dictionary& parameters, Input::KeyIdentifier key_identifier);
 	// Builds the parameters for a generic mouse event.
 	// Builds the parameters for a generic mouse event.

+ 15 - 2
Include/RmlUi/Core/Element.h

@@ -66,6 +66,11 @@ class TransformState;
 struct ElementMeta;
 struct ElementMeta;
 struct StackingOrderedChild;
 struct StackingOrderedChild;
 
 
+enum class ScrollBehavior {
+	Auto,    // Same as Instant.
+	Smooth,  // Scroll to the destination using a smooth animation.
+	Instant, // Scroll to the destination instantly.
+};
 enum class ScrollAlignment {
 enum class ScrollAlignment {
 	Start,   // Align to the top or left edge of the parent element.
 	Start,   // Align to the top or left edge of the parent element.
 	Center,  // Align to the center of the parent element.
 	Center,  // Align to the center of the parent element.
@@ -76,11 +81,12 @@ enum class ScrollAlignment {
 	Defines behavior of Element::ScrollIntoView.
 	Defines behavior of Element::ScrollIntoView.
  */
  */
 struct ScrollIntoViewOptions {
 struct ScrollIntoViewOptions {
-	ScrollIntoViewOptions(ScrollAlignment vertical = ScrollAlignment::Start, ScrollAlignment horizontal = ScrollAlignment::Nearest) :
-		vertical(vertical), horizontal(horizontal)
+	ScrollIntoViewOptions(ScrollAlignment vertical = ScrollAlignment::Start, ScrollAlignment horizontal = ScrollAlignment::Nearest,
+		ScrollBehavior behavior = ScrollBehavior::Auto) : vertical(vertical), horizontal(horizontal), behavior(behavior)
 	{}
 	{}
 	ScrollAlignment vertical;
 	ScrollAlignment vertical;
 	ScrollAlignment horizontal;
 	ScrollAlignment horizontal;
+	ScrollBehavior behavior;
 };
 };
 
 
 /**
 /**
@@ -522,6 +528,11 @@ public:
 	/// Scrolls the parent element's contents so that this element is visible.
 	/// Scrolls the parent element's contents so that this element is visible.
 	/// @param[in] align_with_top If true, the element will align itself to the top of the parent element's window. If false, the element will be aligned to the bottom of the parent element's window.
 	/// @param[in] align_with_top If true, the element will align itself to the top of the parent element's window. If false, the element will be aligned to the bottom of the parent element's window.
 	void ScrollIntoView(bool align_with_top = true);
 	void ScrollIntoView(bool align_with_top = true);
+	/// Sets the scroll offset of this element to the given coordinates.
+	/// @param[in] position The scroll destination coordinates.
+	/// @param[in] behavior Smooth scrolling behavior.
+	/// @note Smooth scrolling can only be applied to a single element at a time, any active smooth scrolls will be cancelled.
+	void ScrollTo(Vector2f offset, ScrollBehavior behavior = ScrollBehavior::Auto);
 
 
 	/// Append a child to this element.
 	/// Append a child to this element.
 	/// @param[in] element The element to append as a child.
 	/// @param[in] element The element to append as a child.
@@ -583,6 +594,8 @@ public:
 	ElementDecoration* GetElementDecoration() const;
 	ElementDecoration* GetElementDecoration() const;
 	/// Returns the element's scrollbar functionality.
 	/// Returns the element's scrollbar functionality.
 	ElementScroll* GetElementScroll() const;
 	ElementScroll* GetElementScroll() const;
+	/// Returns the element's nearest scroll container that can be scrolled, if any.
+	Element* GetClosestScrollableContainer();
 	/// Returns the element's transform state.
 	/// Returns the element's transform state.
 	const TransformState* GetTransformState() const noexcept;
 	const TransformState* GetTransformState() const noexcept;
 	/// Returns the data model of this element.
 	/// Returns the data model of this element.

+ 1 - 0
Include/RmlUi/Core/ID.h

@@ -140,6 +140,7 @@ enum class PropertyId : uint8_t
 	Drag,
 	Drag,
 	TabIndex,
 	TabIndex,
 	ScrollbarMargin,
 	ScrollbarMargin,
+	OverscrollBehavior,
 
 
 	Perspective,
 	Perspective,
 	PerspectiveOriginX,
 	PerspectiveOriginX,

+ 4 - 0
Include/RmlUi/Core/Math.h

@@ -111,6 +111,10 @@ RMLUICORE_API float AbsoluteValue(float value);
 /// @param[in] value The number of get the absolute value of.
 /// @param[in] value The number of get the absolute value of.
 /// @return The absolute value of the number.
 /// @return The absolute value of the number.
 RMLUICORE_API int AbsoluteValue(int value);
 RMLUICORE_API int AbsoluteValue(int value);
+/// Calculates the component-wise absolute value of a vector.
+/// @param[in] value The vector of get the absolute value of.
+/// @return The absolute value of the vector.
+RMLUICORE_API Vector2f AbsoluteValue(Vector2f value);
 
 
 /// Calculates the cosine of an angle.
 /// Calculates the cosine of an angle.
 /// @param[in] angle The angle to calculate the cosine of, in radians.
 /// @param[in] angle The angle to calculate the cosine of, in radians.

+ 1 - 0
Include/RmlUi/Core/StyleTypes.h

@@ -126,6 +126,7 @@ namespace Style {
 	enum class Drag : uint8_t { None, Drag, DragDrop, Block, Clone };
 	enum class Drag : uint8_t { None, Drag, DragDrop, Block, Clone };
 	enum class TabIndex : uint8_t { None, Auto };
 	enum class TabIndex : uint8_t { None, Auto };
 	enum class Focus : uint8_t { None, Auto };
 	enum class Focus : uint8_t { None, Auto };
+	enum class OverscrollBehavior : uint8_t { Auto, Contain };
 	enum class PointerEvents : uint8_t { None, Auto };
 	enum class PointerEvents : uint8_t { None, Auto };
 
 
 	using PerspectiveOrigin = LengthPercentage;
 	using PerspectiveOrigin = LengthPercentage;

+ 50 - 125
Source/Core/Context.cpp

@@ -27,8 +27,8 @@
  */
  */
 
 
 #include "../../Include/RmlUi/Core/Context.h"
 #include "../../Include/RmlUi/Core/Context.h"
-#include "../../Include/RmlUi/Core/ContextInstancer.h"
 #include "../../Include/RmlUi/Core/ComputedValues.h"
 #include "../../Include/RmlUi/Core/ComputedValues.h"
+#include "../../Include/RmlUi/Core/ContextInstancer.h"
 #include "../../Include/RmlUi/Core/Core.h"
 #include "../../Include/RmlUi/Core/Core.h"
 #include "../../Include/RmlUi/Core/DataModelHandle.h"
 #include "../../Include/RmlUi/Core/DataModelHandle.h"
 #include "../../Include/RmlUi/Core/ElementDocument.h"
 #include "../../Include/RmlUi/Core/ElementDocument.h"
@@ -38,11 +38,10 @@
 #include "../../Include/RmlUi/Core/RenderInterface.h"
 #include "../../Include/RmlUi/Core/RenderInterface.h"
 #include "../../Include/RmlUi/Core/StreamMemory.h"
 #include "../../Include/RmlUi/Core/StreamMemory.h"
 #include "../../Include/RmlUi/Core/SystemInterface.h"
 #include "../../Include/RmlUi/Core/SystemInterface.h"
-#include "../../Include/RmlUi/Core/StreamMemory.h"
-#include "ComputeProperty.h"
 #include "DataModel.h"
 #include "DataModel.h"
 #include "EventDispatcher.h"
 #include "EventDispatcher.h"
 #include "PluginRegistry.h"
 #include "PluginRegistry.h"
+#include "ScrollController.h"
 #include "StreamFile.h"
 #include "StreamFile.h"
 #include <algorithm>
 #include <algorithm>
 #include <iterator>
 #include <iterator>
@@ -50,30 +49,11 @@
 
 
 namespace Rml {
 namespace Rml {
 
 
-static constexpr float DOUBLE_CLICK_TIME = 0.5f;     // [s]
-static constexpr float DOUBLE_CLICK_MAX_DIST = 3.f;  // [dp]
-static constexpr float AUTOSCROLL_SPEED_FACTOR = 0.09f;
-static constexpr float AUTOSCROLL_DEADZONE = 10.0f; // [dp]
+static constexpr float DOUBLE_CLICK_TIME = 0.5f;    // [s]
+static constexpr float DOUBLE_CLICK_MAX_DIST = 3.f; // [dp]
+static constexpr float UNIT_SCROLL_LENGTH = 100.f;  // [dp]
 
 
-// Determines the autoscroll velocity based on the distance from the scroll-start mouse position. [px/s]
-static Vector2f CalculateAutoscrollVelocity(Vector2f scroll_delta, float dp_ratio)
-{
-	auto DeadzoneShift = [](Vector2f value, float deadzone) -> Vector2f {
-		return Vector2f{
-			Math::AbsoluteValue(value.x) < deadzone ? 0.f : value.x,
-			Math::AbsoluteValue(value.y) < deadzone ? 0.f : value.y,
-		};
-	};
-
-	scroll_delta = scroll_delta / dp_ratio;
-	scroll_delta = DeadzoneShift(scroll_delta, AUTOSCROLL_DEADZONE);
-	const Vector2f scroll_delta_abs = {Math::AbsoluteValue(scroll_delta.x), Math::AbsoluteValue(scroll_delta.y)};
-
-	// We use a signed square model for the velocity, which seems to work quite well. This is mostly about feeling and tuning.
-	return AUTOSCROLL_SPEED_FACTOR * scroll_delta * scroll_delta_abs;
-}
-
-Context::Context(const String& name) : name(name), dimensions(0, 0), density_independent_pixel_ratio(1.0f), mouse_position(0, 0), autoscroll_start_position(0, 0), clip_origin(-1, -1), clip_dimensions(-1, -1)
+Context::Context(const String& name) : name(name), dimensions(0, 0), density_independent_pixel_ratio(1.0f), mouse_position(0, 0), clip_origin(-1, -1), clip_dimensions(-1, -1)
 {
 {
 	instancer = nullptr;
 	instancer = nullptr;
 
 
@@ -118,9 +98,7 @@ Context::Context(const String& name) : name(name), dimensions(0, 0), density_ind
 	mouse_active = false;
 	mouse_active = false;
 	enable_cursor = true;
 	enable_cursor = true;
 
 
-	autoscroll_target = nullptr;
-	autoscroll_previous_update_time = 0;
-	autoscroll_holding = false;
+	scroll_controller = MakeUnique<ScrollController>();
 }
 }
 
 
 Context::~Context()
 Context::~Context()
@@ -205,8 +183,7 @@ bool Context::Update()
 {
 {
 	RMLUI_ZoneScoped;
 	RMLUI_ZoneScoped;
 
 
-	if (autoscroll_target)
-		UpdateAutoscroll();
+	scroll_controller->Update(mouse_position, density_independent_pixel_ratio);
 
 
 	// Update the hover chain to detect any new or moved elements under the mouse.
 	// Update the hover chain to detect any new or moved elements under the mouse.
 	if (mouse_active)
 	if (mouse_active)
@@ -751,22 +728,20 @@ bool Context::ProcessMouseButtonDown(int button_index, int key_modifier_state)
 			propagate = hover->DispatchEvent(EventId::Mousedown, parameters);
 			propagate = hover->DispatchEvent(EventId::Mousedown, parameters);
 	}
 	}
 
 
-	if (autoscroll_target)
+	if (scroll_controller->GetMode() == ScrollController::Mode::Autoscroll)
 	{
 	{
-		ResetAutoscroll();
+		scroll_controller->Reset();
 	}
 	}
 	else if (button_index == 2 && hover && propagate)
 	else if (button_index == 2 && hover && propagate)
 	{
 	{
 		Dictionary scroll_parameters;
 		Dictionary scroll_parameters;
 		GenerateMouseEventParameters(scroll_parameters);
 		GenerateMouseEventParameters(scroll_parameters);
+		GenerateKeyModifierEventParameters(scroll_parameters, key_modifier_state);
+		scroll_parameters["autoscroll"] = true;
 
 
-		// Dispatch an event without any scrolling distance, just to see if anyone captures it. If so, we can initiate autoscroll here.
-		if (!hover->DispatchEvent(EventId::Mousescroll, scroll_parameters))
-		{
-			autoscroll_target = hover;
-			autoscroll_start_position = mouse_position;
-			autoscroll_previous_update_time = GetSystemInterface()->GetElapsedTime();
-		}
+		// Dispatch a mouse scroll event, this gives elements an opportunity to block autoscroll from being initialized.
+		if (hover->DispatchEvent(EventId::Mousescroll, scroll_parameters))
+			scroll_controller->ActivateAutoscroll(hover->GetClosestScrollableContainer(), mouse_position);
 	}
 	}
 
 
 	return !IsMouseInteracting();
 	return !IsMouseInteracting();
@@ -844,34 +819,43 @@ bool Context::ProcessMouseButtonUp(int button_index, int key_modifier_state)
 			hover->DispatchEvent(EventId::Mouseup, parameters);
 			hover->DispatchEvent(EventId::Mouseup, parameters);
 	}
 	}
 
 
-	if (autoscroll_target && autoscroll_holding)
-		ResetAutoscroll();
+	// If we have autoscrolled while holding the middle mouse button, release the autoscroll mode now.
+	if (scroll_controller->HasAutoscrollMoved())
+		scroll_controller->Reset();
 
 
 	return result;
 	return result;
 }
 }
 
 
-// Sends a mouse-wheel movement event into RmlUi.
 bool Context::ProcessMouseWheel(float wheel_delta, int key_modifier_state)
 bool Context::ProcessMouseWheel(float wheel_delta, int key_modifier_state)
 {
 {
-	if (autoscroll_target)
+	if (scroll_controller->GetMode() == ScrollController::Mode::Autoscroll)
 	{
 	{
-		ResetAutoscroll();
+		scroll_controller->Reset();
 		return false;
 		return false;
 	}
 	}
-	else if (hover)
+	else if (!hover)
 	{
 	{
-		// The scroll length for a single unit of wheel delta is defined as three default sized lines.
-		const float default_scroll_length = 3.f * DefaultComputedValues.line_height().value * density_independent_pixel_ratio;
+		scroll_controller->Reset();
+		return true;
+	}
 
 
-		Dictionary scroll_parameters;
-		GenerateKeyModifierEventParameters(scroll_parameters, key_modifier_state);
-		scroll_parameters["wheel_delta"] = wheel_delta;
-		scroll_parameters["delta_y"] = wheel_delta * default_scroll_length;
+	Dictionary scroll_parameters;
+	GenerateMouseEventParameters(scroll_parameters);
+	GenerateKeyModifierEventParameters(scroll_parameters, key_modifier_state);
+	scroll_parameters["wheel_delta"] = wheel_delta;
 
 
-		return hover->DispatchEvent(EventId::Mousescroll, scroll_parameters);
-	}
+	// Dispatch a mouse scroll event, this gives elements an opportunity to block scrolling from being performed.
+	if (!hover->DispatchEvent(EventId::Mousescroll, scroll_parameters))
+		return false;
 
 
-	return true;
+	if (scroll_controller->GetMode() != ScrollController::Mode::Smoothscroll)
+		scroll_controller->ActivateSmoothscroll(hover->GetClosestScrollableContainer());
+
+	const float unit_scroll_length = UNIT_SCROLL_LENGTH * density_independent_pixel_ratio;
+	const Vector2f scroll_length = {0.f, wheel_delta * unit_scroll_length};
+
+	scroll_controller->IncrementSmoothscrollTarget(scroll_length);
+	return false;
 }
 }
 
 
 bool Context::ProcessMouseLeave()
 bool Context::ProcessMouseLeave()
@@ -886,7 +870,7 @@ bool Context::ProcessMouseLeave()
 
 
 bool Context::IsMouseInteracting() const
 bool Context::IsMouseInteracting() const
 {
 {
-	return (hover && hover != root.get()) || (active && active != root.get()) || autoscroll_target;
+	return (hover && hover != root.get()) || (active && active != root.get()) || scroll_controller->GetMode() == ScrollController::Mode::Autoscroll;
 }
 }
 
 
 // Gets the context's render interface.
 // Gets the context's render interface.
@@ -1027,8 +1011,8 @@ void Context::OnElementDetach(Element* element)
 			document_focus_history.erase(it);
 			document_focus_history.erase(it);
 	}
 	}
 
 
-	if (element == autoscroll_target)
-		ResetAutoscroll();
+	if (scroll_controller->GetTarget() == element)
+		scroll_controller->Reset();
 }
 }
 
 
 // Internal callback for when a new element gains focus
 // Internal callback for when a new element gains focus
@@ -1151,8 +1135,8 @@ void Context::UpdateHoverChain(Vector2i old_mouse_position, int key_modifier_sta
 	{
 	{
 		String new_cursor_name;
 		String new_cursor_name;
 
 
-		if (autoscroll_target)
-			new_cursor_name = GetScrollCursor();
+		if (scroll_controller->GetMode() == ScrollController::Mode::Autoscroll)
+			new_cursor_name = scroll_controller->GetAutoscrollCursor(mouse_position, density_independent_pixel_ratio);
 		else if(drag)
 		else if(drag)
 			new_cursor_name = drag->GetComputedValues().cursor();
 			new_cursor_name = drag->GetComputedValues().cursor();
 		else if (hover)
 		else if (hover)
@@ -1343,6 +1327,12 @@ void Context::ReleaseDragClone()
 	}
 	}
 }
 }
 
 
+void Context::PerformSmoothscrollOnTarget(Element* target, Vector2f delta_offset)
+{
+	scroll_controller->ActivateSmoothscroll(target);
+	scroll_controller->IncrementSmoothscrollTarget(delta_offset);
+}
+
 DataModel* Context::GetDataModelPtr(const String& name) const
 DataModel* Context::GetDataModelPtr(const String& name) const
 {
 {
 	auto it = data_models.find(name);
 	auto it = data_models.find(name);
@@ -1351,71 +1341,6 @@ DataModel* Context::GetDataModelPtr(const String& name) const
 	return nullptr;
 	return nullptr;
 }
 }
 
 
-String Context::GetScrollCursor() const
-{
-	const Vector2f scroll_delta = Vector2f(mouse_position - autoscroll_start_position);
-	const Vector2f scroll_velocity = CalculateAutoscrollVelocity(scroll_delta, density_independent_pixel_ratio);
-
-	if (scroll_velocity == Vector2f(0.f))
-		return "rmlui-scroll-idle";
-
-	String result = "rmlui-scroll";
-
-	if (scroll_velocity.y > 0.f)
-		result += "-up";
-	else if (scroll_velocity.y < 0.f)
-		result += "-down";
-
-	if (scroll_velocity.x > 0.f)
-		result += "-right";
-	else if (scroll_velocity.x < 0.f)
-		result += "-left";
-
-	return result;
-}
-
-void Context::UpdateAutoscroll()
-{
-	RMLUI_ASSERT(autoscroll_target);
-
-	double current_time = GetSystemInterface()->GetElapsedTime();
-	const float dt = float(current_time - autoscroll_previous_update_time);
-	autoscroll_previous_update_time = current_time;
-
-	const Vector2f scroll_delta = Vector2f(mouse_position - autoscroll_start_position);
-	const Vector2f scroll_velocity = CalculateAutoscrollVelocity(scroll_delta, density_independent_pixel_ratio);
-
-	autoscroll_accumulated_length += scroll_velocity * dt;
-
-	// Only submit the integer part of the scroll length, accumulate and store fractional parts to enable sub-pixel-per-frame scrolling speeds.
-	Vector2f scroll_length_integral = autoscroll_accumulated_length;
-	autoscroll_accumulated_length.x = Math::DecomposeFractionalIntegral(autoscroll_accumulated_length.x, &scroll_length_integral.x);
-	autoscroll_accumulated_length.y = Math::DecomposeFractionalIntegral(autoscroll_accumulated_length.y, &scroll_length_integral.y);
-
-	if (scroll_velocity != Vector2f(0.f))
-		autoscroll_holding = true;
-
-	if (scroll_length_integral != Vector2f(0.f))
-	{
-		Dictionary scroll_parameters;
-		GenerateMouseEventParameters(scroll_parameters);
-		scroll_parameters["delta_x"] = scroll_length_integral.x;
-		scroll_parameters["delta_y"] = scroll_length_integral.y;
-
-		if (autoscroll_target->DispatchEvent(EventId::Mousescroll, scroll_parameters))
-			ResetAutoscroll(); // Scroll event was not handled by any element, meaning that we don't have anything to scroll.
-	}
-}
-
-void Context::ResetAutoscroll()
-{
-	autoscroll_target = nullptr;
-	autoscroll_holding = false;
-	autoscroll_start_position = Vector2i{};
-	autoscroll_accumulated_length = Vector2f{};
-	autoscroll_previous_update_time = 0;
-}
-
 // Builds the parameters for a generic key event.
 // Builds the parameters for a generic key event.
 void Context::GenerateKeyEventParameters(Dictionary& parameters, Input::KeyIdentifier key_identifier)
 void Context::GenerateKeyEventParameters(Dictionary& parameters, Input::KeyIdentifier key_identifier)
 {
 {

+ 47 - 37
Source/Core/Element.cpp

@@ -1280,9 +1280,9 @@ bool Element::DispatchEvent(EventId id, const Dictionary& parameters)
 void Element::ScrollIntoView(const ScrollIntoViewOptions options)
 void Element::ScrollIntoView(const ScrollIntoViewOptions options)
 {
 {
 	const Vector2f size = main_box.GetSize(Box::BORDER);
 	const Vector2f size = main_box.GetSize(Box::BORDER);
+	ScrollBehavior scroll_behavior = options.behavior;
 
 
-	Element* scroll_parent = parent;
-	while (scroll_parent != nullptr)
+	for (Element* scroll_parent = parent; scroll_parent; scroll_parent = scroll_parent->GetParentNode())
 	{
 	{
 		using Style::Overflow;
 		using Style::Overflow;
 		const ComputedValues& computed = scroll_parent->GetComputedValues();
 		const ComputedValues& computed = scroll_parent->GetComputedValues();
@@ -1302,17 +1302,16 @@ void Element::ScrollIntoView(const ScrollIntoViewOptions options)
 			const Vector2f delta_scroll_offset_start = parent_client_offset - relative_offset;
 			const Vector2f delta_scroll_offset_start = parent_client_offset - relative_offset;
 			const Vector2f delta_scroll_offset_end = delta_scroll_offset_start + size - parent_client_size;
 			const Vector2f delta_scroll_offset_end = delta_scroll_offset_start + size - parent_client_size;
 
 
-			Vector2f new_scroll_offset = old_scroll_offset;
-			new_scroll_offset.x += GetScrollOffsetDelta(options.horizontal, delta_scroll_offset_start.x, delta_scroll_offset_end.x);
-			new_scroll_offset.y += GetScrollOffsetDelta(options.vertical, delta_scroll_offset_start.y, delta_scroll_offset_end.y);
+			Vector2f scroll_delta = {
+				scrollable_box_x ? GetScrollOffsetDelta(options.horizontal, delta_scroll_offset_start.x, delta_scroll_offset_end.x) : 0.f,
+				scrollable_box_y ? GetScrollOffsetDelta(options.vertical, delta_scroll_offset_start.y, delta_scroll_offset_end.y) : 0.f,
+			};
 
 
-			if (scrollable_box_x)
-				scroll_parent->SetScrollLeft(new_scroll_offset.x);
-			if (scrollable_box_y)
-				scroll_parent->SetScrollTop(new_scroll_offset.y);
-		}
+			scroll_parent->ScrollTo(old_scroll_offset + scroll_delta, scroll_behavior);
 
 
-		scroll_parent = scroll_parent->GetParentNode();
+			// Currently, only a single scrollable parent can be smooth scrolled at a time, so any other parents must be instant scrolled.
+			scroll_behavior = ScrollBehavior::Instant;
+		}
 	}
 	}
 }
 }
 
 
@@ -1324,6 +1323,21 @@ void Element::ScrollIntoView(bool align_with_top)
 	ScrollIntoView(options);
 	ScrollIntoView(options);
 }
 }
 
 
+void Element::ScrollTo(Vector2f offset, ScrollBehavior behavior)
+{
+	if (behavior == ScrollBehavior::Smooth)
+	{
+		if (Context* context = GetContext())
+		{
+			context->PerformSmoothscrollOnTarget(this, offset - scroll_offset);
+			return;
+		}
+	}
+
+	SetScrollLeft(offset.x);
+	SetScrollTop(offset.y);
+}
+
 // Appends a child to this element
 // Appends a child to this element
 Element* Element::AppendChild(ElementPtr child, bool dom_element)
 Element* Element::AppendChild(ElementPtr child, bool dom_element)
 {
 {
@@ -1969,6 +1983,28 @@ bool Element::IsLayoutDirty()
 	return false;
 	return false;
 }
 }
 
 
+Element* Element::GetClosestScrollableContainer()
+{
+	using namespace Style;
+
+	Overflow overflow_x = meta->computed_values.overflow_x();
+	Overflow overflow_y = meta->computed_values.overflow_y();
+	bool scrollable_x = (overflow_x == Overflow::Auto || overflow_x == Overflow::Scroll);
+	bool scrollable_y = (overflow_y == Overflow::Auto || overflow_y == Overflow::Scroll);
+
+	scrollable_x = (scrollable_x && GetScrollWidth() > GetClientWidth());
+	scrollable_y = (scrollable_y && GetScrollHeight() > GetClientHeight());
+
+	if (scrollable_x || scrollable_y)
+		return this;
+	else if (meta->computed_values.overscroll_behavior() == OverscrollBehavior::Contain)
+		return nullptr;
+	else if (parent)
+		return parent->GetClosestScrollableContainer();
+
+	return nullptr;
+}
+
 void Element::ProcessDefaultAction(Event& event)
 void Element::ProcessDefaultAction(Event& event)
 {
 {
 	if (event == EventId::Mousedown)
 	if (event == EventId::Mousedown)
@@ -1979,32 +2015,6 @@ void Element::ProcessDefaultAction(Event& event)
 			SetPseudoClass("active", true);
 			SetPseudoClass("active", true);
 	}
 	}
 
 
-	if (event == EventId::Mousescroll)
-	{
-		Style::Overflow overflow_x = meta->computed_values.overflow_x();
-		Style::Overflow overflow_y = meta->computed_values.overflow_y();
-		bool scrollable_x = (overflow_x == Style::Overflow::Auto || overflow_x == Style::Overflow::Scroll);
-		bool scrollable_y = (overflow_y == Style::Overflow::Auto || overflow_y == Style::Overflow::Scroll);
-
-		scrollable_x = (scrollable_x && GetScrollWidth() > GetClientWidth());
-		scrollable_y = (scrollable_y && GetScrollHeight() > GetClientHeight());
-
-		if (scrollable_x || scrollable_y)
-		{
-			// Stop the propagation to prevent scrolling in parent elements.
-			event.StopPropagation();
-
-			const Vector2f scroll_delta = {event.GetParameter("delta_x", 0.f), event.GetParameter("delta_y", 0.f)};
-
-			if (scrollable_x)
-				SetScrollLeft(GetScrollLeft() + scroll_delta.x);
-			if (scrollable_y)
-				SetScrollTop(GetScrollTop() + scroll_delta.y);
-		}
-
-		return;
-	}
-
 	if (event.GetPhase() == EventPhase::Target)
 	if (event.GetPhase() == EventPhase::Target)
 	{
 	{
 		switch (event.GetId())
 		switch (event.GetId())

+ 0 - 1
Source/Core/ElementScroll.cpp

@@ -179,7 +179,6 @@ void ElementScroll::FormatScrollbars()
 		slider_length -= Math::Max(user_scrollbar_margin, min_scrollbar_margin);
 		slider_length -= Math::Max(user_scrollbar_margin, min_scrollbar_margin);
 
 
 		scrollbars[i].widget->FormatElements(containing_block, slider_length);
 		scrollbars[i].widget->FormatElements(containing_block, slider_length);
-		scrollbars[i].widget->SetLineHeight(element->GetLineHeight());
 
 
 		int variable_axis = i == VERTICAL ? 0 : 1;
 		int variable_axis = i == VERTICAL ? 0 : 1;
 		Vector2f offset = element_box.GetPosition(Box::PADDING);
 		Vector2f offset = element_box.GetPosition(Box::PADDING);

+ 3 - 0
Source/Core/ElementStyle.cpp

@@ -820,6 +820,9 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 		case PropertyId::ScrollbarMargin:
 		case PropertyId::ScrollbarMargin:
 			values.scrollbar_margin(ComputeLength(p, font_size, document_font_size, dp_ratio, vp_dimensions));
 			values.scrollbar_margin(ComputeLength(p, font_size, document_font_size, dp_ratio, vp_dimensions));
 			break;
 			break;
+		case PropertyId::OverscrollBehavior:
+			values.overscroll_behavior((OverscrollBehavior)p->Get<int>());
+			break;
 		case PropertyId::PointerEvents:
 		case PropertyId::PointerEvents:
 			values.pointer_events((PointerEvents)p->Get<int>());
 			values.pointer_events((PointerEvents)p->Get<int>());
 			break;
 			break;

+ 3 - 16
Source/Core/Elements/WidgetDropDown.cpp

@@ -65,12 +65,13 @@ WidgetDropDown::WidgetDropDown(ElementFormControl* element)
 	selection_element->SetProperty(PropertyId::Clip, Property(Style::Clip::Type::None));
 	selection_element->SetProperty(PropertyId::Clip, Property(Style::Clip::Type::None));
 	selection_element->SetProperty(PropertyId::OverflowY, Property(Style::Overflow::Auto));
 	selection_element->SetProperty(PropertyId::OverflowY, Property(Style::Overflow::Auto));
 
 
+	// Prevent scrolling in the parent document when the mouse is inside the selection box.
+	selection_element->SetProperty(PropertyId::OverscrollBehavior, Property(Style::OverscrollBehavior::Contain));
+
 	parent_element->AddEventListener(EventId::Click, this, true);
 	parent_element->AddEventListener(EventId::Click, this, true);
 	parent_element->AddEventListener(EventId::Blur, this);
 	parent_element->AddEventListener(EventId::Blur, this);
 	parent_element->AddEventListener(EventId::Focus, this);
 	parent_element->AddEventListener(EventId::Focus, this);
 	parent_element->AddEventListener(EventId::Keydown, this, true);
 	parent_element->AddEventListener(EventId::Keydown, this, true);
-
-	selection_element->AddEventListener(EventId::Mousescroll, this);
 }
 }
 
 
 WidgetDropDown::~WidgetDropDown()
 WidgetDropDown::~WidgetDropDown()
@@ -85,8 +86,6 @@ WidgetDropDown::~WidgetDropDown()
 	parent_element->RemoveEventListener(EventId::Blur, this);
 	parent_element->RemoveEventListener(EventId::Blur, this);
 	parent_element->RemoveEventListener(EventId::Focus, this);
 	parent_element->RemoveEventListener(EventId::Focus, this);
 	parent_element->RemoveEventListener(EventId::Keydown, this, true);
 	parent_element->RemoveEventListener(EventId::Keydown, this, true);
-	
-	selection_element->RemoveEventListener(EventId::Mousescroll, this);
 
 
 	DetachScrollEvent();
 	DetachScrollEvent();
 }
 }
@@ -571,18 +570,6 @@ void WidgetDropDown::ProcessEvent(Event& event)
 		}
 		}
 	}
 	}
 	break;
 	break;
-	case EventId::Mousescroll:
-	{
-		if (event.GetCurrentElement() == selection_element)
-		{
-			// Prevent scrolling in the parent window when mouse is inside the selection box.
-			event.StopPropagation();
-			// Stopping propagation also stops all default scrolling actions. However, we still want to be able
-			// to scroll in the selection box, so call the default action manually.
-			selection_element->ProcessDefaultAction(event);
-		}
-	}
-	break;
 	case EventId::Scroll:
 	case EventId::Scroll:
 	{
 	{
 		if (box_visible)
 		if (box_visible)

+ 1 - 1
Source/Core/EventSpecification.cpp

@@ -48,7 +48,7 @@ void Initialize()
 		//      id                 type      interruptible  bubbles     default_action
 		//      id                 type      interruptible  bubbles     default_action
 		{EventId::Invalid       , "invalid"       , false , false , DefaultActionPhase::None},
 		{EventId::Invalid       , "invalid"       , false , false , DefaultActionPhase::None},
 		{EventId::Mousedown     , "mousedown"     , true  , true  , DefaultActionPhase::TargetAndBubble},
 		{EventId::Mousedown     , "mousedown"     , true  , true  , DefaultActionPhase::TargetAndBubble},
-		{EventId::Mousescroll   , "mousescroll"   , true  , true  , DefaultActionPhase::TargetAndBubble},
+		{EventId::Mousescroll   , "mousescroll"   , true  , true  , DefaultActionPhase::None},
 		{EventId::Mouseover     , "mouseover"     , true  , true  , DefaultActionPhase::Target},
 		{EventId::Mouseover     , "mouseover"     , true  , true  , DefaultActionPhase::Target},
 		{EventId::Mouseout      , "mouseout"      , true  , true  , DefaultActionPhase::Target},
 		{EventId::Mouseout      , "mouseout"      , true  , true  , DefaultActionPhase::Target},
 		{EventId::Focus         , "focus"         , false , false , DefaultActionPhase::Target},
 		{EventId::Focus         , "focus"         , false , false , DefaultActionPhase::Target},

+ 5 - 0
Source/Core/Math.cpp

@@ -64,6 +64,11 @@ RMLUICORE_API int AbsoluteValue(int value)
 	return abs(value);
 	return abs(value);
 }
 }
 
 
+RMLUICORE_API Vector2f AbsoluteValue(Vector2f value)
+{
+	return {fabsf(value.x), fabsf(value.y)};
+}
+
 // Calculates the cosine of an angle.
 // Calculates the cosine of an angle.
 RMLUICORE_API float Cos(float angle)
 RMLUICORE_API float Cos(float angle)
 {
 {

+ 239 - 0
Source/Core/ScrollController.cpp

@@ -0,0 +1,239 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "ScrollController.h"
+#include "../../Include/RmlUi/Core/Core.h"
+#include "../../Include/RmlUi/Core/Element.h"
+#include "../../Include/RmlUi/Core/SystemInterface.h"
+
+namespace Rml {
+
+static constexpr float AUTOSCROLL_SPEED_FACTOR = 0.09f;
+static constexpr float AUTOSCROLL_DEADZONE = 10.0f; // [dp]
+
+static constexpr float SMOOTHSCROLL_WINDOW_SIZE = 50.f;        // The window where smoothing is applied, as a distance from scroll start and end. [dp]
+static constexpr float SMOOTHSCROLL_VELOCITY_CONSTANT = 800.f; // The constant velocity, any smoothing is applied on top of this. [dp/s]
+static constexpr float SMOOTHSCROLL_VELOCITY_SQUARE_FACTOR = 0.05f;
+
+// 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_HIGH = 1.f / 15.f; // [s]
+
+// Determines the autoscroll velocity based on the distance from the scroll-start mouse position. [px/s]
+static Vector2f CalculateAutoscrollVelocity(Vector2f target_delta, float dp_ratio)
+{
+	target_delta = target_delta / dp_ratio;
+	target_delta = {
+		Math::AbsoluteValue(target_delta.x) < AUTOSCROLL_DEADZONE ? 0.f : target_delta.x,
+		Math::AbsoluteValue(target_delta.y) < AUTOSCROLL_DEADZONE ? 0.f : target_delta.y,
+	};
+
+	// We use a signed square model for the velocity, which seems to work quite well. This is mostly about feeling and tuning.
+	return AUTOSCROLL_SPEED_FACTOR * target_delta * Math::AbsoluteValue(target_delta);
+}
+
+// Determines the smoothscroll velocity based on the distance to the target, and the distance scrolled so far. [px/s]
+static Vector2f CalculateSmoothscrollVelocity(Vector2f target_delta, Vector2f scrolled_distance, float dp_ratio)
+{
+	scrolled_distance = Math::AbsoluteValue(scrolled_distance) / dp_ratio;
+	target_delta = target_delta / dp_ratio;
+
+	const Vector2f target_delta_abs = Math::AbsoluteValue(target_delta);
+	Vector2f target_delta_signum = {
+		target_delta.x > 0.f ? 1.f : (target_delta.x < 0.f ? -1.f : 0.f),
+		target_delta.y > 0.f ? 1.f : (target_delta.y < 0.f ? -1.f : 0.f),
+	};
+
+	// The window provides velocity smoothing near the start and end of the scroll.
+	const Tween tween(Tween::Exponential, Tween::Out);
+	const Vector2f alpha_in = Math::Min(scrolled_distance / SMOOTHSCROLL_WINDOW_SIZE, Vector2f(1.f));
+	const Vector2f alpha_out = Math::Min(target_delta_abs / SMOOTHSCROLL_WINDOW_SIZE, Vector2f(1.f));
+	const Vector2f smooth_window = {
+		tween(alpha_in.x) * tween(alpha_out.x),
+		tween(alpha_in.y) * tween(alpha_out.y),
+	};
+
+	const Vector2f velocity_constant = Vector2f(SMOOTHSCROLL_VELOCITY_CONSTANT);
+	const Vector2f velocity_square = SMOOTHSCROLL_VELOCITY_SQUARE_FACTOR * target_delta_abs * target_delta_abs;
+
+	// Short scrolls are dominated by the smoothed constant velocity, while the square term is added for quick longer scrolls.
+	return dp_ratio * target_delta_signum * (smooth_window * velocity_constant + velocity_square);
+}
+
+void ScrollController::ActivateAutoscroll(Element* in_target, Vector2i start_position)
+{
+	Reset();
+	if (!in_target)
+		return;
+	target = in_target;
+	mode = Mode::Autoscroll;
+	autoscroll_start_position = start_position;
+	UpdateTime();
+}
+
+void ScrollController::ActivateSmoothscroll(Element* in_target)
+{
+	Reset();
+	if (!in_target)
+		return;
+	target = in_target;
+	mode = Mode::Smoothscroll;
+	UpdateTime();
+}
+
+void ScrollController::Update(Vector2i mouse_position, float dp_ratio)
+{
+	if (mode == Mode::Autoscroll)
+		UpdateAutoscroll(mouse_position, dp_ratio);
+	else if (mode == Mode::Smoothscroll)
+		UpdateSmoothscroll(dp_ratio);
+}
+
+void ScrollController::UpdateAutoscroll(Vector2i mouse_position, float dp_ratio)
+{
+	RMLUI_ASSERT(mode == Mode::Autoscroll && target);
+
+	const float dt = UpdateTime();
+
+	const Vector2f scroll_delta = Vector2f(mouse_position - autoscroll_start_position);
+	const Vector2f scroll_velocity = CalculateAutoscrollVelocity(scroll_delta, dp_ratio);
+
+	autoscroll_accumulated_length += scroll_velocity * dt;
+
+	// Only submit the integer part of the scroll length, accumulate and store fractional parts to enable sub-pixel-per-frame scrolling speeds.
+	Vector2f scroll_length_integral = autoscroll_accumulated_length;
+	autoscroll_accumulated_length.x = Math::DecomposeFractionalIntegral(autoscroll_accumulated_length.x, &scroll_length_integral.x);
+	autoscroll_accumulated_length.y = Math::DecomposeFractionalIntegral(autoscroll_accumulated_length.y, &scroll_length_integral.y);
+
+	if (scroll_velocity != Vector2f(0.f))
+		autoscroll_moved = true;
+
+	PerformScrollOnTarget(scroll_length_integral);
+}
+
+void ScrollController::UpdateSmoothscroll(float dp_ratio)
+{
+	RMLUI_ASSERT(mode == Mode::Smoothscroll && target);
+
+	const Vector2f target_delta = Vector2f(smoothscroll_target_distance - smoothscroll_scrolled_distance);
+	const Vector2f velocity = CalculateSmoothscrollVelocity(target_delta, smoothscroll_scrolled_distance, dp_ratio);
+
+	const float dt = UpdateTime();
+	Vector2f scroll_distance = (velocity * dt).Round();
+
+	for (int i = 0; i < 2; i++)
+	{
+		// Ensure minimum scroll speed of 1px/frame, and clamp the distance to the target in case of overshooting
+		// integration. As opposed to autoscroll, we don't care about fractional speeds here since we want to be fast.
+		if (target_delta[i] > 0.f)
+			scroll_distance[i] = Math::Min(Math::Max(scroll_distance[i], 1.f), target_delta[i]);
+		else if (target_delta[i] < 0.f)
+			scroll_distance[i] = Math::Max(Math::Min(scroll_distance[i], -1.f), target_delta[i]);
+		else
+			scroll_distance[i] = 0.f;
+	}
+
+	smoothscroll_scrolled_distance += scroll_distance;
+	PerformScrollOnTarget(scroll_distance);
+
+	if (scroll_distance == target_delta)
+		Reset();
+}
+
+void ScrollController::PerformScrollOnTarget(Vector2f delta_distance)
+{
+	RMLUI_ASSERT(target);
+	if (delta_distance.x != 0.f)
+		target->SetScrollLeft(target->GetScrollLeft() + delta_distance.x);
+	if (delta_distance.y != 0.f)
+		target->SetScrollTop(target->GetScrollTop() + delta_distance.y);
+}
+
+void ScrollController::IncrementSmoothscrollTarget(Vector2f delta_distance)
+{
+	auto OppositeDirection = [](float a, float b) { return (a < 0.f && b > 0.f) || (a > 0.f && b < 0.f); };
+	Vector2f delta = smoothscroll_target_distance - smoothscroll_scrolled_distance;
+
+	// Reset movement state if we start scrolling in the opposite direction.
+	for (int i = 0; i < 2; i++)
+	{
+		if (OppositeDirection(delta_distance[i], delta[i]))
+		{
+			smoothscroll_target_distance[i] = 0.f;
+			smoothscroll_scrolled_distance[i] = 0.f;
+		}
+	}
+
+	smoothscroll_target_distance += delta_distance;
+}
+
+void ScrollController::Reset()
+{
+	*this = ScrollController{};
+}
+
+String ScrollController::GetAutoscrollCursor(Vector2i mouse_position, float dp_ratio) const
+{
+	RMLUI_ASSERT(mode == Mode::Autoscroll);
+
+	const Vector2f scroll_delta = Vector2f(mouse_position - autoscroll_start_position);
+	const Vector2f scroll_velocity = CalculateAutoscrollVelocity(scroll_delta, dp_ratio);
+
+	if (scroll_velocity == Vector2f(0.f))
+		return "rmlui-scroll-idle";
+
+	String result = "rmlui-scroll";
+
+	if (scroll_velocity.y > 0.f)
+		result += "-up";
+	else if (scroll_velocity.y < 0.f)
+		result += "-down";
+
+	if (scroll_velocity.x > 0.f)
+		result += "-right";
+	else if (scroll_velocity.x < 0.f)
+		result += "-left";
+
+	return result;
+}
+
+bool ScrollController::HasAutoscrollMoved() const
+{
+	return mode == Mode::Autoscroll && autoscroll_moved;
+}
+
+float ScrollController::UpdateTime()
+{
+	const double previous_tick = previous_update_time;
+	previous_update_time = GetSystemInterface()->GetElapsedTime();
+
+	const float dt = float(previous_update_time - previous_tick);
+	return Math::Clamp(dt, DELTA_TIME_CLAMP_LOW, DELTA_TIME_CLAMP_HIGH);
+}
+
+} // namespace Rml

+ 93 - 0
Source/Core/ScrollController.h

@@ -0,0 +1,93 @@
+/*
+ * This source file is part of RmlUi, the HTML/CSS Interface Middleware
+ *
+ * For the latest information, see http://github.com/mikke89/RmlUi
+ *
+ * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
+ * Copyright (c) 2019 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#ifndef RMLUI_CORE_SCROLLCONTROLLER_H
+#define RMLUI_CORE_SCROLLCONTROLLER_H
+
+#include "../../Include/RmlUi/Core/Header.h"
+#include "../../Include/RmlUi/Core/Types.h"
+
+namespace Rml {
+
+/**
+    Implements scrolling behavior that occurs over time.
+
+    Scrolling modes are activated externally, targeting a given element. The actual scrolling takes place during update calls.
+ */
+
+class ScrollController {
+public:
+	enum class Mode {
+		None,
+		Smoothscroll, // Smooth scrolling to target distance.
+		Autoscroll,   // Scrolling with middle mouse button.
+	};
+
+	void ActivateAutoscroll(Element* target, Vector2i start_position);
+
+	void ActivateSmoothscroll(Element* target);
+
+	void Update(Vector2i mouse_position, float dp_ratio);
+
+	void IncrementSmoothscrollTarget(Vector2f delta_distance);
+
+	void Reset();
+
+	// Returns the autoscroll cursor based on the active scroll velocity.
+	String GetAutoscrollCursor(Vector2i mouse_position, float dp_ratio) const;
+	// Returns true if autoscroll mode is active and the cursor has been moved outside the idle scroll area.
+	bool HasAutoscrollMoved() const;
+
+	Mode GetMode() const { return mode; }
+	Element* GetTarget() const { return target; }
+
+private:
+	// Updates time to now, and returns the delta time since the previous time update.
+	float UpdateTime();
+
+	void UpdateAutoscroll(Vector2i mouse_position, float dp_ratio);
+
+	void UpdateSmoothscroll(float dp_ratio);
+
+	void PerformScrollOnTarget(Vector2f delta_distance);
+
+	Mode mode = Mode::None;
+
+	Element* target = nullptr;
+	double previous_update_time = 0;
+
+	Vector2i autoscroll_start_position;
+	Vector2f autoscroll_accumulated_length;
+	bool autoscroll_moved = false;
+
+	Vector2f smoothscroll_target_distance;
+	Vector2f smoothscroll_scrolled_distance;
+};
+
+} // namespace Rml
+#endif

+ 2 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -395,7 +395,9 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	RegisterProperty(PropertyId::Drag, "drag", "none", false, false).AddParser("keyword", "none, drag, drag-drop, block, clone");
 	RegisterProperty(PropertyId::Drag, "drag", "none", false, false).AddParser("keyword", "none, drag, drag-drop, block, clone");
 	RegisterProperty(PropertyId::TabIndex, "tab-index", "none", false, false).AddParser("keyword", "none, auto");
 	RegisterProperty(PropertyId::TabIndex, "tab-index", "none", false, false).AddParser("keyword", "none, auto");
 	RegisterProperty(PropertyId::Focus, "focus", "auto", true, false).AddParser("keyword", "none, auto");
 	RegisterProperty(PropertyId::Focus, "focus", "auto", true, false).AddParser("keyword", "none, auto");
+
 	RegisterProperty(PropertyId::ScrollbarMargin, "scrollbar-margin", "0", false, false).AddParser("length");
 	RegisterProperty(PropertyId::ScrollbarMargin, "scrollbar-margin", "0", false, false).AddParser("length");
+	RegisterProperty(PropertyId::OverscrollBehavior, "overscroll-behavior", "auto", false, false).AddParser("keyword", "auto, contain");
 	RegisterProperty(PropertyId::PointerEvents, "pointer-events", "auto", true, false).AddParser("keyword", "none, auto");
 	RegisterProperty(PropertyId::PointerEvents, "pointer-events", "auto", true, false).AddParser("keyword", "none, auto");
 
 
 	// Perspective and Transform specifications
 	// Perspective and Transform specifications

+ 64 - 84
Source/Core/WidgetScroll.cpp

@@ -29,6 +29,7 @@
 #include "WidgetScroll.h"
 #include "WidgetScroll.h"
 #include "../../Include/RmlUi/Core/ComputedValues.h"
 #include "../../Include/RmlUi/Core/ComputedValues.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/Element.h"
+#include "../../Include/RmlUi/Core/ElementUtilities.h"
 #include "../../Include/RmlUi/Core/Event.h"
 #include "../../Include/RmlUi/Core/Event.h"
 #include "../../Include/RmlUi/Core/Factory.h"
 #include "../../Include/RmlUi/Core/Factory.h"
 #include "../../Include/RmlUi/Core/Property.h"
 #include "../../Include/RmlUi/Core/Property.h"
@@ -37,8 +38,11 @@
 
 
 namespace Rml {
 namespace Rml {
 
 
-static const float DEFAULT_REPEAT_DELAY = 0.5f;
-static const float DEFAULT_REPEAT_PERIOD = 0.1f;
+static constexpr float DEFAULT_REPEAT_DELAY = 0.5f;
+static constexpr float DEFAULT_REPEAT_PERIOD = 0.1f;
+
+static constexpr float SCROLL_LINE_LENGTH = 30.f; // [dp]
+static constexpr float SCROLL_PAGE_FACTOR = 0.8f;
 
 
 WidgetScroll::WidgetScroll(Element* _parent)
 WidgetScroll::WidgetScroll(Element* _parent)
 {
 {
@@ -60,7 +64,6 @@ WidgetScroll::WidgetScroll(Element* _parent)
 
 
 	track_length = 0;
 	track_length = 0;
 	bar_length = 0;
 	bar_length = 0;
-	line_height = 12;
 }
 }
 
 
 WidgetScroll::~WidgetScroll()
 WidgetScroll::~WidgetScroll()
@@ -161,7 +164,10 @@ void WidgetScroll::Update()
 			while (arrow_timers[i] <= 0)
 			while (arrow_timers[i] <= 0)
 			{
 			{
 				arrow_timers[i] += DEFAULT_REPEAT_PERIOD;
 				arrow_timers[i] += DEFAULT_REPEAT_PERIOD;
-				SetBarPosition(i == 0 ? OnLineDecrement() : OnLineIncrement());
+				if (i == 0)
+					ScrollLineUp();
+				else
+					ScrollLineDown();
 			}
 			}
 		}
 		}
 	}
 	}
@@ -172,19 +178,6 @@ void WidgetScroll::SetBarPosition(float _bar_position)
 {
 {
 	bar_position = Math::Clamp(_bar_position, 0.0f, 1.0f);
 	bar_position = Math::Clamp(_bar_position, 0.0f, 1.0f);
 	PositionBar();
 	PositionBar();
-	
-	// 'parent' is the scrollbar element, its parent again is the actual element we want to scroll
-	Element* element_scroll = parent->GetParentNode();
-	if (!element_scroll)
-	{
-		RMLUI_ERROR;
-		return;
-	}
-	
-	if (orientation == VERTICAL)
-		element_scroll->SetScrollTop(bar_position * (element_scroll->GetScrollHeight() - element_scroll->GetClientHeight()));
-	else if (orientation == HORIZONTAL)
-		element_scroll->SetScrollLeft(bar_position * (element_scroll->GetScrollWidth() - element_scroll->GetClientWidth()));
 }
 }
 
 
 // Returns the current position of the bar.
 // Returns the current position of the bar.
@@ -372,57 +365,57 @@ void WidgetScroll::ProcessEvent(Event& event)
 	{
 	{
 		if (event == EventId::Drag)
 		if (event == EventId::Drag)
 		{
 		{
+			float new_bar_position = 0.f;
 			if (orientation == HORIZONTAL)
 			if (orientation == HORIZONTAL)
 			{
 			{
-				float traversable_track_length = track->GetBox().GetSize(Box::CONTENT).x - bar->GetBox().GetSize(Box::CONTENT).x;
+				float traversable_track_length = track->GetBox().GetSize().x - bar->GetBox().GetSize().x;
 				if (traversable_track_length > 0)
 				if (traversable_track_length > 0)
 				{
 				{
 					float traversable_track_origin = track->GetAbsoluteOffset().x + bar_drag_anchor;
 					float traversable_track_origin = track->GetAbsoluteOffset().x + bar_drag_anchor;
-					float new_bar_position = (event.GetParameter< float >("mouse_x", 0) - traversable_track_origin) / traversable_track_length;
-					new_bar_position = Math::Clamp(new_bar_position, 0.0f, 1.0f);
-
-					SetBarPosition(new_bar_position);
+					new_bar_position = (event.GetParameter("mouse_x", 0.f) - traversable_track_origin) / traversable_track_length;
 				}
 				}
 			}
 			}
 			else
 			else
 			{
 			{
-				float traversable_track_length = track->GetBox().GetSize(Box::CONTENT).y - bar->GetBox().GetSize(Box::CONTENT).y;
+				float traversable_track_length = track->GetBox().GetSize().y - bar->GetBox().GetSize().y;
 				if (traversable_track_length > 0)
 				if (traversable_track_length > 0)
 				{
 				{
 					float traversable_track_origin = track->GetAbsoluteOffset().y + bar_drag_anchor;
 					float traversable_track_origin = track->GetAbsoluteOffset().y + bar_drag_anchor;
-					float new_bar_position = (event.GetParameter< float >("mouse_y", 0) - traversable_track_origin) / traversable_track_length;
-					new_bar_position = Math::Clamp(new_bar_position, 0.0f, 1.0f);
-
-					SetBarPosition(new_bar_position);
+					new_bar_position = (event.GetParameter("mouse_y", 0.f) - traversable_track_origin) / traversable_track_length;
 				}
 				}
 			}
 			}
+
+			SetBarPosition(new_bar_position);
+			Scroll(0.f, ScrollBehavior::Instant);
 		}
 		}
 		else if (event == EventId::Dragstart)
 		else if (event == EventId::Dragstart)
 		{
 		{
 			if (orientation == HORIZONTAL)
 			if (orientation == HORIZONTAL)
-				bar_drag_anchor = event.GetParameter< int >("mouse_x", 0) - Math::RealToInteger(bar->GetAbsoluteOffset().x);
+				bar_drag_anchor = event.GetParameter("mouse_x", 0.f) - bar->GetAbsoluteOffset().x;
 			else
 			else
-				bar_drag_anchor = event.GetParameter< int >("mouse_y", 0) - Math::RealToInteger(bar->GetAbsoluteOffset().y);
+				bar_drag_anchor = event.GetParameter("mouse_y", 0.f) - bar->GetAbsoluteOffset().y;
 		}
 		}
 	}
 	}
 	else if (event.GetTargetElement() == track)
 	else if (event.GetTargetElement() == track)
 	{
 	{
 		if (event == EventId::Click)
 		if (event == EventId::Click)
 		{
 		{
+			float click_position = 0.f;
 			if (orientation == HORIZONTAL)
 			if (orientation == HORIZONTAL)
 			{
 			{
-				float mouse_position = event.GetParameter< float >("mouse_x", 0);
-				float click_position = (mouse_position - track->GetAbsoluteOffset().x) / track->GetBox().GetSize().x;
-
-				SetBarPosition(click_position <= bar_position ? OnPageDecrement() : OnPageIncrement());
+				float mouse_position = event.GetParameter("mouse_x", 0.f);
+				click_position = (mouse_position - track->GetAbsoluteOffset().x) / track->GetBox().GetSize().x;
 			}
 			}
 			else
 			else
 			{
 			{
-				float mouse_position = event.GetParameter< float >("mouse_y", 0);
-				float click_position = (mouse_position - track->GetAbsoluteOffset().y) / track->GetBox().GetSize().y;
-
-				SetBarPosition(click_position <= bar_position ? OnPageDecrement() : OnPageIncrement());
+				float mouse_position = event.GetParameter<float>("mouse_y", 0);
+				click_position = (mouse_position - track->GetAbsoluteOffset().y) / track->GetBox().GetSize().y;
 			}
 			}
+
+			if (click_position <= bar_position)
+				ScrollPageUp();
+			else
+				ScrollPageDown();
 		}
 		}
 	}
 	}
 
 
@@ -432,13 +425,13 @@ void WidgetScroll::ProcessEvent(Event& event)
 		{
 		{
 			arrow_timers[0] = DEFAULT_REPEAT_DELAY;
 			arrow_timers[0] = DEFAULT_REPEAT_DELAY;
 			last_update_time = Clock::GetElapsedTime();
 			last_update_time = Clock::GetElapsedTime();
-			SetBarPosition(OnLineDecrement());
+			ScrollLineUp();
 		}
 		}
 		else if (event.GetTargetElement() == arrows[1])
 		else if (event.GetTargetElement() == arrows[1])
 		{
 		{
 			arrow_timers[1] = DEFAULT_REPEAT_DELAY;
 			arrow_timers[1] = DEFAULT_REPEAT_DELAY;
 			last_update_time = Clock::GetElapsedTime();
 			last_update_time = Clock::GetElapsedTime();
-			SetBarPosition(OnLineIncrement());
+			ScrollLineDown();
 		}
 		}
 	}
 	}
 	else if (event == EventId::Mouseup ||
 	else if (event == EventId::Mouseup ||
@@ -468,36 +461,16 @@ void WidgetScroll::PositionBar()
 	}
 	}
 }
 }
 
 
-
-
 // Sets the length of the entire track in some arbitrary unit.
 // Sets the length of the entire track in some arbitrary unit.
-void WidgetScroll::SetTrackLength(float _track_length, bool RMLUI_UNUSED_PARAMETER(force_resize))
+void WidgetScroll::SetTrackLength(float _track_length)
 {
 {
-	RMLUI_UNUSED(force_resize);
-
-	if (track_length != _track_length)
-	{
-		track_length = _track_length;
-		//		GenerateBar();
-	}
+	track_length = _track_length;
 }
 }
 
 
 // Sets the length the bar represents in some arbitrary unit, relative to the track length.
 // Sets the length the bar represents in some arbitrary unit, relative to the track length.
-void WidgetScroll::SetBarLength(float _bar_length, bool RMLUI_UNUSED_PARAMETER(force_resize))
-{
-	RMLUI_UNUSED(force_resize);
-
-	if (bar_length != _bar_length)
-	{
-		bar_length = _bar_length;
-		//		GenerateBar();
-	}
-}
-
-// Sets the line height of the parent element; this is used for scrolling speeds.
-void WidgetScroll::SetLineHeight(float _line_height)
+void WidgetScroll::SetBarLength(float _bar_length)
 {
 {
-	line_height = _line_height;
+	bar_length = _bar_length;
 }
 }
 
 
 // Lays out and resizes the internal elements.
 // Lays out and resizes the internal elements.
@@ -515,42 +488,49 @@ void WidgetScroll::FormatElements(const Vector2f containing_block, float slider_
 	WidgetScroll::FormatElements(containing_block, true, slider_length, relative_bar_length);
 	WidgetScroll::FormatElements(containing_block, true, slider_length, relative_bar_length);
 }
 }
 
 
-// Called when the slider is incremented by one 'line', either by the down / right key or a mouse-click on the
-// increment arrow.
-float WidgetScroll::OnLineIncrement()
+void WidgetScroll::ScrollLineDown()
 {
 {
-	return Scroll(line_height);
+	Scroll(SCROLL_LINE_LENGTH * ElementUtilities::GetDensityIndependentPixelRatio(parent), ScrollBehavior::Smooth);
 }
 }
 
 
-// Called when the slider is decremented by one 'line', either by the up / left key or a mouse-click on the decrement
-// arrow.
-float WidgetScroll::OnLineDecrement()
+void WidgetScroll::ScrollLineUp()
 {
 {
-	return Scroll(-line_height);
+	Scroll(-SCROLL_LINE_LENGTH * ElementUtilities::GetDensityIndependentPixelRatio(parent), ScrollBehavior::Smooth);
 }
 }
 
 
-// Called when the slider is incremented by one 'page', either by the page-up key or a mouse-click on the track
-// below / right of the bar.
-float WidgetScroll::OnPageIncrement()
+void WidgetScroll::ScrollPageDown()
 {
 {
-	return Scroll(bar_length);
+	Scroll(SCROLL_PAGE_FACTOR * bar_length, ScrollBehavior::Smooth);
 }
 }
 
 
-// Called when the slider is incremented by one 'page', either by the page-down key or a mouse-click on the track
-// above / left of the bar.
-float WidgetScroll::OnPageDecrement()
+void WidgetScroll::ScrollPageUp()
 {
 {
-	return Scroll(-bar_length);
+	Scroll(-SCROLL_PAGE_FACTOR * bar_length, ScrollBehavior::Smooth);
 }
 }
 
 
-// Returns the bar position after scrolling for a number of pixels.
-float WidgetScroll::Scroll(float distance)
+void WidgetScroll::Scroll(float distance, ScrollBehavior behavior)
 {
 {
 	float traversable_track_length = (track_length - bar_length);
 	float traversable_track_length = (track_length - bar_length);
-	if (traversable_track_length <= 0)
-		return bar_position;
 
 
-	return (bar_position * traversable_track_length + distance) / traversable_track_length;
+	float new_bar_position = bar_position;
+	if (traversable_track_length > 0.f)
+		new_bar_position = Math::Clamp((bar_position * traversable_track_length + distance) / traversable_track_length, 0.f, 1.f);
+
+	// 'parent' is the scrollbar element, its parent again is the actual element we want to scroll
+	Element* element_scroll = parent->GetParentNode();
+	if (!element_scroll)
+	{
+		RMLUI_ERROR;
+		return;
+	}
+
+	Vector2f scroll_offset = {element_scroll->GetScrollLeft(), element_scroll->GetScrollTop()};
+	if (orientation == HORIZONTAL)
+		scroll_offset.x = new_bar_position * (element_scroll->GetScrollWidth() - element_scroll->GetClientWidth());
+	else
+		scroll_offset.y = new_bar_position * (element_scroll->GetScrollHeight() - element_scroll->GetClientHeight());
+
+	element_scroll->ScrollTo(scroll_offset, behavior);
 }
 }
 
 
 } // namespace Rml
 } // namespace Rml

+ 12 - 30
Source/Core/WidgetScroll.h

@@ -34,6 +34,7 @@
 namespace Rml {
 namespace Rml {
 
 
 class Element;
 class Element;
+enum class ScrollBehavior;
 
 
 /**
 /**
 	A widget for incorporating scrolling functionality into an element.
 	A widget for incorporating scrolling functionality into an element.
@@ -78,18 +79,12 @@ public:
 	/// Sets the length of the entire track in scrollable units (usually lines or characters). This affects the
 	/// Sets the length of the entire track in scrollable units (usually lines or characters). This affects the
 	/// length of the bar element and the speed of scrolling using the mouse-wheel or arrows.
 	/// length of the bar element and the speed of scrolling using the mouse-wheel or arrows.
 	/// @param[in] track_length The length of the track.
 	/// @param[in] track_length The length of the track.
-	/// @param[in] force_resize True to resize the bar immediately, false to wait until the next format.
-	void SetTrackLength(float track_length, bool force_resize = true);
+	void SetTrackLength(float track_length);
 	/// Sets the length the bar represents in scrollable units (usually lines or characters), relative to the track
 	/// Sets the length the bar represents in scrollable units (usually lines or characters), relative to the track
 	/// length. For example, for a scroll bar, this would represent how big each visible 'page' is compared to the
 	/// length. For example, for a scroll bar, this would represent how big each visible 'page' is compared to the
 	/// whole document (which would be set as the track length).
 	/// whole document (which would be set as the track length).
 	/// @param[in] bar_length The length of the slider's bar.
 	/// @param[in] bar_length The length of the slider's bar.
-	/// @param[in] force_resize True to resize the bar immediately, false to wait until the next format.
-	void SetBarLength(float bar_length, bool force_resize = true);
-
-	/// Sets the line height of the parent element; this is used for scrolling speeds.
-	/// @param[in] line_height The line height of the parent element.
-	void SetLineHeight(float line_height);
+	void SetBarLength(float bar_length);
 
 
 	/// Lays out and resizes the internal elements.
 	/// Lays out and resizes the internal elements.
 	/// @param[in] containing_block The padded box containing the slider. This is used to resolve relative properties.
 	/// @param[in] containing_block The padded box containing the slider. This is used to resolve relative properties.
@@ -110,28 +105,16 @@ private:
 	/// @param[in] bar_length The total length of the bar, as a proportion of the track length. If this is -1, the intrinsic length will be used.
 	/// @param[in] bar_length The total length of the bar, as a proportion of the track length. If this is -1, the intrinsic length will be used.
 	void FormatBar(float bar_length = -1);
 	void FormatBar(float bar_length = -1);
 
 
-	// Set the offset on 'bar' from its position.
+	// Set the offset on 'bar' based on its position.
 	void PositionBar();
 	void PositionBar();
 
 
-	/// Called when the slider is incremented by one 'line', either by the down / right key or a mouse-click on the
-	/// increment arrow.
-	/// @return The new position of the bar.
-	float OnLineIncrement();
-	/// Called when the slider is decremented by one 'line', either by the up / left key or a mouse-click on the
-	/// decrement arrow.
-	/// @return The new position of the bar.
-	float OnLineDecrement();
-	/// Called when the slider is incremented by one 'page', either by the page-up key or a mouse-click on the
-	/// track below / right of the bar.
-	/// @return The new position of the bar.
-	float OnPageIncrement();
-	/// Called when the slider is incremented by one 'page', either by the page-down key or a mouse-click on the
-	/// track above / left of the bar.
-	/// @return The new position of the bar.
-	float OnPageDecrement();
-
-	// Returns the bar position after scrolling for a number of pixels.
-	float Scroll(float distance);
+	void ScrollLineDown();
+	void ScrollLineUp();
+	void ScrollPageDown();
+	void ScrollPageUp();
+
+	// Scrolls the parent element by the given distance.
+	void Scroll(float distance, ScrollBehavior behavior);
 
 
 	Element* parent;
 	Element* parent;
 
 
@@ -147,7 +130,7 @@ private:
 	// A number from 0 to 1, indicating how far along the track the bar is.
 	// A number from 0 to 1, indicating how far along the track the bar is.
 	float bar_position;
 	float bar_position;
 	// If the bar is being dragged, this is the pixel offset from the start of the bar to where it was picked up.
 	// If the bar is being dragged, this is the pixel offset from the start of the bar to where it was picked up.
-	int bar_drag_anchor;
+	float bar_drag_anchor;
 
 
 	// Set to the auto-repeat timer if either of the arrow buttons have been pressed, -1 if they haven't.
 	// Set to the auto-repeat timer if either of the arrow buttons have been pressed, -1 if they haven't.
 	float arrow_timers[2];
 	float arrow_timers[2];
@@ -155,7 +138,6 @@ private:
 
 
 	float track_length;
 	float track_length;
 	float bar_length;
 	float bar_length;
-	float line_height;
 };
 };
 
 
 } // namespace Rml
 } // namespace Rml

+ 27 - 0
Tests/Source/UnitTests/Element.cpp

@@ -27,6 +27,7 @@
  */
  */
 
 
 #include "../Common/Mocks.h"
 #include "../Common/Mocks.h"
+#include "../Common/TestsInterface.h"
 #include "../Common/TestsShell.h"
 #include "../Common/TestsShell.h"
 #include "../Common/TypesToString.h"
 #include "../Common/TypesToString.h"
 #include <RmlUi/Core/Context.h>
 #include <RmlUi/Core/Context.h>
@@ -392,6 +393,32 @@ TEST_CASE("Element.ScrollIntoView")
 			CHECK(cells[2][2]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(0, 0));
 			CHECK(cells[2][2]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(0, 0));
 			CHECK(cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(50, 50));
 			CHECK(cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(50, 50));
 		}
 		}
+
+		SUBCASE("Smoothscroll")
+		{
+			TestsSystemInterface* system_interface = TestsShell::GetTestsSystemInterface();
+			system_interface->SetTime(0);
+			cells[3][3]->ScrollIntoView({ScrollAlignment::Nearest, ScrollAlignment::Nearest, ScrollBehavior::Smooth});
+
+			constexpr double dt = 1.0 / 15.0;
+			system_interface->SetTime(dt);
+			Run(context);
+
+			// We don't define the exact offset at this time step, but it should be somewhere between the start and end offsets.
+			Vector2f offset = cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER);
+			CHECK(offset.x > 50.f);
+			CHECK(offset.y > 50.f);
+			CHECK(offset.x < 75.f);
+			CHECK(offset.y < 75.f);
+
+			// After one second it should be at the destination offset.
+			for (double t = 2.0 * dt; t < 1.0; t += dt)
+			{
+				system_interface->SetTime(t);
+				Run(context);
+			}
+			CHECK(cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(50, 50));
+		}
 	}
 	}
 
 
 	document->Close();
 	document->Close();