Browse Source

Initial support for tables

Michael Ragazzon 5 years ago
parent
commit
f529a8cf51

+ 2 - 0
CMake/FileList.cmake

@@ -64,6 +64,7 @@ set(Core_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutInlineBox.h
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutInlineBoxText.h
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutLineBox.h
+    ${PROJECT_SOURCE_DIR}/Source/Core/LayoutTable.h
     ${PROJECT_SOURCE_DIR}/Source/Core/Memory.h
     ${PROJECT_SOURCE_DIR}/Source/Core/PluginRegistry.h
     ${PROJECT_SOURCE_DIR}/Source/Core/Pool.h
@@ -334,6 +335,7 @@ set(Core_SRC_FILES
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutInlineBox.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutInlineBoxText.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/LayoutLineBox.cpp
+    ${PROJECT_SOURCE_DIR}/Source/Core/LayoutTable.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Log.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Math.cpp
     ${PROJECT_SOURCE_DIR}/Source/Core/Memory.cpp

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

@@ -60,7 +60,7 @@ struct NumberAuto {
 using Margin = LengthPercentageAuto;
 using Padding = LengthPercentage;
 
-enum class Display : uint8_t { None, Block, Inline, InlineBlock };
+enum class Display : uint8_t { None, Block, Inline, InlineBlock, Table, TableRow, TableCell };
 enum class Position : uint8_t { Static, Relative, Absolute, Fixed };
 
 using Top = LengthPercentageAuto;

+ 43 - 2
Source/Core/LayoutEngine.cpp

@@ -30,6 +30,7 @@
 #include "LayoutBlockBoxSpace.h"
 #include "LayoutDetails.h"
 #include "LayoutInlineBoxText.h"
+#include "LayoutTable.h"
 #include "Pool.h"
 #include "../../Include/RmlUi/Core/Element.h"
 #include "../../Include/RmlUi/Core/Profiling.h"
@@ -49,9 +50,9 @@ struct LayoutChunk
 static Pool< LayoutChunk > layout_chunk_pool(200, true);
 
 // Formats the contents for a root-level element (usually a document or floating element).
-void LayoutEngine::FormatElement(Element* element, Vector2f containing_block, const Box* override_initial_box)
+void LayoutEngine::FormatElement(Element* element, Vector2f containing_block, const Box* override_initial_box, Vector2f* visible_overflow_size)
 {
-	RMLUI_ASSERT(element);
+	RMLUI_ASSERT(element && containing_block.x >= 0 && containing_block.y >= 0);
 #ifdef RMLUI_ENABLE_PROFILING
 	RMLUI_ZoneScopedC(0xB22222);
 	auto name = CreateString(80, "%s %x", element->GetAddress(false, false).c_str(), element);
@@ -84,6 +85,10 @@ void LayoutEngine::FormatElement(Element* element, Vector2f containing_block, co
 	}
 
 	block_context_box->CloseAbsoluteElements();
+
+	if (visible_overflow_size)
+		*visible_overflow_size = block_context_box->GetVisibleOverflowSize();
+
 	element->OnLayout();
 }
 
@@ -141,6 +146,10 @@ bool LayoutEngine::FormatElement(LayoutBlockBox* block_context_box, Element* ele
 		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::Table:       return FormatElementTable(block_context_box, element);
+
+		case Style::Display::TableRow:
+		case Style::Display::TableCell:   RMLUI_ERROR; /* should always be formatted in the table context formatter, if we get here it means the user has added a sporadic 'display: table-row/-cell', or we have a bug */ break;
 		case Style::Display::None:        RMLUI_ERROR; /* handled above */ break;
 	}
 
@@ -236,6 +245,38 @@ bool LayoutEngine::FormatElementInlineBlock(LayoutBlockBox* block_context_box, E
 	return true;
 }
 
+
+bool LayoutEngine::FormatElementTable(LayoutBlockBox* block_context_box, Element* element_table)
+{
+	LayoutBlockBox* table_block_context_box = nullptr;
+	
+	{
+		Box box;
+		float min_height, max_height;
+		LayoutDetails::BuildBox(box, min_height, max_height, block_context_box, element_table, false);
+
+		table_block_context_box = block_context_box->AddBlockElement(element_table, box, min_height, max_height);
+	}
+
+	if (!table_block_context_box)
+		return false;
+
+	for (int i = 0; i < 2; i++)
+	{
+		LayoutBlockBox::CloseResult result = LayoutTable::FormatTable(table_block_context_box, element_table);
+
+		// If the close failed, it probably means that the table or its parent produced scrollbars. Try again, but only once.
+		if (result == LayoutBlockBox::LAYOUT_SELF)
+			continue;
+		else if (result == LayoutBlockBox::LAYOUT_PARENT)
+			return false;
+		else if (result == LayoutBlockBox::OK)
+			break;
+	}
+
+	return true;
+}
+
 // Executes any special formatting for special elements.
 bool LayoutEngine::FormatElementSpecial(LayoutBlockBox* block_context_box, Element* element)
 {

+ 6 - 2
Source/Core/LayoutEngine.h

@@ -47,7 +47,7 @@ public:
 	/// @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.
-	static void FormatElement(Element* element, Vector2f containing_block, const Box* override_initial_box = nullptr);
+	static void FormatElement(Element* element, Vector2f containing_block, const Box* override_initial_box = nullptr, Vector2f* visible_overflow_size = nullptr);
 
 	/// Positions a single element and its children within a block formatting context.
 	/// @param[in] block_context_box The open block box to layout the element in.
@@ -70,7 +70,11 @@ 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);
-	/// Executes any special formatting for special elements.
+	/// 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.
+	static bool FormatElementTable(LayoutBlockBox* block_context_box, Element* element);
+
 	/// @param[in] block_context_box The open block box to layout the element in.
 	/// @param[in] element The element to parse.
 	/// @return True if the element was parsed as a special element, false otherwise.

+ 469 - 0
Source/Core/LayoutTable.cpp

@@ -0,0 +1,469 @@
+/*
+ * 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 "LayoutTable.h"
+#include "LayoutDetails.h"
+#include "LayoutEngine.h"
+#include "../../Include/RmlUi/Core/Element.h"
+#include "../../Include/RmlUi/Core/Profiling.h"
+#include "../../Include/RmlUi/Core/Types.h"
+#include <cstddef>
+#include <algorithm>
+
+namespace Rml {
+
+
+LayoutTable::CloseResult LayoutTable::FormatTable(LayoutBlockBox* table_block_context_box, Element* element_table)
+{
+	const Vector2f content_top_left = table_block_context_box->GetBox().GetPosition();
+	const Vector2f content_containing_block = Vector2f(table_block_context_box->GetBox().GetSize().x, Math::Max(0.0f, table_block_context_box->GetBox().GetSize().y));
+
+	const Vector2f table_gap(10.f, 0.f);
+
+	Vector<float> column_border_widths = DetermineColumnWidths(element_table, content_containing_block, table_gap);
+
+	// Now that we have the size of each column, we can move on to formatting the elements.
+	// After we format and size an element, we record its height as well, and keep the maximum_height over all cells in the current row.
+	// At the end of a row, we then know the height of the row, and we can proceed by positioning the cells.
+
+	Vector2f table_content_overflow_size;
+
+	Vector2f table_cursor = content_top_left;
+	table_cursor.y -= table_gap.y;
+
+	struct Cell {
+		Element* element_cell;
+		int row_last; // The last row the cell spans
+		int column_begin, column_last;
+		Box box;
+		Vector2f table_offset;
+		float rows_accumulated_height;
+	};
+	Vector<Cell> cells;
+	cells.reserve(column_border_widths.size());
+
+	const int num_table_children = element_table->GetNumChildren();
+	int row = -1;
+	for (int i = 0; i < num_table_children; i++)
+	{
+		Element* element_row = element_table->GetChild(i);
+
+		const ComputedValues& computed_row = element_row->GetComputedValues();
+		if (computed_row.display != Style::Display::TableRow)
+			continue;
+
+		row += 1;
+
+		Box row_box;
+		float row_min_height, row_max_height;
+		LayoutDetails::BuildBox(row_box, content_containing_block, element_row, false, 0.f);
+		LayoutDetails::GetMinMaxHeight(row_min_height, row_max_height, &computed_row, row_box, content_containing_block.y);
+
+		// Prepare the cursor for this row
+		table_cursor.x = content_top_left.x;
+		table_cursor.y += table_gap.y;
+
+		const Vector2f row_element_offset = table_cursor + Vector2f(row_box.GetEdge(Box::MARGIN, Box::LEFT), row_box.GetEdge(Box::MARGIN, Box::TOP));
+
+		{
+			const float row_top_offset = row_box.GetEdge(Box::MARGIN, Box::TOP) + row_box.GetEdge(Box::BORDER, Box::TOP) + row_box.GetEdge(Box::PADDING, Box::TOP);
+			table_cursor.y += row_top_offset;
+
+			for (Cell& cell : cells)
+				cell.rows_accumulated_height += row_top_offset;
+		}
+
+		const int num_cells_spanning_this_row = (int)cells.size();
+		const int num_row_children = element_row->GetNumChildren();
+
+		// For all child cell elements of this row, add them to the list of cells and determine their position.
+		for (int j = 0, column = 0; j < num_row_children; j++)
+		{
+			Element* element_cell = element_row->GetChild(j);
+
+			const ComputedValues& computed_cell = element_cell->GetComputedValues();
+			if (computed_cell.display != Style::Display::TableCell)
+				continue;
+
+			const int row_span = Math::Max(1, element_cell->GetAttribute("rowspan", 1));
+			const int col_span = Math::Max(1, element_cell->GetAttribute("colspan", 1));
+
+			float spanning_column_border_width = 0;
+			int column_last = -1;
+
+			{
+				// Offset the column if we have any rowspan elements from previous rows overlapping with the current column.
+				const int column_offset_from = column;
+
+				for (bool continue_offset_column = true; continue_offset_column; )
+				{
+					continue_offset_column = false;
+					for (int k = 0; k < num_cells_spanning_this_row; k++)
+					{
+						if (column >= cells[k].column_begin && column <= cells[k].column_last)
+						{
+							column = cells[k].column_last + 1;
+							continue_offset_column = true;
+							break;
+						}
+					}
+				}
+
+				column_last = column + col_span - 1;
+
+				if (column_last >= (int)column_border_widths.size())
+				{
+					Log::Message(Log::LT_WARNING, "Too many columns in table row %d: %s\nThe number of columns is %d, as determined by the first table row.",
+						row + 1, element_row->GetAddress().c_str(), (int)column_border_widths.size());
+					break;
+				}
+
+				for (int k = column_offset_from; k < column; k++)
+					table_cursor.x += column_border_widths[k];
+
+				table_cursor.x += table_gap.x * float(column - column_offset_from);
+
+				for (int k = column; k <= column_last; k++)
+					spanning_column_border_width += column_border_widths[k];
+
+				spanning_column_border_width += table_gap.x * float(col_span - 1);
+			}
+
+			// Add the new cell to our list.
+			cells.emplace_back();
+			Cell& cell = cells.back();
+
+			cell.row_last = row + row_span - 1;
+			cell.column_begin = column;
+			cell.column_last = column_last;
+			cell.element_cell = element_cell;
+			cell.table_offset = table_cursor;
+			cell.rows_accumulated_height = 0;
+
+			// Determine the cell's box for formatting later, we may get an indefinite (-1) vertical content size.
+			Box& box = cell.box;
+			LayoutDetails::BuildBox(box, content_containing_block, element_cell, false, 0.f);
+
+			// Determine the box width from the current column width.
+			const float content_width = Math::Max(0.0f, spanning_column_border_width - box.GetSizeAcross(Box::HORIZONTAL, Box::BORDER, Box::PADDING));
+			box.SetContent(Vector2f(content_width, box.GetSize().y));
+
+			table_cursor.x += spanning_column_border_width + table_gap.x;
+
+			column += col_span;
+		}
+
+		// Partition the cells to determine those who end at this row.
+		const auto it_cells_in_row_end = std::partition(cells.begin(), cells.end(), [row](const Cell& cell) { return cell.row_last == row; });
+
+		// Determine the row height.
+		float row_content_height = 0;
+		if (row_box.GetSize().y >= 0)
+		{
+			//  The row has a definite size, use that.
+			row_content_height = row_box.GetSize().y;
+		}
+		else
+		{
+			// The row does not have a definite size, we will use the maximum height of all its cells to determine the row height.
+			// For each cell in this row, or spanning onto this row from any previous rows, increase the row height as necessary to make the cell fit.
+			for (auto it = cells.begin(); it != it_cells_in_row_end; ++it)
+			{
+				Cell& cell = *it;
+				Element* element_cell = cell.element_cell;
+				Box& box = cell.box;
+
+				// If the cell's height is also indefinite, we need to format it to get its height.
+				if (box.GetSize().y < 0)
+				{
+					LayoutEngine::FormatElement(element_cell, content_containing_block, &box);
+					box.SetContent(element_cell->GetBox().GetSize());
+				}
+
+				row_content_height = Math::Max(row_content_height, box.GetSizeAcross(Box::VERTICAL, Box::BORDER) - cell.rows_accumulated_height);
+			}
+		}
+
+		row_content_height = Math::Clamp(row_content_height, row_min_height, row_max_height);
+
+		for (Cell& cell : cells)
+			cell.rows_accumulated_height += row_content_height;
+
+		// Now we have the height of the row, position and format each cell.
+		auto FormatCellsInRow = [row, &cells, it_cells_in_row_end, &table_content_overflow_size, element_table, content_containing_block, content_top_left]() {
+
+			// Assumes the cells are already partitioned.
+			for (auto it = cells.begin(); it != it_cells_in_row_end; ++it)
+			{
+				Cell& cell = *it;
+				Element* element_cell = cell.element_cell;
+				Box& box = cell.box;
+				Style::VerticalAlign vertical_align = cell.element_cell->GetComputedValues().vertical_align;
+
+				if (box.GetSize().y < 0)
+				{
+					const bool is_aligned = (vertical_align.type == Style::VerticalAlign::Middle || vertical_align.type == Style::VerticalAlign::Bottom);
+					if (is_aligned)
+					{
+						// The size of the cell is indefinite, we need to get the height by formatting the cell.
+						LayoutEngine::FormatElement(element_cell, content_containing_block, &box);
+						box.SetContent(element_cell->GetBox().GetSize());
+					}
+					else
+					{
+						box.SetContent(Vector2f(box.GetSize().x, Math::Max(0.0f, cell.rows_accumulated_height - box.GetSizeAcross(Box::VERTICAL, Box::BORDER, Box::PADDING))));
+					}
+				}
+
+				const float available_height = cell.rows_accumulated_height - box.GetSizeAcross(Box::VERTICAL, Box::BORDER);
+
+				if (available_height > 0)
+				{
+					// Pad the cell for vertical alignment
+					float add_padding_top;
+					float add_padding_bottom;
+
+					switch (vertical_align.type)
+					{
+					case Style::VerticalAlign::Bottom:
+						add_padding_top = available_height;
+						add_padding_bottom = 0;
+						break;
+					case Style::VerticalAlign::Middle:
+						add_padding_top = 0.5f * available_height;
+						add_padding_bottom = 0.5f * available_height;
+						break;
+					case Style::VerticalAlign::Top:
+					default:
+						add_padding_top = 0.0f;
+						add_padding_bottom = available_height;
+					}
+
+					box.SetEdge(Box::PADDING, Box::TOP, box.GetEdge(Box::PADDING, Box::TOP) + add_padding_top);
+					box.SetEdge(Box::PADDING, Box::BOTTOM, box.GetEdge(Box::PADDING, Box::BOTTOM) + add_padding_bottom);
+				}
+
+				{
+					// Format the cell in a new block formatting context.
+					Vector2f cell_visible_overflow_size;
+					LayoutEngine::FormatElement(element_cell, content_containing_block, &box, &cell_visible_overflow_size);
+
+					table_content_overflow_size.x = Math::Max(table_content_overflow_size.x, cell.table_offset.x - content_top_left.x + cell_visible_overflow_size.x);
+					table_content_overflow_size.y = Math::Max(table_content_overflow_size.y, cell.table_offset.y - content_top_left.y + cell_visible_overflow_size.y);
+
+					// Set the position of the element within the the table container
+					element_cell->SetOffset(cell.table_offset, element_table);
+				}
+
+			}
+
+			// Remove the formatted cells from pending
+			cells.erase(cells.begin(), it_cells_in_row_end);
+		};
+
+		FormatCellsInRow();
+
+		// TODO: What to do with any remaining row-spanning cells after the last row?
+
+		// Position and size the row element
+		if (row_box.GetSize().y < 0.0f)
+			row_box.SetContent(Vector2f(row_box.GetSize().x, row_content_height));
+
+		element_row->SetOffset(row_element_offset, element_table);
+		element_row->SetBox(row_box);
+
+		const float row_bottom_offset = row_box.GetEdge(Box::MARGIN, Box::BOTTOM) + row_box.GetEdge(Box::BORDER, Box::BOTTOM) + row_box.GetEdge(Box::PADDING, Box::BOTTOM);
+		table_cursor.y += row_content_height + row_bottom_offset;
+
+		for (Cell& cell : cells)
+			cell.rows_accumulated_height += row_bottom_offset;
+	}
+
+	table_block_context_box->ExtendInnerContentSize(table_content_overflow_size);
+
+	CloseResult result = table_block_context_box->Close();
+	return result;
+}
+
+
+LayoutTable::ColumnWidths LayoutTable::DetermineColumnWidths(Element* element_table, const Vector2f containing_block, const Vector2f table_gap)
+{
+	ColumnWidths column_widths;
+
+	// Determine the widths of the cells.
+	Element* element_first_row = nullptr;
+
+	for (int i = 0; i < element_table->GetNumChildren(); i++)
+	{
+		element_first_row = element_table->GetChild(i);
+		if (element_first_row->GetDisplay() != Style::Display::TableRow)
+		{
+			Log::Message(Log::LT_WARNING, "Only table rows ('display: table-row') are allowed as children of tables. %s", element_first_row->GetAddress().c_str());
+		}
+		else
+		{
+			break;
+		}
+	}
+
+	if (!element_first_row)
+	{
+		Log::Message(Log::LT_WARNING, "No rows in table. %s", element_table->GetAddress().c_str());
+		// TODO: Handle this more gracefully.
+		return ColumnWidths();
+	}
+
+
+	const int num_row_children = element_first_row->GetNumChildren();
+
+	struct CellMetrics {
+		float fixed_content_width;
+		float padding_border_edges_width;
+		float flex_width;
+		float min_content_width;
+		float max_content_width;
+	};
+	Vector<CellMetrics> cell_metrics;
+	cell_metrics.reserve(num_row_children);
+
+	for (int i = 0; i < num_row_children; i++)
+	{
+		Element* element_cell = element_first_row->GetChild(i);
+
+		const ComputedValues& computed = element_cell->GetComputedValues();
+
+		if (computed.display != Style::Display::TableCell)
+		{
+			Log::Message(Log::LT_WARNING, "Only table cells ('display: table-cell') are allowed as children of table rows. %s", element_cell->GetAddress().c_str());
+			continue;
+		}
+
+		const float padding_border_edges_width =
+			Math::Max(0.0f, ResolveValue(computed.padding_left, containing_block.x)) +
+			Math::Max(0.0f, ResolveValue(computed.padding_right, containing_block.x)) +
+			Math::Max(0.0f, computed.border_left_width) +
+			Math::Max(0.0f, computed.border_right_width);
+
+		auto min_max_widths = [containing_block_width = containing_block.x](float& min_width, float& max_width, float border_padding_edges_width, const ComputedValues& computed) {
+			min_width = ResolveValue(computed.min_width, containing_block_width);
+			max_width = (computed.max_width.value < 0.f ? FLT_MAX : ResolveValue(computed.max_width, containing_block_width));
+
+			if (computed.box_sizing == Style::BoxSizing::BorderBox)
+			{
+				min_width = Math::Max(0.0f, min_width - border_padding_edges_width);
+				max_width = Math::Max(0.0f, max_width - border_padding_edges_width);
+			}
+		};
+
+		CellMetrics metric = {};
+		metric.padding_border_edges_width = padding_border_edges_width;
+		min_max_widths(metric.min_content_width, metric.max_content_width, padding_border_edges_width, computed);
+
+		if (computed.width.type == Style::Width::Auto)
+		{
+			metric.flex_width = 1;
+		}
+		else
+		{
+			if (computed.box_sizing == Style::BoxSizing::ContentBox)
+				metric.fixed_content_width = ResolveValue(computed.width, containing_block.x);
+			else
+				metric.fixed_content_width = Math::Max(0.f, ResolveValue(computed.width, containing_block.x - padding_border_edges_width));
+
+			metric.fixed_content_width = Math::Clamp(metric.fixed_content_width, metric.min_content_width, metric.max_content_width);
+		}
+
+		const int colspan = Math::Max(1, element_cell->GetAttribute("colspan", 1));
+		if (colspan > 1)
+		{
+			const float width_factor = 1.f / float(colspan);
+			metric.fixed_content_width *= width_factor;
+			metric.min_content_width *= width_factor;
+			metric.max_content_width *= width_factor;
+		}
+
+		for(int j = 0; j < colspan; j++)
+			cell_metrics.push_back(metric);
+	}
+
+
+	auto calculate_fr_to_px_ratio = [table_content_width = containing_block.x, column_gap = table_gap.x, &cell_metrics]() {
+		float sum_fixed_width = 0; // [px]
+		float sum_flex_width = 0;  // [fr]
+
+		for (const CellMetrics& metric : cell_metrics)
+		{
+			sum_flex_width += metric.flex_width;
+			sum_fixed_width += (metric.flex_width == 0.f ? metric.fixed_content_width : 0.0f) + metric.padding_border_edges_width + column_gap;
+		}
+
+		sum_flex_width = Math::Max(1.f, sum_flex_width);
+		sum_fixed_width = Math::Max(0.f, sum_fixed_width - column_gap);
+
+		const float available_flex_width = table_content_width - sum_fixed_width;
+		const float fr_to_px_ratio = available_flex_width / sum_flex_width;
+
+		return fr_to_px_ratio;
+	};
+
+
+	for (bool continue_iteration = true; continue_iteration; )
+	{
+		continue_iteration = false;
+		const float fr_to_px_ratio = calculate_fr_to_px_ratio();
+
+		for (auto& metric : cell_metrics)
+		{
+			if (metric.flex_width > 0)
+			{
+				const float fixed_flex_width = metric.flex_width * fr_to_px_ratio;
+				metric.fixed_content_width = Math::Clamp(fixed_flex_width, metric.min_content_width, metric.max_content_width);
+
+				if (metric.fixed_content_width != fixed_flex_width)
+				{
+					// We met a min/max-constraint, fix the size of this column.
+					metric.flex_width = 0.0f;
+					continue_iteration = true;
+				}
+			}
+		}
+	}
+
+	column_widths.resize(cell_metrics.size());
+
+	for (size_t i = 0; i < cell_metrics.size(); i++)
+	{
+		column_widths[i] = cell_metrics[i].fixed_content_width + cell_metrics[i].padding_border_edges_width;
+	}
+
+	// TODO: What to do with any left-over space? Distribute evenly across columns, or make table smaller etc.
+
+	return column_widths;
+}
+
+} // namespace Rml

+ 57 - 0
Source/Core/LayoutTable.h

@@ -0,0 +1,57 @@
+/*
+ * 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_LAYOUTTABLE_H
+#define RMLUI_CORE_LAYOUTTABLE_H
+
+#include "LayoutBlockBox.h"
+#include "../../Include/RmlUi/Core/Types.h"
+
+namespace Rml {
+
+class Box;
+
+class LayoutTable
+{
+public:
+	using CloseResult = LayoutBlockBox::CloseResult;
+
+	/// 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.
+	static CloseResult FormatTable(LayoutBlockBox* table_block_context_box, Element* element_table);
+
+
+private:
+	using ColumnWidths = Vector<float>;
+
+	static ColumnWidths DetermineColumnWidths(Element* element_table, const Vector2f containing_block, const Vector2f table_gap);
+};
+
+} // namespace Rml
+#endif

+ 1 - 1
Source/Core/StyleSheetSpecification.cpp

@@ -308,7 +308,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");
+	RegisterProperty(PropertyId::Display, "display", "inline", false, true).AddParser("keyword", "none, block, inline, inline-block, table, table-row, 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")

+ 76 - 0
Tests/Data/VisualTests/table_01.rml

@@ -0,0 +1,76 @@
+<rml>
+<head>
+    <title>Table</title>
+    <link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://www.w3.org/TR/2011/REC-CSS2-20110607/visudet.html#inline-replaced-width" />
+	<meta name="Description" content="Basic table" />
+	<style>
+		@spritesheet theme 
+		{
+			src: /assets/invader.tga;
+			invader: 179px 152px 50px 40px;
+		}
+		body { overflow-x: auto; }
+		handle {
+			position: fixed;
+			background: #555a;
+			width: 16px;
+			height: 16px;
+			bottom: 0px;
+			right: 0px;
+			clip: 1;
+			cursor: resize;
+		}
+		table {
+			box-sizing: border-box;
+			display: table;
+			border: 5px #666;
+		}
+		tr {
+			box-sizing: border-box;
+			display: table-row;
+			border-bottom: 5px #ec3;
+			/*background: #3cc;*/
+			padding-top: 15px;
+			padding-bottom: 15px;
+		}
+		td {
+			box-sizing: border-box;
+			display: table-cell;
+			border-right: 5px red;
+			background: #bbb;
+		}
+		.bottom-right {
+			position: absolute;
+			bottom: 0;
+			right: 0;
+			width: 15px;
+			height: 15px;
+		}
+	</style>
+</head>
+
+<body>
+<p>Try resizing the table using the document handle in the lower-right corner.</p>
+<table>
+	<tr>
+		<td style="min-width: 150px; max-width: 250px;" rowspan="2" colspan="2">Hello</td>
+		<td>Whoop!</td>
+		<td style="min-width: 50px; max-width: 250px;">World<br/>Hi!</td>
+	</tr>
+	<tr>
+		<td colspan="2">World<br/>Hi!</td>
+	</tr>
+	<tr>
+		<td style="height: 50%;">Cat<div class="bottom-right">X</div></td>
+		<td style="padding-left: 30px; padding-top: 50px;" colspan="2">Mouse<div class="bottom-right">X</div></td>
+		<td style="border-left: 5px green; border-right-width: 2px; vertical-align: bottom" rowspan="2">RatTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT</td>
+	</tr>
+	<tr>
+		<td style="vertical-align: bottom;">Row<br/>Row</td>
+		<td colspan="2">3</td>
+	</tr>
+</table>
+<handle size_target="#document"></handle>
+</body>
+</rml>