1 /*
2 * Copyright (c) 2025 Huawei Device Co., Ltd.. All rights reserved.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "TextLineJustify.h"
17 #include <algorithm>
18 #include <cstddef>
19 #include <numeric>
20
21 namespace skia {
22 namespace textlayout {
allocateHighLevelOffsets(SkScalar ideographicMaxLen,ClusterLevelsIndices & clusterLevels,SkScalar & allocatedWidth)23 void TextLineJustify::allocateHighLevelOffsets(
24 SkScalar ideographicMaxLen, ClusterLevelsIndices& clusterLevels, SkScalar& allocatedWidth)
25 {
26 // High level allocation: punctuation
27 if (allocatedWidth < 0 || nearlyZero(allocatedWidth)) {
28 return;
29 }
30 // Pre-calculate the punctuation width to obtain the maximum width increment of each punctuation character.
31 SkScalar lastPunctStretch = 0.0f; // Extrusion width to the left of the previous punctuation.
32 constexpr size_t scaleFactor = 6; // Defines the maximum width of 1 / 6 ideographs.
33 for (auto& index : clusterLevels.highLevelIndices) {
34 if (index.isClusterPunct) {
35 SkScalar curPunctWidth = index.punctWidths;
36 SkScalar stretchWidth = std::max(0.0f, (ideographicMaxLen - curPunctWidth) / scaleFactor);
37 index.highLevelOffset = stretchWidth + lastPunctStretch;
38 lastPunctStretch = stretchWidth;
39 } else {
40 index.highLevelOffset = lastPunctStretch;
41 lastPunctStretch = 0.0f;
42 }
43 }
44 SkScalar highLevelMaxWidth =
45 std::accumulate(clusterLevels.highLevelIndices.begin(), clusterLevels.highLevelIndices.end(), 0.0f,
46 [](const SkScalar& a, const HighLevelInfo& b) { return a + b.highLevelOffset; });
47 if (highLevelMaxWidth > allocatedWidth) {
48 std::for_each(clusterLevels.highLevelIndices.begin(), clusterLevels.highLevelIndices.end(),
49 [highLevelMaxWidth, allocatedWidth](HighLevelInfo& val) {
50 val.highLevelOffset = allocatedWidth * val.highLevelOffset / highLevelMaxWidth;
51 });
52 allocatedWidth = 0;
53 } else {
54 allocatedWidth -= highLevelMaxWidth;
55 }
56 }
57
allocateMiddleLevelOffsets(SkScalar ideographicMaxLen,size_t prevClusterNotSpaceCount,ClusterLevelsIndices & clusterLevels,SkScalar & allocatedWidth)58 void TextLineJustify::allocateMiddleLevelOffsets(SkScalar ideographicMaxLen, size_t prevClusterNotSpaceCount,
59 ClusterLevelsIndices& clusterLevels, SkScalar& allocatedWidth)
60 {
61 // Middle level allocation: WhitespaceBreak, between ideographic and non-ideographic characters
62 if (allocatedWidth < 0 || nearlyZero(allocatedWidth)) {
63 return;
64 }
65 constexpr size_t scaleFactor = 12; // Defines the maximum width of 1 / 12 ideographs.
66 size_t totalPartitions = prevClusterNotSpaceCount + clusterLevels.middleLevelIndices.size();
67
68 SkScalar middleLevelMaxWidth = totalPartitions * ideographicMaxLen / scaleFactor;
69 if (middleLevelMaxWidth > allocatedWidth && totalPartitions > 0) {
70 clusterLevels.middleLevelOffset = allocatedWidth / totalPartitions;
71 allocatedWidth = 0;
72 } else {
73 clusterLevels.middleLevelOffset = ideographicMaxLen / scaleFactor;
74 allocatedWidth -= middleLevelMaxWidth;
75 }
76 }
77
allocateLowLevelOffsets(SkScalar ideographicMaxLen,ClusterLevelsIndices & clusterLevels,SkScalar & allocatedWidth)78 void TextLineJustify::allocateLowLevelOffsets(
79 SkScalar ideographicMaxLen, ClusterLevelsIndices& clusterLevels, SkScalar& allocatedWidth)
80 {
81 // Low level allocation: Between ideographic characters
82 if (allocatedWidth < 0 || nearlyZero(allocatedWidth)) {
83 return;
84 }
85 constexpr size_t scaleFactor = 6; // Defines the maximum width of 1 / 6 ideographs.
86
87 SkScalar lowLevelMaxWidth =
88 clusterLevels.LowLevelIndices.size() * ideographicMaxLen / scaleFactor;
89 if (lowLevelMaxWidth > allocatedWidth && clusterLevels.LowLevelIndices.size() > 0) {
90 clusterLevels.lowLevelOffset = allocatedWidth / clusterLevels.LowLevelIndices.size();
91 allocatedWidth = 0;
92 } else {
93 clusterLevels.lowLevelOffset = ideographicMaxLen / scaleFactor;
94 allocatedWidth -= lowLevelMaxWidth;
95 }
96 }
97
allocateRemainingWidth(SkScalar allocatedWidth,size_t prevClusterNotSpaceCount,ClusterLevelsIndices & clusterLevels)98 void TextLineJustify::allocateRemainingWidth(
99 SkScalar allocatedWidth, size_t prevClusterNotSpaceCount, ClusterLevelsIndices& clusterLevels)
100 {
101 // Bottom-up allocation: If the upper limit is reached, the remaining width is evenly allocated.
102 if (allocatedWidth < 0 || nearlyZero(allocatedWidth)) {
103 return;
104 }
105 const size_t totalPatches = clusterLevels.highLevelIndices.size() + clusterLevels.middleLevelIndices.size() +
106 clusterLevels.LowLevelIndices.size();
107 const SkScalar remainingOffset = allocatedWidth / (totalPatches + prevClusterNotSpaceCount);
108 std::for_each(clusterLevels.highLevelIndices.begin(), clusterLevels.highLevelIndices.end(),
109 [remainingOffset](HighLevelInfo& val) { val.highLevelOffset += remainingOffset; });
110 clusterLevels.middleLevelOffset += remainingOffset;
111 clusterLevels.lowLevelOffset += remainingOffset;
112 }
113
usingAutoSpaceWidth(const Cluster * cluster)114 SkScalar TextLineJustify::usingAutoSpaceWidth(const Cluster* cluster)
115 {
116 if (cluster == nullptr) {
117 return 0.0f;
118 }
119 auto& run = cluster->run();
120 auto start = cluster->startPos();
121 auto end = cluster->endPos();
122 auto correction = 0.0f;
123 if (end > start && !run.getAutoSpacings().empty()) {
124 correction = run.getAutoSpacings()[end - 1].fX - run.getAutoSpacings()[start].fY;
125 }
126
127 return cluster->width() + std::max(0.0f, correction);
128 }
129
determineShiftLevelForIdeographic(const Cluster * prevCluster,MiddleLevelInfo & middleLevelInfo)130 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevelForIdeographic(
131 const Cluster* prevCluster, MiddleLevelInfo& middleLevelInfo)
132 {
133 if (prevCluster == nullptr) {
134 return ShiftLevel::Undefined;
135 }
136 if (prevCluster->isIdeographic()) {
137 return ShiftLevel::LowLevel;
138 } else if (prevCluster->isPunctuation()) {
139 return ShiftLevel::HighLevel;
140 } else if (prevCluster->isWhitespaceBreak()) {
141 return ShiftLevel::MiddleLevel;
142 } else {
143 middleLevelInfo.isPrevClusterSpace = false;
144 return ShiftLevel::MiddleLevel;
145 }
146 }
147
determineShiftLevelForPunctuation(const Cluster * cluster,const Cluster * prevCluster,HighLevelInfo & highLevelInfo)148 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevelForPunctuation(
149 const Cluster* cluster, const Cluster* prevCluster, HighLevelInfo& highLevelInfo)
150 {
151 if (cluster == nullptr || prevCluster == nullptr) {
152 return ShiftLevel::Undefined;
153 }
154 // Prevents stretching between ellipsis unicode
155 if (cluster->isEllipsis() && prevCluster->isEllipsis()) {
156 return ShiftLevel::Undefined;
157 }
158 highLevelInfo.isClusterPunct = true;
159 highLevelInfo.punctWidths = usingAutoSpaceWidth(cluster);
160 return ShiftLevel::HighLevel;
161 }
162
determineShiftLevelForWhitespaceBreak(const Cluster * prevCluster)163 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevelForWhitespaceBreak(const Cluster* prevCluster)
164 {
165 if (prevCluster == nullptr) {
166 return ShiftLevel::Undefined;
167 }
168 if (prevCluster->isPunctuation()) {
169 return ShiftLevel::HighLevel;
170 }
171 return ShiftLevel::MiddleLevel;
172 }
173
determineShiftLevelForOtherCases(const Cluster * prevCluster,MiddleLevelInfo & middleLevelInfo)174 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevelForOtherCases(
175 const Cluster* prevCluster, MiddleLevelInfo& middleLevelInfo)
176 {
177 if (prevCluster == nullptr) {
178 return ShiftLevel::Undefined;
179 }
180 if (prevCluster->isIdeographic()) {
181 middleLevelInfo.isPrevClusterSpace = false;
182 return ShiftLevel::MiddleLevel;
183 } else if (prevCluster->isWhitespaceBreak()) {
184 return ShiftLevel::MiddleLevel;
185 } else if (prevCluster->isPunctuation()) {
186 return ShiftLevel::HighLevel;
187 }
188 return ShiftLevel::Undefined;
189 }
190
determineShiftLevel(const Cluster * cluster,const Cluster * prevCluster,HighLevelInfo & highLevelInfo,MiddleLevelInfo & middleLevelInfo,SkScalar & ideographicMaxLen)191 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevel(const Cluster* cluster, const Cluster* prevCluster,
192 HighLevelInfo& highLevelInfo, MiddleLevelInfo& middleLevelInfo, SkScalar& ideographicMaxLen)
193 {
194 if (cluster == nullptr || prevCluster == nullptr) {
195 return ShiftLevel::Undefined;
196 }
197 ShiftLevel shiftLevel = ShiftLevel::Undefined;
198 if (cluster->isIdeographic()) {
199 ideographicMaxLen = std::max(ideographicMaxLen, cluster->width());
200 shiftLevel = determineShiftLevelForIdeographic(prevCluster, middleLevelInfo);
201 } else if (cluster->isPunctuation()) {
202 shiftLevel = determineShiftLevelForPunctuation(cluster, prevCluster, highLevelInfo);
203 } else if (cluster->isWhitespaceBreak()) {
204 shiftLevel = determineShiftLevelForWhitespaceBreak(prevCluster);
205 } else {
206 shiftLevel = determineShiftLevelForOtherCases(prevCluster, middleLevelInfo);
207 }
208 return shiftLevel;
209 }
210
calculateClusterShift(const Cluster * cluster,ClusterIndex index,const ClusterLevelsIndices & clusterLevels)211 SkScalar TextLineJustify::calculateClusterShift(
212 const Cluster* cluster, ClusterIndex index, const ClusterLevelsIndices& clusterLevels)
213 {
214 if (cluster == nullptr) {
215 return 0.0f;
216 }
217 SkScalar step = 0.0f;
218 auto highLevelIterator =
219 std::find_if(clusterLevels.highLevelIndices.begin(), clusterLevels.highLevelIndices.end(),
220 [index](const HighLevelInfo& data) { return data.clusterIndex == index; });
221 auto lowLevelIterator =
222 std::find_if(clusterLevels.middleLevelIndices.begin(), clusterLevels.middleLevelIndices.end(),
223 [index](const MiddleLevelInfo& data) { return data.clusterIndex == index; });
224 if (highLevelIterator != clusterLevels.highLevelIndices.end()) {
225 size_t idx = static_cast<size_t>(std::distance(clusterLevels.highLevelIndices.begin(), highLevelIterator));
226 step = clusterLevels.highLevelIndices[idx].highLevelOffset;
227 } else if (lowLevelIterator != clusterLevels.middleLevelIndices.end()) {
228 // Because both sides of the WhitespaceBreak are equally widened, the
229 // ideographic and non-ideographic characters are only widened once.
230 // So the front is not WhitespaceBreak, and the count increases by 1.
231 step = lowLevelIterator->isPrevClusterSpace ? clusterLevels.middleLevelOffset
232 : clusterLevels.middleLevelOffset * (1 + 1);
233 } else if (std::find(clusterLevels.LowLevelIndices.begin(), clusterLevels.LowLevelIndices.end(), index) !=
234 clusterLevels.LowLevelIndices.end()) {
235 step = clusterLevels.lowLevelOffset;
236 }
237 return step;
238 }
239
justifyShiftCluster(const SkScalar maxWidth,SkScalar textLen,SkScalar ideographicMaxLen,size_t prevClusterNotSpaceCount,ClusterLevelsIndices & clusterLevels)240 void TextLineJustify::justifyShiftCluster(const SkScalar maxWidth, SkScalar textLen, SkScalar ideographicMaxLen,
241 size_t prevClusterNotSpaceCount, ClusterLevelsIndices& clusterLevels)
242 {
243 SkScalar allocatedWidth = maxWidth - textLen - (textLineRef.ellipsis() ? textLineRef.ellipsis()->fAdvanceX() : 0);
244 const SkScalar verifyShift = allocatedWidth;
245 // Allocate offsets for each level
246 allocateHighLevelOffsets(ideographicMaxLen, clusterLevels, allocatedWidth);
247 allocateMiddleLevelOffsets(ideographicMaxLen, prevClusterNotSpaceCount, clusterLevels, allocatedWidth);
248 allocateLowLevelOffsets(ideographicMaxLen, clusterLevels, allocatedWidth);
249 allocateRemainingWidth(allocatedWidth, prevClusterNotSpaceCount, clusterLevels);
250 // Deal with the ghost spaces
251 auto ghostShift = maxWidth - textLineRef.widthWithoutEllipsis();
252 // Reallocate the width of each cluster: Clusters of different levels use different offsets.
253 SkScalar shift = 0.0f;
254 SkScalar prevShift = 0.0f;
255 textLineRef.iterateThroughClustersInGlyphsOrder(false, true,
256 [this, &shift, &prevShift, clusterLevels, ghostShift](
257 const Cluster* cluster, ClusterIndex index, bool ghost) {
258 if (cluster == nullptr) {
259 return true;
260 }
261 if (ghost) {
262 if (cluster->run().leftToRight()) {
263 textLineRef.updateClusterOffsets(cluster, ghostShift, ghostShift);
264 }
265 return true;
266 }
267 SkScalar step = calculateClusterShift(cluster, index, clusterLevels);
268 shift += step;
269 textLineRef.updateClusterOffsets(cluster, shift, prevShift);
270 prevShift = shift;
271 return true;
272 });
273 SkAssertResult(nearlyEqual(shift, verifyShift));
274 }
275
justify(SkScalar maxWidth)276 bool TextLineJustify::justify(SkScalar maxWidth)
277 {
278 SkScalar textLen = 0.0f;
279 SkScalar ideographicMaxLen = 0.0f;
280 ClusterLevelsIndices clusterLevels;
281 size_t prevClusterNotSpaceCount = 0;
282 const Cluster* prevCluster = nullptr;
283 bool isFirstCluster = true;
284 // Calculate text length and define three types of labels to trace cluster stretch level.
285 textLineRef.iterateThroughClustersInGlyphsOrder(
286 false, false, [&](const Cluster* cluster, ClusterIndex index, bool ghost) {
287 if (cluster != nullptr && isFirstCluster) {
288 isFirstCluster = false;
289 prevCluster = cluster;
290 textLen += usingAutoSpaceWidth(cluster);
291 ideographicMaxLen =
292 (cluster->isIdeographic()) ? std::max(ideographicMaxLen, cluster->width()) : ideographicMaxLen;
293 return true;
294 }
295 HighLevelInfo highLevelInfo;
296 MiddleLevelInfo middleLevelInfo;
297 ShiftLevel shiftLevel =
298 determineShiftLevel(cluster, prevCluster, highLevelInfo, middleLevelInfo, ideographicMaxLen);
299 switch (shiftLevel) {
300 case ShiftLevel::HighLevel:
301 highLevelInfo.clusterIndex = index;
302 clusterLevels.highLevelIndices.push_back(highLevelInfo);
303 break;
304 case ShiftLevel::MiddleLevel:
305 // Because both sides of the WhitespaceBreak are equally widened, the
306 // ideographic and non-ideographic characters are only widened once.
307 // So the front is not WhitespaceBreak, and the count increases by 1.
308 prevClusterNotSpaceCount += middleLevelInfo.isPrevClusterSpace ? 0 : 1;
309 middleLevelInfo.clusterIndex = index;
310 clusterLevels.middleLevelIndices.push_back(middleLevelInfo);
311 break;
312 case ShiftLevel::LowLevel:
313 clusterLevels.LowLevelIndices.push_back(index);
314 break;
315 default:
316 break;
317 }
318 textLen += usingAutoSpaceWidth(cluster);
319 prevCluster = cluster;
320 return true;
321 });
322 if (clusterLevels.empty()) {
323 textLineRef.justifyUpdateRtlWidth(maxWidth, textLen);
324 return false;
325 }
326 justifyShiftCluster(maxWidth, textLen, ideographicMaxLen, prevClusterNotSpaceCount, clusterLevels);
327 return true;
328 }
329
330 } // namespace textlayout
331 } // namespace skia
332