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