servo: Merge #14989 - Fix the incrmental reflow behavior of text-overflow (from notriddle:ellipsis_reflow); r=emilio
authorMichael Howell <michael@notriddle.com>
Thu, 12 Jan 2017 17:31:19 -0800
changeset 340555 b0927824bf4f44e9ea3db169d73c3edcfb0464e0
parent 340554 f68b57d62d3ac7e3adc39325361a03f52b6a0baf
child 340556 638716c8f497802f52e8ab536c348d39726cd197
push id31307
push usergszorc@mozilla.com
push dateSat, 04 Feb 2017 00:59:06 +0000
treeherdermozilla-central@94079d43835f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersemilio
servo: Merge #14989 - Fix the incrmental reflow behavior of text-overflow (from notriddle:ellipsis_reflow); r=emilio This patch allows Servo to incrementally reflow truncated fragments correctly. * The untruncated version of a fragment is preserved, and when incrementally reflowing, the untruncated version is what gets reflowed. If it needs truncated, it will get truncated again. * The ellipsis fragments are skipped when incrementally reflowing a line. If it is still needed, it will be recreated. --- - [X] `./mach build -d` does not report any errors - [X] `./mach test-tidy` does not report any errors - [X] These changes fix #14952 - [X] There are tests for these changes OR Source-Repo: https://github.com/servo/servo Source-Revision: 6a04aea4a5a0da583e8cc7fc0f76c9bfea857538
servo/components/layout/display_list_builder.rs
servo/components/layout/fragment.rs
servo/components/layout/inline.rs
--- a/servo/components/layout/display_list_builder.rs
+++ b/servo/components/layout/display_list_builder.rs
@@ -14,17 +14,17 @@ use app_units::{AU_PER_PX, Au};
 use block::{BlockFlow, BlockStackingContextType};
 use canvas_traits::{CanvasData, CanvasMsg, FromLayoutMsg};
 use context::SharedLayoutContext;
 use euclid::{Point2D, Rect, SideOffsets2D, Size2D};
 use flex::FlexFlow;
 use flow::{BaseFlow, Flow, IS_ABSOLUTELY_POSITIONED};
 use flow_ref::FlowRef;
 use fragment::{CoordinateSystem, Fragment, ImageFragmentInfo, ScannedTextFragmentInfo};
-use fragment::SpecificFragmentInfo;
+use fragment::{SpecificFragmentInfo, TruncatedFragmentInfo};
 use gfx::display_list::{BLUR_INFLATION_FACTOR, BaseDisplayItem, BorderDisplayItem};
 use gfx::display_list::{BorderRadii, BoxShadowClipMode, BoxShadowDisplayItem, ClippingRegion};
 use gfx::display_list::{DisplayItem, DisplayItemMetadata, DisplayList, DisplayListSection};
 use gfx::display_list::{GradientDisplayItem, IframeDisplayItem, ImageDisplayItem};
 use gfx::display_list::{LineDisplayItem, OpaqueNode};
 use gfx::display_list::{SolidColorDisplayItem, ScrollRoot, StackingContext, StackingContextType};
 use gfx::display_list::{TextDisplayItem, TextOrientation, WebGLDisplayItem, WebRenderImageInfo};
 use gfx_traits::{ScrollRootId, StackingContextId};
