Browse Source

Merge branch 'input_selection_range'

# Conflicts:
#	Tests/Source/UnitTests/StringUtilities.cpp
Michael Ragazzon 2 years ago
parent
commit
33851f599d

+ 15 - 0
Include/RmlUi/Core/Elements/ElementFormControlInput.h

@@ -63,6 +63,21 @@ public:
 	/// @return True if the form control is to be submitted, false otherwise.
 	/// @return True if the form control is to be submitted, false otherwise.
 	bool IsSubmitted() override;
 	bool IsSubmitted() override;
 
 
+	/// Selects all text.
+	/// @note Only applies to text and password input types.
+	void Select();
+	/// Selects the text in the given character range.
+	/// @param[in] selection_start The first character to be selected.
+	/// @param[in] selection_end The first character *after* the selection.
+	/// @note Only applies to text and password input types.
+	void SetSelectionRange(int selection_start, int selection_end);
+	/// Retrieves the selection range and text.
+	/// @param[out] selection_start The first character selected.
+	/// @param[out] selection_end The first character *after* the selection.
+	/// @param[out] selected_text The selected text.
+	/// @note Only applies to text and password input types.
+	void GetSelection(int* selection_start, int* selection_end, String* selected_text) const;
+
 protected:
 protected:
 	/// Updates the element's underlying type.
 	/// Updates the element's underlying type.
 	void OnUpdate() override;
 	void OnUpdate() override;

+ 14 - 2
Include/RmlUi/Core/Elements/ElementFormControlTextArea.h

