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 &paragraph, 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 &paragraph) 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 &paragraph, FlowLayoutFragment const &fragment) override
375 	{
376 		layoutWidth = widget->width();
377 		layoutHeight = widget->height();
378 		return widget;
379 	}
380 
destroyFragmentsParagraphWidgetElement381 	void destroyFragments(Paragraph &paragraph) 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