Browse Source

Merge pull request #257 from mikke89/flexbox

Flexbox layout
Michael R. P. Ragazzon 4 years ago
parent
commit
352a52e798
38 changed files with 2741 additions and 171 deletions
  1. 2 0
      .clang-format
  2. 2 0
      CMake/FileList.cmake
  3. 22 5
      Include/RmlUi/Core/ComputedValues.h
  4. 2 0
      Include/RmlUi/Core/ElementScroll.h
  5. 12 0
      Include/RmlUi/Core/ID.h
  6. 8 0
      Include/RmlUi/Core/Math.h
  7. 3 1
      Include/RmlUi/Core/PropertySpecification.h
  8. 1 1
      Source/Core/Element.cpp
  9. 17 8
      Source/Core/ElementScroll.cpp
  10. 28 0
      Source/Core/ElementStyle.cpp
  11. 7 3
      Source/Core/ElementUtilities.cpp
  12. 2 0
      Source/Core/LayoutBlockBox.cpp
  13. 32 8
      Source/Core/LayoutDetails.cpp
  14. 65 27
      Source/Core/LayoutDetails.h
  15. 97 39
      Source/Core/LayoutEngine.cpp
  16. 6 1
      Source/Core/LayoutEngine.h
  17. 848 0
      Source/Core/LayoutFlex.cpp
  18. 76 0
      Source/Core/LayoutFlex.h
  19. 10 10
      Source/Core/LayoutTable.cpp
  20. 7 36
      Source/Core/LayoutTableDetails.cpp
  21. 7 23
      Source/Core/LayoutTableDetails.h
  22. 12 0
      Source/Core/Math.cpp
  23. 29 1
      Source/Core/PropertySpecification.cpp
  24. 2 2
      Source/Core/StyleSheetParser.cpp
  25. 17 1
      Source/Core/StyleSheetSpecification.cpp
  26. 71 0
      Tests/Data/VisualTests/flex_01.rml
  27. 72 0
      Tests/Data/VisualTests/flex_02.rml
  28. 73 0
      Tests/Data/VisualTests/flex_03_scroll.rml
  29. 113 0
      Tests/Data/VisualTests/flex_04.rml
  30. 97 0
      Tests/Data/VisualTests/flex_05.rml
  31. 71 0
      Tests/Data/VisualTests/flex_direction.rml
  32. 63 0
      Tests/Data/VisualTests/flex_wrap_column_reverse.rml
  33. 72 0
      Tests/Data/VisualTests/reference/flex_05-ref.rml
  34. 80 0
      Tests/Data/VisualTests/reference/flex_direction-ref.rml
  35. 71 0
      Tests/Data/VisualTests/reference/flex_wrap_column_reverse-ref.rml
  36. 493 0
      Tests/Source/Benchmarks/Flexbox.cpp
  37. 92 0
      Tests/Source/UnitTests/Properties.cpp
  38. 59 5
      Tests/Tools/convert_css_test_suite_to_rml.py

+ 2 - 0
.clang-format

@@ -1,4 +1,6 @@
 AccessModifierOffset: -4
+AllowAllArgumentsOnNextLine: false
+AllowAllParametersOfDeclarationOnNextLine: false
 AlignAfterOpenBracket: DontAlign
 AlignConsecutiveAssignments: false
 AlignConsecutiveDeclarations: false

+ 2 - 0
CMake/FileList.cmake

