changeset 20677:9795e5780df4 draft

-Fix: Improve text caret movement for complex scripts.
author Michael Lutz <michi@icosahedron.de>
date Sun, 21 Jul 2013 15:41:44 +0200
parents 7e084f1014bd
children 71216b5f4bc9
files projects/openttd_vs100.vcxproj projects/openttd_vs100.vcxproj.filters projects/openttd_vs80.vcproj projects/openttd_vs90.vcproj source.list src/string.cpp src/string_base.h src/string_func.h src/textbuf.cpp src/textbuf_type.h
diffstat 10 files changed, 238 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/projects/openttd_vs100.vcxproj
+++ b/projects/openttd_vs100.vcxproj
@@ -595,6 +595,7 @@
     <ClInclude Include="..\src\story_base.h" />
     <ClInclude Include="..\src\story_type.h" />
     <ClInclude Include="..\src\strgen\strgen.h" />
+    <ClInclude Include="..\src\string_base.h" />
     <ClInclude Include="..\src\string_func.h" />
     <ClInclude Include="..\src\string_type.h" />
     <ClInclude Include="..\src\stringfilter_type.h" />
--- a/projects/openttd_vs100.vcxproj.filters
+++ b/projects/openttd_vs100.vcxproj.filters
@@ -1014,6 +1014,9 @@
     <ClInclude Include="..\src\strgen\strgen.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\src\string_base.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
     <ClInclude Include="..\src\string_func.h">
       <Filter>Header Files</Filter>
     </ClInclude>
--- a/projects/openttd_vs80.vcproj
+++ b/projects/openttd_vs80.vcproj
@@ -1655,6 +1655,10 @@
 				>
 			</File>
 			<File
+				RelativePath=".\..\src\string_base.h"
+				>
+			</File>
+			<File
 				RelativePath=".\..\src\string_func.h"
 				>
 			</File>
--- a/projects/openttd_vs90.vcproj
+++ b/projects/openttd_vs90.vcproj
@@ -1652,6 +1652,10 @@
 				>
 			</File>
 			<File
+				RelativePath=".\..\src\string_base.h"
+				>
+			</File>
+			<File
 				RelativePath=".\..\src\string_func.h"
 				>
 			</File>
--- a/source.list
+++ b/source.list
@@ -328,6 +328,7 @@
 story_base.h
 story_type.h
 strgen/strgen.h
+string_base.h
 string_func.h
 string_type.h
 stringfilter_type.h
--- a/src/string.cpp
+++ b/src/string.cpp
@@ -14,6 +14,7 @@
 #include "core/alloc_func.hpp"
 #include "core/math_func.hpp"
 #include "string_func.h"
+#include "string_base.h"
 
 #include "table/control_codes.h"
 
@@ -650,3 +651,123 @@
 	/* Do a normal comparison if ICU is missing or if we cannot create a collator. */
 	return strcasecmp(s1, s2);
 }
