blob: f6e1406b5e6fa5c154d270b7fcbe11e5478f6419 [file]
/*
* 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;
}
}