Browse Source

Continue partial layout - store committed layouts, use proper absolute positioning containing block

Michael Ragazzon 6 months ago
parent
commit
d79288fe9a

+ 3 - 0
Include/RmlUi/Core/Box.h

@@ -99,6 +99,9 @@ public:
 	/// @param area The area to use.
 	Vector2f GetFrameSize(BoxArea area) const;
 
+	/// Compares all area edges against another box, excluding the content area.
+	/// @return True if the boxes represent the same area edges.
+	bool EqualAreaEdges(const Box& other) const;
 	/// Compares the size of the content area and the other area edges.
 	/// @return True if the boxes represent the same area.
 	bool operator==(const Box& rhs) const;

+ 6 - 1
Source/Core/Box.cpp

@@ -122,9 +122,14 @@ Vector2f Box::GetFrameSize(BoxArea area) const
 	};
 }
 
+bool Box::EqualAreaEdges(const Box& other) const
+{
+	return memcmp(area_edges, other.area_edges, sizeof(area_edges)) == 0;
+}
+
 bool Box::operator==(const Box& rhs) const
 {
-	return content == rhs.content && memcmp(area_edges, rhs.area_edges, sizeof(area_edges)) == 0;
+	return content == rhs.content && EqualAreaEdges(rhs);
 }
 
 bool Box::operator!=(const Box& rhs) const

+ 31 - 15
Source/Core/ElementDocument.cpp

@@ -525,33 +525,49 @@ void ElementDocument::UpdateLayout()
 		RMLUI_ZoneScoped;
 		RMLUI_ZoneText(source_url.c_str(), source_url.size());
 
-		Vector2f containing_block(0, 0);
-		if (GetParentNode() != nullptr)
-			containing_block = GetParentNode()->GetBox().GetSize();
-
+		bool force_full_document_layout = false;
 		bool any_layout_updates = false;
 
 		ElementUtilities::BreadthFirstSearch(this, [&](Element* element) {
 			LayoutNode* layout_node = element->GetLayoutNode();
-			if (layout_node->IsDirty())
+			if (!layout_node->IsDirty())
+				return ElementUtilities::CallbackControlFlow::Continue;
+
+			RMLUI_ASSERTMSG(layout_node->IsLayoutBoundary(),
+				"Dirty layout should have propagated to the closest layout boundary during Element::Update().")
+			any_layout_updates = true;
+
+			const Optional<CommittedLayout>& committed_layout = layout_node->GetCommittedLayout();
+			if (!committed_layout)
 			{
-				RMLUI_ASSERTMSG(layout_node->IsLayoutBoundary(),
-					"Dirty layout should have propagated to the closest layout boundary during Element::Update().")
 				if (element->GetOwnerDocument() != this)
-					Log::Message(Log::LT_INFO, "Doing partial layout update on element: %s", element->GetAddress().c_str());
-
-				// TODO: Use correct containing block
-				// TODO: Check if size changed, such that we need to do a layout update in its parent.
-				LayoutEngine::FormatElement(element, containing_block);
-				any_layout_updates = true;
-				return ElementUtilities::CallbackControlFlow::SkipChildren;
+					Log::Message(Log::LT_INFO, "Forcing full layout update due to element: %s", element->GetAddress().c_str());
+				force_full_document_layout = true;
+				return ElementUtilities::CallbackControlFlow::Break;
 			}
-			return ElementUtilities::CallbackControlFlow::Continue;
+
+			if (element->GetOwnerDocument() != this)
+				Log::Message(Log::LT_INFO, "Doing partial layout update on element: %s", element->GetAddress().c_str());
+
+			// TODO: In some cases, we need to check if size changed, such that we need to do a layout update in its parent.
+			LayoutEngine::FormatElement(element, committed_layout->containing_block_size,
+				committed_layout->absolutely_positioning_containing_block_size);
+
+			return ElementUtilities::CallbackControlFlow::SkipChildren;
 		});
 
 		if (!any_layout_updates)
 			Log::Message(Log::LT_INFO, "Didn't update layout on anything for document: %s", GetAddress().c_str());
 