@@ -1397,57 +1397,62 @@ impl FragmentDisplayListBuilding for Fra
                                                   state: &mut DisplayListBuildState,
                                                   stacking_relative_border_box: &Rect<Au>,
                                                   clip: &ClippingRegion) {
         // Compute the context box position relative to the parent stacking context.
         let stacking_relative_content_box =
             self.stacking_relative_content_box(stacking_relative_border_box);
 
         match self.specific {
-            SpecificFragmentInfo::ScannedText(ref text_fragment) => {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(ref text_fragment),
+                ..
+            }) |
+            SpecificFragmentInfo::ScannedText(box ref text_fragment) => {
                 // Create items for shadows.
                 //
                 // NB: According to CSS-BACKGROUNDS, text shadows render in *reverse* order (front
                 // to back).
 
                 for text_shadow in self.style.get_inheritedtext().text_shadow.0.iter().rev() {
                     self.build_display_list_for_text_fragment(state,
-                                                              &**text_fragment,
+                                                              &*text_fragment,
                                                               &stacking_relative_content_box,
                                                               Some(text_shadow),
                                                               clip);
                 }
 
                 // Create the main text display item.
                 self.build_display_list_for_text_fragment(state,
-                                                          &**text_fragment,
+                                                          &*text_fragment,
                                                           &stacking_relative_content_box,
                                                           None,
                                                           clip);
 
                 if opts::get().show_debug_fragment_borders {
                     self.build_debug_borders_around_text_fragments(state,
                                                                    self.style(),
                                                                    stacking_relative_border_box,
                                                                    &stacking_relative_content_box,
-                                                                   &**text_fragment,
+                                                                   &*text_fragment,
                                                                    clip);
                 }
             }
             SpecificFragmentInfo::Generic |
             SpecificFragmentInfo::GeneratedContent(..) |
             SpecificFragmentInfo::Table |
             SpecificFragmentInfo::TableCell |
             SpecificFragmentInfo::TableRow |
             SpecificFragmentInfo::TableWrapper |
             SpecificFragmentInfo::Multicol |
             SpecificFragmentInfo::MulticolColumn |
             SpecificFragmentInfo::InlineBlock(_) |
             SpecificFragmentInfo::InlineAbsoluteHypothetical(_) |
             SpecificFragmentInfo::InlineAbsolute(_) |
+            SpecificFragmentInfo::TruncatedFragment(_) |
             SpecificFragmentInfo::Svg(_) => {
                 if opts::get().show_debug_fragment_borders {
                     self.build_debug_borders_around_fragment(state,
                                                              stacking_relative_border_box,
                                                              clip);
                 }
             }
             SpecificFragmentInfo::Iframe(ref fragment_info) => {
--- a/servo/components/layout/fragment.rs
+++ b/servo/components/layout/fragment.rs
@@ -181,16 +181,22 @@ pub enum SpecificFragmentInfo {
     Table,
     TableCell,
     TableColumn(TableColumnFragmentInfo),
     TableRow,
     TableWrapper,
     Multicol,
     MulticolColumn,
     UnscannedText(Box<UnscannedTextFragmentInfo>),
+
+    /// A container for a fragment that got truncated by text-overflow.
+    /// "Totally truncated fragments" are not rendered at all.
+    /// Text fragments may be partially truncated (in which case this renders like a text fragment).
+    /// Other fragments can only be totally truncated or not truncated at all.
+    TruncatedFragment(Box<TruncatedFragmentInfo>),
 }
 
 impl SpecificFragmentInfo {
     fn restyle_damage(&self) -> RestyleDamage {
         let flow =
             match *self {
                 SpecificFragmentInfo::Canvas(_) |
                 SpecificFragmentInfo::GeneratedContent(_) |
@@ -201,16 +207,17 @@ impl SpecificFragmentInfo {
                 SpecificFragmentInfo::Table |
                 SpecificFragmentInfo::TableCell |
                 SpecificFragmentInfo::TableColumn(_) |
                 SpecificFragmentInfo::TableRow |
                 SpecificFragmentInfo::TableWrapper |
                 SpecificFragmentInfo::Multicol |
                 SpecificFragmentInfo::MulticolColumn |
                 SpecificFragmentInfo::UnscannedText(_) |
+                SpecificFragmentInfo::TruncatedFragment(_) |
                 SpecificFragmentInfo::Generic => return RestyleDamage::empty(),
                 SpecificFragmentInfo::InlineAbsoluteHypothetical(ref info) => &info.flow_ref,
                 SpecificFragmentInfo::InlineAbsolute(ref info) => &info.flow_ref,
                 SpecificFragmentInfo::InlineBlock(ref info) => &info.flow_ref,
             };
 
         flow::base(&**flow).restyle_damage
     }
@@ -232,16 +239,17 @@ impl SpecificFragmentInfo {
             SpecificFragmentInfo::Table => "SpecificFragmentInfo::Table",
             SpecificFragmentInfo::TableCell => "SpecificFragmentInfo::TableCell",
             SpecificFragmentInfo::TableColumn(_) => "SpecificFragmentInfo::TableColumn",
             SpecificFragmentInfo::TableRow => "SpecificFragmentInfo::TableRow",
             SpecificFragmentInfo::TableWrapper => "SpecificFragmentInfo::TableWrapper",
             SpecificFragmentInfo::Multicol => "SpecificFragmentInfo::Multicol",
             SpecificFragmentInfo::MulticolColumn => "SpecificFragmentInfo::MulticolColumn",
             SpecificFragmentInfo::UnscannedText(_) => "SpecificFragmentInfo::UnscannedText",
+            SpecificFragmentInfo::TruncatedFragment(_) => "SpecificFragmentInfo::TruncatedFragment"
         }
     }
 }
 
 impl fmt::Debug for SpecificFragmentInfo {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match *self {
             SpecificFragmentInfo::ScannedText(ref info) => write!(f, "{:?}", info.text()),
@@ -568,21 +576,21 @@ pub struct SplitResult {
     pub inline_start: Option<SplitInfo>,
     /// The part of the fragment that goes on the second line.
     pub inline_end: Option<SplitInfo>,
     /// The text run which is being split.
     pub text_run: Arc<TextRun>,
 }
 
 /// Describes how a fragment should be truncated.
-pub struct TruncationResult {
+struct TruncationResult {
     /// The part of the fragment remaining after truncation.
-    pub split: SplitInfo,
+    split: SplitInfo,
     /// The text run which is being truncated.
-    pub text_run: Arc<TextRun>,
+    text_run: Arc<TextRun>,
 }
 
 /// Data for an unscanned text fragment. Unscanned text fragments are the results of flow
 /// construction that have not yet had their inline-size determined.
 #[derive(Clone)]
 pub struct UnscannedTextFragmentInfo {
     /// The text inside the fragment.
     pub text: Box<str>,
@@ -617,16 +625,25 @@ impl TableColumnFragmentInfo {
                           .and_then(|string| string.parse().ok())
                           .unwrap_or(0);
         TableColumnFragmentInfo {
             span: span,
         }
     }
 }
 
+/// A wrapper for fragments that have been truncated by the `text-overflow` property.
+/// This may have an associated text node, or, if the fragment was completely truncated,
+/// it may act as an invisible marker for incremental reflow.
+#[derive(Clone)]
+pub struct TruncatedFragmentInfo {
+    pub text_info: Option<ScannedTextFragmentInfo>,
+    pub full: Fragment,
+}
+
 impl Fragment {
     /// Constructs a new `Fragment` instance.
     pub fn new<N: ThreadSafeLayoutNode>(node: &N, specific: SpecificFragmentInfo, ctx: &LayoutContext) -> Fragment {
         let style_context = ctx.style_context();
         let style = node.style(style_context);
         let writing_mode = style.writing_mode;
 
         let mut restyle_damage = node.restyle_damage();
@@ -759,24 +776,27 @@ impl Fragment {
     }
 
     /// Transforms this fragment into an ellipsis fragment, preserving all the other data.
     pub fn transform_into_ellipsis(&self,
                                    layout_context: &LayoutContext,
                                    text_overflow_string: String)
                                    -> Fragment {
         let mut unscanned_ellipsis_fragments = LinkedList::new();
-        unscanned_ellipsis_fragments.push_back(self.transform(
-                self.border_box.size,
-                SpecificFragmentInfo::UnscannedText(
-                    box UnscannedTextFragmentInfo::new(text_overflow_string, None))));
+        let mut ellipsis_fragment = self.transform(
+            self.border_box.size,
+            SpecificFragmentInfo::UnscannedText(
+                box UnscannedTextFragmentInfo::new(text_overflow_string, None)));
+        unscanned_ellipsis_fragments.push_back(ellipsis_fragment);
         let ellipsis_fragments = TextRunScanner::new().scan_for_runs(&mut layout_context.font_context(),
                                                                      unscanned_ellipsis_fragments);
         debug_assert!(ellipsis_fragments.len() == 1);
-        ellipsis_fragments.fragments.into_iter().next().unwrap()
+        ellipsis_fragment = ellipsis_fragments.fragments.into_iter().next().unwrap();
+        ellipsis_fragment.flags |= IS_ELLIPSIS;
+        ellipsis_fragment
     }
 
     pub fn restyle_damage(&self) -> RestyleDamage {
         self.restyle_damage | self.specific.restyle_damage()
     }
 
     pub fn contains_node(&self, node_address: OpaqueNode) -> bool {
         node_address == self.node ||
@@ -838,16 +858,17 @@ impl Fragment {
                 let base_quantities = INTRINSIC_INLINE_SIZE_INCLUDES_SPECIFIED;
                 if self.style.get_inheritedtable().border_collapse ==
                         border_collapse::T::separate {
                     base_quantities | INTRINSIC_INLINE_SIZE_INCLUDES_BORDER
                 } else {
                     base_quantities
                 }
             }
+            SpecificFragmentInfo::TruncatedFragment(_) |
             SpecificFragmentInfo::ScannedText(_) |
             SpecificFragmentInfo::TableColumn(_) |
             SpecificFragmentInfo::UnscannedText(_) |
             SpecificFragmentInfo::InlineAbsoluteHypothetical(_) |
             SpecificFragmentInfo::InlineBlock(_) |
             SpecificFragmentInfo::MulticolColumn => {
                 QuantitiesIncludedInIntrinsicInlineSizes::empty()
             }
@@ -1493,17 +1514,21 @@ impl Fragment {
                 inline_size = size_constraint.clamp(inline_size);
 
                 result.union_block(&IntrinsicISizes {
                     minimum_inline_size: inline_size,
                     preferred_inline_size: inline_size,
                 });
             }
 
-            SpecificFragmentInfo::ScannedText(ref text_fragment_info) => {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(ref text_fragment_info),
+                ..
+            }) |
+            SpecificFragmentInfo::ScannedText(box ref text_fragment_info) => {
                 let range = &text_fragment_info.range;
 
                 // See http://dev.w3.org/csswg/css-sizing/#max-content-inline-size.
                 // TODO: Account for soft wrap opportunities.
                 let max_line_inline_size = text_fragment_info.run
                                                              .metrics_for_range(range)
                                                              .advance_width;
 
@@ -1513,16 +1538,22 @@ impl Fragment {
                     max_line_inline_size
                 };
 
                 result.union_block(&IntrinsicISizes {
                     minimum_inline_size: min_line_inline_size,
                     preferred_inline_size: max_line_inline_size,
                 })
             }
+
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: None,
+                ..
+            }) => return IntrinsicISizesContribution::new(),
+
             SpecificFragmentInfo::UnscannedText(..) => {
                 panic!("Unscanned text fragments should have been scanned by now!")
             }
         };
 
         // Take borders and padding for parent inline fragments into account, if necessary.
         if self.is_primary_fragment() {
             let writing_mode = self.style.writing_mode;
@@ -1554,17 +1585,21 @@ impl Fragment {
         result
     }
 
     /// Returns the narrowest inline-size that the first splittable part of this fragment could
     /// possibly be split to. (In most cases, this returns the inline-size of the first word in
     /// this fragment.)
     pub fn minimum_splittable_inline_size(&self) -> Au {
         match self.specific {
-            SpecificFragmentInfo::ScannedText(ref text) => {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(ref text),
+                ..
+            }) |
+            SpecificFragmentInfo::ScannedText(box ref text) => {
                 text.run.minimum_splittable_inline_size(&text.range)
             }
             _ => Au(0),
         }
     }
 
     /// Returns the dimensions of the content box.
     ///
@@ -1616,18 +1651,64 @@ impl Fragment {
                     character_breaking_strategy,
                     max_inline_size,
                     flags)
             }
         }
     }
 
     /// Truncates this fragment to the given `max_inline_size`, using a character-based breaking
+    /// strategy. The resulting fragment will have `SpecificFragmentInfo::TruncatedFragment`,
+    /// preserving the original fragment for use in incremental reflow.
+    ///
+    /// This function will panic if self is already truncated.
+    pub fn truncate_to_inline_size(self, max_inline_size: Au) -> Fragment {
+        if let SpecificFragmentInfo::TruncatedFragment(_) = self.specific {
+            panic!("Cannot truncate an already truncated fragment");
+        }
+        let info = self.calculate_truncate_to_inline_size(max_inline_size);
+        let (size, text_info) = match info {
+            Some(TruncationResult { split: SplitInfo { inline_size, range }, text_run } ) => {
+                let size = LogicalSize::new(self.style.writing_mode,
+                                            inline_size,
+                                            self.border_box.size.block);
+                // Preserve the insertion point if it is in this fragment's range or it is at line end.
+                let (flags, insertion_point) = match self.specific {
+                    SpecificFragmentInfo::ScannedText(ref info) => {
+                        match info.insertion_point {
+                            Some(index) if range.contains(index) => (info.flags, info.insertion_point),
+                            Some(index) if index == ByteIndex(text_run.text.chars().count() as isize - 1) &&
+                                index == range.end() => (info.flags, info.insertion_point),
+                            _ => (info.flags, None)
+                        }
+                    },
+                    _ => (ScannedTextFlags::empty(), None)
+                };
+                let text_info = ScannedTextFragmentInfo::new(
+                    text_run,
+                    range,
+                    size,
+                    insertion_point,
+                    flags);
+                (size, Some(text_info))
+            }
+            None =>
+                (LogicalSize::zero(self.style.writing_mode), None)
+        };
+        let mut result = self.transform(size, SpecificFragmentInfo::Generic);
+        result.specific = SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+            text_info: text_info,
+            full: self,
+        });
+        result
+    }
+
+    /// Truncates this fragment to the given `max_inline_size`, using a character-based breaking
     /// strategy. If no characters could fit, returns `None`.
