1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 2020 Warzone 2100 Project
4
5 Warzone 2100 is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 Warzone 2100 is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Warzone 2100; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19
20 #include <algorithm>
21 #include "lib/framework/frame.h"
22 #include "lib/ivis_opengl/pieblitfunc.h"
23 #include "widget.h"
24 #include "widgint.h"
25 #include "paragraph.h"
26 #include "form.h"
27 #include "tip.h"
28
29 class FlowLayoutElementDescriptor
30 {
31 public:
32 virtual ~FlowLayoutElementDescriptor() = default;
33 virtual unsigned int getWidth(size_t position, size_t length) const = 0;
34 virtual size_t size() const = 0;
35 virtual bool isWhitespace(size_t position) const = 0;
36 virtual bool isLineBreak(size_t position) const = 0;
37
isWord(size_t position) const38 virtual bool isWord(size_t position) const
39 {
40 return position < size() && !isWhitespace(position) && !isLineBreak(position);
41 }
42 };
43
44 /**
45 * FlowLayout implements a word wrapping algorithm.
46 *
47 * It doesn't specify how the text should be structured, it only requires that the elements of the text are described
48 * by FlowLayoutElementDescriptor.
49 *
50 * Each element can contain 0 or more words, and a single word can be part of two sequential elements.
51 *
52 * Example:
53 * - Element 1 = "first sec"
54 * - Element 2 = "ond third"
55 *
56 * In the example above, the resulting text is "first second third", so if possible the algorithm will keep
57 * the second word completely in the same line.
58 **/
59 struct FlowLayout
60 {
61 private:
shouldMergeFragmentFlowLayout62 bool shouldMergeFragment()
63 {
64 return !currentLine.empty() && !partialWord.empty() && currentLine.back().elementId == partialWord.front().elementId;
65 }
66
endWordFlowLayout67 void endWord()
68 {
69 if (shouldMergeFragment())
70 {
71 auto partialWordFront = &partialWord.front();
72 auto currentLineBack = currentLine.back();
73 currentLine.pop_back();
74 partialWordFront->length = partialWordFront->length + partialWordFront->begin - currentLineBack.begin;
75 partialWordFront->width = partialWordFront->offset + partialWordFront->width - currentLineBack.offset;
76 partialWordFront->begin = currentLineBack.begin;
77 partialWordFront->offset = currentLineBack.offset;
78 }
79
80 for (auto fragment: partialWord)
81 {
82 currentLine.push_back(fragment);
83 }
84
85 nextOffset += partialWordWidth;
86 partialWordWidth = 0;
87 partialWord.clear();
88 }
89
pushFragmentFlowLayout90 void pushFragment(FlowLayoutElementDescriptor const &elementDescriptor, size_t begin, size_t end)
91 {
92 auto width = elementDescriptor.getWidth(begin, end - begin);
93 partialWord.push_back({currentElementId, begin, end - begin, width, nextOffset + partialWordWidth});
94 partialWordWidth += width;
95 }
96
placeLineFlowLayout97 void placeLine(FlowLayoutElementDescriptor const &elementDescriptor, size_t begin, size_t end)
98 {
99 auto current = begin;
100
101 while (current < end)
102 {
103 long long fragmentFits;
104
105 if (nextOffset + partialWordWidth + elementDescriptor.getWidth(current, end - current) > maxWidth)
106 // fragment doesn't fit completely in the current line
107 {
108 fragmentFits = current - 1;
109 size_t fragmentDoesntFit = end;
110 while (fragmentDoesntFit - fragmentFits > 1)
111 {
112 auto middle = (fragmentFits + fragmentDoesntFit) / 2;
113 if (nextOffset + partialWordWidth + elementDescriptor.getWidth(current, middle - current) > maxWidth)
114 {
115 fragmentDoesntFit = middle;
116 } else {
117 fragmentFits = middle;
118 }
119 }
120 }
121 else
122 {
123 fragmentFits = end;
124 }
125
126 auto whitespacePosition = fragmentFits + 1;
127 while (whitespacePosition > elementDescriptor.size() || (whitespacePosition > current && !elementDescriptor.isWhitespace(whitespacePosition - 1)))
128 {
129 whitespacePosition--;
130 }
131
132 if (whitespacePosition > current)
133 // the fragment ending with a whitespace fits within the line
134 {
135 pushFragment(elementDescriptor, current, whitespacePosition - 1);
136 endWord();
137 nextOffset += elementDescriptor.getWidth(whitespacePosition - 1, 1);
138 current = whitespacePosition;
139 }
140 else if (fragmentFits == end)
141 // the fragment is a single word, and fits in the line,
142 // but it doesn't end in whitespace and the next element might be part of this word
143 {
144 pushFragment(elementDescriptor, current, fragmentFits);
145 current = fragmentFits;
146 }
147 else if (nextOffset > 0)
148 // word doesn't fit in the current line
149 {
150 breakLine();
151 }
152 else
153 // word doesn't fit in an empty line
154 {
155 auto fragmentEnd = std::max((size_t)fragmentFits, current + 1);
156 pushFragment(elementDescriptor, current, fragmentEnd);
157 current = fragmentEnd;
158 breakLine();
159 }
160 }
161 }
162
163 public:
FlowLayoutFlowLayout164 FlowLayout(unsigned int maxWidth): maxWidth(maxWidth)
165 {
166
167 }
168
appendFlowLayout169 void append(FlowLayoutElementDescriptor const &elementDescriptor)
170 {
171 size_t position = 0;
172 while (position < elementDescriptor.size())
173 {
174 auto lineEnd = position;
175
176 while (lineEnd < elementDescriptor.size() && !elementDescriptor.isLineBreak(lineEnd))
177 {
178 lineEnd++;
179 }
180
181 placeLine(elementDescriptor, position, lineEnd);
182
183 if (lineEnd < elementDescriptor.size())
184 {
185 breakLine();
186 }
187
188 position = lineEnd + 1;
189 }
190
191 currentElementId++;
192 }
193
endFlowLayout194 void end()
195 {
196 endWord();
197
198 if (!currentLine.empty()) {
199 breakLine();
200 }
201 }
202
breakLineFlowLayout203 void breakLine()
204 {
205 endWord();
206 lines.push_back(currentLine);
207 currentLine.clear();
208 nextOffset = 0;
209 }
210
getLinesFlowLayout211 std::vector<std::vector<FlowLayoutFragment>> getLines()
212 {
213 return lines;
214 }
215
216 private:
217 std::vector<FlowLayoutFragment> currentLine;
218 std::vector<std::vector<FlowLayoutFragment>> lines;
219 unsigned int maxWidth;
220 unsigned int currentElementId = 0;
221 unsigned int nextOffset = 0;
222 std::vector<FlowLayoutFragment> partialWord;
223 unsigned int partialWordWidth = 0;
224 };
225
226 /**
227 * Used to render a fragment of text in the paragraph.
228 *
229 * Can be an entire line, or part of a line, but never more than one line.
230 **/
231 class ParagraphTextWidget: public WIDGET
232 {
233 public:
ParagraphTextWidget(std::string text,ParagraphTextStyle const & textStyle)234 ParagraphTextWidget(std::string text, ParagraphTextStyle const &textStyle):
235 WIDGET(WIDG_UNSPECIFIED_TYPE),
236 cachedText(text, textStyle.font),
237 textStyle(textStyle)
238 {
239 setGeometry(x(), y(), cachedText->width(), cachedText->lineSize());
240 }
241
display(int xOffset,int yOffset)242 void display(int xOffset, int yOffset) override
243 {
244 auto x0 = xOffset + x();
245 auto y0 = yOffset + y();
246 pie_UniTransBoxFill(x0, y0, x0 + width() - 1, y0 + height() - 1, textStyle.shadeColour);
247 cachedText->render(x0, y0 - cachedText->aboveBase(), textStyle.fontColour);
248 }
249
run(W_CONTEXT *)250 void run(W_CONTEXT *) override
251 {
252 cachedText.tick();
253 }
254
255 private:
256 WzCachedText cachedText;
257 ParagraphTextStyle textStyle;
258 };
259
260 class FlowLayoutStringDescriptor : public FlowLayoutElementDescriptor
261 {
262 WzString text;
263 std::vector<uint32_t> textUtf32;
264 iV_fonts font;
265
266 public:
FlowLayoutStringDescriptor(WzString const & newText,iV_fonts newFont)267 FlowLayoutStringDescriptor(WzString const &newText, iV_fonts newFont): text(newText), textUtf32(newText.toUtf32()), font(newFont) {}
268
getWidth(size_t position,size_t length) const269 unsigned int getWidth(size_t position, size_t length) const
270 {
271 return iV_GetTextWidth(text.substr(position, length).toUtf8().c_str(), font);
272 }
273
size() const274 size_t size() const
275 {
276 return textUtf32.size();
277 }
278
isWhitespace(size_t position) const279 bool isWhitespace(size_t position) const
280 {
281 switch (textUtf32[position])
282 {
283 case ' ':
284 case '\t':
285 return true;
286 default:
287 return false;
288 }
289 }
290
isLineBreak(size_t position) const291 bool isLineBreak(size_t position) const
292 {
293 return textUtf32[position] == '\n';
294 }
295 };
296
297 struct ParagraphTextElement: public ParagraphElement
298 {
ParagraphTextElementParagraphTextElement299 ParagraphTextElement(std::string const &newText, ParagraphTextStyle const &style): style(style)
300 {
301 text = WzString::fromUtf8(newText);
302 }
303
appendToParagraphTextElement304 void appendTo(FlowLayout &layout) override
305 {
306 layout.append(FlowLayoutStringDescriptor(text, style.font));
307 }
308
createFragmentWidgetParagraphTextElement309 std::shared_ptr<WIDGET> createFragmentWidget(Paragraph ¶graph, FlowLayoutFragment const &fragment) override
310 {
311 auto widget = std::make_shared<ParagraphTextWidget>(text.substr(fragment.begin, fragment.length).toUtf8(), style);
312 paragraph.attach(widget);
313 fragments.push_back(widget);
314 return widget;
315 }
316
isLayoutDirtyParagraphTextElement317 bool isLayoutDirty() const override
318 {
319 return false;
320 }
321
destroyFragmentsParagraphTextElement322 void destroyFragments(Paragraph ¶graph) override
323 {
324 for (auto fragment: fragments)
325 {
326 paragraph.detach(fragment);
327 }
328
329 fragments.clear();
330 }
331
getAboveBaseParagraphTextElement332 int32_t getAboveBase() const override
333 {
334 return iV_GetTextAboveBase(style.font);
335 }
336
337 private:
338 WzString text;
339 ParagraphTextStyle style;
340 std::vector<std::shared_ptr<WIDGET>> fragments;
341 };
342
343 struct ParagraphWidgetElement: public ParagraphElement, FlowLayoutElementDescriptor
344 {
ParagraphWidgetElementParagraphWidgetElement345 ParagraphWidgetElement(const std::shared_ptr<WIDGET> &widget, int32_t aboveBase): widget(widget), aboveBase(aboveBase)
346 {
347 }
348
getWidthParagraphWidgetElement349 unsigned int getWidth(size_t position, size_t length) const override
350 {
351 return position == 0 ? widget->width() : 0;
352 }
353
sizeParagraphWidgetElement354 size_t size() const override
355 {
356 return 1;
357 }
358
isWhitespaceParagraphWidgetElement359 bool isWhitespace(size_t position) const override
360 {
361 return false;
362 }
363
isLineBreakParagraphWidgetElement364 bool isLineBreak(size_t position) const override
365 {
366 return false;
367 }
368
appendToParagraphWidgetElement369 void appendTo(FlowLayout &layout) override
370 {
371 layout.append(*this);
372 }
373
createFragmentWidgetParagraphWidgetElement374 std::shared_ptr<WIDGET> createFragmentWidget(Paragraph ¶graph, FlowLayoutFragment const &fragment) override
375 {
376 layoutWidth = widget->width();
377 layoutHeight = widget->height();
378 return widget;
379 }
380
destroyFragmentsParagraphWidgetElement381 void destroyFragments(Paragraph ¶graph) override
382 {
383 }
384
isLayoutDirtyParagraphWidgetElement385 bool isLayoutDirty() const override
386 {
387 return widget->width() != layoutWidth || widget->height() != layoutHeight;
388 }
389
getAboveBaseParagraphWidgetElement390 int32_t getAboveBase() const override
391 {
392 return aboveBase;
393 }
394
395 private:
396 std::shared_ptr<WIDGET> widget;
397 uint32_t layoutWidth = 0;
398 uint32_t layoutHeight = 0;
399 int32_t aboveBase;
400 };
401
Paragraph(W_INIT const * init)402 Paragraph::Paragraph(W_INIT const *init): WIDGET(init)
403 {
404 }
405
hasElementWithLayoutDirty() const406 bool Paragraph::hasElementWithLayoutDirty() const
407 {
408 for (auto &element: elements)
409 {
410 if (element->isLayoutDirty())
411 {
412 return true;
413 }
414 }
415
416 return false;
417 }
418
updateLayout()419 void Paragraph::updateLayout()
420 {
421 if (!layoutDirty && !hasElementWithLayoutDirty()) {
422 return;
423 }
424
425 layoutDirty = false;
426 layoutWidth = width();
427
428 for (auto &element: elements)
429 {
430 element->destroyFragments(*this);
431 }
432
433 auto nextLineOffset = 0;
434 auto totalHeight = 0;
435 for (const auto& line : calculateLinesLayout())
436 {
437 std::vector<WIDGET *> lineFragments;
438 auto aboveBase = 0;
439 auto belowBase = 0;
440 for (auto fragmentDescriptor: line)
441 {
442 auto fragment = elements[fragmentDescriptor.elementId]->createFragmentWidget(*this, fragmentDescriptor);
443 auto fragmentAboveBase = -elements[fragmentDescriptor.elementId]->getAboveBase();
444 aboveBase = std::max(aboveBase, fragmentAboveBase);
445 belowBase = std::max(belowBase, fragment->height() - fragmentAboveBase);
446 fragment->setGeometry(fragmentDescriptor.offset, nextLineOffset - fragmentAboveBase, fragment->width(), fragment->height());
447 lineFragments.push_back(fragment.get());
448 }
449
450 for (auto fragment: lineFragments)
451 {
452 fragment->setGeometry(fragment->x(), fragment->y() + aboveBase, fragment->width(), fragment->height());
453 }
454
455 totalHeight = nextLineOffset + aboveBase + belowBase;
456 nextLineOffset = totalHeight + lineSpacing;
457 }
458
459 setGeometry(x(), y(), width(), totalHeight);
460 }
461
calculateLinesLayout()462 std::vector<std::vector<FlowLayoutFragment>> Paragraph::calculateLinesLayout()
463 {
464 FlowLayout flowLayout(width());
465 for (auto &element: elements)
466 {
467 element->appendTo(flowLayout);
468 }
469 flowLayout.end();
470
471 return flowLayout.getLines();
472 }
473
addText(std::string const & text)474 void Paragraph::addText(std::string const &text)
475 {
476 layoutDirty = true;
477 elements.push_back(std::unique_ptr<ParagraphTextElement>(new ParagraphTextElement(text, textStyle)));
478 }
479
addWidget(const std::shared_ptr<WIDGET> & widget,int32_t aboveBase)480 void Paragraph::addWidget(const std::shared_ptr<WIDGET> &widget, int32_t aboveBase)
481 {
482 layoutDirty = true;
483 attach(widget);
484 elements.push_back(std::unique_ptr<ParagraphWidgetElement>(new ParagraphWidgetElement(widget, aboveBase)));
485 }
486
geometryChanged()487 void Paragraph::geometryChanged()
488 {
489 if (layoutWidth != width())
490 {
491 layoutDirty = true;
492 }
493
494 updateLayout();
495 }
496
displayRecursive(WidgetGraphicsContext const & context)497 void Paragraph::displayRecursive(WidgetGraphicsContext const& context)
498 {
499 updateLayout();
500 WIDGET::displayRecursive(context);
501 }
502