@@ -34,7 +34,7 @@
 
 
 namespace Rml {
 namespace Rml {
 
 
-class WidgetTextInput;
+class WidgetTextInputMultiLine;
 
 
 /**
 /**
 	Default RmlUi implemention of a text area.
 	Default RmlUi implemention of a text area.
@@ -91,6 +91,18 @@ public:
 	/// @return True if the text area is word-wrapping, false otherwise.
 	/// @return True if the text area is word-wrapping, false otherwise.
 	bool GetWordWrap();
 	bool GetWordWrap();
 
 
+	/// Selects all text.
+	void Select();
+	/// Selects the text in the given character range.
+	/// @param[in] selection_start The first character to be selected.
+	/// @param[in] selection_end The first character *after* the selection.
+	void SetSelectionRange(int selection_start, int selection_end);
+	/// Retrieves the selection range and text.
+	/// @param[out] selection_start The first character selected.
+	/// @param[out] selection_end The first character *after* the selection.
+	/// @param[out] selected_text The selected text.
+	void GetSelection(int* selection_start, int* selection_end, String* selected_text) const;
+
 	/// Returns the control's inherent size, based on the length of the input field and the current font size.
 	/// Returns the control's inherent size, based on the length of the input field and the current font size.
 	/// @return True.
 	/// @return True.
 	bool GetIntrinsicDimensions(Vector2f& dimensions, float& ratio) override;
 	bool GetIntrinsicDimensions(Vector2f& dimensions, float& ratio) override;
@@ -116,7 +128,7 @@ protected:
 	void GetInnerRML(String& content) const override;
 	void GetInnerRML(String& content) const override;
 
 
 private:
 private:
-	WidgetTextInput* widget;		
+	UniquePtr<WidgetTextInputMultiLine> widget;
 };
 };
 
 
 } // namespace Rml
 } // namespace Rml

+ 18 - 0
Source/Core/Elements/ElementFormControlInput.cpp

@@ -68,6 +68,24 @@ bool ElementFormControlInput::IsSubmitted()
 	return type->IsSubmitted();
 	return type->IsSubmitted();
 }
 }
 
 
+void ElementFormControlInput::Select()
+{
+	RMLUI_ASSERT(type);
+	type->Select();
+}
+
+void ElementFormControlInput::SetSelectionRange(int selection_start, int selection_end)
+{
+	RMLUI_ASSERT(type);
+	type->SetSelectionRange(selection_start, selection_end);
+}
+
+void ElementFormControlInput::GetSelection(int* selection_start, int* selection_end, String* selected_text) const
+{
+	RMLUI_ASSERT(type);
+	type->GetSelection(selection_start, selection_end, selected_text);
+}
+
 // Updates the element's underlying type.
 // Updates the element's underlying type.
 void ElementFormControlInput::OnUpdate()
 void ElementFormControlInput::OnUpdate()
 {
 {

+ 17 - 5
Source/Core/Elements/ElementFormControlTextArea.cpp

@@ -39,17 +39,14 @@ namespace Rml {
 // Constructs a new ElementFormControlTextArea.
 // Constructs a new ElementFormControlTextArea.
 ElementFormControlTextArea::ElementFormControlTextArea(const String& tag) : ElementFormControl(tag)
 ElementFormControlTextArea::ElementFormControlTextArea(const String& tag) : ElementFormControl(tag)
 {
 {
-	widget = new WidgetTextInputMultiLine(this);
+	widget = MakeUnique<WidgetTextInputMultiLine>(this);
 
 
 	SetProperty(PropertyId::OverflowX, Property(Style::Overflow::Auto));
 	SetProperty(PropertyId::OverflowX, Property(Style::Overflow::Auto));
 	SetProperty(PropertyId::OverflowY, Property(Style::Overflow::Auto));
 	SetProperty(PropertyId::OverflowY, Property(Style::Overflow::Auto));
 	SetProperty(PropertyId::WhiteSpace, Property(Style::WhiteSpace::Prewrap));
 	SetProperty(PropertyId::WhiteSpace, Property(Style::WhiteSpace::Prewrap));
 }
 }
 
 
-ElementFormControlTextArea::~ElementFormControlTextArea()
-{
-	delete widget;
-}
+ElementFormControlTextArea::~ElementFormControlTextArea() {}
 
 
 // Returns a string representation of the current value of the form control.
 // Returns a string representation of the current value of the form control.
 String ElementFormControlTextArea::GetValue() const
 String ElementFormControlTextArea::GetValue() const
@@ -119,6 +116,21 @@ bool ElementFormControlTextArea::GetWordWrap()
 	return attribute != "nowrap";
 	return attribute != "nowrap";
 }
 }
 
 
+void ElementFormControlTextArea::Select()
+{
+	widget->Select();
+}
+
+void ElementFormControlTextArea::SetSelectionRange(int selection_start, int selection_end)
+{
+	widget->SetSelectionRange(selection_start, selection_end);
+}
+
+void ElementFormControlTextArea::GetSelection(int* selection_start, int* selection_end, String* selected_text) const
+{
+	widget->GetSelection(selection_start, selection_end, selected_text);
+}
+
 // Returns the control's inherent size, based on the length of the input field and the current font size.
 // Returns the control's inherent size, based on the length of the input field and the current font size.
 bool ElementFormControlTextArea::GetIntrinsicDimensions(Vector2f& dimensions, float& /*ratio*/)
 bool ElementFormControlTextArea::GetIntrinsicDimensions(Vector2f& dimensions, float& /*ratio*/)
 {
 {

+ 6 - 0
Source/Core/Elements/InputType.cpp

@@ -93,4 +93,10 @@ void InputType::OnChildRemove()
 {
 {
 }
 }
 
 
+void InputType::Select() {}
+
+void InputType::SetSelectionRange(int /*selection_start*/, int /*selection_end*/) {}
+
+void InputType::GetSelection(int* /*selection_start*/, int* /*selection_end*/, String* /*selected_text*/) const {}
+
 } // namespace Rml
 } // namespace Rml

+ 7 - 0
Source/Core/Elements/InputType.h

@@ -88,6 +88,13 @@ public:
 	/// Sizes the dimensions to the element's inherent size.
 	/// Sizes the dimensions to the element's inherent size.
 	virtual bool GetIntrinsicDimensions(Vector2f& dimensions, float& ratio) = 0;
 	virtual bool GetIntrinsicDimensions(Vector2f& dimensions, float& ratio) = 0;
 
 
+	/// Selects all text.
+	virtual void Select();
+	/// Selects the text in the given character range.
+	virtual void SetSelectionRange(int selection_start, int selection_end);
+	/// Retrieves the selection range and text.
+	virtual void GetSelection(int* selection_start, int* selection_end, String* selected_text) const;
+
 protected:
 protected:
 	ElementFormControlInput* element;
 	ElementFormControlInput* element;
 };
 };

+ 15 - 0
Source/Core/Elements/InputTypeText.cpp

