Browse Source

Refactor and improve text widget navigation and selection

- Make the selection box be extended by newlines again.
- Avoid selection box overflowing the element, by force clipping in such situations.
- For word-wrapped text widgets, allow the edit region to "overflow" with blank spaces. Instead, force the element not to scroll, and clamp the cursor to the content space.
- For `text-align: right`, don't remove any soft-wrapped space at the end of the line from the edit region. Instead, adjust the alignment offset to position the line appropriately.
  - This ensures that newline selection extension also works for righ alignment.
- Share selection and composition IME geometry.
- Adjust composition underline position so that it sits at a fixed position compared to the text cursor.
- Avoid doing unnecessary string width calculations during alignment.
Michael Ragazzon 1 year ago
parent
commit
be9a497b50
2 changed files with 121 additions and 143 deletions
  1. 106 127
      Source/Core/Elements/WidgetTextInput.cpp
  2. 15 16
      Source/Core/Elements/WidgetTextInput.h

+ 106 - 127
Source/Core/Elements/WidgetTextInput.cpp

@@ -50,8 +50,9 @@
 
 
 namespace Rml {
 namespace Rml {
 
 
-static constexpr float CURSOR_BLINK_TIME = 0.7f;  // [s]
-static constexpr float OVERFLOW_TOLERANCE = 0.5f; // [px]
+static constexpr float CURSOR_BLINK_TIME = 0.7f;          // [s]
+static constexpr float OVERFLOW_TOLERANCE = 0.5f;         // [px]
+static constexpr float COMPOSITION_UNDERLINE_WIDTH = 2.f; // [px]
 
 
 enum class CharacterClass { Word, Punctuation, Newline, Whitespace, Undefined };
 enum class CharacterClass { Word, Punctuation, Newline, Whitespace, Undefined };
 static CharacterClass GetCharacterClass(char c)
 static CharacterClass GetCharacterClass(char c)
@@ -193,6 +194,7 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) :
 	parent->SetProperty(PropertyId::Drag, Property(Style::Drag::Drag));
 	parent->SetProperty(PropertyId::Drag, Property(Style::Drag::Drag));
 	parent->SetProperty(PropertyId::WordBreak, Property(Style::WordBreak::BreakWord));
 	parent->SetProperty(PropertyId::WordBreak, Property(Style::WordBreak::BreakWord));
 	parent->SetProperty(PropertyId::TextTransform, Property(Style::TextTransform::None));
 	parent->SetProperty(PropertyId::TextTransform, Property(Style::TextTransform::None));
+	parent->SetProperty(PropertyId::Clip, Property(Style::Clip::Type::Auto));
 
 
 	parent->AddEventListener(EventId::Keydown, this, true);
 	parent->AddEventListener(EventId::Keydown, this, true);
 	parent->AddEventListener(EventId::Textinput, this, true);
 	parent->AddEventListener(EventId::Textinput, this, true);
@@ -242,6 +244,7 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) :
 	ime_composition_end_index = 0;
 	ime_composition_end_index = 0;
 
 
 	last_update_time = 0;
 	last_update_time = 0;
+	ink_overflow = false;
 
 
 	ShowCursor(false);
 	ShowCursor(false);
 }
 }
@@ -467,8 +470,7 @@ void WidgetTextInput::OnRender()
 	ElementUtilities::SetClippingRegion(text_element);
 	ElementUtilities::SetClippingRegion(text_element);
 
 
 	Vector2f text_translation = parent->GetAbsoluteOffset() - Vector2f(parent->GetScrollLeft(), parent->GetScrollTop());
 	Vector2f text_translation = parent->GetAbsoluteOffset() - Vector2f(parent->GetScrollLeft(), parent->GetScrollTop());
-	selection_geometry.Render(text_translation);
-	ime_composition_geometry.Render(text_translation);
+	selection_composition_geometry.Render(text_translation);
 
 
 	if (cursor_visible && !parent->IsDisabled())
 	if (cursor_visible && !parent->IsDisabled())
 	{
 	{
@@ -1059,20 +1061,36 @@ int WidgetTextInput::CalculateLineIndex(float position) const
 	return Math::Clamp(line_index, 0, (int)(lines.size() - 1));
 	return Math::Clamp(line_index, 0, (int)(lines.size() - 1));
 }
 }
 
 
