Browse Source

Text input widget: Move the cursor word by word with ctrl + arrow keys. Better utf-8 handling.

Michael Ragazzon 6 years ago
parent
commit
6e4ba091d8

+ 3 - 0
Include/RmlUi/Core/StringUtilities.h

@@ -198,6 +198,9 @@ public:
 	// Return a pointer to the current position.
 	inline const char* Get() const { return p; }
 
+	// Return offset from the beginning of string. Note: Can return negative if decremented.
+	std::ptrdiff_t Offset() const { return p - view.begin(); }
+
 private:
 	StringView view;
 	// 'p' can be dereferenced if and only if inside [view.begin, view.end)

+ 110 - 36
Source/Controls/WidgetTextInput.cpp

@@ -31,6 +31,7 @@
 #include "../../Include/RmlUi/Core.h"
 #include "../../Include/RmlUi/Controls/ElementFormControl.h"
 #include "../../Include/RmlUi/Core/SystemInterface.h"
+#include "../../Include/RmlUi/Core/StringUtilities.h"
 #include "../Core/Clock.h"
 
 namespace Rml {
@@ -303,10 +304,10 @@ void WidgetTextInput::ProcessEvent(Core::Event& event)
 		switch (key_identifier)
 		{
 		case Core::Input::KI_NUMPAD4:	if (numlock) break;
-		case Core::Input::KI_LEFT:		MoveCursorHorizontal(-1, shift); break;
+		case Core::Input::KI_LEFT:		MoveCursorHorizontal(ctrl ? CursorMovement::PreviousWord : CursorMovement::Left, shift); break;
 
 		case Core::Input::KI_NUMPAD6:	if (numlock) break;
-		case Core::Input::KI_RIGHT:		MoveCursorHorizontal(1, shift); break;
+		case Core::Input::KI_RIGHT:		MoveCursorHorizontal(ctrl ? CursorMovement::NextWord : CursorMovement::Right, shift); break;
 
 		case Core::Input::KI_NUMPAD8:	if (numlock) break;
 		case Core::Input::KI_UP:		MoveCursorVertical(-1, shift); break;
@@ -315,10 +316,10 @@ void WidgetTextInput::ProcessEvent(Core::Event& event)
 		case Core::Input::KI_DOWN:		MoveCursorVertical(1, shift); break;
 
 		case Core::Input::KI_NUMPAD7:	if (numlock) break;
-		case Core::Input::KI_HOME:		MoveCursorHorizontal(-cursor_character_index, shift); break;
+		case Core::Input::KI_HOME:		MoveCursorHorizontal(CursorMovement::BeginLine, shift); break;
 
 		case Core::Input::KI_NUMPAD1:	if (numlock) break;
-		case Core::Input::KI_END:		MoveCursorHorizontal(lines[cursor_line_index].content_length - cursor_character_index, shift); break;
+		case Core::Input::KI_END:		MoveCursorHorizontal(CursorMovement::EndLine, shift); break;
 
 		case Core::Input::KI_BACK:
 		{
@@ -443,6 +444,8 @@ void WidgetTextInput::ProcessEvent(Core::Event& event)
 			cursor_character_index = CalculateCharacterIndex(cursor_line_index, mouse_position.x);
 
 			UpdateAbsoluteCursor();
+			MoveCursorToCharacterBoundaries(false);
+
 			UpdateCursorPosition();
 			ideal_cursor_position = cursor_position.x;
 
@@ -476,7 +479,7 @@ bool WidgetTextInput::AddCharacter(Rml::Core::CodePoint character)
 	Core::String insert = Core::StringUtilities::ToUTF8(character);
 	value.insert(GetCursorIndex(), insert);
 
-	edit_index += insert.size();
+	edit_index += (int)insert.size();
 
 	GetElement()->SetAttribute("value", value);
 	DispatchChangeEvent();
@@ -489,8 +492,11 @@ bool WidgetTextInput::AddCharacter(Rml::Core::CodePoint character)
 // Deletes a character from the string.
 bool WidgetTextInput::DeleteCharacter(bool back)
 {
-	// First, check if we have anything selected; if so, delete that first before we start delete
-	// individual characters.
+	// We set a selection of the next or previous character, and then delete it.
+	// If we already have a selection, we delete that first.
+	if (selection_length <= 0)
+		MoveCursorHorizontal(back ? CursorMovement::Left : CursorMovement::Right, true);
+
 	if (selection_length > 0)
 	{
 		DeleteSelection();
@@ -501,30 +507,7 @@ bool WidgetTextInput::DeleteCharacter(bool back)
 		return true;
 	}
 
-	Core::String value = GetElement()->GetAttribute< Rml::Core::String >("value", "");
-	
-	if (back)
-	{
-		if (GetCursorIndex() == 0)
-			return false;
-
-		value.erase(GetCursorIndex() - 1, 1);
-		edit_index -= 1;
-	}
-	else
-	{
-		if (GetCursorIndex() == (int) value.size())
-			return false;
-
-		value.erase(GetCursorIndex(), 1);
-	}
-
-	GetElement()->SetAttribute("value", value);
-	DispatchChangeEvent();
-
-	UpdateSelection(false);
-
-	return true;
+	return false;
 }
 
 // Copies the selection (if any) to the clipboard.
@@ -543,17 +526,86 @@ int WidgetTextInput::GetCursorIndex() const
 }
 
 // Moves the cursor along the current line.
-void WidgetTextInput::MoveCursorHorizontal(int distance, bool select)
+void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 {
-	// Todo, move properly across multibyte characters
-	absolute_cursor_index += distance;
+	const auto is_nonword_character = [](char c) -> bool {
+		return Core::StringUtilities::IsWhitespace(c) || (c >= '!' && c <= '@');
+	};
+
+	// Whether to seek forward or back to align to utf8 boundaries later.
+	bool seek_forward = false;
+
+	switch (movement)
+	{
+	case CursorMovement::BeginLine:
+		absolute_cursor_index -= cursor_character_index;
+		break;
+	case CursorMovement::PreviousWord:
+		if (cursor_character_index <= 1)
+		{
+			absolute_cursor_index -= 1;
+		}
+		else
+		{
+			bool whitespace_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)
+			{
+				bool is_whitespace = is_nonword_character(*p);
+				if(whitespace_found && !is_whitespace)
+					break;
+				else if(!whitespace_found && is_whitespace)
+					whitespace_found = true;
+			}
+			if (p != p_rend) ++p;
+			absolute_cursor_index += int(p - p_rbegin);
+		}
+		break;
+	case CursorMovement::Left:
+		absolute_cursor_index -= 1;
+		break;
+	case CursorMovement::Right:
+		seek_forward = true;
+		absolute_cursor_index += 1;
+		break;
+	case CursorMovement::NextWord:
+		if (cursor_character_index >= lines[cursor_line_index].content_length)
+		{
+			absolute_cursor_index += 1;
+		}
+		else
+		{
+			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)
+			{
+				bool is_whitespace = is_nonword_character(*p);
+				if (whitespace_found && !is_whitespace)
+					break;
+				else if (!whitespace_found && is_whitespace)
+					whitespace_found = true;
+			}
+			absolute_cursor_index += int(p - p_begin);
+		}
+		break;
+	case CursorMovement::EndLine:
+		absolute_cursor_index += lines[cursor_line_index].content_length - cursor_character_index;
+		break;
+	default:
+		break;
+	}
+	
 	absolute_cursor_index = Rml::Core::Math::Max(0, absolute_cursor_index);
 
 	UpdateRelativeCursor();
-	ideal_cursor_position = cursor_position.x;
+	MoveCursorToCharacterBoundaries(seek_forward);
 
+	ideal_cursor_position = cursor_position.x;
 	UpdateSelection(select);
-
 	ShowCursor(true);
 }
 