-    pub fn truncate_to_inline_size(&self, max_inline_size: Au) -> Option<TruncationResult> {
+    fn calculate_truncate_to_inline_size(&self, max_inline_size: Au) -> Option<TruncationResult> {
         let text_fragment_info =
             if let SpecificFragmentInfo::ScannedText(ref text_fragment_info) = self.specific {
                 text_fragment_info
             } else {
                 return None
             };
 
         let character_breaking_strategy =
@@ -1813,16 +1894,20 @@ impl Fragment {
     }
 
     /// Assigns replaced inline-size, padding, and margins for this fragment only if it is replaced
     /// content per CSS 2.1 § 10.3.2.
     pub fn assign_replaced_inline_size_if_necessary(&mut self,
                                                     container_inline_size: Au,
                                                     container_block_size: Option<Au>) {
         match self.specific {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: None,
+                ..
+            }) |
             SpecificFragmentInfo::Generic |
             SpecificFragmentInfo::GeneratedContent(_) |
             SpecificFragmentInfo::Table |
             SpecificFragmentInfo::TableCell |
             SpecificFragmentInfo::TableRow |
             SpecificFragmentInfo::TableWrapper |
             SpecificFragmentInfo::Multicol |
             SpecificFragmentInfo::MulticolColumn => return,
@@ -1834,16 +1919,17 @@ impl Fragment {
             }
             SpecificFragmentInfo::Canvas(_) |
             SpecificFragmentInfo::Image(_) |
             SpecificFragmentInfo::Iframe(_) |
             SpecificFragmentInfo::InlineBlock(_) |
             SpecificFragmentInfo::InlineAbsoluteHypothetical(_) |
             SpecificFragmentInfo::InlineAbsolute(_) |
             SpecificFragmentInfo::ScannedText(_) |
+            SpecificFragmentInfo::TruncatedFragment(_) |
             SpecificFragmentInfo::Svg(_) => {}
         };
 
         match self.specific {
             // Inline blocks
             SpecificFragmentInfo::InlineAbsoluteHypothetical(ref mut info) => {
                 let block_flow = FlowRef::deref_mut(&mut info.flow_ref).as_mut_block();
                 block_flow.base.position.size.inline =
@@ -1865,17 +1951,21 @@ impl Fragment {
                 self.border_box.size.inline =
                     max(block_flow.base.intrinsic_inline_sizes.minimum_inline_size,
                         block_flow.base.intrinsic_inline_sizes.preferred_inline_size);
                 block_flow.base.block_container_inline_size = self.border_box.size.inline;
                 block_flow.base.block_container_writing_mode = self.style.writing_mode;
             }
 
             // Text
-            SpecificFragmentInfo::ScannedText(ref info) => {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(ref info),
+                ..
+            }) |
+            SpecificFragmentInfo::ScannedText(box ref info) => {
                 // Scanned text fragments will have already had their content inline-sizes assigned
                 // by this point.
                 self.border_box.size.inline = info.content_size.inline +
                     self.border_padding.inline_start_end();
             }
 
             // Replaced elements
             _ if self.is_replaced() => {
@@ -1890,16 +1980,20 @@ impl Fragment {
     }
 
     /// Assign block-size for this fragment if it is replaced content. The inline-size must have
     /// been assigned first.
     ///
     /// Ideally, this should follow CSS 2.1 § 10.6.2.
     pub fn assign_replaced_block_size_if_necessary(&mut self) {
         match self.specific {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: None,
+                ..
+            }) |
             SpecificFragmentInfo::Generic |
             SpecificFragmentInfo::GeneratedContent(_) |
             SpecificFragmentInfo::Table |
             SpecificFragmentInfo::TableCell |
             SpecificFragmentInfo::TableRow |
             SpecificFragmentInfo::TableWrapper |
             SpecificFragmentInfo::Multicol |
             SpecificFragmentInfo::MulticolColumn => return,
@@ -1911,22 +2005,30 @@ impl Fragment {
             }
             SpecificFragmentInfo::Canvas(_) |
             SpecificFragmentInfo::Iframe(_) |
             SpecificFragmentInfo::Image(_) |
             SpecificFragmentInfo::InlineBlock(_) |
             SpecificFragmentInfo::InlineAbsoluteHypothetical(_) |
             SpecificFragmentInfo::InlineAbsolute(_) |
             SpecificFragmentInfo::ScannedText(_) |
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(_),
+                ..
+            }) |
             SpecificFragmentInfo::Svg(_) => {}
         }
 
         match self.specific {
             // Text
-            SpecificFragmentInfo::ScannedText(ref info) => {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(ref info),
+                ..
+            }) |
+            SpecificFragmentInfo::ScannedText(box ref info) => {
                 // Scanned text fragments' content block-sizes are calculated by the text run
                 // scanner during flow construction.
                 self.border_box.size.block = info.content_size.block +
                     self.border_padding.block_start_end();
             }
 
             // Inline blocks
             SpecificFragmentInfo::InlineBlock(ref mut info) => {
@@ -1993,17 +2095,21 @@ impl Fragment {
             SpecificFragmentInfo::Generic | SpecificFragmentInfo::GeneratedContent(_) => {
                 let ascent = self.border_box.size.block + self.margin.block_end;
                 InlineMetrics {
                     space_above_baseline: ascent + self.margin.block_start,
                     space_below_baseline: Au(0),
                     ascent: ascent,
                 }
             }
-            SpecificFragmentInfo::ScannedText(ref info) => {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(ref info),
+                ..
+            }) |
+            SpecificFragmentInfo::ScannedText(box ref info) => {
                 // Fragments with no glyphs don't contribute any inline metrics.
                 // TODO: Filter out these fragments during flow construction?
                 if info.insertion_point.is_none() && info.content_size.inline == Au(0) {
                     return InlineMetrics::new(Au(0), Au(0), Au(0));
                 }
                 // See CSS 2.1 § 10.8.1.
                 let font_metrics = text::font_metrics_for_style(&mut layout_context.font_context(),
                                                                 self.style.clone_font());
@@ -2011,16 +2117,20 @@ impl Fragment {
                 InlineMetrics::from_font_metrics(&info.run.font_metrics, line_height)
             }
             SpecificFragmentInfo::InlineBlock(ref info) => {
                 inline_metrics_of_block(&info.flow_ref, &*self.style)
             }
             SpecificFragmentInfo::InlineAbsoluteHypothetical(ref info) => {
                 inline_metrics_of_block(&info.flow_ref, &*self.style)
             }
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: None,
+                ..
+            }) |
             SpecificFragmentInfo::InlineAbsolute(_) => {
                 InlineMetrics::new(Au(0), Au(0), Au(0))
             }
             SpecificFragmentInfo::Table |
             SpecificFragmentInfo::TableCell |
             SpecificFragmentInfo::TableColumn(_) |
             SpecificFragmentInfo::TableRow |
             SpecificFragmentInfo::TableWrapper |