-float WidgetTextInput::GetAlignmentSpecificTextOffset(const char* p_begin, int line_index) const
+float WidgetTextInput::GetAlignmentSpecificTextOffset(const Line& line) const
 {
 {
-	const float available_width = GetAvailableWidth();
-	const float total_width = (float)ElementUtilities::GetStringWidth(text_element, String(p_begin, lines[line_index].editable_length));
-	auto text_align = GetElement()->GetComputedValues().text_align();
+	const String& value = GetValue();
+	StringView editable_line_string(value, line.value_offset, line.editable_length);
+
+	const Style::TextAlign text_align = parent->GetComputedValues().text_align();
+
+	// Callback to avoid expensive calculation in the cases where it is not needed.
+	auto RemainingWidth = [&] {
+		const float total_width = (float)ElementUtilities::GetStringWidth(text_element, String(editable_line_string));
+		return GetAvailableWidth() - total_width;
+	};
 
 
-	// offset position depending on text align
 	switch (text_align)
 	switch (text_align)
 	{
 	{
-	case Style::TextAlign::Right: return Math::Max(0.0f, (available_width - total_width));
-	case Style::TextAlign::Center: return Math::Max(0.0f, ((available_width - total_width) / 2));
-	default: break;
+	case Style::TextAlign::Left: return 0;
+	case Style::TextAlign::Right:
+	{
+		// For right alignment with soft-wrapped newlines, remove up to a single space to align the last word to the right edge.
+		const bool is_last_line = (line.value_offset + line.size == (int)value.size());
+		const bool is_soft_wrapped = (!is_last_line && line.editable_length == line.size);
+		if (is_soft_wrapped && editable_line_string.size() > 0 && *(editable_line_string.end() - 1) == ' ')
+		{
+			editable_line_string = StringView(editable_line_string.begin(), editable_line_string.end() - 1);
+		}
+		return Math::Max(0.0f, RemainingWidth());
+	}
+	case Style::TextAlign::Center: return Math::Max(0.0f, 0.5f * RemainingWidth());
+	case Style::TextAlign::Justify: return 0;
 	}
 	}
-
 	return 0;
 	return 0;
 }
 }
 
 
@@ -1083,11 +1101,12 @@ int WidgetTextInput::CalculateCharacterIndex(int line_index, float position)
 
 
 	ideal_cursor_position_to_the_right_of_cursor = true;
 	ideal_cursor_position_to_the_right_of_cursor = true;
 
 