@@ -581,6 +633,9 @@ void WidgetTextInput::MoveCursorVertical(int distance, bool select)
 		cursor_character_index = CalculateCharacterIndex(cursor_line_index, ideal_cursor_position);
 
 	UpdateAbsoluteCursor();
+
+	MoveCursorToCharacterBoundaries(false);
+
 	UpdateCursorPosition();
 
 	if (update_ideal_cursor_position)
@@ -591,6 +646,25 @@ void WidgetTextInput::MoveCursorVertical(int distance, bool select)
 	ShowCursor(true);
 }
 
+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 char* p = p_cursor;
+
+	if (forward)
+		p = Core::StringUtilities::SeekForwardU8(p_cursor, p_line_end);
+	else
+		p = Core::StringUtilities::SeekBackU8(p_cursor, p_line_begin);
+
+	if (p != p_cursor)
+	{
+		absolute_cursor_index += int(p - p_cursor);
+		UpdateRelativeCursor();
+	}
+}
+
 // Updates the absolute cursor index from the relative cursor indices.
 void WidgetTextInput::UpdateAbsoluteCursor()
 {

+ 10 - 3
Source/Controls/WidgetTextInput.h

@@ -116,14 +116,19 @@ protected:
 	void DispatchChangeEvent(bool linebreak = false);
 
 private:
+	enum class CursorMovement { BeginLine = -3, PreviousWord = -2, Left = -1, Right = 1, NextWord = 2, EndLine = 3 };
+	
 	/// Moves the cursor along the current line.
-	/// @param[in] x How far to move the cursor.
+	/// @param[in] movement Cursor movement operation.
 	/// @param[in] select True if the movement will also move the selection cursor, false if not.
-	void MoveCursorHorizontal(int distance, bool select);
+	void MoveCursorHorizontal(CursorMovement movement, bool select);
 	/// Moves the cursor up and down the text field.
 	/// @param[in] x How far to move the cursor.
 	/// @param[in] select True if the movement will also move the selection cursor, false if not.
 	void MoveCursorVertical(int distance, bool select);
+	// Move the cursor to utf-8 boundaries, in case it was moved into the middle of a multibyte character.
+	/// @param[in] forward True to seek forward, else back.
+	void MoveCursorToCharacterBoundaries(bool forward);
 
 	/// Updates the absolute cursor index from the relative cursor indices.
 	void UpdateAbsoluteCursor();
@@ -196,10 +201,12 @@ private:
 	typedef std::vector< Line > LineList;
 	LineList lines;
 
+	// Length in number of characters.
 	int max_length;
 
+	// Indices in bytes: Should always be moved along UTF-8 start bytes.
 	int edit_index;
-
+	
 	int absolute_cursor_index;
 	int cursor_line_index;
 	int cursor_character_index;

+ 2 - 2
Source/Core/StringUtilities.cpp

@@ -609,7 +609,8 @@ bool StringView::operator==(const StringView& other) const {
 
 
 
-StringIteratorU8::StringIteratorU8(const char* p_begin, const char* p, const char* p_end) : view(p_begin, p_end), p(p) {
+StringIteratorU8::StringIteratorU8(const char* p_begin, const char* p, const char* p_end) : view(p_begin, p_end), p(p) 
+{
 	SeekForward();
 }
 StringIteratorU8::StringIteratorU8(const String& string) : view(string), p(string.data())
@@ -636,7 +637,6 @@ StringIteratorU8& StringIteratorU8::operator--() {
 	SeekBack();
 	return *this;
 }
-
 inline void StringIteratorU8::SeekBack() {
 	p = StringUtilities::SeekBackU8(p, view.end());
 }