+		if (force_full_document_layout)
+		{
+			Vector2f containing_block;
+			if (Element* parent = GetParentNode())
+				containing_block = parent->GetBox().GetSize();
+
+			LayoutEngine::FormatElement(this, containing_block, containing_block);
+		}
+
 		// Ignore dirtied layout during document formatting. Layouting must not require re-iteration.
 		// In particular, scrollbars being enabled may set the dirty flag, but this case is already handled within the layout engine.
 		layout_dirty = false;

+ 1 - 1
Source/Core/ElementUtilities.cpp

@@ -304,7 +304,7 @@ bool ElementUtilities::GetBoundingBox(Rectanglef& out_rectangle, Element* elemen
 
 void ElementUtilities::FormatElement(Element* element, Vector2f containing_block)
 {
-	LayoutEngine::FormatElement(element, containing_block);
+	LayoutEngine::FormatElement(element, containing_block, containing_block);
 }
 
 void ElementUtilities::BuildBox(Box& box, Vector2f containing_block, Element* element, bool inline_element)

+ 10 - 0
Source/Core/Layout/ContainerBox.cpp

@@ -121,6 +121,16 @@ bool ContainerBox::IsScrollContainer() const
 	return LayoutDetails::IsScrollContainer(overflow_x, overflow_y);
 }
 