-	const char* p_begin = GetValue().data() + lines[line_index].value_offset;
+	const Line& line = lines[line_index];
+	const char* p_begin = GetValue().data() + line.value_offset;
 
 
-	position -= GetAlignmentSpecificTextOffset(p_begin, line_index);
+	position -= GetAlignmentSpecificTextOffset(line);
 
 
-	for (auto it = StringIteratorU8(p_begin, p_begin, p_begin + lines[line_index].editable_length); it;)
+	for (auto it = StringIteratorU8(p_begin, p_begin, p_begin + line.editable_length); it;)
 	{
 	{
 		++it;
 		++it;
 		const int offset = (int)it.offset();
 		const int offset = (int)it.offset();
@@ -1130,8 +1149,11 @@ void WidgetTextInput::ShowCursor(bool show, bool move_to_cursor)
 			else if (parent->GetScrollTop() > cursor_position.y)
 			else if (parent->GetScrollTop() > cursor_position.y)
 				parent->SetScrollTop(cursor_position.y);
 				parent->SetScrollTop(cursor_position.y);
 
 
+			const bool word_wrap = parent->GetComputedValues().white_space() == Style::WhiteSpace::Prewrap;
 			float minimum_scroll_left = (cursor_position.x + cursor_size.x) - GetAvailableWidth();
 			float minimum_scroll_left = (cursor_position.x + cursor_size.x) - GetAvailableWidth();
-			if (parent->GetScrollLeft() < minimum_scroll_left)
+			if (word_wrap)
+				parent->SetScrollLeft(0.f);
+			else if (parent->GetScrollLeft() < minimum_scroll_left)
 				parent->SetScrollLeft(minimum_scroll_left);
 				parent->SetScrollLeft(minimum_scroll_left);
 			else if (parent->GetScrollLeft() > cursor_position.x)
 			else if (parent->GetScrollLeft() > cursor_position.x)
 				parent->SetScrollLeft(cursor_position.x);
 				parent->SetScrollLeft(cursor_position.x);
@@ -1231,25 +1253,10 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 	Vector2f line_position = {0, top_to_baseline};
 	Vector2f line_position = {0, top_to_baseline};
 	bool last_line = false;
 	bool last_line = false;
 
 
-	auto text_align = GetElement()->GetComputedValues().text_align();
-
-	struct Segment {
-		Vector2f position;
-		int width;
-		String content;
-		bool selected;
-		int line_index;
-	};
+	float max_selection_right_edge = 0;
 
 
-	Vector<Segment> segments;
-
-	struct IMESegment {
-		Vector2f position;
-		int width;
-		int line_index;
-	};
-
-	Vector<IMESegment> ime_segments;
+	// Clear the selection background and IME composition geometry, and get the vertices and indices so the new geometry can be generated.
+	Mesh selection_composition_mesh = selection_composition_geometry.Release(Geometry::ReleaseMode::ClearMesh);
 
 
 	// Keep generating lines until all the text content is placed.
 	// Keep generating lines until all the text content is placed.
 	do
 	do
@@ -1269,44 +1276,33 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 		last_line =
 		last_line =
 			text_element->GenerateLine(line_content, line.size, line_width, line_begin, available_width - cursor_size.x, 0, false, false, false);
 			text_element->GenerateLine(line_content, line.size, line_width, line_begin, available_width - cursor_size.x, 0, false, false, false);
 
 
-		// 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'))
+		// Check if the editable length needs to be truncated to dodge a trailing endline.
+		line.editable_length = (int)line_content.size();
+		if (!line_content.empty() && line_content.back() == '\n')
+			line.editable_length -= 1;
+
+		// Include all spaces at the end of this line, if they were not included due to soft-wrapping in `GenerateLine`.
+		// This helps prevent sudden shifts when whitespace wraps down to the next line.
 		{
 		{
 			const String& text = GetValue();
 			const String& text = GetValue();
-			for (int i = 1; i >= 0; --i)
+			size_t i_space_begin = size_t(line_begin + line.editable_length);
+			size_t i_space_end = Math::Min(text.find_first_not_of(' ', i_space_begin), text.size());
+			size_t count = i_space_end - i_space_begin;
+			if (count > 0)
 			{
 			{
-				int index = line_begin + line.size + i;
-				if (index >= (int)text.size())
-					continue;
-
-				if (text[index] != ' ')
-				{
-					orphan.clear();
-					continue;
-				}
-
-				int next_index = index + 1;
-				if (!orphan.empty() || next_index >= (int)text.size() || text[next_index] != ' ')
-					orphan += ' ';
+				line_content.append(count, ' ');
+				line_width += ElementUtilities::GetStringWidth(text_element, " ") * (int)count;
+				line.editable_length += (int)count;
+				line.size += (int)count;
+				// Consume the hard wrap if we have one on this line, so that it doesn't make its own, empty line.
+				if (text[i_space_end] == '\n')
+					line.size += 1;
+				// If the spaces extend all the way to the end, we have consumed all the lines.
+				if (i_space_end == text.size())
+					last_line = true;
 			}
 			}
 		}
 		}
 
 
-		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
 		// Now that we have the string of characters appearing on the new line, we split it into
 		// three parts; the unselected text appearing before any selected text on the line, the
 		// three parts; the unselected text appearing before any selected text on the line, the
 		// selected text on the line, and any unselected text after the selection.
 		// selected text on the line, and any unselected text after the selection.
@@ -1318,7 +1314,7 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 		if (!pre_selection.empty())
 		if (!pre_selection.empty())
 		{
 		{
 			const int width = 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()});
+			text_element->AddLine(line_position + Vector2f{GetAlignmentSpecificTextOffset(line), 0}, pre_selection);
 			line_position.x += width;
 			line_position.x += width;
 		}
 		}
 
 
@@ -1336,19 +1332,22 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 			return float(width_kerning - width_no_kerning);
 			return float(width_kerning - width_no_kerning);
 		};
 		};
 
 
