Skip to content

Commit

Permalink
Fix gaps when rendering backgrounds and borders of fractionally sized…
Browse files Browse the repository at this point in the history
… and positioned elements

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.

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.
  • Loading branch information
mikke89 committed Dec 9, 2024
1 parent 352f087 commit 1dbf2fa
Show file tree
Hide file tree
Showing 25 changed files with 512 additions and 183 deletions.
9 changes: 5 additions & 4 deletions Include/RmlUi/Core/ComputedValues.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include "Animation.h"
#include "Element.h"
#include "RenderBox.h"
#include "StyleTypes.h"
#include "Types.h"
#include <cfloat>
Expand Down Expand Up @@ -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); }
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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; }
Expand All @@ -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; }
Expand Down
9 changes: 9 additions & 0 deletions Include/RmlUi/Core/Element.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -158,6 +159,11 @@ class RMLUICORE_API Element : public ScriptInterface, public EnableObserverPtr<E
/// @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();
Expand Down Expand Up @@ -678,6 +684,7 @@ class RMLUICORE_API Element : public ScriptInterface, public EnableObserverPtr<E

void DirtyAbsoluteOffset();
void DirtyAbsoluteOffsetRecursive();
void UpdateAbsoluteOffsetAndRenderBoxData();
void UpdateOffset();
void SetBaseline(float baseline);

Expand Down Expand Up @@ -728,6 +735,7 @@ class RMLUICORE_API Element : public ScriptInterface, public EnableObserverPtr<E

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;
Expand Down Expand Up @@ -770,6 +778,7 @@ class RMLUICORE_API Element : public ScriptInterface, public EnableObserverPtr<E
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;
Expand Down
40 changes: 17 additions & 23 deletions Include/RmlUi/Core/MeshUtilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@
#define RMLUI_CORE_MESHUTILITIES_H

#include "Header.h"
#include "RenderBox.h"
#include "Types.h"

namespace Rml {

class Box;
struct Mesh;

/**
Expand All @@ -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;
Expand Down
81 changes: 81 additions & 0 deletions Include/RmlUi/Core/RenderBox.h
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions Source/Core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
39 changes: 18 additions & 21 deletions Source/Core/DecoratorGradient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -458,16 +455,16 @@ 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)
return INVALID_DECORATORDATAHANDLE;

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);

Expand All @@ -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;

Expand Down Expand Up @@ -657,16 +654,16 @@ 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)
return INVALID_DECORATORDATAHANDLE;

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();
Expand All @@ -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;

Expand Down
5 changes: 3 additions & 2 deletions Source/Core/DecoratorNinePatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
Loading

0 comments on commit 1dbf2fa

Please sign in to comment.