Selaa lähdekoodia

Merge branch 'textarea'

Michael Ragazzon 3 vuotta sitten
vanhempi
sitoutus
c20b2e71a1

+ 1 - 1
Samples/basic/demo/data/demo.rml

@@ -855,7 +855,7 @@ progress {
 		</div>
 		</div>
 		<h2>Message</h2>
 		<h2>Message</h2>
 		<div>
 		<div>
-			<textarea cols="25" rows="4" wrap="nowrap" name="message">😍 Hello 🌐 World! 😎</textarea>
+			<textarea cols="25" rows="4" name="message">😍 Hello 🌐 World! 😎</textarea>
 		</div>
 		</div>
 		<div style="margin-bottom: 15dp;">
 		<div style="margin-bottom: 15dp;">
 			<input type="submit">Submit</input>
 			<input type="submit">Submit</input>

+ 7 - 6
Source/Core/Elements/ElementFormControlTextArea.cpp

@@ -164,15 +164,16 @@ void ElementFormControlTextArea::OnAttributeChange(const ElementAttributes& chan
 			SetProperty(PropertyId::WhiteSpace, Property(Style::WhiteSpace::Pre));
 			SetProperty(PropertyId::WhiteSpace, Property(Style::WhiteSpace::Pre));
 	}
 	}
 
 
-	if (changed_attributes.find("rows") != changed_attributes.end() ||
-			 changed_attributes.find("cols") != changed_attributes.end())
+	if (changed_attributes.find("rows") != changed_attributes.end() || changed_attributes.find("cols") != changed_attributes.end())
 		DirtyLayout();
 		DirtyLayout();
 
 
-	if (changed_attributes.find("maxlength") != changed_attributes.end())
-		widget->SetMaxLength(GetMaxLength());
+	auto it = changed_attributes.find("maxlength");
+	if (it != changed_attributes.end())
+		widget->SetMaxLength(it->second.Get(-1));
 
 
-	if (changed_attributes.find("value") != changed_attributes.end())
-		widget->SetValue(GetValue());
+	it = changed_attributes.find("value");
+	if (it != changed_attributes.end())
+		widget->SetValue(it->second.Get<String>());
 }
 }
 
 
 // Called when properties on the control are changed.
 // Called when properties on the control are changed.

+ 236 - 221
Source/Core/Elements/WidgetTextInput.cpp

@@ -48,8 +48,16 @@ namespace Rml {
 
 
 static constexpr float CURSOR_BLINK_TIME = 0.7f;
 static constexpr float CURSOR_BLINK_TIME = 0.7f;
 
 
-static bool IsWordCharacter(char c) {
-	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || ((unsigned char)c >= 128);
+enum class CharacterClass { Word, Punctuation, Newline, Whitespace, Undefined };
+static CharacterClass GetCharacterClass(char c)
+{
+	if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || ((unsigned char)c >= 128))
+		return CharacterClass::Word;
+	if ((c >= '!' && c <= '/') || (c >= ':' && c <= '@') || (c >= '[' && c <= '`') || (c >= '{' && c <= '~'))
+		return CharacterClass::Punctuation;
+	if (c == '\n')
+		return CharacterClass::Newline;
+	return CharacterClass::Whitespace;
 }
 }
 
 
 WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) : internal_dimensions(0, 0), scroll_offset(0, 0), selection_geometry(_parent), cursor_position(0, 0), cursor_size(0, 0), cursor_geometry(_parent)
 WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) : internal_dimensions(0, 0), scroll_offset(0, 0), selection_geometry(_parent), cursor_position(0, 0), cursor_size(0, 0), cursor_geometry(_parent)
@@ -61,6 +69,7 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) : internal_dimensi
 	parent->SetProperty(PropertyId::OverflowX, Property(Style::Overflow::Hidden));
 	parent->SetProperty(PropertyId::OverflowX, Property(Style::Overflow::Hidden));
 	parent->SetProperty(PropertyId::OverflowY, Property(Style::Overflow::Hidden));
 	parent->SetProperty(PropertyId::OverflowY, Property(Style::Overflow::Hidden));
 	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->SetClientArea(Box::CONTENT);
 	parent->SetClientArea(Box::CONTENT);
 
 
 	parent->AddEventListener(EventId::Keydown, this, true);
 	parent->AddEventListener(EventId::Keydown, this, true);
@@ -93,11 +102,9 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) : internal_dimensi
 		parent->AppendChild(std::move(unique_selection), false);
 		parent->AppendChild(std::move(unique_selection), false);
 	}
 	}
 
 
-	edit_index = 0;
 	absolute_cursor_index = 0;
 	absolute_cursor_index = 0;
-	cursor_line_index = 0;
-	cursor_character_index = 0;
-	cursor_on_right_side_of_character = true;
+	cursor_wrap_down = false;
+	ideal_cursor_position_to_the_right_of_cursor = true;
 	cancel_next_drag = false;
 	cancel_next_drag = false;
 
 
 	ideal_cursor_position = 0;
 	ideal_cursor_position = 0;
@@ -129,15 +136,28 @@ WidgetTextInput::~WidgetTextInput()
 	parent->RemoveChild(selection_element);
 	parent->RemoveChild(selection_element);
 }
 }
 
 
-// Sets the value of the text field.
-void WidgetTextInput::SetValue(const String& value)
+void WidgetTextInput::SetValue(String value)
 {
 {
-	text_element->SetText(value);
-	FormatElement();
+	const size_t initial_size = value.size();
+	SanitizeValue(value);
 
 
-	UpdateRelativeCursor();
+	if (initial_size != value.size())
+	{
+		parent->SetAttribute("value", value);
+		DispatchChangeEvent();
+	}
+	else
+	{
+		TransformValue(value);
+		RMLUI_ASSERTMSG(value.size() == initial_size, "TransformValue must not change the text length.");
+
+		text_element->SetText(value);
+		FormatElement();
+	}
 }
 }
 
 
+void WidgetTextInput::TransformValue(String& /*value*/) {}
+
 // Sets the maximum length (in characters) of this text field.
 // Sets the maximum length (in characters) of this text field.
 void WidgetTextInput::SetMaxLength(int _max_length)
 void WidgetTextInput::SetMaxLength(int _max_length)
 {
 {
@@ -146,7 +166,7 @@ void WidgetTextInput::SetMaxLength(int _max_length)
 		max_length = _max_length;
 		max_length = _max_length;
 		if (max_length >= 0)
 		if (max_length >= 0)
 		{
 		{
-			String value = GetElement()->GetAttribute< String >("value", "");
+			String value = GetValue();
 
 
 			int num_characters = 0;
 			int num_characters = 0;
 			size_t i_erase = value.size();
 			size_t i_erase = value.size();
@@ -178,8 +198,7 @@ int WidgetTextInput::GetMaxLength() const
 
 
 int WidgetTextInput::GetLength() const
 int WidgetTextInput::GetLength() const
 {
 {
-	const String value = GetElement()->GetAttribute< String >("value", "");
-	size_t result = StringUtilities::LengthUTF8(value);
+	size_t result = StringUtilities::LengthUTF8(GetValue());
 	return (int)result;
 	return (int)result;
 }
 }
 
 
@@ -247,7 +266,7 @@ void WidgetTextInput::OnResize()
 		internal_dimensions = new_internal_dimensions;
 		internal_dimensions = new_internal_dimensions;
 
 
 		FormatElement();
 		FormatElement();
-		UpdateCursorPosition();
+		UpdateCursorPosition(true);
 	}
 	}
 }
 }
 
 
@@ -296,7 +315,7 @@ Element* WidgetTextInput::GetElement() const
 void WidgetTextInput::DispatchChangeEvent(bool linebreak)
 void WidgetTextInput::DispatchChangeEvent(bool linebreak)
 {
 {
 	Dictionary parameters;
 	Dictionary parameters;
-	parameters["value"] = GetElement()->GetAttribute< String >("value", "");
+	parameters["value"] = GetAttributeValue();
 	parameters["linebreak"] = Variant(linebreak);
 	parameters["linebreak"] = Variant(linebreak);
 	GetElement()->DispatchEvent(EventId::Change, parameters);
 	GetElement()->DispatchEvent(EventId::Change, parameters);
 }
 }