-		// Check if the editable length needs to be truncated to dodge a trailing endline.
-		line.editable_length = (int)line_content.size();
-		if (!line_content.empty() && line_content.back() == '\n')
-			line.editable_length -= 1;
-
 		// If there is any selected text on this line, place it in the selected text element and
 		// If there is any selected text on this line, place it in the selected text element and
 		// generate the geometry for its background.
 		// generate the geometry for its background.
 		if (!selection.empty())
 		if (!selection.empty())
 		{
 		{
 			line_position.x += GetKerningBetween(pre_selection, selection);
 			line_position.x += GetKerningBetween(pre_selection, selection);
+
 			const int selection_width = ElementUtilities::GetStringWidth(selected_text_element, selection);
 			const int selection_width = ElementUtilities::GetStringWidth(selected_text_element, selection);
-			segments.push_back({line_position, selection_width, selection, true, (int)lines.size()});
+			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_font_width : 0)), line_height};
+			const Vector2f aligned_position = line_position + Vector2f{GetAlignmentSpecificTextOffset(line), 0};
 
 
+			MeshUtilities::GenerateQuad(selection_composition_mesh, aligned_position - Vector2f(0, top_to_baseline), selection_size,
+				selection_colour);
+			selected_text_element->AddLine(aligned_position, selection);
+
+			max_selection_right_edge = Math::Max(max_selection_right_edge, aligned_position.x + selection_size.x);
 			line_position.x += selection_width;
 			line_position.x += selection_width;
 		}
 		}
 
 
@@ -1357,8 +1356,7 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 		if (!post_selection.empty())
 		if (!post_selection.empty())
 		{
 		{
 			line_position.x += GetKerningBetween(selection, post_selection);
 			line_position.x += GetKerningBetween(selection, post_selection);
-			const int width = ElementUtilities::GetStringWidth(text_element, post_selection);
-			segments.push_back({line_position, width, post_selection, false, (int)lines.size()});
+			text_element->AddLine(line_position + Vector2f{GetAlignmentSpecificTextOffset(line), 0}, post_selection);
 		}
 		}
 
 
 		// We fetch the IME composition on the new line to highlight it.
 		// We fetch the IME composition on the new line to highlight it.
@@ -1368,9 +1366,16 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 		// If there is any IME composition string on the line, create a segment for its underline.
 		// If there is any IME composition string on the line, create a segment for its underline.
 		if (!ime_composition.empty())
 		if (!ime_composition.empty())
 		{
 		{
+			const bool composition_contains_endline = (ime_composition_end_index > line_begin + line.editable_length);
 			const int composition_width = ElementUtilities::GetStringWidth(text_element, ime_composition);
 			const int composition_width = ElementUtilities::GetStringWidth(text_element, ime_composition);
-			const Vector2f composition_position(float(ElementUtilities::GetStringWidth(text_element, ime_pre_composition)), line_position.y);
-			ime_segments.push_back({composition_position, composition_width, (int)lines.size()});
+			const Vector2f composition_position = {
+				float(ElementUtilities::GetStringWidth(text_element, ime_pre_composition)) + GetAlignmentSpecificTextOffset(line),
+				line_position.y - top_to_baseline + line_height - COMPOSITION_UNDERLINE_WIDTH,
+			};
+			Vector2f line_size = {float(composition_width + (composition_contains_endline ? endline_font_width : 0)), COMPOSITION_UNDERLINE_WIDTH};
+
+			MeshUtilities::GenerateLine(selection_composition_mesh, composition_position, line_size,
+				parent->GetComputedValues().color().ToPremultiplied());
 		}
 		}
 
 
 		// Update variables for the next line.
 		// Update variables for the next line.
@@ -1390,54 +1395,22 @@ Vector2f WidgetTextInput::FormatText(float height_constraint)
 	// Clamp the cursor to a valid range.
 	// Clamp the cursor to a valid range.
 	absolute_cursor_index = Math::Min(absolute_cursor_index, (int)GetValue().size());
 	absolute_cursor_index = Math::Min(absolute_cursor_index, (int)GetValue().size());
 
 