+
+#ifdef WITH_ICU
+
+#include <unicode/utext.h>
+#include <unicode/brkiter.h>
+
+/** String iterator using ICU as a backend. */
+class IcuStringIterator : public StringIterator
+{
+	icu::BreakIterator *char_itr; ///< ICU iterator for characters.
+	const char *string;           ///< Iteration string in UTF-8.
+
+public:
+	IcuStringIterator() : char_itr(NULL)
+	{
+		UErrorCode status = U_ZERO_ERROR;
+		this->char_itr = icu::BreakIterator::createCharacterInstance(icu::Locale(_current_language != NULL ? _current_language->isocode : "en"), status);
+	}
+
+	virtual ~IcuStringIterator()
+	{
+		delete this->char_itr;
+	}
+
+	virtual void SetString(const char *s)
+	{
+		this->string = s;
+
+		UText text = UTEXT_INITIALIZER;
+		UErrorCode status = U_ZERO_ERROR;
+		utext_openUTF8(&text, s, -1, &status);
+		this->char_itr->setText(&text, status);
+		this->char_itr->first();
+	}
+
+	virtual size_t SetCurPosition(size_t pos)
+	{
+		/* isBoundary has the documented side-effect of setting the current
+		 * position to the first valid boundary equal to or greater than
+		 * the passed value. */
+		this->char_itr->isBoundary((int32_t)pos);
+		return this->char_itr->current();
+	}
+
+	virtual size_t Next()
+	{
+		int32_t pos = this->char_itr->next();
+		return pos == icu::BreakIterator::DONE ? END : pos;
+	}
+
+	virtual size_t Prev()
+	{
+		int32_t pos = this->char_itr->previous();
+		return pos == icu::BreakIterator::DONE ? END : pos;
+	}
+};
+
+/* static */ StringIterator *StringIterator::Create()
+{
+	return new IcuStringIterator();
+}
+
+#else
+
+/** Fallback simple string iterator. */
+class DefaultStringIterator : public StringIterator
+{
+	const char *string; ///< Current string.
+	size_t len;         ///< String length.
+	size_t cur_pos;     ///< Current iteration position.
+
+public:
+	DefaultStringIterator() : string(NULL)
+	{
+	}
+
+	virtual void SetString(const char *s)
+	{
+		this->string = s;
+		this->len = strlen(s) + 1;
+		this->cur_pos = 0;
+	}
+
+	virtual size_t SetCurPosition(size_t pos)
+	{
+		assert(this->string != NULL && pos < this->len);
+		/* Sanitize in case we get a position inside an UTF-8 sequence. */
+		while (pos > 0 && IsUtf8Part(this->string[pos])) pos--;
+		return this->cur_pos = pos;
+	}
+
+	virtual size_t Next()
+	{
+		assert(this->string != NULL);
+
+		/* Already at the end? */
+		if (this->cur_pos >= this->len) return END;
+
+		WChar c;
+		this->cur_pos += Utf8Decode(&c, this->string + this->cur_pos);
+		return this->cur_pos;
+	}
+
+	virtual size_t Prev()
+	{
+		assert(this->string != NULL);
+
+		/* Already at the beginning? */
+		if (this->cur_pos == 0) return END;
+
+		return this->cur_pos = Utf8PrevChar(this->string + this->cur_pos) - this->string;
+	}
+};
+
+/* static */ StringIterator *StringIterator::Create()
+{
+	return new DefaultStringIterator();
+}
+
+#endif
new file mode 100644
--- /dev/null
+++ b/src/string_base.h
@@ -0,0 +1,60 @@
+/* $Id$ */
+
+/*
+ * This file is part of OpenTTD.
+ * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
+ * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef STRING_BASE_H
+#define STRING_BASE_H
+
+#include "string_type.h"
+
+/** Class for iterating over different kind of parts of a string. */
+class StringIterator {
+public:
+	/** Sentinel to indicate end-of-iteration. */
+	static const size_t END = SIZE_MAX;
+
+	/**
+	 * Create a new iterator instance.
+	 * @return New iterator instance.
+	 */
+	static StringIterator *Create();
+
+	virtual ~StringIterator() {}
+
+	/**
+	 * Set a new iteration string. Must also be called if the string contents
+	 * changed. The cursor is reset to the start of the string.
+	 * @param s New string.
+	 */
+	virtual void SetString(const char *s) = 0;
+
+	/**
+	 * Change the current string cursor.
+	 * @param p New cursor position.
+	 * @return Actual new cursor position at the next valid character boundary.
+	 * @pre p has to be inside the current string.
+	 */
+	virtual size_t SetCurPosition(size_t pos) = 0;
+
+	/**
+	 * Advance the cursor by one iteration unit.
+	 * @return New cursor position (in bytes) or #END if the cursor is already at the end of the string.
+	 */
+	virtual size_t Next() = 0;
+
+	/**
+	 * Move the cursor back by one iteration unit.
+	 * @return New cursor position (in bytes) or #END if the cursor is already at the start of the string.
+	 */
+	virtual size_t Prev() = 0;
+
+protected:
+	StringIterator() {}
+};
+
+#endif /* STRING_BASE_H */
--- a/src/string_func.h
+++ b/src/string_func.h
@@ -147,6 +147,13 @@
 	return ret;
 }
 