@@ -347,10 +366,7 @@ void WidgetTextInput::ProcessEvent(Event& event)
 		{
 		{
 			CursorMovement direction = (ctrl ? CursorMovement::PreviousWord : CursorMovement::Left);
 			CursorMovement direction = (ctrl ? CursorMovement::PreviousWord : CursorMovement::Left);
 			if (DeleteCharacters(direction))
 			if (DeleteCharacters(direction))
-			{
 				FormatElement();
 				FormatElement();
-				UpdateRelativeCursor();
-			}
 
 
 			ShowCursor(true);
 			ShowCursor(true);
 		}
 		}
@@ -361,10 +377,7 @@ void WidgetTextInput::ProcessEvent(Event& event)
 		{
 		{
 			CursorMovement direction = (ctrl ? CursorMovement::NextWord : CursorMovement::Right);
 			CursorMovement direction = (ctrl ? CursorMovement::NextWord : CursorMovement::Right);
 			if (DeleteCharacters(direction))
 			if (DeleteCharacters(direction))
-			{
 				FormatElement();
 				FormatElement();
-				UpdateRelativeCursor();
-			}
 
 
 			ShowCursor(true);
 			ShowCursor(true);
 		}
 		}
@@ -400,6 +413,7 @@ void WidgetTextInput::ProcessEvent(Event& event)
 			{
 			{
 				CopySelection();
 				CopySelection();
 				DeleteSelection();
 				DeleteSelection();
+				ShowCursor(true);
 			}
 			}
 		}
 		}
 		break;
 		break;
@@ -412,6 +426,7 @@ void WidgetTextInput::ProcessEvent(Event& event)
 				GetSystemInterface()->GetClipboardText(clipboard_text);
 				GetSystemInterface()->GetClipboardText(clipboard_text);
 
 
 				AddCharacters(clipboard_text);
 				AddCharacters(clipboard_text);
+				ShowCursor(true);
 			}
 			}
 		}
 		}
 		break;
 		break;
@@ -476,14 +491,13 @@ void WidgetTextInput::ProcessEvent(Event& event)
 			Vector2f mouse_position = Vector2f(event.GetParameter< float >("mouse_x", 0), event.GetParameter< float >("mouse_y", 0));
 			Vector2f mouse_position = Vector2f(event.GetParameter< float >("mouse_x", 0), event.GetParameter< float >("mouse_y", 0));
 			mouse_position -= text_element->GetAbsoluteOffset();
 			mouse_position -= text_element->GetAbsoluteOffset();
 
 
-			cursor_line_index = CalculateLineIndex(mouse_position.y);
-			cursor_character_index = CalculateCharacterIndex(cursor_line_index, mouse_position.x);
+			const int cursor_line_index = CalculateLineIndex(mouse_position.y);
+			const int cursor_character_index = CalculateCharacterIndex(cursor_line_index, mouse_position.x);
 
 
-			UpdateAbsoluteCursor();
-			MoveCursorToCharacterBoundaries(false);
+			SetCursorFromRelativeIndices(cursor_line_index, cursor_character_index);
 
 
-			UpdateCursorPosition();
-			ideal_cursor_position = cursor_position.x;
+			MoveCursorToCharacterBoundaries(false);
+			UpdateCursorPosition(true);
 
 
 			UpdateSelection(event == EventId::Drag || event.GetParameter< int >("shift_key", 0) > 0);
 			UpdateSelection(event == EventId::Drag || event.GetParameter< int >("shift_key", 0) > 0);
 
 
@@ -505,20 +519,12 @@ void WidgetTextInput::ProcessEvent(Event& event)
 	default:
 	default:
 		break;
 		break;
 	}
 	}