@@ -126,4 +126,19 @@ bool InputTypeText::GetIntrinsicDimensions(Vector2f& dimensions, float& /*ratio*
 	return true;
 	return true;
 }
 }
 
 
+void InputTypeText::Select()
+{
+	widget->Select();
+}
+
+void InputTypeText::SetSelectionRange(int selection_start, int selection_end)
+{
+	widget->SetSelectionRange(selection_start, selection_end);
+}
+
+void InputTypeText::GetSelection(int* selection_start, int* selection_end, String* selected_text) const
+{
+	widget->GetSelection(selection_start, selection_end, selected_text);
+}
+
 } // namespace Rml
 } // namespace Rml

+ 7 - 0
Source/Core/Elements/InputTypeText.h

@@ -81,6 +81,13 @@ public:
 	/// @return True.
 	/// @return True.
 	bool GetIntrinsicDimensions(Vector2f& dimensions, float& ratio) override;
 	bool GetIntrinsicDimensions(Vector2f& dimensions, float& ratio) override;
 
 
+	/// Selects all text.
+	void Select() override;
+	/// Selects the text in the given character range.
+	void SetSelectionRange(int selection_start, int selection_end) override;
+	/// Retrieves the selection range and text.
+	void GetSelection(int* selection_start, int* selection_end, String* selected_text) const override;
+
 private:
 private:
 	int size = 20;
 	int size = 20;
 
 

+ 94 - 32
Source/Core/Elements/WidgetTextInput.cpp

@@ -60,6 +60,48 @@ static CharacterClass GetCharacterClass(char c)
 	return CharacterClass::Whitespace;
 	return CharacterClass::Whitespace;
 }
 }
 
 
+static int ConvertCharacterOffsetToByteOffset(const String& value, int character_offset)
+{
+	if (character_offset >= (int)value.size())
+		return (int)value.size();
+
+	int character_count = 0;
+	for (auto it = StringIteratorU8(value); it; ++it)
+	{
+		character_count += 1;
+		if (character_count > character_offset)
+			return (int)it.offset();
+	}
+	return (int)value.size();
+}
+
+static int ConvertByteOffsetToCharacterOffset(const String& value, int byte_offset)
+{
+	int character_count = 0;
+	for (auto it = StringIteratorU8(value); it; ++it)
+	{
+		if (it.offset() >= byte_offset)
+			break;
+		character_count += 1;
+	}
+	return character_count;
+}
+
+// Clamps the value to the given maximum number of unicode code points. Returns true if the value was changed.
+static bool ClampValue(String& value, int max_length)
+{
+	if (max_length >= 0)
+	{
+		int max_byte_length = ConvertCharacterOffsetToByteOffset(value, max_length);
+		if (max_byte_length < (int)value.size())
+		{
+			value.erase((size_t)max_byte_length);
+			return true;
+		}
+	}
+	return false;
+}
+
 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)
 {
 {
 	keyboard_showed = false;
 	keyboard_showed = false;
@@ -168,29 +210,10 @@ void WidgetTextInput::SetMaxLength(int _max_length)
 	if (max_length != _max_length)
 	if (max_length != _max_length)
 	{
 	{
 		max_length = _max_length;
 		max_length = _max_length;
-		if (max_length >= 0)
-		{
-			String value = GetValue();
-
-			int num_characters = 0;
-			size_t i_erase = value.size();
-
-			for (auto it = StringIteratorU8(value); it; ++it)
-			{
-				num_characters += 1;
-				if (num_characters > max_length)
-				{
-					i_erase = size_t(it.offset());
-					break;
-				}
-			}
 
 
-			if(i_erase < value.size())
-			{
-				value.erase(i_erase);
-				GetElement()->SetAttribute("value", value);
-			}
-		}
+		String value = GetValue();
+		if (ClampValue(value, max_length))
+			GetElement()->SetAttribute("value", value);
 	}
 	}
 }
 }
 
 
@@ -206,6 +229,49 @@ int WidgetTextInput::GetLength() const
 	return (int)result;
 	return (int)result;
 }
 }
 
 
+void WidgetTextInput::Select()
+{
+	SetSelectionRange(0, INT_MAX);
+}
+
+void WidgetTextInput::SetSelectionRange(int selection_start, int selection_end)
+{
+	const String& value = GetValue();
+	const int byte_start = ConvertCharacterOffsetToByteOffset(value, selection_start);
+	const int byte_end = ConvertCharacterOffsetToByteOffset(value, selection_end);
+	const bool is_selecting = (byte_start != byte_end);
+	
+	cursor_wrap_down = true;
+	absolute_cursor_index = byte_end;
+
+	bool selection_changed = false;
+	if (is_selecting)
+	{
+		selection_anchor_index = byte_start;
+		selection_changed = UpdateSelection(true);
+	}
+	else
+	{
+		selection_changed = UpdateSelection(false);
+	}
+
+	UpdateCursorPosition(true);
+
+	if (selection_changed)
+		FormatText();
+}
+
+void WidgetTextInput::GetSelection(int* selection_start, int* selection_end, String* selected_text) const
+{
+	const String& value = GetValue();
+	if (selection_start)
+		*selection_start = ConvertByteOffsetToCharacterOffset(value, selection_begin_index);
+	if (selection_end)
+		*selection_end = ConvertByteOffsetToCharacterOffset(value, selection_begin_index + selection_length);
+	if (selected_text)
+		*selected_text = value.substr(Math::Min((size_t)selection_begin_index, (size_t)value.size()), (size_t)selection_length);
+}
+
 // Update the colours of the selected text.
 // Update the colours of the selected text.
 void WidgetTextInput::UpdateSelectionColours()
 void WidgetTextInput::UpdateSelectionColours()
 {
 {
@@ -384,10 +450,7 @@ void WidgetTextInput::ProcessEvent(Event& event)
 		case Input::KI_A:
 		case Input::KI_A:
 		{
 		{
 			if (ctrl)
 			if (ctrl)
-			{
-				selection_changed = MoveCursorHorizontal(CursorMovement::Begin, false);
-				selection_changed |= MoveCursorHorizontal(CursorMovement::End, true);
-			}
+				Select();
 		}
 		}
 		break;
 		break;
 
 
@@ -524,13 +587,13 @@ bool WidgetTextInput::AddCharacters(String string)
 {
 {
 	SanitizeValue(string);
 	SanitizeValue(string);
 
 
-	if (string.empty())
-		return false;
-
 	if (selection_length > 0)
 	if (selection_length > 0)
 		DeleteSelection();
 		DeleteSelection();
 
 
-	if (max_length >= 0 && GetLength() >= max_length)
+	if (max_length >= 0)
+		ClampValue(string, Math::Max(max_length - GetLength(), 0));
+
+	if (string.empty())
 		return false;
 		return false;
 
 
 	String value = GetAttributeValue();
 	String value = GetAttributeValue();
@@ -569,7 +632,7 @@ bool WidgetTextInput::DeleteCharacters(CursorMovement direction)
 void WidgetTextInput::CopySelection()
 void WidgetTextInput::CopySelection()
 {
 {
 	const String& value = GetValue();
 	const String& value = GetValue();
-	const String snippet = value.substr(std::min((size_t)selection_begin_index, (size_t)value.size()), (size_t)selection_length);
+	const String snippet = value.substr(Math::Min((size_t)selection_begin_index, (size_t)value.size()), (size_t)selection_length);
 	GetSystemInterface()->SetClipboardText(snippet);
 	GetSystemInterface()->SetClipboardText(snippet);
 }
 }
 
 
@@ -1181,7 +1244,6 @@ void WidgetTextInput::UpdateCursorPosition(bool update_ideal_cursor_position)
 		ideal_cursor_position = cursor_position.x;
 		ideal_cursor_position = cursor_position.x;
 }
 }
 
 
-// Expand the text selection to the position of the cursor.
 bool WidgetTextInput::UpdateSelection(bool selecting)
 bool WidgetTextInput::UpdateSelection(bool selecting)
 {
 {
 	bool selection_changed = false;
 	bool selection_changed = false;

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

@@ -65,6 +65,18 @@ public:
 	/// Returns the current length (in characters) of this text field.
 	/// Returns the current length (in characters) of this text field.
 	int GetLength() const;
 	int GetLength() const;
 
 
+	/// Selects all text.
+	void Select();
+	/// Selects the text in the given character range.
+	/// @param[in] selection_start The first character to be selected.
+	/// @param[in] selection_end The first character *after* the selection.
+	void SetSelectionRange(int selection_start, int selection_end);
+	/// Retrieves the selection range and text.
+	/// @param[out] selection_start The first character selected.
+	/// @param[out] selection_end The first character *after* the selection.
+	/// @param[out] selected_text The selected text.
+	void GetSelection(int* selection_start, int* selection_end, String* selected_text) const;
+
 	/// Update the colours of the selected text.
 	/// Update the colours of the selected text.
 	void UpdateSelectionColours();
 	void UpdateSelectionColours();
 	/// Generates the text cursor.
 	/// Generates the text cursor.

+ 48 - 0
Tests/Source/UnitTests/StringUtilities.cpp

@@ -103,3 +103,51 @@ TEST_CASE("StringView")
 	CHECK(StringView() == String());
 	CHECK(StringView() == String());
 	CHECK(StringView() == "");
 	CHECK(StringView() == "");
 }
 }
+
+
+#include "../../../Source/Core/Elements/WidgetTextInput.cpp"
+
+TEST_CASE("ConvertByteOffsetToCharacterOffset")
+{
+	// clang-format off
+	CHECK(ConvertByteOffsetToCharacterOffset("", 0) == 0);
+	CHECK(ConvertByteOffsetToCharacterOffset("", 1) == 0);
+    CHECK(ConvertByteOffsetToCharacterOffset("a", 0) == 0);
+    CHECK(ConvertByteOffsetToCharacterOffset("a", 1) == 1);
+	CHECK(ConvertByteOffsetToCharacterOffset("ab", 1) == 1);
+	CHECK(ConvertByteOffsetToCharacterOffset("ab", 2) == 2);
+
+	CHECK(ConvertByteOffsetToCharacterOffset("a\xC2\xA3" "b", 1) == 1);
+	CHECK(ConvertByteOffsetToCharacterOffset("a\xC2\xA3" "b", 2) == 2);
+	CHECK(ConvertByteOffsetToCharacterOffset("a\xC2\xA3" "b", 3) == 2);
+	CHECK(ConvertByteOffsetToCharacterOffset("a\xC2\xA3" "b", 4) == 3);
+
+	CHECK(ConvertByteOffsetToCharacterOffset("a\xE2\x82\xAC" "b", 2) == 2);
+	CHECK(ConvertByteOffsetToCharacterOffset("a\xE2\x82\xAC" "b", 3) == 2);
+	CHECK(ConvertByteOffsetToCharacterOffset("a\xE2\x82\xAC" "b", 4) == 2);
+	CHECK(ConvertByteOffsetToCharacterOffset("a\xE2\x82\xAC" "b", 5) == 3);
+	// clang-format on
+}
+
+TEST_CASE("ConvertCharacterOffsetToByteOffset")
+{
+	// clang-format off
+	CHECK(ConvertCharacterOffsetToByteOffset("", 0) == 0);
+	CHECK(ConvertCharacterOffsetToByteOffset("", 1) == 0);
+    CHECK(ConvertCharacterOffsetToByteOffset("a", 0) == 0);
+    CHECK(ConvertCharacterOffsetToByteOffset("a", 1) == 1);
+	CHECK(ConvertCharacterOffsetToByteOffset("ab", 1) == 1);
+	CHECK(ConvertCharacterOffsetToByteOffset("ab", 2) == 2);
+
+	CHECK(ConvertCharacterOffsetToByteOffset("a\xC2\xA3" "b", 1) == 1);
+	CHECK(ConvertCharacterOffsetToByteOffset("a\xC2\xA3" "b", 2) == 3);
+	CHECK(ConvertCharacterOffsetToByteOffset("a\xC2\xA3" "b", 3) == 4);
+	CHECK(ConvertCharacterOffsetToByteOffset("a\xC2\xA3" "b", 4) == 4);
+
+	CHECK(ConvertCharacterOffsetToByteOffset("a\xE2\x82\xAC" "b", 1) == 1);
+	CHECK(ConvertCharacterOffsetToByteOffset("a\xE2\x82\xAC" "b", 2) == 4);
+	CHECK(ConvertCharacterOffsetToByteOffset("a\xE2\x82\xAC" "b", 3) == 5);
+	CHECK(ConvertCharacterOffsetToByteOffset("a\xE2\x82\xAC" "b", 4) == 5);
+	// clang-format on
+}
+