Browse Source

Add `letter-spacing` property to RCSS (#429)

Co-authored-by: Michael Ragazzon <[email protected]>
Igor Segalla 2 years ago
parent
commit
6df2e64eaa

+ 23 - 19
Include/RmlUi/Core/ComputedValues.h

@@ -112,9 +112,10 @@ namespace Style {
 
 
 	struct InheritedValues {
 	struct InheritedValues {
 		InheritedValues() :
 		InheritedValues() :
-			font_weight(FontWeight::Normal), font_style(FontStyle::Normal), has_font_effect(false), 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), line_height_inherit_type(LineHeight::Number)
+			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),
+			text_transform(TextTransform::None), white_space(WhiteSpace::Normal), word_break(WordBreak::Normal),
+			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
@@ -126,7 +127,8 @@ namespace Style {
 		float opacity = 1;
 		float opacity = 1;
 		Colourb color = Colourb(255, 255, 255);
 		Colourb color = Colourb(255, 255, 255);
 
 
-		FontWeight font_weight;
+		FontWeight font_weight : 10;
+		uint16_t has_letter_spacing : 1;
 
 
 		FontStyle font_style : 1;
 		FontStyle font_style : 1;
 		bool has_font_effect : 1;
 		bool has_font_effect : 1;
@@ -241,6 +243,7 @@ namespace Style {
 		String         cursor()           const;
 		String         cursor()           const;
 		FontFaceHandle font_face_handle() const { return inherited.font_face_handle; }
 		FontFaceHandle font_face_handle() const { return inherited.font_face_handle; }
 		float          font_size()        const { return inherited.font_size; }
 		float          font_size()        const { return inherited.font_size; }
+		float          letter_spacing()   const;
 		bool           has_font_effect()  const { return inherited.has_font_effect; }
 		bool           has_font_effect()  const { return inherited.has_font_effect; }
 		FontStyle      font_style()       const { return inherited.font_style; }
 		FontStyle      font_style()       const { return inherited.font_style; }
 		FontWeight     font_weight()      const { return inherited.font_weight; }
 		FontWeight     font_weight()      const { return inherited.font_weight; }
@@ -330,21 +333,22 @@ namespace Style {
 		void border_left_color  (Colourb value)              { common.border_left_color   = value; }
 		void border_left_color  (Colourb value)              { common.border_left_color   = value; }
 		void has_decorator      (bool value)                 { common.has_decorator       = value; }
 		void has_decorator      (bool value)                 { common.has_decorator       = value; }
 		// Inherited
 		// Inherited
-		void font_face_handle(FontFaceHandle value) { inherited.font_face_handle = value; }
-		void font_size       (float value)          { inherited.font_size        = value; }
-		void has_font_effect (bool value)           { inherited.has_font_effect  = value; }
-		void font_style      (FontStyle value)      { inherited.font_style       = value; }
-		void font_weight     (FontWeight value)     { inherited.font_weight      = value; }
-		void pointer_events  (PointerEvents value)  { inherited.pointer_events   = value; }
-		void focus           (Focus value)          { inherited.focus            = value; }
-		void text_align      (TextAlign value)      { inherited.text_align       = value; }
-		void text_decoration (TextDecoration value) { inherited.text_decoration  = value; }
-		void text_transform  (TextTransform value)  { inherited.text_transform   = value; }
-		void white_space     (WhiteSpace value)     { inherited.white_space      = value; }
-		void word_break      (WordBreak value)      { inherited.word_break       = value; }
-		void color           (Colourb value)        { inherited.color            = 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 font_face_handle  (FontFaceHandle value) { inherited.font_face_handle   = value; }
+		void font_size         (float value)          { inherited.font_size          = value; }
+		void has_letter_spacing(bool value)           { inherited.has_letter_spacing = value; }
+		void has_font_effect   (bool value)           { inherited.has_font_effect    = value; }
+		void font_style        (FontStyle value)      { inherited.font_style         = value; }
+		void font_weight       (FontWeight value)     { inherited.font_weight        = value; }
+		void pointer_events    (PointerEvents value)  { inherited.pointer_events     = value; }
+		void focus             (Focus value)          { inherited.focus              = value; }
+		void text_align        (TextAlign value)      { inherited.text_align         = value; }
+		void text_decoration   (TextDecoration value) { inherited.text_decoration    = value; }
+		void text_transform    (TextTransform value)  { inherited.text_transform     = value; }
+		void white_space       (WhiteSpace value)     { inherited.white_space        = value; }
+		void word_break        (WordBreak value)      { inherited.word_break         = value; }
+		void color             (Colourb value)        { inherited.color              = 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;  }
 		// 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; }

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

@@ -91,9 +91,10 @@ 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] prior_character The optionally-specified character that immediately precedes the string. This may have an impact on the string width due to kerning.
 	/// @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.
 	/// @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, Character prior_character = Character::Null);
+	virtual int GetStringWidth(FontFaceHandle handle, const String& string, float letter_spacing, 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.
@@ -102,10 +103,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[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, GeometryList& geometry);
+		const Colourb& colour, float opacity, float letter_spacing, 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.

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

@@ -129,6 +129,7 @@ enum class PropertyId : uint8_t
 	FontStyle,
 	FontStyle,
 	FontWeight,
 	FontWeight,
 	FontSize,
 	FontSize,
+	LetterSpacing,
 	TextAlign,
 	TextAlign,
 	TextDecoration,
 	TextDecoration,
 	TextTransform,
 	TextTransform,

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

@@ -73,14 +73,14 @@ const FontMetrics& FontEngineInterfaceBitmap::GetFontMetrics(FontFaceHandle hand
 	return handle_bitmap->GetMetrics();
 	return handle_bitmap->GetMetrics();
 }
 }
 
 
-int FontEngineInterfaceBitmap::GetStringWidth(FontFaceHandle handle, const String& string, Character prior_character)
+int FontEngineInterfaceBitmap::GetStringWidth(FontFaceHandle handle, const String& string, float /*letter_spacing*/, 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*/, GeometryList& geometry)
+	const Vector2f& position, const Colourb& colour, float /*opacity*/, float /*letter_spacing*/, 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);
@@ -89,4 +89,4 @@ int FontEngineInterfaceBitmap::GenerateString(FontFaceHandle handle, FontEffects
 int FontEngineInterfaceBitmap::GetVersion(FontFaceHandle /*handle*/)
 int FontEngineInterfaceBitmap::GetVersion(FontFaceHandle /*handle*/)
 {
 {
 	return 0;
 	return 0;
-}
+}

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

@@ -75,11 +75,11 @@ 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, Character prior_character = Character::Null) override;
+	int GetStringWidth(FontFaceHandle handle, const String& string, float letter_spacing, 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, GeometryList& geometry) override;
+		const Colourb& colour, float opacity, float letter_spacing, 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;

+ 5 - 0
Source/Core/ComputedValues.cpp

@@ -68,6 +68,11 @@ String Style::ComputedValues::cursor() const
 	return String();
 	return String();
 }
 }
 
 
+float Style::ComputedValues::letter_spacing() const
+{
+	return inherited.has_letter_spacing ? element->ResolveNumericProperty(element->GetProperty(PropertyId::LetterSpacing), 0.f) : 0.f;
+}
+
 float ResolveValueOr(Style::LengthPercentageAuto length, float base_value, float default_value)
 float ResolveValueOr(Style::LengthPercentageAuto length, float base_value, float default_value)
 {
 {
 	if (length.type == Style::LengthPercentageAuto::Length)
 	if (length.type == Style::LengthPercentageAuto::Length)

+ 4 - 0
Source/Core/ElementStyle.cpp

@@ -784,6 +784,10 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 			// (font-size computed above)
 			// (font-size computed above)
 			dirty_font_face_handle = true;
 			dirty_font_face_handle = true;
 			break;
 			break;
+		case PropertyId::LetterSpacing:
+			values.has_letter_spacing(p->unit != Property::KEYWORD);
+			dirty_font_face_handle = true;
+			break;
 
 
 		case PropertyId::TextAlign:
 		case PropertyId::TextAlign:
 			values.text_align((TextAlign)p->Get<int>());
 			values.text_align((TextAlign)p->Get<int>());

+ 9 - 4
Source/Core/ElementText.cpp

@@ -208,6 +208,8 @@ bool ElementText::GenerateLine(String& line, int& line_length, float& line_width
 							white_space_property == WhiteSpace::Prewrap ||
 							white_space_property == WhiteSpace::Prewrap ||
 							white_space_property == WhiteSpace::Preline;
 							white_space_property == WhiteSpace::Preline;
 
 
+	float letter_spacing = 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 +230,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, break_at_endline, text_transform_property, decode_escape_characters);
 		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);
-		int token_width = font_engine_interface->GetStringWidth(font_face_handle, token, previous_codepoint);
+		int token_width = font_engine_interface->GetStringWidth(font_face_handle, token, letter_spacing, 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)
@@ -252,7 +254,7 @@ bool ElementText::GenerateLine(String& line, int& line_length, float& line_width
 						next_token_begin = token_begin;
 						next_token_begin = token_begin;
 						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, break_at_endline, text_transform_property, decode_escape_characters);
 						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);
-						token_width = font_engine_interface->GetStringWidth(font_face_handle, token, previous_codepoint);
+						token_width = font_engine_interface->GetStringWidth(font_face_handle, token, letter_spacing, previous_codepoint);
 
 
 						if (force_loop_break_after_next || token_width <= max_token_width)
 						if (force_loop_break_after_next || token_width <= max_token_width)
 						{
 						{
@@ -359,7 +361,8 @@ void ElementText::OnPropertyChange(const PropertyIdSet& changed_properties)
 	if (changed_properties.Contains(PropertyId::FontFamily) ||
 	if (changed_properties.Contains(PropertyId::FontFamily) ||
 		changed_properties.Contains(PropertyId::FontWeight) ||
 		changed_properties.Contains(PropertyId::FontWeight) ||
 		changed_properties.Contains(PropertyId::FontStyle) ||
 		changed_properties.Contains(PropertyId::FontStyle) ||
-		changed_properties.Contains(PropertyId::FontSize))
+		changed_properties.Contains(PropertyId::FontSize) ||
+		changed_properties.Contains(PropertyId::LetterSpacing))
 	{
 	{
 		font_face_changed = true;
 		font_face_changed = true;
 
 
@@ -465,7 +468,9 @@ 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)
 {
 {
-	line.width = GetFontEngineInterface()->GenerateString(font_face_handle, font_effects_handle, line.text, line.position, colour, opacity, geometry);
+	const float letter_spacing = GetComputedValues().letter_spacing();
+
+	line.width = GetFontEngineInterface()->GenerateString(font_face_handle, font_effects_handle, line.text, line.position, colour, opacity, letter_spacing, geometry);
 	for (size_t i = 0; i < geometry.size(); ++i)
 	for (size_t i = 0; i < geometry.size(); ++i)
 		geometry[i].SetHostElement(this);
 		geometry[i].SetHostElement(this);
 }
 }

+ 3 - 1
Source/Core/ElementUtilities.cpp

@@ -156,11 +156,13 @@ float ElementUtilities::GetDensityIndependentPixelRatio(Element * element)
 // Returns the width of a string rendered within the context of the given element.
 // Returns the width of a string rendered within the context of the given 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();
+
 	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, prior_character);
+	return GetFontEngineInterface()->GetStringWidth(font_face_handle, string, letter_spacing, prior_character);
 }
 }
 
 
 // Generates the clipping region for an element.
 // Generates the clipping region for an element.

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

@@ -70,17 +70,17 @@ const FontMetrics& FontEngineInterfaceDefault::GetFontMetrics(FontFaceHandle han
 	return handle_default->GetFontMetrics();
 	return handle_default->GetFontMetrics();
 }
 }
 
 
-int FontEngineInterfaceDefault::GetStringWidth(FontFaceHandle handle, const String& string, Character prior_character)
+int FontEngineInterfaceDefault::GetStringWidth(FontFaceHandle handle, const String& string, float letter_spacing, Character prior_character)
 {
 {
 	auto handle_default = reinterpret_cast<FontFaceHandleDefault *>(handle);
 	auto handle_default = reinterpret_cast<FontFaceHandleDefault *>(handle);
-	return handle_default->GetStringWidth(string, prior_character);
+	return handle_default->GetStringWidth(string, 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, GeometryList& geometry)
+	const Vector2f& position, const Colourb& colour, float opacity, float letter_spacing, 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, (int)font_effects_handle);
+	return handle_default->GenerateString(geometry, string, position, colour, opacity, 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, Character prior_character) override;
+	int GetStringWidth(FontFaceHandle, const String& string, float letter_spacing, 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,
-		GeometryList& geometry) override;
+		float letter_spacing, 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;

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

@@ -82,7 +82,7 @@ const FontGlyphMap& FontFaceHandleDefault::GetGlyphs() const
 }
 }
 
 
 // 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 FontFaceHandleDefault::GetStringWidth(const String& string, Character prior_character)