-
 }
 }
 
 
 // Adds a new character to the string at the cursor position.
 // Adds a new character to the string at the cursor position.
 bool WidgetTextInput::AddCharacters(String string)
 bool WidgetTextInput::AddCharacters(String string)
 {
 {
-	// Erase invalid characters from string
-	auto invalid_character = [this](char c) {
-		return ((unsigned char)c <= 127 && !IsCharacterValid(c));
-	};
-	string.erase(
-		std::remove_if(string.begin(), string.end(), invalid_character),
-		string.end()
-	);
+	SanitizeValue(string);
 
 
 	if (string.empty())
 	if (string.empty())
 		return false;
 		return false;
@@ -529,15 +535,15 @@ bool WidgetTextInput::AddCharacters(String string)
 	if (max_length >= 0 && GetLength() >= max_length)
 	if (max_length >= 0 && GetLength() >= max_length)
 		return false;
 		return false;
 
 
-	String value = GetElement()->GetAttribute< String >("value", "");
-	
-	value.insert(std::min<size_t>(GetCursorIndex(), value.size()), string);
+	String value = GetAttributeValue();
+	value.insert(std::min<size_t>((size_t)absolute_cursor_index, value.size()), string);
 
 
-	edit_index += (int)string.size();
+	parent->SetAttribute("value", value);
+	absolute_cursor_index += (int)string.size();
 
 
-	GetElement()->SetAttribute("value", value);
 	DispatchChangeEvent();
 	DispatchChangeEvent();
 
 
+	UpdateCursorPosition(true);
 	UpdateSelection(false);
 	UpdateSelection(false);
 
 
 	return true;
 	return true;
@@ -567,20 +573,22 @@ bool WidgetTextInput::DeleteCharacters(CursorMovement direction)
 // Copies the selection (if any) to the clipboard.
 // Copies the selection (if any) to the clipboard.
 void WidgetTextInput::CopySelection()
 void WidgetTextInput::CopySelection()
 {
 {
-	const String value = GetElement()->GetAttribute< String >("value", "");
-	const String snippet = value.substr(std::min(size_t(selection_begin_index), size_t(value.size())), selection_length);
+	const String& value = GetValue();
+	const String snippet = value.substr(std::min((size_t)selection_begin_index, (size_t)value.size()), (size_t)selection_length);
 	GetSystemInterface()->SetClipboardText(snippet);
 	GetSystemInterface()->SetClipboardText(snippet);
 }
 }
 
 
-// Returns the absolute index of the cursor.
-int WidgetTextInput::GetCursorIndex() const
-{
-	return edit_index;
-}
-
 // Moves the cursor along the current line.
 // Moves the cursor along the current line.
 void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 {
 {
+	const String& value = GetValue();
+
+	int cursor_line_index = 0, cursor_character_index = 0;
+	GetRelativeCursorIndices(cursor_line_index, cursor_character_index);
+
+	// By default the cursor wraps down when located on softbreaks. This may be overridden by setting the cursor using relative indices.
+	cursor_wrap_down = true;
+
 	// Whether to seek forward or back to align to utf8 boundaries later.
 	// Whether to seek forward or back to align to utf8 boundaries later.
 	bool seek_forward = false;
 	bool seek_forward = false;
 
 
@@ -590,31 +598,31 @@ void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 		absolute_cursor_index = 0;
 		absolute_cursor_index = 0;
 		break;
 		break;
 	case CursorMovement::BeginLine:
 	case CursorMovement::BeginLine:
-		absolute_cursor_index -= cursor_character_index;
+		SetCursorFromRelativeIndices(cursor_line_index, 0);
 		break;
 		break;
 	case CursorMovement::PreviousWord:
 	case CursorMovement::PreviousWord:
-		if (cursor_character_index <= 1)
-		{
-			absolute_cursor_index -= 1;
-		}
-		else
+	{
+		// First skip whitespace, then skip all characters of the same class as the first non-whitespace character.
+		CharacterClass skip_character_class = CharacterClass::Whitespace;
+		const char* p_rend = value.data();
+		const char* p_rbegin = p_rend + absolute_cursor_index;
+		const char* p = p_rbegin - 1;
+		for (; p > p_rend; --p)
 		{
 		{
-			bool word_character_found = false;
-			const char* p_rend = lines[cursor_line_index].content.data();
-			const char* p_rbegin = p_rend + cursor_character_index;
-			const char* p = p_rbegin - 1;
-			for (; p > p_rend; --p)
+			const CharacterClass character_class = GetCharacterClass(*p);
+			if (character_class != skip_character_class)
 			{
 			{
-				bool is_word_character = IsWordCharacter(*p);
-				if(word_character_found && !is_word_character)
+				if (skip_character_class == CharacterClass::Whitespace)
+					skip_character_class = character_class;
+				else
 					break;
 					break;
-				else if(is_word_character)
-					word_character_found = true;
 			}
 			}
-			if (p != p_rend) ++p;
-			absolute_cursor_index += int(p - p_rbegin);
 		}
 		}
-		break;
+		if (p != p_rend)
+			++p;
+		absolute_cursor_index += int(p - p_rbegin);
+	}
+	break;
 	case CursorMovement::Left:
 	case CursorMovement::Left:
 		if (!select && selection_length > 0)
 		if (!select && selection_length > 0)
 			absolute_cursor_index = selection_begin_index;
 			absolute_cursor_index = selection_begin_index;
@@ -629,29 +637,31 @@ void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 			absolute_cursor_index += 1;
 			absolute_cursor_index += 1;
 		break;
 		break;
 	case CursorMovement::NextWord:
 	case CursorMovement::NextWord:
-		if (cursor_character_index >= lines[cursor_line_index].content_length)
-		{
-			absolute_cursor_index += 1;
-		}
-		else
+	{
+		// First skip all characters of the same class as the first character, then skip any whitespace.
+		CharacterClass skip_character_class = CharacterClass::Undefined;
+		const char* p_begin = value.data() + absolute_cursor_index;
+		const char* p_end = value.data() + value.size();
+		const char* p = p_begin;
+		for (; p < p_end; ++p)
 		{
 		{
-			bool whitespace_found = false;
-			const char* p_begin = lines[cursor_line_index].content.data() + cursor_character_index;
-			const char* p_end = lines[cursor_line_index].content.data() + lines[cursor_line_index].content_length;
-			const char* p = p_begin;
-			for (; p < p_end; ++p)
+			const CharacterClass character_class = GetCharacterClass(*p);
+			if (skip_character_class == CharacterClass::Undefined)
+				skip_character_class = character_class;
+			
+			if (character_class != skip_character_class)
 			{
 			{
-				bool is_whitespace = !IsWordCharacter(*p);
-				if (whitespace_found && !is_whitespace)
+				if (character_class == CharacterClass::Whitespace)
+					skip_character_class = CharacterClass::Whitespace;
+				else
 					break;
 					break;
-				else if (is_whitespace)
-					whitespace_found = true;
 			}
 			}
-			absolute_cursor_index += int(p - p_begin);
 		}
 		}
-		break;
+		absolute_cursor_index += int(p - p_begin);
+	}
+	break;
 	case CursorMovement::EndLine:
 	case CursorMovement::EndLine:
-		absolute_cursor_index += lines[cursor_line_index].content_length - cursor_character_index;
+		SetCursorFromRelativeIndices(cursor_line_index, lines[cursor_line_index].editable_length);
 		break;
 		break;
 	case CursorMovement::End:
 	case CursorMovement::End:
 		absolute_cursor_index = INT_MAX;
 		absolute_cursor_index = INT_MAX;
@@ -659,13 +669,11 @@ void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 	default:
 	default:
 		break;
 		break;
 	}
 	}
-	
-	absolute_cursor_index = Math::Max(0, absolute_cursor_index);
 
 
-	UpdateRelativeCursor();
 	MoveCursorToCharacterBoundaries(seek_forward);
 	MoveCursorToCharacterBoundaries(seek_forward);
 
 
-	ideal_cursor_position = cursor_position.x;
+	UpdateCursorPosition(true);
+
 	UpdateSelection(select);
 	UpdateSelection(select);
 	ShowCursor(true);
 	ShowCursor(true);
 }
 }
@@ -673,6 +681,9 @@ void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 // Moves the cursor up and down the text field.
 // Moves the cursor up and down the text field.
 void WidgetTextInput::MoveCursorVertical(int distance, bool select)
 void WidgetTextInput::MoveCursorVertical(int distance, bool select)
 {
 {
+	int cursor_line_index = 0, cursor_character_index = 0;
+	GetRelativeCursorIndices(cursor_line_index, cursor_character_index);
+
 	bool update_ideal_cursor_position = false;
 	bool update_ideal_cursor_position = false;
 	cursor_line_index += distance;
 	cursor_line_index += distance;
 
 
@@ -683,69 +694,60 @@ void WidgetTextInput::MoveCursorVertical(int distance, bool select)
 
 
 		update_ideal_cursor_position = true;
 		update_ideal_cursor_position = true;
 	}
 	}
-	else if (cursor_line_index >= (int) lines.size())
+	else if (cursor_line_index >= (int)lines.size())
 	{
 	{
-		cursor_line_index = (int) lines.size() - 1;
-		cursor_character_index = (int) lines[cursor_line_index].content_length;
+		cursor_line_index = (int)lines.size() - 1;
+		cursor_character_index = (int)lines[cursor_line_index].editable_length;
 
 
 		update_ideal_cursor_position = true;
 		update_ideal_cursor_position = true;
 	}
 	}
 	else
 	else
 		cursor_character_index = CalculateCharacterIndex(cursor_line_index, ideal_cursor_position);
 		cursor_character_index = CalculateCharacterIndex(cursor_line_index, ideal_cursor_position);
 
 
-	UpdateAbsoluteCursor();
+	SetCursorFromRelativeIndices(cursor_line_index, cursor_character_index);
 
 
 	MoveCursorToCharacterBoundaries(false);
 	MoveCursorToCharacterBoundaries(false);
-
-	UpdateCursorPosition();
-
-	if (update_ideal_cursor_position)
-		ideal_cursor_position = cursor_position.x;
+	UpdateCursorPosition(update_ideal_cursor_position);
 
 
 	UpdateSelection(select);
 	UpdateSelection(select);
