Browse Source

Add attributes and properties for language and direction (#563)

Matthew Schäfer 1 year ago
parent
commit
f9b8f6bbf6

+ 1 - 0
CMake/FileList.cmake

@@ -205,6 +205,7 @@ set(Core_PUB_HDR_FILES
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/StyleTypes.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/StyleTypes.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/SystemInterface.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/SystemInterface.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Texture.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Texture.h
+    ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/TextShapingContext.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Traits.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Traits.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Transform.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/Transform.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/TransformPrimitive.h
     ${PROJECT_SOURCE_DIR}/Include/RmlUi/Core/TransformPrimitive.h

+ 1 - 0
Include/RmlUi/Core.h

@@ -82,6 +82,7 @@
 #include "Core/StyleSheetSpecification.h"
 #include "Core/StyleSheetSpecification.h"
 #include "Core/StyleTypes.h"
 #include "Core/StyleTypes.h"
 #include "Core/SystemInterface.h"
 #include "Core/SystemInterface.h"
+#include "Core/TextShapingContext.h"
 #include "Core/Texture.h"
 #include "Core/Texture.h"
 #include "Core/Transform.h"
 #include "Core/Transform.h"
 #include "Core/TransformPrimitive.h"
 #include "Core/TransformPrimitive.h"

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

@@ -115,7 +115,7 @@ namespace Style {
 			font_weight(FontWeight::Normal), has_letter_spacing(0), font_style(FontStyle::Normal), has_font_effect(false),
 			font_weight(FontWeight::Normal), has_letter_spacing(0), font_style(FontStyle::Normal), has_font_effect(false),
 			pointer_events(PointerEvents::Auto), focus(Focus::Auto), text_align(TextAlign::Left), text_decoration(TextDecoration::None),
 			pointer_events(PointerEvents::Auto), focus(Focus::Auto), text_align(TextAlign::Left), text_decoration(TextDecoration::None),
 			text_transform(TextTransform::None), white_space(WhiteSpace::Normal), word_break(WordBreak::Normal),
 			text_transform(TextTransform::None), white_space(WhiteSpace::Normal), word_break(WordBreak::Normal),
-			line_height_inherit_type(LineHeight::Number)
+			direction(Direction::Auto), line_height_inherit_type(LineHeight::Number)
 		{}
 		{}
 
 
 		// Font face used to render text and resolve ex properties. Does not represent a true property
 		// Font face used to render text and resolve ex properties. Does not represent a true property
@@ -141,9 +141,13 @@ namespace Style {
 		WhiteSpace white_space : 3;
 		WhiteSpace white_space : 3;
 		WordBreak word_break : 2;
 		WordBreak word_break : 2;
 
 
+		Direction direction : 2;
+
 		LineHeight::InheritType line_height_inherit_type : 1;
 		LineHeight::InheritType line_height_inherit_type : 1;
 		float line_height = 12.f * 1.2f;
 		float line_height = 12.f * 1.2f;
 		float line_height_inherit = 1.2f;
 		float line_height_inherit = 1.2f;
+
+		String language = "";
 	};
 	};
 
 
 	struct RareValues {
 	struct RareValues {
@@ -257,6 +261,8 @@ namespace Style {
 		Colourb        color()            const { return inherited.color; }
 		Colourb        color()            const { return inherited.color; }
 		float          opacity()          const { return inherited.opacity; }
 		float          opacity()          const { return inherited.opacity; }
 		LineHeight     line_height()      const { return LineHeight(inherited.line_height, inherited.line_height_inherit_type, inherited.line_height_inherit); }
 		LineHeight     line_height()      const { return LineHeight(inherited.line_height, inherited.line_height_inherit_type, inherited.line_height_inherit); }
+		const String&  language()         const { return inherited.language; }
+		Direction      direction()        const { return inherited.direction; }
 
 
 		// -- Rare --
 		// -- Rare --
 		MinWidth          min_width()                  const { return LengthPercentage(rare.min_width_type, rare.min_width); }
 		MinWidth          min_width()                  const { return LengthPercentage(rare.min_width_type, rare.min_width); }
@@ -349,6 +355,8 @@ namespace Style {
 		void color             (Colourb value)        { inherited.color              = value; }
 		void color             (Colourb value)        { inherited.color              = value; }
 		void opacity           (float value)          { inherited.opacity            = value; }
 		void opacity           (float value)          { inherited.opacity            = value; }
 		void line_height       (LineHeight value)     { inherited.line_height = value.value; inherited.line_height_inherit_type = value.inherit_type; inherited.line_height_inherit = value.inherit_value;  }
 		void line_height       (LineHeight value)     { inherited.line_height = value.value; inherited.line_height_inherit_type = value.inherit_type; inherited.line_height_inherit = value.inherit_value;  }
+		void language          (const String& value)  { inherited.language           = value; }
+		void direction         (Direction value)      { inherited.direction          = value; }
 		// Rare
 		// Rare
 		void min_width                 (MinWidth value)          { rare.min_width_type             = value.type; rare.min_width                  = value.value; }
 		void min_width                 (MinWidth value)          { rare.min_width_type             = value.type; rare.min_width                  = value.value; }
 		void max_width                 (MaxWidth value)          { rare.max_width_type             = value.type; rare.max_width                  = value.value; }
 		void max_width                 (MaxWidth value)          { rare.max_width_type             = value.type; rare.max_width                  = value.value; }

+ 6 - 4
Include/RmlUi/Core/FontEngineInterface.h

@@ -32,6 +32,7 @@
 #include "Geometry.h"
 #include "Geometry.h"
 #include "Header.h"
 #include "Header.h"
 #include "StyleTypes.h"
 #include "StyleTypes.h"
+#include "TextShapingContext.h"
 #include "Types.h"
 #include "Types.h"
 
 
 namespace Rml {
 namespace Rml {
@@ -90,11 +91,12 @@ public:
 	/// Called by RmlUi when it wants to retrieve the width of a string when rendered with this handle.
 	/// Called by RmlUi when it wants to retrieve the width of a string when rendered with this handle.
 	/// @param[in] handle The font handle.
 	/// @param[in] handle The font handle.
 	/// @param[in] string The string to measure.
 	/// @param[in] string The string to measure.
-	/// @param[in] letter_spacing The letter spacing size in pixels.
+	/// @param[in] text_shaping_context Additional parameters that provide context for text shaping.
 	/// @param[in] prior_character The optionally-specified character that immediately precedes the string. This may have an impact on the string
 	/// @param[in] prior_character The optionally-specified character that immediately precedes the string. This may have an impact on the string
 	/// width due to kerning.
 	/// width due to kerning.
 	/// @return The width, in pixels, this string will occupy if rendered with this handle.
 	/// @return The width, in pixels, this string will occupy if rendered with this handle.
-	virtual int GetStringWidth(FontFaceHandle handle, const String& string, float letter_spacing, Character prior_character = Character::Null);
+	virtual int GetStringWidth(FontFaceHandle handle, const String& string, const TextShapingContext& text_shaping_context,
+		Character prior_character = Character::Null);
 
 
 	/// Called by RmlUi when it wants to retrieve the geometry required to render a single line of text.
 	/// Called by RmlUi when it wants to retrieve the geometry required to render a single line of text.
 	/// @param[in] face_handle The font handle.
 	/// @param[in] face_handle The font handle.
@@ -103,11 +105,11 @@ public:
 	/// @param[in] position The position of the baseline of the first character to render.
 	/// @param[in] position The position of the baseline of the first character to render.
 	/// @param[in] colour The colour to render the text. Colour alpha is premultiplied with opacity.
 	/// @param[in] colour The colour to render the text. Colour alpha is premultiplied with opacity.
 	/// @param[in] opacity The opacity of the text, should be applied to font effects.
 	/// @param[in] opacity The opacity of the text, should be applied to font effects.
-	/// @param[in] letter_spacing The letter spacing size in pixels.
+	/// @param[in] text_shaping_context Additional parameters that provide context for text shaping.
 	/// @param[out] geometry An array of geometries to generate the geometry into.
 	/// @param[out] geometry An array of geometries to generate the geometry into.
 	/// @return The width, in pixels, of the string geometry.
 	/// @return The width, in pixels, of the string geometry.
 	virtual int GenerateString(FontFaceHandle face_handle, FontEffectsHandle font_effects_handle, const String& string, const Vector2f& position,
 	virtual int GenerateString(FontFaceHandle face_handle, FontEffectsHandle font_effects_handle, const String& string, const Vector2f& position,
-		const Colourb& colour, float opacity, float letter_spacing, GeometryList& geometry);
+		const Colourb& colour, float opacity, const TextShapingContext& text_shaping_context, GeometryList& geometry);
 
 
 	/// Called by RmlUi to determine if the text geometry is required to be re-generated. Whenever the returned version
 	/// Called by RmlUi to determine if the text geometry is required to be re-generated. Whenever the returned version
 	/// is changed, all geometry belonging to the given face handle will be re-generated.
 	/// is changed, all geometry belonging to the given face handle will be re-generated.

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

@@ -175,6 +175,9 @@ enum class PropertyId : uint8_t {
 	NavDown,
 	NavDown,
 	NavLeft,
 	NavLeft,
 
 
+	RmlUi_Language,
+	RmlUi_Direction,
+
 	NumDefinedIds,
 	NumDefinedIds,
 	FirstCustomId = NumDefinedIds,
 	FirstCustomId = NumDefinedIds,
 
 

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

@@ -160,6 +160,8 @@ namespace Style {
 
 
 	enum class Nav : uint8_t { None, Auto, Horizontal, Vertical };
 	enum class Nav : uint8_t { None, Auto, Horizontal, Vertical };
 
 
+	enum class Direction : uint8_t { Auto, Ltr, Rtl };
+
 	class ComputedValues;
 	class ComputedValues;
 
 
 } // namespace Style
 } // namespace Style

+ 47 - 0
Include/RmlUi/Core/TextShapingContext.h

@@ -0,0 +1,47 @@
+/*
+ * 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-2023 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_TEXTSHAPINGCONTEXT_H
+#define RMLUI_CORE_TEXTSHAPINGCONTEXT_H
+
+#include "StyleTypes.h"
+#include "Types.h"
+
+namespace Rml {
+
+/*
+    Data extracted from the properties of an element to help provide context for text shaping and spacing.
+*/
+struct TextShapingContext {
+	const String& language;
+	Style::Direction text_direction = Style::Direction::Auto;
+	float letter_spacing = 0.0f; // Measured in pixels.
+};
+
+} // namespace Rml
+#endif

+ 3 - 2
Samples/basic/bitmapfont/src/FontEngineInterfaceBitmap.cpp

@@ -74,14 +74,15 @@ const FontMetrics& FontEngineInterfaceBitmap::GetFontMetrics(FontFaceHandle hand
 	return handle_bitmap->GetMetrics();
 	return handle_bitmap->GetMetrics();
 }
 }
 
 
-int FontEngineInterfaceBitmap::GetStringWidth(FontFaceHandle handle, const String& string, float /*letter_spacing*/, Character prior_character)
+int FontEngineInterfaceBitmap::GetStringWidth(FontFaceHandle handle, const String& string, const TextShapingContext& /*text_shaping_context*/,
+	Character prior_character)
 {
 {
 	auto handle_bitmap = reinterpret_cast<FontFaceBitmap*>(handle);
 	auto handle_bitmap = reinterpret_cast<FontFaceBitmap*>(handle);
 	return handle_bitmap->GetStringWidth(string, prior_character);
 	return handle_bitmap->GetStringWidth(string, prior_character);
 }
 }
 
 
 int FontEngineInterfaceBitmap::GenerateString(FontFaceHandle handle, FontEffectsHandle /*font_effects_handle*/, const String& string,
 int FontEngineInterfaceBitmap::GenerateString(FontFaceHandle handle, FontEffectsHandle /*font_effects_handle*/, const String& string,
-	const Vector2f& position, const Colourb& colour, float /*opacity*/, float /*letter_spacing*/, GeometryList& geometry)
+	const Vector2f& position, const Colourb& colour, float /*opacity*/, const TextShapingContext& /*text_shaping_context*/, GeometryList& geometry)
 {
 {
 	auto handle_bitmap = reinterpret_cast<FontFaceBitmap*>(handle);
 	auto handle_bitmap = reinterpret_cast<FontFaceBitmap*>(handle);
 	return handle_bitmap->GenerateString(string, position, colour, geometry);
 	return handle_bitmap->GenerateString(string, position, colour, geometry);

+ 4 - 2
Samples/basic/bitmapfont/src/FontEngineInterfaceBitmap.h

@@ -49,6 +49,7 @@ using Rml::Style::FontWeight;
 using Rml::FontEffectList;
 using Rml::FontEffectList;
 using Rml::FontMetrics;
 using Rml::FontMetrics;
 using Rml::GeometryList;
 using Rml::GeometryList;
+using Rml::TextShapingContext;
 
 
 class FontEngineInterfaceBitmap : public Rml::FontEngineInterface {
 class FontEngineInterfaceBitmap : public Rml::FontEngineInterface {
 public:
 public:
@@ -73,11 +74,12 @@ public:
 	const FontMetrics& GetFontMetrics(FontFaceHandle handle) override;
 	const FontMetrics& GetFontMetrics(FontFaceHandle handle) override;
 
 
 	/// Called by RmlUi when it wants to retrieve the width of a string when rendered with this handle.
 	/// Called by RmlUi when it wants to retrieve the width of a string when rendered with this handle.
-	int GetStringWidth(FontFaceHandle handle, const String& string, float letter_spacing, Character prior_character = Character::Null) override;
+	int GetStringWidth(FontFaceHandle handle, const String& string, const TextShapingContext& text_shaping_context,
+		Character prior_character = Character::Null) override;
 
 
 	/// Called by RmlUi when it wants to retrieve the geometry required to render a single line of text.
 	/// Called by RmlUi when it wants to retrieve the geometry required to render a single line of text.
 	int GenerateString(FontFaceHandle face_handle, FontEffectsHandle font_effects_handle, const String& string, const Vector2f& position,
 	int GenerateString(FontFaceHandle face_handle, FontEffectsHandle font_effects_handle, const String& string, const Vector2f& position,
-		const Colourb& colour, float opacity, float letter_spacing, GeometryList& geometry) override;
+		const Colourb& colour, float opacity, const TextShapingContext& text_shaping_context, GeometryList& geometry) override;
 
 
 	/// Called by RmlUi to determine if the text geometry is required to be re-generated.eometry.
 	/// Called by RmlUi to determine if the text geometry is required to be re-generated.eometry.
 	int GetVersion(FontFaceHandle handle) override;
 	int GetVersion(FontFaceHandle handle) override;

+ 26 - 0
Source/Core/Element.cpp

@@ -1645,6 +1645,32 @@ void Element::OnAttributeChange(const ElementAttributes& changed_attributes)
 			else if (value.GetType() != Variant::NONE)
 			else if (value.GetType() != Variant::NONE)
 				Log::Message(Log::LT_WARNING, "Invalid 'style' attribute, string type required. In element: %s", GetAddress().c_str());
 				Log::Message(Log::LT_WARNING, "Invalid 'style' attribute, string type required. In element: %s", GetAddress().c_str());
 		}
 		}
+		else if (attribute == "lang")
+		{
+			if (value.GetType() == Variant::STRING)
+				meta->style.SetProperty(PropertyId::RmlUi_Language, Property(value.GetReference<String>(), Unit::STRING));
+			else if (value.GetType() != Variant::NONE)
+				Log::Message(Log::LT_WARNING, "Invalid 'lang' attribute, string type required. In element: %s", GetAddress().c_str());
+		}
+		else if (attribute == "dir")
+		{
+			if (value.GetType() == Variant::STRING)
+			{
+				const String& dir_value = value.GetReference<String>();
+
+				if (dir_value == "auto")
+					meta->style.SetProperty(PropertyId::RmlUi_Direction, Property(Style::Direction::Auto));
+				else if (dir_value == "ltr")
+					meta->style.SetProperty(PropertyId::RmlUi_Direction, Property(Style::Direction::Ltr));
+				else if (dir_value == "rtl")
+					meta->style.SetProperty(PropertyId::RmlUi_Direction, Property(Style::Direction::Rtl));
+				else
+					Log::Message(Log::LT_WARNING, "Invalid 'dir' attribute '%s', value must be 'auto', 'ltr', or 'rtl'. In element: %s",
+						dir_value.c_str(), GetAddress().c_str());
+			}
+			else if (value.GetType() != Variant::NONE)
+				Log::Message(Log::LT_WARNING, "Invalid 'dir' attribute, string type required. In element: %s", GetAddress().c_str());
+		}
 	}
 	}
 
 
 	// Any change to the attributes may affect which styles apply to the current element, in particular due to attribute selectors, ID selectors, and
 	// Any change to the attributes may affect which styles apply to the current element, in particular due to attribute selectors, ID selectors, and

+ 7 - 0
Source/Core/ElementStyle.cpp

@@ -856,6 +856,13 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 			values.flex_basis(ComputeLengthPercentageAuto(p, font_size, document_font_size, dp_ratio, vp_dimensions));
 			values.flex_basis(ComputeLengthPercentageAuto(p, font_size, document_font_size, dp_ratio, vp_dimensions));
 			break;
 			break;
 
 
+		case PropertyId::RmlUi_Language:
+			values.language(p->Get<String>());
+			break;
+		case PropertyId::RmlUi_Direction:
+			values.direction(p->Get<Direction>());
+			break;
+
 		// Fetched from element's properties.
 		// Fetched from element's properties.
 		case PropertyId::Cursor:
 		case PropertyId::Cursor:
 		case PropertyId::Transition:
 		case PropertyId::Transition:

+ 15 - 12
Source/Core/ElementText.cpp

@@ -36,6 +36,7 @@
 #include "../../Include/RmlUi/Core/GeometryUtilities.h"
 #include "../../Include/RmlUi/Core/GeometryUtilities.h"
 #include "../../Include/RmlUi/Core/Profiling.h"
 #include "../../Include/RmlUi/Core/Profiling.h"
 #include "../../Include/RmlUi/Core/Property.h"
 #include "../../Include/RmlUi/Core/Property.h"
+#include "../../Include/RmlUi/Core/TextShapingContext.h"
 #include "ComputeProperty.h"
 #include "ComputeProperty.h"
 #include "ElementDefinition.h"
 #include "ElementDefinition.h"
 #include "ElementStyle.h"
 #include "ElementStyle.h"
@@ -196,7 +197,7 @@ bool ElementText::GenerateLine(String& line, int& line_length, float& line_width
 
 
 	// Determine how we are processing white-space while formatting the text.
 	// Determine how we are processing white-space while formatting the text.
 	using namespace Style;
 	using namespace Style;
-	auto& computed = GetComputedValues();
+	const auto& computed = GetComputedValues();
 	WhiteSpace white_space_property = computed.white_space();
 	WhiteSpace white_space_property = computed.white_space();
 	bool collapse_white_space =
 	bool collapse_white_space =
 		white_space_property == WhiteSpace::Normal || white_space_property == WhiteSpace::Nowrap || white_space_property == WhiteSpace::Preline;
 		white_space_property == WhiteSpace::Normal || white_space_property == WhiteSpace::Nowrap || white_space_property == WhiteSpace::Preline;
@@ -205,8 +206,7 @@ bool ElementText::GenerateLine(String& line, int& line_length, float& line_width
 	bool break_at_endline =
 	bool break_at_endline =
 		white_space_property == WhiteSpace::Pre || white_space_property == WhiteSpace::Prewrap || white_space_property == WhiteSpace::Preline;
 		white_space_property == WhiteSpace::Pre || white_space_property == WhiteSpace::Prewrap || white_space_property == WhiteSpace::Preline;
 
 
-	float letter_spacing = computed.letter_spacing();
-
+	const TextShapingContext text_shaping_context{ computed.language(), computed.direction(), computed.letter_spacing() };
 	TextTransform text_transform_property = computed.text_transform();
 	TextTransform text_transform_property = computed.text_transform();
 	WordBreak word_break = computed.word_break();
 	WordBreak word_break = computed.word_break();
 
 
@@ -228,7 +228,7 @@ bool ElementText::GenerateLine(String& line, int& line_length, float& line_width
 		// Generate the next token and determine its pixel-length.
 		// Generate the next token and determine its pixel-length.
 		bool break_line = BuildToken(token, next_token_begin, string_end, line.empty() && trim_whitespace_prefix, collapse_white_space,
 		bool break_line = BuildToken(token, next_token_begin, string_end, line.empty() && trim_whitespace_prefix, collapse_white_space,
 			break_at_endline, text_transform_property, decode_escape_characters);
 			break_at_endline, text_transform_property, decode_escape_characters);
-		int token_width = font_engine_interface->GetStringWidth(font_face_handle, token, letter_spacing, previous_codepoint);
+		int token_width = font_engine_interface->GetStringWidth(font_face_handle, token, text_shaping_context, previous_codepoint);
 
 
 		// If we're breaking to fit a line box, check if the token can fit on the line before we add it.
 		// If we're breaking to fit a line box, check if the token can fit on the line before we add it.
 		if (break_at_line)
 		if (break_at_line)
@@ -253,7 +253,7 @@ bool ElementText::GenerateLine(String& line, int& line_length, float& line_width
 						const char* partial_string_end = StringUtilities::SeekBackwardUTF8(token_begin + i, token_begin);
 						const char* partial_string_end = StringUtilities::SeekBackwardUTF8(token_begin + i, token_begin);
 						BuildToken(token, next_token_begin, partial_string_end, line.empty() && trim_whitespace_prefix, collapse_white_space,
 						BuildToken(token, next_token_begin, partial_string_end, line.empty() && trim_whitespace_prefix, collapse_white_space,
 							break_at_endline, text_transform_property, decode_escape_characters);
 							break_at_endline, text_transform_property, decode_escape_characters);
-						token_width = font_engine_interface->GetStringWidth(font_face_handle, token, letter_spacing, previous_codepoint);
+						token_width = font_engine_interface->GetStringWidth(font_face_handle, token, text_shaping_context, previous_codepoint);
 
 
 						if (force_loop_break_after_next || token_width <= max_token_width)
 						if (force_loop_break_after_next || token_width <= max_token_width)
 						{
 						{
@@ -353,11 +353,13 @@ void ElementText::OnPropertyChange(const PropertyIdSet& changed_properties)
 		}
 		}
 	}
 	}
 
 
-	if (changed_properties.Contains(PropertyId::FontFamily) || //
-		changed_properties.Contains(PropertyId::FontWeight) || //
-		changed_properties.Contains(PropertyId::FontStyle) ||  //
-		changed_properties.Contains(PropertyId::FontSize) ||   //
-		changed_properties.Contains(PropertyId::LetterSpacing))
+	if (changed_properties.Contains(PropertyId::FontFamily) ||     //
+		changed_properties.Contains(PropertyId::FontWeight) ||     //
+		changed_properties.Contains(PropertyId::FontStyle) ||      //
+		changed_properties.Contains(PropertyId::FontSize) ||       //
+		changed_properties.Contains(PropertyId::LetterSpacing) ||  //
+		changed_properties.Contains(PropertyId::RmlUi_Language) || //
+		changed_properties.Contains(PropertyId::RmlUi_Direction))
 	{
 	{
 		font_face_changed = true;
 		font_face_changed = true;
 
 
@@ -460,10 +462,11 @@ void ElementText::GenerateGeometry(const FontFaceHandle font_face_handle)
 
 
 void ElementText::GenerateGeometry(const FontFaceHandle font_face_handle, Line& line)
 void ElementText::GenerateGeometry(const FontFaceHandle font_face_handle, Line& line)
 {
 {
-	const float letter_spacing = GetComputedValues().letter_spacing();
+	const auto& computed = GetComputedValues();
+	const TextShapingContext text_shaping_context{ computed.language(), computed.direction(), computed.letter_spacing() };
 
 
 	line.width = GetFontEngineInterface()->GenerateString(font_face_handle, font_effects_handle, line.text, line.position, colour, opacity,
 	line.width = GetFontEngineInterface()->GenerateString(font_face_handle, font_effects_handle, line.text, line.position, colour, opacity,
-		letter_spacing, geometry);
+		text_shaping_context, geometry);
 }
 }
 
 
 void ElementText::GenerateDecoration(const FontFaceHandle font_face_handle)
 void ElementText::GenerateDecoration(const FontFaceHandle font_face_handle)

+ 4 - 2
Source/Core/ElementUtilities.cpp

@@ -34,6 +34,7 @@
 #include "../../Include/RmlUi/Core/Factory.h"
 #include "../../Include/RmlUi/Core/Factory.h"
 #include "../../Include/RmlUi/Core/FontEngineInterface.h"
 #include "../../Include/RmlUi/Core/FontEngineInterface.h"
 #include "../../Include/RmlUi/Core/RenderInterface.h"
 #include "../../Include/RmlUi/Core/RenderInterface.h"
+#include "../../Include/RmlUi/Core/TextShapingContext.h"
 #include "DataController.h"
 #include "DataController.h"
 #include "DataModel.h"
 #include "DataModel.h"
 #include "DataView.h"
 #include "DataView.h"
@@ -155,13 +156,14 @@ float ElementUtilities::GetDensityIndependentPixelRatio(Element* element)
 
 
 int ElementUtilities::GetStringWidth(Element* element, const String& string, Character prior_character)
 int ElementUtilities::GetStringWidth(Element* element, const String& string, Character prior_character)
 {
 {
-	const float letter_spacing = element->GetComputedValues().letter_spacing();
+	const auto& computed = element->GetComputedValues();
+	const TextShapingContext text_shaping_context{ computed.language(), computed.direction(), computed.letter_spacing() };
 
 
 	FontFaceHandle font_face_handle = element->GetFontFaceHandle();
 	FontFaceHandle font_face_handle = element->GetFontFaceHandle();
 	if (font_face_handle == 0)
 	if (font_face_handle == 0)
 		return 0;
 		return 0;
 
 
-	return GetFontEngineInterface()->GetStringWidth(font_face_handle, string, letter_spacing, prior_character);
+	return GetFontEngineInterface()->GetStringWidth(font_face_handle, string, text_shaping_context, prior_character);
 }
 }
 
 
 bool ElementUtilities::GetClippingRegion(Vector2i& clip_origin, Vector2i& clip_dimensions, Element* element)
 bool ElementUtilities::GetClippingRegion(Vector2i& clip_origin, Vector2i& clip_dimensions, Element* element)

+ 6 - 4
Source/Core/FontEngineDefault/FontEngineInterfaceDefault.cpp

@@ -71,17 +71,19 @@ const FontMetrics& FontEngineInterfaceDefault::GetFontMetrics(FontFaceHandle han
 	return handle_default->GetFontMetrics();
 	return handle_default->GetFontMetrics();
 }
 }
 
 
-int FontEngineInterfaceDefault::GetStringWidth(FontFaceHandle handle, const String& string, float letter_spacing, Character prior_character)
+int FontEngineInterfaceDefault::GetStringWidth(FontFaceHandle handle, const String& string, const TextShapingContext& text_shaping_context,
+	Character prior_character)
 {
 {
 	auto handle_default = reinterpret_cast<FontFaceHandleDefault*>(handle);
 	auto handle_default = reinterpret_cast<FontFaceHandleDefault*>(handle);
-	return handle_default->GetStringWidth(string, letter_spacing, prior_character);
+	return handle_default->GetStringWidth(string, text_shaping_context.letter_spacing, prior_character);
 }
 }
 
 
 int FontEngineInterfaceDefault::GenerateString(FontFaceHandle handle, FontEffectsHandle font_effects_handle, const String& string,
 int FontEngineInterfaceDefault::GenerateString(FontFaceHandle handle, FontEffectsHandle font_effects_handle, const String& string,
-	const Vector2f& position, const Colourb& colour, float opacity, float letter_spacing, GeometryList& geometry)
+	const Vector2f& position, const Colourb& colour, float opacity, const TextShapingContext& text_shaping_context, GeometryList& geometry)
 {
 {
 	auto handle_default = reinterpret_cast<FontFaceHandleDefault*>(handle);
 	auto handle_default = reinterpret_cast<FontFaceHandleDefault*>(handle);
-	return handle_default->GenerateString(geometry, string, position, colour, opacity, letter_spacing, (int)font_effects_handle);
+	return handle_default->GenerateString(geometry, string, position, colour, opacity, text_shaping_context.letter_spacing,
+		(int)font_effects_handle);
 }
 }
 
 
 int FontEngineInterfaceDefault::GetVersion(FontFaceHandle handle)
 int FontEngineInterfaceDefault::GetVersion(FontFaceHandle handle)

+ 2 - 2
Source/Core/FontEngineDefault/FontEngineInterfaceDefault.h

@@ -56,11 +56,11 @@ public:
 	const FontMetrics& GetFontMetrics(FontFaceHandle handle) override;
 	const FontMetrics& GetFontMetrics(FontFaceHandle handle) override;
 
 
 	/// Returns the width a string will take up if rendered with this handle.
 	/// Returns the width a string will take up if rendered with this handle.
-	int GetStringWidth(FontFaceHandle, const String& string, float letter_spacing, Character prior_character) override;
+	int GetStringWidth(FontFaceHandle, const String& string, const TextShapingContext& text_shaping_context, Character prior_character) override;
 
 
 	/// Generates the geometry required to render a single line of text.
 	/// Generates the geometry required to render a single line of text.
 	int GenerateString(FontFaceHandle, FontEffectsHandle, const String& string, const Vector2f& position, const Colourb& colour, float opacity,
 	int GenerateString(FontFaceHandle, FontEffectsHandle, const String& string, const Vector2f& position, const Colourb& colour, float opacity,
-		float letter_spacing, GeometryList& geometry) override;
+		const TextShapingContext& text_shaping_context, GeometryList& geometry) override;
 
 
 	/// Returns the current version of the font face.
 	/// Returns the current version of the font face.
 	int GetVersion(FontFaceHandle handle) override;
 	int GetVersion(FontFaceHandle handle) override;

+ 4 - 2
Source/Core/FontEngineInterface.cpp

@@ -62,13 +62,15 @@ const FontMetrics& FontEngineInterface::GetFontMetrics(FontFaceHandle /*handle*/
 	return metrics;
 	return metrics;
 }
 }
 
 
-int FontEngineInterface::GetStringWidth(FontFaceHandle /*handle*/, const String& /*string*/, float /*letter_spacing*/, Character /*prior_character*/)
+int FontEngineInterface::GetStringWidth(FontFaceHandle /*handle*/, const String& /*string*/, const TextShapingContext& /*text_shaping_context*/,
+	Character /*prior_character*/)
 {
 {
 	return 0;
 	return 0;
 }
 }
 
 
 int FontEngineInterface::GenerateString(FontFaceHandle /*face_handle*/, FontEffectsHandle /*font_effects_handle*/, const String& /*string*/,
 int FontEngineInterface::GenerateString(FontFaceHandle /*face_handle*/, FontEffectsHandle /*font_effects_handle*/, const String& /*string*/,
-	const Vector2f& /*position*/, const Colourb& /*colour*/, float /*opacity*/, float /*letter_spacing*/, GeometryList& /*geometry*/)
+	const Vector2f& /*position*/, const Colourb& /*colour*/, float /*opacity*/, const TextShapingContext& /*text_shaping_context*/,
+	GeometryList& /*geometry*/)
 {
 {
 	return 0;
 	return 0;
 }
 }

+ 4 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -429,6 +429,10 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	RegisterShorthand(ShorthandId::Flex, "flex", "flex-grow, flex-shrink, flex-basis", ShorthandType::Flex);
 	RegisterShorthand(ShorthandId::Flex, "flex", "flex-grow, flex-shrink, flex-basis", ShorthandType::Flex);
 	RegisterShorthand(ShorthandId::FlexFlow, "flex-flow", "flex-direction, flex-wrap", ShorthandType::FallThrough);
 	RegisterShorthand(ShorthandId::FlexFlow, "flex-flow", "flex-direction, flex-wrap", ShorthandType::FallThrough);
 
 
+	// Internationalization properties (internal)
+	RegisterProperty(PropertyId::RmlUi_Language, "--rmlui-language", "", true, true).AddParser("string");
+	RegisterProperty(PropertyId::RmlUi_Direction, "--rmlui-direction", "auto", true, true).AddParser("keyword", "auto, ltr, rtl");
+
 	RMLUI_ASSERTMSG(instance->properties.shorthand_map->AssertAllInserted(ShorthandId::NumDefinedIds), "Missing specification for one or more Shorthand IDs.");
 	RMLUI_ASSERTMSG(instance->properties.shorthand_map->AssertAllInserted(ShorthandId::NumDefinedIds), "Missing specification for one or more Shorthand IDs.");
 	RMLUI_ASSERTMSG(instance->properties.property_map->AssertAllInserted(PropertyId::NumDefinedIds), "Missing specification for one or more Property IDs.");
 	RMLUI_ASSERTMSG(instance->properties.property_map->AssertAllInserted(PropertyId::NumDefinedIds), "Missing specification for one or more Property IDs.");
 	// clang-format on
 	// clang-format on

+ 141 - 0
Tests/Source/UnitTests/Localization.cpp

@@ -0,0 +1,141 @@
+/*
+ * 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-2023 The RmlUi Team, and contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ */
+
+#include "../Common/TestsShell.h"
+#include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/ComputedValues.h>
+#include <RmlUi/Core/Core.h>
+#include <RmlUi/Core/Element.h>
+#include <RmlUi/Core/ElementDocument.h>
+#include <RmlUi/Core/StyleTypes.h>
+#include <RmlUi/Core/StringUtilities.h>
+#include <doctest.h>
+
+using namespace Rml;
+
+static const String document_localization_rml = R"(
+<rml>
+<head>
+</head>
+
+<body>
+	<div id="parent" lang="en" dir="ltr">
+		<span id="cell0"/>
+		<span id="cell1" lang="nl"/>
+		<span id="cell2" dir="auto"/>
+		<span id="cell3" lang="ar" dir="rtl"/>
+	</div>
+</body>
+</rml>
+)";
+
+TEST_CASE("Localization")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_localization_rml);
+	REQUIRE(document);
+	document->Show();
+
+	Element* parent_element = document->GetElementById("parent");
+	REQUIRE(parent_element);
+	Element* cells[4]{};
+	for (int i = 0; i < 4; ++i)
+	{
+		cells[i] = document->GetElementById(CreateString(8, "cell%d", i));
+		REQUIRE(cells[i]);
+	}
+
+	TestsShell::RenderLoop();
+
+	SUBCASE("Language")
+	{
+		REQUIRE(document->GetProperty(PropertyId::RmlUi_Language)->Get<String>() == "");
+		REQUIRE(document->GetComputedValues().language() == "");
+		REQUIRE(parent_element->GetProperty(PropertyId::RmlUi_Language)->Get<String>() == "en");
+		REQUIRE(parent_element->GetComputedValues().language() == "en");
+
+		CHECK(cells[0]->GetProperty(PropertyId::RmlUi_Language)->Get<String>() == "en");
+		CHECK(cells[0]->GetComputedValues().language() == "en");
+		CHECK(cells[1]->GetProperty(PropertyId::RmlUi_Language)->Get<String>() == "nl");
+		CHECK(cells[1]->GetComputedValues().language() == "nl");
+		CHECK(cells[2]->GetProperty(PropertyId::RmlUi_Language)->Get<String>() == "en");
+		CHECK(cells[2]->GetComputedValues().language() == "en");
+		CHECK(cells[3]->GetProperty(PropertyId::RmlUi_Language)->Get<String>() == "ar");
+		CHECK(cells[3]->GetComputedValues().language() == "ar");
+
+		SUBCASE("Change language")
+		{
+			parent_element->SetAttribute("lang", "es");
+			TestsShell::RenderLoop();
+
+			REQUIRE(parent_element->GetProperty(PropertyId::RmlUi_Language)->Get<String>() == "es");
+			REQUIRE(parent_element->GetComputedValues().language() == "es");
+
+			CHECK(cells[0]->GetProperty(PropertyId::RmlUi_Language)->Get<String>() == "es");
+			CHECK(cells[0]->GetComputedValues().language() == "es");
+			CHECK(cells[1]->GetProperty(PropertyId::RmlUi_Language)->Get<String>() == "nl");
+			CHECK(cells[1]->GetComputedValues().language() == "nl");
+		}
+	}
+
+	SUBCASE("Direction")
+	{
+		REQUIRE(document->GetProperty(PropertyId::RmlUi_Direction)->Get<Style::Direction>() == Style::Direction::Auto);
+		REQUIRE(document->GetComputedValues().direction() == Style::Direction::Auto);
+		REQUIRE(parent_element->GetProperty(PropertyId::RmlUi_Direction)->Get<Style::Direction>() == Style::Direction::Ltr);
+		REQUIRE(parent_element->GetComputedValues().direction() == Style::Direction::Ltr);
+
+		CHECK(cells[0]->GetProperty(PropertyId::RmlUi_Direction)->Get<Style::Direction>() == Style::Direction::Ltr);
+		CHECK(cells[0]->GetComputedValues().direction() == Style::Direction::Ltr);
+		CHECK(cells[1]->GetProperty(PropertyId::RmlUi_Direction)->Get<Style::Direction>() == Style::Direction::Ltr);
+		CHECK(cells[1]->GetComputedValues().direction() == Style::Direction::Ltr);
+		CHECK(cells[2]->GetProperty(PropertyId::RmlUi_Direction)->Get<Style::Direction>() == Style::Direction::Auto);
+		CHECK(cells[2]->GetComputedValues().direction() == Style::Direction::Auto);
+		CHECK(cells[3]->GetProperty(PropertyId::RmlUi_Direction)->Get<Style::Direction>() == Style::Direction::Rtl);
+		CHECK(cells[3]->GetComputedValues().direction() == Style::Direction::Rtl);
+
+		SUBCASE("Change direction")
+		{
+			parent_element->SetAttribute("dir", "rtl");
+			TestsShell::RenderLoop();
+
+			REQUIRE(parent_element->GetProperty(PropertyId::RmlUi_Direction)->Get<Style::Direction>() == Style::Direction::Rtl);
+			REQUIRE(parent_element->GetComputedValues().direction() == Style::Direction::Rtl);
+
+			CHECK(cells[0]->GetProperty(PropertyId::RmlUi_Direction)->Get<Style::Direction>() == Style::Direction::Rtl);
+			CHECK(cells[0]->GetComputedValues().direction() == Style::Direction::Rtl);
+			CHECK(cells[2]->GetProperty(PropertyId::RmlUi_Direction)->Get<Style::Direction>() == Style::Direction::Auto);
+			CHECK(cells[2]->GetComputedValues().direction() == Style::Direction::Auto);
+		}
+	}
+
+	document->Close();
+	TestsShell::ShutdownShell();
+}