← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/rtl_wordwrap into lp:widelands

 

GunChleoc has proposed merging lp:~widelands-dev/widelands/rtl_wordwrap into lp:widelands.

Requested reviews:
  Widelands Developers (widelands-dev)

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/rtl_wordwrap/+merge/273968

Wordwrap now uses the new font renderer, which fixes positioning bugs for Right-to-left languages. I also implemented the line break rules for Japanese into WordWrap. You can see it in action e.g. in the map descriptions when selecting a map for a new game.

TextLayout now has a new estimate function (calc_width_for_wrapping) for glyph width, as the old one was too inaccurate for estimating the width of the lines for Arabic. We still need an estimate function rather than getting the exact width, because rendering the textures just for an estimate is way too slow (takes about 100x longer). The old function (calc_bare_width) is still used in RichText for now, so I have left it in.

There is one bug left in the MultilineEditbox for Arabic - the cursor isn't shown in the correct position. You can see this bug when editing the Map Options. Everything should be fine for RTL languages like Latin script though. I have added a TODO comment for this bug.
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/rtl_wordwrap into lp:widelands.
=== modified file 'src/graphic/font.cc'
--- src/graphic/font.cc	2015-09-22 07:21:52 +0000
+++ src/graphic/font.cc	2015-10-09 11:02:48 +0000
@@ -52,11 +52,12 @@
 /**
  * Open a font file and load the corresponding font.
  */
-Font::Font(const std::string & name, int size)
+Font::Font(const std::string & name, int input_size)
 {
 	// Load the TrueType Font
 	std::string filename = "i18n/fonts/";
 	filename += name;
+	m_size = input_size;
 
 	//  We must keep this File Read open, otherwise the following calls are
 	//  crashing. do not know why...
@@ -66,7 +67,7 @@
 	if (!ops)
 		throw wexception("could not load font!: RWops Pointer invalid");
 
-	m_font = TTF_OpenFontIndexRW(ops, 1, size, 0);
+	m_font = TTF_OpenFontIndexRW(ops, 1, input_size, 0);
 	if (!m_font)
 		throw wexception("could not load font!: %s", TTF_GetError());
 
@@ -108,6 +109,14 @@
 }
 
 /**
+ * \return the maximum height of glyphs of this font. NOCOM
+ */
+uint32_t Font::size() const
+{
+	return m_size;
+}
+
+/**
  * \return the maximum ascent from the font baseline
  */
 uint32_t Font::ascent() const

=== modified file 'src/graphic/font.h'
--- src/graphic/font.h	2014-11-27 12:02:08 +0000
+++ src/graphic/font.h	2015-10-09 11:02:48 +0000
@@ -45,6 +45,7 @@
 	static void shutdown();
 	static Font * get(const std::string & name, int size);
 
+	uint32_t size() const;
 	uint32_t ascent() const;
 	uint32_t height() const;
 	uint32_t lineskip() const;
@@ -64,6 +65,8 @@
 	 */
 	int32_t m_computed_typical_miny;
 	int32_t m_computed_typical_maxy;
+
+	int m_size;
 };
 
 } // namespace UI

=== modified file 'src/graphic/text/bidi.cc'
--- src/graphic/text/bidi.cc	2015-09-28 18:47:57 +0000
+++ src/graphic/text/bidi.cc	2015-10-09 11:02:48 +0000
@@ -22,10 +22,10 @@
 #include <map>
 #include <string>
 
-#include <unicode/unistr.h>
 #include <unicode/utypes.h>
 
 #include "base/log.h"
+#include "graphic/text/font_set.h"
 
 namespace {
 // TODO(GunChleoc): Have a look at the ICU API to see which helper functions can be gained from there.
@@ -441,17 +441,6 @@
 };
 
 
-// Helper to printf ICU strings for testing
-const char* icustring2char(icu::UnicodeString convertme) {
-	std::string result;
-	convertme.toUTF8String(result);
-	return result.c_str();
-}
-const char* icuchar2char(UChar convertme) {
-	icu::UnicodeString temp(convertme);
-	return icustring2char(temp);
-}
-
 const std::set<std::string> kRTLScripts = {
 	{"arabic", "devanagari", "hebrew", "mandaic", "nko", "samaritan", "syriac", "thaana"},
 };
