Browse Source

Implement the 'word-break' property.

Michael Ragazzon 5 years ago
parent
commit
1fe43873a2

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

@@ -113,6 +113,7 @@ enum class TextAlign : uint8_t { Left, Right, Center, Justify };
 enum class TextDecoration : uint8_t { None, Underline, Overline, LineThrough };
 enum class TextTransform : uint8_t { None, Capitalize, Uppercase, Lowercase };
 enum class WhiteSpace : uint8_t { Normal, Pre, Nowrap, Prewrap, Preline };
+enum class WordBreak : uint8_t { Normal, BreakAll, BreakWord };
 
 enum class Drag : uint8_t { None, Drag, DragDrop, Block, Clone };
 enum class TabIndex : uint8_t { None, Auto };
@@ -184,6 +185,7 @@ struct ComputedValues
 	TextDecoration text_decoration = TextDecoration::None;
 	TextTransform text_transform = TextTransform::None;
 	WhiteSpace white_space = WhiteSpace::Normal;
+	WordBreak word_break = WordBreak::Normal;
 
 	String cursor;
 

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

@@ -123,6 +123,7 @@ enum class PropertyId : uint8_t
 	TextDecoration,
 	TextTransform,
 	WhiteSpace,
+	WordBreak,
 	Cursor,
 	Drag,
 	TabIndex,

+ 4 - 0
Source/Core/ElementStyle.cpp

@@ -598,6 +598,7 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 		values.text_decoration = parent_values->text_decoration;
 		values.text_transform = parent_values->text_transform;
 		values.white_space = parent_values->white_space;
+		values.word_break = parent_values->word_break;
 
 		values.cursor = parent_values->cursor;
 		values.focus = parent_values->focus;
@@ -786,6 +787,9 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 		case PropertyId::WhiteSpace:
 			values.white_space = (WhiteSpace)p->Get< int >();
 			break;
+		case PropertyId::WordBreak:
+			values.word_break = (WordBreak)p->Get< int >();
+			break;
 
 		case PropertyId::Cursor:
 			values.cursor = p->Get< String >();

+ 49 - 9
Source/Core/ElementTextDefault.cpp

