What’s this blog about?
This is an explanation to a PR I submitted on the Servo community to implement basic support for text-overflow: ellipsis property on Servo.
Scope & Limitations
I would like to emphasize that this PR attempts to add basic support for text-overflow property, not a full-fledged feature.
Specifically, some limitations include:
- This PR only adds support for single-valued
text-overflow, so anything involvingfirstvalue, such astext-overflow: ellipsis ellipsis, is not supported yet. - No
RTLsupport. - According to the CSS specification, if there isn’t enough space for the
ellipsis glyph, then theellipsis glyphmust be clipped. This has not been supported yet. Instead, what will happen is one or more characters (except for the first one) will be clipped to make more room for theellipsis glyph. - the CSS specification also mentioned that if the
ellipsis glyphis not available for a particular font, then we should fallback to three dots.... This fallback hasn’t been implemented yet. - No support for
floatyet. I’m not sure how to best explain it now, but this will be elaborated in a later section.
Despite this, the code in this PR will be written (at least I’ll attempt it to the best of my ability) in a way that makes it possible for future contributors to extend text-overflow property without significantly changing the logic.
For example, to shape the ellipsis glyph, this PR uses a general approach that makes it possible to add text-overflow: 'string_value' with minimal change to the code logic (I plan to add this support in the near future).
General idea
At a high level, this approach involves two stages of the rendering pipeline, the fragment tree & display list constructions.
During fragment tree construction, we will check if text-overflow: ellipsis & overflow: hidden. If so, then we will add a TextFragment containing the ellipsis glyph after the first TextFragment that causes the cumulative advance to exceed the containing block’s width.

Specifically, everytime we create a new TextFragment, we take note of its total advance and add it to the cumulative advance. We then compare this cumulative advance to the width of the containing block (which is the width of the inline formatting context).
One flaw in this approach is that, since this assumes that the total width we have is the width of the containing block, the code won’t work if there’s float in the inline formatting context.
For example, from WPT’s css/css-ui/text-overflow-006.html:
<!DOCTYPE html>
<meta charset="utf-8">
<title>CSS Basic User Interface Test: text-overflow applied at edge of block container</title>
<link rel="author" title="Florian Rivoal" href="http://florian.rivoal.net/">
<link rel="help" href="http://www.w3.org/TR/css3-ui/#text-overflow">
<link rel="match" href="reference/text-overflow-006-ref.html">
<meta name="assert" content="Checks that the elipsis is applied at the edge of the line box, not the end of the block container, when these are different.">
<style>
div {
white-space: pre;
font-family: monospace;
text-overflow: ellipsis;
overflow: hidden;
width: 9.5ch;
/* 9ch ought to be enough,
but Safari seems to have aliasing issues that make the ellipsis character larger than 1ch by a bit.
Adding an extra .5 does not change the validity of the test,
and lets safari fit “PASS…” in the space provided.
This issue may be a bug, but if so, it is unrelated to what this test is testing,
so no need to force a fail when an easy workaround is available.
*/
}
span { float: right; }
</style>
<p>Test passes if “PASS…” appears below.</p>
<div><span> </span>PASS FAIL</div>
The code won’t ellipse the text.
TODO: I think that in the future, instead of directly using the width of the containing block, we subtract the width of the containing block with any overlapping
floats.
Anyway, going back to implementing text-overflow: ellipsis, in the TextFragment before the Ellipsis TextFragment, we will give it the width of the Ellipsis glyph (by adding a field in the TextFragment struct). This information will be used later on during the display list construction stage, where the clipping of the aforementioned TextFragment will happen.
You may wonder, Why not clip the
TextFragmentduring thefragment treeconstruction?
Well, it might be cleaner that way, but per the CSS specification,
“Ellipsing only affects rendering and must not affect layout nor dispatching of pointer events: The UA should dispatch any pointer event on the ellipsis to the elided element, as if text-overflow had been none”
So this is the reason why clipping has been done in the display list construction phase.
There is, however, one edge case that requires us to violate this rule, which will be explained in a later section.
Implementation
Now, let’s discuss the idea mentioned in previous section deeper.
1. Fragment tree construction stage
We can focus on components/layout/flow/inline/line.rs. Here, LineItemLayout.layout_text_item() is where the TextFragment is created and appended to LineItemLayout.current_state.fragments.
First, we decide if we are allowed to ellipse in the first place (when text-overflow: ellipsis, overflow: hidden). The variable can_be_ellided will be responsible for this.
Next, we check the following:
- Is the cumulative advance (from
LineItemLayout.current_state.inline_advance) greater than the bounding box (fromLineItemLayout.layout.containing_block.size.inline)?- Will any glyph from the current
TextRunLineItembe shown (original_inline_advance<LineItemLayout.layout.containing_block.size.inline)?
To explain why check #2 is important, consider the case where the entirety of the TextRunLineItem will be hidden. In this case, check #1 will return true, so a second ellipsis TextFragment will be created:

Which is wasteful. Therefore, we add check #2.
If all checks pass, then we proceed by creating the ellipsis glyph, which will be handled by fn form_overflow_marker(). Next, we create overflow_marker_content_rect, which is the containing rectangle for the ellipsis glyph. The creation of this containing rectangle will be handled by fn form_overflow_marker_containing_rect().
It is worth noting that both functions have been made generic and can be used to add support for text-overflow: 'string' without any change to their code.
Afterwards, we insert both the TextFragment and ellipsis TextFragment to LineItemLayout.current_state.fragments.
There are, however, some additional fields to the TextFragment struct:
pub(crate) struct TextFragment {
pub base: BaseFragment,
pub inline_styles: SharedInlineStyles,
pub rect: PhysicalRect<Au>,
pub font_metrics: FontMetrics,
pub font_key: FontInstanceKey,
#[conditional_malloc_size_of]
pub glyphs: Vec<Arc<GlyphStore>>,
/// Extra space to add for each justification opportunity.
pub justification_adjustment: Au,
pub selection_range: Option<ServoRange<ByteIndex>>,
/// relevant fields used for handling `text-overflow: ellipsis`
pub parent_width: Au,
// the width of left & right text overflow marker. CSS specs refers to them as `first` & `second`.
// when `text-overflow: ellipsis`, right clip is the width of the ellipsis glyph.
pub overflow_marker_width: (Au, Au),
pub contains_first_character_of_the_line: bool,
pub inline_offset: Au,
}
These four fields will be used during display list construction stage to clip the TextFragment content later on. For TextFragment, overflow_marker_width.1 is the total advance of the ellipsis glyph.
A tuple is used to represent overflow_marker_width because we may add support for double-valued text-overflow (such as text-overflow: ellipsis ellipsis) in the future.
2. Display list construction stage
Next, display list construction. The function we’re interested in is Fragment.build_display_list_for_text_fragment, where it will call fn glyphs().
In fn glyphs(), the following loop is responsible for creating the GlyphInstances:
for run in glyph_runs {
for glyph in run.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), run.len())) {
if !run.is_whitespace() || include_whitespace {
let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
let point = units::LayoutPoint::new(
baseline_origin.x.to_f32_px() + glyph_offset.x.to_f32_px(),
baseline_origin.y.to_f32_px() + glyph_offset.y.to_f32_px(),
);
let glyph = wr::GlyphInstance {
index: glyph.id(),
point,
};
glyphs.push(glyph);
}
if glyph.char_is_word_separator() {
baseline_origin.x += justification_adjustment;
}
baseline_origin.x += glyph.advance();
}
}
Clipping will be done here. Specifically, for every iteration, we check if the total advance plus the ellipsis glyph advance is less than the containing block’s width. If so, then append the GlyphInstance to the vector. Else, do nothing.
let mut glyphs = vec![];
let mut total_advance = inline_offset;
let max_total_advance = containing_block_width - text_clip_boundaries.1;
for run in glyph_runs {
for glyph in run.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), run.len())) {
total_advance += glyph.advance();
if !run.is_whitespace() || include_whitespace {
let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
let point = units::LayoutPoint::new(
baseline_origin.x.to_f32_px() + glyph_offset.x.to_f32_px(),
baseline_origin.y.to_f32_px() + glyph_offset.y.to_f32_px(),
);
let glyph = wr::GlyphInstance {
index: glyph.id(),
point,
};
// first glyph must never be ellided. otherwise, check if it's time to crop.
// The first character or atomic inline-level element on a line must be clipped rather than ellipsed.
// <https://www.w3.org/TR/css-ui-3/#text-overflow>
if total_advance <= max_total_advance || (glyphs.is_empty() && contains_first_character_of_the_line) {
glyphs.push(glyph);
}
}
if glyph.char_is_word_separator() {
baseline_origin.x += justification_adjustment;
total_advance += justification_adjustment;
}
baseline_origin.x += glyph.advance();
}
}
Here, total_advance is initially set to inline_offset (which is obtained from TextFragment::inline_offset) instead of Au(0). We do this because we may not be processing the first TextFragment of the line.
For example, going back to the previous example,

By the time we process TextFragment 3, our total_advance is already 80px, not 0px.
And that’s pretty much it!
Conclusion
This blog aims to explain how my implementation of text-overflow: ellipsis works by first explaining a high level overview, before diving into the specific implementation.
My implementation itself involves two stages of the rendering pipeline, namely the fragment tree and display list construction stages.
Acknowledgement
I’ve been working on this code with the help of minghuaw