• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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