+int FontFaceHandleDefault::GetStringWidth(const String& string, float letter_spacing, Character prior_character)
 {
 {
 	int width = 0;
 	int width = 0;
 	for (auto it_string = StringIteratorU8(string); it_string; ++it_string)
 	for (auto it_string = StringIteratorU8(string); it_string; ++it_string)
@@ -98,11 +98,12 @@ int FontFaceHandleDefault::GetStringWidth(const String& string, Character prior_
 
 
 		// Adjust the cursor for this character's advance.
 		// Adjust the cursor for this character's advance.
 		width += glyph->advance;
 		width += glyph->advance;
+		width += (int)letter_spacing;
 
 
 		prior_character = character;
 		prior_character = character;
 	}
 	}
 
 
-	return width;
+	return Math::Max(width, 0);
 }
 }
 
 
 // Generates, if required, the layer configuration for a given array of font effects.
 // Generates, if required, the layer configuration for a given array of font effects.
@@ -191,7 +192,7 @@ bool FontFaceHandleDefault::GenerateLayerTexture(UniquePtr<const byte[]>& textur
 
 
 // Generates the geometry required to render a single line of text.
 // Generates the geometry required to render a single line of text.
 int FontFaceHandleDefault::GenerateString(GeometryList& geometry, const String& string, const Vector2f position, const Colourb colour,
 int FontFaceHandleDefault::GenerateString(GeometryList& geometry, const String& string, const Vector2f position, const Colourb colour,
-	const float opacity, const int layer_configuration_index)
+	const float opacity, const float letter_spacing, const int layer_configuration_index)
 {
 {
 	int geometry_index = 0;
 	int geometry_index = 0;
 	int line_width = 0;
 	int line_width = 0;
@@ -262,6 +263,7 @@ int FontFaceHandleDefault::GenerateString(GeometryList& geometry, const String&
 			layer->GenerateGeometry(&geometry[geometry_index], character, Vector2f(position.x + line_width, position.y), glyph_color);
 			layer->GenerateGeometry(&geometry[geometry_index], character, Vector2f(position.x + line_width, position.y), glyph_color);
 
 
 			line_width += glyph->advance;
 			line_width += glyph->advance;
+			line_width += (int)letter_spacing;
 			prior_character = character;
 			prior_character = character;
 		}
 		}
 
 
@@ -271,7 +273,7 @@ int FontFaceHandleDefault::GenerateString(GeometryList& geometry, const String&
 	// Cull any excess geometry from a previous generation.
 	// Cull any excess geometry from a previous generation.
 	geometry.resize(geometry_index);
 	geometry.resize(geometry_index);
 
 
-	return line_width;
+	return Math::Max(line_width, 0);
 }
 }
 
 
 bool FontFaceHandleDefault::UpdateLayersOnDirty()
 bool FontFaceHandleDefault::UpdateLayersOnDirty()

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

@@ -62,7 +62,7 @@ public:
 	/// @param[in] string The string to measure.
 	/// @param[in] string The string to measure.
 	/// @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.
 	/// @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.
 	/// @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.
-	int GetStringWidth(const String& string, Character prior_character = Character::Null);
+	int GetStringWidth(const String& string, float letter_spacing, Character prior_character = Character::Null);
 
 
 	/// Generates, if required, the layer configuration for a given list of font effects.
 	/// Generates, if required, the layer configuration for a given list of font effects.
 	/// @param[in] font_effects The list of font effects to generate the configuration for.
 	/// @param[in] font_effects The list of font effects to generate the configuration for.
@@ -84,7 +84,7 @@ public:
 	/// @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] layer_configuration Face configuration index to use for generating string.
 	/// @param[in] layer_configuration Face configuration index to use for generating string.
 	/// @return The width, in pixels, of the string geometry.
 	/// @return The width, in pixels, of the string geometry.
-	int GenerateString(GeometryList& geometry, const String& string, Vector2f position, Colourb colour, float opacity, int layer_configuration = 0);
+	int GenerateString(GeometryList& geometry, const String& string, Vector2f position, Colourb colour, float opacity, float letter_spacing, int layer_configuration = 0);
 
 
 	/// Version is changed whenever the layers are dirtied, requiring regeneration of string geometry.
 	/// Version is changed whenever the layers are dirtied, requiring regeneration of string geometry.
 	int GetVersion() const;
 	int GetVersion() const;

+ 2 - 2
Source/Core/FontEngineInterface.cpp

@@ -62,13 +62,13 @@ const FontMetrics& FontEngineInterface::GetFontMetrics(FontFaceHandle /*handle*/
 	return metrics;
 	return metrics;
 }
 }
 
 