@@ -722,6 +711,17 @@
 	return result;
 }
 
+// Helper to convert ICU strings to C++ strings
+std::string icustring2string(const icu::UnicodeString& convertme) {
+	std::string result;
+	convertme.toUTF8String(result);
+	return result;
+}
+std::string icuchar2string(const UChar& convertme) {
+	const icu::UnicodeString temp(convertme);
+	return icustring2string(temp);
+}
+
 // True if a string contains a character from a CJK code block
 bool has_cjk_character(const char* input) {
 	bool result = false;
@@ -757,11 +757,16 @@
 }
 
 bool cannot_start_line(const UChar& c) {
-	return kCannottStartLineJapanese.count(c) == 1;
+	return is_diacritic(c) || is_punctuation_char(c) || kCannottStartLineJapanese.count(c) == 1;
 }
 
 bool cannot_end_line(const UChar& c) {
 	return kCannotEndLineJapanese.count(c) == 1;
 }
 
+bool is_diacritic(const UChar& c) {
+	return kArabicDiacritics.count(c) == 1;
+}
+
+
 } // namespace UI

=== modified file 'src/graphic/text/bidi.h'
--- src/graphic/text/bidi.h	2015-09-28 18:47:57 +0000
+++ src/graphic/text/bidi.h	2015-10-09 11:02:48 +0000
@@ -24,19 +24,21 @@
 #include <vector>
 
 #include <unicode/uchar.h>
-
-#include "graphic/text/font_set.h"
+#include <unicode/unistr.h>
 
 // BiDi support for RTL languages
 namespace i18n {
-	std::string make_ligatures(const char* input);
-	std::string line2bidi(const char* input);
-	std::vector<std::string> split_cjk_word(const char* input);
-	bool has_rtl_character(const char* input);
-	bool has_rtl_character(std::vector<std::string> input);
-	bool has_cjk_character(const char* input);
-	bool cannot_start_line(const UChar& c);
-	bool cannot_end_line(const UChar& c);
+std::string make_ligatures(const char* input);
+std::string line2bidi(const char* input);
+std::vector<std::string> split_cjk_word(const char* input);
+bool has_rtl_character(const char* input);
+bool has_rtl_character(std::vector<std::string> input);
+std::string icustring2string(const UnicodeString& convertme);
+std::string icuchar2string(const UChar& convertme);
+bool has_cjk_character(const char* input);
+bool cannot_start_line(const UChar& c);
+bool cannot_end_line(const UChar& c);
+bool is_diacritic(const UChar& c);
 
 } // namespace UI
 

=== modified file 'src/graphic/text/rt_parse.cc'
--- src/graphic/text/rt_parse.cc	2014-09-09 17:15:20 +0000
+++ src/graphic/text/rt_parse.cc	2015-10-09 11:02:48 +0000
@@ -25,6 +25,7 @@
 #include <vector>
 
 #include <SDL.h>
+#include <boost/algorithm/string.hpp>
 #include <boost/format.hpp>
 
 #include "graphic/text/rt_errors_impl.h"
@@ -138,8 +139,11 @@
 		size_t line = ts.line(), col = ts.col();
 		std::string text = ts.till_any("<");
 		if (text != "") {
-			if (!tc.text_allowed)
+			if (!tc.text_allowed) {
 				throw SyntaxErrorImpl(line, col, "no text, as only tags are allowed here", text, ts.peek(100));
+			}
+			boost::replace_all(text, "&gt;", ">");
+			boost::replace_all(text, "&lt;", "<");
 			m_childs.push_back(new Child(text));
 		}
 
@@ -181,6 +185,7 @@
 		tc.allowed_attrs.insert("padding_b");
 		tc.allowed_attrs.insert("padding_t");
 		tc.allowed_attrs.insert("db_show_spaces");
+		tc.allowed_attrs.insert("keep_spaces"); // Keeps blank spaces intact for text editing
 		tc.allowed_attrs.insert("background");
 
 		tc.allowed_childs.insert("p");

