Browse Source

Add support for text-align for text inputs (#455)

Maximilian Stark 2 years ago
parent
commit
2ba8093b36

+ 82 - 21
Source/Core/Elements/WidgetTextInput.cpp

@@ -909,6 +909,23 @@ int WidgetTextInput::CalculateLineIndex(float position) const
 	return Math::Clamp(line_index, 0, (int)(lines.size() - 1));
 }
 
+float WidgetTextInput::GetAlignmentSpecificTextOffset(const char* p_begin, int line_index) const
+{
+	const float client_width = parent->GetClientWidth();
+	const float total_width = (float)ElementUtilities::GetStringWidth(text_element, String(p_begin, lines[line_index].editable_length));
+	auto text_align = GetElement()->GetComputedValues().text_align();
+
+	// offset position depending on text align
+	switch (text_align)
+	{
+	case Style::TextAlign::Right: return Math::Max(0.0f, (client_width - total_width));
+	case Style::TextAlign::Center: return Math::Max(0.0f, ((client_width - total_width) / 2));
+	default: break;
+	}
+
+	return 0;
+}
+
 int WidgetTextInput::CalculateCharacterIndex(int line_index, float position)
 {
 	int prev_offset = 0;
@@ -918,6 +935,8 @@ int WidgetTextInput::CalculateCharacterIndex(int line_index, float position)
 
 	const char* p_begin = GetValue().data() + lines[line_index].value_offset;
 
+	position -= GetAlignmentSpecificTextOffset(p_begin, line_index);
+
 	for (auto it = StringIteratorU8(p_begin, p_begin, p_begin + lines[line_index].editable_length); it;)
 	{
 		++it;
@@ -1061,6 +1080,18 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 	Vector2f line_position(0, font_baseline);
 	bool last_line = false;
 
+	auto text_align = GetElement()->GetComputedValues().text_align();
+
+	struct Segment {
+		Vector2f position;
+		int width;
+		String content;
+		bool selected;
+		int line_index;
+	};
+
+	Vector<Segment> segments;
+
 	// Keep generating lines until all the text content is placed.
 	do
 	{
@@ -1080,10 +1111,10 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 
 		// If this line terminates in a soft-return (word wrap), then the line may be leaving a space or two behind as an orphan. If so, we must
 		// append the orphan onto the line even though it will push the line outside of the input field's bounds.
+		String orphan;
 		if (!last_line && (line_content.empty() || line_content.back() != '\n'))
 		{
 			const String& text = GetValue();
-			String orphan;
 			for (int i = 1; i >= 0; --i)
 			{
 				int index = line_begin + line.size + i;
@@ -1100,13 +1131,20 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 				if (!orphan.empty() || next_index >= (int)text.size() || text[next_index] != ' ')
 					orphan += ' ';
 			}
+		}
 
-			if (!orphan.empty())
-			{
-				line_content += orphan;
-				line.size += (int)orphan.size();
-				line_width += ElementUtilities::GetStringWidth(text_element, orphan);
-			}
+		if (!orphan.empty())
+		{
+			line_content += orphan;
+			line.size += (int)orphan.size();
+			line_width += ElementUtilities::GetStringWidth(text_element, orphan);
+		}
+
+		// visually remove trailing space if right aligned
+		if (!last_line && text_align == Style::TextAlign::Right && !line_content.empty() && line_content.back() == ' ')
+		{
+			line_content.pop_back();
+			line_width -= ElementUtilities::GetStringWidth(text_element, " ");
 		}
 
 		// Now that we have the string of characters appearing on the new line, we split it into
@@ -1119,8 +1157,9 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 		// the beginning of this line, then this will be empty).
 		if (!pre_selection.empty())
 		{
-			text_element->AddLine(line_position, pre_selection);
-			line_position.x += ElementUtilities::GetStringWidth(text_element, pre_selection);
+			const int width = ElementUtilities::GetStringWidth(text_element, pre_selection);
+			segments.push_back({line_position, width, pre_selection, false, (int)lines.size()});
+			line_position.x += width;
 		}
 
 		// Return the extra kerning that would result in joining two strings.
@@ -1147,16 +1186,8 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 		if (!selection.empty())
 		{
 			line_position.x += GetKerningBetween(pre_selection, selection);
-			selected_text_element->AddLine(line_position, selection);
-
 			const int selection_width = ElementUtilities::GetStringWidth(selected_text_element, selection);
-			const bool selection_contains_endline = (selection_begin_index + selection_length > line_begin + line.editable_length);
-			const Vector2f selection_size(float(selection_width + (selection_contains_endline ? endline_selection_width : 0)), line_height);
-
-			selection_vertices.resize(selection_vertices.size() + 4);
-			selection_indices.resize(selection_indices.size() + 6);
-			GeometryUtilities::GenerateQuad(&selection_vertices[selection_vertices.size() - 4], &selection_indices[selection_indices.size() - 6],
-				line_position - Vector2f(0.f, font_baseline), selection_size, selection_colour, (int)selection_vertices.size() - 4);
+			segments.push_back({line_position, selection_width, selection, true, (int)lines.size()});
 
 			line_position.x += selection_width;
 		}
@@ -1166,7 +1197,8 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 		if (!post_selection.empty())
 		{
 			line_position.x += GetKerningBetween(selection, post_selection);
-			text_element->AddLine(line_position, post_selection);
+			const int width = ElementUtilities::GetStringWidth(text_element, post_selection);
+			segments.push_back({line_position, width, post_selection, false, (int)lines.size()});
 		}
 
 		// Update variables for the next line.
@@ -1186,6 +1218,31 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 	// Clamp the cursor to a valid range.
 	absolute_cursor_index = Math::Min(absolute_cursor_index, (int)GetValue().size());
 
+	// Transform segments according to text alignment
+	for (auto& it : segments)
+	{
+		auto const& line = lines[it.line_index];
+		const char* p_begin = GetValue().data() + line.value_offset;
+		float offset = GetAlignmentSpecificTextOffset(p_begin, it.line_index);
+
+		it.position.x += offset;
+
+		if (it.selected)
+		{
+			const bool selection_contains_endline = (selection_begin_index + selection_length > line_begin + lines[it.line_index].editable_length);
+			const Vector2f selection_size(float(it.width + (selection_contains_endline ? endline_selection_width : 0)), line_height);
+
+			selection_vertices.resize(selection_vertices.size() + 4);
+			selection_indices.resize(selection_indices.size() + 6);
+			GeometryUtilities::GenerateQuad(&selection_vertices[selection_vertices.size() - 4], &selection_indices[selection_indices.size() - 6],
+				it.position - Vector2f(0, font_baseline), selection_size, selection_colour, (int)selection_vertices.size() - 4);
+
+			selected_text_element->AddLine(it.position, it.content);
+		}
+		else
+			text_element->AddLine(it.position, it.content);
+	}
+
 	return content_area;
 }
 
@@ -1227,10 +1284,14 @@ void WidgetTextInput::UpdateCursorPosition(bool update_ideal_cursor_position)
 	int cursor_line_index = 0, cursor_character_index = 0;
 	GetRelativeCursorIndices(cursor_line_index, cursor_character_index);
 
-	cursor_position.x =
-		(float)ElementUtilities::GetStringWidth(text_element, GetValue().substr(lines[cursor_line_index].value_offset, cursor_character_index));
+	auto const& line = lines[cursor_line_index];
+	const char* p_begin = GetValue().data() + line.value_offset;
+
+	cursor_position.x = (float)ElementUtilities::GetStringWidth(text_element, String(p_begin, cursor_character_index));
 	cursor_position.y = -1.f + (float)cursor_line_index * text_element->GetLineHeight();
 
+	cursor_position.x += GetAlignmentSpecificTextOffset(p_begin, cursor_line_index);
+
 	if (update_ideal_cursor_position)
 		ideal_cursor_position = cursor_position.x;
 }

+ 2 - 0
Source/Core/Elements/WidgetTextInput.h

@@ -95,6 +95,8 @@ public:
 protected:
 	enum class CursorMovement { Begin = -4, BeginLine = -3, PreviousWord = -2, Left = -1, Right = 1, NextWord = 2, EndLine = 3, End = 4 };
 
+	float GetAlignmentSpecificTextOffset(const char* p_begin, int line_index) const;
+
 	/// Processes the "keydown" and "textinput" event to write to the input field, and the "focus" and
 	/// "blur" to set the state of the cursor.
 	void ProcessEvent(Event& event) override;

+ 46 - 0
Tests/Data/VisualTests/text_alignment.rml

@@ -0,0 +1,46 @@
+<rml>
+
+	<head>
+		<title>Text-alignment property</title>
+		<link type="text/rcss" href="../style.rcss" />
+		<meta name="Description" content="Text-alignment property for inputs." />
+		<style>
+			body {
+				background: #ddd;
+				color: #444;
+			}
+
+			textarea {
+				background-color: #bbb;
+			}
+
+			h1 {
+				margin-top: 0.3em;
+				font-size: 1.1em;
+			}
+		</style>
+	</head>
+
+	<body>
+		<input style="text-align:left" value="left aligned" />
+		<br />
+		<input style="text-align:center" value="center aligned" />
+		<br />
+		<input style="text-align:right" value="right aligned" />
+		<br />
+		<textarea style="text-align:left" rows="3">left aligned overflow overflow overflow overflow
+line-break</textarea>
+		<br />
+		<textarea style="text-align:center" rows="3">center aligned overflow overflow overflow overflow
+line-break</textarea>
+		<br />
+		<textarea style="text-align:right" rows="3">right aligned overflow overflow overflow overflow
+line-break</textarea>
+		<br />
+		<textarea style="text-align:left" rows="3" wrap="nowrap">left aligned nowrap nowrap nowrap nowrap nowrap</textarea>
+		<br />
+		<textarea style="text-align:center" rows="3" wrap="nowrap">center aligned nowrap nowrap nowrap nowrap nowrap</textarea>
+		<br />
+		<textarea style="text-align:right" rows="3" wrap="nowrap">right aligned nowrap nowrap nowrap nowrap nowrap</textarea>
+	</body>
+</rml>