| /* | |
| * Copyright 2026 Rive | |
| */ | |
| #include "rive/text/line_break.hpp" | |
| #include <cassert> | |
| #include <vector> | |
| using namespace rive; | |
| namespace rive | |
| { | |
| extern const uint32_t kLineBreakHighStart; | |
| extern const uint32_t kLineBreakTopShift; | |
| extern const uint32_t kLineBreakBlockShift; | |
| extern const uint8_t kLineBreakDefault; | |
| extern const LineBreakProps kLineBreakExtClasses[]; | |
| extern const uint8_t kLineBreakTop[]; | |
| extern const uint16_t kLineBreakMid[]; | |
| extern const uint8_t kLineBreakData[]; | |
| } // namespace rive | |
| LineBreakProps rive::lineBreakProps(uint32_t cp) | |
| { | |
| if (cp >= kLineBreakHighStart) | |
| { | |
| return kLineBreakExtClasses[kLineBreakDefault]; | |
| } | |
| uint32_t midEntries = 1u << (kLineBreakTopShift - kLineBreakBlockShift); | |
| uint32_t mid = kLineBreakTop[cp >> kLineBreakTopShift] * midEntries + | |
| ((cp >> kLineBreakBlockShift) & (midEntries - 1)); | |
| uint32_t blockSize = 1u << kLineBreakBlockShift; | |
| uint32_t data = kLineBreakMid[mid] * blockSize + (cp & (blockSize - 1)); | |
| return kLineBreakExtClasses[kLineBreakData[data]]; | |
| } | |
| namespace | |
| { | |
| using C = LineBreakClass; | |
| // One entry per codepoint that survives LB9 (combining sequences collapse | |
| // onto their base), so the rules can index neighbours directly. | |
| struct Item | |
| { | |
| C cls; | |
| uint8_t flags; | |
| bool afterZwj; // the codepoint just before this item is a ZWJ | |
| uint32_t index; // first codepoint of the item | |
| }; | |
| // Items live on the stack for typical UI strings. | |
| constexpr size_t kInlineItems = 256; | |
| bool isHardBreak(C c) | |
| { | |
| return c == C::BK || c == C::CR || c == C::LF || c == C::NL; | |
| } | |
| bool isAlphabetic(C c) { return c == C::AL || c == C::HL; } | |
| bool isBrahmicBase(const Item& i) | |
| { | |
| return i.cls == C::AK || i.cls == C::AS || | |
| (i.flags & lineBreakDottedCircle) != 0; | |
| } | |
| bool isAkOrDottedCircle(const Item& i) | |
| { | |
| return i.cls == C::AK || (i.flags & lineBreakDottedCircle) != 0; | |
| } | |
| bool isEastAsian(const Item& i) { return (i.flags & lineBreakEastAsian) != 0; } | |
| bool isKorean(C c) | |
| { | |
| return c == C::JL || c == C::JV || c == C::JT || c == C::H2 || c == C::H3; | |
| } | |
| // Decides the boundary before items[k] (1 <= k < items.size()). Rules run in | |
| // UAX #14 order and the first match wins. | |
| LineBreak breakBefore(const Item* items, | |
| size_t count, | |
| size_t k, | |
| size_t beforeSpaces, | |
| size_t regionalIndicators) | |
| { | |
| const Item& prev = items[k - 1]; | |
| const Item& next = items[k]; | |
| C p = prev.cls; | |
| C n = next.cls; | |
| // Class of the last item before any run of spaces ending at k - 1. SP | |
| // stands in when there is none so no rule matches it. | |
| C ps = beforeSpaces < count ? items[beforeSpaces].cls : C::SP; | |
| bool atStart = k == 1; | |
| // LB4, LB5 | |
| if (p == C::BK) | |
| { | |
| return LineBreak::mandatory; | |
| } | |
| if (p == C::CR && n == C::LF) | |
| { | |
| return LineBreak::none; | |
| } | |
| if (p == C::CR || p == C::LF || p == C::NL) | |
| { | |
| return LineBreak::mandatory; | |
| } | |
| // LB6, LB7 | |
| if (isHardBreak(n) || n == C::SP || n == C::ZW) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB8 | |
| if (p == C::ZW || (p == C::SP && ps == C::ZW)) | |
| { | |
| return LineBreak::allowed; | |
| } | |
| // LB8a | |
| if (next.afterZwj) | |
| { | |
| return LineBreak::none; | |
| } | |
| // The two dominant pairs skip the rule chain: no rule between here and | |
| // LB28 touches letter-letter, and none before LB31 touches ideograph | |
| // pairs. | |
| if (isAlphabetic(p) && isAlphabetic(n)) | |
| { | |
| return LineBreak::none; | |
| } | |
| if (p == C::ID && n == C::ID) | |
| { | |
| return LineBreak::allowed; | |
| } | |
| // LB11, LB12 | |
| if (n == C::WJ || p == C::WJ || p == C::GL) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB12a | |
| if (n == C::GL && p != C::SP && p != C::BA && p != C::HY && p != C::HH) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB13 | |
| if (n == C::CL || n == C::CP || n == C::EX || n == C::SY) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB14 | |
| if (p == C::OP || (p == C::SP && ps == C::OP)) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB15a | |
| { | |
| size_t j = p == C::SP ? beforeSpaces : k - 1; | |
| if (j < count && items[j].cls == C::QU && | |
| (items[j].flags & lineBreakPi) != 0) | |
| { | |
| bool ok = j == 0; | |
| if (!ok) | |
| { | |
| C b = items[j - 1].cls; | |
| ok = isHardBreak(b) || b == C::OP || b == C::QU || b == C::GL || | |
| b == C::SP || b == C::ZW; | |
| } | |
| if (ok) | |
| { | |
| return LineBreak::none; | |
| } | |
| } | |
| } | |
| // LB15b | |
| if (n == C::QU && (next.flags & lineBreakPf) != 0) | |
| { | |
| bool ok = k + 1 == count; | |
| if (!ok) | |
| { | |
| C a = items[k + 1].cls; | |
| ok = a == C::SP || a == C::GL || a == C::WJ || a == C::CL || | |
| a == C::QU || a == C::CP || a == C::EX || a == C::IS || | |
| a == C::SY || isHardBreak(a) || a == C::ZW; | |
| } | |
| if (ok) | |
| { | |
| return LineBreak::none; | |
| } | |
| } | |
| // LB15c | |
| if (p == C::SP && n == C::IS && k + 1 < count && items[k + 1].cls == C::NU) | |
| { | |
| return LineBreak::allowed; | |
| } | |
| // LB15d | |
| if (n == C::IS) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB16 | |
| if (n == C::NS && (p == C::CL || p == C::CP || | |
| (p == C::SP && (ps == C::CL || ps == C::CP)))) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB17 | |
| if (n == C::B2 && (p == C::B2 || (p == C::SP && ps == C::B2))) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB18 | |
| if (p == C::SP) | |
| { | |
| return LineBreak::allowed; | |
| } | |
| // LB19 | |
| if (n == C::QU && (next.flags & lineBreakPi) == 0) | |
| { | |
| return LineBreak::none; | |
| } | |
| if (p == C::QU && (prev.flags & lineBreakPf) == 0) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB19a | |
| if (n == C::QU) | |
| { | |
| if (!isEastAsian(prev) || k + 1 == count || !isEastAsian(items[k + 1])) | |
| { | |
| return LineBreak::none; | |
| } | |
| } | |
| if (p == C::QU) | |
| { | |
| if (!isEastAsian(next) || atStart || !isEastAsian(items[k - 2])) | |
| { | |
| return LineBreak::none; | |
| } | |
| } | |
| // LB20 | |
| if (n == C::CB || p == C::CB) | |
| { | |
| return LineBreak::allowed; | |
| } | |
| // LB20a | |
| if ((p == C::HY || p == C::HH) && isAlphabetic(n)) | |
| { | |
| bool ok = atStart; | |
| if (!ok) | |
| { | |
| C b = items[k - 2].cls; | |
| ok = isHardBreak(b) || b == C::SP || b == C::ZW || b == C::CB || | |
| b == C::GL; | |
| } | |
| if (ok) | |
| { | |
| return LineBreak::none; | |
| } | |
| } | |
| // LB21 | |
| if (n == C::BA || n == C::HH || n == C::HY || n == C::NS || p == C::BB) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB21a | |
| if ((p == C::HY || p == C::HH) && n != C::HL && !atStart && | |
| items[k - 2].cls == C::HL) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB21b | |
| if (p == C::SY && n == C::HL) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB22 | |
| if (n == C::IN) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB23 | |
| if ((isAlphabetic(p) && n == C::NU) || (p == C::NU && isAlphabetic(n))) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB23a | |
| if (p == C::PR && (n == C::ID || n == C::EB || n == C::EM)) | |
| { | |
| return LineBreak::none; | |
| } | |
| if ((p == C::ID || p == C::EB || p == C::EM) && n == C::PO) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB24 | |
| if (((p == C::PR || p == C::PO) && isAlphabetic(n)) || | |
| (isAlphabetic(p) && (n == C::PR || n == C::PO))) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB25 | |
| if (n == C::PO || n == C::PR || n == C::NU) | |
| { | |
| // NU (SY | IS)* (CL | CP)? × (PO | PR) and NU (SY | IS)* × NU | |
| size_t j = k - 1; | |
| bool searching = true; | |
| if (n != C::NU && (items[j].cls == C::CL || items[j].cls == C::CP)) | |
| { | |
| searching = j > 0; | |
| j--; | |
| } | |
| while (searching && (items[j].cls == C::SY || items[j].cls == C::IS)) | |
| { | |
| searching = j > 0; | |
| j--; | |
| } | |
| if (searching && items[j].cls == C::NU) | |
| { | |
| return LineBreak::none; | |
| } | |
| } | |
| if (p == C::PO || p == C::PR) | |
| { | |
| if (n == C::NU) | |
| { | |
| return LineBreak::none; | |
| } | |
| if (n == C::OP && k + 1 < count) | |
| { | |
| if (items[k + 1].cls == C::NU) | |
| { | |
| return LineBreak::none; | |
| } | |
| if (items[k + 1].cls == C::IS && k + 2 < count && | |
| items[k + 2].cls == C::NU) | |
| { | |
| return LineBreak::none; | |
| } | |
| } | |
| } | |
| if ((p == C::HY || p == C::IS) && n == C::NU) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB26 | |
| if (p == C::JL && (n == C::JL || n == C::JV || n == C::H2 || n == C::H3)) | |
| { | |
| return LineBreak::none; | |
| } | |
| if ((p == C::JV || p == C::H2) && (n == C::JV || n == C::JT)) | |
| { | |
| return LineBreak::none; | |
| } | |
| if ((p == C::JT || p == C::H3) && n == C::JT) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB27 | |
| if ((isKorean(p) && n == C::PO) || (p == C::PR && isKorean(n))) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB28 | |
| if (isAlphabetic(p) && isAlphabetic(n)) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB28a | |
| if (p == C::AP && isBrahmicBase(next)) | |
| { | |
| return LineBreak::none; | |
| } | |
| if (isBrahmicBase(prev) && (n == C::VF || n == C::VI)) | |
| { | |
| return LineBreak::none; | |
| } | |
| if (p == C::VI && !atStart && isBrahmicBase(items[k - 2]) && | |
| isAkOrDottedCircle(next)) | |
| { | |
| return LineBreak::none; | |
| } | |
| if (isBrahmicBase(prev) && isBrahmicBase(next) && k + 1 < count && | |
| items[k + 1].cls == C::VF) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB29 | |
| if (p == C::IS && isAlphabetic(n)) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB30 | |
| if ((isAlphabetic(p) || p == C::NU) && n == C::OP && !isEastAsian(next)) | |
| { | |
| return LineBreak::none; | |
| } | |
| if (p == C::CP && !isEastAsian(prev) && (isAlphabetic(n) || n == C::NU)) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB30a | |
| if (p == C::RI && n == C::RI && (regionalIndicators & 1)) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB30b | |
| if (n == C::EM && | |
| (p == C::EB || (prev.flags & lineBreakPictographicCn) != 0)) | |
| { | |
| return LineBreak::none; | |
| } | |
| // LB31 | |
| return LineBreak::allowed; | |
| } | |
| } // namespace | |
| void rive::computeLineBreaks(Span<const uint32_t> text, Span<LineBreak> out) | |
| { | |
| size_t n = text.size(); | |
| assert(out.size() == n + 1); | |
| for (size_t i = 0; i <= n; i++) | |
| { | |
| out[i] = LineBreak::none; | |
| } | |
| if (n == 0) | |
| { | |
| return; | |
| } | |
| out[n] = LineBreak::allowed; | |
| // LB1 is baked into the table; CJ resolves to NS (strict) here. LB9 and | |
| // LB10: combining marks and ZWJ attach to any base except hard breaks, | |
| // spaces and ZW, otherwise they act as AL. | |
| Item inlineItems[kInlineItems]; | |
| std::vector<Item> heapItems; | |
| Item* items = inlineItems; | |
| if (n > kInlineItems) | |
| { | |
| heapItems.resize(n); | |
| items = heapItems.data(); | |
| } | |
| size_t count = 0; | |
| bool afterZwj = false; | |
| for (uint32_t i = 0; i < n; i++) | |
| { | |
| LineBreakProps props = lineBreakProps(text[i]); | |
| C cls = props.cls; | |
| bool isZwj = cls == C::ZWJ; | |
| if (cls == C::CJ) | |
| { | |
| cls = C::NS; | |
| } | |
| if (cls == C::CM || cls == C::ZWJ) | |
| { | |
| if (count > 0) | |
| { | |
| C base = items[count - 1].cls; | |
| if (!isHardBreak(base) && base != C::SP && base != C::ZW) | |
| { | |
| afterZwj = isZwj; | |
| continue; | |
| } | |
| } | |
| cls = C::AL; | |
| props.flags = 0; | |
| } | |
| items[count++] = {cls, props.flags, afterZwj, i}; | |
| afterZwj = isZwj; | |
| } | |
| size_t beforeSpaces = count; | |
| // Count of consecutive regional indicators ending at k - 1. | |
| size_t regionalIndicators = 0; | |
| for (size_t k = 1; k < count; k++) | |
| { | |
| if (items[k - 1].cls != C::SP) | |
| { | |
| beforeSpaces = k - 1; | |
| } | |
| regionalIndicators = | |
| items[k - 1].cls == C::RI ? regionalIndicators + 1 : 0; | |
| out[items[k].index] = | |
| breakBefore(items, count, k, beforeSpaces, regionalIndicators); | |
| } | |
| if (isHardBreak(items[count - 1].cls)) | |
| { | |
| out[n] = LineBreak::mandatory; | |
| } | |
| } |