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.second.isClusterPunct) {
35 SkScalar curPunctWidth = index.second.punctWidths;
36 SkScalar stretchWidth = std::max(0.0f, (ideographicMaxLen - curPunctWidth) / scaleFactor);
37 index.second.highLevelOffset = stretchWidth + lastPunctStretch;
38 lastPunctStretch = stretchWidth;
39 } else {
40 index.second.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 auto& b) { return a + b.second.highLevelOffset; });
47 if (highLevelMaxWidth > allocatedWidth) {
48 std::for_each(clusterLevels.highLevelIndices.begin(), clusterLevels.highLevelIndices.end(),
49 [highLevelMaxWidth, allocatedWidth](auto& val) {
50 val.second.highLevelOffset = allocatedWidth * val.second.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](auto& val) { val.second.highLevelOffset += remainingOffset; });
110 clusterLevels.middleLevelOffset += remainingOffset;
111 clusterLevels.lowLevelOffset += remainingOffset;
112 }
113
determineShiftLevelForIdeographic(const Cluster * prevCluster,MiddleLevelInfo & middleLevelInfo)114 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevelForIdeographic(
115 const Cluster* prevCluster, MiddleLevelInfo& middleLevelInfo)
116 {
117 if (prevCluster == nullptr) {
118 return ShiftLevel::Undefined;
119 }
120 if (prevCluster->isIdeographic()) {
121 return ShiftLevel::LowLevel;
122 } else if (prevCluster->isPunctuation()) {
123 return ShiftLevel::HighLevel;
124 } else if (prevCluster->isWhitespaceBreak()) {
125 return ShiftLevel::MiddleLevel;
126 } else {
127 middleLevelInfo.isPrevClusterSpace = false;
128 return ShiftLevel::MiddleLevel;
129 }
130 }
131
determineShiftLevelForPunctuation(const Cluster * cluster,const Cluster * prevCluster,HighLevelInfo & highLevelInfo)132 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevelForPunctuation(
133 const Cluster* cluster, const Cluster* prevCluster, HighLevelInfo& highLevelInfo)
134 {
135 if (cluster == nullptr || prevCluster == nullptr) {
136 return ShiftLevel::Undefined;
137 }
138 // Prevents stretching between ellipsis unicode
139 if (cluster->isEllipsis() && prevCluster->isEllipsis()) {
140 return ShiftLevel::Undefined;
141 }
142 highLevelInfo.isClusterPunct = true;
143 highLevelInfo.punctWidths = textLineRef.usingAutoSpaceWidth(cluster);
144 return ShiftLevel::HighLevel;
145 }
146
determineShiftLevelForWhitespaceBreak(const Cluster * prevCluster)147 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevelForWhitespaceBreak(const Cluster* prevCluster)
148 {
149 if (prevCluster == nullptr) {
150 return ShiftLevel::Undefined;
151 }
152 if (prevCluster->isPunctuation()) {
153 return ShiftLevel::HighLevel;
154 }
155 return ShiftLevel::MiddleLevel;
156 }
157
determineShiftLevelForOtherCases(const Cluster * prevCluster,MiddleLevelInfo & middleLevelInfo)158 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevelForOtherCases(
159 const Cluster* prevCluster, MiddleLevelInfo& middleLevelInfo)
160 {
161 if (prevCluster == nullptr) {
162 return ShiftLevel::Undefined;
163 }
164 if (prevCluster->isIdeographic()) {
165 middleLevelInfo.isPrevClusterSpace = false;
166 return ShiftLevel::MiddleLevel;
167 } else if (prevCluster->isWhitespaceBreak()) {
168 return ShiftLevel::MiddleLevel;
169 } else if (prevCluster->isPunctuation()) {
170 return ShiftLevel::HighLevel;
171 }
172 return ShiftLevel::Undefined;
173 }
174
determineShiftLevel(const Cluster * cluster,const Cluster * prevCluster,HighLevelInfo & highLevelInfo,MiddleLevelInfo & middleLevelInfo,SkScalar & ideographicMaxLen)175 TextLineJustify::ShiftLevel TextLineJustify::determineShiftLevel(const Cluster* cluster, const Cluster* prevCluster,
176 HighLevelInfo& highLevelInfo, MiddleLevelInfo& middleLevelInfo, SkScalar& ideographicMaxLen)
177 {
178 if (cluster == nullptr || prevCluster == nullptr) {
179 return ShiftLevel::Undefined;
180 }
181 ShiftLevel shiftLevel = ShiftLevel::Undefined;
182 if (cluster->isIdeographic()) {
183 ideographicMaxLen = std::max(ideographicMaxLen, cluster->width());
184 shiftLevel = determineShiftLevelForIdeographic(prevCluster, middleLevelInfo);
185 } else if (cluster->isPunctuation()) {
186 shiftLevel = determineShiftLevelForPunctuation(cluster, prevCluster, highLevelInfo);
187 } else if (cluster->isWhitespaceBreak()) {
188 shiftLevel = determineShiftLevelForWhitespaceBreak(prevCluster);
189 } else {
190 shiftLevel = determineShiftLevelForOtherCases(prevCluster, middleLevelInfo);
191 }
192 return shiftLevel;
193 }
194
calculateClusterShift(const Cluster * cluster,ClusterIndex index,const ClusterLevelsIndices & clusterLevels)195 SkScalar TextLineJustify::calculateClusterShift(
196 const Cluster* cluster, ClusterIndex index, const ClusterLevelsIndices& clusterLevels)
197 {
198 if (cluster == nullptr) {
199 return 0.0f;
200 }
201 SkScalar step = 0.0f;
202 auto highLevelIterator = clusterLevels.highLevelIndices.find(index);
203 auto lowLevelIterator = clusterLevels.middleLevelIndices.find(index);
204 if (highLevelIterator != clusterLevels.highLevelIndices.end()) {
205 step = highLevelIterator->second.highLevelOffset;
206 } else if (lowLevelIterator != clusterLevels.middleLevelIndices.end()) {
207 // Because both sides of the WhitespaceBreak are equally widened, the
208 // ideographic and non-ideographic characters are only widened once.
209 // So the front is not WhitespaceBreak, and the count increases by 1.
210 step = lowLevelIterator->second.isPrevClusterSpace ? clusterLevels.middleLevelOffset
211 : clusterLevels.middleLevelOffset * (1 + 1);
212 } else if (clusterLevels.LowLevelIndices.count(index)) {
213 step = clusterLevels.lowLevelOffset;
214 }
215 return step;
216 }
217
justifyShiftCluster(const SkScalar maxWidth,SkScalar textLen,SkScalar ideographicMaxLen,size_t prevClusterNotSpaceCount,ClusterLevelsIndices & clusterLevels)218 void TextLineJustify::justifyShiftCluster(const SkScalar maxWidth, SkScalar textLen, SkScalar ideographicMaxLen,
219 size_t prevClusterNotSpaceCount, ClusterLevelsIndices& clusterLevels)
220 {
221 SkScalar allocatedWidth = maxWidth - textLen - (textLineRef.ellipsis() ? textLineRef.ellipsis()->fAdvanceX() : 0);
222 if (textLineRef.getBreakWithHyphen()) {
223 allocatedWidth -= textLineRef.fHyphenRun->fAdvanceX();
224 }
225 const SkScalar verifyShift = allocatedWidth;
226 // Allocate offsets for each level
227 allocateHighLevelOffsets(ideographicMaxLen, clusterLevels, allocatedWidth);
228 allocateMiddleLevelOffsets(ideographicMaxLen, prevClusterNotSpaceCount, clusterLevels, allocatedWidth);
229 allocateLowLevelOffsets(ideographicMaxLen, clusterLevels, allocatedWidth);
230 allocateRemainingWidth(allocatedWidth, prevClusterNotSpaceCount, clusterLevels);
231 // Deal with the ghost spaces
232 auto ghostShift = maxWidth - textLineRef.widthWithoutEllipsis();
233 // Reallocate the width of each cluster: Clusters of different levels use different offsets.
234 SkScalar shift = 0.0f;
235 SkScalar prevShift = 0.0f;
236 textLineRef.iterateThroughClustersInGlyphsOrder(false, true,
237 [this, &shift, &prevShift, clusterLevels, ghostShift](
238 const Cluster* cluster, ClusterIndex index, bool ghost) {
239 if (cluster == nullptr) {
240 return true;
241 }
242 if (ghost) {
243 if (cluster->run().leftToRight()) {
244 textLineRef.updateClusterOffsets(cluster, ghostShift, ghostShift);
245 }
246 return true;
247 }
248 SkScalar step = calculateClusterShift(cluster, index, clusterLevels);
249 shift += step;
250 textLineRef.updateClusterOffsets(cluster, shift, prevShift);
251 prevShift = shift;
252 return true;
253 });
254 SkAssertResult(nearlyEqual(shift, verifyShift));
255 }
256
justify(SkScalar maxWidth)257 bool TextLineJustify::justify(SkScalar maxWidth)
258 {
259 SkScalar textLen = 0.0f;
260 SkScalar ideographicMaxLen = 0.0f;
261 ClusterLevelsIndices clusterLevels;
262 size_t prevClusterNotSpaceCount = 0;
263 const Cluster* prevCluster = nullptr;
264 bool isFirstCluster = true;
265 // Calculate text length and define three types of labels to trace cluster stretch level.
266 textLineRef.iterateThroughClustersInGlyphsOrder(
267 false, false, [&](const Cluster* cluster, ClusterIndex index, bool ghost) {
268 if (cluster != nullptr && isFirstCluster) {
269 isFirstCluster = false;
270 prevCluster = cluster;
271 textLen += textLineRef.usingAutoSpaceWidth(cluster);
272 ideographicMaxLen =
273 (cluster->isIdeographic()) ? std::max(ideographicMaxLen, cluster->width()) : ideographicMaxLen;
274 return true;
275 }
276 HighLevelInfo highLevelInfo;
277 MiddleLevelInfo middleLevelInfo;
278 ShiftLevel shiftLevel =
279 determineShiftLevel(cluster, prevCluster, highLevelInfo, middleLevelInfo, ideographicMaxLen);
280 switch (shiftLevel) {
281 case ShiftLevel::HighLevel:
282 highLevelInfo.clusterIndex = index;
283 clusterLevels.highLevelIndices.emplace(index, highLevelInfo);
284 break;
285 case ShiftLevel::MiddleLevel:
286 // Because both sides of the WhitespaceBreak are equally widened, the
287 // ideographic and non-ideographic characters are only widened once.
288 // So the front is not WhitespaceBreak, and the count increases by 1.
289 prevClusterNotSpaceCount += middleLevelInfo.isPrevClusterSpace ? 0 : 1;
290 middleLevelInfo.clusterIndex = index;
291 clusterLevels.middleLevelIndices.emplace(index, middleLevelInfo);
292 break;
293 case ShiftLevel::LowLevel:
294 clusterLevels.LowLevelIndices.emplace(index);
295 break;
296 default:
297 break;
298 }
299 textLen += textLineRef.usingAutoSpaceWidth(cluster);
300 prevCluster = cluster;
301 return true;
302 });
303 if (clusterLevels.empty()) {
304 textLineRef.justifyUpdateRtlWidth(maxWidth, textLen);
305 return false;
306 }
307 justifyShiftCluster(maxWidth, textLen, ideographicMaxLen, prevClusterNotSpaceCount, clusterLevels);
308 return true;
309 }
310
311 } // namespace textlayout
312 } // namespace skia
313