-	// Clear the selection background geometry, and get the vertices and indices so the new geometry can be generated.
-	Mesh selection_mesh = selection_geometry.Release(Geometry::ReleaseMode::ClearMesh);
-
-	// Transform segments according to text alignment
-	for (Segment& it : segments)
+	selection_composition_geometry = parent->GetRenderManager()->MakeGeometry(std::move(selection_composition_mesh));
+
+	// Overflow is automatically caught by any text overflowing the content area. However, sometimes it is possible that
+	// the selection box extends beyond the text and outside the content area. This can even overflow the element
+	// itself. In particular, when the selection includes newlines near the right edge. We don't want the selection box
+	// to take part in the scrollable region of the element, which would be one way to ensure that it is always clipped.
+	// Instead, we here detect such possible overflow manually and force the element to clip. This will clip any parts
+	// of the selection box that is overflowing. Maybe in the future we'll have a better way to specify ink overflow and
+	// have that automatically clipped.
+	const bool new_ink_overflow = (max_selection_right_edge > available_width + parent->GetBox().GetEdge(BoxArea::Padding, BoxEdge::Right));
+	if (new_ink_overflow != ink_overflow)
 	{
 	{
-		const auto& 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_font_width : 0)), line_height);
-
-			MeshUtilities::GenerateQuad(selection_mesh, it.position - Vector2f(0, top_to_baseline), selection_size, selection_colour);
-
-			selected_text_element->AddLine(it.position, it.content);
-		}
-		else
-			text_element->AddLine(it.position, it.content);
+		ink_overflow = new_ink_overflow;
+		parent->SetProperty(PropertyId::Clip, Property(ink_overflow ? Style::Clip::Type::Always : Style::Clip::Type::Auto));
 	}
 	}
 
 
-	selection_geometry = parent->GetRenderManager()->MakeGeometry(std::move(selection_mesh));
-
-	// Clear the IME composition geometry, and get the vertices and indices so the new geometry can be generated.
-	Mesh ime_composition_mesh = ime_composition_geometry.Release(Geometry::ReleaseMode::ClearMesh);
-
-	// Transform IME segments according to text alignment.
-	for (auto& it : ime_segments)
-	{
-		const auto& 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;
-		it.position.y += font_metrics.underline_position;
-
-		const bool composition_contains_endline = (ime_composition_end_index > line_begin + lines[it.line_index].editable_length);
-		const Vector2f line_size(float(it.width + (composition_contains_endline ? endline_font_width : 0)), font_metrics.underline_thickness);
-
-		MeshUtilities::GenerateLine(ime_composition_mesh, it.position, line_size, parent->GetComputedValues().color().ToPremultiplied());
-	}
-
-	ime_composition_geometry = parent->GetRenderManager()->MakeGeometry(std::move(ime_composition_mesh));
-
 	return content_area;
 	return content_area;
 }
 }
 
 
@@ -1473,12 +1446,18 @@ void WidgetTextInput::UpdateCursorPosition(bool update_ideal_cursor_position)
 	GetRelativeCursorIndices(cursor_line_index, cursor_character_index);
 	GetRelativeCursorIndices(cursor_line_index, cursor_character_index);
 
 
 	const auto& line = lines[cursor_line_index];
 	const auto& line = lines[cursor_line_index];
-	const char* p_begin = GetValue().data() + line.value_offset;
+	const int string_width_pre_cursor =
+		ElementUtilities::GetStringWidth(text_element, String(StringView(GetValue(), line.value_offset, cursor_character_index)));
+	const float alignment_offset = GetAlignmentSpecificTextOffset(line);
 
 
-	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 = {
+		(float)string_width_pre_cursor + alignment_offset,
+		-1.f + (float)cursor_line_index * text_element->GetLineHeight(),
+	};
 
 
-	cursor_position.x += GetAlignmentSpecificTextOffset(p_begin, cursor_line_index);
+	const bool word_wrap = parent->GetComputedValues().white_space() == Style::WhiteSpace::Prewrap;
+	if (word_wrap)
+		cursor_position.x = Math::Min(cursor_position.x, GetAvailableWidth() - cursor_size.x);
 
 
 	if (update_ideal_cursor_position)
 	if (update_ideal_cursor_position)
 		ideal_cursor_position = cursor_position.x;
 		ideal_cursor_position = cursor_position.x;

+ 15 - 16
Source/Core/Elements/WidgetTextInput.h

@@ -106,8 +106,6 @@ public:
 protected:
 protected:
 	enum class CursorMovement { Begin = -4, BeginLine = -3, PreviousWord = -2, Left = -1, Right = 1, NextWord = 2, EndLine = 3, End = 4 };
 	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
 	/// Processes the "keydown" and "textinput" event to write to the input field, and the "focus" and
 	/// "blur" to set the state of the cursor.
 	/// "blur" to set the state of the cursor.
 	void ProcessEvent(Event& event) override;
 	void ProcessEvent(Event& event) override;