-
 	ShowCursor(true);
 	ShowCursor(true);
 }
 }
 
 
 void WidgetTextInput::MoveCursorToCharacterBoundaries(bool forward)
 void WidgetTextInput::MoveCursorToCharacterBoundaries(bool forward)
 {
 {
-	const char* p_line_begin = lines[cursor_line_index].content.data();
-	const char* p_line_end = p_line_begin + lines[cursor_line_index].content_length;
-	const char* p_cursor = p_line_begin + cursor_character_index;
+	const String& value = GetValue();
+	absolute_cursor_index = Math::Min(absolute_cursor_index, (int)value.size());
+
+	const char* p_begin = value.data();
+	const char* p_end = p_begin + value.size();
+	const char* p_cursor = p_begin + absolute_cursor_index;
 	const char* p = p_cursor;
 	const char* p = p_cursor;
 
 
 	if (forward)
 	if (forward)
-		p = StringUtilities::SeekForwardUTF8(p_cursor, p_line_end);
+		p = StringUtilities::SeekForwardUTF8(p_cursor, p_end);
 	else
 	else
-		p = StringUtilities::SeekBackwardUTF8(p_cursor, p_line_begin);
+		p = StringUtilities::SeekBackwardUTF8(p_cursor, p_begin);
 
 
 	if (p != p_cursor)
 	if (p != p_cursor)
-	{
 		absolute_cursor_index += int(p - p_cursor);
 		absolute_cursor_index += int(p - p_cursor);
-		UpdateRelativeCursor();
-	}
 }
 }
 
 
 void WidgetTextInput::ExpandSelection()
 void WidgetTextInput::ExpandSelection()
 {
 {
-	const char* const p_begin = lines[cursor_line_index].content.data();
-	const char* const p_end = p_begin + lines[cursor_line_index].content_length;
-	const char* const p_index = p_begin + cursor_character_index;
-
-	// If true, we are expanding word characters, if false, whitespace characters.
-	// The first character encountered defines the bool.
-	bool expanding_word = false;
-	bool expanding_word_set = false;
-
-	auto character_is_wrong_type = [&expanding_word_set, &expanding_word](const char* p) -> bool {
-		bool is_word_character = IsWordCharacter(*p);
-		if (expanding_word_set && (expanding_word != is_word_character))
+	const String& value = GetValue();
+	const char* const p_begin = value.data();
+	const char* const p_end = p_begin + value.size();
+	const char* const p_index = p_begin + absolute_cursor_index;
+
+	// The first character encountered defines the character class to expand.
+	CharacterClass expanding_character_class = CharacterClass::Undefined;
+
+	auto character_is_wrong_type = [&expanding_character_class](const char* p) -> bool {
+		const CharacterClass character_class = GetCharacterClass(*p);
+		if (expanding_character_class == CharacterClass::Undefined)
+			expanding_character_class = character_class;
+		else if (character_class != expanding_character_class)
 			return true;
 			return true;
-		if (!expanding_word_set)
-		{
-			expanding_word = is_word_character;
-			expanding_word_set = true;
-		}
 		return false;
 		return false;
 	};
 	};
 
 
@@ -767,7 +769,7 @@ void WidgetTextInput::ExpandSelection()
 	const char* p_left = p_index;
 	const char* p_left = p_index;
 	const char* p_right = p_index;
 	const char* p_right = p_index;
 
 
-	if (cursor_on_right_side_of_character)
+	if (ideal_cursor_position_to_the_right_of_cursor)
 	{
 	{
 		p_right = search_right();
 		p_right = search_right();
 		p_left = search_left();
 		p_left = search_left();
@@ -778,66 +780,79 @@ void WidgetTextInput::ExpandSelection()
 		p_right = search_right();
 		p_right = search_right();
 	}
 	}
 
 
+	cursor_wrap_down = true;
 	absolute_cursor_index -= int(p_index - p_left);
 	absolute_cursor_index -= int(p_index - p_left);
-	UpdateRelativeCursor();
 	MoveCursorToCharacterBoundaries(false);
 	MoveCursorToCharacterBoundaries(false);
 	UpdateSelection(false);
 	UpdateSelection(false);
 
 
 	absolute_cursor_index += int(p_right - p_left);
 	absolute_cursor_index += int(p_right - p_left);
-	UpdateRelativeCursor();
 	MoveCursorToCharacterBoundaries(true);
 	MoveCursorToCharacterBoundaries(true);
 	UpdateSelection(true);
 	UpdateSelection(true);
+
+	UpdateCursorPosition(true);
 }
 }
 
 
-// Updates the absolute cursor index from the relative cursor indices.
-void WidgetTextInput::UpdateAbsoluteCursor()
+const String& WidgetTextInput::GetValue() const
 {
 {
-	RMLUI_ASSERT(cursor_line_index < (int) lines.size())
-
-	absolute_cursor_index = cursor_character_index;
-	edit_index = cursor_character_index;
+	return text_element->GetText();
+}
 
 
-	for (int i = 0; i < cursor_line_index; i++)
-	{
-		absolute_cursor_index += (int)lines[i].content.size();
-		edit_index += (int)lines[i].content.size() + lines[i].extra_characters;
-	}
+String WidgetTextInput::GetAttributeValue() const
+{
+	return parent->GetAttribute("value", String());
 }
 }
 
 
-// Updates the relative cursor indices from the absolute cursor index.
-void WidgetTextInput::UpdateRelativeCursor()
+void WidgetTextInput::GetRelativeCursorIndices(int& out_cursor_line_index, int& out_cursor_character_index) const
 {
 {
-	int num_characters = 0;
-	edit_index = absolute_cursor_index;
+	int line_begin = 0;
 
 
 	for (size_t i = 0; i < lines.size(); i++)
 	for (size_t i = 0; i < lines.size(); i++)
 	{
 	{
-		if (num_characters + lines[i].content_length >= absolute_cursor_index)
-		{
-			cursor_line_index = (int) i;
-			cursor_character_index = absolute_cursor_index - num_characters;
-
-			UpdateCursorPosition();
+		// Test if the absolute index is located on this line.
+		const int cursor_relative_line_end = absolute_cursor_index - (line_begin + lines[i].editable_length);
 
 
+		if (cursor_relative_line_end <= 0)
+		{
+			// Test if the absolute index is located after the editable length. This is usually because we are located just to the right of the ending
+			// '\n' character. Then we wrap down to the beginning of the next line. If we are located at a soft break (due to word wrapping) then the
+			// cursor wrap state determines whether or not we wrap down.
+			if (cursor_relative_line_end >= (cursor_wrap_down ? 0 : 1) && (int)i + 1 < (int)lines.size())
+			{
+				out_cursor_line_index = (int)i + 1;
+				out_cursor_character_index = 0;
+			}
+			else
+			{
+				out_cursor_line_index = (int)i;
+				out_cursor_character_index = Math::Max(absolute_cursor_index - line_begin, 0);
+			}
 			return;
 			return;
 		}
 		}
 
 
-		num_characters += (int) lines[i].content.size();
-		edit_index += lines[i].extra_characters;
+		line_begin += lines[i].size;
 	}
 	}
 
 
 	// We shouldn't ever get here; this means we actually couldn't find where the absolute cursor said it was. So we'll
 	// We shouldn't ever get here; this means we actually couldn't find where the absolute cursor said it was. So we'll
-	// just set the relative cursors to the very end of the text field, and update the absolute cursor to point here.
-	cursor_line_index = (int) lines.size() - 1;
-	cursor_character_index = lines[cursor_line_index].content_length;
-	absolute_cursor_index = num_characters;
-	edit_index = num_characters;
+	// just set the relative cursors to the very end of the text field.
+	out_cursor_line_index = (int)lines.size() - 1;
+	out_cursor_character_index = lines[out_cursor_line_index].editable_length;
+}
+
+void WidgetTextInput::SetCursorFromRelativeIndices(int cursor_line_index, int cursor_character_index)
+{
+	RMLUI_ASSERT(cursor_line_index < (int)lines.size())
 
 
-	UpdateCursorPosition();
+	absolute_cursor_index = cursor_character_index;
+
+	for (int i = 0; i < cursor_line_index; i++)
+		absolute_cursor_index += lines[i].size;
+
+	// Don't wrap down if we're located at the end of the line.
+	cursor_wrap_down = !(cursor_character_index >= lines[cursor_line_index].editable_length);
 }
 }
 
 
 // Calculates the line index under a specific vertical position.
 // Calculates the line index under a specific vertical position.
