1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.h"
6
7 #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.h"
8 #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.h"
9 #include "third_party/blink/renderer/core/layout/ng/inline/ng_logical_line_item.h"
10 #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h"
11 #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h"
12 #include "third_party/blink/renderer/platform/fonts/font_baseline.h"
13 #include "third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper.h"
14 #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h"
15
16 namespace blink {
17
18 namespace {
19
IsLeftMostOffset(const ShapeResult & shape_result,unsigned offset)20 bool IsLeftMostOffset(const ShapeResult& shape_result, unsigned offset) {
21 if (shape_result.Rtl())
22 return offset == shape_result.NumCharacters();
23 return offset == 0;
24 }
25
IsRightMostOffset(const ShapeResult & shape_result,unsigned offset)26 bool IsRightMostOffset(const ShapeResult& shape_result, unsigned offset) {
27 if (shape_result.Rtl())
28 return offset == 0;
29 return offset == shape_result.NumCharacters();
30 }
31
32 } // namespace
33
NGLineTruncator(const NGLineInfo & line_info)34 NGLineTruncator::NGLineTruncator(const NGLineInfo& line_info)
35 : line_style_(&line_info.LineStyle()),
36 available_width_(line_info.AvailableWidth() - line_info.TextIndent()),
37 line_direction_(line_info.BaseDirection()) {}
38
EllipsisStyle() const39 const ComputedStyle& NGLineTruncator::EllipsisStyle() const {
40 // The ellipsis is styled according to the line style.
41 // https://drafts.csswg.org/css-ui/#ellipsing-details
42 DCHECK(line_style_);
43 return *line_style_;
44 }
45
SetupEllipsis()46 void NGLineTruncator::SetupEllipsis() {
47 const Font& font = EllipsisStyle().GetFont();
48 ellipsis_font_data_ = font.PrimaryFont();
49 DCHECK(ellipsis_font_data_);
50 ellipsis_text_ =
51 ellipsis_font_data_ && ellipsis_font_data_->GlyphForCharacter(
52 kHorizontalEllipsisCharacter)
53 ? String(&kHorizontalEllipsisCharacter, 1)
54 : String(u"...");
55 HarfBuzzShaper shaper(ellipsis_text_);
56 ellipsis_shape_result_ =
57 ShapeResultView::Create(shaper.Shape(&font, line_direction_).get());
58 ellipsis_width_ = ellipsis_shape_result_->SnappedWidth();
59 }
60
PlaceEllipsisNextTo(NGLogicalLineItems * line_box,NGLogicalLineItem * ellipsized_child)61 LayoutUnit NGLineTruncator::PlaceEllipsisNextTo(
62 NGLogicalLineItems* line_box,
63 NGLogicalLineItem* ellipsized_child) {
64 // Create the ellipsis, associating it with the ellipsized child.
65 DCHECK(ellipsized_child->HasInFlowFragment());
66 LayoutObject* ellipsized_layout_object =
67 ellipsized_child->GetMutableLayoutObject();
68 DCHECK(ellipsized_layout_object);
69 DCHECK(ellipsized_layout_object->IsInline());
70 DCHECK(ellipsized_layout_object->IsText() ||
71 ellipsized_layout_object->IsAtomicInlineLevel());
72
73 // Now the offset of the ellpisis is determined. Place the ellpisis into the
74 // line box.
75 LayoutUnit ellipsis_inline_offset =
76 IsLtr(line_direction_)
77 ? ellipsized_child->InlineOffset() + ellipsized_child->inline_size
78 : ellipsized_child->InlineOffset() - ellipsis_width_;
79 FontHeight ellipsis_metrics;
80 DCHECK(ellipsis_font_data_);
81 if (ellipsis_font_data_) {
82 ellipsis_metrics = ellipsis_font_data_->GetFontMetrics().GetFontHeight(
83 line_style_->GetFontBaseline());
84 }
85
86 DCHECK(ellipsis_text_);
87 DCHECK(ellipsis_shape_result_.get());
88 NGTextFragmentBuilder builder(line_style_->GetWritingMode());
89 builder.SetText(ellipsized_layout_object, ellipsis_text_, &EllipsisStyle(),
90 NGStyleVariant::kEllipsis, std::move(ellipsis_shape_result_),
91 {ellipsis_width_, ellipsis_metrics.LineHeight()});
92 line_box->AddChild(
93 builder.ToTextFragment(),
94 LogicalOffset{ellipsis_inline_offset, -ellipsis_metrics.ascent},
95 ellipsis_width_, 0);
96 return ellipsis_inline_offset;
97 }
98
AddTruncatedChild(wtf_size_t source_index,bool leave_one_character,LayoutUnit position,TextDirection edge,NGLogicalLineItems * line_box,NGInlineLayoutStateStack * box_states)99 wtf_size_t NGLineTruncator::AddTruncatedChild(
100 wtf_size_t source_index,
101 bool leave_one_character,
102 LayoutUnit position,
103 TextDirection edge,
104 NGLogicalLineItems* line_box,
105 NGInlineLayoutStateStack* box_states) {
106 NGLogicalLineItems& line = *line_box;
107 const NGLogicalLineItem& source_item = line[source_index];
108 DCHECK(source_item.shape_result);
109 scoped_refptr<ShapeResult> shape_result =
110 source_item.shape_result->CreateShapeResult();
111 unsigned text_offset = shape_result->OffsetToFit(position, edge);
112 if (IsLtr(edge) ? IsLeftMostOffset(*shape_result, text_offset)
113 : IsRightMostOffset(*shape_result, text_offset)) {
114 if (!leave_one_character)
115 return kDidNotAddChild;
116 text_offset =
117 shape_result->OffsetToFit(shape_result->PositionForOffset(
118 IsRtl(edge) == shape_result->Rtl()
119 ? 1
120 : shape_result->NumCharacters() - 1),
121 edge);
122 }
123
124 const wtf_size_t new_index = line.size();
125 line.AddChild(TruncateText(source_item, *shape_result, text_offset, edge));
126 box_states->ChildInserted(new_index);
127 return new_index;
128 }
129
TruncateLine(LayoutUnit line_width,NGLogicalLineItems * line_box,NGInlineLayoutStateStack * box_states)130 LayoutUnit NGLineTruncator::TruncateLine(LayoutUnit line_width,
131 NGLogicalLineItems* line_box,
132 NGInlineLayoutStateStack* box_states) {
133 DCHECK(std::all_of(line_box->begin(), line_box->end(),
134 [](const auto& item) { return !item.text_fragment; }));
135
136 // Shape the ellipsis and compute its inline size.
137 SetupEllipsis();
138
139 // Loop children from the logical last to the logical first to determine where
140 // to place the ellipsis. Children maybe truncated or moved as part of the
141 // process.
142 NGLogicalLineItem* ellipsized_child = nullptr;
143 base::Optional<NGLogicalLineItem> truncated_child;
144 if (IsLtr(line_direction_)) {
145 NGLogicalLineItem* first_child = line_box->FirstInFlowChild();
146 for (auto it = line_box->rbegin(); it != line_box->rend(); it++) {
147 auto& child = *it;
148 if (EllipsizeChild(line_width, ellipsis_width_, &child == first_child,
149 &child, &truncated_child)) {
150 ellipsized_child = &child;
151 break;
152 }
153 }
154 } else {
155 NGLogicalLineItem* first_child = line_box->LastInFlowChild();
156 for (auto& child : *line_box) {
157 if (EllipsizeChild(line_width, ellipsis_width_, &child == first_child,
158 &child, &truncated_child)) {
159 ellipsized_child = &child;
160 break;
161 }
162 }
163 }
164
165 // Abort if ellipsis could not be placed.
166 if (!ellipsized_child)
167 return line_width;
168
169 // Truncate the text fragment if needed.
170 if (truncated_child) {
171 // In order to preserve layout information before truncated, hide the
172 // original fragment and insert a truncated one.
173 size_t child_index_to_truncate = ellipsized_child - line_box->begin();
174 line_box->InsertChild(child_index_to_truncate + 1,
175 std::move(*truncated_child));
176 box_states->ChildInserted(child_index_to_truncate + 1);
177 NGLogicalLineItem* child_to_truncate =
178 &(*line_box)[child_index_to_truncate];
179 ellipsized_child = std::next(child_to_truncate);
180
181 HideChild(child_to_truncate);
182 DCHECK_LE(ellipsized_child->inline_size, child_to_truncate->inline_size);
183 if (UNLIKELY(IsRtl(line_direction_))) {
184 ellipsized_child->rect.offset.inline_offset +=
185 child_to_truncate->inline_size - ellipsized_child->inline_size;
186 }
187 }
188
189 // Create the ellipsis, associating it with the ellipsized child.
190 LayoutUnit ellipsis_inline_offset =
191 PlaceEllipsisNextTo(line_box, ellipsized_child);
192 return std::max(ellipsis_inline_offset + ellipsis_width_, line_width);
193 }
194
195 // This function was designed to work only with <input type=file>.
196 // We assume the line box contains:
197 // (Optional) children without in-flow fragments
198 // Children with in-flow fragments, and
199 // (Optional) children without in-flow fragments
200 // in this order, and the children with in-flow fragments have no padding,
201 // no border, and no margin.
202 // Children with IsPlaceholder() can appear anywhere.
TruncateLineInTheMiddle(LayoutUnit line_width,NGLogicalLineItems * line_box,NGInlineLayoutStateStack * box_states)203 LayoutUnit NGLineTruncator::TruncateLineInTheMiddle(
204 LayoutUnit line_width,
205 NGLogicalLineItems* line_box,
206 NGInlineLayoutStateStack* box_states) {
207 // Shape the ellipsis and compute its inline size.
208 SetupEllipsis();
209
210 NGLogicalLineItems& line = *line_box;
211 wtf_size_t initial_index_left = kNotFound;
212 wtf_size_t initial_index_right = kNotFound;
213 for (wtf_size_t i = 0; i < line_box->size(); ++i) {
214 auto& child = line[i];
215 if (child.IsPlaceholder())
216 continue;
217 if (!child.shape_result) {
218 if (initial_index_right != kNotFound)
219 break;
220 continue;
221 }
222 // Skip pseudo elements like ::before.
223 if (!child.GetNode())
224 continue;
225
226 if (initial_index_left == kNotFound)
227 initial_index_left = i;
228 initial_index_right = i;
229 }
230 // There are no truncatable children.
231 if (initial_index_left == kNotFound)
232 return line_width;
233 DCHECK_NE(initial_index_right, kNotFound);
234 DCHECK(line[initial_index_left].HasInFlowFragment());
235 DCHECK(line[initial_index_right].HasInFlowFragment());
236
237 // line[]:
238 // s s s p f f p f f s s
239 // ^ ^
240 // initial_index_left |
241 // initial_index_right
242 // s: child without in-flow fragment
243 // p: placeholder child
244 // f: child with in-flow fragment
245
246 const LayoutUnit static_width_left = line[initial_index_left].InlineOffset();
247 LayoutUnit static_width_right = LayoutUnit(0);
248 if (initial_index_right + 1 < line.size()) {
249 const NGLogicalLineItem& item = line[initial_index_right + 1];
250 // |line_width| and/or InlineOffset() might be saturated.
251 if (line_width <= item.InlineOffset())
252 return line_width;
253 static_width_right =
254 line_width - item.InlineOffset() + item.margin_line_left;
255 }
256 const LayoutUnit available_width =
257 available_width_ - static_width_left - static_width_right;
258 if (available_width <= ellipsis_width_)
259 return line_width;
260 LayoutUnit available_width_left = (available_width - ellipsis_width_) / 2;
261 LayoutUnit available_width_right = available_width_left;
262
263 // Children for ellipsis and truncated fragments will have index which
264 // is >= new_child_start.
265 const wtf_size_t new_child_start = line.size();
266
267 wtf_size_t index_left = initial_index_left;
268 wtf_size_t index_right = initial_index_right;
269
270 if (IsLtr(line_direction_)) {
271 // Find truncation point at the left, truncate, and add an ellipsis.
272 while (available_width_left >= line[index_left].inline_size) {
273 available_width_left -= line[index_left++].inline_size;
274 if (index_left >= line.size()) {
275 // We have a logic bug. Do nothing.
276 return line_width;
277 }
278 }
279 DCHECK_LE(index_left, index_right);
280 DCHECK(!line[index_left].IsPlaceholder());
281 wtf_size_t new_index = AddTruncatedChild(
282 index_left, index_left == initial_index_left, available_width_left,
283 TextDirection::kLtr, line_box, box_states);
284 if (new_index == kDidNotAddChild) {
285 DCHECK_GT(index_left, initial_index_left);
286 DCHECK_GT(index_left, 0u);
287 wtf_size_t i = index_left;
288 while (!line[--i].HasInFlowFragment())
289 DCHECK(line[i].IsPlaceholder());
290 PlaceEllipsisNextTo(line_box, &line[i]);
291 available_width_right += available_width_left;
292 } else {
293 PlaceEllipsisNextTo(line_box, &line[new_index]);
294 available_width_right +=
295 available_width_left - line[new_index].inline_size;
296 }
297
298 // Find truncation point at the right.
299 while (available_width_right >= line[index_right].inline_size) {
300 available_width_right -= line[index_right].inline_size;
301 if (index_right == 0) {
302 // We have a logic bug. We proceed anyway because |line| was already
303 // modified.
304 break;
305 }
306 --index_right;
307 }
308 LayoutUnit new_modified_right_offset =
309 line[line.size() - 1].InlineOffset() + ellipsis_width_;
310 DCHECK_LE(index_left, index_right);
311 DCHECK(!line[index_right].IsPlaceholder());
312 if (available_width_right > 0) {
313 new_index = AddTruncatedChild(
314 index_right, false,
315 line[index_right].inline_size - available_width_right,
316 TextDirection::kRtl, line_box, box_states);
317 if (new_index != kDidNotAddChild) {
318 line[new_index].rect.offset.inline_offset = new_modified_right_offset;
319 new_modified_right_offset += line[new_index].inline_size;
320 }
321 }
322 // Shift unchanged children at the right of the truncated child.
323 // It's ok to modify existing children's offsets because they are not
324 // web-exposed.
325 LayoutUnit offset_diff = line[index_right].InlineOffset() +
326 line[index_right].inline_size -
327 new_modified_right_offset;
328 for (wtf_size_t i = index_right + 1; i < new_child_start; ++i)
329 line[i].rect.offset.inline_offset -= offset_diff;
330 line_width -= offset_diff;
331
332 } else {
333 // Find truncation point at the right, truncate, and add an ellipsis.
334 while (available_width_right >= line[index_right].inline_size) {
335 available_width_right -= line[index_right].inline_size;
336 if (index_right == 0) {
337 // We have a logic bug. Do nothing.
338 return line_width;
339 }
340 --index_right;
341 }
342 DCHECK_LE(index_left, index_right);
343 DCHECK(!line[index_right].IsPlaceholder());
344 wtf_size_t new_index =
345 AddTruncatedChild(index_right, index_right == initial_index_right,
346 line[index_right].inline_size - available_width_right,
347 TextDirection::kRtl, line_box, box_states);
348 if (new_index == kDidNotAddChild) {
349 DCHECK_LT(index_right, initial_index_right);
350 wtf_size_t i = index_right;
351 while (!line[++i].HasInFlowFragment())
352 DCHECK(line[i].IsPlaceholder());
353 PlaceEllipsisNextTo(line_box, &line[i]);
354 available_width_left += available_width_right;
355 } else {
356 line[new_index].rect.offset.inline_offset +=
357 line[index_right].inline_size - line[new_index].inline_size;
358 PlaceEllipsisNextTo(line_box, &line[new_index]);
359 available_width_left +=
360 available_width_right - line[new_index].inline_size;
361 }
362 LayoutUnit ellipsis_offset = line[line.size() - 1].InlineOffset();
363
364 // Find truncation point at the left.
365 while (available_width_left >= line[index_left].inline_size) {
366 available_width_left -= line[index_left++].inline_size;
367 if (index_left >= line.size()) {
368 // We have a logic bug. We proceed anyway because |line| was already
369 // modified.
370 break;
371 }
372 }
373 DCHECK_LE(index_left, index_right);
374 DCHECK(!line[index_left].IsPlaceholder());
375 if (available_width_left > 0) {
376 new_index = AddTruncatedChild(index_left, false, available_width_left,
377 TextDirection::kLtr, line_box, box_states);
378 if (new_index != kDidNotAddChild) {
379 line[new_index].rect.offset.inline_offset =
380 ellipsis_offset - line[new_index].inline_size;
381 }
382 }
383
384 // Shift unchanged children at the left of the truncated child.
385 // It's ok to modify existing children's offsets because they are not
386 // web-exposed.
387 LayoutUnit offset_diff =
388 line[line.size() - 1].InlineOffset() - line[index_left].InlineOffset();
389 for (wtf_size_t i = index_left; i > 0; --i)
390 line[i - 1].rect.offset.inline_offset += offset_diff;
391 line_width -= offset_diff;
392 }
393 // Hide left/right truncated children and children between them.
394 for (wtf_size_t i = index_left; i <= index_right; ++i) {
395 if (line[i].HasInFlowFragment())
396 HideChild(&line[i]);
397 }
398
399 return line_width;
400 }
401
402 // Hide this child from being painted. Leaves a hidden fragment so that layout
403 // queries such as |offsetWidth| work as if it is not truncated.
HideChild(NGLogicalLineItem * child)404 void NGLineTruncator::HideChild(NGLogicalLineItem* child) {
405 DCHECK(child->HasInFlowFragment());
406
407 if (const NGPhysicalTextFragment* text = child->text_fragment.get()) {
408 child->text_fragment = text->CloneAsHiddenForPaint();
409 return;
410 }
411
412 if (const NGLayoutResult* layout_result = child->layout_result.get()) {
413 // Need to propagate OOF descendants in this inline-block child.
414 const auto& fragment =
415 To<NGPhysicalBoxFragment>(layout_result->PhysicalFragment());
416 if (fragment.HasOutOfFlowPositionedDescendants())
417 return;
418
419 child->layout_result = fragment.CloneAsHiddenForPaint();
420 return;
421 }
422
423 if (child->inline_item) {
424 child->is_hidden_for_paint = true;
425 return;
426 }
427
428 NOTREACHED();
429 }
430
431 // Return the offset to place the ellipsis.
432 //
433 // This function may truncate or move the child so that the ellipsis can fit.
EllipsizeChild(LayoutUnit line_width,LayoutUnit ellipsis_width,bool is_first_child,NGLogicalLineItem * child,base::Optional<NGLogicalLineItem> * truncated_child)434 bool NGLineTruncator::EllipsizeChild(
435 LayoutUnit line_width,
436 LayoutUnit ellipsis_width,
437 bool is_first_child,
438 NGLogicalLineItem* child,
439 base::Optional<NGLogicalLineItem>* truncated_child) {
440 DCHECK(truncated_child && !*truncated_child);
441
442 // Leave out-of-flow children as is.
443 if (!child->HasInFlowFragment())
444 return false;
445
446 // Inline boxes should not be ellipsized. Usually they will be created in the
447 // later phase, but empty inline box are already created.
448 if (child->IsInlineBox())
449 return false;
450
451 // Can't place ellipsis if this child is completely outside of the box.
452 LayoutUnit child_inline_offset =
453 IsLtr(line_direction_)
454 ? child->InlineOffset()
455 : line_width - (child->InlineOffset() + child->inline_size);
456 LayoutUnit space_for_child = available_width_ - child_inline_offset;
457 if (space_for_child <= 0) {
458 // This child is outside of the content box, but we still need to hide it.
459 // When the box has paddings, this child outside of the content box maybe
460 // still inside of the clipping box.
461 if (!is_first_child)
462 HideChild(child);
463 return false;
464 }
465
466 // At least part of this child is in the box.
467 // If |child| can fit in the space, truncate this line at the end of |child|.
468 space_for_child -= ellipsis_width;
469 if (space_for_child >= child->inline_size)
470 return true;
471
472 // If not all of this child can fit, try to truncate.
473 if (TruncateChild(space_for_child, is_first_child, *child, truncated_child))
474 return true;
475
476 // This child is partially in the box, but it can't be truncated to fit. It
477 // should not be visible because earlier sibling will be truncated.
478 if (!is_first_child)
479 HideChild(child);
480 return false;
481 }
482
483 // Truncate the specified child. Returns true if truncated successfully, false
484 // otherwise.
485 //
486 // Note that this function may return true even if it can't fit the child when
487 // |is_first_child|, because the spec defines that the first character or atomic
488 // inline-level element on a line must be clipped rather than ellipsed.
489 // https://drafts.csswg.org/css-ui/#text-overflow
TruncateChild(LayoutUnit space_for_child,bool is_first_child,const NGLogicalLineItem & child,base::Optional<NGLogicalLineItem> * truncated_child)490 bool NGLineTruncator::TruncateChild(
491 LayoutUnit space_for_child,
492 bool is_first_child,
493 const NGLogicalLineItem& child,
494 base::Optional<NGLogicalLineItem>* truncated_child) {
495 DCHECK(truncated_child && !*truncated_child);
496 DCHECK(!child.text_fragment);
497
498 // If the space is not enough, try the next child.
499 if (space_for_child <= 0 && !is_first_child)
500 return false;
501
502 // Only text fragments can be truncated.
503 if (!child.shape_result)
504 return is_first_child;
505
506 // TODO(layout-dev): Add support for OffsetToFit to ShapeResultView to avoid
507 // this copy.
508 scoped_refptr<ShapeResult> shape_result =
509 child.shape_result->CreateShapeResult();
510 DCHECK(shape_result);
511 const NGTextOffset original_offset = child.text_offset;
512 // Compute the offset to truncate.
513 unsigned offset_to_fit = shape_result->OffsetToFit(
514 IsLtr(line_direction_) ? space_for_child
515 : shape_result->Width() - space_for_child,
516 line_direction_);
517 DCHECK_LE(offset_to_fit, original_offset.Length());
518 if (!offset_to_fit || offset_to_fit == original_offset.Length()) {
519 if (!is_first_child)
520 return false;
521 offset_to_fit = !offset_to_fit ? 1 : offset_to_fit - 1;
522 }
523 *truncated_child =
524 TruncateText(child, *shape_result, offset_to_fit, line_direction_);
525 return true;
526 }
527
TruncateText(const NGLogicalLineItem & item,const ShapeResult & shape_result,unsigned offset_to_fit,TextDirection direction)528 NGLogicalLineItem NGLineTruncator::TruncateText(const NGLogicalLineItem& item,
529 const ShapeResult& shape_result,
530 unsigned offset_to_fit,
531 TextDirection direction) {
532 const NGTextOffset new_text_offset =
533 direction == shape_result.Direction()
534 ? NGTextOffset(item.StartOffset(), item.StartOffset() + offset_to_fit)
535 : NGTextOffset(item.StartOffset() + offset_to_fit, item.EndOffset());
536 scoped_refptr<ShapeResultView> new_shape_result = ShapeResultView::Create(
537 &shape_result, new_text_offset.start, new_text_offset.end);
538 DCHECK(item.inline_item);
539 return NGLogicalLineItem(item, std::move(new_shape_result), new_text_offset);
540 }
541
542 } // namespace blink
543