Pārlūkot izejas kodu

Prevent gaps when rendering fractionally sized elements, resolves #438

Previously, when placing elements border-to-border one could often see 1px gaps between the elements. This was often the case if their position or size was fractionally sized. Which is a relatively common situation, particularly when using dp-dependent layout and scaling the dp-ratio by a fractional size.

To address this, we adjust the rounded/rendered sizes of elements based on their absolute position, in such a way that the bottom-right of one element exactly matches the top-left of the next element.

One implication of this is that the size of the element depends on its absolute position. Every time the element's size changes, its geometry needs to be re-generated. However, this isn't as bad as it sounds. In fact, as long as we move or scroll elements at integer pixel intervals, then its effective size won't change. Both element scrolling and the handle element already rounded to integer pixels, so we almost never need to re-generate geometry when interacting with those. With that said, changing absolute coordinates manually (top/right/bottom/left) without rounding will typically cause a lot of geometry re-generation.

When rendering, we effectively have to match the position and size of the background in a way that replicates how the layout engine decides to place the next element. Due to floating-point precision, there are still some rare cases where we will see gaps. This is because the result of floating-point operations depend on the order of the operations (being non-associative). We try to match the layout engine in the sense that we add small relative values before we add the large absolute value, and that seemed to help a lot, but this will never match exactly. A more robust solution would use fixed-point arithmetic for layout and rendering. However, this commit alone should cover at least 99% of gaps.

Adds RenderBox class which represents the data needed to generate a pixel-accurate mesh from an element's box. The element can construct a render box which can be submitted to the MeshUtilities to generate a background and border mesh. The background and decorators have been updated to use the RenderBox.

Some tests are added to ensure that geometry is not re-generated excessively due to this change. Benchmarks show no measurable performance regression.

This commit fixes several 1px-issues:
- Gap of 1px between border or backgrounds of neighboring elements.
- Overlap of 1px between border or backgrounds of neighboring elements.
- Table cell backgrounds 1px over the table border.
- Clipping area off by 1px compared to border.

Breaking changes:
- Changed the signature of `MeshUtilities::GenerateBackground` and `MeshUtilities::GenerateBackgroundBorder`. They now take a `RenderBox` class as input. One can use the new `Element::GetRenderBox` function to construct it.
- Changed `ComputedValues::border_radius` to return an array instead of Vector4f.
Michael Ragazzon 1 gadu atpakaļ
vecāks
revīzija
b197f985b3

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

@@ -30,6 +30,7 @@
 
 #include "Animation.h"
 #include "Element.h"
+#include "RenderBox.h"
 #include "StyleTypes.h"
 #include "Types.h"
 #include <cfloat>