-int WidgetTextInput::CalculateLineIndex(float position)
+int WidgetTextInput::CalculateLineIndex(float position) const
 {
 {
 	float line_height = parent->GetLineHeight();
 	float line_height = parent->GetLineHeight();
 	int line_index = Math::RealToInteger(position / line_height);
 	int line_index = Math::RealToInteger(position / line_height);
@@ -850,14 +865,16 @@ int WidgetTextInput::CalculateCharacterIndex(int line_index, float position)
 	int prev_offset = 0;
 	int prev_offset = 0;
 	float prev_line_width = 0;
 	float prev_line_width = 0;
 
 
-	cursor_on_right_side_of_character = true;
+	ideal_cursor_position_to_the_right_of_cursor = true;
 
 
-	for(auto it = StringIteratorU8(lines[line_index].content, 0, lines[line_index].content_length); it; )
+	const char* p_begin = GetValue().data() + lines[line_index].value_offset;
+
+	for (auto it = StringIteratorU8(p_begin, p_begin, p_begin + lines[line_index].editable_length); it;)
 	{
 	{
 		++it;
 		++it;
-		int offset = (int)it.offset();
+		const int offset = (int)it.offset();
 
 
-		float line_width = (float) ElementUtilities::GetStringWidth(text_element, lines[line_index].content.substr(0, offset));
+		const float line_width = (float)ElementUtilities::GetStringWidth(text_element, String(p_begin, (size_t)offset));
 		if (line_width > position)
 		if (line_width > position)
 		{
 		{
 			if (position - prev_line_width < line_width - position)
 			if (position - prev_line_width < line_width - position)
@@ -866,7 +883,7 @@ int WidgetTextInput::CalculateCharacterIndex(int line_index, float position)
 			}
 			}
 			else
 			else
 			{
 			{
-				cursor_on_right_side_of_character = false;
+				ideal_cursor_position_to_the_right_of_cursor = false;
 				return offset;
 				return offset;
 			}
 			}
 		}
 		}
@@ -975,8 +992,6 @@ void WidgetTextInput::FormatElement()
 // Formats the input element's text field.
 // Formats the input element's text field.
 Vector2f WidgetTextInput::FormatText()
 Vector2f WidgetTextInput::FormatText()
 {
 {
-	absolute_cursor_index = edit_index;
-
 	Vector2f content_area(0, 0);
 	Vector2f content_area(0, 0);
 
 
 	// Clear the old lines, and all the lines in the text elements.
 	// Clear the old lines, and all the lines in the text elements.
@@ -991,7 +1006,9 @@ Vector2f WidgetTextInput::FormatText()
 	Vector< int >& selection_indices = selection_geometry.GetIndices();
 	Vector< int >& selection_indices = selection_geometry.GetIndices();
 
 
 	// Determine the line-height of the text element.
 	// Determine the line-height of the text element.
-	float line_height = parent->GetLineHeight();
+	const float line_height = parent->GetLineHeight();
+	// When the selection contains endlines we expand the selection area by this width.
+	const int endline_selection_width = int(0.4f * parent->GetComputedValues().font_size());
 
 
 	int line_begin = 0;
 	int line_begin = 0;
 	Vector2f line_position(0, 0);
 	Vector2f line_position(0, 0);
@@ -1000,28 +1017,23 @@ Vector2f WidgetTextInput::FormatText()
 	// Keep generating lines until all the text content is placed.
 	// Keep generating lines until all the text content is placed.
 	do
 	do
 	{
 	{
-		Line line;
-		line.extra_characters = 0;
+		Line line = {};
+		line.value_offset = line_begin;
 		float line_width;
 		float line_width;
+		String line_content;
 
 
 		// Generate the next line.
 		// Generate the next line.
-		last_line = text_element->GenerateLine(line.content, line.content_length, line_width, line_begin, parent->GetClientWidth() - cursor_size.x, 0, false, false);
-
-		// If this line terminates in a soft-return, 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.
-		bool soft_return = false;
-		if (!last_line &&
-			(line.content.empty() ||
-			 line.content[line.content.size() - 1] != '\n'))
-		{
-			soft_return = true;
+		last_line = text_element->GenerateLine(line_content, line.size, line_width, line_begin, parent->GetClientWidth() - cursor_size.x, 0, false, false);
 
 
-			const String& text = text_element->GetText();
+		// 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.
+		if (!last_line && (line_content.empty() || line_content.back() != '\n'))
+		{
+			const String& text = GetValue();
 			String orphan;
 			String orphan;
 			for (int i = 1; i >= 0; --i)
 			for (int i = 1; i >= 0; --i)
 			{
 			{
-				int index = line_begin + line.content_length + i;
+				int index = line_begin + line.size + i;
 				if (index >= (int) text.size())
 				if (index >= (int) text.size())
 					continue;
 					continue;
 
 
@@ -1040,18 +1052,17 @@ Vector2f WidgetTextInput::FormatText()
 
 
 			if (!orphan.empty())
 			if (!orphan.empty())
 			{
 			{
-				line.content += orphan;
-				line.content_length += (int) orphan.size();
+				line_content += orphan;
+				line.size += (int)orphan.size();
 				line_width += ElementUtilities::GetStringWidth(text_element, orphan);
 				line_width += ElementUtilities::GetStringWidth(text_element, orphan);
 			}
 			}
 		}
 		}
 
 
-
 		// 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.
 		String pre_selection, selection, post_selection;
 		String pre_selection, selection, post_selection;
-		GetLineSelection(pre_selection, selection, post_selection, line.content, line_begin);
+		GetLineSelection(pre_selection, selection, post_selection, line_content, line_begin);
 
 
 		// The pre-selected text is placed, if there is any (if the selection starts on or before
 		// The pre-selected text is placed, if there is any (if the selection starts on or before
 		// the beginning of this line, then this will be empty).
 		// the beginning of this line, then this will be empty).
@@ -1074,17 +1085,26 @@ Vector2f WidgetTextInput::FormatText()
 			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);
 			selected_text_element->AddLine(line_position, selection);
 			selected_text_element->AddLine(line_position, selection);
+			
 			const int selection_width = ElementUtilities::GetStringWidth(selected_text_element, 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_vertices.resize(selection_vertices.size() + 4);
 			selection_indices.resize(selection_indices.size() + 6);
 			selection_indices.resize(selection_indices.size() + 6);
-			GeometryUtilities::GenerateQuad(&selection_vertices[selection_vertices.size() - 4], &selection_indices[selection_indices.size() - 6], line_position, Vector2f((float)selection_width, line_height), selection_colour, (int)selection_vertices.size() - 4);
+			GeometryUtilities::GenerateQuad(&selection_vertices[selection_vertices.size() - 4], &selection_indices[selection_indices.size() - 6],
+				line_position, selection_size, selection_colour, (int)selection_vertices.size() - 4);
 
 
 			line_position.x += selection_width;
 			line_position.x += selection_width;
 		}
 		}
@@ -1097,36 +1117,23 @@ Vector2f WidgetTextInput::FormatText()
 			text_element->AddLine(line_position, post_selection);
 			text_element->AddLine(line_position, post_selection);
 		}
 		}
 
 
-
 		// Update variables for the next line.
 		// Update variables for the next line.
-		line_begin += line.content_length;
+		line_begin += line.size;
 		line_position.x = 0;
 		line_position.x = 0;
 		line_position.y += line_height;
 		line_position.y += line_height;
 
 
-		// Grow the content area width-wise if this line is the longest so far, and push the
-		// height out.
+		// Grow the content area width-wise if this line is the longest so far, and push the height out.
 		content_area.x = Math::Max(content_area.x, line_width + cursor_size.x);
 		content_area.x = Math::Max(content_area.x, line_width + cursor_size.x);
 		content_area.y = line_position.y;
 		content_area.y = line_position.y;
 
 
-		// Push a trailing '\r' token onto the back to indicate a soft return if necessary.
-		if (soft_return)
-		{
-			line.content += '\r';
-			line.extra_characters -= 1;
-
-			if (edit_index >= line_begin)
-				absolute_cursor_index += 1;
-		}
-
-		// Push the new line into our array of lines, but first check if its content length needs to be truncated to
-		// dodge a trailing endline.
-		if (!line.content.empty() &&
-			line.content[line.content.size() - 1] == '\n')
-			line.content_length -= 1;
-		lines.push_back(line);
+		// Finally, push the new line into our array of lines.
+		lines.push_back(std::move(line));
 	}
 	}
 	while (!last_line);
 	while (!last_line);
 
 
+	// Clamp the cursor to a valid range.
+	absolute_cursor_index = Math::Min(absolute_cursor_index, (int)GetValue().size());
+
 	return content_area;
 	return content_area;
 }
 }
 
 