=== modified file 'src/graphic/text/rt_render.cc'
--- src/graphic/text/rt_render.cc	2015-09-28 18:47:57 +0000
+++ src/graphic/text/rt_render.cc	2015-10-09 11:02:48 +0000
@@ -400,7 +400,7 @@
 	Texture* render(TextureCache* texture_cache) override {
 		if (m_show_spaces) {
 			Texture* rv = new Texture(m_w, m_h);
-			fill_rect(Rect(0, 0, m_w, m_h), RGBAColor(0xff, 0, 0, 0xff), rv);
+			fill_rect(Rect(0, 0, m_w, m_h), RGBAColor(0xcc, 0, 0, 0xcc), rv);
 			return rv;
 		}
 		return TextNode::render(texture_cache);
@@ -711,17 +711,26 @@
 	if (i18n::has_rtl_character(txt.c_str())) {
 		std::string previous_word;
 		std::vector<RenderNode*>::iterator it = text_nodes.begin();
+		std::vector<WordSpacerNode*> spacer_nodes;
 
 		// Collect the word nodes
 		while (ts.pos() < txt.size()) {
+			std::size_t cpos = ts.pos();
 			ts.skip_ws();
+			spacer_nodes.clear();
+
+			// We only know if the spacer goes to the left or right after having a look at the current word.
+			for (uint16_t ws_indx = 0; ws_indx < ts.pos() - cpos; ws_indx++) {
+				spacer_nodes.push_back(new WordSpacerNode(font_cache_.get_font(&ns), ns));
+			}
+
 			word = ts.till_any_or_end(" \t\n\r");
 			if (!word.empty()) {
 				bool word_is_bidi = i18n::has_rtl_character(word.c_str());
 				word = i18n::make_ligatures(word.c_str());
 				if (word_is_bidi || i18n::has_rtl_character(previous_word.c_str())) {
-					if (!previous_word.empty()) {
-						text_nodes.insert(text_nodes.begin(), new WordSpacerNode(font_cache_.get_font(&ns), ns));
+					for (WordSpacerNode* spacer: spacer_nodes) {
+						it = text_nodes.insert(text_nodes.begin(), spacer);
 					}
 					if (word_is_bidi) {
 						word = i18n::line2bidi(word.c_str());
@@ -732,8 +741,11 @@
 					if (it < text_nodes.end()) {
 						++it;
 					}
-					if (!previous_word.empty()) {
-						it = text_nodes.insert(it, new WordSpacerNode(font_cache_.get_font(&ns), ns));
+					for (WordSpacerNode* spacer: spacer_nodes) {
+						it = text_nodes.insert(it, spacer);
+						if (it < text_nodes.end()) {
+							++it;
+						}
 					}
 					it = text_nodes.insert(it, new TextNode(font_cache_.get_font(&ns), ns, word));
 				}
@@ -749,7 +761,7 @@
 		while (ts.pos() < txt.size()) {
 			std::size_t cpos = ts.pos();
 			ts.skip_ws();
-			if (ts.pos() != cpos) {
+			for (uint16_t ws_indx = 0; ws_indx < ts.pos() - cpos; ws_indx++) {
 				nodes.push_back(new WordSpacerNode(font_cache_.get_font(&ns), ns));
 			}
 			word = ts.till_any_or_end(" \t\n\r");
@@ -1072,8 +1084,9 @@
 			else if (align == "center" || align == "middle") m_rn->set_valign(UI::Align::Align_Center);
 		}
 	}
-private:
+protected:
 	bool shrink_to_fit_;
+private:
 	uint16_t m_w;
 	SubTagRenderNode* m_rn;
 };
@@ -1089,6 +1102,7 @@
 	void handle_unique_attributes() override {
 		const AttrMap& a = m_tag.attrs();
 		WordSpacerNode::show_spaces(a.has("db_show_spaces") ? a["db_show_spaces"].get_bool() : 0);
+		shrink_to_fit_ = shrink_to_fit_ && (a.has("keep_spaces") ? !a["keep_spaces"].get_bool() : true);
 	}
 };
 

=== modified file 'src/graphic/text_layout.cc'
--- src/graphic/text_layout.cc	2015-09-24 18:45:27 +0000
+++ src/graphic/text_layout.cc	2015-10-09 11:02:48 +0000
@@ -22,6 +22,7 @@
 #include <map>
 
 #include <SDL_ttf.h>
+#include <boost/algorithm/string.hpp>
 #include <boost/format.hpp>
 
 #include "base/utf8.h"
@@ -30,12 +31,19 @@
 #include "graphic/text/font_set.h"
 #include "graphic/text_constants.h"
 
+std::string richtext_escape(const std::string& given_text) {
+	std::string text = given_text;
+	boost::replace_all(text, ">", "&gt;");
+	boost::replace_all(text, "<", "&lt;");
+	return text;
+}
+
 std::string as_game_tip(const std::string& txt) {
 	static boost::format f
 		("<rt padding_l=48 padding_t=28 padding_r=48 padding_b=28>"
 		 "<p align=center><font color=21211b face=serif size=16>%s</font></p></rt>");
 
-	f % txt;
+	f % richtext_escape(txt);
 	return f.str();
 }
 
@@ -43,7 +51,7 @@
 	static boost::format f("<rt><p><font face=serif size=13 bold=1 color=%s>%s</font></p></rt>");
 
 	f % UI_FONT_CLR_FG.hex_value();
-	f % txt;
+	f % richtext_escape(txt);
 	return f.str();
 }
 std::string as_uifont(const std::string & txt, int size, const RGBColor& clr) {
@@ -53,7 +61,7 @@
 
 	f % size;
 	f % clr.hex_value();
-	f % txt;
+	f % richtext_escape(txt);
 	return f.str();
 }
 
@@ -62,7 +70,7 @@
 
 	f % UI_FONT_SIZE_SMALL;
 	f % UI_FONT_TOOLTIP_CLR.hex_value();
-	f % txt;
+	f % richtext_escape(txt);
 	return f.str();
 }
 
@@ -70,7 +78,7 @@
 	static boost::format f
 		("<rt><p><font face=condensed size=10 bold=0 color=%s>%s</font></p></rt>");
 	f % UI_FONT_TOOLTIP_CLR.hex_value();
-	f % txt;
+	f % richtext_escape(txt);
 	return f.str();
 }
 
@@ -94,6 +102,31 @@
 }
 
 /**
+ * Get a width estimate for text wrapping.
+ */
+uint32_t TextStyle::calc_width_for_wrapping(const UChar& c) const {
+	int result = 0;
+	TTF_GlyphMetrics(font->get_ttf_font(), c, nullptr, nullptr, nullptr, nullptr, &result);
+	return result;
+}
+
+/**
+ * Get a width estimate for text wrapping.
+ */
+uint32_t TextStyle::calc_width_for_wrapping(const std::string & text) const
+{
+	int result = 0;
+	const icu::UnicodeString parseme(text.c_str());
+	for (int i = 0; i < parseme.length(); ++i) {
+		UChar c = parseme.charAt(i);
+		if (!i18n::is_diacritic(c)) {
+			result += calc_width_for_wrapping(c);
+		}
+	}
+	return result;
+}
+
+/**
  * Compute the bare width (without caret padding) of the given string.
  */
 uint32_t TextStyle::calc_bare_width(const std::string & text) const

=== modified file 'src/graphic/text_layout.h'
--- src/graphic/text_layout.h	2014-12-06 12:22:35 +0000
+++ src/graphic/text_layout.h	2015-10-09 11:02:48 +0000
@@ -21,6 +21,7 @@
 #define WL_GRAPHIC_TEXT_LAYOUT_H
 
 #include <string>
+#include <unicode/uchar.h>
 
 #include "graphic/font.h"
 #include "graphic/color.h"
@@ -34,6 +35,11 @@
 }
 
 /**
+ * Escapes reserved characters for Richtext.
+ */
+std::string richtext_escape(const std::string& given_text);
+
+/**
  * Convenience functions to convert simple text into a valid block
  * of rich text which can be rendered.
  */