@@ -213,7 +214,7 @@ namespace Style {
 		explicit ComputedValues(Element* element) : element(element) {}
 
 		// clang-format off
-		
+
 		// -- Common --
 		LengthPercentageAuto width()               const { return LengthPercentageAuto(common.width_type, common.width_value); }
 		LengthPercentageAuto height()              const { return LengthPercentageAuto(common.height_type, common.height_value); }
@@ -248,7 +249,7 @@ namespace Style {
 		Colourb              border_bottom_color() const { return common.border_bottom_color; }
 		Colourb              border_left_color()   const { return common.border_left_color; }
 		bool                 has_decorator()       const { return common.has_decorator; }
-		
+
 		// -- Inherited --
 		String         font_family()      const;
 		String         cursor()           const;
@@ -301,7 +302,7 @@ namespace Style {
 		float             border_top_right_radius()    const { return (float)rare.border_top_right_radius; }
 		float             border_bottom_right_radius() const { return (float)rare.border_bottom_right_radius; }
 		float             border_bottom_left_radius()  const { return (float)rare.border_bottom_left_radius; }
-		Vector4f          border_radius()              const { return {(float)rare.border_top_left_radius,     (float)rare.border_top_right_radius,
+		CornerSizes       border_radius()              const { return {(float)rare.border_top_left_radius,     (float)rare.border_top_right_radius,
 		                                                               (float)rare.border_bottom_right_radius, (float)rare.border_bottom_left_radius}; }
 		Clip              clip()                       const { return rare.clip; }
 		Drag              drag()                       const { return rare.drag; }
@@ -315,7 +316,7 @@ namespace Style {
 		bool              has_filter()                 const { return rare.has_filter; }
 		bool              has_backdrop_filter()        const { return rare.has_backdrop_filter; }
 		bool              has_box_shadow()             const { return rare.has_box_shadow; }
-		
+
 		// -- Assignment --
 		// Common
 		void width              (LengthPercentageAuto value) { common.width_type          = value.type; common.width_value          = value.value; }

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

@@ -35,6 +35,7 @@
 #include "Header.h"
 #include "ObserverPtr.h"
 #include "Property.h"
+#include "RenderBox.h"
 #include "ScriptInterface.h"
 #include "ScrollTypes.h"
 #include "StyleTypes.h"
@@ -158,6 +159,11 @@ public:
 	/// @param[out] offset The offset of the box relative to the element's border box.
 	/// @return The requested box.
 	const Box& GetBox(int index, Vector2f& offset);
+	/// Returns one of the render boxes describing how to generate the geometry for the corresponding element's box.
+	/// @param[in] fill_area The box area that acts as the background, or fill, of the render box.
+	/// @param[in] index The index of the desired box, with 0 being the main box. If outside of bounds, the main render box will be returned.
+	/// @return The requested render box.
+	RenderBox GetRenderBox(BoxArea fill_area = BoxArea::Padding, int index = 0);
 	/// Returns the number of boxes making up this element's geometry.
 	/// @return the number of boxes making up this element's geometry.
 	int GetNumBoxes();
@@ -678,6 +684,7 @@ private:
 
 	void DirtyAbsoluteOffset();
 	void DirtyAbsoluteOffsetRecursive();
+	void UpdateAbsoluteOffsetAndRenderBoxData();
 	void UpdateOffset();
 	void SetBaseline(float baseline);
 
@@ -728,6 +735,7 @@ private:
 
 	bool offset_fixed;
 	bool absolute_offset_dirty;
+	bool rounded_main_padding_size_dirty : 1;
 
 	bool dirty_definition : 1; // Implies dirty child definitions as well.
 	bool dirty_child_definitions : 1;
@@ -770,6 +778,7 @@ private:
 	Vector2f relative_offset_position; // the offset of a relatively positioned element
 
 	Vector2f absolute_offset;
+	Vector2f rounded_main_padding_size;
 
 	// The offset this element adds to its logical children due to scrolling content.
 	Vector2f scroll_offset;

+ 17 - 23
Include/RmlUi/Core/MeshUtilities.h

@@ -30,11 +30,11 @@
 #define RMLUI_CORE_MESHUTILITIES_H
 
 #include "Header.h"
+#include "RenderBox.h"
 #include "Types.h"
 
 namespace Rml {
 
-class Box;
 struct Mesh;
 
 /**
@@ -43,50 +43,44 @@ struct Mesh;
  */
 class RMLUICORE_API MeshUtilities {
 public:
-	/// Generates a quad from a position, size and colour.
+	/// Generates a quad from a position, size and color.
 	/// @param[out] mesh A mesh to append the generated vertices and indices into.
 	/// @param[in] origin The origin of the quad to generate.
 	/// @param[in] dimensions The dimensions of the quad to generate.
-	/// @param[in] colour The colour to be assigned to each of the quad's vertices.
-	static void GenerateQuad(Mesh& mesh, Vector2f origin, Vector2f dimensions, ColourbPremultiplied colour);
-	/// Generates a quad from a position, size, colour and texture coordinates.
+	/// @param[in] color The color to be assigned to each of the quad's vertices.
+	static void GenerateQuad(Mesh& mesh, Vector2f origin, Vector2f dimensions, ColourbPremultiplied color);
+	/// Generates a quad from a position, size, color and texture coordinates.
 	/// @param[out] mesh A mesh to append the generated vertices and indices into.
 	/// @param[in] origin The origin of the quad to generate.
 	/// @param[in] dimensions The dimensions of the quad to generate.
-	/// @param[in] colour The colour to be assigned to each of the quad's vertices.
+	/// @param[in] color The color to be assigned to each of the quad's vertices.
 	/// @param[in] top_left_texcoord The texture coordinates at the top-left of the quad.
 	/// @param[in] bottom_right_texcoord The texture coordinates at the bottom-right of the quad.
-	static void GenerateQuad(Mesh& mesh, Vector2f origin, Vector2f dimensions, ColourbPremultiplied colour, Vector2f top_left_texcoord,
+	static void GenerateQuad(Mesh& mesh, Vector2f origin, Vector2f dimensions, ColourbPremultiplied color, Vector2f top_left_texcoord,
 		Vector2f bottom_right_texcoord);
 
 	/// Generates the geometry required to render a line.
 	/// @param[out] mesh A mesh to append the generated vertices and indices into.
 	/// @param[in] position The top-left position the line.
-	/// @param[in] position The size of the line.
+	/// @param[in] size The size of the line.
 	/// @param[in] color The color to draw the line in.
 	static void GenerateLine(Mesh& mesh, Vector2f position, Vector2f size, ColourbPremultiplied color);
 
-	/// Generates the geometry for an element's background and border, with support for the border-radius property.
+	/// Generates the geometry for an element's background and border, with support for border-radius.
 	/// @param[out] mesh A mesh to append the generated vertices and indices into.
-	/// @param[in] box The box which determines the background and border geometry.
-	/// @param[in] offset Offset the position of the generated vertices.
-	/// @param[in] border_radius The border radius in pixel units in the following order: top-left, top-right, bottom-right, bottom-left.
-	/// @param[in] background_colour The colour applied to the background, set alpha to zero to not generate the background.
-	/// @param[in] border_colours A four-element array of border colors in top-right-bottom-left order.
+	/// @param[in] render_box The render box which determines the background and border geometry.
+	/// @param[in] background_color The color applied to the background, set alpha to zero to not generate the background.
+	/// @param[in] border_colors A four-element array of border colors in top-right-bottom-left order.
 	/// @note Vertex positions are relative to the border-box, vertex texture coordinates are default initialized.
-	static void GenerateBackgroundBorder(Mesh& mesh, const Box& box, Vector2f offset, Vector4f border_radius, ColourbPremultiplied background_colour,
-		const ColourbPremultiplied border_colours[4]);
+	static void GenerateBackgroundBorder(Mesh& mesh, const RenderBox& render_box, ColourbPremultiplied background_color,
+		const ColourbPremultiplied border_colors[4]);
 
 	/// Generates the background geometry for an element's area, with support for border-radius.
 	/// @param[out] mesh A mesh to append the generated vertices and indices into.
-	/// @param[in] box The box which determines the background geometry.
-	/// @param[in] offset Offset the position of the generated vertices.
-	/// @param[in] border_radius The border radius in pixel units in the following order: top-left, top-right, bottom-right, bottom-left.
-	/// @param[in] colour The colour applied to the background.
-	/// @param[in] area Either the border, padding or content area to be filled.
+	/// @param[in] render_box The render box which determines the background geometry.
+	/// @param[in] color The color applied to the background.
 	/// @note Vertex positions are relative to the border-box, vertex texture coordinates are default initialized.
-	static void GenerateBackground(Mesh& mesh, const Box& box, Vector2f offset, Vector4f border_radius, ColourbPremultiplied colour,
-		BoxArea area = BoxArea::Padding);
+	static void GenerateBackground(Mesh& mesh, const RenderBox& render_box, ColourbPremultiplied color);
 
 private:
 	MeshUtilities() = delete;

+ 81 - 0
Include/RmlUi/Core/RenderBox.h

@@ -0,0 +1,81 @@
+/*
+ * 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-2024 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_RENDERBOX_H
+#define RMLUI_CORE_RENDERBOX_H
+
+#include "Types.h"
+
+namespace Rml {
+
+// Ordered by top, right, bottom, left.
+using EdgeSizes = Array<float, 4>;
+// Ordered by top-left, top-right, bottom-right, bottom-left.
+using CornerSizes = Array<float, 4>;
+
+/**
+    Provides the data needed to generate a mesh for a given element's box.
+ */
+
+class RenderBox {
+public:
+	RenderBox(Vector2f fill_size, Vector2f border_offset, EdgeSizes border_widths, CornerSizes border_radius) :
+		fill_size(fill_size), border_offset(border_offset), border_widths(border_widths), border_radius(border_radius)
+	{}
+
+	/// Returns the size of the fill area of the box.
+	Vector2f GetFillSize() const { return fill_size; }
+	/// Sets the size of the fill area of the box.
+	void SetFillSize(Vector2f value) { fill_size = value; }
+	/// Returns the offset from the border area to the fill area of the box.
+	Vector2f GetFillOffset() const { return {border_widths[3], border_widths[0]}; }
+
+	/// Returns the offset to the border area of the box.
+	Vector2f GetBorderOffset() const { return border_offset; }
+	/// Sets the border offset.
+	void SetBorderOffset(Vector2f value) { border_offset = value; }
+
+	/// Returns the border widths of the box.
+	EdgeSizes GetBorderWidths() const { return border_widths; }
+	/// Sets the border widths of the box.
+	void SetBorderWidths(EdgeSizes value) { border_widths = value; }
+
+	/// Returns the border radius of the box.
+	CornerSizes GetBorderRadius() const { return border_radius; }
+	/// Sets the border radius of the box.
+	void SetBorderRadius(CornerSizes value) { border_radius = value; }
+
+private:
+	Vector2f fill_size;
+	Vector2f border_offset;
+	EdgeSizes border_widths;
+	CornerSizes border_radius;
+};
+
+} // namespace Rml
+#endif

+ 1 - 0
Source/Core/CMakeLists.txt

@@ -301,6 +301,7 @@ target_sources(rmlui_core PRIVATE
 	"${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/PropertyParser.h"
 	"${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/PropertySpecification.h"
 	"${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Rectangle.h"
+	"${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/RenderBox.h"
 	"${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/RenderInterface.h"
 	"${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/RenderInterfaceCompatibility.h"
 	"${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/RenderManager.h"

+ 18 - 21
Source/Core/DecoratorGradient.cpp

@@ -189,19 +189,18 @@ bool DecoratorStraightGradient::Initialise(const Direction in_direction, const C
 
 DecoratorDataHandle DecoratorStraightGradient::GenerateElementData(Element* element, BoxArea paint_area) const
 {
-	const Box& box = element->GetBox();
-
+	const RenderBox render_box = element->GetRenderBox(paint_area);
 	const ComputedValues& computed = element->GetComputedValues();
 	const float opacity = computed.opacity();
 
 	Mesh mesh;
-	MeshUtilities::GenerateBackground(mesh, element->GetBox(), Vector2f(0), computed.border_radius(), ColourbPremultiplied(), paint_area);
+	MeshUtilities::GenerateBackground(mesh, render_box, ColourbPremultiplied());
 
 	ColourbPremultiplied colour_start = start.ToPremultiplied(opacity);
 	ColourbPremultiplied colour_stop = stop.ToPremultiplied(opacity);
 
-	const Vector2f offset = box.GetPosition(paint_area);
-	const Vector2f size = box.GetSize(paint_area);
+	const Vector2f offset = render_box.GetFillOffset();
+	const Vector2f size = render_box.GetFillSize();
 
 	Vector<Vertex>& vertices = mesh.vertices;
 
@@ -296,10 +295,8 @@ DecoratorDataHandle DecoratorLinearGradient::GenerateElementData(Element* elemen
 
 	RMLUI_ASSERT(!color_stops.empty());
 
-	const Box& box = element->GetBox();
-	const Vector2f dimensions = box.GetSize(paint_area);
-
-	LinearGradientShape gradient_shape = CalculateShape(dimensions);
+	const RenderBox render_box = element->GetRenderBox(paint_area);
+	LinearGradientShape gradient_shape = CalculateShape(render_box.GetFillSize());
 
 	// One-pixel minimum color stop spacing to avoid aliasing.
 	const float soft_spacing = 1.f / gradient_shape.length;
@@ -320,9 +317,9 @@ DecoratorDataHandle DecoratorLinearGradient::GenerateElementData(Element* elemen
 	Mesh mesh;
 	const ComputedValues& computed = element->GetComputedValues();
 	const byte alpha = byte(computed.opacity() * 255.f);
-	MeshUtilities::GenerateBackground(mesh, box, Vector2f(), computed.border_radius(), ColourbPremultiplied(alpha, alpha), paint_area);
+	MeshUtilities::GenerateBackground(mesh, render_box, ColourbPremultiplied(alpha, alpha));
 
-	const Vector2f render_offset = box.GetPosition(paint_area);
+	const Vector2f render_offset = render_box.GetFillOffset();
 	for (Vertex& vertex : mesh.vertices)
 		vertex.tex_coord = vertex.position - render_offset;
 
@@ -458,7 +455,7 @@ bool DecoratorRadialGradient::Initialise(bool in_repeating, Shape in_shape, Size
 	return !color_stops.empty();
 }
 
-DecoratorDataHandle DecoratorRadialGradient::GenerateElementData(Element* element, BoxArea box_area) const
+DecoratorDataHandle DecoratorRadialGradient::GenerateElementData(Element* element, BoxArea paint_area) const
 {
 	RenderManager* render_manager = element->GetRenderManager();
 	if (!render_manager)
@@ -466,8 +463,8 @@ DecoratorDataHandle DecoratorRadialGradient::GenerateElementData(Element* elemen
 
 	RMLUI_ASSERT(!color_stops.empty() && (shape == Shape::Circle || shape == Shape::Ellipse));
 
-	const Box& box = element->GetBox();
-	const Vector2f dimensions = box.GetSize(box_area);
+	const RenderBox render_box = element->GetRenderBox(paint_area);
+	const Vector2f dimensions = render_box.GetFillSize();
 
 	RadialGradientShape gradient_shape = CalculateRadialGradientShape(element, dimensions);
 
@@ -489,9 +486,9 @@ DecoratorDataHandle DecoratorRadialGradient::GenerateElementData(Element* elemen
 	Mesh mesh;
 	const ComputedValues& computed = element->GetComputedValues();
 	const byte alpha = byte(computed.opacity() * 255.f);
-	MeshUtilities::GenerateBackground(mesh, box, Vector2f(), computed.border_radius(), ColourbPremultiplied(alpha, alpha), box_area);
+	MeshUtilities::GenerateBackground(mesh, render_box, ColourbPremultiplied(alpha, alpha));
 
-	const Vector2f render_offset = box.GetPosition(box_area);
+	const Vector2f render_offset = render_box.GetFillOffset();
 	for (Vertex& vertex : mesh.vertices)
 		vertex.tex_coord = vertex.position - render_offset;
 
@@ -657,7 +654,7 @@ bool DecoratorConicGradient::Initialise(bool in_repeating, float in_angle, Vecto
 	return !color_stops.empty();
 }
 
-DecoratorDataHandle DecoratorConicGradient::GenerateElementData(Element* element, BoxArea box_area) const
+DecoratorDataHandle DecoratorConicGradient::GenerateElementData(Element* element, BoxArea paint_area) const
 {
 	RenderManager* render_manager = element->GetRenderManager();
 	if (!render_manager)
@@ -665,8 +662,8 @@ DecoratorDataHandle DecoratorConicGradient::GenerateElementData(Element* element
 
 	RMLUI_ASSERT(!color_stops.empty());
 
-	const Box& box = element->GetBox();
-	const Vector2f dimensions = box.GetSize(box_area);
+	const RenderBox render_box = element->GetRenderBox(paint_area);
+	const Vector2f dimensions = render_box.GetFillSize();
 
 	const Vector2f center =
 		Vector2f{element->ResolveNumericValue(position.x, dimensions.x), element->ResolveNumericValue(position.y, dimensions.y)}.Round();
@@ -686,9 +683,9 @@ DecoratorDataHandle DecoratorConicGradient::GenerateElementData(Element* element
 	Mesh mesh;
 	const ComputedValues& computed = element->GetComputedValues();
 	const byte alpha = byte(computed.opacity() * 255.f);
-	MeshUtilities::GenerateBackground(mesh, box, Vector2f(), computed.border_radius(), ColourbPremultiplied(alpha, alpha), box_area);
+	MeshUtilities::GenerateBackground(mesh, render_box, ColourbPremultiplied(alpha, alpha));
 
-	const Vector2f render_offset = box.GetPosition(box_area);
+	const Vector2f render_offset = render_box.GetFillOffset();
 	for (Vertex& vertex : mesh.vertices)
 		vertex.tex_coord = vertex.position - render_offset;
 

+ 3 - 2
Source/Core/DecoratorNinePatch.cpp

@@ -61,8 +61,9 @@ DecoratorDataHandle DecoratorNinePatch::GenerateElementData(Element* element, Bo
 	Texture texture = GetTexture();
 	const Vector2f texture_dimensions(texture.GetDimensions());
 
-	const Vector2f surface_offset = element->GetBox().GetPosition(paint_area);
-	const Vector2f surface_dimensions = element->GetBox().GetSize(paint_area).Round();
+	const RenderBox render_box = element->GetRenderBox(paint_area);
+	const Vector2f surface_offset = render_box.GetFillOffset();
+	const Vector2f surface_dimensions = render_box.GetFillSize();
 
 	const ColourbPremultiplied quad_colour = computed.image_color().ToPremultiplied(computed.opacity());
 

+ 5 - 5
Source/Core/DecoratorShader.cpp

@@ -52,14 +52,14 @@ bool DecoratorShader::Initialise(String&& in_value)
 	return true;
 }
 
-DecoratorDataHandle DecoratorShader::GenerateElementData(Element* element, BoxArea render_area) const
+DecoratorDataHandle DecoratorShader::GenerateElementData(Element* element, BoxArea paint_area) const
 {
 	RenderManager* render_manager = element->GetRenderManager();
 	if (!render_manager)
 		return INVALID_DECORATORDATAHANDLE;
 
-	const Box& box = element->GetBox();
-	const Vector2f dimensions = box.GetSize(render_area);
+	const RenderBox render_box = element->GetRenderBox(paint_area);
+	const Vector2f dimensions = render_box.GetFillSize();
 	CompiledShader shader = render_manager->CompileShader("shader", Dictionary{{"value", Variant(value)}, {"dimensions", Variant(dimensions)}});
 	if (!shader)
 		return INVALID_DECORATORDATAHANDLE;
@@ -68,9 +68,9 @@ DecoratorDataHandle DecoratorShader::GenerateElementData(Element* element, BoxAr
 
 	const ComputedValues& computed = element->GetComputedValues();
 	const byte alpha = byte(computed.opacity() * 255.f);
-	MeshUtilities::GenerateBackground(mesh, box, Vector2f(), computed.border_radius(), ColourbPremultiplied(alpha, alpha), render_area);
+	MeshUtilities::GenerateBackground(mesh, render_box, ColourbPremultiplied(alpha, alpha));
 
-	const Vector2f offset = box.GetPosition(render_area);
+	const Vector2f offset = render_box.GetFillOffset();
 	for (Vertex& vertex : mesh.vertices)
 		vertex.tex_coord = (vertex.position - offset) / dimensions;
 

+ 3 - 2
Source/Core/DecoratorTiledBox.cpp

@@ -105,8 +105,9 @@ DecoratorDataHandle DecoratorTiledBox::GenerateElementData(Element* element, Box
 		tiles[i].CalculateDimensions(GetTexture(tiles[i].texture_index));
 	}
 
-	const Vector2f offset = element->GetBox().GetPosition(paint_area);
-	const Vector2f size = element->GetBox().GetSize(paint_area);
+	const RenderBox render_box = element->GetRenderBox(paint_area);
+	const Vector2f offset = render_box.GetFillOffset();
+	const Vector2f size = render_box.GetFillSize();
 
 	// Calculate the natural dimensions of tile corners and edges.
 	const Vector2f natural_top_left = tiles[TOP_LEFT_CORNER].GetNaturalDimensions(element);

+ 3 - 2
Source/Core/DecoratorTiledHorizontal.cpp

@@ -82,8 +82,9 @@ DecoratorDataHandle DecoratorTiledHorizontal::GenerateElementData(Element* eleme
 	for (int i = 0; i < 3; i++)
 		tiles[i].CalculateDimensions(GetTexture(tiles[i].texture_index));
 
-	const Vector2f offset = element->GetBox().GetPosition(paint_area);
-	const Vector2f size = element->GetBox().GetSize(paint_area);
+	const RenderBox render_box = element->GetRenderBox(paint_area);
+	const Vector2f offset = render_box.GetFillOffset();
+	const Vector2f size = render_box.GetFillSize();
 
 	Vector2f left_dimensions = tiles[LEFT].GetNaturalDimensions(element);
 	Vector2f right_dimensions = tiles[RIGHT].GetNaturalDimensions(element);

+ 3 - 2
Source/Core/DecoratorTiledImage.cpp

@@ -52,8 +52,9 @@ DecoratorDataHandle DecoratorTiledImage::GenerateElementData(Element* element, B
 
 	const ComputedValues& computed = element->GetComputedValues();
 
-	const Vector2f offset = element->GetBox().GetPosition(paint_area);
-	const Vector2f size = element->GetBox().GetSize(paint_area);
+	const RenderBox render_box = element->GetRenderBox(paint_area);
+	const Vector2f offset = render_box.GetFillOffset();
+	const Vector2f size = render_box.GetFillSize();
 
 	// Generate the geometry for the tile.
 	Mesh mesh;

+ 3 - 2
Source/Core/DecoratorTiledVertical.cpp

@@ -83,8 +83,9 @@ DecoratorDataHandle DecoratorTiledVertical::GenerateElementData(Element* element
 	for (int i = 0; i < 3; i++)
 		tiles[i].CalculateDimensions(GetTexture(tiles[i].texture_index));
 
-	const Vector2f offset = element->GetBox().GetPosition(paint_area);
-	const Vector2f size = element->GetBox().GetSize(paint_area);
+	const RenderBox render_box = element->GetRenderBox(paint_area);
+	const Vector2f offset = render_box.GetFillOffset();
+	const Vector2f size = render_box.GetFillSize();
 
 	Vector2f top_dimensions = tiles[TOP].GetNaturalDimensions(element);
 	Vector2f bottom_dimensions = tiles[BOTTOM].GetNaturalDimensions(element);

+ 89 - 23
Source/Core/Element.cpp

@@ -93,9 +93,9 @@ static float GetScrollOffsetDelta(ScrollAlignment alignment, float begin_offset,
 
 Element::Element(const String& tag) :
 	local_stacking_context(false), local_stacking_context_forced(false), stacking_context_dirty(false), computed_values_are_default_initialized(true),
-	visible(true), offset_fixed(false), absolute_offset_dirty(true), dirty_definition(false), dirty_child_definitions(false), dirty_animation(false),
-	dirty_transition(false), dirty_transform(false), dirty_perspective(false), tag(tag), relative_offset_base(0, 0), relative_offset_position(0, 0),
-	absolute_offset(0, 0), scroll_offset(0, 0)
+	visible(true), offset_fixed(false), absolute_offset_dirty(true), rounded_main_padding_size_dirty(true), dirty_definition(false),
+	dirty_child_definitions(false), dirty_animation(false), dirty_transition(false), dirty_transform(false), dirty_perspective(false), tag(tag),
+	relative_offset_base(0, 0), relative_offset_position(0, 0), absolute_offset(0, 0), scroll_offset(0, 0)
 {
 	RMLUI_ASSERT(tag == StringUtilities::ToLower(tag));
 	parent = nullptr;
@@ -206,10 +206,7 @@ void Element::Render()
 	RMLUI_ZoneText(name.c_str(), name.size());
 #endif
 
-	// TODO: This is a work-around for the dirty offset not being properly updated when used by containing block children. This results
-	// in scrolling not working properly. We don't care about the return value, the call is only used to force the absolute offset to update.
-	if (absolute_offset_dirty)
-		GetAbsoluteOffset(BoxArea::Border);
+	UpdateAbsoluteOffsetAndRenderBoxData();
 
 	// Rebuild our stacking context if necessary.
 	if (stacking_context_dirty)
@@ -382,28 +379,51 @@ Vector2f Element::GetRelativeOffset(BoxArea area)
 
 Vector2f Element::GetAbsoluteOffset(BoxArea area)
 {
-	if (absolute_offset_dirty)
+	UpdateAbsoluteOffsetAndRenderBoxData();
+	return area == BoxArea::Border ? absolute_offset : absolute_offset + GetBox().GetPosition(area);
+}
+
+void Element::UpdateAbsoluteOffsetAndRenderBoxData()
+{
+	if (absolute_offset_dirty || rounded_main_padding_size_dirty)
 	{
 		absolute_offset_dirty = false;
+		rounded_main_padding_size_dirty = false;
 
+		Vector2f offset_from_ancestors;
 		if (offset_parent)
-			absolute_offset = offset_parent->GetAbsoluteOffset(BoxArea::Border) + relative_offset_base + relative_offset_position;
-		else
-			absolute_offset = relative_offset_base + relative_offset_position;
+			offset_from_ancestors = offset_parent->GetAbsoluteOffset(BoxArea::Border);
 
 		if (!offset_fixed)
 		{
 			// Add any parent scrolling onto our position as well.
 			if (offset_parent)
-				absolute_offset -= offset_parent->scroll_offset;
+				offset_from_ancestors -= offset_parent->scroll_offset;
 
 			// Finally, there may be relatively positioned elements between ourself and our containing block, add their relative offsets as well.
 			for (Element* ancestor = parent; ancestor && ancestor != offset_parent; ancestor = ancestor->parent)
-				absolute_offset += ancestor->relative_offset_position;
+				offset_from_ancestors += ancestor->relative_offset_position;
 		}
-	}
 
-	return absolute_offset + GetBox().GetPosition(area);
+		const Vector2f relative_offset = relative_offset_base + relative_offset_position;
+		absolute_offset = relative_offset + offset_from_ancestors;
+
+		// Next, we find the rounded size of the box so that elements can be placed border-to-border next to each other
+		// without any gaps. To achieve this, we have to adjust their rounded/rendered sizes based on their position, in
+		// such a way that the bottom-right of this element exactly matches the top-left of the next element. The order
+		// of floating-point operations matter here, we want to replicate the operations in the layout engine as close
+		// as possible to avoid any gaps.
+		const Vector2f main_padding_size = main_box.GetSize(BoxArea::Padding);
+		const Vector2f bottom_right_absolute_offset = (relative_offset + main_padding_size) + offset_from_ancestors;
+		const Vector2f new_rounded_main_padding_size = bottom_right_absolute_offset.Round() - absolute_offset.Round();
+		if (new_rounded_main_padding_size != rounded_main_padding_size)
+		{
+			rounded_main_padding_size = new_rounded_main_padding_size;
+			meta->background_border.DirtyBackground();
+			meta->background_border.DirtyBorder();
+			meta->effects.DirtyEffectsData();
+		}
+	}
 }
 
 void Element::SetClipArea(BoxArea _clip_area)
@@ -430,11 +450,20 @@ void Element::SetBox(const Box& box)
 {
 	if (box != main_box || additional_boxes.size() > 0)
 	{
+#ifdef RMLUI_DEBUG
+		for (const BoxEdge edge : {BoxEdge::Top, BoxEdge::Right, BoxEdge::Bottom, BoxEdge::Left})
+		{
+			const float border_width = box.GetEdge(BoxArea::Border, edge);
+			if (border_width != Math::Round(border_width))
+				Log::Message(Log::LT_WARNING, "Expected integer border width but got %g px on element: %s", border_width, GetAddress().c_str());
+		}
+#endif
+
 		main_box = box;
 		additional_boxes.clear();
 
 		OnResize();
-
+		rounded_main_padding_size_dirty = true;
 		meta->background_border.DirtyBackground();
 		meta->background_border.DirtyBorder();
 		meta->effects.DirtyEffectsData();
@@ -444,9 +473,7 @@ void Element::SetBox(const Box& box)
 void Element::AddBox(const Box& box, Vector2f offset)
 {
 	additional_boxes.emplace_back(PositionedBox{box, offset});
-
 	OnResize();
-
 	meta->background_border.DirtyBackground();
 	meta->background_border.DirtyBorder();
 	meta->effects.DirtyEffectsData();
@@ -461,18 +488,57 @@ const Box& Element::GetBox(int index, Vector2f& offset)
 {
 	offset = Vector2f(0);
 
-	if (index < 1)
-		return main_box;
-
 	const int additional_box_index = index - 1;
-	if (additional_box_index >= (int)additional_boxes.size())
+	if (index < 1 || additional_box_index >= (int)additional_boxes.size())
 		return main_box;
 
 	offset = additional_boxes[additional_box_index].offset;
-
 	return additional_boxes[additional_box_index].box;
 }
 
+RenderBox Element::GetRenderBox(BoxArea fill_area, int index)
+{
+	RMLUI_ASSERTMSG(fill_area >= BoxArea::Border && fill_area <= BoxArea::Content,
+		"Render box can only be generated with fill area of border, padding or content.");
+
+	UpdateAbsoluteOffsetAndRenderBoxData();
+
+	struct BoxReference {
+		const Box& box;
+		Vector2f padding_size;
+		Vector2f offset;
+	};
+	auto GetBoxAndOffset = [this, index]() {
+		const int additional_box_index = index - 1;
+		if (index < 1 || additional_box_index >= (int)additional_boxes.size())
+			return BoxReference{main_box, rounded_main_padding_size, {}};
+		const PositionedBox& positioned_box = additional_boxes[additional_box_index];
+		return BoxReference{positioned_box.box, positioned_box.box.GetSize(BoxArea::Padding), positioned_box.offset.Round()};
+	};
+
+	BoxReference box = GetBoxAndOffset();
+
+	EdgeSizes edge_sizes = {};
+	for (int area = (int)BoxArea::Border; area < (int)fill_area; area++)
+	{
+		edge_sizes[0] += box.box.GetEdge(BoxArea(area), BoxEdge::Top);
+		edge_sizes[1] += box.box.GetEdge(BoxArea(area), BoxEdge::Right);
+		edge_sizes[2] += box.box.GetEdge(BoxArea(area), BoxEdge::Bottom);
+		edge_sizes[3] += box.box.GetEdge(BoxArea(area), BoxEdge::Left);
+	}
+	Vector2f inner_size;
+	switch (fill_area)
+	{
+	case BoxArea::Border: inner_size = box.padding_size + box.box.GetFrameSize(BoxArea::Border); break;
+	case BoxArea::Padding: inner_size = box.padding_size; break;
+	case BoxArea::Content: inner_size = box.padding_size - box.box.GetFrameSize(BoxArea::Padding); break;
+	case BoxArea::Margin:
+	case BoxArea::Auto: RMLUI_ERROR;
+	}
+
+	return RenderBox{inner_size, box.offset, edge_sizes, meta->computed_values.border_radius()};
+}
+
 int Element::GetNumBoxes()
 {
 	return 1 + (int)additional_boxes.size();

+ 11 - 13
Source/Core/ElementBackgroundBorder.cpp

@@ -60,7 +60,10 @@ void ElementBackgroundBorder::Render(Element* element)
 	if (shadow && shadow->geometry)
 		shadow->geometry.Render(element->GetAbsoluteOffset(BoxArea::Border), shadow->texture);
 	else if (Background* background = GetBackground(BackgroundType::BackgroundBorder))
-		background->geometry.Render(element->GetAbsoluteOffset(BoxArea::Border));
+	{
+		auto offset = element->GetAbsoluteOffset(BoxArea::Border);
+		background->geometry.Render(offset);
+	}
 }
 
 void ElementBackgroundBorder::DirtyBackground()
@@ -84,15 +87,13 @@ Geometry* ElementBackgroundBorder::GetClipGeometry(Element* element, BoxArea cli
 	default: RMLUI_ERROR; return nullptr;
 	}
 
+	RenderManager* render_manager = element->GetRenderManager();
 	Geometry& geometry = GetOrCreateBackground(type).geometry;
-	if (!geometry)
+	if (render_manager && !geometry)
 	{
 		Mesh mesh = geometry.Release(Geometry::ReleaseMode::ClearMesh);
-		const Box& box = element->GetBox();
-		const Vector4f border_radius = element->GetComputedValues().border_radius();
-		MeshUtilities::GenerateBackground(mesh, box, {}, border_radius, ColourbPremultiplied(255), clip_area);
-		if (RenderManager* render_manager = element->GetRenderManager())
-			geometry = render_manager->MakeGeometry(std::move(mesh));
+		MeshUtilities::GenerateBackground(mesh, element->GetRenderBox(clip_area), ColourbPremultiplied(255));
+		geometry = render_manager->MakeGeometry(std::move(mesh));
 	}
 
 	return &geometry;
@@ -144,17 +145,14 @@ void ElementBackgroundBorder::GenerateGeometry(Element* element)
 		ConvertColor(computed.border_bottom_color()),
 		ConvertColor(computed.border_left_color()),
 	};
-	const Vector4f border_radius = computed.border_radius();
+	const CornerSizes border_radius = computed.border_radius();
 
 	Geometry& geometry = GetOrCreateBackground(BackgroundType::BackgroundBorder).geometry;
 	Mesh mesh = geometry.Release(Geometry::ReleaseMode::ClearMesh);
 
 	for (int i = 0; i < element->GetNumBoxes(); i++)
-	{
-		Vector2f offset;
-		const Box& box = element->GetBox(i, offset);
-		MeshUtilities::GenerateBackgroundBorder(mesh, box, offset, border_radius, background_color, border_colors);
-	}
+		MeshUtilities::GenerateBackgroundBorder(mesh, element->GetRenderBox(BoxArea::Padding, i), background_color, border_colors);
+
 	geometry = render_manager->MakeGeometry(std::move(mesh));
 
 	if (has_box_shadow)

+ 3 - 3
Source/Core/ElementUtilities.cpp

@@ -187,7 +187,7 @@ bool ElementUtilities::GetClippingRegion(Element* element, Rectanglei& out_clip_
 				{
 					Geometry* clip_geometry = clipping_element->GetElementBackgroundBorder()->GetClipGeometry(clipping_element, clip_area);
 					const ClipMaskOperation clip_operation = (out_clip_mask_list->empty() ? ClipMaskOperation::Set : ClipMaskOperation::Intersect);
-					const Vector2f absolute_offset = clipping_element->GetAbsoluteOffset(BoxArea::Border);
+					const Vector2f absolute_offset = clipping_element->GetAbsoluteOffset(BoxArea::Border).Round();
 					out_clip_mask_list->push_back(ClipMaskGeometry{clip_operation, clip_geometry, absolute_offset, transform});
 				}
 
@@ -201,8 +201,8 @@ bool ElementUtilities::GetClippingRegion(Element* element, Rectanglei& out_clip_
 			if (has_clipping_content && !disable_scissor_clipping)
 			{
 				// Shrink the scissor region to the element's client area.
-				Vector2f element_offset = clipping_element->GetAbsoluteOffset(clip_area);
-				Vector2f element_size = clipping_element->GetBox().GetSize(clip_area);
+				Vector2f element_offset = clipping_element->GetAbsoluteOffset(clip_area).Round();
+				Vector2f element_size = clipping_element->GetRenderBox(clip_area).GetFillSize();
 				Rectanglef element_region = Rectanglef::FromPositionSize(element_offset, element_size);
 
 				clip_region = element_region.IntersectIfValid(clip_region);

+ 6 - 9
Source/Core/Elements/ElementImage.cpp

@@ -86,8 +86,7 @@ void ElementImage::OnRender()
 	if (geometry_dirty)
 		GenerateGeometry();
 
-	// Render the geometry beginning at this element's content region.
-	geometry.Render(GetAbsoluteOffset(BoxArea::Content).Round(), texture);
+	geometry.Render(GetAbsoluteOffset(BoxArea::Border), texture);
 }
 
 void ElementImage::OnAttributeChange(const ElementAttributes& changed_attributes)
@@ -131,9 +130,7 @@ void ElementImage::OnPropertyChange(const PropertyIdSet& changed_properties)
 	Element::OnPropertyChange(changed_properties);
 
 	if (changed_properties.Contains(PropertyId::ImageColor) || changed_properties.Contains(PropertyId::Opacity))
-	{
-		GenerateGeometry();
-	}
+		geometry_dirty = true;
 }
 
 void ElementImage::OnChildAdd(Element* child)
@@ -147,7 +144,7 @@ void ElementImage::OnChildAdd(Element* child)
 
 void ElementImage::OnResize()
 {
-	GenerateGeometry();
+	geometry_dirty = true;
 }
 
 void ElementImage::OnDpRatioChange()
@@ -185,10 +182,10 @@ void ElementImage::GenerateGeometry()
 	}
 
 	const ComputedValues& computed = GetComputedValues();
-	ColourbPremultiplied quad_colour = computed.image_color().ToPremultiplied(computed.opacity());
-	Vector2f quad_size = GetBox().GetSize(BoxArea::Content).Round();
+	const ColourbPremultiplied quad_colour = computed.image_color().ToPremultiplied(computed.opacity());
+	const RenderBox render_box = GetRenderBox(BoxArea::Content);
 
-	MeshUtilities::GenerateQuad(mesh, Vector2f(0, 0), quad_size, quad_colour, texcoords[0], texcoords[1]);
+	MeshUtilities::GenerateQuad(mesh, render_box.GetFillOffset(), render_box.GetFillSize(), quad_colour, texcoords[0], texcoords[1]);
 
 	if (RenderManager* render_manager = GetRenderManager())
 		geometry = render_manager->MakeGeometry(std::move(mesh));

+ 2 - 2
Source/Core/GeometryBackgroundBorder.cpp

@@ -37,7 +37,7 @@ namespace Rml {
 GeometryBackgroundBorder::GeometryBackgroundBorder(Vector<Vertex>& vertices, Vector<int>& indices) : vertices(vertices), indices(indices) {}
 
 BorderMetrics GeometryBackgroundBorder::ComputeBorderMetrics(Vector2f outer_position, EdgeSizes edge_sizes, Vector2f inner_size,
-	Vector4f outer_radii_def)
+	CornerSizes outer_radii_def)
 {
 	BorderMetrics metrics = {};
 
@@ -68,7 +68,7 @@ BorderMetrics GeometryBackgroundBorder::ComputeBorderMetrics(Vector2f outer_posi
 	if (has_radius)
 	{
 		auto& outer_radii = metrics.outer_radii;
-		outer_radii = {outer_radii_def.x, outer_radii_def.y, outer_radii_def.z, outer_radii_def.w};
+		outer_radii = outer_radii_def;
 
 		// Scale the radii such that we have no overlapping curves.
 		float scale_factor = FLT_MAX;

+ 1 - 1
Source/Core/GeometryBackgroundBorder.h

@@ -72,7 +72,7 @@ public:
 	/// @param inner_size Size of the inner area.
 	/// @param outer_radii The radius of the outer edge at each corner.
 	/// @return The computed metrics.
-	static BorderMetrics ComputeBorderMetrics(Vector2f outer_position, EdgeSizes edge_sizes, Vector2f inner_size, Vector4f outer_radii);
+	static BorderMetrics ComputeBorderMetrics(Vector2f outer_position, EdgeSizes edge_sizes, Vector2f inner_size, CornerSizes outer_radii);
 
 	// Generate geometry for the background, defined by the inner area of the border metrics.
 	void DrawBackground(const BorderMetrics& metrics, ColourbPremultiplied color);

+ 12 - 23
Source/Core/GeometryBoxShadow.cpp

@@ -39,7 +39,7 @@
 namespace Rml {
 
 void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture& out_shadow_texture, RenderManager& render_manager, Element* element,
-	Geometry& background_border_geometry, BoxShadowList shadow_list, const Vector4f border_radius, const float opacity)
+	Geometry& background_border_geometry, BoxShadowList shadow_list, const CornerSizes border_radius, const float opacity)
 {
 	// Find the box-shadow texture dimension and offset required to cover all box-shadows and element boxes combined.
 	Vector2f element_offset_in_texture;
@@ -75,9 +75,8 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 		// Extend the render-texture further to cover all the element's boxes.
 		for (int i = 0; i < element->GetNumBoxes(); i++)
 		{
-			Vector2f offset;
-			const Box& box = element->GetBox(i, offset);
-			texture_region = texture_region.Join(Rectanglef::FromPositionSize(offset, box.GetSize(BoxArea::Border)));
+			const RenderBox box = element->GetRenderBox(BoxArea::Border, i);
+			texture_region = texture_region.Join(Rectanglef::FromPositionSize(box.GetBorderOffset(), box.GetFillSize()));
 		}
 
 		texture_region = texture_region.Extend(-extend_min, extend_max);
@@ -109,14 +108,11 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 		// Generate the geometry for all the element's boxes and extend the render-texture further to cover all of them.
 		for (int i = 0; i < element->GetNumBoxes(); i++)
 		{
-			Vector2f offset;
-			const Box& box = element->GetBox(i, offset);
 			ColourbPremultiplied white(255);
-
 			if (has_inner_shadow)
-				MeshUtilities::GenerateBackground(mesh_padding, box, offset, border_radius, white, BoxArea::Padding);
+				MeshUtilities::GenerateBackground(mesh_padding, element->GetRenderBox(BoxArea::Padding, i), white);
 			if (has_outer_shadow)
-				MeshUtilities::GenerateBackground(mesh_padding_border, box, offset, border_radius, white, BoxArea::Border);
+				MeshUtilities::GenerateBackground(mesh_padding_border, element->GetRenderBox(BoxArea::Border, i), white);
 		}
 
 		const RenderState initial_render_state = render_manager.GetState();
@@ -152,7 +148,7 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 			const float spread_distance = shadow.spread_distance.number;
 			const float blur_radius = shadow.blur_radius.number;
 
-			Vector4f spread_radii = border_radius;
+			CornerSizes spread_radii = border_radius;
 			for (int i = 0; i < 4; i++)
 			{
 				float& radius = spread_radii[i];
@@ -167,22 +163,15 @@ void GeometryBoxShadow::Generate(Geometry& out_shadow_geometry, CallbackTexture&
 
 			Mesh mesh_shadow;
 
-			// Generate the shadow geometry. For outer box-shadows it is rendered normally, while for inner box-shadows it is used as a clipping mask.
+			// Generate the shadow geometry. For outer box-shadows it is rendered normally, while for inset box-shadows it is used as a clipping mask.
 			for (int i = 0; i < element->GetNumBoxes(); i++)
 			{
-				Vector2f offset;
-				Box box = element->GetBox(i, offset);
 				const float signed_spread_distance = (inset ? -spread_distance : spread_distance);
-				offset -= Vector2f(signed_spread_distance);
-
-				for (int j = 0; j < Box::num_edges; j++)
-				{
-					BoxEdge edge = (BoxEdge)j;
-					const float new_size = box.GetEdge(BoxArea::Padding, edge) + signed_spread_distance;
-					box.SetEdge(BoxArea::Padding, edge, new_size);
-				}
-
-				MeshUtilities::GenerateBackground(mesh_shadow, box, offset, spread_radii, shadow.color, inset ? BoxArea::Padding : BoxArea::Border);
+				RenderBox render_box = element->GetRenderBox(inset ? BoxArea::Padding : BoxArea::Border, i);
+				render_box.SetFillSize(Math::Max(render_box.GetFillSize() + Vector2f(2.f * signed_spread_distance), Vector2f{0.001f}));
+				render_box.SetBorderRadius(spread_radii);
+				render_box.SetBorderOffset(render_box.GetBorderOffset() - Vector2f(signed_spread_distance));
+				MeshUtilities::GenerateBackground(mesh_shadow, render_box, shadow.color);
 			}
 
 			CompiledFilter blur;

+ 2 - 1
Source/Core/GeometryBoxShadow.h

@@ -29,6 +29,7 @@
 #ifndef RMLUI_CORE_GEOMETRYBOXSHADOW_H
 #define RMLUI_CORE_GEOMETRYBOXSHADOW_H
 
+#include "../../Include/RmlUi/Core/RenderBox.h"
 #include "../../Include/RmlUi/Core/Types.h"
 
 namespace Rml {
@@ -50,7 +51,7 @@ public:
 	/// @param[in] border_radius The border radius of the element.
 	/// @param[in] opacity The opacity of the element.
 	static void Generate(Geometry& out_shadow_geometry, CallbackTexture& out_shadow_texture, RenderManager& render_manager, Element* element,
-		Geometry& background_border_geometry, BoxShadowList shadow_list, Vector4f border_radius, float opacity);
+		Geometry& background_border_geometry, BoxShadowList shadow_list, CornerSizes border_radius, float opacity);
 };
 
 } // namespace Rml

+ 16 - 42
Source/Core/MeshUtilities.cpp

@@ -82,31 +82,22 @@ void MeshUtilities::GenerateLine(Mesh& mesh, Vector2f position, Vector2f size, C
 	MeshUtilities::GenerateQuad(mesh, position, size, color);
 }
 
-void MeshUtilities::GenerateBackgroundBorder(Mesh& out_mesh, const Box& box, Vector2f offset, Vector4f border_radius,
-	ColourbPremultiplied background_color, const ColourbPremultiplied border_colors[4])
+void MeshUtilities::GenerateBackgroundBorder(Mesh& out_mesh, const RenderBox& render_box, ColourbPremultiplied background_color,
+	const ColourbPremultiplied border_colors[4])
 {
 	RMLUI_ASSERT(border_colors);
 
 	Vector<Vertex>& vertices = out_mesh.vertices;
 	Vector<int>& indices = out_mesh.indices;
 
-	EdgeSizes border_widths = {
-		// TODO: Move rounding to computed values (round border only).
-		Math::Round(box.GetEdge(BoxArea::Border, BoxEdge::Top)),
-		Math::Round(box.GetEdge(BoxArea::Border, BoxEdge::Right)),
-		Math::Round(box.GetEdge(BoxArea::Border, BoxEdge::Bottom)),
-		Math::Round(box.GetEdge(BoxArea::Border, BoxEdge::Left)),
-	};
-
+	const EdgeSizes& border_widths = render_box.GetBorderWidths();
 	int num_borders = 0;
-
 	for (int i = 0; i < 4; i++)
 		if (border_colors[i].alpha > 0 && border_widths[i] > 0)
 			num_borders += 1;
 
-	const Vector2f padding_size = box.GetSize(BoxArea::Padding).Round();
-
-	const bool has_background = (background_color.alpha > 0 && padding_size.x > 0 && padding_size.y > 0);
+	const Vector2f fill_size = render_box.GetFillSize();
+	const bool has_background = (background_color.alpha > 0 && fill_size.x > 0 && fill_size.y > 0);
 	const bool has_border = (num_borders > 0);
 
 	if (!has_background && !has_border)
@@ -120,7 +111,8 @@ void MeshUtilities::GenerateBackgroundBorder(Mesh& out_mesh, const Box& box, Vec
 
 	// Generate the geometry.
 	GeometryBackgroundBorder geometry(vertices, indices);
-	const BorderMetrics metrics = GeometryBackgroundBorder::ComputeBorderMetrics(offset.Round(), border_widths, padding_size, border_radius);
+	const BorderMetrics metrics =
+		GeometryBackgroundBorder::ComputeBorderMetrics(render_box.GetBorderOffset(), border_widths, fill_size, render_box.GetBorderRadius());
 
 	if (has_background)
 		geometry.DrawBackground(metrics, background_color);
@@ -130,19 +122,15 @@ void MeshUtilities::GenerateBackgroundBorder(Mesh& out_mesh, const Box& box, Vec
 
 #if 0
 	// Debug draw vertices
-	if (border_radius != Vector4f(0))
+	if (render_box.border_radius != CornerSizes{})
 	{
 		const int num_vertices = (int)vertices.size();
 		const int num_indices = (int)indices.size();
-
-		vertices.resize(num_vertices + 4 * num_vertices);
-		indices.resize(num_indices + 6 * num_indices);
+		vertices.reserve(num_vertices + 4 * num_vertices);
+		indices.reserve(num_indices + 6 * num_indices);
 
 		for (int i = 0; i < num_vertices; i++)
-		{
-			MeshUtilities::GenerateQuad(vertices.data() + num_vertices + 4 * i, indices.data() + num_indices + 6 * i, vertices[i].position,
-				Vector2f(3, 3), ColourbPremultiplied(255, 0, (i % 2) == 0 ? 0 : 255), num_vertices + 4 * i);
-		}
+			MeshUtilities::GenerateQuad(out_mesh, vertices[i].position, Vector2f(3, 3), ColourbPremultiplied(255, 0, (i % 2) == 0 ? 0 : 255));
 	}
 #endif
 
@@ -155,29 +143,15 @@ void MeshUtilities::GenerateBackgroundBorder(Mesh& out_mesh, const Box& box, Vec
 #endif
 }
 
-void MeshUtilities::GenerateBackground(Mesh& out_mesh, const Box& box, Vector2f offset, Vector4f border_radius, ColourbPremultiplied color,
-	BoxArea fill_area)
+void MeshUtilities::GenerateBackground(Mesh& out_mesh, const RenderBox& render_box, ColourbPremultiplied color)
 {
-	RMLUI_ASSERTMSG(fill_area >= BoxArea::Border && fill_area <= BoxArea::Content,
-		"Rectangle geometry only supports border, padding and content boxes.");
-
-	EdgeSizes edge_sizes = {};
-	for (int area = (int)BoxArea::Border; area < (int)fill_area; area++)
-	{
-		// TODO: Move rounding to computed values (round border only).
-		edge_sizes[0] += Math::Round(box.GetEdge(BoxArea(area), BoxEdge::Top));
-		edge_sizes[1] += Math::Round(box.GetEdge(BoxArea(area), BoxEdge::Right));
-		edge_sizes[2] += Math::Round(box.GetEdge(BoxArea(area), BoxEdge::Bottom));
-		edge_sizes[3] += Math::Round(box.GetEdge(BoxArea(area), BoxEdge::Left));
-	}
-
-	const Vector2f inner_size = box.GetSize(fill_area).Round();
-
-	const bool has_background = (color.alpha > 0 && inner_size.x > 0 && inner_size.y > 0);
+	const Vector2f fill_size = render_box.GetFillSize();
+	const bool has_background = (color.alpha > 0 && fill_size.x > 0 && fill_size.y > 0);
 	if (!has_background)
 		return;
 
-	const BorderMetrics metrics = GeometryBackgroundBorder::ComputeBorderMetrics(offset.Round(), edge_sizes, inner_size, border_radius);
+	const BorderMetrics metrics = GeometryBackgroundBorder::ComputeBorderMetrics(render_box.GetBorderOffset(), render_box.GetBorderWidths(),
+		fill_size, render_box.GetBorderRadius());
 
 	Vector<Vertex>& vertices = out_mesh.vertices;
 	Vector<int>& indices = out_mesh.indices;

+ 2 - 0
Source/Core/RenderManager.cpp

@@ -238,6 +238,8 @@ CompiledGeometryHandle RenderManager::GetCompiledGeometryHandle(StableVectorInde
 void RenderManager::Render(const Geometry& geometry, Vector2f translation, Texture texture, const CompiledShader& shader)
 {
 	RMLUI_ASSERT(geometry);
+	RMLUI_ASSERTMSG(translation == translation.Round(), "RenderManager::Render expects translation to be rounded");
+
 	if (geometry.render_manager != this || (shader && shader.render_manager != this) || (texture && texture.render_manager != this))
 	{
 		RMLUI_ERRORMSG("Trying to render geometry with resources constructed in different render managers.");

+ 2 - 3
Source/Debugger/DebuggerPlugin.cpp

@@ -168,9 +168,8 @@ void DebuggerPlugin::Render()
 					ElementUtilities::ApplyTransform(*element);
 					for (int j = 0; j < element->GetNumBoxes(); ++j)
 					{
-						Vector2f box_offset;
-						const Box& box = element->GetBox(j, box_offset);
-						Geometry::RenderOutline(element->GetAbsoluteOffset(BoxArea::Border) + box_offset, box.GetSize(BoxArea::Border),
+						const RenderBox box = element->GetRenderBox(BoxArea::Border, j);
+						Geometry::RenderOutline(element->GetAbsoluteOffset(BoxArea::Border) + box.GetBorderOffset(), box.GetFillSize(),
 							Colourb(255, 0, 0, 128), 1);
 					}
 

+ 1 - 0
Tests/Source/UnitTests/CMakeLists.txt

@@ -9,6 +9,7 @@ add_executable(${TARGET_NAME}
 	Debugger.cpp
 	Decorator.cpp
 	Element.cpp
+	ElementBackgroundBorder.cpp
 	ElementDocument.cpp
 	ElementHandle.cpp
 	ElementFormControlSelect.cpp

+ 214 - 0
Tests/Source/UnitTests/ElementBackgroundBorder.cpp

@@ -0,0 +1,214 @@
+/*
+ * 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-2024 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 "../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 <float.h>
+
+using namespace Rml;
+
+static String GenerateRowsRml(int num_rows, String row_rml)
+{
+	String rml;
+	rml.reserve(num_rows * row_rml.size());
+	for (int i = 0; i < num_rows; i++)
+		rml += row_rml;
+	return rml;
+}
+
+static const String document_basic_rml = R"(
+<rml>
+<head>
+<title>Demo</title>
+<link type="text/rcss" href="/assets/rml.rcss" />
+<link type="text/rcss" href="/../Tests/Data/style.rcss" />
+<style>
+	body {
+		width: 800px;
+		height: 800px;
+	}
+	#wrapper {
+		height: 300.3px;
+		overflow-y: scroll;
+		background-color: #333;
+	}
+	#wrapper > div {
+		height: 100.25px;
+		background-color: #c33;
+		margin: 5.333px 0;
+		position: relative;
+	}
+</style>
+</head>
+<body>
+	<div id="wrapper">
+	</div>
+</body>
+</rml>
+)";
+
+TEST_CASE("ElementBackgroundBorder.render_stats")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_basic_rml);
+	REQUIRE(document);
+	document->Show();
+
+	constexpr int num_rows = 10;
+	const String row_rml = "<div/>";
+	const String inner_rml = GenerateRowsRml(num_rows, row_rml);
+
+	Element* wrapper = document->GetElementById("wrapper");
+	REQUIRE(wrapper);
+	wrapper->SetInnerRML(inner_rml);
+
+	context->Update();
+	context->Render();
+
+	TestsShell::RenderLoop();
+
+	TestsRenderInterface* render_interface = TestsShell::GetTestsRenderInterface();
+	if (!render_interface)
+		return;
+
+	MESSAGE(TestsShell::GetRenderStats());
+	render_interface->Reset();
+
+	for (int i = 1; i < 50; i++)
+	{
+		wrapper->SetScrollTop(1.3333f * float(i));
+		context->Update();
+		context->Render();
+	}
+
+	wrapper->SetScrollTop(FLT_MAX);
+	context->Update();
+	context->Render();
+
+	// Ensure that we have no unnecessary compile geometry commands. The geometry for the background-border should not
+	// change in size as long as scrolling occurs in integer increments.
+	CHECK(render_interface->GetCounters().compile_geometry == 0);
+
+	document->Close();
+	TestsShell::ShutdownShell();
+}
+
+static const String document_relative_offset_rml = R"(
+<rml>
+<head>
+<title>Demo</title>
+<link type="text/rcss" href="/assets/rml.rcss" />
+<link type="text/rcss" href="/../Tests/Data/style.rcss" />
+<style>
+	body {
+		width: 800px;
+		height: 800px;
+	}
+	#wrapper {
+		height: 600px;
+		background-color: #333;
+	}
+	#wrapper > div {
+		height: 10.333px;
+		background-color: #c33;
+		position: relative;
+	}
+</style>
+</head>
+<body>
+	<div id="wrapper">
+	</div>
+</body>
+</rml>
+)";
+
+TEST_CASE("ElementBackgroundBorder.background_edges_line_up_with_relative_offset")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_relative_offset_rml);
+	REQUIRE(document);
+	document->Show();
+
+	constexpr int num_children = 10;
+	const String row_rml = "<div/>";
+	const String inner_rml = GenerateRowsRml(num_children, row_rml);
+
+	Element* wrapper = document->GetElementById("wrapper");
+	REQUIRE(wrapper);
+	wrapper->SetInnerRML(inner_rml);
+
+	context->Update();
+	context->Render();
+
+	TestsShell::RenderLoop();
+
+	TestsRenderInterface* render_interface = TestsShell::GetTestsRenderInterface();
+	if (!render_interface)
+		return;
+
+	MESSAGE(TestsShell::GetRenderStats());
+	render_interface->Reset();
+
+	for (int i = 1; i < 100; i++)
+	{
+		for (int child_index = 0; child_index < num_children; child_index++)
+		{
+			Element* child = wrapper->GetChild(child_index);
+			child->SetProperty(PropertyId::Top, Property(1.3333f * float(i), Unit::PX));
+		}
+
+		context->Update();
+		context->Render();
+
+		for (int child_index = 0; child_index < num_children - 1; child_index++)
+		{
+			Element* current_child = wrapper->GetChild(child_index);
+			Element* next_child = wrapper->GetChild(child_index + 1);
+			Vector2f current_bottom_right = current_child->GetAbsoluteOffset(BoxArea::Border).Round() + current_child->GetRenderBox().GetFillSize();
+			Vector2f next_top_left = next_child->GetAbsoluteOffset(BoxArea::Border).Round();
+			CHECK(current_bottom_right.y == next_top_left.y);
+		}
+	}
+
+	// When changing the position using fractional increments we expect the size of the backgrounds to change, resulting
+	// in new geometry. This is done to ensure that the top and bottom of each background lines up with the one for the
+	// next element, thereby avoiding any gaps.
+	CHECK(render_interface->GetCounters().compile_geometry > 0);
+	MESSAGE("Compile geometry after movement: ", render_interface->GetCounters().compile_geometry);
+
+	document->Close();
+	TestsShell::ShutdownShell();
+}