@@ -1156,13 +1163,19 @@ void WidgetTextInput::GenerateCursor()
 	GeometryUtilities::GenerateQuad(&vertices[0], &indices[0], Vector2f(0, 0), cursor_size, color);
 	GeometryUtilities::GenerateQuad(&vertices[0], &indices[0], Vector2f(0, 0), cursor_size, color);
 }
 }
 
 
-void WidgetTextInput::UpdateCursorPosition()
+void WidgetTextInput::UpdateCursorPosition(bool update_ideal_cursor_position)
 {
 {
 	if (text_element->GetFontFaceHandle() == 0)
 	if (text_element->GetFontFaceHandle() == 0)
 		return;
 		return;
 
 
-	cursor_position.x = (float) ElementUtilities::GetStringWidth(text_element, lines[cursor_line_index].content.substr(0, cursor_character_index));
+	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));
 	cursor_position.y = -1.f + (float)cursor_line_index * text_element->GetLineHeight();
 	cursor_position.y = -1.f + (float)cursor_line_index * text_element->GetLineHeight();
+
+	if (update_ideal_cursor_position)
+		ideal_cursor_position = cursor_position.x;
 }
 }
 
 
 // Expand the text selection to the position of the cursor.
 // Expand the text selection to the position of the cursor.
@@ -1170,7 +1183,7 @@ void WidgetTextInput::UpdateSelection(bool selecting)
 {
 {
 	if (!selecting)
 	if (!selecting)
 	{
 	{
-		selection_anchor_index = edit_index;
+		selection_anchor_index = absolute_cursor_index;
 		ClearSelection();
 		ClearSelection();
 	}
 	}
 	else
 	else
@@ -1178,14 +1191,14 @@ void WidgetTextInput::UpdateSelection(bool selecting)
 		int new_begin_index;
 		int new_begin_index;
 		int new_end_index;
 		int new_end_index;
 
 
-		if (edit_index > selection_anchor_index)
+		if (absolute_cursor_index > selection_anchor_index)
 		{
 		{
 			new_begin_index = selection_anchor_index;
 			new_begin_index = selection_anchor_index;
-			new_end_index = edit_index;
+			new_end_index = absolute_cursor_index;
 		}
 		}
 		else
 		else
 		{
 		{
-			new_begin_index = edit_index;
+			new_begin_index = absolute_cursor_index;
 			new_end_index = selection_anchor_index;
 			new_end_index = selection_anchor_index;
 		}
 		}
 
 
@@ -1215,14 +1228,16 @@ void WidgetTextInput::DeleteSelection()
 {
 {
 	if (selection_length > 0)
 	if (selection_length > 0)
 	{
 	{
-		const String& value = GetElement()->GetAttribute< String >("value", "");
-
-		String new_value = value.substr(0, selection_begin_index) + value.substr(std::min(size_t(selection_begin_index + selection_length), size_t(value.size())));
+		String new_value = GetAttributeValue();
+		const size_t selection_end = std::min(size_t(selection_begin_index + selection_length), (size_t)new_value.size()) ;
+		
+		new_value = new_value.substr(0, selection_begin_index) + new_value.substr(selection_end);
 		GetElement()->SetAttribute("value", new_value);
 		GetElement()->SetAttribute("value", new_value);
 
 
 		// Move the cursor to the beginning of the old selection.
 		// Move the cursor to the beginning of the old selection.
 		absolute_cursor_index = selection_begin_index;
 		absolute_cursor_index = selection_begin_index;
-		UpdateRelativeCursor();
+
+		UpdateCursorPosition(true);
 
 
 		// Erase our record of the selection.
 		// Erase our record of the selection.
 		ClearSelection();
 		ClearSelection();
@@ -1230,24 +1245,24 @@ void WidgetTextInput::DeleteSelection()
 }
 }
 
 
 // Split one line of text into three parts, based on the current selection.
 // Split one line of text into three parts, based on the current selection.
-void WidgetTextInput::GetLineSelection(String& pre_selection, String& selection, String& post_selection, const String& line, int line_begin)
+void WidgetTextInput::GetLineSelection(String& pre_selection, String& selection, String& post_selection, const String& line, int line_begin) const
 {
 {
+	const int selection_end = selection_begin_index + selection_length;
+
 	// Check if we have any selection at all, and if so if the selection is on this line.
 	// Check if we have any selection at all, and if so if the selection is on this line.
-	if (selection_length <= 0 ||
-		selection_begin_index + selection_length < line_begin ||
-		selection_begin_index > line_begin + (int) line.size())
+	if (selection_length <= 0 || selection_end < line_begin || selection_begin_index > line_begin + (int)line.size())
 	{
 	{
 		pre_selection = line;
 		pre_selection = line;
 		return;
 		return;
 	}
 	}
 
 
-	int line_length = (int)line.size();
+	const int line_length = (int)line.size();
 	using namespace Math;
 	using namespace Math;
 
 
 	// Split the line up into its three parts, depending on the size and placement of the selection.
 	// Split the line up into its three parts, depending on the size and placement of the selection.
 	pre_selection = line.substr(0, Max(0, selection_begin_index - line_begin));
 	pre_selection = line.substr(0, Max(0, selection_begin_index - line_begin));
 	selection = line.substr(Clamp(selection_begin_index - line_begin, 0, line_length), Max(0, selection_length + Min(0, selection_begin_index - line_begin)));
 	selection = line.substr(Clamp(selection_begin_index - line_begin, 0, line_length), Max(0, selection_length + Min(0, selection_begin_index - line_begin)));
-	post_selection = line.substr(Clamp(selection_begin_index + selection_length - line_begin, 0, line_length));
+	post_selection = line.substr(Clamp(selection_end - line_begin, 0, line_length));
 }
 }
 
 
 void WidgetTextInput::SetKeyboardActive(bool active)
 void WidgetTextInput::SetKeyboardActive(bool active)

+ 35 - 30
Source/Core/Elements/WidgetTextInput.h

@@ -52,7 +52,8 @@ public:
 
 
 	/// Sets the value of the text field.
 	/// Sets the value of the text field.
 	/// @param[in] value The new value to set on the text field.
 	/// @param[in] value The new value to set on the text field.
-	virtual void SetValue(const String& value);
+	/// @note The value will be sanitized and synchronized with the element's value attribute.
+	void SetValue(String value);
 
 
 	/// Sets the maximum length (in characters) of this text field.
 	/// Sets the maximum length (in characters) of this text field.
 	/// @param[in] max_length The new maximum length of the text field. A number lower than zero will mean infinite characters.
 	/// @param[in] max_length The new maximum length of the text field. A number lower than zero will mean infinite characters.
@@ -97,16 +98,15 @@ protected:
 	/// @param[in] direction Movement of cursor for deletion.
 	/// @param[in] direction Movement of cursor for deletion.
 	/// @return True if a character was deleted, false otherwise.
 	/// @return True if a character was deleted, false otherwise.
 	bool DeleteCharacters(CursorMovement direction);
 	bool DeleteCharacters(CursorMovement direction);
-	/// Returns true if the given character is permitted in the input field, false if not.
-	/// @param[in] character The character to validate.
-	/// @return True if the character is allowed, false if not.
-	virtual bool IsCharacterValid(char character) = 0;
+
+	/// Removes any invalid characters from the string.
+	virtual void SanitizeValue(String& value) = 0;
+	/// Transforms the displayed value of the text box, typically used for password fields.
+	/// @note Only use this for transforming characters, do not modify the length of the string.
+	virtual void TransformValue(String& value);
 	/// Called when the user pressed enter.
 	/// Called when the user pressed enter.
 	virtual void LineBreak() = 0;
 	virtual void LineBreak() = 0;
 
 
-	/// Returns the absolute index of the cursor.
-	int GetCursorIndex() const;
-
 	/// Gets the parent element containing the widget.
 	/// Gets the parent element containing the widget.
 	Element* GetElement() const;
 	Element* GetElement() const;
 
 
@@ -114,7 +114,12 @@ protected:
 	void DispatchChangeEvent(bool linebreak = false);
 	void DispatchChangeEvent(bool linebreak = false);
 
 
 private:
 private:
-	
+	/// 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.
+	const String& GetValue() const;
+	/// Returns the underlying text from the element's value attribute.
+	String GetAttributeValue() const;
+
 	/// Moves the cursor along the current line.
 	/// Moves the cursor along the current line.
 	/// @param[in] movement Cursor movement operation.
 	/// @param[in] movement Cursor movement operation.
 	/// @param[in] select True if the movement will also move the selection cursor, false if not.
 	/// @param[in] select True if the movement will also move the selection cursor, false if not.
@@ -129,15 +134,15 @@ private:
 	// Expands the cursor, selecting the current word or nearby whitespace.
 	// Expands the cursor, selecting the current word or nearby whitespace.
 	void ExpandSelection();
 	void ExpandSelection();
 
 
-	/// Updates the absolute cursor index from the relative cursor indices.
-	void UpdateAbsoluteCursor();
-	/// Updates the relative cursor indices from the absolute cursor index.
-	void UpdateRelativeCursor();
+	/// Returns the relative indices from the current absolute index.
+	void GetRelativeCursorIndices(int& out_cursor_line_index, int& out_cursor_character_index) const;
+	/// Sets the absolute cursor index from the given relative indices.
+	void SetCursorFromRelativeIndices(int line_index, int character_index);
 
 
 	/// Calculates the line index under a specific vertical position.
 	/// Calculates the line index under a specific vertical position.
 	/// @param[in] position The position to query.
 	/// @param[in] position The position to query.
 	/// @return The index of the line under the mouse cursor.
 	/// @return The index of the line under the mouse cursor.
-	int CalculateLineIndex(float position);
+	int CalculateLineIndex(float position) const;
 	/// 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.
@@ -157,7 +162,8 @@ private:
 	Vector2f FormatText();
 	Vector2f FormatText();
 
 
 	/// Updates the position to render the cursor.
 	/// Updates the position to render the cursor.
-	void UpdateCursorPosition();
+	/// @param[in] update_ideal_cursor_position Generally should be true on horizontal movement and false on vertical movement.
+	void UpdateCursorPosition(bool update_ideal_cursor_position);
 
 
 	/// Expand or shrink the text selection to the position of the cursor.
 	/// Expand or shrink the text selection to the position of the cursor.
 	/// @param[in] selecting True if the new position of the cursor should expand / contract the selection area, false if it should only set the anchor for future selections.
 	/// @param[in] selecting True if the new position of the cursor should expand / contract the selection area, false if it should only set the anchor for future selections.
@@ -175,18 +181,16 @@ private:
 	/// @param[out] post_selection The section of unselected text after any selected text on the line. If there is no selection on the line, then this will be empty.
 	/// @param[out] post_selection The section of unselected text after any selected text on the line. If there is no selection on the line, then this will be empty.
 	/// @param[in] line The text making up the line.
 	/// @param[in] line The text making up the line.
 	/// @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 GetLineSelection(String& pre_selection, String& selection, String& post_selection, const String& line, int line_begin);
+	void GetLineSelection(String& pre_selection, String& selection, String& post_selection, const String& line, int line_begin) const;
 
 
 	struct Line
 	struct Line
 	{
 	{
-		// The contents of the line (including the trailing endline, if that terminated the line).
-		String content;
+		// 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).
 		// The length of the editable characters on the line (excluding any trailing endline).
-		int content_length;
-
-		// The number of extra characters at the end of the content that are not present in the actual value; in the
-		// case of a soft return, this may be negative.
-		int extra_characters;
+		int editable_length;
 	};
 	};
 
 
 	ElementFormControl* parent;
 	ElementFormControl* parent;
@@ -196,20 +200,21 @@ private:
 	Vector2f internal_dimensions;
 	Vector2f internal_dimensions;
 	Vector2f scroll_offset;
 	Vector2f scroll_offset;
 
 
-	typedef Vector< Line > LineList;
+	using LineList = Vector<Line>;
 	LineList lines;
 	LineList lines;
 
 
 	// Length in number of characters.
 	// Length in number of characters.
 	int max_length;
 	int max_length;
 
 
-	// Indices in bytes: Should always be moved along UTF-8 start bytes.
-	int edit_index;
-	
+	// -- All indices are in bytes: Should always be moved along UTF-8 start bytes. --
+
+	// Absolute cursor index. Byte index into the text field's value.
 	int absolute_cursor_index;
 	int absolute_cursor_index;
-	int cursor_line_index;
-	int cursor_character_index;
+	// When the cursor is located at the very end of a word-wrapped line there are two valid positions for the same absolute index: at the end of the
+	// line and at the beginning of the next line. This state determines which of these lines the cursor is placed on visually.
+	bool cursor_wrap_down;
 
 
-	bool cursor_on_right_side_of_character;
+	bool ideal_cursor_position_to_the_right_of_cursor;
 	bool cancel_next_drag;
 	bool cancel_next_drag;
 
 
 	// Selection. The start and end indices of the selection are in absolute coordinates.
 	// Selection. The start and end indices of the selection are in absolute coordinates.

+ 7 - 13
Source/Core/Elements/WidgetTextInputMultiLine.cpp

@@ -15,7 +15,7 @@
  *
  *
  * The above copyright notice and this permission notice shall be included in
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  * all copies or substantial portions of the Software.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -29,26 +29,20 @@
 #include "WidgetTextInputMultiLine.h"
 #include "WidgetTextInputMultiLine.h"
 #include "../../../Include/RmlUi/Core/Dictionary.h"
 #include "../../../Include/RmlUi/Core/Dictionary.h"
 #include "../../../Include/RmlUi/Core/ElementText.h"
 #include "../../../Include/RmlUi/Core/ElementText.h"
+#include <algorithm>
 
 
 namespace Rml {
 namespace Rml {
 
 
-WidgetTextInputMultiLine::WidgetTextInputMultiLine(ElementFormControl* parent) : WidgetTextInput(parent)
-{
-}
+WidgetTextInputMultiLine::WidgetTextInputMultiLine(ElementFormControl* parent) : WidgetTextInput(parent) {}
 
 
-WidgetTextInputMultiLine::~WidgetTextInputMultiLine()
-{
-}
+WidgetTextInputMultiLine::~WidgetTextInputMultiLine() {}
 
 
-// Returns true if the given character is permitted in the input field, false if not.
-bool WidgetTextInputMultiLine::IsCharacterValid(char character)
+void WidgetTextInputMultiLine::SanitizeValue(String& value)
 {
 {
-	return character != '\t';
+	value.erase(std::remove_if(value.begin(), value.end(), [](char c) { return c == '\r' || c == '\t'; }), value.end());
 }
 }
 
 
 // Called when the user pressed enter.
 // Called when the user pressed enter.
-void WidgetTextInputMultiLine::LineBreak()
-{
-}
+void WidgetTextInputMultiLine::LineBreak() {}
 
 
 } // namespace Rml
 } // namespace Rml

+ 7 - 10
Source/Core/Elements/WidgetTextInputMultiLine.h

@@ -15,7 +15,7 @@
  *
  *
  * The above copyright notice and this permission notice shall be included in
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  * all copies or substantial portions of the Software.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -34,24 +34,21 @@
 namespace Rml {
 namespace Rml {
 
 
 /**
 /**
-	A specialisation of the text input widget for multi-line text fields.
+    A specialisation of the text input widget for multi-line text fields.
 
 
-	@author Peter Curry
+    @author Peter Curry
  */
  */
 
 
-class WidgetTextInputMultiLine : public WidgetTextInput
-{
+class WidgetTextInputMultiLine : public WidgetTextInput {
 public:
 public:
 	WidgetTextInputMultiLine(ElementFormControl* parent);
 	WidgetTextInputMultiLine(ElementFormControl* parent);
 	virtual ~WidgetTextInputMultiLine();
 	virtual ~WidgetTextInputMultiLine();
 
 
 protected:
 protected:
-	/// Returns true if the given character is permitted in the input field, false if not.
-	/// @param[in] character The character to validate.
-	/// @return True if the character is allowed, false if not.
-	bool IsCharacterValid(char character) override;
+	/// Removes any invalid characters from the string.
+	void SanitizeValue(String& value) override;
 	/// Called when the user pressed enter.
 	/// Called when the user pressed enter.
-	void LineBreak() override;		
+	void LineBreak() override;
 };
 };
 
 
 } // namespace Rml
 } // namespace Rml

+ 6 - 43
Source/Core/Elements/WidgetTextInputSingleLine.cpp

@@ -15,7 +15,7 @@
  *
  *
  * The above copyright notice and this permission notice shall be included in
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  * all copies or substantial portions of the Software.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -26,60 +26,23 @@
  *
  *
  */
  */
 
 
+#include "WidgetTextInputSingleLine.h"
 #include "../../../Include/RmlUi/Core/Dictionary.h"
 #include "../../../Include/RmlUi/Core/Dictionary.h"
 #include "../../../Include/RmlUi/Core/ElementText.h"
 #include "../../../Include/RmlUi/Core/ElementText.h"
-#include "WidgetTextInputSingleLine.h"
+#include <algorithm>
 
 
 namespace Rml {
 namespace Rml {
 
 
-WidgetTextInputSingleLine::WidgetTextInputSingleLine(ElementFormControl* parent) : WidgetTextInput(parent)
-{
-}
+WidgetTextInputSingleLine::WidgetTextInputSingleLine(ElementFormControl* parent) : WidgetTextInput(parent) {}
 
 
-WidgetTextInputSingleLine::~WidgetTextInputSingleLine()
+void WidgetTextInputSingleLine::SanitizeValue(String& value)
 {
 {
+	value.erase(std::remove_if(value.begin(), value.end(), [](char c) { return c == '\r' || c == '\n' || c == '\t'; }), value.end());
 }
 }
 
 
-// Sets the value of the text field. The value will be stripped of end-lines.
-void WidgetTextInputSingleLine::SetValue(const String& value)
-{
-	String new_value = value;
-	SanitiseValue(new_value);
-
-	WidgetTextInput::SetValue(new_value);
-}
-
-// Returns true if the given character is permitted in the input field, false if not.
-bool WidgetTextInputSingleLine::IsCharacterValid(char character)
-{
-	return character != '\t' && character != '\n' && character != '\r';
-}
-
-// Called when the user pressed enter.
 void WidgetTextInputSingleLine::LineBreak()
 void WidgetTextInputSingleLine::LineBreak()
 {
 {
 	DispatchChangeEvent(true);
 	DispatchChangeEvent(true);
 }
 }
 
 
-// Strips all \n and \r characters from the string.
-void WidgetTextInputSingleLine::SanitiseValue(String& value)
-{
-	String new_value;
-	for (String::size_type i = 0; i < value.size(); ++i)
-	{
-		switch (value[i])
-		{
-			case '\n':
-			case '\r':
-			case '\t':
-				break;
-
-			default:
-				new_value += value[i];
-		}
-	}
-
-	value = new_value;
-}
-
 } // namespace Rml
 } // namespace Rml

+ 6 - 17
Source/Core/Elements/WidgetTextInputSingleLine.h

@@ -15,7 +15,7 @@
  *
  *
  * The above copyright notice and this permission notice shall be included in
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  * all copies or substantial portions of the Software.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -34,31 +34,20 @@
 namespace Rml {
 namespace Rml {
 
 
 /**
 /**
-	A specialisation of the text input widget for single-line input fields.
+    A specialisation of the text input widget for single-line input fields.
 
 
-	@author Peter Curry
+    @author Peter Curry
  */
  */
 
 
-class WidgetTextInputSingleLine : public WidgetTextInput
-{
+class WidgetTextInputSingleLine : public WidgetTextInput {
 public:
 public:
 	WidgetTextInputSingleLine(ElementFormControl* parent);
 	WidgetTextInputSingleLine(ElementFormControl* parent);
-	virtual ~WidgetTextInputSingleLine();
-
-	/// Sets the value of the text field. The value will be stripped of end-lines.
-	/// @param value[in] The new value to set on the text field.
-	void SetValue(const String& value) override;
 
 
 protected:
 protected:
-	/// Returns true if the given character is permitted in the input field, false if not.
-	/// @param[in] character The character to validate.
-	/// @return True if the character is allowed, false if not.
-	bool IsCharacterValid(char character) override;
+	/// Removes any invalid characters from the string.
+	void SanitizeValue(String& value) override;
 	/// Called when the user pressed enter.
 	/// Called when the user pressed enter.
 	void LineBreak() override;
 	void LineBreak() override;
-
-	/// Strips all \n and \r characters from the string.
-	void SanitiseValue(String& value);
 };
 };
 
 
 } // namespace Rml
 } // namespace Rml

+ 6 - 14
Source/Core/Elements/WidgetTextInputSingleLinePassword.cpp

@@ -15,7 +15,7 @@
  *
  *
  * The above copyright notice and this permission notice shall be included in
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  * all copies or substantial portions of the Software.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -26,25 +26,17 @@
  *
  *
  */
  */
 
 
-#include "../../../Include/RmlUi/Core/ElementText.h"
 #include "WidgetTextInputSingleLinePassword.h"
 #include "WidgetTextInputSingleLinePassword.h"
+#include "../../../Include/RmlUi/Core/ElementText.h"
 
 
 namespace Rml {
 namespace Rml {
 
 
-WidgetTextInputSingleLinePassword::WidgetTextInputSingleLinePassword(ElementFormControl* parent) : WidgetTextInputSingleLine(parent)
-{
-}
-
-WidgetTextInputSingleLinePassword::~WidgetTextInputSingleLinePassword()
-{
-}
+WidgetTextInputSingleLinePassword::WidgetTextInputSingleLinePassword(ElementFormControl* parent) : WidgetTextInputSingleLine(parent) {}
 
 
-// Sets the value of the password field.
-void WidgetTextInputSingleLinePassword::SetValue(const String& value)
+void WidgetTextInputSingleLinePassword::TransformValue(String& value)
 {
 {
-	String sanitised_value(value);
-	SanitiseValue(sanitised_value);
-	WidgetTextInput::SetValue(String(sanitised_value.size(), '*'));
+	for (auto& c : value)
+		c = '*';
 }
 }
 
 
 } // namespace Rml
 } // namespace Rml

+ 6 - 8
Source/Core/Elements/WidgetTextInputSingleLinePassword.h

@@ -15,7 +15,7 @@
  *
  *
  * The above copyright notice and this permission notice shall be included in
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  * all copies or substantial portions of the Software.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -34,18 +34,16 @@
 namespace Rml {
 namespace Rml {
 
 
 /**
 /**
-	@author Peter Curry
+    @author Peter Curry
  */
  */
 
 
-class WidgetTextInputSingleLinePassword : public WidgetTextInputSingleLine
-{
+class WidgetTextInputSingleLinePassword : public WidgetTextInputSingleLine {
 public:
 public:
 	WidgetTextInputSingleLinePassword(ElementFormControl* parent);
 	WidgetTextInputSingleLinePassword(ElementFormControl* parent);
-	virtual ~WidgetTextInputSingleLinePassword();
 
 
-	/// Sets the value of the password field.
-	/// @param value[in] The new password to set on the field.
-	void SetValue(const String& value) override;
+protected:
+	/// Transforms the displayed value of the text box, typically used for password fields.
+	void TransformValue(String& value) override;
 };
 };
 
 
 } // namespace Rml
 } // namespace Rml