• 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 #ifdef ENABLE_TEXT_ENHANCE
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 
314 #endif // ENABLE_TEXT_ENHANCE
315