1 // -*- Mode: C++; tab-width:2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 // vi:tw=80:et:ts=2:sts=2
3 //
4 // -----------------------------------------------------------------------
5 //
6 // This file is part of RLVM, a RealLive virtual machine clone.
7 //
8 // -----------------------------------------------------------------------
9 //
10 // Copyright (C) 2006, 2007 Elliot Glaysher
11 //
12 // This program is free software; you can redistribute it and/or modify
13 // it under the terms of the GNU General Public License as published by
14 // the Free Software Foundation; either version 3 of the License, or
15 // (at your option) any later version.
16 //
17 // This program is distributed in the hope that it will be useful,
18 // but WITHOUT ANY WARRANTY; without even the implied warranty of
19 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 // GNU General Public License for more details.
21 //
22 // You should have received a copy of the GNU General Public License
23 // along with this program; if not, write to the Free Software
24 // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
25 //
26 // -----------------------------------------------------------------------
27 
28 #include "systems/base/text_window.h"
29 
30 #include <algorithm>
31 #include <iomanip>
32 #include <ostream>
33 #include <string>
34 #include <utility>
35 #include <vector>
36 
37 #include "libreallive/defs.h"
38 #include "libreallive/gameexe.h"
39 #include "machine/rlmachine.h"
40 #include "systems/base/graphics_system.h"
41 #include "systems/base/selection_element.h"
42 #include "systems/base/sound_system.h"
43 #include "systems/base/surface.h"
44 #include "systems/base/system.h"
45 #include "systems/base/system_error.h"
46 #include "systems/base/text_system.h"
47 #include "systems/base/text_waku.h"
48 #include "utf8cpp/utf8.h"
49 #include "utilities/exception.h"
50 #include "utilities/graphics.h"
51 #include "utilities/string_utilities.h"
52 
53 using std::bind;
54 using std::endl;
55 using std::ostringstream;
56 using std::ref;
57 using std::setfill;
58 using std::setw;
59 using std::unique_ptr;
60 using std::vector;
61 using std::placeholders::_1;
62 using std::placeholders::_2;
63 
64 struct TextWindow::FaceSlot {
FaceSlotTextWindow::FaceSlot65   explicit FaceSlot(const std::vector<int>& vec)
66       : x(vec.at(0)),
67         y(vec.at(1)),
68         behind(vec.at(2)),
69         hide_other_windows(vec.at(3)),
70         unknown(vec.at(4)) {}
71 
72   int x, y;
73 
74   // 0 if layered in front or window background. 1 if behind.
75   int behind;
76 
77   // Speculation: This makes ALMA work correctly and doesn't appear to harm
78   // P_BRIDE.
79   int hide_other_windows;
80 
81   // Unknown.
82   int unknown;
83 
84   // The current face loaded. NULL whenever no face is loaded.
85   std::shared_ptr<const Surface> face_surface;
86 };
87 
88 // -----------------------------------------------------------------------
89 // TextWindow
90 // -----------------------------------------------------------------------
91 
TextWindow(System & system,int window_num)92 TextWindow::TextWindow(System& system, int window_num)
93     : window_num_(window_num),
94       text_insertion_point_x_(0),
95       text_insertion_point_y_(0),
96       text_wrapping_point_x_(0),
97       ruby_begin_point_(-1),
98       current_line_number_(0),
99       current_indentation_in_pixels_(0),
100       current_indentation_in_chars_(0),
101       last_token_was_name_(false),
102       use_indentation_(0),
103       colour_(),
104       filter_(0),
105       is_visible_(0),
106       in_selection_mode_(0),
107       next_char_italic_(false),
108       system_(system),
109       text_system_(system.text()) {
110   Gameexe& gexe = system.gameexe();
111 
112   // POINT
113   Size size = GetScreenSize(gexe);
114   screen_width_ = size.width();
115   screen_height_ = size.height();
116 
117   // Base form for everything to follow.
118   GameexeInterpretObject window(gexe("WINDOW", window_num));
119 
120   // Handle: #WINDOW.index.ATTR_MOD, #WINDOW_ATTR, #WINDOW.index.ATTR
121   window_attr_mod_ = window("ATTR_MOD");
122   if (window_attr_mod_ == 0)
123     SetRGBAF(system.text().window_attr());
124   else
125     SetRGBAF(window("ATTR"));
126 
127   default_font_size_in_pixels_ = window("MOJI_SIZE").ToInt(25);
128   set_font_size_in_pixels(default_font_size_in_pixels_);
129   SetWindowSizeInCharacters(window("MOJI_CNT"));
130   SetSpacingBetweenCharacters(window("MOJI_REP"));
131   set_ruby_text_size(window("LUBY_SIZE").ToInt(0));
132   SetTextboxPadding(window("MOJI_POS"));
133 
134   SetWindowPosition(window("POS"));
135 
136   SetDefaultTextColor(gexe("COLOR_TABLE", 0));
137 
138   // INDENT_USE appears to default to on. See the first scene in the
139   // game with Nagisa, paying attention to indentation; then check the
140   // Gameexe.ini.
141   set_use_indentation(window("INDENT_USE").ToInt(1));
142 
143   SetKeycursorMod(window("KEYCUR_MOD"));
144   set_action_on_pause(window("R_COMMAND_MOD").ToInt(0));
145 
146   // Main textbox waku
147   waku_set_ = window("WAKU_SETNO").ToInt(0);
148   textbox_waku_.reset(TextWaku::Create(system_, *this, waku_set_, 0));
149 
150   // Name textbox if that setting has been enabled.
151   set_name_mod(window("NAME_MOD").ToInt(0));
152   if (name_mod_ == 1 && window("NAME_WAKU_SETNO").Exists()) {
153     name_waku_set_ = window("NAME_WAKU_SETNO");
154     namebox_waku_.reset(TextWaku::Create(system_, *this, name_waku_set_, 0));
155     SetNameSpacingBetweenCharacters(window("NAME_MOJI_REP"));
156     SetNameboxPadding(window("NAME_MOJI_POS"));
157     // Ignoring NAME_WAKU_MIN for now
158     SetNameboxPosition(window("NAME_POS"));
159     name_waku_dir_set_ = window("NAME_WAKU_DIR").ToInt(0);
160     namebox_centering_ = window("NAME_CENTERING").ToInt(0);
161     minimum_namebox_size_ = window("NAME_MOJI_MIN").ToInt(4);
162     name_size_ = window("NAME_MOJI_SIZE");
163   }
164 
165   // Load #FACE information.
166   GameexeFilteringIterator it = gexe.filtering_begin(window.key() + ".FACE");
167   GameexeFilteringIterator end = gexe.filtering_end();
168   for (; it != end; ++it) {
169     // Retrieve the face slot number
170     std::vector<std::string> GetKeyParts = it->GetKeyParts();
171 
172     try {
173       int slot = std::stoi(GetKeyParts.at(3));
174       if (slot < kNumFaceSlots) {
175         face_slot_[slot].reset(new FaceSlot(it->ToIntVector()));
176       }
177     }
178     catch (...) {
179       // Parsing failure. Ignore this key.
180     }
181   }
182 }
183 
~TextWindow()184 TextWindow::~TextWindow() {}
185 
Execute()186 void TextWindow::Execute() {
187   if (is_visible() && !system_.graphics().is_interface_hidden()) {
188     textbox_waku_->Execute();
189   }
190 }
191 
SetTextboxPadding(const vector<int> & pos_data)192 void TextWindow::SetTextboxPadding(const vector<int>& pos_data) {
193   upper_box_padding_ = pos_data.at(0);
194   lower_box_padding_ = pos_data.at(1);
195   left_box_padding_ = pos_data.at(2);
196   right_box_padding_ = pos_data.at(3);
197 }
198 
SetName(const std::string & utf8name,const std::string & next_char)199 void TextWindow::SetName(const std::string& utf8name,
200                          const std::string& next_char) {
201   if (name_mod_ == 0) {
202     std::string interpreted_name = text_system_.InterpretName(utf8name);
203 
204     // Display the name in one pass
205     PrintTextToFunction(
206         bind(&TextWindow::DisplayCharacter, ref(*this), _1, _2),
207         interpreted_name, next_char);
208     SetIndentation();
209   }
210 
211   SetNameWithoutDisplay(utf8name);
212 }
213 
SetNameWithoutDisplay(const std::string & utf8name)214 void TextWindow::SetNameWithoutDisplay(const std::string& utf8name) {
215   if (name_mod_ == 1) {
216     std::string interpreted_name = text_system_.InterpretName(utf8name);
217 
218     namebox_characters_ = 0;
219     try {
220       namebox_characters_ = utf8::distance(interpreted_name.begin(),
221                                            interpreted_name.end());
222     }
223     catch (...) {
224       // If utf8name isn't a real UTF-8 string, possibly overestimate:
225       namebox_characters_ = interpreted_name.size();
226     }
227 
228     namebox_characters_ = std::max(namebox_characters_, minimum_namebox_size_);
229 
230     RenderNameInBox(interpreted_name);
231   }
232 
233   last_token_was_name_ = true;
234 }
235 
SetDefaultTextColor(const vector<int> & colour)236 void TextWindow::SetDefaultTextColor(const vector<int>& colour) {
237   default_colour_ = RGBColour(colour.at(0), colour.at(1), colour.at(2));
238 }
239 
SetFontColor(const vector<int> & colour)240 void TextWindow::SetFontColor(const vector<int>& colour) {
241   font_colour_ = RGBColour(colour.at(0), colour.at(1), colour.at(2));
242 }
243 
SetWindowSizeInCharacters(const vector<int> & pos_data)244 void TextWindow::SetWindowSizeInCharacters(const vector<int>& pos_data) {
245   x_window_size_in_chars_ = pos_data.at(0);
246   y_window_size_in_chars_ = pos_data.at(1);
247 }
248 
SetSpacingBetweenCharacters(const vector<int> & pos_data)249 void TextWindow::SetSpacingBetweenCharacters(const vector<int>& pos_data) {
250   x_spacing_ = pos_data.at(0);
251   y_spacing_ = pos_data.at(1);
252 }
253 
SetWindowPosition(const vector<int> & pos_data)254 void TextWindow::SetWindowPosition(const vector<int>& pos_data) {
255   origin_ = pos_data.at(0);
256   x_distance_from_origin_ = pos_data.at(1);
257   y_distance_from_origin_ = pos_data.at(2);
258 }
259 
GetTextWindowSize() const260 Size TextWindow::GetTextWindowSize() const {
261   return Size(
262       (x_window_size_in_chars_ * (default_font_size_in_pixels_ + x_spacing_)),
263       (y_window_size_in_chars_ *
264        (default_font_size_in_pixels_ + y_spacing_ + ruby_size_)));
265 }
266 
GetTextSurfaceSize() const267 Size TextWindow::GetTextSurfaceSize() const {
268   // There is one extra character in each line to accommodate squeezed
269   // punctuation.
270   return GetTextWindowSize() + Size(default_font_size_in_pixels_, 0);
271 }
272 
GetWindowRect() const273 Rect TextWindow::GetWindowRect() const {
274   // This absolutely needs to know the size of the on main backing waku if we
275   // want to draw things correctly! If we are going to offset this text box
276   // from the top or the bottom, we MUST know what the size of the image
277   // graphic is if we want accurate calculations, because some image graphics
278   // are significantly larger than GetTextWindowSize() + the paddings.
279   //
280   // RealLive is definitely correcting programmer errors which places textboxes
281   // offscreen. For example, take P_BRAVE (please!): #WINDOW.002.POS=2:78,6,
282   // and a waku that refers to PM_WIN.g00 for it's image. That image is 800x300
283   // pixels. The image is still centered perfectly, even though it's supposed
284   // to be shifted 78 pixels right since the origin is the bottom
285   // left. Expanding this number didn't change the position offscreen.
286   Size boxSize = textbox_waku_->GetSize(GetTextSurfaceSize());
287 
288   int x, y;
289   switch (origin_) {
290     case 0:
291     case 2:
292       x = x_distance_from_origin_;
293       break;
294     case 1:
295     case 3:
296       x = screen_width_ - boxSize.width() - x_distance_from_origin_;
297       break;
298     default:
299       throw SystemError("Invalid origin");
300   }
301 
302   switch (origin_) {
303     case 0:  // Top and left
304     case 1:  // Top and right
305       y = y_distance_from_origin_;
306       break;
307     case 2:  // Bottom and left
308     case 3:  // Bottom and right
309       y = screen_height_ - boxSize.height() - y_distance_from_origin_;
310       break;
311     default:
312       throw SystemError("Invalid origin");
313   }
314 
315   // Now that we have the coordinate that the programmer wanted to position the
316   // box at, possibly move the box so it fits on screen.
317   if ((x + boxSize.width()) > screen_width_)
318     x -= (x + boxSize.width()) - screen_width_;
319   if (x < 0)
320     x = 0;
321 
322   if ((y + boxSize.height()) > screen_height_)
323     y -= (y + boxSize.height()) - screen_height_;
324   if (y < 0)
325     y = 0;
326 
327   return Rect(x, y, boxSize);
328 }
329 
GetTextSurfaceRect() const330 Rect TextWindow::GetTextSurfaceRect() const {
331   Rect window = GetWindowRect();
332 
333   Point textOrigin =
334       window.origin() + Size(left_box_padding_, upper_box_padding_);
335 
336   Size rectSize = GetTextSurfaceSize();
337   rectSize += Size(right_box_padding_, lower_box_padding_);
338 
339   return Rect(textOrigin, rectSize);
340 }
341 
GetNameboxWakuRect() const342 Rect TextWindow::GetNameboxWakuRect() const {
343   // Like the main GetWindowRect(), we need to ask the waku what size it wants to
344   // be.
345   Size boxSize = namebox_waku_->GetSize(GetNameboxTextArea());
346 
347   // The waku is offset from the top left corner of the text window.
348   Rect r = GetWindowRect();
349   return Rect(Point(r.x() + namebox_x_offset_,
350                     r.y() + namebox_y_offset_ - boxSize.height()),
351               boxSize);
352 }
353 
GetNameboxTextArea() const354 Size TextWindow::GetNameboxTextArea() const {
355   // TODO(erg): This seems excessively wide.
356   return Size(
357       2 * horizontal_namebox_padding_ + namebox_characters_ * name_size_,
358       vertical_namebox_padding_ + name_size_);
359 }
360 
SetNameSpacingBetweenCharacters(const std::vector<int> & pos_data)361 void TextWindow::SetNameSpacingBetweenCharacters(
362     const std::vector<int>& pos_data) {
363   name_x_spacing_ = pos_data.at(0);
364 }
365 
SetNameboxPadding(const std::vector<int> & pos_data)366 void TextWindow::SetNameboxPadding(const std::vector<int>& pos_data) {
367   if (pos_data.size() >= 1)
368     horizontal_namebox_padding_ = pos_data.at(0);
369   if (pos_data.size() >= 2)
370     vertical_namebox_padding_ = pos_data.at(1);
371 }
372 
SetNameboxPosition(const vector<int> & pos_data)373 void TextWindow::SetNameboxPosition(const vector<int>& pos_data) {
374   namebox_x_offset_ = pos_data.at(0);
375   namebox_y_offset_ = pos_data.at(1);
376 }
377 
SetKeycursorMod(const vector<int> & keycur)378 void TextWindow::SetKeycursorMod(const vector<int>& keycur) {
379   keycursor_type_ = keycur.at(0);
380   keycursor_pos_ = Point(keycur.at(1), keycur.at(2));
381 }
382 
KeycursorPosition(const Size & cursor_size) const383 Point TextWindow::KeycursorPosition(const Size& cursor_size) const {
384   switch (keycursor_type_) {
385     case 0:
386       return GetTextSurfaceRect().lower_right() - cursor_size;
387     case 1:
388       return GetTextSurfaceRect().origin() +
389              Point(text_insertion_point_x_, text_insertion_point_y_);
390     case 2:
391       return GetTextSurfaceRect().origin() + keycursor_pos_;
392     default:
393       throw SystemError("Invalid keycursor type");
394   }
395 }
396 
FaceOpen(const std::string & filename,int index)397 void TextWindow::FaceOpen(const std::string& filename, int index) {
398   if (face_slot_[index]) {
399     face_slot_[index]->face_surface =
400         system_.graphics().GetSurfaceNamed(filename);
401 
402     if (face_slot_[index]->hide_other_windows) {
403       system_.text().HideAllTextWindowsExcept(window_number());
404     }
405   }
406 }
407 
FaceClose(int index)408 void TextWindow::FaceClose(int index) {
409   if (face_slot_[index]) {
410     face_slot_[index]->face_surface.reset();
411 
412     if (face_slot_[index]->hide_other_windows) {
413       system_.text().HideAllTextWindowsExcept(window_number());
414     }
415   }
416 }
417 
NextCharIsItalic()418 void TextWindow::NextCharIsItalic() {
419   next_char_italic_ = true;
420 }
421 
422 // TODO(erg): Make this pass the #WINDOW_ATTR colour off wile rendering the
423 // waku_backing.
Render(std::ostream * tree)424 void TextWindow::Render(std::ostream* tree) {
425   std::shared_ptr<Surface> text_surface = GetTextSurface();
426 
427   if (text_surface && is_visible()) {
428     Size surface_size = text_surface->GetSize();
429 
430     // POINT
431     Point box = GetWindowRect().origin();
432 
433     if (tree) {
434       *tree << "  Text Window #" << window_num_ << endl;
435     }
436 
437     Point textOrigin = GetTextSurfaceRect().origin();
438 
439     textbox_waku_->Render(tree, box, surface_size);
440     RenderFaces(tree, 1);
441 
442     if (in_selection_mode()) {
443       for_each(selections_.begin(), selections_.end(),
444                [](unique_ptr<SelectionElement>& e) { e->Render(); });
445     } else {
446       std::shared_ptr<Surface> name_surface = GetNameSurface();
447       if (name_surface) {
448         Rect r = GetNameboxWakuRect();
449 
450         if (namebox_waku_) {
451           // TODO(erg): The waku needs to be adjusted to be the minimum size of
452           // the window in characters
453           namebox_waku_->Render(tree, r.origin(), GetNameboxTextArea());
454         }
455 
456         Point insertion_point = namebox_waku_->InsertionPoint(
457             r,
458             Size(horizontal_namebox_padding_, vertical_namebox_padding_),
459             name_surface->GetSize(),
460             namebox_centering_);
461         name_surface->RenderToScreen(
462             name_surface->GetRect(),
463             Rect(insertion_point, name_surface->GetSize()),
464             255);
465 
466         if (tree) {
467           *tree << "    Name Area: " << r << endl;
468         }
469       }
470 
471       RenderFaces(tree, 0);
472       RenderKoeReplayButtons(tree);
473 
474       text_surface->RenderToScreen(
475           Rect(Point(0, 0), surface_size), Rect(textOrigin, surface_size), 255);
476 
477       if (tree) {
478         *tree << "    Text Area: " << Rect(textOrigin, surface_size) << endl;
479       }
480     }
481   }
482 }
483 
RenderFaces(std::ostream * tree,int behind)484 void TextWindow::RenderFaces(std::ostream* tree, int behind) {
485   for (int i = 0; i < kNumFaceSlots; ++i) {
486     if (face_slot_[i] && face_slot_[i]->face_surface &&
487         face_slot_[i]->behind == behind) {
488       const std::shared_ptr<const Surface>& surface =
489           face_slot_[i]->face_surface;
490 
491       Rect dest(GetWindowRect().x() + face_slot_[i]->x,
492                 GetWindowRect().y() + face_slot_[i]->y,
493                 surface->GetSize());
494       surface->RenderToScreen(surface->GetRect(), dest, 255);
495 
496       if (tree) {
497         *tree << "    Face Slot #" << i << ": " << dest << endl;
498       }
499     }
500   }
501 }
502 
RenderKoeReplayButtons(std::ostream * tree)503 void TextWindow::RenderKoeReplayButtons(std::ostream* tree) {
504   for (std::vector<std::pair<Point, int>>::const_iterator it =
505            koe_replay_button_.begin();
506        it != koe_replay_button_.end();
507        ++it) {
508     koe_replay_info_->icon->RenderToScreen(
509         Rect(Point(0, 0), koe_replay_info_->icon->GetSize()),
510         Rect(GetTextSurfaceRect().origin() + it->first,
511              koe_replay_info_->icon->GetSize()),
512         255);
513   }
514 }
515 
ClearWin()516 void TextWindow::ClearWin() {
517   text_insertion_point_x_ = 0;
518   text_insertion_point_y_ = ruby_text_size();
519   text_wrapping_point_x_ = 0;
520   current_indentation_in_pixels_ = 0;
521   current_indentation_in_chars_ = 0;
522   current_line_number_ = 0;
523   ruby_begin_point_ = -1;
524   font_colour_ = default_colour_;
525   koe_replay_button_.clear();
526 }
527 
DisplayCharacter(const std::string & current,const std::string & rest)528 bool TextWindow::DisplayCharacter(const std::string& current,
529                                   const std::string& rest) {
530   // If this text page is already full, save some time and reject
531   // early.
532   if (IsFull())
533     return false;
534 
535   set_is_visible(true);
536 
537   if (current != "") {
538     int cur_codepoint = Codepoint(current);
539     bool indent_after_spacing = false;
540 
541     // But if the last character was a lenticular bracket, we need to indent
542     // now. See doc/notes/NamesAndIndentation.txt for more details.
543     if (last_token_was_name_) {
544       if (name_mod_ == 0) {
545         if (IsOpeningQuoteMark(cur_codepoint))
546           indent_after_spacing = true;
547       } else if (name_mod_ == 2) {
548         if (IsOpeningQuoteMark(cur_codepoint)) {
549           indent_after_spacing = true;
550         }
551       }
552     }
553 
554     // If the width of this glyph plus the spacing will put us over the
555     // edge of the window, then line increment.
556     if (MustLineBreak(cur_codepoint, rest)) {
557       HardBrake();
558 
559       if (IsFull())
560         return false;
561     }
562 
563     RGBColour shadow = RGBAColour::Black().rgb();
564     text_system_.RenderGlyphOnto(current,
565                                  font_size_in_pixels(),
566                                  next_char_italic_,
567                                  font_colour_,
568                                  &shadow,
569                                  text_insertion_point_x_,
570                                  text_insertion_point_y_,
571                                  GetTextSurface());
572     next_char_italic_ = false;
573     text_wrapping_point_x_ += GetWrappingWidthFor(cur_codepoint);
574 
575     if (cur_codepoint < 127) {
576       // This is a basic ASCII character. In normal RealLive, western text
577       // appears to be treated as half width monospace. If we're here, we are
578       // either in a manually laid out western game (therefore we should try to
579       // fit onto the monospace grid) or we're in rlbabel (in which case, our
580       // insertion point will be manually set by the bytecode immediately after
581       // this character).
582       if (text_system_.FontIsMonospaced()) {
583         // If our font is monospaced (ie msgothic.ttc), we want to follow the
584         // game's layout instructions perfectly.
585         text_insertion_point_x_ += GetWrappingWidthFor(cur_codepoint);
586       } else {
587         // If out font has different widths for 'i' and 'm', we aren't using
588         // the recommended font so we'll try laying out the text so that
589         // kerning looks better. This is the common case.
590         text_insertion_point_x_ +=
591             text_system_.GetCharWidth(font_size_in_pixels(), cur_codepoint);
592       }
593     } else {
594       // Move the insertion point forward one character
595       text_insertion_point_x_ += font_size_in_pixels_ + x_spacing_;
596     }
597 
598     if (indent_after_spacing)
599       SetIndentation();
600   }
601 
602   // When we aren't rendering a piece of text with a ruby gloss, mark
603   // the screen as dirty so that this character renders.
604   if (ruby_begin_point_ == -1) {
605     system_.graphics().MarkScreenAsDirty(GUT_TEXTSYS);
606   }
607 
608   last_token_was_name_ = false;
609 
610   return true;
611 }
612 
613 // Lines we still get wrong in CLANNAD Prologue:
614 //
615 // <rlmax> = Official RealLive's breaking
616 // <rlvm> = Where rlvm places the line break
617 //
618 // - "Whose ides was it to put a school at the top of a giant <rlmax> slope,<rlvm> anyway?"
619 //
620 
MustLineBreak(int cur_codepoint,const std::string & rest)621 bool TextWindow::MustLineBreak(int cur_codepoint, const std::string& rest) {
622   int char_width = GetWrappingWidthFor(cur_codepoint);
623   bool cur_codepoint_is_kinsoku = IsKinsoku(cur_codepoint) ||
624                                   cur_codepoint == 0x20;
625   int normal_width =
626       x_window_size_in_chars_ * (default_font_size_in_pixels_ + x_spacing_);
627   int extended_width = normal_width + default_font_size_in_pixels_;
628 
629   // If this character is a kinsoku, and will squeeze onto this line, don't
630   // break and don't follow any of the further rules.
631   if (cur_codepoint_is_kinsoku &&
632       (text_wrapping_point_x_ + char_width <= extended_width)) {
633     return false;
634   }
635 
636   // If this character won't fit on the line normally, break.
637   if (text_wrapping_point_x_ + char_width > normal_width) {
638     return true;
639   }
640 
641   // If this character will fit on the line, but the next n characters are
642   // kinsoku characters OR wrapping roman characters and one of them won't,
643   // then break.
644   if (!cur_codepoint_is_kinsoku && rest != "") {
645     int final_insertion_x = text_wrapping_point_x_ + char_width;
646 
647     string::const_iterator cur = rest.begin();
648     string::const_iterator end = rest.end();
649     while (cur != end) {
650       int point = utf8::next(cur, end);
651       if (IsKinsoku(point)) {
652         final_insertion_x += GetWrappingWidthFor(point);
653 
654         if (final_insertion_x > extended_width) {
655           return true;
656         }
657       // OK, is this correct? I'm now having places where we prematurely break.
658       } else if (IsWrappingRomanCharacter(point)) {
659         final_insertion_x += GetWrappingWidthFor(point);
660 
661         if (final_insertion_x > normal_width) {
662           return true;
663         }
664       } else {
665         break;
666       }
667     }
668   }
669 
670   return false;
671 }
672 
IsFull() const673 bool TextWindow::IsFull() const {
674   return current_line_number_ >= y_window_size_in_chars_;
675 }
676 
KoeMarker(int id)677 void TextWindow::KoeMarker(int id) {
678   if (!koe_replay_info_) {
679     koe_replay_info_.reset(new KoeReplayInfo);
680     Gameexe& gexe = system_.gameexe();
681     GameexeInterpretObject replay_icon(gexe("KOEREPLAYICON"));
682 
683     koe_replay_info_->icon =
684         system_.graphics().GetSurfaceNamed(replay_icon("NAME"));
685     std::vector<int> reppos = replay_icon("REPPOS");
686     if (reppos.size() == 2)
687       koe_replay_info_->repos = Size(reppos[0], reppos[1]);
688   }
689 
690   Point p = Point(text_insertion_point_x_, text_insertion_point_y_) +
691             koe_replay_info_->repos;
692   koe_replay_button_.emplace_back(p, id);
693 }
694 
HardBrake()695 void TextWindow::HardBrake() {
696   text_insertion_point_x_ = current_indentation_in_pixels_;
697   text_insertion_point_y_ += line_height();
698   text_wrapping_point_x_ = current_indentation_in_chars_;
699   current_line_number_++;
700 }
701 
SetIndentation()702 void TextWindow::SetIndentation() {
703   current_indentation_in_pixels_ = text_insertion_point_x_;
704   current_indentation_in_chars_ = text_wrapping_point_x_;
705 }
706 
ResetIndentation()707 void TextWindow::ResetIndentation() {
708   current_indentation_in_pixels_ = 0;
709   current_indentation_in_chars_ = 0;
710 }
711 
MarkRubyBegin()712 void TextWindow::MarkRubyBegin() {
713   ruby_begin_point_ = text_insertion_point_x_;
714 }
715 
SetRGBAF(const vector<int> & attr)716 void TextWindow::SetRGBAF(const vector<int>& attr) {
717   colour_ = RGBAColour(attr.at(0), attr.at(1), attr.at(2), attr.at(3));
718   set_filter(attr.at(4));
719 }
720 
SetMousePosition(const Point & pos)721 void TextWindow::SetMousePosition(const Point& pos) {
722   if (in_selection_mode()) {
723     for_each(selections_.begin(),
724              selections_.end(),
725              [&](unique_ptr<SelectionElement>& e) {
726                e->SetMousePosition(pos); });
727   }
728 
729   textbox_waku_->SetMousePosition(pos);
730 }
731 
HandleMouseClick(RLMachine & machine,const Point & pos,bool pressed)732 bool TextWindow::HandleMouseClick(RLMachine& machine,
733                                   const Point& pos,
734                                   bool pressed) {
735   if (in_selection_mode()) {
736     bool found =
737       find_if(selections_.begin(), selections_.end(),
738               [&](unique_ptr<SelectionElement> &e) {
739                 return e->HandleMouseClick(pos, pressed);
740               }) != selections_.end();
741 
742     if (found)
743       return true;
744   }
745 
746   for (std::vector<std::pair<Point, int>>::const_iterator it =
747            koe_replay_button_.begin();
748        it != koe_replay_button_.end();
749        ++it) {
750     Rect r = Rect(GetTextSurfaceRect().origin() + it->first,
751                   koe_replay_info_->icon->GetSize());
752     if (r.Contains(pos)) {
753       // We only want to actually replay the voice clip once, but we want to
754       // catch both clicks.
755       if (pressed)
756         system_.sound().KoePlay(it->second);
757       return true;
758     }
759   }
760 
761   if (is_visible() && !machine.system().graphics().is_interface_hidden()) {
762     return textbox_waku_->HandleMouseClick(machine, pos, pressed);
763   }
764 
765   return false;
766 }
767 
StartSelectionMode()768 void TextWindow::StartSelectionMode() { in_selection_mode_ = true; }
769 
SetSelectionCallback(const std::function<void (int)> & in)770 void TextWindow::SetSelectionCallback(const std::function<void(int)>& in) {
771   selection_callback_ = in;
772 }
773 
EndSelectionMode()774 void TextWindow::EndSelectionMode() {
775   in_selection_mode_ = false;
776   selection_callback_ = nullptr;
777   selections_.clear();
778   ClearWin();
779 }
780 
GetWrappingWidthFor(int cur_codepoint)781 int TextWindow::GetWrappingWidthFor(int cur_codepoint) {
782   if (cur_codepoint < 127) {
783     return std::floor((font_size_in_pixels_ + x_spacing_) / 2.0f);
784   } else {
785     return font_size_in_pixels_ + x_spacing_;
786   }
787 }
788