@@ -202,7 +202,7 @@ bool ElementTextDefault::GenerateLine(String& line, int& line_length, float& lin
 	bool collapse_white_space = white_space_property == WhiteSpace::Normal ||
 								white_space_property == WhiteSpace::Nowrap ||
 								white_space_property == WhiteSpace::Preline;
-	bool break_at_line = maximum_line_width >= 0 && 
+	bool break_at_line = (maximum_line_width >= 0) && 
 		                   (white_space_property == WhiteSpace::Normal ||
 							white_space_property == WhiteSpace::Prewrap ||
 							white_space_property == WhiteSpace::Preline);
@@ -210,8 +210,10 @@ bool ElementTextDefault::GenerateLine(String& line, int& line_length, float& lin
 							white_space_property == WhiteSpace::Prewrap ||
 							white_space_property == WhiteSpace::Preline;
 
-	// Determine what (if any) text transformation we are putting the characters through.
 	TextTransform text_transform_property = computed.text_transform;
+	WordBreak word_break = computed.word_break;
+
+	FontEngineInterface* font_engine_interface = GetFontEngineInterface();
 
 	// Starting at the line_begin character, we generate sections of the text (we'll call them tokens) depending on the
 	// white-space parsing parameters. Each section is then appended to the line if it can fit. If not, or if an
@@ -228,21 +230,59 @@ bool ElementTextDefault::GenerateLine(String& line, int& line_length, float& lin
 
 		// 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);
-		int token_width = GetFontEngineInterface()->GetStringWidth(font_face_handle, token, previous_codepoint);
+		int token_width = font_engine_interface->GetStringWidth(font_face_handle, token, 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 (break_at_line)
 		{
-			if (!line.empty() &&
-				(line_width + token_width > maximum_line_width ||
-				 (LastToken(next_token_begin, string_end, collapse_white_space, break_at_endline) && line_width + token_width > maximum_line_width - right_spacing_width)))
+			const bool is_last_token = LastToken(next_token_begin, string_end, collapse_white_space, break_at_endline);
+			int max_token_width = int(maximum_line_width - (is_last_token ? line_width + right_spacing_width : line_width));
+
+			if (token_width > max_token_width)
 			{
-				return false;
+				if (word_break == WordBreak::BreakAll || (word_break == WordBreak::BreakWord && line.empty()))
+				{
+					// Try to break up the word
+					max_token_width = int(maximum_line_width - line_width);
+					const int token_max_size = int(next_token_begin - token_begin);
+					bool force_loop_break_after_next = false;
+
+					// @performance: Can be made much faster. Use string width heuristics and logarithmic search.
+					for (int i = token_max_size - 1; i > 0; --i)
+					{
+						token.clear();
+						next_token_begin = token_begin;
+						const char* partial_string_end = StringUtilities::SeekBackwardUTF8(token_begin + i, token_begin);
+						break_line = 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);
+
+						if (force_loop_break_after_next || token_width <= max_token_width)
+						{
+							break;
+						}
+						else if (next_token_begin == token_begin)
+						{
+							// This means the first character of the token doesn't fit. Let it overflow into the next line if we can.
+							if (!line.empty())
+								return false;
+
+							// Not even the first character of the line fits. Go back to consume the first character even though it will overflow.
+							i += 2;
+							force_loop_break_after_next = true;
+						}
+					}
+
+					break_line = true;
+				}
+				else if (!line.empty())
+				{
+					// Let the token overflow into the next line.
+					return false;
+				}
 			}
 		}
 
-		// The token can fit on the end of the line, so add it onto the end and increment our width and length
-		// counters.
+		// The token can fit on the end of the line, so add it onto the end and increment our width and length counters.
 		line += token;
 		line_length += (int)(next_token_begin - token_begin);
 		line_width += token_width;

+ 1 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -366,6 +366,7 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	RegisterProperty(PropertyId::TextDecoration, "text-decoration", "none", true, false).AddParser("keyword", "none, underline, overline, line-through");
 	RegisterProperty(PropertyId::TextTransform, "text-transform", "none", true, true).AddParser("keyword", "none, capitalize, uppercase, lowercase");
 	RegisterProperty(PropertyId::WhiteSpace, "white-space", "normal", true, true).AddParser("keyword", "normal, pre, nowrap, pre-wrap, pre-line");
+	RegisterProperty(PropertyId::WordBreak, "word-break", "normal", true, true).AddParser("keyword", "normal, break-all, break-word");
 
 	RegisterProperty(PropertyId::Cursor, "cursor", "", true, false).AddParser("string");
 

+ 53 - 0
Tests/Data/VisualTests/word_break.rml

@@ -0,0 +1,53 @@
+<rml>
+<head>
+    <title>Word-break property</title>
+    <link type="text/rcss" href="../style.rcss"/>
+	<link rel="help" href="https://drafts.csswg.org/css-text-3/#word-break-property" />
+	<meta name="Description" content="Word-break property." />
+	<style>
+		body {
+			background: #ddd;
+			color: #444;
+		}
+		h1 {
+			margin-top: 0.5em;
+			font-size: 1.1em;
+		}
+		p {
+			color: #45e;
+		}
+		.box {
+			width: 60px;
+			border: 2px #aaa;
+		}
+		.zero {
+			width: 0px;
+		}
+		.break-all {
+			word-break: break-all;
+		}
+		.break-word {
+			word-break: break-word;
+		}
+		
+	</style>
+</head>
+
+<body>
+	<h1>Fixed-width box</h1>
+	<p>word-break: normal</p>
+	<div class="box">A very veeery veeeeeeeeeeeery long word.</div>
+	<p>word-break: break-all</p>
+	<div class="box break-all">A very veeery veeeeeeeeeeeery long word.</div>
+	<p>word-break: break-word</p>
+	<div class="box break-word">A very veeery veeeeeeeeeeeery long word.</div>
+	<hr/>
+	<h1>Zero-width box</h1>
+	<p>word-break: normal</p>
+	<div class="box zero">A WORD</div>
+	<p>word-break: break-all</p>
+	<div class="box zero break-all">A WORD</div>
+	<p>word-break: break-word</p>
+	<div class="box zero break-word">A WORD</div>
+</body>
+</rml>

+ 18 - 11
Tests/Data/description.rml

@@ -58,21 +58,26 @@
 	#filter_text {
 		color: #ffc;
 	}
+	#content {
+		overflow: hidden auto;	
+	}
 	#goto {
-		position: absolute;
-		left: 20px;
-		bottom: 30px;
-		right: 20px;
+		margin: 0 20px 10px 20px;
 		color: #ddb;
 	}
-	#hovertext {
+	#bottom {
 		position: absolute;
-		left: 20px;
-		bottom: 10px;
-		right: 20px;
-		font-size: 0.9em;
+		left: 0;
+		bottom: 0;
+		right: 0;
 		text-align: left;
+		word-break: break-all;
+	}
+	#hovertext {
+		margin: 0px 20px 10px 20px;
+		font-size: 0.9em;
 		color: #aaa;
+		min-height: 1.5em;
 	}
 	#hovertext.confirmation {
 		color: #afa;
@@ -83,7 +88,9 @@
 <div id="header"/>
 <div id="filter"><input id="filterinput" type="text" value=""/><br/><span id="filter_text">Filtered 2 of 63.</span></div>
 <div id="content"/>
-<div id="goto"/>
-<div id="hovertext"/>
+<div id="bottom">
+	<div id="goto"/>
+	<div id="hovertext"/>
+</div>
 </body>
 </rml>