Просмотр исходного кода

Further improvements to text input elements:
- Select word by double click
- Page up/down for navigation
- Jump to beginning/end of selection on navigation.
Also, double clicks only happen if inside a small radius of the first click.

Michael Ragazzon 6 лет назад
Родитель
Сommit
8207ac4c63

+ 2 - 0
Include/RmlUi/Core/Context.h

@@ -253,6 +253,8 @@ private:
 	Element* last_click_element;
 	// The time the last click occured.
 	double last_click_time;
+	// Mouse position during the last mouse_down event.
+	Vector2i last_click_mouse_position;
 
 	// Enables cursor handling.
 	bool enable_cursor;

+ 115 - 8
Source/Controls/WidgetTextInput.cpp

@@ -37,7 +37,11 @@
 namespace Rml {
 namespace Controls {
 
-static const float CURSOR_BLINK_TIME = 0.7f;
+static constexpr float CURSOR_BLINK_TIME = 0.7f;
+
+static bool IsWordCharacter(char c) {
+	return !Core::StringUtilities::IsWhitespace(c) && !(c >= '!' && c <= '@');
+}
 
 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)
 {
@@ -55,6 +59,7 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) : internal_dimensi
 	parent->AddEventListener(Core::EventId::Focus, this, true);
 	parent->AddEventListener(Core::EventId::Blur, this, true);
 	parent->AddEventListener(Core::EventId::Mousedown, this, true);
+	parent->AddEventListener(Core::EventId::Dblclick, this, true);
 	parent->AddEventListener(Core::EventId::Drag, this, true);
 
 	Core::ElementPtr unique_text = Core::Factory::InstanceElement(parent, "#text", "#text", Rml::Core::XMLAttributes());
@@ -83,6 +88,8 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) : internal_dimensi
 	absolute_cursor_index = 0;
 	cursor_line_index = 0;
 	cursor_character_index = 0;
+	cursor_on_right_side_of_character = true;
+	cancel_next_drag = false;
 
 	ideal_cursor_position = 0;
 
@@ -104,6 +111,7 @@ WidgetTextInput::~WidgetTextInput()
 	parent->RemoveEventListener(Core::EventId::Focus, this, true);
 	parent->RemoveEventListener(Core::EventId::Blur, this, true);
 	parent->RemoveEventListener(Core::EventId::Mousedown, this, true);
+	parent->RemoveEventListener(Core::EventId::Dblclick, this, true);
 	parent->RemoveEventListener(Core::EventId::Drag, this, true);
 
 	// Remove all the children added by the text widget.
@@ -319,6 +327,12 @@ void WidgetTextInput::ProcessEvent(Core::Event& event)
 		case Core::Input::KI_NUMPAD1:	if (numlock) break;
 		case Core::Input::KI_END:		MoveCursorHorizontal(ctrl ? CursorMovement::End : CursorMovement::EndLine, shift); break;
 
+		case Core::Input::KI_NUMPAD3:	if (numlock) break;
+		case Core::Input::KI_PRIOR:		MoveCursorVertical(-int(internal_dimensions.y / parent->GetLineHeight()) + 1, shift); break;
+
+		case Core::Input::KI_NUMPAD9:	if (numlock) break;
+		case Core::Input::KI_NEXT:		MoveCursorVertical(int(internal_dimensions.y / parent->GetLineHeight()) - 1, shift); break;
+
 		case Core::Input::KI_BACK:
 		{
 			CursorMovement direction = (ctrl ? CursorMovement::PreviousWord : CursorMovement::Left);
@@ -437,8 +451,15 @@ void WidgetTextInput::ProcessEvent(Core::Event& event)
 		}
 	}
 	break;
-	case EventId::Mousedown:
 	case EventId::Drag:
+		if (cancel_next_drag)
+		{
+			// We currently ignore drag events right after a double click. They would need to be handled
+			// specially by selecting whole words at a time, which is not yet implemented.
+			break;
+		}
+		// Else, fall through:
+	case EventId::Mousedown:
 	{
 		if (event.GetTargetElement() == parent)
 		{
@@ -456,7 +477,17 @@ void WidgetTextInput::ProcessEvent(Core::Event& event)
 
 			UpdateSelection(event == Core::EventId::Drag || event.GetParameter< int >("shift_key", 0) > 0);
 
-			ShowCursor(true);
+			ShowCursor(true); 
+			cancel_next_drag = false;
+		}
+	}
+	break;
+	case EventId::Dblclick:
+	{
+		if (event.GetTargetElement() == parent)
+		{
+			ExpandSelection();
+			cancel_next_drag = true;
 		}
 	}
 	break;
@@ -568,7 +599,7 @@ void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 			const char* p = p_rbegin - 1;
 			for (; p > p_rend; --p)
 			{
-				bool is_word_character = !is_nonword_character(*p);
+				bool is_word_character = IsWordCharacter(*p);
 				if(word_character_found && !is_word_character)
 					break;
 				else if(is_word_character)
@@ -579,11 +610,17 @@ void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 		}
 		break;
 	case CursorMovement::Left:
-		absolute_cursor_index -= 1;
+		if (!select && selection_length > 0)
+			absolute_cursor_index = selection_begin_index;
+		else
+			absolute_cursor_index -= 1;
 		break;
 	case CursorMovement::Right:
 		seek_forward = true;
-		absolute_cursor_index += 1;
+		if (!select && selection_length > 0)
+			absolute_cursor_index = selection_begin_index + selection_length;
+		else
+			absolute_cursor_index += 1;
 		break;
 	case CursorMovement::NextWord:
 		if (cursor_character_index >= lines[cursor_line_index].content_length)
@@ -598,7 +635,7 @@ void WidgetTextInput::MoveCursorHorizontal(CursorMovement movement, bool select)
 			const char* p = p_begin;
 			for (; p < p_end; ++p)
 			{
-				bool is_whitespace = is_nonword_character(*p);
+				bool is_whitespace = !IsWordCharacter(*p);
 				if (whitespace_found && !is_whitespace)
 					break;
 				else if (is_whitespace)
@@ -683,6 +720,69 @@ void WidgetTextInput::MoveCursorToCharacterBoundaries(bool forward)
 	}
 }
 