-int FontEngineInterface::GetStringWidth(FontFaceHandle /*handle*/, const String& /*string*/, Character /*prior_character*/)
+int FontEngineInterface::GetStringWidth(FontFaceHandle /*handle*/, const String& /*string*/, float /*letter_spacing*/, 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*/, GeometryList& /*geometry*/)
+	const Vector2f& /*position*/, const Colourb& /*colour*/, float /*opacity*/, float /*letter_spacing*/, GeometryList& /*geometry*/)
 {
 {
 	return 0;
 	return 0;
 }
 }

+ 1 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -378,6 +378,7 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	RegisterProperty(PropertyId::FontStyle, "font-style", "normal", true, true).AddParser("keyword", "normal, italic");
 	RegisterProperty(PropertyId::FontStyle, "font-style", "normal", true, true).AddParser("keyword", "normal, italic");
 	RegisterProperty(PropertyId::FontWeight, "font-weight", "normal", true, true).AddParser("keyword", "normal=400, bold=700").AddParser("number");
 	RegisterProperty(PropertyId::FontWeight, "font-weight", "normal", true, true).AddParser("keyword", "normal=400, bold=700").AddParser("number");
 	RegisterProperty(PropertyId::FontSize, "font-size", "12px", true, true).AddParser("length").AddParser("length_percent").SetRelativeTarget(RelativeTarget::ParentFontSize);
 	RegisterProperty(PropertyId::FontSize, "font-size", "12px", true, true).AddParser("length").AddParser("length_percent").SetRelativeTarget(RelativeTarget::ParentFontSize);
