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 14 class LayoutTextListener : public TextListener { 15 public: 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 43 private: 44 TextDocumentLayout* fLayout; 45 }; 46 47 48 49 TextDocumentLayout::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 61 TextDocumentLayout::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 74 TextDocumentLayout::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 88 TextDocumentLayout::~TextDocumentLayout() 89 { 90 SetTextDocument(NULL); 91 } 92 93 94 void 95 TextDocumentLayout::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 112 void 113 TextDocumentLayout::Invalidate() 114 { 115 if (fDocument.IsSet()) 116 InvalidateParagraphs(0, fDocument->CountParagraphs()); 117 } 118 119 120 void 121 TextDocumentLayout::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 164 void 165 TextDocumentLayout::SetWidth(float width) 166 { 167 if (fWidth != width) { 168 fWidth = width; 169 fLayoutValid = false; 170 } 171 } 172 173 174 float 175 TextDocumentLayout::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 191 void 192 TextDocumentLayout::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 209 int32 210 TextDocumentLayout::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 227 int32 228 TextDocumentLayout::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 241 int32 242 TextDocumentLayout::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 255 int32 256 TextDocumentLayout::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 272 void 273 TextDocumentLayout::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 293 void 294 TextDocumentLayout::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 313 int32 314 TextDocumentLayout::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 339 void 340 TextDocumentLayout::_ValidateLayout() 341 { 342 if (!fLayoutValid) { 343 _Layout(); 344 fLayoutValid = true; 345 } 346 } 347 348 349 void 350 TextDocumentLayout::_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 378 void 379 TextDocumentLayout::_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 399 int32 400 TextDocumentLayout::_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 431 int32 432 TextDocumentLayout::_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