@@ -2259,16 +2369,17 @@ impl Fragment {
             SpecificFragmentInfo::Iframe(_) |
             SpecificFragmentInfo::Image(_) |
             SpecificFragmentInfo::ScannedText(_) |
             SpecificFragmentInfo::Svg(_) |
             SpecificFragmentInfo::Table |
             SpecificFragmentInfo::TableCell |
             SpecificFragmentInfo::TableColumn(_) |
             SpecificFragmentInfo::TableRow |
+            SpecificFragmentInfo::TruncatedFragment(_) |
             SpecificFragmentInfo::Multicol |
             SpecificFragmentInfo::UnscannedText(_) => true,
         }
     }
 
     /// Determines the inline sizes of inline-block fragments. These cannot be fully computed until
     /// inline size assignment has run for the child flow: thus it is computed "late", during
     /// block size assignment.
@@ -2346,16 +2457,17 @@ impl Fragment {
                   Size2D::new(stacking_relative_border_box.size.width - border_padding.horizontal(),
                               stacking_relative_border_box.size.height - border_padding.vertical()))
     }
 
     /// Returns true if this fragment establishes a new stacking context and false otherwise.
     pub fn establishes_stacking_context(&self) -> bool {
         // Text fragments shouldn't create stacking contexts.
         match self.specific {
+            SpecificFragmentInfo::TruncatedFragment(_) |
             SpecificFragmentInfo::ScannedText(_) |
             SpecificFragmentInfo::UnscannedText(_) => return false,
             _ => {}
         }
 
         if self.style().get_effects().opacity != 1.0 {
             return true
         }
@@ -2476,30 +2588,38 @@ impl Fragment {
 
         // FIXME(pcwalton): Sometimes excessively fancy glyphs can make us draw outside our border
         // box too.
         overflow
     }
 
     pub fn requires_line_break_afterward_if_wrapping_on_newlines(&self) -> bool {
         match self.specific {
-            SpecificFragmentInfo::ScannedText(ref scanned_text) => {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(ref scanned_text),
+                ..
+            }) |
+            SpecificFragmentInfo::ScannedText(box ref scanned_text) => {
                 scanned_text.requires_line_break_afterward_if_wrapping_on_newlines()
             }
             _ => false,
         }
     }
 
     pub fn strip_leading_whitespace_if_necessary(&mut self) -> WhitespaceStrippingResult {
         if self.white_space().preserve_spaces() {
             return WhitespaceStrippingResult::RetainFragment
         }
 
         match self.specific {
-            SpecificFragmentInfo::ScannedText(ref mut scanned_text_fragment_info) => {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(ref mut scanned_text_fragment_info),
+                ..
+            }) |
+            SpecificFragmentInfo::ScannedText(box ref mut scanned_text_fragment_info) => {
                 let leading_whitespace_byte_count = scanned_text_fragment_info.text()
                     .find(|c| !char_is_whitespace(c))
                     .unwrap_or(scanned_text_fragment_info.text().len());
 
                 let whitespace_len = ByteIndex(leading_whitespace_byte_count as isize);
                 let whitespace_range = Range::new(scanned_text_fragment_info.range.begin(),
                                                   whitespace_len);
                 let text_bounds =
@@ -2543,17 +2663,21 @@ impl Fragment {
 
     /// Returns true if the entire fragment was stripped.
     pub fn strip_trailing_whitespace_if_necessary(&mut self) -> WhitespaceStrippingResult {
         if self.white_space().preserve_spaces() {
             return WhitespaceStrippingResult::RetainFragment
         }
 
         match self.specific {
-            SpecificFragmentInfo::ScannedText(ref mut scanned_text_fragment_info) => {
+            SpecificFragmentInfo::TruncatedFragment(box TruncatedFragmentInfo {
+                text_info: Some(ref mut scanned_text_fragment_info),
+                ..
+            }) |
+            SpecificFragmentInfo::ScannedText(box ref mut scanned_text_fragment_info) => {
                 let mut trailing_whitespace_start_byte = 0;
                 for (i, c) in scanned_text_fragment_info.text().char_indices().rev() {
                     if !char_is_whitespace(c) {
                         trailing_whitespace_start_byte = i + c.len_utf8();
                         break;
                     }
                 }
                 let whitespace_start = ByteIndex(trailing_whitespace_start_byte as isize);
@@ -2729,16 +2853,17 @@ impl Fragment {
             SpecificFragmentInfo::TableColumn(_) |
             SpecificFragmentInfo::TableRow |
             SpecificFragmentInfo::TableWrapper => false,
             SpecificFragmentInfo::Canvas(_) |
             SpecificFragmentInfo::GeneratedContent(_) |
             SpecificFragmentInfo::Iframe(_) |
             SpecificFragmentInfo::Image(_) |
             SpecificFragmentInfo::ScannedText(_) |
+            SpecificFragmentInfo::TruncatedFragment(_) |
             SpecificFragmentInfo::Svg(_) |
             SpecificFragmentInfo::UnscannedText(_) => true
         }
     }
 
     /// Returns the 4D matrix representing this fragment's transform.
     pub fn transform_matrix(&self, stacking_relative_border_box: &Rect<Au>) -> Matrix4D<f32> {
         let mut transform = Matrix4D::identity();
@@ -2992,16 +3117,18 @@ impl Overflow {
 
 bitflags! {
     pub flags FragmentFlags: u8 {
         // TODO(stshine): find a better name since these flags can also be used for grid item.
         /// Whether this fragment represents a child in a row flex container.
         const IS_INLINE_FLEX_ITEM = 0b0000_0001,
         /// Whether this fragment represents a child in a column flex container.
         const IS_BLOCK_FLEX_ITEM = 0b0000_0010,
+        /// Whether this fragment represents the generated text from a text-overflow clip.
+        const IS_ELLIPSIS = 0b0000_0100,
     }
 }
 
 /// Specified distances from the margin edge of a block to its content in the inline direction.
 /// These are returned by `guess_inline_content_edge_offsets()` and are used in the float placement
 /// speculation logic.
 #[derive(Copy, Clone, Debug)]
 pub struct SpeculatedInlineContentEdgeOffsets {
--- a/servo/components/layout/inline.rs
+++ b/servo/components/layout/inline.rs
@@ -10,16 +10,17 @@ use context::{LayoutContext, SharedLayou
 use display_list_builder::{DisplayListBuildState, InlineFlowDisplayListBuilding};
 use euclid::{Point2D, Size2D};
 use floats::{FloatKind, Floats, PlacementInfo};
 use flow::{self, BaseFlow, Flow, FlowClass, ForceNonfloatedFlag, IS_ABSOLUTELY_POSITIONED};
 use flow::{CONTAINS_TEXT_OR_REPLACED_FRAGMENTS, EarlyAbsolutePositionInfo, MutableFlowUtils};
 use flow::OpaqueFlow;
 use flow_ref::FlowRef;
 use fragment::{CoordinateSystem, Fragment, FragmentBorderBoxIterator, Overflow};
+use fragment::IS_ELLIPSIS;
 use fragment::SpecificFragmentInfo;
 use gfx::display_list::OpaqueNode;
 use gfx::font::FontMetrics;
 use gfx::font_context::FontContext;
 use gfx_traits::print_tree::PrintTree;
 use layout_debug;
 use model::IntrinsicISizesContribution;
 use range::{Range, RangeIndex};
@@ -327,16 +328,25 @@ impl LineBreaker {
         loop {
             // Acquire the next fragment to lay out from the work list or fragment list, as
             // appropriate.
             let fragment = match self.next_unbroken_fragment(&mut old_fragment_iter) {
                 None => break,
                 Some(fragment) => fragment,
             };
 
+            // Do not reflow truncated fragments. Reflow the original fragment only.
+            let fragment = if fragment.flags.contains(IS_ELLIPSIS) {
+                continue
+            } else if let SpecificFragmentInfo::TruncatedFragment(info) = fragment.specific {
+                info.full
+            } else {
+                fragment
+            };
+
             // Try to append the fragment.
             self.reflow_fragment(fragment, flow, layout_context);
         }
 
         if !self.pending_line_is_empty() {
             debug!("LineBreaker: partially full line {} at end of scanning; committing it",
                     self.lines.len());
             self.flush_current_line()
@@ -702,23 +712,19 @@ impl LineBreaker {
                 } else {
                     None
                 }
             }
         };
 
         if let Some(string) = ellipsis {
             let ellipsis = fragment.transform_into_ellipsis(layout_context, string);
-            if let Some(truncation_info) =
-                    fragment.truncate_to_inline_size(available_inline_size -
-                                                     ellipsis.margin_box_inline_size()) {
-                let fragment = fragment.transform_with_split_info(&truncation_info.split,
-                                                                  truncation_info.text_run);
-                self.push_fragment_to_line_ignoring_text_overflow(fragment, layout_context);
-            }
+            let truncated = fragment.truncate_to_inline_size(available_inline_size -
+                                                             ellipsis.margin_box_inline_size());
+            self.push_fragment_to_line_ignoring_text_overflow(truncated, layout_context);
             self.push_fragment_to_line_ignoring_text_overflow(ellipsis, layout_context);
         } else {
             self.push_fragment_to_line_ignoring_text_overflow(fragment, layout_context);
         }
 
         if line_flush_mode == LineFlushMode::Flush {
             self.flush_current_line()
         }