+	RegisterProperty(PropertyId::LetterSpacing, "letter-spacing", "normal", true, true).AddParser("keyword", "normal").AddParser("length");
 	RegisterShorthand(ShorthandId::Font, "font", "font-style, font-weight, font-size, font-family", ShorthandType::FallThrough);
 	RegisterShorthand(ShorthandId::Font, "font", "font-style, font-weight, font-size, font-family", ShorthandType::FallThrough);
 
 
 	RegisterProperty(PropertyId::TextAlign, "text-align", "left", true, true).AddParser("keyword", "left, right, center, justify");
 	RegisterProperty(PropertyId::TextAlign, "text-align", "left", true, true).AddParser("keyword", "left, right, center, justify");

+ 44 - 0
Tests/Data/VisualTests/letter_spacing.rml

@@ -0,0 +1,44 @@
+<rml>
+<head>
+    <title>'letter-spacing' property</title>
+    <link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://w3c.github.io/csswg-drafts/css-text/#letter-spacing-property" />
+	<meta name="Description" content="Test 'letter-spacing' property at different lengths. Negative string widths should not crash the library." />
+	<style>
+		body {
+			font-size: 14px;
+			padding: 0;
+			border: 8dp #ddd;
+		}
+		h1 {
+			color: blue;
+			margin-bottom: 0;
+		}
+		hr { margin: 1em 0; }
+		p { margin-top: 0; }
+		.color { color: #a33; }
+		.normal { letter-spacing: normal; color: green; }
+	</style>
+</head>
+
+<body>
+<h1>normal</h1>        <p style="letter-spacing: normal">In consectetur, neque <span class="normal">dignissim tincidunt</span> dapibus, leo <span class="color">metus molestie</span> erat.</p>
+<h1>1px</h1>           <p style="letter-spacing:    1px">In consectetur, neque <span class="normal">dignissim tincidunt</span> dapibus, leo <span class="color">metus molestie</span> erat.</p>
+<h1>-1px</h1>          <p style="letter-spacing:   -1px">In consectetur, neque <span class="normal">dignissim tincidunt</span> dapibus, leo <span class="color">metus molestie</span> erat.</p>
+<h1>5x</h1>            <p style="letter-spacing:    5px">In consectetur, neque <span class="normal">dignissim tincidunt</span> dapibus, leo <span class="color">metus molestie</span> erat.</p>
+<h1>0.4em (5.6px)</h1> <p style="letter-spacing:  0.4em">In consectetur, neque <span class="normal">dignissim tincidunt</span> dapibus, leo <span class="color">metus molestie</span> erat.</p>
+<h1>-5px</h1>          <p style="letter-spacing:   -5px">In consectetur, neque <span class="normal">dignissim tincidunt</span> dapibus, leo <span class="color">metus molestie</span> erat.</p>
+<h1>20px</h1>          <p style="letter-spacing:   20px">In consectetur, neque <span class="normal">dignissim tincidunt</span> dapibus, leo <span class="color">metus molestie</span> erat.</p>
+<h1>-20px</h1>         <p style="letter-spacing:  -20px">In consectetur, neque <span class="normal">dignissim tincidunt</span> dapibus, leo <span class="color">metus molestie</span> erat.</p>
+
+<hr/>
+<h1>20px (left-aligned)</h1>     <p style="letter-spacing:  20px; text-align: left;  ">abc</p>
+<h1>20px (centered)</h1>         <p style="letter-spacing:  20px; text-align: center;">abc</p>
+<h1>20px (right-aligned)</h1>    <p style="letter-spacing:  20px; text-align: right; ">abc</p>
+
+<hr/>
+<h1>letter-spacing on inner element</h1>    <p style="letter-spacing:  0.5em;">a<span style="letter-spacing: 2em; background: #eee">bb</span>c</p>
+
+<handle size_target="#document"/>
+</body>
+</rml>