|
16 | 16 | #include "utils/StringUtils.h" |
17 | 17 | #include "utils/log.h" |
18 | 18 |
|
| 19 | +#include <algorithm> |
| 20 | +#include <functional> |
19 | 21 | #include <limits> |
20 | 22 |
|
21 | 23 | CGUIString::CGUIString(iString start, iString end, bool carriageReturn) |
@@ -579,63 +581,115 @@ void CGUITextLayout::WrapText(const vecText &text, float maxWidth) |
579 | 581 | continue; |
580 | 582 | } |
581 | 583 |
|
582 | | - vecText::const_iterator pos = line.m_text.begin(); |
583 | | - vecText::const_iterator lastBeginPos = line.m_text.begin(); |
584 | | - vecText::const_iterator lastSpacePos = line.m_text.end(); |
585 | | - vecText curLine; |
586 | | - |
587 | | - while (pos < line.m_text.end()) |
| 584 | + std::vector<character_t> tmp; |
| 585 | + tmp.reserve(line.m_text.size()); |
| 586 | + auto widthOf = [&](const std::vector<character_t>::const_iterator start, |
| 587 | + const std::vector<character_t>::const_iterator end) -> float |
588 | 588 | { |
589 | | - // Get the current letter in the string |
590 | | - const character_t& letter = *pos; |
| 589 | + if (start == end) |
| 590 | + return 0.0f; |
| 591 | + tmp.assign(start, end); |
| 592 | + return m_font->GetTextWidth(tmp); |
| 593 | + }; |
591 | 594 |
|
592 | | - if (CanWrapAtLetter(letter)) // Check for a space char |
593 | | - lastSpacePos = pos; |
| 595 | + auto skipLeadingSpaces = [&](vecText::const_iterator& it) |
| 596 | + { |
| 597 | + it = std::find_if_not(it, line.m_text.end(), |
| 598 | + std::bind_front(&CGUITextLayout::CanWrapAtLetter, this)); |
| 599 | + }; |
594 | 600 |
|
595 | | - curLine.emplace_back(letter); |
| 601 | + auto current = line.m_text.begin(); |
| 602 | + skipLeadingSpaces(current); |
596 | 603 |
|
597 | | - const float currWidth = m_font->GetTextWidth(curLine); |
| 604 | + float currentWidth = 0.0f; |
| 605 | + std::vector<character_t>::const_iterator currentStart = current; |
| 606 | + // track end without trailing spaces |
| 607 | + std::vector<character_t>::const_iterator lastNonSpaceInLine = currentStart; |
598 | 608 |
|
599 | | - if (currWidth > maxWidth) |
| 609 | + while (current != line.m_text.end()) |
| 610 | + { |
| 611 | + // Find next candidate wrap position |
| 612 | + auto wordEnd = std::find_if(current, line.m_text.end(), |
| 613 | + std::bind_front(&CGUITextLayout::CanWrapAtLetter, this)); |
| 614 | + const bool hasSpace = (wordEnd != line.m_text.end()); |
| 615 | + const float wordWidth = widthOf(current, wordEnd); |
| 616 | + const float spaceWidth = hasSpace ? widthOf(wordEnd, wordEnd + 1) : 0.0f; |
| 617 | + |
| 618 | + // Try to include word + trailing space |
| 619 | + if (currentWidth + wordWidth + spaceWidth <= maxWidth) |
600 | 620 | { |
601 | | - if (lastSpacePos > pos) // No space char where split the line, so split by char |
602 | | - { |
603 | | - // If the pos is equal to lastBeginPos, maxWidth is not large enough to contain 1 character |
604 | | - // Push a line with the single character and move on to the next character. |
605 | | - if (pos == lastBeginPos) |
606 | | - ++pos; |
607 | | - |
608 | | - CGUIString linePart{lastBeginPos, pos, false}; |
609 | | - m_lines.emplace_back(linePart); |
610 | | - } |
611 | | - else |
612 | | - { |
613 | | - CGUIString linePart{lastBeginPos, lastSpacePos, false}; |
614 | | - m_lines.emplace_back(linePart); |
| 621 | + currentWidth += wordWidth + spaceWidth; |
| 622 | + lastNonSpaceInLine = wordEnd; // exclude trailing space |
| 623 | + current = wordEnd; |
| 624 | + if (hasSpace) |
| 625 | + ++current; |
| 626 | + continue; |
| 627 | + } |
615 | 628 |
|
616 | | - pos = lastSpacePos + 1; |
617 | | - lastSpacePos = line.m_text.end(); |
618 | | - } |
| 629 | + // Try to include word without trailing space |
| 630 | + if (currentWidth + wordWidth <= maxWidth) |
| 631 | + { |
| 632 | + m_lines.emplace_back(currentStart, wordEnd, false); |
| 633 | + if (m_lines.size() >= nMaxLines) |
| 634 | + return; |
619 | 635 |
|
620 | | - curLine.clear(); |
621 | | - lastBeginPos = pos; |
| 636 | + current = wordEnd; |
| 637 | + skipLeadingSpaces(current); |
| 638 | + currentStart = current; |
| 639 | + currentWidth = 0.0f; |
| 640 | + lastNonSpaceInLine = currentStart; |
| 641 | + continue; |
| 642 | + } |
622 | 643 |
|
| 644 | + // word itself doesn't fit after existing content: wrap before it (trim trailing spaces) |
| 645 | + if (currentWidth > 0.0f) |
| 646 | + { |
| 647 | + const std::vector<character_t>::const_iterator emitEnd = |
| 648 | + lastNonSpaceInLine > currentStart ? lastNonSpaceInLine : wordEnd; |
| 649 | + m_lines.emplace_back(currentStart, emitEnd, false); |
623 | 650 | if (m_lines.size() >= nMaxLines) |
624 | 651 | return; |
625 | 652 |
|
| 653 | + // Start a new line; keep pos at start of the overflowing word |
| 654 | + skipLeadingSpaces(current); |
| 655 | + currentStart = current; |
| 656 | + currentWidth = 0.0f; |
| 657 | + lastNonSpaceInLine = currentStart; |
626 | 658 | continue; |
627 | 659 | } |
628 | 660 |
|
629 | | - ++pos; |
630 | | - } |
| 661 | + if (current == wordEnd) |
| 662 | + break; |
631 | 663 |
|
632 | | - // Add the remaining text part |
633 | | - if (!curLine.empty()) |
634 | | - { |
635 | | - CGUIString linePart{curLine.begin(), curLine.end(), false}; |
636 | | - m_lines.emplace_back(linePart); |
| 664 | + // current line is empty and word is too long: split by character using a safe linear scan. |
| 665 | + // Do not assume monotonic width because shaping/kerning can make width shrink or grow non-linearly. |
| 666 | + tmp.clear(); |
| 667 | + size_t bestCount = 0; |
| 668 | + for (auto it = current; it != wordEnd; ++it) |
| 669 | + { |
| 670 | + tmp.push_back(*it); |
| 671 | + if (m_font->GetTextWidth(tmp) <= maxWidth) |
| 672 | + bestCount = tmp.size(); |
| 673 | + } |
| 674 | + if (bestCount == 0) |
| 675 | + bestCount = 1; // ensure progress even if a single glyph is wider than maxWidth |
| 676 | + |
| 677 | + const auto cut = current + static_cast<ptrdiff_t>(bestCount); |
| 678 | + m_lines.emplace_back(current, cut, false); |
| 679 | + if (m_lines.size() >= nMaxLines) |
| 680 | + return; |
| 681 | + |
| 682 | + current = cut; |
| 683 | + skipLeadingSpaces(current); |
| 684 | + currentStart = current; |
| 685 | + currentWidth = 0.0f; |
| 686 | + lastNonSpaceInLine = currentStart; |
637 | 687 | } |
638 | 688 |
|
| 689 | + // Add remaining text part of this paragraph line |
| 690 | + if (currentStart < line.m_text.end()) |
| 691 | + m_lines.emplace_back(currentStart, line.m_text.end(), false); |
| 692 | + |
639 | 693 | // Restore carriage return marker for the end of paragraph |
640 | 694 | if (!m_lines.empty()) |
641 | 695 | m_lines.back().m_carriageReturn = line.m_carriageReturn; |
@@ -767,5 +821,3 @@ void CGUITextLayout::Reset() |
767 | 821 | m_lastUtf8Text.clear(); |
768 | 822 | m_textWidth = m_textHeight = 0; |
769 | 823 | } |
770 | | - |
771 | | - |
|
0 commit comments