@@ -142,6 +140,15 @@ protected:
 	void DispatchChangeEvent(bool linebreak = false);
 	void DispatchChangeEvent(bool linebreak = false);
 
 
 private:
 private:
+	struct Line {
+		// Offset into the text field's value.
+		int value_offset;
+		// The size of the contents of the line (including the trailing endline, if that terminated the line).
+		int size;
+		// The length of the editable characters on the line (excluding any trailing endline).
+		int editable_length;
+	};
+
 	/// Returns the displayed value of the text field.
 	/// Returns the displayed value of the text field.
 	/// @note For password fields this would only return the displayed asterisks '****', while the attribute value below contains the underlying text.
 	/// @note For password fields this would only return the displayed asterisks '****', while the attribute value below contains the underlying text.
 	const String& GetValue() const;
 	const String& GetValue() const;
@@ -176,7 +183,6 @@ private:
 	/// Calculates the character index along a line under a specific horizontal position.
 	/// Calculates the character index along a line under a specific horizontal position.
 	/// @param[in] line_index The line to query.
 	/// @param[in] line_index The line to query.
 	/// @param[in] position The position to query.
 	/// @param[in] position The position to query.
-	/// @param[out] on_right_side True if position is on the right side of the returned character, else left side.
 	/// @return The index of the character under the mouse cursor.
 	/// @return The index of the character under the mouse cursor.
 	int CalculateCharacterIndex(int line_index, float position);
 	int CalculateCharacterIndex(int line_index, float position);
 
 
@@ -224,20 +230,14 @@ private:
 	/// @param[in] line_begin The absolute index at the beginning of the line.
 	/// @param[in] line_begin The absolute index at the beginning of the line.
 	void GetLineIMEComposition(String& pre_composition, String& ime_composition, const String& line, int line_begin) const;
 	void GetLineIMEComposition(String& pre_composition, String& ime_composition, const String& line, int line_begin) const;
 
 
+	/// Returns the offset that aligns the contents of the line according to the 'text-align' property.
+	float GetAlignmentSpecificTextOffset(const Line& line) const;
+
 	/// Returns the width available for the text contents without overflowing, that is, the content area subtracted by any scrollbar.
 	/// Returns the width available for the text contents without overflowing, that is, the content area subtracted by any scrollbar.
 	float GetAvailableWidth() const;
 	float GetAvailableWidth() const;
 	/// Returns the height available for the text contents without overflowing, that is, the content area subtracted by any scrollbar.
 	/// Returns the height available for the text contents without overflowing, that is, the content area subtracted by any scrollbar.
 	float GetAvailableHeight() const;
 	float GetAvailableHeight() const;
 
 
-	struct Line {
-		// Offset into the text field's value.
-		int value_offset;
-		// The size of the contents of the line (including the trailing endline, if that terminated the line).
-		int size;
-		// The length of the editable characters on the line (excluding any trailing endline).
-		int editable_length;
-	};
-
 	ElementFormControl* parent;
 	ElementFormControl* parent;
 
 
 	ElementText* text_element;
 	ElementText* text_element;
@@ -272,15 +272,12 @@ private:
 	// The colour of the background of selected text.
 	// The colour of the background of selected text.
 	ColourbPremultiplied selection_colour;
 	ColourbPremultiplied selection_colour;
 	// The selection background.
 	// The selection background.
-	Geometry selection_geometry;
+	Geometry selection_composition_geometry;
 
 
 	// IME composition range. The start and end indices are in absolute coordinates.
 	// IME composition range. The start and end indices are in absolute coordinates.
 	int ime_composition_begin_index;
 	int ime_composition_begin_index;
 	int ime_composition_end_index;
 	int ime_composition_end_index;
 
 
-	// The IME composition text highlighting.
-	Geometry ime_composition_geometry;
-
 	// The IME context for this widget.
 	// The IME context for this widget.
 	UniquePtr<WidgetTextInputContext> text_input_context;
 	UniquePtr<WidgetTextInputContext> text_input_context;
 
 
@@ -292,6 +289,8 @@ private:
 	/// @param[in] active True if need activate keyboard, false if need deactivate.
 	/// @param[in] active True if need activate keyboard, false if need deactivate.
 	void SetKeyboardActive(bool active);
 	void SetKeyboardActive(bool active);
 
 
+	bool ink_overflow;
+
 	double last_update_time;
 	double last_update_time;
 
 
 	// The cursor geometry.
 	// The cursor geometry.