Browse Source

Extend Element::ScrollIntoView parameters, merges #353 with modifications

Co-authored-by:  Eugene Kozlov <[email protected]>
Michael Ragazzon 3 years ago
parent
commit
bd37fd2c5a
3 changed files with 244 additions and 17 deletions
  1. 20 0
      Include/RmlUi/Core/Element.h
  2. 44 17
      Source/Core/Element.cpp
  3. 180 0
      Tests/Source/UnitTests/Element.cpp

+ 20 - 0
Include/RmlUi/Core/Element.h

@@ -66,6 +66,23 @@ class TransformState;
 struct ElementMeta;
 struct ElementMeta;
 struct StackingOrderedChild;
 struct StackingOrderedChild;
 
 
+enum class ScrollAlignment {
+	Start,   // Align to the top or left edge of the parent element.
+	Center,  // Align to the center of the parent element.
+	End,     // Align to the bottom or right edge of the parent element.
+	Nearest, // Align with minimal scroll change.
+};
+/**
+	Defines behavior of Element::ScrollIntoView.
+ */
+struct ScrollIntoViewOptions {
+	ScrollIntoViewOptions(ScrollAlignment vertical = ScrollAlignment::Start, ScrollAlignment horizontal = ScrollAlignment::Nearest) :
+		vertical(vertical), horizontal(horizontal)
+	{}
+	ScrollAlignment vertical;
+	ScrollAlignment horizontal;
+};
+
 /**
 /**
 	A generic element in the DOM tree.
 	A generic element in the DOM tree.
 
 
@@ -499,6 +516,9 @@ public:
 	/// Sends an event to this element by event id.
 	/// Sends an event to this element by event id.
 	bool DispatchEvent(EventId id, const Dictionary& parameters);
 	bool DispatchEvent(EventId id, const Dictionary& parameters);
 
 
+	/// Scrolls the parent element's contents so that this element is visible.
+	/// @param[in] options Scroll parameters that control desired element alignment relative to the parent.
+	void ScrollIntoView(ScrollIntoViewOptions options);
 	/// 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);

+ 44 - 17
Source/Core/Element.cpp

@@ -70,6 +70,27 @@ namespace Rml {
 // Determines how many levels up in the hierarchy the OnChildAdd and OnChildRemove are called (starting at the child itself)
 // Determines how many levels up in the hierarchy the OnChildAdd and OnChildRemove are called (starting at the child itself)
 static constexpr int ChildNotifyLevels = 2;
 static constexpr int ChildNotifyLevels = 2;
 
 
+// Helper function to select scroll offset delta
+static float GetScrollOffsetDelta(ScrollAlignment alignment, float begin_offset, float end_offset)
+{
+	switch (alignment)
+	{
+	case ScrollAlignment::Start: return begin_offset;
+	case ScrollAlignment::Center: return (begin_offset + end_offset) / 2.0f;
+	case ScrollAlignment::End: return end_offset;
+	case ScrollAlignment::Nearest:
+		if (begin_offset >= 0.0 && end_offset <= 0.0)
+			return 0.0f; // Element is already visible, don't scroll
+		else if (begin_offset < 0.0 && end_offset < 0.0)
+			return Math::Max(begin_offset, end_offset);
+		else if (begin_offset > 0.0 && end_offset > 0.0)
+			return Math::Min(begin_offset, end_offset);
+		else
+			return 0.0f; // Shouldn't happen
+	}
+	return 0.f;
+}
+
 // Meta objects for element collected in a single struct to reduce memory allocations
 // Meta objects for element collected in a single struct to reduce memory allocations
 struct ElementMeta
 struct ElementMeta
 {
 {
@@ -85,7 +106,6 @@ struct ElementMeta
 
 
 static Pool< ElementMeta > element_meta_chunk_pool(200, true);
 static Pool< ElementMeta > element_meta_chunk_pool(200, true);
 
 
-
 Element::Element(const String& tag) :
 Element::Element(const String& tag) :
 	local_stacking_context(false), local_stacking_context_forced(false), stacking_context_dirty(false), computed_values_are_default_initialized(true),
 	local_stacking_context(false), local_stacking_context_forced(false), stacking_context_dirty(false), computed_values_are_default_initialized(true),
 	visible(true), offset_fixed(false), absolute_offset_dirty(true), dirty_definition(false), dirty_child_definitions(false), dirty_animation(false),
 	visible(true), offset_fixed(false), absolute_offset_dirty(true), dirty_definition(false), dirty_child_definitions(false), dirty_animation(false),
@@ -1257,11 +1277,9 @@ bool Element::DispatchEvent(EventId id, const Dictionary& parameters)
 }
 }
 
 
 // Scrolls the parent element's contents so that this element is visible.
 // Scrolls the parent element's contents so that this element is visible.
-void Element::ScrollIntoView(bool align_with_top)
+void Element::ScrollIntoView(const ScrollIntoViewOptions options)
 {
 {
-	Vector2f size(0, 0);
-	if (!align_with_top)
-		size.y = main_box.GetSize(Box::BORDER).y;
+	const Vector2f size = main_box.GetSize(Box::BORDER);
 
 
 	Element* scroll_parent = parent;
 	Element* scroll_parent = parent;
 	while (scroll_parent != nullptr)
 	while (scroll_parent != nullptr)
@@ -1271,32 +1289,41 @@ void Element::ScrollIntoView(bool align_with_top)
 		const bool scrollable_box_x = (computed.overflow_x() != Overflow::Visible && computed.overflow_x() != Overflow::Hidden);
 		const bool scrollable_box_x = (computed.overflow_x() != Overflow::Visible && computed.overflow_x() != Overflow::Hidden);
 		const bool scrollable_box_y = (computed.overflow_y() != Overflow::Visible && computed.overflow_y() != Overflow::Hidden);
 		const bool scrollable_box_y = (computed.overflow_y() != Overflow::Visible && computed.overflow_y() != Overflow::Hidden);
 
 
-		const Vector2f parent_scroll_size = { scroll_parent->GetScrollWidth(), scroll_parent->GetScrollHeight() };
-		const Vector2f parent_client_size = { scroll_parent->GetClientWidth(), scroll_parent->GetClientHeight() };
+		const Vector2f parent_scroll_size = {scroll_parent->GetScrollWidth(), scroll_parent->GetScrollHeight()};
+		const Vector2f parent_client_size = {scroll_parent->GetClientWidth(), scroll_parent->GetClientHeight()};
 
 
-		if ((scrollable_box_x && parent_scroll_size.x > parent_client_size.x) ||
-			(scrollable_box_y && parent_scroll_size.y > parent_client_size.y))
+		if ((scrollable_box_x && parent_scroll_size.x > parent_client_size.x) || (scrollable_box_y && parent_scroll_size.y > parent_client_size.y))
 		{
 		{
 			const Vector2f relative_offset = scroll_parent->GetAbsoluteOffset(Box::BORDER) - GetAbsoluteOffset(Box::BORDER);
 			const Vector2f relative_offset = scroll_parent->GetAbsoluteOffset(Box::BORDER) - GetAbsoluteOffset(Box::BORDER);
 
 
-			Vector2f scroll_offset(scroll_parent->GetScrollLeft(), scroll_parent->GetScrollTop());
-			scroll_offset -= relative_offset;
-			scroll_offset.x += scroll_parent->GetClientLeft();
-			scroll_offset.y += scroll_parent->GetClientTop();
+			const Vector2f old_scroll_offset = {scroll_parent->GetScrollLeft(), scroll_parent->GetScrollTop()};
+			const Vector2f parent_client_offset = {scroll_parent->GetClientLeft(), scroll_parent->GetClientTop()};
 
 
-			if (!align_with_top)
-				scroll_offset.y -= (parent_client_size.y - size.y);
+			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;
+
+			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);
 
 
 			if (scrollable_box_x)
 			if (scrollable_box_x)
-				scroll_parent->SetScrollLeft(scroll_offset.x);
+				scroll_parent->SetScrollLeft(new_scroll_offset.x);
 			if (scrollable_box_y)
 			if (scrollable_box_y)
-				scroll_parent->SetScrollTop(scroll_offset.y);
+				scroll_parent->SetScrollTop(new_scroll_offset.y);
 		}
 		}
 
 
 		scroll_parent = scroll_parent->GetParentNode();
 		scroll_parent = scroll_parent->GetParentNode();
 	}
 	}
 }
 }
 
 
+void Element::ScrollIntoView(bool align_with_top)
+{
+	ScrollIntoViewOptions options;
+	options.vertical = (align_with_top ? ScrollAlignment::Start : ScrollAlignment::End);
+	options.horizontal = ScrollAlignment::Nearest;
+	ScrollIntoView(options);
+}
+
 // 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)
 {
 {

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

@@ -70,6 +70,86 @@ static const String document_clone_rml = R"(
 </rml>
 </rml>
 )";
 )";
 
 
+static const String document_scroll_rml = R"(
+<rml>
+<head>
+	<title>Test</title>
+	<style>
+		body {
+			left: 0;
+			top: 0;
+			width: 100px;
+			height: 100px;
+			padding: 0;
+			margin: 0;
+			overflow: scroll;
+		}
+		div {
+			display: block;
+			width: 200px;
+			height: 50px;
+			padding: 0;
+			margin: 0;
+		}
+		#row0 { background: #3a3; }
+		#row1 { background: #aa3; }
+		#row2 { background: #3aa; }
+		#row3 { background: #aaa; }
+		span {
+			display: inline-block;
+			box-sizing: border-box;
+			width: 50px;
+			height: 50px;
+			padding: 0;
+			margin: 0;
+			border: 1px #333;
+		}
+		span:nth-child(even) { border-color: #fff; }
+		scrollbarvertical,
+		scrollbarhorizontal {
+			width: 0;
+			height: 0;
+		}
+	</style>
+</head>
+
+<body id="scrollable">
+<div id="row0">
+	<span id="cell00"></span>
+	<span id="cell01"></span>
+	<span id="cell02"></span>
+	<span id="cell03"></span>
+</div>
+<div id="row1">
+	<span id="cell10"></span>
+	<span id="cell11"></span>
+	<span id="cell12"></span>
+	<span id="cell13"></span>
+</div>
+<div id="row2">
+	<span id="cell20"></span>
+	<span id="cell21"></span>
+	<span id="cell22"></span>
+	<span id="cell23"></span>
+</div>
+<div id="row3">
+	<span id="cell30"></span>
+	<span id="cell31"></span>
+	<span id="cell32"></span>
+	<span id="cell33"></span>
+</div>
+</body>
+</rml>
+)";
+
+void Run(Context* context)
+{
+	context->Update();
+	context->Render();
+
+	TestsShell::RenderLoop();
+}
+
 TEST_CASE("Element")
 TEST_CASE("Element")
 {
 {
 	Context* context = TestsShell::GetContext();
 	Context* context = TestsShell::GetContext();
@@ -214,3 +294,103 @@ TEST_CASE("Element")
 	document->Close();
 	document->Close();
 	TestsShell::ShutdownShell();
 	TestsShell::ShutdownShell();
 }
 }
+
+TEST_CASE("Element.ScrollIntoView")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_scroll_rml);
+	REQUIRE(document);
+	document->Show();
+
+	Run(context);
+
+	Element* scrollable = document->GetElementById("scrollable");
+	REQUIRE(scrollable);
+	Element* cells[4][4]{};
+	for (int i = 0; i < 4; ++i)
+	{
+		for (int j = 0; j < 4; ++j)
+		{
+			cells[i][j] = document->GetElementById(CreateString(8, "cell%d%d", i, j));
+			REQUIRE(cells[i][j]);
+		}
+	}
+
+	REQUIRE(cells[0][0]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(0, 0));
+	REQUIRE(cells[2][2]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(100, 100));
+	REQUIRE(cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(150, 150));
+	REQUIRE(scrollable->GetScrollLeft() == 0);
+	REQUIRE(scrollable->GetScrollTop() == 0);
+
+	SUBCASE("LegacyScroll")
+	{
+		cells[2][2]->ScrollIntoView(true);
+
+		Run(context);
+
+		REQUIRE(cells[0][0]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(-100, -100));
+		REQUIRE(cells[2][2]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(0, 0));
+		REQUIRE(cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(50, 50));
+		REQUIRE(scrollable->GetScrollLeft() == 100);
+		REQUIRE(scrollable->GetScrollTop() == 100);
+
+		cells[2][2]->ScrollIntoView(false);
+
+		Run(context);
+
+		REQUIRE(cells[0][0]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(-100, -50));
+		REQUIRE(cells[2][2]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(0, 50));
+		REQUIRE(cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(50, 100));
+		REQUIRE(scrollable->GetScrollLeft() == 100);
+		REQUIRE(scrollable->GetScrollTop() == 50);
+	}
+
+	SUBCASE("AdvancedScroll")
+	{
+		cells[2][2]->ScrollIntoView({ScrollAlignment::Center, ScrollAlignment::Center});
+
+		Run(context);
+
+		REQUIRE(cells[0][0]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(-75, -75));
+		REQUIRE(cells[2][2]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(25, 25));
+		REQUIRE(cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(75, 75));
+
+		SUBCASE("NearestAlready")
+		{
+			cells[2][2]->ScrollIntoView(ScrollAlignment::Nearest);
+
+			Run(context);
+
+			REQUIRE(cells[0][0]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(-75, -75));
+			REQUIRE(cells[2][2]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(25, 25));
+			REQUIRE(cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(75, 75));
+		}
+
+		SUBCASE("NearestBefore")
+		{
+			cells[1][1]->ScrollIntoView(ScrollAlignment::Nearest);
+
+			Run(context);
+
+			REQUIRE(cells[0][0]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(-50, -50));
+			REQUIRE(cells[1][1]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(0, 0));
+			REQUIRE(cells[2][2]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(50, 50));
+		}
+
+		SUBCASE("NearestAfter")
+		{
+			cells[3][3]->ScrollIntoView(ScrollAlignment::Nearest);
+
+			Run(context);
+
+			REQUIRE(cells[1][1]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(-50, -50));
+			REQUIRE(cells[2][2]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(0, 0));
+			REQUIRE(cells[3][3]->GetAbsoluteOffset(Rml::Box::Area::BORDER) == Vector2f(50, 50));
+		}
+	}
+
+	document->Close();
+	TestsShell::ShutdownShell();
+}