+static inline const char *Utf8PrevChar(const char *s)
+{
+	const char *ret = s;
+	while (IsUtf8Part(*--ret)) {}
+	return ret;
+}
+
 size_t Utf8StringLength(const char *s);
 
 /**
--- a/src/textbuf.cpp
+++ b/src/textbuf.cpp
@@ -87,11 +87,11 @@
 	this->bytes -= len;
 	this->chars--;
 
+	if (backspace) this->caretpos -= len;
+
+	this->UpdateStringIter();
 	this->UpdateWidth();
-	if (backspace) {
-		this->caretpos -= len;
-		this->UpdateCaretPosition();
-	}
+	this->UpdateCaretPosition();
 }
 
 /**
@@ -147,6 +147,7 @@
 	memset(this->buf, 0, this->max_bytes);
 	this->bytes = this->chars = 1;
 	this->pixels = this->caretpos = this->caretxoffs = 0;
+	this->UpdateStringIter();
 }
 
 /**
@@ -163,10 +164,11 @@
 		memmove(this->buf + this->caretpos + len, this->buf + this->caretpos, this->bytes - this->caretpos);
 		Utf8Encode(this->buf + this->caretpos, key);
 		this->chars++;
-		this->bytes  += len;
+		this->bytes    += len;
+		this->caretpos += len;
+
+		this->UpdateStringIter();
 		this->UpdateWidth();
-
-		this->caretpos   += len;
 		this->UpdateCaretPosition();
 		return true;
 	}
@@ -210,6 +212,7 @@
 	assert(this->chars <= this->max_chars);
 	this->buf[this->bytes - 1] = '\0'; // terminating zero
 
+	this->UpdateStringIter();
 	this->UpdateWidth();
 	this->UpdateCaretPosition();
 
@@ -234,11 +237,14 @@
 {
 	assert(this->CanMoveCaretLeft());
 
+	size_t pos = this->char_iter->Prev();
+	if (pos == StringIterator::END) pos = 0;
+
+	this->caretpos = (uint16)pos;
+	this->UpdateCaretPosition();
+
 	WChar c;
-	const char *s = Utf8PrevChar(this->buf + this->caretpos);
-	Utf8Decode(&c, s);
-	this->caretpos    = s - this->buf;
-	this->UpdateCaretPosition();
+	Utf8Decode(&c, this->buf + this->caretpos);
 
 	return c;
 }
@@ -261,14 +267,24 @@
 {
 	assert(this->CanMoveCaretRight());
 
-	WChar c;
-	this->caretpos   += (uint16)Utf8Decode(&c, this->buf + this->caretpos);
+	size_t pos = this->char_iter->Next();
+	if (pos == StringIterator::END) pos = this->bytes - 1;
+
+	this->caretpos = (uint16)pos;
 	this->UpdateCaretPosition();
 
+	WChar c;
 	Utf8Decode(&c, this->buf + this->caretpos);
 	return c;
 }
 
+/** Update the character iter after the text has changed. */
+void Textbuf::UpdateStringIter()
+{
+	this->char_iter->SetString(this->buf);
+	this->caretpos = (uint16)this->char_iter->SetCurPosition(this->caretpos);
+}
+
 /** Update pixel width of the text. */
 void Textbuf::UpdateWidth()
 {
@@ -372,6 +388,8 @@
 	assert(max_bytes != 0);
 	assert(max_chars != 0);
 
+	this->char_iter = StringIterator::Create();
+
 	this->afilter    = CS_ALPHANUMERAL;
 	this->max_bytes  = max_bytes;
 	this->max_chars  = max_chars == UINT16_MAX ? max_bytes : max_chars;
@@ -381,6 +399,7 @@
 
 Textbuf::~Textbuf()
 {
+	delete this->char_iter;
 	free(this->buf);
 }
 
@@ -437,6 +456,7 @@
 	assert(this->chars <= this->max_chars);
 
 	this->caretpos = this->bytes - 1;
+	this->UpdateStringIter();
 	this->UpdateWidth();
 
 	this->UpdateCaretPosition();
--- a/src/textbuf_type.h
+++ b/src/textbuf_type.h
@@ -14,6 +14,7 @@
 
 #include "string_type.h"
 #include "strings_type.h"
+#include "string_base.h"
 
 /**
  * Return values for Textbuf::HandleKeypress
@@ -61,6 +62,8 @@
 	void UpdateSize();
 
 private:
+	StringIterator *char_iter;
+
 	bool CanDelChar(bool backspace);
 	WChar GetNextDelChar(bool backspace);
 	void DelChar(bool backspace);
@@ -69,6 +72,7 @@
 	bool CanMoveCaretRight();
 	WChar MoveCaretRight();
 
+	void UpdateStringIter();
 	void UpdateWidth();
 	void UpdateCaretPosition();
 };