1/*
2 * Copyright 2013-2015, Stephan A��mus <superstippi@gmx.de>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5
6#include "TextDocumentLayout.h"
7
8#include <new>
9#include <stdio.h>
10
11#include <View.h>
12
13
14class LayoutTextListener : public TextListener {
15public:
16	LayoutTextListener(TextDocumentLayout* layout)
17		:
18		fLayout(layout)
19	{
20	}
21
22	virtual	void TextChanging(TextChangingEvent& event)
23	{
24	}
25
26	virtual	void TextChanged(const TextChangedEvent& event)
27	{
28//		printf("TextChanged(%" B_PRIi32 ", %" B_PRIi32 ")\n",
29//			event.FirstChangedParagraph(),
30//			event.ChangedParagraphCount());
31		// TODO: The event does not contain useful data. Make the event
32		// system work so only the affected paragraphs are updated.
33		// I think we need "first affected", "last affected" (both relative
34		// to the original paragraph count), and than how many new paragraphs
35		// are between these. From the difference in old number of paragraphs
36		// inbetween and the new count, we know how many new paragraphs are
37		// missing, and the rest in the range needs to be updated.
38//		fLayout->InvalidateParagraphs(event.FirstChangedParagraph(),
39//			event.ChangedParagraphCount());
40		fLayout->Invalidate();
41	}
42
43private:
44	TextDocumentLayout*	fLayout;
45};
46
47
48
49TextDocumentLayout::TextDocumentLayout()
50	:
51	fWidth(0.0f),
52	fLayoutValid(false),
53
54	fDocument(),
55	fTextListener(new(std::nothrow) LayoutTextListener(this), true),
56	fParagraphLayouts()
57{
58}
59
60
61TextDocumentLayout::TextDocumentLayout(const TextDocumentRef& document)
62	:
63	fWidth(0.0f),
64	fLayoutValid(false),
65
66	fDocument(),
67	fTextListener(new(std::nothrow) LayoutTextListener(this), true),
68	fParagraphLayouts()
69{
70	SetTextDocument(document);
71}
72
73
74TextDocumentLayout::TextDocumentLayout(const TextDocumentLayout& other)
75	:
76	fWidth(other.fWidth),
77	fLayoutValid(other.fLayoutValid),
78
79	fDocument(other.fDocument),
80	fTextListener(new(std::nothrow) LayoutTextListener(this), true),
81	fParagraphLayouts(other.fParagraphLayouts)
82{
83	if (fDocument.IsSet())
84		fDocument->AddListener(fTextListener);
85}
86
87
88TextDocumentLayout::~TextDocumentLayout()
89{
90	SetTextDocument(NULL);
91}
92
93
94void
95TextDocumentLayout::SetTextDocument(const TextDocumentRef& document)
96{
97	if (fDocument == document)
98		return;
99
100	if (fDocument.IsSet())
101		fDocument->RemoveListener(fTextListener);
102
103	fDocument = document;
104	_Init();
105	fLayoutValid = false;
106
107	if (fDocument.IsSet())
108		fDocument->AddListener(fTextListener);
109}
110
111
112void
113TextDocumentLayout::Invalidate()
114{
115	if (fDocument.IsSet())
116		InvalidateParagraphs(0, fDocument->CountParagraphs());
117}
118
119
120void
121TextDocumentLayout::InvalidateParagraphs(int32 start, int32 count)
122{
123	if (start < 0 || count == 0 || !fDocument.IsSet())
124		return;
125
126	fLayoutValid = false;
127
128	while (count > 0) {
129		const int32 paragraphCount = fDocument->CountParagraphs();
130		if (start >= paragraphCount)
131			break;
132		const Paragraph& paragraph = fDocument->ParagraphAtIndex(start);
133		if (start >= static_cast<int32>(fParagraphLayouts.size())) {
134			ParagraphLayoutRef layout(new(std::nothrow) ParagraphLayout(
135				paragraph), true);
136			if (!layout.IsSet()) {
137				fprintf(stderr, "TextDocumentLayout::InvalidateParagraphs() - "
138					"out of memory\n");
139				return;
140			}
141			try {
142				fParagraphLayouts.push_back(ParagraphLayoutInfo(0.0f, layout));
143			}
144			catch (std::bad_alloc& ba) {
145				fprintf(stderr, "bad_alloc when invalidating paragraphs\n");
146				return;
147			}
148		} else {
149			const ParagraphLayoutInfo& info = fParagraphLayouts[start];
150			info.layout->SetParagraph(paragraph);
151		}
152
153		start++;
154		count--;
155	}
156
157	// Remove any extra paragraph layouts
158	while (fDocument->CountParagraphs()
159			< static_cast<int32>(fParagraphLayouts.size()))
160		fParagraphLayouts.erase(fParagraphLayouts.end() - 1);
161}
162
163
164void
165TextDocumentLayout::SetWidth(float width)
166{
167	if (fWidth != width) {
168		fWidth = width;
169		fLayoutValid = false;
170	}
171}
172
173
174float
175TextDocumentLayout::Height()
176{
177	_ValidateLayout();
178
179	float height = 0.0f;
180
181	if (fParagraphLayouts.size() > 0) {
182		const ParagraphLayoutInfo& lastLayout
183			= fParagraphLayouts[fParagraphLayouts.size() - 1];
184		height = lastLayout.y + lastLayout.layout->Height();
185	}
186
187	return height;
188}
189
190
191void
192TextDocumentLayout::Draw(BView* view, const BPoint& offset,
193	const BRect& updateRect)
194{
195	_ValidateLayout();
196
197	int layoutCount = fParagraphLayouts.size();
198	for (int i = 0; i < layoutCount; i++) {
199		const ParagraphLayoutInfo& layout = fParagraphLayouts[i];
200		BPoint location(offset.x, offset.y + layout.y);
201		if (location.y > updateRect.bottom)
202			break;
203		if (location.y + layout.layout->Height() > updateRect.top)
204			layout.layout->Draw(view, location);
205	}
206}
207
208
209int32
210TextDocumentLayout::LineIndexForOffset(int32 textOffset)
211{
212	int32 index = _ParagraphLayoutIndexForOffset(textOffset);
213	if (index >= 0) {
214		int32 lineIndex = 0;
215		for (int32 i = 0; i < index; i++) {
216			lineIndex += fParagraphLayouts[i].layout->CountLines();
217		}
218
219		const ParagraphLayoutInfo& info = fParagraphLayouts[index];
220		return lineIndex + info.layout->LineIndexForOffset(textOffset);
221	}
222
223	return 0;
224}
225
226
227int32
228TextDocumentLayout::FirstOffsetOnLine(int32 lineIndex)
229{
230	int32 paragraphOffset;
231	int32 index = _ParagraphLayoutIndexForLineIndex(lineIndex, paragraphOffset);
232	if (index >= 0) {
233		const ParagraphLayoutInfo& info = fParagraphLayouts[index];
234		return info.layout->FirstOffsetOnLine(lineIndex) + paragraphOffset;
235	}
236
237	return 0;
238}
239
240
241int32
242TextDocumentLayout::LastOffsetOnLine(int32 lineIndex)
243{
244	int32 paragraphOffset;
245	int32 index = _ParagraphLayoutIndexForLineIndex(lineIndex, paragraphOffset);
246	if (index >= 0) {
247		const ParagraphLayoutInfo& info = fParagraphLayouts[index];
248		return info.layout->LastOffsetOnLine(lineIndex) + paragraphOffset;
249	}
250
251	return 0;
252}
253
254
255int32
256TextDocumentLayout::CountLines()
257{
258	_ValidateLayout();
259
260	int32 lineCount = 0;
261
262	int32 count = fParagraphLayouts.size();
263	for (int32 i = 0; i < count; i++) {
264		const ParagraphLayoutInfo& info = fParagraphLayouts[i];
265		lineCount += info.layout->CountLines();
266	}
267
268	return lineCount;
269}
270
271
272void
273TextDocumentLayout::GetLineBounds(int32 lineIndex, float& x1, float& y1,
274	float& x2, float& y2)
275{
276	int32 paragraphOffset;
277	int32 index = _ParagraphLayoutIndexForLineIndex(lineIndex, paragraphOffset);
278	if (index >= 0) {
279		const ParagraphLayoutInfo& info = fParagraphLayouts[index];
280		info.layout->GetLineBounds(lineIndex, x1, y1, x2, y2);
281		y1 += info.y;
282		y2 += info.y;
283		return;
284	}
285
286	x1 = 0.0f;
287	y1 = 0.0f;
288	x2 = 0.0f;
289	y2 = 0.0f;
290}
291
292
293void
294TextDocumentLayout::GetTextBounds(int32 textOffset, float& x1, float& y1,
295	float& x2, float& y2)
296{
297	int32 index = _ParagraphLayoutIndexForOffset(textOffset);
298	if (index >= 0) {
299		const ParagraphLayoutInfo& info = fParagraphLayouts[index];
300		info.layout->GetTextBounds(textOffset, x1, y1, x2, y2);
301		y1 += info.y;
302		y2 += info.y;
303		return;
304	}
305
306	x1 = 0.0f;
307	y1 = 0.0f;
308	x2 = 0.0f;
309	y2 = 0.0f;
310}
311
312
313int32
314TextDocumentLayout::TextOffsetAt(float x, float y, bool& rightOfCenter)
315{
316	_ValidateLayout();
317
318	int32 textOffset = 0;
319	rightOfCenter = false;
320
321	int32 paragraphs = fParagraphLayouts.size();
322	for (int32 i = 0; i < paragraphs; i++) {
323		const ParagraphLayoutInfo& info = fParagraphLayouts[i];
324		if (y > info.y + info.layout->Height()) {
325			textOffset += info.layout->CountGlyphs();
326			continue;
327		}
328
329		textOffset += info.layout->TextOffsetAt(x, y - info.y, rightOfCenter);
330		break;
331	}
332
333	return textOffset;
334}
335
336// #pragma mark - private
337
338
339void
340TextDocumentLayout::_ValidateLayout()
341{
342	if (!fLayoutValid) {
343		_Layout();
344		fLayoutValid = true;
345	}
346}
347
348
349void
350TextDocumentLayout::_Init()
351{
352	fParagraphLayouts.clear();
353
354	if (!fDocument.IsSet())
355		return;
356
357	int paragraphCount = fDocument->CountParagraphs();
358	for (int i = 0; i < paragraphCount; i++) {
359		const Paragraph& paragraph = fDocument->ParagraphAtIndex(i);
360		ParagraphLayoutRef layout(new(std::nothrow) ParagraphLayout(paragraph),
361			true);
362		if (!layout.IsSet()) {
363			fprintf(stderr, "TextDocumentLayout::_Layout() - out of memory\n");
364			return;
365		}
366		try {
367			fParagraphLayouts.push_back(ParagraphLayoutInfo(0.0f, layout));
368		}
369		catch (std::bad_alloc& ba) {
370			fprintf(stderr, "bad_alloc when inititalizing the text document "
371				"layout\n");
372			return;
373		}
374	}
375}
376
377
378void
379TextDocumentLayout::_Layout()
380{
381	float y = 0.0f;
382
383	int layoutCount = fParagraphLayouts.size();
384	for (int i = 0; i < layoutCount; i++) {
385		ParagraphLayoutInfo info = fParagraphLayouts[i];
386		const ParagraphStyle& style = info.layout->Style();
387
388		if (i > 0)
389			y += style.SpacingTop();
390
391		fParagraphLayouts[i] = ParagraphLayoutInfo(y, info.layout);
392
393		info.layout->SetWidth(fWidth);
394		y += info.layout->Height() + style.SpacingBottom();
395	}
396}
397
398
399int32
400TextDocumentLayout::_ParagraphLayoutIndexForOffset(int32& textOffset)
401{
402	_ValidateLayout();
403
404	int32 paragraphs = fParagraphLayouts.size();
405	for (int32 i = 0; i < paragraphs - 1; i++) {
406		const ParagraphLayoutInfo& info = fParagraphLayouts[i];
407
408		int32 length = info.layout->CountGlyphs();
409		if (textOffset >= length) {
410			textOffset -= length;
411			continue;
412		}
413
414		return i;
415	}
416
417	if (paragraphs > 0) {
418		const ParagraphLayoutInfo& info
419			= fParagraphLayouts[fParagraphLayouts.size() - 1];
420
421		// Return last paragraph if the textOffset is still within or
422		// exactly behind the last valid offset in that paragraph.
423		int32 length = info.layout->CountGlyphs();
424		if (textOffset <= length)
425			return paragraphs - 1;
426	}
427
428	return -1;
429}
430
431int32
432TextDocumentLayout::_ParagraphLayoutIndexForLineIndex(int32& lineIndex,
433	int32& paragraphOffset)
434{
435	_ValidateLayout();
436
437	paragraphOffset = 0;
438	int32 paragraphs = fParagraphLayouts.size();
439	for (int32 i = 0; i < paragraphs; i++) {
440		const ParagraphLayoutInfo& info = fParagraphLayouts[i];
441
442		int32 lineCount = info.layout->CountLines();
443		if (lineIndex >= lineCount) {
444			lineIndex -= lineCount;
445			paragraphOffset += info.layout->CountGlyphs();
446			continue;
447		}
448
449		return i;
450	}
451
452	return -1;
453}
454