+bool ContainerBox::IsMaxContentConstraint() const
+{
+	if (parent_container)
+		return parent_container->IsMaxContentConstraint();
+	// TODO: Very hacky
+	if (const Box* box = GetIfBox())
+		return box->GetSize().x >= 10'000.f;
+	return false;
+}
+
 void ContainerBox::ClosePositionedElements()
 {
 	// Any relatively positioned elements that we act as containing block for may need to have their positions

+ 19 - 0
Source/Core/Layout/ContainerBox.h

@@ -62,6 +62,8 @@ public:
 
 	/// Returns true if this container can have scrollbars enabled, as determined by its overflow properties.
 	bool IsScrollContainer() const;
+	/// Returns true if this container is being layed-out under max-content constraint.
+	bool IsMaxContentConstraint() const;
 
 	void AssertMatchesParentContainer(ContainerBox* container_box) const
 	{
@@ -126,6 +128,7 @@ class RootBox final : public ContainerBox {
 public:
 	RootBox(Vector2f containing_block) : ContainerBox(Type::Root, nullptr, nullptr), box(containing_block) {}
 	RootBox(const Box& box) : ContainerBox(Type::Root, nullptr, nullptr), box(box) {}
+	RootBox(Vector2f containing_block, RootBox* absolute_root) : ContainerBox(Type::Root, nullptr, absolute_root), box(containing_block) {}
 
 	const Box* GetIfBox() const override { return &box; }
 	String DebugDumpTree(int depth) const override;
@@ -179,5 +182,21 @@ private:
 	Box box;
 };
 
+/**
+    A box which is produced when we matched the existing layout.
+
+*/
+class CachedContainer final : public ContainerBox {
+public:
+	CachedContainer(Vector2f containing_block) : ContainerBox(Type::Root, nullptr, nullptr), box(containing_block) {}
+	CachedContainer(const Box& box) : ContainerBox(Type::Root, nullptr, nullptr), box(box) {}
+
+	const Box* GetIfBox() const override { return &box; }
+	String DebugDumpTree(int depth) const override;
+
+private:
+	Box box;
+};
+
 } // namespace Rml
 #endif

+ 24 - 0
Source/Core/Layout/FormattingContext.cpp

@@ -36,6 +36,7 @@
 #include "FormattingContextDebug.h"
 #include "LayoutBox.h"
 #include "LayoutDetails.h"
+#include "LayoutNode.h"
 #include "ReplacedFormattingContext.h"
 #include "TableFormattingContext.h"
 
@@ -97,6 +98,22 @@ UniquePtr<LayoutBox> FormattingContext::FormatIndependent(ContainerBox* parent_c
 	}
 #endif
 
+	const bool is_under_max_content_constraint = parent_container->IsMaxContentConstraint();
+	if (type != FormattingContextType::None && !is_under_max_content_constraint)
+	{
+		LayoutNode* layout_node = element->GetLayoutNode();
+		if (layout_node->CommittedLayoutMatches(parent_container->GetContainingBlockSize(Style::Position::Static),
+				parent_container->GetContainingBlockSize(Style::Position::Static), override_initial_box))
+		{
+			Log::Message(Log::LT_DEBUG, "Layout cache match on element: %s", element->GetAddress().c_str());
+			// TODO: Construct a new CachedContainer and return that.
+			// TODO: How to deal with ShrinkToFitWidth, in particular for the returned box? Store it in the LayoutNode?
+			// Maybe best not to use this committed layout at all during max-content layouting. Instead, skip this here,
+			// return zero in the CacheContainerBox, and make a separate LayoutNode cache for shrink-to-fit width that
+			// is fetched in LayoutDetails::ShrinkToFitWidth().
+		}
+	}
+
 	UniquePtr<LayoutBox> layout_box;
 	switch (type)
 	{
@@ -107,6 +124,13 @@ UniquePtr<LayoutBox> FormattingContext::FormatIndependent(ContainerBox* parent_c
 	case FormattingContextType::None: break;
 	}
 
+	if (layout_box && !is_under_max_content_constraint)
+	{
+		LayoutNode* layout_node = element->GetLayoutNode();
+		layout_node->CommitLayout(parent_container->GetContainingBlockSize(Style::Position::Static),
+			parent_container->GetContainingBlockSize(Style::Position::Absolute), override_initial_box);
+	}
+
 #ifdef RMLUI_DEBUG
 	if (tracker_entry)
 	{

+ 0 - 10
Source/Core/Layout/FormattingContextDebug.cpp

@@ -138,16 +138,6 @@ void FormatIndependentDebugTracker::LogMessage() const
 	Log::Message(Log::LT_INFO, "%s", ToString().c_str());
 }
 
-int FormatIndependentDebugTracker::CountCachedEntries() const
-{
-	return (int)std::count_if(entries.begin(), entries.end(), [](const auto& entry) { return entry.layout.has_value(); });
-}
-
-int FormatIndependentDebugTracker::CountFormattedEntries() const
-{
-	return (int)entries.size() - CountCachedEntries();
-}
-
 int FormatIndependentDebugTracker::CountEntries() const
 {
 	return (int)entries.size();

+ 0 - 2
Source/Core/Layout/FormattingContextDebug.h

@@ -66,8 +66,6 @@ public:
 	String ToString() const;
 	void LogMessage() const;
 	int CountEntries() const;
-	int CountCachedEntries() const;
-	int CountFormattedEntries() const;
 };
 
 #endif // RMLUI_DEBUG

+ 1 - 1
Source/Core/Layout/LayoutBox.h

@@ -50,7 +50,7 @@ public:
 	virtual const Box* GetIfBox() const;
 	// Returns the baseline of the last line of this box, if any. Returns true if a baseline was found, otherwise false.
 	virtual bool GetBaselineOfLastLine(float& out_baseline) const;
-	// Calculates the box's inner content width, i.e. the size used to calculate the shrink-to-fit width.
+	// Calculates the box's inner content width, i.e. the size used to calculate the shrink-to-fit width. Only valid under max-content constraint.
 	virtual float GetShrinkToFitWidth() const;
 
 	// Debug dump layout tree.

+ 2 - 2
Source/Core/Layout/LayoutDetails.cpp

@@ -219,10 +219,10 @@ float LayoutDetails::GetShrinkToFitWidth(Element* element, Vector2f containing_b
 	// Use a large size for the box content width, so that it is practically unconstrained. This makes the formatting
 	// procedure act as if under a maximum content constraint. Children with percentage sizing values may be scaled
 	// based on this width (such as 'width' or 'margin'), if so, the layout is considered undefined like in CSS 2.
-	const float max_content_constraint_width = containing_block.x + 10000.f;
+	const float max_content_constraint_width = containing_block.x + 10'000.f;
 	box.SetContent({max_content_constraint_width, box.GetSize().y});
 
-	// First, format the element under the above generated box. Then we ask the resulting box for its shrink-to-fit
+	// First, format the element under the above-generated box. Then we ask the resulting box for its shrink-to-fit
 	// width. For block containers, this is essentially its largest line or child box.
 	// @performance. Some formatting can be simplified, e.g. absolute elements do not contribute to the shrink-to-fit
 	// width. Also, children of elements with a fixed width and height don't need to be formatted further.

+ 3 - 2
Source/Core/Layout/LayoutEngine.cpp

@@ -35,11 +35,12 @@
 
 namespace Rml {
 
-void LayoutEngine::FormatElement(Element* element, Vector2f containing_block)
+void LayoutEngine::FormatElement(Element* element, Vector2f containing_block, Vector2f absolutely_positioning_containing_block)
 {
 	RMLUI_ASSERT(element && containing_block.x >= 0 && containing_block.y >= 0);
 
-	RootBox root(containing_block);
+	RootBox absolute_root(absolutely_positioning_containing_block);
+	RootBox root(containing_block, &absolute_root);
 
 	auto layout_box = FormattingContext::FormatIndependent(&root, element, nullptr, FormattingContextType::Block);
 	if (!layout_box)

+ 2 - 1
Source/Core/Layout/LayoutEngine.h

@@ -46,7 +46,8 @@ public:
 	/// Formats the contents for a root-level element, usually a document, or a replaced element with custom formatting.
 	/// @param[in] element The element to lay out.
 	/// @param[in] containing_block The size of the containing block.
-	static void FormatElement(Element* element, Vector2f containing_block);
+	/// @param[in] absolutely_positioning_containing_block The size of the absolutely positioning containing block.
+	static void FormatElement(Element* element, Vector2f containing_block, Vector2f absolutely_positioning_containing_block);
 };
 
 } // namespace Rml

+ 53 - 0
Source/Core/Layout/LayoutNode.h

@@ -29,6 +29,8 @@
 #ifndef RMLUI_CORE_LAYOUT_LAYOUTNODE_H
 #define RMLUI_CORE_LAYOUT_LAYOUTNODE_H
 
+#include "../../../Include/RmlUi/Core/Box.h"
+#include "../../../Include/RmlUi/Core/Element.h"
 #include "../../../Include/RmlUi/Core/Types.h"
 
 namespace Rml {
@@ -55,6 +57,13 @@ inline DirtyLayoutType operator&(DirtyLayoutType lhs, DirtyLayoutType rhs)
 	return DirtyLayoutType(T(lhs) & T(rhs));
 }
 
+struct CommittedLayout {
+	Vector2f containing_block_size;
+	Vector2f absolutely_positioning_containing_block_size;
+
+	Optional<Box> override_box;
+};
+
 /*
     A LayoutNode TODO
 */
@@ -72,6 +81,48 @@ public:
 	bool IsDirty() const { return dirty_flag != DirtyLayoutType::None; }
 	bool IsSelfDirty() const { return !(dirty_flag == DirtyLayoutType::None || dirty_flag == DirtyLayoutType::Child); }
 
+	void CommitLayout(Vector2f containing_block_size, Vector2f absolutely_positioning_containing_block_size, const Box* override_box)
+	{
+		committed_layout.emplace(CommittedLayout{
+			containing_block_size,
+			absolutely_positioning_containing_block_size,
+			override_box ? Optional<Box>(*override_box) : Optional<Box>(),
+		});
+		ClearDirty();
+	}
+
+	bool CommittedLayoutMatches(Vector2f containing_block_size, Vector2f absolutely_positioning_containing_block_size, const Box* override_box) const
+	{
+		if (IsDirty())
+			return false;
+		if (!committed_layout.has_value())
+			return false;
+		if (committed_layout->containing_block_size != containing_block_size ||
+			committed_layout->absolutely_positioning_containing_block_size != absolutely_positioning_containing_block_size)
+			return false;
+
+		if (!override_box)
+			return !committed_layout->override_box.has_value();
+
+		const Box& compare_box = committed_layout->override_box.has_value() ? *committed_layout->override_box : element->GetBox();
+		if (!override_box->EqualAreaEdges(*committed_layout->override_box))
+			return false;
+
+		if (override_box->GetSize() == compare_box.GetSize())
+			return true;
+
+		// Lastly, if we have an indefinite size on the committed box, see if the layed-out size matches the input override box.
+		Vector2f compare_size = compare_box.GetSize();
+		if (compare_size.x < 0.f)
+			compare_size.x = element->GetBox().GetSize().x;
+		if (compare_size.y < 0.f)
+			compare_size.y = element->GetBox().GetSize().y;
+
+		return override_box->GetSize() == compare_size;
+	}
+
+	const Optional<CommittedLayout>& GetCommittedLayout() const { return committed_layout; }
+
 	// A.k.a. reflow root.
 	bool IsLayoutBoundary() const;
 
@@ -86,6 +137,8 @@ private:
 
 	// Store this for partial layout updates / reflow. If this changes, always do layout again, regardless of any cache.
 	Vector2f containing_block;
+
+	Optional<CommittedLayout> committed_layout;
 };
 
 } // namespace Rml

+ 2 - 5
Tests/Source/UnitTests/LayoutIsolation.cpp

@@ -203,7 +203,7 @@ TEST_CASE("LayoutIsolation.InsideOutsideFormattingContexts")
 TEST_CASE("LayoutIsolation.FullLayoutFormatIndependentCount")
 {
 	Context* context = TestsShell::GetContext();
-	auto& format_independent_tracker = FormatIndependentDebugTracker::Initialize();
+	FormatIndependentDebugTracker format_independent_tracker;
 	ElementDocument* document = context->LoadDocumentFromMemory(document_isolation_rml);
 
 	document->Show();
@@ -219,7 +219,6 @@ TEST_CASE("LayoutIsolation.FullLayoutFormatIndependentCount")
 	// number while working on the flex formatting engine. If this fails for any other reason, it is likely a bug.
 	CHECK(format_independent_tracker.entries.size() == 10);
 
-	FormatIndependentDebugTracker::Shutdown();
 	document->Close();
 	TestsShell::ShutdownShell();
 }
@@ -283,13 +282,12 @@ TEST_CASE("LayoutIsolation.Absolute")
 {
 	Context* context = TestsShell::GetContext();
 
-	auto& format_independent_tracker = FormatIndependentDebugTracker::Initialize();
-
 	ElementDocument* document = context->LoadDocumentFromMemory(document_isolation_absolute_rml);
 	document->Show();
 
 	TestsShell::RenderLoop();
 
+	FormatIndependentDebugTracker format_independent_tracker;
 	SUBCASE("Modify absolute content")
 	{
 		Element* element = document->GetElementById("absolute-item");
@@ -365,7 +363,6 @@ TEST_CASE("LayoutIsolation.Absolute")
 	}
 
 	Log::Message(Log::LT_DEBUG, "%s", format_independent_tracker.ToString().c_str());
-	FormatIndependentDebugTracker::Shutdown();
 
 	document->Close();
 	TestsShell::ShutdownShell();