@@ -64,6 +64,7 @@ set(Core_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutBlockBoxSpace.h
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutDetails.h
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutEngine.h
+    ${PROJECT_SOURCE_DIR}/Source/Core/LayoutFlex.h
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutInlineBox.h
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutInlineBoxText.h
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutLineBox.h
@@ -339,6 +340,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutBlockBoxSpace.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutDetails.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutEngine.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/LayoutFlex.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutInlineBox.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutInlineBoxText.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutLineBox.cpp

+ 22 - 5
Include/RmlUi/Core/ComputedValues.h

@@ -32,9 +32,7 @@
 #include "Animation.h"
 
 namespace Rml {
-
-namespace Style
-{
+namespace Style {
 
 struct LengthPercentageAuto {
 	enum Type { Auto, Length, Percentage } type = Length;
@@ -60,7 +58,7 @@ struct NumberAuto {
 using Margin = LengthPercentageAuto;
 using Padding = LengthPercentage;
 
-enum class Display : uint8_t { None, Block, Inline, InlineBlock, Table, TableRow, TableRowGroup, TableColumn, TableColumnGroup, TableCell };
+enum class Display : uint8_t { None, Block, Inline, InlineBlock, Flex, Table, TableRow, TableRowGroup, TableColumn, TableColumnGroup, TableCell };
 enum class Position : uint8_t { Static, Relative, Absolute, Fixed };
 
 using Top = LengthPercentageAuto;
@@ -135,6 +133,14 @@ using TransformOrigin = LengthPercentage;
 enum class OriginX : uint8_t { Left, Center, Right };
 enum class OriginY : uint8_t { Top, Center, Bottom };
 
+enum class AlignContent : uint8_t { FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround, Stretch };
+enum class AlignItems : uint8_t { FlexStart, FlexEnd, Center, Baseline, Stretch };
+enum class AlignSelf : uint8_t { Auto, FlexStart, FlexEnd, Center, Baseline, Stretch };
+using FlexBasis = LengthPercentageAuto;
+enum class FlexDirection : uint8_t { Row, RowReverse, Column, ColumnReverse };
+enum class FlexWrap : uint8_t { Nowrap, Wrap, WrapReverse };
+enum class JustifyContent : uint8_t { FlexStart, FlexEnd, Center, SpaceBetween, SpaceAround };
+
 
 /* 
 	A computed value is a value resolved as far as possible :before: introducing layouting. See CSS specs for details of each property.
@@ -223,8 +229,19 @@ struct ComputedValues
 
 	bool has_decorator = false;
 	bool has_font_effect = false;
+
+	AlignContent align_content = AlignContent::Stretch;
+	AlignItems align_items = AlignItems::Stretch;
+	AlignSelf align_self = AlignSelf::Auto;
+	FlexDirection flex_direction = FlexDirection::Row;
+	FlexWrap flex_wrap = FlexWrap::Nowrap;
+	JustifyContent justify_content = JustifyContent::FlexStart;
+	FlexBasis flex_basis = { FlexBasis::Auto };
+	float flex_grow = 0.f;
+	float flex_shrink = 1.f;
 };
-}
+
+} // namespace Style
 
 
 // Resolves a computed LengthPercentage value to the base unit 'px'. 

+ 2 - 0
Include/RmlUi/Core/ElementScroll.h

@@ -98,6 +98,8 @@ private:
 	bool CreateScrollbar(Orientation orientation);
 	// Creates the scrollbar corner.
 	bool CreateCorner();
+	// Update properties of scroll elements immediately after construction.
+	void UpdateScrollElementProperties(Element* scroll_element);
 
 	Element* element;
 

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

@@ -59,6 +59,8 @@ enum class ShorthandId : uint8_t
 	Gap,
 	PerspectiveOrigin,
 	TransformOrigin,
+	Flex,
+	FlexFlow,
 
 	NumDefinedIds,
 	FirstCustomId = NumDefinedIds,
@@ -159,6 +161,16 @@ enum class PropertyId : uint8_t
 
 	FillImage,
 
+	AlignContent,
+	AlignItems,
+	AlignSelf,
+	FlexBasis,
+	FlexDirection,
+	FlexGrow,
+	FlexShrink,
+	FlexWrap,
+	JustifyContent,
+
 	NumDefinedIds,
 	FirstCustomId = NumDefinedIds,
 

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

@@ -82,6 +82,14 @@ Type Lerp(float t, Type v0, Type v1)
 	return v0 * (1.0f - t) + v1 * t;
 }
 
+/// Element-wise maximum.
+template <>
+RMLUICORE_API Vector2f Max<Vector2f>(Vector2f a, Vector2f b);
+/// Element-wise minimum.
+template <>
+RMLUICORE_API Vector2f Min<Vector2f>(Vector2f a, Vector2f b);
+
+/// Color interpolation.
 RMLUICORE_API Colourb RoundedLerp(float t, Colourb c0, Colourb c1);
 
 /// Evaluates if a number is, or close to, zero.

+ 3 - 1
Include/RmlUi/Core/PropertySpecification.h

@@ -55,7 +55,9 @@ enum class ShorthandType
 	// Repeatedly resolves the full value string on each property, whether it is a normal property or another shorthand.
 	RecursiveRepeat,
 	// Comma-separated list of properties or shorthands, the number of declared values must match the specified.
-	RecursiveCommaSeparated
+	RecursiveCommaSeparated,
+	// The 'flex' shorthand has some special behavior but otherwise acts like 'FallThrough'.
+	Flex
 };
 
 

+ 1 - 1
Source/Core/Element.cpp

@@ -2260,7 +2260,7 @@ void Element::BuildStackingContext(ElementList* new_stacking_context)
 				ordered_child.order = RenderOrder::Positioned;
 			else if (child->GetFloat() != Style::Float::None)
 				ordered_child.order = RenderOrder::Floating;
-			else if (child_display == Style::Display::Block || child_display == Style::Display::Table)
+			else if (child_display == Style::Display::Block || child_display == Style::Display::Table || child_display == Style::Display::Flex)
 				ordered_child.order = RenderOrder::Block;
 			else
 				ordered_child.order = RenderOrder::Inline;

+ 17 - 8
Source/Core/ElementScroll.cpp

@@ -209,7 +209,6 @@ void ElementScroll::FormatScrollbars()
 	}
 }
 
-
 // Creates one of the scroll component's scrollbar.
 bool ElementScroll::CreateScrollbar(Orientation orientation)
 {
@@ -226,12 +225,7 @@ bool ElementScroll::CreateScrollbar(Orientation orientation)
 
 	Element* child = element->AppendChild(std::move(scrollbar_element), false);
 
-	// The construction of scrollbars can occur during layouting, then we need some properties and computed values straight away.
-	Context* context = element->GetContext();
-	
-	const float dp_ratio = (context ? context->GetDensityIndependentPixelRatio() : 1.0f);
-	const Vector2f vp_dimensions = (context ? Vector2f(context->GetDimensions()) : Vector2f(1.0f));
-	child->Update(dp_ratio, vp_dimensions);
+	UpdateScrollElementProperties(child);
 
 	return true;
 }
@@ -244,11 +238,26 @@ bool ElementScroll::CreateCorner()
 
 	ElementPtr corner_element = Factory::InstanceElement(element, "*", "scrollbarcorner", XMLAttributes());
 	corner = corner_element.get();
-	element->AppendChild(std::move(corner_element), false);
+	Element* child = element->AppendChild(std::move(corner_element), false);
+
+	UpdateScrollElementProperties(child);
 
 	return true;
 }
 
+void ElementScroll::UpdateScrollElementProperties(Element* scroll_element)
+{
+	// The construction of scrollbars can occur during layouting, then we need some properties and computed values straight away.
+	// In particular their size. Furthermore, updating these properties straight away avoids dirtying the document after layouting,
+	// which may result in one less additional and unnecessary layouting procedure.
+
+	Context* context = element->GetContext();
+
+	const float dp_ratio = (context ? context->GetDensityIndependentPixelRatio() : 1.0f);
+	const Vector2f vp_dimensions = (context ? Vector2f(context->GetDimensions()) : Vector2f(1.0f));
+	scroll_element->Update(dp_ratio, vp_dimensions);
+}
+
 ElementScroll::Scrollbar::Scrollbar()
 {}
 

+ 28 - 0
Source/Core/ElementStyle.cpp

@@ -905,6 +905,34 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 			values.has_font_effect = (p->unit == Property::FONTEFFECT);
 			break;
 
+		case PropertyId::AlignContent:
+			values.align_content = (AlignContent)p->Get<int>();
+			break;
+		case PropertyId::AlignItems:
+			values.align_items = (AlignItems)p->Get<int>();
+			break;
+		case PropertyId::AlignSelf:
+			values.align_self = (AlignSelf)p->Get<int>();
+			break;
+		case PropertyId::FlexBasis:
+			values.flex_basis = ComputeLengthPercentageAuto(p, font_size, document_font_size, dp_ratio, vp_dimensions);
+			break;
+		case PropertyId::FlexDirection:
+			values.flex_direction = (FlexDirection)p->Get<int>();
+			break;
+		case PropertyId::FlexGrow:
+			values.flex_grow = p->Get<float>();
+			break;
+		case PropertyId::FlexShrink:
+			values.flex_shrink = p->Get<float>();
+			break;
+		case PropertyId::FlexWrap:
+			values.flex_wrap = (FlexWrap)p->Get<int>();
+			break;
+		case PropertyId::JustifyContent:
+			values.justify_content = (JustifyContent)p->Get<int>();
+			break;
+
 		// Unhandled properties. Must be manually retrieved with 'GetProperty()'.
 		case PropertyId::FillImage:
 		case PropertyId::CaretColor:

+ 7 - 3
Source/Core/ElementUtilities.cpp

@@ -201,8 +201,12 @@ bool ElementUtilities::GetClippingRegion(Vector2i& clip_origin, Vector2i& clip_d
 				clipping_element->GetClientHeight() < clipping_element->GetScrollHeight() - 0.5f)
 			{
 				const Box::Area client_area = clipping_element->GetClientArea();
-				const Vector2i element_origin(clipping_element->GetAbsoluteOffset(client_area));
-				const Vector2i element_dimensions(clipping_element->GetBox().GetSize(client_area));
+				Vector2f element_origin_f = clipping_element->GetAbsoluteOffset(client_area);
+				Vector2f element_dimensions_f = clipping_element->GetBox().GetSize(client_area);
+				Math::SnapToPixelGrid(element_origin_f, element_dimensions_f);
+
+				const Vector2i element_origin(element_origin_f);
+				const Vector2i element_dimensions(element_dimensions_f);
 				
 				if (clip_origin == Vector2i(-1, -1) && clip_dimensions == Vector2i(-1, -1))
 				{
@@ -299,7 +303,7 @@ void ElementUtilities::FormatElement(Element* element, Vector2f containing_block
 // Generates the box for an element.
 void ElementUtilities::BuildBox(Box& box, Vector2f containing_block, Element* element, bool inline_element)
 {
-	LayoutDetails::BuildBox(box, containing_block, element, inline_element);
+	LayoutDetails::BuildBox(box, containing_block, element, inline_element ? BoxContext::Inline : BoxContext::Block);
 }
 
 // Sizes an element, and positions it within its parent offset from the borders of its content area.

+ 2 - 0
Source/Core/LayoutBlockBox.cpp

@@ -772,6 +772,8 @@ bool LayoutBlockBox::CatchVerticalOverflow(float cursor)
 			box_cursor = 0;
 			interrupted_chain = nullptr;
 
+			inner_content_size = Vector2f(0);
+
 			return false;
 		}
 	}

+ 32 - 8
Source/Core/LayoutDetails.cpp

@@ -46,7 +46,7 @@ static inline float BorderSizeToContentSize(float border_size, float border_padd
 }
 
 // Generates the box for an element.
-void LayoutDetails::BuildBox(Box& box, Vector2f containing_block, Element* element, bool inline_element, float override_shrink_to_fit_width)
+void LayoutDetails::BuildBox(Box& box, Vector2f containing_block, Element* element, BoxContext box_context, float override_shrink_to_fit_width)
 {
 	if (!element)
 	{
@@ -81,7 +81,7 @@ void LayoutDetails::BuildBox(Box& box, Vector2f containing_block, Element* eleme
 
 	// Calculate the content area and constraints. 'auto' width and height are handled later.
 	// For inline non-replaced elements, width and height are ignored, so we can skip the calculations.
-	if (!inline_element || replaced_element)
+	if (box_context == BoxContext::Block || box_context == BoxContext::FlexOrTable || replaced_element)
 	{
 		if (content_area.x < 0 && computed.width.type != Style::Width::Auto)
 			content_area.x = ResolveValue(computed.width, containing_block.x);
@@ -125,15 +125,16 @@ void LayoutDetails::BuildBox(Box& box, Vector2f containing_block, Element* eleme
 	box.SetContent(content_area);
 
 	// Evaluate the margins, and width and height if they are auto.
-	BuildBoxSizeAndMargins(box, min_size, max_size, containing_block, element, inline_element, replaced_element, override_shrink_to_fit_width);
+	BuildBoxSizeAndMargins(box, min_size, max_size, containing_block, element, box_context, replaced_element, override_shrink_to_fit_width);
 }
 
 // Generates the box for an element placed in a block box.
-void LayoutDetails::BuildBox(Box& box, float& min_height, float& max_height, LayoutBlockBox* containing_box, Element* element, bool inline_element, float override_shrink_to_fit_width)
+void LayoutDetails::BuildBox(Box& box, float& min_height, float& max_height, LayoutBlockBox* containing_box, Element* element, BoxContext box_context,
+	float override_shrink_to_fit_width)
 {
 	Vector2f containing_block = LayoutDetails::GetContainingBlock(containing_box);
 
-	BuildBox(box, containing_block, element, inline_element, override_shrink_to_fit_width);
+	BuildBox(box, containing_block, element, box_context, override_shrink_to_fit_width);
 
 	if (element)
 		GetDefiniteMinMaxHeight(min_height, max_height, element->GetComputedValues(), box, containing_block.y);
@@ -213,11 +214,12 @@ Vector2f LayoutDetails::GetContainingBlock(const LayoutBlockBox* containing_box)
 }
 
 
-void LayoutDetails::BuildBoxSizeAndMargins(Box& box, Vector2f min_size, Vector2f max_size, Vector2f containing_block, Element* element, bool inline_element, bool replaced_element, float override_shrink_to_fit_width)
+void LayoutDetails::BuildBoxSizeAndMargins(Box& box, Vector2f min_size, Vector2f max_size, Vector2f containing_block, Element* element,
+	BoxContext box_context, bool replaced_element, float override_shrink_to_fit_width)
 {
 	const ComputedValues& computed = element->GetComputedValues();
 
-	if (inline_element)
+	if (box_context == BoxContext::Inline || box_context == BoxContext::FlexOrTable)
 	{
 		// For inline elements, their calculations are straightforward. No worrying about auto margins and dimensions, etc.
 		// Evaluate the margins. Any declared as 'auto' will resolve to 0.
@@ -240,7 +242,7 @@ float LayoutDetails::GetShrinkToFitWidth(Element* element, Vector2f containing_b
 
 	Box box;
 	float min_height, max_height;
-	LayoutDetails::BuildBox(box, containing_block, element, false, containing_block.x);
+	LayoutDetails::BuildBox(box, containing_block, element, BoxContext::Block, containing_block.x);
 	LayoutDetails::GetDefiniteMinMaxHeight(min_height, max_height, element->GetComputedValues(), box, containing_block.y);
 
 	// First we need to format the element, then we get the shrink-to-fit width based on the largest line or box.
@@ -266,6 +268,28 @@ float LayoutDetails::GetShrinkToFitWidth(Element* element, Vector2f containing_b
 	return Math::Min(containing_block.x, block_context_box->GetShrinkToFitWidth());
 }
 
+ComputedAxisSize LayoutDetails::BuildComputedHorizontalSize(const ComputedValues& computed)
+{
+	return ComputedAxisSize{computed.width, computed.min_width, computed.max_width, computed.padding_left, computed.padding_right,
+		computed.margin_left, computed.margin_right, computed.border_left_width, computed.border_right_width, computed.box_sizing};
+}
+
+ComputedAxisSize LayoutDetails::BuildComputedVerticalSize(const ComputedValues& computed)
+{
+	return ComputedAxisSize{computed.height, computed.min_height, computed.max_height, computed.padding_top, computed.padding_bottom,
+		computed.margin_top, computed.margin_bottom, computed.border_top_width, computed.border_bottom_width, computed.box_sizing};
+}
+
+void LayoutDetails::GetEdgeSizes(float& margin_a, float& margin_b, float& padding_border_a, float& padding_border_b,
+	const ComputedAxisSize& computed_size, const float base_value)
+{
+	margin_a = ResolveValue(computed_size.margin_a, base_value);
+	margin_b = ResolveValue(computed_size.margin_b, base_value);
+
+	padding_border_a = Math::Max(0.0f, ResolveValue(computed_size.padding_a, base_value)) + Math::Max(0.0f, computed_size.border_a);
+	padding_border_b = Math::Max(0.0f, ResolveValue(computed_size.padding_b, base_value)) + Math::Max(0.0f, computed_size.border_b);
+}
+
 Vector2f LayoutDetails::CalculateSizeForReplacedElement(const Vector2f specified_content_size, const Vector2f min_size, const Vector2f max_size, const Vector2f intrinsic_size, const float intrinsic_ratio)
 {
 	// Start with the element's specified width and height. If any of them are auto, use the element's intrinsic

+ 65 - 27
Source/Core/LayoutDetails.h

@@ -15,7 +15,7 @@
  *
  * 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
@@ -29,37 +29,56 @@
 #ifndef RMLUI_CORE_LAYOUTDETAILS_H
 #define RMLUI_CORE_LAYOUTDETAILS_H
 
-#include "LayoutBlockBox.h"
+#include "../../Include/RmlUi/Core/ComputedValues.h"
+#include "../../Include/RmlUi/Core/Types.h"
 
 namespace Rml {
 
 class Box;
+class LayoutBlockBox;
 
 /**
-	Layout functions for sizing elements.
-	
-	Corresponds to the CSS 2.1 specification, 'Section 10. Visual formatting model details'.
- */
+    ComputedAxisSize is an abstraction of an element's computed size properties along a single axis, either horizontally or vertically,
+    allowing eg. rows and columns alike to use the same algorithms. Here, 'a' means left or top, 'b' means right or bottom.
+*/
+struct ComputedAxisSize {
+	Style::LengthPercentageAuto size;
+	Style::LengthPercentage min_size, max_size;
+	Style::Padding padding_a, padding_b;
+	Style::Margin margin_a, margin_b;
+	float border_a, border_b;
+	Style::BoxSizing box_sizing;
+};
+
+enum class BoxContext { Block, Inline, FlexOrTable };
+
+/**
+    Layout functions for sizing elements.
 
-class LayoutDetails
-{
+    Some procedures based on the CSS 2.1 specification, 'Section 10. Visual formatting model details'.
+ */
+class LayoutDetails {
 public:
 	/// Generates the box for an element.
 	/// @param[out] box The box to be built.
 	/// @param[in] containing_block The dimensions of the content area of the block containing the element.
 	/// @param[in] element The element to build the box for.
-	/// @param[in] inline_element True if the element is placed in an inline context, false if not.
-	/// @param[in] override_shrink_to_fit_width Provide a fixed shrink-to-fit width instead of formatting the element when its properties allow shrinking.
-	static void BuildBox(Box& box, Vector2f containing_block, Element* element, bool inline_element = false, float override_shrink_to_fit_width = -1);
+	/// @param[in] box_context The formatting context in which the box is generated.
+	/// @param[in] override_shrink_to_fit_width Provide a fixed shrink-to-fit width instead of formatting the element when its properties allow
+	/// shrinking.
+	static void BuildBox(Box& box, Vector2f containing_block, Element* element, BoxContext box_context = BoxContext::Block,
+		float override_shrink_to_fit_width = -1);
 	/// Generates the box for an element placed in a block box.
 	/// @param[out] box The box to be built.
 	/// @param[out] min_height The minimum height of the element's box.
 	/// @param[out] max_height The maximum height of the element's box.
 	/// @param[in] containing_box The block box containing the element.
 	/// @param[in] element The element to build the box for.
-	/// @param[in] inline_element True if the element is placed in an inline context, false if not.
-	/// @param[in] override_shrink_to_fit_width Provide a fixed shrink-to-fit width instead of formatting the element when its properties allow shrinking.
-	static void BuildBox(Box& box, float& min_height, float& max_height, LayoutBlockBox* containing_box, Element* element, bool inline_element, float override_shrink_to_fit_width = -1);
+	/// @param[in] box_context The formatting context in which the box is generated.
+	/// @param[in] override_shrink_to_fit_width Provide a fixed shrink-to-fit width instead of formatting the element when its properties allow
+	/// shrinking.
+	static void BuildBox(Box& box, float& min_height, float& max_height, LayoutBlockBox* containing_box, Element* element,
+		BoxContext box_context = BoxContext::Block, float override_shrink_to_fit_width = -1);
 
 	// Retrieves the minimum and maximum width from an element's computed values.
 	static void GetMinMaxWidth(float& min_width, float& max_width, const ComputedValues& computed, const Box& box, float containing_block_width);
@@ -67,44 +86,63 @@ public:
 	// Retrieves the minimum and maximum height from an element's computed values.
 	static void GetMinMaxHeight(float& min_height, float& max_height, const ComputedValues& computed, const Box& box, float containing_block_height);
 
-	// Retrieves the minimum and maximum height, set to the box's content height if it is definite (>= 0), otherwise retrieves the minimum and maximum heights from an element's computed values.
-	static void GetDefiniteMinMaxHeight(float& min_height, float& max_height, const ComputedValues& computed, const Box& box, float containing_block_height);
+	// Retrieves the minimum and maximum height, set to the box's content height if it is definite (>= 0), otherwise retrieves the minimum and maximum
+	// heights from an element's computed values.
+	static void GetDefiniteMinMaxHeight(float& min_height, float& max_height, const ComputedValues& computed, const Box& box,
+		float containing_block_height);
 
 	/// Returns the fully-resolved, fixed-width and -height containing block from a block box.
 	/// @param[in] containing_box The leaf box.
 	/// @return The dimensions of the content area, using the latest fixed dimensions for width and height in the hierarchy.
 	static Vector2f GetContainingBlock(const LayoutBlockBox* containing_box);
 
-	/// Builds margins of a Box, and resolves any auto width or height for non-inline elements. The height may be left unresolved if it depends on the element's children.
-	/// @param[in,out] box The box to generate. The padding and borders must be set on the box already. The content area is used instead of the width and height properties, and -1 means auto width/height.
+	/// Builds margins of a Box, and resolves any auto width or height for non-inline elements. The height may be left unresolved if it depends on the
+	/// element's children.
+	/// @param[in,out] box The box to generate. The padding and borders must be set on the box already. The content area is used instead of the width
+	/// and height properties, and -1 means auto width/height.
 	/// @param[in] min_size The element's minimum width and height.
 	/// @param[in] max_size The element's maximum width and height.
 	/// @param[in] containing_block The size of the containing block.
 	/// @param[in] element The element the box is being generated for.
-	/// @param[in] inline_element True when the element is an inline element.
+	/// @param[in] box_context The formatting context in which the box is generated.
 	/// @param[in] replaced_element True when the element is a replaced element.
-	/// @param[in] override_shrink_to_fit_width Provide a fixed shrink-to-fit width instead of formatting the element when its properties allow shrinking.
-	static void BuildBoxSizeAndMargins(Box& box, Vector2f min_size, Vector2f max_size, Vector2f containing_block, Element* element, bool inline_element, bool replaced_element, float override_shrink_to_fit_width = -1);
+	/// @param[in] override_shrink_to_fit_width Provide a fixed shrink-to-fit width instead of formatting the element when its properties allow
+	/// shrinking.
+	static void BuildBoxSizeAndMargins(Box& box, Vector2f min_size, Vector2f max_size, Vector2f containing_block, Element* element,
+		BoxContext box_context, bool replaced_element, float override_shrink_to_fit_width = -1);
 
-private:
 	/// Formats the element and returns the width of its contents.
 	static float GetShrinkToFitWidth(Element* element, Vector2f containing_block);
 
+	/// Build computed axis size along the horizontal direction (width and friends).
+	static ComputedAxisSize BuildComputedHorizontalSize(const ComputedValues& computed);
+	/// Build computed axis size along the vertical direction (height and friends).
+	static ComputedAxisSize BuildComputedVerticalSize(const ComputedValues& computed);
+	/// Resolve edge sizes from a computed axis size.
+	static void GetEdgeSizes(float& margin_a, float& margin_b, float& padding_border_a, float& padding_border_b,
+		const ComputedAxisSize& computed_size, float base_value);
+
+private:
 	/// Calculates and returns the content size for replaced elements.
-	static Vector2f CalculateSizeForReplacedElement(Vector2f specified_content_size, Vector2f min_size, Vector2f max_size, Vector2f intrinsic_size, float intrinsic_ratio);
+	static Vector2f CalculateSizeForReplacedElement(Vector2f specified_content_size, Vector2f min_size, Vector2f max_size, Vector2f intrinsic_size,
+		float intrinsic_ratio);
 
 	/// Builds the block-specific width and horizontal margins of a Box.
-	/// @param[in,out] box The box to generate. The padding and borders must be set on the box already. The content area is used instead of the width property, and -1 means auto width.
+	/// @param[in,out] box The box to generate. The padding and borders must be set on the box already. The content area is used instead of the width
+	/// property, and -1 means auto width.
 	/// @param[in] computed The computed values of the element the box is being generated for.
 	/// @param[in] min_width The minimum content width of the element.
 	/// @param[in] max_width The maximum content width of the element.
 	/// @param[in] containing_block The size of the containing block.
 	/// @param[in] element The element the box is being generated for.
 	/// @param[in] replaced_element True when the element is a replaced element.
-	/// @param[in] override_shrink_to_fit_width Provide a fixed shrink-to-fit width instead of formatting the element when its properties allow shrinking.
-	static void BuildBoxWidth(Box& box, const ComputedValues& computed, float min_width, float max_width, Vector2f containing_block, Element* element, bool replaced_element, float override_shrink_to_fit_width = -1);
+	/// @param[in] override_shrink_to_fit_width Provide a fixed shrink-to-fit width instead of formatting the element when its properties allow
+	/// shrinking.
+	static void BuildBoxWidth(Box& box, const ComputedValues& computed, float min_width, float max_width, Vector2f containing_block, Element* element,
+		bool replaced_element, float override_shrink_to_fit_width = -1);
 	/// Builds the block-specific height and vertical margins of a Box.
-	/// @param[in,out] box The box to generate. The padding and borders must be set on the box already. The content area is used instead of the height property, and -1 means auto height.
+	/// @param[in,out] box The box to generate. The padding and borders must be set on the box already. The content area is used instead of the height
+	/// property, and -1 means auto height.
 	/// @param[in] computed The computed values of the element the box is being generated for.
 	/// @param[in] min_height The minimum content height of the element.
 	/// @param[in] max_height The maximum content height of the element.

+ 97 - 39
Source/Core/LayoutEngine.cpp

@@ -29,6 +29,7 @@
 #include "LayoutEngine.h"
 #include "LayoutBlockBoxSpace.h"
 #include "LayoutDetails.h"
+#include "LayoutFlex.h"
 #include "LayoutInlineBoxText.h"
 #include "LayoutTable.h"
 #include "Pool.h"
@@ -72,7 +73,7 @@ void LayoutEngine::FormatElement(Element* element, Vector2f containing_block, co
 	if (override_initial_box)
 		box = *override_initial_box;
 	else
-		LayoutDetails::BuildBox(box, containing_block, element, false);
+		LayoutDetails::BuildBox(box, containing_block, element);
 
 	float min_height, max_height;
 	LayoutDetails::GetDefiniteMinMaxHeight(min_height, max_height, element->GetComputedValues(), box, containing_block.y);
@@ -145,14 +146,34 @@ bool LayoutEngine::FormatElement(LayoutBlockBox* block_context_box, Element* ele
 	if (FormatElementSpecial(block_context_box, element))
 		return true;
 
+	const Style::Display display = element->GetDisplay();
+
 	// Fetch the display property, and don't lay this element out if it is set to a display type of none.
-	if (computed.display == Style::Display::None)
+	if (display == Style::Display::None)
 		return true;
 
+	// Tables and flex boxes need to be specially handled when they are absolutely positioned or floated. Currently it is assumed for both
+	// FormatElement(element, containing_block) and GetShrinkToFitWidth(), and possibly others, that they are strictly called on block boxes.
+	// The mentioned functions need to be updated if we want to support all combinations of display, position, and float properties.
+	auto uses_unsupported_display_position_float_combination = [display, element](const char* abs_positioned_or_floated) -> bool {
+		if (display == Style::Display::Flex || display == Style::Display::Table)
+		{
+			const char* element_type = (display == Style::Display::Flex ? "Flex" : "Table");
+			Log::Message(Log::LT_WARNING,
+				"%s elements cannot be %s. Instead, wrap it within a parent block element which is %s. Element will not be formatted: %s",
+				element_type, abs_positioned_or_floated, abs_positioned_or_floated, element->GetAddress().c_str());
+			return true;
+		}
+		return false;
+	};
+
 	// Check for an absolute position; if this has been set, then we remove it from the flow and add it to the current
 	// block box to be laid out and positioned once the block has been closed and sized.
 	if (computed.position == Style::Position::Absolute || computed.position == Style::Position::Fixed)
 	{
+		if (uses_unsupported_display_position_float_combination("absolutely positioned"))
+			return true;
+
 		// Display the element as a block element.
 		block_context_box->AddAbsoluteElement(element);
 		return true;
@@ -161,16 +182,20 @@ bool LayoutEngine::FormatElement(LayoutBlockBox* block_context_box, Element* ele
 	// If the element is floating, we remove it from the flow.
 	if (computed.float_ != Style::Float::None)
 	{
+		if (uses_unsupported_display_position_float_combination("floated"))
+			return true;
+
 		LayoutEngine::FormatElement(element, LayoutDetails::GetContainingBlock(block_context_box));
 		return block_context_box->AddFloatElement(element);
 	}
 
-	// The element is nothing exceptional, so we treat it as a normal block, inline or replaced element.
-	switch (computed.display)
+	// The element is nothing exceptional, so format it according to its display property.
+	switch (display)
 	{
 		case Style::Display::Block:       return FormatElementBlock(block_context_box, element);
 		case Style::Display::Inline:      return FormatElementInline(block_context_box, element);
 		case Style::Display::InlineBlock: return FormatElementInlineBlock(block_context_box, element);
+		case Style::Display::Flex:        return FormatElementFlex(block_context_box, element);
 		case Style::Display::Table:       return FormatElementTable(block_context_box, element);
 
 		case Style::Display::TableRow:
@@ -179,37 +204,9 @@ bool LayoutEngine::FormatElement(LayoutBlockBox* block_context_box, Element* ele
 		case Style::Display::TableColumnGroup:
 		case Style::Display::TableCell:
 		{
-			// These elements should have been handled within FormatElementTable.
-			// See if we are located in an absolutely positioned or floating table element. Then,
-			// we will have issues and end up here because these properties establish a new block 
-			// formatting context, but then tables need to be specially handled and they are not
-			// yet. Both FormatElement(element, containing_block) and GetShrinkToFitWidth() need
-			// to handle tables if we want to fix this.
-			Element* table_ancestor = element->GetParentNode();
-			while (table_ancestor && table_ancestor->GetDisplay() != Style::Display::Table)
-				table_ancestor = table_ancestor->GetParentNode();
-
-			if (table_ancestor)
-			{
-				const auto float_ = table_ancestor->GetFloat();
-				const auto position = table_ancestor->GetPosition();
-				const char* warning_msg = nullptr;
-
-				if (float_ != Style::Float::None)
-					warning_msg = "Table element cannot be floating. Instead, wrap it within a floating parent element.";
-				else if (position == Style::Position::Absolute || position == Style::Position::Fixed)
-					warning_msg = "Table element cannot be absolutely positioned. Instead, wrap it within an absolutely positioned parent element.";
-
-				if (warning_msg)
-				{
-					Log::Message(Log::LT_WARNING, "%s In element %s", warning_msg, table_ancestor->GetAddress().c_str());
-					return true;
-				}
-			}
-
-			// Seems like our issue isn't with the table element, instead we're encountering table parts in the wild!
+			// These elements should have been handled within FormatElementTable, seems like we're encountering table parts in the wild.
 			const Property* display_property = element->GetProperty(PropertyId::Display);
-			Log::Message(Log::LT_WARNING, "Element has a display type '%s', but is not located in a table. It will not be formatted. In element %s",
+			Log::Message(Log::LT_WARNING, "Element has a display type '%s', but is not located in a table. Element will not be formatted: %s",
 				display_property ? display_property->ToString().c_str() : "*unknown*",
 				element->GetAddress().c_str()
 			);
@@ -228,7 +225,7 @@ bool LayoutEngine::FormatElementBlock(LayoutBlockBox* block_context_box, Element
 
 	Box box;
 	float min_height, max_height;
-	LayoutDetails::BuildBox(box, min_height, max_height, block_context_box, element, false);
+	LayoutDetails::BuildBox(box, min_height, max_height, block_context_box, element);
 
 	LayoutBlockBox* new_block_context_box = block_context_box->AddBlockElement(element, box, min_height, max_height);
 	if (new_block_context_box == nullptr)
@@ -280,7 +277,7 @@ bool LayoutEngine::FormatElementInline(LayoutBlockBox* block_context_box, Elemen
 	const Vector2f containing_block = LayoutDetails::GetContainingBlock(block_context_box);
 
 	Box box;
-	LayoutDetails::BuildBox(box, containing_block, element, true);
+	LayoutDetails::BuildBox(box, containing_block, element, BoxContext::Inline);
 	LayoutInlineBox* inline_box = block_context_box->AddInlineElement(element, box);
 
 	// Format the element's children.
@@ -310,6 +307,67 @@ bool LayoutEngine::FormatElementInlineBlock(LayoutBlockBox* block_context_box, E
 	return true;
 }
 
+bool LayoutEngine::FormatElementFlex(LayoutBlockBox* block_context_box, Element* element)
+{
+	const ComputedValues& computed = element->GetComputedValues();
+	const Vector2f containing_block = LayoutDetails::GetContainingBlock(block_context_box);
+	RMLUI_ASSERT(containing_block.x >= 0.f);
+
+	// Build the initial box as specified by the flex's style, as if it was a normal block element.
+	Box box;
+	LayoutDetails::BuildBox(box, containing_block, element, BoxContext::Block);
+
+	Vector2f min_size, max_size;
+	LayoutDetails::GetMinMaxWidth(min_size.x, max_size.x, computed, box, containing_block.x);
+	LayoutDetails::GetMinMaxHeight(min_size.y, max_size.y, computed, box, containing_block.y);
+
+	// Add the flex container element as if it was a normal block element.
+	LayoutBlockBox* flex_block_context_box = block_context_box->AddBlockElement(element, box, min_size.y, max_size.y);
+	if (!flex_block_context_box)
+		return false;
+
+	// Format the flexbox and all its children.
+	ElementList absolutely_positioned_elements;
+	Vector2f formatted_content_size, content_overflow_size;
+	LayoutFlex::Format(
+		box, min_size, max_size, containing_block, element, formatted_content_size, content_overflow_size, absolutely_positioned_elements);
+
+	// Set the box content size to match the one determined by the formatting procedure.
+	flex_block_context_box->GetBox().SetContent(formatted_content_size);
+	// Set the inner content size so that any overflow can be caught.
+	flex_block_context_box->ExtendInnerContentSize(content_overflow_size);
+
+	// Finally, add any absolutely positioned flex children.
+	for (Element* abs_element : absolutely_positioned_elements)
+		flex_block_context_box->AddAbsoluteElement(abs_element);
+
+	// Close the block box, this may result in scrollbars being added to ourself or our parent.
+	const auto close_result = flex_block_context_box->Close();
+	if (close_result == LayoutBlockBox::LAYOUT_PARENT)
+	{
+		// Scollbars added to parent, bail out to reformat all its children.
+		return false;
+	}
+	else if (close_result == LayoutBlockBox::LAYOUT_SELF)
+	{
+		// Scrollbars added to flex container, it needs to be formatted again to account for changed width or height.
+		absolutely_positioned_elements.clear();
+
+		LayoutFlex::Format(
+			box, min_size, max_size, containing_block, element, formatted_content_size, content_overflow_size, absolutely_positioned_elements);
+
+		flex_block_context_box->GetBox().SetContent(formatted_content_size);
+		flex_block_context_box->ExtendInnerContentSize(content_overflow_size);
+
+		if (flex_block_context_box->Close() == LayoutBlockBox::LAYOUT_PARENT)
+			return false;
+	}
+
+	element->OnLayout();
+
+	return true;
+}
+
 
 bool LayoutEngine::FormatElementTable(LayoutBlockBox* block_context_box, Element* element_table)
 {
@@ -317,9 +375,9 @@ bool LayoutEngine::FormatElementTable(LayoutBlockBox* block_context_box, Element
 
 	const Vector2f containing_block = LayoutDetails::GetContainingBlock(block_context_box);
 
-	// Build the initial box as specified by the table's style, as if it were a normal block element.
+	// Build the initial box as specified by the table's style, as if it was a normal block element.
 	Box box;
-	LayoutDetails::BuildBox(box, containing_block, element_table, false);
+	LayoutDetails::BuildBox(box, containing_block, element_table, BoxContext::Block);
 
 	Vector2f min_size, max_size;
 	LayoutDetails::GetMinMaxWidth(min_size.x, max_size.x, computed_table, box, containing_block.x);
@@ -335,7 +393,7 @@ bool LayoutEngine::FormatElementTable(LayoutBlockBox* block_context_box, Element
 	if (final_content_size != initial_content_size)
 	{
 		// Perform this step to re-evaluate any auto margins.
-		LayoutDetails::BuildBoxSizeAndMargins(box, min_size, max_size, containing_block, element_table, false, true);
+		LayoutDetails::BuildBoxSizeAndMargins(box, min_size, max_size, containing_block, element_table, BoxContext::Block, true);
 	}
 
 	// Now that the box is finalized, we can add table as a block element. If we did it earlier, eg. just before formatting the table,

+ 6 - 1
Source/Core/LayoutEngine.h

@@ -43,7 +43,8 @@ class Box;
 class LayoutEngine
 {
 public:
-	/// Formats the contents for a root-level element (usually a document, floating or replaced element). Establishes a new block formatting context.
+	/// Formats the contents for a root-level element, usually a document, absolutely positioned, floating, or replaced element. Establishes a new
+	/// block formatting context.
 	/// @param[in] element The element to lay out.
 	/// @param[in] containing_block The size of the containing block.
 	/// @param[in] override_initial_box Optional pointer to a box to override the generated box for the element.
@@ -71,6 +72,10 @@ private:
 	/// @param[in] block_context_box The open block box to layout the element in.
 	/// @param[in] element The inline-block element.
 	static bool FormatElementInlineBlock(LayoutBlockBox* block_context_box, Element* element);
+	/// Formats and positions a flexbox.
+	/// @param[in] block_context_box The open block box to layout the element in.
+	/// @param[in] element The flex container element.
+	static bool FormatElementFlex(LayoutBlockBox* block_context_box, Element* element);
 	/// Formats and positions a table, including all table-rows and table-cells contained within.
 	/// @param[in] block_context_box The open block box to layout the element in.
 	/// @param[in] element The table element.

+ 848 - 0
Source/Core/LayoutFlex.cpp

@@ -0,0 +1,848 @@
+/*
+ * 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 "LayoutFlex.h"
+#include "../../Include/RmlUi/Core/Element.h"
+#include "../../Include/RmlUi/Core/ElementScroll.h"
+#include "../../Include/RmlUi/Core/Types.h"
+#include "LayoutDetails.h"
+#include "LayoutEngine.h"
+#include <algorithm>
+#include <float.h>
+#include <numeric>
+
+namespace Rml {
+
+void LayoutFlex::Format(const Box& box, const Vector2f min_size, const Vector2f max_size, const Vector2f flex_containing_block, Element* element_flex,
+	Vector2f& out_formatted_content_size, Vector2f& out_content_overflow_size, ElementList& out_absolutely_positioned_elements)
+{
+	ElementScroll* element_scroll = element_flex->GetElementScroll();
+	const Vector2f scrollbar_size = {element_scroll->GetScrollbarSize(ElementScroll::VERTICAL),
+		element_scroll->GetScrollbarSize(ElementScroll::HORIZONTAL)};
+
+	Vector2f flex_content_offset = box.GetPosition();
+
+	const Vector2f box_content_size = box.GetSize();
+	const bool auto_height = (box_content_size.y < 0.0f);
+
+	Vector2f flex_available_content_size = Math::Max(box_content_size - scrollbar_size, Vector2f(0.f));
+	Vector2f flex_content_containing_block = flex_available_content_size;
+
+	if (auto_height)
+	{
+		flex_available_content_size.y = -1.f; // Negative means infinite space
+		flex_content_containing_block.y = flex_containing_block.y;
+	}
+
+	Math::SnapToPixelGrid(flex_content_offset, flex_available_content_size);
+
+	// Construct the layout object and format the table.
+	LayoutFlex layout_flex(element_flex, flex_available_content_size, flex_content_containing_block, flex_content_offset, min_size, max_size,
+		out_absolutely_positioned_elements);
+
+	layout_flex.Format();
+
+	// Output the size of the formatted flexbox. The width is determined as a normal block box so we don't need to change that.
+	out_formatted_content_size = box_content_size;
+	if (auto_height)
+		out_formatted_content_size = Vector2f(box_content_size.x, layout_flex.flex_resulting_content_size.y + scrollbar_size.y);
+
+	out_content_overflow_size = layout_flex.flex_content_overflow_size;
+}
+
+LayoutFlex::LayoutFlex(Element* element_flex, Vector2f flex_available_content_size, Vector2f flex_content_containing_block,
+	Vector2f flex_content_offset, Vector2f flex_min_size, Vector2f flex_max_size, ElementList& absolutely_positioned_elements) :
+	element_flex(element_flex),
+	flex_available_content_size(flex_available_content_size), flex_content_containing_block(flex_content_containing_block),
+	flex_content_offset(flex_content_offset), flex_min_size(flex_min_size), flex_max_size(flex_max_size),
+	absolutely_positioned_elements(absolutely_positioned_elements)
+{}
+
+struct FlexItem {
+	// In the following, suffix '_a' means flex start edge while '_b' means flex end edge.
+	struct Size {
+		bool auto_margin_a, auto_margin_b;
+		bool auto_size;
+		float margin_a, margin_b;
+		float sum_edges_a;        // Start edge: margin (non-auto) + border + padding
+		float sum_edges;          // Inner->outer size
+		float min_size, max_size; // Inner size
+	};
+
+	Element* element;
+	Box box;
+
+	// Filled during the build step.
+	Size main;
+	Size cross;
+	float flex_shrink_factor;
+	float flex_grow_factor;
+	Style::AlignSelf align_self; // 'Auto' is replaced by container's 'align-items' value
+
+	float inner_flex_base_size;   // Inner size
+	float flex_base_size;         // Outer size
+	float hypothetical_main_size; // Outer size
+
+	// Used for resolving flexible length
+	enum class Violation : std::uint8_t { None = 0, Min, Max };
+	bool frozen;
+	Violation violation;
+	float target_main_size; // Outer size
+	float used_main_size;   // Outer size (without auto margins)
+	float main_auto_margin_size_a, main_auto_margin_size_b;
+	float main_offset;
+
+	// Used for resolving cross size
+	float hypothetical_cross_size; // Outer size
+	float used_cross_size;         // Outer size
+	float cross_offset;            // Offset within line
+	float cross_baseline_top;      // Only used for baseline cross alignment
+};
+
+struct FlexLine {
+	FlexLine(Vector<FlexItem>&& items) : items(std::move(items)) {}
+	Vector<FlexItem> items;
+	float accumulated_hypothetical_main_size = 0;
+	float cross_size = 0; // Excludes line spacing
+	float cross_spacing_a = 0, cross_spacing_b = 0;
+	float cross_offset = 0;
+};
+
+struct FlexContainer {
+	Vector<FlexLine> lines;
+};
+
+static void GetItemSizing(FlexItem::Size& destination, const ComputedAxisSize& computed_size, const float base_value, const bool direction_reverse)
+{
+	float margin_a, margin_b, padding_border_a, padding_border_b;
+	LayoutDetails::GetEdgeSizes(margin_a, margin_b, padding_border_a, padding_border_b, computed_size, base_value);
+
+	const float padding_border = padding_border_a + padding_border_b;
+	const float margin = margin_a + margin_b;
+
+	destination.auto_margin_a = (computed_size.margin_a.type == Style::Margin::Auto);
+	destination.auto_margin_b = (computed_size.margin_b.type == Style::Margin::Auto);
+
+	destination.auto_size = (computed_size.size.type == Style::LengthPercentageAuto::Auto);
+
+	destination.margin_a = margin_a;
+	destination.margin_b = margin_b;
+	destination.sum_edges = padding_border + margin;
+	destination.sum_edges_a = (direction_reverse ? padding_border_b + margin_b : padding_border_a + margin_a);
+
+	destination.min_size = ResolveValue(computed_size.min_size, base_value);
+	destination.max_size = (computed_size.max_size.value < 0.f ? FLT_MAX : ResolveValue(computed_size.max_size, base_value));
+
+	if (computed_size.box_sizing == Style::BoxSizing::BorderBox)
+	{
+		destination.min_size = Math::Max(0.0f, destination.min_size - padding_border);
+		if (destination.max_size < FLT_MAX)
+			destination.max_size = Math::Max(0.0f, destination.max_size - padding_border);
+	}
+
+	if (direction_reverse)
+	{
+		std::swap(destination.auto_margin_a, destination.auto_margin_b);
+		std::swap(destination.margin_a, destination.margin_b);
+	}
+}
+
+void LayoutFlex::Format()
+{
+	// The following procedure is generally based on the CSS flexible box layout algorithm.
+	// For details, see https://drafts.csswg.org/css-flexbox/#layout-algorithm
+
+	const ComputedValues& computed_flex = element_flex->GetComputedValues();
+	const Style::FlexDirection direction = computed_flex.flex_direction;
+
+	const bool main_axis_horizontal = (direction == Style::FlexDirection::Row || direction == Style::FlexDirection::RowReverse);
+	const bool direction_reverse = (direction == Style::FlexDirection::RowReverse || direction == Style::FlexDirection::ColumnReverse);
+	const bool flex_single_line = (computed_flex.flex_wrap == Style::FlexWrap::Nowrap);
+	const bool wrap_reverse = (computed_flex.flex_wrap == Style::FlexWrap::WrapReverse);
+
+	const float main_available_size = (main_axis_horizontal ? flex_available_content_size.x : flex_available_content_size.y);
+	const float cross_available_size = (!main_axis_horizontal ? flex_available_content_size.x : flex_available_content_size.y);
+
+	const float main_min_size = (main_axis_horizontal ? flex_min_size.x : flex_min_size.y);
+	const float main_max_size = (main_axis_horizontal ? flex_max_size.x : flex_max_size.y);
+	const float cross_min_size = (main_axis_horizontal ? flex_min_size.y : flex_min_size.x);
+	const float cross_max_size = (main_axis_horizontal ? flex_max_size.y : flex_max_size.x);
+
+	// For the purpose of placing items we make infinite size a big value.
+	const float main_wrap_size = Math::Clamp(main_available_size < 0.0f ? FLT_MAX : main_available_size, main_min_size, main_max_size);
+
+	// For the purpose of resolving lengths, infinite main size becomes zero.
+	const float main_size_base_value = (main_available_size < 0.0f ? 0.0f : main_available_size);
+	const float cross_size_base_value = (cross_available_size < 0.0f ? 0.0f : cross_available_size);
+
+	// -- Build a list of all flex items with base size information --
+	Vector<FlexItem> items;
+
+	const int num_flex_children = element_flex->GetNumChildren();
+	for (int i = 0; i < num_flex_children; i++)
+	{
+		Element* element = element_flex->GetChild(i);
+		const ComputedValues& computed = element->GetComputedValues();
+
+		if (computed.display == Style::Display::None)
+		{
+			continue;
+		}
+		else if (computed.position == Style::Position::Absolute || computed.position == Style::Position::Fixed)
+		{
+			absolutely_positioned_elements.push_back(element);
+			continue;
+		}
+
+		FlexItem item = {};
+		item.element = element;
+		LayoutDetails::BuildBox(item.box, flex_content_containing_block, element, BoxContext::FlexOrTable, 0.0f);
+
+		Style::LengthPercentageAuto item_main_size;
+
+		{
+			const ComputedAxisSize computed_main_size =
+				main_axis_horizontal ? LayoutDetails::BuildComputedHorizontalSize(computed) : LayoutDetails::BuildComputedVerticalSize(computed);
+			const ComputedAxisSize computed_cross_size =
+				!main_axis_horizontal ? LayoutDetails::BuildComputedHorizontalSize(computed) : LayoutDetails::BuildComputedVerticalSize(computed);
+
+			GetItemSizing(item.main, computed_main_size, main_size_base_value, direction_reverse);
+			GetItemSizing(item.cross, computed_cross_size, cross_size_base_value, wrap_reverse);
+
+			item_main_size = computed_main_size.size;
+		}
+
+		item.flex_shrink_factor = computed.flex_shrink;
+		item.flex_grow_factor = computed.flex_grow;
+		item.align_self = computed.align_self;
+
+		static_assert(int(Style::AlignSelf::FlexStart) == int(Style::AlignItems::FlexStart) + 1 &&
+				int(Style::AlignSelf::Stretch) == int(Style::AlignItems::Stretch) + 1,
+			"It is assumed below that align items is a shifted version (no auto value) of align self.");
+
+		// Use the container's align-items property if align-self is auto.
+		if (item.align_self == Style::AlignSelf::Auto)
+			item.align_self = static_cast<Style::AlignSelf>(static_cast<int>(computed_flex.align_items) + 1);
+
+		const float sum_padding_border = item.main.sum_edges - (item.main.margin_a + item.main.margin_b);
+
+		// Find the flex base size (possibly negative when using border box sizing)
+		if (computed.flex_basis.type != Style::FlexBasis::Auto)
+		{
+			item.inner_flex_base_size = ResolveValue(computed.flex_basis, main_size_base_value);
+			if (computed.box_sizing == Style::BoxSizing::BorderBox)
+				item.inner_flex_base_size -= sum_padding_border;
+		}
+		else if (!item.main.auto_size)
+		{
+			item.inner_flex_base_size = ResolveValue(item_main_size, main_size_base_value);
+			if (computed.box_sizing == Style::BoxSizing::BorderBox)
+				item.inner_flex_base_size -= sum_padding_border;
+		}
+		else if (main_axis_horizontal)
+		{
+			item.inner_flex_base_size = LayoutDetails::GetShrinkToFitWidth(element, flex_content_containing_block);
+		}
+		else
+		{
+			const Vector2f initial_box_size = item.box.GetSize();
+			RMLUI_ASSERT(initial_box_size.y < 0.f);
+
+			Box format_box = item.box;
+			if (initial_box_size.x < 0.f)
+				format_box.SetContent(Vector2f(flex_available_content_size.x - item.cross.sum_edges, initial_box_size.y));
+
+			LayoutEngine::FormatElement(element, flex_content_containing_block, &format_box);
+			item.inner_flex_base_size = element->GetBox().GetSize().y;
+		}
+
+		// Calculate the hypothetical main size (clamped flex base size).
+		item.hypothetical_main_size = Math::Clamp(item.inner_flex_base_size, item.main.min_size, item.main.max_size) + item.main.sum_edges;
+		item.flex_base_size = item.inner_flex_base_size + item.main.sum_edges;
+
+		items.push_back(std::move(item));
+	}
+
+	if (items.empty())
+	{
+		return;
+	}
+
+	// -- Collect the items into lines --
+	FlexContainer container;
+
+	if (flex_single_line)
+	{
+		container.lines.emplace_back(std::move(items));
+	}
+	else
+	{
+		float cursor = 0;
+
+		Vector<FlexItem> line_items;
+
+		for (FlexItem& item : items)
+		{
+			cursor += item.hypothetical_main_size;
+
+			if (!line_items.empty() && cursor > main_wrap_size)
+			{
+				// Break into new line.
+				container.lines.emplace_back(std::move(line_items));
+				cursor = item.hypothetical_main_size;
+				line_items = {std::move(item)};
+			}
+			else
+			{
+				// Add item to current line.
+				line_items.push_back(std::move(item));
+			}
+		}
+
+		if (!line_items.empty())
+			container.lines.emplace_back(std::move(line_items));
+
+		items.clear();
+		items.shrink_to_fit();
+	}
+
+	for (FlexLine& line : container.lines)
+	{
+		line.accumulated_hypothetical_main_size = std::accumulate(line.items.begin(), line.items.end(), 0.0f,
+			[](float value, const FlexItem& item) { return value + item.hypothetical_main_size; });
+	}
+
+	// If the available main size is infinite, the used main size becomes the accumulated outer size of all items of the widest line.
+	const float used_main_size_unconstrained = main_available_size >= 0.f
+		? main_available_size
+		: std::max_element(container.lines.begin(), container.lines.end(), [](const FlexLine& a, const FlexLine& b) {
+			  return a.accumulated_hypothetical_main_size < b.accumulated_hypothetical_main_size;
+		  })->accumulated_hypothetical_main_size;
+
+	const float used_main_size = Math::Clamp(used_main_size_unconstrained, main_min_size, main_max_size);
+
+	// -- Determine main size --
+	// Resolve flexible lengths to find the used main size of all items.
+	for (FlexLine& line : container.lines)
+	{
+		const float available_flex_space = used_main_size - line.accumulated_hypothetical_main_size; // Possibly negative
+
+		const bool flex_mode_grow = (available_flex_space > 0.f);
+
+		auto FlexFactor = [flex_mode_grow](const FlexItem& item) { return (flex_mode_grow ? item.flex_grow_factor : item.flex_shrink_factor); };
+
+		// Initialize items and freeze inflexible items.
+		for (FlexItem& item : line.items)
+		{
+			item.target_main_size = item.flex_base_size;
+
+			if (FlexFactor(item) == 0.f || (flex_mode_grow && item.flex_base_size > item.hypothetical_main_size) ||
+				(!flex_mode_grow && item.flex_base_size < item.hypothetical_main_size))
+			{
+				item.frozen = true;
+				item.target_main_size = item.hypothetical_main_size;
+			}
+		}
+
+		auto RemainingFreeSpace = [used_main_size, &line]() {
+			return used_main_size - std::accumulate(line.items.begin(), line.items.end(), 0.f, [](float value, const FlexItem& item) {
+				return value + (item.frozen ? item.target_main_size : item.flex_base_size);
+			});
+		};
+
+		const float initial_free_space = RemainingFreeSpace();
+
+		// Now iteratively distribute or shrink the size of all the items, until all the items are frozen.
+		while (!std::all_of(line.items.begin(), line.items.end(), [](const FlexItem& item) { return item.frozen; }))
+		{
+			float remaining_free_space = RemainingFreeSpace();
+
+			const float flex_factor_sum = std::accumulate(line.items.begin(), line.items.end(), 0.f,
+				[&FlexFactor](float value, const FlexItem& item) { return value + (item.frozen ? 0.0f : FlexFactor(item)); });
+
+			if (flex_factor_sum < 1.f)
+			{
+				const float scaled_initial_free_space = initial_free_space * flex_factor_sum;
+				if (Math::AbsoluteValue(scaled_initial_free_space) < Math::AbsoluteValue(remaining_free_space))
+					remaining_free_space = scaled_initial_free_space;
+			}
+
+			if (remaining_free_space != 0.f)
+			{
+				// Distribute free space proportionally to flex factors
+				if (flex_mode_grow)
+				{
+					for (FlexItem& item : line.items)
+					{
+						if (!item.frozen)
+						{
+							const float distribute_ratio = item.flex_grow_factor / flex_factor_sum;
+							item.target_main_size = item.flex_base_size + distribute_ratio * remaining_free_space;
+						}
+					}
+				}
+				else
+				{
+					const float scaled_flex_shrink_factor_sum =
+						std::accumulate(line.items.begin(), line.items.end(), 0.f, [](float value, const FlexItem& item) {
+							return value + (item.frozen ? 0.0f : item.flex_shrink_factor * item.inner_flex_base_size);
+						});
+					const float scaled_flex_shrink_factor_sum_nonzero = (scaled_flex_shrink_factor_sum == 0 ? 1 : scaled_flex_shrink_factor_sum);
+
+					for (FlexItem& item : line.items)
+					{
+						if (!item.frozen)
+						{
+							const float scaled_flex_shrink_factor = item.flex_shrink_factor * item.inner_flex_base_size;
+							const float distribute_ratio = scaled_flex_shrink_factor / scaled_flex_shrink_factor_sum_nonzero;
+							item.target_main_size = item.flex_base_size - distribute_ratio * Math::AbsoluteValue(remaining_free_space);
+						}
+					}
+				}
+			}
+
+			// Clamp min/max violations
+			float total_minmax_violation = 0.f;
+
+			for (FlexItem& item : line.items)
+			{
+				if (!item.frozen)
+				{
+					const float inner_target_main_size = Math::Max(0.0f, item.target_main_size - item.main.sum_edges);
+					const float clamped_target_main_size =
+						Math::Clamp(inner_target_main_size, item.main.min_size, item.main.max_size) + item.main.sum_edges;
+
+					const float violation_diff = clamped_target_main_size - item.target_main_size;
+					item.violation = (violation_diff > 0.0f ? FlexItem::Violation::Min
+															: (violation_diff < 0.f ? FlexItem::Violation::Max : FlexItem::Violation::None));
+					item.target_main_size = clamped_target_main_size;
+
+					total_minmax_violation += violation_diff;
+				}
+			}
+
+			for (FlexItem& item : line.items)
+			{
+				if (total_minmax_violation > 0.0f)
+					item.frozen |= (item.violation == FlexItem::Violation::Min);
+				else if (total_minmax_violation < 0.0f)
+					item.frozen |= (item.violation == FlexItem::Violation::Max);
+				else
+					item.frozen = true;
+			}
+		}
+
+		// Now, each item's used main size is found!
+		for (FlexItem& item : line.items)
+			item.used_main_size = item.target_main_size;
+	}
+
+	// -- Align main axis (§9.5) --
+	// Main alignment is done before cross sizing. Due to rounding to the pixel grid, the main size can
+	// change slightly after main alignment/offseting. Also, the cross sizing depends on the main sizing
+	// so doing it in this order ensures no surprises (overflow/wrapping issues) due to pixel rounding.
+	for (FlexLine& line : container.lines)
+	{
+		const float remaining_free_space = used_main_size -
+			std::accumulate(line.items.begin(), line.items.end(), 0.f, [](float value, const FlexItem& item) { return value + item.used_main_size; });
+
+		if (remaining_free_space > 0.0f)
+		{
+			const int num_auto_margins = std::accumulate(line.items.begin(), line.items.end(), 0,
+				[](int value, const FlexItem& item) { return value + int(item.main.auto_margin_a) + int(item.main.auto_margin_b); });
+
+			if (num_auto_margins > 0)
+			{
+				// Distribute the remaining space to the auto margins.
+				const float space_per_auto_margin = remaining_free_space / float(num_auto_margins);
+				for (FlexItem& item : line.items)
+				{
+					if (item.main.auto_margin_a)
+						item.main_auto_margin_size_a = space_per_auto_margin;
+					if (item.main.auto_margin_b)
+						item.main_auto_margin_size_b = space_per_auto_margin;
+				}
+			}
+			else
+			{
+				// Distribute the remaining space based on the 'justify-content' property.
+				using Style::JustifyContent;
+				const int num_items = int(line.items.size());
+
+				switch (computed_flex.justify_content)
+				{
+				case JustifyContent::SpaceBetween:
+					if (num_items > 1)
+					{
+						const float space_per_edge = remaining_free_space / float(2 * num_items - 2);
+						for (int i = 0; i < num_items; i++)
+						{
+							FlexItem& item = line.items[i];
+							if (i > 0)
+								item.main_auto_margin_size_a = space_per_edge;
+							if (i < num_items - 1)
+								item.main_auto_margin_size_b = space_per_edge;
+						}
+						break;
+					}
+					//-fallthrough
+				case JustifyContent::FlexStart:
+					line.items.back().main_auto_margin_size_b = remaining_free_space;
+					break;
+				case JustifyContent::FlexEnd:
+					line.items.front().main_auto_margin_size_a = remaining_free_space;
+					break;
+				case JustifyContent::Center:
+					line.items.front().main_auto_margin_size_a = 0.5f * remaining_free_space;
+					line.items.back().main_auto_margin_size_b = 0.5f * remaining_free_space;
+					break;
+				case JustifyContent::SpaceAround:
+				{
+					const float space_per_edge = remaining_free_space / float(2 * num_items);
+					for (FlexItem& item : line.items)
+					{
+						item.main_auto_margin_size_a = space_per_edge;
+						item.main_auto_margin_size_b = space_per_edge;
+					}
+				}
+				break;
+				}
+			}
+		}
+
+		// Now find the offset and snap the outer edges to the pixel grid.
+		float cursor = 0.0f;
+		for (FlexItem& item : line.items)
+		{
+			if (direction_reverse)
+				item.main_offset = used_main_size - (cursor + item.used_main_size + item.main_auto_margin_size_a - item.main.margin_b);
+			else
+				item.main_offset = cursor + item.main.margin_a + item.main_auto_margin_size_a;
+
+			cursor += item.used_main_size + item.main_auto_margin_size_a + item.main_auto_margin_size_b;
+			Math::SnapToPixelGrid(item.main_offset, item.used_main_size);
+		}
+	}
+
+	// -- Determine cross size (§9.4) --
+	// First, determine the cross size of each item, format it if necessary.
+	for (FlexLine& line : container.lines)
+	{
+		for (FlexItem& item : line.items)
+		{
+			const Vector2f content_size = item.box.GetSize();
+			const float used_main_size_inner = item.used_main_size - item.main.sum_edges;
+
+			if (main_axis_horizontal)
+			{
+				if (content_size.y < 0.0f)
+				{
+					item.box.SetContent(Vector2f(used_main_size_inner, content_size.y));
+					LayoutEngine::FormatElement(item.element, flex_content_containing_block, &item.box);
+					item.hypothetical_cross_size = item.element->GetBox().GetSize().y + item.cross.sum_edges;
+				}
+				else
+				{
+					item.hypothetical_cross_size = content_size.y + item.cross.sum_edges;
+				}
+			}
+			else
+			{
+				if (content_size.x < 0.0f || item.cross.auto_size)
+				{
+					item.box.SetContent(Vector2f(content_size.x, used_main_size_inner));
+					item.hypothetical_cross_size =
+						LayoutDetails::GetShrinkToFitWidth(item.element, flex_content_containing_block) + item.cross.sum_edges;
+				}
+				else
+				{
+					item.hypothetical_cross_size = content_size.x + item.cross.sum_edges;
+				}
+			}
+		}
+	}
+
+	// Determine cross size of each line.
+	if (cross_available_size >= 0.f && flex_single_line && container.lines.size() == 1)
+	{
+		container.lines[0].cross_size = cross_available_size;
+	}
+	else
+	{
+		for (FlexLine& line : container.lines)
+		{
+			const float largest_hypothetical_cross_size =
+				std::max_element(line.items.begin(), line.items.end(), [](const FlexItem& a, const FlexItem& b) {
+					return a.hypothetical_cross_size < b.hypothetical_cross_size;
+				})->hypothetical_cross_size;
+
+			// Currently, we don't handle the case where baseline alignment could extend the line's cross size, see CSS specs 9.4.8.
+			line.cross_size = Math::Max(0.0f, Math::RoundFloat(largest_hypothetical_cross_size));
+
+			if (flex_single_line)
+				line.cross_size = Math::Clamp(line.cross_size, cross_min_size, cross_max_size);
+		}
+	}
+
+	// Stretch out the lines if we have extra space.
+	if (cross_available_size >= 0.f && computed_flex.align_content == Style::AlignContent::Stretch)
+	{
+		int remaining_space = static_cast<int>(cross_available_size -
+			std::accumulate(container.lines.begin(), container.lines.end(), 0.f,
+				[](float value, const FlexLine& line) { return value + line.cross_size; }));
+
+		if (remaining_space > 0)
+		{
+			// Here we use integer math to ensure all space is distributed to pixel boundaries.
+			const int num_lines = (int)container.lines.size();
+			for (int i = 0; i < num_lines; i++)
+			{
+				const int add_space_to_line = remaining_space / (num_lines - i);
+				remaining_space -= add_space_to_line;
+				container.lines[i].cross_size += static_cast<float>(add_space_to_line);
+			}
+		}
+	}
+
+	// Determine the used cross size of items.
+	for (FlexLine& line : container.lines)
+	{
+		for (FlexItem& item : line.items)
+		{
+			const bool stretch_item = (item.align_self == Style::AlignSelf::Stretch);
+			if (stretch_item && item.cross.auto_size && !item.cross.auto_margin_a && !item.cross.auto_margin_b)
+			{
+				item.used_cross_size =
+					Math::Clamp(line.cross_size - item.cross.sum_edges, item.cross.min_size, item.cross.max_size) + item.cross.sum_edges;
+				// Here we are supposed to re-format the item with the new size, so that percentages can be resolved, see CSS specs Sec. 9.4.11. Seems
+				// very slow, we skip this for now.
+			}
+			else
+			{
+				item.used_cross_size = item.hypothetical_cross_size;
+			}
+		}
+	}
+
+	// -- Align cross axis (§9.6) --
+	for (FlexLine& line : container.lines)
+	{
+		constexpr float UndefinedBaseline = -FLT_MAX;
+		float max_baseline_edge_distance = UndefinedBaseline;
+		FlexItem* max_baseline_item = nullptr;
+
+		for (FlexItem& item : line.items)
+		{
+			const float remaining_space = line.cross_size - item.used_cross_size;
+
+			item.cross_offset = item.cross.margin_a;
+			item.cross_baseline_top = UndefinedBaseline;
+
+			const int num_auto_margins = int(item.cross.auto_margin_a) + int(item.cross.auto_margin_b);
+			if (num_auto_margins > 0)
+			{
+				const float space_per_auto_margin = Math::Max(remaining_space, 0.0f) / float(num_auto_margins);
+				item.cross_offset = item.cross.margin_a + (item.cross.auto_margin_a ? space_per_auto_margin : 0.f);
+			}
+			else
+			{
+				using Style::AlignSelf;
+				const AlignSelf align_self = item.align_self;
+
+				switch (align_self)
+				{
+				case AlignSelf::Auto:
+					// Never encountered here: should already have been replaced by container's align-items property.
+					RMLUI_ERROR;
+					break;
+				case AlignSelf::FlexStart:
+					// Do nothing, cross offset set above with this behavior.
+					break;
+				case AlignSelf::FlexEnd:
+					item.cross_offset = item.cross.margin_a + remaining_space;
+					break;
+				case AlignSelf::Center:
+					item.cross_offset = item.cross.margin_a + 0.5f * remaining_space;
+					break;
+				case AlignSelf::Baseline:
+				{
+					// We don't currently have a good way to get the true baseline here, so we make a very rough zero-effort approximation.
+					const float baseline_heuristic = 0.5f * item.element->GetLineHeight();
+					const float sum_edges_top = (wrap_reverse ? item.cross.sum_edges - item.cross.sum_edges_a : item.cross.sum_edges_a);
+
+					item.cross_baseline_top = sum_edges_top + baseline_heuristic;
+
+					const float baseline_edge_distance = (wrap_reverse ? item.used_cross_size - item.cross_baseline_top : item.cross_baseline_top);
+					if (baseline_edge_distance > max_baseline_edge_distance)
+					{
+						max_baseline_item = &item;
+						max_baseline_edge_distance = baseline_edge_distance;
+					}
+				}
+				break;
+				case AlignSelf::Stretch:
+					// Handled above
+					break;
+				}
+			}
+
+			if (wrap_reverse)
+			{
+				const float reverse_offset = line.cross_size - item.used_cross_size + item.cross.margin_a + item.cross.margin_b;
+				item.cross_offset = reverse_offset - item.cross_offset;
+			}
+		}
+
+		if (max_baseline_item)
+		{
+			// Align all baseline items such that their baselines are aligned with the one with the max. baseline distance.
+			// Cross offset for all baseline items are currently set as in 'flex-start'.
+			const float max_baseline_margin_top = (wrap_reverse ? max_baseline_item->cross.margin_b : max_baseline_item->cross.margin_a);
+			const float line_top_to_baseline_distance =
+				max_baseline_item->cross_offset - max_baseline_margin_top + max_baseline_item->cross_baseline_top;
+
+			for (FlexItem& item : line.items)
+			{
+				if (item.cross_baseline_top != UndefinedBaseline)
+				{
+					const float margin_top = (wrap_reverse ? item.cross.margin_b : item.cross.margin_a);
+					item.cross_offset = line_top_to_baseline_distance - item.cross_baseline_top + margin_top;
+				}
+			}
+		}
+
+		// Snap the outer item cross edges to the pixel grid.
+		for (FlexItem& item : line.items)
+			Math::SnapToPixelGrid(item.cross_offset, item.used_cross_size);
+	}
+
+	const float accumulated_lines_cross_size = std::accumulate(container.lines.begin(), container.lines.end(), 0.f,
+		[](float value, const FlexLine& line) { return value + line.cross_size; });
+
+	// If the available cross size is infinite, the used cross size becomes the accumulated line cross size.
+	const float used_cross_size_unconstrained = cross_available_size >= 0.f ? cross_available_size : accumulated_lines_cross_size;
+	const float used_cross_size = Math::Clamp(used_cross_size_unconstrained, cross_min_size, cross_max_size);
+
+	// Align the lines along the cross-axis.
+	{
+		const float remaining_free_space = used_cross_size - accumulated_lines_cross_size;
+		const int num_lines = int(container.lines.size());
+
+		if (remaining_free_space > 0.f)
+		{
+			using Style::AlignContent;
+
+			switch (computed_flex.align_content)
+			{
+			case AlignContent::SpaceBetween:
+				if (num_lines > 1)
+				{
+					const float space_per_edge = remaining_free_space / float(2 * num_lines - 2);
+					for (int i = 0; i < num_lines; i++)
+					{
+						FlexLine& line = container.lines[i];
+						if (i > 0)
+							line.cross_spacing_a = space_per_edge;
+						if (i < num_lines - 1)
+							line.cross_spacing_b = space_per_edge;
+					}
+				}
+				//-fallthrough
+			case AlignContent::FlexStart:
+				container.lines.back().cross_spacing_b = remaining_free_space;
+				break;
+			case AlignContent::FlexEnd:
+				container.lines.front().cross_spacing_a = remaining_free_space;
+				break;
+			case AlignContent::Center:
+				container.lines.front().cross_spacing_a = 0.5f * remaining_free_space;
+				container.lines.back().cross_spacing_b = 0.5f * remaining_free_space;
+				break;
+			case AlignContent::SpaceAround:
+			{
+				const float space_per_edge = remaining_free_space / float(2 * num_lines);
+				for (FlexLine& line : container.lines)
+				{
+					line.cross_spacing_a = space_per_edge;
+					line.cross_spacing_b = space_per_edge;
+				}
+			}
+			break;
+			case AlignContent::Stretch:
+				// Handled above.
+				break;
+			}
+		}
+
+		// Now find the offset and snap the line edges to the pixel grid.
+		float cursor = 0.f;
+		for (FlexLine& line : container.lines)
+		{
+			if (wrap_reverse)
+				line.cross_offset = used_cross_size - (cursor + line.cross_spacing_a + line.cross_size);
+			else
+				line.cross_offset = cursor + line.cross_spacing_a;
+
+			cursor += line.cross_spacing_a + line.cross_size + line.cross_spacing_b;
+			Math::SnapToPixelGrid(line.cross_offset, line.cross_size);
+		}
+	}
+
+	auto MainCrossToVec2 = [main_axis_horizontal](const float v_main, const float v_cross) {
+		return main_axis_horizontal ? Vector2f(v_main, v_cross) : Vector2f(v_cross, v_main);
+	};
+
+	// -- Format items --
+	for (FlexLine& line : container.lines)
+	{
+		for (FlexItem& item : line.items)
+		{
+			const Vector2f item_size = MainCrossToVec2(item.used_main_size - item.main.sum_edges, item.used_cross_size - item.cross.sum_edges);
+			const Vector2f item_offset = MainCrossToVec2(item.main_offset, line.cross_offset + item.cross_offset);
+
+			item.box.SetContent(item_size);
+
+			Vector2f cell_visible_overflow_size;
+			LayoutEngine::FormatElement(item.element, flex_content_containing_block, &item.box, &cell_visible_overflow_size);
+
+			// Set the position of the element within the the flex container
+			item.element->SetOffset(flex_content_offset + item_offset, element_flex);
+
+			// The cell contents may overflow, propagate this to the flex container.
+			const Vector2f overflow_size = item_offset + cell_visible_overflow_size -
+				Vector2f(item.box.GetEdge(Box::MARGIN, Box::LEFT), item.box.GetEdge(Box::MARGIN, Box::TOP));
+
+			flex_content_overflow_size.x = Math::Max(flex_content_overflow_size.x, overflow_size.x);
+			flex_content_overflow_size.y = Math::Max(flex_content_overflow_size.y, overflow_size.y);
+		}
+	}
+
+	flex_resulting_content_size = MainCrossToVec2(used_main_size, used_cross_size);
+}
+
+} // namespace Rml

+ 76 - 0
Source/Core/LayoutFlex.h

@@ -0,0 +1,76 @@
+/*
+ * 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_LAYOUTFLEX_H
+#define RMLUI_CORE_LAYOUTFLEX_H
+
+#include "../../Include/RmlUi/Core/Types.h"
+
+namespace Rml {
+
+class Box;
+
+class LayoutFlex {
+public:
+	/// Formats a flexible box, including all elements contained within.
+	/// @param[in] box The box used for dimensioning the flex container.
+	/// @param[in] min_size Minimum width and height of the flexbox.
+	/// @param[in] max_size Maximum width and height of the flexbox.
+	/// @param[in] containing_block Flexbox's containing block size.
+	/// @param[in] element_flex The flex container element.
+	/// @param[out] out_formatted_content_size The flex container element's used size.
+	/// @param[out] out_content_overflow_size  The content size of the flexbox's overflowing content.
+	/// @param[out] out_absolutely_positioned_elements List of absolutely positioned elements within the flexbox.
+	static void Format(const Box& box, Vector2f min_size, Vector2f max_size, Vector2f containing_block, Element* element_flex,
+		Vector2f& out_formatted_content_size, Vector2f& out_content_overflow_size, ElementList& out_absolutely_positioned_elements);
+
+private:
+	LayoutFlex(Element* element_flex, Vector2f flex_available_content_size, Vector2f flex_content_containing_block, Vector2f flex_content_offset,
+		Vector2f flex_min_size, Vector2f flex_max_size, ElementList& absolutely_positioned_elements);
+
+	// Format the flexbox.
+	void Format();
+
+	Element* const element_flex;
+
+	const Vector2f flex_available_content_size;
+	const Vector2f flex_content_containing_block;
+	const Vector2f flex_content_offset;
+	const Vector2f flex_min_size;
+	const Vector2f flex_max_size;
+
+	// The final size of the table which will be determined by the size of its columns, rows, and spacing.
+	Vector2f flex_resulting_content_size;
+	// Overflow size in case flex items overflow the container or contents of any flex items overflow their box (without being caught by the item).
+	Vector2f flex_content_overflow_size;
+
+	ElementList& absolutely_positioned_elements;
+};
+
+} // namespace Rml
+#endif

+ 10 - 10
Source/Core/LayoutTable.cpp

@@ -123,7 +123,7 @@ void LayoutTable::DetermineColumnWidths()
 	{
 		if (Element* element_group = grid.columns[i].element_group)
 		{
-			const ComputedTrackSize computed = BuildComputedColumnSize(element_group->GetComputedValues());
+			const ComputedAxisSize computed = LayoutDetails::BuildComputedHorizontalSize(element_group->GetComputedValues());
 			const int span = grid.columns[i].group_span;
 
 			sizing.ApplyGroupElement(i, span, computed);
@@ -131,7 +131,7 @@ void LayoutTable::DetermineColumnWidths()
 
 		if (Element* element_column = grid.columns[i].element_column)
 		{
-			const ComputedTrackSize computed = BuildComputedColumnSize(element_column->GetComputedValues());
+			const ComputedAxisSize computed = LayoutDetails::BuildComputedHorizontalSize(element_column->GetComputedValues());
 			const int span = grid.columns[i].column_span;
 
 			sizing.ApplyTrackElement(i, span, computed);
@@ -143,7 +143,7 @@ void LayoutTable::DetermineColumnWidths()
 	{
 		if (Element* element_cell = grid.columns[i].element_cell)
 		{
-			const ComputedTrackSize computed = BuildComputedColumnSize(element_cell->GetComputedValues());
+			const ComputedAxisSize computed = LayoutDetails::BuildComputedHorizontalSize(element_cell->GetComputedValues());
 			const int colspan = grid.columns[i].cell_span;
 
 			sizing.ApplyCellElement(i, colspan, computed);
@@ -183,7 +183,7 @@ void LayoutTable::InitializeCellBoxes()
 		Box& box = cells[i];
 
 		// Determine the cell's box for formatting later, we may get an indefinite (-1) vertical content size.
-		LayoutDetails::BuildBox(box, table_initial_content_size, grid.cells[i].element_cell, false, 0.f);
+		LayoutDetails::BuildBox(box, table_initial_content_size, grid.cells[i].element_cell, BoxContext::FlexOrTable, 0.f);
 
 		// Determine the cell's content width. Include any spanning columns in the cell width.
 		const float cell_border_width = GetSpanningCellBorderSize(columns, grid.cells[i].column_begin, grid.cells[i].column_last);
@@ -223,7 +223,7 @@ void LayoutTable::DetermineRowHeights()
 		if (Element* element_group = grid.rows[i].element_group)
 		{
 			// The padding/border/margin of column groups are used, but their widths are ignored.
-			const ComputedTrackSize computed = BuildComputedRowSize(element_group->GetComputedValues());
+			const ComputedAxisSize computed = LayoutDetails::BuildComputedVerticalSize(element_group->GetComputedValues());
 			const int span = grid.rows[i].group_span;
 
 			sizing.ApplyGroupElement(i, span, computed);
@@ -232,7 +232,7 @@ void LayoutTable::DetermineRowHeights()
 		if (Element* element_row = grid.rows[i].element_row)
 		{
 			// The padding/border/margin and widths of columns are used.
-			const ComputedTrackSize computed = BuildComputedRowSize(element_row->GetComputedValues());
+			const ComputedAxisSize computed = LayoutDetails::BuildComputedVerticalSize(element_row->GetComputedValues());
 			
 			if (computed.size.type == Style::LengthPercentageAuto::Percentage)
 				percentage_size_used = true;
@@ -318,11 +318,10 @@ void LayoutTable::FormatRows()
 	RMLUI_ASSERT(rows.size() == grid.rows.size());
 
 	// Size and position the row and row group elements.
-	//   @performance: Maybe build the box using a simpler algorithm. Eg. we don't do anything for auto
-	//                 margins. Some of the information is already gathered in the TrackMetric.
 	auto FormatRow = [this](Element* element, float content_height, float offset_y) {
 		Box box;
-		LayoutDetails::BuildBox(box, table_initial_content_size, element, false, 0.0f);
+		// We use inline context here because we only care about padding, border, and (non-auto) margin.
+		LayoutDetails::BuildBox(box, table_initial_content_size, element, BoxContext::Inline, 0.0f);
 		const Vector2f content_size(
 			table_resulting_content_size.x - box.GetSizeAcross(Box::HORIZONTAL, Box::MARGIN, Box::PADDING),
 			content_height
@@ -353,7 +352,8 @@ void LayoutTable::FormatColumns()
 	// Size and position the column and column group elements.
 	auto FormatColumn = [this](Element* element, float content_width, float offset_x) {
 		Box box;
-		LayoutDetails::BuildBox(box, table_initial_content_size, element, false, 0.0f);
+		// We use inline context here because we only care about padding, border, and (non-auto) margin.
+		LayoutDetails::BuildBox(box, table_initial_content_size, element, BoxContext::Inline, 0.0f);
 		const Vector2f content_size(
 			content_width,
 			table_resulting_content_size.y - box.GetSizeAcross(Box::VERTICAL, Box::MARGIN, Box::PADDING)

+ 7 - 36
Source/Core/LayoutTableDetails.cpp

@@ -27,6 +27,7 @@
  */
 
 #include "LayoutTableDetails.h"
+#include "LayoutDetails.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include <algorithm>
 #include <float.h>
@@ -296,42 +297,12 @@ void TableGrid::PushRow(Element* element_row, ElementList cell_elements)
 	open_cells.erase(open_cells.begin(), it_cells_in_row_end);
 }
 
-
-ComputedTrackSize BuildComputedColumnSize(const ComputedValues& computed)
-{
-	return ComputedTrackSize{
-		computed.width,
-		computed.min_width, computed.max_width,
-		computed.padding_left, computed.padding_right,
-		computed.margin_left, computed.margin_right,
-		computed.border_left_width, computed.border_right_width,
-		computed.box_sizing
-	};
-}
-
-
-ComputedTrackSize BuildComputedRowSize(const ComputedValues& computed)
+void TracksSizing::GetEdgeSizes(float& margin_a, float& margin_b, float& padding_border_a, float& padding_border_b, const ComputedAxisSize& computed) const
 {
-	return ComputedTrackSize{
-		computed.height,
-		computed.min_height, computed.max_height,
-		computed.padding_top, computed.padding_bottom,
-		computed.margin_top, computed.margin_bottom,
-		computed.border_top_width, computed.border_bottom_width,
-		computed.box_sizing
-	};
-}
-
-void TracksSizing::GetEdgeSizes(float& margin_a, float& margin_b, float& padding_border_a, float& padding_border_b, const ComputedTrackSize& computed) const
-{
-	margin_a = ResolveValue(computed.margin_a, table_initial_content_size);
-	margin_b = ResolveValue(computed.margin_b, table_initial_content_size);
-
-	padding_border_a = Math::Max(0.0f, ResolveValue(computed.padding_a, table_initial_content_size)) + Math::Max(0.0f, computed.border_a);
-	padding_border_b = Math::Max(0.0f, ResolveValue(computed.padding_b, table_initial_content_size)) + Math::Max(0.0f, computed.border_b);
+	LayoutDetails::GetEdgeSizes(margin_a, margin_b, padding_border_a, padding_border_b, computed, table_initial_content_size);
 }
 
-void TracksSizing::ApplyGroupElement(const int index, const int span, const ComputedTrackSize& computed)
+void TracksSizing::ApplyGroupElement(const int index, const int span, const ComputedAxisSize& computed)
 {
 	RMLUI_ASSERT(span >= 1 && index + span - 1 < (int)metrics.size());
 
@@ -351,7 +322,7 @@ void TracksSizing::ApplyGroupElement(const int index, const int span, const Comp
 	metric_last.sum_margin_b = margin_b;
 }
 
-void TracksSizing::ApplyTrackElement(const int index, const int span, const ComputedTrackSize& computed)
+void TracksSizing::ApplyTrackElement(const int index, const int span, const ComputedAxisSize& computed)
 {
 	RMLUI_ASSERT(span >= 1 && index + span - 1 < (int)metrics.size());
 
@@ -384,7 +355,7 @@ void TracksSizing::ApplyTrackElement(const int index, const int span, const Comp
 	}
 }
 
-void TracksSizing::ApplyCellElement(const int index, const int span, const ComputedTrackSize& computed)
+void TracksSizing::ApplyCellElement(const int index, const int span, const ComputedAxisSize& computed)
 {
 	//  Merge the metrics of the cell with the existing track: If the existing track
 	//  has auto min-/max-/size, we use the cell's min-/max-/size if it has any.
@@ -429,7 +400,7 @@ void TracksSizing::ApplyCellElement(const int index, const int span, const Compu
 	}
 }
 
-void TracksSizing::InitializeSize(TrackMetric& metric, float& margin_a, float& margin_b, float& padding_border_a, float& padding_border_b, const ComputedTrackSize& computed, const int span, const Style::BoxSizing target_box) const
+void TracksSizing::InitializeSize(TrackMetric& metric, float& margin_a, float& margin_b, float& padding_border_a, float& padding_border_b, const ComputedAxisSize& computed, const int span, const Style::BoxSizing target_box) const
 {
 	RMLUI_ASSERT(span >= 1);
 

+ 7 - 23
Source/Core/LayoutTableDetails.h

@@ -35,6 +35,8 @@
 
 namespace Rml {
 
+struct ComputedAxisSize;
+
 /*
 	TableGrid builds the structure of the table, that is a list of rows, columns, and cells, taking
 	spanning attributes into account to position cells.
@@ -84,24 +86,6 @@ private:
 	CellList open_cells;
 };
 
-
-/*
-	ComputedTrackSize is an abstraction for computed size properties of rows and column elements alike which
-	allows them to use the same algorithms. Here, 'a' means left or top, 'b' means right or bottom.
-*/
-struct ComputedTrackSize {
-	Style::LengthPercentageAuto size;
-	Style::LengthPercentage min_size, max_size;
-	Style::Padding padding_a, padding_b;
-	Style::Margin margin_a, margin_b;
-	float border_a, border_b;
-	Style::BoxSizing box_sizing;
-};
-
-ComputedTrackSize BuildComputedColumnSize(const ComputedValues& computed);
-ComputedTrackSize BuildComputedRowSize(const ComputedValues& computed);
-
-
 enum class TrackSizingMode { Auto, Fixed, Flexible };
 
 /*
@@ -136,21 +120,21 @@ public:
 	{}
 
 	// Apply group element. This sets the initial size of edges.
-	void ApplyGroupElement(const int index, const int span, const ComputedTrackSize& computed);
+	void ApplyGroupElement(const int index, const int span, const ComputedAxisSize& computed);
 	// Apply track element. This merges its edges, and sets the initial content size.
-	void ApplyTrackElement(const int index, const int span, const ComputedTrackSize& computed);
+	void ApplyTrackElement(const int index, const int span, const ComputedAxisSize& computed);
 	// Apply cell element for column sizing. This merges its content size and margins.
-	void ApplyCellElement(const int index, const int span, const ComputedTrackSize& computed);
+	void ApplyCellElement(const int index, const int span, const ComputedAxisSize& computed);
 
 	// Convert flexible size to fixed size for all tracks.
 	void ResolveFlexibleSize();
 
 private:
-	void GetEdgeSizes(float& margin_a, float& margin_b, float& padding_border_a, float& padding_border_b, const ComputedTrackSize& computed) const;
+	void GetEdgeSizes(float& margin_a, float& margin_b, float& padding_border_a, float& padding_border_b, const ComputedAxisSize& computed) const;
 
 	// Fill the track metric with fixed, flexible and min/max size, based on the element's computed values.
 	void InitializeSize(TrackMetric& metric, float& margin_a, float& margin_b, float& padding_border_a,
-		float& padding_border_b, const ComputedTrackSize& computed, const int span, const Style::BoxSizing target_box) const;
+		float& padding_border_b, const ComputedAxisSize& computed, const int span, const Style::BoxSizing target_box) const;
 
 	TrackMetricList& metrics;
 	const float table_initial_content_size;

+ 12 - 0
Source/Core/Math.cpp

@@ -248,6 +248,18 @@ RMLUICORE_API bool RandomBool()
 	return RandomInteger(2) == 1;
 }
 
+template <>
+Vector2f Max<Vector2f>(Vector2f a, Vector2f b)
+{
+	return Vector2f(Max(a.x, b.x), Max(a.y, b.y));
+}
+
+template <>
+Vector2f Min<Vector2f>(Vector2f a, Vector2f b)
+{
+	return Vector2f(Min(a.x, b.x), Min(a.y, b.y));
+}
+
 Colourb RoundedLerp(float t, Colourb v0, Colourb v1)
 {
 	return Colourb{

+ 29 - 1
Source/Core/PropertySpecification.cpp

@@ -270,6 +270,31 @@ bool PropertySpecification::ParseShorthandDeclaration(PropertyDictionary& dictio
 	if (!shorthand_definition)
 		return false;
 
+	// Handle the special behavior of the flex shorthand first, otherwise it acts like 'FallThrough'.
+	if (shorthand_definition->type == ShorthandType::Flex)
+	{
+		RMLUI_ASSERT(shorthand_definition->items.size() == 3);
+		if (!property_values.empty() && property_values[0] == "none")
+		{
+			property_values = {"0", "0", "auto"};
+		}
+		else
+		{
+			// Default values when omitted from the 'flex' shorthand is specified here. These defaults are special
+			// for this shorthand only, otherwise each underlying property has a different default value.
+			const char* default_omitted_values[] = {"1", "1", "0"}; // flex-grow, flex-shrink, flex-basis
+			Property new_property;
+			bool result = true;
+			for (int i = 0; i < 3; i++)
+			{
+				auto& item = shorthand_definition->items[i];
+				result &= item.property_definition->ParseValue(new_property, default_omitted_values[i]);
+				dictionary.SetProperty(item.property_id, new_property);
+			}
+			RMLUI_ASSERT(result);
+		}
+	}
+
 	// If this definition is a 'box'-style shorthand (x-top, x-right, x-bottom, x-left, etc) and there are fewer
 	// than four values
 	if (shorthand_definition->type == ShorthandType::Box &&
@@ -362,6 +387,9 @@ bool PropertySpecification::ParseShorthandDeclaration(PropertyDictionary& dictio
 	}
 	else
 	{
+		RMLUI_ASSERT(shorthand_definition->type == ShorthandType::Box || shorthand_definition->type == ShorthandType::FallThrough ||
+			shorthand_definition->type == ShorthandType::Replicate || shorthand_definition->type == ShorthandType::Flex);
+
 		size_t value_index = 0;
 		size_t property_index = 0;
 
@@ -373,7 +401,7 @@ bool PropertySpecification::ParseShorthandDeclaration(PropertyDictionary& dictio
 			{
 				// This definition failed to parse; if we're falling through, try the next property. If there is no
 				// next property, then abort!
-				if (shorthand_definition->type == ShorthandType::FallThrough)
+				if (shorthand_definition->type == ShorthandType::FallThrough || shorthand_definition->type == ShorthandType::Flex)
 				{
 					if (property_index + 1 < shorthand_definition->items.size())
 						continue;

+ 2 - 2
Source/Core/StyleSheetParser.cpp

@@ -844,7 +844,7 @@ bool StyleSheetParser::ReadProperties(AbstractPropertyParser& property_parser)
 			case QUOTE:
 			{
 				value += character;
-				if (character == '"' && previous_character != '/')
+				if (character == '"' && previous_character != '\\')
 					state = VALUE;
 			}
 			break;
@@ -862,7 +862,7 @@ bool StyleSheetParser::ReadProperties(AbstractPropertyParser& property_parser)
 		if (!property_parser.Parse(name, value))
 			Log::Message(Log::LT_WARNING, "Syntax error parsing property declaration '%s: %s;' in %s: %d.", name.c_str(), value.c_str(), stream_file_name.c_str(), line_number);
 	}
-	else if (!name.empty() || !value.empty())
+	else if (!StringUtilities::StripWhitespace(name).empty() || !value.empty())
 	{
 		Log::Message(Log::LT_WARNING, "Invalid property declaration '%s':'%s' at %s:%d", name.c_str(), value.c_str(), stream_file_name.c_str(), line_number);
 	}

+ 17 - 1
Source/Core/StyleSheetSpecification.cpp

@@ -319,7 +319,7 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	RegisterProperty(PropertyId::BorderBottomLeftRadius, "border-bottom-left-radius", "0px", false, false).AddParser("length");
 	RegisterShorthand(ShorthandId::BorderRadius, "border-radius", "border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius", ShorthandType::Box);
 
-	RegisterProperty(PropertyId::Display, "display", "inline", false, true).AddParser("keyword", "none, block, inline, inline-block, table, table-row, table-row-group, table-column, table-column-group, table-cell");
+	RegisterProperty(PropertyId::Display, "display", "inline", false, true).AddParser("keyword", "none, block, inline, inline-block, flex, table, table-row, table-row-group, table-column, table-column-group, table-cell");
 	RegisterProperty(PropertyId::Position, "position", "static", false, true).AddParser("keyword", "static, relative, absolute, fixed");
 	RegisterProperty(PropertyId::Top, "top", "auto", false, false)
 		.AddParser("keyword", "auto")
@@ -422,6 +422,22 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	// Rare properties (not added to computed values)
 	RegisterProperty(PropertyId::FillImage, "fill-image", "", false, false).AddParser("string");
 
+	// Flexbox
+	RegisterProperty(PropertyId::AlignContent, "align-content", "stretch", false, true).AddParser("keyword", "flex-start, flex-end, center, space-between, space-around, stretch");
+	RegisterProperty(PropertyId::AlignItems, "align-items", "stretch", false, true).AddParser("keyword", "flex-start, flex-end, center, baseline, stretch");
+	RegisterProperty(PropertyId::AlignSelf, "align-self", "auto", false, true).AddParser("keyword", "auto, flex-start, flex-end, center, baseline, stretch");
+	
+	RegisterProperty(PropertyId::FlexBasis, "flex-basis", "auto", false, true).AddParser("keyword", "auto").AddParser("length_percent");
+	RegisterProperty(PropertyId::FlexDirection, "flex-direction", "row", false, true).AddParser("keyword", "row, row-reverse, column, column-reverse");
+
+	RegisterProperty(PropertyId::FlexGrow, "flex-grow", "0", false, true).AddParser("number");
+	RegisterProperty(PropertyId::FlexShrink, "flex-shrink", "1", false, true).AddParser("number");
+	RegisterProperty(PropertyId::FlexWrap, "flex-wrap", "nowrap", false, true).AddParser("keyword", "nowrap, wrap, wrap-reverse");
+	RegisterProperty(PropertyId::JustifyContent, "justify-content", "flex-start", false, true).AddParser("keyword", "flex-start, flex-end, center, space-between, space-around");
+
+	RegisterShorthand(ShorthandId::Flex, "flex", "flex-grow, flex-shrink, flex-basis", ShorthandType::Flex);
+	RegisterShorthand(ShorthandId::FlexFlow, "flex-flow", "flex-direction, flex-wrap", ShorthandType::FallThrough);
+
 	RMLUI_ASSERTMSG(instance->properties.shorthand_map->AssertAllInserted(ShorthandId::NumDefinedIds), "Missing specification for one or more Shorthand IDs.");
 	RMLUI_ASSERTMSG(instance->properties.property_map->AssertAllInserted(PropertyId::NumDefinedIds), "Missing specification for one or more Property IDs.");
 }

+ 71 - 0
Tests/Data/VisualTests/flex_01.rml

@@ -0,0 +1,71 @@
+<rml>
+<head>
+    <title>Flex 01 - Basic flexbox layout</title>
+    <link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-flexbox/" />
+	<meta name="Description" content="Basic flexible box layout." />
+	<meta name="Animation" content="Hover over the articles to animate their size." />
+	<style>
+		header, article { display: block; }
+		h1 { font-size: 1.5em; }
+		h2 { font-size: 1.3em; }
+		
+		header {
+			background-color: #9777d9;
+			border: 5dp #666;
+		}
+		h1 {
+			text-align: center;
+			color: white;
+			line-height: 100dp;
+		}
+		section {
+			display: flex;
+			background: #666;
+			border: 5dp #666;
+		}
+		article {
+			padding: 10dp;
+			margin: 0 5dp;
+			background-color: #edd3c0;
+			flex: 1;
+			transition: 0.3s flex-grow cubic-out;
+		}
+		article:hover {
+			flex: 1.2;
+			background-color: #fde3d0;
+		}
+		h2 {
+			text-align: center;
+			background-color: #eb6e14;
+			margin: -10dp -10dp 0;
+			padding: 10dp 0;
+		}
+		article:hover h2 {
+			background-color: #fb7e24;
+		}
+	</style>
+</head>
+
+<body>
+<header>
+	<h1>Header</h1>
+</header>
+<section>
+	<article>
+		<h2>First article</h2>
+		<p>Etiam libero lorem, lacinia non augue lobortis, tincidunt consequat justo. Sed id enim tempor, feugiat tortor id, rhoncus enim. Quisque pretium neque eu felis tincidunt fringilla. Mauris laoreet enim neque, iaculis cursus lorem mollis sed. Nulla pretium euismod nulla sed convallis. Curabitur in tempus sem. Phasellus suscipit vitae nulla nec ultricies.</p>
+	</article>
+	<article>
+		<h2>Second article</h2>
+		<p>Ut volutpat, odio et facilisis molestie, lacus elit euismod enim, et tempor lacus sapien finibus ipsum. Aliquam erat volutpat. Nullam risus turpis, hendrerit ac fermentum in, dapibus non risus.</p>
+	</article>
+	<article>
+		<h2>Third article</h2>
+		<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed aliquet commodo nisi, id cursus enim eleifend vitae. Praesent turpis lorem, commodo id tempus sit amet, faucibus et libero. Aliquam malesuada ultrices leo, ut molestie tortor posuere sit amet. Proin vitae tortor a sem consequat gravida. Maecenas sed egestas dolor.</p>
+		<p>In gravida ligula in turpis molestie varius. Ut sed velit id tellus aliquet aliquet. Nulla et leo tellus. Ut a convallis dolor, eu rutrum enim. Nam vitae ultrices dui. Aliquam semper eros ut ultrices rutrum.</p>
+	</article>
+</section>
+<handle size_target="#document"/>
+</body>
+</rml>

+ 72 - 0
Tests/Data/VisualTests/flex_02.rml

@@ -0,0 +1,72 @@
+<rml>
+<head>
+    <link type="text/rcss" href="/../Tests/Data/style.rcss" />
+    <title>Flex 02 - Various features</title>
+	<meta name="Description" content="Testing various flexible box layout features. Should be comparable to the output in a web browser." />
+    <style>
+        .flex-container {
+            display: flex;
+            margin: 10dp 20dp;
+            background-color: #333;
+            max-height: 210dp;
+            flex-wrap: wrap-reverse;
+        }
+
+        .flex-item {
+            width: 50dp;
+            margin: 20dp;
+            background-color: #eee;
+            height: 50dp;
+            text-align: center;
+        }
+
+        .flex-direction-row {
+            flex-direction: row;
+        }
+        .flex-direction-row-reverse {
+            flex-direction: row-reverse;
+        }
+        .flex-direction-column {
+            flex-direction: column;
+        }
+        .flex-direction-column-reverse {
+            flex-direction: column-reverse;
+        }
+        .absolute {
+            margin: 0;
+            position: absolute;
+            right: 0;
+            bottom: 10dp;
+        }
+    </style>
+</head>
+
+<body>
+    <div class="flex-container flex-direction-row" style="position: relative">
+        <div class="flex-item absolute">Abs</div>
+        <div class="flex-item" style="margin: 50dp;">1</div>
+        <div class="flex-item" style="margin-top: auto">2</div>
+        <div class="flex-item" style="margin: auto">3</div>
+    </div>
+
+    <div class="flex-container flex-direction-row-reverse" style="height: 200dp; justify-content: space-around;">
+        <div class="flex-item">1</div>
+        <div class="flex-item" style="margin-bottom: auto;">2</div>
+        <div class="flex-item" style="margin-right: 40dp;">3</div>
+    </div>
+
+    <div class="flex-container flex-direction-column">
+        <div class="flex-item" id="test" style="margin-right: auto">1</div>
+        <div class="flex-item">2</div>
+        <div class="flex-item">3</div>
+    </div>
+
+    <div class="flex-container flex-direction-column-reverse">
+        <div class="flex-item">1</div>
+        <div class="flex-item">2 LONG_OVERFLOWING_WORD</div>
+        <div class="flex-item">3</div>
+    </div>
+
+    <handle size_target="#document"/>
+</body>
+</rml>

+ 73 - 0
Tests/Data/VisualTests/flex_03_scroll.rml

@@ -0,0 +1,73 @@
+<rml>
+<head>
+	<title>Flex 03 - Scrolling container</title>
+	<link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-flexbox/" />
+	<meta name="Description" content="Flex container with scrollbars." />
+	<style>
+		.flex {
+			display: flex;
+			background-color: #555;
+			margin: 5dp 20dp 15dp;
+			border: 2dp #333;
+			justify-content: space-between;
+			color: #d44fff;
+		}
+		.auto {
+			overflow: auto;
+		}
+		.scroll {
+			overflow: scroll;
+		}
+		.flex div {
+			flex: 0 1 auto;
+			width: 50dp;
+			height: 50dp;
+			margin: 20dp;
+			background-color: #eee;
+			line-height: 50dp;
+			text-align: center;
+		}
+		.flex div.tall {
+			height: 80dp;
+			width: 15dp;
+			margin: 0;
+			border: 2dp #d44fff;
+		}
+	</style>
+</head>
+
+<body>
+overflow: scroll
+<div class="flex scroll" id="scroll">
+	<div>Hello<div class="tall"/></div>
+	<div>big world!</div>
+	<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+</div>
+overflow: auto
+<div class="flex auto" id="auto">
+	<div>Hello<div class="tall"/></div>
+	<div>big world!</div>
+	<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+</div>
+overflow: auto - only vertical overflow
+<div class="flex auto" id="vertical">
+	<div>Hello<div class="tall"/></div>
+	<div>big world!</div>
+	<div>LONG</div>
+</div>
+overflow: auto - only horizontal overflow
+<div class="flex auto" id="horizontal">
+	<div>Hello</div>
+	<div>big</div>
+	<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+</div>
+overflow: visible
+<div class="flex" id="visible">
+	<div>Hello<div class="tall"/></div>
+	<div>big world!</div>
+	<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+</div>
+<handle size_target="#document"/>
+</body>
+</rml>

+ 113 - 0
Tests/Data/VisualTests/flex_04.rml

@@ -0,0 +1,113 @@
+<rml>
+<head>
+    <title>Flex 04 - Flex shorthand</title>
+    <link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-flexbox/#flex-property" />
+	<meta name="Description" content="The default value of flex should allow the items to shrink but not grow. With 'flex: auto' the items should fill the container and also allow shrinking. With 'flex: none' the items neither shrink nor grow with the container size. Finally, with 'flex: &amp;lt;number&amp;gt;' each item should be given that proportion of the available space." />
+	<meta name="Instructions" content="Resize the document to see the resizing behavior of the flexboxes." />
+	<style>
+		h1 {
+			text-align: center;
+			margin-bottom: 5dp;
+			padding: 0.25em 0;
+			border-color: #aaa;
+			border-width: 1dp 0;
+		}
+		h1:first-child { margin-top: 0; }
+		h1, h2 { white-space: nowrap; }
+		.flex {
+			display: flex;
+			margin: 3dp 0 7dp;
+			border: 2dp #333;
+			overflow: hidden;
+			justify-content: space-around;
+			align-items: flex-end;
+			flex-wrap: wrap;
+		}
+		.flex > div {
+			border: 2dp #d66;
+			background: #fff;
+			text-align: center;
+		}
+		.flex.nowrap {
+			flex-wrap: nowrap;
+		}
+		.flex.auto > div {
+			flex: 1 1 auto;
+		}
+		.flex.none > div {
+			flex: none;
+		}
+		.flex.number > div {
+			flex: 1;
+		}
+		.wrapper {
+			min-height: 165dp;
+		}
+	</style>
+</head>
+
+<body>
+<h1>flex: <em>default value</em> (0 1 auto)</h1>
+<div class="wrapper">
+	<h2>flex-wrap: wrap</h2>
+	<div class="flex">
+		<div>Hello</div>
+		<div>big world!</div>
+		<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+	</div>
+	<h2>flex-wrap: nowrap</h2>
+	<div class="flex nowrap">
+		<div>Hello</div>
+		<div>big world!</div>
+		<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+	</div>
+</div>
+<h1>flex: auto (1 1 auto)</h1>
+<div class="wrapper">
+	<h2>flex-wrap: wrap</h2>
+	<div class="flex auto">
+		<div>Hello</div>
+		<div>big world!</div>
+		<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+	</div>
+	<h2>flex-wrap: nowrap</h2>
+	<div class="flex nowrap auto">
+		<div>Hello</div>
+		<div>big world!</div>
+		<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+	</div>
+</div>
+<h1>flex: none (0 0 auto)</h1>
+<div class="wrapper">
+	<h2>flex-wrap: wrap</h2>
+	<div class="flex none">
+		<div>Hello</div>
+		<div>big world!</div>
+		<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+	</div>
+	<h2>flex-wrap: nowrap</h2>
+	<div class="flex nowrap none">
+		<div>Hello</div>
+		<div>big world!</div>
+		<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+	</div>
+</div>
+<h1>flex: 1 (1 1 0)</h1>
+<div class="wrapper">
+	<h2>flex-wrap: wrap</h2>
+	<div class="flex number">
+		<div>Hello</div>
+		<div>big world!</div>
+		<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+	</div>
+	<h2>flex-wrap: nowrap</h2>
+	<div class="flex nowrap number">
+		<div>Hello</div>
+		<div>big world!</div>
+		<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+	</div>
+</div>
+<handle size_target="#document"/>
+</body>
+</rml>

+ 97 - 0
Tests/Data/VisualTests/flex_05.rml

@@ -0,0 +1,97 @@
+<rml>
+<head>
+    <title>Flex 05 - Height constraints</title>
+    <link type="text/rcss" href="../style.rcss"/>
+    <link rel="match" href="reference/flex_05-ref.rml"/>
+	<link rel="help" href="https://drafts.csswg.org/css-flexbox/" />
+	<meta name="Description" content="The flex container should always respect (min-/max-) height constraints." />
+	<style>
+		h1 {
+			text-align: center;
+			margin-bottom: 5dp;
+			padding: 0.25em 0;
+			border-color: #333;
+			border-width: 1dp 0;
+		}
+		h1:first-child { margin-top: 0; }
+		h1, h2 { white-space: nowrap; }
+		.flex {
+			display: flex;
+			margin: 1dp 0 3dp;
+			border: 1dp #999;
+			overflow: hidden;
+			justify-content: space-around;
+			align-items: flex-start;
+		}
+		.flex > div {
+			border: 2dp #d66;
+			background: #fff;
+			text-align: center;
+		}
+		.wrap {
+			flex-wrap: wrap;
+		}
+		.column {
+			flex-direction: column;
+		}
+		.height {
+			height: 40dp;
+		}
+		.min-height {
+			min-height: 40dp;
+		}
+		.max-height {
+			max-height: 40dp;
+		}
+	</style>
+</head>
+
+<body>
+<h1>height, rows</h1>
+<div class="flex wrap height">
+	<div>Hello<br/>world!<br/>More<br/>Text</div><div>More Text</div>
+	<div>Hello world!</div><div>More Text</div>
+</div>
+<div class="flex height">
+	<div>Hello world!</div><div>More Text</div>
+</div>
+<h1>height, columns</h1>
+<div class="flex column wrap height">
+	<div>Hello<br/>world!<br/>More<br/>Text</div><div>More Text</div>
+	<div>Hello world!</div><div>More Text</div>
+</div>
+<div class="flex column height">
+	<div>Hello<br/>world!</div><div>More<br/>Text</div>
+</div>
+<h1>min-height, rows</h1>
+<div class="flex wrap min-height">
+	<div>Hello world!</div>
+</div>
+<div class="flex min-height">
+	<div>Hello world!</div>
+</div>
+<h1>min-height, columns</h1>
+<div class="flex column wrap min-height">
+	<div>Hello world!</div>
+</div>
+<div class="flex column min-height">
+	<div>Hello world!</div>
+</div>
+<h1>max-height, rows</h1>
+<div class="flex wrap max-height">
+	<div>Hello<br/>world!<br/>More<br/>Text</div>
+</div>
+<div class="flex max-height">
+	<div>Hello<br/>world!<br/>More<br/>Text</div>
+</div>
+<h1>max-height, columns</h1>
+<div class="flex column wrap max-height">
+	<div>Hello<br/>world!<br/>More<br/>Text</div><div>More<br/>Text</div>
+	<div>Hello<br/>world!</div><div>More<br/>Text</div>
+</div>
+<div class="flex column max-height">
+	<div>Hello<br/>world!</div><div>More<br/>Text</div>
+</div>
+<handle size_target="#document"/>
+</body>
+</rml>

+ 71 - 0
Tests/Data/VisualTests/flex_direction.rml

@@ -0,0 +1,71 @@
+<rml>
+<head>
+    <link type="text/rcss" href="/../Tests/Data/style.rcss" />
+    <title>CSS Test: flex flow direction</title>
+    <link href="https://test.csswg.org/suites/css-flexbox-1_dev/nightly-unstable/xhtml1/flex-direction.xht" rel="source" />
+    <link href="http://www.github.com/sskyy" rel="author" title="houzhenyu" />
+    <link href="http://www.w3.org/TR/css-flexbox-1/#flex-direction" rel="help" />
+    <link href="reference/flex_direction-ref.rml" rel="match" />
+    <meta content="The flow of flex items in the the flex container should observe the flex-direction property." name="assert" />
+    <style>
+        .flex-container {
+            display: flex;
+            margin: 20dp;
+            background-color: #333;
+        }
+        
+        .flex-item {
+            width: 50dp;
+            height: 50dp;
+            margin: 20dp;
+            background-color: #eee;
+            line-height: 50dp;
+            text-align: center;
+        }
+        
+        .flex-container.flex-direction-row {
+            flex-direction: row;
+        }
+        .flex-container.flex-direction-row-reverse {
+            flex-direction: row-reverse;
+        }
+        .flex-container.flex-direction-column {
+            flex-direction: column;
+        }
+        .flex-container.flex-direction-column-reverse {
+            flex-direction: column-reverse;
+        }
+    </style>
+</head>
+<body>
+    <h1>flex-direction:row</h1>
+    <div class="flex-container flex-direction-row">
+        <div class="flex-item">1</div>
+        <div class="flex-item">2</div>
+        <div class="flex-item">3</div>
+    </div>
+
+    <h1>flex-direction:row-reverse</h1>
+    <div class="flex-container flex-direction-row-reverse">
+        <div class="flex-item">1</div>
+        <div class="flex-item">2</div>
+        <div class="flex-item">3</div>
+    </div>
+
+    <h1>flex-direction:column</h1>
+    <div class="flex-container flex-direction-column">
+        <div class="flex-item">1</div>
+        <div class="flex-item">2</div>
+        <div class="flex-item">3</div>
+    </div>
+
+    <h1>flex-direction:column-reverse</h1>
+    <div class="flex-container flex-direction-column-reverse">
+        <div class="flex-item">1</div>
+        <div class="flex-item">2</div>
+        <div class="flex-item">3</div>
+    </div>
+    
+    <handle size_target="#document"/>
+</body>
+</rml>

+ 63 - 0
Tests/Data/VisualTests/flex_wrap_column_reverse.rml

@@ -0,0 +1,63 @@
+<rml>
+<head>
+  <link type="text/rcss" href="/../Tests/Data/style.rcss" />
+  <title>CSS Test: flex container multiline wrapping in column-reverse direction</title>
+  <link href="https://test.csswg.org/suites/css-flexbox-1_dev/nightly-unstable/xhtml1/multi-line-wrap-with-column-reverse.xht" rel="source" />
+  <link href="mailto:[email protected]" rel="author" title="tmtysk" />
+  <link href="mailto:[email protected]" rel="reviewer" title="Tab Atkins" />
+  <link href="http://www.w3.org/TR/css-flexbox-1/#flex-wrap-property" rel="help" />
+  <link href="http://www.w3.org/TR/css-flexbox-1/#flex-direction-property" rel="help" />
+  <link href="reference/flex_wrap_column_reverse-ref.rml" rel="match" />
+  <meta content="This test check that a flex container wraps blocks multiline in column-reverse direction." name="assert" />
+  <style>
+    *, p { margin:0; padding:0; font-size:100%; line-height:1; }
+
+    #test {
+      display: flex;
+      flex-direction: column-reverse;
+      flex-wrap: wrap;
+      height: 300dp;
+    }
+
+    p {
+      margin-top: 10dp;
+      margin-right: 10dp;
+      background-color: #eee;
+    }
+
+    #col1-row1 {
+        height: 90dp;
+    }
+
+    #col1-row2 {
+        height: 90dp;
+    }
+
+    #col1-row3 {
+        height: 90dp;
+    }
+
+    #col2-row1 {
+        height: 140dp;
+    }
+
+    #col2-row2 {
+        height: 140dp;
+    }
+
+    #col3-row1 {
+        height: 290dp;
+    }
+  </style>
+</head>
+<body>
+  <div id="test">
+    <p id="col1-row3">1-3</p>
+    <p id="col1-row2">1-2</p>
+    <p id="col1-row1">1-1</p>
+    <p id="col2-row2">2-2</p>
+    <p id="col2-row1">2-1</p>
+    <p id="col3-row1">3-1</p>
+  </div>
+</body>
+</rml>

+ 72 - 0
Tests/Data/VisualTests/reference/flex_05-ref.rml

@@ -0,0 +1,72 @@
+<rml>
+<head>
+    <title>Flex 05 ref</title>
+    <link type="text/rcss" href="../../style.rcss"/>
+	<meta name="Reference" content="The size of the flex containers should match the size of the block boxes in the reference." />
+	<style>
+		h1 {
+			text-align: center;
+			margin-bottom: 5dp;
+			padding: 0.25em 0;
+			border-color: #333;
+			border-width: 1dp 0;
+		}
+		h1:first-child { margin-top: 0; }
+		h1, h2 { white-space: nowrap; }
+		.flex {
+			display: block;
+			margin: 1dp 0 3dp;
+			border: 1dp #999;
+		}
+		.wrap {
+			flex-wrap: wrap;
+		}
+		.column {
+			flex-direction: column;
+		}
+		.height {
+			height: 40dp;
+		}
+		.min-height {
+			height: 40dp;
+		}
+		.max-height {
+			height: 40dp;
+		}
+	</style>
+</head>
+
+<body>
+<h1>height, rows</h1>
+<div class="flex wrap height">
+</div>
+<div class="flex height">
+</div>
+<h1>height, columns</h1>
+<div class="flex column wrap height">
+</div>
+<div class="flex column height">
+</div>
+<h1>min-height, rows</h1>
+<div class="flex wrap min-height">
+</div>
+<div class="flex min-height">
+</div>
+<h1>min-height, columns</h1>
+<div class="flex column wrap min-height">
+</div>
+<div class="flex column min-height">
+</div>
+<h1>max-height, rows</h1>
+<div class="flex wrap max-height">
+</div>
+<div class="flex max-height">
+</div>
+<h1>max-height, columns</h1>
+<div class="flex column wrap max-height">
+</div>
+<div class="flex column max-height">
+</div>
+<handle size_target="#document"/>
+</body>
+</rml>

+ 80 - 0
Tests/Data/VisualTests/reference/flex_direction-ref.rml

@@ -0,0 +1,80 @@
+<rml>
+<head>
+    <link type="text/rcss" href="/../Tests/Data/style.rcss" />
+    <title>CSS Test: flex flow direction</title>
+    <link href="https://test.csswg.org/suites/css-flexbox-1_dev/nightly-unstable/xhtml1/reference/flex-direction.xht" rel="source" />
+    <link href="http://www.github.com/sskyy" rel="author" title="houzhenyu" />
+    <style>
+        .flex-container{
+            display:block;
+            margin:20px;
+            background-color: #333;
+            line-height: 0px;
+        }
+        .flex-item{
+            display: inline-block;
+            width:50px;
+            height:50px;
+            margin:20px 20px;
+            background-color: #eee;
+            line-height: 50px;
+            text-align: center;
+        }
+
+        .flex-container.flex-direction-row{
+            flex-direction : row;
+        }
+        .flex-container.flex-direction-row-reverse{
+            text-align: right;
+        }
+
+        .flex-container.flex-direction-column{
+            padding:20px 0px;
+        }
+
+        .flex-container.flex-direction-column .flex-item{
+            display: block;
+            margin:40px 20px;
+        }
+
+        .flex-container.flex-direction-column .flex-item.first{
+            margin-top:0px;
+        }
+        .flex-container.flex-direction-column .flex-item.last{
+            margin-bottom:0px;
+        }
+
+        .flex-container.flex-direction-column-reverse{
+            padding:20px 0px;
+        }
+
+        .flex-container.flex-direction-column-reverse .flex-item{
+            display: block;
+            margin:40px 20px;
+        }
+
+        .flex-container.flex-direction-column-reverse .flex-item.first{
+            margin-top:0px;
+        }
+        .flex-container.flex-direction-column-reverse .flex-item.last{
+            margin-bottom:0px;
+        }
+    </style>
+</head>
+
+<body>
+    <h1>flex-direction:row</h1>
+    <div class="flex-container flex-direction-row"><div class="flex-item">1</div><div class="flex-item">2</div><div class="flex-item">3</div></div>
+
+    <h1>flex-direction:row-reverse</h1>
+    <div class="flex-container flex-direction-row-reverse"><div class="flex-item">3</div><div class="flex-item">2</div><div class="flex-item">1</div></div>
+
+    <h1>flex-direction:column</h1>
+    <div class="flex-container flex-direction-column"><div class="flex-item first">1</div><div class="flex-item">2</div><div class="flex-item last">3</div></div>
+
+    <h1>flex-direction:column-reverse</h1>
+    <div class="flex-container flex-direction-column-reverse"><div class="flex-item first">3</div><div class="flex-item">2</div><div class="flex-item last">1</div></div>
+
+    <handle size_target="#document"/>
+</body>
+</rml>

+ 71 - 0
Tests/Data/VisualTests/reference/flex_wrap_column_reverse-ref.rml

@@ -0,0 +1,71 @@
+<rml>
+<head>
+  <link type="text/rcss" href="/../Tests/Data/style.rcss" />
+  <title>CSS Test Reference: flex container multiline wrapping in column-reverse direction</title>
+  <link href="mailto:[email protected]" rel="author" title="tmtysk" />
+  <link href="mailto:[email protected]" rel="reviewer" title="Tab Atkins" />
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+      font-size: 100%;
+      line-height: 1;
+    }
+
+    .test {
+      width: 300px;
+      float: left;
+      width: 33.3333%;
+    }
+
+    p {
+      margin-top: 10px;
+      margin-right: 10px;
+      background-color: #eee;
+    }
+
+    #row1-col1 {
+      height: 90px;
+    }
+
+    #row1-col2 {
+      height: 90px;
+    }
+
+    #row1-col3 {
+      height: 90px;
+    }
+
+    #row2-col1 {
+      height: 140px;
+    }
+
+    #row2-col2 {
+      height: 140px;
+    }
+
+    #row3-col1 {
+      height: 290px;
+    }
+
+    .clear {
+      clear: both;
+    }
+  </style>
+</head>
+<body>
+  <div class="test">
+    <p id="row1-col1">1-1</p>
+    <p id="row1-col2">1-2</p>
+    <p id="row1-col3">1-3</p>
+  </div>
+  <div class="test">
+    <p id="row2-col1">2-1</p>
+    <p id="row2-col2">2-2</p>
+  </div>
+  <div class="test">
+    <p id="row3-col1">3-1</p>
+  </div>
+  <div class="clear"></div>
+</body>
+</rml>

+ 493 - 0
Tests/Source/Benchmarks/Flexbox.cpp

@@ -0,0 +1,493 @@
+/*
+ * 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 "../Common/TestsShell.h"
+#include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/Element.h>
+#include <RmlUi/Core/ElementDocument.h>
+#include <RmlUi/Core/Types.h>
+#include <doctest.h>
+#include <nanobench.h>
+
+using namespace ankerl;
+using namespace Rml;
+
+static const String rml_flexbox_basic_document = R"(
+<rml>
+<head>
+    <title>Flex 01 - Basic flexbox (slow, content-based sizing)</title>
+    <link type="text/rcss" href="/../Tests/Data/style.rcss"/>
+	<style>
+		header, article { display: block; }
+		h1 { font-size: 1.5em; }
+		h2 { font-size: 1.3em; }
+		
+		header {
+			background-color: #9777d9;
+			border: 5dp #666;
+		}
+		h1 {
+			text-align: center;
+			color: white;
+			line-height: 100dp;
+		}
+		section {
+			display: flex;
+			background: #666;
+			border: 5dp #666;
+		}
+		article {
+			padding: 10dp;
+			margin: 0 5dp;
+			background-color: #edd3c0;
+		}
+		h2 {
+			text-align: center;
+			background-color: #eb6e14;
+			margin: -10dp -10dp 0;
+			padding: 10dp 0; 
+		}
+	</style>
+</head>
+<body>
+</body>
+</rml>
+)";
+
+static const String rml_flexbox_basic_document_fast = R"(
+<rml>
+<head>
+    <title>Flex 01 - Basic flexbox (fast, not content based)</title>
+    <link type="text/rcss" href="/../Tests/Data/style.rcss"/>
+	<style>
+		header, article { display: block; }
+		h1 { font-size: 1.5em; }
+		h2 { font-size: 1.3em; }
+		
+		header {
+			background-color: #9777d9;
+			border: 5dp #666;
+		}
+		h1 {
+			text-align: center;
+			color: white;
+			line-height: 100dp;
+		}
+		section {
+			display: flex;
+			background: #666;
+			border: 5dp #666;
+			height: 650px;
+		}
+		article {
+			padding: 10dp;
+			margin: 0 5dp;
+			background-color: #edd3c0;
+			flex: 1;
+			box-sizing: border-box;
+			height: 100%;
+		}
+		h2 {
+			text-align: center;
+			background-color: #eb6e14;
+			margin: -10dp -10dp 0;
+			padding: 10dp 0; 
+		}
+	</style>
+</head>
+<body>
+</body>
+</rml>
+)";
+
+static const String rml_flexbox_basic_document_float_reference = R"(
+<rml>
+<head>
+    <title>Flex 01 - Basic flexbox (float comparison)</title>
+    <link type="text/rcss" href="/../Tests/Data/style.rcss"/>
+	<style>
+		header, article { display: block; }
+		h1 { font-size: 1.5em; }
+		h2 { font-size: 1.3em; }
+		
+		header {
+			background-color: #9777d9;
+			border: 5dp #666;
+		}
+		h1 {
+			text-align: center;
+			color: white;
+			line-height: 100dp;
+		}
+		section {
+			display: block;
+			background: #666;
+			border: 5dp #666;
+		}
+		article {
+			padding: 10dp;
+			margin: 0 5dp;
+			background-color: #edd3c0;
+			float: left;
+			width: 30%;
+			box-sizing: border-box;
+		}
+		h2 {
+			text-align: center;
+			background-color: #eb6e14;
+			margin: -10dp -10dp 0;
+			padding: 10dp 0; 
+		}
+	</style>
+</head>
+<body>
+</body>
+</rml>
+)";
+
+static const String rml_flexbox_basic_body = R"(
+<header>
+	<h1>Header</h1>
+</header>
+<section>
+	<article>
+		<h2>First article</h2>
+		<p>Etiam libero lorem, lacinia non augue lobortis, tincidunt consequat justo. Sed id enim tempor, feugiat tortor id, rhoncus enim. Quisque pretium neque eu felis tincidunt fringilla. Mauris laoreet enim neque, iaculis cursus lorem mollis sed. Nulla pretium euismod nulla sed convallis. Curabitur in tempus sem. Phasellus suscipit vitae nulla nec ultricies.</p>
+	</article>
+	<article>
+		<h2>Second article</h2>
+		<p>Ut volutpat, odio et facilisis molestie, lacus elit euismod enim, et tempor lacus sapien finibus ipsum. Aliquam erat volutpat. Nullam risus turpis, hendrerit ac fermentum in, dapibus non risus.</p>
+	</article>
+	<article>
+		<h2>Third article</h2>
+		<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed aliquet commodo nisi, id cursus enim eleifend vitae. Praesent turpis lorem, commodo id tempus sit amet, faucibus et libero. Aliquam malesuada ultrices leo, ut molestie tortor posuere sit amet. Proin vitae tortor a sem consequat gravida. Maecenas sed egestas dolor.</p>
+		<p>In gravida ligula in turpis molestie varius. Ut sed velit id tellus aliquet aliquet. Nulla et leo tellus. Ut a convallis dolor, eu rutrum enim. Nam vitae ultrices dui. Aliquam semper eros ut ultrices rutrum.</p>
+	</article>
+</section>
+)";
+
+static const String rml_flexbox_mixed_document = R"(
+<rml>
+<head>
+    <title>Flex 02 - Various features</title>
+    <link type="text/rcss" href="/../Tests/Data/style.rcss"/>
+	<style>
+        .flex-container {
+            display: flex;
+            margin: 10px 20px;
+            background-color: #333;
+            max-height: 210px;
+            flex-wrap: wrap-reverse;
+        }
+
+        .flex-item {
+            width: 50px;
+            margin: 20px;
+            background-color: #eee;
+            height: 50px;
+            text-align: center;
+        }
+
+        .flex-direction-row {
+            flex-direction: row;
+        }
+        .flex-direction-row-reverse {
+            flex-direction: row-reverse;
+        }
+        .flex-direction-column {
+            flex-direction: column;
+        }
+        .flex-direction-column-reverse {
+            flex-direction: column-reverse;
+        }
+        .absolute {
+            margin: 0;
+            position: absolute;
+            right: 0;
+            bottom: 10px;
+        }
+	</style>
+</head>
+
+<body>
+</body>
+</rml>
+)";
+
+static const String rml_flexbox_mixed_body = R"(
+<div class="flex-container flex-direction-row" style="position: relative">
+    <div class="flex-item absolute">Abs</div>
+    <div class="flex-item" style="margin: 50px;">1</div>
+    <div class="flex-item" style="margin-top: auto">2</div>
+    <div class="flex-item" style="margin: auto">3</div>
+</div>
+<div class="flex-container flex-direction-row-reverse" style="height: 200px; justify-content: space-around;">
+    <div class="flex-item">1</div>
+    <div class="flex-item" style="margin-bottom: auto;">2</div>
+    <div class="flex-item" style="margin-right: 40px;">3</div>
+</div>
+<div class="flex-container flex-direction-column">
+    <div class="flex-item" id="test" style="margin-right: auto">1</div>
+    <div class="flex-item">2</div>
+    <div class="flex-item">3</div>
+</div>
+<div class="flex-container flex-direction-column-reverse">
+    <div class="flex-item">1</div>
+    <div class="flex-item">2 LONG_OVERFLOWING_WORD</div>
+    <div class="flex-item">3</div>
+</div>
+)";
+
+static const String rml_flexbox_scroll_document = R"(
+<rml>
+<head>
+    <title>Flex 03 - Scrolling container</title>
+    <link type="text/rcss" href="/../Tests/Data/style.rcss"/>
+	<style>
+		.flex {
+			display: flex;
+			background-color: #555;
+			margin: 5dp 20dp 15dp;
+			border: 2dp #333;
+			justify-content: space-between;
+			color: #d44fff;
+		}
+		.auto {
+			overflow: auto;
+		}
+		.scroll {
+			overflow: scroll;
+		}
+		.flex div {
+			flex: 0 1 auto;
+			width: 50dp;
+			height: 50dp;
+			margin: 20dp;
+			background-color: #eee;
+			line-height: 50dp;
+			text-align: center;
+		}
+		.flex div.tall {
+			height: 80dp;
+			width: 15dp;
+			margin: 0;
+			border: 2dp #d44fff;
+		}
+	</style>
+</head>
+<body>
+</body>
+</rml>
+)";
+
+static const String rml_flexbox_scroll_body = R"(
+overflow: scroll
+<div class="flex scroll" id="scroll">
+	<div>Hello<div class="tall"/></div>
+	<div>big world!</div>
+	<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+</div>
+overflow: auto
+<div class="flex auto" id="auto">
+	<div>Hello<div class="tall"/></div>
+	<div>big world!</div>
+	<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+</div>
+overflow: auto - only vertical overflow
+<div class="flex auto" id="vertical">
+	<div>Hello<div class="tall"/></div>
+	<div>big world!</div>
+	<div>LONG</div>
+</div>
+overflow: auto - only horizontal overflow
+<div class="flex auto" id="horizontal">
+	<div>Hello</div>
+	<div>big</div>
+	<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+</div>
+overflow: visible
+<div class="flex" id="visible">
+	<div>Hello<div class="tall"/></div>
+	<div>big world!</div>
+	<div>LOOOOOOOOOOOOOOOOOOOOONG</div>
+</div>
+)";
+
+TEST_CASE("flexbox")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	{
+		nanobench::Bench bench;
+		bench.title("Flexbox basic layout");
+		bench.relative(true);
+
+		// Construct the flexbox layout document.
+		ElementDocument* document = context->LoadDocumentFromMemory(rml_flexbox_basic_document);
+		REQUIRE(document);
+		document->Show();
+
+		document->SetInnerRML(rml_flexbox_basic_body);
+		context->Update();
+		context->Render();
+
+		TestsShell::RenderLoop();
+
+		// Compare to an almost equivalent fast flexbox layout where we try to eliminate any content-based sizing. Uses the same body rml.
+		ElementDocument* document_fast = context->LoadDocumentFromMemory(rml_flexbox_basic_document_fast);
+		REQUIRE(document_fast);
+		document_fast->Show();
+
+		document_fast->SetInnerRML(rml_flexbox_basic_body);
+		context->Update();
+		context->Render();
+
+		TestsShell::RenderLoop();
+
+		// Finally, add a reference document based on layout with float boxes instead of flexbox. Uses the same body rml.
+		ElementDocument* document_float_reference = context->LoadDocumentFromMemory(rml_flexbox_basic_document_float_reference);
+		REQUIRE(document_float_reference);
+		document_float_reference->Show();
+
+		document_float_reference->SetInnerRML(rml_flexbox_basic_body);
+		context->Update();
+		context->Render();
+
+		TestsShell::RenderLoop();
+
+		bench.run("Update (unmodified)", [&] { context->Update(); });
+
+		bench.run("Render", [&] { context->Render(); });
+
+		bench.run("SetInnerRML", [&] { document->SetInnerRML(rml_flexbox_scroll_body); });
+
+		bench.run("SetInnerRML + Update (float reference)", [&] {
+			document_float_reference->SetInnerRML(rml_flexbox_basic_body);
+			context->Update();
+		});
+		bench.run("SetInnerRML + Update (fast version)", [&] {
+			document_fast->SetInnerRML(rml_flexbox_basic_body);
+			context->Update();
+		});
+		bench.run("SetInnerRML + Update", [&] {
+			document->SetInnerRML(rml_flexbox_basic_body);
+			context->Update();
+		});
+
+		bench.run("SetInnerRML + Update + Render (float reference)", [&] {
+			document_float_reference->SetInnerRML(rml_flexbox_basic_body);
+			context->Update();
+			context->Render();
+		});
+		bench.run("SetInnerRML + Update + Render (fast version)", [&] {
+			document_fast->SetInnerRML(rml_flexbox_basic_body);
+			context->Update();
+			context->Render();
+		});
+		bench.run("SetInnerRML + Update + Render", [&] {
+			document->SetInnerRML(rml_flexbox_basic_body);
+			context->Update();
+			context->Render();
+		});
+
+		document->Close();
+		document_fast->Close();
+		document_float_reference->Close();
+	}
+
+	{
+		nanobench::Bench bench;
+		bench.title("Flexbox mixed");
+		bench.relative(true);
+
+		ElementDocument* document = context->LoadDocumentFromMemory(rml_flexbox_mixed_document);
+		REQUIRE(document);
+		document->Show();
+
+		document->SetInnerRML(rml_flexbox_mixed_body);
+		context->Update();
+		context->Render();
+
+		TestsShell::RenderLoop();
+
+		bench.run("Update (unmodified)", [&] { context->Update(); });
+
+		bench.run("Render", [&] { context->Render(); });
+
+		bench.run("SetInnerRML", [&] { document->SetInnerRML(rml_flexbox_scroll_body); });
+
+		bench.run("SetInnerRML + Update", [&] {
+			document->SetInnerRML(rml_flexbox_mixed_body);
+			context->Update();
+		});
+
+		bench.run("SetInnerRML + Update + Render", [&] {
+			document->SetInnerRML(rml_flexbox_mixed_body);
+			context->Update();
+			context->Render();
+		});
+
+		document->Close();
+	}
+
+	{
+		nanobench::Bench bench;
+		bench.title("Flexbox scroll");
+		bench.relative(true);
+
+		ElementDocument* document = context->LoadDocumentFromMemory(rml_flexbox_scroll_document);
+		REQUIRE(document);
+		document->Show();
+
+		document->SetInnerRML(rml_flexbox_scroll_body);
+		context->Update();
+		context->Render();
+
+		TestsShell::RenderLoop();
+
+		bench.run("Update (unmodified)", [&] { context->Update(); });
+
+		bench.run("Render", [&] { context->Render(); });
+
+		bench.run("SetInnerRML", [&] { document->SetInnerRML(rml_flexbox_scroll_body); });
+
+		bench.run("SetInnerRML + Update", [&] {
+			document->SetInnerRML(rml_flexbox_scroll_body);
+			context->Update();
+		});
+
+		bench.run("SetInnerRML + Update + Render", [&] {
+			document->SetInnerRML(rml_flexbox_scroll_body);
+			context->Update();
+			context->Render();
+		});
+
+		document->Close();
+	}
+}

+ 92 - 0
Tests/Source/UnitTests/Properties.cpp

@@ -0,0 +1,92 @@
+/*
+ * 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 "../Common/TestsInterface.h"
+#include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/Core.h>
+#include <RmlUi/Core/Element.h>
+#include <RmlUi/Core/ElementDocument.h>
+#include <doctest.h>
+
+using namespace Rml;
+
+TEST_CASE("Properties")
+{
+	const Vector2i window_size(1024, 768);
+
+	TestsSystemInterface system_interface;
+	TestsRenderInterface render_interface;
+
+	SetRenderInterface(&render_interface);
+	SetSystemInterface(&system_interface);
+
+	Rml::Initialise();
+
+	Context* context = Rml::CreateContext("main", window_size);
+	ElementDocument* document = context->CreateDocument();
+
+	struct FlexTestCase {
+		String flex_value;
+
+		struct ExpectedValues {
+			float flex_grow;
+			float flex_shrink;
+			String flex_basis;
+		} expected;
+	};
+
+	FlexTestCase tests[] = {
+		{"", {0.f, 1.f, "auto"}},
+		{"none", {0.f, 0.f, "auto"}},
+		{"auto", {1.f, 1.f, "auto"}},
+		{"1", {1.f, 1.f, "0px"}},
+		{"2", {2.f, 1.f, "0px"}},
+		{"2 0", {2.f, 0.f, "0px"}},
+		{"2 3", {2.f, 3.f, "0px"}},
+		{"2 auto", {2.f, 1.f, "auto"}},
+		{"2 0 auto", {2.f, 0.f, "auto"}},
+		{"0 0 auto", {0.f, 0.f, "auto"}},
+		{"0 0 50px", {0.f, 0.f, "50px"}},
+		{"0 0 50px", {0.f, 0.f, "50px"}},
+		{"0 0 0", {0.f, 0.f, "0px"}},
+	};
+
+	for (const FlexTestCase& test : tests)
+	{
+		if (!test.flex_value.empty())
+		{
+			CHECK(document->SetProperty("flex", test.flex_value));
+		}
+
+		CHECK(document->GetProperty<float>("flex-grow") == test.expected.flex_grow);
+		CHECK(document->GetProperty<float>("flex-shrink") == test.expected.flex_shrink);
+		CHECK(document->GetProperty("flex-basis")->ToString() == test.expected.flex_basis);
+	}
+
+	Rml::Shutdown();
+}

+ 59 - 5
Tests/Tools/convert_css_test_suite_to_rml.py

@@ -82,6 +82,19 @@ if args.clean:
 			except Exception as e:
 				print('Failed to delete {}. Reason: {}'.format(path, e))
 
+html_color_mapping = {
+	"lightblue": "#add8e6",
+	"lightgrey": "#d3d3d3",
+	"lightgray": "#d3d3d3",
+	"lightgreen": "#90ee90",
+	"pink": "#ffc0cb",
+	"coral": "#ff7f50",
+	"slateblue": "#6a5acd",
+	"steelblue": "#4682b4",
+	"tan": "#d2b48c",
+	"violet": "#ee82ee",
+}
+
 def border_format(side: str, type: str, content: str):
 	# Side: (empty)/-top/-right/-bottom/-left
 	# Type: (empty)/-width/-style/-color
@@ -90,8 +103,13 @@ def border_format(side: str, type: str, content: str):
 	content = content.replace("medium", "3px")
 	content = content.replace("thin", "1px")
 
-	if type == "-width" or type == "-color":
+	if type == "-width":
 		return "border" + side + type + ": " + content
+	if type == "-color":
+		color = content.strip()
+		if color in html_color_mapping:
+			color = html_color_mapping[color]
+		return "border" + side + type + ": " + color
 
 	# Convert style to width. This is not perfect, but seems to be the most used case.
 	if type == "-style":
@@ -116,6 +134,8 @@ def border_format(side: str, type: str, content: str):
 	color = re.search(r'\b([a-z]+|#[0-9a-f]+)\b', content)
 	if color:
 		color = color.group(1)
+		if color in html_color_mapping:
+			color = html_color_mapping[color]
 	else:
 		color = "black"
 
@@ -148,6 +168,8 @@ assert( border_find_replace("margin:10px; border:20px solid black; padding:30px;
 assert( border_find_replace(" border-left: 7px solid navy; border-right: 17px solid navy; } ") == ' border-left: 7px navy; border-right: 17px navy; } ' )
 assert( border_find_replace(" border: blue solid 3px; ") == ' border: 3px blue; ' )
 assert( border_find_replace(" border: solid lime; ") == ' border: 3px lime; ' )
+assert( border_find_replace(" border: 1px pink; ") == ' border: 1px #ffc0cb; ' )
+assert( border_find_replace(" border-color: pink; ") == ' border-color: #ffc0cb; ' )
 assert( border_find_replace(" border: 0; ") == ' border: 0 black; ' )
 assert( border_find_replace(" border-bottom: 0.25em solid green; ") == ' border-bottom: 0.25em green; ' )
 assert( border_find_replace(" border-width: 0; ") == ' border-width:  0; ' )
@@ -216,10 +238,41 @@ def process_file(in_file):
 		line = re.sub(r'(line-height:)\s*normal', r'\1 1.2em', line, flags = re.IGNORECASE)
 		line = re.sub(r'cyan', r'aqua', line, flags = re.IGNORECASE)
 
+		line = re.sub(r'flex: none;', r'flex: 0 0 auto;', line, flags = re.IGNORECASE)
+		line = re.sub(r'align-content:\s*(start|end)', r'align-content: flex-\1', line, flags = re.IGNORECASE)
+		line = re.sub(r'align-content:\s*space-evenly', r'align-content: space-around', line, flags = re.IGNORECASE)
+		line = re.sub(r'justify-content:\s*space-evenly', r'justify-content: space-around', line, flags = re.IGNORECASE)
+		line = re.sub(r'justify-content:\s*left', r'justify-content: flex-start', line, flags = re.IGNORECASE)
+		line = re.sub(r'justify-content:\s*right', r'justify-content: flex-end', line, flags = re.IGNORECASE)
+
 		if re.search(r'background:[^;}\"]*fixed', line, flags = re.IGNORECASE):
 			print("File '{}' skipped since it uses unsupported background.".format(in_file))
 			return False
 		line = re.sub(r'background:(\s*([a-z]+|#[0-9a-f]+)\s*[;}\"])', r'background-color:\1', line, flags = re.IGNORECASE)
+		prev_end = 0
+		new_line = ""
+		for match in re.finditer(r'background-color:([^;]*)([;"])', line, flags = re.IGNORECASE):
+			color = match.group(1).strip()
+			delimiter = match.group(2)
+			if color in html_color_mapping:
+				color = html_color_mapping[color]
+			new_line += line[prev_end:match.start()] + 'background-color: ' + color + delimiter
+			prev_end = match.end()
+			
+		new_line += line[prev_end:]
+		line = new_line
+
+		prev_end = 0
+		new_line = ""
+		for match in re.finditer(r'calc\(\s*(\d+)(\w{1,3})\s*/\s*(\d)\s*\)', line, flags = re.IGNORECASE):
+			num = match.group(1)
+			unit = match.group(2)
+			den = match.group(3)
+			calc_result = "{}{}".format(float(num) / float(den), unit)
+			new_line += line[prev_end:match.start()] + calc_result
+			prev_end = match.end()
+		new_line += line[prev_end:]
+		line = new_line
 
 		line = border_find_replace(line)
 
@@ -229,7 +282,7 @@ def process_file(in_file):
 		if flags_match and flags_match[1] != '' and flags_match[1] != 'interactive':
 			print("File '{}' skipped due to flags '{}'".format(in_file, flags_match[1]))
 			return False
-		if re.search(r'(display:[^;]*(table|run-in|list-item))|(<table)', line, flags = re.IGNORECASE):
+		if re.search(r'(display:[^;]*(table|run-in|list-item|inline-flex))|(<table)', line, flags = re.IGNORECASE):
 			print("File '{}' skipped since it uses tables.".format(in_file))
 			return False
 		if re.search(r'visibility:[^;]*collapse|z-index:\s*[0-9\.]+%', line, flags = re.IGNORECASE):
@@ -250,9 +303,10 @@ def process_file(in_file):
 		if re.search(r'@font-face|font:|ahem', line, flags = re.IGNORECASE):
 			print("File '{}' skipped since it uses special fonts.".format(in_file))
 			return False
-		if re.search(r'(direction:[^;]*[;"])|(content:[^;]*[;"])|(outline:[^;]*[;"])|(quote:[^;]*[;"])|(border-spacing:[^;]*[;"])|(border-collapse:[^;]*[;"])|(background:[^;]*[;"])|(box-sizing:[^;]*[;"])', line, flags = re.IGNORECASE)\
-			or re.search(r'(font-variant:[^;]*[;"])|(font-kerning:[^;]*[;"])|(font-feature-settings:[^;]*[;"])|(background-image:[^;]*[;"])|(caption-side:[^;]*[;"])|(clip:[^;]*[;"])|(page-break-inside:[^;]*[;"])|(word-spacing:[^;]*[;"])', line, flags = re.IGNORECASE)\
-			or re.search(r'(writing-mode:[^;]*[;"])|(text-orientation:[^;]*[;"])|(text-indent:[^;]*[;"])|(page-break-after:[^;]*[;"])|(column[^:]*:[^;]*[;"])|(empty-cells:[^;]*[;"])', line, flags = re.IGNORECASE):
+		if re.search(r'\b((direction:[^;]*[;"])|(content:[^;]*[;"])|(outline:[^;]*[;"])|(quote:[^;]*[;"])|(border-spacing:[^;]*[;"])|(border-collapse:[^;]*[;"])|(background:[^;]*[;"]))', line, flags = re.IGNORECASE)\
+			or re.search(r'\b((font-variant:[^;]*[;"])|(font-kerning:[^;]*[;"])|(font-feature-settings:[^;]*[;"])|(background-image:[^;]*[;"])|(caption-side:[^;]*[;"])|(clip:[^;]*[;"])|(page-break-inside:[^;]*[;"])|(word-spacing:[^;]*[;"]))', line, flags = re.IGNORECASE)\
+			or re.search(r'\b((writing-mode:[^;]*[;"])|(text-orientation:[^;]*[;"])|(text-indent:[^;]*[;"])|(page-break-after:[^;]*[;"])|(page-break-before:[^;]*[;"])|(column[^:]*:[^;]*[;"])|(empty-cells:[^;]*[;"]))', line, flags = re.IGNORECASE)\
+			or re.search(r'\b((aspect-ratio:[^;]*[;"])|(flex-basis:[^;]*[;"])|(order:[^;]*[;"]))', line, flags = re.IGNORECASE):
 			print("File '{}' skipped since it uses unsupported CSS properties.".format(in_file))
 			return False
 		data += line