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