@@ -71,6 +77,8 @@
 	static const TextStyle & ui_big();
 	static const TextStyle & ui_small();
 	uint32_t calc_bare_width(const std::string & text) const;
+	uint32_t calc_width_for_wrapping(const UChar& c) const;
+	uint32_t calc_width_for_wrapping(const std::string & text) const;
 	void calc_bare_height_heuristic(const std::string & text, int32_t & miny, int32_t & maxy) const;
 	void setup() const;
 

=== modified file 'src/graphic/wordwrap.cc'
--- src/graphic/wordwrap.cc	2015-09-28 06:41:58 +0000
+++ src/graphic/wordwrap.cc	2015-10-09 11:02:48 +0000
@@ -23,12 +23,42 @@
 
 #include "graphic/wordwrap.h"
 
+#include <boost/format.hpp>
+#include <unicode/uchar.h>
+#include <unicode/unistr.h>
+
 #include "base/log.h"
-#include "graphic/font_handler.h"
 #include "graphic/font_handler1.h"
+#include "graphic/graphic.h"
 #include "graphic/rendertarget.h"
 #include "graphic/text/bidi.h"
 
+namespace {
+std::string as_editorfont(const std::string& text,
+								  int ptsize = UI_FONT_SIZE_SMALL,
+								  const RGBColor& clr = UI_FONT_CLR_FG) {
+	// UI Text is always bold due to historic reasons
+	static boost::format
+			f("<rt keep_spaces=1><p><font face=serif size=%i bold=1 shadow=1 color=%s>%s</font></p></rt>");
+	f % ptsize;
+	f % clr.hex_value();
+	f % richtext_escape(text);
+	return f.str();
+}
+
+// This is inefficient; only call when we need the exact width.
+uint32_t text_width(const std::string& text, int ptsize) {
+	return UI::g_fh1->render(as_editorfont(text, ptsize - UI::g_fh1->fontset().size_offset()))->width();
+}
+
+// This is inefficient; only call when we need the exact height.
+uint32_t text_height(const std::string& text, int ptsize) {
+	return UI::g_fh1->render(as_editorfont(text.empty() ? "." : text,
+														ptsize - UI::g_fh1->fontset().size_offset()))->height();
+}
+
+} // namespace
+
 namespace UI {
 
 /**
@@ -36,12 +66,12 @@
  * and a default-constructed text style.
  */
 WordWrap::WordWrap() :
-	m_wrapwidth(std::numeric_limits<uint32_t>::max())
+	m_wrapwidth(std::numeric_limits<uint32_t>::max()), m_draw_caret(false), mode_(WordWrap::Mode::kDisplay)
 {
 }
 
 WordWrap::WordWrap(const TextStyle & style, uint32_t gwrapwidth) :
-	m_style(style)
+	m_style(style), m_draw_caret(false), mode_(WordWrap::Mode::kDisplay)
 {
 	m_wrapwidth = gwrapwidth;
 
@@ -82,11 +112,9 @@
  * Perform the wrapping computations for the given text and fill in
  * the private data containing the wrapped results.
  */
-void WordWrap::wrap(const std::string & text)
+void WordWrap::wrap(const std::string & text, WordWrap::Mode mode)
 {
-	//static int count = 0;
-	//log("word_wrap_text(%u): %i\n", m_wrapwidth, ++count);
-
+	mode_ = mode;
 	m_lines.clear();
 
 	std::string::size_type line_start = 0;
@@ -117,6 +145,11 @@
 	 std::string::size_type & line_end,
 	 std::string::size_type & next_line_start)
 {
+	int32_t minimum_chars = 1; // So we won't get empty lines
+	// Keep lines from getting too wide, because calc_width_for_wrapping is not exact.
+	uint32_t margin = m_style.calc_width_for_wrapping(0x2003); // Em space
+	assert(m_wrapwidth > margin);
+
 	std::string::size_type orig_end = text.find('\n', line_start);
 	if (orig_end == std::string::npos)
 		orig_end = text.size();
@@ -131,9 +164,9 @@
 
 
 	// Optimism: perhaps the entire line fits?
-	// TODO(GunChleoc): Arabic: Multiple calls of make_ligatures are inefficient.
-	if (m_style.calc_bare_width(i18n::make_ligatures(text.substr(line_start, orig_end - line_start).c_str()))
-		 <= m_wrapwidth) {
+	if (m_style.calc_width_for_wrapping(
+			 i18n::make_ligatures(text.substr(line_start, orig_end - line_start).c_str()))
+		 <= m_wrapwidth - margin) {
 		line_end = orig_end;
 		next_line_start = orig_end + 1;
 		return;
@@ -150,8 +183,9 @@
 	while (end_upper - end_lower > 4) {
 		std::string::size_type mid = end_lower + (end_upper - end_lower + 1) / 2;
 
-		if (m_style.calc_bare_width(i18n::make_ligatures(text.substr(line_start, mid - line_start).c_str()))
-			 <= m_wrapwidth) {
+		if (m_style.calc_width_for_wrapping(
+				 i18n::make_ligatures(text.substr(line_start, mid - line_start).c_str()))
+			 <= m_wrapwidth - margin) {
 			end_lower = mid;
 		} else {
 			end_upper = mid - 1;
@@ -171,9 +205,9 @@
 			break; // we already know that this cannot possibly fit
 
 		// check whether the next word still fits
-		if (m_style.calc_bare_width(
+		if (m_style.calc_width_for_wrapping(
 				 i18n::make_ligatures(text.substr(line_start, nextspace - line_start).c_str()))
-			 > m_wrapwidth)
+			 > m_wrapwidth - margin)
 			break;
 
 		space = nextspace;
@@ -185,20 +219,43 @@
 		return;
 	}
 
-	// Nasty special case: the line starts with a single word that is too big to fit
-	// Continue the binary search until we narrowed down exactly how many characters fit
-	while (end_upper > end_lower) {
-		std::string::size_type mid = end_lower + (end_upper - end_lower + 1) / 2;
-
-		if (m_style.calc_bare_width(i18n::make_ligatures(text.substr(line_start, mid - line_start).c_str()))
-			 <= m_wrapwidth) {
-			end_lower = mid;
-		} else {
-			end_upper = mid - 1;
+
+	// The line didn't fit.
+	// We just do a linear search ahead until we hit the max.
+	// Operating on single glyphs will keep the texture cache small, and we won't need to call make_ligatures.
+
+	const icu::UnicodeString unicode_word(text.substr(line_start, orig_end).c_str());
+	uint32_t line_width = 0;
+	int32_t end = -1;
+	icu::UnicodeString unicode_line;
+
+	while ((line_width < (m_wrapwidth - margin)) && (end < unicode_word.length())) {
+		++end;
+		UChar c = unicode_word.charAt(end);
+		// Diacritics do not add to the line width
+		if (!i18n::is_diacritic(c)) {
+			line_width += m_style.calc_width_for_wrapping(c);
 		}
-	}
-
-	next_line_start = line_end = end_lower;
+		unicode_line += c;
+	}
+
+	// Find last space
+	int32_t last_space = unicode_line.lastIndexOf(0x0020); // space character
+	if (last_space > minimum_chars) {
+		end = last_space;
+	}
+
+	// Make sure that diacritics stay with their base letters, and that
+	// start/end line rules are being followed.
+	while (end > minimum_chars &&
+			 (i18n::cannot_start_line(unicode_line.charAt(end)) ||
+			  i18n::cannot_end_line(unicode_line.charAt(end - 1)))) {
+		--end;
+	}
+	assert(end > 0);
+
+	next_line_start = line_end =
+			(i18n::icustring2string(unicode_word.tempSubString(0, end)).size() + line_start);
 }
 
 
@@ -212,7 +269,7 @@
 	uint32_t calculated_width = 0;
 
 	for (uint32_t line = 0; line < m_lines.size(); ++line) {
-		uint32_t linewidth = m_style.calc_bare_width(m_lines[line].text);
+		uint32_t linewidth = text_width(m_lines[line].text, m_style.font->size());
 		if (linewidth > calculated_width)
 			calculated_width = linewidth;
 	}
@@ -225,10 +282,12 @@
  */
 uint32_t WordWrap::height() const
 {
-	uint16_t fontheight = m_style.font->height();
-	uint32_t lineskip = m_style.font->lineskip();
+	uint16_t fontheight = 0;
+	if (!m_lines.empty()) {
+		fontheight = text_height(m_lines[0].text, m_style.font->size());
+	}
 
-	return fontheight + (m_lines.size() - 1) * lineskip;
+	return fontheight * (m_lines.size()) + 2 * LINE_MARGIN;
 }
 
 /**
@@ -273,8 +332,8 @@
  */
 void WordWrap::draw(RenderTarget & dst, Point where, Align align, uint32_t caret)
 {
-	uint16_t fontheight = m_style.font->height();
-	uint32_t lineskip = m_style.font->lineskip();
+	if (m_lines.empty()) return;
+
 	uint32_t caretline, caretpos;
 
 	calc_wrapped_pos(caret, caretline, caretpos);
@@ -289,22 +348,44 @@
 	}
 
 	++where.y;
-	for (uint32_t line = 0; line < m_lines.size(); ++line, where.y += lineskip) {
+
+	Align alignment = mirror_alignment(align);
+
+	uint16_t fontheight = text_height(m_lines[0].text, m_style.font->size());
+	for (uint32_t line = 0; line < m_lines.size(); ++line, where.y += fontheight) {
 		if (where.y >= dst.height() || int32_t(where.y + fontheight) <= 0)
 			continue;
 
-		// Right-align text for RTL languages
-		// TODO(GunChleoc): Arabic: we have a ragged edge here for Arabic,
-		// just like in richtext.cc - bug in TTF_SizeUTF8?
-		Point drawpos(UI::g_fh1->fontset().is_rtl() ?
-							  where.x + m_wrapwidth
-							  - m_style.calc_bare_width(i18n::make_ligatures(m_lines[line].text.c_str())) - 2 :
-							  where.x,
-						  where.y);
-
-		g_fh->draw_text
-			(dst, m_style, drawpos, m_lines[line].text.c_str(), Align(align & Align_Horizontal),
-			 line == caretline ? caretpos : std::numeric_limits<uint32_t>::max());
+		Point point(where.x, where.y);
+
+		if (alignment & Align_Right) {
+			point.x += m_wrapwidth - LINE_MARGIN;
+		} else if (alignment & Align_HCenter) {
+			point.x += m_wrapwidth / 2;
+		}
+
+		const Image* entry_text_im =
+				UI::g_fh1->render(mode_ == WordWrap::Mode::kDisplay ?
+											as_uifont(m_lines[line].text,
+														 m_style.font->size() - UI::g_fh1->fontset().size_offset(),
+														 m_style.fg) :
+											as_editorfont(m_lines[line].text,
+															  m_style.font->size() - UI::g_fh1->fontset().size_offset(),
+															  m_style.fg));
+		UI::correct_for_align(alignment, entry_text_im->width(), fontheight, &point);
+		dst.blit(point, entry_text_im);
+
+		if (mode_ == WordWrap::Mode::kEditor && m_draw_caret && line == caretline) {
+			std::string line_to_caret = m_lines[line].text.substr(0, caretpos);
+			// TODO(GunChleoc): Arabic: Fix cursor position for BIDI text.
+			int caret_x = text_width(line_to_caret, m_style.font->size());
+
+			const Image* caret_image = g_gr->images().get("pics/caret.png");
+			Point caretpt;
+			caretpt.x = point.x + caret_x - caret_image->width() + LINE_MARGIN;
+			caretpt.y = point.y + (fontheight - caret_image->height()) / 2;
+			dst.blit(caretpt, caret_image);
+		}
 	}
 }
 

=== modified file 'src/graphic/wordwrap.h'
--- src/graphic/wordwrap.h	2014-11-27 12:02:08 +0000
+++ src/graphic/wordwrap.h	2015-10-09 11:02:48 +0000
@@ -33,6 +33,11 @@
  * Helper struct that provides word wrapping and related functionality.
  */
 struct WordWrap {
+	enum class Mode {
+		kDisplay,
+		kEditor
+	};
+
 	WordWrap();
 	WordWrap(const TextStyle & style, uint32_t wrapwidth = std::numeric_limits<uint32_t>::max());
 
@@ -41,10 +46,11 @@
 
 	uint32_t wrapwidth() const;
 
-	void wrap(const std::string & text);
+	void wrap(const std::string & text, WordWrap::Mode mode = WordWrap::Mode::kDisplay);
 
 	uint32_t width() const;
 	uint32_t height() const;
+	void set_draw_caret(bool draw_it) {m_draw_caret = draw_it;}
 
 	void draw
 		(RenderTarget & dst, Point where, Align align = Align_Left,
@@ -60,7 +66,7 @@
 		std::string text;
 
 		/// Starting offset of this line within the original un-wrapped text
-		uint32_t start;
+		size_t start;
 	};
 
 	void compute_end_of_line
@@ -71,6 +77,9 @@
 
 	TextStyle m_style;
 	uint32_t m_wrapwidth;
+	bool m_draw_caret;
+
+	WordWrap::Mode mode_;
 
 	std::vector<LineData> m_lines;
 };

=== modified file 'src/ui_basic/multilineeditbox.cc'
--- src/ui_basic/multilineeditbox.cc	2015-10-05 06:53:34 +0000
+++ src/ui_basic/multilineeditbox.cc	2015-10-09 11:02:48 +0000
@@ -221,6 +221,7 @@
 
 	do {
 		--cursor;
+		// TODO(GunChleoc): When switchover to g_fh1 is complete, see if we can go full ICU here.
 	} while (cursor > 0 && Utf8::is_utf8_extended(text[cursor]));
 
 	return cursor;
@@ -483,6 +484,8 @@
 
 	d->refresh_ww();
 
+	d->ww.set_draw_caret(has_focus());
+
 	d->ww.draw
 		(dst, Point(0, -int32_t(d->scrollbar.get_scrollpos())), Align_Left,
 		 has_focus() ? d->cursor_pos : std::numeric_limits<uint32_t>::max());
@@ -564,7 +567,7 @@
 	ww.set_style(textstyle);
 	ww.set_wrapwidth(owner.get_w() - ms_scrollbar_w);
 
-	ww.wrap(text);
+	ww.wrap(text, WordWrap::Mode::kEditor);
 	ww_valid = true;
 
 	int32_t textheight = ww.height();

=== modified file 'src/ui_basic/multilinetextarea.cc'
--- src/ui_basic/multilinetextarea.cc	2014-11-27 16:43:37 +0000
+++ src/ui_basic/multilinetextarea.cc	2015-10-09 11:02:48 +0000
@@ -117,28 +117,36 @@
 {
 	uint32_t height;
 
-	if (m_text.compare(0, 3, "<rt")) {
-		m->isrichtext = false;
-		m->ww.set_wrapwidth(get_eff_w());
-		m->ww.wrap(m_text);
-		height = m->ww.height();
-	} else {
-		m->isrichtext = true;
-		m->rt.set_width(get_eff_w() - 2 * RICHTEXT_MARGIN);
-		m->rt.parse(m_text);
-		height = m->rt.height() + 2 * RICHTEXT_MARGIN;
+	// We wrap the text twice. We need to do this to account for the presence/absence of the scollbar.
+	bool scroolbar_was_enabled = m_scrollbar.is_enabled();
+	for (int i = 0; i < 2; ++i) {
+		if (m_text.compare(0, 3, "<rt")) {
+			m->isrichtext = false;
+			m->ww.set_wrapwidth(get_eff_w());
+			m->ww.wrap(m_text);
+			height = m->ww.height();
+		} else {
+			m->isrichtext = true;
+			m->rt.set_width(get_eff_w() - 2 * RICHTEXT_MARGIN);
+			m->rt.parse(m_text);
+			height = m->rt.height() + 2 * RICHTEXT_MARGIN;
+		}
+
+		bool setbottom = false;
+
+		if (m_scrollmode == ScrollLog)
+			if (m_scrollbar.get_scrollpos() >= m_scrollbar.get_steps() - 1)
+				setbottom = true;
+
+		m_scrollbar.set_steps(height - get_h());
+		if (setbottom)
+			m_scrollbar.set_scrollpos(height - get_h());
+
+		if (m_scrollbar.is_enabled() == scroolbar_was_enabled) {
+			break; // No need to wrap twice.
+		}
 	}
 
-	bool setbottom = false;
-
-	if (m_scrollmode == ScrollLog)
-		if (m_scrollbar.get_scrollpos() >= m_scrollbar.get_steps() - 1)
-			setbottom = true;
-
-	m_scrollbar.set_steps(height - get_h());
-	if (setbottom)
-		m_scrollbar.set_scrollpos(height - get_h());
-
 	update(0, 0, get_eff_w(), get_h());
 }
 

=== modified file 'src/ui_basic/multilinetextarea.h'
--- src/ui_basic/multilinetextarea.h	2014-10-28 12:53:29 +0000
+++ src/ui_basic/multilinetextarea.h	2015-10-09 11:02:48 +0000
@@ -57,7 +57,7 @@
 	void set_font(std::string name, int32_t size, RGBColor fg);
 
 	uint32_t scrollbar_w() const {return 24;}
-	uint32_t get_eff_w() const {return get_w() - scrollbar_w();}
+	uint32_t get_eff_w() const {return m_scrollbar.is_enabled() ? get_w() - scrollbar_w() : get_w();}
 
 	void set_color(RGBColor fg) {m_fcolor = fg;}
 


Follow ups