+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))
+			return true;
+		if (!expanding_word_set)
+		{
+			expanding_word = is_word_character;
+			expanding_word_set = true;
+		}
+		return false;
+	};
+
+	auto search_left = [&]() -> const char* {
+		const char* p = p_index;
+		for (; p > p_begin; p--)
+			if (character_is_wrong_type(p - 1))
+				break;
+		return p;
+	};
+	auto search_right = [&]() -> const char* {
+		const char* p = p_index;
+		for (; p < p_end; p++)
+			if (character_is_wrong_type(p))
+				break;
+		return p;
+	};
+
+	const char* p_left = p_index;
+	const char* p_right = p_index;
+
+	if (cursor_on_right_side_of_character)
+	{
+		p_right = search_right();
+		p_left = search_left();
+	}
+	else
+	{
+		p_left = search_left();
+		p_right = search_right();
+	}
+
+	absolute_cursor_index -= (p_index - p_left);
+	UpdateRelativeCursor();
+	MoveCursorToCharacterBoundaries(false);
+	UpdateSelection(false);
+
+	absolute_cursor_index += (p_right - p_left);
+	UpdateRelativeCursor();
+	MoveCursorToCharacterBoundaries(true);
+	UpdateSelection(true);
+}
+
 // Updates the absolute cursor index from the relative cursor indices.
 void WidgetTextInput::UpdateAbsoluteCursor()
 {
@@ -743,7 +843,9 @@ int WidgetTextInput::CalculateCharacterIndex(int line_index, float position)
 {
 	int prev_offset = 0;
 	float prev_line_width = 0;
-	
+
+	cursor_on_right_side_of_character = true;
+
 	for(auto it = Core::StringIteratorU8(lines[line_index].content, 0, lines[line_index].content_length); it; )
 	{
 		++it;
@@ -753,9 +855,14 @@ int WidgetTextInput::CalculateCharacterIndex(int line_index, float position)
 		if (line_width > position)
 		{
 			if (position - prev_line_width < line_width - position)
+			{
 				return prev_offset;
+			}
 			else
+			{
+				cursor_on_right_side_of_character = false;
 				return offset;
+			}
 		}
 
 		prev_line_width = line_width;

+ 6 - 0
Source/Controls/WidgetTextInput.h

@@ -130,6 +130,8 @@ private:
 	// 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);
+	// Expands the cursor, selecting the current word or nearby whitespace.
+	void ExpandSelection();
 
 	/// Updates the absolute cursor index from the relative cursor indices.
 	void UpdateAbsoluteCursor();
@@ -143,6 +145,7 @@ private:
 	/// Calculates the character index along a line under a specific horizontal position.
 	/// @param[in] line_index The line to query.
 	/// @param[in] position The position to query.
+	/// @param[out] on_right_side True if position is on the right side of the returned character, else left side.
 	/// @return The index of the character under the mouse cursor.
 	int CalculateCharacterIndex(int line_index, float position);
 
@@ -212,6 +215,9 @@ private:
 	int cursor_line_index;
 	int cursor_character_index;
 
+	bool cursor_on_right_side_of_character;
+	bool cancel_next_drag;
+
 	// Selection. The start and end indices of the selection are in absolute coordinates.
 	Core::Element* selection_element;
 	int selection_anchor_index;

+ 11 - 3
Source/Core/Context.cpp

@@ -39,7 +39,8 @@
 namespace Rml {
 namespace Core {
 
-const float DOUBLE_CLICK_TIME = 0.5f;
+static constexpr float DOUBLE_CLICK_TIME = 0.5f;     // [s]
+static constexpr float DOUBLE_CLICK_MAX_DIST = 3.f;  // [dp]
 
 Context::Context(const String& name) : name(name), dimensions(0, 0), density_independent_pixel_ratio(1.0f), mouse_position(0, 0), clip_origin(-1, -1), clip_dimensions(-1, -1)
 {
@@ -75,6 +76,7 @@ Context::Context(const String& name) : name(name), dimensions(0, 0), density_ind
 
 	last_click_element = nullptr;
 	last_click_time = 0;
+	last_click_mouse_position = Vector2i(0, 0);
 }
 
 Context::~Context()
@@ -616,9 +618,14 @@ void Context::ProcessMouseButtonDown(int button_index, int key_modifier_state)
 		{
 			// Check for a double-click on an element; if one has occured, we send the 'dblclick' event to the hover
 			// element. If not, we'll start a timer to catch the next one.
+			float mouse_distance_squared = float((mouse_position - last_click_mouse_position).SquaredMagnitude());
+			float max_mouse_distance = DOUBLE_CLICK_MAX_DIST * density_independent_pixel_ratio;
+
 			double click_time = GetSystemInterface()->GetElapsedTime();
+
 			if (active == last_click_element &&
-				float(click_time - last_click_time) < DOUBLE_CLICK_TIME)
+				float(click_time - last_click_time) < DOUBLE_CLICK_TIME &&
+				mouse_distance_squared < max_mouse_distance * max_mouse_distance)
 			{
 				if (hover)
 					propagate = hover->DispatchEvent(EventId::Dblclick, parameters);
@@ -630,10 +637,11 @@ void Context::ProcessMouseButtonDown(int button_index, int key_modifier_state)
 			{
 				last_click_element = active;
 				last_click_time = click_time;
-			
 			}
 		}
 
+		last_click_mouse_position = mouse_position;
+
 		for (ElementSet::iterator itr = hover_chain.begin(); itr != hover_chain.end(); ++itr)
 			active_chain.push_back((*itr));